Merge branch 'main' into feat/Desktop-app

This commit is contained in:
Sparsh 2026-05-19 13:14:29 +05:30 committed by GitHub
commit 81eb63cd9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 106 additions and 8 deletions

View file

@ -61,11 +61,13 @@ function resolveGitnexusBin(): string | null {
.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.
// On Windows, npm global installs can surface multiple launchers for the
// same package (e.g. a POSIX shell shim plus .cmd/.bat wrappers). Claude
// and the other MCP hosts need a directly spawnable command path, so only
// accept the Windows wrapper. If it is missing, fall back to the slower
// npx entry instead of persisting a non-spawnable shim path.
const cmdLine = lines.find((l) => /\.(cmd|bat)$/i.test(l));
return cmdLine || lines[0] || null;
return cmdLine || null;
}
return lines[0] || null;

View file

@ -15,8 +15,13 @@ const execFileMock = vi.fn((...args: any[]) => {
}
});
const execFileSyncMock = vi.fn(() => {
throw new Error('not found');
});
vi.mock('child_process', () => ({
execFile: execFileMock,
execFileSync: execFileSyncMock,
}));
describe('setupCommand codex execution', () => {
@ -74,6 +79,21 @@ describe('setupCommand codex execution', () => {
);
});
it('uses Windows npx fallback arguments when where returns only a non-wrapper shim', async () => {
execFileSyncMock.mockReturnValueOnce('C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\n');
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
expect(execFileMock).toHaveBeenCalledWith(
'codex',
['mcp', 'add', 'gitnexus', '--', 'cmd', '/c', 'npx', '-y', NPX_REF, 'mcp'],
{ shell: true },
expect.any(Function),
);
});
it('invokes codex mcp add without shell on non-Windows and does not write fallback config', async () => {
setPlatform('darwin');

View file

@ -241,6 +241,50 @@ describe('setupOpenCode — JSONC preservation', () => {
});
});
it('uses Windows npx fallback when where returns only a non-wrapper shim', async () => {
setPlatform('win32');
execFileSyncMock.mockReturnValueOnce('C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\n');
const jsonc = `{
"model": "test",
"mcp": {}
}`;
await fs.writeFile(opencodeJsonPath(), jsonc, 'utf-8');
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
const raw = await fs.readFile(opencodeJsonPath(), 'utf-8');
const config = parseJsonc(raw);
expect(config.mcp.gitnexus).toEqual({
type: 'local',
command: ['cmd', '/c', 'npx', '-y', NPX_REF, 'mcp'],
});
});
it('uses Windows npx fallback when where returns only a .ps1 path', async () => {
setPlatform('win32');
execFileSyncMock.mockReturnValueOnce('C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.ps1\n');
const jsonc = `{
"model": "test",
"mcp": {}
}`;
await fs.writeFile(opencodeJsonPath(), jsonc, 'utf-8');
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
const raw = await fs.readFile(opencodeJsonPath(), 'utf-8');
const config = parseJsonc(raw);
expect(config.mcp.gitnexus).toEqual({
type: 'local',
command: ['cmd', '/c', 'npx', '-y', NPX_REF, 'mcp'],
});
});
it('preserves tab indentation in existing file', async () => {
const tabbed = `{\n\t"model": "test"\n}`;
await fs.writeFile(opencodeJsonPath(), tabbed, 'utf-8');
@ -360,6 +404,22 @@ describe('setupCursor — JSONC preservation', () => {
const raw = await fs.readFile(mcpPath(), 'utf-8');
expect(raw).toBe(corrupt);
});
it('uses Windows npx fallback when where returns only a non-wrapper shim', async () => {
setPlatform('win32');
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(mcpPath(), 'utf-8');
const config = parseJsonc(raw);
expect(config.mcpServers.gitnexus).toEqual({
command: 'cmd',
args: ['/c', 'npx', '-y', NPX_REF, 'mcp'],
});
});
});
describe('setupClaudeCode — JSONC preservation', () => {

View file

@ -282,9 +282,9 @@ describe('setupClaudeCode', () => {
).resolves.toBeUndefined();
});
it('falls back to first line on Windows when no .cmd/.bat wrapper found', async () => {
it('falls back to npx on Windows when no .cmd/.bat wrapper is found', async () => {
setPlatform('win32');
// Edge case: where returns only the POSIX script (no .cmd wrapper)
// Edge case: where returns only a non-spawnable shim (no .cmd wrapper)
execFileSyncMock.mockReturnValueOnce('C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus\n');
const { setupCommand } = await import('../../src/cli/setup.js');
@ -294,8 +294,24 @@ describe('setupClaudeCode', () => {
const config = JSON.parse(raw);
expect(config.mcpServers.gitnexus).toEqual({
command: 'C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus',
args: ['mcp'],
command: 'cmd',
args: ['/c', 'npx', '-y', NPX_REF, 'mcp'],
});
});
it('falls back to npx on Windows when where returns only a .ps1 path', async () => {
setPlatform('win32');
execFileSyncMock.mockReturnValueOnce('C:\\Users\\dev\\AppData\\Roaming\\npm\\gitnexus.ps1\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: 'cmd',
args: ['/c', 'npx', '-y', NPX_REF, 'mcp'],
});
});
});