/** * Config file writing for the CodeGraph installer * Writes to claude.json, settings.json, and CLAUDE.md */ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { InstallLocation } from './prompts'; import { CLAUDE_MD_TEMPLATE, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END, } from './claude-md-template'; /** * Get the path to the Claude config directory */ function getClaudeConfigDir(location: InstallLocation): string { if (location === 'global') { return path.join(os.homedir(), '.claude'); } return path.join(process.cwd(), '.claude'); } /** * Get the path to the claude.json file * - Global: ~/.claude.json (root level) * - Local: ./.claude.json (project root) */ function getClaudeJsonPath(location: InstallLocation): string { if (location === 'global') { return path.join(os.homedir(), '.claude.json'); } return path.join(process.cwd(), '.claude.json'); } /** * Get the path to the settings.json file * - Global: ~/.claude/settings.json * - Local: ./.claude/settings.json */ function getSettingsJsonPath(location: InstallLocation): string { const configDir = getClaudeConfigDir(location); return path.join(configDir, 'settings.json'); } /** * Read a JSON file, returning an empty object if it doesn't exist */ function readJsonFile(filePath: string): Record { try { if (fs.existsSync(filePath)) { const content = fs.readFileSync(filePath, 'utf-8'); return JSON.parse(content); } } catch { // Ignore parse errors, return empty object } return {}; } /** * Write a JSON file, creating parent directories if needed */ function writeJsonFile(filePath: string, data: Record): void { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); } /** * Get the MCP server configuration for the given location */ function getMcpServerConfig(location: InstallLocation): Record { if (location === 'global') { // Global: use 'codegraph' command directly (assumes globally installed) return { type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'], }; } // Local: use npx to run the package return { type: 'stdio', command: 'npx', args: ['@colbymchenry/codegraph', 'serve', '--mcp'], }; } /** * Write the MCP server configuration to claude.json */ export function writeMcpConfig(location: InstallLocation): void { const claudeJsonPath = getClaudeJsonPath(location); const config = readJsonFile(claudeJsonPath); // Ensure mcpServers object exists if (!config.mcpServers) { config.mcpServers = {}; } // Add or update codegraph server config.mcpServers.codegraph = getMcpServerConfig(location); writeJsonFile(claudeJsonPath, config); } /** * Get the list of permissions for CodeGraph tools */ function getCodeGraphPermissions(): string[] { return [ 'mcp__codegraph__codegraph_search', 'mcp__codegraph__codegraph_context', 'mcp__codegraph__codegraph_callers', 'mcp__codegraph__codegraph_callees', 'mcp__codegraph__codegraph_impact', 'mcp__codegraph__codegraph_node', 'mcp__codegraph__codegraph_status', ]; } /** * Write permissions to settings.json */ export function writePermissions(location: InstallLocation): void { const settingsPath = getSettingsJsonPath(location); const settings = readJsonFile(settingsPath); // Ensure permissions object exists if (!settings.permissions) { settings.permissions = {}; } // Ensure allow array exists if (!Array.isArray(settings.permissions.allow)) { settings.permissions.allow = []; } // Add CodeGraph permissions (avoiding duplicates) const codegraphPermissions = getCodeGraphPermissions(); for (const permission of codegraphPermissions) { if (!settings.permissions.allow.includes(permission)) { settings.permissions.allow.push(permission); } } writeJsonFile(settingsPath, settings); } /** * Check if MCP config already exists for CodeGraph */ export function hasMcpConfig(location: InstallLocation): boolean { const claudeJsonPath = getClaudeJsonPath(location); const config = readJsonFile(claudeJsonPath); return !!config.mcpServers?.codegraph; } /** * Check if permissions already exist for CodeGraph */ export function hasPermissions(location: InstallLocation): boolean { const settingsPath = getSettingsJsonPath(location); const settings = readJsonFile(settingsPath); const permissions = settings.permissions?.allow; if (!Array.isArray(permissions)) { return false; } // Check if at least one CodeGraph permission exists return permissions.some((p: string) => p.startsWith('mcp__codegraph__')); } // ============================================================================= // Hooks Configuration // ============================================================================= /** * Get the hooks configuration for Claude Code auto-sync. * * PostToolUse(Edit|Write) → mark-dirty (async, non-blocking) * Stop → sync-if-dirty (sync, ensures fresh index before next user turn) */ function getHooksConfig(location: InstallLocation): Record { const command = location === 'global' ? 'codegraph' : 'npx @colbymchenry/codegraph'; return { PostToolUse: [ { matcher: 'Edit|Write', hooks: [ { type: 'command', command: `${command} mark-dirty`, async: true, }, ], }, ], Stop: [ { hooks: [ { type: 'command', command: `${command} sync-if-dirty`, }, ], }, ], }; } /** * Check if Claude Code hooks already exist for CodeGraph */ export function hasHooks(location: InstallLocation): boolean { const settingsPath = getSettingsJsonPath(location); const settings = readJsonFile(settingsPath); const hooks = settings.hooks; if (!hooks) return false; // Check if any hook command references codegraph const json = JSON.stringify(hooks); return json.includes('codegraph mark-dirty') || json.includes('codegraph sync-if-dirty'); } /** * Write Claude Code hooks to settings.json for auto-sync. * Merges with existing hooks, deduplicating any previous codegraph entries. */ export function writeHooks(location: InstallLocation): void { const settingsPath = getSettingsJsonPath(location); const settings = readJsonFile(settingsPath); if (!settings.hooks) { settings.hooks = {}; } const newHooks = getHooksConfig(location); // For each hook event (PostToolUse, Stop), merge with existing entries for (const [event, newEntries] of Object.entries(newHooks)) { if (!Array.isArray(settings.hooks[event])) { settings.hooks[event] = []; } // Remove any existing codegraph entries for this event settings.hooks[event] = (settings.hooks[event] as any[]).filter((entry: any) => { // Keep entries that don't reference codegraph const entryJson = JSON.stringify(entry); return !entryJson.includes('codegraph mark-dirty') && !entryJson.includes('codegraph sync-if-dirty'); }); // Add new codegraph entries settings.hooks[event].push(...(newEntries as any[])); } writeJsonFile(settingsPath, settings); } /** * Get the path to CLAUDE.md * - Global: ~/.claude/CLAUDE.md * - Local: ./.claude/CLAUDE.md */ function getClaudeMdPath(location: InstallLocation): string { const configDir = getClaudeConfigDir(location); return path.join(configDir, 'CLAUDE.md'); } /** * Check if CLAUDE.md has CodeGraph section */ export function hasClaudeMdSection(location: InstallLocation): boolean { const claudeMdPath = getClaudeMdPath(location); try { if (fs.existsSync(claudeMdPath)) { const content = fs.readFileSync(claudeMdPath, 'utf-8'); return content.includes(CODEGRAPH_SECTION_START) || content.includes('## CodeGraph'); } } catch { // Ignore errors } return false; } /** * Write or update CLAUDE.md with CodeGraph instructions * * If the file exists and has a CodeGraph section (marked or unmarked), * it will be replaced. Otherwise, the template is appended. */ export function writeClaudeMd(location: InstallLocation): { created: boolean; updated: boolean } { const claudeMdPath = getClaudeMdPath(location); const configDir = getClaudeConfigDir(location); // Ensure directory exists if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // Check if file exists if (!fs.existsSync(claudeMdPath)) { // Create new file with just the CodeGraph section fs.writeFileSync(claudeMdPath, CLAUDE_MD_TEMPLATE + '\n'); return { created: true, updated: false }; } // Read existing content let content = fs.readFileSync(claudeMdPath, 'utf-8'); // Check for marked section (from previous installer) if (content.includes(CODEGRAPH_SECTION_START)) { // Replace the marked section const startIdx = content.indexOf(CODEGRAPH_SECTION_START); const endIdx = content.indexOf(CODEGRAPH_SECTION_END); if (endIdx > startIdx) { // Replace existing marked section const before = content.substring(0, startIdx); const after = content.substring(endIdx + CODEGRAPH_SECTION_END.length); content = before + CLAUDE_MD_TEMPLATE + after; fs.writeFileSync(claudeMdPath, content); return { created: false, updated: true }; } } // Check for unmarked "## CodeGraph" section (from manual setup) const codegraphHeaderRegex = /\n## CodeGraph\n/; const match = content.match(codegraphHeaderRegex); if (match && match.index !== undefined) { // Find the end of the CodeGraph section (next ## header or end of file) const sectionStart = match.index; const afterSection = content.substring(sectionStart + 1); const nextHeaderMatch = afterSection.match(/\n## [^#]/); let sectionEnd: number; if (nextHeaderMatch && nextHeaderMatch.index !== undefined) { sectionEnd = sectionStart + 1 + nextHeaderMatch.index; } else { sectionEnd = content.length; } // Replace the section const before = content.substring(0, sectionStart); const after = content.substring(sectionEnd); content = before + '\n' + CLAUDE_MD_TEMPLATE + after; fs.writeFileSync(claudeMdPath, content); return { created: false, updated: true }; } // No existing section, append to end content = content.trimEnd() + '\n\n' + CLAUDE_MD_TEMPLATE + '\n'; fs.writeFileSync(claudeMdPath, content); return { created: false, updated: false }; }