CLI: share root-option parsing across argv, context, and routes

This commit is contained in:
Gustavo Madeira Santana
2026-03-02 19:50:32 -05:00
parent e3aee80358
commit c585ee4750
5 changed files with 70 additions and 97 deletions

View File

@@ -3,6 +3,7 @@
import { loadConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/config.js";
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
import { resolveOpenClawAgentDir } from "./agent-paths.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
@@ -69,40 +70,17 @@ const MODEL_CACHE = new Map<string, number>();
let loadPromise: Promise<void> | null = null;
let configuredWindowsPrimed = false;
function isValueToken(arg: string | undefined): boolean {
if (!arg || arg === "--") {
return false;
}
if (!arg.startsWith("-")) {
return true;
}
return /^-\d+(?:\.\d+)?$/.test(arg);
}
function getCommandPathFromArgv(argv: string[]): string[] {
const args = argv.slice(2);
const tokens: string[] = [];
let skipNextAsRootValue = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === "--") {
if (!arg || arg === FLAG_TERMINATOR) {
break;
}
if (skipNextAsRootValue) {
skipNextAsRootValue = false;
continue;
}
if (arg === "--profile" || arg === "--log-level") {
const next = args[i + 1];
skipNextAsRootValue = isValueToken(next);
continue;
}
if (
arg === "--dev" ||
arg === "--no-color" ||
arg.startsWith("--profile=") ||
arg.startsWith("--log-level=")
) {
const consumed = consumeRootOptionToken(args, i);
if (consumed > 0) {
i += consumed - 1;
continue;
}
if (arg.startsWith("-")) {

View File

@@ -1,11 +1,13 @@
import { isBunRuntime, isNodeRuntime } from "../daemon/runtime-binary.js";
import {
consumeRootOptionToken,
FLAG_TERMINATOR,
isValueToken,
} from "../infra/cli-root-options.js";
const HELP_FLAGS = new Set(["-h", "--help"]);
const VERSION_FLAGS = new Set(["-V", "--version"]);
const ROOT_VERSION_ALIAS_FLAG = "-v";
const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]);
const ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level"]);
const FLAG_TERMINATOR = "--";
export function hasHelpOrVersion(argv: string[]): boolean {
return (
@@ -13,19 +15,6 @@ export function hasHelpOrVersion(argv: string[]): boolean {
);
}
function isValueToken(arg: string | undefined): boolean {
if (!arg) {
return false;
}
if (arg === FLAG_TERMINATOR) {
return false;
}
if (!arg.startsWith("-")) {
return true;
}
return /^-\d+(?:\.\d+)?$/.test(arg);
}
function parsePositiveInt(value: string): number | undefined {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
@@ -62,17 +51,9 @@ export function hasRootVersionAlias(argv: string[]): boolean {
hasAlias = true;
continue;
}
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;
}
const consumed = consumeRootOptionToken(args, i);
if (consumed > 0) {
i += consumed - 1;
continue;
}
if (arg.startsWith("-")) {
@@ -109,17 +90,9 @@ function isRootInvocationForFlags(
hasTarget = true;
continue;
}
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;
}
const consumed = consumeRootOptionToken(args, i);
if (consumed > 0) {
i += consumed - 1;
continue;
}
// Unknown flags and subcommand-scoped help/version should fall back to Commander.
@@ -193,17 +166,9 @@ function getCommandPathInternal(
break;
}
if (opts.skipRootOptions) {
if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) {
continue;
}
if (ROOT_BOOLEAN_FLAGS.has(arg)) {
continue;
}
if (ROOT_VALUE_FLAGS.has(arg)) {
const next = args[i + 1];
if (isValueToken(next)) {
i += 1;
}
const consumed = consumeRootOptionToken(args, i);
if (consumed > 0) {
i += consumed - 1;
continue;
}
}

View File

@@ -1,3 +1,4 @@
import { consumeRootOptionToken } from "../../infra/cli-root-options.js";
import { defaultRuntime } from "../../runtime.js";
import { getFlagValue, getPositiveIntFlagValue, getVerboseFlag, hasFlag } from "../argv.js";
@@ -99,16 +100,6 @@ const routeMemoryStatus: RouteSpec = {
},
};
function isValueToken(arg: string | undefined): boolean {
if (!arg || arg === "--") {
return false;
}
if (!arg.startsWith("-")) {
return true;
}
return /^-\d+(?:\.\d+)?$/.test(arg);
}
function getCommandPositionals(argv: string[]): string[] {
const out: string[] = [];
const args = argv.slice(2);
@@ -119,17 +110,9 @@ function getCommandPositionals(argv: string[]): string[] {
break;
}
if (!commandStarted) {
if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) {
continue;
}
if (arg === "--dev" || arg === "--no-color") {
continue;
}
if (arg === "--profile" || arg === "--log-level") {
const next = args[i + 1];
if (isValueToken(next)) {
i += 1;
}
const consumed = consumeRootOptionToken(args, i);
if (consumed > 0) {
i += consumed - 1;
continue;
}
}

View File

@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { consumeRootOptionToken } from "./cli-root-options.js";
describe("consumeRootOptionToken", () => {
it("consumes boolean and inline root options", () => {
expect(consumeRootOptionToken(["--dev"], 0)).toBe(1);
expect(consumeRootOptionToken(["--profile=work"], 0)).toBe(1);
expect(consumeRootOptionToken(["--log-level=debug"], 0)).toBe(1);
});
it("consumes split root value option only when next token is a value", () => {
expect(consumeRootOptionToken(["--profile", "work"], 0)).toBe(2);
expect(consumeRootOptionToken(["--profile", "--no-color"], 0)).toBe(1);
expect(consumeRootOptionToken(["--profile", "--"], 0)).toBe(1);
});
});

View File

@@ -0,0 +1,31 @@
export const FLAG_TERMINATOR = "--";
const ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]);
const ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level"]);
export function isValueToken(arg: string | undefined): boolean {
if (!arg || arg === FLAG_TERMINATOR) {
return false;
}
if (!arg.startsWith("-")) {
return true;
}
return /^-\d+(?:\.\d+)?$/.test(arg);
}
export function consumeRootOptionToken(args: ReadonlyArray<string>, index: number): number {
const arg = args[index];
if (!arg) {
return 0;
}
if (ROOT_BOOLEAN_FLAGS.has(arg)) {
return 1;
}
if (arg.startsWith("--profile=") || arg.startsWith("--log-level=")) {
return 1;
}
if (ROOT_VALUE_FLAGS.has(arg)) {
return isValueToken(args[index + 1]) ? 2 : 1;
}
return 0;
}