diff --git a/src/core/cli-runner.ts b/src/core/cli-runner.ts index 67f9839..ca4e645 100644 --- a/src/core/cli-runner.ts +++ b/src/core/cli-runner.ts @@ -1,8 +1,21 @@ import { spawn } from "node:child_process"; +const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; + +function cmdQuote(value: string): string { + // ponytail: minimal cmd.exe quoting for Windows .cmd shims; callers still pass argv, not shell text. + return `"${value.replace(/(["^&|<>()%!])/g, "^$1")}"`; +} + export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promise { return new Promise((resolve) => { - const child = spawn(bin, args, { + const isWin = process.platform === "win32"; + const command = isWin ? "cmd.exe" : bin; + const commandArgs = isWin + ? ["/d", "/s", "/c", [cmdQuote(bin), ...args.map(cmdQuote)].join(" ")] + : args; + + const child = spawn(command, commandArgs, { shell: false, timeout: timeoutMs, windowsHide: true, @@ -10,13 +23,30 @@ export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promis let stdout = ""; let stderr = ""; - child.stdout.on("data", (d) => { stdout += d.toString(); }); - child.stderr.on("data", (d) => { stderr += d.toString(); }); + let killedForOutput = false; + const append = (target: "stdout" | "stderr", chunk: Buffer) => { + if (stdout.length + stderr.length + chunk.length > MAX_OUTPUT_BYTES) { + killedForOutput = true; + child.kill(); + return; + } + if (target === "stdout") stdout += chunk.toString(); + else stderr += chunk.toString(); + }; + + child.stdout.on("data", (d) => append("stdout", d)); + child.stderr.on("data", (d) => append("stderr", d)); child.on("error", (err) => resolve(`[CLI error: ${err.message.slice(0, 200)}]`)); child.on("close", (code) => { - const out = stdout.trim(); - if (out) resolve(out); - else resolve(`[CLI exited ${code ?? "unknown"}: ${stderr.slice(0, 200) || "no output"}]`); + if (killedForOutput) { + resolve(`[CLI error: output exceeded ${MAX_OUTPUT_BYTES} bytes]`); + return; + } + if (code === 0) { + resolve(stdout.trim()); + return; + } + resolve(`[CLI exited ${code ?? "unknown"}: ${(stderr || stdout).slice(0, 400) || "no output"}]`); }); }); }