fix(installer): persist Linux supported PATH

This commit is contained in:
Peter Steinberger
2026-05-10 05:08:46 +01:00
parent aac9ebd4f3
commit 207bcd6b20
2 changed files with 139 additions and 2 deletions

View File

@@ -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

View File

@@ -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<typeof runInstallShell> | 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<typeof runInstallShell> | 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");