CLI: restore lightweight root help and scoped status plugin preload

This commit is contained in:
Vincent Koc
2026-03-15 17:37:36 -07:00
parent c455cccd3d
commit f87e7be55e
7 changed files with 127 additions and 38 deletions

View File

@@ -2,14 +2,32 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
import { loadConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging.js";
import { loadOpenClawPlugins } from "../plugins/loader.js";
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
import type { PluginLogger } from "../plugins/types.js";
const log = createSubsystemLogger("plugins");
let pluginRegistryLoaded = false;
let pluginRegistryLoaded: "none" | "channels" | "all" = "none";
export function ensurePluginRegistryLoaded(): void {
if (pluginRegistryLoaded) {
export type PluginRegistryScope = "channels" | "all";
function resolveChannelPluginIds(params: {
config: ReturnType<typeof loadConfig>;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): string[] {
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})
.plugins.filter((plugin) => plugin.channels.length > 0)
.map((plugin) => plugin.id);
}
export function ensurePluginRegistryLoaded(options?: { scope?: PluginRegistryScope }): void {
const scope = options?.scope ?? "all";
if (pluginRegistryLoaded === "all" || pluginRegistryLoaded === scope) {
return;
}
const active = getActivePluginRegistry();
@@ -19,7 +37,7 @@ export function ensurePluginRegistryLoaded(): void {
active &&
(active.plugins.length > 0 || active.channels.length > 0 || active.tools.length > 0)
) {
pluginRegistryLoaded = true;
pluginRegistryLoaded = "all";
return;
}
const config = loadConfig();
@@ -34,6 +52,15 @@ export function ensurePluginRegistryLoaded(): void {
config,
workspaceDir,
logger,
...(scope === "channels"
? {
onlyPluginIds: resolveChannelPluginIds({
config,
workspaceDir,
env: process.env,
}),
}
: {}),
});
pluginRegistryLoaded = true;
pluginRegistryLoaded = scope;
}

View File

@@ -149,7 +149,7 @@ describe("registerPreActionHooks", () => {
runtime: runtimeMock,
commandPath: ["status"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledTimes(1);
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
expect(process.title).toBe("openclaw-status");
vi.clearAllMocks();
@@ -164,7 +164,7 @@ describe("registerPreActionHooks", () => {
runtime: runtimeMock,
commandPath: ["message", "send"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledTimes(1);
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "all" });
});
it("skips help/version preaction and respects banner opt-out", async () => {

View File

@@ -67,6 +67,10 @@ function loadPluginRegistryModule() {
return pluginRegistryModulePromise;
}
function resolvePluginRegistryScope(commandPath: string[]): "channels" | "all" {
return commandPath[0] === "status" || commandPath[0] === "health" ? "channels" : "all";
}
function getRootCommand(command: Command): Command {
let current = command;
while (current.parent) {
@@ -136,7 +140,7 @@ export function registerPreActionHooks(program: Command, programVersion: string)
// Load plugins for commands that need channel access
if (PLUGIN_REQUIRED_COMMANDS.has(commandPath[0])) {
const { ensurePluginRegistryLoaded } = await loadPluginRegistryModule();
ensurePluginRegistryLoaded();
ensurePluginRegistryLoaded({ scope: resolvePluginRegistryScope(commandPath) });
}
});
}

View File

@@ -37,7 +37,7 @@ describe("tryRouteCli", () => {
vi.resetModules();
({ tryRouteCli } = await import("./route.js"));
findRoutedCommandMock.mockReturnValue({
loadPlugins: false,
loadPlugins: true,
run: runRouteMock,
});
});
@@ -59,6 +59,7 @@ describe("tryRouteCli", () => {
suppressDoctorStdout: true,
}),
);
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
});
it("does not pass suppressDoctorStdout for routed non-json commands", async () => {
@@ -68,6 +69,7 @@ describe("tryRouteCli", () => {
runtime: expect.any(Object),
commandPath: ["status"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
});
it("routes status when root options precede the command", async () => {
@@ -80,5 +82,6 @@ describe("tryRouteCli", () => {
runtime: expect.any(Object),
commandPath: ["status"],
});
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "channels" });
});
});

View File

@@ -22,7 +22,12 @@ async function prepareRoutedCommand(params: {
const shouldLoadPlugins =
typeof params.loadPlugins === "function" ? params.loadPlugins(params.argv) : params.loadPlugins;
if (shouldLoadPlugins) {
ensurePluginRegistryLoaded();
ensurePluginRegistryLoaded({
scope:
params.commandPath[0] === "status" || params.commandPath[0] === "health"
? "channels"
: "all",
});
}
}

26
src/entry.test.ts Normal file
View File

@@ -0,0 +1,26 @@
import { describe, expect, it, vi } from "vitest";
import { tryHandleRootHelpFastPath } from "./entry.js";
describe("entry root help fast path", () => {
it("renders root help without importing the full program", () => {
const outputRootHelpMock = vi.fn();
const handled = tryHandleRootHelpFastPath(["node", "openclaw", "--help"], {
outputRootHelp: outputRootHelpMock,
});
expect(handled).toBe(true);
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
});
it("ignores non-root help invocations", () => {
const outputRootHelpMock = vi.fn();
const handled = tryHandleRootHelpFastPath(["node", "openclaw", "status", "--help"], {
outputRootHelp: outputRootHelpMock,
});
expect(handled).toBe(false);
expect(outputRootHelpMock).not.toHaveBeenCalled();
});
});

View File

@@ -145,24 +145,6 @@ if (
return true;
}
function tryHandleRootHelpFastPath(argv: string[]): boolean {
if (!isRootHelpInvocation(argv)) {
return false;
}
import("./cli/program.js")
.then(({ buildProgram }) => {
buildProgram().outputHelp();
})
.catch((error) => {
console.error(
"[openclaw] Failed to display help:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exitCode = 1;
});
return true;
}
process.argv = normalizeWindowsArgv(process.argv);
if (!ensureExperimentalWarningSuppressed()) {
@@ -179,16 +161,58 @@ if (
process.argv = parsed.argv;
}
if (!tryHandleRootVersionFastPath(process.argv) && !tryHandleRootHelpFastPath(process.argv)) {
import("./cli/run-main.js")
.then(({ runCli }) => runCli(process.argv))
.catch((error) => {
console.error(
"[openclaw] Failed to start CLI:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exitCode = 1;
});
if (!tryHandleRootVersionFastPath(process.argv)) {
runMainOrRootHelp(process.argv);
}
}
}
export function tryHandleRootHelpFastPath(
argv: string[],
deps: {
outputRootHelp?: () => void;
onError?: (error: unknown) => void;
} = {},
): boolean {
if (!isRootHelpInvocation(argv)) {
return false;
}
const handleError =
deps.onError ??
((error: unknown) => {
console.error(
"[openclaw] Failed to display help:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exitCode = 1;
});
if (deps.outputRootHelp) {
try {
deps.outputRootHelp();
} catch (error) {
handleError(error);
}
return true;
}
import("./cli/program/root-help.js")
.then(({ outputRootHelp }) => {
outputRootHelp();
})
.catch(handleError);
return true;
}
function runMainOrRootHelp(argv: string[]): void {
if (tryHandleRootHelpFastPath(argv)) {
return;
}
import("./cli/run-main.js")
.then(({ runCli }) => runCli(argv))
.catch((error) => {
console.error(
"[openclaw] Failed to start CLI:",
error instanceof Error ? (error.stack ?? error.message) : error,
);
process.exitCode = 1;
});
}