Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
9.1 kB · 273 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274import { existsSync } from "node:fs";import { delimiter, win32 } from "node:path";import { getBinDir } from "../config.js";import { recordOrphanProcessState } from "../core/orphan-process-journal.js";import { spawnHidden, spawnSyncHidden } from "./child-process.js";
export interface ShellConfig { shell: string; args: string[];}
/** System32\bash.exe is the WSL launcher (runs Linux-side), so %SystemRoot% matches are only a last resort. */export function orderWindowsBashCandidates(matches: readonly string[], systemRoot: string | undefined): string[] { if (!systemRoot) return [...matches]; const prefix = win32.join(systemRoot, "\\").toLowerCase(); const underSystemRoot = (match: string) => win32.normalize(match).toLowerCase().startsWith(prefix); return [...matches.filter((match) => !underSystemRoot(match)), ...matches.filter(underSystemRoot)];}
/** * Find bash executable on PATH (cross-platform) */function findBashOnPath(): string | null { if (process.platform === "win32") { // Windows: Use 'where' and verify file exists (where can return non-existent paths) try { const result = spawnSyncHidden("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 }); if (result.status === 0 && result.stdout) { const matches = result.stdout.trim().split(/\r?\n/).filter(Boolean); for (const match of orderWindowsBashCandidates(matches, process.env.SystemRoot)) { if (existsSync(match)) { return match; } } } } catch { // Ignore errors } return null; }
// Unix: Use 'which' and trust its output (handles Termux and special filesystems) try { const result = spawnSyncHidden("which", ["bash"], { encoding: "utf-8", timeout: 5000 }); if (result.status === 0 && result.stdout) { const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; if (firstMatch) { return firstMatch; } } } catch { // Ignore errors } return null;}
/** * Resolve shell configuration based on platform and an optional explicit shell path. * Resolution order: * 1. User-specified shellPath * 2. On Windows: Git Bash in known locations, then bash on PATH * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh */export function getShellConfig(customShellPath?: string): ShellConfig { // 1. Check user-specified shell path if (customShellPath) { if (existsSync(customShellPath)) { return { shell: customShellPath, args: ["-c"] }; } throw new Error(`Custom shell path not found: ${customShellPath}`); }
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)) { return { shell: path, args: ["-c"] }; } }
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.) const bashOnPath = findBashOnPath(); if (bashOnPath) { return { shell: bashOnPath, args: ["-c"] }; }
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 settings.json\n\n" + `Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`, ); }
// Unix: try /bin/bash, then bash on PATH, then fallback to sh if (existsSync("/bin/bash")) { return { shell: "/bin/bash", args: ["-c"] }; }
const bashOnPath = findBashOnPath(); if (bashOnPath) { return { shell: bashOnPath, args: ["-c"] }; }
return { shell: "sh", args: ["-c"] };}
// Hardcoded literals: ProgramFiles env vars are ambient attacker-influenceable// input, the same trust-laundering class as PATH.const WINDOWS_GIT_BASH_PATHS = ["C:\\Program Files\\Git\\bin\\bash.exe", "C:\\Program Files (x86)\\Git\\bin\\bash.exe"];
/** * Absolute default shell for the kernel's bash(): explicit shellPath wins; POSIX * uses /bin/bash else /bin/sh (absolute, never PATH — the kernel inherits a * user-influenced PATH); win32 uses only the canonical Git Bash install paths, * never PATH (a repo-controlled PATH/where.exe must not pick the kernel shell). * undefined = no shell found: kernel startup must not fail, bash() raises its * teaching error. */export function resolveKernelBashShell(customShellPath?: string): string | undefined { const explicit = customShellPath?.trim(); if (explicit) { return explicit; } if (process.platform !== "win32") { return existsSync("/bin/bash") ? "/bin/bash" : "/bin/sh"; } for (const path of WINDOWS_GIT_BASH_PATHS) { if (existsSync(path)) { return path; } } return undefined;}
export function getShellEnv(): NodeJS.ProcessEnv { const binDir = getBinDir(); const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH"; const currentPath = process.env[pathKey] ?? ""; const pathEntries = currentPath.split(delimiter).filter(Boolean); const hasBinDir = pathEntries.includes(binDir); const updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);
// Agent-spawned shells never have a usable stdin (stdio: ["ignore", "pipe", "pipe"]), // so any interactive prompt opened via /dev/tty is a guaranteed hang until killed: // `git commit` without -m launches $EDITOR, credential helpers block waiting for a // password, pagers read the terminal directly. Make those cases fail fast or no-op // instead of hanging. // // These deliberately override inherited terminal settings (an EDITOR=vim inherited // from the launching shell is exactly the hang we are preventing, and stdin is // ignored even for user `!` commands, so an interactive editor can never receive // keystrokes anyway). A user who wants a prompt in a specific command can override // inline (`GIT_EDITOR=vim git commit`), which takes precedence over exported vars. return { ...process.env, [pathKey]: updatedPath, GIT_EDITOR: "true", GIT_SEQUENCE_EDITOR: "true", GIT_TERMINAL_PROMPTS: "0", GIT_ASKPASS: "true", SSH_ASKPASS_REQUIRE: "never", EDITOR: "true", VISUAL: "true", PAGER: "cat", GIT_PAGER: "cat", DEBIAN_FRONTEND: "noninteractive", };}
/** * 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) * - Characters with undefined code points */export function sanitizeBinaryOutput(str: string): string { // Use Array.from to properly iterate over code points (not code units) // This handles surrogate pairs correctly and catches edge cases where // codePointAt() might return undefined return Array.from(str) .filter((char) => { // Filter out characters that cause string-width to crash // This includes: // - Unicode format characters // - Lone surrogates (already filtered by Array.from) // - Control chars except \t \n \r // - Characters with undefined code points
const code = char.codePointAt(0);
// Skip if code point is undefined (edge case with invalid strings) if (code === undefined) return false;
// Allow tab, newline, carriage return if (code === 0x09 || code === 0x0a || code === 0x0d) return true;
// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d) if (code <= 0x1f) return false;
// Filter out Unicode format characters if (code >= 0xfff9 && code <= 0xfffb) return false;
return true; }) .join("");}
/** * Detached child processes must be tracked so they can be killed on parent * shutdown signals (SIGHUP/SIGTERM). */const trackedDetachedChildPids = new Set<number>();
export function trackDetachedChildPid(pid: number): void { trackedDetachedChildPids.add(pid); recordOrphanProcessState(pid, true);}
export function untrackDetachedChildPid(pid: number): void { trackedDetachedChildPids.delete(pid); recordOrphanProcessState(pid, false);}
export function killTrackedDetachedChildren(): void { for (const pid of trackedDetachedChildPids) { killProcessTree(pid); recordOrphanProcessState(pid, false); } trackedDetachedChildPids.clear();}
/** * 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 { spawnHidden("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 } } }}