/** * Check whether a binary is available on PATH. * * Pure module: no I/O side effects beyond `spawnSync`. No Pi runtime imports, * no `src/` imports — kept self-contained so it can be unit-tested without * jiti and reused in any future tooling. * * Why this exists (v0.14.0): when the user has a formatter configured but * the binary is not installed, we degrade gracefully to the built-in autofix * path (default mode) instead of leaving the file with raw `newText` drift. * Detection happens at `session_start` time so the rest of the cascade — * `tool_call` / `tool_result` — sees a `ResolvedFormatter[]` that already * excludes the missing binaries. */ import { spawnSync } from 'node:child_process'; const PROBE = process.platform === 'win32' ? 'where' : 'which'; /** * Return true when `bin` resolves to an executable on PATH. * * Implementation notes: * - Uses `which` on POSIX and `where` on Windows. Both are standard on * every supported platform (Linux, macOS, Windows 10+). * - `spawnSync` with an argv array does NOT invoke a shell, so command * injection through bin names is not possible. * - Any error (probe missing, sandbox blocking, etc.) returns false * (conservative — we treat "can't tell" as "not available"). * - Empty string returns false (no probe is meaningful). * * Cost: one `fork`+`execve` per unique binary per `session_start`. With the * default config (a handful of binaries) and only the ones matched against * the current project's filetypes being probed, this is well under 50ms in * total. Negligible. * * @param bin - The executable name to probe (e.g. `"prettier"`, `"biome"`). * @returns `true` if the binary resolves on PATH, `false` otherwise (including * empty input and error cases). */ export function binaryAvailable(bin: string): boolean { if (!bin) return false; try { const result = spawnSync(PROBE, [bin], { stdio: 'ignore', windowsHide: true, }); return result.status === 0; } catch { return false; } }