fix(cli): keep startup help metadata on fast path

This commit is contained in:
Vincent Koc
2026-04-26 21:11:23 -07:00
parent 716b3faf7e
commit 4c3c3abe1a
9 changed files with 80 additions and 26 deletions

View File

@@ -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.

View File

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

View File

@@ -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", () => {

View File

@@ -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;
}

View File

@@ -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<string, unknown>;
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.

View File

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

View File

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

View File

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

View File

@@ -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<string, unknown> | null {
for (const metadataPath of resolveStartupMetadataPathCandidates(moduleUrl)) {
try {
return JSON.parse(fs.readFileSync(metadataPath, "utf8")) as Record<string, unknown>;
} catch {
// Try the next bundled/source layout before falling back to dynamic startup work.
}
}
return null;
}
export const __testing = {
resolveStartupMetadataPathCandidates,
};