Skip to content

Commit 83bf422

Browse files
committed
fix(cli): do not hoist a bare parent value option with no following value
1 parent dbd4e4f commit 83bf422

3 files changed

Lines changed: 84 additions & 1 deletion

File tree

extensions/browser/src/cli/browser-cli.lazy.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,43 @@ describe("registerBrowserCli lazy browser subcommands", () => {
209209
expect(tabsCommand.parent?.opts().browserProfile).toBe("remote");
210210
});
211211

212+
it("preserves the missing-value error for a bare --browser-profile after the subcommand (#55563 regression 1)", async () => {
213+
// A bare parent value option with no following value must not be hoisted
214+
// alone: hoisting `--browser-profile` before `tabs` would let Commander
215+
// consume `tabs` as the profile value. Leaving it in place preserves the
216+
// missing-value/unknown-option error and the tabs action never runs.
217+
const program = new Command();
218+
program.name("openclaw");
219+
program.enablePositionalOptions();
220+
221+
registerBrowserCli(program, ["node", "openclaw", "browser", "tabs", "--browser-profile"]);
222+
223+
await expect(
224+
program.parseAsync(["browser", "tabs", "--browser-profile"], { from: "user" }),
225+
).rejects.toThrow();
226+
expect(manageMocks.tabsAction).not.toHaveBeenCalled();
227+
});
228+
229+
it("preserves the missing-value error when --browser-profile is followed by another flag (#55563 regression 1)", async () => {
230+
const program = new Command();
231+
program.name("openclaw");
232+
program.enablePositionalOptions();
233+
234+
registerBrowserCli(program, [
235+
"node",
236+
"openclaw",
237+
"browser",
238+
"tabs",
239+
"--browser-profile",
240+
"--json",
241+
]);
242+
243+
await expect(
244+
program.parseAsync(["browser", "tabs", "--browser-profile", "--json"], { from: "user" }),
245+
).rejects.toThrow();
246+
expect(manageMocks.tabsAction).not.toHaveBeenCalled();
247+
});
248+
212249
it("skips browser option values when selecting the lazy command group", async () => {
213250
const program = new Command();
214251
program.name("openclaw");

src/cli/program/helpers.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,47 @@ describe("hoistParentOptionsBeforeSubcommand", () => {
243243
]);
244244
});
245245

246+
it("does not hoist a bare value option with no following value", () => {
247+
const { browser, tabs } = buildBrowserTree();
248+
const argv = ["node", "openclaw", "browser", "tabs", "--browser-profile"];
249+
expect(
250+
hoistParentOptionsBeforeSubcommand({
251+
argv,
252+
parentCommand: browser,
253+
subcommandName: "tabs",
254+
subcommandCommand: tabs,
255+
}),
256+
).toEqual(argv);
257+
});
258+
259+
it("does not hoist a value option followed by another flag", () => {
260+
// The bare `--browser-profile` (no following value) is left in place so the
261+
// missing-value error is preserved; the trailing `--json` is a valid parent
262+
// boolean option and is still hoisted before the subcommand.
263+
const { browser, tabs } = buildBrowserTree();
264+
expect(
265+
hoistParentOptionsBeforeSubcommand({
266+
argv: ["node", "openclaw", "browser", "tabs", "--browser-profile", "--json"],
267+
parentCommand: browser,
268+
subcommandName: "tabs",
269+
subcommandCommand: tabs,
270+
}),
271+
).toEqual(["node", "openclaw", "browser", "--json", "tabs", "--browser-profile"]);
272+
});
273+
274+
it("does not hoist an empty --flag= form", () => {
275+
const { browser, tabs } = buildBrowserTree();
276+
const argv = ["node", "openclaw", "browser", "tabs", "--browser-profile="];
277+
expect(
278+
hoistParentOptionsBeforeSubcommand({
279+
argv,
280+
parentCommand: browser,
281+
subcommandName: "tabs",
282+
subcommandCommand: tabs,
283+
}),
284+
).toEqual(argv);
285+
});
286+
246287
it("returns argv unchanged when the parent command is the root", () => {
247288
const root = new Command().name("openclaw").option("--json", "", false);
248289
root.command("status");

src/cli/program/helpers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,12 @@ function consumeHoistableOption(
169169
if (equalsIndex !== -1) {
170170
return arg.slice(equalsIndex + 1).trim() ? 1 : 0;
171171
}
172-
return isHoistableValueToken(args[index + 1]) ? 2 : 1;
172+
// A parent value option without a real following value (bare flag at the
173+
// end, or followed by another flag) must NOT be hoisted alone: hoisting the
174+
// bare flag before the subcommand would let Commander consume the next token
175+
// (e.g. the subcommand name) as the option value on reparse, swallowing the
176+
// missing-argument error. Leave it in place so Commander reports the error.
177+
return isHoistableValueToken(args[index + 1]) ? 2 : 0;
173178
}
174179

175180
/**

0 commit comments

Comments
 (0)