refactor: streamline routed cli setup
This commit is contained in:
@@ -5,8 +5,12 @@ import {
|
|||||||
getFlagValue,
|
getFlagValue,
|
||||||
getCommandPath,
|
getCommandPath,
|
||||||
getPrimaryCommand,
|
getPrimaryCommand,
|
||||||
|
getPositiveIntFlagValue,
|
||||||
|
getVerboseFlag,
|
||||||
hasHelpOrVersion,
|
hasHelpOrVersion,
|
||||||
hasFlag,
|
hasFlag,
|
||||||
|
shouldMigrateState,
|
||||||
|
shouldMigrateStateFromPath,
|
||||||
} from "./argv.js";
|
} from "./argv.js";
|
||||||
|
|
||||||
describe("argv helpers", () => {
|
describe("argv helpers", () => {
|
||||||
@@ -46,6 +50,27 @@ describe("argv helpers", () => {
|
|||||||
expect(getFlagValue(["node", "clawdbot", "--", "--timeout=99"], "--timeout")).toBeUndefined();
|
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", () => {
|
it("builds parse argv from raw args", () => {
|
||||||
const nodeArgv = buildParseArgv({
|
const nodeArgv = buildParseArgv({
|
||||||
programName: "clawdbot",
|
programName: "clawdbot",
|
||||||
@@ -73,4 +98,19 @@ describe("argv helpers", () => {
|
|||||||
});
|
});
|
||||||
expect(fallbackArgv).toEqual(["node", "clawdbot", "status"]);
|
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);
|
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 {
|
export function hasFlag(argv: string[], name: string): boolean {
|
||||||
const args = argv.slice(2);
|
const args = argv.slice(2);
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
@@ -39,6 +45,24 @@ export function getFlagValue(argv: string[], name: string): string | null | unde
|
|||||||
return undefined;
|
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[] {
|
export function getCommandPath(argv: string[], depth = 2): string[] {
|
||||||
const args = argv.slice(2);
|
const args = argv.slice(2);
|
||||||
const path: string[] = [];
|
const path: string[] = [];
|
||||||
@@ -83,12 +107,15 @@ export function buildParseArgv(params: {
|
|||||||
return ["node", programName || "clawdbot", ...normalizedArgv];
|
return ["node", programName || "clawdbot", ...normalizedArgv];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isReadOnlyCommand(argv: string[]): boolean {
|
export function shouldMigrateStateFromPath(path: string[]): boolean {
|
||||||
const path = getCommandPath(argv, 2);
|
if (path.length === 0) return true;
|
||||||
if (path.length === 0) return false;
|
|
||||||
const [primary, secondary] = path;
|
const [primary, secondary] = path;
|
||||||
if (primary === "health" || primary === "status" || primary === "sessions") return true;
|
if (primary === "health" || primary === "status" || primary === "sessions") return false;
|
||||||
if (primary === "memory" && secondary === "status") return true;
|
if (primary === "memory" && secondary === "status") return false;
|
||||||
if (primary === "agents" && secondary === "list") return true;
|
if (primary === "agent") return false;
|
||||||
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,
|
resolveDefaultAgentId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./program/config-guard.js", () => ({
|
|
||||||
ensureConfigReady: vi.fn(async () => {}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
getMemorySearchManager.mockReset();
|
getMemorySearchManager.mockReset();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { defaultRuntime } from "../runtime.js";
|
|||||||
import { formatDocsLink } from "../terminal/links.js";
|
import { formatDocsLink } from "../terminal/links.js";
|
||||||
import { colorize, isRich, theme } from "../terminal/theme.js";
|
import { colorize, isRich, theme } from "../terminal/theme.js";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { ensureConfigReady } from "./program/config-guard.js";
|
|
||||||
|
|
||||||
type MemoryCommandOptions = {
|
type MemoryCommandOptions = {
|
||||||
agent?: string;
|
agent?: string;
|
||||||
@@ -53,7 +52,6 @@ function resolveAgentIds(cfg: ReturnType<typeof loadConfig>, agent?: string): st
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
export async function runMemoryStatus(opts: MemoryCommandOptions) {
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
|
||||||
setVerbose(Boolean(opts.verbose));
|
setVerbose(Boolean(opts.verbose));
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const agentIds = resolveAgentIds(cfg, opts.agent);
|
const agentIds = resolveAgentIds(cfg, opts.agent);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import { emitCliBanner } from "../banner.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";
|
import { ensureConfigReady } from "./config-guard.js";
|
||||||
|
|
||||||
function setProcessTitleForCommand(actionCommand: Command) {
|
function setProcessTitleForCommand(actionCommand: Command) {
|
||||||
@@ -22,7 +22,7 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
|||||||
if (hasHelpOrVersion(argv)) return;
|
if (hasHelpOrVersion(argv)) return;
|
||||||
const [primary] = getCommandPath(argv, 1);
|
const [primary] = getCommandPath(argv, 1);
|
||||||
if (primary === "doctor") return;
|
if (primary === "doctor") return;
|
||||||
const migrateState = !isReadOnlyCommand(argv);
|
const migrateState = shouldMigrateState(argv);
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState });
|
await ensureConfigReady({ runtime: defaultRuntime, migrateState });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,30 @@ import { ensurePluginRegistryLoaded } from "./plugin-registry.js";
|
|||||||
import { isTruthyEnvValue } from "../infra/env.js";
|
import { isTruthyEnvValue } from "../infra/env.js";
|
||||||
import { emitCliBanner } from "./banner.js";
|
import { emitCliBanner } from "./banner.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { getCommandPath, getFlagValue, hasFlag, hasHelpOrVersion } from "./argv.js";
|
import {
|
||||||
import { parsePositiveIntOrUndefined } from "./program/helpers.js";
|
getCommandPath,
|
||||||
|
getFlagValue,
|
||||||
|
getPositiveIntFlagValue,
|
||||||
|
getVerboseFlag,
|
||||||
|
hasFlag,
|
||||||
|
hasHelpOrVersion,
|
||||||
|
shouldMigrateStateFromPath,
|
||||||
|
} from "./argv.js";
|
||||||
import { ensureConfigReady } from "./program/config-guard.js";
|
import { ensureConfigReady } from "./program/config-guard.js";
|
||||||
import { runMemoryStatus } from "./memory-cli.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> {
|
export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
||||||
if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_ROUTE_FIRST)) return false;
|
if (isTruthyEnvValue(process.env.CLAWDBOT_DISABLE_ROUTE_FIRST)) return false;
|
||||||
if (hasHelpOrVersion(argv)) 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 path = getCommandPath(argv, 2);
|
||||||
const [primary, secondary] = path;
|
const [primary, secondary] = path;
|
||||||
if (!primary) return false;
|
if (!primary) return false;
|
||||||
|
const migrateState = shouldMigrateStateFromPath(path);
|
||||||
|
|
||||||
if (primary === "health") {
|
if (primary === "health") {
|
||||||
emitCliBanner(VERSION, { argv });
|
await prepareRoutedCommand({ argv, migrateState, loadPlugins: true });
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
|
||||||
ensurePluginRegistryLoaded();
|
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const verbose = hasFlag(argv, "--verbose") || hasFlag(argv, "--debug");
|
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
||||||
const timeout = getFlagValue(argv, "--timeout");
|
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
|
||||||
if (timeout === null) return false;
|
if (timeoutMs === null) return false;
|
||||||
const timeoutMs = parsePositiveIntOrUndefined(timeout);
|
|
||||||
setVerbose(verbose);
|
setVerbose(verbose);
|
||||||
await healthCommand({ json, timeoutMs, verbose }, defaultRuntime);
|
await healthCommand({ json, timeoutMs, verbose }, defaultRuntime);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "status") {
|
if (primary === "status") {
|
||||||
emitCliBanner(VERSION, { argv });
|
await prepareRoutedCommand({ argv, migrateState, loadPlugins: true });
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
|
||||||
ensurePluginRegistryLoaded();
|
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const deep = hasFlag(argv, "--deep");
|
const deep = hasFlag(argv, "--deep");
|
||||||
const all = hasFlag(argv, "--all");
|
const all = hasFlag(argv, "--all");
|
||||||
const usage = hasFlag(argv, "--usage");
|
const usage = hasFlag(argv, "--usage");
|
||||||
const verbose = hasFlag(argv, "--verbose") || hasFlag(argv, "--debug");
|
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
||||||
const timeout = getFlagValue(argv, "--timeout");
|
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
|
||||||
if (timeout === null) return false;
|
if (timeoutMs === null) return false;
|
||||||
const timeoutMs = parsePositiveIntOrUndefined(timeout);
|
|
||||||
setVerbose(verbose);
|
setVerbose(verbose);
|
||||||
await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime);
|
await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "sessions") {
|
if (primary === "sessions") {
|
||||||
emitCliBanner(VERSION, { argv });
|
await prepareRoutedCommand({ argv, migrateState });
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: false });
|
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const verbose = hasFlag(argv, "--verbose");
|
const verbose = getVerboseFlag(argv);
|
||||||
const store = getFlagValue(argv, "--store");
|
const store = getFlagValue(argv, "--store");
|
||||||
if (store === null) return false;
|
if (store === null) return false;
|
||||||
const active = getFlagValue(argv, "--active");
|
const active = getFlagValue(argv, "--active");
|
||||||
@@ -67,8 +80,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "agents" && secondary === "list") {
|
if (primary === "agents" && secondary === "list") {
|
||||||
emitCliBanner(VERSION, { argv });
|
await prepareRoutedCommand({ argv, migrateState });
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: true });
|
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const bindings = hasFlag(argv, "--bindings");
|
const bindings = hasFlag(argv, "--bindings");
|
||||||
await agentsListCommand({ json, bindings }, defaultRuntime);
|
await agentsListCommand({ json, bindings }, defaultRuntime);
|
||||||
@@ -76,7 +88,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "memory" && secondary === "status") {
|
if (primary === "memory" && secondary === "status") {
|
||||||
emitCliBanner(VERSION, { argv });
|
await prepareRoutedCommand({ argv, migrateState });
|
||||||
const agent = getFlagValue(argv, "--agent");
|
const agent = getFlagValue(argv, "--agent");
|
||||||
if (agent === null) return false;
|
if (agent === null) return false;
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
|
|||||||
Reference in New Issue
Block a user