#!/usr/bin/env node import { execSync, spawn } from 'node:child_process'; import path from 'node:path'; import process from 'node:process'; const DEFAULT_PORTS = [5173, 9230]; const KILLABLE_NAMES = new Set(['electron', 'aionui', 'aionui.exe']); const log = (...args) => console.log('[dev-bootstrap]', ...args); const warn = (...args) => console.warn('[dev-bootstrap]', ...args); function run(command) { return execSync(command, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim(); } function isWindows() { return process.platform === 'win32'; } function parseArgs(argv) { const [command = 'doctor', ...rest] = argv; const flags = new Set(rest.filter((x) => x.startsWith('--'))); const values = rest.filter((x) => !x.startsWith('--')); return { command, values, flags }; } function getPidsListeningOnPort(port) { try { if (isWindows()) { const output = run(`netstat -ano -p tcp | findstr :${port}`); const lines = output.split(/\r?\n/).filter(Boolean); const pids = new Set(); for (const line of lines) { if (!/\bLISTENING\b/i.test(line)) continue; const parts = line.trim().split(/\s+/); const pid = Number(parts[parts.length - 1]); if (Number.isFinite(pid) && pid > 0) pids.add(pid); } return [...pids]; } const output = run(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t || true`); return output .split(/\r?\n/) .map((x) => Number(x.trim())) .filter((x) => Number.isFinite(x) && x > 0); } catch { return []; } } function getProcessName(pid) { try { if (isWindows()) { const output = run( `powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"` ); return output.trim(); } const output = run(`ps -p ${pid} -o comm=`); return path.basename(output.trim()); } catch { return ''; } } function listLikelyConflictingProcesses() { try { if (isWindows()) { const output = run( "powershell -NoProfile -Command \"Get-Process | Where-Object { $_.ProcessName -in @('electron','AionUi','node','bun') } | Select-Object ProcessName,Id | ConvertTo-Json -Compress\"" ); const parsed = output ? JSON.parse(output) : []; return Array.isArray(parsed) ? parsed : [parsed]; } const output = run(`ps -A -o pid=,comm= | egrep "electron|AionUi|node|bun" || true`); return output .split(/\r?\n/) .filter(Boolean) .map((line) => { const [pidRaw, ...nameParts] = line.trim().split(/\s+/); return { Id: Number(pidRaw), ProcessName: nameParts.join(' ') }; }) .filter((x) => Number.isFinite(x.Id)); } catch { return []; } } function killPid(pid) { if (!pid || pid === process.pid) return false; try { process.kill(pid, 'SIGKILL'); return true; } catch { return false; } } function cleanupPorts(ports) { const killed = []; for (const port of ports) { const pids = getPidsListeningOnPort(port); for (const pid of pids) { const name = (getProcessName(pid) || '').toLowerCase(); if (!name) continue; if (!KILLABLE_NAMES.has(name) && name !== 'node' && name !== 'bun') continue; if (killPid(pid)) { killed.push({ pid, port, name }); } } } return killed; } function cleanupByName() { const processes = listLikelyConflictingProcesses(); const killed = []; for (const proc of processes) { const pid = Number(proc.Id ?? proc.id); const rawName = String(proc.ProcessName ?? proc.name ?? '').toLowerCase(); if (!pid || pid === process.pid) continue; if (!['electron', 'aionui'].some((k) => rawName.includes(k))) continue; if (killPid(pid)) { killed.push({ pid, name: rawName }); } } return killed; } function doctor() { log(`platform=${process.platform} node=${process.version}`); try { log(`bun=${run('bun --version')}`); } catch { warn('bun not found in PATH'); } const listeners = DEFAULT_PORTS.map((port) => ({ port, pids: getPidsListeningOnPort(port), })); for (const item of listeners) { if (item.pids.length === 0) { log(`port ${item.port}: free`); continue; } const names = item.pids.map((pid) => `${pid}:${getProcessName(pid) || 'unknown'}`).join(', '); warn(`port ${item.port}: occupied by ${names}`); } } function launch(scriptName, withExtensions) { if (!scriptName) { throw new Error( 'Missing script name. Usage: node scripts/dev-bootstrap.mjs launch [--extensions]' ); } const killedByName = cleanupByName(); const killedByPort = cleanupPorts(DEFAULT_PORTS); if (killedByName.length > 0 || killedByPort.length > 0) { log(`killed ${killedByName.length + killedByPort.length} stale process(es)`); } const env = { ...process.env }; if (withExtensions) { env.AIONUI_EXTENSIONS_PATH = path.resolve(process.cwd(), 'examples'); log(`AIONUI_EXTENSIONS_PATH=${env.AIONUI_EXTENSIONS_PATH}`); } const child = spawn('bun', ['run', scriptName], { cwd: process.cwd(), env, stdio: 'inherit', shell: isWindows(), }); child.on('exit', (code, signal) => { if (signal) { process.kill(process.pid, signal); return; } process.exit(code ?? 0); }); } function main() { const { command, values, flags } = parseArgs(process.argv.slice(2)); if (command === 'doctor') { doctor(); return; } if (command === 'launch') { launch(values[0], flags.has('--extensions')); return; } throw new Error(`Unknown command: ${command}`); } main();