mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:10:49 +00:00
fix(installer): warn about duplicate global installs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user