From 1272774ec21c82368325c152f928018d8c8e12af Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 08:25:38 +0100 Subject: [PATCH] fix(setup): prefer .cmd/.bat wrapper from Windows `where` output (#1299) * Initial plan * fix(setup): prefer .cmd wrapper from Windows `where` output On Windows, `where gitnexus` returns multiple entries including the POSIX shell script and the .cmd wrapper. The code previously took the first line (shell script), which cannot be spawned directly by Node.js child_process on Windows. Now we prefer the .cmd entry when available. Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e6b54037-87fb-4195-b157-4cfcafce5f5d Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix(setup): also handle .bat wrappers and add fallback test Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e6b54037-87fb-4195-b157-4cfcafce5f5d Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * revert package-lock.json and add CRLF/.bat/.CMD test variants - Revert package-lock.json to match base (no dependency changes needed) - Add CRLF line ending test (Windows `where` produces \r\n) - Add .bat wrapper test - Add uppercase .CMD extension test (case-insensitive regex) Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/7ed71368-b3e8-44de-9f13-85af4effaf25 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * chore: format code --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> Co-authored-by: Gergo Magyar --- gitnexus/src/cli/setup.ts | 24 ++++++--- gitnexus/test/unit/setup.test.ts | 91 ++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/gitnexus/src/cli/setup.ts b/gitnexus/src/cli/setup.ts index 9a2be4505..b301d7f38 100644 --- a/gitnexus/src/cli/setup.ts +++ b/gitnexus/src/cli/setup.ts @@ -32,15 +32,27 @@ interface SetupResult { */ function resolveGitnexusBin(): string | null { try { - const cmd = process.platform === 'win32' ? 'where' : 'which'; - const resolved = execFileSync(cmd, ['gitnexus'], { + const isWin = process.platform === 'win32'; + const cmd = isWin ? 'where' : 'which'; + const output = execFileSync(cmd, ['gitnexus'], { encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], - }) - .split('\n')[0] - .trim(); - return resolved || null; + }); + const lines = output + .split('\n') + .map((l) => l.trim()) + .filter(Boolean); + + if (isWin) { + // On Windows, `where` returns multiple entries (e.g. the POSIX shell + // script AND the .cmd/.bat wrapper). Prefer the wrapper because + // child_process.spawn() cannot execute a shell script directly. + const cmdLine = lines.find((l) => /\.(cmd|bat)$/i.test(l)); + return cmdLine || lines[0] || null; + } + + return lines[0] || null; } catch { return null; } diff --git a/gitnexus/test/unit/setup.test.ts b/gitnexus/test/unit/setup.test.ts index 4544ddb02..6cab31467 100644 --- a/gitnexus/test/unit/setup.test.ts +++ b/gitnexus/test/unit/setup.test.ts @@ -185,4 +185,95 @@ describe('setupClaudeCode', () => { args: ['-y', 'gitnexus@latest', 'mcp'], }); }); + + it('picks .cmd wrapper from Windows where output (multiple lines)', async () => { + setPlatform('win32'); + // `where gitnexus` on Windows returns the POSIX script first, then .cmd + execFileSyncMock.mockReturnValueOnce( + 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\nC:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.cmd\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: 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.cmd', + args: ['mcp'], + }); + }); + + it('handles CRLF line endings from Windows where output', async () => { + setPlatform('win32'); + // Windows `where` produces CRLF line endings + execFileSyncMock.mockReturnValueOnce( + 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\r\nC:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.cmd\r\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: 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.cmd', + args: ['mcp'], + }); + }); + + it('picks .bat wrapper when .cmd is not present', async () => { + setPlatform('win32'); + execFileSyncMock.mockReturnValueOnce( + 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\nC:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.bat\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: 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.bat', + args: ['mcp'], + }); + }); + + it('handles uppercase .CMD extension (case-insensitive match)', async () => { + setPlatform('win32'); + execFileSyncMock.mockReturnValueOnce( + 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\nC:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.CMD\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: 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.CMD', + args: ['mcp'], + }); + }); + + it('falls back to first line on Windows when no .cmd/.bat wrapper found', async () => { + setPlatform('win32'); + // Edge case: where returns only the POSIX script (no .cmd wrapper) + execFileSyncMock.mockReturnValueOnce('C:\\Users\\dev\\AppData\\Roaming\\npm\\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: 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus', + args: ['mcp'], + }); + }); });