perf(cli): reduce read-only startup overhead
This commit is contained in:
50
src/cli/program/config-guard.test.ts
Normal file
50
src/cli/program/config-guard.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user