mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-17 23:52:36 +00:00
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / scope-parity (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
* deps: add jsonc-parser for JSONC-safe config editing
* fix: use jsonc-parser to preserve comments in opencode.json during setup
- Add mergeJsoncFile() using parseTree/modify/applyEdits pipeline
- Add getOpenCodeMcpEntry() for OpenCode MCP format { type: local, command: [...] }
- Replace readJsonFile+writeJsonFile in setupOpenCode with mergeJsoncFile
- Fix wipe bug: JSON.parse on JSONC comments caused catch block to reset config to {}
- Add 9 tests for JSONC comment preservation, corrupt file safety, and format
* fix: use parseTree error collection and detect indentation
- Pass parseErrors array to parseTree() instead of checking
(tree as any).errors which was always undefined — a real bug
that allowed corrupt files to be rewritten
- Detect tab indentation from file content to avoid mixed
indentation in modified JSONC files
- Fix JSDoc to match actual fallback behavior (JSON.parse, not
readJsonFile)
- Strengthen corrupt-file test to assert exact content match
* style(setup): fix prettier formatting on mergeJsoncFile
* fix(setup): remove dead JSON.parse fallback, detect space-indent width, fix JSDoc
- Remove the semantically unreachable JSON.parse fallback branch in
mergeJsoncFile (jsonc-parser's parseTree is a strict superset of
JSON.parse, so the fallback can never fire for content JSON.parse
would accept)
- Replace binary tab/space detection with detectIndentation() that
measures actual indent width from the first indented line
- Fix JSDoc: 'valid JSON that is not valid JSONC' is impossible by
definition
- Add tests for tab indentation and 4-space indentation preservation
619 lines
19 KiB
TypeScript
619 lines
19 KiB
TypeScript
/**
|
|
* Setup Command
|
|
*
|
|
* One-time global MCP configuration writer.
|
|
* Detects installed AI editors and writes the appropriate MCP config
|
|
* so the GitNexus MCP server is available in all projects.
|
|
*/
|
|
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
import { execFile, execFileSync } from 'child_process';
|
|
import { promisify } from 'util';
|
|
import { fileURLToPath } from 'url';
|
|
import { glob } from 'glob';
|
|
import { parseTree, modify, applyEdits, ParseError } from 'jsonc-parser';
|
|
import { getGlobalDir } from '../storage/repo-manager.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
interface SetupResult {
|
|
configured: string[];
|
|
skipped: string[];
|
|
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.
|
|
*
|
|
* 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',
|
|
args: ['/c', 'npx', '-y', 'gitnexus@latest', 'mcp'],
|
|
};
|
|
}
|
|
return {
|
|
command: 'npx',
|
|
args: ['-y', 'gitnexus@latest', 'mcp'],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* OpenCode uses a different MCP format: { type: "local", command: [...] }
|
|
* where command is a flat array (command + args combined).
|
|
*/
|
|
function getOpenCodeMcpEntry() {
|
|
const bin = resolveGitnexusBin();
|
|
|
|
if (bin) {
|
|
return { type: 'local', command: [bin, 'mcp'] };
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
return { type: 'local', command: ['cmd', '/c', 'npx', '-y', 'gitnexus@latest', 'mcp'] };
|
|
}
|
|
return { type: 'local', command: ['npx', '-y', 'gitnexus@latest', 'mcp'] };
|
|
}
|
|
|
|
/**
|
|
* Merge gitnexus entry into an existing MCP config JSON object.
|
|
* Returns the updated config.
|
|
*/
|
|
function mergeMcpConfig(existing: any): any {
|
|
if (!existing || typeof existing !== 'object') {
|
|
existing = {};
|
|
}
|
|
if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
|
|
existing.mcpServers = {};
|
|
}
|
|
existing.mcpServers.gitnexus = getMcpEntry();
|
|
return existing;
|
|
}
|
|
|
|
/**
|
|
* Try to read a JSON file, returning null if it doesn't exist or is invalid.
|
|
*/
|
|
async function readJsonFile(filePath: string): Promise<any | null> {
|
|
try {
|
|
const raw = await fs.readFile(filePath, 'utf-8');
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write JSON to a file, creating parent directories if needed.
|
|
*/
|
|
async function writeJsonFile(filePath: string, data: any): Promise<void> {
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
}
|
|
|
|
/**
|
|
* Detect indentation style from file content.
|
|
* Returns formatting options matching the file's existing style.
|
|
*/
|
|
function detectIndentation(raw: string): { tabSize: number; insertSpaces: boolean } {
|
|
const firstIndented = raw.match(/^( +|\t)/m);
|
|
if (!firstIndented) return { tabSize: 2, insertSpaces: true };
|
|
if (firstIndented[1] === '\t') return { tabSize: 1, insertSpaces: false };
|
|
return { tabSize: firstIndented[1].length, insertSpaces: true };
|
|
}
|
|
|
|
/**
|
|
* Merge a key/value pair into a JSONC config file, preserving comments and formatting.
|
|
* If the file is genuinely corrupt (not valid JSONC), leaves it untouched.
|
|
*/
|
|
async function mergeJsoncFile(
|
|
filePath: string,
|
|
keyPath: string[],
|
|
value: unknown,
|
|
): Promise<boolean> {
|
|
let raw: string;
|
|
try {
|
|
raw = await fs.readFile(filePath, 'utf-8');
|
|
} catch {
|
|
raw = '';
|
|
}
|
|
|
|
if (raw.trim().length === 0) {
|
|
const config: any = {};
|
|
let parent: any = config;
|
|
for (let i = 0; i < keyPath.length; i++) {
|
|
if (i === keyPath.length - 1) {
|
|
parent[keyPath[i]] = value;
|
|
} else {
|
|
parent[keyPath[i]] = {};
|
|
parent = parent[keyPath[i]];
|
|
}
|
|
}
|
|
await writeJsonFile(filePath, config);
|
|
return true;
|
|
}
|
|
|
|
const parseErrors: ParseError[] = [];
|
|
const tree = parseTree(raw, parseErrors);
|
|
|
|
if (tree && tree.type === 'object' && parseErrors.length === 0) {
|
|
const formattingOptions = detectIndentation(raw);
|
|
const edits = modify(raw, keyPath, value, { formattingOptions });
|
|
const result = applyEdits(raw, edits);
|
|
await fs.writeFile(filePath, result, 'utf-8');
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a directory exists
|
|
*/
|
|
async function dirExists(dirPath: string): Promise<boolean> {
|
|
try {
|
|
const stat = await fs.stat(dirPath);
|
|
return stat.isDirectory();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ─── Editor-specific setup ─────────────────────────────────────────
|
|
|
|
async function setupCursor(result: SetupResult): Promise<void> {
|
|
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
if (!(await dirExists(cursorDir))) {
|
|
result.skipped.push('Cursor (not installed)');
|
|
return;
|
|
}
|
|
|
|
const mcpPath = path.join(cursorDir, 'mcp.json');
|
|
try {
|
|
const existing = await readJsonFile(mcpPath);
|
|
const updated = mergeMcpConfig(existing);
|
|
await writeJsonFile(mcpPath, updated);
|
|
result.configured.push('Cursor');
|
|
} catch (err: any) {
|
|
result.errors.push(`Cursor: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function setupClaudeCode(result: SetupResult): Promise<void> {
|
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
if (!(await dirExists(claudeDir))) {
|
|
result.skipped.push('Claude Code (not installed)');
|
|
return;
|
|
}
|
|
|
|
// 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}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install GitNexus skills to ~/.claude/skills/ for Claude Code.
|
|
*/
|
|
async function installClaudeCodeSkills(result: SetupResult): Promise<void> {
|
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
if (!(await dirExists(claudeDir))) return;
|
|
|
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
try {
|
|
const installed = await installSkillsTo(skillsDir);
|
|
if (installed.length > 0) {
|
|
result.configured.push(`Claude Code skills (${installed.length} skills → ~/.claude/skills/)`);
|
|
}
|
|
} catch (err: any) {
|
|
result.errors.push(`Claude Code skills: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install GitNexus hooks to ~/.claude/settings.json for Claude Code.
|
|
* Merges hook config without overwriting existing hooks.
|
|
*/
|
|
async function installClaudeCodeHooks(result: SetupResult): Promise<void> {
|
|
const claudeDir = path.join(os.homedir(), '.claude');
|
|
if (!(await dirExists(claudeDir))) return;
|
|
|
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
|
|
// Source hooks bundled within the gitnexus package (hooks/claude/)
|
|
const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
|
|
|
|
// Copy unified hook script to ~/.claude/hooks/gitnexus/
|
|
const destHooksDir = path.join(claudeDir, 'hooks', 'gitnexus');
|
|
|
|
try {
|
|
await fs.mkdir(destHooksDir, { recursive: true });
|
|
|
|
const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs');
|
|
const dest = path.join(destHooksDir, 'gitnexus-hook.cjs');
|
|
try {
|
|
let content = await fs.readFile(src, 'utf-8');
|
|
// Inject resolved CLI path so the copied hook can find the CLI
|
|
// even when it's no longer inside the npm package tree
|
|
const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js');
|
|
const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/');
|
|
const jsonCli = JSON.stringify(normalizedCli);
|
|
content = content.replace(
|
|
"let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');",
|
|
`let cliPath = ${jsonCli};`,
|
|
);
|
|
await fs.writeFile(dest, content, 'utf-8');
|
|
} catch {
|
|
// Script not found in source — skip
|
|
}
|
|
|
|
const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/');
|
|
const hookCmd = `node "${hookPath.replace(/"/g, '\\"')}"`;
|
|
|
|
// Merge hook config into ~/.claude/settings.json
|
|
const existing = (await readJsonFile(settingsPath)) || {};
|
|
if (!existing.hooks) existing.hooks = {};
|
|
|
|
// NOTE: SessionStart hooks are broken on Windows (Claude Code bug #23576).
|
|
// Session context is delivered via CLAUDE.md / skills instead.
|
|
|
|
// Helper: add a hook entry if one with 'gitnexus-hook' isn't already registered
|
|
interface HookEntry {
|
|
hooks?: Array<{ command?: string }>;
|
|
}
|
|
function ensureHookEntry(
|
|
eventName: string,
|
|
matcher: string,
|
|
timeout: number,
|
|
statusMessage: string,
|
|
) {
|
|
if (!existing.hooks[eventName]) existing.hooks[eventName] = [];
|
|
const hasHook = existing.hooks[eventName].some((h: HookEntry) =>
|
|
h.hooks?.some((hh) => hh.command?.includes('gitnexus-hook')),
|
|
);
|
|
if (!hasHook) {
|
|
existing.hooks[eventName].push({
|
|
matcher,
|
|
hooks: [{ type: 'command', command: hookCmd, timeout, statusMessage }],
|
|
});
|
|
}
|
|
}
|
|
|
|
ensureHookEntry('PreToolUse', 'Grep|Glob|Bash', 10, 'Enriching with GitNexus graph context...');
|
|
ensureHookEntry('PostToolUse', 'Bash', 10, 'Checking GitNexus index freshness...');
|
|
|
|
await writeJsonFile(settingsPath, existing);
|
|
result.configured.push('Claude Code hooks (PreToolUse, PostToolUse)');
|
|
} catch (err: any) {
|
|
result.errors.push(`Claude Code hooks: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function setupOpenCode(result: SetupResult): Promise<void> {
|
|
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
|
if (!(await dirExists(opencodeDir))) {
|
|
result.skipped.push('OpenCode (not installed)');
|
|
return;
|
|
}
|
|
|
|
const configPath = path.join(opencodeDir, 'opencode.json');
|
|
try {
|
|
const ok = await mergeJsoncFile(configPath, ['mcp', 'gitnexus'], getOpenCodeMcpEntry());
|
|
if (ok) {
|
|
result.configured.push('OpenCode');
|
|
} else {
|
|
result.errors.push(
|
|
'OpenCode: opencode.json is corrupt — skipping to preserve existing content',
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
result.errors.push(`OpenCode: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a TOML section for Codex MCP config (~/.codex/config.toml).
|
|
*/
|
|
function getCodexMcpTomlSection(): string {
|
|
const entry = getMcpEntry();
|
|
const command = JSON.stringify(entry.command);
|
|
const args = `[${entry.args.map((arg) => JSON.stringify(arg)).join(', ')}]`;
|
|
return `[mcp_servers.gitnexus]\ncommand = ${command}\nargs = ${args}\n`;
|
|
}
|
|
|
|
/**
|
|
* Append GitNexus MCP server config to Codex's config.toml if missing.
|
|
*/
|
|
async function upsertCodexConfigToml(configPath: string): Promise<void> {
|
|
let existing = '';
|
|
try {
|
|
existing = await fs.readFile(configPath, 'utf-8');
|
|
} catch {
|
|
existing = '';
|
|
}
|
|
|
|
if (existing.includes('[mcp_servers.gitnexus]')) {
|
|
return;
|
|
}
|
|
|
|
const section = getCodexMcpTomlSection();
|
|
const nextContent = existing.trim().length > 0 ? `${existing.trimEnd()}\n\n${section}` : section;
|
|
|
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
await fs.writeFile(configPath, `${nextContent.trimEnd()}\n`, 'utf-8');
|
|
}
|
|
|
|
async function setupCodex(result: SetupResult): Promise<void> {
|
|
const codexDir = path.join(os.homedir(), '.codex');
|
|
if (!(await dirExists(codexDir))) {
|
|
result.skipped.push('Codex (not installed)');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const entry = getMcpEntry();
|
|
await execFileAsync('codex', ['mcp', 'add', 'gitnexus', '--', entry.command, ...entry.args], {
|
|
shell: process.platform === 'win32',
|
|
});
|
|
result.configured.push('Codex');
|
|
return;
|
|
} catch {
|
|
// Fallback for environments where `codex` binary isn't on PATH.
|
|
}
|
|
|
|
try {
|
|
const configPath = path.join(codexDir, 'config.toml');
|
|
await upsertCodexConfigToml(configPath);
|
|
result.configured.push('Codex (MCP added to ~/.codex/config.toml)');
|
|
} catch (err: any) {
|
|
result.errors.push(`Codex: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// ─── Skill Installation ───────────────────────────────────────────
|
|
|
|
/**
|
|
* Install GitNexus skills to a target directory.
|
|
* Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md
|
|
* following the Agent Skills standard (Cursor, Claude Code, and Codex).
|
|
*
|
|
* Supports two source layouts:
|
|
* - Flat file: skills/{name}.md → copied as SKILL.md
|
|
* - Directory: skills/{name}/SKILL.md → copied recursively (includes references/, etc.)
|
|
*/
|
|
async function installSkillsTo(targetDir: string): Promise<string[]> {
|
|
const installed: string[] = [];
|
|
const skillsRoot = path.join(__dirname, '..', '..', 'skills');
|
|
|
|
let flatFiles: string[] = [];
|
|
let dirSkillFiles: string[] = [];
|
|
try {
|
|
[flatFiles, dirSkillFiles] = await Promise.all([
|
|
glob('*.md', { cwd: skillsRoot }),
|
|
glob('*/SKILL.md', { cwd: skillsRoot }),
|
|
]);
|
|
} catch {
|
|
return [];
|
|
}
|
|
|
|
const skillSources = new Map<string, { isDirectory: boolean }>();
|
|
|
|
for (const relPath of dirSkillFiles) {
|
|
skillSources.set(path.dirname(relPath), { isDirectory: true });
|
|
}
|
|
for (const relPath of flatFiles) {
|
|
const skillName = path.basename(relPath, '.md');
|
|
if (!skillSources.has(skillName)) {
|
|
skillSources.set(skillName, { isDirectory: false });
|
|
}
|
|
}
|
|
|
|
for (const [skillName, source] of skillSources) {
|
|
const skillDir = path.join(targetDir, skillName);
|
|
|
|
try {
|
|
if (source.isDirectory) {
|
|
const dirSource = path.join(skillsRoot, skillName);
|
|
await copyDirRecursive(dirSource, skillDir);
|
|
installed.push(skillName);
|
|
} else {
|
|
const flatSource = path.join(skillsRoot, `${skillName}.md`);
|
|
const content = await fs.readFile(flatSource, 'utf-8');
|
|
await fs.mkdir(skillDir, { recursive: true });
|
|
await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
|
|
installed.push(skillName);
|
|
}
|
|
} catch {
|
|
// Source skill not found — skip
|
|
}
|
|
}
|
|
|
|
return installed;
|
|
}
|
|
|
|
/**
|
|
* Recursively copy a directory tree.
|
|
*/
|
|
async function copyDirRecursive(src: string, dest: string): Promise<void> {
|
|
await fs.mkdir(dest, { recursive: true });
|
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const srcPath = path.join(src, entry.name);
|
|
const destPath = path.join(dest, entry.name);
|
|
if (entry.isDirectory()) {
|
|
await copyDirRecursive(srcPath, destPath);
|
|
} else {
|
|
await fs.copyFile(srcPath, destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install global Cursor skills to ~/.cursor/skills/gitnexus/
|
|
*/
|
|
async function installCursorSkills(result: SetupResult): Promise<void> {
|
|
const cursorDir = path.join(os.homedir(), '.cursor');
|
|
if (!(await dirExists(cursorDir))) return;
|
|
|
|
const skillsDir = path.join(cursorDir, 'skills');
|
|
try {
|
|
const installed = await installSkillsTo(skillsDir);
|
|
if (installed.length > 0) {
|
|
result.configured.push(`Cursor skills (${installed.length} skills → ~/.cursor/skills/)`);
|
|
}
|
|
} catch (err: any) {
|
|
result.errors.push(`Cursor skills: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install global OpenCode skills to ~/.config/opencode/skill/gitnexus/
|
|
*/
|
|
async function installOpenCodeSkills(result: SetupResult): Promise<void> {
|
|
const opencodeDir = path.join(os.homedir(), '.config', 'opencode');
|
|
if (!(await dirExists(opencodeDir))) return;
|
|
|
|
const skillsDir = path.join(opencodeDir, 'skill');
|
|
try {
|
|
const installed = await installSkillsTo(skillsDir);
|
|
if (installed.length > 0) {
|
|
result.configured.push(
|
|
`OpenCode skills (${installed.length} skills → ~/.config/opencode/skill/)`,
|
|
);
|
|
}
|
|
} catch (err: any) {
|
|
result.errors.push(`OpenCode skills: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install global Codex skills to ~/.agents/skills/gitnexus/
|
|
*/
|
|
async function installCodexSkills(result: SetupResult): Promise<void> {
|
|
const codexDir = path.join(os.homedir(), '.codex');
|
|
if (!(await dirExists(codexDir))) return;
|
|
|
|
const skillsDir = path.join(os.homedir(), '.agents', 'skills');
|
|
try {
|
|
const installed = await installSkillsTo(skillsDir);
|
|
if (installed.length > 0) {
|
|
result.configured.push(`Codex skills (${installed.length} skills → ~/.agents/skills/)`);
|
|
}
|
|
} catch (err: any) {
|
|
result.errors.push(`Codex skills: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// ─── Main command ──────────────────────────────────────────────────
|
|
|
|
export const setupCommand = async () => {
|
|
console.log('');
|
|
console.log(' GitNexus Setup');
|
|
console.log(' ==============');
|
|
console.log('');
|
|
|
|
// Ensure global directory exists
|
|
const globalDir = getGlobalDir();
|
|
await fs.mkdir(globalDir, { recursive: true });
|
|
|
|
const result: SetupResult = {
|
|
configured: [],
|
|
skipped: [],
|
|
errors: [],
|
|
};
|
|
|
|
// Detect and configure each editor's MCP
|
|
await setupCursor(result);
|
|
await setupClaudeCode(result);
|
|
await setupOpenCode(result);
|
|
await setupCodex(result);
|
|
|
|
// Install global skills for platforms that support them
|
|
await installClaudeCodeSkills(result);
|
|
await installClaudeCodeHooks(result);
|
|
await installCursorSkills(result);
|
|
await installOpenCodeSkills(result);
|
|
await installCodexSkills(result);
|
|
|
|
// Print results
|
|
if (result.configured.length > 0) {
|
|
console.log(' Configured:');
|
|
for (const name of result.configured) {
|
|
console.log(` + ${name}`);
|
|
}
|
|
}
|
|
|
|
if (result.skipped.length > 0) {
|
|
console.log('');
|
|
console.log(' Skipped:');
|
|
for (const name of result.skipped) {
|
|
console.log(` - ${name}`);
|
|
}
|
|
}
|
|
|
|
if (result.errors.length > 0) {
|
|
console.log('');
|
|
console.log(' Errors:');
|
|
for (const err of result.errors) {
|
|
console.log(` ! ${err}`);
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
console.log(' Summary:');
|
|
console.log(
|
|
` MCP configured for: ${result.configured.filter((c) => !c.includes('skills')).join(', ') || 'none'}`,
|
|
);
|
|
console.log(
|
|
` Skills installed to: ${result.configured.filter((c) => c.includes('skills')).length > 0 ? result.configured.filter((c) => c.includes('skills')).join(', ') : 'none'}`,
|
|
);
|
|
console.log('');
|
|
console.log(' Next steps:');
|
|
console.log(' 1. cd into any git repo');
|
|
console.log(' 2. Run: gitnexus analyze');
|
|
console.log(' 3. Open the repo in your editor — MCP is ready!');
|
|
console.log('');
|
|
};
|