mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 04:05:03 +00:00
fix(cli): add early-exit fast paths for --version/--help before heavy imports (#5871)
This commit is contained in:
committed by
Vincent Koc
parent
b297bae027
commit
83d609926e
@@ -2,7 +2,9 @@ import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js"
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { CHAT_CHANNEL_ORDER } from "../channels/registry.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { ensurePluginRegistryLoaded } from "./plugin-registry.js";
|
||||
// NOTE: plugin-registry.ts is NOT imported here to avoid pulling in
|
||||
// plugins/loader.ts → jiti at startup (which is slow on low-powered devices).
|
||||
// The OPENCLAW_EAGER_CHANNEL_OPTIONS path reads from the registry without force-loading.
|
||||
|
||||
function dedupe(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -21,7 +23,9 @@ export function resolveCliChannelOptions(): string[] {
|
||||
const catalog = listChannelPluginCatalogEntries().map((entry) => entry.id);
|
||||
const base = dedupe([...CHAT_CHANNEL_ORDER, ...catalog]);
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_EAGER_CHANNEL_OPTIONS)) {
|
||||
ensurePluginRegistryLoaded();
|
||||
// Reads plugin channel IDs from the registry if already populated.
|
||||
// (ensurePluginRegistryLoaded is intentionally not called here to avoid
|
||||
// loading jiti; plugins are loaded by the preaction hook for real commands.)
|
||||
const pluginIds = listChannelPlugins().map((plugin) => plugin.id);
|
||||
return dedupe([...base, ...pluginIds]);
|
||||
}
|
||||
|
||||
@@ -34,4 +34,36 @@ describe("createProgramContext", () => {
|
||||
agentChannelOptions: "last",
|
||||
});
|
||||
});
|
||||
|
||||
// Fast-path correctness: channel options must NOT be resolved until a command
|
||||
// actually needs them. This ensures --help/--version never trigger catalog discovery.
|
||||
it("does not call resolveCliChannelOptions before channelOptions is accessed", () => {
|
||||
resolveCliChannelOptionsMock.mockClear();
|
||||
createProgramContext(); // create context without accessing any channel option getter
|
||||
expect(resolveCliChannelOptionsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls resolveCliChannelOptions lazily when channelOptions getter is first accessed", () => {
|
||||
resolveCliChannelOptionsMock.mockClear().mockReturnValue(["discord"]);
|
||||
const ctx = createProgramContext();
|
||||
const _ = ctx.channelOptions;
|
||||
expect(resolveCliChannelOptionsMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("caches channel options so resolveCliChannelOptions is called only once per context", () => {
|
||||
resolveCliChannelOptionsMock.mockClear().mockReturnValue(["telegram"]);
|
||||
const ctx = createProgramContext();
|
||||
const _a = ctx.channelOptions;
|
||||
const _b = ctx.messageChannelOptions;
|
||||
const _c = ctx.agentChannelOptions;
|
||||
// All three getters share the same underlying cache.
|
||||
expect(resolveCliChannelOptionsMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("programVersion is accessible without triggering channel option resolution", () => {
|
||||
resolveCliChannelOptionsMock.mockClear();
|
||||
const ctx = createProgramContext();
|
||||
expect(ctx.programVersion).toBe("9.9.9-test");
|
||||
expect(resolveCliChannelOptionsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,11 +9,27 @@ export type ProgramContext = {
|
||||
};
|
||||
|
||||
export function createProgramContext(): ProgramContext {
|
||||
const channelOptions = resolveCliChannelOptions();
|
||||
// Defer resolveCliChannelOptions() until a command actually needs channel option strings.
|
||||
// This avoids the catalog discovery (discoverOpenClawPlugins) and module loading
|
||||
// during --help, --version, and other fast-path invocations.
|
||||
let _channelOptions: string[] | undefined;
|
||||
function getChannelOptions(): string[] {
|
||||
if (_channelOptions === undefined) {
|
||||
_channelOptions = resolveCliChannelOptions();
|
||||
}
|
||||
return _channelOptions;
|
||||
}
|
||||
|
||||
return {
|
||||
programVersion: VERSION,
|
||||
channelOptions,
|
||||
messageChannelOptions: channelOptions.join("|"),
|
||||
agentChannelOptions: ["last", ...channelOptions].join("|"),
|
||||
get channelOptions() {
|
||||
return getChannelOptions();
|
||||
},
|
||||
get messageChannelOptions() {
|
||||
return getChannelOptions().join("|");
|
||||
},
|
||||
get agentChannelOptions() {
|
||||
return ["last", ...getChannelOptions()].join("|");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ import { getSubCliCommandsWithSubcommands } from "./register.subclis.js";
|
||||
|
||||
const CLI_NAME = resolveCliName();
|
||||
const CLI_NAME_PATTERN = escapeRegExp(CLI_NAME);
|
||||
const ROOT_COMMANDS_WITH_SUBCOMMANDS = new Set([
|
||||
...getCoreCliCommandsWithSubcommands(),
|
||||
...getSubCliCommandsWithSubcommands(),
|
||||
]);
|
||||
const ROOT_COMMANDS_HINT =
|
||||
"Hint: commands suffixed with * have subcommands. Run <command> --help for details.";
|
||||
|
||||
@@ -44,6 +40,12 @@ const EXAMPLES = [
|
||||
] as const;
|
||||
|
||||
export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
// Built here (not at module level) to defer the work until help is actually configured.
|
||||
const rootCommandsWithSubcommands = new Set([
|
||||
...getCoreCliCommandsWithSubcommands(),
|
||||
...getSubCliCommandsWithSubcommands(),
|
||||
]);
|
||||
|
||||
program
|
||||
.name(CLI_NAME)
|
||||
.description("")
|
||||
@@ -73,7 +75,7 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
optionTerm: (option) => theme.option(option.flags),
|
||||
subcommandTerm: (cmd) => {
|
||||
const isRootCommand = cmd.parent === program;
|
||||
const hasSubcommands = isRootCommand && ROOT_COMMANDS_WITH_SUBCOMMANDS.has(cmd.name());
|
||||
const hasSubcommands = isRootCommand && rootCommandsWithSubcommands.has(cmd.name());
|
||||
return theme.command(hasSubcommands ? `${cmd.name()} *` : cmd.name());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
|
||||
import { isMainModule } from "./infra/is-main.js";
|
||||
import { installProcessWarningFilter } from "./infra/warning-filter.js";
|
||||
import { attachChildProcessBridge } from "./process/child-process-bridge.js";
|
||||
import { VERSION } from "./version.js";
|
||||
|
||||
const ENTRY_WRAPPER_PAIRS = [
|
||||
{ wrapperBasename: "openclaw.mjs", entryBasename: "entry.js" },
|
||||
@@ -130,6 +131,14 @@ if (
|
||||
process.argv = parsed.argv;
|
||||
}
|
||||
|
||||
// Fast path: print version and exit before loading heavy CLI modules.
|
||||
// This avoids the full plugin/config startup on low-powered devices (e.g. Pi4b).
|
||||
const argv = process.argv;
|
||||
if (argv.includes("--version") || argv.includes("-V")) {
|
||||
console.log(VERSION);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
import("./cli/run-main.js")
|
||||
.then(({ runCli }) => runCli(process.argv))
|
||||
.catch((error) => {
|
||||
|
||||
Reference in New Issue
Block a user