diff --git a/CHANGELOG.md b/CHANGELOG.md index 066f07d..6babe5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- `dx -v` now prints the CLI version, matching the documented lowercase version flag. Version checks are also skipped for help/version output and global-option-only invocations, preventing update prompts from interfering with those commands. + ## 0.5.5 - 2026-07-13 ### Updated diff --git a/src/cli.test.ts b/src/cli.test.ts index 8433d11..ae564d8 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,5 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import cliPackage from "../package.json" with { type: "json" }; + const originalEnv = { ...process.env }; beforeEach(() => { @@ -42,10 +44,47 @@ describe("cli", () => { const output = stdout.join(""); expect(output).toContain("Usage: dx [options] [command]"); + expect(output).toContain("-v, --version"); + expect(output).not.toContain("-V, --version"); expect(stderr.join("")).toBe(""); expect(exitSpy).not.toHaveBeenCalled(); }); + it("shows the current version with the lowercase version flag", async () => { + const stdout: string[] = []; + const stderr: string[] = []; + + vi.spyOn(process.stdout, "write").mockImplementation((( + chunk: string | Uint8Array, + ) => { + stdout.push(String(chunk)); + return true; + }) as typeof process.stdout.write); + + vi.spyOn(process.stderr, "write").mockImplementation((( + chunk: string | Uint8Array, + ) => { + stderr.push(String(chunk)); + return true; + }) as typeof process.stderr.write); + + const exitSpy = vi.spyOn(process, "exit").mockImplementation((( + code?: string | number | null, + ) => { + throw new Error(`process.exit unexpectedly called with ${code}`); + }) as typeof process.exit); + + const { run } = await import("./cli.js"); + + await expect(run(["node", "dx", "-v"])).rejects.toThrow( + "process.exit unexpectedly called with 0", + ); + + expect(stdout.join("")).toBe(`${cliPackage.version}\n`); + expect(stderr.join("")).toBe(""); + expect(exitSpy).toHaveBeenCalledWith(0); + }); + it("shows command usage when a command group is invoked without a subcommand", async () => { const stdout: string[] = []; const stderr: string[] = []; diff --git a/src/cli.ts b/src/cli.ts index b3abdb7..dc611dc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -55,7 +55,7 @@ function createProgram(): Command { program .name("dx") .description("DX CLI") - .version(cliPackage.version) + .version(cliPackage.version, "-v, --version") .addOption(new Option("--json", "Print machine-readable JSON")) .addOption( new Option("--agent ", "Agent name to send as an HTTP header"), diff --git a/src/versionCheck.test.ts b/src/versionCheck.test.ts index 39b785e..a1e7327 100644 --- a/src/versionCheck.test.ts +++ b/src/versionCheck.test.ts @@ -192,6 +192,18 @@ describe("checkForNewVersion", () => { expect(mockBuildRuntimeSafe).not.toHaveBeenCalled(); }); + it.each([ + ["short version flag", ["node", "dx", "-v"]], + ["long version flag", ["node", "dx", "--version"]], + ["top-level help flag", ["node", "dx", "--help"]], + ["subcommand help flag", ["node", "dx", "scorecards", "--help"]], + ["global option without a command", ["node", "dx", "--json"]], + ])("returns 'disabled' for %s", async (_label, argv) => { + const result = await checkForNewVersion(argv); + expect(result).toEqual({ status: "disabled" }); + expect(mockBuildRuntimeSafe).not.toHaveBeenCalled(); + }); + it("returns 'disabled' when user is not logged in", async () => { mockBuildRuntimeSafe.mockResolvedValue(null); const result = await checkForNewVersion(scorecardsList); @@ -279,6 +291,9 @@ describe("promptVersionUpdate", () => { const result = await promptVersionUpdate("0.4.0"); expect(result).toEqual({ shouldUpdate: true, latestVersion: "0.4.0" }); + expect(mockSelect).toHaveBeenCalledWith(expect.any(Object), { + output: process.stderr, + }); Object.defineProperty(process.stdin, "isTTY", { value: undefined, diff --git a/src/versionCheck.ts b/src/versionCheck.ts index 5e89f96..b158d38 100644 --- a/src/versionCheck.ts +++ b/src/versionCheck.ts @@ -20,6 +20,7 @@ const VERSION_CHECK_INTERVAL_HOURS = 24; const SNOOZE_DAYS = 7; const SKIP_COMMANDS = new Set(["auth", "init"]); +const SKIP_OPTIONS = new Set(["-h", "--help", "-v", "--version"]); export interface VersionCheckResult { shouldUpdate: boolean; @@ -111,7 +112,16 @@ export async function checkForNewVersion( return { status: "disabled" }; } - const topLevelCommand = argv.slice(2).find((arg) => !arg.startsWith("-")); + const args = argv.slice(2); + if (args.some((arg) => SKIP_OPTIONS.has(arg))) { + return { status: "disabled" }; + } + + const topLevelCommand = args.find((arg) => !arg.startsWith("-")); + if (!topLevelCommand) { + return { status: "disabled" }; + } + if (topLevelCommand && SKIP_COMMANDS.has(topLevelCommand)) { return { status: "disabled" }; } @@ -198,14 +208,19 @@ export async function promptVersionUpdate( ], { useStderr: true }, ); - const choice = await select({ - message: "What would you like to do?", - choices: [ - { name: "Update now", value: "update" }, - { name: `Remind me later (in ${SNOOZE_DAYS} days)`, value: "snooze" }, - { name: "Skip this version", value: "skip" }, - ], - }); + const choice = await select( + { + message: "What would you like to do?", + choices: [ + { name: "Update now", value: "update" }, + { name: `Remind me later (in ${SNOOZE_DAYS} days)`, value: "snooze" }, + { name: "Skip this version", value: "skip" }, + ], + }, + { + output: process.stderr, + }, + ); if (choice === "update") { persistVersionPromptSelection(undefined);