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
10 changes: 8 additions & 2 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -1886,7 +1886,7 @@ function doctorNextCommand(byName, context) {
const auth = byName.get("auth");
if (auth?.status === "fail") {
return {
command: `gittensory-mcp login --profile ${context.profileName}`,
command: `gittensory-mcp login --profile ${shellArg(context.profileName ?? "default")}`,
reason: "Authenticate the active profile so doctor, plan, preflight, and packet commands can call the API.",
};
}
Expand Down Expand Up @@ -1920,11 +1920,17 @@ function doctorNextCommand(byName, context) {
};
}
return {
command: `gittensory-mcp preflight --login ${context.login ?? "<github-login>"} --repo ${context.repoFullName ?? "owner/repo"} --json`,
command: `gittensory-mcp preflight --login ${shellArg(context.login ?? "<github-login>")} --repo ${shellArg(context.repoFullName ?? "owner/repo")} --json`,
reason: "Run branch preflight next; source upload remains disabled.",
};
}

function shellArg(value) {
const text = String(value ?? "");
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) return text;
return `'${text.replace(/'/g, `'"'"'`)}'`;
}

function initClient(options) {
const client = String(options.print ?? options.client ?? "").toLowerCase();
if (!client) throw new Error("Pass --print codex, --print claude, --print cursor, or --print mcp.");
Expand Down
21 changes: 21 additions & 0 deletions test/unit/mcp-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,27 @@ describe("gittensory-mcp CLI", () => {
expect(localScorer?.detail).not.toMatch(join(process.cwd(), "test/fixtures"));
});

it("shell-quotes doctor next command values derived from local repo metadata", async () => {
tempDir = createPacketRepo();
git(tempDir, "remote", "set-url", "origin", "git@github.com:owner/repo$(touch /tmp/av_pwned).git");
const url = await startFixtureServer();
const env = {
GITTENSORY_API_URL: url,
GITTENSORY_TOKEN: "session-token",
GITTENSORY_CONFIG_DIR: tempDir,
GITTENSOR_SCORE_PREVIEW_CMD: `node ${join(process.cwd(), "test/fixtures/local-scorer/scorer-success.mjs")}`,
GITTENSORY_SKIP_NPM_VERSION_CHECK: "true",
};

const payload = JSON.parse(await runAsync(["doctor", "--cwd", tempDir, "--json"], env)) as { nextCommand: { command: string } };
expect(payload.nextCommand.command).toBe("gittensory-mcp preflight --login JSONbored --repo 'owner/repo$(touch /tmp/av_pwned)' --json");
expect(payload.nextCommand.command).not.toContain("--repo owner/repo$(");

const humanOutput = await runAsync(["doctor", "--cwd", tempDir], env);
expect(humanOutput).toContain("gittensory-mcp preflight --login JSONbored --repo 'owner/repo$(touch /tmp/av_pwned)' --json");
expect(humanOutput).not.toContain("--repo owner/repo$(");
});

it("uses doctor as a first-run auth checklist when no local session is configured", async () => {
tempDir = mkdtempSync(join(tmpdir(), "gittensory-cli-"));
const url = await startFixtureServer();
Expand Down