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
3 changes: 3 additions & 0 deletions packages/gittensory-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ gittensory-mcp config --json
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
Expand Down Expand Up @@ -128,6 +129,8 @@ Use `--profile <name>` on `login`, `logout`, `whoami`, `config`, `status`, and `

`gittensory-mcp config` prints the resolved effective configuration and the source that supplied each value (`environment`, `profile`, `config`, or `default`): the active API URL and its source, active profile and profile count, whether a config file is present and which environment variable steers its location, the cache-dir source, whether a token is configured and where it came from, and whether `GITTENSORY_UPLOAD_SOURCE` has enabled the unsupported source-upload setting. It never prints token values or local absolute paths. Add `--json` for machine-readable output.

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.
Expand Down
9 changes: 6 additions & 3 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,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({
Expand Down Expand Up @@ -1747,7 +1747,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 [--agent-profile miner-planner|maintainer-triage|repo-owner-intake] [--json]
gittensory-mcp decision-pack --login <github-login> [--json]
Expand Down Expand Up @@ -2173,6 +2173,9 @@ async function doctor(options) {
}
}
}
// 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 doctorStatus(checks) {
Expand Down
60 changes: 59 additions & 1 deletion test/unit/mcp-cli-doctor.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { closeFixtureServer, createPacketRepo, git, runAsync, startFixtureServer } from "./support/mcp-cli-harness";
import { bin, closeFixtureServer, createPacketRepo, git, runAsync, startFixtureServer } from "./support/mcp-cli-harness";

describe("gittensory-mcp CLI — doctor", () => {
let tempDir: string | null = null;
Expand Down Expand Up @@ -409,4 +410,61 @@ describe("gittensory-mcp CLI — doctor", () => {
// Sanity: upgrade guidance still surfaces in human-readable output.
expect(statusOutput).toContain("npm install -g @jsonbored/gittensory-mcp@latest");
});

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/);
});
});