Skip to content
Open
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 @@ -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
Expand Down Expand Up @@ -123,6 +124,8 @@ gittensory-mcp logout --profile work

Use `--profile <name>` 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.
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 @@ -185,8 +185,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 @@ -1394,7 +1394,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 <github-login> [--json]
Expand Down Expand Up @@ -1820,6 +1820,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
57 changes: 57 additions & 0 deletions test/unit/mcp-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,63 @@ describe("gittensory-mcp CLI", () => {
expect(() => run(["completion"])).toThrow(/Usage: gittensory-mcp completion <bash\|zsh\|fish>/);
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<string, string> = {}) {
Expand Down