import { spawn } from "node:child_process"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; const shouldReset = process.argv.includes("--reset"); const colors = { api: "\x1b[38;5;180m", worker: "\x1b[38;5;110m", admin: "\x1b[38;5;117m", submission: "\x1b[38;5;216m", system: "\x1b[38;5;246m", reset: "\x1b[0m" }; const services = [ { id: "api", title: "API", command: npmCommand, args: ["run", "dev:api"], url: "http://localhost:4300/health", waitForReady: true }, { id: "worker", title: "Worker", command: npmCommand, args: ["run", "dev:worker"], url: "http://localhost:4301/health", waitForReady: true }, { id: "admin", title: "Admin", command: npmCommand, args: ["run", "dev:admin"], url: "http://localhost:4200" }, { id: "submission", title: "Submission", command: npmCommand, args: ["run", "dev:submission"], url: "http://localhost:4100" } ]; const prefixLine = (serviceId, title, line) => { const color = colors[serviceId] ?? colors.system; return `${color}[${title}]${colors.reset} ${line}`; }; const attachStream = (child, streamName, serviceId, title) => { const stream = child[streamName]; if (!stream) { return; } let buffer = ""; stream.setEncoding("utf8"); stream.on("data", (chunk) => { buffer += chunk; const lines = buffer.split(/\r?\n/); buffer = lines.pop() ?? ""; for (const line of lines) { if (line.length > 0) { console.log(prefixLine(serviceId, title, line)); } } }); stream.on("end", () => { if (buffer.length > 0) { console.log(prefixLine(serviceId, title, buffer)); } }); }; const childProcesses = []; let shuttingDown = false; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const waitForService = async (service, timeoutMs = 20_000) => { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { try { const response = await fetch(service.url); if (response.ok) { return true; } } catch { // keep waiting } await delay(400); } return false; }; const shutdown = (reason = "shutdown") => { if (shuttingDown) { return; } shuttingDown = true; console.log(prefixLine("system", "SYSTEM", `Stopping local stack (${reason})...`)); for (const child of childProcesses) { if (!child.killed) { child.kill("SIGTERM"); } } setTimeout(() => { for (const child of childProcesses) { if (!child.killed) { child.kill("SIGKILL"); } } }, 1200).unref(); }; process.on("SIGINT", () => shutdown("SIGINT")); process.on("SIGTERM", () => shutdown("SIGTERM")); if (shouldReset) { console.log(prefixLine("system", "SYSTEM", "Resetting runtime state before startup...")); const reset = spawn(npmCommand, ["run", "reset:runtime"], { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"] }); attachStream(reset, "stdout", "system", "RESET"); attachStream(reset, "stderr", "system", "RESET"); const resetExitCode = await new Promise((resolve) => { reset.on("exit", (code) => resolve(code ?? 1)); }); if (resetExitCode !== 0) { process.exit(resetExitCode); } } console.log(prefixLine("system", "SYSTEM", "Starting local Good Grief stack...")); for (const service of services) { console.log(prefixLine("system", "SYSTEM", `${service.title}: ${service.url}`)); } for (const service of services) { const child = spawn(service.command, service.args, { cwd: repoRoot, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] }); childProcesses.push(child); attachStream(child, "stdout", service.id, service.title); attachStream(child, "stderr", service.id, service.title); child.on("exit", (code, signal) => { if (!shuttingDown) { const reason = signal ? `${service.title} exited from ${signal}` : `${service.title} exited with code ${code ?? 1}`; console.error(prefixLine(service.id, service.title, reason)); shutdown(reason); process.exitCode = code ?? 1; } }); if (service.waitForReady) { const ready = await waitForService(service); if (ready) { console.log(prefixLine("system", "SYSTEM", `${service.title} ready.`)); } else { console.log(prefixLine("system", "SYSTEM", `${service.title} did not report ready before timeout.`)); } } } await new Promise(() => {});