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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@decodo/cli",
"version": "0.1.2",
"version": "0.1.3",
"description": "Official CLI for the Decodo APIs",
"license": "MIT",
"type": "module",
Expand Down
18 changes: 18 additions & 0 deletions src/cli/services/configure-commander-exit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Command, CommanderError } from "commander";
import { EXIT } from "../../platform/constants.js";

function applyCommanderExit(
command: Command,
handler: (err: CommanderError) => never
): void {
command.exitOverride(handler);
for (const subcommand of command.commands) {
applyCommanderExit(subcommand, handler);
}
}

export function configureCommanderExit(program: Command): void {
applyCommanderExit(program, (err: CommanderError) => {
process.exit(err.exitCode === 0 ? EXIT.OK : EXIT.USAGE);
});
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Command } from "commander";
import { createCommands } from "./cli/register.js";
import { configureCommanderExit } from "./cli/services/configure-commander-exit.js";
import { handleCliError } from "./platform/services/handle-cli-error.js";

function readVersion(): string {
Expand Down Expand Up @@ -31,6 +32,8 @@ async function main(): Promise<void> {
program.addCommand(command);
}

configureCommanderExit(program);

await program.parseAsync(process.argv);
}

Expand Down
57 changes: 57 additions & 0 deletions tests/cli/services/configure-commander-exit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { configureCommanderExit } from "../../../src/cli/services/configure-commander-exit.js";

describe("configureCommanderExit", () => {
let exitCode: number | undefined;

beforeEach(() => {
exitCode = undefined;

vi.spyOn(process, "exit").mockImplementation((code) => {
exitCode = code as number;
throw new Error(`process.exit:${code}`);
});
});

afterEach(() => {
vi.restoreAllMocks();
});

it("maps parse errors to exit code 2", async () => {
const program = new Command().option("--known", "a known flag");
configureCommanderExit(program);

await expect(
program.parseAsync(["--unknown"], { from: "user" })
).rejects.toThrow("process.exit:2");

expect(exitCode).toBe(2);
});

it("maps help to exit code 0", async () => {
const program = new Command()
.name("test-cli")
.option("--known", "a known flag");
configureCommanderExit(program);

await expect(
program.parseAsync(["--help"], { from: "user" })
).rejects.toThrow("process.exit:0");

expect(exitCode).toBe(0);
});

it("maps subcommand parse errors to exit code 2", async () => {
const program = new Command().addCommand(
new Command("search").argument("<query>", "search query")
);
configureCommanderExit(program);

await expect(
program.parseAsync(["search"], { from: "user" })
).rejects.toThrow("process.exit:2");

expect(exitCode).toBe(2);
});
});
48 changes: 38 additions & 10 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,56 @@ import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const rootDir = dirname(fileURLToPath(import.meta.url));
const cliPath = join(rootDir, "..", "build", "esm", "index.js");
const packageJson = JSON.parse(
readFileSync(join(rootDir, "..", "package.json"), "utf8")
) as { version: string };

function runCli(args: string[]): { exitCode: number; stderr: string } {
try {
execFileSync(process.execPath, [cliPath, ...args], { encoding: "utf8" });
return { exitCode: 0, stderr: "" };
} catch (err) {
const execErr = err as { status?: number; stderr?: string };
return {
exitCode: execErr.status ?? 1,
stderr: execErr.stderr ?? "",
};
}
}

describe("cli", () => {
it("prints the package version with --version", () => {
const output = execFileSync(
process.execPath,
[join(rootDir, "..", "build", "esm", "index.js"), "--version"],
{ encoding: "utf8" }
).trim();
const output = execFileSync(process.execPath, [cliPath, "--version"], {
encoding: "utf8",
}).trim();

expect(output).toBe(packageJson.version);
});

it("shows verbose flag in root help", () => {
const output = execFileSync(
process.execPath,
[join(rootDir, "..", "build", "esm", "index.js"), "--help"],
{ encoding: "utf8" }
);
const output = execFileSync(process.execPath, [cliPath, "--help"], {
encoding: "utf8",
});

expect(output).toContain("-v, --verbose");
});

it.each([
["unknown flag", ["--bad-flag"], 2],
["unknown command", ["nosuchcmd"], 2],
["missing required arg", ["search"], 2],
["invalid choice", ["search", "q", "--engine", "yahoo"], 2],
])("exits with code 2 on %s", (_label, args, expectedExit) => {
const { exitCode } = runCli(args);
expect(exitCode).toBe(expectedExit);
});

it.each([
["--version", 0],
["--help", 0],
])("exits with code 0 for %s", (flag, expectedExit) => {
const { exitCode } = runCli([flag]);
expect(exitCode).toBe(expectedExit);
});
});
Loading