From 207bcd6b207cf8abcb543d09d46c64899c4e9ea9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 05:08:46 +0100 Subject: [PATCH] fix(installer): persist Linux supported PATH --- scripts/install.sh | 39 +++++++++++- test/scripts/install-sh.test.ts | 102 ++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 38bced676f5..52d8e038635 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1366,6 +1366,32 @@ prepend_path_dir() { refresh_shell_command_cache } +persist_shell_path_prepend() { + local dir="${1%/}" + if [[ -z "$dir" ]]; then + return 1 + fi + + local path_line="export PATH=\"${dir}:\$PATH\"" + local wrote_rc=0 + for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do + if [[ -f "$rc" ]]; then + if ! grep -Fq "$dir" "$rc"; then + local tmp_rc="${rc}.openclaw-tmp" + { + printf '%s\n' "$path_line" + cat "$rc" + } > "$tmp_rc" + mv "$tmp_rc" "$rc" + fi + wrote_rc=1 + fi + done + if [[ "$wrote_rc" -eq 0 ]]; then + printf '%s\n' "$path_line" >> "$HOME/.bashrc" + fi +} + promote_supported_node_binary() { local candidates=() local candidate dir seen_dirs=":" @@ -1397,6 +1423,9 @@ promote_supported_node_binary() { seen_dirs="${seen_dirs}${dir}:" if node_binary_is_at_least_required "$candidate"; then prepend_path_dir "$dir" || continue + if [[ "$OS" == "linux" ]]; then + persist_shell_path_prepend "$dir" || true + fi ui_info "Using Node.js runtime at ${candidate}" return 0 fi @@ -1623,8 +1652,8 @@ install_node() { exit 1 fi - promote_supported_node_binary || true ui_success "Node.js v${NODE_DEFAULT_MAJOR} installed" + activate_supported_node_on_path || true print_active_node_paths || true fi } @@ -1738,7 +1767,12 @@ fix_npm_permissions() { for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do if [[ -f "$rc" ]]; then if ! grep -q ".npm-global" "$rc"; then - echo "$path_line" >> "$rc" + local tmp_rc="${rc}.openclaw-tmp" + { + printf '%s\n' "$path_line" + cat "$rc" + } > "$tmp_rc" + mv "$tmp_rc" "$rc" fi wrote_rc=1 fi @@ -2665,6 +2699,7 @@ main() { if ! check_node; then install_node fi + activate_supported_node_on_path || true if ! ensure_default_node_active_shell; then exit 1 fi diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index 8a498ca5086..1383b22a077 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -155,6 +155,64 @@ describe("install.sh", () => { expect(output).toContain("version=v22.22.0"); }); + it("persists a supported Linux Node path before noninteractive shell guards", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-linux-node-path-")); + const home = join(tmp, "home"); + const oldBin = join(tmp, "old/bin"); + const installedBin = join(tmp, "usr/bin"); + mkdirSync(home, { recursive: true }); + mkdirSync(oldBin, { recursive: true }); + mkdirSync(installedBin, { recursive: true }); + + const oldNode = join(oldBin, "node"); + const installedNode = join(installedBin, "node"); + writeFileSync( + join(home, ".bashrc"), + ["case $- in", " *i*) ;;", " *) return ;;", "esac", ""].join("\n"), + ); + writeFileSync( + oldNode, + [ + "#!/usr/bin/env bash", + 'if [[ "${1:-}" == "-p" ]]; then echo "20 20"; exit 0; fi', + 'if [[ "${1:-}" == "-v" ]]; then echo "v20.20.0"; exit 0; fi', + "", + ].join("\n"), + ); + writeFileSync( + installedNode, + [ + "#!/usr/bin/env bash", + 'if [[ "${1:-}" == "-p" ]]; then echo "24 13"; exit 0; fi', + 'if [[ "${1:-}" == "-v" ]]; then echo "v24.13.0"; exit 0; fi', + "", + ].join("\n"), + ); + chmodSync(oldNode, 0o755); + chmodSync(installedNode, 0o755); + + let result: ReturnType | undefined; + try { + result = runInstallShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + OS=linux + HOME=${JSON.stringify(home)} + PATH=${JSON.stringify(`${oldBin}:${installedBin}:/usr/bin:/bin`)} + ui_info() { :; } + activate_supported_node_on_path + printf 'first=%s\\n' "$(sed -n '1p' "$HOME/.bashrc")" + HOME=${JSON.stringify(home)} PATH=${JSON.stringify(`${oldBin}:${installedBin}:/usr/bin:/bin`)} bash -lc '. "$HOME/.bashrc"; printf "node=%s\\n" "$(command -v node)"' + `); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + + expect(result?.status).toBe(0); + expect(result?.stdout).toContain(`first=export PATH="${installedBin}:$PATH"`); + expect(result?.stdout).toContain(`node=${installedNode}`); + }); + it("warns before redirecting an unwritable npm prefix", () => { const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-prefix-")); const home = join(tmp, "home"); @@ -208,6 +266,50 @@ describe("install.sh", () => { expect(result?.stdout).not.toContain("has been saved"); }); + it("persists npm prefix PATH before noninteractive shell guards", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-prefix-shell-")); + const home = join(tmp, "home"); + mkdirSync(home, { recursive: true }); + writeFileSync( + join(home, ".bashrc"), + ["case $- in", " *i*) ;;", " *) return ;;", "esac", ""].join("\n"), + ); + + let result: ReturnType | undefined; + try { + result = runInstallShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + OS=linux + HOME=${JSON.stringify(home)} + PATH=/usr/bin:/bin + prefix=${JSON.stringify(join(tmp, "root-owned-prefix"))} + npm() { + if [[ "$1" == "config" && "$2" == "get" && "$3" == "prefix" ]]; then + printf '%s\\n' "$prefix" + return 0 + fi + if [[ "$1" == "config" && "$2" == "set" && "$3" == "prefix" ]]; then + return 0 + fi + return 1 + } + ui_info() { :; } + ui_warn() { :; } + ui_success() { :; } + fix_npm_permissions + printf 'first=%s\\n' "$(sed -n '1p' "$HOME/.bashrc")" + HOME=${JSON.stringify(home)} PATH=/usr/bin:/bin bash -lc '. "$HOME/.bashrc"; printf "path=%s\\n" "\${PATH%%:*}"' + `); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + + expect(result?.status).toBe(0); + expect(result?.stdout).toContain('first=export PATH="$HOME/.npm-global/bin:$PATH"'); + expect(result?.stdout).toContain(`path=${home}/.npm-global/bin`); + }); + it("uses a quoted absolute openclaw path in follow-up commands when npm bin is not on the original PATH", () => { const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-command-")); const npmBin = join(tmp, "npm bin");