diff --git a/CHANGELOG.md b/CHANGELOG.md index 75512053c82d..6539703cb8fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ Docs: https://docs.openclaw.ai - Gateway: omit internal stream-error placeholder entries from agent prompt history so failed assistant turns are not replayed as model-authored text. (#85652) Thanks @anyech. - Sessions: enforce the session write-lock max-hold policy during lock acquisition so long-held locks can be reclaimed before the stale-lock window. (#85764) Thanks @njuboy11. - Sessions/status: preserve user-facing model, fallback, usage, and cost attribution when internal subagent handoff runs use fallback models. (#85726, fixes #85082) Thanks @brokemac79. +- Install/update: honor `OPENCLAW_HOME` when deriving default dev checkout and installer onboarding paths, while keeping explicit `OPENCLAW_GIT_DIR` and `OPENCLAW_CONFIG_PATH` overrides authoritative. Fixes #54014. Thanks @robertPiro. - Models: prune retired Groq, GitHub Copilot, OpenAI, xAI, and old Claude catalog entries, with doctor migration to upgrade existing configs to current provider refs. - Doctor/update: recognize junction-backed source checkouts as git installs by comparing canonical paths before showing package-manager update guidance. Fixes #82215. Thanks @igormf. - Channels: honor `/verbose on` for tool/progress summaries across direct chats, groups, channels, and forum topics while preserving quiet default behavior. (#85488) Thanks @kurplunkin. diff --git a/docs/cli/update.md b/docs/cli/update.md index 8a8c4b7f0f66..4783cd883c21 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -91,7 +91,8 @@ Options: When you switch channels explicitly (`--channel ...`), OpenClaw also keeps the install method aligned: -- `dev` → ensures a git checkout (default: `~/openclaw`, override with `OPENCLAW_GIT_DIR`), +- `dev` → ensures a git checkout (default: `~/openclaw`, or `$OPENCLAW_HOME/openclaw` when + `OPENCLAW_HOME` is set; override with `OPENCLAW_GIT_DIR`), updates it, and installs the global CLI from that checkout. - `stable` → installs from npm using `latest`. - `beta` → prefers npm dist-tag `beta`, but falls back to `latest` when beta is diff --git a/docs/help/environment.md b/docs/help/environment.md index 50dcd56e3cec..2158f4e7d6b0 100644 --- a/docs/help/environment.md +++ b/docs/help/environment.md @@ -138,12 +138,12 @@ shorthand values. ## Path-related env vars -| Variable | Purpose | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENCLAW_HOME` | Override the home directory used for all internal path resolution (`~/.openclaw/`, agent dirs, sessions, credentials). Useful when running OpenClaw as a dedicated service user. | -| `OPENCLAW_STATE_DIR` | Override the state directory (default `~/.openclaw`). | -| `OPENCLAW_CONFIG_PATH` | Override the config file path (default `~/.openclaw/openclaw.json`). | -| `OPENCLAW_INCLUDE_ROOTS` | Path-list of directories where `$include` directives may resolve files outside the config directory (default: none — `$include` is confined to the config dir). Tilde-expanded. | +| Variable | Purpose | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENCLAW_HOME` | Override the home directory used for internal OpenClaw path defaults (`~/.openclaw/`, agent dirs, sessions, credentials, installer onboarding, and the default dev checkout). Useful when running OpenClaw as a dedicated service user. | +| `OPENCLAW_STATE_DIR` | Override the state directory (default `~/.openclaw`). | +| `OPENCLAW_CONFIG_PATH` | Override the config file path (default `~/.openclaw/openclaw.json`). | +| `OPENCLAW_INCLUDE_ROOTS` | Path-list of directories where `$include` directives may resolve files outside the config directory (default: none — `$include` is confined to the config dir). Tilde-expanded. | ## Logging @@ -157,7 +157,7 @@ shorthand values. ### `OPENCLAW_HOME` -When set, `OPENCLAW_HOME` replaces the system home directory (`$HOME` / `os.homedir()`) for all internal path resolution. This enables full filesystem isolation for headless service accounts. +When set, `OPENCLAW_HOME` replaces the system home directory (`$HOME` / `os.homedir()`) for internal OpenClaw path defaults. This includes the default state directory, config path, agent directories, credentials, installer onboarding workspace, and the default dev checkout used by `openclaw update --channel dev`. **Precedence:** `OPENCLAW_HOME` > `$HOME` > `USERPROFILE` > Termux `PREFIX` home fallback on Android > `os.homedir()` @@ -173,6 +173,8 @@ When set, `OPENCLAW_HOME` replaces the system home directory (`$HOME` / `os.home `OPENCLAW_HOME` can also be set to a tilde path (e.g. `~/svc`), which gets expanded using the same OS home fallback chain before use. +Explicit path variables such as `OPENCLAW_STATE_DIR`, `OPENCLAW_CONFIG_PATH`, and `OPENCLAW_GIT_DIR` still take precedence. OS-account tasks such as shell startup file detection, package-manager setup, and host `~` expansion may still use the real system home. + ## nvm users: web_fetch TLS failures If Node.js was installed via **nvm** (not the system package manager), the built-in `fetch()` uses diff --git a/docs/install/development-channels.md b/docs/install/development-channels.md index 13237e5b1554..2415c2198713 100644 --- a/docs/install/development-channels.md +++ b/docs/install/development-channels.md @@ -40,7 +40,8 @@ install method: - **`stable`** (git installs): checks out the latest stable git tag. - **`beta`** (git installs): prefers the latest beta git tag, but falls back to the latest stable git tag when beta is missing or older. -- **`dev`**: ensures a git checkout (default `~/openclaw`, override with +- **`dev`**: ensures a git checkout (default `~/openclaw`, or + `$OPENCLAW_HOME/openclaw` when `OPENCLAW_HOME` is set; override with `OPENCLAW_GIT_DIR`), switches to `main`, rebases on upstream, builds, and installs the global CLI from that checkout. diff --git a/docs/install/installer.md b/docs/install/installer.md index 96254dfaf734..8b85427c961a 100644 --- a/docs/install/installer.md +++ b/docs/install/installer.md @@ -154,19 +154,20 @@ The script exits with code `2` for invalid method selection or invalid `--instal -| Variable | Description | -| ------------------------------------------------- | --------------------------------------------- | -| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method | -| `OPENCLAW_VERSION=latest\|next\|\|` | npm version, dist-tag, or package spec | -| `OPENCLAW_BETA=0\|1` | Use beta if available | -| `OPENCLAW_GIT_DIR=` | Checkout directory | -| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates | -| `OPENCLAW_NO_PROMPT=1` | Disable prompts | -| `OPENCLAW_NO_ONBOARD=1` | Skip onboarding | -| `OPENCLAW_DRY_RUN=1` | Dry run mode | -| `OPENCLAW_VERBOSE=1` | Debug mode | -| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level | -| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) | +| Variable | Description | +| ------------------------------------------------- | ------------------------------------------------------------------ | +| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method | +| `OPENCLAW_VERSION=latest\|next\|\|` | npm version, dist-tag, or package spec | +| `OPENCLAW_BETA=0\|1` | Use beta if available | +| `OPENCLAW_HOME=` | Base directory for OpenClaw state and default git/onboarding paths | +| `OPENCLAW_GIT_DIR=` | Checkout directory | +| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates | +| `OPENCLAW_NO_PROMPT=1` | Disable prompts | +| `OPENCLAW_NO_ONBOARD=1` | Skip onboarding | +| `OPENCLAW_DRY_RUN=1` | Dry run mode | +| `OPENCLAW_VERBOSE=1` | Debug mode | +| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level | +| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) | @@ -256,17 +257,18 @@ by default, plus git-checkout installs under the same prefix flow. -| Variable | Description | -| ------------------------------------------- | --------------------------------------------- | -| `OPENCLAW_PREFIX=` | Install prefix | -| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method | -| `OPENCLAW_VERSION=` | OpenClaw version or dist-tag | -| `OPENCLAW_NODE_VERSION=` | Node version | -| `OPENCLAW_GIT_DIR=` | Git checkout directory for git installs | -| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates for existing checkouts | -| `OPENCLAW_NO_ONBOARD=1` | Skip onboarding | -| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level | -| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) | +| Variable | Description | +| ------------------------------------------- | ------------------------------------------------------------------ | +| `OPENCLAW_PREFIX=` | Install prefix | +| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method | +| `OPENCLAW_VERSION=` | OpenClaw version or dist-tag | +| `OPENCLAW_NODE_VERSION=` | Node version | +| `OPENCLAW_HOME=` | Base directory for OpenClaw state and default git/onboarding paths | +| `OPENCLAW_GIT_DIR=` | Git checkout directory for git installs | +| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates for existing checkouts | +| `OPENCLAW_NO_ONBOARD=1` | Skip onboarding | +| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level | +| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) | diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index a16a6e80731a..e72766e0d8d2 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -29,13 +29,34 @@ ensure_home_env() { ensure_home_env +resolve_openclaw_effective_home() { + local openclaw_home="${OPENCLAW_HOME:-}" + if [[ -z "$openclaw_home" ]]; then + echo "$HOME" + return 0 + fi + + case "$openclaw_home" in + \~) + echo "$HOME" + ;; + \~/*) + echo "${HOME}/${openclaw_home#~/}" + ;; + *) + echo "$openclaw_home" + ;; + esac +} + +OPENCLAW_EFFECTIVE_HOME="$(resolve_openclaw_effective_home)" PREFIX="${OPENCLAW_PREFIX:-${HOME}/.openclaw}" OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}" NODE_VERSION="${OPENCLAW_NODE_VERSION:-22.22.0}" SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}" NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}" INSTALL_METHOD="${OPENCLAW_INSTALL_METHOD:-npm}" -GIT_DIR="${OPENCLAW_GIT_DIR:-${HOME}/openclaw}" +GIT_DIR="${OPENCLAW_GIT_DIR:-${OPENCLAW_EFFECTIVE_HOME}/openclaw}" GIT_UPDATE="${OPENCLAW_GIT_UPDATE:-1}" JSON=0 RUN_ONBOARD=0 @@ -46,11 +67,11 @@ print_usage() { cat < Install prefix (default: ~/.openclaw) + --prefix Install prefix (default: ~/.openclaw; use \$OPENCLAW_PREFIX to override) --install-method, --method npm|git Install via npm (default) or from a git checkout --npm Shortcut for --install-method npm --git, --github Shortcut for --install-method git - --git-dir, --dir Checkout directory (default: ~/openclaw) + --git-dir, --dir Checkout directory (default: ~/openclaw, or \$OPENCLAW_HOME/openclaw) --version OpenClaw version (default: latest) --node-version Node version (default: 22.22.0) --onboard Run "openclaw onboard" after install @@ -61,6 +82,8 @@ Environment variables: SHARP_IGNORE_GLOBAL_LIBVIPS=0|1 Default: 1 (avoid sharp building against global libvips) OPENCLAW_NPM_LOGLEVEL=error|warn|notice Default: error (hide npm deprecation noise) OPENCLAW_INSTALL_METHOD=git|npm + OPENCLAW_HOME=... + OPENCLAW_PREFIX=... OPENCLAW_VERSION=latest|next| OPENCLAW_GIT_DIR=... OPENCLAW_GIT_UPDATE=0|1 @@ -100,7 +123,7 @@ download_file() { } cleanup_legacy_submodules() { - local repo_dir="${1:-${OPENCLAW_GIT_DIR:-${HOME}/openclaw}}" + local repo_dir="${1:-${OPENCLAW_GIT_DIR:-${OPENCLAW_EFFECTIVE_HOME}/openclaw}}" local legacy_dir="${repo_dir}/Peekaboo" if [[ -d "$legacy_dir" ]]; then emit_json "{\"event\":\"step\",\"name\":\"legacy-submodule\",\"status\":\"start\",\"path\":\"${legacy_dir//\"/\\\"}\"}" diff --git a/scripts/install.sh b/scripts/install.sh index 4ddd69c97bbc..3d4f3a99b9d6 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -39,6 +39,23 @@ mktempfile() { echo "$f" } +resolve_openclaw_effective_home() { + local openclaw_home="${OPENCLAW_HOME:-}" + if [[ -z "$openclaw_home" ]]; then + echo "$HOME" + return + fi + if [[ "$openclaw_home" == "~" ]]; then + echo "$HOME" + return + fi + if [[ "$openclaw_home" == \~/* ]]; then + echo "${HOME}${openclaw_home:1}" + return + fi + echo "$openclaw_home" +} + DOWNLOADER="" detect_downloader() { if command -v curl &> /dev/null; then @@ -1024,7 +1041,7 @@ DRY_RUN=${OPENCLAW_DRY_RUN:-0} INSTALL_METHOD=${OPENCLAW_INSTALL_METHOD:-} OPENCLAW_VERSION=${OPENCLAW_VERSION:-latest} USE_BETA=${OPENCLAW_BETA:-0} -GIT_DIR_DEFAULT="${HOME}/openclaw" +GIT_DIR_DEFAULT="$(resolve_openclaw_effective_home)/openclaw" GIT_DIR=${OPENCLAW_GIT_DIR:-$GIT_DIR_DEFAULT} GIT_UPDATE=${OPENCLAW_GIT_UPDATE:-1} SHARP_IGNORE_GLOBAL_LIBVIPS="${SHARP_IGNORE_GLOBAL_LIBVIPS:-1}" @@ -2388,6 +2405,7 @@ install_openclaw_from_git() { ensure_pnpm_binary_for_scripts if [[ ! -d "$repo_dir" ]]; then + mkdir -p "$(dirname "$repo_dir")" run_quiet_step "Cloning OpenClaw" git clone "$repo_url" "$repo_dir" fi @@ -2585,10 +2603,12 @@ maybe_open_dashboard() { resolve_workspace_dir() { local profile="${OPENCLAW_PROFILE:-default}" + local effective_home + effective_home="$(resolve_openclaw_effective_home)" if [[ "${profile}" != "default" ]]; then - echo "${HOME}/.openclaw/workspace-${profile}" + echo "${effective_home}/.openclaw/workspace-${profile}" else - echo "${HOME}/.openclaw/workspace" + echo "${effective_home}/.openclaw/workspace" fi } @@ -2597,10 +2617,19 @@ run_bootstrap_onboarding_if_needed() { return fi - local config_path="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}" - if [[ -f "${config_path}" || -f "$HOME/.clawdbot/clawdbot.json" ]]; then + 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 + fi + fi local workspace workspace="$(resolve_workspace_dir)" @@ -3033,8 +3062,10 @@ main() { user_claw="$(openclaw_command_for_user "${OPENCLAW_BIN:-}")" ui_info "Skipping onboard (requested); run ${user_claw} onboard later" else - local config_path="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}" - if [[ -f "${config_path}" || -f "$HOME/.clawdbot/clawdbot.json" ]]; then + 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" run_doctor should_open_dashboard=true diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index f65aac577039..4fc55affdcf8 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -310,7 +310,7 @@ const { defaultRuntime } = await import("../runtime.js"); const { updateCommand, updateFinalizeCommand, updateStatusCommand, updateWizardCommand } = await import("./update-cli.js"); const updateCliShared = await import("./update-cli/shared.js"); -const { resolveGitInstallDir } = updateCliShared; +const { ensureGitCheckout, resolveGitInstallDir } = updateCliShared; const { spawnSync } = await import("node:child_process"); function requireValue(value: T | undefined, label: string): T { @@ -5486,9 +5486,57 @@ describe("update-cli", () => { it("uses ~/openclaw as the default dev checkout directory", async () => { const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue("/tmp/oc-home"); - await withEnvAsync({ OPENCLAW_GIT_DIR: undefined }, async () => { - expect(resolveGitInstallDir()).toBe(path.posix.join("/tmp/oc-home", "openclaw")); + try { + await withEnvAsync( + { + HOME: undefined, + OPENCLAW_GIT_DIR: undefined, + OPENCLAW_HOME: undefined, + USERPROFILE: undefined, + }, + async () => { + expect(resolveGitInstallDir()).toBe(path.posix.join("/tmp/oc-home", "openclaw")); + }, + ); + } finally { + homedirSpy.mockRestore(); + } + }); + + it("uses OPENCLAW_HOME for the default dev checkout directory", async () => { + const homedirSpy = vi.spyOn(os, "homedir").mockReturnValue("/tmp/oc-home"); + try { + await withEnvAsync( + { OPENCLAW_GIT_DIR: undefined, OPENCLAW_HOME: "/srv/openclaw-home" }, + async () => { + expect(resolveGitInstallDir()).toBe(path.posix.join("/srv/openclaw-home", "openclaw")); + }, + ); + } finally { + homedirSpy.mockRestore(); + } + }); + + it("creates the parent directory before cloning the default dev checkout", async () => { + const root = await createTrackedTempDir("openclaw-update-home-"); + const home = path.join(root, "custom-openclaw-home"); + const checkoutDir = path.join(home, "openclaw"); + + await withEnvAsync({ OPENCLAW_GIT_DIR: undefined, OPENCLAW_HOME: home }, async () => { + const dir = resolveGitInstallDir(); + expect(dir).toBe(checkoutDir); + await ensureGitCheckout({ dir, timeoutMs: 1_000, env: process.env }); }); - homedirSpy.mockRestore(); + + expect((await fs.stat(home)).isDirectory()).toBe(true); + const cloneCall = vi + .mocked(runCommandWithTimeout) + .mock.calls.find((call) => call[0][0] === "git" && call[0][1] === "clone"); + expect(cloneCall?.[0]).toEqual([ + "git", + "clone", + "https://github.com/openclaw/openclaw.git", + checkoutDir, + ]); }); }); diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index 2ac30f6b7c99..9536513595a1 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { resolveRequiredHomeDir } from "../../infra/home-dir.js"; import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js"; import { readPackageName, readPackageVersion } from "../../infra/package-json.js"; import { normalizePackageTagInput } from "../../infra/package-tag.js"; @@ -141,7 +142,7 @@ export function resolveGitInstallDir(): string { } function resolveDefaultGitDir(): string { - const home = os.homedir(); + const home = resolveRequiredHomeDir(process.env, os.homedir); if (home.startsWith("/")) { return path.posix.join(home, "openclaw"); } @@ -221,6 +222,7 @@ export async function ensureGitCheckout(params: { const gitEnv = params.env ?? (await createGlobalInstallEnv()); const dirExists = await pathExists(params.dir); if (!dirExists) { + await fs.mkdir(path.dirname(params.dir), { recursive: true }); return await runUpdateStep({ name: "git clone", argv: ["git", "clone", OPENCLAW_REPO_URL, params.dir], diff --git a/test/scripts/install-cli.test.ts b/test/scripts/install-cli.test.ts index 7e3eb4c1dc6f..5bfff10a9a80 100644 --- a/test/scripts/install-cli.test.ts +++ b/test/scripts/install-cli.test.ts @@ -1,5 +1,7 @@ import { spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; const SCRIPT_PATH = "scripts/install-cli.sh"; @@ -18,6 +20,38 @@ function runInstallCliShell(script: string, env: NodeJS.ProcessEnv = {}) { describe("install-cli.sh", () => { const script = readFileSync(SCRIPT_PATH, "utf8"); + it("keeps HOME for default prefix while OPENCLAW_HOME controls git checkout paths", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-home-")); + const osHome = join(tmp, "os-home"); + const openclawHome = join(tmp, "openclaw-home"); + mkdirSync(osHome, { recursive: true }); + mkdirSync(openclawHome, { recursive: true }); + + let result: ReturnType | undefined; + try { + result = runInstallCliShell( + [ + `cd ${JSON.stringify(process.cwd())}`, + `source ${JSON.stringify(SCRIPT_PATH)}`, + 'printf "prefix=%s\\ngit=%s\\n" "$PREFIX" "$GIT_DIR"', + ].join("\n"), + { + HOME: osHome, + OPENCLAW_HOME: openclawHome, + OPENCLAW_GIT_DIR: undefined, + OPENCLAW_PREFIX: undefined, + }, + ); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + + expect(result?.status).toBe(0); + const output = result?.stdout ?? ""; + expect(output).toContain(`prefix=${join(osHome, ".openclaw")}`); + expect(output).toContain(`git=${join(openclawHome, "openclaw")}`); + }); + it("resolves requested git install versions to checkout refs", () => { const result = runInstallCliShell(` set -euo pipefail diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index 50d3eadf540c..a75c5d74cc44 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -41,6 +41,86 @@ describe("install.sh", () => { expect(script).toContain('cmd+=(--no-fund --no-audit "$freshness_flag" install -g "$spec")'); }); + it("uses OPENCLAW_HOME for git and onboarding defaults", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-home-")); + const osHome = join(tmp, "os-home"); + const openclawHome = join(tmp, "openclaw-home"); + mkdirSync(osHome, { recursive: true }); + mkdirSync(openclawHome, { recursive: true }); + + let result: ReturnType | undefined; + try { + result = runInstallShell( + [ + `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)"', + ].join("\n"), + { + HOME: osHome, + OPENCLAW_HOME: openclawHome, + OPENCLAW_GIT_DIR: undefined, + TERM: "dumb", + }, + ); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + + 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"', + ); + expect(mkdirParentIndex).toBeGreaterThan(-1); + expect(cloneIndex).toBeGreaterThan(-1); + expect(mkdirParentIndex).toBeLessThan(cloneIndex); + }); + + it("skips bootstrap onboarding when legacy HOME config exists with OPENCLAW_HOME", () => { + 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 }); + writeFileSync(join(legacyConfigDir, "openclaw.json"), "{}\n"); + writeFileSync(join(bootstrapDir, "BOOTSTRAP.md"), "# bootstrap\n"); + + let result: ReturnType | undefined; + try { + result = runInstallShell( + [ + `cd ${JSON.stringify(process.cwd())}`, + `source ${JSON.stringify(SCRIPT_PATH)}`, + "NO_ONBOARD=0", + "run_bootstrap_onboarding_if_needed", + ].join("\n"), + { + HOME: osHome, + OPENCLAW_HOME: openclawHome, + OPENCLAW_CONFIG_PATH: undefined, + TERM: "dumb", + }, + ); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + + expect(result?.status).toBe(0); + expect(result?.stdout ?? "").not.toContain("BOOTSTRAP.md found"); + expect(result?.stderr ?? "").toBe(""); + }); + it("rejects OpenClaw GitHub source targets for npm installs", () => { const result = runInstallShell(` set -euo pipefail