diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index aa27943e..b1ffd9bb 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -1,6 +1,85 @@ import type { AgentTool } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; import { spawn } from "child_process"; +import { existsSync } from "fs"; + +/** + * Find Git Bash installation on Windows + * Searches common installation paths + */ +function findGitBash(): string | null { + if (process.platform !== "win32") { + return null; + } + + const paths = ["C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files (x86)\\Git\\bin\\bash.exe"]; + + // Also check ProgramFiles environment variables + const programFiles = process.env.ProgramFiles; + if (programFiles) { + paths.push(`${programFiles}\\Git\\bin\\bash.exe`); + } + + const programFilesX86 = process.env["ProgramFiles(x86)"]; + if (programFilesX86) { + paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`); + } + + for (const path of paths) { + if (existsSync(path)) { + return path; + } + } + + return null; +} + +/** + * Get shell configuration based on platform + */ +function getShellConfig(): { shell: string; args: string[] } { + if (process.platform === "win32") { + const gitBash = findGitBash(); + if (!gitBash) { + const paths = ["C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files (x86)\\Git\\bin\\bash.exe"]; + const pathList = paths.map((p) => ` - ${p}`).join("\n"); + throw new Error( + `Git Bash not found in standard installation locations:\n${pathList}\n\nPlease install Git for Windows from https://git-scm.com/download/win using the default installation path.`, + ); + } + return { shell: gitBash, args: ["-c"] }; + } + return { shell: "sh", args: ["-c"] }; +} + +/** + * Kill a process and all its children + */ +function killProcessTree(pid: number): void { + if (process.platform === "win32") { + // Use taskkill on Windows to kill process tree + try { + spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { + stdio: "ignore", + detached: true, + }); + } catch (e) { + // Ignore errors if taskkill fails + } + } else { + // Use SIGKILL on Unix/Linux/Mac + try { + process.kill(-pid, "SIGKILL"); + } catch (e) { + // Fallback to killing just the child if process group kill fails + try { + process.kill(pid, "SIGKILL"); + } catch (e2) { + // Process already dead + } + } + } +} const bashSchema = Type.Object({ command: Type.String({ description: "Bash command to execute" }), @@ -19,7 +98,8 @@ export const bashTool: AgentTool = { signal?: AbortSignal, ) => { return new Promise((resolve, _reject) => { - const child = spawn("sh", ["-c", command], { + const { shell, args } = getShellConfig(); + const child = spawn(shell, [...args, command], { detached: true, stdio: ["ignore", "pipe", "pipe"], }); @@ -115,17 +195,7 @@ export const bashTool: AgentTool = { // Handle abort signal - kill entire process tree const onAbort = () => { if (child.pid) { - // Kill the entire process group (negative PID kills all processes in the group) - try { - process.kill(-child.pid, "SIGKILL"); - } catch (e) { - // Fallback to killing just the child if process group kill fails - try { - child.kill("SIGKILL"); - } catch (e2) { - // Process already dead - } - } + killProcessTree(child.pid); } };