perf(cli): trim help startup imports

This commit is contained in:
Peter Steinberger 2026-03-15 18:20:33 -07:00
parent 74a57ace10
commit 9c89a74f84
No known key found for this signature in database
10 changed files with 350 additions and 85 deletions

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
@ -15,6 +15,21 @@ if (!isLinux && !isMac) {
const repoRoot = process.cwd();
const tmpHome = mkdtempSync(path.join(os.tmpdir(), "openclaw-startup-memory-"));
const tmpDir = process.env.TMPDIR || process.env.TEMP || process.env.TMP || os.tmpdir();
const rssHookPath = path.join(tmpHome, "measure-rss.mjs");
const MAX_RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
writeFileSync(
rssHookPath,
[
"process.on('exit', () => {",
" const usage = typeof process.resourceUsage === 'function' ? process.resourceUsage() : null;",
` if (usage && typeof usage.maxRSS === 'number') console.error('${MAX_RSS_MARKER}' + String(usage.maxRSS));`,
"});",
"",
].join("\n"),
"utf8",
);
const DEFAULT_LIMITS_MB = {
help: 500,
@ -26,13 +41,13 @@ const cases = [
{
id: "help",
label: "--help",
args: ["node", "openclaw.mjs", "--help"],
args: ["openclaw.mjs", "--help"],
limitMb: Number(process.env.OPENCLAW_STARTUP_MEMORY_HELP_MB ?? DEFAULT_LIMITS_MB.help),
},
{
id: "statusJson",
label: "status --json",
args: ["node", "openclaw.mjs", "status", "--json"],
args: ["openclaw.mjs", "status", "--json"],
limitMb: Number(
process.env.OPENCLAW_STARTUP_MEMORY_STATUS_JSON_MB ?? DEFAULT_LIMITS_MB.statusJson,
),
@ -40,7 +55,7 @@ const cases = [
{
id: "gatewayStatus",
label: "gateway status",
args: ["node", "openclaw.mjs", "gateway", "status"],
args: ["openclaw.mjs", "gateway", "status"],
limitMb: Number(
process.env.OPENCLAW_STARTUP_MEMORY_GATEWAY_STATUS_MB ?? DEFAULT_LIMITS_MB.gatewayStatus,
),
@ -48,30 +63,44 @@ const cases = [
];
function parseMaxRssMb(stderr) {
if (isLinux) {
const match = stderr.match(/^\s*Maximum resident set size \(kbytes\):\s*(\d+)\s*$/im);
if (!match) {
return null;
}
return Number(match[1]) / 1024;
}
const match = stderr.match(/^\s*(\d+)\s+maximum resident set size\s*$/im);
const match = stderr.match(new RegExp(`^${MAX_RSS_MARKER}(\\d+)\\s*$`, "m"));
if (!match) {
return null;
}
return Number(match[1]) / (1024 * 1024);
return Number(match[1]) / 1024;
}
function runCase(testCase) {
function buildBenchEnv() {
const env = {
...process.env,
HOME: tmpHome,
USERPROFILE: tmpHome,
XDG_CONFIG_HOME: path.join(tmpHome, ".config"),
XDG_DATA_HOME: path.join(tmpHome, ".local", "share"),
XDG_CACHE_HOME: path.join(tmpHome, ".cache"),
PATH: process.env.PATH ?? "",
TMPDIR: tmpDir,
TEMP: tmpDir,
TMP: tmpDir,
LANG: process.env.LANG ?? "C.UTF-8",
TERM: process.env.TERM ?? "dumb",
};
const timeArgs = isLinux ? ["-v", ...testCase.args] : ["-l", ...testCase.args];
const result = spawnSync("/usr/bin/time", timeArgs, {
if (process.env.LC_ALL) {
env.LC_ALL = process.env.LC_ALL;
}
if (process.env.CI) {
env.CI = process.env.CI;
}
if (process.env.NODE_DISABLE_COMPILE_CACHE) {
env.NODE_DISABLE_COMPILE_CACHE = process.env.NODE_DISABLE_COMPILE_CACHE;
}
return env;
}
function runCase(testCase) {
const env = buildBenchEnv();
const result = spawnSync(process.execPath, ["--import", rssHookPath, ...testCase.args], {
cwd: repoRoot,
env,
encoding: "utf8",

View File

@ -0,0 +1,24 @@
import fs from "node:fs";
import JSON5 from "json5";
import { resolveConfigPath } from "../config/paths.js";
import type { TaglineMode } from "./tagline.js";
function parseTaglineMode(value: unknown): TaglineMode | undefined {
if (value === "random" || value === "default" || value === "off") {
return value;
}
return undefined;
}
export function readCliBannerTaglineMode(
env: NodeJS.ProcessEnv = process.env,
): TaglineMode | undefined {
try {
const configPath = resolveConfigPath(env);
const raw = fs.readFileSync(configPath, "utf8");
const parsed: { cli?: { banner?: { taglineMode?: unknown } } } = JSON5.parse(raw);
return parseTaglineMode(parsed.cli?.banner?.taglineMode);
} catch {
return undefined;
}
}

View File

@ -1,9 +1,9 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.fn();
const readCliBannerTaglineModeMock = vi.fn();
vi.mock("../config/config.js", () => ({
loadConfig: loadConfigMock,
vi.mock("./banner-config-lite.js", () => ({
readCliBannerTaglineMode: readCliBannerTaglineModeMock,
}));
let formatCliBannerLine: typeof import("./banner.js").formatCliBannerLine;
@ -13,15 +13,13 @@ beforeAll(async () => {
});
beforeEach(() => {
loadConfigMock.mockReset();
loadConfigMock.mockReturnValue({});
readCliBannerTaglineModeMock.mockReset();
readCliBannerTaglineModeMock.mockReturnValue(undefined);
});
describe("formatCliBannerLine", () => {
it("hides tagline text when cli.banner.taglineMode is off", () => {
loadConfigMock.mockReturnValue({
cli: { banner: { taglineMode: "off" } },
});
readCliBannerTaglineModeMock.mockReturnValue("off");
const line = formatCliBannerLine("2026.3.7", {
commit: "abc1234",
@ -32,9 +30,7 @@ describe("formatCliBannerLine", () => {
});
it("uses default tagline when cli.banner.taglineMode is default", () => {
loadConfigMock.mockReturnValue({
cli: { banner: { taglineMode: "default" } },
});
readCliBannerTaglineModeMock.mockReturnValue("default");
const line = formatCliBannerLine("2026.3.7", {
commit: "abc1234",
@ -45,9 +41,7 @@ describe("formatCliBannerLine", () => {
});
it("prefers explicit tagline mode over config", () => {
loadConfigMock.mockReturnValue({
cli: { banner: { taglineMode: "off" } },
});
readCliBannerTaglineModeMock.mockReturnValue("off");
const line = formatCliBannerLine("2026.3.7", {
commit: "abc1234",

View File

@ -1,8 +1,8 @@
import { loadConfig } from "../config/config.js";
import { resolveCommitHash } from "../infra/git-commit.js";
import { visibleWidth } from "../terminal/ansi.js";
import { isRich, theme } from "../terminal/theme.js";
import { hasRootVersionAlias } from "./argv.js";
import { readCliBannerTaglineMode } from "./banner-config-lite.js";
import { pickTagline, type TaglineMode, type TaglineOptions } from "./tagline.js";
type BannerOptions = TaglineOptions & {
@ -48,12 +48,7 @@ function resolveTaglineMode(options: BannerOptions): TaglineMode | undefined {
if (explicit) {
return explicit;
}
try {
return parseTaglineMode(loadConfig().cli?.banner?.taglineMode);
} catch {
// Fall back to default random behavior when config is missing/invalid.
return undefined;
}
return readCliBannerTaglineMode(options.env);
}
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {

View File

@ -3,8 +3,15 @@ import { getPrimaryCommand, hasHelpOrVersion } from "../argv.js";
import { reparseProgramFromActionArgs } from "./action-reparse.js";
import { removeCommandByName } from "./command-tree.js";
import type { ProgramContext } from "./context.js";
import {
type CoreCliCommandDescriptor,
getCoreCliCommandDescriptors,
getCoreCliCommandsWithSubcommands,
} from "./core-command-descriptors.js";
import { registerSubCliCommands } from "./register.subclis.js";
export { getCoreCliCommandDescriptors, getCoreCliCommandsWithSubcommands };
type CommandRegisterParams = {
program: Command;
ctx: ProgramContext;
@ -16,12 +23,6 @@ export type CommandRegistration = {
register: (params: CommandRegisterParams) => void;
};
type CoreCliCommandDescriptor = {
name: string;
description: string;
hasSubcommands: boolean;
};
type CoreCliEntry = {
commands: CoreCliCommandDescriptor[];
register: (params: CommandRegisterParams) => Promise<void> | void;
@ -217,34 +218,8 @@ const coreEntries: CoreCliEntry[] = [
},
];
function collectCoreCliCommandNames(predicate?: (command: CoreCliCommandDescriptor) => boolean) {
const seen = new Set<string>();
const names: string[] = [];
for (const entry of coreEntries) {
for (const command of entry.commands) {
if (predicate && !predicate(command)) {
continue;
}
if (seen.has(command.name)) {
continue;
}
seen.add(command.name);
names.push(command.name);
}
}
return names;
}
export function getCoreCliCommandDescriptors(): ReadonlyArray<CoreCliCommandDescriptor> {
return coreEntries.flatMap((entry) => entry.commands);
}
export function getCoreCliCommandNames(): string[] {
return collectCoreCliCommandNames();
}
export function getCoreCliCommandsWithSubcommands(): string[] {
return collectCoreCliCommandNames((command) => command.hasSubcommands);
return getCoreCliCommandDescriptors().map((command) => command.name);
}
function removeEntryCommands(program: Command, entry: CoreCliEntry) {

View File

@ -0,0 +1,104 @@
export type CoreCliCommandDescriptor = {
name: string;
description: string;
hasSubcommands: boolean;
};
export const CORE_CLI_COMMAND_DESCRIPTORS = [
{
name: "setup",
description: "Initialize local config and agent workspace",
hasSubcommands: false,
},
{
name: "onboard",
description: "Interactive onboarding wizard for gateway, workspace, and skills",
hasSubcommands: false,
},
{
name: "configure",
description: "Interactive setup wizard for credentials, channels, gateway, and agent defaults",
hasSubcommands: false,
},
{
name: "config",
description:
"Non-interactive config helpers (get/set/unset/file/validate). Default: starts setup wizard.",
hasSubcommands: true,
},
{
name: "backup",
description: "Create and verify local backup archives for OpenClaw state",
hasSubcommands: true,
},
{
name: "doctor",
description: "Health checks + quick fixes for the gateway and channels",
hasSubcommands: false,
},
{
name: "dashboard",
description: "Open the Control UI with your current token",
hasSubcommands: false,
},
{
name: "reset",
description: "Reset local config/state (keeps the CLI installed)",
hasSubcommands: false,
},
{
name: "uninstall",
description: "Uninstall the gateway service + local data (CLI remains)",
hasSubcommands: false,
},
{
name: "message",
description: "Send, read, and manage messages",
hasSubcommands: true,
},
{
name: "memory",
description: "Search and reindex memory files",
hasSubcommands: true,
},
{
name: "agent",
description: "Run one agent turn via the Gateway",
hasSubcommands: false,
},
{
name: "agents",
description: "Manage isolated agents (workspaces, auth, routing)",
hasSubcommands: true,
},
{
name: "status",
description: "Show channel health and recent session recipients",
hasSubcommands: false,
},
{
name: "health",
description: "Fetch health from the running gateway",
hasSubcommands: false,
},
{
name: "sessions",
description: "List stored conversation sessions",
hasSubcommands: true,
},
{
name: "browser",
description: "Manage OpenClaw's dedicated browser (Chrome/Chromium)",
hasSubcommands: true,
},
] as const satisfies ReadonlyArray<CoreCliCommandDescriptor>;
export function getCoreCliCommandDescriptors(): ReadonlyArray<CoreCliCommandDescriptor> {
return CORE_CLI_COMMAND_DESCRIPTORS;
}
export function getCoreCliCommandsWithSubcommands(): string[] {
return CORE_CLI_COMMAND_DESCRIPTORS.filter((command) => command.hasSubcommands).map(
(command) => command.name,
);
}

View File

@ -7,9 +7,9 @@ import { hasFlag, hasRootVersionAlias } from "../argv.js";
import { formatCliBannerLine, hasEmittedCliBanner } from "../banner.js";
import { replaceCliName, resolveCliName } from "../cli-name.js";
import { CLI_LOG_LEVEL_VALUES, parseCliLogLevelOption } from "../log-level-option.js";
import { getCoreCliCommandsWithSubcommands } from "./command-registry.js";
import type { ProgramContext } from "./context.js";
import { getSubCliCommandsWithSubcommands } from "./register.subclis.js";
import { getCoreCliCommandsWithSubcommands } from "./core-command-descriptors.js";
import { getSubCliCommandsWithSubcommands } from "./subcli-descriptors.js";
const CLI_NAME = resolveCliName();
const CLI_NAME_PATTERN = escapeRegExp(CLI_NAME);

View File

@ -4,13 +4,17 @@ import { isTruthyEnvValue } from "../../infra/env.js";
import { getPrimaryCommand, hasHelpOrVersion } from "../argv.js";
import { reparseProgramFromActionArgs } from "./action-reparse.js";
import { removeCommand, removeCommandByName } from "./command-tree.js";
import {
getSubCliCommandsWithSubcommands,
getSubCliEntries as getSubCliEntryDescriptors,
type SubCliDescriptor,
} from "./subcli-descriptors.js";
export { getSubCliCommandsWithSubcommands };
type SubCliRegistrar = (program: Command) => Promise<void> | void;
type SubCliEntry = {
name: string;
description: string;
hasSubcommands: boolean;
type SubCliEntry = SubCliDescriptor & {
register: SubCliRegistrar;
};
@ -309,12 +313,8 @@ const entries: SubCliEntry[] = [
},
];
export function getSubCliEntries(): SubCliEntry[] {
return entries;
}
export function getSubCliCommandsWithSubcommands(): string[] {
return entries.filter((entry) => entry.hasSubcommands).map((entry) => entry.name);
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
return getSubCliEntryDescriptors();
}
export async function registerSubCliByName(program: Command, name: string): Promise<boolean> {

View File

@ -1,8 +1,8 @@
import { Command } from "commander";
import { VERSION } from "../../version.js";
import { getCoreCliCommandDescriptors } from "./command-registry.js";
import { getCoreCliCommandDescriptors } from "./core-command-descriptors.js";
import { configureProgramHelp } from "./help.js";
import { getSubCliEntries } from "./register.subclis.js";
import { getSubCliEntries } from "./subcli-descriptors.js";
function buildRootHelpProgram(): Command {
const program = new Command();

View File

@ -0,0 +1,144 @@
export type SubCliDescriptor = {
name: string;
description: string;
hasSubcommands: boolean;
};
export const SUB_CLI_DESCRIPTORS = [
{ name: "acp", description: "Agent Control Protocol tools", hasSubcommands: true },
{
name: "gateway",
description: "Run, inspect, and query the WebSocket Gateway",
hasSubcommands: true,
},
{ name: "daemon", description: "Gateway service (legacy alias)", hasSubcommands: true },
{ name: "logs", description: "Tail gateway file logs via RPC", hasSubcommands: false },
{
name: "system",
description: "System events, heartbeat, and presence",
hasSubcommands: true,
},
{
name: "models",
description: "Discover, scan, and configure models",
hasSubcommands: true,
},
{
name: "approvals",
description: "Manage exec approvals (gateway or node host)",
hasSubcommands: true,
},
{
name: "nodes",
description: "Manage gateway-owned node pairing and node commands",
hasSubcommands: true,
},
{
name: "devices",
description: "Device pairing + token management",
hasSubcommands: true,
},
{
name: "node",
description: "Run and manage the headless node host service",
hasSubcommands: true,
},
{
name: "sandbox",
description: "Manage sandbox containers for agent isolation",
hasSubcommands: true,
},
{
name: "tui",
description: "Open a terminal UI connected to the Gateway",
hasSubcommands: false,
},
{
name: "cron",
description: "Manage cron jobs via the Gateway scheduler",
hasSubcommands: true,
},
{
name: "dns",
description: "DNS helpers for wide-area discovery (Tailscale + CoreDNS)",
hasSubcommands: true,
},
{
name: "docs",
description: "Search the live OpenClaw docs",
hasSubcommands: false,
},
{
name: "hooks",
description: "Manage internal agent hooks",
hasSubcommands: true,
},
{
name: "webhooks",
description: "Webhook helpers and integrations",
hasSubcommands: true,
},
{
name: "qr",
description: "Generate iOS pairing QR/setup code",
hasSubcommands: false,
},
{
name: "clawbot",
description: "Legacy clawbot command aliases",
hasSubcommands: true,
},
{
name: "pairing",
description: "Secure DM pairing (approve inbound requests)",
hasSubcommands: true,
},
{
name: "plugins",
description: "Manage OpenClaw plugins and extensions",
hasSubcommands: true,
},
{
name: "channels",
description: "Manage connected chat channels (Telegram, Discord, etc.)",
hasSubcommands: true,
},
{
name: "directory",
description: "Lookup contact and group IDs (self, peers, groups) for supported chat channels",
hasSubcommands: true,
},
{
name: "security",
description: "Security tools and local config audits",
hasSubcommands: true,
},
{
name: "secrets",
description: "Secrets runtime reload controls",
hasSubcommands: true,
},
{
name: "skills",
description: "List and inspect available skills",
hasSubcommands: true,
},
{
name: "update",
description: "Update OpenClaw and inspect update channel status",
hasSubcommands: true,
},
{
name: "completion",
description: "Generate shell completion script",
hasSubcommands: false,
},
] as const satisfies ReadonlyArray<SubCliDescriptor>;
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
return SUB_CLI_DESCRIPTORS;
}
export function getSubCliCommandsWithSubcommands(): string[] {
return SUB_CLI_DESCRIPTORS.filter((entry) => entry.hasSubcommands).map((entry) => entry.name);
}