fix(ai-tools): detect AI CLIs on Windows through PATHEXT
which() looked for the bare name only, so on Windows every tool (claude.exe, codex.cmd, ...) read installed: false. It now tries each PATHEXT extension (default .EXE;.CMD;.BAT), the bare name only when it already carries one - npm's extensionless shim beside codex.cmd is a sh script Windows cannot run. spawnTarget() gives a .cmd/.bat shell: true and a quoted path, used by the install/update job runner and the npm/version probes, which Node otherwise refuses with EINVAL (CVE-2024-27980). Catalog argv only.
This commit is contained in:
+38
-7
@@ -260,19 +260,49 @@ const has = (id) => typeof id === "string" && cat().has(id);
|
||||
function which(name, pathStr = process.env.PATH || "") {
|
||||
for (const dir of pathStr.split(path.delimiter)) {
|
||||
if (!dir) continue;
|
||||
const p = path.join(dir, name);
|
||||
try {
|
||||
if (fs.statSync(p).isFile()) {
|
||||
fs.accessSync(p, fs.constants.X_OK);
|
||||
return p;
|
||||
for (const candidate of executableNames(name)) {
|
||||
const p = path.join(dir, candidate);
|
||||
try {
|
||||
if (fs.statSync(p).isFile()) {
|
||||
fs.accessSync(p, fs.constants.X_OK);
|
||||
return p;
|
||||
}
|
||||
} catch {
|
||||
/* not here */
|
||||
}
|
||||
} catch {
|
||||
/* not here */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The file names `name` can run as. On Windows only a `PATHEXT` extension
|
||||
* makes a file runnable (`claude.exe`, `codex.cmd`): the bare name is tried
|
||||
* only when it already carries one, because npm's extensionless `codex` next
|
||||
* to `codex.cmd` is a POSIX sh shim Windows cannot start.
|
||||
*/
|
||||
function executableNames(name) {
|
||||
if (process.platform !== "win32") return [name];
|
||||
const exts = (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean);
|
||||
const lower = name.toLowerCase();
|
||||
if (exts.some((e) => lower.endsWith(e.toLowerCase()))) return [name];
|
||||
return exts.map((e) => name + e.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* How to start `file` with spawn/execFile: on Windows a `.cmd`/`.bat` (npm,
|
||||
* an npm-installed CLI) runs only through cmd.exe — Node refuses it otherwise
|
||||
* (EINVAL, CVE-2024-27980) — so it gets `shell: true` and a quoted path, as
|
||||
* `C:\Program Files\nodejs\npm.cmd` holds a space. Only for argv built from
|
||||
* release catalog files, never from a request: cmd.exe re-parses the args.
|
||||
*/
|
||||
function spawnTarget(file) {
|
||||
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(file)) {
|
||||
return { file: `"${file}"`, shell: true };
|
||||
}
|
||||
return { file, shell: false };
|
||||
}
|
||||
|
||||
/** `{installed, path, command}` for one tool, cached until refresh() unless `fresh`. */
|
||||
function detect(id, { fresh = false } = {}) {
|
||||
if (!fresh && detected.has(id)) return detected.get(id);
|
||||
@@ -426,6 +456,7 @@ module.exports = {
|
||||
view,
|
||||
validate,
|
||||
which,
|
||||
spawnTarget,
|
||||
siteOf,
|
||||
ROUTER_CAPABLE,
|
||||
CONFIG_SECRET_RE,
|
||||
|
||||
Reference in New Issue
Block a user