diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f2b01064b..512f50b8cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/startup: read generated startup metadata from the bundled `dist` layout before falling back to live help rendering, so root/browser help and channel-option bootstrap stay on the fast path. Thanks @vincentkoc. - Matrix/E2EE: stabilize recovery and broken-device QA flows while avoiding Matrix device-cleanup sync races that could leave shutdown-time crypto work running. Thanks @gumadeiras. - Cron: classify isolated runs as errors from structured embedded-run execution-denial metadata, with final-output marker fallback for `SYSTEM_RUN_DENIED`, `INVALID_REQUEST`, and approval-binding refusals, so blocked commands no longer appear green in cron history. Fixes #67172; carries forward #67186. Thanks @oc-gh-dr, @hclsys, and @1yihui. - Onboarding/GitHub Copilot: add manifest-owned `--github-copilot-token` support for non-interactive setup, including env fallback, tokenRef storage in ref mode, saved-profile reuse, and current Copilot default-model wiring. Refs #50002 and supersedes #50003. Thanks @scottgl9. diff --git a/src/cli/channel-options.ts b/src/cli/channel-options.ts index 80b162afa5b..4459ada06fd 100644 --- a/src/cli/channel-options.ts +++ b/src/cli/channel-options.ts @@ -1,7 +1,5 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; import { CHAT_CHANNEL_ORDER } from "../channels/ids.js"; +import { readCliStartupMetadata } from "./startup-metadata.js"; function dedupe(values: string[]): string[] { const seen = new Set(); @@ -23,14 +21,8 @@ function loadPrecomputedChannelOptions(): string[] | null { return precomputedChannelOptions; } try { - const metadataPath = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "..", - "cli-startup-metadata.json", - ); - const raw = fs.readFileSync(metadataPath, "utf8"); - const parsed = JSON.parse(raw) as { channelOptions?: unknown }; - if (Array.isArray(parsed.channelOptions)) { + const parsed = readCliStartupMetadata(import.meta.url) as { channelOptions?: unknown } | null; + if (parsed && Array.isArray(parsed.channelOptions)) { precomputedChannelOptions = dedupe( parsed.channelOptions.filter((value): value is string => typeof value === "string"), ); diff --git a/src/cli/command-registration-policy.test.ts b/src/cli/command-registration-policy.test.ts index 463b2a258dc..8c6fe25bac4 100644 --- a/src/cli/command-registration-policy.test.ts +++ b/src/cli/command-registration-policy.test.ts @@ -36,6 +36,20 @@ describe("command-registration-policy", () => { hasBuiltinPrimary: false, }), ).toBe(false); + expect( + shouldSkipPluginCommandRegistration({ + argv: ["node", "openclaw", "help", "--help"], + primary: "help", + hasBuiltinPrimary: false, + }), + ).toBe(true); + expect( + shouldSkipPluginCommandRegistration({ + argv: ["node", "openclaw", "help", "voicecall"], + primary: "help", + hasBuiltinPrimary: false, + }), + ).toBe(false); }); it("matches lazy subcommand registration policy", () => { diff --git a/src/cli/command-registration-policy.ts b/src/cli/command-registration-policy.ts index f5a2b718380..638e87693eb 100644 --- a/src/cli/command-registration-policy.ts +++ b/src/cli/command-registration-policy.ts @@ -14,6 +14,9 @@ export function shouldSkipPluginCommandRegistration(params: { if (params.hasBuiltinPrimary) { return true; } + if (params.primary === "help" && resolveCliArgvInvocation(params.argv).hasHelpOrVersion) { + return true; + } if (!params.primary) { return resolveCliArgvInvocation(params.argv).hasHelpOrVersion; } diff --git a/src/cli/root-help-metadata.ts b/src/cli/root-help-metadata.ts index 2bd4431d663..1977fbc6d1e 100644 --- a/src/cli/root-help-metadata.ts +++ b/src/cli/root-help-metadata.ts @@ -1,6 +1,4 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { readCliStartupMetadata } from "./startup-metadata.js"; let precomputedRootHelpText: string | null | undefined; let precomputedBrowserHelpText: string | null | undefined; @@ -14,17 +12,13 @@ function loadPrecomputedHelpText( return cache; } try { - const metadataPath = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "..", - "cli-startup-metadata.json", - ); - const raw = fs.readFileSync(metadataPath, "utf8"); - const parsed = JSON.parse(raw) as Record; - const value = parsed[key]; - if (typeof value === "string" && value.length > 0) { - setCache(value); - return value; + const parsed = readCliStartupMetadata(import.meta.url); + if (parsed) { + const value = parsed[key]; + if (typeof value === "string" && value.length > 0) { + setCache(value); + return value; + } } } catch { // Fall back to live help rendering. diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index 3bab0f30d6b..49cd8d20657 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -146,8 +146,10 @@ describe("shouldUseRootHelpFastPath", () => { it("uses the fast path for root help only", () => { expect(shouldUseRootHelpFastPath(["node", "openclaw", "--help"])).toBe(true); expect(shouldUseRootHelpFastPath(["node", "openclaw", "--profile", "work", "-h"])).toBe(true); + expect(shouldUseRootHelpFastPath(["node", "openclaw", "help", "--help"])).toBe(true); expect(shouldUseRootHelpFastPath(["node", "openclaw", "status", "--help"])).toBe(false); expect(shouldUseRootHelpFastPath(["node", "openclaw", "--help", "status"])).toBe(false); + expect(shouldUseRootHelpFastPath(["node", "openclaw", "help", "gateway"])).toBe(false); }); }); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index f639b8a4e71..563d463d3f5 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -69,9 +69,13 @@ export function shouldEnsureCliPath(argv: string[]): boolean { } export function shouldUseRootHelpFastPath(argv: string[]): boolean { + const invocation = resolveCliArgvInvocation(argv); return ( process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH !== "1" && - resolveCliArgvInvocation(argv).isRootHelpInvocation + (invocation.isRootHelpInvocation || + (invocation.commandPath.length === 1 && + invocation.commandPath[0] === "help" && + invocation.hasHelpOrVersion)) ); } diff --git a/src/cli/startup-metadata.test.ts b/src/cli/startup-metadata.test.ts new file mode 100644 index 00000000000..f8ed8792c91 --- /dev/null +++ b/src/cli/startup-metadata.test.ts @@ -0,0 +1,16 @@ +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { describe, expect, it } from "vitest"; +import { __testing } from "./startup-metadata.js"; + +describe("startup metadata path resolution", () => { + it("checks metadata beside the bundled chunk before the legacy parent path", () => { + const moduleDir = path.resolve("dist"); + const moduleUrl = pathToFileURL(path.join(moduleDir, "root-help-metadata-abc123.js")).href; + + expect(__testing.resolveStartupMetadataPathCandidates(moduleUrl)).toEqual([ + path.join(moduleDir, "cli-startup-metadata.json"), + path.join(path.dirname(moduleDir), "cli-startup-metadata.json"), + ]); + }); +}); diff --git a/src/cli/startup-metadata.ts b/src/cli/startup-metadata.ts new file mode 100644 index 00000000000..3cc89087069 --- /dev/null +++ b/src/cli/startup-metadata.ts @@ -0,0 +1,28 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const STARTUP_METADATA_FILE = "cli-startup-metadata.json"; + +function resolveStartupMetadataPathCandidates(moduleUrl: string): string[] { + const moduleDir = path.dirname(fileURLToPath(moduleUrl)); + return [ + path.resolve(moduleDir, STARTUP_METADATA_FILE), + path.resolve(moduleDir, "..", STARTUP_METADATA_FILE), + ]; +} + +export function readCliStartupMetadata(moduleUrl: string): Record | null { + for (const metadataPath of resolveStartupMetadataPathCandidates(moduleUrl)) { + try { + return JSON.parse(fs.readFileSync(metadataPath, "utf8")) as Record; + } catch { + // Try the next bundled/source layout before falling back to dynamic startup work. + } + } + return null; +} + +export const __testing = { + resolveStartupMetadataPathCandidates, +};