Merge pull request #490 from zander-raycraft/main

Updating claude mcp init for window and adding OS detection when installing
This commit is contained in:
Zander Raycraft 2026-03-23 17:47:03 -05:00 • committed by GitHub
commit e6aea1c82b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 174 additions and 10 deletions

View file

@ -107,7 +107,11 @@ If you prefer manual configuration:
**Claude Code** (full support — MCP + skills + hooks):
```bash
# macOS / Linux
claude mcp add gitnexus -- npx -y gitnexus@latest mcp
# Windows
claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp
```
**Codex** (full support — MCP + skills):

View file

@ -53,7 +53,11 @@ If you prefer to configure manually instead of using `gitnexus setup`:
### Claude Code (full support — MCP + skills + hooks)
```bash
# macOS / Linux
claude mcp add gitnexus -- npx -y gitnexus@latest mcp
# Windows
claude mcp add gitnexus -- cmd /c npx -y gitnexus@latest mcp
```
### Codex (full support — MCP + skills)

View file

@ -111,20 +111,21 @@ async function setupCursor(result: SetupResult): Promise<void> {
async function setupClaudeCode(result: SetupResult): Promise<void> {
const claudeDir = path.join(os.homedir(), '.claude');
const hasClaude = await dirExists(claudeDir);
if (!hasClaude) {
if (!(await dirExists(claudeDir))) {
result.skipped.push('Claude Code (not installed)');
return;
}
// Claude Code uses a JSON settings file at ~/.claude.json or claude mcp add
console.log('');
console.log(' Claude Code detected. Run this command to add GitNexus MCP:');
console.log('');
console.log(' claude mcp add gitnexus -- npx -y gitnexus mcp');
console.log('');
result.configured.push('Claude Code (MCP manual step printed)');
// Claude Code stores MCP config in ~/.claude.json
const mcpPath = path.join(os.homedir(), '.claude.json');
try {
const existing = await readJsonFile(mcpPath);
const updated = mergeMcpConfig(existing);
await writeJsonFile(mcpPath, updated);
result.configured.push('Claude Code');
} catch (err: any) {
result.errors.push(`Claude Code: ${err.message}`);
}
}
/**

View file

@ -0,0 +1,155 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
const execFileMock = vi.fn((...args: any[]) => {
const callback = args.at(-1);
if (typeof callback === 'function') {
callback(null, '', '');
}
});
vi.mock('child_process', () => ({
execFile: execFileMock,
}));
describe('setupClaudeCode', () => {
let tempHome: string;
let originalHome: string | undefined;
let originalUserProfile: string | undefined;
let platformDescriptor: PropertyDescriptor | undefined;
const setPlatform = (value: NodeJS.Platform) => {
Object.defineProperty(process, 'platform', {
value,
configurable: true,
});
};
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
originalHome = process.env.HOME;
originalUserProfile = process.env.USERPROFILE;
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-claude-setup-'));
process.env.HOME = tempHome;
process.env.USERPROFILE = tempHome;
// Only create ~/.claude — no other editor directories so their
// setup functions skip and don't pollute assertions.
await fs.mkdir(path.join(tempHome, '.claude'), { recursive: true });
platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
vi.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(async () => {
vi.restoreAllMocks();
if (platformDescriptor) {
Object.defineProperty(process, 'platform', platformDescriptor);
}
process.env.HOME = originalHome;
process.env.USERPROFILE = originalUserProfile;
await fs.rm(tempHome, { recursive: true, force: true });
});
it('writes win32 MCP entry with cmd wrapper', async () => {
setPlatform('win32');
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', 'gitnexus@latest', 'mcp'],
});
});
it('writes non-win32 MCP entry with npx directly', async () => {
setPlatform('darwin');
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'],
});
});
it('skips when ~/.claude directory does not exist', async () => {
await fs.rm(path.join(tempHome, '.claude'), { recursive: true, force: true });
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
await expect(
fs.access(path.join(tempHome, '.claude.json')),
).rejects.toThrow();
});
it('preserves existing keys in ~/.claude.json', async () => {
setPlatform('linux');
await fs.writeFile(
path.join(tempHome, '.claude.json'),
JSON.stringify({ existingKey: 'keep-me', mcpServers: { other: { command: 'foo' } } }),
'utf-8',
);
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.existingKey).toBe('keep-me');
expect(config.mcpServers.other).toEqual({ command: 'foo' });
expect(config.mcpServers.gitnexus).toBeDefined();
});
it('handles missing ~/.claude.json (creates fresh)', async () => {
setPlatform('linux');
// Ensure no pre-existing file
await fs.rm(path.join(tempHome, '.claude.json'), { force: true });
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).toBeDefined();
});
it('handles corrupt JSON gracefully', async () => {
setPlatform('linux');
await fs.writeFile(
path.join(tempHome, '.claude.json'),
'{ this is not valid json !!!',
'utf-8',
);
const { setupCommand } = await import('../../src/cli/setup.js');
await setupCommand();
// readJsonFile returns null on invalid JSON, so mergeMcpConfig
// creates a fresh config — the file should now be valid.
const raw = await fs.readFile(path.join(tempHome, '.claude.json'), 'utf-8');
const config = JSON.parse(raw);
expect(config.mcpServers.gitnexus).toBeDefined();
});
});