perf(cli): trim help startup imports
This commit is contained in:
parent
74a57ace10
commit
9c89a74f84
@ -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",
|
||||
|
||||
24
src/cli/banner-config-lite.ts
Normal file
24
src/cli/banner-config-lite.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
104
src/cli/program/core-command-descriptors.ts
Normal file
104
src/cli/program/core-command-descriptors.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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();
|
||||
|
||||
144
src/cli/program/subcli-descriptors.ts
Normal file
144
src/cli/program/subcli-descriptors.ts
Normal 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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user