From 217c0fa81d8d0fe4d54979ea3f7a9057c0de0bda Mon Sep 17 00:00:00 2001 From: Simon van Meegdenburg <49989016+Simonvm9114@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:00:55 +0200 Subject: [PATCH] Resolve binaries with Bun.which instead of POSIX lookups (fixes #2295) On native Windows, DetectEnv reported bun, git and the harness binary as missing: detectTool() and detectHarness() ran `command -v` through execSync, which uses cmd.exe on win32, and cmd.exe has no `command` built-in. This happened even inside Claude Code's Bash tool and hooks. ClipSource's requireBinary() spawned `which`, which does not exist on Windows outside Git Bash, so it died on ENOENT with ffmpeg installed. - InstallEngine.ts (both shipped copies): new findBin() uses Bun.which, which is cross-platform and PATHEXT-aware (finds npm's claude.cmd). Outside bun it falls back to `where` on win32, `command -v` elsewhere. - ClipSource.ts: requireBinary() uses Bun.which. Verified on Windows 11, bun 1.4.2, from PowerShell and from Git Bash: DetectEnv now reports bun and git installed and the claude-code harness as "detected"; ClipSource passes its ffmpeg/ffprobe checks and stops at the (dummy) source file. The non-bun fallback resolves git via `where` under Node. macOS/Linux take the same Bun.which path; not run there. Co-Authored-By: Claude Opus 5.5 --- LifeOS/Tools/InstallEngine.ts | 19 +++++++++++++++++-- LifeOS/install/LIFEOS/TOOLS/ClipSource.ts | 4 +++- .../skills/LifeOS/Tools/InstallEngine.ts | 19 +++++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/LifeOS/Tools/InstallEngine.ts b/LifeOS/Tools/InstallEngine.ts index b5b1b76b4d..be2c05fac2 100644 --- a/LifeOS/Tools/InstallEngine.ts +++ b/LifeOS/Tools/InstallEngine.ts @@ -89,6 +89,21 @@ function tryExec(cmd: string): string | null { } } +/** + * Absolute path of a binary on PATH, or null. Never `command -v` through + * tryExec on Windows: execSync runs cmd.exe there, which has no `command` + * built-in, so every probe failed and DetectEnv reported bun, git and the + * harness binary missing while running on bun (public issue #2295). + * Bun.which is cross-platform and PATHEXT-aware (finds npm's claude.cmd); + * the shell probes are the fallback when this file runs outside bun. + */ +function findBin(name: string): string | null { + const bun = (globalThis as { Bun?: { which(bin: string): string | null } }).Bun; + if (bun) return bun.which(name); + const out = tryExec(process.platform === "win32" ? `where ${name}` : `command -v ${name}`); + return out ? out.split(/\r?\n/)[0] : null; +} + export function detectOS(): OsInfo { const platform: Platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux"; @@ -109,7 +124,7 @@ export function detectOS(): OsInfo { } export function detectTool(name: string, versionCmd: string): ToolInfo { - const path = tryExec(`command -v ${name}`); + const path = findBin(name); if (!path) return { installed: false }; const out = tryExec(versionCmd); const m = out?.match(/(\d+\.\d+[.\d]*)/); @@ -133,7 +148,7 @@ export function detectHarness(home: string): HarnessInfo { { name: "cursor", root: join(home, ".cursor"), skills: "skills", bin: "cursor" }, { name: "openclaw", root: join(home, ".openclaw"), skills: "skills", bin: "openclaw" }, ]; - const hasBin = (c: (typeof candidates)[number]) => !!tryExec(`command -v ${c.bin}`); + const hasBin = (c: (typeof candidates)[number]) => !!findBin(c.bin); const info = (c: (typeof candidates)[number], confidence: HarnessInfo["confidence"]): HarnessInfo => ({ name: c.name, configRoot: c.root, diff --git a/LifeOS/install/LIFEOS/TOOLS/ClipSource.ts b/LifeOS/install/LIFEOS/TOOLS/ClipSource.ts index 04b04c2807..fd499c9f21 100644 --- a/LifeOS/install/LIFEOS/TOOLS/ClipSource.ts +++ b/LifeOS/install/LIFEOS/TOOLS/ClipSource.ts @@ -135,7 +135,9 @@ async function run(cmd: string[], label: string): Promise { } function requireBinary(bin: string, why: string): void { - const found = Bun.spawnSync(["which", bin]).exitCode === 0; + // Bun.which, not a spawned `which`: Windows has no which.exe outside Git Bash, + // so spawnSync threw ENOENT with ffmpeg installed (public issue #2295). + const found = Bun.which(bin) !== null; if (!found) die(`${bin} not installed — needed to ${why}. brew install ${bin}`); } diff --git a/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts b/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts index b5b1b76b4d..be2c05fac2 100644 --- a/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts +++ b/LifeOS/install/skills/LifeOS/Tools/InstallEngine.ts @@ -89,6 +89,21 @@ function tryExec(cmd: string): string | null { } } +/** + * Absolute path of a binary on PATH, or null. Never `command -v` through + * tryExec on Windows: execSync runs cmd.exe there, which has no `command` + * built-in, so every probe failed and DetectEnv reported bun, git and the + * harness binary missing while running on bun (public issue #2295). + * Bun.which is cross-platform and PATHEXT-aware (finds npm's claude.cmd); + * the shell probes are the fallback when this file runs outside bun. + */ +function findBin(name: string): string | null { + const bun = (globalThis as { Bun?: { which(bin: string): string | null } }).Bun; + if (bun) return bun.which(name); + const out = tryExec(process.platform === "win32" ? `where ${name}` : `command -v ${name}`); + return out ? out.split(/\r?\n/)[0] : null; +} + export function detectOS(): OsInfo { const platform: Platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux"; @@ -109,7 +124,7 @@ export function detectOS(): OsInfo { } export function detectTool(name: string, versionCmd: string): ToolInfo { - const path = tryExec(`command -v ${name}`); + const path = findBin(name); if (!path) return { installed: false }; const out = tryExec(versionCmd); const m = out?.match(/(\d+\.\d+[.\d]*)/); @@ -133,7 +148,7 @@ export function detectHarness(home: string): HarnessInfo { { name: "cursor", root: join(home, ".cursor"), skills: "skills", bin: "cursor" }, { name: "openclaw", root: join(home, ".openclaw"), skills: "skills", bin: "openclaw" }, ]; - const hasBin = (c: (typeof candidates)[number]) => !!tryExec(`command -v ${c.bin}`); + const hasBin = (c: (typeof candidates)[number]) => !!findBin(c.bin); const info = (c: (typeof candidates)[number], confidence: HarnessInfo["confidence"]): HarnessInfo => ({ name: c.name, configRoot: c.root,