shell-execution.ts
1 import { spawn } from "child_process"; 2 import { getShellConfig } from "./command-validation.js"; 3 4 export interface ExecutionResult { 5 stdout: string; 6 stderr: string; 7 exitCode: number | null; 8 signal: string | null; 9 timedOut: boolean; 10 error?: Error; 11 } 12 13 export interface ExecutionOptions { 14 workdir?: string; 15 timeout?: number; 16 env?: Record<string, string>; 17 } 18 19 /** 20 * Execute a shell command with proper security and resource management 21 */ 22 export async function executeShellCommand( 23 command: string, 24 options: ExecutionOptions = {} 25 ): Promise<ExecutionResult> { 26 const { workdir = process.cwd(), timeout = 30000, env } = options; 27 const shellConfig = getShellConfig(); 28 29 return new Promise((resolve) => { 30 let stdout = ""; 31 let stderr = ""; 32 let timedOut = false; 33 let resolved = false; 34 35 // Spawn the shell process 36 const child = spawn(shellConfig.shell, [...shellConfig.args, command], { 37 cwd: workdir, 38 env: { ...process.env, ...env }, 39 shell: false, // Already using explicit shell 40 windowsHide: true, 41 }); 42 43 // Set up timeout 44 const timeoutHandle = setTimeout(() => { 45 if (!resolved) { 46 timedOut = true; 47 child.kill("SIGTERM"); 48 49 // Force kill after 5 seconds if SIGTERM doesn't work 50 setTimeout(() => { 51 if (!resolved) { 52 child.kill("SIGKILL"); 53 } 54 }, 5000); 55 } 56 }, timeout); 57 58 // Capture stdout 59 if (child.stdout) { 60 child.stdout.on("data", (data: Buffer) => { 61 stdout += data.toString(); 62 }); 63 } 64 65 // Capture stderr 66 if (child.stderr) { 67 child.stderr.on("data", (data: Buffer) => { 68 stderr += data.toString(); 69 }); 70 } 71 72 // Handle process completion 73 child.on("close", (exitCode: number | null, signal: string | null) => { 74 if (resolved) return; 75 resolved = true; 76 clearTimeout(timeoutHandle); 77 78 resolve({ 79 stdout: stdout.trim(), 80 stderr: stderr.trim(), 81 exitCode, 82 signal, 83 timedOut, 84 }); 85 }); 86 87 // Handle errors 88 child.on("error", (error: Error) => { 89 if (resolved) return; 90 resolved = true; 91 clearTimeout(timeoutHandle); 92 93 resolve({ 94 stdout: stdout.trim(), 95 stderr: stderr.trim(), 96 exitCode: null, 97 signal: null, 98 timedOut: false, 99 error, 100 }); 101 }); 102 }); 103 }