refactor: share cli runtime error handling

This commit is contained in:
Peter Steinberger
2026-01-19 00:52:17 +00:00
parent c532d161c4
commit 1fec41b3df
16 changed files with 288 additions and 452 deletions

View File

@@ -4,7 +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";
import { runCommandWithRuntime } from "../../cli-utils.js";
export type MessageCliHelpers = {
withMessageBase: (command: Command) => Command;
@@ -31,10 +31,11 @@ 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 {
await runCommandWithRuntime(
defaultRuntime,
async () => {
await messageCommand(
{
...(() => {
@@ -49,10 +50,12 @@ export function createMessageCliHelpers(
deps,
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
}
},
(err) => {
defaultRuntime.error(danger(String(err)));
defaultRuntime.exit(1);
},
);
};
// `message` is only used for `message.help({ error: true })`, keep the

View File

@@ -8,8 +8,8 @@ import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { hasExplicitOptions } from "../command-options.js";
import { createDefaultDeps } from "../deps.js";
import { runCommandWithRuntime } from "../cli-utils.js";
import { collectOption } from "./helpers.js";
import { ensureConfigReady } from "./config-guard.js";
export function registerAgentCommands(program: Command, args: { agentChannelOptions: string }) {
program
@@ -54,17 +54,13 @@ 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).
const deps = createDefaultDeps();
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await agentCliCommand(opts, defaultRuntime, deps);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
const agents = program
@@ -82,16 +78,12 @@ ${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 runCommandWithRuntime(defaultRuntime, async () => {
await agentsListCommand(
{ json: Boolean(opts.json), bindings: Boolean(opts.bindings) },
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
agents
@@ -104,8 +96,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 {
await runCommandWithRuntime(defaultRuntime, async () => {
const hasFlags = hasExplicitOptions(command, [
"workspace",
"model",
@@ -126,10 +117,7 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
defaultRuntime,
{ hasFlags },
);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
agents
@@ -138,8 +126,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 runCommandWithRuntime(defaultRuntime, async () => {
await agentsDeleteCommand(
{
id: String(id),
@@ -148,19 +135,12 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/agent", "docs.clawd.bot/cli/agent
},
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
agents.action(async () => {
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await agentsListCommand({}, defaultRuntime);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
}

View File

@@ -7,6 +7,7 @@ import {
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
export function registerConfigureCommand(program: Command) {
program
@@ -24,7 +25,7 @@ export function registerConfigureCommand(program: Command) {
[] as string[],
)
.action(async (opts) => {
try {
await runCommandWithRuntime(defaultRuntime, async () => {
const sections: string[] = Array.isArray(opts.section)
? opts.section
.map((value: unknown) => (typeof value === "string" ? value.trim() : ""))
@@ -45,9 +46,6 @@ export function registerConfigureCommand(program: Command) {
}
await configureCommandWithSections(sections as never, defaultRuntime);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
}

View File

@@ -6,6 +6,7 @@ import { uninstallCommand } from "../../commands/uninstall.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
export function registerMaintenanceCommands(program: Command) {
program
@@ -24,7 +25,7 @@ export function registerMaintenanceCommands(program: Command) {
.option("--generate-gateway-token", "Generate and configure a gateway token", false)
.option("--deep", "Scan system services for extra gateway installs", false)
.action(async (opts) => {
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await doctorCommand(defaultRuntime, {
workspaceSuggestions: opts.workspaceSuggestions,
yes: Boolean(opts.yes),
@@ -34,10 +35,7 @@ export function registerMaintenanceCommands(program: Command) {
generateGatewayToken: Boolean(opts.generateGatewayToken),
deep: Boolean(opts.deep),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
program
@@ -50,14 +48,11 @@ export function registerMaintenanceCommands(program: Command) {
)
.option("--no-open", "Print URL but do not launch a browser", false)
.action(async (opts) => {
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await dashboardCommand(defaultRuntime, {
noOpen: Boolean(opts.noOpen),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
program
@@ -73,17 +68,14 @@ export function registerMaintenanceCommands(program: Command) {
.option("--non-interactive", "Disable prompts (requires --scope + --yes)", false)
.option("--dry-run", "Print actions without removing files", false)
.action(async (opts) => {
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await resetCommand(defaultRuntime, {
scope: opts.scope,
yes: Boolean(opts.yes),
nonInteractive: Boolean(opts.nonInteractive),
dryRun: Boolean(opts.dryRun),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
program
@@ -103,7 +95,7 @@ export function registerMaintenanceCommands(program: Command) {
.option("--non-interactive", "Disable prompts (requires --yes)", false)
.option("--dry-run", "Print actions without removing files", false)
.action(async (opts) => {
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await uninstallCommand(defaultRuntime, {
service: Boolean(opts.service),
state: Boolean(opts.state),
@@ -114,9 +106,6 @@ export function registerMaintenanceCommands(program: Command) {
nonInteractive: Boolean(opts.nonInteractive),
dryRun: Boolean(opts.dryRun),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
}

View File

@@ -11,6 +11,7 @@ import type {
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
function resolveInstallDaemonFlag(
command: unknown,
@@ -94,7 +95,7 @@ export function registerOnboardCommand(program: Command) {
.option("--node-manager <name>", "Node manager for skills: npm|pnpm|bun")
.option("--json", "Output JSON summary", false)
.action(async (opts, command) => {
try {
await runCommandWithRuntime(defaultRuntime, async () => {
const installDaemon = resolveInstallDaemonFlag(command, {
installDaemon: Boolean(opts.installDaemon),
});
@@ -147,9 +148,6 @@ export function registerOnboardCommand(program: Command) {
},
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
}

View File

@@ -5,6 +5,7 @@ import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { hasExplicitOptions } from "../command-options.js";
import { runCommandWithRuntime } from "../cli-utils.js";
export function registerSetupCommand(program: Command) {
program
@@ -25,7 +26,7 @@ export function registerSetupCommand(program: Command) {
.option("--remote-url <url>", "Remote Gateway WebSocket URL")
.option("--remote-token <token>", "Remote Gateway token (optional)")
.action(async (opts, command) => {
try {
await runCommandWithRuntime(defaultRuntime, async () => {
const hasWizardFlags = hasExplicitOptions(command, [
"wizard",
"nonInteractive",
@@ -47,9 +48,6 @@ export function registerSetupCommand(program: Command) {
return;
}
await setupCommand({ workspace: opts.workspace as string | undefined }, defaultRuntime);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
}

View File

@@ -6,8 +6,22 @@ import { setVerbose } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
import { parsePositiveIntOrUndefined } from "./helpers.js";
import { ensureConfigReady } from "./config-guard.js";
function resolveVerbose(opts: { verbose?: boolean; debug?: boolean }): boolean {
return Boolean(opts.verbose || opts.debug);
}
function parseTimeoutMs(timeout: unknown): number | null | undefined {
const parsed = parsePositiveIntOrUndefined(timeout);
if (timeout !== undefined && parsed === undefined) {
defaultRuntime.error("--timeout must be a positive integer (milliseconds)");
defaultRuntime.exit(1);
return null;
}
return parsed;
}
export function registerStatusHealthSessionsCommands(program: Command) {
program
@@ -38,16 +52,13 @@ 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);
const verbose = resolveVerbose(opts);
setVerbose(verbose);
const timeout = parsePositiveIntOrUndefined(opts.timeout);
if (opts.timeout !== undefined && timeout === undefined) {
defaultRuntime.error("--timeout must be a positive integer (milliseconds)");
defaultRuntime.exit(1);
const timeout = parseTimeoutMs(opts.timeout);
if (timeout === null) {
return;
}
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await statusCommand(
{
json: Boolean(opts.json),
@@ -59,10 +70,7 @@ Examples:
},
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
program
@@ -78,16 +86,13 @@ 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);
const verbose = resolveVerbose(opts);
setVerbose(verbose);
const timeout = parsePositiveIntOrUndefined(opts.timeout);
if (opts.timeout !== undefined && timeout === undefined) {
defaultRuntime.error("--timeout must be a positive integer (milliseconds)");
defaultRuntime.exit(1);
const timeout = parseTimeoutMs(opts.timeout);
if (timeout === null) {
return;
}
try {
await runCommandWithRuntime(defaultRuntime, async () => {
await healthCommand(
{
json: Boolean(opts.json),
@@ -96,10 +101,7 @@ Examples:
},
defaultRuntime,
);
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
});
program
@@ -126,7 +128,6 @@ 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(
{