From 2ab08c8a197e9b3de2d78ece8234de70d903ab39 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 14 May 2026 14:09:12 +0800 Subject: [PATCH] fix(cli): keep plugin parent help lightweight --- CHANGELOG.md | 1 + ...led-channel-catalog-read.fail-soft.test.ts | 8 +++--- src/channels/bundled-channel-catalog-read.ts | 25 ++++++++++++++++--- src/cli/program/register.subclis-core.ts | 6 ++--- src/cli/program/register.subclis.test.ts | 4 +-- src/cli/run-main-policy.ts | 24 ++++++++++++++++-- src/cli/run-main.test.ts | 9 +++++++ 7 files changed, 62 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a99a73326..3392e5031c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai - Plugins: discover provider plugins from `setup.providers[].envVars` credentials during provider discovery while keeping the deprecated `providerAuthEnvVars` fallback. (#81542) Thanks @JARVIS-Glasses. - Docs/Codex harness: clarify that per-agent `CODEX_HOME` isolates `~/.codex` while inherited `HOME` intentionally keeps `.agents` discovery and subprocess user-home state available. +- CLI/plugins: keep bare plugin and parent-command help on the lightweight path, avoiding plugin registry discovery before rendering help. - CLI tables: preserve muted/color styling on wrapped continuation lines after multiline cells, keeping `openclaw plugins list` descriptions readable. - iOS: restore first-use Contacts, Calendar, and Reminders permission prompts and add Privacy & Access status/actions in Settings. Thanks @BunsDev. - Canvas: return not found for malformed percent-encoded Canvas/A2UI/document asset paths and keep decoded parent traversal blocked before path normalization. diff --git a/src/channels/bundled-channel-catalog-read.fail-soft.test.ts b/src/channels/bundled-channel-catalog-read.fail-soft.test.ts index f775384cc7d..6f2fe169404 100644 --- a/src/channels/bundled-channel-catalog-read.fail-soft.test.ts +++ b/src/channels/bundled-channel-catalog-read.fail-soft.test.ts @@ -7,15 +7,13 @@ afterEach(() => { }); describe("listBundledChannelCatalogEntries discovery failures", () => { - it("falls back when bundled plugin catalog discovery is unavailable during import", async () => { + it("falls back when bundled package metadata is unavailable during import", async () => { vi.doMock("../infra/openclaw-root.js", () => ({ resolveOpenClawPackageRootSync: () => null, resolveOpenClawPackageRoot: async () => null, })); - vi.doMock("../plugins/channel-catalog-registry.js", () => ({ - listChannelCatalogEntries() { - throw new ReferenceError("Cannot access 'discoverOpenClawPlugins' before initialization."); - }, + vi.doMock("../plugins/bundled-dir.js", () => ({ + resolveBundledPluginsDir: () => undefined, })); const catalog = await importFreshModule( diff --git a/src/channels/bundled-channel-catalog-read.ts b/src/channels/bundled-channel-catalog-read.ts index 007c8d1ee8d..8dc311512d5 100644 --- a/src/channels/bundled-channel-catalog-read.ts +++ b/src/channels/bundled-channel-catalog-read.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { tryReadJsonSync } from "../infra/json-files.js"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; -import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js"; +import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import type { PluginPackageChannel } from "../plugins/manifest.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; @@ -21,6 +21,7 @@ type BundledChannelCatalogEntry = { const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH = path.join("dist", "channel-catalog.json"); const officialCatalogFileCache = new Map(); +const bundledPackageCatalogCache = new Map(); function listPackageRoots(): string[] { return [ @@ -29,10 +30,28 @@ function listPackageRoots(): string[] { ].filter((entry, index, all): entry is string => Boolean(entry) && all.indexOf(entry) === index); } -function readBundledExtensionCatalogEntriesSync(): PluginPackageChannel[] { +function readBundledExtensionCatalogEntriesSync(): ChannelCatalogEntryLike[] { + const pluginsDir = resolveBundledPluginsDir(); + if (!pluginsDir) { + return []; + } + const cached = bundledPackageCatalogCache.get(pluginsDir); + if (cached !== undefined) { + return cached ?? []; + } try { - return listChannelCatalogEntries({ origin: "bundled" }).map((entry) => entry.channel); + const entries = fs + .readdirSync(pluginsDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .flatMap((entry): ChannelCatalogEntryLike[] => { + const packageJsonPath = path.join(pluginsDir, entry.name, "package.json"); + const parsed = tryReadJsonSync(packageJsonPath); + return parsed ? [parsed] : []; + }); + bundledPackageCatalogCache.set(pluginsDir, entries); + return entries; } catch { + bundledPackageCatalogCache.set(pluginsDir, null); return []; } } diff --git a/src/cli/program/register.subclis-core.ts b/src/cli/program/register.subclis-core.ts index 04c64af93c4..99fbd2d9a0f 100644 --- a/src/cli/program/register.subclis-core.ts +++ b/src/cli/program/register.subclis-core.ts @@ -65,14 +65,14 @@ async function registerSubCliWithPluginCommands( const invocation = resolveCliArgvInvocation(process.argv); const shouldRegisterPluginCommands = !invocation.hasHelpOrVersion && - (invocation.commandPath.length <= 1 || - resolveCliCommandPathPolicy(invocation.commandPath).loadPlugins !== "never"); - const { registerPluginCliCommandsFromValidatedConfig } = await import("../../plugins/cli.js"); + resolveCliCommandPathPolicy(invocation.commandPath).loadPlugins !== "never"; if (pluginCliPosition === "before" && shouldRegisterPluginCommands) { + const { registerPluginCliCommandsFromValidatedConfig } = await import("../../plugins/cli.js"); await registerPluginCliCommandsFromValidatedConfig(program); } await registerSubCli(); if (pluginCliPosition === "after" && shouldRegisterPluginCommands) { + const { registerPluginCliCommandsFromValidatedConfig } = await import("../../plugins/cli.js"); await registerPluginCliCommandsFromValidatedConfig(program); } } diff --git a/src/cli/program/register.subclis.test.ts b/src/cli/program/register.subclis.test.ts index 9cea56efd08..adc644116eb 100644 --- a/src/cli/program/register.subclis.test.ts +++ b/src/cli/program/register.subclis.test.ts @@ -253,13 +253,13 @@ describe("registerSubCliCommands", () => { expect(registerPluginCliCommandsFromValidatedConfig).not.toHaveBeenCalled(); }); - it("keeps plugin CLI registrations available for the plugins command root", async () => { + it("does not preload plugin CLI registrations for bare plugin parent help", async () => { process.argv = ["node", "openclaw", "plugins"]; const program = new Command().name("openclaw"); await registerSubCliByName(program, "plugins"); expect(registerPluginsCli).toHaveBeenCalledTimes(1); - expect(registerPluginCliCommandsFromValidatedConfig).toHaveBeenCalledTimes(1); + expect(registerPluginCliCommandsFromValidatedConfig).not.toHaveBeenCalled(); }); }); diff --git a/src/cli/run-main-policy.ts b/src/cli/run-main-policy.ts index 7a96b9409ca..9d63da97c71 100644 --- a/src/cli/run-main-policy.ts +++ b/src/cli/run-main-policy.ts @@ -18,6 +18,22 @@ import { import { isReservedNonPluginCommandRoot } from "./command-registration-policy.js"; const ROOT_HELP_ALIASES = new Set(["tools"]); +const BARE_PARENT_DEFAULT_HELP_COMMANDS = new Set([ + "approvals", + "channels", + "cron", + "devices", + "mcp", + "plugins", +]); + +function isBareParentDefaultHelpArgv(argv: string[]): boolean { + const invocation = resolveCliArgvInvocation(argv); + const [primary, extra] = invocation.commandPath; + return !invocation.hasHelpOrVersion && primary !== undefined && extra === undefined + ? BARE_PARENT_DEFAULT_HELP_COMMANDS.has(primary) + : false; +} export function rewriteUpdateFlagArgv(argv: string[]): string[] { const index = argv.indexOf("--update"); @@ -32,7 +48,11 @@ export function rewriteUpdateFlagArgv(argv: string[]): string[] { export function shouldEnsureCliPath(argv: string[]): boolean { const invocation = resolveCliArgvInvocation(argv); - if (invocation.hasHelpOrVersion || shouldStartCrestodianForBareRoot(argv)) { + if ( + invocation.hasHelpOrVersion || + shouldStartCrestodianForBareRoot(argv) || + isBareParentDefaultHelpArgv(argv) + ) { return false; } return resolveCliCommandPathPolicy(invocation.commandPath).ensureCliPath; @@ -91,7 +111,7 @@ export function shouldStartProxyForCli(argv: string[]): boolean { if (invocation.hasHelpOrVersion || !primary) { return false; } - if (invocation.commandPath.length === 1 && primary === "channels") { + if (isBareParentDefaultHelpArgv(policyArgv)) { return false; } return resolveCliNetworkProxyPolicy(policyArgv) === "default"; diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index b51b3538025..65b09a91855 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -114,6 +114,8 @@ describe("shouldEnsureCliPath", () => { it("skips path bootstrap for read-only fast paths", () => { expect(shouldEnsureCliPath(["node", "openclaw"])).toBe(false); expect(shouldEnsureCliPath(["node", "openclaw", "--profile", "work"])).toBe(false); + expect(shouldEnsureCliPath(["node", "openclaw", "plugins"])).toBe(false); + expect(shouldEnsureCliPath(["node", "openclaw", "mcp"])).toBe(false); expect(shouldEnsureCliPath(["node", "openclaw", "status"])).toBe(false); expect(shouldEnsureCliPath(["node", "openclaw", "--log-level", "debug", "status"])).toBe(false); expect(shouldEnsureCliPath(["node", "openclaw", "sessions", "--json"])).toBe(false); @@ -170,6 +172,13 @@ describe("shouldStartProxyForCli", () => { expect(shouldStartProxyForCli(["node", "openclaw", "--update"])).toBe(true); expect(shouldStartProxyForCli(["node", "openclaw", "--profile", "p", "--update"])).toBe(true); }); + + it("skips managed proxy routing for bare parent default help", () => { + expect(shouldStartProxyForCli(["node", "openclaw", "plugins"])).toBe(false); + expect(shouldStartProxyForCli(["node", "openclaw", "channels"])).toBe(false); + expect(shouldStartProxyForCli(["node", "openclaw", "devices"])).toBe(false); + expect(shouldStartProxyForCli(["node", "openclaw", "mcp"])).toBe(false); + }); }); describe("shouldUseRootHelpFastPath", () => {