Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | 53x 53x 53x 53x 53x 53x 53x 53x 57x 128x 10x 10x 10x 10x 5x 5x 5x 10x 118x 118x 118x 118x 1x 117x 41x 41x 76x 118x 96x 96x | import { spawnSync, type SpawnSyncOptions } from 'node:child_process';
/**
* Error thrown when a command fails.
*/
export class ExecError extends Error {
constructor(
public readonly command: string,
public readonly exitCode: number | null,
public readonly stderr: string,
public readonly signal: string | null,
public readonly code?: string,
options?: { cause?: unknown }
) {
const details = stderr || (signal ? `Killed by signal ${signal}` : 'Unknown error');
super(`Command failed: ${command}\n${details}`, options);
this.name = 'ExecError';
}
}
/**
* Options for exec functions.
*/
export interface ExecOptions {
cwd?: string;
env?: Record<string, string>;
timeout?: number;
}
/**
* Git environment variables that disable interactive prompts.
* - GIT_TERMINAL_PROMPT=0: Disables git's internal prompts
* - GIT_SSH_COMMAND with BatchMode=yes: Makes SSH fail instead of prompting for passphrase
*/
export const GIT_NON_INTERACTIVE_ENV = {
GIT_TERMINAL_PROMPT: '0',
GIT_SSH_COMMAND: 'ssh -o BatchMode=yes',
};
/**
* Build spawn options for non-interactive execution.
* - stdin: 'ignore' maps to /dev/null, ensuring immediate EOF on read (no hangs)
* - stdout/stderr: 'pipe' to capture output
*/
function buildSpawnOptions(options?: ExecOptions): SpawnSyncOptions {
return {
encoding: 'utf-8',
cwd: options?.cwd,
env: options?.env ? { ...process.env, ...options.env } : process.env,
timeout: options?.timeout,
stdio: ['ignore', 'pipe', 'pipe'],
};
}
/**
* Execute a shell command in non-interactive mode.
* Uses piped stdio to avoid passing terminal to child process.
*
* @param command - The shell command to execute
* @param options - Execution options (cwd, env, timeout)
* @returns The trimmed stdout output
* @throws ExecError if the command fails
*/
export function execNonInteractive(command: string, options?: ExecOptions): string {
const spawnOptions = buildSpawnOptions(options);
// Use shell to execute the command string
const result = spawnSync(command, {
...spawnOptions,
shell: true,
});
Iif (result.error) {
throw new ExecError(command, null, result.error.message, null, (result.error as NodeJS.ErrnoException).code, { cause: result.error });
}
if (result.status !== 0) {
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
throw new ExecError(command, result.status, stderr, result.signal?.toString() ?? null);
}
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
return stdout.trim();
}
/**
* Execute a file with arguments in non-interactive mode.
* Uses execFile semantics (no shell), avoiding shell injection vulnerabilities.
* Uses piped stdio to avoid passing terminal to child process.
*
* @param file - The executable to run
* @param args - Arguments to pass to the executable
* @param options - Execution options (cwd, env, timeout)
* @returns The trimmed stdout output
* @throws ExecError if the command fails
*/
export function execFileNonInteractive(
file: string,
args: string[],
options?: ExecOptions
): string {
const spawnOptions = buildSpawnOptions(options);
const command = `${file} ${args.join(' ')}`;
const result = spawnSync(file, args, spawnOptions);
if (result.error) {
throw new ExecError(command, null, result.error.message, null, (result.error as NodeJS.ErrnoException).code, { cause: result.error });
}
if (result.status !== 0) {
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
throw new ExecError(command, result.status, stderr, result.signal?.toString() ?? null);
}
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
return stdout.trim();
}
/**
* Execute a git command in non-interactive mode.
* Combines execFileNonInteractive with GIT_NON_INTERACTIVE_ENV for
* defense-in-depth against SSH prompts.
*
* @param args - Arguments to pass to git
* @param options - Execution options (cwd, env, timeout)
* @returns The trimmed stdout output
* @throws ExecError if the command fails
*/
export function execGitNonInteractive(args: string[], options?: ExecOptions): string {
const env = {
...options?.env,
...GIT_NON_INTERACTIVE_ENV, // Always override to ensure non-interactive
};
return execFileNonInteractive('git', args, { ...options, env });
}
|