@claude-vector/core
Version:
Core vector search engine for code intelligence
301 lines (261 loc) • 8.28 kB
JavaScript
/**
* 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;
}
}