perf: speed up secrets and nodes help startup (#84818)

Merged via squash.

Prepared head SHA: d65ae1bd58
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com>
Reviewed-by: @frankekn
This commit is contained in:
Frank Yang
2026-05-21 16:51:57 +08:00
committed by GitHub
parent e3b77d6d2c
commit 233765b361
14 changed files with 806 additions and 46 deletions
+1
View File
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
- Agents/Pi: disable the embedded pi-coding-agent runtime auto-retry so OpenClaw's own retry and failover loop does not replay failed tool calls through a nested SDK retry. Fixes #73781. (#74434) Thanks @yelog.
- CLI/perf: keep `setup --help`, `onboard --help`, and `configure --help` out of the full wizard runtime while preserving the existing help output. (#84488) Thanks @frankekn.
- CLI/perf: keep `agents --help` out of agents action/runtime imports so help, completion, and command discovery paths avoid loading the full agents runtime. (#84483) Thanks @frankekn.
- CLI/perf: keep `secrets --help` and `nodes --help` on the precomputed help path so parent help avoids loading action-heavy command runtime modules. (#84818) Thanks @frankekn.
## 2026.5.20
+23 -6
View File
@@ -328,8 +328,21 @@ const buildMissingEntryErrorMessage = async () => {
const isBareRootHelpInvocation = (argv) =>
argv.length === 3 && (argv[2] === "--help" || argv[2] === "-h");
const isBrowserHelpInvocation = (argv) =>
argv.length === 4 && argv[2] === "browser" && (argv[3] === "--help" || argv[3] === "-h");
const resolvePrecomputedCommandHelp = (argv) => {
if (argv.length !== 4 || (argv[3] !== "--help" && argv[3] !== "-h")) {
return null;
}
if (argv[2] === "browser") {
return { command: "browser", metadataKey: "browserHelpText" };
}
if (argv[2] === "secrets") {
return { command: "secrets", metadataKey: "secretsHelpText" };
}
if (argv[2] === "nodes") {
return { command: "nodes", metadataKey: "nodesHelpText" };
}
return null;
};
const isHelpFastPathDisabled = () =>
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";
@@ -440,11 +453,15 @@ const tryOutputBareRootHelp = async () => {
return false;
};
const tryOutputBrowserHelp = () => {
if (!isBrowserHelpInvocation(process.argv)) {
const tryOutputPrecomputedCommandHelp = () => {
const commandHelp = resolvePrecomputedCommandHelp(process.argv);
if (!commandHelp) {
return false;
}
const precomputed = loadPrecomputedHelpText("browserHelpText");
if (commandHelp.command === "nodes" && shouldDeferRootHelpToRuntimeEntry()) {
return false;
}
const precomputed = loadPrecomputedHelpText(commandHelp.metadataKey);
if (!precomputed) {
return false;
}
@@ -455,7 +472,7 @@ const tryOutputBrowserHelp = () => {
if (!waitingForCompileCacheRespawn) {
if (!isHelpFastPathDisabled() && (await tryOutputBareRootHelp())) {
// OK
} else if (!isHelpFastPathDisabled() && tryOutputBrowserHelp()) {
} else if (!isHelpFastPathDisabled() && tryOutputPrecomputedCommandHelp()) {
// OK
} else {
await installProcessWarningFilter();
+129 -12
View File
@@ -27,6 +27,7 @@ const outputPath = path.join(distDir, "cli-startup-metadata.json");
const extensionsDir = path.join(rootDir, "extensions");
const ROOT_HELP_RENDER_TIMEOUT_MS = 120_000;
const BROWSER_HELP_RENDER_TIMEOUT_MS = 120_000;
const COMMAND_HELP_RENDER_TIMEOUT_MS = 120_000;
const CORE_CHANNEL_ORDER = [
"telegram",
"whatsapp",
@@ -71,26 +72,74 @@ function resolveRootHelpBundleIdentity(
};
}
function updateHashFromFiles(hash: ReturnType<typeof createHash>, files: string[]) {
function updateHashFromFiles(
hash: ReturnType<typeof createHash>,
files: string[],
sourceRootDir: string = rootDir,
): void {
for (const file of files.toSorted()) {
hash.update(`${path.relative(rootDir, file)}\0`);
hash.update(`${path.relative(sourceRootDir, file)}\0`);
hash.update(readFileSync(file));
hash.update("\0");
}
}
function resolveBrowserHelpSourceSignature(): string {
function resolveBrowserHelpSourceSignature(sourceRootDir: string = rootDir): string {
const hash = createHash("sha1");
const browserCliDir = path.join(rootDir, "extensions/browser/src/cli");
const browserCliDir = path.join(sourceRootDir, "extensions/browser/src/cli");
const browserCliFiles = readdirSync(browserCliDir)
.filter((entry) => entry.endsWith(".ts"))
.map((entry) => path.join(browserCliDir, entry));
updateHashFromFiles(hash, browserCliFiles);
updateHashFromFiles(hash, [
path.join(rootDir, "src/cli/program/help.ts"),
path.join(rootDir, "src/cli/program/context.ts"),
path.join(rootDir, "src/cli/banner.ts"),
]);
updateHashFromFiles(hash, browserCliFiles, sourceRootDir);
updateHashFromFiles(
hash,
[
path.join(sourceRootDir, "src/cli/program/help.ts"),
path.join(sourceRootDir, "src/cli/program/context.ts"),
path.join(sourceRootDir, "src/cli/banner.ts"),
],
sourceRootDir,
);
return hash.digest("hex");
}
function resolveSecretsHelpSourceSignature(sourceRootDir: string = rootDir): string {
const hash = createHash("sha1");
updateHashFromFiles(
hash,
[
path.join(sourceRootDir, "src/cli/secrets-cli.ts"),
path.join(sourceRootDir, "src/cli/program/help.ts"),
path.join(sourceRootDir, "src/cli/program/context.ts"),
path.join(sourceRootDir, "src/cli/banner.ts"),
],
sourceRootDir,
);
return hash.digest("hex");
}
function resolveNodesHelpSourceSignature(sourceRootDir: string = rootDir): string {
const hash = createHash("sha1");
const nodesCliDir = path.join(sourceRootDir, "src/cli/nodes-cli");
const nodesCliFiles = readdirSync(nodesCliDir)
.filter((entry) => entry.endsWith(".ts") && !entry.endsWith(".test.ts"))
.map((entry) => path.join(nodesCliDir, entry));
updateHashFromFiles(hash, nodesCliFiles, sourceRootDir);
updateHashFromFiles(
hash,
[
path.join(sourceRootDir, "extensions/canvas/cli-metadata.ts"),
path.join(sourceRootDir, "extensions/canvas/index.ts"),
path.join(sourceRootDir, "extensions/canvas/src/a2ui-jsonl.ts"),
path.join(sourceRootDir, "extensions/canvas/src/cli-helpers.ts"),
path.join(sourceRootDir, "extensions/canvas/src/cli.ts"),
path.join(sourceRootDir, "src/cli/program/help.ts"),
path.join(sourceRootDir, "src/cli/program/context.ts"),
path.join(sourceRootDir, "src/cli/banner.ts"),
path.join(sourceRootDir, "src/plugins/register-plugin-cli-command-groups.ts"),
],
sourceRootDir,
);
return hash.digest("hex");
}
@@ -312,20 +361,68 @@ function renderSourceBrowserHelpText(
return result.stdout ?? "";
}
function renderSourceCommandHelpText(
command: "nodes" | "secrets",
renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
): string {
const result = spawnSync(
process.execPath,
["--import", "tsx", "openclaw.mjs", command, "--help"],
{
cwd: rootDir,
encoding: "utf8",
env: {
...renderContext.env,
OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1",
},
timeout: COMMAND_HELP_RENDER_TIMEOUT_MS,
},
);
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
const stderr = result.stderr?.trim();
throw new Error(
`Failed to render source ${command} help` +
(stderr ? `: ${stderr}` : result.signal ? `: terminated by ${result.signal}` : ""),
);
}
return result.stdout ?? "";
}
function renderSourceSecretsHelpText(
renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
): string {
return renderSourceCommandHelpText("secrets", renderContext);
}
function renderSourceNodesHelpText(
renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
): string {
return renderSourceCommandHelpText("nodes", renderContext);
}
export async function writeCliStartupMetadata(options?: {
distDir?: string;
outputPath?: string;
extensionsDir?: string;
sourceRootDir?: string;
renderBundledRootHelpText?: typeof renderBundledRootHelpText;
renderSourceRootHelpText?: typeof renderSourceRootHelpText;
renderSourceBrowserHelpText?: typeof renderSourceBrowserHelpText;
renderSourceSecretsHelpText?: typeof renderSourceSecretsHelpText;
renderSourceNodesHelpText?: typeof renderSourceNodesHelpText;
}): Promise<void> {
const resolvedDistDir = options?.distDir ?? distDir;
const resolvedOutputPath = options?.outputPath ?? outputPath;
const resolvedExtensionsDir = options?.extensionsDir ?? extensionsDir;
const resolvedSourceRootDir = options?.sourceRootDir ?? rootDir;
const channelCatalog = readBundledChannelCatalog(resolvedExtensionsDir);
const bundleIdentity = resolveRootHelpBundleIdentity(resolvedDistDir);
const browserHelpSourceSignature = resolveBrowserHelpSourceSignature();
const browserHelpSourceSignature = resolveBrowserHelpSourceSignature(resolvedSourceRootDir);
const secretsHelpSourceSignature = resolveSecretsHelpSourceSignature(resolvedSourceRootDir);
const nodesHelpSourceSignature = resolveNodesHelpSourceSignature(resolvedSourceRootDir);
const bundledPluginsDir = path.join(resolvedDistDir, "extensions");
const renderContext = createIsolatedRootHelpRenderContext(
existsSync(bundledPluginsDir) ? bundledPluginsDir : resolvedExtensionsDir,
@@ -336,16 +433,26 @@ export async function writeCliStartupMetadata(options?: {
const existing = JSON.parse(readFileSync(resolvedOutputPath, "utf8")) as {
rootHelpBundleSignature?: unknown;
browserHelpSourceSignature?: unknown;
secretsHelpSourceSignature?: unknown;
nodesHelpSourceSignature?: unknown;
channelCatalogSignature?: unknown;
browserHelpText?: unknown;
secretsHelpText?: unknown;
nodesHelpText?: unknown;
};
if (
bundleIdentity &&
existing.rootHelpBundleSignature === bundleIdentity.signature &&
existing.browserHelpSourceSignature === browserHelpSourceSignature &&
existing.secretsHelpSourceSignature === secretsHelpSourceSignature &&
existing.nodesHelpSourceSignature === nodesHelpSourceSignature &&
existing.channelCatalogSignature === channelCatalog.signature &&
typeof existing.browserHelpText === "string" &&
existing.browserHelpText.length > 0
existing.browserHelpText.length > 0 &&
typeof existing.secretsHelpText === "string" &&
existing.secretsHelpText.length > 0 &&
typeof existing.nodesHelpText === "string" &&
existing.nodesHelpText.length > 0
) {
return;
}
@@ -365,6 +472,12 @@ export async function writeCliStartupMetadata(options?: {
const browserHelpText = (options?.renderSourceBrowserHelpText ?? renderSourceBrowserHelpText)(
renderContext,
);
const secretsHelpText = (options?.renderSourceSecretsHelpText ?? renderSourceSecretsHelpText)(
renderContext,
);
const nodesHelpText = (options?.renderSourceNodesHelpText ?? renderSourceNodesHelpText)(
renderContext,
);
mkdirSync(resolvedDistDir, { recursive: true });
writeFileSync(
@@ -376,7 +489,11 @@ export async function writeCliStartupMetadata(options?: {
channelCatalogSignature: channelCatalog.signature,
rootHelpBundleSignature: bundleIdentity?.signature ?? null,
browserHelpSourceSignature,
secretsHelpSourceSignature,
nodesHelpSourceSignature,
browserHelpText,
secretsHelpText,
nodesHelpText,
rootHelpText,
},
null,
+50
View File
@@ -184,6 +184,42 @@ vi.mock("../commands/agents.commands.list.js", () => {
return { agentsListCommand: vi.fn(async () => {}) };
});
vi.mock("@clack/prompts", () => {
loaded.mark("clack-prompts");
return {
confirm: vi.fn(async () => true),
};
});
vi.mock("../secrets/apply.js", () => {
loaded.mark("secrets-apply-runtime");
return {
runSecretsApply: vi.fn(async () => ({})),
};
});
vi.mock("../secrets/audit.js", () => {
loaded.mark("secrets-audit-runtime");
return {
resolveSecretsAuditExitCode: vi.fn(() => 0),
runSecretsAudit: vi.fn(async () => ({})),
};
});
vi.mock("../secrets/configure.js", () => {
loaded.mark("secrets-configure-runtime");
return {
runSecretsConfigureInteractive: vi.fn(async () => ({})),
};
});
vi.mock("../secrets/plan.js", () => {
loaded.mark("secrets-plan-runtime");
return {
isSecretsApplyPlan: vi.fn(() => true),
};
});
function makeProgram(): Command {
const program = new Command();
program.name("openclaw");
@@ -297,4 +333,18 @@ describe("subcommand help cold imports", () => {
expect(loaded.modules).not.toContain("agents-identity-command");
expect(loaded.modules).not.toContain("agents-list-command");
});
it("keeps secrets help out of secrets action modules", async () => {
const { registerSecretsCli } = await import("./secrets-cli.js");
const program = makeProgram();
registerSecretsCli(program);
await expectHelpExit(program, ["secrets", "--help"]);
expect(loaded.modules).not.toContain("clack-prompts");
expect(loaded.modules).not.toContain("secrets-apply-runtime");
expect(loaded.modules).not.toContain("secrets-audit-runtime");
expect(loaded.modules).not.toContain("secrets-configure-runtime");
expect(loaded.modules).not.toContain("secrets-plan-runtime");
});
});
+41 -1
View File
@@ -2,9 +2,17 @@ import { readCliStartupMetadata } from "./startup-metadata.js";
let precomputedRootHelpText: string | null | undefined;
let precomputedBrowserHelpText: string | null | undefined;
let precomputedSecretsHelpText: string | null | undefined;
let precomputedNodesHelpText: string | null | undefined;
type PrecomputedHelpTextKey =
| "rootHelpText"
| "browserHelpText"
| "secretsHelpText"
| "nodesHelpText";
function loadPrecomputedHelpText(
key: "rootHelpText" | "browserHelpText",
key: PrecomputedHelpTextKey,
cache: string | null | undefined,
setCache: (value: string | null) => void,
): string | null {
@@ -39,6 +47,18 @@ export function loadPrecomputedBrowserHelpText(): string | null {
});
}
export function loadPrecomputedSecretsHelpText(): string | null {
return loadPrecomputedHelpText("secretsHelpText", precomputedSecretsHelpText, (value) => {
precomputedSecretsHelpText = value;
});
}
export function loadPrecomputedNodesHelpText(): string | null {
return loadPrecomputedHelpText("nodesHelpText", precomputedNodesHelpText, (value) => {
precomputedNodesHelpText = value;
});
}
export function outputPrecomputedRootHelpText(): boolean {
const rootHelpText = loadPrecomputedRootHelpText();
if (!rootHelpText) {
@@ -57,10 +77,30 @@ export function outputPrecomputedBrowserHelpText(): boolean {
return true;
}
export function outputPrecomputedSecretsHelpText(): boolean {
const secretsHelpText = loadPrecomputedSecretsHelpText();
if (!secretsHelpText) {
return false;
}
process.stdout.write(secretsHelpText);
return true;
}
export function outputPrecomputedNodesHelpText(): boolean {
const nodesHelpText = loadPrecomputedNodesHelpText();
if (!nodesHelpText) {
return false;
}
process.stdout.write(nodesHelpText);
return true;
}
export const testing = {
resetPrecomputedRootHelpTextForTests(): void {
precomputedRootHelpText = undefined;
precomputedBrowserHelpText = undefined;
precomputedSecretsHelpText = undefined;
precomputedNodesHelpText = undefined;
},
};
export { testing as __testing };
+32 -1
View File
@@ -11,6 +11,7 @@ import {
normalizeOptionalLowercaseString,
} from "../shared/string-coerce.js";
import { resolveCliArgvInvocation } from "./argv-invocation.js";
import { hasFlag } from "./argv.js";
import {
resolveCliCommandPathPolicy,
resolveCliNetworkProxyPolicy,
@@ -85,7 +86,37 @@ export function shouldUseBrowserHelpFastPath(
return (
invocation.commandPath.length === 1 &&
invocation.commandPath[0] === "browser" &&
invocation.hasHelpOrVersion
(hasFlag(argv, "--help") || hasFlag(argv, "-h"))
);
}
export function shouldUseSecretsHelpFastPath(
argv: string[],
env: NodeJS.ProcessEnv = process.env,
): boolean {
if (env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1") {
return false;
}
const invocation = resolveCliArgvInvocation(argv);
return (
invocation.commandPath.length === 1 &&
invocation.commandPath[0] === "secrets" &&
(hasFlag(argv, "--help") || hasFlag(argv, "-h"))
);
}
export function shouldUseNodesHelpFastPath(
argv: string[],
env: NodeJS.ProcessEnv = process.env,
): boolean {
if (env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1") {
return false;
}
const invocation = resolveCliArgvInvocation(argv);
return (
invocation.commandPath.length === 1 &&
invocation.commandPath[0] === "nodes" &&
(hasFlag(argv, "--help") || hasFlag(argv, "-h"))
);
}
+47
View File
@@ -19,6 +19,8 @@ const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
const outputRootHelpMock = vi.hoisted(() => vi.fn());
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const outputPrecomputedBrowserHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const outputPrecomputedSecretsHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const outputPrecomputedNodesHelpTextMock = vi.hoisted(() => vi.fn(() => false));
const loadRootHelpRenderOptionsForConfigSensitivePluginsMock = vi.hoisted(() =>
vi.fn<() => Promise<RootHelpRenderOptions | null>>(async () => null),
);
@@ -170,7 +172,9 @@ vi.mock("./program/root-help.js", () => ({
vi.mock("./root-help-metadata.js", () => ({
outputPrecomputedBrowserHelpText: outputPrecomputedBrowserHelpTextMock,
outputPrecomputedNodesHelpText: outputPrecomputedNodesHelpTextMock,
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
outputPrecomputedSecretsHelpText: outputPrecomputedSecretsHelpTextMock,
}));
vi.mock("./root-help-live-config.js", () => ({
@@ -255,7 +259,9 @@ describe("runCli exit behavior", () => {
hasMemoryRuntimeMock.mockReturnValue(false);
listAgentHarnessIdsMock.mockReturnValue([]);
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
outputPrecomputedNodesHelpTextMock.mockReturnValue(false);
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
outputPrecomputedSecretsHelpTextMock.mockReturnValue(false);
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValue(null);
tryOutputSetupOnboardConfigureHelpMock.mockResolvedValue(true);
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false);
@@ -412,6 +418,47 @@ describe("runCli exit behavior", () => {
exitSpy.mockRestore();
});
it("renders secrets help from startup metadata without building the full program", async () => {
outputPrecomputedSecretsHelpTextMock.mockReturnValueOnce(true);
await runCli(["node", "openclaw", "secrets", "--help"]);
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(outputPrecomputedSecretsHelpTextMock).toHaveBeenCalledTimes(1);
expect(buildProgramMock).not.toHaveBeenCalled();
expect(registerSubCliByNameMock).not.toHaveBeenCalled();
});
it("renders nodes help from startup metadata without building the full program", async () => {
outputPrecomputedNodesHelpTextMock.mockReturnValueOnce(true);
await runCli(["node", "openclaw", "nodes", "--help"]);
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(outputPrecomputedNodesHelpTextMock).toHaveBeenCalledTimes(1);
expect(buildProgramMock).not.toHaveBeenCalled();
expect(registerSubCliByNameMock).not.toHaveBeenCalled();
});
it("defers nodes help startup metadata when plugin config can change command metadata", async () => {
const argv = ["node", "openclaw", "nodes", "--help"];
const parseAsync = vi.fn().mockResolvedValueOnce(undefined);
const program = {
commands: [{ name: () => "nodes", aliases: () => [] }],
parseAsync,
};
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValueOnce({ env: {} });
outputPrecomputedNodesHelpTextMock.mockReturnValueOnce(true);
buildProgramMock.mockReturnValueOnce(program);
await runCli(argv);
expect(loadRootHelpRenderOptionsForConfigSensitivePluginsMock).toHaveBeenCalledTimes(1);
expect(outputPrecomputedNodesHelpTextMock).not.toHaveBeenCalled();
expect(registerSubCliByNameMock.mock.calls).toEqual([[program, "nodes", argv]]);
expect(parseAsync).toHaveBeenCalledWith(argv);
});
it("keeps root help on the precomputed path without proxy bootstrap", async () => {
outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true);
+21
View File
@@ -8,7 +8,9 @@ import {
shouldStartCrestodianForModernOnboard,
shouldStartProxyForCli,
shouldUseBrowserHelpFastPath,
shouldUseNodesHelpFastPath,
shouldUseRootHelpFastPath,
shouldUseSecretsHelpFastPath,
shouldUseSetupOnboardConfigureHelpFastPath,
} from "./run-main-policy.js";
import { isGatewayRunFastPathArgv } from "./run-main.js";
@@ -210,6 +212,25 @@ describe("shouldUseBrowserHelpFastPath", () => {
false,
);
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "status", "--help"])).toBe(false);
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "browser", "--version"])).toBe(false);
});
});
describe("parent command help fast paths", () => {
it("use fast paths for secrets and nodes parent help only", () => {
expect(shouldUseSecretsHelpFastPath(["node", "openclaw", "secrets", "--help"])).toBe(true);
expect(shouldUseSecretsHelpFastPath(["node", "openclaw", "secrets", "-h"])).toBe(true);
expect(shouldUseSecretsHelpFastPath(["node", "openclaw", "secrets", "--version"])).toBe(false);
expect(shouldUseSecretsHelpFastPath(["node", "openclaw", "secrets", "audit", "--help"])).toBe(
false,
);
expect(shouldUseNodesHelpFastPath(["node", "openclaw", "nodes", "--help"])).toBe(true);
expect(shouldUseNodesHelpFastPath(["node", "openclaw", "nodes", "-h"])).toBe(true);
expect(shouldUseNodesHelpFastPath(["node", "openclaw", "nodes", "--version"])).toBe(false);
expect(shouldUseNodesHelpFastPath(["node", "openclaw", "nodes", "invoke", "--help"])).toBe(
false,
);
});
});
+30 -13
View File
@@ -22,10 +22,7 @@ import {
consumeGatewayFastPathRootOptionToken,
consumeGatewayRunOptionToken,
} from "./gateway-run-argv.js";
import {
hasJsonOutputFlag,
withConsoleLogsRoutedToStderrForJson,
} from "./json-output-mode.js";
import { hasJsonOutputFlag, withConsoleLogsRoutedToStderrForJson } from "./json-output-mode.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
import { getCoreCliCommandNames } from "./program/core-command-descriptors.js";
import { getSubCliEntries } from "./program/subcli-descriptors.js";
@@ -37,7 +34,9 @@ import {
shouldStartCrestodianForModernOnboard,
shouldStartProxyForCli,
shouldUseBrowserHelpFastPath,
shouldUseNodesHelpFastPath,
shouldUseRootHelpFastPath,
shouldUseSecretsHelpFastPath,
shouldUseSetupOnboardConfigureHelpFastPath,
} from "./run-main-policy.js";
import { normalizeWindowsArgv } from "./windows-argv.js";
@@ -49,7 +48,9 @@ export {
shouldStartCrestodianForModernOnboard,
shouldStartProxyForCli,
shouldUseBrowserHelpFastPath,
shouldUseNodesHelpFastPath,
shouldUseRootHelpFastPath,
shouldUseSecretsHelpFastPath,
shouldUseSetupOnboardConfigureHelpFastPath,
} from "./run-main-policy.js";
@@ -559,6 +560,27 @@ export async function runCli(argv: string[] = process.argv) {
}
}
if (shouldUseSecretsHelpFastPath(normalizedArgv)) {
const { outputPrecomputedSecretsHelpText } = await import("./root-help-metadata.js");
if (outputPrecomputedSecretsHelpText()) {
return;
}
}
if (shouldUseNodesHelpFastPath(normalizedArgv)) {
const { loadRootHelpRenderOptionsForConfigSensitivePlugins } =
await import("./root-help-live-config.js");
const liveRootHelpOptions = await loadRootHelpRenderOptionsForConfigSensitivePlugins(
process.env,
);
if (!liveRootHelpOptions) {
const { outputPrecomputedNodesHelpText } = await import("./root-help-metadata.js");
if (outputPrecomputedNodesHelpText()) {
return;
}
}
}
const shouldRunBareRootCrestodian = shouldStartCrestodianForBareRoot(normalizedArgv);
const shouldRunModernOnboardCrestodian = shouldStartCrestodianForModernOnboard(normalizedArgv);
if (shouldRunBareRootCrestodian || shouldRunModernOnboardCrestodian) {
@@ -743,15 +765,10 @@ export async function runCli(argv: string[] = process.argv) {
const { registerPluginCliCommandsFromValidatedConfig } =
await import("../plugins/cli.js");
return await withConsoleLogsRoutedToStderrForJson(parseArgv, () =>
registerPluginCliCommandsFromValidatedConfig(
program,
undefined,
undefined,
{
mode: "lazy",
primary,
},
),
registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, {
mode: "lazy",
primary,
}),
);
});
if (config) {
+20 -11
View File
@@ -1,13 +1,8 @@
import fs from "node:fs";
import { confirm } from "@clack/prompts";
import type { Command } from "commander";
import { danger } from "../globals.js";
import { formatErrorMessage } from "../infra/errors.js";
import { defaultRuntime } from "../runtime.js";
import { runSecretsApply } from "../secrets/apply.js";
import { resolveSecretsAuditExitCode, runSecretsAudit } from "../secrets/audit.js";
import { runSecretsConfigureInteractive } from "../secrets/configure.js";
import { isSecretsApplyPlan, type SecretsApplyPlan } from "../secrets/plan.js";
import type { SecretsApplyPlan } from "../secrets/plan.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
import { formatCliCommand } from "./command-format.js";
@@ -37,8 +32,12 @@ type SecretsApplyOptions = {
json?: boolean;
};
function readPlanFile(pathname: string): SecretsApplyPlan {
const raw = fs.readFileSync(pathname, "utf8");
async function readPlanFile(pathname: string): Promise<SecretsApplyPlan> {
const [{ readFileSync }, { isSecretsApplyPlan }] = await Promise.all([
import("node:fs"),
import("../secrets/plan.js"),
]);
const raw = readFileSync(pathname, "utf8");
const parsed = JSON.parse(raw) as unknown;
if (!isSecretsApplyPlan(parsed)) {
throw new Error(
@@ -48,7 +47,7 @@ function readPlanFile(pathname: string): SecretsApplyPlan {
return parsed;
}
export function registerSecretsCli(program: Command) {
export function registerSecretsCli(program: Command): void {
const secrets = program
.command("secrets")
.description("Secrets runtime controls")
@@ -106,6 +105,8 @@ export function registerSecretsCli(program: Command) {
.option("--json", "Output JSON", false)
.action(async (opts: SecretsAuditOptions) => {
try {
const { resolveSecretsAuditExitCode, runSecretsAudit } =
await import("../secrets/audit.js");
const report = await runSecretsAudit({
allowExec: Boolean(opts.allowExec),
});
@@ -169,6 +170,7 @@ export function registerSecretsCli(program: Command) {
.option("--json", "Output JSON", false)
.action(async (opts: SecretsConfigureOptions) => {
try {
const { runSecretsConfigureInteractive } = await import("../secrets/configure.js");
const configured = await runSecretsConfigureInteractive({
providersOnly: Boolean(opts.providersOnly),
skipProviderSetup: Boolean(opts.skipProviderSetup),
@@ -176,7 +178,8 @@ export function registerSecretsCli(program: Command) {
allowExecInPreflight: Boolean(opts.allowExec),
});
if (opts.planOut) {
fs.writeFileSync(opts.planOut, `${JSON.stringify(configured.plan, null, 2)}\n`, "utf8");
const { writeFileSync } = await import("node:fs");
writeFileSync(opts.planOut, `${JSON.stringify(configured.plan, null, 2)}\n`, "utf8");
}
if (opts.json) {
defaultRuntime.writeJson({
@@ -212,6 +215,7 @@ export function registerSecretsCli(program: Command) {
let shouldApply = Boolean(opts.apply);
if (!shouldApply && !opts.json) {
const { confirm } = await import("@clack/prompts");
const approved = await confirm({
message: "Apply this plan now?",
initialValue: true,
@@ -223,6 +227,7 @@ export function registerSecretsCli(program: Command) {
if (shouldApply) {
const needsIrreversiblePrompt = Boolean(opts.apply);
if (needsIrreversiblePrompt && !opts.yes && !opts.json) {
const { confirm } = await import("@clack/prompts");
const confirmed = await confirm({
message:
"This migration is one-way for migrated plaintext values. Continue with apply?",
@@ -233,6 +238,7 @@ export function registerSecretsCli(program: Command) {
return;
}
}
const { runSecretsApply } = await import("../secrets/apply.js");
const result = await runSecretsApply({
plan: configured.plan,
write: true,
@@ -267,7 +273,10 @@ export function registerSecretsCli(program: Command) {
.option("--json", "Output JSON", false)
.action(async (opts: SecretsApplyOptions) => {
try {
const plan = readPlanFile(opts.from);
const [{ runSecretsApply }, plan] = await Promise.all([
import("../secrets/apply.js"),
readPlanFile(opts.from),
]);
const result = await runSecretsApply({
plan,
write: !opts.dryRun,
+192 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { tryHandleRootHelpFastPath } from "./entry.js";
import { tryHandlePrecomputedCommandHelpFastPath, tryHandleRootHelpFastPath } from "./entry.js";
describe("entry root help fast path", () => {
it("prefers precomputed root help text when available", async () => {
@@ -97,3 +97,194 @@ describe("entry root help fast path", () => {
expect(outputRootHelpCalls).toBe(0);
});
});
describe("entry precomputed command help fast path", () => {
it("renders browser help from startup metadata without importing the full program", async () => {
let outputPrecomputedBrowserHelpTextCalls = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "browser", "--help"],
{
env: {},
outputPrecomputedBrowserHelpText: () => {
outputPrecomputedBrowserHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(true);
expect(outputPrecomputedBrowserHelpTextCalls).toBe(1);
});
it("renders secrets help from startup metadata without importing the full program", async () => {
let outputPrecomputedSecretsHelpTextCalls = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "secrets", "--help"],
{
env: {},
outputPrecomputedSecretsHelpText: () => {
outputPrecomputedSecretsHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(true);
expect(outputPrecomputedSecretsHelpTextCalls).toBe(1);
});
it("renders nodes help from startup metadata without importing the full program", async () => {
let outputPrecomputedNodesHelpTextCalls = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "nodes", "--help"],
{
env: {},
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => null,
outputPrecomputedNodesHelpText: () => {
outputPrecomputedNodesHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(true);
expect(outputPrecomputedNodesHelpTextCalls).toBe(1);
});
it("defers nodes help when plugin config can change command metadata", async () => {
let outputPrecomputedNodesHelpTextCalls = 0;
let liveConfigChecks = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "nodes", "--help"],
{
env: {},
loadRootHelpRenderOptionsForConfigSensitivePlugins: async () => {
liveConfigChecks += 1;
return { env: {} };
},
outputPrecomputedNodesHelpText: () => {
outputPrecomputedNodesHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(false);
expect(liveConfigChecks).toBe(1);
expect(outputPrecomputedNodesHelpTextCalls).toBe(0);
});
it("falls through when startup metadata is unavailable", async () => {
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "secrets", "--help"],
{
env: {},
outputPrecomputedSecretsHelpText: () => false,
},
);
expect(handled).toBe(false);
});
it("ignores nested subcommand help invocations", async () => {
let outputPrecomputedNodesHelpTextCalls = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "nodes", "invoke", "--help"],
{
env: {},
outputPrecomputedNodesHelpText: () => {
outputPrecomputedNodesHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(false);
expect(outputPrecomputedNodesHelpTextCalls).toBe(0);
});
it("ignores command version invocations", async () => {
let outputPrecomputedNodesHelpTextCalls = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "nodes", "--version"],
{
env: {},
outputPrecomputedNodesHelpText: () => {
outputPrecomputedNodesHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(false);
expect(outputPrecomputedNodesHelpTextCalls).toBe(0);
});
it("respects the startup help fast path kill switch", async () => {
let outputPrecomputedSecretsHelpTextCalls = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "secrets", "--help"],
{
env: { OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1" },
outputPrecomputedSecretsHelpText: () => {
outputPrecomputedSecretsHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(false);
expect(outputPrecomputedSecretsHelpTextCalls).toBe(0);
});
it("respects the process env startup help fast path kill switch", async () => {
let outputPrecomputedSecretsHelpTextCalls = 0;
const original = process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH = "1";
try {
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "secrets", "--help"],
{
outputPrecomputedSecretsHelpText: () => {
outputPrecomputedSecretsHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(false);
expect(outputPrecomputedSecretsHelpTextCalls).toBe(0);
} finally {
if (original === undefined) {
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
} else {
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH = original;
}
}
});
it("skips the host command help fast path when a container target is active", async () => {
let outputPrecomputedSecretsHelpTextCalls = 0;
const handled = await tryHandlePrecomputedCommandHelpFastPath(
["node", "openclaw", "--container", "demo", "secrets", "--help"],
{
env: {},
outputPrecomputedSecretsHelpText: () => {
outputPrecomputedSecretsHelpTextCalls += 1;
return true;
},
},
);
expect(handled).toBe(false);
expect(outputPrecomputedSecretsHelpTextCalls).toBe(0);
});
});
+78 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import process from "node:process";
import { fileURLToPath } from "node:url";
import { isRootHelpInvocation } from "./cli/argv.js";
import { getCommandPathWithRootOptions, hasFlag, isRootHelpInvocation } from "./cli/argv.js";
import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
import type { RootHelpRenderOptions } from "./cli/program/root-help.js";
@@ -23,6 +23,9 @@ const ENTRY_WRAPPER_PAIRS = [
{ wrapperBasename: "openclaw.js", entryBasename: "entry.js" },
] as const;
type PrecomputedCommandHelpName = "browser" | "secrets" | "nodes";
type OutputPrecomputedHelpText = () => boolean;
function shouldForceReadOnlyAuthStore(argv: string[]): boolean {
const tokens = argv.slice(2).filter((token) => token.length > 0 && !token.startsWith("-"));
for (let index = 0; index < tokens.length - 1; index += 1) {
@@ -205,10 +208,84 @@ export async function tryHandleRootHelpFastPath(
}
}
function resolvePrecomputedCommandHelpName(argv: string[]): PrecomputedCommandHelpName | null {
if (!hasFlag(argv, "--help") && !hasFlag(argv, "-h")) {
return null;
}
const commandPath = getCommandPathWithRootOptions(argv, 2);
if (commandPath.length !== 1) {
return null;
}
const [commandName] = commandPath;
if (commandName === "browser" || commandName === "secrets" || commandName === "nodes") {
return commandName;
}
return null;
}
export async function tryHandlePrecomputedCommandHelpFastPath(
argv: string[],
deps: {
outputPrecomputedBrowserHelpText?: OutputPrecomputedHelpText;
outputPrecomputedSecretsHelpText?: OutputPrecomputedHelpText;
outputPrecomputedNodesHelpText?: OutputPrecomputedHelpText;
loadRootHelpRenderOptionsForConfigSensitivePlugins?: (
env?: NodeJS.ProcessEnv,
) => Promise<RootHelpRenderOptions | null>;
env?: NodeJS.ProcessEnv;
} = {},
): Promise<boolean> {
const env = deps.env ?? process.env;
if (env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1") {
return false;
}
if (resolveCliContainerTarget(argv, env)) {
return false;
}
const commandName = resolvePrecomputedCommandHelpName(argv);
if (!commandName) {
return false;
}
try {
if (commandName === "nodes") {
const loadRootHelpRenderOptionsForConfigSensitivePlugins =
deps.loadRootHelpRenderOptionsForConfigSensitivePlugins ??
(await import("./cli/root-help-live-config.js"))
.loadRootHelpRenderOptionsForConfigSensitivePlugins;
const liveRootHelpOptions = await loadRootHelpRenderOptionsForConfigSensitivePlugins(env);
if (liveRootHelpOptions) {
return false;
}
}
if (commandName === "browser") {
const outputPrecomputedBrowserHelpText =
deps.outputPrecomputedBrowserHelpText ??
(await import("./cli/root-help-metadata.js")).outputPrecomputedBrowserHelpText;
return outputPrecomputedBrowserHelpText();
}
if (commandName === "secrets") {
const outputPrecomputedSecretsHelpText =
deps.outputPrecomputedSecretsHelpText ??
(await import("./cli/root-help-metadata.js")).outputPrecomputedSecretsHelpText;
return outputPrecomputedSecretsHelpText();
}
const outputPrecomputedNodesHelpText =
deps.outputPrecomputedNodesHelpText ??
(await import("./cli/root-help-metadata.js")).outputPrecomputedNodesHelpText;
return outputPrecomputedNodesHelpText();
} catch {
return false;
}
}
async function runMainOrRootHelp(argv: string[]): Promise<void> {
if (await tryHandleRootHelpFastPath(argv)) {
return;
}
if (await tryHandlePrecomputedCommandHelpFastPath(argv)) {
return;
}
try {
const { runCli } = await gatewayEntryStartupTrace.measure(
"run-main-import",
+60
View File
@@ -221,6 +221,32 @@ describe("openclaw launcher", () => {
expect(result.stdout).toBe("PRECOMPUTED help\n");
});
it.each([
{ command: "browser", metadataKey: "browserHelpText" },
{ command: "secrets", metadataKey: "secretsHelpText" },
{ command: "nodes", metadataKey: "nodesHelpText" },
])("uses precomputed $command help before loading the runtime entry", async (params) => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await fs.writeFile(
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
JSON.stringify({ [params.metadataKey]: `PRECOMPUTED ${params.command} help\n` }),
"utf8",
);
const result = spawnSync(
process.execPath,
[path.join(fixtureRoot, "openclaw.mjs"), params.command, "--help"],
{
cwd: fixtureRoot,
env: launcherEnv(),
encoding: "utf8",
},
);
expect(result.status).toBe(0);
expect(result.stdout).toBe(`PRECOMPUTED ${params.command} help\n`);
});
it("defers root help to the runtime entry when plugin config can change help", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
const configPath = path.join(fixtureRoot, "openclaw.json");
@@ -251,6 +277,40 @@ describe("openclaw launcher", () => {
expect(result.stdout).not.toContain("PRECOMPUTED");
});
it("defers nodes help to the runtime entry when plugin config can change help", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
const configPath = path.join(fixtureRoot, "openclaw.json");
await fs.writeFile(
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
JSON.stringify({ nodesHelpText: "PRECOMPUTED nodes help\n" }),
"utf8",
);
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
"process.stdout.write('RUNTIME ENTRY\\n');\n",
"utf8",
);
await fs.writeFile(
configPath,
JSON.stringify({ plugins: { entries: { canvas: { enabled: false } } } }),
"utf8",
);
const result = spawnSync(
process.execPath,
[path.join(fixtureRoot, "openclaw.mjs"), "nodes", "--help"],
{
cwd: fixtureRoot,
env: launcherEnv({ OPENCLAW_CONFIG_PATH: configPath }),
encoding: "utf8",
},
);
expect(result.status).toBe(0);
expect(result.stdout).toBe("RUNTIME ENTRY\n");
expect(result.stdout).not.toContain("PRECOMPUTED");
});
it("checks the OPENCLAW_HOME default config path before using precomputed root help", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
const openclawHome = path.join(fixtureRoot, "home");
@@ -4,6 +4,35 @@ import { describe, expect, it } from "vitest";
import { writeCliStartupMetadata } from "../../scripts/write-cli-startup-metadata.ts";
import { createScriptTestHarness } from "./test-helpers.js";
function writeFixtureFile(rootDir: string, relativePath: string, contents: string): void {
const filePath = path.join(rootDir, relativePath);
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, contents, "utf8");
}
function writeStartupMetadataSourceSignatureFixture(rootDir: string): void {
const fixtures = new Map<string, string>([
["extensions/browser/src/cli/browser-cli.ts", "export const browserHelp = 'browser';\n"],
["extensions/canvas/cli-metadata.ts", "export const canvasMetadata = 'canvas';\n"],
["extensions/canvas/index.ts", "export const canvasEntry = 'canvas';\n"],
["extensions/canvas/src/a2ui-jsonl.ts", "export const a2uiJsonl = 'canvas';\n"],
["extensions/canvas/src/cli-helpers.ts", "export const canvasHelpers = 'canvas';\n"],
["extensions/canvas/src/cli.ts", "export const canvasCliHelp = 'canvas';\n"],
["src/cli/banner.ts", "export const banner = 'openclaw';\n"],
["src/cli/nodes-cli/register.ts", "export const nodesHelp = 'nodes';\n"],
["src/cli/program/context.ts", "export const context = 'context';\n"],
["src/cli/program/help.ts", "export const help = 'help';\n"],
[
"src/plugins/register-plugin-cli-command-groups.ts",
"export const pluginCommandGroups = 'plugins';\n",
],
["src/cli/secrets-cli.ts", "export const secretsHelp = 'secrets';\n"],
]);
for (const [relativePath, contents] of fixtures) {
writeFixtureFile(rootDir, relativePath, contents);
}
}
describe("write-cli-startup-metadata", () => {
const { createTempDir } = createScriptTestHarness();
@@ -38,17 +67,70 @@ describe("write-cli-startup-metadata", () => {
},
renderSourceRootHelpText: () => "Usage: openclaw\n",
renderSourceBrowserHelpText: () => "Usage: openclaw browser\n",
renderSourceSecretsHelpText: () => "Usage: openclaw secrets\n",
renderSourceNodesHelpText: () => "Usage: openclaw nodes\n",
});
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
browserHelpText: string;
channelOptions: string[];
nodesHelpText: string;
rootHelpText: string;
secretsHelpText: string;
};
expect(written.channelOptions).toContain("matrix");
expect(written.browserHelpText).toContain("Usage:");
expect(written.browserHelpText).toContain("openclaw browser");
expect(written.secretsHelpText).toContain("Usage:");
expect(written.secretsHelpText).toContain("openclaw secrets");
expect(written.nodesHelpText).toContain("Usage:");
expect(written.nodesHelpText).toContain("openclaw nodes");
expect(written.rootHelpText).toContain("Usage:");
expect(written.rootHelpText).toContain("openclaw");
});
it("regenerates nodes help when bundled canvas CLI help sources change", async () => {
const tempRoot = createTempDir("openclaw-startup-metadata-signature-");
const distDir = path.join(tempRoot, "dist");
const extensionsDir = path.join(tempRoot, "extensions");
const outputPath = path.join(distDir, "cli-startup-metadata.json");
let nodesRenderCount = 0;
writeStartupMetadataSourceSignatureFixture(tempRoot);
writeFixtureFile(distDir, "root-help-fixture.js", "export function outputRootHelp() {}\n");
const writeMetadata = async (): Promise<void> => {
await writeCliStartupMetadata({
distDir,
outputPath,
extensionsDir,
sourceRootDir: tempRoot,
renderBundledRootHelpText: async () => "Usage: openclaw\n",
renderSourceBrowserHelpText: () => "Usage: openclaw browser\n",
renderSourceSecretsHelpText: () => "Usage: openclaw secrets\n",
renderSourceNodesHelpText: () => {
nodesRenderCount += 1;
return `Usage: openclaw nodes ${nodesRenderCount}\n`;
},
});
};
await writeMetadata();
await writeMetadata();
expect(nodesRenderCount).toBe(1);
writeFixtureFile(
tempRoot,
"extensions/canvas/src/cli.ts",
"export const canvasCliHelp = 'canvas changed help';\n",
);
await writeMetadata();
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
nodesHelpText: string;
};
expect(nodesRenderCount).toBe(2);
expect(written.nodesHelpText).toContain("openclaw nodes 2");
});
});