diff --git a/CHANGELOG.md b/CHANGELOG.md index 88e3558e537..63e3eb7e9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle. - WhatsApp: update Baileys to `7.0.0-rc13` and drop the obsolete logger type patch. +- Install/update: reject OpenClaw GitHub source package targets early and point moving-main users at the dev/git install path instead of the broken npm source-install flow. - Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285) - Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037) - Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001. diff --git a/docs/cli/update.md b/docs/cli/update.md index b619b69926f..5d08cad06cf 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -23,7 +23,6 @@ openclaw update wizard openclaw update --channel beta openclaw update --channel dev openclaw update --tag beta -openclaw update --tag main openclaw update --dry-run openclaw update --no-restart openclaw update --yes @@ -35,7 +34,7 @@ openclaw --update - `--no-restart`: skip restarting the Gateway service after a successful update. Package-manager updates that do restart the Gateway verify the restarted service reports the expected updated version before the command succeeds. - `--channel `: set the update channel (git + npm; persisted in config). -- `--tag `: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`. +- `--tag `: override the package target for this update only. Use `--channel dev`, not `--tag main`, for the moving GitHub `main` checkout. - `--dry-run`: preview planned update actions (channel/tag/target/restart flow) without writing config, installing, syncing plugins, or restarting. - `--json`: print machine-readable `UpdateRunResult` JSON, including `postUpdate.plugins.warnings` when corrupt or unloadable managed plugins need diff --git a/docs/install/development-channels.md b/docs/install/development-channels.md index 05c8efccd3d..eb3cf6e6166 100644 --- a/docs/install/development-channels.md +++ b/docs/install/development-channels.md @@ -60,8 +60,8 @@ openclaw update --tag 2026.4.1-beta.1 # Install from the beta dist-tag (one-off, does not persist) openclaw update --tag beta -# Install from GitHub main branch (npm tarball) -openclaw update --tag main +# Switch to the moving GitHub main checkout +openclaw update --channel dev # Install a specific npm package spec openclaw update --tag openclaw@2026.4.1-beta.1 @@ -72,6 +72,9 @@ Notes: - `--tag` applies to **package (npm) installs only**. Git installs ignore it. - The tag is not persisted. Your next `openclaw update` uses your configured channel as usual. +- OpenClaw does not support npm GitHub source installs for `openclaw/openclaw`. + Use `--channel dev` or `--install-method git --version main` for the moving + `main` checkout. - Downgrade protection: if the target version is older than your current version, OpenClaw prompts for confirmation (skip with `--yes`). - `--channel beta` is different from `--tag beta`: the channel flow can fall back diff --git a/docs/install/index.md b/docs/install/index.md index 764c277e50e..07eb1031c40 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -131,10 +131,10 @@ openclaw onboard --install-daemon Or skip the link and use `pnpm openclaw ...` from inside the repo. See [Setup](/start/setup) for full development workflows. -### Install from GitHub main +### Install from the GitHub main checkout ```bash -npm install -g github:openclaw/openclaw#main +curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --version main ``` ### Containers and package managers diff --git a/docs/install/installer.md b/docs/install/installer.md index 9a252da66e5..dcea5726d22 100644 --- a/docs/install/installer.md +++ b/docs/install/installer.md @@ -119,9 +119,9 @@ The script exits with code `2` for invalid method selection or invalid `--instal curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git ``` - + ```bash - curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --version main + curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --version main ``` @@ -157,7 +157,7 @@ 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\|main\|\|` | npm version, dist-tag, or package spec | +| `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 | @@ -315,9 +315,9 @@ by default, plus git-checkout installs under the same prefix flow. & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git ``` - + ```powershell - & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -Tag main + & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git -Tag main ``` diff --git a/docs/install/updating.md b/docs/install/updating.md index 3dec3f78b85..c9b29cf7607 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -21,7 +21,6 @@ To switch channels or target a specific version: ```bash openclaw update --channel beta openclaw update --channel dev -openclaw update --tag main openclaw update --dry-run # preview without applying ``` @@ -35,6 +34,10 @@ installer has its own `--verbose` flag, but that flag is not part of the beta tag is missing or older than the latest stable release. Use `--tag beta` if you want the raw npm beta dist-tag for a one-off package update. +Use `--channel dev` for the moving GitHub `main` checkout. Package updates do +not support npm GitHub source installs for `openclaw/openclaw`; target a +published dist-tag, exact version, or built tarball instead. + For managed plugins, beta-channel fallback is a warning: the core update can still succeed while a plugin uses its recorded default/latest release because no plugin beta is available. diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index 9a8e13215da..a16a6e80731 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -364,6 +364,27 @@ run_pnpm() { "${PNPM_CMD[@]}" "$@" } +to_lowercase_ascii() { + printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' +} + +is_openclaw_source_package_install_spec() { + local value="${1:-}" + local normalized_value="" + normalized_value="$(to_lowercase_ascii "$value")" + normalized_value="${normalized_value#openclaw@}" + + [[ "$normalized_value" == "main" ]] && return 0 + [[ "$normalized_value" =~ ^github:openclaw/openclaw($|[#/]) ]] && return 0 + + normalized_value="${normalized_value#git+}" + [[ "$normalized_value" =~ ^https?://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + [[ "$normalized_value" =~ ^ssh://git@github\.com[:/]openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + [[ "$normalized_value" =~ ^git://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + [[ "$normalized_value" =~ ^git@github\.com:openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + return 1 +} + resolve_git_openclaw_ref() { local requested="${OPENCLAW_VERSION:-latest}" local resolved_version="" @@ -624,6 +645,9 @@ fix_npm_prefix_if_needed() { install_openclaw() { local requested="${OPENCLAW_VERSION:-latest}" + if is_openclaw_source_package_install_spec "$requested"; then + fail "npm installs do not support OpenClaw GitHub source targets like '${requested}'. Use --install-method git --version main, latest, beta, an exact version, or a built .tgz package." + fi local freshness_flag="--min-release-age=0" local min_release_age="" min_release_age="$(env -u NPM_CONFIG_BEFORE -u npm_config_before "$(npm_bin)" config get min-release-age 2>/dev/null || true)" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index eab13fa8765..0fe5ba79e64 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -511,10 +511,45 @@ function Resolve-NpmOpenClawInstallSpec { return "$PackageName@$trimmedTag" } +function Test-OpenClawSourcePackageInstallSpec { + param([string]$RequestedTag) + + if ([string]::IsNullOrWhiteSpace($RequestedTag)) { + return $false + } + + $normalizedTag = $RequestedTag.Trim().ToLowerInvariant() + if ($normalizedTag.StartsWith("openclaw@")) { + $normalizedTag = $normalizedTag.Substring("openclaw@".Length) + } + + if ($normalizedTag -eq "main") { + return $true + } + if ($normalizedTag -match '^github:openclaw/openclaw($|[#/])') { + return $true + } + + if ($normalizedTag.StartsWith("git+")) { + $normalizedTag = $normalizedTag.Substring("git+".Length) + } + return ( + $normalizedTag -match '^https?://github\.com/openclaw/openclaw(\.git)?($|[?#])' -or + $normalizedTag -match '^ssh://git@github\.com[:/]openclaw/openclaw(\.git)?($|[?#])' -or + $normalizedTag -match '^git://github\.com/openclaw/openclaw(\.git)?($|[?#])' -or + $normalizedTag -match '^git@github\.com:openclaw/openclaw(\.git)?($|[?#])' + ) +} + function Install-OpenClaw { if ([string]::IsNullOrWhiteSpace($Tag)) { $Tag = "latest" } + if (Test-OpenClawSourcePackageInstallSpec -RequestedTag $Tag) { + Write-Host "Error: npm installs do not support OpenClaw GitHub source targets like '$Tag'." -ForegroundColor Red + Write-Host "Use -InstallMethod git -Tag main for the moving main checkout, or use latest, beta, an exact version, or a built .tgz package." -ForegroundColor Yellow + return $false + } if (-not (Ensure-Git)) { return $false } diff --git a/scripts/install.sh b/scripts/install.sh index de3603cdd58..4ddd69c97bb 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1047,7 +1047,7 @@ Options: --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 - --version npm install target (default: latest; use "main" for GitHub main) + --version npm install target (default: latest) --beta Use beta if available, else latest --git-dir, --dir Checkout directory (default: ~/openclaw) --no-git-update Skip git pull for existing checkout @@ -1060,7 +1060,7 @@ Options: Environment variables: OPENCLAW_INSTALL_METHOD=git|npm - OPENCLAW_VERSION=latest|next|main|| + OPENCLAW_VERSION=latest|next|| OPENCLAW_BETA=0|1 OPENCLAW_GIT_DIR=... OPENCLAW_GIT_UPDATE=0|1 @@ -1076,7 +1076,7 @@ Examples: curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard --verify - curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --version main + curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --version main curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboard EOF } @@ -2444,6 +2444,23 @@ is_explicit_package_install_spec() { [[ "$value" == *"://"* || "$value" == *"#"* || "$value" =~ ^(file|github|git\+ssh|git\+https|git\+http|git\+file|npm): ]] } +is_openclaw_source_package_install_spec() { + local value="${1:-}" + local normalized_value="" + normalized_value="$(to_lowercase_ascii "$value")" + normalized_value="${normalized_value#openclaw@}" + + [[ "$normalized_value" == "main" ]] && return 0 + [[ "$normalized_value" =~ ^github:openclaw/openclaw($|[#/]) ]] && return 0 + + normalized_value="${normalized_value#git+}" + [[ "$normalized_value" =~ ^https?://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + [[ "$normalized_value" =~ ^ssh://git@github\.com[:/]openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + [[ "$normalized_value" =~ ^git://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + [[ "$normalized_value" =~ ^git@github\.com:openclaw/openclaw(\.git)?($|[?#]) ]] && return 0 + return 1 +} + can_resolve_registry_package_version() { local value="${1:-}" local normalized_value="" @@ -2499,6 +2516,12 @@ install_openclaw() { OPENCLAW_VERSION="latest" fi + if is_openclaw_source_package_install_spec "${OPENCLAW_VERSION}"; then + ui_error "npm installs do not support OpenClaw GitHub source targets like '${OPENCLAW_VERSION}'." + ui_info "Use --install-method git --version main for the moving main checkout, or use latest, beta, an exact version, or a built .tgz package." + return 1 + fi + local resolved_version="" if can_resolve_registry_package_version "${OPENCLAW_VERSION}"; then resolved_version="$(npm view "${package_name}@${OPENCLAW_VERSION}" version 2>/dev/null || true)" diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 06a4e66e34a..ba02acf4824 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -2074,22 +2074,6 @@ describe("update-cli", () => { }, expectedSpec: "openclaw@next", }, - { - name: "main shorthand", - run: async () => { - mockPackageInstallStatus(createCaseDir("openclaw-update")); - await updateCommand({ yes: true, tag: "main" }); - }, - expectedSpec: "github:openclaw/openclaw#main", - }, - { - name: "explicit git package spec", - run: async () => { - mockPackageInstallStatus(createCaseDir("openclaw-update")); - await updateCommand({ yes: true, tag: "github:openclaw/openclaw#main" }); - }, - expectedSpec: "github:openclaw/openclaw#main", - }, { name: "OPENCLAW_UPDATE_PACKAGE_SPEC override", run: async () => { @@ -2116,6 +2100,22 @@ describe("update-cli", () => { }, ); + it.each(["main", "github:openclaw/openclaw#main", "openclaw@github:openclaw/openclaw#main"])( + "rejects OpenClaw GitHub source package updates: %s", + async (tag) => { + mockPackageInstallStatus(createCaseDir("openclaw-update")); + + await updateCommand({ yes: true, tag }); + + expect(packageInstallCommandCall()).toBeUndefined(); + expect(defaultRuntime.exit).toHaveBeenCalledWith(1); + const errors = vi.mocked(defaultRuntime.error).mock.calls.map((call) => String(call[0])); + expect(errors.join("\n")).toContain("Unsupported package update target"); + expect(errors.join("\n")).toContain("openclaw/openclaw"); + expect(errors.join("\n")).toContain("openclaw update --channel dev"); + }, + ); + it("fails package updates when the installed correction version does not match the requested target", async () => { const tempDir = createCaseDir("openclaw-update"); const nodeModules = path.join(tempDir, "node_modules"); diff --git a/src/cli/update-cli.ts b/src/cli/update-cli.ts index 360e9c345f1..571ce42306d 100644 --- a/src/cli/update-cli.ts +++ b/src/cli/update-cli.ts @@ -58,7 +58,6 @@ export function registerUpdateCli(program: Command) { ["openclaw update --channel beta", "Switch to beta channel (git + npm)"], ["openclaw update --channel dev", "Switch to dev channel (git + npm)"], ["openclaw update --tag beta", "One-off update to a dist-tag or version"], - ["openclaw update --tag main", "One-off package install from GitHub main"], ["openclaw update --dry-run", "Preview actions without changing anything"], ["openclaw update --no-restart", "Update without restarting the service"], ["openclaw update --json", "Output result as JSON"], @@ -78,6 +77,7 @@ ${theme.heading("Switch channels:")} - Use --channel stable|beta|dev to persist the update channel in config - Run openclaw update status to see the active channel and source - Use --tag for a one-off package update without persisting + - Use --channel dev, not --tag main, for the moving GitHub main checkout ${theme.heading("Non-interactive:")} - Use --yes to accept downgrade prompts diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 377e362b6e1..c52332c8984 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -69,6 +69,7 @@ import { createGlobalInstallEnv, cleanupGlobalRenameDirs, globalInstallArgs, + isOpenClawSourcePackageInstallSpec, resolveGlobalInstallTarget, resolveGlobalInstallSpec, resolvePnpmGlobalDirFromGlobalRoot, @@ -976,6 +977,14 @@ async function resolvePackageRuntimePreflightError(params: { ].join("\n"); } +function formatUnsupportedOpenClawSourcePackageTargetMessage(target: string): string { + return [ + `Unsupported package update target: ${target}.`, + "OpenClaw package updates use published npm artifacts or built tarballs; npm GitHub source installs for openclaw/openclaw do not reliably produce an installable package.", + "Use `openclaw update --channel dev` for the moving main checkout, or target `latest`, `beta`, an exact version, or a built `.tgz` package spec.", + ].join("\n"); +} + async function resolvePackageRuntimeForPreflight(params: { nodeRunner?: string; timeoutMs?: number; @@ -3080,6 +3089,11 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { tag, env: process.env, }); + if (isOpenClawSourcePackageInstallSpec(packageInstallSpec)) { + defaultRuntime.error(formatUnsupportedOpenClawSourcePackageTargetMessage(packageInstallSpec)); + defaultRuntime.exit(1); + return; + } } if (opts.dryRun) { diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts index 895ad1643e4..743732b0b87 100644 --- a/src/infra/update-global.test.ts +++ b/src/infra/update-global.test.ts @@ -26,6 +26,7 @@ import { globalInstallFallbackArgs, isExplicitPackageInstallSpec, isMainPackageTarget, + isOpenClawSourcePackageInstallSpec, OPENCLAW_MAIN_PACKAGE_SPEC, resolveGlobalInstallCommand, resolveGlobalPackageRoot, @@ -249,6 +250,30 @@ describe("update global helpers", () => { expect(canResolveRegistryVersionForPackageTarget("/tmp/openclaw-main.tgz")).toBe(false); }); + it("classifies OpenClaw GitHub source package specs as unsupported package targets", () => { + expect(isOpenClawSourcePackageInstallSpec("main")).toBe(true); + expect(isOpenClawSourcePackageInstallSpec("github:openclaw/openclaw#main")).toBe(true); + expect(isOpenClawSourcePackageInstallSpec("openclaw@github:openclaw/openclaw#main")).toBe( + true, + ); + expect(isOpenClawSourcePackageInstallSpec("OpenClaw@github:openclaw/openclaw#main")).toBe( + true, + ); + expect( + isOpenClawSourcePackageInstallSpec("git+https://github.com/openclaw/openclaw.git#main"), + ).toBe(true); + expect(isOpenClawSourcePackageInstallSpec("https://example.com/openclaw-main.tgz")).toBe( + false, + ); + expect( + isOpenClawSourcePackageInstallSpec( + "https://github.com/openclaw/openclaw/releases/download/v2026.5.20/openclaw.tgz", + ), + ).toBe(false); + expect(isOpenClawSourcePackageInstallSpec("github:other/openclaw#main")).toBe(false); + expect(isOpenClawSourcePackageInstallSpec("beta")).toBe(false); + }); + it("detects install managers from resolved roots and on-disk presence", async () => { await withTempDir({ prefix: "openclaw-update-global-" }, async (base) => { const npmRoot = path.join(base, "npm-root"); diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index a0f4fdafb38..609d7a24968 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -89,7 +89,30 @@ export function isExplicitPackageInstallSpec(value: string): boolean { function stripPrimaryPackageAlias(spec: string): string { const normalized = normalizePackageTarget(spec); const prefix = `${PRIMARY_PACKAGE_NAME}@`; - return normalized.startsWith(prefix) ? normalized.slice(prefix.length).trim() : normalized; + return normalized.toLowerCase().startsWith(prefix) + ? normalized.slice(prefix.length).trim() + : normalized; +} + +export function isOpenClawSourcePackageInstallSpec(value: string): boolean { + if (isMainPackageTarget(value)) { + return true; + } + const target = stripPrimaryPackageAlias(value); + const normalizedTarget = normalizeLowercaseStringOrEmpty(target); + if (!normalizedTarget) { + return false; + } + if (/^github:openclaw\/openclaw(?:$|[#/])/u.test(normalizedTarget)) { + return true; + } + const gitUrl = normalizedTarget.replace(/^git\+/u, ""); + return ( + /^https?:\/\/github\.com\/openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl) || + /^ssh:\/\/git@github\.com[:/]openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl) || + /^git:\/\/github\.com\/openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl) || + /^git@github\.com:openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl) + ); } function isPnpmOpenClawSourceInstallSpec(spec: string): boolean { diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index 7f0d9ca2636..37ae3beda58 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -1742,15 +1742,17 @@ describe("runGatewayUpdate", () => { expect(calls).toContain(expectedInstallCommand); }); - it("updates global npm installs from the GitHub main package spec", async () => { + it("rejects global npm updates from the GitHub main package spec", async () => { const { calls, result } = await runNpmGlobalUpdateCase({ expectedInstallCommand: npmGlobalInstallCommand("github:openclaw/openclaw#main"), tag: "main", }); - expect(result.status).toBe("ok"); + expect(result.status).toBe("error"); expect(result.mode).toBe("npm"); - expect(calls).toContain(npmGlobalInstallCommand("github:openclaw/openclaw#main")); + expect(result.reason).toBe("unsupported-package-target"); + expect(result.steps[0]?.name).toBe("package target validation"); + expect(calls).not.toContain(npmGlobalInstallCommand("github:openclaw/openclaw#main")); }); it("runs doctor after global npm updates before reporting success", async () => { diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 4c96082f1f9..d8d0f4be526 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -25,6 +25,7 @@ import { cleanupGlobalRenameDirs, createGlobalInstallEnv, detectGlobalInstallManagerForRoot, + isOpenClawSourcePackageInstallSpec, resolveGlobalInstallTarget, resolveGlobalInstallSpec, type GlobalInstallManager, @@ -1453,6 +1454,30 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< tag, env: globalInstallEnv, }); + if (isOpenClawSourcePackageInstallSpec(spec)) { + const durationMs = Date.now() - startedAt; + return { + status: "error", + mode: globalManager, + root: pkgRoot, + reason: "unsupported-package-target", + before: { version: beforeVersion }, + after: { version: beforeVersion }, + steps: [ + { + name: "package target validation", + command: `validate package target ${spec}`, + cwd: pkgRoot, + durationMs, + exitCode: 1, + stdoutTail: null, + stderrTail: + "OpenClaw package updates use published npm artifacts or built tarballs; use the dev channel for GitHub main.", + }, + ], + durationMs, + }; + } const packageUpdate = await runGlobalPackageUpdateSteps({ installTarget, installSpec: spec, diff --git a/test/scripts/install-cli.test.ts b/test/scripts/install-cli.test.ts index 29b0c595602..7e3eb4c1dc6 100644 --- a/test/scripts/install-cli.test.ts +++ b/test/scripts/install-cli.test.ts @@ -96,4 +96,17 @@ describe("install-cli.sh", () => { expect(script).toContain('freshness_flag="--before=$(date -u'); expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before"); }); + + it("rejects OpenClaw GitHub source targets for npm installs", () => { + const result = runInstallCliShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + OPENCLAW_VERSION=main + install_openclaw + `); + + expect(result.status).toBe(1); + expect(result.stdout).toContain("npm installs do not support OpenClaw GitHub source targets"); + expect(result.stdout).toContain("--install-method git --version main"); + }); }); diff --git a/test/scripts/install-ps1.test.ts b/test/scripts/install-ps1.test.ts index 093c4bf0d4b..f2e374eea8c 100644 --- a/test/scripts/install-ps1.test.ts +++ b/test/scripts/install-ps1.test.ts @@ -104,6 +104,16 @@ describe("install.ps1 failure handling", () => { ); }); + it("rejects OpenClaw GitHub source targets for npm installs", () => { + const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw"); + const sourceTargetBody = extractFunctionBody(source, "Test-OpenClawSourcePackageInstallSpec"); + expect(sourceTargetBody).toContain('$normalizedTag -eq "main"'); + expect(sourceTargetBody).toContain("^github:openclaw/openclaw"); + expect(npmInstallBody).toContain("Test-OpenClawSourcePackageInstallSpec -RequestedTag $Tag"); + expect(npmInstallBody).toContain("npm installs do not support OpenClaw GitHub source targets"); + expect(npmInstallBody).toContain("-InstallMethod git -Tag main"); + }); + it("cleans legacy git submodules only from the selected git checkout", () => { const gitInstallBody = extractFunctionBody(source, "Install-OpenClawFromGit"); const mainBody = extractFunctionBody(source, "Main"); diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index ddfb97a443f..50d3eadf540 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -41,6 +41,24 @@ describe("install.sh", () => { expect(script).toContain('cmd+=(--no-fund --no-audit "$freshness_flag" install -g "$spec")'); }); + it("rejects OpenClaw GitHub source targets for npm installs", () => { + const result = runInstallShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + set +e + OPENCLAW_VERSION=main + USE_BETA=0 + install_openclaw + status=$? + printf 'status=%s\\n' "$status" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("status=1"); + expect(result.stdout).toContain("npm installs do not support OpenClaw GitHub source targets"); + expect(result.stdout).toContain("--install-method git --version main"); + }); + it("exports noninteractive apt env during Linux startup", () => { expect(script).toMatch( /detect_os_or_die\s+if \[\[ "\$OS" == "linux" \]\]; then\s+export DEBIAN_FRONTEND="\$\{DEBIAN_FRONTEND:-noninteractive\}"\s+export NEEDRESTART_MODE="\$\{NEEDRESTART_MODE:-a\}"\s+fi/m,