Skip to content

Commit e1a1e66

Browse files
luanvdwclaude
andcommitted
feat(version): implement --version flag and version subcommand
Closes PRO-174. Adds two surfaces over the same model: - `prisma-cli --version` — universal early-exit utility flag at the root. Prints `prisma-cli <version>` to stdout in human mode; emits the standard success envelope (`command: "version"`, `result: {version}`) when combined with `--json`. Requires no auth, no project context, no network. - `prisma-cli version` — top-level subcommand (peer to auth / branch / project / app) that uses the `show` output pattern. Reports the full environment block: CLI name + version, Node version, OS platform/arch, best-effort invocation label (bunx | npx | global | dev | unknown). Implementation: - New `lib/version.ts` reads the bundled package metadata via `createRequire(import.meta.url)` so it works under both tsx dev mode and the tsdown-unbundled `dist/` output. Throws `VERSION_UNAVAILABLE` (defensive; not expected) if metadata cannot be read. - New `controllers/version.ts`, `presenters/version.ts`, `commands/version/` follow the same controller / presenter / command shape as `auth`. - New `types/version.ts` defines `VersionResult` and `VersionInvocation`. - `cli.ts` intercepts `--version` in argv before commander dispatches (mirrors the existing bare-help short-circuit). The bare-help resolver now skips leaf commands so `prisma-cli version` runs its action instead of being treated as a group-help command. - `shell/command-meta.ts` registers the `version` descriptor. - `shell/global-flags.ts` adds `--version` to `COMPACT_GLOBAL_OPTION_FLAGS` so help rendering still surfaces the "Global options:" header at the program root. Tests: - `tests/version.test.ts` — 8 cases covering the flag, the subcommand, both with `--json`, no-auth/no-project/no-network preconditions, help inclusion, and `--version` overriding subcommand parsing when both appear in argv. All 178 existing + new tests pass. Build clean. No regressions on auth, project, branch, or app surfaces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 93d7fb4 commit e1a1e66

9 files changed

Lines changed: 432 additions & 3 deletions

File tree

packages/cli/src/cli.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import process from "node:process";
22

3-
import { Command, CommanderError } from "commander";
3+
import { Command, CommanderError, Option } from "commander";
44

55
import { createAppCommand } from "./commands/app";
66
import { createAuthCommand } from "./commands/auth";
77
import { createBranchCommand } from "./commands/branch";
88

99
import { createProjectCommand } from "./commands/project";
10+
import { createVersionCommand } from "./commands/version";
11+
import { runVersion } from "./controllers/version";
12+
import { getCliName, getCliVersion } from "./lib/version";
1013
import { attachCommandDescriptor } from "./shell/command-meta";
14+
import { CliError } from "./shell/errors";
1115
import { addCompactGlobalFlags } from "./shell/global-flags";
16+
import { writeHumanError, writeJsonError, writeJsonSuccess } from "./shell/output";
1217
import { disposePromptState } from "./shell/prompt";
13-
import { configureRuntimeCommand, type CliRuntime } from "./shell/runtime";
18+
import { configureRuntimeCommand, createCommandContext, type CliRuntime } from "./shell/runtime";
19+
import { createShellUi } from "./shell/ui";
1420

1521
export interface RunCliOptions extends Partial<CliRuntime> {
1622
argv?: string[];
@@ -22,6 +28,10 @@ export async function runCli(options: RunCliOptions = {}): Promise<number> {
2228
process.exitCode = 0;
2329

2430
try {
31+
if (runtime.argv.includes("--version")) {
32+
return await handleVersionFlag(runtime);
33+
}
34+
2535
const bareHelpCommand = resolveBareHelpCommand(program, runtime.argv);
2636

2737
if (bareHelpCommand) {
@@ -49,10 +59,13 @@ export function createProgram(runtime: CliRuntime): Command {
4959

5060
addCompactGlobalFlags(program);
5161

62+
program.addOption(new Option("--version", "Print the CLI version and exit."));
63+
5264
program
5365
.name("prisma")
5466
.showSuggestionAfterError();
5567

68+
program.addCommand(createVersionCommand(runtime));
5669
program.addCommand(createAuthCommand(runtime));
5770
program.addCommand(createBranchCommand(runtime));
5871
program.addCommand(createProjectCommand(runtime));
@@ -61,6 +74,54 @@ export function createProgram(runtime: CliRuntime): Command {
6174
return program;
6275
}
6376

77+
async function handleVersionFlag(runtime: CliRuntime): Promise<number> {
78+
const wantsJson = runtime.argv.includes("--json");
79+
const output = { stdout: runtime.stdout, stderr: runtime.stderr };
80+
81+
try {
82+
if (wantsJson) {
83+
const context = await createCommandContext(runtime, buildVersionFlagFlags(runtime));
84+
const success = await runVersion(context);
85+
writeJsonSuccess(output, {
86+
command: success.command,
87+
result: { version: success.result.cli.version },
88+
warnings: success.warnings,
89+
nextSteps: success.nextSteps,
90+
});
91+
return 0;
92+
}
93+
94+
const versionLine = `${getCliName()} ${getCliVersion()}`;
95+
runtime.stdout.write(`${versionLine}\n`);
96+
return 0;
97+
} catch (error) {
98+
if (error instanceof CliError) {
99+
if (wantsJson) {
100+
writeJsonError(output, "version", error);
101+
} else {
102+
const ui = createShellUi(runtime, buildVersionFlagFlags(runtime));
103+
writeHumanError(output, ui, error, { trace: false });
104+
}
105+
106+
return error.exitCode;
107+
}
108+
109+
throw error;
110+
}
111+
}
112+
113+
function buildVersionFlagFlags(runtime: CliRuntime) {
114+
return {
115+
json: runtime.argv.includes("--json"),
116+
quiet: false,
117+
verbose: false,
118+
trace: false,
119+
yes: false,
120+
interactive: undefined,
121+
color: undefined,
122+
};
123+
}
124+
64125
function resolveBareHelpCommand(program: Command, argv: string[]): Command | null {
65126
if (argv.length === 0) {
66127
return program;
@@ -70,7 +131,19 @@ function resolveBareHelpCommand(program: Command, argv: string[]): Command | nul
70131
return null;
71132
}
72133

73-
return program.commands.find((command) => command.name() === argv[0]) ?? null;
134+
const candidate = program.commands.find((command) => command.name() === argv[0]) ?? null;
135+
136+
if (!candidate) {
137+
return null;
138+
}
139+
140+
// Group commands (with subcommands) print help when invoked bare.
141+
// Leaf commands (no subcommands) own their own action and must execute.
142+
if (candidate.commands.length === 0) {
143+
return null;
144+
}
145+
146+
return candidate;
74147
}
75148

76149
function resolveRuntime(options: RunCliOptions): CliRuntime {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Command } from "commander";
2+
3+
import { runVersion } from "../../controllers/version";
4+
import { renderVersionSuccess } from "../../presenters/version";
5+
import { attachCommandDescriptor } from "../../shell/command-meta";
6+
import { runCommand } from "../../shell/command-runner";
7+
import { addGlobalFlags } from "../../shell/global-flags";
8+
import { configureRuntimeCommand, type CliRuntime } from "../../shell/runtime";
9+
import type { VersionResult } from "../../types/version";
10+
11+
export function createVersionCommand(runtime: CliRuntime): Command {
12+
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("version"), runtime), "version");
13+
14+
addGlobalFlags(command);
15+
16+
command.action(async (options) => {
17+
await runCommand<VersionResult>(
18+
runtime,
19+
"version",
20+
options as Record<string, unknown>,
21+
(context) => runVersion(context),
22+
{
23+
renderHuman: (context, descriptor, result) => renderVersionSuccess(context, descriptor, result),
24+
},
25+
);
26+
});
27+
28+
return command;
29+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { buildVersionResult } from "../lib/version";
2+
import type { CommandSuccess } from "../shell/output";
3+
import type { CommandContext } from "../shell/runtime";
4+
import type { VersionResult } from "../types/version";
5+
6+
export async function runVersion(context: CommandContext): Promise<CommandSuccess<VersionResult>> {
7+
const result = buildVersionResult(context.runtime.env, context.runtime.argv);
8+
9+
return {
10+
command: "version",
11+
result,
12+
warnings: [],
13+
nextSteps: [],
14+
};
15+
}

packages/cli/src/lib/version.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { createRequire } from "node:module";
2+
import process from "node:process";
3+
4+
import { CliError } from "../shell/errors";
5+
import type { VersionInvocation, VersionResult } from "../types/version";
6+
7+
interface PackageMetadata {
8+
name?: string;
9+
version?: string;
10+
}
11+
12+
const requireFromHere = createRequire(import.meta.url);
13+
14+
function readPackageMetadata(): PackageMetadata {
15+
try {
16+
return requireFromHere("../../package.json") as PackageMetadata;
17+
} catch {
18+
return {};
19+
}
20+
}
21+
22+
export function getCliVersion(): string {
23+
const pkg = readPackageMetadata();
24+
25+
if (!pkg.version) {
26+
throw new CliError({
27+
code: "VERSION_UNAVAILABLE",
28+
domain: "cli",
29+
summary: "CLI version metadata is missing from the installed package",
30+
why: "The bundled package.json could not be read or did not contain a version field.",
31+
fix: "Reinstall the CLI from the npm registry, or check your install path is intact.",
32+
exitCode: 1,
33+
});
34+
}
35+
36+
return pkg.version;
37+
}
38+
39+
// Published bin name is the agreed user-facing identifier for the preview.
40+
// We hard-code "prisma-cli" because the bin name and the npm package name differ:
41+
// the npm package is "@prisma/cli", but the binary on PATH is "prisma-cli".
42+
export function getCliName(): string {
43+
return "prisma-cli";
44+
}
45+
46+
export function detectInvocation(env: NodeJS.ProcessEnv, argv: readonly string[]): VersionInvocation {
47+
if (env.npm_config_user_agent?.startsWith("bun")) {
48+
return "bunx";
49+
}
50+
51+
if (env.npm_lifecycle_event === "npx" || env.npm_execpath?.includes("/_npx/") || env.npm_config_user_agent?.startsWith("npm")) {
52+
return "npx";
53+
}
54+
55+
const entry = argv[1] ?? "";
56+
57+
if (entry.endsWith(".ts") || entry.includes("/tsx/")) {
58+
return "dev";
59+
}
60+
61+
if (entry.includes("/_npx/")) {
62+
return "npx";
63+
}
64+
65+
if (entry.includes("/.bun/")) {
66+
return "bunx";
67+
}
68+
69+
if (entry.includes("/node_modules/.bin/") || entry.endsWith("/prisma-cli")) {
70+
return "global";
71+
}
72+
73+
return "unknown";
74+
}
75+
76+
export function buildVersionResult(env: NodeJS.ProcessEnv, argv: readonly string[]): VersionResult {
77+
return {
78+
cli: {
79+
name: getCliName(),
80+
version: getCliVersion(),
81+
},
82+
node: {
83+
version: process.version,
84+
},
85+
os: {
86+
platform: process.platform,
87+
arch: process.arch,
88+
},
89+
invocation: detectInvocation(env, argv),
90+
};
91+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { renderShow } from "../output/patterns";
2+
import type { CommandDescriptor } from "../shell/command-meta";
3+
import type { CommandContext } from "../shell/runtime";
4+
import type { VersionResult } from "../types/version";
5+
6+
export function renderVersionSuccess(
7+
context: CommandContext,
8+
descriptor: CommandDescriptor,
9+
result: VersionResult,
10+
): string[] {
11+
return renderShow(
12+
{
13+
title: "Showing CLI build and environment.",
14+
descriptor,
15+
fields: [
16+
{ key: result.cli.name, value: result.cli.version },
17+
{ key: "node", value: result.node.version },
18+
{ key: "os", value: `${result.os.platform} ${result.os.arch}` },
19+
{ key: "invocation", value: result.invocation, tone: result.invocation === "unknown" ? "dim" : "default" },
20+
],
21+
},
22+
context.ui,
23+
);
24+
}

packages/cli/src/shell/command-meta.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const DESCRIPTORS: CommandDescriptor[] = [
2020
longDescription:
2121
"Deploy your app with isolated infrastructure for every branch",
2222
},
23+
{
24+
id: "version",
25+
path: ["prisma", "version"],
26+
description: "Show CLI build and environment",
27+
examples: ["prisma-cli version", "prisma-cli version --json"],
28+
},
2329
{
2430
id: "auth",
2531
path: ["prisma", "auth"],

packages/cli/src/shell/global-flags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const COMPACT_GLOBAL_OPTION_FLAGS = [
1717
"--trace",
1818
"--no-interactive",
1919
"-y, --yes",
20+
"--version",
2021
];
2122

2223
export function addGlobalFlags(command: Command): Command {

packages/cli/src/types/version.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface VersionResult {
2+
cli: {
3+
name: string;
4+
version: string;
5+
};
6+
node: {
7+
version: string;
8+
};
9+
os: {
10+
platform: string;
11+
arch: string;
12+
};
13+
invocation: VersionInvocation;
14+
}
15+
16+
export type VersionInvocation = "bunx" | "npx" | "global" | "dev" | "unknown";
17+
18+
export interface VersionFlagResult {
19+
version: string;
20+
}

0 commit comments

Comments
 (0)