mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 22:36:06 +00:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user