perf(cli): reduce read-only startup overhead

This commit is contained in:
Peter Steinberger
2026-02-14 01:18:20 +00:00
parent 54a242eaad
commit f86840f4df
8 changed files with 326 additions and 17 deletions

View File

@@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const loadAndMaybeMigrateDoctorConfigMock = vi.hoisted(() => vi.fn());
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
vi.mock("../../commands/doctor-config-flow.js", () => ({
loadAndMaybeMigrateDoctorConfig: loadAndMaybeMigrateDoctorConfigMock,
}));
vi.mock("../../config/config.js", () => ({
readConfigFileSnapshot: readConfigFileSnapshotMock,
}));
function makeSnapshot() {
return {
exists: false,
valid: true,
issues: [],
legacyIssues: [],
path: "/tmp/openclaw.json",
};
}
function makeRuntime() {
return {
error: vi.fn(),
exit: vi.fn(),
};
}
describe("ensureConfigReady", () => {
beforeEach(() => {
vi.clearAllMocks();
readConfigFileSnapshotMock.mockResolvedValue(makeSnapshot());
});
it("skips doctor flow for read-only fast path commands", async () => {
vi.resetModules();
const { ensureConfigReady } = await import("./config-guard.js");
await ensureConfigReady({ runtime: makeRuntime() as never, commandPath: ["status"] });
expect(loadAndMaybeMigrateDoctorConfigMock).not.toHaveBeenCalled();
});
it("runs doctor flow for commands that may mutate state", async () => {
vi.resetModules();
const { ensureConfigReady } = await import("./config-guard.js");
await ensureConfigReady({ runtime: makeRuntime() as never, commandPath: ["message"] });
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -3,6 +3,7 @@ import { loadAndMaybeMigrateDoctorConfig } from "../../commands/doctor-config-fl
import { readConfigFileSnapshot } from "../../config/config.js";
import { colorize, isRich, theme } from "../../terminal/theme.js";
import { shortenHomePath } from "../../utils.js";
import { shouldMigrateStateFromPath } from "../argv.js";
import { formatCliCommand } from "../command-format.js";
const ALLOWED_INVALID_COMMANDS = new Set(["doctor", "logs", "health", "help", "status"]);
@@ -28,7 +29,8 @@ export async function ensureConfigReady(params: {
runtime: RuntimeEnv;
commandPath?: string[];
}): Promise<void> {
if (!didRunDoctorConfigFlow) {
const commandPath = params.commandPath ?? [];
if (!didRunDoctorConfigFlow && shouldMigrateStateFromPath(commandPath)) {
didRunDoctorConfigFlow = true;
await loadAndMaybeMigrateDoctorConfig({
options: { nonInteractive: true },
@@ -37,8 +39,8 @@ export async function ensureConfigReady(params: {
}
const snapshot = await readConfigFileSnapshot();
const commandName = params.commandPath?.[0];
const subcommandName = params.commandPath?.[1];
const commandName = commandPath[0];
const subcommandName = commandPath[1];
const allowInvalid = commandName
? ALLOWED_INVALID_COMMANDS.has(commandName) ||
(commandName === "gateway" &&

View File

@@ -103,6 +103,34 @@ function getCommandPositionals(argv: string[]): string[] {
return out;
}
function getFlagValues(argv: string[], name: string): string[] | null {
const values: string[] = [];
const args = argv.slice(2);
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === "--") {
break;
}
if (arg === name) {
const next = args[i + 1];
if (!next || next === "--" || next.startsWith("-")) {
return null;
}
values.push(next);
i += 1;
continue;
}
if (arg.startsWith(`${name}=`)) {
const value = arg.slice(name.length + 1).trim();
if (!value) {
return null;
}
values.push(value);
}
}
return values;
}
const routeConfigGet: RouteSpec = {
match: (path) => path[0] === "config" && path[1] === "get",
run: async (argv) => {
@@ -132,6 +160,80 @@ const routeConfigUnset: RouteSpec = {
},
};
const routeModelsList: RouteSpec = {
match: (path) => path[0] === "models" && path[1] === "list",
run: async (argv) => {
const provider = getFlagValue(argv, "--provider");
if (provider === null) {
return false;
}
const all = hasFlag(argv, "--all");
const local = hasFlag(argv, "--local");
const json = hasFlag(argv, "--json");
const plain = hasFlag(argv, "--plain");
const { modelsListCommand } = await import("../../commands/models.js");
await modelsListCommand({ all, local, provider, json, plain }, defaultRuntime);
return true;
},
};
const routeModelsStatus: RouteSpec = {
match: (path) => path[0] === "models" && path[1] === "status",
run: async (argv) => {
const probeProvider = getFlagValue(argv, "--probe-provider");
if (probeProvider === null) {
return false;
}
const probeTimeout = getFlagValue(argv, "--probe-timeout");
if (probeTimeout === null) {
return false;
}
const probeConcurrency = getFlagValue(argv, "--probe-concurrency");
if (probeConcurrency === null) {
return false;
}
const probeMaxTokens = getFlagValue(argv, "--probe-max-tokens");
if (probeMaxTokens === null) {
return false;
}
const agent = getFlagValue(argv, "--agent");
if (agent === null) {
return false;
}
const probeProfileValues = getFlagValues(argv, "--probe-profile");
if (probeProfileValues === null) {
return false;
}
const probeProfile =
probeProfileValues.length === 0
? undefined
: probeProfileValues.length === 1
? probeProfileValues[0]
: probeProfileValues;
const json = hasFlag(argv, "--json");
const plain = hasFlag(argv, "--plain");
const check = hasFlag(argv, "--check");
const probe = hasFlag(argv, "--probe");
const { modelsStatusCommand } = await import("../../commands/models.js");
await modelsStatusCommand(
{
json,
plain,
check,
probe,
probeProvider,
probeProfile,
probeTimeout,
probeConcurrency,
probeMaxTokens,
agent,
},
defaultRuntime,
);
return true;
},
};
const routes: RouteSpec[] = [
routeHealth,
routeStatus,
@@ -140,6 +242,8 @@ const routes: RouteSpec[] = [
routeMemoryStatus,
routeConfigGet,
routeConfigUnset,
routeModelsList,
routeModelsStatus,
];
export function findRoutedCommand(path: string[]): RouteSpec | null {