#!/usr/bin/env node /** * Automaker - Cross-Platform Development Environment Setup and Launch Script * * This script works on Windows, macOS, and Linux. */ import { execSync } from 'child_process'; import fs from 'fs'; import http from 'http'; import path from 'path'; import readline from 'readline'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); const treeKill = require('tree-kill'); const crossSpawn = require('cross-spawn'); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Colors for terminal output (works on modern terminals including Windows) const colors = { green: '\x1b[0;32m', blue: '\x1b[0;34m', yellow: '\x1b[1;33m', red: '\x1b[0;31m', reset: '\x1b[0m', }; const isWindows = process.platform === 'win32'; // Track background processes for cleanup let serverProcess = null; let webProcess = null; let electronProcess = null; /** * Print colored output */ function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } /** * Print the header banner */ function printHeader() { console.log('╔═══════════════════════════════════════════════════════╗'); console.log('║ Automaker Development Environment ║'); console.log('╚═══════════════════════════════════════════════════════╝'); console.log(''); } /** * Execute a command synchronously and return stdout */ function execCommand(command, options = {}) { try { return execSync(command, { encoding: 'utf8', stdio: 'pipe', ...options }).trim(); } catch { return null; } } /** * Get process IDs using a specific port (cross-platform) */ function getProcessesOnPort(port) { const pids = new Set(); if (isWindows) { // Windows: Use netstat to find PIDs try { const output = execCommand(`netstat -ano | findstr :${port}`); if (output) { const lines = output.split('\n'); for (const line of lines) { // Match lines with LISTENING or ESTABLISHED on our port const match = line.match(/:\d+\s+.*?(\d+)\s*$/); if (match) { const pid = parseInt(match[1], 10); if (pid > 0) pids.add(pid); } } } } catch { // Ignore errors } } else { // Unix: Use lsof try { const output = execCommand(`lsof -ti:${port}`); if (output) { output.split('\n').forEach(pid => { const parsed = parseInt(pid.trim(), 10); if (parsed > 0) pids.add(parsed); }); } } catch { // Ignore errors } } return Array.from(pids); } /** * Kill a process by PID (cross-platform) */ function killProcess(pid) { try { if (isWindows) { execCommand(`taskkill /F /PID ${pid}`); } else { process.kill(pid, 'SIGKILL'); } return true; } catch { return false; } } /** * Kill processes on a port and wait for it to be freed */ async function killPort(port) { const pids = getProcessesOnPort(port); if (pids.length === 0) { log(`✓ Port ${port} is available`, 'green'); return true; } log(`Killing process(es) on port ${port}: ${pids.join(', ')}`, 'yellow'); for (const pid of pids) { killProcess(pid); } // Wait for port to be freed (max 5 seconds) for (let i = 0; i < 10; i++) { await sleep(500); const remainingPids = getProcessesOnPort(port); if (remainingPids.length === 0) { log(`✓ Port ${port} is now free`, 'green'); return true; } } log(`Warning: Port ${port} may still be in use`, 'red'); return false; } /** * Sleep for a given number of milliseconds */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Check if the server health endpoint is responding */ function checkHealth() { return new Promise((resolve) => { const req = http.get('http://localhost:3008/api/health', (res) => { resolve(res.statusCode === 200); }); req.on('error', () => resolve(false)); req.setTimeout(2000, () => { req.destroy(); resolve(false); }); }); } /** * Prompt the user for input */ function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }); }); } /** * Run npm command using cross-spawn for Windows compatibility */ function runNpm(args, options = {}) { const spawnOptions = { stdio: 'inherit', cwd: __dirname, ...options, }; // cross-spawn handles Windows .cmd files automatically return crossSpawn('npm', args, spawnOptions); } /** * Run npx command using cross-spawn for Windows compatibility */ function runNpx(args, options = {}) { const spawnOptions = { stdio: 'inherit', cwd: __dirname, ...options, }; // cross-spawn handles Windows .cmd files automatically return crossSpawn('npx', args, spawnOptions); } /** * Kill a process tree using tree-kill */ function killProcessTree(pid) { return new Promise((resolve) => { if (!pid) { resolve(); return; } treeKill(pid, 'SIGTERM', (err) => { if (err) { // Try force kill if graceful termination fails treeKill(pid, 'SIGKILL', () => resolve()); } else { resolve(); } }); }); } /** * Cleanup function to kill all spawned processes */ async function cleanup() { console.log('\nCleaning up...'); const killPromises = []; if (serverProcess && !serverProcess.killed && serverProcess.pid) { killPromises.push(killProcessTree(serverProcess.pid)); } if (webProcess && !webProcess.killed && webProcess.pid) { killPromises.push(killProcessTree(webProcess.pid)); } if (electronProcess && !electronProcess.killed && electronProcess.pid) { killPromises.push(killProcessTree(electronProcess.pid)); } await Promise.all(killPromises); } /** * Main function */ async function main() { // Change to script directory process.chdir(__dirname); printHeader(); // Check if node_modules exists if (!fs.existsSync(path.join(__dirname, 'node_modules'))) { log('Installing dependencies...', 'blue'); const install = runNpm(['install'], { stdio: 'inherit' }); await new Promise((resolve, reject) => { install.on('close', (code) => { if (code === 0) resolve(); else reject(new Error(`npm install failed with code ${code}`)); }); }); } // Install Playwright browsers from apps/ui where @playwright/test is installed log('Checking Playwright browsers...', 'yellow'); try { await new Promise((resolve) => { const playwright = crossSpawn( 'npx', ['playwright', 'install', 'chromium'], { stdio: 'ignore', cwd: path.join(__dirname, 'apps', 'ui') } ); playwright.on('close', () => resolve()); playwright.on('error', () => resolve()); }); } catch { // Ignore errors - Playwright install is optional } // Kill any existing processes on required ports log('Checking for processes on ports 3007 and 3008...', 'yellow'); await killPort(3007); await killPort(3008); console.log(''); // Show menu console.log('═══════════════════════════════════════════════════════'); console.log(' Select Application Mode:'); console.log('═══════════════════════════════════════════════════════'); console.log(' 1) Web Application (Browser)'); console.log(' 2) Desktop Application (Electron)'); console.log('═══════════════════════════════════════════════════════'); console.log(''); // Setup cleanup handlers let cleaningUp = false; const handleExit = async (signal) => { if (cleaningUp) return; cleaningUp = true; await cleanup(); process.exit(0); }; process.on('SIGINT', () => handleExit('SIGINT')); process.on('SIGTERM', () => handleExit('SIGTERM')); // Prompt for choice while (true) { const choice = await prompt('Enter your choice (1 or 2): '); if (choice === '1') { console.log(''); log('Launching Web Application...', 'blue'); // Start the backend server log('Starting backend server on port 3008...', 'blue'); // Create logs directory if (!fs.existsSync(path.join(__dirname, 'logs'))) { fs.mkdirSync(path.join(__dirname, 'logs'), { recursive: true }); } // Start server in background const logStream = fs.createWriteStream(path.join(__dirname, 'logs', 'server.log')); serverProcess = runNpm(['run', 'dev:server'], { stdio: ['ignore', 'pipe', 'pipe'], }); serverProcess.stdout?.pipe(logStream); serverProcess.stderr?.pipe(logStream); log('Waiting for server to be ready...', 'yellow'); // Wait for server health check const maxRetries = 30; let serverReady = false; for (let i = 0; i < maxRetries; i++) { if (await checkHealth()) { serverReady = true; break; } process.stdout.write('.'); await sleep(1000); } console.log(''); if (!serverReady) { log('Error: Server failed to start', 'red'); console.log('Check logs/server.log for details'); cleanup(); process.exit(1); } log('✓ Server is ready!', 'green'); log(`The application will be available at: http://localhost:3007`, 'green'); console.log(''); // Start web app webProcess = runNpm(['run', 'dev:web'], { stdio: 'inherit' }); await new Promise((resolve) => { webProcess.on('close', resolve); }); break; } else if (choice === '2') { console.log(''); log('Launching Desktop Application...', 'blue'); log('(Electron will start its own backend server)', 'yellow'); console.log(''); electronProcess = runNpm(['run', 'dev:electron'], { stdio: 'inherit' }); await new Promise((resolve) => { electronProcess.on('close', resolve); }); break; } else { log('Invalid choice. Please enter 1 or 2.', 'red'); } } } // Run main function main().catch((err) => { console.error(err); cleanup(); process.exit(1); });