diff --git a/CHANGELOG.md b/CHANGELOG.md index bf73ffc..2c9a8bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Blog: restore the missing Discord link in the VirusTotal partnership post footer (#96, thanks @gandli). - Dependencies: bump `@lucide/astro` to `0.577.0` and sync `bun.lock` (#99, thanks @dependabot). - CI: update Bun setup pin and move the install-smoke Node setup to the pinned `actions/setup-node` v6 SHA (#98, thanks @dependabot). +- Installer: recover from older PATH-bound Node runtimes after install, but keep the fallback `openclaw` shim in `~/.local/bin` instead of mutating version-manager bins (#68, thanks @rolandkakonyi). ## 2026-02-22 - Installer: make gum behavior fully automatic (interactive TTYs get gum, headless shells get plain status), and remove manual gum toggles. diff --git a/public/install.sh b/public/install.sh index 47429db..c10911a 100755 --- a/public/install.sh +++ b/public/install.sh @@ -959,6 +959,7 @@ NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}" NPM_SILENT_FLAG="--silent" VERBOSE="${OPENCLAW_VERBOSE:-0}" OPENCLAW_BIN="" +SELECTED_NODE_BIN="" PNPM_CMD=() HELP=0 @@ -1301,6 +1302,163 @@ check_node() { fi } +node_major_from_binary() { + local node_bin="$1" + if [[ -z "$node_bin" || ! -x "$node_bin" ]]; then + return 1 + fi + "$node_bin" -p 'process.versions.node.split(".")[0]' 2>/dev/null || true +} + +node_is_supported_binary() { + local node_bin="$1" + local major="" + major="$(node_major_from_binary "$node_bin")" + if [[ ! "$major" =~ ^[0-9]+$ ]]; then + return 1 + fi + [[ "$major" -ge 22 ]] +} + +has_supported_node() { + local node_bin="" + node_bin="$(command -v node 2>/dev/null || true)" + if [[ -z "$node_bin" ]]; then + return 1 + fi + node_is_supported_binary "$node_bin" +} + +prepend_path_dir() { + local dir="${1%/}" + if [[ -z "$dir" || ! -d "$dir" ]]; then + return 1 + fi + local current=":${PATH:-}:" + current="${current//:${dir}:/:}" + current="${current#:}" + current="${current%:}" + if [[ -n "$current" ]]; then + export PATH="${dir}:${current}" + else + export PATH="${dir}" + fi + hash -r 2>/dev/null || true +} + +ensure_supported_node_on_path() { + if has_supported_node; then + SELECTED_NODE_BIN="$(command -v node 2>/dev/null || true)" + return 0 + fi + + local -a candidates=() + local candidate="" + while IFS= read -r candidate; do + [[ -n "$candidate" ]] && candidates+=("$candidate") + done < <(type -aP node 2>/dev/null || true) + candidates+=( + "/usr/bin/node" + "/usr/local/bin/node" + "/opt/homebrew/bin/node" + "/opt/homebrew/opt/node@22/bin/node" + "/usr/local/opt/node@22/bin/node" + ) + + local seen=":" + for candidate in "${candidates[@]}"; do + if [[ -z "$candidate" || ! -x "$candidate" ]]; then + continue + fi + case "$seen" in + *":$candidate:"*) continue ;; + esac + seen="${seen}${candidate}:" + + if node_is_supported_binary "$candidate"; then + prepend_path_dir "$(dirname "$candidate")" || continue + SELECTED_NODE_BIN="$candidate" + ui_info "Using Node.js runtime at ${candidate}" + return 0 + fi + done + + return 1 +} + +original_path_node_bin() { + if [[ -z "${ORIGINAL_PATH:-}" ]]; then + return 1 + fi + PATH="$ORIGINAL_PATH" command -v node 2>/dev/null || true +} + +original_path_has_supported_node() { + local node_bin="" + node_bin="$(original_path_node_bin)" + if [[ -z "$node_bin" ]]; then + return 1 + fi + node_is_supported_binary "$node_bin" +} + +find_openclaw_entry_path() { + local npm_root="" + npm_root="$(npm root -g 2>/dev/null || true)" + if [[ -z "$npm_root" ]]; then + return 1 + fi + local entry_js="${npm_root}/openclaw/dist/entry.js" + if [[ -f "$entry_js" ]]; then + echo "$entry_js" + return 0 + fi + local entry_mjs="${npm_root}/openclaw/dist/entry.mjs" + if [[ -f "$entry_mjs" ]]; then + echo "$entry_mjs" + return 0 + fi + return 1 +} + +install_openclaw_compat_shim() { + if [[ "$INSTALL_METHOD" != "npm" ]]; then + return 0 + fi + if original_path_has_supported_node; then + return 0 + fi + + local node_bin="${SELECTED_NODE_BIN:-}" + if [[ -z "$node_bin" ]]; then + node_bin="$(command -v node 2>/dev/null || true)" + fi + if [[ -z "$node_bin" || ! -x "$node_bin" ]] || ! node_is_supported_binary "$node_bin"; then + return 1 + fi + + local entry_path="" + entry_path="$(find_openclaw_entry_path || true)" + if [[ -z "$entry_path" ]]; then + return 1 + fi + + local target_dir="$HOME/.local/bin" + ensure_user_local_bin_on_path + + mkdir -p "$target_dir" + local shim_path="${target_dir}/openclaw" + cat > "$shim_path" </dev/null || echo '22+')" + return 0 +} + # Install Node.js install_node() { if [[ "$OS" == "macos" ]]; then @@ -2147,6 +2305,14 @@ main() { if ! check_node; then install_node fi + ensure_supported_node_on_path || true + if ! has_supported_node; then + ui_error "Node.js v22+ is required but could not be activated on PATH" + echo "Detected node: $(command -v node 2>/dev/null || echo '(not found)')" + echo "Current version: $(node -v 2>/dev/null || echo 'unknown')" + echo "Install Node.js 22+ manually: https://nodejs.org" + exit 1 + fi ui_stage "Installing OpenClaw" @@ -2183,6 +2349,7 @@ main() { # Step 5: OpenClaw install_openclaw + install_openclaw_compat_shim || true fi ui_stage "Finalizing setup" diff --git a/scripts/test-install-sh-unit.sh b/scripts/test-install-sh-unit.sh index 878bbbc..84354e1 100644 --- a/scripts/test-install-sh-unit.sh +++ b/scripts/test-install-sh-unit.sh @@ -510,4 +510,67 @@ echo "==> case: install_openclaw_from_git (deps step uses run_pnpm function)" assert_eq "$deps_cmd" "run_pnpm" "install_openclaw_from_git dependencies command" ) +echo "==> case: install_openclaw_compat_shim (always uses user-local bin)" +( + root="${TMP_DIR}/case-openclaw-compat-shim" + home_dir="${root}/home" + selected_bin_dir="${root}/node22/bin" + original_bin_dir="${root}/nvm/bin" + pkg_dir="${root}/npm-root/openclaw/dist" + + mkdir -p "${home_dir}" "${selected_bin_dir}" "${original_bin_dir}" "${pkg_dir}" + : > "${pkg_dir}/entry.js" + + cat >"${selected_bin_dir}/node" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "-p" ]]; then + echo "22" + exit 0 +fi +if [[ "${1:-}" == "-v" ]]; then + echo "v22.12.0" + exit 0 +fi +exit 0 +EOF + chmod +x "${selected_bin_dir}/node" + + cat >"${original_bin_dir}/node" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "-p" ]]; then + echo "20" + exit 0 +fi +if [[ "${1:-}" == "-v" ]]; then + echo "v20.18.0" + exit 0 +fi +exit 0 +EOF + chmod +x "${original_bin_dir}/node" + + export HOME="${home_dir}" + export PATH="/usr/bin:/bin" + export INSTALL_METHOD="npm" + export SELECTED_NODE_BIN="${selected_bin_dir}/node" + export ORIGINAL_PATH="${original_bin_dir}:/usr/bin:/bin" + + ui_warn() { :; } + ensure_user_local_bin_on_path() { + mkdir -p "${HOME}/.local/bin" + export PATH="${HOME}/.local/bin:${PATH}" + } + refresh_shell_command_cache() { hash -r 2>/dev/null || true; } + find_openclaw_entry_path() { + echo "${pkg_dir}/entry.js" + } + + install_openclaw_compat_shim + + got="$(command -v openclaw || true)" + assert_eq "$got" "${home_dir}/.local/bin/openclaw" "install_openclaw_compat_shim wrapper path" +) + echo "OK"