惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
J
Java Code Geeks
腾讯CDC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
博客园 - 司徒正美
Stack Overflow Blog
Stack Overflow Blog
美团技术团队
L
LangChain Blog
WordPress大学
WordPress大学
A
About on SuperTechFans
Martin Fowler
Martin Fowler
月光博客
月光博客
Y
Y Combinator Blog
U
Unit 42
D
Docker
Recent Announcements
Recent Announcements
Hugging Face - Blog
Hugging Face - Blog
B
Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
Google Developers Blog
Last Week in AI
Last Week in AI
T
The Blog of Author Tim Ferriss
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
feat: add update finalization command · openclaw/openclaw...
shakkernerd · 2026-05-15 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -5,6 +5,7 @@ import { registerUpdateCli } from "./update-cli.js";

55
66

const mocks = vi.hoisted(() => ({

77

updateCommand: vi.fn(async (_opts: unknown) => {}),

8+

updateFinalizeCommand: vi.fn(async (_opts: unknown) => {}),

89

updateStatusCommand: vi.fn(async (_opts: unknown) => {}),

910

updateWizardCommand: vi.fn(async (_opts: unknown) => {}),

1011

defaultRuntime: {

@@ -16,10 +17,17 @@ const mocks = vi.hoisted(() => ({

1617

},

1718

}));

1819
19-

const { updateCommand, updateStatusCommand, updateWizardCommand, defaultRuntime } = mocks;

20+

const {

21+

updateCommand,

22+

updateFinalizeCommand,

23+

updateStatusCommand,

24+

updateWizardCommand,

25+

defaultRuntime,

26+

} = mocks;

2027
2128

vi.mock("./update-cli/update-command.js", () => ({

2229

updateCommand: (opts: unknown) => mocks.updateCommand(opts),

30+

updateFinalizeCommand: (opts: unknown) => mocks.updateFinalizeCommand(opts),

2331

}));

2432
2533

vi.mock("./update-cli/status.js", () => ({

@@ -41,6 +49,7 @@ function firstCallOptions(mock: { mock: { calls: unknown[][] } }) {

4149

describe("update cli option collisions", () => {

4250

beforeEach(() => {

4351

updateCommand.mockClear();

52+

updateFinalizeCommand.mockClear();

4453

updateStatusCommand.mockClear();

4554

updateWizardCommand.mockClear();

4655

defaultRuntime.log.mockClear();

@@ -61,6 +70,23 @@ describe("update cli option collisions", () => {

6170

expect((opts as { json?: boolean; timeout?: string } | undefined)?.timeout).toBe("9");

6271

},

6372

},

73+

{

74+

name: "forwards parent-captured --json/--timeout to hidden `update finalize`",

75+

argv: ["update", "finalize", "--json", "--timeout", "17", "--no-restart"],

76+

assert: () => {

77+

expect(updateFinalizeCommand).toHaveBeenCalledTimes(1);

78+

const opts = firstCallOptions(updateFinalizeCommand);

79+

expect(

80+

(opts as { json?: boolean; timeout?: string; restart?: boolean } | undefined)?.json,

81+

).toBe(true);

82+

expect(

83+

(opts as { json?: boolean; timeout?: string; restart?: boolean } | undefined)?.timeout,

84+

).toBe("17");

85+

expect(

86+

(opts as { json?: boolean; timeout?: string; restart?: boolean } | undefined)?.restart,

87+

).toBe(false);

88+

},

89+

},

6490

{

6591

name: "forwards parent-captured --timeout to `update wizard`",

6692

argv: ["update", "wizard", "--timeout", "13"],

Original file line numberDiff line numberDiff line change

@@ -278,7 +278,8 @@ const { runCommandWithTimeout } = await import("../process/exec.js");

278278

const { runDaemonRestart, runDaemonInstall } = await import("./daemon-cli.js");

279279

const { doctorCommand } = await import("../commands/doctor.js");

280280

const { defaultRuntime } = await import("../runtime.js");

281-

const { updateCommand, updateStatusCommand, updateWizardCommand } = await import("./update-cli.js");

281+

const { updateCommand, updateFinalizeCommand, updateStatusCommand, updateWizardCommand } =

282+

await import("./update-cli.js");

282283

const updateCliShared = await import("./update-cli/shared.js");

283284

const { resolveGitInstallDir } = updateCliShared;

284285

const { spawnSync } = await import("node:child_process");

@@ -3356,6 +3357,48 @@ describe("update-cli", () => {

33563357

}

33573358

});

33583359
3360+

it("updateFinalizeCommand runs doctor and plugin convergence with full update env", async () => {

3361+

await withEnvAsync(

3362+

{

3363+

OPENCLAW_UPDATE_IN_PROGRESS: undefined,

3364+

OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: undefined,

3365+

},

3366+

async () => {

3367+

let doctorEnv: NodeJS.ProcessEnv | undefined;

3368+

vi.mocked(doctorCommand).mockImplementationOnce(async () => {

3369+

doctorEnv = { ...process.env };

3370+

});

3371+

vi.mocked(defaultRuntime.writeJson).mockClear();

3372+
3373+

await updateFinalizeCommand({ json: true, yes: true, timeout: "9", restart: false });

3374+
3375+

expect(doctorEnv?.OPENCLAW_UPDATE_IN_PROGRESS).toBe("1");

3376+

expect(doctorEnv?.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE).toBe("1");

3377+

expect(process.env.OPENCLAW_UPDATE_IN_PROGRESS).toBeUndefined();

3378+

expect(process.env.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE).toBeUndefined();

3379+

expect(doctorCommand).toHaveBeenCalledWith(defaultRuntime, {

3380+

nonInteractive: true,

3381+

yes: true,

3382+

});

3383+

expect(syncPluginCall()?.channel).toBe("stable");

3384+

expect(lastNpmPluginUpdateCall()?.timeoutMs).toBe(9_000);

3385+

const output = lastWriteJsonCall() as

3386+

| {

3387+

status?: string;

3388+

mode?: string;

3389+

restart?: boolean;

3390+

postUpdate?: { doctor?: { status?: string }; plugins?: { status?: string } };

3391+

}

3392+

| undefined;

3393+

expect(output?.status).toBe("ok");

3394+

expect(output?.mode).toBe("finalize");

3395+

expect(output?.restart).toBe(false);

3396+

expect(output?.postUpdate?.doctor?.status).toBe("ok");

3397+

expect(output?.postUpdate?.plugins?.status).toBe("ok");

3398+

},

3399+

);

3400+

});

3401+
33593402

it.each([

33603403

{

33613404

name: "update command invalid timeout",

Original file line numberDiff line numberDiff line change

@@ -6,15 +6,21 @@ import { inheritOptionFromParent } from "./command-options.js";

66

import { formatHelpExamples } from "./help-format.js";

77

import {

88

type UpdateCommandOptions,

9+

type UpdateFinalizeOptions,

910

type UpdateStatusOptions,

1011

type UpdateWizardOptions,

1112

} from "./update-cli/shared.js";

1213

import { updateStatusCommand } from "./update-cli/status.js";

13-

import { updateCommand } from "./update-cli/update-command.js";

14+

import { updateCommand, updateFinalizeCommand } from "./update-cli/update-command.js";

1415

import { updateWizardCommand } from "./update-cli/wizard.js";

1516
16-

export { updateCommand, updateStatusCommand, updateWizardCommand };

17-

export type { UpdateCommandOptions, UpdateStatusOptions, UpdateWizardOptions };

17+

export { updateCommand, updateFinalizeCommand, updateStatusCommand, updateWizardCommand };

18+

export type {

19+

UpdateCommandOptions,

20+

UpdateFinalizeOptions,

21+

UpdateStatusOptions,

22+

UpdateWizardOptions,

23+

};

1824
1925

function inheritedUpdateJson(command?: Command): boolean {

2026

return Boolean(inheritOptionFromParent<boolean>(command, "json"));

@@ -106,6 +112,32 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.openclaw.ai/cli/up

106112

}

107113

});

108114
115+

update

116+

.command("finalize", { hidden: true })

117+

.description("Run OpenClaw update finalization after an external core runtime change")

118+

.option("--json", "Output result as JSON", false)

119+

.option("--channel <stable|beta|dev>", "Persist update channel for finalization")

120+

.option(

121+

"--timeout <seconds>",

122+

"Timeout for update finalization steps in seconds (default: 1800)",

123+

)

124+

.option("--yes", "Skip confirmation prompts (non-interactive)", false)

125+

.option("--no-restart", "Accepted for update command parity; finalization never restarts")

126+

.action(async (opts, command) => {

127+

try {

128+

await updateFinalizeCommand({

129+

json: Boolean(opts.json) || inheritedUpdateJson(command),

130+

channel: opts.channel as string | undefined,

131+

timeout: inheritedUpdateTimeout(opts, command),

132+

yes: Boolean(opts.yes),

133+

restart: Boolean(opts.restart),

134+

});

135+

} catch (err) {

136+

defaultRuntime.error(String(err));

137+

defaultRuntime.exit(1);

138+

}

139+

});

140+
109141

update

110142

.command("wizard")

111143

.description("Interactive update wizard")

Original file line numberDiff line numberDiff line change

@@ -39,6 +39,14 @@ export type UpdateStatusOptions = {

3939

timeout?: string;

4040

};

4141
42+

export type UpdateFinalizeOptions = {

43+

json?: boolean;

44+

channel?: string;

45+

timeout?: string;

46+

yes?: boolean;

47+

restart?: boolean;

48+

};

49+
4250

export type UpdateWizardOptions = {

4351

timeout?: string;

4452

};

Original file line numberDiff line numberDiff line change

@@ -114,6 +114,7 @@ import {

114114

runUpdateStep,

115115

tryWriteCompletionCache,

116116

type UpdateCommandOptions,

117+

type UpdateFinalizeOptions,

117118

} from "./shared.js";

118119

import { suppressDeprecations } from "./suppress-deprecations.js";

119120

@@ -1767,6 +1768,120 @@ async function runPostCorePluginUpdate(params: {

17671768

});

17681769

}

17691770
1771+

type UpdateFinalizeResult = {

1772+

status: "ok" | "warning" | "error";

1773+

mode: "finalize";

1774+

root: string;

1775+

channel: "stable" | "beta" | "dev";

1776+

restart: false;

1777+

postUpdate: {

1778+

doctor: {

1779+

status: "ok";

1780+

};

1781+

plugins: PostCorePluginUpdateResult;

1782+

};

1783+

};

1784+
1785+

function withUpdateFinalizationEnv<T>(run: () => Promise<T>): Promise<T> {

1786+

const previousUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS;

1787+

const previousParentSupportsDoctorConfigWrite =

1788+

process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV];

1789+

process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1";

1790+

process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV] = "1";

1791+

return run().finally(() => {

1792+

if (previousUpdateInProgress === undefined) {

1793+

delete process.env.OPENCLAW_UPDATE_IN_PROGRESS;

1794+

} else {

1795+

process.env.OPENCLAW_UPDATE_IN_PROGRESS = previousUpdateInProgress;

1796+

}

1797+

if (previousParentSupportsDoctorConfigWrite === undefined) {

1798+

delete process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV];

1799+

} else {

1800+

process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV] =

1801+

previousParentSupportsDoctorConfigWrite;

1802+

}

1803+

});

1804+

}

1805+
1806+

export async function updateFinalizeCommand(opts: UpdateFinalizeOptions): Promise<void> {

1807+

suppressDeprecations();

1808+

const timeoutMs = parseTimeoutMsOrExit(opts.timeout);

1809+

if (timeoutMs === null) {

1810+

return;

1811+

}

1812+

assertConfigWriteAllowedInCurrentMode();

1813+
1814+

const root = await resolveUpdateRoot();

1815+

let configSnapshot = await readConfigFileSnapshot();

1816+

const requestedChannel = normalizeUpdateChannel(opts.channel);

1817+

if (opts.channel && !requestedChannel) {

1818+

defaultRuntime.error(`--channel must be "stable", "beta", or "dev" (got "${opts.channel}")`);

1819+

defaultRuntime.exit(1);

1820+

return;

1821+

}

1822+

const storedChannel = configSnapshot.valid

1823+

? normalizeUpdateChannel(configSnapshot.config.update?.channel)

1824+

: null;

1825+

const channel = requestedChannel ?? storedChannel ?? DEFAULT_PACKAGE_CHANNEL;

1826+

if (requestedChannel) {

1827+

configSnapshot = await persistRequestedUpdateChannel({

1828+

configSnapshot,

1829+

requestedChannel,

1830+

});

1831+

}

1832+
1833+

const pluginInstallRecords = await loadInstalledPluginIndexInstallRecords();

1834+

const pluginUpdate = await withUpdateFinalizationEnv(async () => {

1835+

await createUpdateConfigSnapshot();

1836+

await doctorCommand(defaultRuntime, {

1837+

nonInteractive: true,

1838+

yes: opts.yes === true,

1839+

});

1840+

return await runPostCorePluginUpdate({

1841+

root,

1842+

channel,

1843+

configSnapshot,

1844+

opts: {

1845+

json: opts.json,

1846+

timeout: opts.timeout,

1847+

yes: opts.yes,

1848+

restart: false,

1849+

},

1850+

timeoutMs: timeoutMs ?? DEFAULT_UPDATE_STEP_TIMEOUT_MS,

1851+

pluginInstallRecords,

1852+

});

1853+

});

1854+
1855+

const result: UpdateFinalizeResult = {

1856+

status:

1857+

pluginUpdate.status === "error"

1858+

? "error"

1859+

: pluginUpdate.status === "warning"

1860+

? "warning"

1861+

: "ok",

1862+

mode: "finalize",

1863+

root,

1864+

channel,

1865+

restart: false,

1866+

postUpdate: {

1867+

doctor: {

1868+

status: "ok",

1869+

},

1870+

plugins: pluginUpdate,

1871+

},

1872+

};

1873+
1874+

await tryWriteCompletionCache(root, Boolean(opts.json));

1875+

if (opts.json) {

1876+

defaultRuntime.writeJson(result);

1877+

} else if (result.status === "ok") {

1878+

defaultRuntime.log(theme.muted("Update finalization completed."));

1879+

}

1880+

if (result.status === "error") {

1881+

defaultRuntime.exit(1);

1882+

}

1883+

}

1884+
17701885

async function persistRequestedUpdateChannel(params: {

17711886

configSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;

17721887

requestedChannel: "stable" | "beta" | "dev" | null;