fix: use PATH-aware resolution for provider availability and fix Windows cmd escaping
Provider isAvailable() was using executableExists() which only checks filesystem paths, not PATH. Commands like ["claude", "--flag"] would show as unavailable even though they'd launch fine. Switch to isCommandAvailable() which uses findExecutable() for proper PATH resolution. Un-export executableExists from the public API. Fix Windows cmd.exe metacharacter escaping — &, |, ^, <, >, (, ), ! are now properly escaped with ^, and % is doubled. Add shared escapeWindowsCmdValue helper used by both quoteWindowsCommand and quoteWindowsArgument. Replace local quoteForCmd in spawn.ts. Add server-tests-windows CI job to catch Windows-specific regressions.
This commit is contained in:
@@ -105,6 +105,20 @@ export async function isCommandAvailable(command: string): Promise<boolean> {
|
||||
return (await findExecutable(command)) !== null;
|
||||
}
|
||||
|
||||
function escapeWindowsCmdValue(value: string): string {
|
||||
if (process.platform !== "win32") return value;
|
||||
|
||||
const isQuoted = value.startsWith('"') && value.endsWith('"');
|
||||
const unquoted = isQuoted ? value.slice(1, -1) : value;
|
||||
const escaped = unquoted.replace(/%/g, "%%").replace(/([&|^<>()!])/g, "^$1");
|
||||
|
||||
if (isQuoted || escaped.includes(" ")) {
|
||||
return `"${escaped}"`;
|
||||
}
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
/**
|
||||
* When spawning with `shell: true` on Windows, the command is passed to
|
||||
* `cmd.exe /d /s /c "command args"`. The `/s` strips outer quotes, so a
|
||||
@@ -112,10 +126,7 @@ export async function isCommandAvailable(command: string): Promise<boolean> {
|
||||
* space. Wrapping it in quotes produces the correct `"C:\Program Files\..." args`.
|
||||
*/
|
||||
export function quoteWindowsCommand(command: string): string {
|
||||
if (process.platform !== "win32") return command;
|
||||
if (!command.includes(" ")) return command;
|
||||
if (command.startsWith('"') && command.endsWith('"')) return command;
|
||||
return `"${command}"`;
|
||||
return escapeWindowsCmdValue(command);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,8 +135,5 @@ export function quoteWindowsCommand(command: string): string {
|
||||
* child process sees it.
|
||||
*/
|
||||
export function quoteWindowsArgument(argument: string): string {
|
||||
if (process.platform !== "win32") return argument;
|
||||
if (!argument.includes(" ")) return argument;
|
||||
if (argument.startsWith('"') && argument.endsWith('"')) return argument;
|
||||
return `"${argument}"`;
|
||||
return escapeWindowsCmdValue(argument);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user