diff --git a/CLAUDE.md b/CLAUDE.md index d46d1284..128cd8d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,4 +172,5 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali - `DATA_DIR` - Data storage directory (default: ./data) - `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory - `AUTOMAKER_MOCK_AGENT=true` - Enable mock agent mode for CI testing +- `AUTOMAKER_AUTO_LOGIN=true` - Skip login prompt in development (disabled when NODE_ENV=production) - `VITE_HOSTNAME` - Hostname for frontend API URLs (default: localhost) diff --git a/README.md b/README.md index 3f9889fc..75705673 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,7 @@ npm run lint - `VITE_SKIP_ELECTRON` - Skip Electron in dev mode - `OPEN_DEVTOOLS` - Auto-open DevTools in Electron - `AUTOMAKER_SKIP_SANDBOX_WARNING` - Skip sandbox warning dialog (useful for dev/CI) +- `AUTOMAKER_AUTO_LOGIN=true` - Skip login prompt in development (ignored when NODE_ENV=production) ### Authentication Setup diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 38f0fa43..43c65992 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -113,24 +113,37 @@ export function isRequestLoggingEnabled(): boolean { return requestLoggingEnabled; } +// Width for log box content (excluding borders) +const BOX_CONTENT_WIDTH = 67; + // Check for required environment variables const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; if (!hasAnthropicKey) { + const wHeader = '⚠️ WARNING: No Claude authentication configured'.padEnd(BOX_CONTENT_WIDTH); + const w1 = 'The Claude Agent SDK requires authentication to function.'.padEnd(BOX_CONTENT_WIDTH); + const w2 = 'Set your Anthropic API key:'.padEnd(BOX_CONTENT_WIDTH); + const w3 = ' export ANTHROPIC_API_KEY="sk-ant-..."'.padEnd(BOX_CONTENT_WIDTH); + const w4 = 'Or use the setup wizard in Settings to configure authentication.'.padEnd( + BOX_CONTENT_WIDTH + ); + logger.warn(` -╔═══════════════════════════════════════════════════════════════════════╗ -║ ⚠️ WARNING: No Claude authentication configured ║ -║ ║ -║ The Claude Agent SDK requires authentication to function. ║ -║ ║ -║ Set your Anthropic API key: ║ -║ export ANTHROPIC_API_KEY="sk-ant-..." ║ -║ ║ -║ Or use the setup wizard in Settings to configure authentication. ║ -╚═══════════════════════════════════════════════════════════════════════╝ +╔═════════════════════════════════════════════════════════════════════╗ +║ ${wHeader}║ +╠═════════════════════════════════════════════════════════════════════╣ +║ ║ +║ ${w1}║ +║ ║ +║ ${w2}║ +║ ${w3}║ +║ ║ +║ ${w4}║ +║ ║ +╚═════════════════════════════════════════════════════════════════════╝ `); } else { - logger.info('✓ ANTHROPIC_API_KEY detected (API key auth)'); + logger.info('✓ ANTHROPIC_API_KEY detected'); } // Initialize security @@ -649,40 +662,74 @@ const startServer = (port: number, host: string) => { ? 'enabled (password protected)' : 'enabled' : 'disabled'; - const portStr = port.toString().padEnd(4); + + // Build URLs for display + const listenAddr = `${host}:${port}`; + const httpUrl = `http://${HOSTNAME}:${port}`; + const wsEventsUrl = `ws://${HOSTNAME}:${port}/api/events`; + const wsTerminalUrl = `ws://${HOSTNAME}:${port}/api/terminal/ws`; + const healthUrl = `http://${HOSTNAME}:${port}/api/health`; + + const sHeader = '🚀 Automaker Backend Server'.padEnd(BOX_CONTENT_WIDTH); + const s1 = `Listening: ${listenAddr}`.padEnd(BOX_CONTENT_WIDTH); + const s2 = `HTTP API: ${httpUrl}`.padEnd(BOX_CONTENT_WIDTH); + const s3 = `WebSocket: ${wsEventsUrl}`.padEnd(BOX_CONTENT_WIDTH); + const s4 = `Terminal WS: ${wsTerminalUrl}`.padEnd(BOX_CONTENT_WIDTH); + const s5 = `Health: ${healthUrl}`.padEnd(BOX_CONTENT_WIDTH); + const s6 = `Terminal: ${terminalStatus}`.padEnd(BOX_CONTENT_WIDTH); + logger.info(` -╔═══════════════════════════════════════════════════════╗ -║ Automaker Backend Server ║ -╠═══════════════════════════════════════════════════════╣ -║ Listening: ${host}:${port}${' '.repeat(Math.max(0, 34 - host.length - port.toString().length))}║ -║ HTTP API: http://${HOSTNAME}:${portStr} ║ -║ WebSocket: ws://${HOSTNAME}:${portStr}/api/events ║ -║ Terminal: ws://${HOSTNAME}:${portStr}/api/terminal/ws ║ -║ Health: http://${HOSTNAME}:${portStr}/api/health ║ -║ Terminal: ${terminalStatus.padEnd(37)}║ -╚═══════════════════════════════════════════════════════╝ +╔═════════════════════════════════════════════════════════════════════╗ +║ ${sHeader}║ +╠═════════════════════════════════════════════════════════════════════╣ +║ ║ +║ ${s1}║ +║ ${s2}║ +║ ${s3}║ +║ ${s4}║ +║ ${s5}║ +║ ${s6}║ +║ ║ +╚═════════════════════════════════════════════════════════════════════╝ `); }); server.on('error', (error: NodeJS.ErrnoException) => { if (error.code === 'EADDRINUSE') { + const portStr = port.toString(); + const nextPortStr = (port + 1).toString(); + const killCmd = `lsof -ti:${portStr} | xargs kill -9`; + const altCmd = `PORT=${nextPortStr} npm run dev:server`; + + const eHeader = `❌ ERROR: Port ${portStr} is already in use`.padEnd(BOX_CONTENT_WIDTH); + const e1 = 'Another process is using this port.'.padEnd(BOX_CONTENT_WIDTH); + const e2 = 'To fix this, try one of:'.padEnd(BOX_CONTENT_WIDTH); + const e3 = '1. Kill the process using the port:'.padEnd(BOX_CONTENT_WIDTH); + const e4 = ` ${killCmd}`.padEnd(BOX_CONTENT_WIDTH); + const e5 = '2. Use a different port:'.padEnd(BOX_CONTENT_WIDTH); + const e6 = ` ${altCmd}`.padEnd(BOX_CONTENT_WIDTH); + const e7 = '3. Use the init.sh script which handles this:'.padEnd(BOX_CONTENT_WIDTH); + const e8 = ' ./init.sh'.padEnd(BOX_CONTENT_WIDTH); + logger.error(` -╔═══════════════════════════════════════════════════════╗ -║ ❌ ERROR: Port ${port} is already in use ║ -╠═══════════════════════════════════════════════════════╣ -║ Another process is using this port. ║ -║ ║ -║ To fix this, try one of: ║ -║ ║ -║ 1. Kill the process using the port: ║ -║ lsof -ti:${port} | xargs kill -9 ║ -║ ║ -║ 2. Use a different port: ║ -║ PORT=${port + 1} npm run dev:server ║ -║ ║ -║ 3. Use the init.sh script which handles this: ║ -║ ./init.sh ║ -╚═══════════════════════════════════════════════════════╝ +╔═════════════════════════════════════════════════════════════════════╗ +║ ${eHeader}║ +╠═════════════════════════════════════════════════════════════════════╣ +║ ║ +║ ${e1}║ +║ ║ +║ ${e2}║ +║ ║ +║ ${e3}║ +║ ${e4}║ +║ ║ +║ ${e5}║ +║ ${e6}║ +║ ║ +║ ${e7}║ +║ ${e8}║ +║ ║ +╚═════════════════════════════════════════════════════════════════════╝ `); process.exit(1); } else { diff --git a/apps/server/src/lib/auth.ts b/apps/server/src/lib/auth.ts index ea9aa42a..1deef0db 100644 --- a/apps/server/src/lib/auth.ts +++ b/apps/server/src/lib/auth.ts @@ -130,21 +130,47 @@ function ensureApiKey(): string { // API key - always generated/loaded on startup for CSRF protection const API_KEY = ensureApiKey(); +// Width for log box content (excluding borders) +const BOX_CONTENT_WIDTH = 67; + // Print API key to console for web mode users (unless suppressed for production logging) if (process.env.AUTOMAKER_HIDE_API_KEY !== 'true') { + const autoLoginEnabled = process.env.AUTOMAKER_AUTO_LOGIN === 'true'; + const autoLoginStatus = autoLoginEnabled ? 'enabled (auto-login active)' : 'disabled'; + + // Build box lines with exact padding + const header = '🔐 API Key for Web Mode Authentication'.padEnd(BOX_CONTENT_WIDTH); + const line1 = "When accessing via browser, you'll be prompted to enter this key:".padEnd( + BOX_CONTENT_WIDTH + ); + const line2 = API_KEY.padEnd(BOX_CONTENT_WIDTH); + const line3 = 'In Electron mode, authentication is handled automatically.'.padEnd( + BOX_CONTENT_WIDTH + ); + const line4 = `Auto-login (AUTOMAKER_AUTO_LOGIN): ${autoLoginStatus}`.padEnd(BOX_CONTENT_WIDTH); + const tipHeader = '💡 Tips'.padEnd(BOX_CONTENT_WIDTH); + const line5 = 'Set AUTOMAKER_API_KEY env var to use a fixed key'.padEnd(BOX_CONTENT_WIDTH); + const line6 = 'Set AUTOMAKER_AUTO_LOGIN=true to skip the login prompt'.padEnd(BOX_CONTENT_WIDTH); + logger.info(` -╔═══════════════════════════════════════════════════════════════════════╗ -║ 🔐 API Key for Web Mode Authentication ║ -╠═══════════════════════════════════════════════════════════════════════╣ -║ ║ -║ When accessing via browser, you'll be prompted to enter this key: ║ -║ ║ -║ ${API_KEY} -║ ║ -║ In Electron mode, authentication is handled automatically. ║ -║ ║ -║ 💡 Tip: Set AUTOMAKER_API_KEY env var to use a fixed key for dev ║ -╚═══════════════════════════════════════════════════════════════════════╝ +╔═════════════════════════════════════════════════════════════════════╗ +║ ${header}║ +╠═════════════════════════════════════════════════════════════════════╣ +║ ║ +║ ${line1}║ +║ ║ +║ ${line2}║ +║ ║ +║ ${line3}║ +║ ║ +║ ${line4}║ +║ ║ +╠═════════════════════════════════════════════════════════════════════╣ +║ ${tipHeader}║ +╠═════════════════════════════════════════════════════════════════════╣ +║ ${line5}║ +║ ${line6}║ +╚═════════════════════════════════════════════════════════════════════╝ `); } else { logger.info('API key banner hidden (AUTOMAKER_HIDE_API_KEY=true)'); diff --git a/apps/server/src/routes/auth/index.ts b/apps/server/src/routes/auth/index.ts index e4ff2c45..558065c4 100644 --- a/apps/server/src/routes/auth/index.ts +++ b/apps/server/src/routes/auth/index.ts @@ -117,9 +117,27 @@ export function createAuthRoutes(): Router { * * Returns whether the current request is authenticated. * Used by the UI to determine if login is needed. + * + * If AUTOMAKER_AUTO_LOGIN=true is set, automatically creates a session + * for unauthenticated requests (useful for development). */ - router.get('/status', (req, res) => { - const authenticated = isRequestAuthenticated(req); + router.get('/status', async (req, res) => { + let authenticated = isRequestAuthenticated(req); + + // Auto-login for development: create session automatically if enabled + // Only works in non-production environments as a safeguard + if ( + !authenticated && + process.env.AUTOMAKER_AUTO_LOGIN === 'true' && + process.env.NODE_ENV !== 'production' + ) { + const sessionToken = await createSession(); + const cookieOptions = getSessionCookieOptions(); + const cookieName = getSessionCookieName(); + res.cookie(cookieName, sessionToken, cookieOptions); + authenticated = true; + } + res.json({ success: true, authenticated,