From 5f13af6b68603c49367e1540212779d1dcafafb2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 30 Apr 2026 04:39:33 +0100 Subject: [PATCH] fix: warn before npm prefix redirection (#73890) (thanks @Sayeem3051) --- CHANGELOG.md | 1 + scripts/install.sh | 3 ++ test/scripts/install-sh.test.ts | 53 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85c02a8d2b4..8f1f557a364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -286,6 +286,7 @@ Docs: https://docs.openclaw.ai - Commands: keep channel-prefixed owner allowlist entries scoped to matching providers so webchat command contexts cannot inherit external channel owners. Thanks @zsxsoft. - Auth/device pairing: bound bootstrap handoff token issuance, redemption, and approved pairing baselines to the documented per-role scope allowlist, so bootstrap approvals cannot persistently grant `operator.admin`, `operator.pairing`, or `node.exec` scopes. Thanks @eleqtrizit. - Providers/GitHub Copilot: support the GUI/RPC wizard device-code auth flow so onboarding from non-TTY clients (gateway RPC bridge, GUI wizards) completes instead of returning empty profiles. Dangerous-state handling now distinguishes `access_denied` and `expired_token` from transport errors. (#73290) Thanks @indierawk2k2. +- Installer/Linux: warn before switching an unwritable npm global prefix to `~/.npm-global`, then tell users to run future global updates with `npm i -g openclaw@latest` without `sudo` so npm keeps using the redirected user prefix. Fixes #44365; carries forward #50479. Thanks @Sayeem3051. ## 2026.4.27 diff --git a/scripts/install.sh b/scripts/install.sh index 6e1084d958a..edaf1559d80 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1633,9 +1633,12 @@ fix_npm_permissions() { return 0 fi + ui_warn "npm global prefix is not writable: ${npm_prefix}" + ui_warn "The installer will switch npm's user prefix to ${HOME}/.npm-global; npm normally writes that setting to ~/.npmrc." ui_info "Configuring npm for user-local installs" mkdir -p "$HOME/.npm-global" npm config set prefix "$HOME/.npm-global" + ui_warn "Avoid sudo npm i -g for future OpenClaw updates; use npm i -g openclaw@latest so npm keeps using this user prefix instead of a different global prefix." # shellcheck disable=SC2016 local path_line='export PATH="$HOME/.npm-global/bin:$PATH"' diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index a903bb17e32..6b520fbd4de 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -108,6 +108,59 @@ describe("install.sh", () => { expect(output).toContain(`path=${nvmNode}`); expect(output).toContain("version=v22.22.1"); }); + + it("warns before redirecting an unwritable npm prefix", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-prefix-")); + const home = join(tmp, "home"); + const events = join(tmp, "events.log"); + mkdirSync(home, { recursive: true }); + + let result: ReturnType | undefined; + try { + result = runInstallShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + OS=linux + HOME=${JSON.stringify(home)} + prefix=${JSON.stringify(join(tmp, "root-owned-prefix"))} + events=${JSON.stringify(events)} + npm() { + if [[ "$1" == "config" && "$2" == "get" && "$3" == "prefix" ]]; then + printf '%s\\n' "$prefix" + return 0 + fi + if [[ "$1" == "config" && "$2" == "set" && "$3" == "prefix" ]]; then + printf 'npm-set:%s\\n' "$4" >> "$events" + return 0 + fi + return 1 + } + ui_info() { printf 'info:%s\\n' "$*" >> "$events"; } + ui_warn() { printf 'warn:%s\\n' "$*" >> "$events"; } + ui_success() { printf 'success:%s\\n' "$*" >> "$events"; } + fix_npm_permissions + cat "$events" + `); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + + expect(result?.status).toBe(0); + const lines = (result?.stdout ?? "").trim().split("\n"); + const warningIndex = lines.findIndex((line) => + line.includes("The installer will switch npm's user prefix"), + ); + const npmSetIndex = lines.findIndex((line) => line.startsWith("npm-set:")); + const noSudoWarningIndex = lines.findIndex((line) => line.includes("Avoid sudo npm i -g")); + expect(warningIndex).toBeGreaterThanOrEqual(0); + expect(npmSetIndex).toBeGreaterThan(warningIndex); + expect(noSudoWarningIndex).toBeGreaterThan(npmSetIndex); + expect(result?.stdout).toContain("npm global prefix is not writable"); + expect(result?.stdout).toContain("npm normally writes that setting to ~/.npmrc"); + expect(result?.stdout).toContain("npm i -g openclaw@latest"); + expect(result?.stdout).toContain("using this user prefix"); + expect(result?.stdout).not.toContain("has been saved"); + }); }); describe("install.sh macOS Homebrew Node behavior", () => {