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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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[] = [];
Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>", "Agent name to send as an HTTP header"),
Expand Down
15 changes: 15 additions & 0 deletions src/versionCheck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 24 additions & 9 deletions src/versionCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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" };
}
Expand Down Expand Up @@ -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);
Expand Down
Loading