| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- /**
- * 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.
- * Distinguishes between missing files (returns {}) and corrupted
- * files (logs warning, returns {}).
- */
- function readJsonFile(filePath: string): Record<string, any> {
- if (!fs.existsSync(filePath)) {
- return {};
- }
- try {
- const content = fs.readFileSync(filePath, 'utf-8');
- return JSON.parse(content);
- } catch (err) {
- const msg = err instanceof Error ? err.message : String(err);
- console.warn(` Warning: Could not parse ${path.basename(filePath)}: ${msg}`);
- console.warn(` A backup will be created before overwriting.`);
- // Create a backup of the corrupted file
- try {
- const backupPath = filePath + '.backup';
- fs.copyFileSync(filePath, backupPath);
- } catch { /* ignore backup failure */ }
- return {};
- }
- }
- /**
- * Write a file atomically by writing to a temp file then renaming.
- * Prevents corruption if the process crashes mid-write.
- */
- function atomicWriteFileSync(filePath: string, content: string): void {
- const dir = path.dirname(filePath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
- const tmpPath = filePath + '.tmp.' + process.pid;
- try {
- fs.writeFileSync(tmpPath, content);
- fs.renameSync(tmpPath, filePath);
- } catch (err) {
- // Clean up temp file on failure
- try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
- throw err;
- }
- }
- /**
- * Write a JSON file, creating parent directories if needed
- */
- function writeJsonFile(filePath: string, data: Record<string, any>): void {
- atomicWriteFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
- }
- /**
- * When true, all configs use `npx @colbymchenry/codegraph` instead of the
- * bare `codegraph` command. Set by the installer when global install fails.
- */
- let useNpxFallback = false;
- export function setUseNpxFallback(value: boolean): void {
- useNpxFallback = value;
- }
- /**
- * Get the MCP server configuration for the given location
- */
- function getMcpServerConfig(location: InstallLocation): Record<string, any> {
- if (location === 'global' && !useNpxFallback) {
- // Global: use 'codegraph' command directly (globally installed and in PATH)
- return {
- type: 'stdio',
- command: 'codegraph',
- args: ['serve', '--mcp'],
- };
- }
- // Local or npx fallback: 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<string, any> {
- const command = (location === 'global' && !useNpxFallback) ? 'codegraph' : 'npx @colbymchenry/codegraph';
- return {
- PostToolUse: [
- {
- matcher: 'Edit|Write',
- hooks: [
- {
- type: 'command',
- command: `${command} mark-dirty`,
- async: true,
- },
- ],
- },
- ],
- Stop: [
- {
- matcher: '.*',
- 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
- atomicWriteFileSync(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;
- atomicWriteFileSync(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 h2 header or end of file)
- // Use negative lookahead (?!#) to match "## X" but not "### X"
- 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;
- atomicWriteFileSync(claudeMdPath, content);
- return { created: false, updated: true };
- }
- // No existing section, append to end
- content = content.trimEnd() + '\n\n' + CLAUDE_MD_TEMPLATE + '\n';
- atomicWriteFileSync(claudeMdPath, content);
- return { created: false, updated: false };
- }
|