mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 12:30:49 +00:00
CLI: support package-manager installs from GitHub main (#47630)
* CLI: resolve package-manager main install specs * CLI: skip registry resolution for raw package specs * CLI: support main package target updates * CLI: document package update specs in help * Tests: cover package install spec resolution * Tests: cover npm main-package updates * Tests: cover update --tag main * Installer: support main package targets * Installer: support main package targets on Windows * Docs: document package-manager main updates * Docs: document installer main targets * Docs: document npm and pnpm main installs * Docs: document update --tag main * Changelog: note package-manager main installs * Update src/infra/update-global.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
|
||||
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
|
||||
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
|
||||
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ 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 --json
|
||||
@@ -31,7 +32,7 @@ openclaw --update
|
||||
|
||||
- `--no-restart`: skip restarting the Gateway service after a successful update.
|
||||
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
|
||||
- `--tag <dist-tag|version>`: override the npm dist-tag or version for this update only.
|
||||
- `--tag <dist-tag|version|spec>`: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`.
|
||||
- `--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.
|
||||
- `--timeout <seconds>`: per-step timeout (default is 1200s).
|
||||
|
||||
@@ -102,6 +102,16 @@ For VPS/cloud hosts, avoid third-party "1-click" marketplace images when possibl
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Want the current GitHub `main` head with a package-manager install?
|
||||
|
||||
```bash
|
||||
npm install -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="From source" icon="github">
|
||||
|
||||
@@ -116,6 +116,11 @@ 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
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="GitHub main via npm">
|
||||
```bash
|
||||
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --version main
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Dry run">
|
||||
```bash
|
||||
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --dry-run
|
||||
@@ -126,39 +131,39 @@ The script exits with code `2` for invalid method selection or invalid `--instal
|
||||
<AccordionGroup>
|
||||
<Accordion title="Flags reference">
|
||||
|
||||
| Flag | Description |
|
||||
| ------------------------------- | ---------------------------------------------------------- |
|
||||
| `--install-method npm\|git` | Choose install method (default: `npm`). Alias: `--method` |
|
||||
| `--npm` | Shortcut for npm method |
|
||||
| `--git` | Shortcut for git method. Alias: `--github` |
|
||||
| `--version <version\|dist-tag>` | npm version or dist-tag (default: `latest`) |
|
||||
| `--beta` | Use beta dist-tag if available, else fallback to `latest` |
|
||||
| `--git-dir <path>` | Checkout directory (default: `~/openclaw`). Alias: `--dir` |
|
||||
| `--no-git-update` | Skip `git pull` for existing checkout |
|
||||
| `--no-prompt` | Disable prompts |
|
||||
| `--no-onboard` | Skip onboarding |
|
||||
| `--onboard` | Enable onboarding |
|
||||
| `--dry-run` | Print actions without applying changes |
|
||||
| `--verbose` | Enable debug output (`set -x`, npm notice-level logs) |
|
||||
| `--help` | Show usage (`-h`) |
|
||||
| Flag | Description |
|
||||
| ------------------------------------- | ---------------------------------------------------------- |
|
||||
| `--install-method npm\|git` | Choose install method (default: `npm`). Alias: `--method` |
|
||||
| `--npm` | Shortcut for npm method |
|
||||
| `--git` | Shortcut for git method. Alias: `--github` |
|
||||
| `--version <version\|dist-tag\|spec>` | npm version, dist-tag, or package spec (default: `latest`) |
|
||||
| `--beta` | Use beta dist-tag if available, else fallback to `latest` |
|
||||
| `--git-dir <path>` | Checkout directory (default: `~/openclaw`). Alias: `--dir` |
|
||||
| `--no-git-update` | Skip `git pull` for existing checkout |
|
||||
| `--no-prompt` | Disable prompts |
|
||||
| `--no-onboard` | Skip onboarding |
|
||||
| `--onboard` | Enable onboarding |
|
||||
| `--dry-run` | Print actions without applying changes |
|
||||
| `--verbose` | Enable debug output (`set -x`, npm notice-level logs) |
|
||||
| `--help` | Show usage (`-h`) |
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Environment variables reference">
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------------- | --------------------------------------------- |
|
||||
| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method |
|
||||
| `OPENCLAW_VERSION=latest\|next\|<semver>` | npm version or dist-tag |
|
||||
| `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\|main\|<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`) |
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -276,6 +281,11 @@ Designed for environments where you want everything under a local prefix (defaul
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="GitHub main via npm">
|
||||
```powershell
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -Tag main
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Custom git directory">
|
||||
```powershell
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git -GitDir "C:\openclaw"
|
||||
@@ -299,14 +309,14 @@ Designed for environments where you want everything under a local prefix (defaul
|
||||
<AccordionGroup>
|
||||
<Accordion title="Flags reference">
|
||||
|
||||
| Flag | Description |
|
||||
| ------------------------- | ------------------------------------------------------ |
|
||||
| `-InstallMethod npm\|git` | Install method (default: `npm`) |
|
||||
| `-Tag <tag>` | npm dist-tag (default: `latest`) |
|
||||
| `-GitDir <path>` | Checkout directory (default: `%USERPROFILE%\openclaw`) |
|
||||
| `-NoOnboard` | Skip onboarding |
|
||||
| `-NoGitUpdate` | Skip `git pull` |
|
||||
| `-DryRun` | Print actions only |
|
||||
| Flag | Description |
|
||||
| --------------------------- | ---------------------------------------------------------- |
|
||||
| `-InstallMethod npm\|git` | Install method (default: `npm`) |
|
||||
| `-Tag <tag\|version\|spec>` | npm dist-tag, version, or package spec (default: `latest`) |
|
||||
| `-GitDir <path>` | Checkout directory (default: `%USERPROFILE%\openclaw`) |
|
||||
| `-NoOnboard` | Skip onboarding |
|
||||
| `-NoGitUpdate` | Skip `git pull` |
|
||||
| `-DryRun` | Print actions only |
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -65,7 +65,25 @@ openclaw update --channel dev
|
||||
openclaw update --channel stable
|
||||
```
|
||||
|
||||
Use `--tag <dist-tag|version>` for a one-off install tag/version.
|
||||
Use `--tag <dist-tag|version|spec>` for a one-off package target override.
|
||||
|
||||
For the current GitHub `main` head via a package-manager install:
|
||||
|
||||
```bash
|
||||
openclaw update --tag main
|
||||
```
|
||||
|
||||
Manual equivalents:
|
||||
|
||||
```bash
|
||||
npm i -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
You can also pass an explicit package spec to `--tag` for one-off updates (for example a GitHub ref or tarball URL).
|
||||
|
||||
See [Development channels](/install/development-channels) for channel semantics and release notes.
|
||||
|
||||
|
||||
@@ -200,13 +200,15 @@ function Ensure-Git {
|
||||
}
|
||||
|
||||
function Install-OpenClawNpm {
|
||||
param([string]$Version = "latest")
|
||||
param([string]$Target = "latest")
|
||||
|
||||
$installSpec = Resolve-PackageInstallSpec -Target $Target
|
||||
|
||||
Write-Host "Installing OpenClaw (openclaw@$Version)..." -Level info
|
||||
Write-Host "Installing OpenClaw ($installSpec)..." -Level info
|
||||
|
||||
try {
|
||||
# Use -ExecutionPolicy Bypass to handle restricted execution policy
|
||||
npm install -g openclaw@$Version --no-fund --no-audit 2>&1
|
||||
npm install -g $installSpec --no-fund --no-audit 2>&1
|
||||
Write-Host "OpenClaw installed" -Level success
|
||||
return $true
|
||||
} catch {
|
||||
@@ -257,6 +259,34 @@ node "%~dp0..\openclaw\dist\entry.js" %*
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-ExplicitPackageInstallSpec {
|
||||
param([string]$Target)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Target)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return $Target.Contains("://") -or
|
||||
$Target.Contains("#") -or
|
||||
$Target -match '^(file|github|git\+ssh|git\+https|git\+http|git\+file|npm):'
|
||||
}
|
||||
|
||||
function Resolve-PackageInstallSpec {
|
||||
param([string]$Target = "latest")
|
||||
|
||||
$trimmed = $Target.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
||||
return "openclaw@latest"
|
||||
}
|
||||
if ($trimmed.ToLowerInvariant() -eq "main") {
|
||||
return "github:openclaw/openclaw#main"
|
||||
}
|
||||
if (Test-ExplicitPackageInstallSpec -Target $trimmed) {
|
||||
return $trimmed
|
||||
}
|
||||
return "openclaw@$trimmed"
|
||||
}
|
||||
|
||||
function Add-ToPath {
|
||||
param([string]$Path)
|
||||
|
||||
@@ -301,9 +331,9 @@ function Main {
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[DRY RUN] Would install OpenClaw via npm (tag: $Tag)" -Level info
|
||||
Write-Host "[DRY RUN] Would install OpenClaw via npm ($((Resolve-PackageInstallSpec -Target $Tag)))" -Level info
|
||||
} else {
|
||||
if (!(Install-OpenClawNpm -Version $Tag)) {
|
||||
if (!(Install-OpenClawNpm -Target $Tag)) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,7 +1011,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 <version|dist-tag> npm install: version (default: latest)
|
||||
--version <version|dist-tag|spec> npm install target (default: latest; use "main" for GitHub main)
|
||||
--beta Use beta if available, else latest
|
||||
--git-dir, --dir <path> Checkout directory (default: ~/openclaw)
|
||||
--no-git-update Skip git pull for existing checkout
|
||||
@@ -1024,7 +1024,7 @@ Options:
|
||||
|
||||
Environment variables:
|
||||
OPENCLAW_INSTALL_METHOD=git|npm
|
||||
OPENCLAW_VERSION=latest|next|<semver>
|
||||
OPENCLAW_VERSION=latest|next|main|<semver>|<spec>
|
||||
OPENCLAW_BETA=0|1
|
||||
OPENCLAW_GIT_DIR=...
|
||||
OPENCLAW_GIT_UPDATE=0|1
|
||||
@@ -1040,6 +1040,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 --no-onboard
|
||||
EOF
|
||||
}
|
||||
@@ -1963,6 +1964,43 @@ resolve_beta_version() {
|
||||
echo "$beta"
|
||||
}
|
||||
|
||||
is_explicit_package_install_spec() {
|
||||
local value="${1:-}"
|
||||
[[ "$value" == *"://"* || "$value" == *"#"* || "$value" =~ ^(file|github|git\+ssh|git\+https|git\+http|git\+file|npm): ]]
|
||||
}
|
||||
|
||||
can_resolve_registry_package_version() {
|
||||
local value="${1:-}"
|
||||
if [[ -z "$value" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "${value,,}" == "main" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if is_explicit_package_install_spec "$value"; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
resolve_package_install_spec() {
|
||||
local package_name="$1"
|
||||
local value="$2"
|
||||
if [[ "${value,,}" == "main" ]]; then
|
||||
echo "github:openclaw/openclaw#main"
|
||||
return 0
|
||||
fi
|
||||
if is_explicit_package_install_spec "$value"; then
|
||||
echo "$value"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$value" == "latest" ]]; then
|
||||
echo "${package_name}@latest"
|
||||
return 0
|
||||
fi
|
||||
echo "${package_name}@${value}"
|
||||
}
|
||||
|
||||
install_openclaw() {
|
||||
local package_name="openclaw"
|
||||
if [[ "$USE_BETA" == "1" ]]; then
|
||||
@@ -1983,18 +2021,16 @@ install_openclaw() {
|
||||
fi
|
||||
|
||||
local resolved_version=""
|
||||
resolved_version="$(npm view "${package_name}@${OPENCLAW_VERSION}" version 2>/dev/null || true)"
|
||||
if can_resolve_registry_package_version "${OPENCLAW_VERSION}"; then
|
||||
resolved_version="$(npm view "${package_name}@${OPENCLAW_VERSION}" version 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -n "$resolved_version" ]]; then
|
||||
ui_info "Installing OpenClaw v${resolved_version}"
|
||||
else
|
||||
ui_info "Installing OpenClaw (${OPENCLAW_VERSION})"
|
||||
fi
|
||||
local install_spec=""
|
||||
if [[ "${OPENCLAW_VERSION}" == "latest" ]]; then
|
||||
install_spec="${package_name}@latest"
|
||||
else
|
||||
install_spec="${package_name}@${OPENCLAW_VERSION}"
|
||||
fi
|
||||
install_spec="$(resolve_package_install_spec "${package_name}" "${OPENCLAW_VERSION}")"
|
||||
|
||||
if ! install_openclaw_npm "${install_spec}"; then
|
||||
ui_warn "npm install failed; retrying"
|
||||
|
||||
@@ -549,6 +549,48 @@ describe("update-cli", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps --tag main to the GitHub main package spec for package updates", async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
mockPackageInstallStatus(tempDir);
|
||||
|
||||
await updateCommand({ yes: true, tag: "main" });
|
||||
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
[
|
||||
"npm",
|
||||
"i",
|
||||
"-g",
|
||||
"github:openclaw/openclaw#main",
|
||||
"--no-fund",
|
||||
"--no-audit",
|
||||
"--loglevel=error",
|
||||
],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes explicit git package specs through for package updates", async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
mockPackageInstallStatus(tempDir);
|
||||
|
||||
await updateCommand({ yes: true, tag: "github:openclaw/openclaw#main" });
|
||||
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeout).toHaveBeenCalledWith(
|
||||
[
|
||||
"npm",
|
||||
"i",
|
||||
"-g",
|
||||
"github:openclaw/openclaw#main",
|
||||
"--no-fund",
|
||||
"--no-audit",
|
||||
"--loglevel=error",
|
||||
],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("updateCommand outputs JSON when --json is set", async () => {
|
||||
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
|
||||
vi.mocked(defaultRuntime.log).mockClear();
|
||||
|
||||
@@ -39,7 +39,10 @@ export function registerUpdateCli(program: Command) {
|
||||
.option("--no-restart", "Skip restarting the gateway service after a successful update")
|
||||
.option("--dry-run", "Preview update actions without making changes", false)
|
||||
.option("--channel <stable|beta|dev>", "Persist update channel (git + npm)")
|
||||
.option("--tag <dist-tag|version>", "Override npm dist-tag or version for this update")
|
||||
.option(
|
||||
"--tag <dist-tag|version|spec>",
|
||||
"Override the package target for this update (dist-tag, version, or package spec)",
|
||||
)
|
||||
.option("--timeout <seconds>", "Timeout for each update step in seconds (default: 1200)")
|
||||
.option("--yes", "Skip confirmation prompts (non-interactive)", false)
|
||||
.addHelpText("after", () => {
|
||||
@@ -48,6 +51,7 @@ 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"],
|
||||
@@ -66,7 +70,7 @@ ${theme.heading("What this does:")}
|
||||
${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 <dist-tag|version> for a one-off npm update without persisting
|
||||
- Use --tag <dist-tag|version|spec> for a one-off package update without persisting
|
||||
|
||||
${theme.heading("Non-interactive:")}
|
||||
- Use --yes to accept downgrade prompts
|
||||
|
||||
@@ -10,6 +10,7 @@ import { trimLogTail } from "../../infra/restart-sentinel.js";
|
||||
import { parseSemver } from "../../infra/runtime-guard.js";
|
||||
import { fetchNpmTagVersion } from "../../infra/update-check.js";
|
||||
import {
|
||||
canResolveRegistryVersionForPackageTarget,
|
||||
detectGlobalInstallManagerByPresence,
|
||||
detectGlobalInstallManagerForRoot,
|
||||
type CommandRunner,
|
||||
@@ -77,6 +78,9 @@ export async function resolveTargetVersion(
|
||||
tag: string,
|
||||
timeoutMs?: number,
|
||||
): Promise<string | null> {
|
||||
if (!canResolveRegistryVersionForPackageTarget(tag)) {
|
||||
return null;
|
||||
}
|
||||
const direct = normalizeVersionTag(tag);
|
||||
if (direct) {
|
||||
return direct;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
checkUpdateStatus,
|
||||
} from "../../infra/update-check.js";
|
||||
import {
|
||||
canResolveRegistryVersionForPackageTarget,
|
||||
createGlobalInstallEnv,
|
||||
cleanupGlobalRenameDirs,
|
||||
globalInstallArgs,
|
||||
@@ -731,22 +732,31 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
let targetVersion: string | null = null;
|
||||
let downgradeRisk = false;
|
||||
let fallbackToLatest = false;
|
||||
let packageInstallSpec: string | null = null;
|
||||
|
||||
if (updateInstallKind !== "git") {
|
||||
currentVersion = switchToPackage ? null : await readPackageVersion(root);
|
||||
targetVersion = explicitTag
|
||||
? await resolveTargetVersion(tag, timeoutMs)
|
||||
: await resolveNpmChannelTag({ channel, timeoutMs }).then((resolved) => {
|
||||
tag = resolved.tag;
|
||||
fallbackToLatest = channel === "beta" && resolved.tag === "latest";
|
||||
return resolved.version;
|
||||
});
|
||||
if (explicitTag) {
|
||||
targetVersion = await resolveTargetVersion(tag, timeoutMs);
|
||||
} else {
|
||||
targetVersion = await resolveNpmChannelTag({ channel, timeoutMs }).then((resolved) => {
|
||||
tag = resolved.tag;
|
||||
fallbackToLatest = channel === "beta" && resolved.tag === "latest";
|
||||
return resolved.version;
|
||||
});
|
||||
}
|
||||
const cmp =
|
||||
currentVersion && targetVersion ? compareSemverStrings(currentVersion, targetVersion) : null;
|
||||
downgradeRisk =
|
||||
canResolveRegistryVersionForPackageTarget(tag) &&
|
||||
!fallbackToLatest &&
|
||||
currentVersion != null &&
|
||||
(targetVersion == null || (cmp != null && cmp > 0));
|
||||
packageInstallSpec = resolveGlobalInstallSpec({
|
||||
packageName: DEFAULT_PACKAGE_NAME,
|
||||
tag,
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.dryRun) {
|
||||
@@ -772,7 +782,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
} else if (updateInstallKind === "git") {
|
||||
actions.push(`Run git update flow on channel ${channel} (fetch/rebase/build/doctor)`);
|
||||
} else {
|
||||
actions.push(`Run global package manager update with spec openclaw@${tag}`);
|
||||
actions.push(`Run global package manager update with spec ${packageInstallSpec ?? tag}`);
|
||||
}
|
||||
actions.push("Run plugin update sync after core update");
|
||||
actions.push("Refresh shell completion cache (if needed)");
|
||||
@@ -789,6 +799,9 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
if (fallbackToLatest) {
|
||||
notes.push("Beta channel resolves to latest for this run (fallback).");
|
||||
}
|
||||
if (explicitTag && !canResolveRegistryVersionForPackageTarget(tag)) {
|
||||
notes.push("Non-registry package specs skip npm version lookup and downgrade previews.");
|
||||
}
|
||||
|
||||
printDryRunPreview(
|
||||
{
|
||||
@@ -803,7 +816,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
requestedChannel,
|
||||
storedChannel,
|
||||
effectiveChannel: channel,
|
||||
tag,
|
||||
tag: packageInstallSpec ?? tag,
|
||||
currentVersion,
|
||||
targetVersion,
|
||||
downgradeRisk,
|
||||
|
||||
@@ -4,11 +4,15 @@ import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import {
|
||||
canResolveRegistryVersionForPackageTarget,
|
||||
cleanupGlobalRenameDirs,
|
||||
detectGlobalInstallManagerByPresence,
|
||||
detectGlobalInstallManagerForRoot,
|
||||
globalInstallArgs,
|
||||
globalInstallFallbackArgs,
|
||||
isExplicitPackageInstallSpec,
|
||||
isMainPackageTarget,
|
||||
OPENCLAW_MAIN_PACKAGE_SPEC,
|
||||
resolveGlobalPackageRoot,
|
||||
resolveGlobalInstallSpec,
|
||||
resolveGlobalRoot,
|
||||
@@ -60,6 +64,40 @@ describe("update global helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps main and explicit install specs for global installs", () => {
|
||||
expect(resolveGlobalInstallSpec({ packageName: "openclaw", tag: "main" })).toBe(
|
||||
OPENCLAW_MAIN_PACKAGE_SPEC,
|
||||
);
|
||||
expect(
|
||||
resolveGlobalInstallSpec({
|
||||
packageName: "openclaw",
|
||||
tag: "github:openclaw/openclaw#feature/my-branch",
|
||||
}),
|
||||
).toBe("github:openclaw/openclaw#feature/my-branch");
|
||||
expect(
|
||||
resolveGlobalInstallSpec({
|
||||
packageName: "openclaw",
|
||||
tag: "https://example.com/openclaw-main.tgz",
|
||||
}),
|
||||
).toBe("https://example.com/openclaw-main.tgz");
|
||||
});
|
||||
|
||||
it("classifies main and raw install specs separately from registry selectors", () => {
|
||||
expect(isMainPackageTarget("main")).toBe(true);
|
||||
expect(isMainPackageTarget(" MAIN ")).toBe(true);
|
||||
expect(isMainPackageTarget("beta")).toBe(false);
|
||||
|
||||
expect(isExplicitPackageInstallSpec("github:openclaw/openclaw#main")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("https://example.com/openclaw-main.tgz")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("file:/tmp/openclaw-main.tgz")).toBe(true);
|
||||
expect(isExplicitPackageInstallSpec("beta")).toBe(false);
|
||||
|
||||
expect(canResolveRegistryVersionForPackageTarget("latest")).toBe(true);
|
||||
expect(canResolveRegistryVersionForPackageTarget("2026.3.14")).toBe(true);
|
||||
expect(canResolveRegistryVersionForPackageTarget("main")).toBe(false);
|
||||
expect(canResolveRegistryVersionForPackageTarget("github:openclaw/openclaw#main")).toBe(false);
|
||||
});
|
||||
|
||||
it("detects install managers from resolved roots and on-disk presence", async () => {
|
||||
const base = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-global-"));
|
||||
const npmRoot = path.join(base, "npm-root");
|
||||
|
||||
@@ -14,12 +14,41 @@ export type CommandRunner = (
|
||||
const PRIMARY_PACKAGE_NAME = "openclaw";
|
||||
const ALL_PACKAGE_NAMES = [PRIMARY_PACKAGE_NAME] as const;
|
||||
const GLOBAL_RENAME_PREFIX = ".";
|
||||
export const OPENCLAW_MAIN_PACKAGE_SPEC = "github:openclaw/openclaw#main";
|
||||
const NPM_GLOBAL_INSTALL_QUIET_FLAGS = ["--no-fund", "--no-audit", "--loglevel=error"] as const;
|
||||
const NPM_GLOBAL_INSTALL_OMIT_OPTIONAL_FLAGS = [
|
||||
"--omit=optional",
|
||||
...NPM_GLOBAL_INSTALL_QUIET_FLAGS,
|
||||
] as const;
|
||||
|
||||
function normalizePackageTarget(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
export function isMainPackageTarget(value: string): boolean {
|
||||
return normalizePackageTarget(value).toLowerCase() === "main";
|
||||
}
|
||||
|
||||
export function isExplicitPackageInstallSpec(value: string): boolean {
|
||||
const trimmed = normalizePackageTarget(value);
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
trimmed.includes("://") ||
|
||||
trimmed.includes("#") ||
|
||||
/^(?:file|github|git\+ssh|git\+https|git\+http|git\+file|npm):/i.test(trimmed)
|
||||
);
|
||||
}
|
||||
|
||||
export function canResolveRegistryVersionForPackageTarget(value: string): boolean {
|
||||
const trimmed = normalizePackageTarget(value);
|
||||
if (!trimmed) {
|
||||
return true;
|
||||
}
|
||||
return !isMainPackageTarget(trimmed) && !isExplicitPackageInstallSpec(trimmed);
|
||||
}
|
||||
|
||||
async function resolvePortableGitPathPrepend(
|
||||
env: NodeJS.ProcessEnv | undefined,
|
||||
): Promise<string[]> {
|
||||
@@ -68,7 +97,14 @@ export function resolveGlobalInstallSpec(params: {
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
return `${params.packageName}@${params.tag}`;
|
||||
const target = normalizePackageTarget(params.tag);
|
||||
if (isMainPackageTarget(target)) {
|
||||
return OPENCLAW_MAIN_PACKAGE_SPEC;
|
||||
}
|
||||
if (isExplicitPackageInstallSpec(target)) {
|
||||
return target;
|
||||
}
|
||||
return `${params.packageName}@${target}`;
|
||||
}
|
||||
|
||||
export async function createGlobalInstallEnv(
|
||||
|
||||
@@ -441,6 +441,20 @@ describe("runGatewayUpdate", () => {
|
||||
expect(calls.some((call) => call === expectedInstallCommand)).toBe(true);
|
||||
});
|
||||
|
||||
it("updates global npm installs from the GitHub main package spec", async () => {
|
||||
const { calls, result } = await runNpmGlobalUpdateCase({
|
||||
expectedInstallCommand:
|
||||
"npm i -g github:openclaw/openclaw#main --no-fund --no-audit --loglevel=error",
|
||||
tag: "main",
|
||||
});
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
expect(result.mode).toBe("npm");
|
||||
expect(calls).toContain(
|
||||
"npm i -g github:openclaw/openclaw#main --no-fund --no-audit --loglevel=error",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to global npm update when git is missing from PATH", async () => {
|
||||
const { nodeModules, pkgRoot } = await createGlobalPackageFixture(tempDir);
|
||||
const { calls, runCommand } = createGlobalInstallHarness({
|
||||
|
||||
Reference in New Issue
Block a user