Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +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 `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.

Expand Down
2 changes: 1 addition & 1 deletion src/auth/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
65 changes: 65 additions & 0 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
catalogEntryAsProviderSettings,
CliHelpError,
CliUserError,
CliVersionError,
CLI_HELP_TEXT,
CLI_VERSION_TEXT,
KEYLESS_API_KEY,
loadConfig,
providerCatalogToSettings,
Expand All @@ -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,
Expand Down Expand Up @@ -200,6 +203,20 @@ async function expectCliHelp(argv: readonly string[]): Promise<void> {
}
}

async function expectCliVersion(argv: readonly string[]): Promise<void> {
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).toContain(pkg.version);
expect(version.message).toBe(`corbits v${pkg.version}`);
}
}

describe("loadConfig", () => {
test("resolves provider from the global settings file", async () => {
const cwd = await emptyCwd();
Expand Down Expand Up @@ -951,6 +968,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"],
Expand Down
25 changes: 24 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -736,6 +737,7 @@ Flags:
--config selects another source
--auto / --no-auto auto mode on/off
--help, -h show this help
--version, -V show version
`;

/**
Expand All @@ -751,6 +753,24 @@ export class CliHelpError extends Error {
}
}

/** Printed for `corbits --version` / `-V`. Matches `package.json` version. */
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
* `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.
Expand Down Expand Up @@ -781,7 +801,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(
Expand All @@ -801,6 +821,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];

Expand Down
10 changes: 9 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 7 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Loading