mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 18:01:22 +00:00
The bash tool is named "bash" and described as executing bash commands, but was using sh on Unix. On many distros (Ubuntu, Debian, Alpine, etc.), /bin/sh is a POSIX-only shell that doesn't support bash syntax like [[ ]], arrays, or here-strings. This caused the LLM to write bash syntax that failed, wasting tokens on rewrites. Now prefers /bin/bash on Unix, falling back to sh only if bash isn't found.
139 lines
4 KiB
TypeScript
139 lines
4 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { spawn, spawnSync } from "child_process";
|
|
import { SettingsManager } from "../core/settings-manager.js";
|
|
|
|
let cachedShellConfig: { shell: string; args: string[] } | null = null;
|
|
|
|
/**
|
|
* Find bash executable on PATH (Windows)
|
|
*/
|
|
function findBashOnPath(): string | null {
|
|
try {
|
|
const result = spawnSync("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 });
|
|
if (result.status === 0 && result.stdout) {
|
|
const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
|
|
if (firstMatch && existsSync(firstMatch)) {
|
|
return firstMatch;
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get shell configuration based on platform.
|
|
* Resolution order:
|
|
* 1. User-specified shellPath in settings.json
|
|
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
* 3. On Unix: /bin/bash
|
|
* 4. Fallback: sh
|
|
*/
|
|
export function getShellConfig(): { shell: string; args: string[] } {
|
|
if (cachedShellConfig) {
|
|
return cachedShellConfig;
|
|
}
|
|
|
|
const settings = SettingsManager.create();
|
|
const customShellPath = settings.getShellPath();
|
|
|
|
// 1. Check user-specified shell path
|
|
if (customShellPath) {
|
|
if (existsSync(customShellPath)) {
|
|
cachedShellConfig = { shell: customShellPath, args: ["-c"] };
|
|
return cachedShellConfig;
|
|
}
|
|
throw new Error(
|
|
`Custom shell path not found: ${customShellPath}\nPlease update shellPath in ~/.pi/agent/settings.json`,
|
|
);
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
// 2. Try Git Bash in known locations
|
|
const paths: string[] = [];
|
|
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)) {
|
|
cachedShellConfig = { shell: path, args: ["-c"] };
|
|
return cachedShellConfig;
|
|
}
|
|
}
|
|
|
|
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
|
const bashOnPath = findBashOnPath();
|
|
if (bashOnPath) {
|
|
cachedShellConfig = { shell: bashOnPath, args: ["-c"] };
|
|
return cachedShellConfig;
|
|
}
|
|
|
|
throw new Error(
|
|
`No bash shell found. Options:\n` +
|
|
` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
|
|
` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
|
|
` 3. Set shellPath in ~/.pi/agent/settings.json\n\n` +
|
|
`Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`,
|
|
);
|
|
}
|
|
|
|
// Unix: prefer bash over sh
|
|
if (existsSync("/bin/bash")) {
|
|
cachedShellConfig = { shell: "/bin/bash", args: ["-c"] };
|
|
return cachedShellConfig;
|
|
}
|
|
|
|
cachedShellConfig = { shell: "sh", args: ["-c"] };
|
|
return cachedShellConfig;
|
|
}
|
|
|
|
/**
|
|
* Sanitize binary output for display/storage.
|
|
* Removes characters that crash string-width or cause display issues:
|
|
* - Control characters (except tab, newline, carriage return)
|
|
* - Lone surrogates
|
|
* - Unicode Format characters (crash string-width due to a bug)
|
|
*/
|
|
export function sanitizeBinaryOutput(str: string): string {
|
|
// Fast path: use regex to remove problematic characters
|
|
// - \p{Format}: Unicode format chars like \u0601 that crash string-width
|
|
// - \p{Surrogate}: Lone surrogates from invalid UTF-8
|
|
// - Control chars except \t \n \r
|
|
return str.replace(/[\p{Format}\p{Surrogate}]/gu, "").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
}
|
|
|
|
/**
|
|
* Kill a process and all its children (cross-platform)
|
|
*/
|
|
export 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 {
|
|
// Ignore errors if taskkill fails
|
|
}
|
|
} else {
|
|
// Use SIGKILL on Unix/Linux/Mac
|
|
try {
|
|
process.kill(-pid, "SIGKILL");
|
|
} catch {
|
|
// Fallback to killing just the child if process group kill fails
|
|
try {
|
|
process.kill(pid, "SIGKILL");
|
|
} catch {
|
|
// Process already dead
|
|
}
|
|
}
|
|
}
|
|
}
|