1
0

directory.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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
  75. # These are local to each machine and should not be committed
  76. # Database
  77. *.db
  78. *.db-wal
  79. *.db-shm
  80. # Cache
  81. cache/
  82. # Logs
  83. *.log
  84. # Hook markers
  85. .dirty
  86. `;
  87. fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
  88. }
  89. }
  90. /**
  91. * Remove the .codegraph directory
  92. */
  93. export function removeDirectory(projectRoot: string): void {
  94. const codegraphDir = getCodeGraphDir(projectRoot);
  95. if (!fs.existsSync(codegraphDir)) {
  96. return;
  97. }
  98. // Verify .codegraph is a real directory, not a symlink pointing elsewhere
  99. const lstat = fs.lstatSync(codegraphDir);
  100. if (lstat.isSymbolicLink()) {
  101. // Only remove the symlink itself, never follow it for recursive delete
  102. fs.unlinkSync(codegraphDir);
  103. return;
  104. }
  105. if (!lstat.isDirectory()) {
  106. // Not a directory - remove the single file
  107. fs.unlinkSync(codegraphDir);
  108. return;
  109. }
  110. // Recursively remove directory
  111. fs.rmSync(codegraphDir, { recursive: true, force: true });
  112. }
  113. /**
  114. * Get all files in the .codegraph directory
  115. */
  116. export function listDirectoryContents(projectRoot: string): string[] {
  117. const codegraphDir = getCodeGraphDir(projectRoot);
  118. if (!fs.existsSync(codegraphDir)) {
  119. return [];
  120. }
  121. const files: string[] = [];
  122. function walkDir(dir: string, prefix: string = ''): void {
  123. const entries = fs.readdirSync(dir, { withFileTypes: true });
  124. for (const entry of entries) {
  125. const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
  126. // Skip symlinks to prevent following links outside .codegraph
  127. if (entry.isSymbolicLink()) {
  128. continue;
  129. }
  130. if (entry.isDirectory()) {
  131. walkDir(path.join(dir, entry.name), relativePath);
  132. } else {
  133. files.push(relativePath);
  134. }
  135. }
  136. }
  137. walkDir(codegraphDir);
  138. return files;
  139. }
  140. /**
  141. * Get the total size of the .codegraph directory in bytes
  142. */
  143. export function getDirectorySize(projectRoot: string): number {
  144. const codegraphDir = getCodeGraphDir(projectRoot);
  145. if (!fs.existsSync(codegraphDir)) {
  146. return 0;
  147. }
  148. let totalSize = 0;
  149. function walkDir(dir: string): void {
  150. const entries = fs.readdirSync(dir, { withFileTypes: true });
  151. for (const entry of entries) {
  152. // Skip symlinks to prevent following links outside .codegraph
  153. if (entry.isSymbolicLink()) {
  154. continue;
  155. }
  156. const fullPath = path.join(dir, entry.name);
  157. if (entry.isDirectory()) {
  158. walkDir(fullPath);
  159. } else {
  160. const stats = fs.statSync(fullPath);
  161. totalSize += stats.size;
  162. }
  163. }
  164. }
  165. walkDir(codegraphDir);
  166. return totalSize;
  167. }
  168. /**
  169. * Ensure a subdirectory exists within .codegraph
  170. */
  171. export function ensureSubdirectory(projectRoot: string, subdirName: string): string {
  172. if (subdirName.includes('..') || subdirName.includes(path.sep) || subdirName.includes('/')) {
  173. throw new Error(`Invalid subdirectory name: ${subdirName}`);
  174. }
  175. const subdirPath = path.join(getCodeGraphDir(projectRoot), subdirName);
  176. if (!fs.existsSync(subdirPath)) {
  177. fs.mkdirSync(subdirPath, { recursive: true });
  178. }
  179. return subdirPath;
  180. }
  181. /**
  182. * Check if the .codegraph directory has valid structure
  183. */
  184. export function validateDirectory(projectRoot: string): {
  185. valid: boolean;
  186. errors: string[];
  187. } {
  188. const errors: string[] = [];
  189. const codegraphDir = getCodeGraphDir(projectRoot);
  190. if (!fs.existsSync(codegraphDir)) {
  191. errors.push('CodeGraph directory does not exist');
  192. return { valid: false, errors };
  193. }
  194. if (!fs.statSync(codegraphDir).isDirectory()) {
  195. errors.push('.codegraph exists but is not a directory');
  196. return { valid: false, errors };
  197. }
  198. // Auto-repair missing .gitignore (non-critical file)
  199. const gitignorePath = path.join(codegraphDir, '.gitignore');
  200. if (!fs.existsSync(gitignorePath)) {
  201. try {
  202. 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`;
  203. fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
  204. } catch {
  205. // Non-fatal: warn but don't block
  206. errors.push('.gitignore missing in .codegraph directory and could not be created');
  207. }
  208. }
  209. return {
  210. valid: errors.length === 0,
  211. errors,
  212. };
  213. }