Files
webstudio/packages/cli/scripts/run-agent-command.ts

83 lines
2.3 KiB
TypeScript

import { spawn } from "node:child_process";
export const runAgentCommand = ({
command,
cwd,
env,
timeoutMs,
onStdoutLine,
signal,
}: {
command: string;
cwd: string;
env: NodeJS.ProcessEnv;
timeoutMs: number;
onStdoutLine?: (line: string) => void;
signal?: AbortSignal;
}) =>
new Promise<{ exitCode: number; durationMs: number }>((resolve, reject) => {
const startedAt = Date.now();
const child = spawn(process.env.SHELL ?? "/bin/sh", ["-c", command], {
cwd,
env,
stdio: [
"ignore",
onStdoutLine === undefined ? "ignore" : "pipe",
"ignore",
],
detached: process.platform !== "win32",
});
let stdoutBuffer = "";
if (onStdoutLine !== undefined && child.stdout !== null) {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
const lines = stdoutBuffer.split("\n");
stdoutBuffer = lines.pop() ?? "";
for (const line of lines) {
onStdoutLine(line);
}
});
}
const terminate = () => {
if (child.pid !== undefined) {
try {
process.kill(process.platform === "win32" ? child.pid : -child.pid);
} catch {}
}
};
const onAbort = () => terminate();
signal?.addEventListener("abort", onAbort, { once: true });
const cleanup = () => {
clearTimeout(timeout);
signal?.removeEventListener("abort", onAbort);
};
const timeout = setTimeout(() => {
terminate();
cleanup();
reject(new Error(`Agent command timed out after ${timeoutMs}ms.`));
}, timeoutMs);
child.once("error", (error) => {
cleanup();
reject(error);
});
child.once("close", (code, childSignal) => {
cleanup();
if (onStdoutLine !== undefined && stdoutBuffer.length > 0) {
onStdoutLine(stdoutBuffer);
}
if (childSignal !== null) {
reject(new Error(`Agent command exited from signal ${childSignal}.`));
return;
}
if (signal?.aborted) {
reject(new Error("Agent command was aborted."));
return;
}
resolve({ exitCode: code ?? -1, durationMs: Date.now() - startedAt });
});
if (signal?.aborted) {
terminate();
}
});