Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 80 additions & 38 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,28 @@ export function isRequestLoggingEnabled(): boolean {
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;

if (!hasAnthropicKey) {
const wHeader = '⚠️ WARNING: No Claude authentication configured'.padEnd(67);
const w1 = 'The Claude Agent SDK requires authentication to function.'.padEnd(67);
const w2 = 'Set your Anthropic API key:'.padEnd(67);
const w3 = ' export ANTHROPIC_API_KEY="sk-ant-..."'.padEnd(67);
const w4 = 'Or use the setup wizard in Settings to configure authentication.'.padEnd(67);

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
Expand Down Expand Up @@ -618,40 +626,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(67);
const s1 = `Listening: ${listenAddr}`.padEnd(67);
const s2 = `HTTP API: ${httpUrl}`.padEnd(67);
const s3 = `WebSocket: ${wsEventsUrl}`.padEnd(67);
const s4 = `Terminal WS: ${wsTerminalUrl}`.padEnd(67);
const s5 = `Health: ${healthUrl}`.padEnd(67);
const s6 = `Terminal: ${terminalStatus}`.padEnd(67);

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(67);
const e1 = 'Another process is using this port.'.padEnd(67);
const e2 = 'To fix this, try one of:'.padEnd(67);
const e3 = '1. Kill the process using the port:'.padEnd(67);
const e4 = ` ${killCmd}`.padEnd(67);
const e5 = '2. Use a different port:'.padEnd(67);
const e6 = ` ${altCmd}`.padEnd(67);
const e7 = '3. Use the init.sh script which handles this:'.padEnd(67);
const e8 = ' ./init.sh'.padEnd(67);

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 {
Expand Down
43 changes: 31 additions & 12 deletions apps/server/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,38 @@ const API_KEY = ensureApiKey();

// 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 (67 chars content width, 69 for emoji lines)
const header = '🔐 API Key for Web Mode Authentication'.padEnd(67);
const line1 = "When accessing via browser, you'll be prompted to enter this key:".padEnd(67);
const line2 = API_KEY.padEnd(67);
const line3 = 'In Electron mode, authentication is handled automatically.'.padEnd(67);
const line4 = `Auto-login (AUTOMAKER_AUTO_LOGIN): ${autoLoginStatus}`.padEnd(67);
const tipHeader = '💡 Tips'.padEnd(67);
const line5 = 'Set AUTOMAKER_API_KEY env var to use a fixed key'.padEnd(67);
const line6 = 'Set AUTOMAKER_AUTO_LOGIN=true to skip the login prompt'.padEnd(67);

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)');
Expand Down
17 changes: 15 additions & 2 deletions apps/server/src/routes/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,22 @@ 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
if (!authenticated && process.env.AUTOMAKER_AUTO_LOGIN === 'true') {
const sessionToken = await createSession();
const cookieOptions = getSessionCookieOptions();
const cookieName = getSessionCookieName();
res.cookie(cookieName, sessionToken, cookieOptions);
authenticated = true;
}

res.json({
success: true,
authenticated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ export function ProjectSwitcherItem({
const IconComponent = getIconComponent();
const hasCustomIcon = !!project.customIconPath;

// Create a sanitized project name for test ID (lowercase, hyphens instead of spaces)
const sanitizedName = project.name.toLowerCase().replace(/\s+/g, '-');

return (
<button
onClick={onClick}
onContextMenu={onContextMenu}
data-testid={`project-switcher-project-${sanitizedName}`}
className={cn(
'group w-full aspect-square rounded-xl flex items-center justify-center relative overflow-hidden',
'transition-all duration-200 ease-out',
Expand All @@ -60,7 +64,6 @@ export function ProjectSwitcherItem({
'hover:scale-105 active:scale-95'
)}
title={project.name}
data-testid={`project-switcher-${project.id}`}
>
{hasCustomIcon ? (
<img
Expand Down