From ab665c0766bc339b62d788ce38045cae968f7e9d Mon Sep 17 00:00:00 2001 From: glorydavid03023 Date: Wed, 3 Jun 2026 19:53:59 -0500 Subject: [PATCH] feat(mcp): let doctor --exit-code gate CI with a non-zero exit on failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gittensory-mcp doctor` computes a status (ok / warnings / needs_attention) but the process always exits 0, so it cannot gate a CI step or pre-commit hook. Add an opt-in `--exit-code` flag: when a diagnostic check fails (status "needs_attention"), the process exits 1. The diagnostic report is still printed; only the exit code changes. The default is unchanged — without `--exit-code`, doctor still exits 0 — so existing scripts that ignore the exit code keep working. Warnings also stay at exit 0; only hard failures are surfaced as a non-zero code. Implemented by letting runCli's command result carry an optional numeric exit code (doctor is the only command that returns one); every other command still resolves to exit 0. Tests cover the default exit-0-on-failure behavior, the non-zero exit under --exit-code on failure, and exit 0 under --exit-code when checks pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/gittensory-mcp/README.md | 3 + packages/gittensory-mcp/bin/gittensory-mcp.js | 9 ++- test/unit/mcp-cli.test.ts | 57 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/gittensory-mcp/README.md b/packages/gittensory-mcp/README.md index 6a9cc284f6..ed48a82b64 100644 --- a/packages/gittensory-mcp/README.md +++ b/packages/gittensory-mcp/README.md @@ -35,6 +35,7 @@ gittensory-mcp whoami gittensory-mcp status gittensory-mcp changelog gittensory-mcp doctor +gittensory-mcp doctor --exit-code gittensory-mcp profile list gittensory-mcp profile create work gittensory-mcp profile switch work @@ -123,6 +124,8 @@ gittensory-mcp logout --profile work Use `--profile ` on `login`, `logout`, `whoami`, `status`, and `doctor`, or set `GITTENSORY_PROFILE`. `logout` only clears the selected local profile unless `--all` is passed. Profile output redacts session tokens and local config paths. +By default `gittensory-mcp doctor` always exits 0. Pass `--exit-code` to make it exit non-zero when a diagnostic check fails (`status: "needs_attention"`), so it can gate a CI step or pre-commit hook. Warnings still exit 0. + ## Base-Agent Mode The agent commands are copilot-only. They rank, explain, preflight, and draft public-safe packets, but they do not edit code, open PRs, post comments, close, merge, or label from the local wrapper. diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index 9491186422..2fe716fbba 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -184,8 +184,8 @@ const agentRunIdShape = { }; if (cliArgs[0] && cliArgs[0] !== "--stdio") { - await runCli(cliArgs); - process.exit(0); + const exitCode = await runCli(cliArgs); + process.exit(typeof exitCode === "number" ? exitCode : 0); } const server = new McpServer({ @@ -1392,7 +1392,7 @@ function printHelp() { gittensory-mcp status [--profile name] [--json] gittensory-mcp profile list|create|switch|remove [name] [--json] gittensory-mcp changelog [--json] - gittensory-mcp doctor [--profile name] [--cwd path] [--json] + gittensory-mcp doctor [--profile name] [--cwd path] [--exit-code] [--json] gittensory-mcp cache status|clear [--json] gittensory-mcp init-client --print codex|claude|cursor|mcp [--json] gittensory-mcp decision-pack --login [--json] @@ -1796,6 +1796,9 @@ async function doctor(options) { if (check.remediation) process.stdout.write(` ${check.remediation}\n`); } } + // Opt-in: let `doctor` gate CI/pre-commit by exiting non-zero when a check fails. The default + // stays exit 0 so existing scripts that ignore the exit code keep working. + return options.exitCode && payload.status === "needs_attention" ? 1 : 0; } function initClient(options) { diff --git a/test/unit/mcp-cli.test.ts b/test/unit/mcp-cli.test.ts index 081c26045c..d5676b01df 100644 --- a/test/unit/mcp-cli.test.ts +++ b/test/unit/mcp-cli.test.ts @@ -953,6 +953,63 @@ describe("gittensory-mcp CLI", () => { expect(() => run(["completion"])).toThrow(/Usage: gittensory-mcp completion /); expect(() => run(["completion", "powershell"])).toThrow(/Unsupported shell: powershell/); }); + + it("keeps doctor exit code 0 by default even when a check fails", async () => { + tempDir = mkdtempSync(join(tmpdir(), "gittensory-cli-")); + const url = await startFixtureServer(); + // No token configured -> the auth check fails -> status "needs_attention". + const payload = JSON.parse( + await runAsync(["doctor", "--json"], { + GITTENSORY_API_URL: url, + GITTENSORY_CONFIG_DIR: tempDir, + GITTENSORY_SKIP_NPM_VERSION_CHECK: "true", + }), + ) as { status: string; checks: Array<{ name: string; status: string }> }; + expect(payload.status).toBe("needs_attention"); + expect(payload.checks).toEqual(expect.arrayContaining([expect.objectContaining({ name: "auth", status: "fail" })])); + }); + + it("exits non-zero from doctor --exit-code when a check fails", async () => { + tempDir = mkdtempSync(join(tmpdir(), "gittensory-cli-")); + const url = await startFixtureServer(); + let exitCode = 0; + let stdout = ""; + try { + stdout = execFileSync("node", [bin, "doctor", "--exit-code", "--json"], { + encoding: "utf8", + env: { + ...process.env, + GITTENSORY_API_TIMEOUT_MS: "1000", + GITTENSORY_API_URL: url, + GITTENSORY_CONFIG_DIR: tempDir, + GITTENSORY_SKIP_NPM_VERSION_CHECK: "true", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + } catch (error) { + const execError = error as { status?: number | null; stdout?: string }; + exitCode = execError.status ?? 0; + stdout = execError.stdout ?? ""; + } + expect(exitCode).toBe(1); + // The diagnostic report is still printed; only the process exit code changes. + expect((JSON.parse(stdout) as { status: string }).status).toBe("needs_attention"); + }); + + it("keeps doctor --exit-code at 0 when checks pass", async () => { + tempDir = mkdtempSync(join(tmpdir(), "gittensory-cli-")); + const url = await startFixtureServer(); + // runAsync resolves only on a zero exit code, so reaching the assertion proves exit 0. + const payload = JSON.parse( + await runAsync(["doctor", "--exit-code", "--json"], { + GITTENSORY_API_URL: url, + GITTENSORY_TOKEN: "session-token", + GITTENSORY_CONFIG_DIR: tempDir, + GITTENSORY_SKIP_NPM_VERSION_CHECK: "true", + }), + ) as { status: string }; + expect(payload.status).toMatch(/ok|warnings/); + }); }); function run(args: string[], env: Record = {}) {