fix(installer): load nvm before node detection

This commit is contained in:
Peter Steinberger
2026-04-26 06:37:50 +01:00
parent cbf9c60f1d
commit 036b422fc6
3 changed files with 89 additions and 3 deletions

View File

@@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io.
- Installer: load nvm before Node.js detection so `curl | bash` installs respect nvm-managed Node instead of stale system Node. Fixes #49556. Thanks @heavenlxj.
- Docker: copy patched dependency files into runtime images so downstream `pnpm install` layers keep working. Fixes #69224. Thanks @gucasbrg.
- Agents/runtime: submit heartbeat, cron, and exec wakeups as transient runtime context instead of visible user prompts, keeping synthetic system work out of chat transcripts. Fixes #66496 and #66814. Thanks @jeades and @mandomaker.
- Telegram: include native quote excerpts automatically for threaded replies and reply tags when the original Telegram text is available, without adding another config knob. Fixes #6975. Thanks @rex05ai.

View File

@@ -1413,6 +1413,24 @@ ensure_default_node_active_shell() {
return 1
}
load_nvm_for_node_detection() {
local nvm_dir="${NVM_DIR:-}"
if [[ -z "$nvm_dir" && -s "$HOME/.nvm/nvm.sh" ]]; then
nvm_dir="$HOME/.nvm"
fi
if [[ -z "$nvm_dir" || ! -s "$nvm_dir/nvm.sh" ]]; then
return 0
fi
export NVM_DIR="$nvm_dir"
# shellcheck disable=SC1090
. "$NVM_DIR/nvm.sh" --no-use >/dev/null 2>&1 || . "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
if command -v nvm >/dev/null 2>&1; then
nvm use default --silent >/dev/null 2>&1 || nvm use node --silent >/dev/null 2>&1 || true
fi
refresh_shell_command_cache
}
check_node() {
if command -v node &> /dev/null; then
NODE_VERSION="$(node_major_version || true)"
@@ -2369,6 +2387,7 @@ main() {
install_homebrew
# Step 2: Node.js
load_nvm_for_node_detection
if ! check_node; then
install_node
fi

View File

@@ -1,22 +1,23 @@
import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const SCRIPT_PATH = "scripts/install.sh";
function runInstallShell(script: string) {
function runInstallShell(script: string, env: NodeJS.ProcessEnv = {}) {
return spawnSync("bash", ["-c", script], {
encoding: "utf8",
env: {
...process.env,
OPENCLAW_INSTALL_SH_NO_RUN: "1",
...env,
},
});
}
describe("install.sh apt behavior", () => {
describe("install.sh", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
it("runs apt-get through noninteractive wrappers", () => {
@@ -41,6 +42,71 @@ describe("install.sh apt behavior", () => {
'run_quiet_step "Configuring NodeSource repository" sudo -E bash "$tmp"',
);
});
it("loads nvm before checking Node.js so stale system Node does not win", () => {
expect(script).toMatch(
/# Step 2: Node\.js\s+load_nvm_for_node_detection\s+if ! check_node; then/,
);
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-nvm-"));
const home = join(tmp, "home");
const systemBin = join(tmp, "system-bin");
const nvmBin = join(home, ".nvm/versions/node/v22.22.1/bin");
mkdirSync(systemBin, { recursive: true });
mkdirSync(nvmBin, { recursive: true });
mkdirSync(join(home, ".nvm"), { recursive: true });
const systemNode = join(systemBin, "node");
const nvmNode = join(nvmBin, "node");
writeFileSync(systemNode, "#!/bin/sh\necho v8.11.3\n");
writeFileSync(nvmNode, "#!/bin/sh\necho v22.22.1\n");
chmodSync(systemNode, 0o755);
chmodSync(nvmNode, 0o755);
writeFileSync(
join(home, ".nvm/nvm.sh"),
[
'NVM_DIR="${NVM_DIR:-$HOME/.nvm}"',
"export NVM_DIR",
"nvm() {",
' if [ "$1" = "use" ]; then',
' export PATH="$NVM_DIR/versions/node/v22.22.1/bin:$PATH"',
" return 0",
" fi",
" return 0",
"}",
"",
].join("\n"),
);
let result: ReturnType<typeof runInstallShell> | undefined;
try {
result = runInstallShell(
[
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
"set +e",
"load_nvm_for_node_detection",
"check_node",
"status=$?",
'printf "status=%s\\npath=%s\\nversion=%s\\n" "$status" "$(command -v node)" "$(node -v)"',
"exit $status",
].join("\n"),
{
HOME: home,
PATH: `${systemBin}:/usr/bin:/bin`,
TERM: "dumb",
},
);
} finally {
rmSync(tmp, { force: true, recursive: true });
}
expect(result?.status).toBe(0);
const output = result?.stdout ?? "";
expect(output).toContain("status=0");
expect(output).toContain(`path=${nvmNode}`);
expect(output).toContain("version=v22.22.1");
});
});
describe("install.sh macOS Homebrew Node behavior", () => {