Compare commits

...
Author SHA1 Message Date
abhigyanpatwariandClaude Sonnet 4.6 9c78439d24 fix(setup): strip \r from resolveGitnexusBin on Windows multi-match
Move .trim() after .split('\n')[0] so the trailing carriage return is
removed from the first line when `where` returns multiple results on Windows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 10:58:13 +05:30
Abhigyan PatwariandClaude Opus 4.6 c5dcc0172a fix(setup): prefer global gitnexus binary over npx for MCP config
`npx -y gitnexus@latest mcp` with a cold cache takes ~110s to resolve
native dependencies (tree-sitter, onnxruntime), exceeding Claude Code's
30s MCP connection timeout. `getMcpEntry()` now checks for a globally
installed `gitnexus` binary first (starts in ~1s) and only falls back
to npx when not found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:11:47 +05:30
2 changed files with 76 additions and 2 deletions
+35 -2
View File
@@ -9,7 +9,7 @@
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { execFile } from 'child_process';
import { execFile, execFileSync } from 'child_process';
import { promisify } from 'util';
import { fileURLToPath } from 'url';
import { glob } from 'glob';
@@ -25,11 +25,44 @@ interface SetupResult {
errors: string[];
}
/**
* Resolve the absolute path to the `gitnexus` binary if it's installed
* globally (or via npm -g / yarn global). Returns null when not found.
*/
function resolveGitnexusBin(): string | null {
try {
const cmd = process.platform === 'win32' ? 'where' : 'which';
const resolved = execFileSync(cmd, ['gitnexus'], {
encoding: 'utf-8',
timeout: 5000,
stdio: ['ignore', 'pipe', 'ignore'],
})
.split('\n')[0]
.trim();
return resolved || null;
} catch {
return null;
}
}
/**
* The MCP server entry for all editors.
* On Windows, npx must be invoked via cmd /c since it's a .cmd script.
*
* Prefers the globally-installed `gitnexus` binary (starts in ~1 s) over
* `npx -y gitnexus@latest` (cold-cache install of native deps can take
* >60 s, exceeding Claude Code's 30 s MCP connection timeout).
*
* Falls back to npx when the binary isn't on PATH — e.g. first-time
* users who ran `npx gitnexus analyze` but haven't done `npm i -g`.
*/
function getMcpEntry() {
const bin = resolveGitnexusBin();
if (bin) {
return { command: bin, args: ['mcp'] };
}
// Fallback: npx (works without a global install, but slow cold-start)
if (process.platform === 'win32') {
return {
command: 'cmd',
+41
View File
@@ -10,8 +10,15 @@ const execFileMock = vi.fn((...args: any[]) => {
}
});
// By default, execFileSync throws (simulating `which gitnexus` not found)
// so getMcpEntry() falls back to the npx path.
const execFileSyncMock = vi.fn(() => {
throw new Error('not found');
});
vi.mock('child_process', () => ({
execFile: execFileMock,
execFileSync: execFileSyncMock,
}));
describe('setupClaudeCode', () => {
@@ -150,4 +157,38 @@ describe('setupClaudeCode', () => {
expect(config.mcpServers.gitnexus).toBeDefined();
});
it('uses global binary path when gitnexus is on PATH', async () => {
setPlatform('darwin');
execFileSyncMock.mockReturnValueOnce('/usr/local/bin/gitnexus\n');
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
const raw = await fs.readFile(path.join(tempHome, '.claude.json'), 'utf-8');
const config = JSON.parse(raw);
expect(config.mcpServers.gitnexus).toEqual({
command: '/usr/local/bin/gitnexus',
args: ['mcp'],
});
});
it('falls back to npx when gitnexus is not on PATH', async () => {
setPlatform('darwin');
execFileSyncMock.mockImplementationOnce(() => {
throw new Error('not found');
});
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
const raw = await fs.readFile(path.join(tempHome, '.claude.json'), 'utf-8');
const config = JSON.parse(raw);
expect(config.mcpServers.gitnexus).toEqual({
command: 'npx',
args: ['-y', 'gitnexus@latest', 'mcp'],
});
});
});