fix(update): repair daemon-cli compat exports after self-update

This commit is contained in:
Peter Steinberger
2026-02-13 03:25:28 +01:00
parent c32b92b7a5
commit 711597c02b
4 changed files with 149 additions and 2 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
});
});

View File

@@ -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<string, string> | 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<string, string>();
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<LegacyDaemonCliExport, string> | 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,
};
}