refactor: streamline routed cli setup

This commit is contained in:
Peter Steinberger
2026-01-19 00:52:13 +00:00
parent 989543c9c3
commit c532d161c4
6 changed files with 110 additions and 37 deletions

View File

@@ -5,8 +5,12 @@ import {
getFlagValue,
getCommandPath,
getPrimaryCommand,
getPositiveIntFlagValue,
getVerboseFlag,
hasHelpOrVersion,
hasFlag,
shouldMigrateState,
shouldMigrateStateFromPath,
} from "./argv.js";
describe("argv helpers", () => {
@@ -46,6 +50,27 @@ describe("argv helpers", () => {
expect(getFlagValue(["node", "clawdbot", "--", "--timeout=99"], "--timeout")).toBeUndefined();
});
it("parses verbose flags", () => {
expect(getVerboseFlag(["node", "clawdbot", "status", "--verbose"])).toBe(true);
expect(getVerboseFlag(["node", "clawdbot", "status", "--debug"])).toBe(false);
expect(getVerboseFlag(["node", "clawdbot", "status", "--debug"], { includeDebug: true })).toBe(
true,
);
});
it("parses positive integer flag values", () => {
expect(getPositiveIntFlagValue(["node", "clawdbot", "status"], "--timeout")).toBeUndefined();
expect(
getPositiveIntFlagValue(["node", "clawdbot", "status", "--timeout"], "--timeout"),
).toBeNull();
expect(
getPositiveIntFlagValue(["node", "clawdbot", "status", "--timeout", "5000"], "--timeout"),
).toBe(5000);
expect(
getPositiveIntFlagValue(["node", "clawdbot", "status", "--timeout", "nope"], "--timeout"),
).toBeUndefined();
});
it("builds parse argv from raw args", () => {
const nodeArgv = buildParseArgv({
programName: "clawdbot",
@@ -73,4 +98,19 @@ describe("argv helpers", () => {
});
expect(fallbackArgv).toEqual(["node", "clawdbot", "status"]);
});
it("decides when to migrate state", () => {
expect(shouldMigrateState(["node", "clawdbot", "status"])).toBe(false);
expect(shouldMigrateState(["node", "clawdbot", "health"])).toBe(false);
expect(shouldMigrateState(["node", "clawdbot", "sessions"])).toBe(false);
expect(shouldMigrateState(["node", "clawdbot", "memory", "status"])).toBe(false);
expect(shouldMigrateState(["node", "clawdbot", "agent", "--message", "hi"])).toBe(false);
expect(shouldMigrateState(["node", "clawdbot", "agents", "list"])).toBe(true);
expect(shouldMigrateState(["node", "clawdbot", "message", "send"])).toBe(true);
});
it("reuses command path for migrate state decisions", () => {
expect(shouldMigrateStateFromPath(["status"])).toBe(false);
expect(shouldMigrateStateFromPath(["agents", "list"])).toBe(true);
});
});

View File

@@ -13,6 +13,12 @@ function isValueToken(arg: string | undefined): boolean {
return /^-\d+(?:\.\d+)?$/.test(arg);
}
function parsePositiveInt(value: string): number | undefined {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed <= 0) return undefined;
return parsed;
}
export function hasFlag(argv: string[], name: string): boolean {
const args = argv.slice(2);
for (const arg of args) {
@@ -39,6 +45,24 @@ export function getFlagValue(argv: string[], name: string): string | null | unde
return undefined;
}
export function getVerboseFlag(
argv: string[],
options?: { includeDebug?: boolean },
): boolean {
if (hasFlag(argv, "--verbose")) return true;
if (options?.includeDebug && hasFlag(argv, "--debug")) return true;
return false;
}
export function getPositiveIntFlagValue(
argv: string[],
name: string,
): number | null | undefined {
const raw = getFlagValue(argv, name);
if (raw === null || raw === undefined) return raw;
return parsePositiveInt(raw);
}
export function getCommandPath(argv: string[], depth = 2): string[] {
const args = argv.slice(2);
const path: string[] = [];
@@ -83,12 +107,15 @@ export function buildParseArgv(params: {
return ["node", programName || "clawdbot", ...normalizedArgv];
}
export function isReadOnlyCommand(argv: string[]): boolean {
const path = getCommandPath(argv, 2);
if (path.length === 0) return false;
export function shouldMigrateStateFromPath(path: string[]): boolean {
if (path.length === 0) return true;
const [primary, secondary] = path;
if (primary === "health" || primary === "status" || primary === "sessions") return true;
if (primary === "memory" && secondary === "status") return true;
if (primary === "agents" && secondary === "list") return true;
return false;
if (primary === "health" || primary === "status" || primary === "sessions") return false;
if (primary === "memory" && secondary === "status") return false;
if (primary === "agent") return false;
return true;
}
export function shouldMigrateState(argv: string[]): boolean {
return shouldMigrateStateFromPath(getCommandPath(argv, 2));
}

View File

@@ -17,10 +17,6 @@ vi.mock("../agents/agent-scope.js", () => ({
resolveDefaultAgentId,
}));
vi.mock("./program/config-guard.js", () => ({
ensureConfigReady: vi.fn(async () => {}),
}));
afterEach(async () => {
vi.restoreAllMocks();
getMemorySearchManager.mockReset();

View File

@@ -13,7 +13,6 @@ import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
import { resolveStateDir } from "../config/paths.js";
import { ensureConfigReady } from "./program/config-guard.js";
type MemoryCommandOptions = {
agent?: string;
@@ -53,7 +52,6 @@ function resolveAgentIds(cfg: ReturnType<typeof loadConfig>, agent?: string): st
}
export async function runMemoryStatus(opts: MemoryCommandOptions) {
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
setVerbose(Boolean(opts.verbose));
const cfg = loadConfig();
const agentIds = resolveAgentIds(cfg, opts.agent);

View File

@@ -1,7 +1,7 @@
import type { Command } from "commander";
import { defaultRuntime } from "../../runtime.js";
import { emitCliBanner } from "../banner.js";
import { getCommandPath, hasHelpOrVersion, isReadOnlyCommand } from "../argv.js";
import { getCommandPath, hasHelpOrVersion, shouldMigrateState } from "../argv.js";
import { ensureConfigReady } from "./config-guard.js";
function setProcessTitleForCommand(actionCommand: Command) {
@@ -22,7 +22,7 @@ export function registerPreActionHooks(program: Command, programVersion: string)
if (hasHelpOrVersion(argv)) return;
const [primary] = getCommandPath(argv, 1);
if (primary === "doctor") return;
const migrateState = !isReadOnlyCommand(argv);
const migrateState = shouldMigrateState(argv);
await ensureConfigReady({ runtime: defaultRuntime, migrateState });
});
}

View File

@@ -8,11 +8,30 @@ import { ensurePluginRegistryLoaded } from "./plugin-registry.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { emitCliBanner } from "./banner.js";
import { VERSION } from "../version.js";
import { getCommandPath, getFlagValue, hasFlag, hasHelpOrVersion } from "./argv.js";
import { parsePositiveIntOrUndefined } from "./program/helpers.js";
import {
getCommandPath,
getFlagValue,
getPositiveIntFlagValue,
getVerboseFlag,
hasFlag,
hasHelpOrVersion,
shouldMigrateStateFromPath,
} from "./argv.js";
import { ensureConfigReady } from "./program/config-guard.js";
import { runMemoryStatus } from "./memory-cli.js";
async function prepareRoutedCommand(params: {
argv: string[];
migrateState: boolean;
loadPlugins?: boolean;
}) {
emitCliBanner(VERSION, { argv: params.argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: params.migrateState });
if (params.loadPlugins) {
ensurePluginRegistryLoaded();
}
}
export async function tryRouteCli(argv: string[]): Promise<boolean> {
if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_ROUTE_FIRST)) return false;
if (hasHelpOrVersion(argv)) return false;
@@ -20,43 +39,37 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
const path = getCommandPath(argv, 2);
const [primary, secondary] = path;
if (!primary) return false;
const migrateState = shouldMigrateStateFromPath(path);
if (primary === "health") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
ensurePluginRegistryLoaded();
await prepareRoutedCommand({ argv, migrateState, loadPlugins: true });
const json = hasFlag(argv, "--json");
const verbose = hasFlag(argv, "--verbose") || hasFlag(argv, "--debug");
const timeout = getFlagValue(argv, "--timeout");
if (timeout === null) return false;
const timeoutMs = parsePositiveIntOrUndefined(timeout);
const verbose = getVerboseFlag(argv, { includeDebug: true });
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
if (timeoutMs === null) return false;
setVerbose(verbose);
await healthCommand({ json, timeoutMs, verbose }, defaultRuntime);
return true;
}
if (primary === "status") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
ensurePluginRegistryLoaded();
await prepareRoutedCommand({ argv, migrateState, loadPlugins: true });
const json = hasFlag(argv, "--json");
const deep = hasFlag(argv, "--deep");
const all = hasFlag(argv, "--all");
const usage = hasFlag(argv, "--usage");
const verbose = hasFlag(argv, "--verbose") || hasFlag(argv, "--debug");
const timeout = getFlagValue(argv, "--timeout");
if (timeout === null) return false;
const timeoutMs = parsePositiveIntOrUndefined(timeout);
const verbose = getVerboseFlag(argv, { includeDebug: true });
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
if (timeoutMs === null) return false;
setVerbose(verbose);
await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime);
return true;
}
if (primary === "sessions") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
await prepareRoutedCommand({ argv, migrateState });
const json = hasFlag(argv, "--json");
const verbose = hasFlag(argv, "--verbose");
const verbose = getVerboseFlag(argv);
const store = getFlagValue(argv, "--store");
if (store === null) return false;
const active = getFlagValue(argv, "--active");
@@ -67,8 +80,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
}
if (primary === "agents" && secondary === "list") {
emitCliBanner(VERSION, { argv });
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
await prepareRoutedCommand({ argv, migrateState });
const json = hasFlag(argv, "--json");
const bindings = hasFlag(argv, "--bindings");
await agentsListCommand({ json, bindings }, defaultRuntime);
@@ -76,7 +88,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
}
if (primary === "memory" && secondary === "status") {
emitCliBanner(VERSION, { argv });
await prepareRoutedCommand({ argv, migrateState });
const agent = getFlagValue(argv, "--agent");
if (agent === null) return false;
const json = hasFlag(argv, "--json");