mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix: honor OPENCLAW_HOME defaults (#85802)
* fix: honor OPENCLAW_HOME defaults * fix(install): preserve openclaw home upgrade defaults * fix(install): satisfy shellcheck tilde patterns
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -154,19 +154,20 @@ The script exits with code `2` for invalid method selection or invalid `--instal
|
||||
|
||||
<Accordion title="Environment variables reference">
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------------------- | --------------------------------------------- |
|
||||
| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method |
|
||||
| `OPENCLAW_VERSION=latest\|next\|<semver>\|<spec>` | npm version, dist-tag, or package spec |
|
||||
| `OPENCLAW_BETA=0\|1` | Use beta if available |
|
||||
| `OPENCLAW_GIT_DIR=<path>` | 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\|<semver>\|<spec>` | npm version, dist-tag, or package spec |
|
||||
| `OPENCLAW_BETA=0\|1` | Use beta if available |
|
||||
| `OPENCLAW_HOME=<path>` | Base directory for OpenClaw state and default git/onboarding paths |
|
||||
| `OPENCLAW_GIT_DIR=<path>` | 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`) |
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -256,17 +257,18 @@ by default, plus git-checkout installs under the same prefix flow.
|
||||
|
||||
<Accordion title="Environment variables reference">
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------------- | --------------------------------------------- |
|
||||
| `OPENCLAW_PREFIX=<path>` | Install prefix |
|
||||
| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method |
|
||||
| `OPENCLAW_VERSION=<ver>` | OpenClaw version or dist-tag |
|
||||
| `OPENCLAW_NODE_VERSION=<ver>` | Node version |
|
||||
| `OPENCLAW_GIT_DIR=<path>` | 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=<path>` | Install prefix |
|
||||
| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method |
|
||||
| `OPENCLAW_VERSION=<ver>` | OpenClaw version or dist-tag |
|
||||
| `OPENCLAW_NODE_VERSION=<ver>` | Node version |
|
||||
| `OPENCLAW_HOME=<path>` | Base directory for OpenClaw state and default git/onboarding paths |
|
||||
| `OPENCLAW_GIT_DIR=<path>` | 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`) |
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -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 <<EOF
|
||||
Usage: install-cli.sh [options]
|
||||
--json Emit NDJSON events (no human output)
|
||||
--prefix <path> Install prefix (default: ~/.openclaw)
|
||||
--prefix <path> 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 <path> Checkout directory (default: ~/openclaw)
|
||||
--git-dir, --dir <path> Checkout directory (default: ~/openclaw, or \$OPENCLAW_HOME/openclaw)
|
||||
--version <ver> OpenClaw version (default: latest)
|
||||
--node-version <ver> 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|<semver>
|
||||
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//\"/\\\"}\"}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T>(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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<typeof runInstallCliShell> | 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
|
||||
|
||||
@@ -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<typeof runInstallShell> | 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<typeof runInstallShell> | 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
|
||||
|
||||
Reference in New Issue
Block a user