/ src / utils / shell-execution.ts
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  }