Files
2026-07-13 13:20:22 +08:00

205 lines
5.5 KiB
JavaScript

#!/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 <start|webui|cli> [--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();