config.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /**
  2. * Configuration Management
  3. *
  4. * Load, save, and validate CodeGraph configuration.
  5. */
  6. import * as fs from 'fs';
  7. import * as path from 'path';
  8. import { CodeGraphConfig, DEFAULT_CONFIG, Language, NodeKind } from './types';
  9. /**
  10. * Configuration filename
  11. */
  12. export const CONFIG_FILENAME = 'config.json';
  13. /**
  14. * Get the config file path for a project
  15. */
  16. export function getConfigPath(projectRoot: string): string {
  17. return path.join(projectRoot, '.codegraph', CONFIG_FILENAME);
  18. }
  19. /**
  20. * Validate a configuration object
  21. */
  22. export function validateConfig(config: unknown): config is CodeGraphConfig {
  23. if (typeof config !== 'object' || config === null) {
  24. return false;
  25. }
  26. const c = config as Record<string, unknown>;
  27. // Required fields
  28. if (typeof c.version !== 'number') return false;
  29. if (typeof c.rootDir !== 'string') return false;
  30. if (!Array.isArray(c.include)) return false;
  31. if (!Array.isArray(c.exclude)) return false;
  32. if (!Array.isArray(c.languages)) return false;
  33. if (!Array.isArray(c.frameworks)) return false;
  34. if (typeof c.maxFileSize !== 'number') return false;
  35. if (typeof c.extractDocstrings !== 'boolean') return false;
  36. if (typeof c.trackCallSites !== 'boolean') return false;
  37. if (typeof c.enableEmbeddings !== 'boolean') return false;
  38. // Validate include/exclude are string arrays
  39. if (!c.include.every((p) => typeof p === 'string')) return false;
  40. if (!c.exclude.every((p) => typeof p === 'string')) return false;
  41. // Validate languages
  42. const validLanguages: Language[] = [
  43. 'typescript',
  44. 'javascript',
  45. 'python',
  46. 'go',
  47. 'rust',
  48. 'java',
  49. 'unknown',
  50. ];
  51. if (!c.languages.every((l) => validLanguages.includes(l as Language))) return false;
  52. // Validate frameworks
  53. for (const fw of c.frameworks) {
  54. if (typeof fw !== 'object' || fw === null) return false;
  55. const framework = fw as Record<string, unknown>;
  56. if (typeof framework.name !== 'string') return false;
  57. }
  58. // Validate custom patterns if present
  59. if (c.customPatterns !== undefined) {
  60. if (!Array.isArray(c.customPatterns)) return false;
  61. for (const pattern of c.customPatterns) {
  62. if (typeof pattern !== 'object' || pattern === null) return false;
  63. const p = pattern as Record<string, unknown>;
  64. if (typeof p.name !== 'string') return false;
  65. if (typeof p.pattern !== 'string') return false;
  66. if (typeof p.kind !== 'string') return false;
  67. }
  68. }
  69. return true;
  70. }
  71. /**
  72. * Merge configuration with defaults
  73. */
  74. function mergeConfig(
  75. defaults: CodeGraphConfig,
  76. overrides: Partial<CodeGraphConfig>
  77. ): CodeGraphConfig {
  78. return {
  79. version: overrides.version ?? defaults.version,
  80. rootDir: overrides.rootDir ?? defaults.rootDir,
  81. include: overrides.include ?? defaults.include,
  82. exclude: overrides.exclude ?? defaults.exclude,
  83. languages: overrides.languages ?? defaults.languages,
  84. frameworks: overrides.frameworks ?? defaults.frameworks,
  85. maxFileSize: overrides.maxFileSize ?? defaults.maxFileSize,
  86. extractDocstrings: overrides.extractDocstrings ?? defaults.extractDocstrings,
  87. trackCallSites: overrides.trackCallSites ?? defaults.trackCallSites,
  88. enableEmbeddings: overrides.enableEmbeddings ?? defaults.enableEmbeddings,
  89. customPatterns: overrides.customPatterns ?? defaults.customPatterns,
  90. };
  91. }
  92. /**
  93. * Load configuration from a project
  94. */
  95. export function loadConfig(projectRoot: string): CodeGraphConfig {
  96. const configPath = getConfigPath(projectRoot);
  97. if (!fs.existsSync(configPath)) {
  98. // Return default config with adjusted rootDir
  99. return {
  100. ...DEFAULT_CONFIG,
  101. rootDir: projectRoot,
  102. };
  103. }
  104. try {
  105. const content = fs.readFileSync(configPath, 'utf-8');
  106. const parsed = JSON.parse(content) as unknown;
  107. // Merge with defaults to ensure all fields are present
  108. const merged = mergeConfig(DEFAULT_CONFIG, parsed as Partial<CodeGraphConfig>);
  109. merged.rootDir = projectRoot; // Always use actual project root
  110. if (!validateConfig(merged)) {
  111. throw new Error('Invalid configuration format');
  112. }
  113. return merged;
  114. } catch (error) {
  115. if (error instanceof SyntaxError) {
  116. throw new Error(`Invalid JSON in config file: ${configPath}`);
  117. }
  118. throw error;
  119. }
  120. }
  121. /**
  122. * Save configuration to a project
  123. */
  124. export function saveConfig(projectRoot: string, config: CodeGraphConfig): void {
  125. const configPath = getConfigPath(projectRoot);
  126. const dir = path.dirname(configPath);
  127. // Ensure directory exists
  128. if (!fs.existsSync(dir)) {
  129. fs.mkdirSync(dir, { recursive: true });
  130. }
  131. // Create a copy without rootDir (it's always derived from project path)
  132. const toSave = { ...config };
  133. delete (toSave as Partial<CodeGraphConfig>).rootDir;
  134. const content = JSON.stringify(toSave, null, 2);
  135. // Atomic write: write to temp file then rename to prevent partial/corrupt configs
  136. const tmpPath = configPath + '.tmp';
  137. fs.writeFileSync(tmpPath, content, 'utf-8');
  138. fs.renameSync(tmpPath, configPath);
  139. }
  140. /**
  141. * Create default configuration for a new project
  142. */
  143. export function createDefaultConfig(projectRoot: string): CodeGraphConfig {
  144. return {
  145. ...DEFAULT_CONFIG,
  146. rootDir: projectRoot,
  147. };
  148. }
  149. /**
  150. * Update specific configuration values
  151. */
  152. export function updateConfig(
  153. projectRoot: string,
  154. updates: Partial<CodeGraphConfig>
  155. ): CodeGraphConfig {
  156. const current = loadConfig(projectRoot);
  157. const updated = mergeConfig(current, updates);
  158. updated.rootDir = projectRoot;
  159. saveConfig(projectRoot, updated);
  160. return updated;
  161. }
  162. /**
  163. * Add patterns to include list
  164. */
  165. export function addIncludePatterns(projectRoot: string, patterns: string[]): CodeGraphConfig {
  166. const config = loadConfig(projectRoot);
  167. const newPatterns = patterns.filter((p) => !config.include.includes(p));
  168. config.include = [...config.include, ...newPatterns];
  169. saveConfig(projectRoot, config);
  170. return config;
  171. }
  172. /**
  173. * Add patterns to exclude list
  174. */
  175. export function addExcludePatterns(projectRoot: string, patterns: string[]): CodeGraphConfig {
  176. const config = loadConfig(projectRoot);
  177. const newPatterns = patterns.filter((p) => !config.exclude.includes(p));
  178. config.exclude = [...config.exclude, ...newPatterns];
  179. saveConfig(projectRoot, config);
  180. return config;
  181. }
  182. /**
  183. * Add a custom pattern
  184. */
  185. export function addCustomPattern(
  186. projectRoot: string,
  187. name: string,
  188. pattern: string,
  189. kind: NodeKind
  190. ): CodeGraphConfig {
  191. const config = loadConfig(projectRoot);
  192. if (!config.customPatterns) {
  193. config.customPatterns = [];
  194. }
  195. // Check for duplicate name
  196. const existing = config.customPatterns.find((p) => p.name === name);
  197. if (existing) {
  198. existing.pattern = pattern;
  199. existing.kind = kind;
  200. } else {
  201. config.customPatterns.push({ name, pattern, kind });
  202. }
  203. saveConfig(projectRoot, config);
  204. return config;
  205. }
  206. /**
  207. * Check if a file path matches the include/exclude patterns
  208. */
  209. export function shouldIncludeFile(filePath: string, config: CodeGraphConfig): boolean {
  210. // Simple glob matching (for now, just check if any pattern matches)
  211. // A full implementation would use a proper glob library
  212. const matchesPattern = (pattern: string, path: string): boolean => {
  213. // Convert glob to regex (simplified)
  214. const regexStr = pattern
  215. .replace(/\./g, '\\.')
  216. .replace(/\*\*/g, '.*')
  217. .replace(/\*/g, '[^/]*')
  218. .replace(/\?/g, '.');
  219. const regex = new RegExp(`^${regexStr}$`);
  220. return regex.test(path);
  221. };
  222. // Check exclude patterns first
  223. for (const pattern of config.exclude) {
  224. if (matchesPattern(pattern, filePath)) {
  225. return false;
  226. }
  227. }
  228. // Check include patterns
  229. for (const pattern of config.include) {
  230. if (matchesPattern(pattern, filePath)) {
  231. return true;
  232. }
  233. }
  234. // Default to not including if no pattern matches
  235. return false;
  236. }