Skip to content

Commit 37ca488

Browse files
feat(cli): add --version and -V flags (#1230)
* feat(cli): add --version and -V flags * fix(auth): import mock-module from testkit The store test imported tests/helpers/mock-module.js, which is not in the tree. The helper lives in testkit. * feat(cli): name the binary in --version output
1 parent 90d02ae commit 37ca488

6 files changed

Lines changed: 107 additions & 5 deletions

File tree

‎docs/IMPLEMENTATION.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts`
424424
| `--auto` | true (default) | Force auto mode on (workspace writes + unconstrained shell without prompts) |
425425
| `--no-auto` | false | Start with auto mode off (ask on every consequential action); no in-session key toggles it |
426426
| `--help`, `-h` | — | Show help (exit 0 via `CliHelpError`) |
427+
| `--version`, `-V` | — | Print `corbits v` plus the running `package.json` version (exit 0 via `CliVersionError`) |
427428

428429
Positional arguments after flags are joined into the optional initial task delivered when the TUI mounts. With no positional task, the operator starts from an empty prompt.
429430

‎src/auth/store.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { tmpdir } from "node:os";
1111
import { join } from "node:path";
1212
import { type } from "arktype";
1313

14-
import { withMockedModule } from "../../tests/helpers/mock-module.js";
14+
import { withMockedModule } from "../../testkit/mock-module.js";
1515
import type { BaseTokens } from "./store.js";
1616

1717
let failingUnlinkPath: string | undefined;

‎src/config.test.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {
2020
catalogEntryAsProviderSettings,
2121
CliHelpError,
2222
CliUserError,
23+
CliVersionError,
2324
CLI_HELP_TEXT,
25+
CLI_VERSION_TEXT,
2426
KEYLESS_API_KEY,
2527
loadConfig,
2628
providerCatalogToSettings,
@@ -30,6 +32,7 @@ import {
3032
SOURCE_MAX_TOKENS,
3133
} from "./config/index.js";
3234
import { DIRECTOR_IDS } from "./agent/directors/types.js";
35+
import pkg from "../package.json" with { type: "json" };
3336
import {
3437
clearSourceCredentials,
3538
peekSourceCredentialSecret,
@@ -200,6 +203,20 @@ async function expectCliHelp(argv: readonly string[]): Promise<void> {
200203
}
201204
}
202205

206+
async function expectCliVersion(argv: readonly string[]): Promise<void> {
207+
try {
208+
await loadConfig([...argv], { globalSettingsPath: NO_SETTINGS });
209+
expect.unreachable("expected CliVersionError");
210+
} catch (err) {
211+
expect(err).toBeInstanceOf(CliVersionError);
212+
const version = err as CliVersionError;
213+
expect(version.exitCode).toBe(0);
214+
expect(version.message).toBe(CLI_VERSION_TEXT);
215+
expect(version.message).toContain(pkg.version);
216+
expect(version.message).toBe(`corbits v${pkg.version}`);
217+
}
218+
}
219+
203220
describe("loadConfig", () => {
204221
test("resolves provider from the global settings file", async () => {
205222
const cwd = await emptyCwd();
@@ -951,6 +968,54 @@ describe("loadConfig", () => {
951968
},
952969
);
953970

971+
test("CLI_HELP_TEXT documents --version and -V", () => {
972+
expect(CLI_HELP_TEXT).toContain("--version, -V");
973+
});
974+
975+
test.each([
976+
[["--version"]],
977+
[["-V"]],
978+
[["--auto", "--version"]],
979+
[["--auto", "-V"]],
980+
[["ship it", "--version"]],
981+
[["ship", "it", "--version"]],
982+
[["--cwd", ".", "--version"]],
983+
[["--provider", "fireworks", "--version"]],
984+
[["exec", "--version"]],
985+
[["exec", "--director", "--version"]],
986+
[["resume", "--pick", "--version"]],
987+
[["resume", "-V"]],
988+
[["resume", "--version"]],
989+
[["continue", "-V"]],
990+
// Value flags must not swallow --version / -V as their value.
991+
[["--provider", "--version"]],
992+
[["--provider", "-V"]],
993+
[["--model", "--version"]],
994+
[["--model", "-V"]],
995+
[["--cwd", "--version"]],
996+
[["--cwd", "-V"]],
997+
[["--config", "--version"]],
998+
[["--config", "-V"]],
999+
[["--profile", "--version"]],
1000+
[["--profile", "-V"]],
1001+
])(
1002+
"%j throws CliVersionError with exitCode 0 and package version",
1003+
async (argv) => {
1004+
await expectCliVersion(argv);
1005+
},
1006+
);
1007+
1008+
test.each([
1009+
[["--version", "--help"]],
1010+
[["-V", "-h"]],
1011+
[["--help", "--version"]],
1012+
])(
1013+
"%j prefers CliHelpError when help and version are both present",
1014+
async (argv) => {
1015+
await expectCliHelp(argv);
1016+
},
1017+
);
1018+
9541019
test.each([
9551020
// Other flag-shaped tokens are not accepted as values.
9561021
[["--provider", "--auto"], "--provider requires a value"],

‎src/config/index.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../session/index.js";
1010
import { loadState } from "../session/state.js";
1111
import { COMMAND_NAME } from "../branding.js";
12+
import pkg from "../../package.json" with { type: "json" };
1213

1314
import { isDirectorId } from "../agent/directors/registry.js";
1415
import { DIRECTOR_IDS, type DirectorId } from "../agent/directors/types.js";
@@ -736,6 +737,7 @@ Flags:
736737
--config selects another source
737738
--auto / --no-auto auto mode on/off
738739
--help, -h show this help
740+
--version, -V show version
739741
`;
740742

741743
/**
@@ -751,6 +753,24 @@ export class CliHelpError extends Error {
751753
}
752754
}
753755

756+
/** Printed for `corbits --version` / `-V`. Matches `package.json` version. */
757+
export const CLI_VERSION_TEXT = `${COMMAND_NAME} v${
758+
typeof pkg.version === "string" ? pkg.version : "0.0.0"
759+
}`;
760+
761+
/**
762+
* Thrown when the operator asked for the CLI version. Entry points must print
763+
* `message` to stdout and exit 0 — not treat this as a crash.
764+
*/
765+
export class CliVersionError extends Error {
766+
readonly exitCode = 0 as const;
767+
768+
constructor(text: string = CLI_VERSION_TEXT) {
769+
super(text);
770+
this.name = "CliVersionError";
771+
}
772+
}
773+
754774
/**
755775
* Thrown for a recoverable operator mistake. Entry points must print
756776
* `message` to stderr and exit 1 — not dump a stack.
@@ -781,7 +801,7 @@ export interface LoadConfigOptions {
781801
}
782802

783803
function isFlagToken(arg: string): boolean {
784-
return arg.startsWith("--") || arg === "-h" || arg === "-p";
804+
return arg.startsWith("--") || arg === "-h" || arg === "-p" || arg === "-V";
785805
}
786806

787807
export async function loadConfig(
@@ -801,6 +821,9 @@ export async function loadConfig(
801821
if (argv.some((arg) => arg === "--help" || arg === "-h")) {
802822
throw new CliHelpError();
803823
}
824+
if (argv.some((arg) => arg === "--version" || arg === "-V")) {
825+
throw new CliVersionError();
826+
}
804827

805828
const args = [...argv];
806829

‎src/index.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import type { Config } from "./config/index.js";
6-
import { CliHelpError, CliUserError } from "./config/index.js";
6+
import { CliHelpError, CliUserError, CliVersionError } from "./config/index.js";
77
import {
88
resetPricingMetadataRefreshForTests,
99
schedulePricingMetadataRefresh,
@@ -171,6 +171,14 @@ test("CliHelpError prints help to stdout and exits 0", () => {
171171
expect(exit.text).toBe("usage: corbits\n");
172172
});
173173

174+
test("CliVersionError prints version to stdout and exits 0", () => {
175+
const err = new CliVersionError("1.2.3");
176+
const exit = cliCaughtExit(err);
177+
expect(exit.stream).toBe("stdout");
178+
expect(exit.code).toBe(0);
179+
expect(exit.text).toBe("1.2.3\n");
180+
});
181+
174182
test("generic Error still dumps a stack to stderr", () => {
175183
const err = new Error("No session abc for this project");
176184
const exit = cliCaughtExit(err);

‎src/index.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { getLogger } from "@intx/log";
22
import { LOG_NAMESPACE_ROOT } from "./branding.js";
33
import { primeCrashReporting } from "./crash/report.js";
4-
import { loadConfig, CliHelpError, CliUserError } from "./config/index.js";
4+
import {
5+
loadConfig,
6+
CliHelpError,
7+
CliUserError,
8+
CliVersionError,
9+
} from "./config/index.js";
510
import {
611
ensureTelemetrySettings,
712
globalSettingsPath,
@@ -139,7 +144,7 @@ export function cliCaughtExit(err: unknown): {
139144
text: string;
140145
code: number;
141146
} {
142-
if (err instanceof CliHelpError) {
147+
if (err instanceof CliHelpError || err instanceof CliVersionError) {
143148
return { stream: "stdout", text: `${err.message}\n`, code: err.exitCode };
144149
}
145150
if (err instanceof CliUserError) {

0 commit comments

Comments
 (0)