import { spawn } from "node:child_process"; const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; export interface RunCliOptions { stdin?: string; windowsCmdShim?: boolean; } function isCmdShim(bin: string, force = false): boolean { return process.platform === "win32" && (force || /(?:\.cmd|\.bat)$/i.test(bin)); } export function runCli( bin: string, args: string[], timeoutMs = 120_000, options: RunCliOptions = {}, ): Promise { return new Promise((resolve) => { // ponytail: only Windows command shims need cmd.exe; real .exe paths keep argv semantics. const command = isCmdShim(bin, options.windowsCmdShim) ? process.env.ComSpec ?? "cmd.exe" : bin; const commandArgs = isCmdShim(bin, options.windowsCmdShim) ? ["/d", "/s", "/c", bin, ...args] : args; const child = spawn(command, commandArgs, { shell: false, timeout: timeoutMs, windowsHide: true, }); let stdout = ""; let stderr = ""; 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) => { 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"}]`); }); if (options.stdin !== undefined) child.stdin.end(options.stdin); }); }