feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s

- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers
  with editable model names via Combobox in admin settings
- Fix OpenRouter broken by normalizeProvider bug in config.ts
- Convert agent-created notes from Markdown to HTML (TipTap rich text)
- Add Notification model + in-app notifications for agent results
- Agent notification click opens the created note directly
- Add note count display on notebook and inbox headers
- Fix checklist toggle in card view (persist state via localCheckItems)
- Add checklist creation option in tabs/list view (dropdown on + button)
- Fix image description ENOENT error with HTTP fallback
- Improve UI contrast across all themes (input, border, checkbox visibility)
- Add font family setting (Inter vs System Default) in Appearance settings
- Fix CSS font-sans variable conflict (removed dead Geist references)
- Update README with new features and 8 providers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-05-01 16:14:07 +02:00
parent 1345403a31
commit dbd49d6fcb
64 changed files with 4124 additions and 1392 deletions

474
scripts/deploy-docker.ps1 Normal file
View File

@@ -0,0 +1,474 @@
# ============================================================
# Memento - Docker Deploy Script (Windows PowerShell)
# ============================================================
# Usage:
# .\scripts\deploy-docker.ps1 # Full setup
# .\scripts\deploy-docker.ps1 -EnvOnly # Generate .env.docker only
# .\scripts\deploy-docker.ps1 -Build # Build + deploy (no env setup)
# .\scripts\deploy-docker.ps1 -Stop # Stop all containers
# .\scripts\deploy-docker.ps1 -Logs # Show logs
# ============================================================
[CmdletBinding()]
param(
[switch]$EnvOnly = $false,
[switch]$Build = $false,
[switch]$Full = $false,
[switch]$Stop = $false,
[switch]$Logs = $false
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Split-Path -Parent $ScriptDir
$EnvFile = Join-Path $ProjectDir ".env.docker"
# -----------------------------------------------------------
# Helpers
# -----------------------------------------------------------
function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Blue }
function Write-Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Write-Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Write-Err($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
function Get-RandomSecret {
$bytes = New-Object byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
[Convert]::ToBase64String($bytes)
}
function Get-RandomPassword {
$bytes = New-Object byte[] 16
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
($bytes | ForEach-Object { $_.ToString("x2") }) -join ""
}
function Ask-Input {
param([string]$Prompt, [string]$Default = "")
if ($Default) {
Write-Host " ? $Prompt [$Default]: " -ForegroundColor Cyan -NoNewline
} else {
Write-Host " ? $Prompt: " -ForegroundColor Cyan -NoNewline
}
$result = Read-Host
if ([string]::IsNullOrWhiteSpace($result)) { $result = $Default }
return $result
}
function Ask-Required {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result)) { return $result }
Write-Host " This field is required." -ForegroundColor Red
}
}
function Ask-Email {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result) -and $result -match '^[^@]+@[^@]+\.[^@]+$') {
return $result
}
Write-Host " Please enter a valid email address." -ForegroundColor Red
}
}
# -----------------------------------------------------------
# Check dependencies
# -----------------------------------------------------------
function Check-Deps {
Write-Step "Checking dependencies..."
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Err "Docker is not installed. Install from: https://docs.docker.com/desktop/install/windows-install/"
}
try {
docker info 2>&1 | Out-Null
} catch {
Write-Err "Docker daemon is not running. Start Docker Desktop first."
}
try {
docker compose version 2>&1 | Out-Null
$script:ComposeCmd = "docker compose"
} catch {
if (Get-Command docker-compose -ErrorAction SilentlyContinue) {
$script:ComposeCmd = "docker-compose"
} else {
Write-Err "Docker Compose is not installed. Install from: https://docs.docker.com/compose/install/"
}
}
Write-Ok "All dependencies met"
}
# -----------------------------------------------------------
# Generate .env.docker
# -----------------------------------------------------------
function Generate-Env {
Write-Step "Configuring Memento for Docker deployment"
if (Test-Path $EnvFile) {
Write-Warn ".env.docker already exists."
$confirm = Ask-Input "Overwrite?" "N"
if ($confirm -notmatch "^[Yy]") {
Write-Info "Keeping existing .env.docker"
return
}
}
Write-Host ""
Write-Host " This wizard will guide you through the configuration." -ForegroundColor White
Write-Host " Press Enter to accept defaults in [brackets]." -ForegroundColor White
Write-Host ""
# ---- Core ----
Write-Step "Core configuration"
$url = Ask-Input "App URL (NEXTAUTH_URL)" "http://localhost:3000"
$secret = Get-RandomSecret
Write-Info "Auto-generated NEXTAUTH_SECRET"
$adminEmail = Ask-Email "Admin email (first user with this email becomes ADMIN)"
$allowReg = Ask-Input "Allow public registration" "true"
if ($allowReg -match "^[Yy]|^[Yy]es|^true|^1") { $allowReg = "true" } else { $allowReg = "false" }
# ---- PostgreSQL ----
Write-Step "PostgreSQL configuration"
$pgPort = Ask-Input "PostgreSQL exposed port" "5433"
$pgDb = Ask-Input "PostgreSQL database name" "memento"
$pgUser = Ask-Input "PostgreSQL username" "memento"
$pgPass = Get-RandomPassword
Write-Info "Auto-generated secure PostgreSQL password"
# ---- AI Provider ----
Write-Step "AI Provider configuration"
Write-Host " Choose your AI provider:"
Write-Host " 1) OpenAI"
Write-Host " 2) Ollama (local, requires Ollama container)"
Write-Host " 3) OpenRouter"
Write-Host " 4) Custom OpenAI-compatible (Groq, Together, Mistral, etc.)"
Write-Host " 5) Skip AI configuration"
Write-Host ""
$aiChoice = Ask-Input "Choice" "5"
$aiTagsProvider = $aiTagsModel = $aiEmbedProvider = $aiEmbedModel = ""
$aiChatProvider = $aiChatModel = $openaiKey = $customKey = $customUrl = $ollamaUrl = ""
switch ($aiChoice) {
"1" {
$aiTagsProvider = "openai"; $aiTagsModel = "gpt-4o-mini"
$aiEmbedProvider = "openai"; $aiEmbedModel = "text-embedding-3-small"
$aiChatProvider = "openai"; $aiChatModel = "gpt-4o-mini"
$openaiKey = Ask-Required "OpenAI API Key"
}
"2" {
$aiTagsProvider = "ollama"; $aiTagsModel = "granite4:latest"
$aiEmbedProvider = "ollama"; $aiEmbedModel = "embeddinggemma:latest"
$aiChatProvider = "ollama"; $aiChatModel = "granite4:latest"
$ollamaUrl = Ask-Input "Ollama base URL" "http://ollama:11434"
}
"3" {
$aiTagsProvider = "custom"; $aiTagsModel = "google/gemma-3-27b-it"
$aiEmbedProvider = "custom"; $aiEmbedModel = "text-embedding-3-small"
$aiChatProvider = "custom"; $aiChatModel = "google/gemma-3-27b-it"
$customUrl = "https://openrouter.ai/api/v1"
$customKey = Ask-Required "OpenRouter API Key"
$customUrl = Ask-Input "OpenRouter base URL" $customUrl
}
"4" {
$aiTagsProvider = "custom"
$aiEmbedProvider = "custom"
$aiChatProvider = "custom"
$customKey = Ask-Required "Custom provider API Key"
$customUrl = Ask-Required "Custom provider base URL"
$aiTagsModel = Ask-Input "Model for tags" "gpt-4o-mini"
$aiEmbedModel = Ask-Input "Model for embeddings" "text-embedding-3-small"
$aiChatModel = Ask-Input "Model for chat" "gpt-4o-mini"
}
"5" {
Write-Info "Skipping AI configuration. You can configure it later in the admin panel."
}
default { Write-Err "Invalid choice" }
}
# ---- MCP Server ----
Write-Step "MCP Server configuration"
Write-Host " The MCP server allows external tools (Claude, Cline, N8N) to interact with Memento."
Write-Host ""
$mcpEnable = Ask-Input "Enable MCP server?" "yes"
$mcpEnable = ($mcpEnable -match "^[Yy]|^[Yy]es|^true|^1")
$mcpPort = "3001"; $mcpServerMode = "sse"; $mcpServerUrl = ""; $mcpApiKey = ""
if ($mcpEnable) {
$mcpPort = Ask-Input "MCP server port" "3001"
$mcpServerMode = Ask-Input "MCP server mode (sse or stdio)" "sse"
$mcpServerUrl = "$($url -replace ':\d+$',''):$mcpPort"
$mcpAuth = Ask-Input "Require MCP authentication?" "yes"
if ($mcpAuth -match "^[Yy]|^[Yy]es|^true|^1") {
$mcpApiKey = Get-RandomPassword
Write-Info "Auto-generated MCP API key"
}
}
# ---- Email ----
Write-Step "Email configuration (optional, needed for password reset)"
Write-Host " Choose an email provider:"
Write-Host " 1) Resend"
Write-Host " 2) SMTP"
Write-Host " 3) Skip"
Write-Host ""
$emailChoice = Ask-Input "Choice" "3"
$resendKey = $smtpHost = $smtpPort = $smtpUser = $smtpPass = $smtpFrom = ""
switch ($emailChoice) {
"1" { $resendKey = Ask-Required "Resend API Key" }
"2" {
$smtpHost = Ask-Required "SMTP Host"
$smtpPort = Ask-Input "SMTP Port" "587"
$smtpUser = Ask-Required "SMTP Username"
$smtpPass = Ask-Required "SMTP Password"
$smtpFrom = Ask-Required "SMTP From email"
}
}
# ---- Ollama container ----
$enableOllama = "no"
if ($aiChoice -eq "2") {
$enableOllama = "yes"
} else {
Write-Step "Ollama container (optional)"
$enableOllama = Ask-Input "Also start Ollama container?" "no"
$enableOllama = $(if ($enableOllama -match "^[Yy]|^[Yy]es|^true|^1") { "yes" } else { "no" })
}
# ---- Web Search ----
Write-Step "Web Search configuration (optional)"
Write-Host " Choose a web search provider:"
Write-Host " 1) SearXNG (self-hosted)"
Write-Host " 2) Brave Search"
Write-Host " 3) Skip"
Write-Host ""
$searchChoice = Ask-Input "Choice" "3"
$searxngUrl = $braveKey = ""
switch ($searchChoice) {
"1" { $searxngUrl = Ask-Required "SearXNG URL" }
"2" { $braveKey = Ask-Required "Brave Search API Key" }
}
# ---- Write .env.docker ----
Write-Step "Writing .env.docker"
$envContent = @"
# =============================================================================
# Memento - Docker Environment (auto-generated by deploy-docker.ps1)
# =============================================================================
# Generated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
# =============================================================================
# Core
NEXTAUTH_URL="$url"
NEXTAUTH_SECRET="$secret"
ADMIN_EMAIL="$adminEmail"
ALLOW_REGISTRATION="$allowReg"
# PostgreSQL
POSTGRES_PORT=$pgPort
POSTGRES_DB=$pgDb
POSTGRES_USER=$pgUser
POSTGRES_PASSWORD="$pgPass"
"@
if ($aiChoice -ne "5") {
$envContent += @"
# AI - Tags
AI_PROVIDER_TAGS=$aiTagsProvider
AI_MODEL_TAGS="$aiTagsModel"
# AI - Embeddings
AI_PROVIDER_EMBEDDING=$aiEmbedProvider
AI_MODEL_EMBEDDING="$aiEmbedModel"
# AI - Chat
AI_PROVIDER_CHAT=$aiChatProvider
AI_MODEL_CHAT="$aiChatModel"
"@
if ($openaiKey) { $envContent += "`nOPENAI_API_KEY=`"$openaiKey`"" }
if ($customKey) { $envContent += "`nCUSTOM_OPENAI_API_KEY=`"$customKey`"`nCUSTOM_OPENAI_BASE_URL=`"$customUrl`"" }
if ($ollamaUrl) { $envContent += "`nOLLAMA_BASE_URL=`"$ollamaUrl`"" }
}
if ($mcpEnable) {
$envContent += @"
# MCP Server
MCP_MODE="$mcpServerMode"
MCP_PORT="$mcpPort"
MCP_SERVER_MODE="$mcpServerMode"
MCP_SERVER_URL="$mcpServerUrl"
"@
if ($mcpApiKey) { $envContent += "`nMCP_API_KEY=`"$mcpApiKey`"" }
} else {
$envContent += @"
# MCP Server (disabled)
MCP_SERVER_MODE="disabled"
"@
}
if ($resendKey) { $envContent += "`n`n# Email - Resend`nRESEND_API_KEY=`"$resendKey`"" }
if ($smtpHost) {
$envContent += @"
# Email - SMTP
SMTP_HOST="$smtpHost"
SMTP_PORT="$smtpPort"
SMTP_USER="$smtpUser"
SMTP_PASS="$smtpPass"
SMTP_FROM="$smtpFrom"
"@
}
if ($searxngUrl) { $envContent += "`n`n# Web Search - SearXNG`nWEB_SEARCH_PROVIDER=`"searxng`"`nSEARXNG_URL=`"$searxngUrl`"" }
if ($braveKey) { $envContent += "`n`n# Web Search - Brave`nWEB_SEARCH_PROVIDER=`"brave`"`nBRAVE_SEARCH_API_KEY=`"$braveKey`"" }
$envContent | Set-Content -Path $EnvFile -Encoding UTF8
Write-Ok ".env.docker created at $EnvFile"
Write-Host ""
Write-Host " Configuration summary:" -ForegroundColor White
Write-Host " URL: $url"
Write-Host " Admin email: $adminEmail"
Write-Host " Registration: $allowReg"
Write-Host " PostgreSQL user: $pgUser / db: $pgDb"
Write-Host " AI provider: $(if ($aiChoice -eq '5') { 'skipped' } else { $aiTagsProvider })"
Write-Host " MCP server: $(if ($mcpEnable) { "enabled ($mcpServerMode)" } else { 'disabled' })"
Write-Host " Email: $(if ($emailChoice -eq '3') { 'skipped' } else { 'configured' })"
Write-Host " Ollama container: $enableOllama"
Write-Host " (sensitive values are hidden)"
}
# -----------------------------------------------------------
# Build and deploy
# -----------------------------------------------------------
function Deploy-Containers {
if (-not (Test-Path $EnvFile)) {
Write-Err ".env.docker not found. Run: .\scripts\deploy-docker.ps1 -EnvOnly"
}
Push-Location $ProjectDir
# Determine Ollama profile
$envContent = Get-Content $EnvFile -Raw
$ollamaProfile = ""
if ($envContent -match 'OLLAMA_BASE_URL="http://ollama' -or $envContent -match 'AI_PROVIDER_TAGS=ollama') {
$ollamaProfile = "--profile ollama"
}
Write-Step "Building Docker containers..."
Invoke-Expression "$ComposeCmd build --parallel" 2>&1
Write-Step "Starting containers..."
Invoke-Expression "$ComposeCmd up -d $ollamaProfile" 2>&1
Write-Step "Waiting for services to be healthy..."
$retries = 0
$maxRetries = 45
while ($retries -lt $maxRetries) {
$status = Invoke-Expression "$ComposeCmd ps --format '{{.Status}}'" 2>$null
$unhealthy = ($status | Where-Object { $_ -notmatch "healthy|Up" }).Count
if ($unhealthy -eq 0) { break }
$retries++
Start-Sleep -Seconds 2
Write-Host -NoNewline "."
}
Write-Host ""
if ($retries -ge $maxRetries) {
Write-Warn "Some containers may still be starting. Check status with: .\scripts\deploy-docker.ps1 -Logs"
}
Write-Step "Waiting for database migrations (handled by entrypoint)..."
# The docker-entrypoint.sh runs prisma migrate deploy automatically on every start.
# It handles: fresh DB, existing DB with migrations, and P3005 baseline recovery.
# No manual migration step needed here.
Write-Host ""
Write-Host "============================================" -ForegroundColor Green
Write-Host " Memento is running!" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Green
Write-Host ""
Invoke-Expression "$ComposeCmd ps"
$appUrl = (Select-String -Path $EnvFile -Pattern '^NEXTAUTH_URL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
$admEmail = (Select-String -Path $EnvFile -Pattern '^ADMIN_EMAIL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
Write-Host " App: $appUrl"
Write-Host " Admin: Register with $admEmail to get admin access"
Write-Host ""
Write-Host " Useful commands:" -ForegroundColor Cyan
Write-Host " .\scripts\deploy-docker.ps1 -Logs View logs"
Write-Host " .\scripts\deploy-docker.ps1 -Stop Stop containers"
Write-Host " .\scripts\deploy-docker.ps1 -EnvOnly Reconfigure .env.docker"
Pop-Location
}
# -----------------------------------------------------------
# Stop containers
# -----------------------------------------------------------
function Stop-Containers {
Push-Location $ProjectDir
Write-Step "Stopping containers..."
Invoke-Expression "$ComposeCmd down" 2>&1
Write-Ok "Containers stopped"
Pop-Location
}
# -----------------------------------------------------------
# Show logs
# -----------------------------------------------------------
function Show-Logs {
Push-Location $ProjectDir
Invoke-Expression "$ComposeCmd logs -f --tail=100"
Pop-Location
}
# -----------------------------------------------------------
# Main
# -----------------------------------------------------------
Check-Deps
if ($EnvOnly) {
Generate-Env
} elseif ($Build) {
Deploy-Containers
} elseif ($Stop) {
Stop-Containers
} elseif ($Logs) {
Show-Logs
} else {
# Default: full setup
Generate-Env
Write-Host ""
Deploy-Containers
}

592
scripts/deploy-docker.sh Executable file
View File

@@ -0,0 +1,592 @@
#!/bin/bash
set -euo pipefail
# ============================================================
# Memento - Docker Deploy Script (macOS / Linux)
# ============================================================
# Usage:
# ./scripts/deploy-docker.sh # Full setup
# ./scripts/deploy-docker.sh --env-only # Generate .env.docker only
# ./scripts/deploy-docker.sh --build # Build + deploy (no env setup)
# ./scripts/deploy-docker.sh --stop # Stop all containers
# ./scripts/deploy-docker.sh --logs # Show logs
# ============================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
ENV_FILE="$PROJECT_DIR/.env.docker"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
step() { echo -e "\n${CYAN}${BOLD}==>${NC} ${BOLD}$*${NC}"; }
# -----------------------------------------------------------
# Check dependencies
# -----------------------------------------------------------
check_deps() {
step "Checking dependencies..."
if ! command -v docker &>/dev/null; then
error "Docker is not installed.
macOS: https://docs.docker.com/desktop/install/mac-install/
Linux: https://docs.docker.com/engine/install/"
fi
if ! docker info &>/dev/null 2>&1; then
error "Docker daemon is not running. Start Docker first."
fi
if docker compose version &>/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose &>/dev/null; then
COMPOSE_CMD="docker-compose"
else
error "Docker Compose is not installed.
Install: https://docs.docker.com/compose/install/"
fi
if ! command -v openssl &>/dev/null; then
warn "openssl not found. Will use /dev/urandom for secrets."
fi
ok "All dependencies met"
}
# -----------------------------------------------------------
# Helper: generate random secret
# -----------------------------------------------------------
gen_secret() {
if command -v openssl &>/dev/null; then
openssl rand -base64 32 2>/dev/null
else
head -c 32 /dev/urandom | base64
fi
}
gen_password() {
if command -v openssl &>/dev/null; then
openssl rand -hex 16 2>/dev/null
else
head -c 16 /dev/urandom | hexdump -v -e '/1 "%02x"'
fi
}
# -----------------------------------------------------------
# Ask a question with default
# -----------------------------------------------------------
ask() {
local prompt="$1"
local default="${2:-}"
local var="$3"
local result
if [ -n "$default" ]; then
echo -ne " ${CYAN}?${NC} ${prompt} [${default}]: "
else
echo -ne " ${CYAN}?${NC} ${prompt}: "
fi
read -r result
result="${result:-$default}"
eval "$var=\"\$result\""
}
ask_required() {
local prompt="$1"
local var="$2"
local result
while true; do
echo -ne " ${CYAN}?${NC} ${prompt} (required): "
read -r result
if [ -n "$result" ]; then
eval "$var=\"\$result\""
return
fi
echo -e " ${RED}This field is required.${NC}"
done
}
ask_email() {
local prompt="$1"
local var="$2"
local result
while true; do
echo -ne " ${CYAN}?${NC} ${prompt} (required): "
read -r result
if [ -n "$result" ] && echo "$result" | grep -qE '^[^@]+@[^@]+\.[^@]+$'; then
eval "$var=\"\$result\""
return
fi
echo -e " ${RED}Please enter a valid email address.${NC}"
done
}
# -----------------------------------------------------------
# Generate .env.docker interactively
# -----------------------------------------------------------
generate_env() {
step "Configuring Memento for Docker deployment"
if [ -f "$ENV_FILE" ]; then
warn ".env.docker already exists."
echo -ne " ${CYAN}?${NC} Overwrite? [y/N]: "
read -r confirm
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && { info "Keeping existing .env.docker"; return 0; }
fi
echo ""
echo -e "${BOLD} This wizard will guide you through the configuration.${NC}"
echo -e "${BOLD} Press Enter to accept defaults in [brackets].${NC}"
echo ""
# ---- Core ----
step "Core configuration"
local url="http://localhost:3000"
ask "App URL (NEXTAUTH_URL)" "$url" url
local secret
secret=$(gen_secret)
info "Auto-generated NEXTAUTH_SECRET"
local admin_email
ask_email "Admin email (first user with this email becomes ADMIN)" admin_email
local allow_reg="true"
ask "Allow public registration" "$allow_reg" allow_reg
# Normalize
case "$allow_reg" in
[Yy]|[Yy]es|true|1) allow_reg="true" ;;
*) allow_reg="false" ;;
esac
# ---- PostgreSQL ----
step "PostgreSQL configuration"
local pg_port="5433"
local pg_db="memento"
local pg_user="memento"
local pg_pass
pg_pass=$(gen_password)
ask "PostgreSQL exposed port" "$pg_port" pg_port
ask "PostgreSQL database name" "$pg_db" pg_db
ask "PostgreSQL username" "$pg_user" pg_user
info "Auto-generated secure PostgreSQL password"
# ---- AI Provider ----
step "AI Provider configuration"
echo " Choose your AI provider:"
echo " 1) OpenAI"
echo " 2) Ollama (local, requires Ollama container)"
echo " 3) OpenRouter"
echo " 4) Custom OpenAI-compatible (Groq, Together, Mistral, etc.)"
echo " 5) Skip AI configuration"
echo ""
local ai_choice="5"
ask "Choice" "$ai_choice" ai_choice
local ai_tags_provider="" ai_tags_model=""
local ai_embed_provider="" ai_embed_model=""
local ai_chat_provider="" ai_chat_model=""
local openai_key="" custom_key="" custom_url="" ollama_url=""
case "$ai_choice" in
1)
ai_tags_provider="openai"; ai_tags_model="gpt-4o-mini"
ai_embed_provider="openai"; ai_embed_model="text-embedding-3-small"
ai_chat_provider="openai"; ai_chat_model="gpt-4o-mini"
ask_required "OpenAI API Key" openai_key
;;
2)
ai_tags_provider="ollama"; ai_tags_model="granite4:latest"
ai_embed_provider="ollama"; ai_embed_model="embeddinggemma:latest"
ai_chat_provider="ollama"; ai_chat_model="granite4:latest"
ollama_url="http://ollama:11434"
ask "Ollama base URL" "$ollama_url" ollama_url
;;
3)
ai_tags_provider="custom"; ai_tags_model="google/gemma-3-27b-it"
ai_embed_provider="custom"; ai_embed_model="text-embedding-3-small"
ai_chat_provider="custom"; ai_chat_model="google/gemma-3-27b-it"
custom_url="https://openrouter.ai/api/v1"
ask_required "OpenRouter API Key" custom_key
ask "OpenRouter base URL" "$custom_url" custom_url
;;
4)
ai_tags_provider="custom"
ai_embed_provider="custom"
ai_chat_provider="custom"
ask_required "Custom provider API Key" custom_key
ask_required "Custom provider base URL" custom_url
ask "Model for tags" "gpt-4o-mini" ai_tags_model
ask "Model for embeddings" "text-embedding-3-small" ai_embed_model
ask "Model for chat" "gpt-4o-mini" ai_chat_model
;;
5)
info "Skipping AI configuration. You can configure it later in the admin panel."
;;
*)
error "Invalid choice"
;;
esac
# ---- MCP Server ----
step "MCP Server configuration"
echo " The MCP server allows external tools (Claude, Cline, N8N) to interact with Memento."
echo ""
local mcp_enable="yes"
ask "Enable MCP server?" "$mcp_enable" mcp_enable
case "$mcp_enable" in
[Nn]|[Nn]o|false|0)
mcp_enable="false"
;;
*)
mcp_enable="true"
;;
esac
local mcp_port="3001"
local mcp_server_mode="sse"
local mcp_server_url=""
local mcp_api_key=""
if [ "$mcp_enable" = "true" ]; then
ask "MCP server port" "$mcp_port" mcp_port
ask "MCP server mode (sse or stdio)" "$mcp_server_mode" mcp_server_mode
mcp_server_url="${url%:*}:${mcp_port}"
local mcp_auth="yes"
ask "Require MCP authentication?" "$mcp_auth" mcp_auth
case "$mcp_auth" in
[Nn]|[Nn]o|false|0) ;;
*)
mcp_api_key=$(gen_password)
info "Auto-generated MCP API key"
;;
esac
fi
# ---- Email ----
step "Email configuration (optional, needed for password reset)"
echo " Choose an email provider:"
echo " 1) Resend"
echo " 2) SMTP"
echo " 3) Skip"
echo ""
local email_choice="3"
ask "Choice" "$email_choice" email_choice
local resend_key="" smtp_host="" smtp_port="" smtp_user="" smtp_pass="" smtp_from=""
case "$email_choice" in
1)
ask_required "Resend API Key" resend_key
;;
2)
ask_required "SMTP Host" smtp_host
ask "SMTP Port" "587" smtp_port
ask_required "SMTP Username" smtp_user
ask_required "SMTP Password" smtp_pass
ask_required "SMTP From email" smtp_from
;;
esac
# ---- Ollama container ----
local enable_ollama="no"
if [ "$ai_choice" = "2" ]; then
enable_ollama="yes"
else
step "Ollama container (optional)"
ask "Also start Ollama container?" "$enable_ollama" enable_ollama
case "$enable_ollama" in
[Yy]|[Yy]es|true|1) enable_ollama="yes" ;;
*) enable_ollama="no" ;;
esac
fi
# ---- Web Search (optional) ----
step "Web Search configuration (optional)"
echo " Choose a web search provider:"
echo " 1) SearXNG (self-hosted)"
echo " 2) Brave Search"
echo " 3) Skip"
echo ""
local search_choice="3"
ask "Choice" "$search_choice" search_choice
local searxng_url="" brave_key=""
case "$search_choice" in
1)
ask_required "SearXNG URL" searxng_url
;;
2)
ask_required "Brave Search API Key" brave_key
;;
esac
# ---- Write .env.docker ----
step "Writing .env.docker"
cat > "$ENV_FILE" << EOF
# =============================================================================
# Memento - Docker Environment (auto-generated by deploy-docker.sh)
# =============================================================================
# Generated on $(date '+%Y-%m-%d %H:%M:%S')
# =============================================================================
# Core
NEXTAUTH_URL="${url}"
NEXTAUTH_SECRET="${secret}"
ADMIN_EMAIL="${admin_email}"
ALLOW_REGISTRATION="${allow_reg}"
# PostgreSQL
POSTGRES_PORT=${pg_port}
POSTGRES_DB=${pg_db}
POSTGRES_USER=${pg_user}
POSTGRES_PASSWORD="${pg_pass}"
EOF
# AI config
if [ "$ai_choice" != "5" ]; then
cat >> "$ENV_FILE" << EOF
# AI - Tags
AI_PROVIDER_TAGS=${ai_tags_provider}
AI_MODEL_TAGS="${ai_tags_model}"
# AI - Embeddings
AI_PROVIDER_EMBEDDING=${ai_embed_provider}
AI_MODEL_EMBEDDING="${ai_embed_model}"
# AI - Chat
AI_PROVIDER_CHAT=${ai_chat_provider}
AI_MODEL_CHAT="${ai_chat_model}"
EOF
if [ -n "$openai_key" ]; then
echo "OPENAI_API_KEY=\"${openai_key}\"" >> "$ENV_FILE"
fi
if [ -n "$custom_key" ]; then
echo "CUSTOM_OPENAI_API_KEY=\"${custom_key}\"" >> "$ENV_FILE"
echo "CUSTOM_OPENAI_BASE_URL=\"${custom_url}\"" >> "$ENV_FILE"
fi
if [ -n "$ollama_url" ]; then
echo "OLLAMA_BASE_URL=\"${ollama_url}\"" >> "$ENV_FILE"
fi
fi
# MCP config
if [ "$mcp_enable" = "true" ]; then
cat >> "$ENV_FILE" << EOF
# MCP Server
MCP_MODE="${mcp_server_mode}"
MCP_PORT="${mcp_port}"
MCP_SERVER_MODE="${mcp_server_mode}"
MCP_SERVER_URL="${mcp_server_url}"
EOF
if [ -n "$mcp_api_key" ]; then
echo "MCP_API_KEY=\"${mcp_api_key}\"" >> "$ENV_FILE"
fi
else
cat >> "$ENV_FILE" << EOF
# MCP Server (disabled)
MCP_SERVER_MODE="disabled"
EOF
fi
# Email config
if [ -n "$resend_key" ]; then
cat >> "$ENV_FILE" << EOF
# Email - Resend
RESEND_API_KEY="${resend_key}"
EOF
fi
if [ -n "$smtp_host" ]; then
cat >> "$ENV_FILE" << EOF
# Email - SMTP
SMTP_HOST="${smtp_host}"
SMTP_PORT="${smtp_port}"
SMTP_USER="${smtp_user}"
SMTP_PASS="${smtp_pass}"
SMTP_FROM="${smtp_from}"
EOF
fi
# Web Search
if [ -n "$searxng_url" ]; then
cat >> "$ENV_FILE" << EOF
# Web Search - SearXNG
WEB_SEARCH_PROVIDER="searxng"
SEARXNG_URL="${searxng_url}"
EOF
fi
if [ -n "$brave_key" ]; then
cat >> "$ENV_FILE" << EOF
# Web Search - Brave
WEB_SEARCH_PROVIDER="brave"
BRAVE_SEARCH_API_KEY="${brave_key}"
EOF
fi
ok ".env.docker created at $ENV_FILE"
echo ""
echo -e " ${BOLD}Configuration summary:${NC}"
echo " URL: $url"
echo " Admin email: $admin_email"
echo " Registration: $allow_reg"
echo " PostgreSQL user: $pg_user / db: $pg_db"
echo " AI provider: $([ "$ai_choice" = "5" ] && echo "skipped" || echo "$ai_tags_provider")"
echo " MCP server: $([ "$mcp_enable" = "true" ] && echo "enabled ($mcp_server_mode)" || echo "disabled")"
echo " Email: $([ "$email_choice" = "3" ] && echo "skipped" || echo "configured")"
echo " Ollama container: $enable_ollama"
echo " (sensitive values are hidden)"
}
# -----------------------------------------------------------
# Build and deploy
# -----------------------------------------------------------
deploy() {
[ -f "$ENV_FILE" ] || error ".env.docker not found. Run: $0 --env-only"
cd "$PROJECT_DIR"
# Determine Ollama profile
local ollama_profile=""
if grep -q 'OLLAMA_BASE_URL="http://ollama' "$ENV_FILE" 2>/dev/null || \
grep -q 'AI_PROVIDER_TAGS=ollama' "$ENV_FILE" 2>/dev/null; then
ollama_profile="--profile ollama"
fi
step "Building Docker containers..."
$COMPOSE_CMD build --parallel 2>&1
step "Starting containers..."
$COMPOSE_CMD up -d $ollama_profile 2>&1
step "Waiting for services to be healthy..."
local retries=0
local max_retries=45
while [ $retries -lt $max_retries ]; do
local unhealthy
unhealthy=$($COMPOSE_CMD ps --format '{{.Status}}' 2>/dev/null | grep -c -v "healthy\|Up" || true)
if [ "$unhealthy" -eq 0 ]; then
break
fi
retries=$((retries + 1))
sleep 2
printf "."
done
echo ""
if [ $retries -ge $max_retries ]; then
warn "Some containers may still be starting. Check status with: $0 --logs"
fi
step "Waiting for database migrations (handled by entrypoint)..."
# The docker-entrypoint.sh runs prisma migrate deploy automatically on every start.
# It handles: fresh DB, existing DB with migrations, and P3005 baseline recovery.
# No manual migration step needed here.
echo ""
echo -e "${GREEN}${BOLD}============================================${NC}"
echo -e "${GREEN}${BOLD} Memento is running!${NC}"
echo -e "${GREEN}${BOLD}============================================${NC}"
echo ""
$COMPOSE_CMD ps
echo ""
local app_url
app_url=$(grep '^NEXTAUTH_URL=' "$ENV_FILE" | cut -d= -f2 | tr -d '"')
local admin_email
admin_email=$(grep '^ADMIN_EMAIL=' "$ENV_FILE" | cut -d= -f2 | tr -d '"')
echo -e " ${BOLD}App:${NC} $app_url"
echo -e " ${BOLD}Admin:${NC} Register with $admin_email to get admin access"
echo -e " ${BOLD}MCP:${NC} $([ -n "$(grep '^MCP_SERVER_MODE=sse' "$ENV_FILE" 2>/dev/null)" ] && echo "$app_url:$(grep '^MCP_PORT=' "$ENV_FILE" | cut -d= -f2 | tr -d '"')/mcp" || echo "disabled")"
echo ""
echo -e " ${CYAN}Useful commands:${NC}"
echo " $0 --logs View logs"
echo " $0 --stop Stop containers"
echo " $0 --env-only Reconfigure .env.docker"
}
# -----------------------------------------------------------
# Stop containers
# -----------------------------------------------------------
stop_containers() {
cd "$PROJECT_DIR"
step "Stopping containers..."
$COMPOSE_CMD down
ok "Containers stopped"
}
# -----------------------------------------------------------
# Show logs
# -----------------------------------------------------------
show_logs() {
cd "$PROJECT_DIR"
$COMPOSE_CMD logs -f --tail=100
}
# -----------------------------------------------------------
# Main
# -----------------------------------------------------------
check_deps
ACTION="${1:---full}"
case "$ACTION" in
--env-only)
generate_env
;;
--build)
deploy
;;
--full)
generate_env
echo ""
deploy
;;
--stop)
stop_containers
;;
--logs)
show_logs
;;
*)
echo "Usage: $0 [--env-only | --build | --full | --stop | --logs]"
echo ""
echo " --env-only Generate .env.docker interactively"
echo " --build Build and start containers (requires existing .env.docker)"
echo " --full Generate .env.docker + build + deploy (default)"
echo " --stop Stop all containers"
echo " --logs Show container logs"
exit 1
;;
esac

510
scripts/deploy-local.ps1 Normal file
View File

@@ -0,0 +1,510 @@
# ============================================================
# Memento - Local Deploy Script (Windows PowerShell)
# ============================================================
# Usage:
# .\scripts\deploy-local.ps1 # Full setup
# .\scripts\deploy-local.ps1 -EnvOnly # Generate .env only
# .\scripts\deploy-local.ps1 -Start # Start the app (dev or prod)
# .\scripts\deploy-local.ps1 -Migrate # Run database migrations
# .\scripts\deploy-local.ps1 -Install # Install npm dependencies
# ============================================================
[CmdletBinding()]
param(
[switch]$EnvOnly = $false,
[switch]$Start = $false,
[switch]$Migrate = $false,
[switch]$Install = $false,
[switch]$Full = $false
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Split-Path -Parent $ScriptDir
$AppDir = Join-Path $ProjectDir "memento-note"
$McpDir = Join-Path $ProjectDir "mcp-server"
$EnvFile = Join-Path $AppDir ".env"
$McpEnvFile = Join-Path $McpDir ".env"
# -----------------------------------------------------------
# Helpers
# -----------------------------------------------------------
function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Blue }
function Write-Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
function Write-Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Write-Err($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 }
function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
function Get-RandomSecret {
$bytes = New-Object byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($bytes)
[Convert]::ToBase64String($bytes)
}
function Ask-Input {
param([string]$Prompt, [string]$Default = "")
if ($Default) {
Write-Host " ? $Prompt [$Default]: " -ForegroundColor Cyan -NoNewline
} else {
Write-Host " ? $Prompt: " -ForegroundColor Cyan -NoNewline
}
$result = Read-Host
if ([string]::IsNullOrWhiteSpace($result)) { $result = $Default }
return $result
}
function Ask-Required {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result)) { return $result }
Write-Host " This field is required." -ForegroundColor Red
}
}
function Ask-Email {
param([string]$Prompt)
while ($true) {
Write-Host " ? $Prompt (required): " -ForegroundColor Cyan -NoNewline
$result = Read-Host
if (-not [string]::IsNullOrWhiteSpace($result) -and $result -match '^[^@]+@[^@]+\.[^@]+$') {
return $result
}
Write-Host " Please enter a valid email address." -ForegroundColor Red
}
}
# -----------------------------------------------------------
# Check dependencies
# -----------------------------------------------------------
function Check-Deps {
Write-Step "Checking dependencies..."
$missing = @()
# Node.js
$node = Get-Command node -ErrorAction SilentlyContinue
if ($node) {
$nodeVersion = & node -v 2>$null
Write-Ok "Node.js $nodeVersion"
} else {
$missing += "node"
}
# npm
$npm = Get-Command npm -ErrorAction SilentlyContinue
if ($npm) {
$npmVersion = & npm -v 2>$null
Write-Ok "npm $npmVersion"
} else {
$missing += "npm"
}
# PostgreSQL
$psql = Get-Command psql -ErrorAction SilentlyContinue
if ($psql) {
$pgVersion = & psql --version 2>$null | Select-Object -First 1
Write-Ok "PostgreSQL client $pgVersion"
} else {
$missing += "psql"
}
if ($missing.Count -gt 0) {
Write-Host ""
Write-Warn "Missing dependencies: $($missing -join ', ')"
Write-Host ""
Write-Host " Installation instructions:" -ForegroundColor White
Write-Host ""
foreach ($dep in $missing) {
switch ($dep) {
"node" {
Write-Host " Node.js + npm:"
Write-Host " Download: https://nodejs.org/"
Write-Host " Winget: winget install OpenJS.NodeJS.LTS"
Write-Host ""
}
"npm" {
Write-Host " npm:"
Write-Host " Usually included with Node.js installer"
Write-Host ""
}
"psql" {
Write-Host " PostgreSQL:"
Write-Host " Download: https://www.postgresql.org/download/windows/"
Write-Host " Winget: winget install PostgreSQL.PostgreSQL"
Write-Host ""
}
}
}
$cont = Ask-Input "Continue anyway?" "N"
if ($cont -notmatch "^[Yy]") { exit 1 }
}
}
# -----------------------------------------------------------
# Generate .env
# -----------------------------------------------------------
function Generate-Env {
Write-Step "Configuring Memento for local development"
if (Test-Path $EnvFile) {
Write-Warn ".env already exists at $EnvFile"
$confirm = Ask-Input "Overwrite?" "N"
if ($confirm -notmatch "^[Yy]") {
Write-Info "Keeping existing .env"
return
}
}
Write-Host ""
Write-Host " This wizard will guide you through the configuration." -ForegroundColor White
Write-Host " Press Enter to accept defaults in [brackets]." -ForegroundColor White
Write-Host ""
# ---- Database ----
Write-Step "Database configuration"
$dbHost = Ask-Input "PostgreSQL host" "localhost"
$dbPort = Ask-Input "PostgreSQL port" "5432"
$dbName = Ask-Input "PostgreSQL database name" "memento"
$dbUser = Ask-Input "PostgreSQL username" "memento"
$dbPass = Ask-Input "PostgreSQL password" "memento"
$dbUrl = "postgresql://${dbUser}:${dbPass}@${dbHost}:${dbPort}/${dbName}"
# Check DB connectivity
if (Get-Command psql -ErrorAction SilentlyContinue) {
Write-Step "Testing database connection..."
$env:PGPASSWORD = $dbPass
try {
& psql -h $dbHost -p $dbPort -U $dbUser -d $dbName -c "SELECT 1" 2>&1 | Out-Null
Write-Ok "Database connection successful"
} catch {
Write-Warn "Could not connect to database. It may not exist yet."
Write-Host ""
Write-Host " Create it with:"
Write-Host " createdb $dbName"
Write-Host " psql -c `"CREATE USER $dbUser WITH PASSWORD '$dbPass';`""
Write-Host " psql -c `"GRANT ALL PRIVILEGES ON DATABASE $dbName TO $dbUser;`""
Write-Host ""
$cont = Ask-Input "Continue anyway?" "Y"
if ($cont -match "^[Nn]") { exit 1 }
}
Remove-Item Env:PGPASSWORD
}
# ---- Core ----
Write-Step "Core configuration"
$url = Ask-Input "App URL (NEXTAUTH_URL)" "http://localhost:3000"
$secret = Get-RandomSecret
Write-Info "Auto-generated NEXTAUTH_SECRET"
$adminEmail = Ask-Email "Admin email (first user with this email becomes ADMIN)"
$allowReg = Ask-Input "Allow public registration" "true"
if ($allowReg -match "^[Yy]|^[Yy]es|^true|^1") { $allowReg = "true" } else { $allowReg = "false" }
# ---- AI Provider ----
Write-Step "AI Provider configuration"
Write-Host " Choose your AI provider:"
Write-Host " 1) OpenAI"
Write-Host " 2) Ollama (local)"
Write-Host " 3) Custom OpenAI-compatible (OpenRouter, Groq, Together, etc.)"
Write-Host " 4) Skip AI configuration"
Write-Host ""
$aiChoice = Ask-Input "Choice" "4"
$aiTagsProvider = $aiTagsModel = $aiEmbedProvider = $aiEmbedModel = ""
$aiChatProvider = $aiChatModel = $openaiKey = $customKey = $customUrl = $ollamaUrl = ""
switch ($aiChoice) {
"1" {
$aiTagsProvider = "openai"; $aiTagsModel = "gpt-4o-mini"
$aiEmbedProvider = "openai"; $aiEmbedModel = "text-embedding-3-small"
$aiChatProvider = "openai"; $aiChatModel = "gpt-4o-mini"
$openaiKey = Ask-Required "OpenAI API Key"
}
"2" {
$aiTagsProvider = "ollama"; $aiTagsModel = "granite4:latest"
$aiEmbedProvider = "ollama"; $aiEmbedModel = "embeddinggemma:latest"
$aiChatProvider = "ollama"; $aiChatModel = "granite4:latest"
$ollamaUrl = Ask-Input "Ollama base URL" "http://localhost:11434"
}
"3" {
$aiTagsProvider = "custom"
$aiEmbedProvider = "custom"
$aiChatProvider = "custom"
$customKey = Ask-Required "Custom provider API Key"
$customUrl = Ask-Required "Custom provider base URL"
$aiTagsModel = Ask-Input "Model for tags" "gpt-4o-mini"
$aiEmbedModel = Ask-Input "Model for embeddings" "text-embedding-3-small"
$aiChatModel = Ask-Input "Model for chat" "gpt-4o-mini"
}
"4" {
Write-Info "Skipping AI configuration. You can configure it later in the admin panel."
}
default { Write-Err "Invalid choice" }
}
# ---- MCP ----
Write-Step "MCP Server configuration (optional)"
$mcpEnable = Ask-Input "Configure MCP server?" "no"
$mcpEnable = ($mcpEnable -match "^[Yy]|^[Yy]es|^true|^1")
$mcpMode = "sse"; $mcpPort = "3001"; $mcpServerUrl = ""
if ($mcpEnable) {
$mcpMode = Ask-Input "MCP mode (sse or stdio)" "sse"
$mcpPort = Ask-Input "MCP port" "3001"
$mcpServerUrl = "http://localhost:${mcpPort}"
}
# ---- Email ----
Write-Step "Email configuration (optional, needed for password reset)"
Write-Host " Choose an email provider:"
Write-Host " 1) Resend"
Write-Host " 2) SMTP"
Write-Host " 3) Skip"
Write-Host ""
$emailChoice = Ask-Input "Choice" "3"
$resendKey = $smtpHost = $smtpPort = $smtpUser = $smtpPass = $smtpFrom = ""
switch ($emailChoice) {
"1" { $resendKey = Ask-Required "Resend API Key" }
"2" {
$smtpHost = Ask-Required "SMTP Host"
$smtpPort = Ask-Input "SMTP Port" "587"
$smtpUser = Ask-Required "SMTP Username"
$smtpPass = Ask-Required "SMTP Password"
$smtpFrom = Ask-Required "SMTP From email"
}
}
# ---- Write .env ----
Write-Step "Writing .env"
$envContent = @"
# =============================================================================
# Memento - Local Environment (auto-generated by deploy-local.ps1)
# =============================================================================
# Generated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
# =============================================================================
# Core
DATABASE_URL="$dbUrl"
NEXTAUTH_SECRET="$secret"
NEXTAUTH_URL="$url"
ADMIN_EMAIL="$adminEmail"
ALLOW_REGISTRATION="$allowReg"
"@
if ($aiChoice -ne "4") {
$envContent += @"
# AI - Tags
AI_PROVIDER_TAGS=$aiTagsProvider
AI_MODEL_TAGS="$aiTagsModel"
# AI - Embeddings
AI_PROVIDER_EMBEDDING=$aiEmbedProvider
AI_MODEL_EMBEDDING="$aiEmbedModel"
# AI - Chat
AI_PROVIDER_CHAT=$aiChatProvider
AI_MODEL_CHAT="$aiChatModel"
"@
if ($openaiKey) { $envContent += "`nOPENAI_API_KEY=`"$openaiKey`"" }
if ($customKey) { $envContent += "`nCUSTOM_OPENAI_API_KEY=`"$customKey`"`nCUSTOM_OPENAI_BASE_URL=`"$customUrl`"" }
if ($ollamaUrl) { $envContent += "`nOLLAMA_BASE_URL=`"$ollamaUrl`"" }
}
if ($mcpEnable) {
$envContent += @"
# MCP Server
MCP_SERVER_MODE="$mcpMode"
MCP_SERVER_URL="$mcpServerUrl"
"@
} else {
$envContent += @"
# MCP Server (disabled)
MCP_SERVER_MODE="disabled"
"@
}
if ($resendKey) { $envContent += "`n`n# Email - Resend`nRESEND_API_KEY=`"$resendKey`"" }
if ($smtpHost) {
$envContent += @"
# Email - SMTP
SMTP_HOST="$smtpHost"
SMTP_PORT="$smtpPort"
SMTP_USER="$smtpUser"
SMTP_PASS="$smtpPass"
SMTP_FROM="$smtpFrom"
"@
}
$envContent | Set-Content -Path $EnvFile -Encoding UTF8
Write-Ok ".env created at $EnvFile"
# MCP server .env
if ($mcpEnable) {
$mcpContent = @"
# =============================================================================
# MCP Server - Local Environment (auto-generated by deploy-local.ps1)
# =============================================================================
DATABASE_URL="$dbUrl"
MCP_MODE="$mcpMode"
PORT="$mcpPort"
APP_BASE_URL="$url"
"@
$mcpContent | Set-Content -Path $McpEnvFile -Encoding UTF8
Write-Ok ".env created at $McpEnvFile"
}
Write-Host ""
Write-Host " Configuration summary:" -ForegroundColor White
Write-Host " URL: $url"
Write-Host " Admin email: $adminEmail"
Write-Host " Database: $dbHost`:$dbPort/$dbName"
Write-Host " AI provider: $(if ($aiChoice -eq '4') { 'skipped' } else { $aiTagsProvider })"
Write-Host " MCP server: $(if ($mcpEnable) { "enabled ($mcpMode)" } else { 'disabled' })"
Write-Host " Email: $(if ($emailChoice -eq '3') { 'skipped' } else { 'configured' })"
}
# -----------------------------------------------------------
# Install dependencies
# -----------------------------------------------------------
function Install-Deps {
Write-Step "Installing dependencies..."
Push-Location $AppDir
if (-not (Test-Path "node_modules")) {
Write-Info "Running npm install..."
& npm install
Write-Ok "Dependencies installed"
} else {
Write-Info "node_modules exists, checking for updates..."
& npm install
}
Pop-Location
}
# -----------------------------------------------------------
# Run migrations
# -----------------------------------------------------------
function Run-Migrations {
if (-not (Test-Path $EnvFile)) {
Write-Err ".env not found. Run: .\scripts\deploy-local.ps1 -EnvOnly"
}
Write-Step "Running database migrations..."
Push-Location $AppDir
try {
& npx prisma migrate deploy 2>&1
} catch {
Write-Warn "migrate deploy failed, trying db push..."
try {
& npx prisma db push --skip-generate 2>&1
} catch {
Write-Err "Database migration failed"
}
}
Write-Ok "Database migrations complete"
Pop-Location
}
# -----------------------------------------------------------
# Start app
# -----------------------------------------------------------
function Start-App {
if (-not (Test-Path $EnvFile)) {
Write-Err ".env not found. Run: .\scripts\deploy-local.ps1 -EnvOnly"
}
Push-Location $AppDir
Write-Host ""
Write-Host " Choose mode:"
Write-Host " 1) Development (npm run dev, with hot reload)"
Write-Host " 2) Production (npm run build + npm start)"
Write-Host ""
$mode = Ask-Input "Choice" "1"
switch ($mode) {
"2" {
Write-Step "Building for production..."
& npm run build
Write-Step "Starting in production mode..."
Write-Info "Starting server on http://localhost:3000"
Write-Info "Press Ctrl+C to stop"
Write-Host ""
& npm start
}
default {
Write-Step "Starting in development mode..."
Write-Info "Starting dev server on http://localhost:3000"
Write-Info "Press Ctrl+C to stop"
Write-Host ""
& npm run dev
}
}
Pop-Location
}
# -----------------------------------------------------------
# Full setup
# -----------------------------------------------------------
function Full-Setup {
Check-Deps
Generate-Env
Install-Deps
Run-Migrations
Write-Host ""
Write-Host "============================================" -ForegroundColor Green
Write-Host " Setup complete!" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Green
Write-Host ""
$admEmail = (Select-String -Path $EnvFile -Pattern '^ADMIN_EMAIL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
$appUrl = (Select-String -Path $EnvFile -Pattern '^NEXTAUTH_URL=(.+)$').Matches[0].Groups[1].Value -replace '"',''
Write-Host " Next steps:" -ForegroundColor White
Write-Host " 1. Start the app: .\scripts\deploy-local.ps1 -Start"
Write-Host " 2. Open: $appUrl"
Write-Host " 3. Register with: $admEmail" -ForegroundColor White
Write-Host " 4. That account will automatically get ADMIN role"
Write-Host ""
}
# -----------------------------------------------------------
# Main
# -----------------------------------------------------------
if ($EnvOnly) {
Check-Deps
Generate-Env
} elseif ($Start) {
Start-App
} elseif ($Migrate) {
Run-Migrations
} elseif ($Install) {
Install-Deps
} else {
# Default: full setup
Full-Setup
}

582
scripts/deploy-local.sh Executable file
View File

@@ -0,0 +1,582 @@
#!/bin/bash
set -euo pipefail
# ============================================================
# Memento - Local Deploy Script (macOS / Linux)
# ============================================================
# Usage:
# ./scripts/deploy-local.sh # Full setup
# ./scripts/deploy-local.sh --env-only # Generate .env only
# ./scripts/deploy-local.sh --start # Start the app (dev or prod)
# ./scripts/deploy-local.sh --stop # Stop the app
# ./scripts/deploy-local.sh --migrate # Run database migrations
# ============================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
APP_DIR="$PROJECT_DIR/memento-note"
ENV_FILE="$APP_DIR/.env"
MCP_DIR="$PROJECT_DIR/mcp-server"
MCP_ENV_FILE="$MCP_DIR/.env"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
step() { echo -e "\n${CYAN}${BOLD}==>${NC} ${BOLD}$*${NC}"; }
# -----------------------------------------------------------
# Generate random secret
# -----------------------------------------------------------
gen_secret() {
if command -v openssl &>/dev/null; then
openssl rand -base64 32 2>/dev/null
else
head -c 32 /dev/urandom | base64
fi
}
# -----------------------------------------------------------
# Ask helpers
# -----------------------------------------------------------
ask() {
local prompt="$1"
local default="${2:-}"
local var="$3"
local result
if [ -n "$default" ]; then
echo -ne " ${CYAN}?${NC} ${prompt} [${default}]: "
else
echo -ne " ${CYAN}?${NC} ${prompt}: "
fi
read -r result
result="${result:-$default}"
eval "$var=\"\$result\""
}
ask_required() {
local prompt="$1"
local var="$2"
local result
while true; do
echo -ne " ${CYAN}?${NC} ${prompt} (required): "
read -r result
if [ -n "$result" ]; then
eval "$var=\"\$result\""
return
fi
echo -e " ${RED}This field is required.${NC}"
done
}
ask_email() {
local prompt="$1"
local var="$2"
local result
while true; do
echo -ne " ${CYAN}?${NC} ${prompt} (required): "
read -r result
if [ -n "$result" ] && echo "$result" | grep -qE '^[^@]+@[^@]+\.[^@]+$'; then
eval "$var=\"\$result\""
return
fi
echo -e " ${RED}Please enter a valid email address.${NC}"
done
}
# -----------------------------------------------------------
# Check dependencies
# -----------------------------------------------------------
check_deps() {
step "Checking dependencies..."
local missing=()
# Node.js
if command -v node &>/dev/null; then
local node_version
node_version=$(node -v 2>/dev/null)
ok "Node.js $node_version"
else
missing+=("node")
fi
# npm
if command -v npm &>/dev/null; then
ok "npm $(npm -v 2>/dev/null)"
else
missing+=("npm")
fi
# PostgreSQL
if command -v psql &>/dev/null; then
ok "PostgreSQL client $(psql --version 2>/dev/null | head -1)"
else
missing+=("psql")
fi
# Check if PostgreSQL server is running
if command -v pg_isready &>/dev/null; then
if pg_isready &>/dev/null 2>&1; then
ok "PostgreSQL server is running"
else
warn "PostgreSQL server does not seem to be running."
echo " Try: brew services start postgresql (macOS) or sudo systemctl start postgresql (Linux)"
fi
fi
if [ ${#missing[@]} -gt 0 ]; then
echo ""
warn "Missing dependencies: ${missing[*]}"
echo ""
echo -e " ${BOLD}Installation instructions:${NC}"
echo ""
for dep in "${missing[@]}"; do
case "$dep" in
node|npm)
echo " Node.js + npm:"
echo " macOS: brew install node"
echo " Linux: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt install -y nodejs"
echo " Or: https://nodejs.org/"
echo ""
;;
psql)
echo " PostgreSQL:"
echo " macOS: brew install postgresql@16 && brew services start postgresql@16"
echo " Linux: sudo apt install -y postgresql postgresql-contrib"
echo " Or: https://www.postgresql.org/download/"
echo ""
;;
esac
done
echo -ne " ${CYAN}?${NC} Continue anyway? [y/N]: "
read -r cont
[[ "$cont" != "y" && "$cont" != "Y" ]] && exit 1
fi
}
# -----------------------------------------------------------
# Generate .env interactively
# -----------------------------------------------------------
generate_env() {
step "Configuring Memento for local development"
if [ -f "$ENV_FILE" ]; then
warn ".env already exists at $ENV_FILE"
echo -ne " ${CYAN}?${NC} Overwrite? [y/N]: "
read -r confirm
[[ "$confirm" != "y" && "$confirm" != "Y" ]] && { info "Keeping existing .env"; return 0; }
fi
echo ""
echo -e "${BOLD} This wizard will guide you through the configuration.${NC}"
echo -e "${BOLD} Press Enter to accept defaults in [brackets].${NC}"
echo ""
# ---- Database ----
step "Database configuration"
local db_host="localhost"
local db_port="5432"
local db_name="memento"
local db_user="memento"
local db_pass="memento"
ask "PostgreSQL host" "$db_host" db_host
ask "PostgreSQL port" "$db_port" db_port
ask "PostgreSQL database name" "$db_name" db_name
ask "PostgreSQL username" "$db_user" db_user
ask "PostgreSQL password" "$db_pass" db_pass
local db_url="postgresql://${db_user}:${db_pass}@${db_host}:${db_port}/${db_name}"
# Check DB connectivity
if command -v pg_isready &>/dev/null; then
step "Testing database connection..."
if PGPASSWORD="$db_pass" psql -h "$db_host" -p "$db_port" -U "$db_user" -d "$db_name" -c "SELECT 1" &>/dev/null 2>&1; then
ok "Database connection successful"
else
warn "Could not connect to database. It may not exist yet."
echo ""
echo " Create it with:"
echo " createdb $db_name"
echo " psql -c \"CREATE USER $db_user WITH PASSWORD '$db_pass';\""
echo " psql -c \"GRANT ALL PRIVILEGES ON DATABASE $db_name TO $db_user;\""
echo ""
echo -ne " ${CYAN}?${NC} Continue anyway? [Y/n]: "
read -r cont
[[ "$cont" == "n" || "$cont" == "N" ]] && exit 1
fi
fi
# ---- Core ----
step "Core configuration"
local url="http://localhost:3000"
ask "App URL (NEXTAUTH_URL)" "$url" url
local secret
secret=$(gen_secret)
info "Auto-generated NEXTAUTH_SECRET"
local admin_email
ask_email "Admin email (first user with this email becomes ADMIN)" admin_email
local allow_reg="true"
ask "Allow public registration" "$allow_reg" allow_reg
case "$allow_reg" in
[Yy]|[Yy]es|true|1) allow_reg="true" ;;
*) allow_reg="false" ;;
esac
# ---- AI Provider ----
step "AI Provider configuration"
echo " Choose your AI provider:"
echo " 1) OpenAI"
echo " 2) Ollama (local)"
echo " 3) Custom OpenAI-compatible (OpenRouter, Groq, Together, etc.)"
echo " 4) Skip AI configuration"
echo ""
local ai_choice="4"
ask "Choice" "$ai_choice" ai_choice
local ai_tags_provider="" ai_tags_model=""
local ai_embed_provider="" ai_embed_model=""
local ai_chat_provider="" ai_chat_model=""
local openai_key="" custom_key="" custom_url="" ollama_url=""
case "$ai_choice" in
1)
ai_tags_provider="openai"; ai_tags_model="gpt-4o-mini"
ai_embed_provider="openai"; ai_embed_model="text-embedding-3-small"
ai_chat_provider="openai"; ai_chat_model="gpt-4o-mini"
ask_required "OpenAI API Key" openai_key
;;
2)
ai_tags_provider="ollama"; ai_tags_model="granite4:latest"
ai_embed_provider="ollama"; ai_embed_model="embeddinggemma:latest"
ai_chat_provider="ollama"; ai_chat_model="granite4:latest"
ollama_url="http://localhost:11434"
ask "Ollama base URL" "$ollama_url" ollama_url
;;
3)
ai_tags_provider="custom"
ai_embed_provider="custom"
ai_chat_provider="custom"
ask_required "Custom provider API Key" custom_key
ask_required "Custom provider base URL" custom_url
ask "Model for tags" "gpt-4o-mini" ai_tags_model
ask "Model for embeddings" "text-embedding-3-small" ai_embed_model
ask "Model for chat" "gpt-4o-mini" ai_chat_model
;;
4)
info "Skipping AI configuration. You can configure it later in the admin panel."
;;
*)
error "Invalid choice"
;;
esac
# ---- MCP ----
step "MCP Server configuration (optional)"
local mcp_enable="no"
ask "Configure MCP server?" "$mcp_enable" mcp_enable
case "$mcp_enable" in
[Yy]|[Yy]es|true|1) mcp_enable="yes" ;;
*) mcp_enable="no" ;;
esac
local mcp_mode="sse" mcp_port="3001" mcp_server_url=""
if [ "$mcp_enable" = "yes" ]; then
ask "MCP mode (sse or stdio)" "$mcp_mode" mcp_mode
ask "MCP port" "$mcp_port" mcp_port
mcp_server_url="http://localhost:${mcp_port}"
fi
# ---- Email ----
step "Email configuration (optional, needed for password reset)"
echo " Choose an email provider:"
echo " 1) Resend"
echo " 2) SMTP"
echo " 3) Skip"
echo ""
local email_choice="3"
ask "Choice" "$email_choice" email_choice
local resend_key="" smtp_host="" smtp_port="" smtp_user="" smtp_pass="" smtp_from=""
case "$email_choice" in
1)
ask_required "Resend API Key" resend_key
;;
2)
ask_required "SMTP Host" smtp_host
ask "SMTP Port" "587" smtp_port
ask_required "SMTP Username" smtp_user
ask_required "SMTP Password" smtp_pass
ask_required "SMTP From email" smtp_from
;;
esac
# ---- Write .env ----
step "Writing .env"
cat > "$ENV_FILE" << EOF
# =============================================================================
# Memento - Local Environment (auto-generated by deploy-local.sh)
# =============================================================================
# Generated on $(date '+%Y-%m-%d %H:%M:%S')
# =============================================================================
# Core
DATABASE_URL="${db_url}"
NEXTAUTH_SECRET="${secret}"
NEXTAUTH_URL="${url}"
ADMIN_EMAIL="${admin_email}"
ALLOW_REGISTRATION="${allow_reg}"
EOF
# AI config
if [ "$ai_choice" != "4" ]; then
cat >> "$ENV_FILE" << EOF
# AI - Tags
AI_PROVIDER_TAGS=${ai_tags_provider}
AI_MODEL_TAGS="${ai_tags_model}"
# AI - Embeddings
AI_PROVIDER_EMBEDDING=${ai_embed_provider}
AI_MODEL_EMBEDDING="${ai_embed_model}"
# AI - Chat
AI_PROVIDER_CHAT=${ai_chat_provider}
AI_MODEL_CHAT="${ai_chat_model}"
EOF
if [ -n "$openai_key" ]; then
echo "OPENAI_API_KEY=\"${openai_key}\"" >> "$ENV_FILE"
fi
if [ -n "$custom_key" ]; then
echo "CUSTOM_OPENAI_API_KEY=\"${custom_key}\"" >> "$ENV_FILE"
echo "CUSTOM_OPENAI_BASE_URL=\"${custom_url}\"" >> "$ENV_FILE"
fi
if [ -n "$ollama_url" ]; then
echo "OLLAMA_BASE_URL=\"${ollama_url}\"" >> "$ENV_FILE"
fi
fi
# MCP config
if [ "$mcp_enable" = "yes" ]; then
cat >> "$ENV_FILE" << EOF
# MCP Server
MCP_SERVER_MODE="${mcp_mode}"
MCP_SERVER_URL="${mcp_server_url}"
EOF
else
cat >> "$ENV_FILE" << EOF
# MCP Server (disabled)
MCP_SERVER_MODE="disabled"
EOF
fi
# Email config
if [ -n "$resend_key" ]; then
cat >> "$ENV_FILE" << EOF
# Email - Resend
RESEND_API_KEY="${resend_key}"
EOF
fi
if [ -n "$smtp_host" ]; then
cat >> "$ENV_FILE" << EOF
# Email - SMTP
SMTP_HOST="${smtp_host}"
SMTP_PORT="${smtp_port}"
SMTP_USER="${smtp_user}"
SMTP_PASS="${smtp_pass}"
SMTP_FROM="${smtp_from}"
EOF
fi
ok ".env created at $ENV_FILE"
# Also generate MCP server .env if MCP enabled
if [ "$mcp_enable" = "yes" ]; then
cat > "$MCP_ENV_FILE" << EOF
# =============================================================================
# MCP Server - Local Environment (auto-generated by deploy-local.sh)
# =============================================================================
DATABASE_URL="${db_url}"
MCP_MODE="${mcp_mode}"
PORT="${mcp_port}"
APP_BASE_URL="${url}"
EOF
ok ".env created at $MCP_ENV_FILE"
fi
echo ""
echo -e " ${BOLD}Configuration summary:${NC}"
echo " URL: $url"
echo " Admin email: $admin_email"
echo " Database: $db_host:$db_port/$db_name"
echo " AI provider: $([ "$ai_choice" = "4" ] && echo "skipped" || echo "$ai_tags_provider")"
echo " MCP server: $([ "$mcp_enable" = "yes" ] && echo "enabled ($mcp_mode)" || echo "disabled")"
echo " Email: $([ "$email_choice" = "3" ] && echo "skipped" || echo "configured")"
}
# -----------------------------------------------------------
# Install dependencies
# -----------------------------------------------------------
install_deps() {
step "Installing dependencies..."
cd "$APP_DIR"
if [ ! -d "node_modules" ]; then
info "Running npm install..."
npm install
ok "Dependencies installed"
else
info "node_modules exists, checking for updates..."
npm install
fi
}
# -----------------------------------------------------------
# Run migrations
# -----------------------------------------------------------
run_migrations() {
[ -f "$ENV_FILE" ] || error ".env not found. Run: $0 --env-only"
step "Running database migrations..."
cd "$APP_DIR"
npx prisma migrate deploy 2>&1 || {
warn "migrate deploy failed, trying db push..."
npx prisma db push --skip-generate 2>&1 || error "Database migration failed"
}
ok "Database migrations complete"
}
# -----------------------------------------------------------
# Start the app
# -----------------------------------------------------------
start_app() {
[ -f "$ENV_FILE" ] || error ".env not found. Run: $0 --env-only"
cd "$APP_DIR"
echo ""
echo " Choose mode:"
echo " 1) Development (npm run dev, with hot reload)"
echo " 2) Production (npm run build + npm start)"
echo ""
local mode="1"
ask "Choice" "$mode" mode
case "$mode" in
2)
step "Building for production..."
npm run build
step "Starting in production mode..."
info "Starting server on http://localhost:3000"
info "Press Ctrl+C to stop"
echo ""
npm start
;;
*)
step "Starting in development mode..."
info "Starting dev server on http://localhost:3000"
info "Press Ctrl+C to stop"
echo ""
npm run dev
;;
esac
}
# -----------------------------------------------------------
# Full setup
# -----------------------------------------------------------
full_setup() {
check_deps
generate_env
install_deps
run_migrations
echo ""
echo -e "${GREEN}${BOLD}============================================${NC}"
echo -e "${GREEN}${BOLD} Setup complete!${NC}"
echo -e "${GREEN}${BOLD}============================================${NC}"
echo ""
local admin_email
admin_email=$(grep '^ADMIN_EMAIL=' "$ENV_FILE" | cut -d= -f2 | tr -d '"')
echo -e " ${BOLD}Next steps:${NC}"
echo " 1. Start the app: $0 --start"
echo " 2. Open: $(grep '^NEXTAUTH_URL=' "$ENV_FILE" | cut -d= -f2 | tr -d '"')"
echo -e " 3. Register with: ${BOLD}${admin_email}${NC}"
echo " 4. That account will automatically get ADMIN role"
echo ""
}
# -----------------------------------------------------------
# Main
# -----------------------------------------------------------
ACTION="${1:---full}"
case "$ACTION" in
--env-only)
check_deps
generate_env
;;
--start)
start_app
;;
--migrate)
run_migrations
;;
--install)
install_deps
;;
--stop)
warn "For local deployment, stop the server with Ctrl+C"
;;
--full)
full_setup
;;
*)
echo "Usage: $0 [--env-only | --start | --migrate | --install | --full | --stop]"
echo ""
echo " --env-only Generate .env interactively"
echo " --start Start the app (dev or prod mode)"
echo " --migrate Run database migrations"
echo " --install Install npm dependencies"
echo " --full Full setup: env + install + migrate (default)"
echo " --stop Reminder to use Ctrl+C"
exit 1
;;
esac

View File

@@ -251,9 +251,8 @@ deploy() {
done
echo ""
info "Initializing database..."
docker compose exec memento-note npx prisma db push --skip-generate 2>/dev/null || \
warn "DB push failed (may already be synced)"
info "Database migrations are handled by the container entrypoint on every start."
info "The entrypoint handles fresh installs, updates, and P3005 baseline recovery automatically."
echo ""
echo "=========================================="
@@ -266,10 +265,18 @@ deploy() {
# Show admin setup hint if first time
local user_count
user_count=$(docker compose exec -T postgres psql -U memento -d memento -t -c 'SELECT COUNT(*) FROM "User"' 2>/dev/null | tr -d ' ' || echo "0")
local admin_email
admin_email=$(grep '^ADMIN_EMAIL=' "$ENV_FILE" 2>/dev/null | cut -d= -f2 | tr -d '"' || echo "")
if [ "$user_count" = "0" ]; then
echo ""
warn "No users found. Register at $(grep NEXTAUTH_URL "$ENV_FILE" | cut -d= -f2 | tr -d '"')/register"
warn "Then run: docker compose exec memento-note npx tsx scripts/grant-all-admins.ts"
warn "No users found."
if [ -n "$admin_email" ]; then
info "Register at $(grep NEXTAUTH_URL "$ENV_FILE" | cut -d= -f2 | tr -d '"')/register"
info "Use email: $admin_email (will automatically get ADMIN role)"
else
info "Register at $(grep NEXTAUTH_URL "$ENV_FILE" | cut -d= -f2 | tr -d '"')/register"
warn "ADMIN_EMAIL is not set. Set it in .env.docker for automatic admin role assignment."
fi
fi
}