/** * Directory Management * * Manages the .codegraph/ directory structure for CodeGraph data. */ import * as fs from 'fs'; import * as path from 'path'; /** * CodeGraph directory name */ export const CODEGRAPH_DIR = '.codegraph'; /** * Get the .codegraph directory path for a project */ export function getCodeGraphDir(projectRoot: string): string { return path.join(projectRoot, CODEGRAPH_DIR); } /** * Check if a project has been initialized with CodeGraph * Requires both .codegraph/ directory AND codegraph.db to exist */ export function isInitialized(projectRoot: string): boolean { const codegraphDir = getCodeGraphDir(projectRoot); if (!fs.existsSync(codegraphDir) || !fs.statSync(codegraphDir).isDirectory()) { return false; } // Must have codegraph.db, not just .codegraph folder const dbPath = path.join(codegraphDir, 'codegraph.db'); return fs.existsSync(dbPath); } /** * Find the nearest parent directory containing .codegraph/ * * Walks up from the given path to find a CodeGraph-initialized project, * similar to how git finds .git/ directories. * * @param startPath - Directory to start searching from * @returns The project root containing .codegraph/, or null if not found */ export function findNearestCodeGraphRoot(startPath: string): string | null { let current = path.resolve(startPath); const root = path.parse(current).root; while (current !== root) { if (isInitialized(current)) { return current; } const parent = path.dirname(current); if (parent === current) break; // Reached filesystem root current = parent; } // Check root as well if (isInitialized(current)) { return current; } return null; } /** * Create the .codegraph directory structure * Note: Only throws if codegraph.db already exists, not just if .codegraph/ exists. */ export function createDirectory(projectRoot: string): void { const codegraphDir = getCodeGraphDir(projectRoot); const dbPath = path.join(codegraphDir, 'codegraph.db'); // Only throw if CodeGraph is actually initialized (db exists) // .codegraph/ folder alone is fine if (fs.existsSync(dbPath)) { throw new Error(`CodeGraph already initialized in ${projectRoot}`); } // Create main directory (if it doesn't exist) fs.mkdirSync(codegraphDir, { recursive: true }); // Create .gitignore inside .codegraph (if it doesn't exist) const gitignorePath = path.join(codegraphDir, '.gitignore'); if (!fs.existsSync(gitignorePath)) { const gitignoreContent = `# CodeGraph data files # These are local to each machine and should not be committed # Database *.db *.db-wal *.db-shm # Cache cache/ # Logs *.log # Hook markers .dirty `; fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8'); } } /** * Remove the .codegraph directory */ export function removeDirectory(projectRoot: string): void { const codegraphDir = getCodeGraphDir(projectRoot); if (!fs.existsSync(codegraphDir)) { return; } // Verify .codegraph is a real directory, not a symlink pointing elsewhere const lstat = fs.lstatSync(codegraphDir); if (lstat.isSymbolicLink()) { // Only remove the symlink itself, never follow it for recursive delete fs.unlinkSync(codegraphDir); return; } if (!lstat.isDirectory()) { // Not a directory - remove the single file fs.unlinkSync(codegraphDir); return; } // Recursively remove directory fs.rmSync(codegraphDir, { recursive: true, force: true }); } /** * Get all files in the .codegraph directory */ export function listDirectoryContents(projectRoot: string): string[] { const codegraphDir = getCodeGraphDir(projectRoot); if (!fs.existsSync(codegraphDir)) { return []; } const files: string[] = []; function walkDir(dir: string, prefix: string = ''): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; // Skip symlinks to prevent following links outside .codegraph if (entry.isSymbolicLink()) { continue; } if (entry.isDirectory()) { walkDir(path.join(dir, entry.name), relativePath); } else { files.push(relativePath); } } } walkDir(codegraphDir); return files; } /** * Get the total size of the .codegraph directory in bytes */ export function getDirectorySize(projectRoot: string): number { const codegraphDir = getCodeGraphDir(projectRoot); if (!fs.existsSync(codegraphDir)) { return 0; } let totalSize = 0; function walkDir(dir: string): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { // Skip symlinks to prevent following links outside .codegraph if (entry.isSymbolicLink()) { continue; } const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walkDir(fullPath); } else { const stats = fs.statSync(fullPath); totalSize += stats.size; } } } walkDir(codegraphDir); return totalSize; } /** * Ensure a subdirectory exists within .codegraph */ export function ensureSubdirectory(projectRoot: string, subdirName: string): string { if (subdirName.includes('..') || subdirName.includes(path.sep) || subdirName.includes('/')) { throw new Error(`Invalid subdirectory name: ${subdirName}`); } const subdirPath = path.join(getCodeGraphDir(projectRoot), subdirName); if (!fs.existsSync(subdirPath)) { fs.mkdirSync(subdirPath, { recursive: true }); } return subdirPath; } /** * Check if the .codegraph directory has valid structure */ export function validateDirectory(projectRoot: string): { valid: boolean; errors: string[]; } { const errors: string[] = []; const codegraphDir = getCodeGraphDir(projectRoot); if (!fs.existsSync(codegraphDir)) { errors.push('CodeGraph directory does not exist'); return { valid: false, errors }; } if (!fs.statSync(codegraphDir).isDirectory()) { errors.push('.codegraph exists but is not a directory'); return { valid: false, errors }; } // Auto-repair missing .gitignore (non-critical file) const gitignorePath = path.join(codegraphDir, '.gitignore'); if (!fs.existsSync(gitignorePath)) { try { const gitignoreContent = `# CodeGraph data files\n# These are local to each machine and should not be committed\n\n# Database\n*.db\n*.db-wal\n*.db-shm\n\n# Cache\ncache/\n\n# Logs\n*.log\n\n# Hook markers\n.dirty\n`; fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8'); } catch { // Non-fatal: warn but don't block errors.push('.gitignore missing in .codegraph directory and could not be created'); } } return { valid: errors.length === 0, errors, }; }