diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 48149520549..683edaa8d37 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -521,6 +521,32 @@ describe("update-cli", () => { expect(updateCall?.[1]?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1"); }); + it("uses OPENCLAW_UPDATE_PACKAGE_SPEC for package updates", async () => { + const tempDir = createCaseDir("openclaw-update"); + mockPackageInstallStatus(tempDir); + + await withEnvAsync( + { OPENCLAW_UPDATE_PACKAGE_SPEC: "http://10.211.55.2:8138/openclaw-next.tgz" }, + async () => { + await updateCommand({ yes: true, tag: "latest" }); + }, + ); + + expect(runGatewayUpdate).not.toHaveBeenCalled(); + expect(runCommandWithTimeout).toHaveBeenCalledWith( + [ + "npm", + "i", + "-g", + "http://10.211.55.2:8138/openclaw-next.tgz", + "--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(); diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 8a8f3217b0b..6063eb5f163 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -27,6 +27,7 @@ import { createGlobalInstallEnv, cleanupGlobalRenameDirs, globalInstallArgs, + resolveGlobalInstallSpec, resolveGlobalPackageRoot, } from "../../infra/update-global.js"; import { runGatewayUpdate, type UpdateRunResult } from "../../infra/update-runner.js"; @@ -277,6 +278,11 @@ async function runPackageInstallUpdate(params: { const packageName = (pkgRoot ? await readPackageName(pkgRoot) : await readPackageName(params.root)) ?? DEFAULT_PACKAGE_NAME; + const installSpec = resolveGlobalInstallSpec({ + packageName, + tag: params.tag, + env: installEnv, + }); const beforeVersion = pkgRoot ? await readPackageVersion(pkgRoot) : null; if (pkgRoot) { @@ -288,7 +294,7 @@ async function runPackageInstallUpdate(params: { const updateStep = await runUpdateStep({ name: "global update", - argv: globalInstallArgs(manager, `${packageName}@${params.tag}`), + argv: globalInstallArgs(manager, installSpec), env: installEnv, timeoutMs: params.timeoutMs, progress: params.progress, diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index 3a96af1fd34..4df88cc2221 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -57,6 +57,20 @@ function applyWindowsPackageInstallEnv(env: Record) { env.NODE_LLAMA_CPP_SKIP_DOWNLOAD = "1"; } +export function resolveGlobalInstallSpec(params: { + packageName: string; + tag: string; + env?: NodeJS.ProcessEnv; +}): string { + const override = + params.env?.OPENCLAW_UPDATE_PACKAGE_SPEC?.trim() || + process.env.OPENCLAW_UPDATE_PACKAGE_SPEC?.trim(); + if (override) { + return override; + } + return `${params.packageName}@${params.tag}`; +} + export async function createGlobalInstallEnv( env?: NodeJS.ProcessEnv, ): Promise { diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index 1e04459f18a..0ba8e1ce3f9 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -583,6 +583,50 @@ describe("runGatewayUpdate", () => { expect(installEnv?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1"); }); + it("uses OPENCLAW_UPDATE_PACKAGE_SPEC for global package updates", async () => { + const nodeModules = path.join(tempDir, "node_modules"); + const pkgRoot = path.join(nodeModules, "openclaw"); + await seedGlobalPackageRoot(pkgRoot); + + const calls: string[] = []; + const runCommand = async (argv: string[]): Promise => { + const key = argv.join(" "); + calls.push(key); + if (key === `git -C ${pkgRoot} rev-parse --show-toplevel`) { + return { stdout: "", stderr: "not a git repository", code: 128 }; + } + if (key === "npm root -g") { + return { stdout: nodeModules, stderr: "", code: 0 }; + } + if (key === "pnpm root -g") { + return { stdout: "", stderr: "", code: 1 }; + } + if ( + key === + "npm i -g http://10.211.55.2:8138/openclaw-next.tgz --no-fund --no-audit --loglevel=error" + ) { + await fs.writeFile( + path.join(pkgRoot, "package.json"), + JSON.stringify({ name: "openclaw", version: "2.0.0" }), + "utf-8", + ); + } + return { stdout: "ok", stderr: "", code: 0 }; + }; + + await withEnvAsync( + { OPENCLAW_UPDATE_PACKAGE_SPEC: "http://10.211.55.2:8138/openclaw-next.tgz" }, + async () => { + const result = await runWithCommand(runCommand, { cwd: pkgRoot }); + expect(result.status).toBe("ok"); + }, + ); + + expect(calls).toContain( + "npm i -g http://10.211.55.2:8138/openclaw-next.tgz --no-fund --no-audit --loglevel=error", + ); + }); + it("updates global bun installs when detected", async () => { const bunInstall = path.join(tempDir, "bun-install"); await withEnvAsync({ BUN_INSTALL: bunInstall }, async () => { diff --git a/src/infra/update-runner.ts b/src/infra/update-runner.ts index 4c863f26e89..e39380c864b 100644 --- a/src/infra/update-runner.ts +++ b/src/infra/update-runner.ts @@ -26,6 +26,7 @@ import { detectGlobalInstallManagerForRoot, globalInstallArgs, globalInstallFallbackArgs, + resolveGlobalInstallSpec, } from "./update-global.js"; export type UpdateStepResult = { @@ -872,9 +873,13 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise< }); const channel = opts.channel ?? DEFAULT_PACKAGE_CHANNEL; const tag = normalizeTag(opts.tag ?? channelToNpmTag(channel)); - const spec = `${packageName}@${tag}`; const steps: UpdateStepResult[] = []; const globalInstallEnv = await createGlobalInstallEnv(); + const spec = resolveGlobalInstallSpec({ + packageName, + tag, + env: globalInstallEnv, + }); const updateStep = await runStep({ runCommand, name: "global update",