Skip to content

Commit a00e8a7

Browse files
Merge pull request #16 from prisma/feat/pro-174-version-command
feat(commands): added --version flag + version subcommand
2 parents abc6b26 + 70fc1f2 commit a00e8a7

12 files changed

Lines changed: 545 additions & 3 deletions

File tree

docs/product/command-spec.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ The preview package includes these command groups:
1515
- `branch`
1616
- `app`
1717

18+
The preview package also includes one top-level utility command:
19+
20+
- `version`
21+
22+
`version` is intentionally outside the workflow groups: it reports CLI build and environment state, requires no auth, no project context, and no network, and is the canonical answer to "is this CLI installed and on the build I expect?"
23+
1824
The Git repository connection slice uses the `git` group. It does not add a
1925
provider-specific `GitHub` group.
2026

@@ -29,6 +35,7 @@ Out of scope for the current preview:
2935
## Global Rules
3036

3137
- Canonical shape is `prisma <group> <action>`.
38+
- `version` is the one top-level command outside that shape (see Scope above).
3239
- Every command supports `--json`.
3340
- Shared global flags are:
3441
- `--json`
@@ -40,6 +47,9 @@ Out of scope for the current preview:
4047
- `-y`, `--yes`
4148
- `--color`
4249
- `--no-color`
50+
- Universal utility flags also work at the program level:
51+
- `--help` — prints help for the root program or the named command and exits 0.
52+
- `--version` — prints the CLI version and exits 0. Honors `--json` for the structured envelope. No short alias (`-v` is reserved for `--verbose`; `-V` is avoided as a near-collision).
4353
- Long flags use kebab-case.
4454
- Boolean negation uses `--no-<flag>`.
4555
- `--json` and non-interactive mode must not block on prompts.
@@ -139,6 +149,78 @@ Rules:
139149
- `workspace` is the active workspace or `null`
140150
- signed-out state is an empty auth state, not an error
141151

152+
## `prisma-cli version`
153+
154+
Purpose:
155+
156+
- report the installed CLI build and a small block of host environment metadata
157+
158+
Behavior:
159+
160+
- requires no auth, no project context, and no network
161+
- reads the package's own version from its bundled metadata
162+
- reports CLI name, CLI version, Node.js version, OS platform, OS architecture, and a best-effort `invocation` label (`bunx`, `npx`, `global`, `dev`, or `unknown`)
163+
- uses the `show` output pattern (see `output-conventions.md`)
164+
- fails only when the bundled CLI metadata cannot be read; this is treated as `VERSION_UNAVAILABLE` and is not expected in practice
165+
166+
In `--json`, `result` uses this shape:
167+
168+
```json
169+
{
170+
"cli": {
171+
"name": "prisma-cli",
172+
"version": "3.0.0-alpha.3"
173+
},
174+
"node": {
175+
"version": "v24.14.1"
176+
},
177+
"os": {
178+
"platform": "darwin",
179+
"arch": "arm64"
180+
},
181+
"invocation": "bunx"
182+
}
183+
```
184+
185+
Rules:
186+
187+
- `cli.name` is the published package's `bin` name (`prisma-cli` in the current preview).
188+
- `cli.version` is the published package version.
189+
- `node.version` mirrors `process.version` exactly, including the leading `v`.
190+
- `os.platform` and `os.arch` mirror `process.platform` and `process.arch`.
191+
- `invocation` is best-effort and falls back to `"unknown"` when no signal is conclusive.
192+
193+
Examples:
194+
195+
```bash
196+
prisma-cli version
197+
prisma-cli version --json
198+
```
199+
200+
## `prisma-cli --version`
201+
202+
Purpose:
203+
204+
- universal smoke-test flag at the root of the program
205+
206+
Behavior:
207+
208+
- prints the CLI version and exits 0
209+
- requires no auth, no project context, and no network
210+
- works before any subcommand parsing — bare `prisma-cli --version` is sufficient
211+
- in human mode, prints a single line to stdout: `prisma-cli <version>`
212+
- in `--json` mode, emits the standard success envelope (see Command Result Envelopes) with `command: "version"` and `result.version: "<version>"`
213+
- `--version` is documented as a universal utility flag in Global Rules, not as a shared global flag (it is an early-exit utility, not a per-command modifier)
214+
215+
Examples:
216+
217+
```bash
218+
prisma-cli --version
219+
prisma-cli --version --json
220+
```
221+
222+
`prisma-cli version` is the richer environment report; `prisma-cli --version` is the terse one-liner. Both report the same `cli.version`. Use the flag for quick checks, the subcommand for support tickets and bug reports.
223+
142224
## `prisma-cli auth login`
143225

144226
Purpose:

docs/product/error-conventions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ These codes are the minimum stable set for the MVP:
169169
- `BUILD_FAILED`
170170
- `RUN_FAILED`
171171
- `DEPLOY_FAILED`
172+
- `VERSION_UNAVAILABLE`
172173

173174
Recommended meanings:
174175

@@ -196,6 +197,7 @@ Recommended meanings:
196197
- `BUILD_FAILED`: build failed before a healthy deployment existed
197198
- `RUN_FAILED`: local framework run command could not be started or exited unsuccessfully
198199
- `DEPLOY_FAILED`: deployment or post-build health failed
200+
- `VERSION_UNAVAILABLE`: CLI could not read its own bundled package metadata to report a version (defensive; not expected in normal installs)
199201

200202
## Exit Codes
201203

docs/product/output-conventions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Current MVP commands map to patterns like this:
6262

6363
| Command | Pattern |
6464
| --- | --- |
65+
| `version` | `show` |
6566
| `auth login` | `mutate` |
6667
| `auth logout` | `mutate` |
6768
| `auth whoami` | `show` |

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
import { createGitCommand } from "./commands/git";
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(createProjectCommand(runtime));
5871
program.addCommand(createGitCommand(runtime));
@@ -62,6 +75,54 @@ export function createProgram(runtime: CliRuntime): Command {
6275
return program;
6376
}
6477

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

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

77150
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+
}

0 commit comments

Comments
 (0)