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.
This commit is contained in:
Tom Boucher 2026-03-17 16:01:16 -04:00 committed by GitHub
parent 7869312769
commit 2306e6bb34
2 changed files with 17 additions and 2 deletions

View file

@ -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<number>((resolve) => {
proc.on("exit", (code: number | null) => resolve(code ?? 1));
});

View file

@ -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 {