From 2306e6bb348e7175762e7739a55d01978c476624 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 16:01:16 -0400 Subject: [PATCH] fix: LSP command resolution and ENOENT crash on Windows/MSYS (#901) (#925) Two fixes: 1. lsp/config.ts: Use `where.exe` instead of `which` on Windows. MSYS's `which` returns POSIX paths (/c/Users/...) that Node's spawn() can't execute. `where.exe` returns native Windows paths. 2. lsp/client.ts: Handle spawn ENOENT error gracefully. When the LSP server binary doesn't exist, the error event now triggers a clean exit instead of bubbling up and crashing auto-mode. --- packages/pi-coding-agent/src/core/lsp/client.ts | 8 ++++++++ packages/pi-coding-agent/src/core/lsp/config.ts | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/pi-coding-agent/src/core/lsp/client.ts b/packages/pi-coding-agent/src/core/lsp/client.ts index 4744494ea..33682fd50 100644 --- a/packages/pi-coding-agent/src/core/lsp/client.ts +++ b/packages/pi-coding-agent/src/core/lsp/client.ts @@ -437,6 +437,14 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT env: env ? { ...process.env, ...env } : undefined, }); + // Handle spawn failure (e.g., ENOENT when the command doesn't exist). + // Without this, the error bubbles up and can crash auto-mode (#901). + proc.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + proc.emit("exit", 1); + } + }); + const exitedPromise = new Promise((resolve) => { proc.on("exit", (code: number | null) => resolve(code ?? 1)); }); diff --git a/packages/pi-coding-agent/src/core/lsp/config.ts b/packages/pi-coding-agent/src/core/lsp/config.ts index fe6226dc1..c17494dc9 100644 --- a/packages/pi-coding-agent/src/core/lsp/config.ts +++ b/packages/pi-coding-agent/src/core/lsp/config.ts @@ -177,9 +177,16 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [ ]; function which(command: string): string | null { - const result = spawnSync("which", [command], { encoding: "utf-8" }); + // On Windows, prefer `where.exe` over `which` — MSYS/Git Bash's `which` + // returns POSIX paths (/c/Users/...) that Node's spawn() can't execute. + // `where.exe` returns native Windows paths (C:\Users\...). + const isWindows = process.platform === "win32"; + const cmd = isWindows ? "where.exe" : "which"; + const result = spawnSync(cmd, [command], { encoding: "utf-8", shell: isWindows }); if (result.status !== 0) return null; - return result.stdout.trim() || null; + // `where.exe` may return multiple lines — take the first + const resolved = result.stdout.trim().split(/\r?\n/)[0]?.trim(); + return resolved || null; } export function resolveCommand(command: string, cwd: string): string | null {