perf(cli): lazy-load daemon service runners

This commit is contained in:
Vincent Koc
2026-04-14 16:43:42 +01:00
parent 25efa8cf81
commit f95c706298
4 changed files with 159 additions and 36 deletions

View File

@@ -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<Record<(typeof LEGACY_DAEMON_CLI_EXPORTS)[number], string>>;
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";

View File

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

View File

@@ -9,6 +9,7 @@ export const LEGACY_DAEMON_CLI_EXPORTS = [
] as const;
type LegacyDaemonCliExport = (typeof LEGACY_DAEMON_CLI_EXPORTS)[number];
type LegacyDaemonCliRunnerExport = Exclude<LegacyDaemonCliExport, "registerDaemonCli">;
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<Record<LegacyDaemonCliRunnerExport, string>> | 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;
}

View File

@@ -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<typeof import("./runners.js")> | 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);
});
}