fix(install): reject PATH runtimes with broken npm (#107825)

* fix(install): reject runtimes with broken npm

* test(installer): use real Node for npm selection

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
ooiuuii
2026-07-17 08:17:07 +08:00
committed by GitHub
parent 382d570cbf
commit 396194b96b
2 changed files with 80 additions and 0 deletions
+5
View File
@@ -447,6 +447,7 @@ link_node_runtime_paths() {
}
linked_node_is_usable() {
local candidate_bin
local current_version
local required_version
@@ -462,6 +463,10 @@ linked_node_is_usable() {
if ! semver_at_least "$current_version" "$required_version"; then
return 1
fi
candidate_bin="$(node_dir)/bin"
if ! PATH="${candidate_bin}:${PATH}" "$(npm_bin)" --version >/dev/null 2>&1; then
return 1
fi
"$(node_bin)" -e '
const { DatabaseSync } = require("node:sqlite");
+75
View File
@@ -641,6 +641,81 @@ describe("install-cli.sh", () => {
}
});
it("skips PATH Node runtimes whose npm command cannot start", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-broken-npm-"));
const badBin = join(tmp, "bad-bin");
const goodBin = join(tmp, "good-bin");
const prefix = join(tmp, "prefix");
const badNpmLog = join(tmp, "bad-npm.log");
const goodNpmLog = join(tmp, "good-npm.log");
const goodNodeLog = join(tmp, "good-node.log");
const badNode = join(badBin, "node");
const badNpm = join(badBin, "npm");
const goodNode = join(goodBin, "node");
const goodNpm = join(goodBin, "npm");
mkdirSync(badBin, { recursive: true });
mkdirSync(goodBin, { recursive: true });
symlinkSync(process.execPath, badNode);
writeFileSync(
goodNode,
[
"#!/bin/bash",
'printf "%s\\n" "$*" >> "$GOOD_NODE_LOG"',
`exec ${JSON.stringify(process.execPath)} "$@"`,
"",
].join("\n"),
);
writeFileSync(
badNpm,
["#!/bin/bash", 'printf "%s\\n" "$*" >> "$BAD_NPM_LOG"', "exit 42", ""].join("\n"),
);
writeFileSync(
goodNpm,
[
"#!/usr/bin/env node",
'require("node:fs").appendFileSync(',
" process.env.GOOD_NPM_LOG,",
' `${process.argv.slice(2).join(" ")}\\n`,',
");",
"",
].join("\n"),
);
chmodSync(badNpm, 0o755);
chmodSync(goodNode, 0o755);
chmodSync(goodNpm, 0o755);
try {
const result = runInstallCliShell(
[
"set -euo pipefail",
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
`export PATH=${JSON.stringify(`${badBin}:${goodBin}:${process.env.PATH ?? ""}`)}`,
`PREFIX=${JSON.stringify(prefix)}`,
"try_link_usable_node_runtime_from_path",
].join("\n"),
{
BAD_NPM_LOG: badNpmLog,
GOOD_NPM_LOG: goodNpmLog,
GOOD_NODE_LOG: goodNodeLog,
},
);
expect(result.status).toBe(0);
const nodeLink = join(prefix, "tools", "node-v24.15.0", "bin", "node");
const npmLink = join(prefix, "tools", "node-v24.15.0", "bin", "npm");
expect(readFileSync(badNpmLog, "utf8")).toBe("--version\n");
expect(readFileSync(goodNpmLog, "utf8")).toBe("--version\n");
expect(readFileSync(goodNodeLog, "utf8")).toContain("npm --version");
expect(lstatSync(nodeLink).isSymbolicLink()).toBe(true);
expect(readlinkSync(nodeLink)).toBe(goodNode);
expect(readlinkSync(npmLink)).toBe(goodNpm);
} finally {
rmSync(tmp, { force: true, recursive: true });
}
});
it("rejects Alpine/musl Node packages below the requested runtime floor", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-alpine-old-node-"));
const bin = join(tmp, "bin");