From 711597c02bfb79041cfcfe6c16b4645335b3d37f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Feb 2026 03:25:28 +0100 Subject: [PATCH] fix(update): repair daemon-cli compat exports after self-update --- CHANGELOG.md | 1 + scripts/write-cli-compat.ts | 28 +++++++++- src/cli/daemon-cli-compat.test.ts | 30 ++++++++++ src/cli/daemon-cli-compat.ts | 92 +++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/cli/daemon-cli-compat.test.ts create mode 100644 src/cli/daemon-cli-compat.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a6638cf93a..01a1791696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ Docs: https://docs.openclaw.ai - Discord: allow channel-edit to archive/lock threads and set auto-archive duration. (#5542) Thanks @stumct. - Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale. - Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik. +- Update/Daemon: fix post-update restart compatibility by generating `dist/cli/daemon-cli.js` with alias-aware exports from hashed daemon bundles, preventing `registerDaemonCli` import failures during `openclaw update`. ## 2026.2.9 diff --git a/scripts/write-cli-compat.ts b/scripts/write-cli-compat.ts index a7a8f9ca42..ac025fd822 100644 --- a/scripts/write-cli-compat.ts +++ b/scripts/write-cli-compat.ts @@ -1,6 +1,10 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { + LEGACY_DAEMON_CLI_EXPORTS, + resolveLegacyDaemonCliAccessors, +} from "../src/cli/daemon-cli-compat.ts"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const distDir = path.join(rootDir, "dist"); @@ -27,12 +31,32 @@ if (candidates.length === 0) { throw new Error("No daemon-cli bundle found in dist; cannot write legacy CLI shim."); } -const target = candidates.toSorted()[0]; +const orderedCandidates = candidates.toSorted(); +const resolved = orderedCandidates + .map((entry) => { + const source = fs.readFileSync(path.join(distDir, entry), "utf8"); + const accessors = resolveLegacyDaemonCliAccessors(source); + return { entry, accessors }; + }) + .find((entry) => Boolean(entry.accessors)); + +if (!resolved?.accessors) { + throw new Error( + `Could not resolve daemon-cli export aliases from dist bundles: ${orderedCandidates.join(", ")}`, + ); +} + +const target = resolved.entry; const relPath = `../${target}`; +const { accessors } = resolved; const contents = "// Legacy shim for pre-tsdown update-cli imports.\n" + - `export { registerDaemonCli, runDaemonInstall, runDaemonRestart, runDaemonStart, runDaemonStatus, runDaemonStop, runDaemonUninstall } from "${relPath}";\n`; + `import * as daemonCli from "${relPath}";\n` + + LEGACY_DAEMON_CLI_EXPORTS.map( + (name) => `export const ${name} = daemonCli.${accessors[name]};`, + ).join("\n") + + "\n"; fs.mkdirSync(cliDir, { recursive: true }); fs.writeFileSync(path.join(cliDir, "daemon-cli.js"), contents); diff --git a/src/cli/daemon-cli-compat.test.ts b/src/cli/daemon-cli-compat.test.ts new file mode 100644 index 0000000000..46c63014a5 --- /dev/null +++ b/src/cli/daemon-cli-compat.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { resolveLegacyDaemonCliAccessors } from "./daemon-cli-compat.js"; + +describe("resolveLegacyDaemonCliAccessors", () => { + it("resolves aliased daemon-cli exports from a bundled chunk", () => { + const bundle = ` + var daemon_cli_exports = /* @__PURE__ */ __exportAll({ registerDaemonCli: () => registerDaemonCli }); + export { runDaemonStop as a, runDaemonStart as i, runDaemonStatus as n, runDaemonUninstall as o, runDaemonRestart as r, runDaemonInstall as s, daemon_cli_exports as t }; + `; + + expect(resolveLegacyDaemonCliAccessors(bundle)).toEqual({ + registerDaemonCli: "t.registerDaemonCli", + runDaemonInstall: "s", + runDaemonRestart: "r", + runDaemonStart: "i", + runDaemonStatus: "n", + runDaemonStop: "a", + runDaemonUninstall: "o", + }); + }); + + it("returns null when required aliases are missing", () => { + const bundle = ` + var daemon_cli_exports = /* @__PURE__ */ __exportAll({ registerDaemonCli: () => registerDaemonCli }); + export { runDaemonRestart as r, daemon_cli_exports as t }; + `; + + expect(resolveLegacyDaemonCliAccessors(bundle)).toBeNull(); + }); +}); diff --git a/src/cli/daemon-cli-compat.ts b/src/cli/daemon-cli-compat.ts new file mode 100644 index 0000000000..04d1b113ee --- /dev/null +++ b/src/cli/daemon-cli-compat.ts @@ -0,0 +1,92 @@ +export const LEGACY_DAEMON_CLI_EXPORTS = [ + "registerDaemonCli", + "runDaemonInstall", + "runDaemonRestart", + "runDaemonStart", + "runDaemonStatus", + "runDaemonStop", + "runDaemonUninstall", +] as const; + +type LegacyDaemonCliExport = (typeof LEGACY_DAEMON_CLI_EXPORTS)[number]; + +const EXPORT_SPEC_RE = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/; +const REGISTER_CONTAINER_RE = + /(?:var|const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:\/\*[\s\S]*?\*\/\s*)?__exportAll\(\{\s*registerDaemonCli\s*:\s*\(\)\s*=>\s*registerDaemonCli\s*\}\)/; + +function parseExportAliases(bundleSource: string): Map | null { + const matches = [...bundleSource.matchAll(/export\s*\{([^}]+)\}\s*;?/g)]; + if (matches.length === 0) { + return null; + } + const last = matches.at(-1); + const body = last?.[1]; + if (!body) { + return null; + } + + const aliases = new Map(); + for (const chunk of body.split(",")) { + const spec = chunk.trim(); + if (!spec) { + continue; + } + const parsed = spec.match(EXPORT_SPEC_RE); + if (!parsed) { + return null; + } + const original = parsed[1]; + const alias = parsed[2] ?? original; + aliases.set(original, alias); + } + return aliases; +} + +function findRegisterContainerSymbol(bundleSource: string): string | null { + return bundleSource.match(REGISTER_CONTAINER_RE)?.[1] ?? null; +} + +export function resolveLegacyDaemonCliAccessors( + bundleSource: string, +): Record | null { + const aliases = parseExportAliases(bundleSource); + if (!aliases) { + return null; + } + + const registerContainer = findRegisterContainerSymbol(bundleSource); + if (!registerContainer) { + return null; + } + const registerContainerAlias = aliases.get(registerContainer); + if (!registerContainerAlias) { + return null; + } + + const runDaemonInstall = aliases.get("runDaemonInstall"); + const runDaemonRestart = aliases.get("runDaemonRestart"); + const runDaemonStart = aliases.get("runDaemonStart"); + const runDaemonStatus = aliases.get("runDaemonStatus"); + const runDaemonStop = aliases.get("runDaemonStop"); + const runDaemonUninstall = aliases.get("runDaemonUninstall"); + if ( + !runDaemonInstall || + !runDaemonRestart || + !runDaemonStart || + !runDaemonStatus || + !runDaemonStop || + !runDaemonUninstall + ) { + return null; + } + + return { + registerDaemonCli: `${registerContainerAlias}.registerDaemonCli`, + runDaemonInstall, + runDaemonRestart, + runDaemonStart, + runDaemonStatus, + runDaemonStop, + runDaemonUninstall, + }; +}