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>
475 lines
16 KiB
PowerShell
475 lines
16 KiB
PowerShell
# ============================================================
|
|
# 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
|
|
}
|