diff --git a/scripts/write-cli-compat.ts b/scripts/write-cli-compat.ts index f818a56ea18..06de472d2fc 100644 --- a/scripts/write-cli-compat.ts +++ b/scripts/write-cli-compat.ts @@ -4,6 +4,8 @@ import { fileURLToPath } from "node:url"; import { LEGACY_DAEMON_CLI_EXPORTS, resolveLegacyDaemonCliAccessors, + resolveLegacyDaemonCliRegisterAccessor, + resolveLegacyDaemonCliRunnerAccessors, } from "../src/cli/daemon-cli-compat.ts"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -21,6 +23,16 @@ const findCandidates = () => return entry.endsWith(".js") || entry.endsWith(".mjs"); }); +const findRunnerCandidates = () => + fs.readdirSync(distDir).filter((entry) => { + const isRunnerBundle = + entry === "runners.js" || entry === "runners.mjs" || entry.startsWith("runners-"); + if (!isRunnerBundle) { + return false; + } + return entry.endsWith(".js") || entry.endsWith(".mjs"); + }); + // In rare cases, build output can land slightly after this script starts (depending on FS timing). // Retry briefly to avoid flaky builds. let candidates = findCandidates(); @@ -28,6 +40,11 @@ for (let i = 0; i < 10 && candidates.length === 0; i++) { await new Promise((resolve) => setTimeout(resolve, 50)); candidates = findCandidates(); } +let runnerCandidates = findRunnerCandidates(); +for (let i = 0; i < 10 && runnerCandidates.length === 0; i++) { + await new Promise((resolve) => setTimeout(resolve, 50)); + runnerCandidates = findRunnerCandidates(); +} if (candidates.length === 0) { throw new Error("No daemon-cli bundle found in dist; cannot write legacy CLI shim."); @@ -41,22 +58,68 @@ const resolved = orderedCandidates return { entry, accessors }; }) .find((entry) => Boolean(entry.accessors)); +const orderedRunnerCandidates = runnerCandidates.toSorted(); -if (!resolved?.accessors) { - throw new Error( - `Could not resolve daemon-cli export aliases from dist bundles: ${orderedCandidates.join(", ")}`, - ); +let daemonTarget: string; +let runnerTarget: string | null; +let accessors: Partial>; +let accessorSources: Partial< + Record<(typeof LEGACY_DAEMON_CLI_EXPORTS)[number], "daemonCli" | "daemonCliRunners"> +>; + +if (resolved?.accessors) { + daemonTarget = resolved.entry; + runnerTarget = null; + accessors = resolved.accessors; + accessorSources = Object.fromEntries( + Object.keys(resolved.accessors).map((key) => [key, "daemonCli"]), + ) as typeof accessorSources; +} else { + const registerResolved = orderedCandidates + .map((entry) => { + const source = fs.readFileSync(path.join(distDir, entry), "utf8"); + const accessor = resolveLegacyDaemonCliRegisterAccessor(source); + return { entry, accessor }; + }) + .find((entry) => Boolean(entry.accessor)); + const runnerResolved = orderedRunnerCandidates + .map((entry) => { + const source = fs.readFileSync(path.join(distDir, entry), "utf8"); + const accessor = resolveLegacyDaemonCliRunnerAccessors(source); + return { entry, accessor }; + }) + .find((entry) => Boolean(entry.accessor)); + + if (!registerResolved?.accessor || !runnerResolved?.accessor) { + throw new Error( + `Could not resolve daemon-cli export aliases from dist bundles: ${orderedCandidates.join(", ")} | runners: ${orderedRunnerCandidates.join(", ")}`, + ); + } + + daemonTarget = registerResolved.entry; + runnerTarget = runnerResolved.entry; + accessors = { + registerDaemonCli: registerResolved.accessor, + ...runnerResolved.accessor, + }; + accessorSources = { + registerDaemonCli: "daemonCli", + runDaemonInstall: runnerResolved.accessor.runDaemonInstall ? "daemonCliRunners" : undefined, + runDaemonRestart: "daemonCliRunners", + runDaemonStart: runnerResolved.accessor.runDaemonStart ? "daemonCliRunners" : undefined, + runDaemonStatus: runnerResolved.accessor.runDaemonStatus ? "daemonCliRunners" : undefined, + runDaemonStop: runnerResolved.accessor.runDaemonStop ? "daemonCliRunners" : undefined, + runDaemonUninstall: runnerResolved.accessor.runDaemonUninstall ? "daemonCliRunners" : undefined, + }; } -const target = resolved.entry; -const relPath = `../${target}`; -const { accessors } = resolved; const missingExportError = (name: string) => `Legacy daemon CLI export "${name}" is unavailable in this build. Please upgrade OpenClaw.`; const buildExportLine = (name: (typeof LEGACY_DAEMON_CLI_EXPORTS)[number]) => { const accessor = accessors[name]; if (accessor) { - return `export const ${name} = daemonCli.${accessor};`; + const sourceBinding = accessorSources[name] ?? "daemonCli"; + return `export const ${name} = ${sourceBinding}.${accessor};`; } if (name === "registerDaemonCli") { return `export const ${name} = () => { throw new Error(${JSON.stringify(missingExportError(name))}); };`; @@ -66,7 +129,10 @@ const buildExportLine = (name: (typeof LEGACY_DAEMON_CLI_EXPORTS)[number]) => { const contents = "// Legacy shim for pre-tsdown update-cli imports.\n" + - `import * as daemonCli from "${relPath}";\n` + + `import * as daemonCli from "../${daemonTarget}";\n` + + (runnerTarget && runnerTarget !== daemonTarget + ? `import * as daemonCliRunners from "../${runnerTarget}";\n` + : "") + LEGACY_DAEMON_CLI_EXPORTS.map(buildExportLine).join("\n") + "\n"; diff --git a/src/cli/daemon-cli-compat.test.ts b/src/cli/daemon-cli-compat.test.ts index 89379336942..20769bbb41b 100644 --- a/src/cli/daemon-cli-compat.test.ts +++ b/src/cli/daemon-cli-compat.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { resolveLegacyDaemonCliAccessors } from "./daemon-cli-compat.js"; +import { + resolveLegacyDaemonCliAccessors, + resolveLegacyDaemonCliRegisterAccessor, + resolveLegacyDaemonCliRunnerAccessors, +} from "./daemon-cli-compat.js"; describe("resolveLegacyDaemonCliAccessors", () => { it("resolves aliased daemon-cli exports from a bundled chunk", () => { @@ -39,4 +43,24 @@ describe("resolveLegacyDaemonCliAccessors", () => { expect(resolveLegacyDaemonCliAccessors(bundle)).toBeNull(); }); + + it("resolves split register and runner bundles", () => { + const daemonBundle = ` + var daemon_cli_exports = /* @__PURE__ */ __exportAll({ registerDaemonCli: () => registerDaemonCli }); + export { addGatewayServiceCommands as n, daemon_cli_exports as t }; + `; + const runnerBundle = ` + export { runDaemonInstall as a, runDaemonUninstall as i, runDaemonStart as n, runDaemonStop as r, runDaemonRestart as t, runDaemonStatus as u }; + `; + + expect(resolveLegacyDaemonCliRegisterAccessor(daemonBundle)).toBe("t.registerDaemonCli"); + expect(resolveLegacyDaemonCliRunnerAccessors(runnerBundle)).toEqual({ + runDaemonInstall: "a", + runDaemonRestart: "t", + runDaemonStart: "n", + runDaemonStatus: "u", + runDaemonStop: "r", + runDaemonUninstall: "i", + }); + }); }); diff --git a/src/cli/daemon-cli-compat.ts b/src/cli/daemon-cli-compat.ts index b5a217d8f7b..c846d7b5157 100644 --- a/src/cli/daemon-cli-compat.ts +++ b/src/cli/daemon-cli-compat.ts @@ -9,6 +9,7 @@ export const LEGACY_DAEMON_CLI_EXPORTS = [ ] as const; type LegacyDaemonCliExport = (typeof LEGACY_DAEMON_CLI_EXPORTS)[number]; +type LegacyDaemonCliRunnerExport = Exclude; export type LegacyDaemonCliAccessors = { registerDaemonCli: string; runDaemonRestart: string; @@ -52,9 +53,7 @@ function findRegisterContainerSymbol(bundleSource: string): string | null { return bundleSource.match(REGISTER_CONTAINER_RE)?.[1] ?? null; } -export function resolveLegacyDaemonCliAccessors( - bundleSource: string, -): LegacyDaemonCliAccessors | null { +export function resolveLegacyDaemonCliRegisterAccessor(bundleSource: string): string | null { const aliases = parseExportAliases(bundleSource); if (!aliases) { return null; @@ -63,6 +62,18 @@ export function resolveLegacyDaemonCliAccessors( const registerContainer = findRegisterContainerSymbol(bundleSource); const registerContainerAlias = registerContainer ? aliases.get(registerContainer) : undefined; const registerDirectAlias = aliases.get("registerDaemonCli"); + return registerContainerAlias + ? `${registerContainerAlias}.registerDaemonCli` + : (registerDirectAlias ?? null); +} + +export function resolveLegacyDaemonCliRunnerAccessors( + bundleSource: string, +): Partial> | null { + const aliases = parseExportAliases(bundleSource); + if (!aliases) { + return null; + } const runDaemonInstall = aliases.get("runDaemonInstall"); const runDaemonRestart = aliases.get("runDaemonRestart"); @@ -70,30 +81,47 @@ export function resolveLegacyDaemonCliAccessors( const runDaemonStatus = aliases.get("runDaemonStatus"); const runDaemonStop = aliases.get("runDaemonStop"); const runDaemonUninstall = aliases.get("runDaemonUninstall"); - if (!(registerContainerAlias || registerDirectAlias) || !runDaemonRestart) { + if (!runDaemonRestart) { + return null; + } + + return { + ...(runDaemonInstall ? { runDaemonInstall } : {}), + runDaemonRestart, + ...(runDaemonStart ? { runDaemonStart } : {}), + ...(runDaemonStatus ? { runDaemonStatus } : {}), + ...(runDaemonStop ? { runDaemonStop } : {}), + ...(runDaemonUninstall ? { runDaemonUninstall } : {}), + }; +} + +export function resolveLegacyDaemonCliAccessors( + bundleSource: string, +): LegacyDaemonCliAccessors | null { + const registerDaemonCli = resolveLegacyDaemonCliRegisterAccessor(bundleSource); + const runnerAccessors = resolveLegacyDaemonCliRunnerAccessors(bundleSource); + if (!registerDaemonCli || !runnerAccessors) { return null; } const accessors: LegacyDaemonCliAccessors = { - registerDaemonCli: registerContainerAlias - ? `${registerContainerAlias}.registerDaemonCli` - : registerDirectAlias!, - runDaemonRestart, + registerDaemonCli, + runDaemonRestart: runnerAccessors.runDaemonRestart!, }; - if (runDaemonInstall) { - accessors.runDaemonInstall = runDaemonInstall; + if (runnerAccessors.runDaemonInstall) { + accessors.runDaemonInstall = runnerAccessors.runDaemonInstall; } - if (runDaemonStart) { - accessors.runDaemonStart = runDaemonStart; + if (runnerAccessors.runDaemonStart) { + accessors.runDaemonStart = runnerAccessors.runDaemonStart; } - if (runDaemonStatus) { - accessors.runDaemonStatus = runDaemonStatus; + if (runnerAccessors.runDaemonStatus) { + accessors.runDaemonStatus = runnerAccessors.runDaemonStatus; } - if (runDaemonStop) { - accessors.runDaemonStop = runDaemonStop; + if (runnerAccessors.runDaemonStop) { + accessors.runDaemonStop = runnerAccessors.runDaemonStop; } - if (runDaemonUninstall) { - accessors.runDaemonUninstall = runDaemonUninstall; + if (runnerAccessors.runDaemonUninstall) { + accessors.runDaemonUninstall = runnerAccessors.runDaemonUninstall; } return accessors; } diff --git a/src/cli/daemon-cli/register-service-commands.ts b/src/cli/daemon-cli/register-service-commands.ts index 2690eb91d7f..b590429f109 100644 --- a/src/cli/daemon-cli/register-service-commands.ts +++ b/src/cli/daemon-cli/register-service-commands.ts @@ -1,15 +1,14 @@ import type { Command } from "commander"; import { inheritOptionFromParent } from "../command-options.js"; -import { - runDaemonInstall, - runDaemonRestart, - runDaemonStart, - runDaemonStatus, - runDaemonStop, - runDaemonUninstall, -} from "./runners.js"; import type { DaemonInstallOptions, GatewayRpcOpts } from "./types.js"; +let daemonRunnersModulePromise: Promise | undefined; + +function loadDaemonRunnersModule() { + daemonRunnersModulePromise ??= import("./runners.js"); + return daemonRunnersModulePromise; +} + function resolveInstallOptions( cmdOpts: DaemonInstallOptions, command?: Command, @@ -48,6 +47,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri .option("--deep", "Scan system-level services", false) .option("--json", "Output JSON", false) .action(async (cmdOpts, command) => { + const { runDaemonStatus } = await loadDaemonRunnersModule(); await runDaemonStatus({ rpc: resolveRpcOptions(cmdOpts, command), probe: Boolean(cmdOpts.probe), @@ -66,6 +66,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri .option("--force", "Reinstall/overwrite if already installed", false) .option("--json", "Output JSON", false) .action(async (cmdOpts, command) => { + const { runDaemonInstall } = await loadDaemonRunnersModule(); await runDaemonInstall(resolveInstallOptions(cmdOpts, command)); }); @@ -74,6 +75,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri .description("Uninstall the Gateway service (launchd/systemd/schtasks)") .option("--json", "Output JSON", false) .action(async (cmdOpts) => { + const { runDaemonUninstall } = await loadDaemonRunnersModule(); await runDaemonUninstall(cmdOpts); }); @@ -82,6 +84,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri .description("Start the Gateway service (launchd/systemd/schtasks)") .option("--json", "Output JSON", false) .action(async (cmdOpts) => { + const { runDaemonStart } = await loadDaemonRunnersModule(); await runDaemonStart(cmdOpts); }); @@ -90,6 +93,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri .description("Stop the Gateway service (launchd/systemd/schtasks)") .option("--json", "Output JSON", false) .action(async (cmdOpts) => { + const { runDaemonStop } = await loadDaemonRunnersModule(); await runDaemonStop(cmdOpts); }); @@ -98,6 +102,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri .description("Restart the Gateway service (launchd/systemd/schtasks)") .option("--json", "Output JSON", false) .action(async (cmdOpts) => { + const { runDaemonRestart } = await loadDaemonRunnersModule(); await runDaemonRestart(cmdOpts); }); }