mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
fix(installer): complete first-run onboarding (#101901)
* fix(installer): complete first-run onboarding * docs(installer): document finalization paths * fix(installer): honor verify without tty * fix(installer): align config finalization
This commit is contained in:
parent
23cf16a7d5
commit
2fbd4cdcba
3 changed files with 468 additions and 141 deletions
|
|
@ -85,10 +85,10 @@ Recommended for most interactive installs on macOS/Linux/WSL.
|
|||
|
||||
</Step>
|
||||
<Step title="Post-install tasks">
|
||||
- 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.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
|
|
|||
|
|
@ -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/tty
|
||||
exec "$claw" onboard
|
||||
fi
|
||||
local user_claw
|
||||
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
|
||||
ui_info "No TTY; run ${user_claw} onboard to finish setup"
|
||||
fi
|
||||
elif [[ "$is_upgrade" == "true" ]]; then
|
||||
ui_info "Upgrade complete"
|
||||
if has_controlling_tty || [[ "$NO_ONBOARD" == "1" || "$NO_PROMPT" == "1" ]]; then
|
||||
local claw="${OPENCLAW_BIN:-}"
|
||||
if [[ -z "$claw" ]]; then
|
||||
claw="$(resolve_openclaw_bin || true)"
|
||||
claw="$(resolve_installed_openclaw_bin || true)"
|
||||
fi
|
||||
if [[ -z "$claw" ]]; then
|
||||
ui_info "Skipping doctor (openclaw not on PATH yet)"
|
||||
|
|
@ -3345,75 +3369,50 @@ main() {
|
|||
doctor_ok=1
|
||||
fi
|
||||
if (( doctor_ok )); then
|
||||
should_open_dashboard=true
|
||||
ui_info "Updating plugins"
|
||||
OPENCLAW_UPDATE_IN_PROGRESS=1 "$claw" plugins update --all || true
|
||||
else
|
||||
ui_warn "Doctor failed; skipping plugin updates"
|
||||
fi
|
||||
else
|
||||
if run_doctor; then
|
||||
should_open_dashboard=true
|
||||
fi
|
||||
local user_claw
|
||||
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
|
||||
ui_info "No TTY; run ${user_claw} doctor and ${user_claw} plugins update --all manually"
|
||||
ui_info "No TTY; run ${user_claw} plugins update --all manually"
|
||||
fi
|
||||
else
|
||||
if [[ "$NO_ONBOARD" == "1" || "$skip_onboard" == "true" ]]; then
|
||||
local user_claw
|
||||
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
|
||||
ui_info "Skipping onboard (requested); run ${user_claw} onboard later"
|
||||
else
|
||||
local effective_home
|
||||
effective_home="$(resolve_openclaw_effective_home)"
|
||||
local config_path="${OPENCLAW_CONFIG_PATH:-$effective_home/.openclaw/openclaw.json}"
|
||||
if [[ -f "${config_path}" || -f "$effective_home/.clawdbot/clawdbot.json" ]]; then
|
||||
ui_info "Config already present; running doctor"
|
||||
if run_doctor; then
|
||||
should_open_dashboard=true
|
||||
fi
|
||||
ui_info "Config already present; skipping onboarding"
|
||||
skip_onboard=true
|
||||
fi
|
||||
ui_info "Starting setup"
|
||||
echo ""
|
||||
if is_promptable; then
|
||||
local claw="${OPENCLAW_BIN:-}"
|
||||
if [[ -z "$claw" ]]; then
|
||||
claw="$(resolve_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/tty
|
||||
exec "$claw" onboard
|
||||
fi
|
||||
local user_claw
|
||||
user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")"
|
||||
ui_info "No TTY; run ${user_claw} onboard to finish setup"
|
||||
return 0
|
||||
ui_info "Config already present; running doctor"
|
||||
if run_doctor; then
|
||||
should_open_dashboard=true
|
||||
fi
|
||||
ui_info "Config already present; skipping onboarding"
|
||||
fi
|
||||
|
||||
if command -v openclaw &> /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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof runInstallShell> | 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<typeof runInstallShell> | 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<typeof runInstallShell> | 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<typeof runInstallShell> | 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<typeof runInstallShell> | 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<typeof runInstallShell> | 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue