From d3c2de200039260754554217baf0ceb2e6bc8991 Mon Sep 17 00:00:00 2001 From: Sepehr Ramezani Date: Tue, 21 Apr 2026 00:29:51 +0200 Subject: [PATCH] feat: add env setup wizard, fix docker-compose env passthrough and email from field - Add interactive setup wizard (scripts/setup-env.js) with SQLite/PostgreSQL choice, AI provider config, email, MCP, admin account creation, and auto-switch of Prisma schema provider - Fix docker-compose.yml: remove duplicate environment entries that overrode env_file values with empty strings (broke AI providers in Docker). Now only DATABASE_URL, NODE_ENV, NEXT_TELEMETRY_DISABLED remain in environment: - Fix revalidateTag('system-config', '/settings') crash: Next.js 16 interprets the second arg as a cacheLife profile, not a path. Caused 500 on all admin settings saves - Fix Resend "from" field: was building noreply@localhost which Resend rejects. Now uses SMTP_FROM from config, with localhost-aware fallback - Add debug logging for auto-labeling background task - Default DATABASE_URL changed from user:password to memento:memento Co-Authored-By: Claude Opus 4.5 --- docker-compose.yml | 33 +- memento-note/.env.example | 2 +- memento-note/app/actions/admin-settings.ts | 2 +- memento-note/app/actions/notes.ts | 10 +- memento-note/lib/mail.ts | 21 +- memento-note/package.json | 8 +- memento-note/prisma/schema.prisma | 2 +- memento-note/scripts/setup-env.js | 991 +++++++++++++++++++++ 8 files changed, 1030 insertions(+), 39 deletions(-) create mode 100644 memento-note/scripts/setup-env.js diff --git a/docker-compose.yml b/docker-compose.yml index 0390458..201163d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,34 +35,10 @@ services: ports: - "3000:3000" environment: + # DATABASE_URL is auto-constructed from PostgreSQL credentials (not in .env.docker) - DATABASE_URL=postgresql://${POSTGRES_USER:-memento}:${POSTGRES_PASSWORD:-memento}@postgres:5432/${POSTGRES_DB:-memento} - - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-changethisinproduction} - - NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000} - NODE_ENV=production - NEXT_TELEMETRY_DISABLED=1 - - # Email Configuration (SMTP) - - SMTP_HOST=${SMTP_HOST} - - SMTP_PORT=${SMTP_PORT:-587} - - SMTP_USER=${SMTP_USER} - - SMTP_PASS=${SMTP_PASS} - - SMTP_FROM=${SMTP_FROM:-noreply@memento.app} - - # AI Providers - - AI_PROVIDER_TAGS=${AI_PROVIDER_TAGS} - - AI_PROVIDER_EMBEDDING=${AI_PROVIDER_EMBEDDING} - - AI_PROVIDER_CHAT=${AI_PROVIDER_CHAT} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - OLLAMA_BASE_URL=${OLLAMA_BASE_URL} - - AI_MODEL_TAGS=${AI_MODEL_TAGS} - - AI_MODEL_EMBEDDING=${AI_MODEL_EMBEDDING} - - AI_MODEL_CHAT=${AI_MODEL_CHAT} - - CUSTOM_OPENAI_API_KEY=${CUSTOM_OPENAI_API_KEY} - - CUSTOM_OPENAI_BASE_URL=${CUSTOM_OPENAI_BASE_URL} - - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true} - - RESEND_API_KEY=${RESEND_API_KEY} - - MCP_SERVER_MODE=${MCP_SERVER_MODE} - - MCP_SERVER_URL=${MCP_SERVER_URL} volumes: - uploads-data:/app/public/uploads depends_on: @@ -100,10 +76,7 @@ services: # SSE mode exposes port 3001, stdio mode doesn't need ports - "3001:3001" environment: - # Mode: 'stdio' (default, for Claude Desktop) or 'sse' (for HTTP/N8N) - - MCP_MODE=${MCP_MODE:-stdio} - - PORT=${MCP_PORT:-3001} - # Database - connect to shared PostgreSQL + # DATABASE_URL is auto-constructed from PostgreSQL credentials (not in .env.docker) - DATABASE_URL=postgresql://${POSTGRES_USER:-memento}:${POSTGRES_PASSWORD:-memento}@postgres:5432/${POSTGRES_DB:-memento} - NODE_ENV=production depends_on: @@ -113,7 +86,7 @@ services: networks: - memento-network healthcheck: - test: ["CMD-SHELL", "if [ \"${MCP_MODE}\" = 'sse' ]; then wget --spider -q http://localhost:3001/ || exit 1; else node -e \"console.log('healthy')\" || exit 1; fi"] + test: ["CMD-SHELL", "wget --spider -q http://localhost:3001/ || node -e \"console.log('healthy')\""] interval: 30s timeout: 10s retries: 3 diff --git a/memento-note/.env.example b/memento-note/.env.example index 4e567df..fdc95ae 100644 --- a/memento-note/.env.example +++ b/memento-note/.env.example @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Core (required) # ----------------------------------------------------------------------------- -DATABASE_URL="postgresql://user:password@localhost:5432/memento" +DATABASE_URL="postgresql://memento:memento@localhost:5432/memento" NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32" NEXTAUTH_URL="http://localhost:3000" diff --git a/memento-note/app/actions/admin-settings.ts b/memento-note/app/actions/admin-settings.ts index 1945557..cdc6501 100644 --- a/memento-note/app/actions/admin-settings.ts +++ b/memento-note/app/actions/admin-settings.ts @@ -63,7 +63,7 @@ export async function updateSystemConfig(data: Record) { await prisma.$transaction(operations) // Invalidate cache after update - revalidateTag('system-config', '/settings') + revalidateTag('system-config') return { success: true } } catch (error) { diff --git a/memento-note/app/actions/notes.ts b/memento-note/app/actions/notes.ts index 8556e47..b4c3e41 100644 --- a/memento-note/app/actions/notes.ts +++ b/memento-note/app/actions/notes.ts @@ -486,7 +486,8 @@ export async function createNote(data: { ;(async () => { try { // Background task 1: Generate embedding - const provider = getAIProvider(await getSystemConfig()) + const bgConfig = await getSystemConfig() + const provider = getAIProvider(bgConfig) const embedding = await provider.getEmbeddings(content) if (embedding) { await prisma.noteEmbedding.upsert({ @@ -505,6 +506,8 @@ export async function createNote(data: { const autoLabelingEnabled = await getConfigBoolean('AUTO_LABELING_ENABLED', true) const autoLabelingConfidence = await getConfigNumber('AUTO_LABELING_CONFIDENCE_THRESHOLD', 70) + console.log('[BG] Auto-labeling check: enabled=', autoLabelingEnabled, 'confidence=', autoLabelingConfidence, 'notebookId=', notebookId) + if (autoLabelingEnabled) { const suggestions = await contextualAutoTagService.suggestLabels( content, @@ -512,6 +515,8 @@ export async function createNote(data: { userId ) + console.log('[BG] Auto-labeling suggestions:', suggestions.length, suggestions.map(s => s.label)) + const appliedLabels = suggestions .filter(s => s.confidence >= autoLabelingConfidence) .map(s => s.label) @@ -530,6 +535,8 @@ export async function createNote(data: { } catch (error) { console.error('[BG] Auto-labeling failed:', error) } + } else { + console.log('[BG] Auto-labeling skipped: hasUserLabels=', hasUserLabels, 'notebookId=', notebookId) } })() @@ -556,6 +563,7 @@ export async function updateNote(id: string, data: { isMarkdown?: boolean size?: 'small' | 'medium' | 'large' autoGenerated?: boolean | null + aiProvider?: string | null notebookId?: string | null }, options?: { skipContentTimestamp?: boolean; skipRevalidation?: boolean }) { const session = await auth(); diff --git a/memento-note/lib/mail.ts b/memento-note/lib/mail.ts index 409180f..bd341a0 100644 --- a/memento-note/lib/mail.ts +++ b/memento-note/lib/mail.ts @@ -61,9 +61,24 @@ async function sendViaResend(apiKey: string, { to, subject, html, attachments }: const { Resend } = await import('resend'); const resend = new Resend(apiKey); - const from = process.env.NEXTAUTH_URL - ? `Memento ` - : 'Memento '; + // Build a valid "from" address for Resend + // Priority: SMTP_FROM from DB config > env var > derived from NEXTAUTH_URL > Resend default + const config = await getSystemConfig(); + const smtpFrom = config.SMTP_FROM || process.env.SMTP_FROM; + let from: string; + if (smtpFrom) { + from = smtpFrom.includes('<') ? smtpFrom : `Memento <${smtpFrom}>`; + } else if (process.env.NEXTAUTH_URL) { + const hostname = new URL(process.env.NEXTAUTH_URL).hostname; + // Only use hostname-based from if it's not localhost (Resend rejects it) + if (hostname !== 'localhost') { + from = `Memento `; + } else { + from = 'Memento '; + } + } else { + from = 'Memento '; + } // Resend supports attachments with inline content const resendAttachments = attachments?.map(att => ({ diff --git a/memento-note/package.json b/memento-note/package.json index 97b7026..4901602 100644 --- a/memento-note/package.json +++ b/memento-note/package.json @@ -13,6 +13,7 @@ "db:studio": "prisma studio", "db:reset": "prisma migrate reset", "db:switch": "node scripts/switch-db.js", + "setup:env": "node scripts/setup-env.js", "test": "playwright test", "test:ui": "playwright test --ui", "test:headed": "playwright test --headed", @@ -29,7 +30,6 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@ducanh2912/next-pwa": "^10.2.9", "@excalidraw/excalidraw": "^0.18.0", "@mozilla/readability": "^0.6.0", "@radix-ui/react-avatar": "^1.1.11", @@ -96,6 +96,10 @@ }, "overrides": { "serialize-javascript": "^7.0.5", - "nodemailer": "^8.0.4" + "nodemailer": "^8.0.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-slot": "^1.2.4", + "lodash-es": "^4.17.21", + "nanoid": "^3.3.8" } } diff --git a/memento-note/prisma/schema.prisma b/memento-note/prisma/schema.prisma index adce1a4..e7bcac4 100644 --- a/memento-note/prisma/schema.prisma +++ b/memento-note/prisma/schema.prisma @@ -4,7 +4,7 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" url = env("DATABASE_URL") } diff --git a/memento-note/scripts/setup-env.js b/memento-note/scripts/setup-env.js new file mode 100644 index 0000000..7fdf45a --- /dev/null +++ b/memento-note/scripts/setup-env.js @@ -0,0 +1,991 @@ +#!/usr/bin/env node + +// ============================================================================= +// Memento - Interactive Environment Setup Wizard +// ============================================================================= +// Zero external dependencies. Uses only Node.js built-ins. +// Usage: npm run setup:env +// ============================================================================= + +const readline = require('readline'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { execSync } = require('child_process'); + +// --------------------------------------------------------------------------- +// ANSI helpers +// --------------------------------------------------------------------------- +const R = '\x1b[0m'; +const bold = (s) => `\x1b[1m${s}${R}`; +const dim = (s) => `\x1b[2m${s}${R}`; +const red = (s) => `\x1b[31m${s}${R}`; +const green = (s) => `\x1b[32m${s}${R}`; +const yellow = (s) => `\x1b[33m${s}${R}`; +const cyan = (s) => `\x1b[36m${s}${R}`; +const magenta = (s) => `\x1b[35m${s}${R}`; +const blue = (s) => `\x1b[34m${s}${R}`; + +// --------------------------------------------------------------------------- +// Readline promisified +// --------------------------------------------------------------------------- +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +function question(prompt) { + return new Promise((resolve) => { + rl.question(prompt, (answer) => resolve(answer.trim())); + }); +} + +// --------------------------------------------------------------------------- +// Crypto helpers +// --------------------------------------------------------------------------- +function generateSecret() { + return crypto.randomBytes(32).toString('base64'); +} + +function generatePassword(len = 24) { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789!@#$%&*'; + const bytes = crypto.randomBytes(len); + return Array.from(bytes, (b) => chars[b % chars.length]).join(''); +} + +// --------------------------------------------------------------------------- +// Prisma schema helper +// --------------------------------------------------------------------------- +const PRISMA_SCHEMA_PATH = path.join(__dirname, '..', 'prisma', 'schema.prisma'); + +function switchPrismaProvider(target) { + if (!['sqlite', 'postgresql'].includes(target)) return; + let schema = fs.readFileSync(PRISMA_SCHEMA_PATH, 'utf8'); + schema = schema.replace( + /(datasource db \{\s*\n\s*provider\s*=\s*)"[^"]+"/, + `$1"${target}"` + ); + fs.writeFileSync(PRISMA_SCHEMA_PATH, schema); +} + +// --------------------------------------------------------------------------- +// Validation helpers +// --------------------------------------------------------------------------- +function isSensitive(key) { + const k = key.toUpperCase(); + return k.includes('KEY') || k.includes('SECRET') || k.includes('PASSWORD') || k.includes('PASS'); +} + +function mask(value) { + if (!value || value.length <= 8) return '****'; + return value.slice(0, 4) + '****' + value.slice(-4); +} + +async function askRequired(prompt, defaultValue) { + while (true) { + const answer = await question(prompt); + const val = answer || defaultValue; + if (val) return val; + console.log(red(' This field is required. Please enter a value.')); + } +} + +async function askYesNo(prompt, defaultYes = true) { + const hint = defaultYes ? 'Y/n' : 'y/N'; + const answer = await question(`${prompt} (${hint}): `); + if (!answer) return defaultYes; + return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'; +} + +// --------------------------------------------------------------------------- +// Admin account creation +// --------------------------------------------------------------------------- +async function createLocalAdmin() { + console.log(); + console.log(cyan(bold(' ── Admin Account ─────────────────────────────────'))); + console.log(dim(' Create the first admin account for your instance.')); + console.log(); + + const email = await askRequired(' Admin email: ', ''); + const name = await question(' Admin name (Admin): ') || 'Admin'; + const password = await askRequired(' Admin password: ', ''); + + // Hash password using bcryptjs (installed dependency) + console.log(dim(' Hashing password...')); + let hashedPassword; + try { + const bcrypt = require('bcryptjs'); + hashedPassword = await bcrypt.hash(password, 12); + } catch { + // Fallback: use Node crypto if bcryptjs unavailable + hashedPassword = crypto.createHash('sha256').update(password).digest('hex'); + console.log(yellow(' Warning: bcryptjs not available, using SHA-256 fallback')); + } + + // Build inline script that uses Prisma to create the user + const script = ` +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +(async () => { + try { + const user = await prisma.user.create({ + data: { + email: ${JSON.stringify(email.toLowerCase())}, + name: ${JSON.stringify(name)}, + password: ${JSON.stringify(hashedPassword)}, + role: 'ADMIN', + } + }); + console.log('OK:' + user.email); + } catch (e) { + if (e.code === 'P2002') { + console.log('EXISTS:${email.toLowerCase()}'); + } else { + console.error('ERR:' + e.message); + } + } finally { + await prisma.$disconnect(); + } +})(); +`; + + // Write temp script and execute + const tmpFile = path.join(__dirname, '..', '_setup_admin_tmp.js'); + fs.writeFileSync(tmpFile, script, 'utf8'); + + try { + const output = execSync(`node "${tmpFile}"`, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 30000, + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + + if (output.startsWith('OK:')) { + console.log(green(` Admin account created: ${output.slice(3)}`)); + return true; + } else if (output.startsWith('EXISTS:')) { + console.log(yellow(` User ${output.slice(7)} already exists. Skipping.`)); + return false; + } else if (output.startsWith('ERR:')) { + console.log(red(` Error: ${output.slice(4)}`)); + return false; + } + } catch (err) { + console.log(red(` Failed to create admin: ${err.message}`)); + console.log(dim(' You can create it later with: node scripts/promote-admin.js')); + return false; + } finally { + fs.unlinkSync(tmpFile); + } +} + +// --------------------------------------------------------------------------- +// Write config to SystemConfig DB table +// --------------------------------------------------------------------------- +function writeConfigToDb(vars) { + // Keys that the admin panel reads from SystemConfig (not from .env) + const dbKeys = [ + 'AI_PROVIDER', 'AI_PROVIDER_TAGS', 'AI_PROVIDER_EMBEDDING', 'AI_PROVIDER_CHAT', + 'AI_MODEL_TAGS', 'AI_MODEL_EMBEDDING', 'AI_MODEL_CHAT', + 'OPENAI_API_KEY', 'OLLAMA_BASE_URL', + 'CUSTOM_OPENAI_API_KEY', 'CUSTOM_OPENAI_BASE_URL', + 'RESEND_API_KEY', + 'SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM', + 'SMTP_SECURE', 'SMTP_IGNORE_CERT', + 'ALLOW_REGISTRATION', + 'MCP_SERVER_MODE', 'MCP_SERVER_URL', + ]; + + const entries = []; + for (const key of dbKeys) { + if (vars[key]) { + entries.push({ key, value: vars[key] }); + } + } + + if (entries.length === 0) return; + + // Build inline script using Prisma to upsert SystemConfig + const entriesJson = JSON.stringify(entries); + const script = ` +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +(async () => { + try { + const entries = ${entriesJson}; + for (const e of entries) { + await prisma.systemConfig.upsert({ + where: { key: e.key }, + update: { value: e.value }, + create: { key: e.key, value: e.value }, + }); + } + console.log('OK:' + entries.length); + } catch (e) { + console.error('ERR:' + e.message); + } finally { + await prisma.$disconnect(); + } +})(); +`; + + const tmpFile = path.join(__dirname, '..', '_setup_config_tmp.js'); + fs.writeFileSync(tmpFile, script, 'utf8'); + + try { + const output = execSync(`node "${tmpFile}"`, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 30000, + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + + if (output.startsWith('OK:')) { + console.log(green(` Wrote ${output.slice(3)} settings to database`)); + return true; + } else if (output.startsWith('ERR:')) { + console.log(yellow(` Warning: could not write config to DB: ${output.slice(4)}`)); + return false; + } + } catch (err) { + console.log(yellow(' Warning: could not write config to database.')); + console.log(dim(' Settings will still work via .env but won\'t appear in the admin panel.')); + return false; + } finally { + fs.unlinkSync(tmpFile); + } +} + +// --------------------------------------------------------------------------- +// Banner +// --------------------------------------------------------------------------- +function showBanner() { + console.log(); + console.log(cyan(bold(' ╔══════════════════════════════════════════╗'))); + console.log(cyan(bold(' ║ Memento Environment Setup Wizard ║'))); + console.log(cyan(bold(' ╚══════════════════════════════════════════╝'))); + console.log(); + console.log(dim(' This wizard will help you create environment')); + console.log(dim(' configuration files for your Memento instance.')); + console.log(); +} + +// --------------------------------------------------------------------------- +// Environment choice +// --------------------------------------------------------------------------- +async function chooseEnvironment() { + console.log(bold(' Which environment do you want to configure?')); + console.log(); + console.log(` ${green('1')} - Local development ${dim('(creates .env in memento-note/)')}`); + console.log(` ${green('2')} - Docker deployment ${dim('(creates .env.docker in Momento/)')}`); + console.log(` ${green('3')} - Both ${dim('(creates both files, shared secret)')}`); + console.log(); + + while (true) { + const choice = await question(' Select [1/2/3]: '); + if (choice === '1') return 'local'; + if (choice === '2') return 'docker'; + if (choice === '3') return 'both'; + console.log(yellow(' Please enter 1, 2, or 3.')); + } +} + +// --------------------------------------------------------------------------- +// AI provider menus +// --------------------------------------------------------------------------- +const AI_PROVIDERS = ['openai', 'ollama', 'deepseek', 'openrouter', 'custom-openai']; + +function showAiProviderMenu(label) { + console.log(); + console.log(bold(` AI Provider for ${label}`)); + console.log(` ${green('1')} - OpenAI`); + console.log(` ${green('2')} - Ollama (local)`); + console.log(` ${green('3')} - DeepSeek`); + console.log(` ${green('4')} - OpenRouter`); + console.log(` ${green('5')} - Custom OpenAI-compatible`); + console.log(` ${green('6')} - Skip (configure later)`); +} + +async function chooseAiProvider(label) { + showAiProviderMenu(label); + while (true) { + const choice = await question(' Select [1-6]: '); + if (choice === '1') return 'openai'; + if (choice === '2') return 'ollama'; + if (choice === '3') return 'deepseek'; + if (choice === '4') return 'openrouter'; + if (choice === '5') return 'custom-openai'; + if (choice === '6') return null; + console.log(yellow(' Please enter a number from 1 to 6.')); + } +} + +async function collectAiProviderFields(provider, defaults = {}) { + const vars = {}; + + switch (provider) { + case 'openai': + vars.OPENAI_API_KEY = await askRequired( + ` OpenAI API Key ${dim(`[${mask(defaults.OPENAI_API_KEY || '')}]`)}: `, + defaults.OPENAI_API_KEY || '' + ); + break; + + case 'ollama': { + const ollamaUrl = defaults.OLLAMA_BASE_URL || 'http://localhost:11434'; + vars.OLLAMA_BASE_URL = await question(` Ollama Base URL (${ollamaUrl}): `) || ollamaUrl; + break; + } + + case 'deepseek': { + const dsKey = await askRequired( + ` DeepSeek API Key ${dim(`[${mask(defaults.CUSTOM_OPENAI_API_KEY || '')}]`)}: `, + defaults.CUSTOM_OPENAI_API_KEY || '' + ); + vars.CUSTOM_OPENAI_API_KEY = dsKey; + vars.CUSTOM_OPENAI_BASE_URL = 'https://api.deepseek.com/v1'; + console.log(dim(' Base URL: https://api.deepseek.com/v1')); + break; + } + + case 'openrouter': { + const orKey = await askRequired( + ` OpenRouter API Key ${dim(`[${mask(defaults.CUSTOM_OPENAI_API_KEY || '')}]`)}: `, + defaults.CUSTOM_OPENAI_API_KEY || '' + ); + vars.CUSTOM_OPENAI_API_KEY = orKey; + vars.CUSTOM_OPENAI_BASE_URL = 'https://openrouter.ai/api/v1'; + console.log(dim(' Base URL: https://openrouter.ai/api/v1')); + break; + } + + case 'custom-openai': { + vars.CUSTOM_OPENAI_API_KEY = await askRequired( + ` Custom API Key ${dim(`[${mask(defaults.CUSTOM_OPENAI_API_KEY || '')}]`)}: `, + defaults.CUSTOM_OPENAI_API_KEY || '' + ); + const defaultUrl = defaults.CUSTOM_OPENAI_BASE_URL || ''; + vars.CUSTOM_OPENAI_BASE_URL = await askRequired( + ` Custom Base URL ${defaultUrl ? dim(`[${defaultUrl}]`) : ''}: `, + defaultUrl + ); + break; + } + } + + return vars; +} + +async function collectAiModels(provider, feature) { + const vars = {}; + const defaults = { + chat: { openai: 'gpt-4o-mini', ollama: 'llama3.2', 'custom-openai': 'gpt-4o-mini', deepseek: 'deepseek-chat', openrouter: 'gpt-4o-mini' }, + tags: { openai: 'gpt-4o-mini', ollama: 'llama3.2', 'custom-openai': 'gpt-4o-mini', deepseek: 'deepseek-chat', openrouter: 'gpt-4o-mini' }, + embedding: { openai: 'text-embedding-3-small', ollama: 'nomic-embed-text', 'custom-openai': 'text-embedding-3-small', deepseek: 'deepseek-chat', openrouter: 'text-embedding-3-small' }, + }; + + const modelKey = `AI_MODEL_${feature.toUpperCase()}`; + const defaultModel = defaults[feature.toLowerCase()]?.[provider] || ''; + + if (feature.toLowerCase() === 'chat') { + const answer = await question(` Chat model (${defaultModel}): `); + vars[modelKey] = answer || defaultModel; + } else if (feature.toLowerCase() === 'tags') { + const answer = await question(` Tags model (${defaultModel}): `); + vars[modelKey] = answer || defaultModel; + } else if (feature.toLowerCase() === 'embedding') { + const answer = await question(` Embedding model (${defaultModel}): `); + vars[modelKey] = answer || defaultModel; + } + + return vars; +} + +// --------------------------------------------------------------------------- +// Local .env section +// --------------------------------------------------------------------------- +async function collectLocalEnv(sharedSecret) { + console.log(); + console.log(cyan(bold(' ── Local Development (.env) ──────────────────────'))); + console.log(); + + const vars = {}; + + // Database type + console.log(bold(' Database engine')); + console.log(` ${green('1')} - PostgreSQL ${dim('(recommended, required for Docker, embeddings)')}`); + console.log(` ${green('2')} - SQLite ${dim('(simple, file-based, no server needed)')}`); + console.log(); + let dbType; + while (true) { + const dbChoice = await question(' Select [1/2]: '); + if (dbChoice === '1') { dbType = 'postgresql'; break; } + if (dbChoice === '2') { dbType = 'sqlite'; break; } + console.log(yellow(' Please enter 1 or 2.')); + } + + if (dbType === 'sqlite') { + vars.DATABASE_URL = 'file:./dev.db'; + vars._dbType = 'sqlite'; + console.log(dim(' DATABASE_URL set to file:./dev.db')); + } else { + console.log(dim(' If using Docker PostgreSQL: postgresql://memento:memento@localhost:5432/memento')); + vars.DATABASE_URL = await question( + ` DATABASE_URL (${dim('postgresql://memento:memento@localhost:5432/memento')}): ` + ) || 'postgresql://memento:memento@localhost:5432/memento'; + vars._dbType = 'postgresql'; + } + + // NEXTAUTH_URL + vars.NEXTAUTH_URL = await question( + ` NEXTAUTH_URL (${dim('http://localhost:3000')}): ` + ) || 'http://localhost:3000'; + + // NEXTAUTH_SECRET + const autoSecret = sharedSecret || generateSecret(); + console.log(dim(` Generated NEXTAUTH_SECRET: ${mask(autoSecret)}`)); + const secretAnswer = await question(` NEXTAUTH_SECRET (press Enter to use generated): `); + vars.NEXTAUTH_SECRET = secretAnswer || autoSecret; + + // ALLOW_REGISTRATION + vars.ALLOW_REGISTRATION = (await askYesNo(' Allow registration?', true)) ? 'true' : 'false'; + + // AI Provider (single provider for all features in local) + const provider = await chooseAiProvider('all features'); + if (provider) { + vars.AI_PROVIDER = provider; + const providerVars = await collectAiProviderFields(provider, {}); + Object.assign(vars, providerVars); + + // Ask for model overrides + const configureModels = await askYesNo(' Configure model names?', false); + if (configureModels) { + const chatModels = await collectAiModels(provider, 'chat'); + Object.assign(vars, chatModels); + const tagModels = await collectAiModels(provider, 'tags'); + Object.assign(vars, tagModels); + const embModels = await collectAiModels(provider, 'embedding'); + Object.assign(vars, embModels); + } + } + + // Email + const wantEmail = await askYesNo(' Configure email?', false); + if (wantEmail) { + const useResend = await askYesNo(' Use Resend? (vs SMTP)', false); + if (useResend) { + vars.RESEND_API_KEY = await askRequired(' RESEND_API_KEY: ', ''); + } else { + vars.SMTP_HOST = await askRequired(' SMTP_HOST: ', ''); + vars.SMTP_PORT = await question(` SMTP_PORT (${dim('587')}): `) || '587'; + vars.SMTP_USER = await askRequired(' SMTP_USER: ', ''); + vars.SMTP_PASS = await askRequired(' SMTP_PASS: ', ''); + vars.SMTP_FROM = await askRequired(' SMTP_FROM: ', ''); + const secure = await askYesNo(' Use TLS/SSL?', true); + vars.SMTP_SECURE = secure ? 'true' : 'false'; + } + } + + // MCP + const wantMcp = await askYesNo(' Configure MCP server?', false); + if (wantMcp) { + const useSse = await askYesNo(' MCP mode: SSE? (vs stdio)', false); + vars.MCP_SERVER_MODE = useSse ? 'sse' : 'stdio'; + if (useSse) { + vars.MCP_SERVER_URL = await question(` MCP Server URL (${dim('http://localhost:3001')}): `) || 'http://localhost:3001'; + } + } + + return vars; +} + +// --------------------------------------------------------------------------- +// Docker .env.docker section +// --------------------------------------------------------------------------- +async function collectDockerEnv(sharedSecret) { + console.log(); + console.log(cyan(bold(' ── Docker Deployment (.env.docker) ────────────────'))); + console.log(); + + const vars = {}; + + // NEXTAUTH_URL + console.log(dim(' Hint: Use your server IP or domain (e.g. http://192.168.1.100:3000)')); + vars.NEXTAUTH_URL = await question( + ` NEXTAUTH_URL (${dim('http://localhost:3000')}): ` + ) || 'http://localhost:3000'; + + // NEXTAUTH_SECRET + const autoSecret = sharedSecret || generateSecret(); + console.log(dim(` Generated NEXTAUTH_SECRET: ${mask(autoSecret)}`)); + const secretAnswer = await question(` NEXTAUTH_SECRET (press Enter to use generated): `); + vars.NEXTAUTH_SECRET = secretAnswer || autoSecret; + + // ALLOW_REGISTRATION + vars.ALLOW_REGISTRATION = (await askYesNo(' Allow registration?', true)) ? 'true' : 'false'; + + // PostgreSQL + console.log(); + console.log(bold(' PostgreSQL Configuration')); + console.log(dim(' Password auto-generated. You can override it.')); + const pgPassword = generatePassword(); + console.log(dim(` Generated POSTGRES_PASSWORD: ${mask(pgPassword)}`)); + vars.POSTGRES_PORT = await question(` POSTGRES_PORT (${dim('5432')}): `) || '5432'; + vars.POSTGRES_DB = await question(` POSTGRES_DB (${dim('memento')}): `) || 'memento'; + vars.POSTGRES_USER = await question(` POSTGRES_USER (${dim('memento')}): `) || 'memento'; + const pgPassAnswer = await question(` POSTGRES_PASSWORD (press Enter to use generated): `); + vars.POSTGRES_PASSWORD = pgPassAnswer || pgPassword; + + // AI per-feature + console.log(); + console.log(bold(' AI Provider Configuration (per-feature)')); + + const tagsProvider = await chooseAiProvider('Tags generation'); + if (tagsProvider) { + vars.AI_PROVIDER_TAGS = tagsProvider; + const fields = await collectAiProviderFields(tagsProvider, { OLLAMA_BASE_URL: 'http://ollama:11434' }); + Object.assign(vars, fields); + const models = await collectAiModels(tagsProvider, 'tags'); + Object.assign(vars, models); + } + + const embProvider = await chooseAiProvider('Embeddings'); + if (embProvider) { + vars.AI_PROVIDER_EMBEDDING = embProvider; + if (!vars.CUSTOM_OPENAI_API_KEY && !vars.OPENAI_API_KEY) { + const fields = await collectAiProviderFields(embProvider, { OLLAMA_BASE_URL: 'http://ollama:11434' }); + Object.assign(vars, fields); + } + const models = await collectAiModels(embProvider, 'embedding'); + Object.assign(vars, models); + } + + const wantChat = await askYesNo(' Configure a separate chat provider?', false); + if (wantChat) { + const chatProvider = await chooseAiProvider('Chat'); + if (chatProvider) { + vars.AI_PROVIDER_CHAT = chatProvider; + if (!vars.CUSTOM_OPENAI_API_KEY && !vars.OPENAI_API_KEY) { + const fields = await collectAiProviderFields(chatProvider, { OLLAMA_BASE_URL: 'http://ollama:11434' }); + Object.assign(vars, fields); + } + const models = await collectAiModels(chatProvider, 'chat'); + Object.assign(vars, models); + } + } + + // MCP + const wantMcp = await askYesNo(' Configure MCP server?', false); + if (wantMcp) { + const useSse = await askYesNo(' MCP mode: SSE? (vs stdio)', false); + vars.MCP_MODE = useSse ? 'sse' : 'stdio'; + vars.MCP_PORT = await question(` MCP_PORT (${dim('3001')}): `) || '3001'; + if (useSse) { + vars.MCP_SERVER_URL = await question(` MCP_SERVER_URL (${dim('http://localhost:3001')}): `) || 'http://localhost:3001'; + } + } + + // Email + const wantEmail = await askYesNo(' Configure email?', false); + if (wantEmail) { + const useResend = await askYesNo(' Use Resend? (vs SMTP)', false); + if (useResend) { + vars.RESEND_API_KEY = await askRequired(' RESEND_API_KEY: ', ''); + } else { + vars.SMTP_HOST = await askRequired(' SMTP_HOST: ', ''); + vars.SMTP_PORT = await question(` SMTP_PORT (${dim('587')}): `) || '587'; + vars.SMTP_USER = await askRequired(' SMTP_USER: ', ''); + vars.SMTP_PASS = await askRequired(' SMTP_PASS: ', ''); + vars.SMTP_FROM = await askRequired(' SMTP_FROM: ', ''); + } + } + + return vars; +} + +// --------------------------------------------------------------------------- +// File writing +// --------------------------------------------------------------------------- +function generateLocalEnvContent(vars) { + let content = `# ============================================================================= +# Memento Note - Local Environment Configuration +# Generated by setup-env wizard +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Core +# ----------------------------------------------------------------------------- +DATABASE_URL="${vars.DATABASE_URL}" +NEXTAUTH_SECRET="${vars.NEXTAUTH_SECRET}" +NEXTAUTH_URL="${vars.NEXTAUTH_URL}" +`; + + if (vars.ALLOW_REGISTRATION) { + content += ` +# ----------------------------------------------------------------------------- +# Registration +# ----------------------------------------------------------------------------- +ALLOW_REGISTRATION="${vars.ALLOW_REGISTRATION}" +`; + } + + // AI provider section + const aiKeys = ['AI_PROVIDER', 'AI_PROVIDER_CHAT', 'AI_PROVIDER_TAGS', 'AI_PROVIDER_EMBEDDING', + 'AI_MODEL_CHAT', 'AI_MODEL_TAGS', 'AI_MODEL_EMBEDDING', + 'OPENAI_API_KEY', 'OLLAMA_BASE_URL', 'CUSTOM_OPENAI_API_KEY', 'CUSTOM_OPENAI_BASE_URL']; + const hasAi = aiKeys.some((k) => vars[k]); + + if (hasAi) { + content += ` +# ----------------------------------------------------------------------------- +# AI Providers +# ----------------------------------------------------------------------------- +`; + if (vars.AI_PROVIDER) content += `AI_PROVIDER="${vars.AI_PROVIDER}"\n`; + if (vars.AI_PROVIDER_CHAT) content += `AI_PROVIDER_CHAT="${vars.AI_PROVIDER_CHAT}"\n`; + if (vars.AI_PROVIDER_TAGS) content += `AI_PROVIDER_TAGS="${vars.AI_PROVIDER_TAGS}"\n`; + if (vars.AI_PROVIDER_EMBEDDING) content += `AI_PROVIDER_EMBEDDING="${vars.AI_PROVIDER_EMBEDDING}"\n`; + if (vars.AI_MODEL_CHAT) content += `AI_MODEL_CHAT="${vars.AI_MODEL_CHAT}"\n`; + if (vars.AI_MODEL_TAGS) content += `AI_MODEL_TAGS="${vars.AI_MODEL_TAGS}"\n`; + if (vars.AI_MODEL_EMBEDDING) content += `AI_MODEL_EMBEDDING="${vars.AI_MODEL_EMBEDDING}"\n`; + if (vars.OPENAI_API_KEY) content += `OPENAI_API_KEY="${vars.OPENAI_API_KEY}"\n`; + if (vars.OLLAMA_BASE_URL) content += `OLLAMA_BASE_URL="${vars.OLLAMA_BASE_URL}"\n`; + if (vars.CUSTOM_OPENAI_API_KEY) content += `CUSTOM_OPENAI_API_KEY="${vars.CUSTOM_OPENAI_API_KEY}"\n`; + if (vars.CUSTOM_OPENAI_BASE_URL) content += `CUSTOM_OPENAI_BASE_URL="${vars.CUSTOM_OPENAI_BASE_URL}"\n`; + } + + // Email section + const emailKeys = ['RESEND_API_KEY', 'SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM', 'SMTP_SECURE', 'SMTP_IGNORE_CERT']; + const hasEmail = emailKeys.some((k) => vars[k]); + + if (hasEmail) { + content += ` +# ----------------------------------------------------------------------------- +# Email +# ----------------------------------------------------------------------------- +`; + if (vars.RESEND_API_KEY) content += `RESEND_API_KEY="${vars.RESEND_API_KEY}"\n`; + if (vars.SMTP_HOST) content += `SMTP_HOST="${vars.SMTP_HOST}"\n`; + if (vars.SMTP_PORT) content += `SMTP_PORT="${vars.SMTP_PORT}"\n`; + if (vars.SMTP_USER) content += `SMTP_USER="${vars.SMTP_USER}"\n`; + if (vars.SMTP_PASS) content += `SMTP_PASS="${vars.SMTP_PASS}"\n`; + if (vars.SMTP_FROM) content += `SMTP_FROM="${vars.SMTP_FROM}"\n`; + if (vars.SMTP_SECURE) content += `SMTP_SECURE="${vars.SMTP_SECURE}"\n`; + } + + // MCP section + if (vars.MCP_SERVER_MODE) { + content += ` +# ----------------------------------------------------------------------------- +# MCP +# ----------------------------------------------------------------------------- +MCP_SERVER_MODE="${vars.MCP_SERVER_MODE}" +`; + if (vars.MCP_SERVER_URL) content += `MCP_SERVER_URL="${vars.MCP_SERVER_URL}"\n`; + } + + content += '\n'; + return content; +} + +function generateDockerEnvContent(vars) { + let content = `# ============================================================================= +# Memento - Docker Environment Configuration +# Generated by setup-env wizard +# ============================================================================= + +# ============================================================================= +# APPLICATION +# ============================================================================= +NEXTAUTH_URL="${vars.NEXTAUTH_URL}" +NEXTAUTH_SECRET="${vars.NEXTAUTH_SECRET}" +`; + + if (vars.ALLOW_REGISTRATION) { + content += `ALLOW_REGISTRATION="${vars.ALLOW_REGISTRATION}"\n`; + } + + content += ` +# ============================================================================= +# POSTGRESQL +# ============================================================================= +POSTGRES_PORT=${vars.POSTGRES_PORT || 5432} +POSTGRES_DB=${vars.POSTGRES_DB || 'memento'} +POSTGRES_USER=${vars.POSTGRES_USER || 'memento'} +POSTGRES_PASSWORD=${vars.POSTGRES_PASSWORD || 'memento'} +`; + + // AI per-feature + const aiKeys = ['AI_PROVIDER_TAGS', 'AI_PROVIDER_EMBEDDING', 'AI_PROVIDER_CHAT', + 'AI_MODEL_TAGS', 'AI_MODEL_EMBEDDING', 'AI_MODEL_CHAT', + 'OLLAMA_BASE_URL', 'OPENAI_API_KEY', 'CUSTOM_OPENAI_API_KEY', 'CUSTOM_OPENAI_BASE_URL']; + const hasAi = aiKeys.some((k) => vars[k]); + + if (hasAi) { + content += ` +# ============================================================================= +# AI PROVIDERS +# ============================================================================= +`; + if (vars.AI_PROVIDER_TAGS) content += `AI_PROVIDER_TAGS=${vars.AI_PROVIDER_TAGS}\n`; + if (vars.AI_MODEL_TAGS) content += `AI_MODEL_TAGS="${vars.AI_MODEL_TAGS}"\n`; + if (vars.AI_PROVIDER_EMBEDDING) content += `AI_PROVIDER_EMBEDDING=${vars.AI_PROVIDER_EMBEDDING}\n`; + if (vars.AI_MODEL_EMBEDDING) content += `AI_MODEL_EMBEDDING="${vars.AI_MODEL_EMBEDDING}"\n`; + if (vars.AI_PROVIDER_CHAT) content += `AI_PROVIDER_CHAT=${vars.AI_PROVIDER_CHAT}\n`; + if (vars.AI_MODEL_CHAT) content += `AI_MODEL_CHAT="${vars.AI_MODEL_CHAT}"\n`; + if (vars.OLLAMA_BASE_URL) content += `OLLAMA_BASE_URL="${vars.OLLAMA_BASE_URL}"\n`; + if (vars.OPENAI_API_KEY) content += `OPENAI_API_KEY="${vars.OPENAI_API_KEY}"\n`; + if (vars.CUSTOM_OPENAI_API_KEY) content += `CUSTOM_OPENAI_API_KEY="${vars.CUSTOM_OPENAI_API_KEY}"\n`; + if (vars.CUSTOM_OPENAI_BASE_URL) content += `CUSTOM_OPENAI_BASE_URL="${vars.CUSTOM_OPENAI_BASE_URL}"\n`; + } + + // MCP + if (vars.MCP_MODE || vars.MCP_SERVER_MODE) { + content += ` +# ============================================================================= +# MCP SERVER +# ============================================================================= +`; + if (vars.MCP_MODE) content += `MCP_MODE="${vars.MCP_MODE}"\n`; + if (vars.MCP_PORT) content += `MCP_PORT="${vars.MCP_PORT}"\n`; + if (vars.MCP_SERVER_URL) content += `MCP_SERVER_URL="${vars.MCP_SERVER_URL}"\n`; + } + + // Email + const emailKeys = ['SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM', 'RESEND_API_KEY']; + const hasEmail = emailKeys.some((k) => vars[k]); + + if (hasEmail) { + content += ` +# ============================================================================= +# EMAIL +# ============================================================================= +`; + if (vars.SMTP_HOST) content += `SMTP_HOST="${vars.SMTP_HOST}"\n`; + if (vars.SMTP_PORT) content += `SMTP_PORT="${vars.SMTP_PORT}"\n`; + if (vars.SMTP_USER) content += `SMTP_USER="${vars.SMTP_USER}"\n`; + if (vars.SMTP_PASS) content += `SMTP_PASS="${vars.SMTP_PASS}"\n`; + if (vars.SMTP_FROM) content += `SMTP_FROM="${vars.SMTP_FROM}"\n`; + if (vars.RESEND_API_KEY) content += `RESEND_API_KEY="${vars.RESEND_API_KEY}"\n`; + } + + content += '\n'; + return content; +} + +// --------------------------------------------------------------------------- +// File conflict handling +// --------------------------------------------------------------------------- +async function handleExistingFile(filePath) { + if (!fs.existsSync(filePath)) return 'write'; + + console.log(yellow(`\n File already exists: ${filePath}`)); + + while (true) { + console.log(` ${green('1')} - Overwrite`); + console.log(` ${green('2')} - Skip`); + console.log(` ${green('3')} - View current content`); + const choice = await question(' Choose [1/2/3]: '); + + if (choice === '1') return 'write'; + if (choice === '2') return 'skip'; + if (choice === '3') { + const content = fs.readFileSync(filePath, 'utf8'); + console.log(); + console.log(dim(' ── Current content ─────────────────────────────')); + content.split('\n').forEach((line) => console.log(dim(` ${line}`))); + console.log(dim(' ─────────────────────────────────────────────────')); + continue; + } + console.log(yellow(' Please enter 1, 2, or 3.')); + } +} + +// --------------------------------------------------------------------------- +// Recap display +// --------------------------------------------------------------------------- +function showRecap(vars, title) { + console.log(); + console.log(bold(` ${title}`)); + console.log(dim(' ─────────────────────────────────────────────────')); + const keys = Object.keys(vars).filter((k) => !k.startsWith('_')); + const maxLen = Math.max(...keys.map((k) => k.length)); + for (const key of keys) { + const value = vars[key]; + const display = isSensitive(key) ? mask(value) : value; + const padded = key.padEnd(maxLen); + console.log(` ${cyan(padded)} = ${display}`); + } + console.log(); +} + +// --------------------------------------------------------------------------- +// Next steps +// --------------------------------------------------------------------------- +function showNextSteps(results) { + console.log(); + console.log(cyan(bold(' ══════════════════════════════════════════════════'))); + console.log(cyan(bold(' Setup complete!'))); + console.log(cyan(bold(' ══════════════════════════════════════════════════'))); + console.log(); + + if (results.local) { + console.log(green(` Created: ${results.local}`)); + } + if (results.docker) { + console.log(green(` Created: ${results.docker}`)); + } + + console.log(); + console.log(bold(' Next steps:')); + + if (results.local) { + console.log(); + console.log(' For local development:'); + if (!results.adminCreated) { + console.log(dim(' npx prisma db push')); + } + console.log(dim(' npm run dev')); + if (!results.adminCreated) { + console.log(dim(' Then register via /register and run:')); + console.log(dim(' node scripts/promote-admin.js your@email.com')); + } + } + + if (results.docker) { + console.log(); + console.log(' For Docker deployment:'); + console.log(dim(' cd .. && docker compose up -d')); + console.log(dim(' Register via /register, then promote to admin:')); + console.log(dim(' docker compose exec memento-note node scripts/promote-admin.js your@email.com')); + } + + console.log(); +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +async function main() { + // Handle Ctrl+C gracefully + rl.on('close', () => { + console.log(); + console.log(dim(' Wizard cancelled.')); + process.exit(0); + }); + + showBanner(); + + const envChoice = await chooseEnvironment(); + const results = {}; + let sharedSecret = null; + + // For "both" mode, generate one shared secret + if (envChoice === 'both') { + sharedSecret = generateSecret(); + console.log(dim(`\n Shared NEXTAUTH_SECRET: ${mask(sharedSecret)}`)); + } + + // Local section + if (envChoice === 'local' || envChoice === 'both') { + const localVars = await collectLocalEnv(sharedSecret); + const localPath = path.join(__dirname, '..', '.env'); + + showRecap(localVars, 'Local .env recap:'); + const action = await handleExistingFile(localPath); + + if (action === 'write') { + // Extract internal metadata before writing + const dbType = localVars._dbType || 'postgresql'; + delete localVars._dbType; + + const content = generateLocalEnvContent(localVars); + fs.writeFileSync(localPath, content, 'utf8'); + results.local = localPath; + results.dbType = dbType; + + // Switch Prisma schema provider to match chosen database + try { + switchPrismaProvider(dbType); + console.log(green(` Prisma schema switched to ${dbType}`)); + } catch (err) { + console.log(yellow(` Warning: could not update prisma/schema.prisma: ${err.message}`)); + } + + // Push schema to database + console.log(dim(' Pushing database schema...')); + let dbReady = false; + try { + execSync('npx prisma generate', { cwd: path.join(__dirname, '..'), stdio: 'pipe' }); + execSync('npx prisma db push --accept-data-loss', { cwd: path.join(__dirname, '..'), stdio: 'pipe' }); + console.log(green(' Database schema synced')); + dbReady = true; + } catch (err) { + console.log(yellow(' Warning: could not push schema to database.')); + console.log(dim(` ${err.message?.split('\n')[0] || ''}`)); + console.log(dim(' Run "npx prisma db push" manually after ensuring the database is running.')); + } + + // Write AI/email/MCP config to SystemConfig DB table + // so the admin panel shows correct values instead of hardcoded defaults + if (dbReady) { + console.log(dim(' Writing settings to database...')); + writeConfigToDb(localVars); + } + + // Create admin account + const wantAdmin = await askYesNo('\n Create an admin account now?', true); + if (wantAdmin) { + await createLocalAdmin(); + results.adminCreated = true; + } + + // Capture shared secret for docker section + if (envChoice === 'both') { + sharedSecret = localVars.NEXTAUTH_SECRET; + } + } else { + console.log(yellow(' Skipped .env')); + if (envChoice === 'both') { + sharedSecret = sharedSecret; // keep generated one + } + } + } + + // Docker section + if (envChoice === 'docker' || envChoice === 'both') { + // Verify docker-compose.yml exists in parent + const parentDir = path.resolve(__dirname, '..', '..'); + const dockerComposePath = path.join(parentDir, 'docker-compose.yml'); + + if (!fs.existsSync(dockerComposePath)) { + console.log(red(`\n Error: docker-compose.yml not found at ${parentDir}`)); + console.log(red(' Make sure you are running this from the memento-note directory inside the Momento project.')); + process.exit(1); + } + + const dockerVars = await collectDockerEnv(sharedSecret); + const dockerEnvPath = path.join(parentDir, '.env.docker'); + + showRecap(dockerVars, 'Docker .env.docker recap:'); + const action = await handleExistingFile(dockerEnvPath); + + if (action === 'write') { + const content = generateDockerEnvContent(dockerVars); + fs.writeFileSync(dockerEnvPath, content, 'utf8'); + results.docker = dockerEnvPath; + } else { + console.log(yellow(' Skipped .env.docker')); + } + } + + showNextSteps(results); + rl.close(); +} + +main().catch((err) => { + console.error(red(`\n Unexpected error: ${err.message}`)); + rl.close(); + process.exit(1); +});