mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(cli): accept --log-level after subcommands
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
isRootVersionInvocation,
|
||||
normalizeGeneratedHelpCommandArgv,
|
||||
normalizeRootHelpTargetArgv,
|
||||
normalizeRootLogLevelArgv,
|
||||
normalizeRootNoColorArgv,
|
||||
shouldMigrateState,
|
||||
shouldMigrateStateFromPath,
|
||||
@@ -227,6 +228,63 @@ describe("argv helpers", () => {
|
||||
).toEqual(["node", "openclaw", "--no-color", "doctor", "--lint", "--json"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "subcommand trailing log-level",
|
||||
argv: ["node", "openclaw", "doctor", "--log-level", "debug", "--json"],
|
||||
expected: ["node", "openclaw", "--log-level", "debug", "doctor", "--json"],
|
||||
},
|
||||
{
|
||||
name: "subcommand trailing log-level equals form",
|
||||
argv: ["node", "openclaw", "doctor", "--log-level=trace", "--json"],
|
||||
expected: ["node", "openclaw", "--log-level=trace", "doctor", "--json"],
|
||||
},
|
||||
{
|
||||
name: "keeps existing root options first",
|
||||
argv: ["node", "openclaw", "--profile", "work", "doctor", "--log-level", "debug"],
|
||||
expected: ["node", "openclaw", "--profile", "work", "--log-level", "debug", "doctor"],
|
||||
},
|
||||
{
|
||||
name: "keeps log-level after possible command option value",
|
||||
argv: ["node", "openclaw", "agent", "--message", "--log-level", "debug"],
|
||||
expected: ["node", "openclaw", "agent", "--message", "--log-level", "debug"],
|
||||
},
|
||||
{
|
||||
name: "flag terminator leaves log-level positional",
|
||||
argv: ["node", "openclaw", "nodes", "run", "--", "--log-level", "debug"],
|
||||
expected: ["node", "openclaw", "nodes", "run", "--", "--log-level", "debug"],
|
||||
},
|
||||
{
|
||||
name: "missing value remains command scoped",
|
||||
argv: ["node", "openclaw", "doctor", "--log-level", "--json"],
|
||||
expected: ["node", "openclaw", "doctor", "--log-level", "--json"],
|
||||
},
|
||||
])("normalizes root --log-level before command parsing: $name", ({ argv, expected }) => {
|
||||
expect(normalizeRootLogLevelArgv(argv)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("allows final command metadata to lift log-level after boolean command flags", () => {
|
||||
const argv = ["node", "openclaw", "doctor", "--lint", "--json", "--log-level", "debug"];
|
||||
|
||||
expect(
|
||||
normalizeRootLogLevelArgv(argv, {
|
||||
shouldPreserveLogLevel: ({ remainingArgs, logLevelIndex }) =>
|
||||
remainingArgs[logLevelIndex - 1] === "--message",
|
||||
}),
|
||||
).toEqual(["node", "openclaw", "--log-level", "debug", "doctor", "--lint", "--json"]);
|
||||
});
|
||||
|
||||
it("preserves log-level when final command metadata owns the option", () => {
|
||||
const argv = ["node", "openclaw", "plugin-cmd", "--log-level", "debug"];
|
||||
|
||||
expect(
|
||||
normalizeRootLogLevelArgv(argv, {
|
||||
shouldPreserveLogLevel: ({ remainingArgs, logLevelIndex }) =>
|
||||
remainingArgs[logLevelIndex] === "--log-level",
|
||||
}),
|
||||
).toEqual(argv);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "root help command",
|
||||
|
||||
+80
-2
@@ -260,17 +260,39 @@ export type NormalizeRootNoColorArgvOptions = {
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
export type NormalizeRootLogLevelArgvOptions = {
|
||||
shouldPreserveLogLevel?: (params: {
|
||||
remainingArgs: readonly string[];
|
||||
logLevelIndex: number;
|
||||
consumed: number;
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
function isPossibleCommandOptionValue(
|
||||
remainingArgs: readonly string[],
|
||||
noColorIndex: number,
|
||||
optionIndex: number,
|
||||
): boolean {
|
||||
const previous = remainingArgs[noColorIndex - 1];
|
||||
const previous = remainingArgs[optionIndex - 1];
|
||||
if (!previous?.startsWith("-") || previous === FLAG_TERMINATOR) {
|
||||
return false;
|
||||
}
|
||||
return !previous.includes("=");
|
||||
}
|
||||
|
||||
function consumeRootLogLevelToken(args: readonly string[], index: number): number {
|
||||
const arg = args[index];
|
||||
if (!arg || arg === FLAG_TERMINATOR) {
|
||||
return 0;
|
||||
}
|
||||
if (arg.startsWith("--log-level=")) {
|
||||
return arg.slice("--log-level=".length).trim() ? 1 : 0;
|
||||
}
|
||||
if (arg === "--log-level") {
|
||||
return isValueToken(args[index + 1]) ? 2 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function normalizeRootNoColorArgv(
|
||||
argv: string[],
|
||||
options: NormalizeRootNoColorArgvOptions = {},
|
||||
@@ -323,6 +345,62 @@ export function normalizeRootNoColorArgv(
|
||||
return [...prefix, ...rootPrefix, ...movedNoColorArgs, ...nextArgs];
|
||||
}
|
||||
|
||||
export function normalizeRootLogLevelArgv(
|
||||
argv: string[],
|
||||
options: NormalizeRootLogLevelArgvOptions = {},
|
||||
): string[] {
|
||||
const prefix = argv.slice(0, 2);
|
||||
const args = argv.slice(2);
|
||||
let rootPrefixEnd = 0;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg || arg === FLAG_TERMINATOR) {
|
||||
break;
|
||||
}
|
||||
const consumed = consumeRootOptionToken(args, index);
|
||||
if (consumed <= 0) {
|
||||
break;
|
||||
}
|
||||
rootPrefixEnd = index + consumed;
|
||||
index += consumed - 1;
|
||||
}
|
||||
|
||||
const rootPrefix = args.slice(0, rootPrefixEnd);
|
||||
const remainingArgs = args.slice(rootPrefixEnd);
|
||||
const movedLogLevelArgs: string[] = [];
|
||||
const nextArgs: string[] = [];
|
||||
for (let index = 0; index < remainingArgs.length; index += 1) {
|
||||
const arg = remainingArgs[index];
|
||||
if (arg === FLAG_TERMINATOR) {
|
||||
nextArgs.push(...remainingArgs.slice(index));
|
||||
break;
|
||||
}
|
||||
const consumed = consumeRootLogLevelToken(remainingArgs, index);
|
||||
if (consumed > 0) {
|
||||
const shouldPreserve =
|
||||
options.shouldPreserveLogLevel?.({
|
||||
remainingArgs,
|
||||
logLevelIndex: index,
|
||||
consumed,
|
||||
}) ?? isPossibleCommandOptionValue(remainingArgs, index);
|
||||
const tokens = remainingArgs.slice(index, index + consumed);
|
||||
if (shouldPreserve) {
|
||||
nextArgs.push(...tokens);
|
||||
} else {
|
||||
movedLogLevelArgs.push(...tokens);
|
||||
}
|
||||
index += consumed - 1;
|
||||
continue;
|
||||
}
|
||||
nextArgs.push(arg);
|
||||
}
|
||||
|
||||
if (movedLogLevelArgs.length === 0) {
|
||||
return argv;
|
||||
}
|
||||
return [...prefix, ...rootPrefix, ...movedLogLevelArgs, ...nextArgs];
|
||||
}
|
||||
|
||||
export function getFlagValue(argv: string[], name: string): string | null | undefined {
|
||||
const args = argv.slice(2);
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
|
||||
+47
-1
@@ -18,6 +18,7 @@ import { resolveCliArgvInvocation } from "./argv-invocation.js";
|
||||
import {
|
||||
normalizeGeneratedHelpCommandArgv,
|
||||
normalizeRootHelpTargetArgv,
|
||||
normalizeRootLogLevelArgv,
|
||||
normalizeRootNoColorArgv,
|
||||
} from "./argv.js";
|
||||
import {
|
||||
@@ -425,6 +426,41 @@ function isNoColorConsumedAsCommandOptionValue(
|
||||
return pendingValue;
|
||||
}
|
||||
|
||||
function isLogLevelConsumedAsCommandOption(
|
||||
program: CommanderCommand,
|
||||
remainingArgs: readonly string[],
|
||||
logLevelIndex: number,
|
||||
): boolean {
|
||||
let command = program;
|
||||
let pendingValue = false;
|
||||
for (let index = 0; index < logLevelIndex; index += 1) {
|
||||
const arg = remainingArgs[index];
|
||||
if (!arg || arg === FLAG_TERMINATOR) {
|
||||
return false;
|
||||
}
|
||||
if (pendingValue) {
|
||||
pendingValue = false;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-")) {
|
||||
const option = findCommandOption(command, arg);
|
||||
if (!option && index === logLevelIndex - 1 && !arg.includes("=")) {
|
||||
return true;
|
||||
}
|
||||
pendingValue = shouldOptionConsumeFollowingToken(option, arg, remainingArgs[index + 1]);
|
||||
continue;
|
||||
}
|
||||
command = findSubcommand(command, arg) ?? command;
|
||||
}
|
||||
|
||||
if (pendingValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const arg = remainingArgs[logLevelIndex];
|
||||
return command !== program && arg !== undefined && findCommandOption(command, arg) !== undefined;
|
||||
}
|
||||
|
||||
function normalizeRootNoColorArgvForProgram(argv: string[], program: CommanderCommand): string[] {
|
||||
return normalizeRootNoColorArgv(argv, {
|
||||
shouldPreserveNoColor: ({ remainingArgs, noColorIndex }) =>
|
||||
@@ -432,6 +468,13 @@ function normalizeRootNoColorArgvForProgram(argv: string[], program: CommanderCo
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeRootLogLevelArgvForProgram(argv: string[], program: CommanderCommand): string[] {
|
||||
return normalizeRootLogLevelArgv(argv, {
|
||||
shouldPreserveLogLevel: ({ remainingArgs, logLevelIndex }) =>
|
||||
isLogLevelConsumedAsCommandOption(program, remainingArgs, logLevelIndex),
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureCliEnvProxyDispatcher(): Promise<void> {
|
||||
try {
|
||||
const { hasEnvHttpProxyAgentConfigured } = await import("../infra/net/proxy-env.js");
|
||||
@@ -1051,7 +1094,10 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
}
|
||||
}
|
||||
|
||||
parseArgv = normalizeRootNoColorArgvForProgram(parseArgv, program);
|
||||
parseArgv = normalizeRootLogLevelArgvForProgram(
|
||||
normalizeRootNoColorArgvForProgram(parseArgv, program),
|
||||
program,
|
||||
);
|
||||
stopStartupProgress();
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user