fix: v2.5.1 — plugin npm runs no longer depend on PATH (Windows update failure)

A Windows self-update failed at "bootstrap": 'npm.cmd' is not recognized.
The update chain runs with the dashboard server's environment, whose PATH
need not reach npm.cmd, and the bootstrap looked npm up through cmd.exe
(shell: true, which also printed DEP0190).

- npmCommand() in scripts/plugin-bootstrap.js runs the npm bundled with the
  running node (npm-cli.js beside process.execPath, Windows and Unix
  layouts) with no shell, and puts that node's directory first on PATH (kept
  under the env's own Path/PATH key) so npm scripts find the same node; npm
  on PATH only when that node ships none
- Used by the runtime install, the UI build (plugin-open.js) and the
  doctor's npm row (plugin-doctor.js)
This commit is contained in:
2026-10-06 19:08:38 +07:00
parent 03ecc9e29a
commit be75572977
10 changed files with 88 additions and 20 deletions
+42
View File
@@ -651,3 +651,45 @@ describe("pruneRetiredPluginRoots", () => {
assert.ok(fs.existsSync(sibling));
});
});
describe("npmCommand", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-npmcmd-"));
after(() => fs.rmSync(tmp, { recursive: true, force: true }));
const cliAt = (...parts) => {
const p = path.join(tmp, ...parts);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, "");
return p;
};
it("runs the node's own bundled npm (Windows layout), no shell, no PATH lookup", () => {
// The "'npm.cmd' is not recognized" failure: the update chain's PATH had no npm.
const cli = cliAt("win", "node_modules", "npm", "bin", "npm-cli.js");
const execPath = path.join(tmp, "win", "node.exe");
const npm = boot.npmCommand(["install"], { execPath, env: { Path: "C:\\other" } });
assert.equal(npm.file, execPath);
assert.deepEqual(npm.args, [cli, "install"]);
assert.equal(npm.shell, false);
// Windows env keys keep their own case — a second "PATH" key would be ambiguous.
assert.equal(npm.env.Path, [path.dirname(execPath), "C:\\other"].join(path.delimiter));
assert.equal(npm.env.PATH, undefined);
});
it("finds the Unix layout (bin/node + lib/node_modules/npm)", () => {
const cli = cliAt("unix", "lib", "node_modules", "npm", "bin", "npm-cli.js");
const execPath = path.join(tmp, "unix", "bin", "node");
const npm = boot.npmCommand(["-v"], { execPath, env: { PATH: "/usr/bin" } });
assert.deepEqual(npm.args, [
path.join(tmp, "unix", "bin", "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
"-v",
]);
assert.ok(fs.existsSync(npm.args[0]) && path.resolve(npm.args[0]) === cli);
assert.equal(npm.env.PATH.split(path.delimiter)[0], path.dirname(execPath));
});
it("falls back to npm on PATH when the node has no bundled npm", () => {
const npm = boot.npmCommand(["-v"], { execPath: path.join(tmp, "bare", "node"), env: {} });
assert.equal(npm.file, process.platform === "win32" ? "npm.cmd" : "npm");
assert.deepEqual(npm.args, ["-v"]);
});
});