fix(installer): warn about duplicate global installs

This commit is contained in:
Peter Steinberger
2026-04-26 06:49:08 +01:00
parent d5eae0d959
commit db0864ad41
3 changed files with 188 additions and 0 deletions

View File

@@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai
- Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io.
- Installer: load nvm before Node.js detection so `curl | bash` installs respect nvm-managed Node instead of stale system Node. Fixes #49556. Thanks @heavenlxj.
- CLI/Volta: respawn raw `openclaw` CLI runs through the named `node` shim when the current Node executable resolves to `volta-shim`, avoiding direct shim execution failures in non-interactive shells. Fixes #68672. Thanks @sanchezm86.
- Installer: warn when multiple npm global roots contain OpenClaw installs, showing active Node/npm/openclaw plus each install path and version so stale version-manager installs are visible. Fixes #40839. Thanks @zhixianio.
- Docker: copy patched dependency files into runtime images so downstream `pnpm install` layers keep working. Fixes #69224. Thanks @gucasbrg.
- Agents/runtime: submit heartbeat, cron, and exec wakeups as transient runtime context instead of visible user prompts, keeping synthetic system work out of chat transcripts. Fixes #66496 and #66814. Thanks @jeades and @mandomaker.
- Telegram: include native quote excerpts automatically for threaded replies and reply tags when the original Telegram text is available, without adding another config knob. Fixes #6975. Thanks @rex05ai.

View File

@@ -1825,6 +1825,148 @@ npm_global_bin_dir() {
return 1
}
canonicalize_dir() {
local dir="$1"
if [[ -z "$dir" || ! -d "$dir" ]]; then
return 1
fi
(cd "$dir" 2>/dev/null && pwd -P) || return 1
}
openclaw_package_version() {
local package_json="$1"
if [[ ! -f "$package_json" ]]; then
echo "unknown"
return 0
fi
local version=""
if command -v node >/dev/null 2>&1; then
version="$(node -e 'const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(String(pkg.version || "unknown"));' "$package_json" 2>/dev/null || true)"
fi
if [[ -z "$version" ]]; then
version="$(sed -n -E 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$package_json" | head -n1)"
fi
echo "${version:-unknown}"
}
emit_npm_root_candidate() {
local root="${1%/}"
if [[ -n "$root" && "$root" == /* ]]; then
echo "$root"
fi
}
collect_openclaw_npm_root_candidates() {
local root=""
root="$(npm root -g 2>/dev/null || true)"
emit_npm_root_candidate "$root"
local npm_cmd=""
while IFS= read -r npm_cmd; do
[[ -n "$npm_cmd" ]] || continue
root="$("$npm_cmd" root -g 2>/dev/null || true)"
emit_npm_root_candidate "$root"
done < <(type -aP npm 2>/dev/null | awk '!seen[$0]++' || true)
local extra_root=""
local old_ifs="$IFS"
IFS=":"
for extra_root in ${OPENCLAW_INSTALL_EXTRA_NPM_ROOTS:-}; do
emit_npm_root_candidate "$extra_root"
done
IFS="$old_ifs"
emit_npm_root_candidate "/opt/homebrew/lib/node_modules"
emit_npm_root_candidate "/usr/local/lib/node_modules"
emit_npm_root_candidate "/usr/lib/node_modules"
local manager_dir=""
local candidate=""
for manager_dir in "${NVM_DIR:-}" "$HOME/.nvm"; do
[[ -n "$manager_dir" && -d "$manager_dir" ]] || continue
for candidate in "$manager_dir"/versions/node/*/lib/node_modules; do
[[ -d "$candidate" ]] && emit_npm_root_candidate "$candidate"
done
done
for manager_dir in "${FNM_DIR:-}" "$HOME/.fnm" "$HOME/.local/share/fnm"; do
[[ -n "$manager_dir" && -d "$manager_dir" ]] || continue
for candidate in "$manager_dir"/node-versions/*/installation/lib/node_modules; do
[[ -d "$candidate" ]] && emit_npm_root_candidate "$candidate"
done
done
for manager_dir in "${VOLTA_HOME:-}" "$HOME/.volta"; do
[[ -n "$manager_dir" && -d "$manager_dir" ]] || continue
for candidate in "$manager_dir"/tools/image/node/*/lib/node_modules; do
[[ -d "$candidate" ]] && emit_npm_root_candidate "$candidate"
done
done
}
find_openclaw_global_installs() {
local seen="|"
local npm_root=""
while IFS= read -r npm_root; do
[[ -n "$npm_root" ]] || continue
local package_dir="${npm_root%/}/openclaw"
local package_json="${package_dir}/package.json"
[[ -f "$package_json" ]] || continue
local real_package_dir=""
real_package_dir="$(canonicalize_dir "$package_dir" || true)"
[[ -n "$real_package_dir" ]] || real_package_dir="$package_dir"
case "$seen" in
*"|${real_package_dir}|"*) continue ;;
esac
seen="${seen}${real_package_dir}|"
local version=""
version="$(openclaw_package_version "$package_json")"
printf '%s\t%s\t%s\n' "$version" "$real_package_dir" "$npm_root"
done < <(collect_openclaw_npm_root_candidates)
}
warn_duplicate_openclaw_global_installs() {
local installs=()
local line=""
while IFS= read -r line; do
[[ -n "$line" ]] && installs+=("$line")
done < <(find_openclaw_global_installs)
if [[ "${#installs[@]}" -le 1 ]]; then
return 0
fi
ui_warn "Multiple OpenClaw global installs detected"
echo " Different Node/npm environments can run different OpenClaw versions."
local active_node active_npm active_openclaw
active_node="$(command -v node 2>/dev/null || true)"
active_npm="$(command -v npm 2>/dev/null || true)"
active_openclaw="${OPENCLAW_BIN:-}"
if [[ -z "$active_openclaw" ]]; then
active_openclaw="$(type -P openclaw 2>/dev/null || true)"
fi
echo -e " Active node: ${INFO}${active_node:-none}${NC}"
echo -e " Active npm: ${INFO}${active_npm:-none}${NC}"
echo -e " Active openclaw: ${INFO}${active_openclaw:-none}${NC}"
echo ""
echo " Found installs:"
local install version package_dir npm_root
for install in "${installs[@]}"; do
IFS=$'\t' read -r version package_dir npm_root <<< "$install"
echo -e " - ${INFO}${version:-unknown}${NC} ${package_dir}"
echo -e " npm root: ${MUTED}${npm_root}${NC}"
done
echo ""
echo " Keep one install source, then remove stale installs with that environment's npm:"
echo " npm uninstall -g openclaw"
}
refresh_shell_command_cache() {
hash -r 2>/dev/null || true
}
@@ -2435,6 +2577,7 @@ main() {
ui_stage "Finalizing setup"
OPENCLAW_BIN="$(resolve_openclaw_bin || true)"
warn_duplicate_openclaw_global_installs || true
# PATH warning: installs can succeed while the user's login shell still lacks npm's global bin dir.
local npm_bin=""

View File

@@ -240,3 +240,47 @@ describe("install.sh macOS Homebrew Node behavior", () => {
}
});
});
describe("install.sh duplicate OpenClaw install detection", () => {
it("warns with concrete package paths and versions for duplicate npm roots", () => {
const result = runInstallShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
root="$(mktemp -d)"
trap 'rm -rf "$root"' EXIT
mkdir -p "$root/brew/openclaw" "$root/fnm/openclaw"
printf '{"version":"2026.3.7"}\\n' > "$root/brew/openclaw/package.json"
printf '{"version":"2026.3.1"}\\n' > "$root/fnm/openclaw/package.json"
collect_openclaw_npm_root_candidates() { printf '%s\\n' "$root/brew" "$root/fnm"; }
OPENCLAW_BIN="$root/fnm/.bin/openclaw"
ui_warn() { echo "WARN: $*"; }
warn_duplicate_openclaw_global_installs
`);
expect(result.status).toBe(0);
expect(result.stdout).toContain("Multiple OpenClaw global installs detected");
expect(result.stdout).toContain("2026.3.7");
expect(result.stdout).toContain("2026.3.1");
expect(result.stdout).toContain("/brew/openclaw");
expect(result.stdout).toContain("/fnm/openclaw");
expect(result.stdout).toContain("Active openclaw:");
expect(result.stdout).toContain("npm uninstall -g openclaw");
});
it("stays quiet when only one OpenClaw npm root exists", () => {
const result = runInstallShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
root="$(mktemp -d)"
trap 'rm -rf "$root"' EXIT
mkdir -p "$root/only/openclaw"
printf '{"version":"2026.3.7"}\\n' > "$root/only/openclaw/package.json"
collect_openclaw_npm_root_candidates() { printf '%s\\n' "$root/only"; }
ui_warn() { echo "WARN: $*"; }
warn_duplicate_openclaw_global_installs
`);
expect(result.status).toBe(0);
expect(result.stdout).not.toContain("Multiple OpenClaw global installs detected");
});
});