refactor: streamline routed cli setup
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user