fix: add visible scripts typecheck progress

This commit is contained in:
Codex
2026-06-10 16:43:02 +00:00
parent 20dda46e03
commit e2c41a893e
7 changed files with 348 additions and 84 deletions
+148 -1
View File
@@ -1,4 +1,4 @@
import { spawn, spawnSync } from "node:child_process";
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
import { closeSync, createWriteStream, existsSync, openSync, readSync, statSync } from "node:fs";
export interface CommandResult {
@@ -9,6 +9,30 @@ export interface CommandResult {
stderr: string;
signal: NodeJS.Signals | null;
timedOut: boolean;
durationMs?: number;
stdoutBytes?: number;
stderrBytes?: number;
stdoutTruncated?: boolean;
stderrTruncated?: boolean;
}
export interface CommandProgress {
elapsedMs: number;
timeoutMs: number | null;
pid: number | null;
stdoutBytes: number;
stderrBytes: number;
lastOutputAgeMs: number | null;
}
interface ObservedCommandOptions {
timeoutMs?: number;
heartbeatMs?: number;
killAfterMs?: number;
env?: NodeJS.ProcessEnv;
input?: string;
maxCaptureChars?: number;
onProgress?: (progress: CommandProgress) => void;
}
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv; input?: string } = {}): CommandResult {
@@ -33,6 +57,129 @@ export function runCommand(command: string[], cwd: string, options: { timeoutMs?
};
}
export async function runCommandObserved(command: string[], cwd: string, options: ObservedCommandOptions = {}): Promise<CommandResult> {
const startedAt = Date.now();
const timeoutMs = options.timeoutMs;
const heartbeatMs = Math.max(0, options.heartbeatMs ?? 0);
const maxCaptureChars = Math.max(1, options.maxCaptureChars ?? 1024 * 1024);
let stdout = "";
let stderr = "";
let stdoutBytes = 0;
let stderrBytes = 0;
let stdoutTruncated = false;
let stderrTruncated = false;
let lastOutputAt: number | null = null;
let timedOut = false;
let timeoutTimer: NodeJS.Timeout | undefined;
let killTimer: NodeJS.Timeout | undefined;
let heartbeatTimer: NodeJS.Timeout | undefined;
const child = spawn(command[0], command.slice(1), {
cwd,
env: options.env,
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
});
child.stdout.on("data", (chunk: Buffer | string) => {
const text = chunk.toString();
stdoutBytes += Buffer.byteLength(text);
lastOutputAt = Date.now();
const captured = appendBounded(stdout, text, maxCaptureChars);
stdout = captured.text;
stdoutTruncated = stdoutTruncated || captured.truncated;
});
child.stderr.on("data", (chunk: Buffer | string) => {
const text = chunk.toString();
stderrBytes += Buffer.byteLength(text);
lastOutputAt = Date.now();
const captured = appendBounded(stderr, text, maxCaptureChars);
stderr = captured.text;
stderrTruncated = stderrTruncated || captured.truncated;
});
child.stdin.end(options.input ?? "");
if (heartbeatMs > 0) {
heartbeatTimer = setInterval(() => {
options.onProgress?.({
elapsedMs: Date.now() - startedAt,
timeoutMs: timeoutMs ?? null,
pid: child.pid ?? null,
stdoutBytes,
stderrBytes,
lastOutputAgeMs: lastOutputAt === null ? null : Date.now() - lastOutputAt,
});
}, heartbeatMs);
}
if (timeoutMs !== undefined) {
timeoutTimer = setTimeout(() => {
timedOut = true;
killProcessTree(child, "SIGTERM");
killTimer = setTimeout(() => {
killProcessTree(child, "SIGKILL");
}, Math.max(100, options.killAfterMs ?? 5_000));
}, timeoutMs);
}
const completion = await new Promise<{ exitCode: number | null; signal: NodeJS.Signals | null; error?: Error }>((resolve) => {
let resolved = false;
const finish = (result: { exitCode: number | null; signal: NodeJS.Signals | null; error?: Error }) => {
if (resolved) return;
resolved = true;
resolve(result);
};
child.on("error", (error) => finish({ exitCode: 127, signal: null, error }));
child.on("close", (code, signal) => finish({ exitCode: code, signal }));
});
if (timeoutTimer !== undefined) clearTimeout(timeoutTimer);
if (killTimer !== undefined) clearTimeout(killTimer);
if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer);
if (completion.error !== undefined && stderr.length === 0) {
stderr = completion.error.message;
stderrBytes = Buffer.byteLength(stderr);
}
return {
command,
cwd,
exitCode: completion.exitCode,
stdout,
stderr,
signal: completion.signal,
timedOut,
durationMs: Date.now() - startedAt,
stdoutBytes,
stderrBytes,
stdoutTruncated,
stderrTruncated,
};
}
function appendBounded(current: string, next: string, maxChars: number): { text: string; truncated: boolean } {
const combined = `${current}${next}`;
if (combined.length <= maxChars) return { text: combined, truncated: false };
return { text: combined.slice(-maxChars), truncated: true };
}
function killProcessTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean {
if (child.pid === undefined) return false;
try {
if (process.platform === "win32") return child.kill(signal);
process.kill(-child.pid, signal);
return true;
} catch {
try {
return child.kill(signal);
} catch {
return false;
}
}
}
export function commandOk(command: string[], cwd: string): boolean {
return runCommand(command, cwd).exitCode === 0;
}