directory.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. /**
  2. * Directory Management
  3. *
  4. * Manages the .codegraph/ directory structure for CodeGraph data.
  5. */
  6. import * as fs from 'fs';
  7. import * as path from 'path';
  8. /**
  9. * CodeGraph directory name
  10. */
  11. export const CODEGRAPH_DIR = '.codegraph';
  12. /**
  13. * Get the .codegraph directory path for a project
  14. */
  15. export function getCodeGraphDir(projectRoot: string): string {
  16. return path.join(projectRoot, CODEGRAPH_DIR);
  17. }
  18. /**
  19. * Check if a project has been initialized with CodeGraph
  20. * Requires both .codegraph/ directory AND codegraph.db to exist
  21. */
  22. export function isInitialized(projectRoot: string): boolean {
  23. const codegraphDir = getCodeGraphDir(projectRoot);
  24. if (!fs.existsSync(codegraphDir) || !fs.statSync(codegraphDir).isDirectory()) {
  25. return false;
  26. }
  27. // Must have codegraph.db, not just .codegraph folder
  28. const dbPath = path.join(codegraphDir, 'codegraph.db');
  29. return fs.existsSync(dbPath);
  30. }
  31. /**
  32. * Find the nearest parent directory containing .codegraph/
  33. *
  34. * Walks up from the given path to find a CodeGraph-initialized project,
  35. * similar to how git finds .git/ directories.
  36. *
  37. * @param startPath - Directory to start searching from
  38. * @returns The project root containing .codegraph/, or null if not found
  39. */
  40. export function findNearestCodeGraphRoot(startPath: string): string | null {
  41. let current = path.resolve(startPath);
  42. const root = path.parse(current).root;
  43. while (current !== root) {
  44. if (isInitialized(current)) {
  45. return current;
  46. }
  47. const parent = path.dirname(current);
  48. if (parent === current) break; // Reached filesystem root
  49. current = parent;
  50. }
  51. // Check root as well
  52. if (isInitialized(current)) {
  53. return current;
  54. }
  55. return null;
  56. }
  57. /**
  58. * Create the .codegraph directory structure
  59. * Note: Only throws if codegraph.db already exists, not just if .codegraph/ exists.
  60. */
  61. export function createDirectory(projectRoot: string): void {
  62. const codegraphDir = getCodeGraphDir(projectRoot);
  63. const dbPath = path.join(codegraphDir, 'codegraph.db');
  64. // Only throw if CodeGraph is actually initialized (db exists)
  65. // .codegraph/ folder alone is fine
  66. if (fs.existsSync(dbPath)) {
  67. throw new Error(`CodeGraph already initialized in ${projectRoot}`);
  68. }
  69. // Create main directory (if it doesn't exist)
  70. fs.mkdirSync(codegraphDir, { recursive: true });
  71. // Create .gitignore inside .codegraph (if it doesn't exist)
  72. const gitignorePath = path.join(codegraphDir, '.gitignore');
  73. if (!fs.existsSync(gitignorePath)) {
  74. const gitignoreContent = `# CodeGraph data files — local to each machine, not for committing.
  75. # Ignore everything in .codegraph/ except this file itself, so transient
  76. # files (the database, daemon.pid, sockets, logs) never show up in git.
  77. *
  78. !.gitignore
  79. `;
  80. fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
  81. }
  82. }
  83. /**
  84. * Remove the .codegraph directory
  85. */
  86. export function removeDirectory(projectRoot: string): void {
  87. const codegraphDir = getCodeGraphDir(projectRoot);
  88. if (!fs.existsSync(codegraphDir)) {
  89. return;
  90. }
  91. // Verify .codegraph is a real directory, not a symlink pointing elsewhere
  92. const lstat = fs.lstatSync(codegraphDir);
  93. if (lstat.isSymbolicLink()) {
  94. // Only remove the symlink itself, never follow it for recursive delete
  95. fs.unlinkSync(codegraphDir);
  96. return;
  97. }
  98. if (!lstat.isDirectory()) {
  99. // Not a directory - remove the single file
  100. fs.unlinkSync(codegraphDir);
  101. return;
  102. }
  103. // Recursively remove directory
  104. fs.rmSync(codegraphDir, { recursive: true, force: true });
  105. }
  106. /**
  107. * Get all files in the .codegraph directory
  108. */
  109. export function listDirectoryContents(projectRoot: string): string[] {
  110. const codegraphDir = getCodeGraphDir(projectRoot);
  111. if (!fs.existsSync(codegraphDir)) {
  112. return [];
  113. }
  114. const files: string[] = [];
  115. function walkDir(dir: string, prefix: string = ''): void {
  116. const entries = fs.readdirSync(dir, { withFileTypes: true });
  117. for (const entry of entries) {
  118. const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
  119. // Skip symlinks to prevent following links outside .codegraph
  120. if (entry.isSymbolicLink()) {
  121. continue;
  122. }
  123. if (entry.isDirectory()) {
  124. walkDir(path.join(dir, entry.name), relativePath);
  125. } else {
  126. files.push(relativePath);
  127. }
  128. }
  129. }
  130. walkDir(codegraphDir);
  131. return files;
  132. }
  133. /**
  134. * Get the total size of the .codegraph directory in bytes
  135. */
  136. export function getDirectorySize(projectRoot: string): number {
  137. const codegraphDir = getCodeGraphDir(projectRoot);
  138. if (!fs.existsSync(codegraphDir)) {
  139. return 0;
  140. }
  141. let totalSize = 0;
  142. function walkDir(dir: string): void {
  143. const entries = fs.readdirSync(dir, { withFileTypes: true });
  144. for (const entry of entries) {
  145. // Skip symlinks to prevent following links outside .codegraph
  146. if (entry.isSymbolicLink()) {
  147. continue;
  148. }
  149. const fullPath = path.join(dir, entry.name);
  150. if (entry.isDirectory()) {
  151. walkDir(fullPath);
  152. } else {
  153. const stats = fs.statSync(fullPath);
  154. totalSize += stats.size;
  155. }
  156. }
  157. }
  158. walkDir(codegraphDir);
  159. return totalSize;
  160. }
  161. /**
  162. * Ensure a subdirectory exists within .codegraph
  163. */
  164. export function ensureSubdirectory(projectRoot: string, subdirName: string): string {
  165. if (subdirName.includes('..') || subdirName.includes(path.sep) || subdirName.includes('/')) {
  166. throw new Error(`Invalid subdirectory name: ${subdirName}`);
  167. }
  168. const subdirPath = path.join(getCodeGraphDir(projectRoot), subdirName);
  169. if (!fs.existsSync(subdirPath)) {
  170. fs.mkdirSync(subdirPath, { recursive: true });
  171. }
  172. return subdirPath;
  173. }
  174. /**
  175. * Check if the .codegraph directory has valid structure
  176. */
  177. export function validateDirectory(projectRoot: string): {
  178. valid: boolean;
  179. errors: string[];
  180. } {
  181. const errors: string[] = [];
  182. const codegraphDir = getCodeGraphDir(projectRoot);
  183. if (!fs.existsSync(codegraphDir)) {
  184. errors.push('CodeGraph directory does not exist');
  185. return { valid: false, errors };
  186. }
  187. if (!fs.statSync(codegraphDir).isDirectory()) {
  188. errors.push('.codegraph exists but is not a directory');
  189. return { valid: false, errors };
  190. }
  191. // Auto-repair missing .gitignore (non-critical file)
  192. const gitignorePath = path.join(codegraphDir, '.gitignore');
  193. if (!fs.existsSync(gitignorePath)) {
  194. try {
  195. const gitignoreContent = `# CodeGraph data files — local to each machine, not for committing.\n# Ignore everything in .codegraph/ except this file itself, so transient\n# files (the database, daemon.pid, sockets, logs) never show up in git.\n*\n!.gitignore\n`;
  196. fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
  197. } catch {
  198. // Non-fatal: warn but don't block
  199. errors.push('.gitignore missing in .codegraph directory and could not be created');
  200. }
  201. }
  202. return {
  203. valid: errors.length === 0,
  204. errors,
  205. };
  206. }