CLI: streamline startup paths and env parsing
Add shared parseBooleanValue()/isTruthyEnvValue() and apply across CLI, gateway, memory, and live-test flags for consistent env handling. Introduce route-first fast paths, lazy subcommand registration, and deferred plugin loading to reduce CLI startup overhead. Centralize config validation via ensureConfigReady() and add config caching/deferred shell env fallback for fewer IO passes. Harden logger initialization/imports and add focused tests for argv, boolean parsing, frontmatter, and CLI subcommands.
This commit is contained in:
committed by
Peter Steinberger
parent
97531f174f
commit
acb523de86
@@ -16,6 +16,7 @@ import { registerSubCliCommands } from "./register.subclis.js";
|
||||
export function buildProgram() {
|
||||
const program = new Command();
|
||||
const ctx = createProgramContext();
|
||||
const argv = process.argv;
|
||||
|
||||
configureProgramHelp(program, ctx);
|
||||
registerPreActionHooks(program, ctx.programVersion);
|
||||
@@ -29,7 +30,7 @@ export function buildProgram() {
|
||||
registerAgentCommands(program, {
|
||||
agentChannelOptions: ctx.agentChannelOptions,
|
||||
});
|
||||
registerSubCliCommands(program);
|
||||
registerSubCliCommands(program, argv);
|
||||
registerStatusHealthSessionsCommands(program);
|
||||
registerBrowserCli(program);
|
||||
|
||||
|
||||
65
src/cli/program/config-guard.ts
Normal file
65
src/cli/program/config-guard.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
isNixMode,
|
||||
loadConfig,
|
||||
migrateLegacyConfig,
|
||||
readConfigFileSnapshot,
|
||||
writeConfigFile,
|
||||
} from "../../config/config.js";
|
||||
import { danger } from "../../globals.js";
|
||||
import { autoMigrateLegacyState } from "../../infra/state-migrations.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
|
||||
export async function ensureConfigReady(params: {
|
||||
runtime: RuntimeEnv;
|
||||
migrateState?: boolean;
|
||||
}): Promise<void> {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.legacyIssues.length > 0) {
|
||||
if (isNixMode) {
|
||||
params.runtime.error(
|
||||
danger(
|
||||
"Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and retry.",
|
||||
),
|
||||
);
|
||||
params.runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const migrated = migrateLegacyConfig(snapshot.parsed);
|
||||
if (migrated.config) {
|
||||
await writeConfigFile(migrated.config);
|
||||
if (migrated.changes.length > 0) {
|
||||
params.runtime.log(
|
||||
`Migrated legacy config entries:\n${migrated.changes
|
||||
.map((entry) => `- ${entry}`)
|
||||
.join("\n")}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const issues = snapshot.legacyIssues
|
||||
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
||||
.join("\n");
|
||||
params.runtime.error(
|
||||
danger(
|
||||
`Legacy config entries detected. Run "clawdbot doctor" (or ask your agent) to migrate.\n${issues}`,
|
||||
),
|
||||
);
|
||||
params.runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot.exists && !snapshot.valid) {
|
||||
params.runtime.error(`Config invalid at ${snapshot.path}.`);
|
||||
for (const issue of snapshot.issues) {
|
||||
params.runtime.error(`- ${issue.path || "<root>"}: ${issue.message}`);
|
||||
}
|
||||
params.runtime.error("Run `clawdbot doctor` to repair, then retry.");
|
||||
params.runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.migrateState !== false) {
|
||||
const cfg = loadConfig();
|
||||
await autoMigrateLegacyState({ cfg });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { createSubsystemLogger } from "../../logging.js";
|
||||
import { loadClawdbotPlugins } from "../../plugins/loader.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import { resolveCliChannelOptions } from "../channel-options.js";
|
||||
|
||||
export type ProgramContext = {
|
||||
programVersion: string;
|
||||
@@ -12,26 +8,8 @@ export type ProgramContext = {
|
||||
agentChannelOptions: string;
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
function primePluginRegistry() {
|
||||
const config = loadConfig();
|
||||
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
|
||||
loadClawdbotPlugins({
|
||||
config,
|
||||
workspaceDir,
|
||||
logger: {
|
||||
info: (msg) => log.info(msg),
|
||||
warn: (msg) => log.warn(msg),
|
||||
error: (msg) => log.error(msg),
|
||||
debug: (msg) => log.debug(msg),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createProgramContext(): ProgramContext {
|
||||
primePluginRegistry();
|
||||
const channelOptions = listChannelPlugins().map((plugin) => plugin.id);
|
||||
const channelOptions = resolveCliChannelOptions();
|
||||
return {
|
||||
programVersion: VERSION,
|
||||
channelOptions,
|
||||
|
||||
@@ -16,3 +16,9 @@ export function parsePositiveIntOrUndefined(value: unknown): number | undefined
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveActionArgs(actionCommand?: import("commander").Command): string[] {
|
||||
if (!actionCommand) return [];
|
||||
const args = (actionCommand as import("commander").Command & { args?: string[] }).args;
|
||||
return Array.isArray(args) ? args : [];
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { danger, setVerbose } from "../../../globals.js";
|
||||
import { CHANNEL_TARGET_DESCRIPTION } from "../../../infra/outbound/channel-target.js";
|
||||
import { defaultRuntime } from "../../../runtime.js";
|
||||
import { createDefaultDeps } from "../../deps.js";
|
||||
import { ensureConfigReady } from "../config-guard.js";
|
||||
|
||||
export type MessageCliHelpers = {
|
||||
withMessageBase: (command: Command) => Command;
|
||||
@@ -30,6 +31,7 @@ export function createMessageCliHelpers(
|
||||
command.requiredOption("-t, --target <dest>", CHANNEL_TARGET_DESCRIPTION);
|
||||
|
||||
const runMessageAction = async (action: string, opts: Record<string, unknown>) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
const deps = createDefaultDeps();
|
||||
try {
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import {
|
||||
isNixMode,
|
||||
loadConfig,
|
||||
migrateLegacyConfig,
|
||||
readConfigFileSnapshot,
|
||||
writeConfigFile,
|
||||
} from "../../config/config.js";
|
||||
import { danger } from "../../globals.js";
|
||||
import { autoMigrateLegacyState } from "../../infra/state-migrations.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { emitCliBanner } from "../banner.js";
|
||||
|
||||
function setProcessTitleForCommand(actionCommand: Command) {
|
||||
@@ -25,52 +15,5 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
||||
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
||||
setProcessTitleForCommand(actionCommand);
|
||||
emitCliBanner(programVersion);
|
||||
if (actionCommand.name() === "doctor") return;
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.legacyIssues.length === 0) return;
|
||||
if (isNixMode) {
|
||||
defaultRuntime.error(
|
||||
danger(
|
||||
"Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and retry.",
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const migrated = migrateLegacyConfig(snapshot.parsed);
|
||||
if (migrated.config) {
|
||||
await writeConfigFile(migrated.config);
|
||||
if (migrated.changes.length > 0) {
|
||||
defaultRuntime.log(
|
||||
`Migrated legacy config entries:\n${migrated.changes
|
||||
.map((entry) => `- ${entry}`)
|
||||
.join("\n")}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const issues = snapshot.legacyIssues
|
||||
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
||||
.join("\n");
|
||||
defaultRuntime.error(
|
||||
danger(
|
||||
`Legacy config entries detected. Run "clawdbot doctor" (or ask your agent) to migrate.\n${issues}`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
||||
if (actionCommand.name() === "doctor") return;
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (snapshot.exists && !snapshot.valid) {
|
||||
defaultRuntime.error(`Config invalid at ${snapshot.path}.`);
|
||||
for (const issue of snapshot.issues) {
|
||||
defaultRuntime.error(`- ${issue.path || "<root>"}: ${issue.message}`);
|
||||
}
|
||||
defaultRuntime.error("Run `clawdbot doctor` to repair, then retry.");
|
||||
process.exit(1);
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
await autoMigrateLegacyState({ cfg });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { theme } from "../../terminal/theme.js";
|
||||
import { hasExplicitOptions } from "../command-options.js";
|
||||
import { createDefaultDeps } from "../deps.js";
|
||||
import { collectOption } from "./helpers.js";
|
||||
import { ensureConfigReady } from "./config-guard.js";
|
||||
|
||||
export function registerAgentCommands(program: Command, args: { agentChannelOptions: string }) {
|
||||
program
|
||||
@@ -57,6 +58,7 @@ Examples:
|
||||
${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent")}`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
||||
const verboseLevel = typeof opts.verbose === "string" ? opts.verbose.toLowerCase() : "";
|
||||
setVerbose(verboseLevel === "on");
|
||||
// Build default deps (keeps parity with other commands; future-proofing).
|
||||
@@ -84,6 +86,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
.option("--bindings", "Include routing bindings", false)
|
||||
.action(async (opts) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
|
||||
try {
|
||||
await agentsListCommand(
|
||||
{ json: Boolean(opts.json), bindings: Boolean(opts.bindings) },
|
||||
@@ -105,6 +108,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
|
||||
.option("--non-interactive", "Disable prompts; requires --workspace", false)
|
||||
.option("--json", "Output JSON summary", false)
|
||||
.action(async (name, opts, command) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
|
||||
try {
|
||||
const hasFlags = hasExplicitOptions(command, [
|
||||
"workspace",
|
||||
@@ -138,6 +142,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
|
||||
.option("--force", "Skip confirmation", false)
|
||||
.option("--json", "Output JSON summary", false)
|
||||
.action(async (id, opts) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
|
||||
try {
|
||||
await agentsDeleteCommand(
|
||||
{
|
||||
@@ -154,6 +159,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
|
||||
});
|
||||
|
||||
agents.action(async () => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
|
||||
try {
|
||||
await agentsListCommand({}, defaultRuntime);
|
||||
} catch (err) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import { parsePositiveIntOrUndefined } from "./helpers.js";
|
||||
import { ensureConfigReady } from "./config-guard.js";
|
||||
|
||||
export function registerStatusHealthSessionsCommands(program: Command) {
|
||||
program
|
||||
@@ -37,6 +38,7 @@ Examples:
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/status", "docs.clawd.bot/cli/status")}\n`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
||||
const verbose = Boolean(opts.verbose || opts.debug);
|
||||
setVerbose(verbose);
|
||||
const timeout = parsePositiveIntOrUndefined(opts.timeout);
|
||||
@@ -76,6 +78,7 @@ Examples:
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/health", "docs.clawd.bot/cli/health")}\n`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
||||
const verbose = Boolean(opts.verbose || opts.debug);
|
||||
setVerbose(verbose);
|
||||
const timeout = parsePositiveIntOrUndefined(opts.timeout);
|
||||
@@ -123,6 +126,7 @@ Shows token usage per session when the agent reports it; set agents.defaults.con
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/sessions", "docs.clawd.bot/cli/sessions")}\n`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
await sessionsCommand(
|
||||
{
|
||||
|
||||
81
src/cli/program/register.subclis.test.ts
Normal file
81
src/cli/program/register.subclis.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Command } from "commander";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { acpAction, registerAcpCli } = vi.hoisted(() => {
|
||||
const action = vi.fn();
|
||||
const register = vi.fn((program: Command) => {
|
||||
program.command("acp").action(action);
|
||||
});
|
||||
return { acpAction: action, registerAcpCli: register };
|
||||
});
|
||||
|
||||
const { nodesAction, registerNodesCli } = vi.hoisted(() => {
|
||||
const action = vi.fn();
|
||||
const register = vi.fn((program: Command) => {
|
||||
const nodes = program.command("nodes");
|
||||
nodes.command("list").action(action);
|
||||
});
|
||||
return { nodesAction: action, registerNodesCli: register };
|
||||
});
|
||||
|
||||
vi.mock("../acp-cli.js", () => ({ registerAcpCli }));
|
||||
vi.mock("../nodes-cli.js", () => ({ registerNodesCli }));
|
||||
|
||||
const { registerSubCliCommands } = await import("./register.subclis.js");
|
||||
|
||||
describe("registerSubCliCommands", () => {
|
||||
const originalArgv = process.argv;
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.CLAWDBOT_DISABLE_LAZY_SUBCOMMANDS;
|
||||
registerAcpCli.mockClear();
|
||||
acpAction.mockClear();
|
||||
registerNodesCli.mockClear();
|
||||
nodesAction.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = originalArgv;
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
it("registers only the primary placeholder and dispatches", async () => {
|
||||
process.argv = ["node", "clawdbot", "acp"];
|
||||
const program = new Command();
|
||||
registerSubCliCommands(program, process.argv);
|
||||
|
||||
expect(program.commands.map((cmd) => cmd.name())).toEqual(["acp"]);
|
||||
|
||||
await program.parseAsync(process.argv);
|
||||
|
||||
expect(registerAcpCli).toHaveBeenCalledTimes(1);
|
||||
expect(acpAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("registers placeholders for all subcommands when no primary", () => {
|
||||
process.argv = ["node", "clawdbot"];
|
||||
const program = new Command();
|
||||
registerSubCliCommands(program, process.argv);
|
||||
|
||||
const names = program.commands.map((cmd) => cmd.name());
|
||||
expect(names).toContain("acp");
|
||||
expect(names).toContain("gateway");
|
||||
expect(registerAcpCli).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("re-parses argv for lazy subcommands", async () => {
|
||||
process.argv = ["node", "clawdbot", "nodes", "list"];
|
||||
const program = new Command();
|
||||
program.name("clawdbot");
|
||||
registerSubCliCommands(program, process.argv);
|
||||
|
||||
expect(program.commands.map((cmd) => cmd.name())).toEqual(["nodes"]);
|
||||
|
||||
await program.parseAsync(["nodes", "list"], { from: "user" });
|
||||
|
||||
expect(registerNodesCli).toHaveBeenCalledTimes(1);
|
||||
expect(nodesAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,53 +1,270 @@
|
||||
import type { Command } from "commander";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { registerPluginCliCommands } from "../../plugins/cli.js";
|
||||
import { registerAcpCli } from "../acp-cli.js";
|
||||
import { registerChannelsCli } from "../channels-cli.js";
|
||||
import { registerCronCli } from "../cron-cli.js";
|
||||
import { registerDaemonCli } from "../daemon-cli.js";
|
||||
import { registerDnsCli } from "../dns-cli.js";
|
||||
import { registerDirectoryCli } from "../directory-cli.js";
|
||||
import { registerDocsCli } from "../docs-cli.js";
|
||||
import { registerExecApprovalsCli } from "../exec-approvals-cli.js";
|
||||
import { registerGatewayCli } from "../gateway-cli.js";
|
||||
import { registerHooksCli } from "../hooks-cli.js";
|
||||
import { registerWebhooksCli } from "../webhooks-cli.js";
|
||||
import { registerLogsCli } from "../logs-cli.js";
|
||||
import { registerModelsCli } from "../models-cli.js";
|
||||
import { registerNodesCli } from "../nodes-cli.js";
|
||||
import { registerNodeCli } from "../node-cli.js";
|
||||
import { registerPairingCli } from "../pairing-cli.js";
|
||||
import { registerPluginsCli } from "../plugins-cli.js";
|
||||
import { registerSandboxCli } from "../sandbox-cli.js";
|
||||
import { registerSecurityCli } from "../security-cli.js";
|
||||
import { registerServiceCli } from "../service-cli.js";
|
||||
import { registerSkillsCli } from "../skills-cli.js";
|
||||
import { registerTuiCli } from "../tui-cli.js";
|
||||
import { registerUpdateCli } from "../update-cli.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { isTruthyEnvValue } from "../../infra/env.js";
|
||||
import { buildParseArgv, getPrimaryCommand, hasHelpOrVersion } from "../argv.js";
|
||||
import { resolveActionArgs } from "./helpers.js";
|
||||
|
||||
export function registerSubCliCommands(program: Command) {
|
||||
registerAcpCli(program);
|
||||
registerDaemonCli(program);
|
||||
registerGatewayCli(program);
|
||||
registerServiceCli(program);
|
||||
registerLogsCli(program);
|
||||
registerModelsCli(program);
|
||||
registerExecApprovalsCli(program);
|
||||
registerNodesCli(program);
|
||||
registerNodeCli(program);
|
||||
registerSandboxCli(program);
|
||||
registerTuiCli(program);
|
||||
registerCronCli(program);
|
||||
registerDnsCli(program);
|
||||
registerDocsCli(program);
|
||||
registerHooksCli(program);
|
||||
registerWebhooksCli(program);
|
||||
registerPairingCli(program);
|
||||
registerPluginsCli(program);
|
||||
registerChannelsCli(program);
|
||||
registerDirectoryCli(program);
|
||||
registerSecurityCli(program);
|
||||
registerSkillsCli(program);
|
||||
registerUpdateCli(program);
|
||||
registerPluginCliCommands(program, loadConfig());
|
||||
type SubCliRegistrar = (program: Command) => Promise<void> | void;
|
||||
|
||||
type SubCliEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
register: SubCliRegistrar;
|
||||
};
|
||||
|
||||
const shouldRegisterPrimaryOnly = (argv: string[]) => {
|
||||
if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_LAZY_SUBCOMMANDS)) return false;
|
||||
if (hasHelpOrVersion(argv)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const shouldEagerRegisterSubcommands = (argv: string[]) => {
|
||||
if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_LAZY_SUBCOMMANDS)) return true;
|
||||
if (hasHelpOrVersion(argv)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const loadConfig = async (): Promise<ClawdbotConfig> => {
|
||||
const mod = await import("../../config/config.js");
|
||||
return mod.loadConfig();
|
||||
};
|
||||
|
||||
const entries: SubCliEntry[] = [
|
||||
{
|
||||
name: "acp",
|
||||
description: "Agent Control Protocol tools",
|
||||
register: async (program) => {
|
||||
const mod = await import("../acp-cli.js");
|
||||
mod.registerAcpCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "daemon",
|
||||
description: "Manage the gateway daemon",
|
||||
register: async (program) => {
|
||||
const mod = await import("../daemon-cli.js");
|
||||
mod.registerDaemonCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gateway",
|
||||
description: "Gateway control",
|
||||
register: async (program) => {
|
||||
const mod = await import("../gateway-cli.js");
|
||||
mod.registerGatewayCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service",
|
||||
description: "Service helpers",
|
||||
register: async (program) => {
|
||||
const mod = await import("../service-cli.js");
|
||||
mod.registerServiceCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "logs",
|
||||
description: "Gateway logs",
|
||||
register: async (program) => {
|
||||
const mod = await import("../logs-cli.js");
|
||||
mod.registerLogsCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "models",
|
||||
description: "Model configuration",
|
||||
register: async (program) => {
|
||||
const mod = await import("../models-cli.js");
|
||||
mod.registerModelsCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "approvals",
|
||||
description: "Exec approvals",
|
||||
register: async (program) => {
|
||||
const mod = await import("../exec-approvals-cli.js");
|
||||
mod.registerExecApprovalsCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nodes",
|
||||
description: "Node commands",
|
||||
register: async (program) => {
|
||||
const mod = await import("../nodes-cli.js");
|
||||
mod.registerNodesCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "node",
|
||||
description: "Node control",
|
||||
register: async (program) => {
|
||||
const mod = await import("../node-cli.js");
|
||||
mod.registerNodeCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sandbox",
|
||||
description: "Sandbox tools",
|
||||
register: async (program) => {
|
||||
const mod = await import("../sandbox-cli.js");
|
||||
mod.registerSandboxCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tui",
|
||||
description: "Terminal UI",
|
||||
register: async (program) => {
|
||||
const mod = await import("../tui-cli.js");
|
||||
mod.registerTuiCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cron",
|
||||
description: "Cron scheduler",
|
||||
register: async (program) => {
|
||||
const mod = await import("../cron-cli.js");
|
||||
mod.registerCronCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dns",
|
||||
description: "DNS helpers",
|
||||
register: async (program) => {
|
||||
const mod = await import("../dns-cli.js");
|
||||
mod.registerDnsCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "docs",
|
||||
description: "Docs helpers",
|
||||
register: async (program) => {
|
||||
const mod = await import("../docs-cli.js");
|
||||
mod.registerDocsCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hooks",
|
||||
description: "Hooks tooling",
|
||||
register: async (program) => {
|
||||
const mod = await import("../hooks-cli.js");
|
||||
mod.registerHooksCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "webhooks",
|
||||
description: "Webhook helpers",
|
||||
register: async (program) => {
|
||||
const mod = await import("../webhooks-cli.js");
|
||||
mod.registerWebhooksCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pairing",
|
||||
description: "Pairing helpers",
|
||||
register: async (program) => {
|
||||
const mod = await import("../pairing-cli.js");
|
||||
mod.registerPairingCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plugins",
|
||||
description: "Plugin management",
|
||||
register: async (program) => {
|
||||
const mod = await import("../plugins-cli.js");
|
||||
mod.registerPluginsCli(program);
|
||||
const { registerPluginCliCommands } = await import("../../plugins/cli.js");
|
||||
registerPluginCliCommands(program, await loadConfig());
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "channels",
|
||||
description: "Channel management",
|
||||
register: async (program) => {
|
||||
const mod = await import("../channels-cli.js");
|
||||
mod.registerChannelsCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "directory",
|
||||
description: "Directory commands",
|
||||
register: async (program) => {
|
||||
const mod = await import("../directory-cli.js");
|
||||
mod.registerDirectoryCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "security",
|
||||
description: "Security helpers",
|
||||
register: async (program) => {
|
||||
const mod = await import("../security-cli.js");
|
||||
mod.registerSecurityCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skills",
|
||||
description: "Skills management",
|
||||
register: async (program) => {
|
||||
const mod = await import("../skills-cli.js");
|
||||
mod.registerSkillsCli(program);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
description: "CLI update helpers",
|
||||
register: async (program) => {
|
||||
const mod = await import("../update-cli.js");
|
||||
mod.registerUpdateCli(program);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function removeCommand(program: Command, command: Command) {
|
||||
const commands = program.commands as Command[];
|
||||
const index = commands.indexOf(command);
|
||||
if (index >= 0) {
|
||||
commands.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function registerLazyCommand(program: Command, entry: SubCliEntry) {
|
||||
const placeholder = program.command(entry.name).description(entry.description);
|
||||
placeholder.allowUnknownOption(true);
|
||||
placeholder.allowExcessArguments(true);
|
||||
placeholder.action(async (...actionArgs) => {
|
||||
removeCommand(program, placeholder);
|
||||
await entry.register(program);
|
||||
const actionCommand = actionArgs.at(-1) as Command | undefined;
|
||||
const root = actionCommand?.parent ?? program;
|
||||
const rawArgs = (root as Command & { rawArgs?: string[] }).rawArgs;
|
||||
const actionArgsList = resolveActionArgs(actionCommand);
|
||||
const fallbackArgv = actionCommand?.name()
|
||||
? [actionCommand.name(), ...actionArgsList]
|
||||
: actionArgsList;
|
||||
const parseArgv = buildParseArgv({
|
||||
programName: program.name(),
|
||||
rawArgs,
|
||||
fallbackArgv,
|
||||
});
|
||||
await program.parseAsync(parseArgv);
|
||||
});
|
||||
}
|
||||
|
||||
export function registerSubCliCommands(program: Command, argv: string[] = process.argv) {
|
||||
if (shouldEagerRegisterSubcommands(argv)) {
|
||||
for (const entry of entries) {
|
||||
void entry.register(program);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const primary = getPrimaryCommand(argv);
|
||||
if (primary && shouldRegisterPrimaryOnly(argv)) {
|
||||
const entry = entries.find((candidate) => candidate.name === primary);
|
||||
if (entry) {
|
||||
registerLazyCommand(program, entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (const candidate of entries) {
|
||||
registerLazyCommand(program, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user