From e1f667ab1088ce5c96babb437bdf481f350984e4 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Tue, 29 Sep 2026 16:14:09 -0700 Subject: [PATCH 1/3] feat(cli): add --version and -V flags --- docs/IMPLEMENTATION.md | 2 ++ src/config.test.ts | 64 ++++++++++++++++++++++++++++++++++++++++++ src/config/index.ts | 24 +++++++++++++++- src/index.test.ts | 10 ++++++- src/index.ts | 9 ++++-- 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index a978f4a99..d6aca13b1 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -404,6 +404,7 @@ This release changes no settings, OAuth-store, or session persistence format and Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts` (that constant is the source of truth; keep this table in sync when flags change). +`--version` / `-V` print `CLI_VERSION_TEXT` (the `package.json` version) and exit 0. | Verb / Flag | Default | Description | | ------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -424,6 +425,7 @@ Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts` | `--auto` | true (default) | Force auto mode on (workspace writes + unconstrained shell without prompts) | | `--no-auto` | false | Start with auto mode off (ask on every consequential action); no in-session key toggles it | | `--help`, `-h` | — | Show help (exit 0 via `CliHelpError`) | +| `--version`, `-V` | — | Print the running `package.json` version (exit 0 via `CliVersionError`) | 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. diff --git a/src/config.test.ts b/src/config.test.ts index f9eee84a6..36de1bbe4 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -20,7 +20,9 @@ import { catalogEntryAsProviderSettings, CliHelpError, CliUserError, + CliVersionError, CLI_HELP_TEXT, + CLI_VERSION_TEXT, KEYLESS_API_KEY, loadConfig, providerCatalogToSettings, @@ -30,6 +32,7 @@ import { SOURCE_MAX_TOKENS, } from "./config/index.js"; import { DIRECTOR_IDS } from "./agent/directors/types.js"; +import pkg from "../package.json" with { type: "json" }; import { clearSourceCredentials, peekSourceCredentialSecret, @@ -200,6 +203,19 @@ async function expectCliHelp(argv: readonly string[]): Promise { } } +async function expectCliVersion(argv: readonly string[]): Promise { + try { + await loadConfig([...argv], { globalSettingsPath: NO_SETTINGS }); + expect.unreachable("expected CliVersionError"); + } catch (err) { + expect(err).toBeInstanceOf(CliVersionError); + const version = err as CliVersionError; + expect(version.exitCode).toBe(0); + expect(version.message).toBe(CLI_VERSION_TEXT); + expect(version.message).toBe(pkg.version); + } +} + describe("loadConfig", () => { test("resolves provider from the global settings file", async () => { const cwd = await emptyCwd(); @@ -951,6 +967,54 @@ describe("loadConfig", () => { }, ); + test("CLI_HELP_TEXT documents --version and -V", () => { + expect(CLI_HELP_TEXT).toContain("--version, -V"); + }); + + test.each([ + [["--version"]], + [["-V"]], + [["--auto", "--version"]], + [["--auto", "-V"]], + [["ship it", "--version"]], + [["ship", "it", "--version"]], + [["--cwd", ".", "--version"]], + [["--provider", "fireworks", "--version"]], + [["exec", "--version"]], + [["exec", "--director", "--version"]], + [["resume", "--pick", "--version"]], + [["resume", "-V"]], + [["resume", "--version"]], + [["continue", "-V"]], + // Value flags must not swallow --version / -V as their value. + [["--provider", "--version"]], + [["--provider", "-V"]], + [["--model", "--version"]], + [["--model", "-V"]], + [["--cwd", "--version"]], + [["--cwd", "-V"]], + [["--config", "--version"]], + [["--config", "-V"]], + [["--profile", "--version"]], + [["--profile", "-V"]], + ])( + "%j throws CliVersionError with exitCode 0 and package version", + async (argv) => { + await expectCliVersion(argv); + }, + ); + + test.each([ + [["--version", "--help"]], + [["-V", "-h"]], + [["--help", "--version"]], + ])( + "%j prefers CliHelpError when help and version are both present", + async (argv) => { + await expectCliHelp(argv); + }, + ); + test.each([ // Other flag-shaped tokens are not accepted as values. [["--provider", "--auto"], "--provider requires a value"], diff --git a/src/config/index.ts b/src/config/index.ts index a108ecf29..66d96dfb0 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -9,6 +9,7 @@ import { } from "../session/index.js"; import { loadState } from "../session/state.js"; import { COMMAND_NAME } from "../branding.js"; +import pkg from "../../package.json" with { type: "json" }; import { isDirectorId } from "../agent/directors/registry.js"; import { DIRECTOR_IDS, type DirectorId } from "../agent/directors/types.js"; @@ -736,6 +737,7 @@ Flags: --config selects another source --auto / --no-auto auto mode on/off --help, -h show this help + --version, -V print version `; /** @@ -751,6 +753,23 @@ export class CliHelpError extends Error { } } +/** Printed for `corbits --version` / `-V`. Matches `package.json` version. */ +export const CLI_VERSION_TEXT = + typeof pkg.version === "string" ? pkg.version : "0.0.0"; + +/** + * Thrown when the operator asked for the CLI version. Entry points must print + * `message` to stdout and exit 0 — not treat this as a crash. + */ +export class CliVersionError extends Error { + readonly exitCode = 0 as const; + + constructor(text: string = CLI_VERSION_TEXT) { + super(text); + this.name = "CliVersionError"; + } +} + /** * Thrown for a recoverable operator mistake. Entry points must print * `message` to stderr and exit 1 — not dump a stack. @@ -781,7 +800,7 @@ export interface LoadConfigOptions { } function isFlagToken(arg: string): boolean { - return arg.startsWith("--") || arg === "-h" || arg === "-p"; + return arg.startsWith("--") || arg === "-h" || arg === "-p" || arg === "-V"; } export async function loadConfig( @@ -801,6 +820,9 @@ export async function loadConfig( if (argv.some((arg) => arg === "--help" || arg === "-h")) { throw new CliHelpError(); } + if (argv.some((arg) => arg === "--version" || arg === "-V")) { + throw new CliVersionError(); + } const args = [...argv]; diff --git a/src/index.test.ts b/src/index.test.ts index a66393660..8e1daacbf 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Config } from "./config/index.js"; -import { CliHelpError, CliUserError } from "./config/index.js"; +import { CliHelpError, CliUserError, CliVersionError } from "./config/index.js"; import { resetPricingMetadataRefreshForTests, schedulePricingMetadataRefresh, @@ -171,6 +171,14 @@ test("CliHelpError prints help to stdout and exits 0", () => { expect(exit.text).toBe("usage: corbits\n"); }); +test("CliVersionError prints version to stdout and exits 0", () => { + const err = new CliVersionError("1.2.3"); + const exit = cliCaughtExit(err); + expect(exit.stream).toBe("stdout"); + expect(exit.code).toBe(0); + expect(exit.text).toBe("1.2.3\n"); +}); + test("generic Error still dumps a stack to stderr", () => { const err = new Error("No session abc for this project"); const exit = cliCaughtExit(err); diff --git a/src/index.ts b/src/index.ts index 1e0b8230e..142a75b00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,12 @@ import { getLogger } from "@intx/log"; import { LOG_NAMESPACE_ROOT } from "./branding.js"; import { primeCrashReporting } from "./crash/report.js"; -import { loadConfig, CliHelpError, CliUserError } from "./config/index.js"; +import { + loadConfig, + CliHelpError, + CliUserError, + CliVersionError, +} from "./config/index.js"; import { ensureTelemetrySettings, globalSettingsPath, @@ -139,7 +144,7 @@ export function cliCaughtExit(err: unknown): { text: string; code: number; } { - if (err instanceof CliHelpError) { + if (err instanceof CliHelpError || err instanceof CliVersionError) { return { stream: "stdout", text: `${err.message}\n`, code: err.exitCode }; } if (err instanceof CliUserError) { From b5d1717b3f74b147d3a19f66535065a7811a59b4 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Tue, 29 Sep 2026 16:18:14 -0700 Subject: [PATCH 2/3] 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. --- src/auth/store.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/store.test.ts b/src/auth/store.test.ts index a29f23cf6..ae0d7a9ed 100644 --- a/src/auth/store.test.ts +++ b/src/auth/store.test.ts @@ -11,7 +11,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { type } from "arktype"; -import { withMockedModule } from "../../tests/helpers/mock-module.js"; +import { withMockedModule } from "../../testkit/mock-module.js"; import type { BaseTokens } from "./store.js"; let failingUnlinkPath: string | undefined; From 9c737a993d2bee2d0d67dca71c8ffe1ddb3e4481 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Tue, 29 Sep 2026 16:19:45 -0700 Subject: [PATCH 3/3] feat(cli): name the binary in --version output --- docs/IMPLEMENTATION.md | 3 +-- src/config.test.ts | 3 ++- src/config/index.ts | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index d6aca13b1..62a1bd75b 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -404,7 +404,6 @@ This release changes no settings, OAuth-store, or session persistence format and Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts` (that constant is the source of truth; keep this table in sync when flags change). -`--version` / `-V` print `CLI_VERSION_TEXT` (the `package.json` version) and exit 0. | Verb / Flag | Default | Description | | ------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -425,7 +424,7 @@ Printed by `corbits --help` / `-h` from `CLI_HELP_TEXT` in `src/config/index.ts` | `--auto` | true (default) | Force auto mode on (workspace writes + unconstrained shell without prompts) | | `--no-auto` | false | Start with auto mode off (ask on every consequential action); no in-session key toggles it | | `--help`, `-h` | — | Show help (exit 0 via `CliHelpError`) | -| `--version`, `-V` | — | Print the running `package.json` version (exit 0 via `CliVersionError`) | +| `--version`, `-V` | — | Print `corbits v` plus the running `package.json` version (exit 0 via `CliVersionError`) | 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. diff --git a/src/config.test.ts b/src/config.test.ts index 36de1bbe4..8e7d2e673 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -212,7 +212,8 @@ async function expectCliVersion(argv: readonly string[]): Promise { const version = err as CliVersionError; expect(version.exitCode).toBe(0); expect(version.message).toBe(CLI_VERSION_TEXT); - expect(version.message).toBe(pkg.version); + expect(version.message).toContain(pkg.version); + expect(version.message).toBe(`corbits v${pkg.version}`); } } diff --git a/src/config/index.ts b/src/config/index.ts index 66d96dfb0..cf6cdeb59 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -737,7 +737,7 @@ Flags: --config selects another source --auto / --no-auto auto mode on/off --help, -h show this help - --version, -V print version + --version, -V show version `; /** @@ -754,8 +754,9 @@ export class CliHelpError extends Error { } /** Printed for `corbits --version` / `-V`. Matches `package.json` version. */ -export const CLI_VERSION_TEXT = - typeof pkg.version === "string" ? pkg.version : "0.0.0"; +export const CLI_VERSION_TEXT = `${COMMAND_NAME} v${ + typeof pkg.version === "string" ? pkg.version : "0.0.0" +}`; /** * Thrown when the operator asked for the CLI version. Entry points must print