mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-12 08:41:34 +00:00
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:
commit
e6aea1c82b
4 changed files with 174 additions and 10 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
155
gitnexus/test/unit/setup.test.ts
Normal file
155
gitnexus/test/unit/setup.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue