61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
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<string> {
|
|
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);
|
|
});
|
|
}
|