mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
8 Commits
v2026.3.12
...
vincentkoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0054bac90b | ||
|
|
8a88b22e4d | ||
|
|
e9ee68f9c5 | ||
|
|
af19c5dd60 | ||
|
|
31c639f351 | ||
|
|
a8f0557c9d | ||
|
|
c5e6209310 | ||
|
|
83d609926e |
@@ -8,6 +8,9 @@ import {
|
||||
getVerboseFlag,
|
||||
hasHelpOrVersion,
|
||||
hasFlag,
|
||||
isRootHelpRequest,
|
||||
isRootNoCommandRequest,
|
||||
isRootVersionRequest,
|
||||
shouldMigrateState,
|
||||
shouldMigrateStateFromPath,
|
||||
} from "./argv.js";
|
||||
@@ -284,4 +287,127 @@ describe("argv helpers", () => {
|
||||
])("reuses command path for migrate state decisions: $path", ({ path, expected }) => {
|
||||
expect(shouldMigrateStateFromPath(path)).toBe(expected);
|
||||
});
|
||||
|
||||
// isRootVersionRequest: guards the entry.ts fast-path exit.
|
||||
// Each case documents a semantic boundary that the fast path must respect.
|
||||
it.each([
|
||||
{
|
||||
name: "--version flag",
|
||||
argv: ["node", "openclaw", "--version"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "-V flag",
|
||||
argv: ["node", "openclaw", "-V"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "-v root alias (no subcommand)",
|
||||
argv: ["node", "openclaw", "-v"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "-v root alias with a root-level flag before it",
|
||||
argv: ["node", "openclaw", "--dev", "-v"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "--version after -- terminator must NOT trigger (forwarded arg)",
|
||||
argv: ["node", "openclaw", "nodes", "run", "--", "git", "--version"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "-V after -- terminator must NOT trigger",
|
||||
argv: ["node", "openclaw", "--", "-V"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "-v with subcommand must NOT trigger (subcommand-scoped flag)",
|
||||
argv: ["node", "openclaw", "acp", "-v"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "normal command without version flag",
|
||||
argv: ["node", "openclaw", "status"],
|
||||
expected: false,
|
||||
},
|
||||
])("isRootVersionRequest: $name", ({ argv, expected }) => {
|
||||
expect(isRootVersionRequest(argv)).toBe(expected);
|
||||
});
|
||||
|
||||
// isRootHelpRequest: guards the entry.ts fast-path help exit.
|
||||
it.each([
|
||||
{
|
||||
name: "--help flag at root",
|
||||
argv: ["node", "openclaw", "--help"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "-h flag at root",
|
||||
argv: ["node", "openclaw", "-h"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "--help after a root boolean flag",
|
||||
argv: ["node", "openclaw", "--dev", "--help"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "--help after a root value flag",
|
||||
argv: ["node", "openclaw", "--profile", "dev", "--help"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "--help after -- terminator must NOT trigger (forwarded arg)",
|
||||
argv: ["node", "openclaw", "nodes", "run", "--", "git", "--help"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "-h after -- terminator must NOT trigger",
|
||||
argv: ["node", "openclaw", "--", "-h"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "--help with subcommand must NOT trigger (subcommand-scoped)",
|
||||
argv: ["node", "openclaw", "status", "--help"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "-h with subcommand must NOT trigger",
|
||||
argv: ["node", "openclaw", "models", "-h"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "normal command without help flag",
|
||||
argv: ["node", "openclaw", "status"],
|
||||
expected: false,
|
||||
},
|
||||
])("isRootHelpRequest: $name", ({ argv, expected }) => {
|
||||
expect(isRootHelpRequest(argv)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "no args",
|
||||
argv: ["node", "openclaw"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "only root flags",
|
||||
argv: ["node", "openclaw", "--dev", "--profile", "work"],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "subcommand present",
|
||||
argv: ["node", "openclaw", "status"],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "non-flag token after terminator is ignored",
|
||||
argv: ["node", "openclaw", "--", "status"],
|
||||
expected: true,
|
||||
},
|
||||
])("isRootNoCommandRequest: $name", ({ argv, expected }) => {
|
||||
expect(isRootNoCommandRequest(argv)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,12 +7,108 @@ const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]);
|
||||
const ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level"]);
|
||||
const FLAG_TERMINATOR = "--";
|
||||
|
||||
/**
|
||||
* Returns true if argv contains any help or version flag.
|
||||
* NOTE: uses `argv.some()` without `--` terminator awareness, so forwarded args
|
||||
* like `nodes run -- git --help` will produce a false positive. Use
|
||||
* `isRootHelpRequest`/`isRootVersionRequest` for fast-path guards where
|
||||
* correctness around forwarded args matters.
|
||||
*/
|
||||
export function hasHelpOrVersion(argv: string[]): boolean {
|
||||
return (
|
||||
argv.some((arg) => HELP_FLAGS.has(arg) || VERSION_FLAGS.has(arg)) || hasRootVersionAlias(argv)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true only when the process is a root-level version request:
|
||||
* - `--version` or `-V` appear before any `--` terminator (so forwarded args like
|
||||
* `nodes run -- git --version` are excluded), and
|
||||
* - `-v` is only matched at root scope (no subcommand before it).
|
||||
* Used by the entry.ts fast path to exit before loading heavy CLI modules.
|
||||
*/
|
||||
export function isRootVersionRequest(argv: string[]): boolean {
|
||||
return hasFlag(argv, "--version") || hasFlag(argv, "-V") || hasRootVersionAlias(argv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true only when the process is a root-level help request:
|
||||
* - `-h` or `--help` appears before any `--` terminator, and
|
||||
* - no subcommand (non-flag token) precedes the help flag.
|
||||
* `openclaw status --help` returns false (subcommand-scoped); `openclaw --help` returns true.
|
||||
* Used by the entry.ts fast path to display help without loading route.ts or run-main.js.
|
||||
*/
|
||||
export function isRootHelpRequest(argv: string[]): boolean {
|
||||
const args = argv.slice(2);
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
if (arg === FLAG_TERMINATOR) {
|
||||
break;
|
||||
}
|
||||
if (arg === "-h" || arg === "--help") {
|
||||
return true;
|
||||
}
|
||||
if (ROOT_BOOLEAN_FLAGS.has(arg)) {
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--profile=")) {
|
||||
continue;
|
||||
}
|
||||
if (ROOT_VALUE_FLAGS.has(arg)) {
|
||||
const next = args[i + 1];
|
||||
if (isValueToken(next)) {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
// Non-flag token is a subcommand — this is a subcommand-scoped help request.
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when argv contains no primary subcommand token.
|
||||
* Root flags are ignored; scanning stops at `--`.
|
||||
* Used by entry.ts to render root help without loading the full CLI path.
|
||||
*/
|
||||
export function isRootNoCommandRequest(argv: string[]): boolean {
|
||||
const args = argv.slice(2);
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
if (arg === FLAG_TERMINATOR) {
|
||||
break;
|
||||
}
|
||||
if (ROOT_BOOLEAN_FLAGS.has(arg)) {
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) {
|
||||
continue;
|
||||
}
|
||||
if (ROOT_VALUE_FLAGS.has(arg)) {
|
||||
const next = args[i + 1];
|
||||
if (isValueToken(next)) {
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValueToken(arg: string | undefined): boolean {
|
||||
if (!arg) {
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,9 @@ import { listChannelPluginCatalogEntries } from "../channels/plugins/catalog.js"
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { CHAT_CHANNEL_ORDER } from "../channels/registry.js";
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { ensurePluginRegistryLoaded } from "./plugin-registry.js";
|
||||
// NOTE: plugin-registry.ts is NOT imported here to avoid pulling in
|
||||
// plugins/loader.ts → jiti at startup (which is slow on low-powered devices).
|
||||
// The OPENCLAW_EAGER_CHANNEL_OPTIONS path reads from the registry without force-loading.
|
||||
|
||||
function dedupe(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -21,7 +23,16 @@ export function resolveCliChannelOptions(): string[] {
|
||||
const catalog = listChannelPluginCatalogEntries().map((entry) => entry.id);
|
||||
const base = dedupe([...CHAT_CHANNEL_ORDER, ...catalog]);
|
||||
if (isTruthyEnvValue(process.env.OPENCLAW_EAGER_CHANNEL_OPTIONS)) {
|
||||
ensurePluginRegistryLoaded();
|
||||
// Emit a deprecation warning: ensurePluginRegistryLoaded() was removed to avoid
|
||||
// pulling plugins/loader.ts → jiti at startup (slow on low-powered devices).
|
||||
// OPENCLAW_EAGER_CHANNEL_OPTIONS no longer force-loads plugin channels; plugin IDs
|
||||
// are only included if the registry was already populated by another code path.
|
||||
process.emitWarning(
|
||||
"OPENCLAW_EAGER_CHANNEL_OPTIONS no longer force-loads plugin channels at startup. " +
|
||||
"Plugin IDs are only included if the registry was pre-loaded by another means. " +
|
||||
"Remove this env var to silence this warning.",
|
||||
{ code: "OPENCLAW_EAGER_CHANNEL_OPTIONS_DEPRECATED" },
|
||||
);
|
||||
const pluginIds = listChannelPlugins().map((plugin) => plugin.id);
|
||||
return dedupe([...base, ...pluginIds]);
|
||||
}
|
||||
|
||||
@@ -34,4 +34,36 @@ describe("createProgramContext", () => {
|
||||
agentChannelOptions: "last",
|
||||
});
|
||||
});
|
||||
|
||||
// Fast-path correctness: channel options must NOT be resolved until a command
|
||||
// actually needs them. This ensures --help/--version never trigger catalog discovery.
|
||||
it("does not call resolveCliChannelOptions before channelOptions is accessed", () => {
|
||||
resolveCliChannelOptionsMock.mockClear();
|
||||
createProgramContext(); // create context without accessing any channel option getter
|
||||
expect(resolveCliChannelOptionsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls resolveCliChannelOptions lazily when channelOptions getter is first accessed", () => {
|
||||
resolveCliChannelOptionsMock.mockClear().mockReturnValue(["discord"]);
|
||||
const ctx = createProgramContext();
|
||||
const _ = ctx.channelOptions;
|
||||
expect(resolveCliChannelOptionsMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("caches channel options so resolveCliChannelOptions is called only once per context", () => {
|
||||
resolveCliChannelOptionsMock.mockClear().mockReturnValue(["telegram"]);
|
||||
const ctx = createProgramContext();
|
||||
const _a = ctx.channelOptions;
|
||||
const _b = ctx.messageChannelOptions;
|
||||
const _c = ctx.agentChannelOptions;
|
||||
// All three getters share the same underlying cache.
|
||||
expect(resolveCliChannelOptionsMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("programVersion is accessible without triggering channel option resolution", () => {
|
||||
resolveCliChannelOptionsMock.mockClear();
|
||||
const ctx = createProgramContext();
|
||||
expect(ctx.programVersion).toBe("9.9.9-test");
|
||||
expect(resolveCliChannelOptionsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,11 +9,27 @@ export type ProgramContext = {
|
||||
};
|
||||
|
||||
export function createProgramContext(): ProgramContext {
|
||||
const channelOptions = resolveCliChannelOptions();
|
||||
// Defer resolveCliChannelOptions() until a command actually needs channel option strings.
|
||||
// This avoids the catalog discovery (discoverOpenClawPlugins) and module loading
|
||||
// during --help, --version, and other fast-path invocations.
|
||||
let _channelOptions: string[] | undefined;
|
||||
function getChannelOptions(): string[] {
|
||||
if (_channelOptions === undefined) {
|
||||
_channelOptions = resolveCliChannelOptions();
|
||||
}
|
||||
return _channelOptions;
|
||||
}
|
||||
|
||||
return {
|
||||
programVersion: VERSION,
|
||||
channelOptions,
|
||||
messageChannelOptions: channelOptions.join("|"),
|
||||
agentChannelOptions: ["last", ...channelOptions].join("|"),
|
||||
get channelOptions() {
|
||||
return getChannelOptions();
|
||||
},
|
||||
get messageChannelOptions() {
|
||||
return getChannelOptions().join("|");
|
||||
},
|
||||
get agentChannelOptions() {
|
||||
return ["last", ...getChannelOptions()].join("|");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ import { getSubCliCommandsWithSubcommands } from "./register.subclis.js";
|
||||
|
||||
const CLI_NAME = resolveCliName();
|
||||
const CLI_NAME_PATTERN = escapeRegExp(CLI_NAME);
|
||||
const ROOT_COMMANDS_WITH_SUBCOMMANDS = new Set([
|
||||
...getCoreCliCommandsWithSubcommands(),
|
||||
...getSubCliCommandsWithSubcommands(),
|
||||
]);
|
||||
const ROOT_COMMANDS_HINT =
|
||||
"Hint: commands suffixed with * have subcommands. Run <command> --help for details.";
|
||||
|
||||
@@ -44,6 +40,12 @@ const EXAMPLES = [
|
||||
] as const;
|
||||
|
||||
export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
// Built here (not at module level) to defer the work until help is actually configured.
|
||||
const rootCommandsWithSubcommands = new Set([
|
||||
...getCoreCliCommandsWithSubcommands(),
|
||||
...getSubCliCommandsWithSubcommands(),
|
||||
]);
|
||||
|
||||
program
|
||||
.name(CLI_NAME)
|
||||
.description("")
|
||||
@@ -73,7 +75,7 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
optionTerm: (option) => theme.option(option.flags),
|
||||
subcommandTerm: (cmd) => {
|
||||
const isRootCommand = cmd.parent === program;
|
||||
const hasSubcommands = isRootCommand && ROOT_COMMANDS_WITH_SUBCOMMANDS.has(cmd.name());
|
||||
const hasSubcommands = isRootCommand && rootCommandsWithSubcommands.has(cmd.name());
|
||||
return theme.command(hasSubcommands ? `${cmd.name()} *` : cmd.name());
|
||||
},
|
||||
});
|
||||
@@ -104,6 +106,9 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) {
|
||||
outputError: (str, write) => write(theme.error(str)),
|
||||
});
|
||||
|
||||
// Defense-in-depth: entry.ts already exits early for version flags before
|
||||
// loading this module. This block handles the case where help is configured
|
||||
// outside the normal entry path (e.g. tests, programmatic use).
|
||||
if (
|
||||
hasFlag(process.argv, "-V") ||
|
||||
hasFlag(process.argv, "--version") ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
shouldEnsureCliPath,
|
||||
shouldRegisterPrimarySubcommand,
|
||||
shouldSkipPluginCommandRegistration,
|
||||
shouldTryRouteFirst,
|
||||
} from "./run-main.js";
|
||||
|
||||
describe("rewriteUpdateFlagArgv", () => {
|
||||
@@ -54,7 +55,7 @@ describe("shouldRegisterPrimarySubcommand", () => {
|
||||
});
|
||||
|
||||
describe("shouldSkipPluginCommandRegistration", () => {
|
||||
it("skips plugin registration for root help/version", () => {
|
||||
it("skips plugin registration when no primary command is present", () => {
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "--help"],
|
||||
@@ -84,23 +85,44 @@ describe("shouldSkipPluginCommandRegistration", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps plugin registration for non-builtin help", () => {
|
||||
it("skips plugin registration for non-builtin help by default", () => {
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "voicecall", "--help"],
|
||||
primary: "voicecall",
|
||||
hasBuiltinPrimary: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps plugin registration for non-builtin command runs", () => {
|
||||
it("skips plugin registration for non-builtin command runs by default", () => {
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "voicecall", "status"],
|
||||
primary: "voicecall",
|
||||
hasBuiltinPrimary: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("skips plugin registration for unknown commands", () => {
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "definitely-not-a-command"],
|
||||
primary: "definitely-not-a-command",
|
||||
hasBuiltinPrimary: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("can force plugin registration via env guard", () => {
|
||||
expect(
|
||||
shouldSkipPluginCommandRegistration({
|
||||
argv: ["node", "openclaw", "unknown-cmd"],
|
||||
primary: "unknown-cmd",
|
||||
hasBuiltinPrimary: false,
|
||||
forcePluginRegistration: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -125,3 +147,14 @@ describe("shouldEnsureCliPath", () => {
|
||||
expect(shouldEnsureCliPath(["node", "openclaw", "acp", "-v"])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldTryRouteFirst", () => {
|
||||
it("enables route-first only for routed commands", () => {
|
||||
expect(shouldTryRouteFirst(["node", "openclaw", "status"])).toBe(true);
|
||||
expect(shouldTryRouteFirst(["node", "openclaw", "health"])).toBe(true);
|
||||
expect(shouldTryRouteFirst(["node", "openclaw", "sessions"])).toBe(true);
|
||||
expect(shouldTryRouteFirst(["node", "openclaw", "models", "status"])).toBe(false);
|
||||
expect(shouldTryRouteFirst(["node", "openclaw", "definitely-not-a-command"])).toBe(false);
|
||||
expect(shouldTryRouteFirst(["node", "openclaw"])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadDotEnv } from "../infra/dotenv.js";
|
||||
import { normalizeEnv } from "../infra/env.js";
|
||||
import { isTruthyEnvValue, normalizeEnv } from "../infra/env.js";
|
||||
import { formatUncaughtError } from "../infra/errors.js";
|
||||
import { isMainModule } from "../infra/is-main.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
@@ -9,7 +9,6 @@ import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||
import { enableConsoleCapture } from "../logging.js";
|
||||
import { getCommandPath, getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||
import { tryRouteCli } from "./route.js";
|
||||
import { normalizeWindowsArgv } from "./windows-argv.js";
|
||||
|
||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||
@@ -31,14 +30,21 @@ export function shouldSkipPluginCommandRegistration(params: {
|
||||
argv: string[];
|
||||
primary: string | null;
|
||||
hasBuiltinPrimary: boolean;
|
||||
forcePluginRegistration?: boolean;
|
||||
}): boolean {
|
||||
if (params.forcePluginRegistration) {
|
||||
return false;
|
||||
}
|
||||
if (params.hasBuiltinPrimary) {
|
||||
return true;
|
||||
}
|
||||
if (!params.primary) {
|
||||
return hasHelpOrVersion(params.argv);
|
||||
// No primary command can never map to a plugin top-level command.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// Unknown primary commands fast-fail without loading plugins/config.
|
||||
// Set OPENCLAW_FORCE_PLUGIN_COMMAND_REGISTRATION=1 to preserve legacy behavior.
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldEnsureCliPath(argv: string[]): boolean {
|
||||
@@ -61,6 +67,13 @@ export function shouldEnsureCliPath(argv: string[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ROUTE_FIRST_COMMANDS = new Set(["status", "health", "sessions"]);
|
||||
|
||||
export function shouldTryRouteFirst(argv: string[]): boolean {
|
||||
const primary = getPrimaryCommand(argv);
|
||||
return primary !== null && ROUTE_FIRST_COMMANDS.has(primary);
|
||||
}
|
||||
|
||||
export async function runCli(argv: string[] = process.argv) {
|
||||
const normalizedArgv = normalizeWindowsArgv(argv);
|
||||
loadDotEnv({ quiet: true });
|
||||
@@ -72,8 +85,11 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
// Enforce the minimum supported runtime before doing any work.
|
||||
assertSupportedRuntime();
|
||||
|
||||
if (await tryRouteCli(normalizedArgv)) {
|
||||
return;
|
||||
if (shouldTryRouteFirst(normalizedArgv)) {
|
||||
const { tryRouteCli } = await import("./route.js");
|
||||
if (await tryRouteCli(normalizedArgv)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
||||
@@ -108,10 +124,14 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
|
||||
const hasBuiltinPrimary =
|
||||
primary !== null && program.commands.some((command) => command.name() === primary);
|
||||
const forcePluginRegistration = isTruthyEnvValue(
|
||||
process.env.OPENCLAW_FORCE_PLUGIN_COMMAND_REGISTRATION,
|
||||
);
|
||||
const shouldSkipPluginRegistration = shouldSkipPluginCommandRegistration({
|
||||
argv: parseArgv,
|
||||
primary,
|
||||
hasBuiltinPrimary,
|
||||
forcePluginRegistration,
|
||||
});
|
||||
if (!shouldSkipPluginRegistration) {
|
||||
// Register plugin CLI commands before parsing
|
||||
|
||||
50
src/entry.ts
50
src/entry.ts
@@ -2,6 +2,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { isRootHelpRequest, isRootNoCommandRequest, isRootVersionRequest } from "./cli/argv.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
|
||||
import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js";
|
||||
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
|
||||
@@ -9,6 +10,7 @@ import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
|
||||
import { isMainModule } from "./infra/is-main.js";
|
||||
import { installProcessWarningFilter } from "./infra/warning-filter.js";
|
||||
import { attachChildProcessBridge } from "./process/child-process-bridge.js";
|
||||
import { VERSION } from "./version.js";
|
||||
|
||||
const ENTRY_WRAPPER_PAIRS = [
|
||||
{ wrapperBasename: "openclaw.mjs", entryBasename: "entry.js" },
|
||||
@@ -130,14 +132,44 @@ if (
|
||||
process.argv = parsed.argv;
|
||||
}
|
||||
|
||||
import("./cli/run-main.js")
|
||||
.then(({ runCli }) => runCli(process.argv))
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[openclaw] Failed to start CLI:",
|
||||
error instanceof Error ? (error.stack ?? error.message) : error,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
// Fast path: print version and exit before loading heavy CLI modules.
|
||||
// isRootVersionRequest handles --version/-V (stops at -- terminator so forwarded
|
||||
// args like `nodes run -- git --version` are excluded) and -v (only at root scope,
|
||||
// not when a subcommand is present). Avoids the full plugin/config startup on
|
||||
// low-powered devices (e.g. Pi4b).
|
||||
const argv = process.argv;
|
||||
if (isRootVersionRequest(argv)) {
|
||||
console.log(VERSION);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Fast path: display root help without loading run-main.js or route.ts.
|
||||
// isRootHelpRequest stops at -- and requires no subcommand before -h/--help,
|
||||
// so `openclaw status --help` still falls through to the full CLI path.
|
||||
// Importing program.js directly avoids the route.ts static import in run-main.ts.
|
||||
if (isRootHelpRequest(argv) || isRootNoCommandRequest(argv)) {
|
||||
import("./cli/program.js")
|
||||
.then(({ buildProgram }) => {
|
||||
buildProgram().outputHelp();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[openclaw] Failed to display help:",
|
||||
error instanceof Error ? (error.stack ?? error.message) : error,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
import("./cli/run-main.js")
|
||||
.then(({ runCli }) => runCli(process.argv))
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[openclaw] Failed to start CLI:",
|
||||
error instanceof Error ? (error.stack ?? error.message) : error,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user