UNPKG

@claude-vector/core

Version:

Core vector search engine for code intelligence

301 lines (261 loc) 8.28 kB
/** * Project adapter for analyzing and configuring different project types */ import fs from 'fs/promises'; import path from 'path'; import micromatch from 'micromatch'; export class ProjectAdapter { constructor(projectPath = process.cwd()) { this.projectPath = projectPath; this.projectInfo = null; } /** * Analyze project structure and detect project type */ async analyzeProject() { const info = { path: this.projectPath, type: 'unknown', language: 'javascript', framework: null, features: [], config: {} }; try { // Check for package.json const packageJsonPath = path.join(this.projectPath, 'package.json'); const packageJson = await this.readJsonFile(packageJsonPath); if (packageJson) { info.name = packageJson.name; info.version = packageJson.version; info.dependencies = packageJson.dependencies || {}; info.devDependencies = packageJson.devDependencies || {}; // Detect framework if (packageJson.dependencies?.next || packageJson.devDependencies?.next) { info.type = 'nextjs'; info.framework = 'next'; // Check for app directory const hasAppDir = await this.pathExists(path.join(this.projectPath, 'app')); const hasPagesDir = await this.pathExists(path.join(this.projectPath, 'pages')); info.features.push(hasAppDir ? 'app-router' : 'pages-router'); } else if (packageJson.dependencies?.react || packageJson.devDependencies?.react) { info.type = 'react'; info.framework = 'react'; } else if (packageJson.dependencies?.vue || packageJson.devDependencies?.vue) { info.type = 'vue'; info.framework = 'vue'; } else if (packageJson.dependencies?.express) { info.type = 'node'; info.framework = 'express'; } // Detect TypeScript if (packageJson.devDependencies?.typescript) { info.language = 'typescript'; info.features.push('typescript'); } // Detect testing framework if (packageJson.devDependencies?.jest) { info.features.push('jest'); } if (packageJson.devDependencies?.vitest) { info.features.push('vitest'); } } // Check for config files const tsConfigPath = path.join(this.projectPath, 'tsconfig.json'); if (await this.pathExists(tsConfigPath)) { info.config.tsconfig = await this.readJsonFile(tsConfigPath); } // Check for Claude search config const claudeConfigPath = path.join(this.projectPath, '.claude-search.config.js'); if (await this.pathExists(claudeConfigPath)) { info.config.claudeSearch = claudeConfigPath; } this.projectInfo = info; return info; } catch (error) { console.error('Error analyzing project:', error); this.projectInfo = info; return info; } } /** * Get default configuration based on project type */ getDefaultConfig() { const baseConfig = { include: ['**/*.{js,jsx,ts,tsx,md,mdx}'], exclude: [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**', '**/.git/**', '**/tmp/**', '**/*.min.js', '**/*.bundle.js' ], chunk: { maxSize: 1000, overlap: 200, minSize: 100 }, embeddings: { model: 'text-embedding-3-small', batchSize: 100 } }; if (!this.projectInfo) { return baseConfig; } // Customize based on project type switch (this.projectInfo.type) { case 'nextjs': baseConfig.include.push('app/**/*', 'pages/**/*', 'components/**/*'); baseConfig.exclude.push('.next/**', 'out/**'); break; case 'react': baseConfig.include.push('src/**/*', 'components/**/*'); break; case 'vue': baseConfig.include.push('**/*.vue', 'src/**/*', 'components/**/*'); break; case 'node': baseConfig.include.push('lib/**/*', 'src/**/*', 'routes/**/*'); break; } // Add test files if testing framework detected if (this.projectInfo.features.includes('jest') || this.projectInfo.features.includes('vitest')) { baseConfig.include.push('**/*.test.{js,jsx,ts,tsx}', '**/*.spec.{js,jsx,ts,tsx}'); } return baseConfig; } /** * Load custom configuration from file */ async loadCustomConfig() { const configPaths = [ '.claude-search.config.js', '.claude-search.config.json', 'claude-search.config.js', 'claude-search.config.json' ]; for (const configPath of configPaths) { const fullPath = path.join(this.projectPath, configPath); if (await this.pathExists(fullPath)) { try { if (configPath.endsWith('.js')) { // Dynamic import for ES modules const module = await import(fullPath); return module.default || module; } else { return await this.readJsonFile(fullPath); } } catch (error) { console.error(`Error loading config from ${configPath}:`, error); } } } return null; } /** * Get final configuration by merging default and custom configs */ async getConfig() { if (!this.projectInfo) { await this.analyzeProject(); } const defaultConfig = this.getDefaultConfig(); const customConfig = await this.loadCustomConfig(); if (!customConfig) { return defaultConfig; } // Deep merge configurations return this.mergeConfigs(defaultConfig, customConfig); } /** * Get all files matching the configuration patterns */ async getFiles(config = null) { const finalConfig = config || await this.getConfig(); const allFiles = await this.walkDirectory(this.projectPath); // Filter files based on include/exclude patterns const includedFiles = micromatch(allFiles, finalConfig.include, { dot: true, basename: true }); const filteredFiles = micromatch.not(includedFiles, finalConfig.exclude, { dot: true, basename: true }); return filteredFiles.map(file => ({ path: file, relativePath: path.relative(this.projectPath, file), absolute: path.resolve(this.projectPath, file) })); } /** * Utility: Check if path exists */ async pathExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Utility: Read JSON file safely */ async readJsonFile(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { return null; } } /** * Utility: Walk directory recursively */ async walkDirectory(dir, fileList = []) { try { const files = await fs.readdir(dir); for (const file of files) { const filePath = path.join(dir, file); const stats = await fs.stat(filePath); if (stats.isDirectory()) { // Skip common directories to improve performance if (!['node_modules', '.git', '.next', 'dist', 'build'].includes(file)) { await this.walkDirectory(filePath, fileList); } } else { fileList.push(filePath); } } } catch (error) { console.error(`Error walking directory ${dir}:`, error); } return fileList; } /** * Utility: Deep merge two objects */ mergeConfigs(base, custom) { const merged = { ...base }; for (const key in custom) { if (Object.prototype.hasOwnProperty.call(custom, key)) { if (Array.isArray(custom[key])) { merged[key] = custom[key]; } else if (typeof custom[key] === 'object' && custom[key] !== null) { merged[key] = this.mergeConfigs(base[key] || {}, custom[key]); } else { merged[key] = custom[key]; } } } return merged; } }