Clone stage per tech spec §6, verbatim mechanics: every git invocation carries a BLANK credential.helper first (suppresses OS keychain helpers) then the inline veect helper; the PAT reaches git via child env only — never argv, URLs, logs, or .git/config. GIT_TERMINAL_PROMPT and GIT_ASKPASS are pinned so a headless clone can never hang on a TTY or ambient IDE askpass. Staged status events (start/heartbeat/final) + ready on success; named failures: invalid-git-url, auth-failed, clone-failed (daemon's 'repository not exported' maps to bad-URL, not auth). Integration suite clones the daemon-served vite-kitchen and an authenticated dumb-HTTP remote that asserts the exact Basic credential git presented, plus leak assertions over logs/argv/.git. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
|
|
/**
|
|
* Minimal child-process runner shared by the stages. Array-args only —
|
|
* never a shell — so URLs and paths cannot be interpreted, and secrets
|
|
* can only travel via `env` (spec §6/§10).
|
|
*/
|
|
export interface CommandResult {
|
|
exitCode: number;
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
export type RunCommand = (
|
|
command: string,
|
|
args: string[],
|
|
options: { cwd: string; env?: Record<string, string> },
|
|
) => Promise<CommandResult>;
|
|
|
|
const MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
|
|
/** Ten minutes: covers a cold `npm install` of a real repo; prevents hangs. */
|
|
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
|
export const runCommand: RunCommand = (command, args, options) =>
|
|
new Promise((resolve, reject) => {
|
|
execFile(
|
|
command,
|
|
args,
|
|
{
|
|
cwd: options.cwd,
|
|
env: { ...process.env, ...options.env },
|
|
maxBuffer: MAX_OUTPUT_BYTES,
|
|
timeout: COMMAND_TIMEOUT_MS,
|
|
},
|
|
(error, stdout, stderr) => {
|
|
if (error === null) {
|
|
resolve({ exitCode: 0, stdout, stderr });
|
|
return;
|
|
}
|
|
// Non-zero exit is a result, not an exception — stages map it to
|
|
// named protocol errors. Spawn-level failures still reject.
|
|
const exitCode = typeof error.code === "number" ? error.code : null;
|
|
if (exitCode !== null) {
|
|
resolve({ exitCode, stdout, stderr });
|
|
return;
|
|
}
|
|
if (error.killed === true) {
|
|
resolve({ exitCode: 124, stdout, stderr: `${stderr}\n(timed out)` });
|
|
return;
|
|
}
|
|
reject(error);
|
|
},
|
|
);
|
|
});
|