Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
167 changes: 167 additions & 0 deletions public/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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" <<EOF
#!/usr/bin/env bash
set -euo pipefail
exec "$node_bin" "$entry_path" "\$@"
EOF
chmod +x "$shim_path"
refresh_shell_command_cache
ui_warn "Configured openclaw shim at ${shim_path} for Node $("$node_bin" -v 2>/dev/null || echo '22+')"
return 0
}

# Install Node.js
install_node() {
if [[ "$OS" == "macos" ]]; then
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -2183,6 +2349,7 @@ main() {

# Step 5: OpenClaw
install_openclaw
install_openclaw_compat_shim || true
fi

ui_stage "Finalizing setup"
Expand Down
63 changes: 63 additions & 0 deletions scripts/test-install-sh-unit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"