fix(cli): document Commander rawArgs dependency

Merges the Clownfish-repaired contributor branch for #91193. Clownfish preflight cleared security/comments/review, accepted pnpm check:changed, and the PR is clean/mergeable on head a05c170345.
This commit is contained in:
Song Zhenlin
2026-06-22 21:01:52 +08:00
committed by GitHub
parent dbc07ad84d
commit afa1045238
2 changed files with 70 additions and 7 deletions

View File

@@ -20,6 +20,10 @@ function setRawArgs(command: Command, rawArgs: string[]): void {
(command as Command & { rawArgs: string[] }).rawArgs = rawArgs;
}
function deleteRawArgs(command: Command): void {
delete (command as Command & { rawArgs?: string[] }).rawArgs;
}
describe("reparseProgramFromActionArgs", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -104,12 +108,33 @@ describe("reparseProgramFromActionArgs", () => {
expect(buildParseArgvMock).toHaveBeenCalledWith({
programName: "openclaw",
rawArgs: ["node", "openclaw", "workspaces", "audit", "export", "--since", "1"],
fallbackArgv: ["export", "--since", "1"],
fallbackArgv: ["workspaces", "audit", "export", "--since", "1"],
});
expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "status"]);
expect(auditParseAsync).not.toHaveBeenCalled();
});
it("reconstructs the full nested command path when Commander rawArgs is missing", async () => {
// #83893: nested lazy commands still need their ancestor path if
// Commander stops exposing root rawArgs at runtime.
const root = new Command().name("openclaw");
const workspaces = root.command("workspaces");
const audit = workspaces.command("audit");
const exportCommand = audit.command("export");
deleteRawArgs(root);
const parseAsync = vi.spyOn(root, "parseAsync").mockResolvedValue(root);
resolveActionArgsMock.mockReturnValue(["--since", "1"]);
await reparseProgramFromActionArgs(audit, [exportCommand]);
expect(buildParseArgvMock).toHaveBeenCalledWith({
programName: "openclaw",
rawArgs: undefined,
fallbackArgv: ["workspaces", "audit", "export", "--since", "1"],
});
expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "status"]);
});
it("uses program root when action command is missing", async () => {
const program = new Command().name("openclaw");
const parseAsync = vi.spyOn(program, "parseAsync").mockResolvedValue(program);
@@ -124,4 +149,23 @@ describe("reparseProgramFromActionArgs", () => {
});
expect(parseAsync).toHaveBeenCalledWith(["node", "openclaw", "status"]);
});
it("falls back to fallbackArgv when Commander rawArgs is missing from the root command", async () => {
// #83893: rawArgs is a Commander runtime field, so the root command must
// still reparse from reconstructed argv if Commander stops exposing it.
const root = new Command().name("openclaw");
const configCommand = root.command("config");
deleteRawArgs(root);
const parseAsync = vi.spyOn(root, "parseAsync").mockResolvedValue(root);
resolveActionArgsMock.mockReturnValue(["set", "key", "value"]);
await reparseProgramFromActionArgs(root, [configCommand]);
expect(buildParseArgvMock).toHaveBeenCalledWith({
programName: "openclaw",
rawArgs: undefined,
fallbackArgv: ["config", "set", "key", "value"],
});
expect(parseAsync).toHaveBeenCalled();
});
});

View File

@@ -3,13 +3,33 @@ import type { Command } from "commander";
import { buildParseArgv } from "../argv.js";
import { resolveActionArgs, resolveCommandOptionArgs } from "./helpers.js";
function getCommandPathFromRoot(command: Command | undefined): string[] {
const path: string[] = [];
let current = command;
while (current?.parent) {
const name = current.name();
if (name) {
path.unshift(name);
}
current = current.parent;
}
return path;
}
function buildFallbackArgv(program: Command, actionCommand: Command | undefined): string[] {
const actionArgsList = resolveActionArgs(actionCommand);
const parentOptionArgs =
actionCommand?.parent === program ? resolveCommandOptionArgs(program) : [];
return actionCommand?.name()
? [...parentOptionArgs, actionCommand.name(), ...actionArgsList]
: [...parentOptionArgs, ...actionArgsList];
const commandPath = getCommandPathFromRoot(actionCommand);
if (commandPath.length === 0) {
return [...parentOptionArgs, ...actionArgsList];
}
return [
...commandPath.slice(0, -1),
...parentOptionArgs,
commandPath[commandPath.length - 1],
...actionArgsList,
];
}
function findRootCommand(cmd: Command): Command {
@@ -27,9 +47,8 @@ export async function reparseProgramFromActionArgs(
): Promise<void> {
const actionCommand = actionArgs.at(-1) as Command | undefined;
// Use the true root program for argv reconstruction and parsing.
// For nested lazy commands (e.g. workspaces → audit), `program` is a sub-command
// whose rawArgs is cleared by Commander's restoreStateBeforeParse(). Only the
// root program retains the rawArgs set by _prepareUserArgs.
// Commander keeps rawArgs as a JS runtime field, not a typed API; if a
// future version removes it, buildParseArgv falls back to reconstructed argv.
const rootProgram = findRootCommand(actionCommand ?? program);
const rawArgs = (rootProgram as Command & { rawArgs?: string[] }).rawArgs;
const fallbackArgv = buildFallbackArgv(program, actionCommand);