// Run a TypeScript entry point as a CHILD process under tsx, stdio inherited, // and resolve with its exit code. For the CLI rows whose target parses its own // argv at import time (the `_parseFlags` bins) or is another package's entry // (the MCP server): importing either into the CLI's process would hand it the // CLI's argv, or make common depend on a package that depends on common. // // Signals: a terminal's Ctrl-C reaches the child directly (same process // group), so SIGINT is only held here — forwarding it too would deliver it // twice, which a bin that treats a second Ctrl-C as "force" would honour. // SIGTERM and SIGHUP come from a supervisor (an MCP client stopping its // server, say) and are forwarded. import { spawn } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; const COMMON = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); export const REPO_ROOT = path.resolve(COMMON, ".."); /** The tsx that a package's own scripts run (its devDependency). */ export function tsxFor(packageDir: string): string { return path.join(packageDir, "node_modules", ".bin", "tsx"); } export function runChild(opts: { command: string; args: string[]; cwd?: string; env?: NodeJS.ProcessEnv; }): Promise { return new Promise((resolve) => { const child = spawn(opts.command, opts.args, { cwd: opts.cwd, env: opts.env ?? process.env, stdio: "inherit", }); const hold = () => {}; const forward = (sig: NodeJS.Signals) => () => { if (!child.killed) child.kill(sig); }; const onTerm = forward("SIGTERM"); const onHup = forward("SIGHUP"); process.on("SIGINT", hold); process.on("SIGTERM", onTerm); process.on("SIGHUP", onHup); const done = (code: number) => { process.off("SIGINT", hold); process.off("SIGTERM", onTerm); process.off("SIGHUP", onHup); resolve(code); }; child.on("error", (err) => { console.error(`${path.basename(opts.command)}: ${err.message}`); done(1); }); child.on("exit", (code, signal) => { done(code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1)); }); }); } function signalNumber(sig: NodeJS.Signals): number | undefined { return ({ SIGHUP: 1, SIGINT: 2, SIGKILL: 9, SIGTERM: 15 } as Record)[sig]; } /** * A common/bin script that reads process.argv itself, run with `argv` exactly * as given. `heapMb` is the NODE_OPTIONS heap the old package.json script gave * it (the corpus-wide passes need more than node's default). */ export function runBinScript(file: string, argv: string[], heapMb?: number): Promise { const env = { ...process.env }; if (heapMb) { env.NODE_OPTIONS = `${env.NODE_OPTIONS ?? ""} --max-old-space-size=${heapMb}`.trim(); } return runChild({ command: tsxFor(COMMON), args: [path.join(COMMON, "bin", file), ...argv], env, }); }