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
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:
474
scripts/deploy-docker.ps1
Normal file
474
scripts/deploy-docker.ps1
Normal 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
592
scripts/deploy-docker.sh
Executable 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
510
scripts/deploy-local.ps1
Normal 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
582
scripts/deploy-local.sh
Executable 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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user