From a02129978d9068233a93e556c6c0efd69e72763e Mon Sep 17 00:00:00 2001 From: Ermin Muratovic Date: Mon, 6 Apr 2026 22:55:44 +0100 Subject: [PATCH] feat: Fix: cforge-dev implement --help shows error instead of usage Co-Authored-By: Claude Sonnet 4.6 --- src/cli/commands/audit.ts | 8 ++- src/cli/commands/plan.ts | 5 ++ src/cli/commands/release.ts | 5 ++ src/cli/commands/verify.ts | 5 ++ src/cli/index.ts | 7 +- tests/cli/commands/help.test.ts | 112 ++++++++++++++++++++++++++++++++ tests/cli/index.test.ts | 55 ++++++++++++++++ 7 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 tests/cli/commands/help.test.ts create mode 100644 tests/cli/index.test.ts diff --git a/src/cli/commands/audit.ts b/src/cli/commands/audit.ts index c4afcb4..9d6dd84 100644 --- a/src/cli/commands/audit.ts +++ b/src/cli/commands/audit.ts @@ -2,8 +2,14 @@ import * as path from "path"; import { ContractLoader } from "../../infrastructure/filesystem/ContractLoader"; import { CForgePromptGenerator } from "../../infrastructure/cforge/CForgePromptGenerator"; import { GovernanceAuditor } from "../../application/use-cases/GovernanceAuditor"; +import { USAGE_AUDIT } from "../validation"; + +export async function auditCommand(flags: string[] = []): Promise { + if (flags.includes("--help")) { + console.log(USAGE_AUDIT); + process.exit(0); + } -export async function auditCommand(): Promise { const workingDir = process.cwd(); const contractsDir = path.join(workingDir, "contracts"); diff --git a/src/cli/commands/plan.ts b/src/cli/commands/plan.ts index c287f3f..7dd0477 100644 --- a/src/cli/commands/plan.ts +++ b/src/cli/commands/plan.ts @@ -17,6 +17,11 @@ function prompt(question: string): Promise { } export async function planCommand(prdFile: string): Promise { + if (prdFile === "--help") { + console.log(USAGE_PLAN); + process.exit(0); + } + const presenceErr = validatePresence(prdFile, USAGE_PLAN); if (presenceErr) { console.error(presenceErr); diff --git a/src/cli/commands/release.ts b/src/cli/commands/release.ts index 373b628..f372ca4 100644 --- a/src/cli/commands/release.ts +++ b/src/cli/commands/release.ts @@ -4,6 +4,11 @@ import { loadContext } from "../utils/loadContext"; import { validatePresence, USAGE_RELEASE } from "../validation"; export async function releaseCommand(milestoneIdStr: string, version: string): Promise { + if (milestoneIdStr === "--help" || version === "--help") { + console.log(USAGE_RELEASE); + process.exit(0); + } + const presenceErr = validatePresence(milestoneIdStr, USAGE_RELEASE) || validatePresence(version, USAGE_RELEASE); if (presenceErr) { console.error(presenceErr); diff --git a/src/cli/commands/verify.ts b/src/cli/commands/verify.ts index d2839e1..758f545 100644 --- a/src/cli/commands/verify.ts +++ b/src/cli/commands/verify.ts @@ -5,6 +5,11 @@ import { loadContext } from "../utils/loadContext"; import { validatePresence, validateNumericIssueNumber, USAGE_VERIFY } from "../validation"; export async function verifyCommand(issueNumberStr: string): Promise { + if (issueNumberStr === "--help") { + console.log(USAGE_VERIFY); + process.exit(0); + } + const presenceErr = validatePresence(issueNumberStr, USAGE_VERIFY); if (presenceErr) { console.error(presenceErr); diff --git a/src/cli/index.ts b/src/cli/index.ts index 09e4e36..4933467 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -30,6 +30,11 @@ export async function main(): Promise { process.exit(0); } + if (command === "--help" || command === "-h") { + console.log(USAGE); + process.exit(0); + } + if (command === "--version" || command === "-v") { console.log(getVersion()); process.exit(0); @@ -52,7 +57,7 @@ export async function main(): Promise { await chatCommand(); break; case "audit": - await auditCommand(); + await auditCommand(args); break; default: console.error(`Unknown command: ${command}\n`); diff --git a/tests/cli/commands/help.test.ts b/tests/cli/commands/help.test.ts new file mode 100644 index 0000000..c8baf3f --- /dev/null +++ b/tests/cli/commands/help.test.ts @@ -0,0 +1,112 @@ +// Mock @octokit/rest which uses ESM and cannot be loaded by ts-jest directly +jest.mock("@octokit/rest", () => ({ Octokit: jest.fn() })); + +import { implementCommand } from "../../../src/cli/commands/implement"; +import { planCommand } from "../../../src/cli/commands/plan"; +import { verifyCommand } from "../../../src/cli/commands/verify"; +import { releaseCommand } from "../../../src/cli/commands/release"; +import { auditCommand } from "../../../src/cli/commands/audit"; +import { + USAGE_IMPLEMENT, + USAGE_VERIFY, + USAGE_RELEASE, + USAGE_PLAN, + USAGE_AUDIT, +} from "../../../src/cli/validation"; + +describe("--help flag handling", () => { + let mockExit: jest.SpyInstance; + let mockLog: jest.SpyInstance; + let mockError: jest.SpyInstance; + + beforeEach(() => { + mockExit = jest.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`process.exit:${code}`); + }) as never); + mockLog = jest.spyOn(console, "log").mockImplementation(() => {}); + mockError = jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + mockExit.mockRestore(); + mockLog.mockRestore(); + mockError.mockRestore(); + }); + + describe("implement --help", () => { + it("prints usage and exits with code 0 when --help is the first argument", async () => { + await expect(implementCommand("--help", [])).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE_IMPLEMENT); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("prints usage and exits with code 0 when --help is in flags", async () => { + await expect(implementCommand(undefined as unknown as string, ["--help"])).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE_IMPLEMENT); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("does not exit with error code when --help is used", async () => { + await expect(implementCommand("--help", [])).rejects.toThrow("process.exit:0"); + expect(mockExit).not.toHaveBeenCalledWith(1); + }); + }); + + describe("plan --help", () => { + it("prints usage and exits with code 0 when --help is passed", async () => { + await expect(planCommand("--help")).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE_PLAN); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("does not exit with error code when --help is used", async () => { + await expect(planCommand("--help")).rejects.toThrow("process.exit:0"); + expect(mockExit).not.toHaveBeenCalledWith(1); + }); + }); + + describe("verify --help", () => { + it("prints usage and exits with code 0 when --help is passed", async () => { + await expect(verifyCommand("--help")).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE_VERIFY); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("does not exit with error code when --help is used", async () => { + await expect(verifyCommand("--help")).rejects.toThrow("process.exit:0"); + expect(mockExit).not.toHaveBeenCalledWith(1); + }); + }); + + describe("release --help", () => { + it("prints usage and exits with code 0 when --help is passed as milestone", async () => { + await expect(releaseCommand("--help", "1.0.0")).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE_RELEASE); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("prints usage and exits with code 0 when --help is passed as version", async () => { + await expect(releaseCommand("1", "--help")).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE_RELEASE); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("does not exit with error code when --help is used", async () => { + await expect(releaseCommand("--help", "1.0.0")).rejects.toThrow("process.exit:0"); + expect(mockExit).not.toHaveBeenCalledWith(1); + }); + }); + + describe("audit --help", () => { + it("prints usage and exits with code 0 when --help is passed", async () => { + await expect(auditCommand(["--help"])).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE_AUDIT); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("does not exit with error code when --help is used", async () => { + await expect(auditCommand(["--help"])).rejects.toThrow("process.exit:0"); + expect(mockExit).not.toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/tests/cli/index.test.ts b/tests/cli/index.test.ts new file mode 100644 index 0000000..dc66466 --- /dev/null +++ b/tests/cli/index.test.ts @@ -0,0 +1,55 @@ +// Mock @octokit/rest which uses ESM and cannot be loaded by ts-jest directly +jest.mock("@octokit/rest", () => ({ Octokit: jest.fn() })); +jest.mock("../../src/infrastructure/github/OctokitGitHubClient"); +jest.mock("../../src/infrastructure/cforge/CForgePromptGenerator"); +jest.mock("../../src/infrastructure/claude/ClaudeCodeRunner"); +jest.mock("../../src/infrastructure/filesystem/ContractLoader"); + +import { main, USAGE } from "../../src/cli/index"; + +describe("global --help", () => { + let originalArgv: string[]; + let mockExit: jest.SpyInstance; + let mockLog: jest.SpyInstance; + let mockError: jest.SpyInstance; + + beforeEach(() => { + originalArgv = process.argv; + mockExit = jest.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`process.exit:${code}`); + }) as never); + mockLog = jest.spyOn(console, "log").mockImplementation(() => {}); + mockError = jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + process.argv = originalArgv; + mockExit.mockRestore(); + mockLog.mockRestore(); + mockError.mockRestore(); + }); + + it("displays global usage and exits with code 0 when --help flag is provided", async () => { + process.argv = ["node", "cforge-dev", "--help"]; + await expect(main()).rejects.toThrow("process.exit:0"); + expect(mockLog).toHaveBeenCalledWith(USAGE); + expect(mockExit).toHaveBeenCalledWith(0); + }); + + it("lists all available commands in global help output", async () => { + process.argv = ["node", "cforge-dev", "--help"]; + await expect(main()).rejects.toThrow("process.exit:0"); + const logOutput = mockLog.mock.calls.map((call: unknown[]) => call[0]).join(""); + expect(logOutput).toContain("plan"); + expect(logOutput).toContain("implement"); + expect(logOutput).toContain("verify"); + expect(logOutput).toContain("release"); + expect(logOutput).toContain("audit"); + }); + + it("does not exit with error code when --help is used", async () => { + process.argv = ["node", "cforge-dev", "--help"]; + await expect(main()).rejects.toThrow("process.exit:0"); + expect(mockExit).not.toHaveBeenCalledWith(1); + }); +});