diff --git a/docs/install/installer.md b/docs/install/installer.md
index 62cdb86f7f8..4f7e5915fa4 100644
--- a/docs/install/installer.md
+++ b/docs/install/installer.md
@@ -85,10 +85,10 @@ Recommended for most interactive installs on macOS/Linux/WSL.
- - Refreshes a loaded gateway service best-effort (`openclaw gateway install --force`, then restart)
- - Runs `openclaw doctor --non-interactive` on upgrades and git installs (best effort)
- - Attempts onboarding when appropriate (TTY available, onboarding not disabled, and bootstrap/config checks pass)
- - Runs a post-install smoke verify when `--verify` is set
+ - Resolves the just-installed `openclaw` binary for follow-up commands
+ - For an unconfigured install, starts onboarding before doctor or gateway probes. With `--no-onboard` or no TTY, it prints the command to finish setup later.
+ - For a configured install, refreshes and restarts a loaded gateway service best-effort and runs doctor. Upgrades update plugins when possible, or print the manual command in a headless prompt-enabled run.
+ - When `--verify` runs, it checks the installed version and checks gateway health only after configuration exists.
diff --git a/scripts/install.sh b/scripts/install.sh
index 84e2930f62d..413049684bc 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -72,6 +72,21 @@ resolve_openclaw_effective_home() {
echo "$openclaw_home"
}
+resolve_openclaw_user_path() {
+ local input="$1"
+ local effective_home
+ effective_home="$(resolve_openclaw_effective_home)"
+ if [[ "$input" == "~" ]]; then
+ echo "$effective_home"
+ elif [[ "$input" == \~/* ]]; then
+ echo "${effective_home}${input:1}"
+ elif [[ "$input" == /* ]]; then
+ echo "$input"
+ else
+ echo "$PWD/$input"
+ fi
+}
+
DOWNLOADER=""
detect_downloader() {
if command -v curl &> /dev/null; then
@@ -2496,7 +2511,7 @@ warn_shell_path_missing_dir() {
echo ""
ui_info "PATH updated in ${rc}: added ${label} (${dir})"
echo " New terminals pick this up automatically."
- echo " For this shell, run: source ${rc}"
+ echo " For this shell, run: source ${rc}; hash -r"
return 0
fi
done
@@ -2515,8 +2530,9 @@ openclaw_command_for_user() {
return 0
fi
- local claw_dir="${claw%/*}"
- if [[ "$claw_dir" != "$claw" ]] && path_has_dir "$ORIGINAL_PATH" "$claw_dir"; then
+ local original_claw=""
+ original_claw="$(PATH="$ORIGINAL_PATH" type -P openclaw 2>/dev/null || true)"
+ if [[ "$original_claw" == "$claw" ]]; then
echo "openclaw"
return 0
fi
@@ -2651,6 +2667,25 @@ resolve_openclaw_bin() {
return 1
}
+resolve_installed_openclaw_bin() {
+ local installed_bin=""
+ if [[ "$INSTALL_METHOD" == "git" ]]; then
+ installed_bin="$HOME/.local/bin/openclaw"
+ elif [[ "$INSTALL_METHOD" == "npm" ]]; then
+ local npm_bin=""
+ npm_bin="$(npm_global_bin_dir || true)"
+ if [[ -n "$npm_bin" ]]; then
+ installed_bin="${npm_bin}/openclaw"
+ fi
+ fi
+
+ if [[ -n "$installed_bin" && -x "$installed_bin" ]]; then
+ echo "$installed_bin"
+ return 0
+ fi
+ resolve_openclaw_bin
+}
+
install_openclaw_from_git() {
local repo_dir="$1"
local repo_url="https://github.com/openclaw/openclaw.git"
@@ -2888,68 +2923,32 @@ maybe_open_dashboard() {
"$claw" dashboard || true
}
-resolve_workspace_dir() {
- local profile="${OPENCLAW_PROFILE:-default}"
+has_openclaw_config() {
local effective_home
effective_home="$(resolve_openclaw_effective_home)"
- if [[ "${profile}" != "default" ]]; then
- echo "${effective_home}/.openclaw/workspace-${profile}"
- else
- echo "${effective_home}/.openclaw/workspace"
- fi
-}
-
-run_bootstrap_onboarding_if_needed() {
- if [[ "${NO_ONBOARD}" == "1" ]]; then
+ if [[ -n "${OPENCLAW_CONFIG_PATH:-}" ]]; then
+ local config_path
+ config_path="$(resolve_openclaw_user_path "$OPENCLAW_CONFIG_PATH")"
+ [[ -f "$config_path" ]]
return
fi
- local effective_home
- effective_home="$(resolve_openclaw_effective_home)"
- local config_path="${OPENCLAW_CONFIG_PATH:-$effective_home/.openclaw/openclaw.json}"
- local legacy_config_path="${HOME}/.openclaw/openclaw.json"
- local legacy_clawdbot_path="${HOME}/.clawdbot/clawdbot.json"
- if [[ -f "${config_path}" || -f "$effective_home/.clawdbot/clawdbot.json" ]]; then
- return
- fi
- if [[ -z "${OPENCLAW_CONFIG_PATH:-}" && "${effective_home}" != "${HOME}" ]]; then
- if [[ -f "$legacy_config_path" || -f "$legacy_clawdbot_path" ]]; then
- return
+ if [[ -n "${OPENCLAW_STATE_DIR:-}" ]]; then
+ local state_dir
+ state_dir="$(resolve_openclaw_user_path "$OPENCLAW_STATE_DIR")"
+ if [[ -f "$state_dir/openclaw.json" || -f "$state_dir/clawdbot.json" ]]; then
+ return 0
fi
+ return 1
fi
- local workspace
- workspace="$(resolve_workspace_dir)"
- local bootstrap="${workspace}/BOOTSTRAP.md"
-
- if [[ ! -f "${bootstrap}" ]]; then
- return
+ if [[ -f "$effective_home/.openclaw/openclaw.json" ||
+ -f "$effective_home/.openclaw/clawdbot.json" ||
+ -f "$effective_home/.clawdbot/openclaw.json" ||
+ -f "$effective_home/.clawdbot/clawdbot.json" ]]; then
+ return 0
fi
-
- if ! is_promptable; then
- local user_claw
- user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
- ui_info "BOOTSTRAP.md found but no TTY; run ${user_claw} onboard to finish setup"
- return
- fi
-
- ui_info "BOOTSTRAP.md found; starting onboarding"
- local claw="${OPENCLAW_BIN:-}"
- if [[ -z "$claw" ]]; then
- claw="$(resolve_openclaw_bin || true)"
- fi
- if [[ -z "$claw" ]]; then
- ui_info "BOOTSTRAP.md found but openclaw not on PATH; skipping onboarding"
- warn_openclaw_not_found
- return
- fi
-
- "$claw" onboard || {
- local user_claw
- user_claw="$(openclaw_command_for_user "$claw")"
- ui_error "Onboarding failed; run ${user_claw} onboard to retry"
- return
- }
+ return 1
}
load_install_version_helpers() {
@@ -3060,7 +3059,9 @@ refresh_gateway_service_if_loaded() {
if run_quiet_step "Restarting gateway service" "$claw" gateway restart; then
ui_success "Gateway service restarted"
else
- ui_warn "Gateway service restart failed; continuing. Run: openclaw gateway restart"
+ local user_claw
+ user_claw="$(openclaw_command_for_user "$claw")"
+ ui_warn "Gateway service restart failed; continuing. Run: ${user_claw} gateway restart"
return 0
fi
@@ -3071,6 +3072,7 @@ verify_installation() {
if [[ "${VERIFY_INSTALL}" != "1" ]]; then
return 0
fi
+ local verify_gateway="${1:-true}"
ui_stage "Verifying installation"
local claw="${OPENCLAW_BIN:-}"
@@ -3085,10 +3087,14 @@ verify_installation() {
run_quiet_step "Checking OpenClaw version" "$claw" --version || return 1
- if is_gateway_daemon_loaded "$claw"; then
+ if [[ "$verify_gateway" != "true" ]]; then
+ ui_info "Setup not complete; skipping gateway service check"
+ elif is_gateway_daemon_loaded "$claw"; then
run_quiet_step "Checking gateway service" "$claw" gateway status --deep || {
+ local user_claw
+ user_claw="$(openclaw_command_for_user "$claw")"
ui_error "Install verify failed: gateway service unhealthy"
- ui_info "Run: openclaw gateway status --deep"
+ ui_info "Run: ${user_claw} gateway status --deep"
return 1
}
else
@@ -3163,7 +3169,6 @@ main() {
is_upgrade=true
fi
local should_open_dashboard=false
- local skip_onboard=false
ui_stage "Preparing environment"
@@ -3218,7 +3223,7 @@ main() {
ui_stage "Finalizing setup"
- OPENCLAW_BIN="$(resolve_openclaw_bin || true)"
+ OPENCLAW_BIN="$(resolve_installed_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.
@@ -3233,21 +3238,11 @@ main() {
fi
fi
- refresh_gateway_service_if_loaded
-
- # Step 6: Run doctor for migrations on upgrades and git installs
- local run_doctor_after=false
- if [[ "$is_upgrade" == "true" || "$INSTALL_METHOD" == "git" ]]; then
- run_doctor_after=true
+ local config_present=false
+ if has_openclaw_config; then
+ config_present=true
+ refresh_gateway_service_if_loaded
fi
- if [[ "$run_doctor_after" == "true" ]]; then
- if run_doctor; then
- should_open_dashboard=true
- fi
- fi
-
- # Step 7: If BOOTSTRAP.md is still present in the workspace, resume onboarding
- run_bootstrap_onboarding_if_needed
local installed_version
installed_version=$(resolve_openclaw_version)
@@ -3304,17 +3299,46 @@ main() {
echo ""
if [[ "$INSTALL_METHOD" == "git" && -n "$final_git_dir" ]]; then
+ local user_claw
+ user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
ui_section "Source install details"
ui_kv "Checkout" "$final_git_dir"
ui_kv "Wrapper" "$HOME/.local/bin/openclaw"
- ui_kv "Update command" "openclaw update"
+ ui_kv "Update command" "${user_claw} update"
ui_kv "Switch to npm" "curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method npm"
+ fi
+
+ if [[ "$config_present" != "true" ]]; then
+ if [[ "$NO_ONBOARD" == "1" ]]; then
+ local user_claw
+ user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
+ ui_info "Skipping onboard (requested); run ${user_claw} onboard later"
+ else
+ ui_info "Starting setup"
+ echo ""
+ if is_promptable; then
+ local claw="${OPENCLAW_BIN:-}"
+ if [[ -z "$claw" ]]; then
+ claw="$(resolve_installed_openclaw_bin || true)"
+ fi
+ if [[ -z "$claw" ]]; then
+ ui_info "Skipping onboarding (openclaw not on PATH yet)"
+ warn_openclaw_not_found
+ return 0
+ fi
+ exec /dev/null; then
+ if [[ "$config_present" == "true" ]]; then
local claw="${OPENCLAW_BIN:-}"
if [[ -z "$claw" ]]; then
- claw="$(resolve_openclaw_bin || true)"
+ claw="$(resolve_installed_openclaw_bin || true)"
fi
if [[ -n "$claw" ]] && is_gateway_daemon_loaded "$claw"; then
+ local user_claw
+ user_claw="$(openclaw_command_for_user "$claw")"
if [[ "$DRY_RUN" == "1" ]]; then
- ui_info "Gateway daemon detected; would restart (openclaw daemon restart)"
+ ui_info "Gateway daemon detected; would restart (${user_claw} daemon restart)"
else
ui_info "Gateway daemon detected; restarting"
if OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" daemon restart >/dev/null 2>&1; then
ui_success "Gateway restarted"
else
- ui_warn "Gateway restart failed; try: openclaw daemon restart"
+ ui_warn "Gateway restart failed; try: ${user_claw} daemon restart"
fi
fi
fi
fi
- if ! verify_installation; then
+ if ! verify_installation "$config_present"; then
exit 1
fi
diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts
index 8d5205a1771..afe2d4f2047 100644
--- a/test/scripts/install-sh.test.ts
+++ b/test/scripts/install-sh.test.ts
@@ -718,7 +718,7 @@ NODE
}
});
- it("uses OPENCLAW_HOME for git and onboarding defaults", () => {
+ it("uses OPENCLAW_HOME for git defaults", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-home-"));
const osHome = join(tmp, "os-home");
const openclawHome = join(tmp, "openclaw-home");
@@ -731,9 +731,7 @@ NODE
[
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
- 'printf "git=%s\\nworkspace=%s\\n" "$GIT_DIR" "$(resolve_workspace_dir)"',
- "OPENCLAW_PROFILE=work",
- 'printf "workspaceProfile=%s\\n" "$(resolve_workspace_dir)"',
+ 'printf "git=%s\\n" "$GIT_DIR"',
].join("\n"),
{
HOME: osHome,
@@ -749,10 +747,6 @@ NODE
expect(result?.status).toBe(0);
const output = result?.stdout ?? "";
expect(output).toContain(`git=${join(openclawHome, "openclaw")}`);
- expect(output).toContain(`workspace=${join(openclawHome, ".openclaw", "workspace")}`);
- expect(output).toContain(
- `workspaceProfile=${join(openclawHome, ".openclaw", "workspace-work")}`,
- );
const mkdirParentIndex = script.indexOf('mkdir -p "$(dirname "$repo_dir")"');
const cloneIndex = script.indexOf(
'run_quiet_step "Cloning OpenClaw" git clone "$repo_url" "$repo_dir"',
@@ -762,16 +756,14 @@ NODE
expect(mkdirParentIndex).toBeLessThan(cloneIndex);
});
- it("skips bootstrap onboarding when legacy HOME config exists with OPENCLAW_HOME", () => {
+ it("does not treat OS HOME config as active when OPENCLAW_HOME is set", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-legacy-config-"));
const osHome = join(tmp, "os-home");
const openclawHome = join(tmp, "openclaw-home");
const legacyConfigDir = join(osHome, ".openclaw");
- const bootstrapDir = join(openclawHome, ".openclaw", "workspace");
mkdirSync(legacyConfigDir, { recursive: true });
- mkdirSync(bootstrapDir, { recursive: true });
+ mkdirSync(openclawHome, { recursive: true });
writeFileSync(join(legacyConfigDir, "openclaw.json"), "{}\n");
- writeFileSync(join(bootstrapDir, "BOOTSTRAP.md"), "# bootstrap\n");
let result: ReturnType | undefined;
try {
@@ -779,8 +771,7 @@ NODE
[
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
- "NO_ONBOARD=0",
- "run_bootstrap_onboarding_if_needed",
+ 'if has_openclaw_config; then printf "configured=1\\n"; else printf "configured=0\\n"; fi',
].join("\n"),
{
HOME: osHome,
@@ -794,10 +785,244 @@ NODE
}
expect(result?.status).toBe(0);
- expect(result?.stdout ?? "").not.toContain("BOOTSTRAP.md found");
+ expect(result?.stdout).toContain("configured=0");
expect(result?.stderr ?? "").toBe("");
});
+ it.each(["openclaw.json", "clawdbot.json"])(
+ "detects %s under OPENCLAW_STATE_DIR",
+ (configName) => {
+ const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-state-config-"));
+ const stateDir = join(tmp, "state");
+ mkdirSync(stateDir, { recursive: true });
+ writeFileSync(join(stateDir, configName), "{}\n");
+
+ let result: ReturnType | undefined;
+ try {
+ result = runInstallShell(
+ [
+ `cd ${JSON.stringify(process.cwd())}`,
+ `source ${JSON.stringify(SCRIPT_PATH)}`,
+ 'if has_openclaw_config; then printf "configured=1\\n"; else printf "configured=0\\n"; fi',
+ ].join("\n"),
+ {
+ OPENCLAW_CONFIG_PATH: undefined,
+ OPENCLAW_STATE_DIR: stateDir,
+ TERM: "dumb",
+ },
+ );
+ } finally {
+ rmSync(tmp, { force: true, recursive: true });
+ }
+
+ expect(result?.status).toBe(0);
+ expect(result?.stdout).toContain("configured=1");
+ expect(result?.stderr ?? "").toBe("");
+ },
+ );
+
+ it("does not fall back to home config when OPENCLAW_STATE_DIR is set", () => {
+ const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-state-override-"));
+ const home = join(tmp, "home");
+ const stateDir = join(tmp, "state");
+ mkdirSync(join(home, ".openclaw"), { recursive: true });
+ mkdirSync(stateDir, { recursive: true });
+ writeFileSync(join(home, ".openclaw", "openclaw.json"), "{}\n");
+
+ let result: ReturnType | undefined;
+ try {
+ result = runInstallShell(
+ [
+ `cd ${JSON.stringify(process.cwd())}`,
+ `source ${JSON.stringify(SCRIPT_PATH)}`,
+ 'if has_openclaw_config; then printf "configured=1\\n"; else printf "configured=0\\n"; fi',
+ ].join("\n"),
+ {
+ HOME: home,
+ OPENCLAW_CONFIG_PATH: undefined,
+ OPENCLAW_HOME: undefined,
+ OPENCLAW_STATE_DIR: stateDir,
+ TERM: "dumb",
+ },
+ );
+ } finally {
+ rmSync(tmp, { force: true, recursive: true });
+ }
+
+ expect(result?.status).toBe(0);
+ expect(result?.stdout).toContain("configured=0");
+ expect(result?.stderr ?? "").toBe("");
+ });
+
+ it.each([
+ {
+ expected: /No TTY; run .*\/\.local\/bin\/openclaw onboard to finish setup/,
+ name: "starts setup",
+ noOnboard: 0,
+ },
+ {
+ expected: /Skipping onboard .*run .*\/\.local\/bin\/openclaw onboard later/,
+ name: "honors --no-onboard",
+ noOnboard: 1,
+ },
+ ])(
+ "$name for an unconfigured git install replacing an existing binary",
+ ({ expected, noOnboard }) => {
+ const result = runInstallShell(`
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ INSTALL_METHOD=git
+ GIT_DIR="$HOME/openclaw"
+ NO_ONBOARD=${noOnboard}
+ NO_PROMPT=1
+ VERIFY_INSTALL=1
+ OS=linux
+
+ bootstrap_gum_temp() { :; }
+ print_installer_banner() { :; }
+ print_gum_status() { :; }
+ detect_os_or_die() { OS=linux; }
+ detect_openclaw_checkout() { return 1; }
+ show_install_plan() { :; }
+ check_existing_openclaw() { return 0; }
+ load_nvm_for_node_detection() { :; }
+ check_node() { return 0; }
+ activate_supported_node_on_path() { :; }
+ ensure_default_node_active_shell() { return 0; }
+ npm() { return 1; }
+ install_openclaw_from_git() {
+ mkdir -p "$HOME/.local/bin"
+ printf '#!/bin/sh\\nexit 0\\n' > "$HOME/.local/bin/openclaw"
+ chmod +x "$HOME/.local/bin/openclaw"
+ export PATH="$HOME/.local/bin:$PATH"
+ }
+ resolve_openclaw_bin() { printf '%s\\n' "$HOME/.local/bin/openclaw"; }
+ warn_duplicate_openclaw_global_installs() { :; }
+ npm_global_bin_dir() { :; }
+ warn_shell_path_missing_dir() { :; }
+ refresh_gateway_service_if_loaded() { printf 'gateway-refresh-called\\n'; }
+ run_doctor() {
+ printf 'doctor-called\\n'
+ return 0
+ }
+ resolve_openclaw_version() { printf 'test-version\\n'; }
+ is_gateway_daemon_loaded() {
+ printf 'gateway-probe-called\\n'
+ return 1
+ }
+ maybe_open_dashboard() { :; }
+ show_footer_links() { :; }
+
+ main
+ `);
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).not.toContain("doctor-called");
+ expect(result.stdout).not.toContain("gateway-refresh-called");
+ expect(result.stdout).not.toContain("gateway-probe-called");
+ expect(result.stdout).toMatch(/Update command:.*\/\.local\/bin\/openclaw update/);
+ expect(result.stdout).toMatch(expected);
+ },
+ );
+
+ it("honors --verify for an unconfigured install without a TTY", () => {
+ const result = runInstallShell(`
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ INSTALL_METHOD=git
+ GIT_DIR="$HOME/openclaw"
+ NO_ONBOARD=0
+ NO_PROMPT=1
+ VERIFY_INSTALL=1
+ OS=linux
+
+ bootstrap_gum_temp() { :; }
+ print_installer_banner() { :; }
+ print_gum_status() { :; }
+ detect_os_or_die() { OS=linux; }
+ detect_openclaw_checkout() { return 1; }
+ show_install_plan() { :; }
+ check_existing_openclaw() { return 0; }
+ load_nvm_for_node_detection() { :; }
+ check_node() { return 0; }
+ activate_supported_node_on_path() { :; }
+ ensure_default_node_active_shell() { return 0; }
+ npm() { return 1; }
+ install_openclaw_from_git() {
+ mkdir -p "$HOME/.local/bin"
+ printf '#!/bin/sh\\nexit 1\\n' > "$HOME/.local/bin/openclaw"
+ chmod +x "$HOME/.local/bin/openclaw"
+ export PATH="$HOME/.local/bin:$PATH"
+ }
+ resolve_openclaw_bin() { printf '%s\\n' "$HOME/.local/bin/openclaw"; }
+ warn_duplicate_openclaw_global_installs() { :; }
+ npm_global_bin_dir() { :; }
+ warn_shell_path_missing_dir() { :; }
+ refresh_gateway_service_if_loaded() { :; }
+ resolve_openclaw_version() { printf 'test-version\\n'; }
+ maybe_open_dashboard() { :; }
+ show_footer_links() { :; }
+
+ main
+ `);
+
+ expect(result.status).toBe(1);
+ expect(result.stdout).toMatch(/No TTY; run .*\/\.local\/bin\/openclaw onboard to finish setup/);
+ });
+
+ it("runs migration doctor for a configured upgrade without a TTY", () => {
+ const result = runInstallShell(`
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ INSTALL_METHOD=npm
+ NO_ONBOARD=0
+ NO_PROMPT=0
+ OS=linux
+ mkdir -p "$HOME/.openclaw"
+ printf '{}\\n' > "$HOME/.openclaw/openclaw.json"
+
+ bootstrap_gum_temp() { :; }
+ print_installer_banner() { :; }
+ print_gum_status() { :; }
+ detect_os_or_die() { OS=linux; }
+ detect_openclaw_checkout() { return 1; }
+ show_install_plan() { :; }
+ check_existing_openclaw() { return 0; }
+ load_nvm_for_node_detection() { :; }
+ check_node() { return 0; }
+ activate_supported_node_on_path() { :; }
+ ensure_default_node_active_shell() { return 0; }
+ check_git() { return 0; }
+ fix_npm_permissions() { :; }
+ install_openclaw() {
+ mkdir -p "$HOME/.local/bin"
+ printf '#!/bin/sh\\nexit 0\\n' > "$HOME/.local/bin/openclaw"
+ chmod +x "$HOME/.local/bin/openclaw"
+ export PATH="$HOME/.local/bin:$PATH"
+ }
+ resolve_openclaw_bin() { printf '%s\\n' "$HOME/.local/bin/openclaw"; }
+ warn_duplicate_openclaw_global_installs() { :; }
+ npm_global_bin_dir() { :; }
+ warn_shell_path_missing_dir() { :; }
+ refresh_gateway_service_if_loaded() { :; }
+ run_doctor() {
+ printf 'doctor-called\\n'
+ return 0
+ }
+ resolve_openclaw_version() { printf 'test-version\\n'; }
+ is_gateway_daemon_loaded() { return 1; }
+ verify_installation() { return 0; }
+ maybe_open_dashboard() { printf 'dashboard-called\\n'; }
+ show_footer_links() { :; }
+
+ main
+ `);
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("doctor-called");
+ expect(result.stdout).toContain("dashboard-called");
+ });
+
it("rejects OpenClaw GitHub source targets for npm installs", () => {
const result = runInstallShell(`
set -euo pipefail
@@ -1357,12 +1582,17 @@ NODE
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");
+ const staleBin = join(tmp, "stale-bin");
const visibleBin = join(tmp, "visible-bin");
mkdirSync(npmBin, { recursive: true });
+ mkdirSync(staleBin, { recursive: true });
mkdirSync(visibleBin, { recursive: true });
const openclawBin = join(npmBin, "openclaw");
+ const staleOpenclawBin = join(staleBin, "openclaw");
writeFileSync(openclawBin, "#!/bin/sh\nexit 0\n");
+ writeFileSync(staleOpenclawBin, "#!/bin/sh\nexit 0\n");
chmodSync(openclawBin, 0o755);
+ chmodSync(staleOpenclawBin, 0o755);
let result: ReturnType | undefined;
try {
@@ -1373,6 +1603,8 @@ NODE
printf 'missing=%s\\n' "$(openclaw_command_for_user "${openclawBin}")"
ORIGINAL_PATH=${JSON.stringify(`${npmBin}:${visibleBin}:/usr/bin:/bin`)}
printf 'present=%s\\n' "$(openclaw_command_for_user "${openclawBin}")"
+ ORIGINAL_PATH=${JSON.stringify(`${staleBin}:${npmBin}:/usr/bin:/bin`)}
+ printf 'shadowed=%s\\n' "$(openclaw_command_for_user "${openclawBin}")"
`);
} finally {
rmSync(tmp, { recursive: true, force: true });
@@ -1381,6 +1613,105 @@ NODE
expect(result?.status).toBe(0);
expect(result?.stdout).toContain(`missing=${openclawBin.replace(/ /g, "\\ ")}`);
expect(result?.stdout).toContain("present=openclaw");
+ expect(result?.stdout).toContain(`shadowed=${openclawBin.replace(/ /g, "\\ ")}`);
+ });
+
+ it("prefers the binary owned by the completed install method over stale PATH entries", () => {
+ const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-selected-bin-"));
+ const home = join(tmp, "home");
+ const npmBin = join(tmp, "npm-bin");
+ const staleBin = join(tmp, "stale-bin");
+ const gitBin = join(home, ".local", "bin");
+ mkdirSync(npmBin, { recursive: true });
+ mkdirSync(staleBin, { recursive: true });
+ mkdirSync(gitBin, { recursive: true });
+ for (const bin of [
+ join(npmBin, "openclaw"),
+ join(staleBin, "openclaw"),
+ join(gitBin, "openclaw"),
+ ]) {
+ writeFileSync(bin, "#!/bin/sh\nexit 0\n");
+ chmodSync(bin, 0o755);
+ }
+
+ let result: ReturnType | undefined;
+ try {
+ result = runInstallShell(
+ `
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ INSTALL_METHOD=git
+ printf 'git=%s\\n' "$(resolve_installed_openclaw_bin)"
+ INSTALL_METHOD=npm
+ npm_global_bin_dir() { printf '%s\\n' "${npmBin}"; }
+ printf 'npm=%s\\n' "$(resolve_installed_openclaw_bin)"
+ `,
+ {
+ HOME: home,
+ PATH: `${staleBin}:${process.env.PATH ?? ""}`,
+ },
+ );
+ } finally {
+ rmSync(tmp, { recursive: true, force: true });
+ }
+
+ expect(result?.status).toBe(0);
+ expect(result?.stdout).toContain(`git=${join(gitBin, "openclaw")}`);
+ expect(result?.stdout).toContain(`npm=${join(npmBin, "openclaw")}`);
+ });
+
+ it("uses the selected binary in gateway recovery guidance", () => {
+ const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-gateway-guidance-"));
+ const currentBin = join(tmp, "current bin");
+ const staleBin = join(tmp, "stale-bin");
+ mkdirSync(currentBin, { recursive: true });
+ mkdirSync(staleBin, { recursive: true });
+ const openclawBin = join(currentBin, "openclaw");
+ writeFileSync(openclawBin, "#!/bin/sh\nexit 0\n");
+ writeFileSync(join(staleBin, "openclaw"), "#!/bin/sh\nexit 0\n");
+ chmodSync(openclawBin, 0o755);
+ chmodSync(join(staleBin, "openclaw"), 0o755);
+
+ let result: ReturnType | undefined;
+ try {
+ result = runInstallShell(`
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ OPENCLAW_BIN=${JSON.stringify(openclawBin)}
+ ORIGINAL_PATH=${JSON.stringify(`${staleBin}:${currentBin}:/usr/bin:/bin`)}
+ VERIFY_INSTALL=1
+ is_gateway_daemon_loaded() { return 0; }
+ run_quiet_step() {
+ case "$1" in
+ "Restarting gateway service"|"Checking gateway service") return 1 ;;
+ *) return 0 ;;
+ esac
+ }
+ refresh_gateway_service_if_loaded
+ verify_installation true || true
+ `);
+ } finally {
+ rmSync(tmp, { recursive: true, force: true });
+ }
+
+ const quotedBin = openclawBin.replace(/ /g, "\\ ");
+ expect(result?.status).toBe(0);
+ expect(result?.stdout).toContain(`Run: ${quotedBin} gateway restart`);
+ expect(result?.stdout).toContain(`Run: ${quotedBin} gateway status --deep`);
+ });
+
+ it("refreshes the shell command cache after loading a persisted PATH update", () => {
+ const result = runInstallShell(`
+ set -euo pipefail
+ source "${SCRIPT_PATH}"
+ printf 'export PATH="$HOME/.local/bin:$PATH"\\n' > "$HOME/.bashrc"
+ ORIGINAL_PATH="/usr/bin:/bin"
+ warn_shell_path_missing_dir "$HOME/.local/bin" "user-local bin dir"
+ `);
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("For this shell, run: source ");
+ expect(result.stdout).toContain("; hash -r");
});
it("resolves requested git install versions to checkout refs", () => {
@@ -1703,10 +2034,7 @@ describe("install.sh doctor cancellation and dashboard guard", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
it("guards every run_doctor caller against failure", () => {
- // Both run_doctor call sites must guard the return value so a
- // failed or cancelled doctor does not launch the dashboard.
- // The upgrade path uses: if run_doctor; then should_open_dashboard=true; fi
- // The existing-config path must also guard: if run_doctor; then ...
+ // A failed or cancelled doctor must not launch the dashboard.
expect(script).toContain("if run_doctor; then");
// Ensure there is no bare "run_doctor" call followed by
// "should_open_dashboard=true" without an if-guard