mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 23:02:02 +03:00
perf(cli): reduce read-only startup overhead
This commit is contained in:
+86
-7
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { spawn } from "node:child_process";
|
import { spawn, spawnSync } from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
@@ -15,6 +15,7 @@ const distEntry = path.join(distRoot, "/entry.js");
|
|||||||
const buildStampPath = path.join(distRoot, ".buildstamp");
|
const buildStampPath = path.join(distRoot, ".buildstamp");
|
||||||
const srcRoot = path.join(cwd, "src");
|
const srcRoot = path.join(cwd, "src");
|
||||||
const configFiles = [path.join(cwd, "tsconfig.json"), path.join(cwd, "package.json")];
|
const configFiles = [path.join(cwd, "tsconfig.json"), path.join(cwd, "package.json")];
|
||||||
|
const gitWatchedPaths = ["src", "tsconfig.json", "package.json"];
|
||||||
|
|
||||||
const statMtime = (filePath) => {
|
const statMtime = (filePath) => {
|
||||||
try {
|
try {
|
||||||
@@ -74,12 +75,70 @@ const findLatestMtime = (dirPath, shouldSkip) => {
|
|||||||
return latest;
|
return latest;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runGit = (args) => {
|
||||||
|
try {
|
||||||
|
const result = spawnSync("git", args, {
|
||||||
|
cwd,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (result.stdout ?? "").trim();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveGitHead = () => {
|
||||||
|
const head = runGit(["rev-parse", "HEAD"]);
|
||||||
|
return head || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasDirtySourceTree = () => {
|
||||||
|
const output = runGit([
|
||||||
|
"status",
|
||||||
|
"--porcelain",
|
||||||
|
"--untracked-files=normal",
|
||||||
|
"--",
|
||||||
|
...gitWatchedPaths,
|
||||||
|
]);
|
||||||
|
if (output === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return output.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readBuildStamp = () => {
|
||||||
|
const mtime = statMtime(buildStampPath);
|
||||||
|
if (mtime == null) {
|
||||||
|
return { mtime: null, head: null };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(buildStampPath, "utf8").trim();
|
||||||
|
if (!raw.startsWith("{")) {
|
||||||
|
return { mtime, head: null };
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const head = typeof parsed?.head === "string" && parsed.head.trim() ? parsed.head.trim() : null;
|
||||||
|
return { mtime, head };
|
||||||
|
} catch {
|
||||||
|
return { mtime, head: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSourceMtimeChanged = (stampMtime) => {
|
||||||
|
const srcMtime = findLatestMtime(srcRoot, isExcludedSource);
|
||||||
|
return srcMtime != null && srcMtime > stampMtime;
|
||||||
|
};
|
||||||
|
|
||||||
const shouldBuild = () => {
|
const shouldBuild = () => {
|
||||||
if (env.OPENCLAW_FORCE_BUILD === "1") {
|
if (env.OPENCLAW_FORCE_BUILD === "1") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const stampMtime = statMtime(buildStampPath);
|
const stamp = readBuildStamp();
|
||||||
if (stampMtime == null) {
|
if (stamp.mtime == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (statMtime(distEntry) == null) {
|
if (statMtime(distEntry) == null) {
|
||||||
@@ -88,13 +147,29 @@ const shouldBuild = () => {
|
|||||||
|
|
||||||
for (const filePath of configFiles) {
|
for (const filePath of configFiles) {
|
||||||
const mtime = statMtime(filePath);
|
const mtime = statMtime(filePath);
|
||||||
if (mtime != null && mtime > stampMtime) {
|
if (mtime != null && mtime > stamp.mtime) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const srcMtime = findLatestMtime(srcRoot, isExcludedSource);
|
const currentHead = resolveGitHead();
|
||||||
if (srcMtime != null && srcMtime > stampMtime) {
|
if (currentHead && !stamp.head) {
|
||||||
|
return hasSourceMtimeChanged(stamp.mtime);
|
||||||
|
}
|
||||||
|
if (currentHead && stamp.head && currentHead !== stamp.head) {
|
||||||
|
return hasSourceMtimeChanged(stamp.mtime);
|
||||||
|
}
|
||||||
|
if (currentHead) {
|
||||||
|
const dirty = hasDirtySourceTree();
|
||||||
|
if (dirty === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (dirty === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSourceMtimeChanged(stamp.mtime)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -125,7 +200,11 @@ const runNode = () => {
|
|||||||
const writeBuildStamp = () => {
|
const writeBuildStamp = () => {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(distRoot, { recursive: true });
|
fs.mkdirSync(distRoot, { recursive: true });
|
||||||
fs.writeFileSync(buildStampPath, `${Date.now()}\n`);
|
const stamp = {
|
||||||
|
builtAt: Date.now(),
|
||||||
|
head: resolveGitHead(),
|
||||||
|
};
|
||||||
|
fs.writeFileSync(buildStampPath, `${JSON.stringify(stamp)}\n`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Best-effort stamp; still allow the runner to start.
|
// Best-effort stamp; still allow the runner to start.
|
||||||
logRunner(`Failed to write build stamp: ${error?.message ?? "unknown error"}`);
|
logRunner(`Failed to write build stamp: ${error?.message ?? "unknown error"}`);
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ describe("argv helpers", () => {
|
|||||||
expect(shouldMigrateState(["node", "openclaw", "status"])).toBe(false);
|
expect(shouldMigrateState(["node", "openclaw", "status"])).toBe(false);
|
||||||
expect(shouldMigrateState(["node", "openclaw", "health"])).toBe(false);
|
expect(shouldMigrateState(["node", "openclaw", "health"])).toBe(false);
|
||||||
expect(shouldMigrateState(["node", "openclaw", "sessions"])).toBe(false);
|
expect(shouldMigrateState(["node", "openclaw", "sessions"])).toBe(false);
|
||||||
|
expect(shouldMigrateState(["node", "openclaw", "config", "get", "update"])).toBe(false);
|
||||||
|
expect(shouldMigrateState(["node", "openclaw", "config", "unset", "update"])).toBe(false);
|
||||||
|
expect(shouldMigrateState(["node", "openclaw", "models", "list"])).toBe(false);
|
||||||
|
expect(shouldMigrateState(["node", "openclaw", "models", "status"])).toBe(false);
|
||||||
expect(shouldMigrateState(["node", "openclaw", "memory", "status"])).toBe(false);
|
expect(shouldMigrateState(["node", "openclaw", "memory", "status"])).toBe(false);
|
||||||
expect(shouldMigrateState(["node", "openclaw", "agent", "--message", "hi"])).toBe(false);
|
expect(shouldMigrateState(["node", "openclaw", "agent", "--message", "hi"])).toBe(false);
|
||||||
expect(shouldMigrateState(["node", "openclaw", "agents", "list"])).toBe(true);
|
expect(shouldMigrateState(["node", "openclaw", "agents", "list"])).toBe(true);
|
||||||
@@ -152,6 +156,8 @@ describe("argv helpers", () => {
|
|||||||
|
|
||||||
it("reuses command path for migrate state decisions", () => {
|
it("reuses command path for migrate state decisions", () => {
|
||||||
expect(shouldMigrateStateFromPath(["status"])).toBe(false);
|
expect(shouldMigrateStateFromPath(["status"])).toBe(false);
|
||||||
|
expect(shouldMigrateStateFromPath(["config", "get"])).toBe(false);
|
||||||
|
expect(shouldMigrateStateFromPath(["models", "status"])).toBe(false);
|
||||||
expect(shouldMigrateStateFromPath(["agents", "list"])).toBe(true);
|
expect(shouldMigrateStateFromPath(["agents", "list"])).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -155,6 +155,12 @@ export function shouldMigrateStateFromPath(path: string[]): boolean {
|
|||||||
if (primary === "health" || primary === "status" || primary === "sessions") {
|
if (primary === "health" || primary === "status" || primary === "sessions") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (primary === "config" && (secondary === "get" || secondary === "unset")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (primary === "models" && (secondary === "list" || secondary === "status")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (primary === "memory" && secondary === "status") {
|
if (primary === "memory" && secondary === "status") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { readConfigFileSnapshot } from "../../config/config.js";
|
||||||
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
||||||
import { shortenHomePath } from "../../utils.js";
|
import { shortenHomePath } from "../../utils.js";
|
||||||
|
import { shouldMigrateStateFromPath } from "../argv.js";
|
||||||
import { formatCliCommand } from "../command-format.js";
|
import { formatCliCommand } from "../command-format.js";
|
||||||
|
|
||||||
const ALLOWED_INVALID_COMMANDS = new Set(["doctor", "logs", "health", "help", "status"]);
|
const ALLOWED_INVALID_COMMANDS = new Set(["doctor", "logs", "health", "help", "status"]);
|
||||||
@@ -28,7 +29,8 @@ export async function ensureConfigReady(params: {
|
|||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
commandPath?: string[];
|
commandPath?: string[];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (!didRunDoctorConfigFlow) {
|
const commandPath = params.commandPath ?? [];
|
||||||
|
if (!didRunDoctorConfigFlow && shouldMigrateStateFromPath(commandPath)) {
|
||||||
didRunDoctorConfigFlow = true;
|
didRunDoctorConfigFlow = true;
|
||||||
await loadAndMaybeMigrateDoctorConfig({
|
await loadAndMaybeMigrateDoctorConfig({
|
||||||
options: { nonInteractive: true },
|
options: { nonInteractive: true },
|
||||||
@@ -37,8 +39,8 @@ export async function ensureConfigReady(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
const commandName = params.commandPath?.[0];
|
const commandName = commandPath[0];
|
||||||
const subcommandName = params.commandPath?.[1];
|
const subcommandName = commandPath[1];
|
||||||
const allowInvalid = commandName
|
const allowInvalid = commandName
|
||||||
? ALLOWED_INVALID_COMMANDS.has(commandName) ||
|
? ALLOWED_INVALID_COMMANDS.has(commandName) ||
|
||||||
(commandName === "gateway" &&
|
(commandName === "gateway" &&
|
||||||
|
|||||||
@@ -103,6 +103,34 @@ function getCommandPositionals(argv: string[]): string[] {
|
|||||||
return out;
|
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 = {
|
const routeConfigGet: RouteSpec = {
|
||||||
match: (path) => path[0] === "config" && path[1] === "get",
|
match: (path) => path[0] === "config" && path[1] === "get",
|
||||||
run: async (argv) => {
|
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[] = [
|
const routes: RouteSpec[] = [
|
||||||
routeHealth,
|
routeHealth,
|
||||||
routeStatus,
|
routeStatus,
|
||||||
@@ -140,6 +242,8 @@ const routes: RouteSpec[] = [
|
|||||||
routeMemoryStatus,
|
routeMemoryStatus,
|
||||||
routeConfigGet,
|
routeConfigGet,
|
||||||
routeConfigUnset,
|
routeConfigUnset,
|
||||||
|
routeModelsList,
|
||||||
|
routeModelsStatus,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function findRoutedCommand(path: string[]): RouteSpec | null {
|
export function findRoutedCommand(path: string[]): RouteSpec | null {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
rewriteUpdateFlagArgv,
|
rewriteUpdateFlagArgv,
|
||||||
|
shouldEnsureCliPath,
|
||||||
shouldRegisterPrimarySubcommand,
|
shouldRegisterPrimarySubcommand,
|
||||||
shouldSkipPluginCommandRegistration,
|
shouldSkipPluginCommandRegistration,
|
||||||
} from "./run-main.js";
|
} from "./run-main.js";
|
||||||
@@ -71,6 +72,16 @@ describe("shouldSkipPluginCommandRegistration", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips plugin registration for builtin command runs", () => {
|
||||||
|
expect(
|
||||||
|
shouldSkipPluginCommandRegistration({
|
||||||
|
argv: ["node", "openclaw", "sessions", "--json"],
|
||||||
|
primary: "sessions",
|
||||||
|
hasBuiltinPrimary: true,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps plugin registration for non-builtin help", () => {
|
it("keeps plugin registration for non-builtin help", () => {
|
||||||
expect(
|
expect(
|
||||||
shouldSkipPluginCommandRegistration({
|
shouldSkipPluginCommandRegistration({
|
||||||
@@ -80,4 +91,33 @@ describe("shouldSkipPluginCommandRegistration", () => {
|
|||||||
}),
|
}),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps plugin registration for non-builtin command runs", () => {
|
||||||
|
expect(
|
||||||
|
shouldSkipPluginCommandRegistration({
|
||||||
|
argv: ["node", "openclaw", "voicecall", "status"],
|
||||||
|
primary: "voicecall",
|
||||||
|
hasBuiltinPrimary: false,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shouldEnsureCliPath", () => {
|
||||||
|
it("skips path bootstrap for help/version invocations", () => {
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "--help"])).toBe(false);
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "-V"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips path bootstrap for read-only fast paths", () => {
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "status"])).toBe(false);
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "sessions", "--json"])).toBe(false);
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "config", "get", "update"])).toBe(false);
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "models", "status", "--json"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps path bootstrap for mutating or unknown commands", () => {
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "message", "send"])).toBe(true);
|
||||||
|
expect(shouldEnsureCliPath(["node", "openclaw", "voicecall", "status"])).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+29
-7
@@ -10,7 +10,7 @@ import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
|||||||
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||||
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||||
import { enableConsoleCapture } from "../logging.js";
|
import { enableConsoleCapture } from "../logging.js";
|
||||||
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
import { getCommandPath, getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||||
import { tryRouteCli } from "./route.js";
|
import { tryRouteCli } from "./route.js";
|
||||||
|
|
||||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||||
@@ -33,20 +33,42 @@ export function shouldSkipPluginCommandRegistration(params: {
|
|||||||
primary: string | null;
|
primary: string | null;
|
||||||
hasBuiltinPrimary: boolean;
|
hasBuiltinPrimary: boolean;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
if (!hasHelpOrVersion(params.argv)) {
|
if (params.hasBuiltinPrimary) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!params.primary) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return params.hasBuiltinPrimary;
|
if (!params.primary) {
|
||||||
|
return hasHelpOrVersion(params.argv);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldEnsureCliPath(argv: string[]): boolean {
|
||||||
|
if (hasHelpOrVersion(argv)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [primary, secondary] = getCommandPath(argv, 2);
|
||||||
|
if (!primary) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (primary === "status" || primary === "health" || primary === "sessions") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (primary === "config" && (secondary === "get" || secondary === "unset")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (primary === "models" && (secondary === "list" || secondary === "status")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runCli(argv: string[] = process.argv) {
|
export async function runCli(argv: string[] = process.argv) {
|
||||||
const normalizedArgv = stripWindowsNodeExec(argv);
|
const normalizedArgv = stripWindowsNodeExec(argv);
|
||||||
loadDotEnv({ quiet: true });
|
loadDotEnv({ quiet: true });
|
||||||
normalizeEnv();
|
normalizeEnv();
|
||||||
ensureOpenClawCliOnPath();
|
if (shouldEnsureCliPath(normalizedArgv)) {
|
||||||
|
ensureOpenClawCliOnPath();
|
||||||
|
}
|
||||||
|
|
||||||
// Enforce the minimum supported runtime before doing any work.
|
// Enforce the minimum supported runtime before doing any work.
|
||||||
assertSupportedRuntime();
|
assertSupportedRuntime();
|
||||||
|
|||||||
Reference in New Issue
Block a user