mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
perf(cli): speed up precomputed command help startup
* perf: speed up precomputed command help * perf: precompute sessions and tasks help * Speed up precomputed command help startup * Speed up precomputed command help startup --------- Co-authored-by: Zeheng Huang <153708448+hunjaiboy@users.noreply.github.com> Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
This commit is contained in:
parent
7626ca38b3
commit
3895c9341b
13 changed files with 504 additions and 27 deletions
125
openclaw.mjs
125
openclaw.mjs
|
|
@ -371,22 +371,120 @@ const buildMissingEntryErrorMessage = async () => {
|
|||
const isBareRootHelpInvocation = (argv) =>
|
||||
argv.length === 3 && (argv[2] === "--help" || argv[2] === "-h");
|
||||
|
||||
const resolvePrecomputedCommandHelp = (argv) => {
|
||||
if (argv.length !== 4 || (argv[3] !== "--help" && argv[3] !== "-h")) {
|
||||
return null;
|
||||
const LAUNCHER_HELP_FLAGS = new Set(["-h", "--help"]);
|
||||
const LAUNCHER_ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]);
|
||||
const LAUNCHER_ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level", "--container"]);
|
||||
const LAUNCHER_PRECOMPUTED_COMMAND_HELP = {
|
||||
browser: { command: "browser", metadataKey: "browserHelpText" },
|
||||
secrets: { command: "secrets", metadataKey: "secretsHelpText" },
|
||||
nodes: { command: "nodes", metadataKey: "nodesHelpText" },
|
||||
};
|
||||
const LAUNCHER_PRECOMPUTED_SUBCOMMAND_HELP = new Set([
|
||||
"doctor",
|
||||
"gateway",
|
||||
"models",
|
||||
"plugins",
|
||||
"sessions",
|
||||
"tasks",
|
||||
]);
|
||||
|
||||
const isLauncherRootOptionValueToken = (arg) => {
|
||||
if (!arg || arg === "--") {
|
||||
return false;
|
||||
}
|
||||
if (argv[2] === "browser") {
|
||||
return { command: "browser", metadataKey: "browserHelpText" };
|
||||
if (!arg.startsWith("-")) {
|
||||
return true;
|
||||
}
|
||||
if (argv[2] === "secrets") {
|
||||
return { command: "secrets", metadataKey: "secretsHelpText" };
|
||||
return /^-\d+(?:\.\d+)?$/.test(arg);
|
||||
};
|
||||
|
||||
const consumeLauncherRootOptionToken = (args, index) => {
|
||||
const arg = args[index];
|
||||
if (!arg) {
|
||||
return 0;
|
||||
}
|
||||
if (argv[2] === "nodes") {
|
||||
return { command: "nodes", metadataKey: "nodesHelpText" };
|
||||
if (LAUNCHER_ROOT_BOOLEAN_FLAGS.has(arg)) {
|
||||
return 1;
|
||||
}
|
||||
if (
|
||||
arg.startsWith("--profile=") ||
|
||||
arg.startsWith("--log-level=") ||
|
||||
arg.startsWith("--container=")
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
if (LAUNCHER_ROOT_VALUE_FLAGS.has(arg)) {
|
||||
return isLauncherRootOptionValueToken(args[index + 1]) ? 2 : 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const hasLauncherContainerTarget = (argv) => {
|
||||
if (normalizeLauncherMetadataValue(process.env.OPENCLAW_CONTAINER)) {
|
||||
return true;
|
||||
}
|
||||
const args = argv.slice(2);
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg || arg === "--") {
|
||||
return false;
|
||||
}
|
||||
if (arg === "--container" || arg.startsWith("--container=")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const resolvePrecomputedCommandHelpByName = (commandName) => {
|
||||
if (Object.hasOwn(LAUNCHER_PRECOMPUTED_COMMAND_HELP, commandName)) {
|
||||
return LAUNCHER_PRECOMPUTED_COMMAND_HELP[commandName];
|
||||
}
|
||||
if (LAUNCHER_PRECOMPUTED_SUBCOMMAND_HELP.has(commandName)) {
|
||||
return {
|
||||
command: commandName,
|
||||
metadataKey: "subcommandHelpText",
|
||||
subcommandKey: commandName,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const resolvePrecomputedCommandHelp = (argv) => {
|
||||
const args = argv.slice(2);
|
||||
let commandHelp = null;
|
||||
let sawHelp = false;
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg || arg === "--") {
|
||||
return null;
|
||||
}
|
||||
if (!commandHelp) {
|
||||
const consumed = consumeLauncherRootOptionToken(args, index);
|
||||
if (consumed > 0) {
|
||||
index += consumed - 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
return null;
|
||||
}
|
||||
commandHelp = resolvePrecomputedCommandHelpByName(arg);
|
||||
if (!commandHelp) {
|
||||
return null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (LAUNCHER_HELP_FLAGS.has(arg)) {
|
||||
sawHelp = true;
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return commandHelp && sawHelp ? commandHelp : null;
|
||||
};
|
||||
|
||||
const isHelpFastPathDisabled = () =>
|
||||
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";
|
||||
|
||||
|
|
@ -456,11 +554,11 @@ const shouldDeferRootHelpToRuntimeEntry = () => {
|
|||
return false;
|
||||
};
|
||||
|
||||
const loadPrecomputedHelpText = (key) => {
|
||||
const loadPrecomputedHelpText = (key, subkey) => {
|
||||
try {
|
||||
const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
const value = parsed?.[key];
|
||||
const value = subkey ? parsed?.[key]?.[subkey] : parsed?.[key];
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -650,10 +748,13 @@ const tryOutputPrecomputedCommandHelp = () => {
|
|||
if (!commandHelp) {
|
||||
return false;
|
||||
}
|
||||
if (hasLauncherContainerTarget(process.argv)) {
|
||||
return false;
|
||||
}
|
||||
if (commandHelp.command === "nodes" && shouldDeferRootHelpToRuntimeEntry()) {
|
||||
return false;
|
||||
}
|
||||
const precomputed = loadPrecomputedHelpText(commandHelp.metadataKey);
|
||||
const precomputed = loadPrecomputedHelpText(commandHelp.metadataKey, commandHelp.subcommandKey);
|
||||
if (!precomputed) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -663,6 +663,10 @@ function parseMaxRssMb(stderr: string): number | null {
|
|||
return Number(lastMatch[1]) / 1024;
|
||||
}
|
||||
|
||||
function nodeImportSpecifierForPath(filePath: string): string {
|
||||
return pathToFileURL(filePath).href;
|
||||
}
|
||||
|
||||
function buildCpuOrHeapFlags(options: { cpuProfDir?: string; heapProfDir?: string }): string[] {
|
||||
const flags: string[] = [];
|
||||
if (options.cpuProfDir) {
|
||||
|
|
@ -697,7 +701,7 @@ async function runSample(params: {
|
|||
}
|
||||
const nodeArgs = [
|
||||
"--import",
|
||||
params.rssHookPath,
|
||||
nodeImportSpecifierForPath(params.rssHookPath),
|
||||
...buildCpuOrHeapFlags({
|
||||
cpuProfDir: params.cpuProfDir,
|
||||
heapProfDir: params.heapProfDir,
|
||||
|
|
@ -1268,6 +1272,7 @@ async function main(): Promise<void> {
|
|||
export const testing = {
|
||||
buildConfigFixture,
|
||||
collectFailedSamples,
|
||||
nodeImportSpecifierForPath,
|
||||
parseGatewayPortEnv,
|
||||
parseNonNegativeInt,
|
||||
parsePositiveInt,
|
||||
|
|
|
|||
|
|
@ -173,6 +173,10 @@ function formatCaseCommand(testCase) {
|
|||
return `node ${testCase.args.join(" ")}`;
|
||||
}
|
||||
|
||||
function nodeImportSpecifierForPath(filePath) {
|
||||
return pathToFileURL(filePath).href;
|
||||
}
|
||||
|
||||
function buildBenchEnv() {
|
||||
if (!tmpHome) {
|
||||
throw new Error("temporary home is not initialized");
|
||||
|
|
@ -218,14 +222,18 @@ function runCase(testCase, params = {}) {
|
|||
const env = buildBenchEnv();
|
||||
const spawn = params.spawnSync ?? defaultSpawnSync;
|
||||
const timeoutMs = params.timeoutMs ?? COMMAND_TIMEOUT_MS;
|
||||
const result = spawn(process.execPath, ["--import", rssHookPath, ...testCase.args], {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
timeout: timeoutMs,
|
||||
killSignal: "SIGKILL",
|
||||
});
|
||||
const result = spawn(
|
||||
process.execPath,
|
||||
["--import", nodeImportSpecifierForPath(rssHookPath), ...testCase.args],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
timeout: timeoutMs,
|
||||
killSignal: "SIGKILL",
|
||||
},
|
||||
);
|
||||
const stderr = result.stderr ?? "";
|
||||
const maxRssMb = parseMaxRssMb(stderr);
|
||||
const matrixBootstrapWarning = /matrix: crypto runtime bootstrap failed/i.test(stderr);
|
||||
|
|
@ -373,6 +381,7 @@ function runStartupMemoryCheck(argv = process.argv.slice(2), params = {}) {
|
|||
*/
|
||||
export const testing = {
|
||||
cases,
|
||||
nodeImportSpecifierForPath,
|
||||
parseArgs,
|
||||
readPositiveIntEnv,
|
||||
readPositiveNumberEnv,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,14 @@ const COMMAND_HELP_RENDER_TIMEOUT_MS = 120_000;
|
|||
const COMMAND_HELP_RENDER_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
||||
const COMMAND_HELP_RENDER_KILL_GRACE_MS = 5_000;
|
||||
const COMMAND_HELP_RENDER_CONCURRENCY = 2;
|
||||
const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = ["doctor", "gateway", "models", "plugins"] as const;
|
||||
const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = [
|
||||
"doctor",
|
||||
"gateway",
|
||||
"models",
|
||||
"plugins",
|
||||
"sessions",
|
||||
"tasks",
|
||||
] as const;
|
||||
const CORE_CHANNEL_ORDER = [
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
|
|
@ -249,6 +256,7 @@ function resolveSubcommandHelpSourceSignature(sourceRootDir: string = rootDir):
|
|||
path.join(sourceRootDir, "src/cli/help-format.ts"),
|
||||
path.join(sourceRootDir, "src/cli/daemon-cli/register-service-commands.ts"),
|
||||
path.join(sourceRootDir, "src/cli/program/register.maintenance.ts"),
|
||||
path.join(sourceRootDir, "src/cli/program/register.status-health-sessions.ts"),
|
||||
path.join(sourceRootDir, "src/cli/gateway-cli.ts"),
|
||||
path.join(sourceRootDir, "src/cli/gateway-cli/register.ts"),
|
||||
path.join(sourceRootDir, "src/cli/gateway-cli/run-command.ts"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
// Cached startup metadata readers for precomputed root and subcommand help text.
|
||||
import { readCliStartupMetadata } from "./startup-metadata.js";
|
||||
|
||||
export type PrecomputedSubcommandHelpName = "doctor" | "gateway" | "models" | "plugins";
|
||||
export type PrecomputedSubcommandHelpName =
|
||||
| "doctor"
|
||||
| "gateway"
|
||||
| "models"
|
||||
| "plugins"
|
||||
| "sessions"
|
||||
| "tasks";
|
||||
|
||||
let precomputedRootHelpText: string | null | undefined;
|
||||
let precomputedBrowserHelpText: string | null | undefined;
|
||||
|
|
@ -139,7 +145,9 @@ function isPrecomputedSubcommandHelpName(
|
|||
commandName === "doctor" ||
|
||||
commandName === "gateway" ||
|
||||
commandName === "models" ||
|
||||
commandName === "plugins"
|
||||
commandName === "plugins" ||
|
||||
commandName === "sessions" ||
|
||||
commandName === "tasks"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,14 @@ import { getSubCliParentDefaultHelpCommands } from "./program/subcli-descriptors
|
|||
|
||||
const ROOT_HELP_ALIASES = new Set(["tools"]);
|
||||
const SETUP_ONBOARD_CONFIGURE_HELP_COMMANDS = new Set(["setup", "onboard", "configure"]);
|
||||
const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = new Set(["doctor", "gateway", "models", "plugins"]);
|
||||
const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = new Set([
|
||||
"doctor",
|
||||
"gateway",
|
||||
"models",
|
||||
"plugins",
|
||||
"sessions",
|
||||
"tasks",
|
||||
]);
|
||||
const HELP_FLAGS = new Set(["-h", "--help"]);
|
||||
const VERSION_FLAGS = new Set(["-V", "--version"]);
|
||||
const BARE_PARENT_DEFAULT_HELP_COMMANDS = new Set([
|
||||
|
|
|
|||
|
|
@ -319,6 +319,12 @@ describe("resolvePrecomputedSubcommandHelpFastPath", () => {
|
|||
expect(
|
||||
resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "plugins", "--help"]),
|
||||
).toBe("plugins");
|
||||
expect(
|
||||
resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "sessions", "--help"]),
|
||||
).toBe("sessions");
|
||||
expect(resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "tasks", "-h"])).toBe(
|
||||
"tasks",
|
||||
);
|
||||
expect(
|
||||
resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "doctor", "--version"]),
|
||||
).toBeNull();
|
||||
|
|
|
|||
|
|
@ -155,6 +155,68 @@ describe("entry precomputed command help fast path", () => {
|
|||
expect(outputPrecomputedNodesHelpTextCalls).toBe(1);
|
||||
});
|
||||
|
||||
it.each(["doctor", "sessions", "tasks"])(
|
||||
"renders precomputed %s help from startup metadata without importing the full program",
|
||||
async (commandName) => {
|
||||
const outputPrecomputedSubcommandHelpTextCalls: string[] = [];
|
||||
|
||||
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
||||
["node", "openclaw", commandName, "--help"],
|
||||
{
|
||||
env: {},
|
||||
outputPrecomputedSubcommandHelpText: (requestedCommandName) => {
|
||||
outputPrecomputedSubcommandHelpTextCalls.push(requestedCommandName);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputPrecomputedSubcommandHelpTextCalls).toEqual([commandName]);
|
||||
},
|
||||
);
|
||||
|
||||
it("renders precomputed subcommand help with leading root options", async () => {
|
||||
const outputPrecomputedSubcommandHelpTextCalls: string[] = [];
|
||||
|
||||
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
||||
["node", "openclaw", "--profile", "work", "--no-color", "models", "-h"],
|
||||
{
|
||||
env: {},
|
||||
outputPrecomputedSubcommandHelpText: (commandName) => {
|
||||
outputPrecomputedSubcommandHelpTextCalls.push(commandName);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(outputPrecomputedSubcommandHelpTextCalls).toEqual(["models"]);
|
||||
});
|
||||
|
||||
it("keeps subcommand help fast path strict for extra or mixed flags", async () => {
|
||||
const invocations = [
|
||||
["node", "openclaw", "doctor", "--help", "--bogus"],
|
||||
["node", "openclaw", "doctor", "--help", "extra"],
|
||||
["node", "openclaw", "doctor", "--version", "-h"],
|
||||
["node", "openclaw", "--bogus", "doctor", "--help"],
|
||||
];
|
||||
let outputPrecomputedSubcommandHelpTextCalls = 0;
|
||||
|
||||
for (const argv of invocations) {
|
||||
const handled = await tryHandlePrecomputedCommandHelpFastPath(argv, {
|
||||
env: {},
|
||||
outputPrecomputedSubcommandHelpText: () => {
|
||||
outputPrecomputedSubcommandHelpTextCalls += 1;
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
expect(handled).toBe(false);
|
||||
}
|
||||
expect(outputPrecomputedSubcommandHelpTextCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("defers nodes help when plugin config can change command metadata", async () => {
|
||||
let outputPrecomputedNodesHelpTextCalls = 0;
|
||||
let liveConfigChecks = 0;
|
||||
|
|
@ -288,4 +350,22 @@ describe("entry precomputed command help fast path", () => {
|
|||
expect(handled).toBe(false);
|
||||
expect(outputPrecomputedSecretsHelpTextCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("skips the host command help fast path when a container target comes from env", async () => {
|
||||
let outputPrecomputedBrowserHelpTextCalls = 0;
|
||||
|
||||
const handled = await tryHandlePrecomputedCommandHelpFastPath(
|
||||
["node", "openclaw", "browser", "--help"],
|
||||
{
|
||||
env: { OPENCLAW_CONTAINER: "demo" },
|
||||
outputPrecomputedBrowserHelpText: () => {
|
||||
outputPrecomputedBrowserHelpTextCalls += 1;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(handled).toBe(false);
|
||||
expect(outputPrecomputedBrowserHelpTextCalls).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
77
src/entry.ts
77
src/entry.ts
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "./entry.compile-cache.js";
|
||||
import { buildCliRespawnPlan, runCliRespawnPlan } from "./entry.respawn.js";
|
||||
import { tryHandleRootVersionFastPath } from "./entry.version-fast-path.js";
|
||||
import { consumeRootOptionToken } from "./infra/cli-root-options.js";
|
||||
import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
|
||||
import { isMainModule } from "./infra/is-main.js";
|
||||
import { ensureOpenClawExecMarkerOnProcess } from "./infra/openclaw-exec-env.js";
|
||||
|
|
@ -26,8 +27,18 @@ const ENTRY_WRAPPER_PAIRS = [
|
|||
] as const;
|
||||
|
||||
type PrecomputedCommandHelpName = "browser" | "secrets" | "nodes";
|
||||
type PrecomputedSubcommandHelpName =
|
||||
| "doctor"
|
||||
| "gateway"
|
||||
| "models"
|
||||
| "plugins"
|
||||
| "sessions"
|
||||
| "tasks";
|
||||
type OutputPrecomputedHelpText = () => boolean;
|
||||
|
||||
const HELP_FLAGS = new Set(["-h", "--help"]);
|
||||
const VERSION_FLAGS = new Set(["-V", "--version"]);
|
||||
|
||||
const loadRootHelpLiveConfigModule = async () => await import("./cli/root-help-live-config.js");
|
||||
const loadRootHelpMetadataModule = async () => await import("./cli/root-help-metadata.js");
|
||||
|
||||
|
|
@ -227,12 +238,69 @@ function resolvePrecomputedCommandHelpName(argv: string[]): PrecomputedCommandHe
|
|||
return null;
|
||||
}
|
||||
|
||||
function resolvePrecomputedSubcommandHelpName(
|
||||
argv: string[],
|
||||
): PrecomputedSubcommandHelpName | null {
|
||||
const args = argv.slice(2);
|
||||
let commandName: PrecomputedSubcommandHelpName | null = null;
|
||||
let sawHelp = false;
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg || arg === "--") {
|
||||
return null;
|
||||
}
|
||||
if (VERSION_FLAGS.has(arg)) {
|
||||
return null;
|
||||
}
|
||||
if (!commandName) {
|
||||
const consumed = consumeRootOptionToken(args, index);
|
||||
if (consumed > 0) {
|
||||
index += consumed - 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
return null;
|
||||
}
|
||||
commandName = resolvePrecomputedSubcommandName(arg);
|
||||
if (!commandName) {
|
||||
return null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (HELP_FLAGS.has(arg)) {
|
||||
sawHelp = true;
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return commandName && sawHelp ? commandName : null;
|
||||
}
|
||||
|
||||
function resolvePrecomputedSubcommandName(
|
||||
commandName: string,
|
||||
): PrecomputedSubcommandHelpName | null {
|
||||
if (
|
||||
commandName === "doctor" ||
|
||||
commandName === "gateway" ||
|
||||
commandName === "models" ||
|
||||
commandName === "plugins" ||
|
||||
commandName === "sessions" ||
|
||||
commandName === "tasks"
|
||||
) {
|
||||
return commandName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function tryHandlePrecomputedCommandHelpFastPath(
|
||||
argv: string[],
|
||||
deps: {
|
||||
outputPrecomputedBrowserHelpText?: OutputPrecomputedHelpText;
|
||||
outputPrecomputedSecretsHelpText?: OutputPrecomputedHelpText;
|
||||
outputPrecomputedNodesHelpText?: OutputPrecomputedHelpText;
|
||||
outputPrecomputedSubcommandHelpText?: (commandName: string) => boolean;
|
||||
loadRootHelpRenderOptionsForConfigSensitivePlugins?: (
|
||||
env?: NodeJS.ProcessEnv,
|
||||
) => Promise<RootHelpRenderOptions | null>;
|
||||
|
|
@ -247,11 +315,18 @@ export async function tryHandlePrecomputedCommandHelpFastPath(
|
|||
return false;
|
||||
}
|
||||
const commandName = resolvePrecomputedCommandHelpName(argv);
|
||||
if (!commandName) {
|
||||
const subcommandName = commandName ? null : resolvePrecomputedSubcommandHelpName(argv);
|
||||
if (!commandName && !subcommandName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (subcommandName) {
|
||||
const outputPrecomputedSubcommandHelpText =
|
||||
deps.outputPrecomputedSubcommandHelpText ??
|
||||
(await loadRootHelpMetadataModule()).outputPrecomputedSubcommandHelpText;
|
||||
return outputPrecomputedSubcommandHelpText(subcommandName);
|
||||
}
|
||||
if (commandName === "nodes") {
|
||||
const loadRootHelpRenderOptionsForConfigSensitivePlugins =
|
||||
deps.loadRootHelpRenderOptionsForConfigSensitivePlugins ??
|
||||
|
|
|
|||
|
|
@ -480,6 +480,125 @@ describe("openclaw launcher", () => {
|
|||
expect(result.stdout).toBe(`PRECOMPUTED ${params.command} help\n`);
|
||||
});
|
||||
|
||||
it.each(["doctor", "gateway", "models", "plugins", "sessions", "tasks"])(
|
||||
"uses precomputed %s help before loading the runtime entry",
|
||||
async (command) => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ subcommandHelpText: { [command]: `PRECOMPUTED ${command} help\n` } }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(fixtureRoot, "openclaw.mjs"), command, "--help"],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe(`PRECOMPUTED ${command} help\n`);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses precomputed subcommand help with leading root options", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ subcommandHelpText: { models: "PRECOMPUTED models help\n" } }),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(fixtureRoot, "openclaw.mjs"), "--profile", "work", "--no-color", "models", "-h"],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("PRECOMPUTED models help\n");
|
||||
});
|
||||
|
||||
it("defers precomputed subcommand help to the runtime entry when container env is set", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ subcommandHelpText: { models: "PRECOMPUTED models help\n" } }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
"process.stdout.write('RUNTIME ENTRY\\n');\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(fixtureRoot, "openclaw.mjs"), "models", "--help"],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv({ OPENCLAW_CONTAINER: "demo" }),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("RUNTIME ENTRY\n");
|
||||
expect(result.stdout).not.toContain("PRECOMPUTED");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "container env",
|
||||
args: ["browser", "--help"],
|
||||
env: { OPENCLAW_CONTAINER: "demo" },
|
||||
},
|
||||
{
|
||||
name: "root --container flag",
|
||||
args: ["--container", "demo", "browser", "--help"],
|
||||
env: {},
|
||||
},
|
||||
{
|
||||
name: "root --container=value flag",
|
||||
args: ["--container=demo", "browser", "--help"],
|
||||
env: {},
|
||||
},
|
||||
])("defers precomputed command help to the runtime entry with $name", async (params) => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "cli-startup-metadata.json"),
|
||||
JSON.stringify({ browserHelpText: "PRECOMPUTED browser help\n" }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
"process.stdout.write('RUNTIME ENTRY\\n');\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(fixtureRoot, "openclaw.mjs"), ...params.args],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(params.env),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("RUNTIME ENTRY\n");
|
||||
expect(result.stdout).not.toContain("PRECOMPUTED");
|
||||
});
|
||||
|
||||
it("defers root help to the runtime entry when plugin config can change help", async () => {
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
const configPath = path.join(fixtureRoot, "openclaw.json");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Bench Cli Startup tests cover bench cli startup script behavior.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { testing } from "../../scripts/bench-cli-startup.ts";
|
||||
import { withEnv } from "../../src/test-utils/env.js";
|
||||
|
|
@ -187,6 +188,12 @@ describe("bench-cli-startup", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("passes generated import hook paths as file URL specifiers", () => {
|
||||
const hookPath = resolve("measure-rss.mjs");
|
||||
|
||||
expect(testing.nodeImportSpecifierForPath(hookPath)).toBe(pathToFileURL(hookPath).href);
|
||||
});
|
||||
|
||||
it("fails reports with no measured samples", () => {
|
||||
expect(
|
||||
testing.collectFailedSamples({
|
||||
|
|
|
|||
|
|
@ -218,4 +218,42 @@ describe("check-cli-startup-memory", () => {
|
|||
),
|
||||
).toThrow("--help did not report max RSS");
|
||||
});
|
||||
|
||||
it("passes the generated RSS hook as a Node import URL", () => {
|
||||
if (process.platform !== "darwin" && process.platform !== "linux") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tempRoot = makeTempRoot();
|
||||
const seenArgs: string[][] = [];
|
||||
|
||||
const result = testing.runStartupMemoryCheck(
|
||||
[
|
||||
"--json",
|
||||
path.join(tempRoot, "startup-memory.json"),
|
||||
"--summary",
|
||||
path.join(tempRoot, "summary.md"),
|
||||
],
|
||||
{
|
||||
platform: "linux",
|
||||
spawnSync: (_command: string, args: string[]) => {
|
||||
seenArgs.push(args);
|
||||
return {
|
||||
error: null,
|
||||
signal: null,
|
||||
status: 0,
|
||||
stderr: "__OPENCLAW_MAX_RSS_KB__=1024\n",
|
||||
stdout: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.skipped).toBe(false);
|
||||
expect(seenArgs).toHaveLength(testing.cases.length);
|
||||
for (const args of seenArgs) {
|
||||
expect(args[0]).toBe("--import");
|
||||
expect(args[1]).toMatch(/^file:/u);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ function writeStartupMetadataSourceSignatureFixture(rootDir: string): void {
|
|||
["src/cli/models-cli.ts", "export const modelsHelp = 'models';\n"],
|
||||
["src/cli/nodes-cli/register.ts", "export const nodesHelp = 'nodes';\n"],
|
||||
["src/cli/program/register.maintenance.ts", "export const maintenanceHelp = 'maintenance';\n"],
|
||||
[
|
||||
"src/cli/program/register.status-health-sessions.ts",
|
||||
"export const statusHealthSessionsHelp = 'sessions';\n",
|
||||
],
|
||||
["src/cli/program/context.ts", "export const context = 'context';\n"],
|
||||
["src/cli/program/help.ts", "export const help = 'help';\n"],
|
||||
["src/cli/plugins-cli.ts", "export const pluginsHelp = 'plugins';\n"],
|
||||
|
|
@ -364,6 +368,8 @@ describe("write-cli-startup-metadata", () => {
|
|||
gateway: "Usage: openclaw gateway\n",
|
||||
models: "Usage: openclaw models\n",
|
||||
plugins: "Usage: openclaw plugins\n",
|
||||
sessions: "Usage: openclaw sessions\n",
|
||||
tasks: "Usage: openclaw tasks\n",
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -379,6 +385,8 @@ describe("write-cli-startup-metadata", () => {
|
|||
gateway: string;
|
||||
models: string;
|
||||
plugins: string;
|
||||
sessions: string;
|
||||
tasks: string;
|
||||
};
|
||||
};
|
||||
expect(written.channelOptions).toContain("matrix");
|
||||
|
|
@ -395,6 +403,8 @@ describe("write-cli-startup-metadata", () => {
|
|||
expect(written.subcommandHelpText.gateway).toContain("openclaw gateway");
|
||||
expect(written.subcommandHelpText.models).toContain("openclaw models");
|
||||
expect(written.subcommandHelpText.plugins).toContain("openclaw plugins");
|
||||
expect(written.subcommandHelpText.sessions).toContain("openclaw sessions");
|
||||
expect(written.subcommandHelpText.tasks).toContain("openclaw tasks");
|
||||
});
|
||||
|
||||
it("renders independent startup help snapshots concurrently", async () => {
|
||||
|
|
@ -452,6 +462,8 @@ describe("write-cli-startup-metadata", () => {
|
|||
gateway: "Usage: openclaw gateway\n",
|
||||
models: "Usage: openclaw models\n",
|
||||
plugins: "Usage: openclaw plugins\n",
|
||||
sessions: "Usage: openclaw sessions\n",
|
||||
tasks: "Usage: openclaw tasks\n",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -500,6 +512,8 @@ describe("write-cli-startup-metadata", () => {
|
|||
gateway: "Usage: openclaw gateway\n",
|
||||
models: "Usage: openclaw models\n",
|
||||
plugins: "Usage: openclaw plugins\n",
|
||||
sessions: "Usage: openclaw sessions\n",
|
||||
tasks: "Usage: openclaw tasks\n",
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue