CLI: unify routed config positional parsing

This commit is contained in:
Gustavo Madeira Santana
2026-03-02 21:11:16 -05:00
parent d3c637d193
commit 15a0455d04
4 changed files with 176 additions and 31 deletions

View File

@@ -102,6 +102,38 @@ describe("program routes", () => {
expect(runConfigUnsetMock).toHaveBeenCalledWith({ path: "update.channel" });
});
it("passes config get path when root value options appear after subcommand", async () => {
const route = expectRoute(["config", "get"]);
await expect(
route?.run([
"node",
"openclaw",
"config",
"get",
"--log-level",
"debug",
"update.channel",
"--json",
]),
).resolves.toBe(true);
expect(runConfigGetMock).toHaveBeenCalledWith({ path: "update.channel", json: true });
});
it("passes config unset path when root value options appear after subcommand", async () => {
const route = expectRoute(["config", "unset"]);
await expect(
route?.run(["node", "openclaw", "config", "unset", "--profile", "work", "update.channel"]),
).resolves.toBe(true);
expect(runConfigUnsetMock).toHaveBeenCalledWith({ path: "update.channel" });
});
it("returns false for config get route when unknown option appears", async () => {
await expectRunFalse(
["config", "get"],
["node", "openclaw", "config", "get", "--mystery", "value", "update.channel"],
);
});
it("returns false for memory status route when --agent value is missing", async () => {
await expectRunFalse(["memory", "status"], ["node", "openclaw", "memory", "status", "--agent"]);
});

View File

@@ -1,6 +1,12 @@
import { consumeRootOptionToken, isValueToken } from "../../infra/cli-root-options.js";
import { isValueToken } from "../../infra/cli-root-options.js";
import { defaultRuntime } from "../../runtime.js";
import { getFlagValue, getPositiveIntFlagValue, getVerboseFlag, hasFlag } from "../argv.js";
import {
getCommandPositionalsWithRootOptions,
getFlagValue,
getPositiveIntFlagValue,
getVerboseFlag,
hasFlag,
} from "../argv.js";
export type RouteSpec = {
match: (path: string[]) => boolean;
@@ -100,31 +106,6 @@ const routeMemoryStatus: RouteSpec = {
},
};
function getCommandPositionals(argv: string[]): string[] {
const out: string[] = [];
const args = argv.slice(2);
let commandStarted = false;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === "--") {
break;
}
if (!commandStarted) {
const consumed = consumeRootOptionToken(args, i);
if (consumed > 0) {
i += consumed - 1;
continue;
}
}
if (arg.startsWith("-")) {
continue;
}
commandStarted = true;
out.push(arg);
}
return out;
}
function getFlagValues(argv: string[], name: string): string[] | null {
const values: string[] = [];
const args = argv.slice(2);
@@ -156,8 +137,14 @@ function getFlagValues(argv: string[], name: string): string[] | null {
const routeConfigGet: RouteSpec = {
match: (path) => path[0] === "config" && path[1] === "get",
run: async (argv) => {
const positionals = getCommandPositionals(argv);
const pathArg = positionals[2];
const positionals = getCommandPositionalsWithRootOptions(argv, {
commandPath: ["config", "get"],
booleanFlags: ["--json"],
});
if (!positionals || positionals.length !== 1) {
return false;
}
const pathArg = positionals[0];
if (!pathArg) {
return false;
}
@@ -171,8 +158,13 @@ const routeConfigGet: RouteSpec = {
const routeConfigUnset: RouteSpec = {
match: (path) => path[0] === "config" && path[1] === "unset",
run: async (argv) => {
const positionals = getCommandPositionals(argv);
const pathArg = positionals[2];
const positionals = getCommandPositionalsWithRootOptions(argv, {
commandPath: ["config", "unset"],
});
if (!positionals || positionals.length !== 1) {
return false;
}
const pathArg = positionals[0];
if (!pathArg) {
return false;
}