goodgrief/scripts/run-local.mjs
2026-04-08 10:06:54 -07:00

188 lines
4.6 KiB
JavaScript

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(() => {});