src / exec.ts
import { spawn } from "child_process";
export interface RunOptions {
cwd: string;
timeoutMs: number;
maxChars: number;
signal?: AbortSignal;
}
export interface RunResult {
/** stdout and stderr interleaved in arrival order, capped at maxChars. */
output: string;
exitCode: number | null;
timedOut: boolean;
aborted: boolean;
truncated: boolean;
}
/** The child must not be able to read the jev key back out of its environment. */
export function scrubbedEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const copy = { ...env };
delete copy.JEV_API_KEY;
delete copy.TYPESAFE_API_KEY;
return copy;
}
/** Runs `command` through /bin/sh -c in its own process group so a timeout or abort kills the whole tree. */
export function runCommand(command: string, options: RunOptions): Promise<RunResult> {
return new Promise((resolve, reject) => {
const child = spawn("/bin/sh", ["-c", command], {
cwd: options.cwd,
env: scrubbedEnv(),
stdio: ["ignore", "pipe", "pipe"],
detached: true,
});
let output = "";
let truncated = false;
let timedOut = false;
let aborted = false;
const append = (chunk: string) => {
const room = options.maxChars - output.length;
if (chunk.length > room) {
output += chunk.slice(0, Math.max(room, 0));
truncated = true;
} else {
output += chunk;
}
};
child.stdout.setEncoding("utf-8");
child.stderr.setEncoding("utf-8");
child.stdout.on("data", append);
child.stderr.on("data", append);
const kill = () => {
try {
process.kill(-child.pid!, "SIGKILL");
} catch {
child.kill("SIGKILL");
}
};
const timer = setTimeout(() => {
timedOut = true;
kill();
}, options.timeoutMs);
const onAbort = () => {
aborted = true;
kill();
};
if (options.signal?.aborted) onAbort();
else options.signal?.addEventListener("abort", onAbort, { once: true });
const cleanup = () => {
clearTimeout(timer);
options.signal?.removeEventListener("abort", onAbort);
};
child.on("error", error => {
cleanup();
reject(error);
});
child.on("close", code => {
cleanup();
resolve({ output, exitCode: code, timedOut, aborted, truncated });
});
});
}
src / exec.ts
import { spawn } from "child_process";
export interface RunOptions {
cwd: string;
timeoutMs: number;
maxChars: number;
signal?: AbortSignal;
}
export interface RunResult {
/** stdout and stderr interleaved in arrival order, capped at maxChars. */
output: string;
exitCode: number | null;
timedOut: boolean;
aborted: boolean;
truncated: boolean;
}
/** The child must not be able to read the jev key back out of its environment. */
export function scrubbedEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const copy = { ...env };
delete copy.JEV_API_KEY;
delete copy.TYPESAFE_API_KEY;
return copy;
}
/** Runs `command` through /bin/sh -c in its own process group so a timeout or abort kills the whole tree. */
export function runCommand(command: string, options: RunOptions): Promise<RunResult> {
return new Promise((resolve, reject) => {
const child = spawn("/bin/sh", ["-c", command], {
cwd: options.cwd,
env: scrubbedEnv(),
stdio: ["ignore", "pipe", "pipe"],
detached: true,
});
let output = "";
let truncated = false;
let timedOut = false;
let aborted = false;
const append = (chunk: string) => {
const room = options.maxChars - output.length;
if (chunk.length > room) {
output += chunk.slice(0, Math.max(room, 0));
truncated = true;
} else {
output += chunk;
}
};
child.stdout.setEncoding("utf-8");
child.stderr.setEncoding("utf-8");
child.stdout.on("data", append);
child.stderr.on("data", append);
const kill = () => {
try {
process.kill(-child.pid!, "SIGKILL");
} catch {
child.kill("SIGKILL");
}
};
const timer = setTimeout(() => {
timedOut = true;
kill();
}, options.timeoutMs);
const onAbort = () => {
aborted = true;
kill();
};
if (options.signal?.aborted) onAbort();
else options.signal?.addEventListener("abort", onAbort, { once: true });
const cleanup = () => {
clearTimeout(timer);
options.signal?.removeEventListener("abort", onAbort);
};
child.on("error", error => {
cleanup();
reject(error);
});
child.on("close", code => {
cleanup();
resolve({ output, exitCode: code, timedOut, aborted, truncated });
});
});
}