261 lines
6.7 KiB
TypeScript
261 lines
6.7 KiB
TypeScript
/**
|
|
* 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,
|
|
};
|
|
}
|