Skip to content

Commit b7e01e3

Browse files
committed
Recognize CLI help regardless of argument position
Value flags were binding the next token even when it was --help or another flag. Scan argv for help first, and reject flag-shaped tokens as required option values.
1 parent 475dda2 commit b7e01e3

3 files changed

Lines changed: 76 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1313

1414
## [Unreleased]
1515

16+
### Fixed
17+
18+
- CLI `--help` / `-h` is recognized in any argument position. Value flags no
19+
longer swallow help or other flag-shaped tokens as their option values.
20+
1621
## [0.3.11] - 2026-08-31
1722

1823
### Changed

src/config.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,67 @@ describe("loadConfig", () => {
741741
}
742742
});
743743

744+
test("--help after flags throws CliHelpError", async () => {
745+
await expect(
746+
loadConfig(["--force", "--help"], { globalSettingsPath: NO_SETTINGS }),
747+
).rejects.toBeInstanceOf(CliHelpError);
748+
await expect(
749+
loadConfig(["--force", "-h"], { globalSettingsPath: NO_SETTINGS }),
750+
).rejects.toBeInstanceOf(CliHelpError);
751+
});
752+
753+
test("--help after a positional throws CliHelpError", async () => {
754+
await expect(
755+
loadConfig(["ship it", "--help"], { globalSettingsPath: NO_SETTINGS }),
756+
).rejects.toBeInstanceOf(CliHelpError);
757+
});
758+
759+
test("resume -h / --help throws CliHelpError instead of treating it as a session id", async () => {
760+
await expect(
761+
loadConfig(["resume", "-h"], { globalSettingsPath: NO_SETTINGS }),
762+
).rejects.toBeInstanceOf(CliHelpError);
763+
await expect(
764+
loadConfig(["resume", "--help"], { globalSettingsPath: NO_SETTINGS }),
765+
).rejects.toBeInstanceOf(CliHelpError);
766+
await expect(
767+
loadConfig(["continue", "-h"], { globalSettingsPath: NO_SETTINGS }),
768+
).rejects.toBeInstanceOf(CliHelpError);
769+
});
770+
771+
test("value flags do not swallow --help / -h as their value", async () => {
772+
for (const flag of ["--provider", "--model", "--cwd", "--config", "--profile"] as const) {
773+
await expect(
774+
loadConfig([flag, "--help"], { globalSettingsPath: NO_SETTINGS }),
775+
).rejects.toBeInstanceOf(CliHelpError);
776+
await expect(
777+
loadConfig([flag, "-h"], { globalSettingsPath: NO_SETTINGS }),
778+
).rejects.toBeInstanceOf(CliHelpError);
779+
}
780+
});
781+
782+
test("value flags reject other flag-shaped tokens as values", async () => {
783+
await expect(
784+
loadConfig(["--provider", "--force"], { globalSettingsPath: NO_SETTINGS }),
785+
).rejects.toThrow("--provider requires a value");
786+
await expect(
787+
loadConfig(["--model", "--cwd"], { globalSettingsPath: NO_SETTINGS }),
788+
).rejects.toThrow("--model requires a value");
789+
await expect(
790+
loadConfig(["exec", "--director", "--force", "ship it"], {
791+
globalSettingsPath: NO_SETTINGS,
792+
}),
793+
).rejects.toThrow("--director requires a value");
794+
});
795+
796+
test("value flags still error clearly when the value is omitted", async () => {
797+
await expect(loadConfig(["--provider"], { globalSettingsPath: NO_SETTINGS })).rejects.toThrow(
798+
"--provider requires a value",
799+
);
800+
await expect(loadConfig(["--model"], { globalSettingsPath: NO_SETTINGS })).rejects.toThrow(
801+
"--model requires a value",
802+
);
803+
});
804+
744805
test("rejects unknown flags", async () => {
745806
await expect(loadConfig(["--unknown"], { globalSettingsPath: NO_SETTINGS })).rejects.toThrow(
746807
/unrecognized flag/,

src/config/index.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,12 @@ export async function loadConfig(
567567
argv: readonly string[],
568568
options: LoadConfigOptions = {},
569569
): Promise<Config | UnconfiguredConfig> {
570+
// Help wins in any position, including after subcommands and immediately
571+
// after a value flag that would otherwise swallow the token as its value.
572+
if (argv.some((arg) => arg === "--help" || arg === "-h")) {
573+
throw new CliHelpError();
574+
}
575+
570576
const args = [...argv];
571577

572578
// Leading subcommand: `corbits exec "prompt"` (alias: `run`). Default is TUI.
@@ -603,10 +609,6 @@ export async function loadConfig(
603609
}
604610
}
605611

606-
if (args[0] === "--help" || args[0] === "-h") {
607-
throw new CliHelpError();
608-
}
609-
610612
let cwd = process.cwd();
611613
let force = false;
612614
let dangerouslySkipPermissions = false;
@@ -626,7 +628,10 @@ export async function loadConfig(
626628
const positional: string[] = [];
627629

628630
const requireValue = (flag: string, value: string | undefined): string => {
629-
if (value === undefined) {
631+
// Flag-shaped tokens are never option values. `--provider --force` and a
632+
// trailing `--provider` both surface as a missing value rather than binding
633+
// the next flag (or accepting `--help`, which is already handled above).
634+
if (value === undefined || value.startsWith("-")) {
630635
throw new Error(`${flag} requires a value`);
631636
}
632637
return value;

0 commit comments

Comments
 (0)