Skip to content

Commit 2c0c9c9

Browse files
authored
perf(cli): speed up onboarding help startup (#84488)
Merged via squash. Prepared head SHA: b3b086e Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com> Co-authored-by: frankekn <4488090+frankekn@users.noreply.github.com> Reviewed-by: @frankekn
1 parent 2585249 commit 2c0c9c9

11 files changed

Lines changed: 260 additions & 14 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Docs: https://docs.openclaw.ai
88

99
- Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so `doctor-core-checks` no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.
1010

11+
### Fixes
12+
13+
- 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.
14+
1115
## 2026.5.20
1216

1317
### Changes

src/cli/help-cold-imports.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ vi.mock("./progress.js", () => {
4040
};
4141
});
4242

43+
vi.mock("../runtime.js", () => {
44+
loaded.mark("default-runtime");
45+
return {
46+
defaultRuntime: {
47+
error: vi.fn(),
48+
exit: vi.fn(),
49+
log: vi.fn(),
50+
writeJson: vi.fn(),
51+
writeStdout: vi.fn(),
52+
},
53+
};
54+
});
55+
4356
vi.mock("../commands/doctor.js", () => {
4457
loaded.mark("doctor-command");
4558
return { doctorCommand: vi.fn(async () => {}) };
@@ -117,6 +130,26 @@ vi.mock("../commands/flows.js", () => {
117130
};
118131
});
119132

133+
vi.mock("../commands/configure.commands.js", () => {
134+
loaded.mark("configure-command");
135+
return { configureCommandFromSectionsArg: vi.fn(async () => {}) };
136+
});
137+
138+
vi.mock("../commands/configure.wizard.js", () => {
139+
loaded.mark("configure-wizard");
140+
return { runConfigureWizard: vi.fn(async () => {}) };
141+
});
142+
143+
vi.mock("../commands/onboard.js", () => {
144+
loaded.mark("onboard-command");
145+
return { setupWizardCommand: vi.fn(async () => {}) };
146+
});
147+
148+
vi.mock("../commands/setup.js", () => {
149+
loaded.mark("setup-command");
150+
return { setupCommand: vi.fn(async () => {}) };
151+
});
152+
120153
function makeProgram(): Command {
121154
const program = new Command();
122155
program.name("openclaw");
@@ -180,4 +213,39 @@ describe("subcommand help cold imports", () => {
180213
expect(loaded.modules).not.toContain("tasks-command");
181214
expect(loaded.modules).not.toContain("flows-command");
182215
});
216+
217+
it("keeps configure help out of configure action/wizard modules", async () => {
218+
const { registerConfigureCommand } = await import("./program/register.configure.js");
219+
const program = makeProgram();
220+
221+
registerConfigureCommand(program);
222+
await expectHelpExit(program, ["configure", "--help"]);
223+
224+
expect(loaded.modules).not.toContain("configure-command");
225+
expect(loaded.modules).not.toContain("configure-wizard");
226+
expect(loaded.modules).not.toContain("default-runtime");
227+
});
228+
229+
it("keeps setup help out of setup and onboard action modules", async () => {
230+
const { registerSetupCommand } = await import("./program/register.setup.js");
231+
const program = makeProgram();
232+
233+
registerSetupCommand(program);
234+
await expectHelpExit(program, ["setup", "--help"]);
235+
236+
expect(loaded.modules).not.toContain("setup-command");
237+
expect(loaded.modules).not.toContain("onboard-command");
238+
expect(loaded.modules).not.toContain("default-runtime");
239+
});
240+
241+
it("keeps onboard help out of onboard action modules", async () => {
242+
const { registerOnboardCommand } = await import("./program/register.onboard.js");
243+
const program = makeProgram();
244+
245+
registerOnboardCommand(program);
246+
await expectHelpExit(program, ["onboard", "--help"]);
247+
248+
expect(loaded.modules).not.toContain("onboard-command");
249+
expect(loaded.modules).not.toContain("default-runtime");
250+
});
183251
});

src/cli/program/register.configure.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ const mocks = vi.hoisted(() => ({
1313

1414
const { configureCommandFromSectionsArgMock, runtime } = mocks;
1515

16-
vi.mock("../../commands/configure.js", () => ({
16+
vi.mock("../../commands/configure.shared.js", () => ({
1717
CONFIGURE_WIZARD_SECTIONS: ["auth", "channels", "gateway", "agent"],
18+
}));
19+
20+
vi.mock("../../commands/configure.commands.js", () => ({
1821
configureCommandFromSectionsArg: mocks.configureCommandFromSectionsArgMock,
1922
}));
2023

src/cli/program/register.configure.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import type { Command } from "commander";
2-
import {
3-
CONFIGURE_WIZARD_SECTIONS,
4-
configureCommandFromSectionsArg,
5-
} from "../../commands/configure.js";
6-
import { defaultRuntime } from "../../runtime.js";
2+
import { CONFIGURE_WIZARD_SECTIONS } from "../../commands/configure.shared.js";
73
import { formatDocsLink } from "../../terminal/links.js";
84
import { theme } from "../../terminal/theme.js";
95
import { runCommandWithRuntime } from "../cli-utils.js";
106

11-
export function registerConfigureCommand(program: Command) {
7+
export function registerConfigureCommand(program: Command): void {
128
program
139
.command("configure")
1410
.description("Interactive configuration for credentials, channels, gateway, and agent defaults")
@@ -24,7 +20,10 @@ export function registerConfigureCommand(program: Command) {
2420
[] as string[],
2521
)
2622
.action(async (opts) => {
23+
const { defaultRuntime } = await import("../../runtime.js");
2724
await runCommandWithRuntime(defaultRuntime, async () => {
25+
const { configureCommandFromSectionsArg } =
26+
await import("../../commands/configure.commands.js");
2827
await configureCommandFromSectionsArg(opts.section, defaultRuntime);
2928
});
3029
});

src/cli/program/register.onboard.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ import type {
1111
SecretInputMode,
1212
TailscaleMode,
1313
} from "../../commands/onboard-types.js";
14-
import { setupWizardCommand } from "../../commands/onboard.js";
1514
import { resolveManifestProviderOnboardAuthFlags } from "../../plugins/provider-auth-choices.js";
16-
import { defaultRuntime } from "../../runtime.js";
1715
import { formatDocsLink } from "../../terminal/links.js";
1816
import { theme } from "../../terminal/theme.js";
1917
import { runCommandWithRuntime } from "../cli-utils.js";
@@ -89,7 +87,7 @@ function pickOnboardProviderAuthOptionValues(
8987
);
9088
}
9189

92-
export function registerOnboardCommand(program: Command) {
90+
export function registerOnboardCommand(program: Command): void {
9391
const command = program
9492
.command("onboard")
9593
.description("Guided setup for auth, models, Gateway, workspace, channels, and skills")
@@ -177,6 +175,7 @@ export function registerOnboardCommand(program: Command) {
177175
.option("--json", "Output JSON summary", false);
178176

179177
command.action(async (opts, commandRuntime) => {
178+
const { defaultRuntime } = await import("../../runtime.js");
180179
await runCommandWithRuntime(defaultRuntime, async () => {
181180
if (opts.modern) {
182181
const { runCrestodian } = await import("../../crestodian/crestodian.js");
@@ -196,6 +195,7 @@ export function registerOnboardCommand(program: Command) {
196195
const providerAuthOptionValues = pickOnboardProviderAuthOptionValues(
197196
opts as Record<string, unknown>,
198197
);
198+
const { setupWizardCommand } = await import("../../commands/onboard.js");
199199
await setupWizardCommand(
200200
{
201201
workspace: opts.workspace as string | undefined,

src/cli/program/register.setup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import type { Command } from "commander";
2-
import { setupWizardCommand } from "../../commands/onboard.js";
3-
import { setupCommand } from "../../commands/setup.js";
4-
import { defaultRuntime } from "../../runtime.js";
52
import { formatDocsLink } from "../../terminal/links.js";
63
import { theme } from "../../terminal/theme.js";
74
import { runCommandWithRuntime } from "../cli-utils.js";
85
import { hasExplicitOptions } from "../command-options.js";
96

10-
export function registerSetupCommand(program: Command) {
7+
export function registerSetupCommand(program: Command): void {
118
program
129
.command("setup")
1310
.description("Create baseline config/workspace files; use --wizard for full onboarding")
@@ -34,6 +31,7 @@ export function registerSetupCommand(program: Command) {
3431
.option("--remote-url <url>", "Remote Gateway WebSocket URL")
3532
.option("--remote-token <token>", "Remote Gateway token (optional)")
3633
.action(async (opts, command) => {
34+
const { defaultRuntime } = await import("../../runtime.js");
3735
await runCommandWithRuntime(defaultRuntime, async () => {
3836
const hasWizardFlags = hasExplicitOptions(command, [
3937
"wizard",
@@ -46,6 +44,7 @@ export function registerSetupCommand(program: Command) {
4644
"remoteToken",
4745
]);
4846
if (opts.wizard || hasWizardFlags) {
47+
const { setupWizardCommand } = await import("../../commands/onboard.js");
4948
await setupWizardCommand(
5049
{
5150
workspace: opts.workspace as string | undefined,
@@ -61,6 +60,7 @@ export function registerSetupCommand(program: Command) {
6160
);
6261
return;
6362
}
63+
const { setupCommand } = await import("../../commands/setup.js");
6464
await setupCommand({ workspace: opts.workspace as string | undefined }, defaultRuntime);
6565
});
6666
});

src/cli/run-main-policy.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getCoreCliParentDefaultHelpCommands } from "./program/core-command-desc
2020
import { getSubCliParentDefaultHelpCommands } from "./program/subcli-descriptors.js";
2121

2222
const ROOT_HELP_ALIASES = new Set(["tools"]);
23+
const SETUP_ONBOARD_CONFIGURE_HELP_COMMANDS = new Set(["setup", "onboard", "configure"]);
2324
const BARE_PARENT_DEFAULT_HELP_COMMANDS = new Set([
2425
...getCoreCliParentDefaultHelpCommands(),
2526
...getSubCliParentDefaultHelpCommands(),
@@ -88,6 +89,21 @@ export function shouldUseBrowserHelpFastPath(
8889
);
8990
}
9091

92+
export function shouldUseSetupOnboardConfigureHelpFastPath(
93+
argv: string[],
94+
env: NodeJS.ProcessEnv = process.env,
95+
): boolean {
96+
if (env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1") {
97+
return false;
98+
}
99+
const invocation = resolveCliArgvInvocation(argv);
100+
return (
101+
invocation.commandPath.length === 1 &&
102+
SETUP_ONBOARD_CONFIGURE_HELP_COMMANDS.has(invocation.commandPath[0] ?? "") &&
103+
invocation.hasHelpOrVersion
104+
);
105+
}
106+
91107
export function shouldStartCrestodianForBareRoot(argv: string[]): boolean {
92108
const invocation = resolveCliArgvInvocation(argv);
93109
return invocation.commandPath.length === 0 && !invocation.hasHelpOrVersion;

src/cli/run-main.exit.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const outputPrecomputedBrowserHelpTextMock = vi.hoisted(() => vi.fn(() => false)
2222
const loadRootHelpRenderOptionsForConfigSensitivePluginsMock = vi.hoisted(() =>
2323
vi.fn<() => Promise<RootHelpRenderOptions | null>>(async () => null),
2424
);
25+
const tryOutputSetupOnboardConfigureHelpMock = vi.hoisted(() => vi.fn(async () => true));
2526
const buildProgramMock = vi.hoisted(() => vi.fn());
2627
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
2728
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
@@ -177,6 +178,10 @@ vi.mock("./root-help-live-config.js", () => ({
177178
loadRootHelpRenderOptionsForConfigSensitivePluginsMock,
178179
}));
179180

181+
vi.mock("./setup-onboard-configure-help-fast-path.js", () => ({
182+
tryOutputSetupOnboardConfigureHelp: tryOutputSetupOnboardConfigureHelpMock,
183+
}));
184+
180185
vi.mock("./program.js", () => ({
181186
buildProgram: buildProgramMock,
182187
}));
@@ -252,6 +257,7 @@ describe("runCli exit behavior", () => {
252257
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
253258
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
254259
loadRootHelpRenderOptionsForConfigSensitivePluginsMock.mockResolvedValue(null);
260+
tryOutputSetupOnboardConfigureHelpMock.mockResolvedValue(true);
255261
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false);
256262
loadConfigMock.mockReturnValue({});
257263
startProxyMock.mockResolvedValue(null);
@@ -418,6 +424,20 @@ describe("runCli exit behavior", () => {
418424
expect(runCrestodianMock).not.toHaveBeenCalled();
419425
});
420426

427+
it("renders setup/onboard/configure help without building the full program", async () => {
428+
await runCli(["node", "openclaw", "setup", "--help"]);
429+
430+
expect(tryOutputSetupOnboardConfigureHelpMock).toHaveBeenCalledWith([
431+
"node",
432+
"openclaw",
433+
"setup",
434+
"--help",
435+
]);
436+
expect(tryRouteCliMock).not.toHaveBeenCalled();
437+
expect(buildProgramMock).not.toHaveBeenCalled();
438+
expect(registerPluginCliCommandsFromValidatedConfigMock).not.toHaveBeenCalled();
439+
});
440+
421441
it("renders root help without building the full program", async () => {
422442
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
423443
throw new Error(`unexpected process.exit(${String(code)})`);

src/cli/run-main.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
shouldStartProxyForCli,
1010
shouldUseBrowserHelpFastPath,
1111
shouldUseRootHelpFastPath,
12+
shouldUseSetupOnboardConfigureHelpFastPath,
1213
} from "./run-main-policy.js";
1314
import { isGatewayRunFastPathArgv } from "./run-main.js";
1415

@@ -212,6 +213,39 @@ describe("shouldUseBrowserHelpFastPath", () => {
212213
});
213214
});
214215

216+
describe("shouldUseSetupOnboardConfigureHelpFastPath", () => {
217+
it("uses the fast path only for setup, onboard, and configure help", () => {
218+
expect(
219+
shouldUseSetupOnboardConfigureHelpFastPath(["node", "openclaw", "setup", "--help"]),
220+
).toBe(true);
221+
expect(shouldUseSetupOnboardConfigureHelpFastPath(["node", "openclaw", "onboard", "-h"])).toBe(
222+
true,
223+
);
224+
expect(
225+
shouldUseSetupOnboardConfigureHelpFastPath([
226+
"node",
227+
"openclaw",
228+
"--profile",
229+
"work",
230+
"configure",
231+
"-h",
232+
]),
233+
).toBe(true);
234+
expect(
235+
shouldUseSetupOnboardConfigureHelpFastPath([
236+
"node",
237+
"openclaw",
238+
"onboard",
239+
"status",
240+
"--help",
241+
]),
242+
).toBe(false);
243+
expect(
244+
shouldUseSetupOnboardConfigureHelpFastPath(["node", "openclaw", "status", "--help"]),
245+
).toBe(false);
246+
});
247+
});
248+
215249
describe("resolveMissingPluginCommandMessage", () => {
216250
it("explains plugins.allow misses for a bundled plugin command", () => {
217251
expect(

src/cli/run-main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
shouldStartProxyForCli,
3939
shouldUseBrowserHelpFastPath,
4040
shouldUseRootHelpFastPath,
41+
shouldUseSetupOnboardConfigureHelpFastPath,
4142
} from "./run-main-policy.js";
4243
import { normalizeWindowsArgv } from "./windows-argv.js";
4344

@@ -49,6 +50,7 @@ export {
4950
shouldStartProxyForCli,
5051
shouldUseBrowserHelpFastPath,
5152
shouldUseRootHelpFastPath,
53+
shouldUseSetupOnboardConfigureHelpFastPath,
5254
} from "./run-main-policy.js";
5355

5456
type Awaitable<T> = T | Promise<T>;
@@ -549,6 +551,14 @@ export async function runCli(argv: string[] = process.argv) {
549551
}
550552
}
551553

554+
if (shouldUseSetupOnboardConfigureHelpFastPath(normalizedArgv)) {
555+
const { tryOutputSetupOnboardConfigureHelp } =
556+
await import("./setup-onboard-configure-help-fast-path.js");
557+
if (await tryOutputSetupOnboardConfigureHelp(normalizedArgv)) {
558+
return;
559+
}
560+
}
561+
552562
const shouldRunBareRootCrestodian = shouldStartCrestodianForBareRoot(normalizedArgv);
553563
const shouldRunModernOnboardCrestodian = shouldStartCrestodianForModernOnboard(normalizedArgv);
554564
if (shouldRunBareRootCrestodian || shouldRunModernOnboardCrestodian) {

0 commit comments

Comments
 (0)