ProjectContextManager.js•18.9 kB
"use strict";
/**
* ProjectContextManager
*
* Manages project-level context including:
* - Project structure analysis
* - Framework and dependency detection
* - Git history and recent changes
* - Context caching
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectContextManager = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const glob_1 = require("glob");
const simple_git_1 = require("simple-git");
const chokidar = __importStar(require("chokidar"));
const lodash_1 = require("lodash");
const Logger_1 = __importDefault(require("../utils/Logger"));
// Framework detection patterns
const FRAMEWORK_PATTERNS = {
react: ['react', 'jsx', 'tsx', 'createContext', 'useState', 'useEffect'],
vue: ['vue', 'createApp', 'defineComponent', '<template>', '<script setup>'],
angular: ['@angular', 'NgModule', 'Component', 'Injectable'],
nextjs: ['next.config', 'getStaticProps', 'getServerSideProps', '_app.tsx', 'pages/'],
express: ['express', 'app.use', 'app.get', 'app.post', 'Router()'],
nestjs: ['@nestjs', '@Controller', '@Injectable', '@Module'],
};
class ProjectContextManager {
constructor(config = {}) {
this.context = null;
this.lastUpdateTime = 0;
this.fileWatcher = null;
this.git = null;
this.log = Logger_1.default.createChildLogger('ProjectContextManager');
this.config = {
projectPath: config.projectPath || process.cwd(),
ignorePatterns: config.ignorePatterns || [
'node_modules',
'dist',
'.git',
'build',
'coverage',
'*.log',
],
cacheTTL: config.cacheTTL || 900, // 15 minutes by default
maxContextSize: config.maxContextSize || 10000,
};
try {
this.git = (0, simple_git_1.simpleGit)(this.config.projectPath);
}
catch (error) {
this.log.warn('Git integration not available', error);
}
this.setupFileWatcher();
}
/**
* Get the project context, refreshing if needed
*/
async getContext(forceRefresh = false) {
const now = Date.now();
const cacheExpired = now - this.lastUpdateTime > this.config.cacheTTL * 1000;
if (!this.context || forceRefresh || cacheExpired) {
this.log.info('Refreshing project context...');
this.context = await this.analyzeProject();
this.lastUpdateTime = now;
}
return this.context;
}
/**
* Analyze the entire project and build context
*/
async analyzeProject() {
try {
const projectPath = this.config.projectPath;
// Get project name from package.json or directory name
let projectName = path.basename(projectPath);
const packageJsonPath = path.join(projectPath, 'package.json');
// Initialize context structure
const context = {
timestamp: Date.now(),
projectName,
frameworks: [],
dependencies: {},
devDependencies: {},
fileStructure: { directories: {}, files: [] },
recentChanges: [],
branchInfo: { currentBranch: '', branches: [], remotes: [] },
patterns: [],
};
// Read package.json if exists
if (await fs.pathExists(packageJsonPath)) {
try {
const packageJson = await fs.readJson(packageJsonPath);
context.projectName = packageJson.name || projectName;
context.dependencies = packageJson.dependencies || {};
context.devDependencies = packageJson.devDependencies || {};
// Infer frameworks from dependencies
context.frameworks = this.detectFrameworks(packageJson);
}
catch (error) {
this.log.error('Error parsing package.json', error);
}
}
// Build file structure
context.fileStructure = await this.buildFileStructure(projectPath);
// Get git information if available
if (this.git) {
try {
// Get recent changes
const gitLog = await this.git.log({ maxCount: 10 });
context.recentChanges = gitLog.all.map(commit => {
return {
file: '', // Will be filled in with diff info in a production version
status: 'committed',
date: new Date(commit.date).toISOString(),
author: commit.author_name,
message: commit.message,
};
});
// Get branch info
const branches = await this.git.branchLocal();
const remotes = await this.git.getRemotes(true);
context.branchInfo = {
currentBranch: branches.current,
branches: branches.all,
remotes: remotes.map(remote => `${remote.name}: ${remote.refs.fetch}`),
};
}
catch (error) {
this.log.warn('Error getting git information', error);
}
}
// Detect code patterns
context.patterns = await this.detectCodePatterns(projectPath, context.frameworks);
this.log.info(`Project context refreshed for ${context.projectName}`);
return context;
}
catch (error) {
this.log.error('Error analyzing project', error);
throw new Error(`Failed to analyze project: ${error.message}`);
}
}
/**
* Detect frameworks used in the project
*/
detectFrameworks(packageJson) {
const frameworks = [];
const allDependencies = {
...(packageJson.dependencies || {}),
...(packageJson.devDependencies || {})
};
// Check dependencies
for (const [framework, patterns] of Object.entries(FRAMEWORK_PATTERNS)) {
const hasFramework = patterns.some(pattern => Object.keys(allDependencies).some(dep => dep.includes(pattern)));
if (hasFramework) {
frameworks.push(framework);
}
}
// Check for custom configuration files
const configFiles = Object.keys(packageJson.scripts || {}).join(' ');
if (!frameworks.includes('nextjs') && configFiles.includes('next')) {
frameworks.push('nextjs');
}
if (!frameworks.includes('react') && (configFiles.includes('react-scripts') || configFiles.includes('vite'))) {
frameworks.push('react');
}
if (!frameworks.includes('vue') && configFiles.includes('vue-cli-service')) {
frameworks.push('vue');
}
return frameworks;
}
/**
* Build file structure recursively
*/
async buildFileStructure(dirPath, isRoot = true) {
const structure = { directories: {}, files: [] };
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(dirPath, entry.name);
// Skip ignored patterns
if (isRoot && this.shouldIgnore(entry.name)) {
continue;
}
if (entry.isDirectory()) {
// Process subdirectory
structure.directories[entry.name] = await this.buildFileStructure(entryPath, false);
}
else {
// Add file
structure.files.push(entry.name);
}
}
}
catch (error) {
this.log.warn(`Error reading directory ${dirPath}`, error);
}
return structure;
}
/**
* Check if a path should be ignored
*/
shouldIgnore(pathName) {
return this.config.ignorePatterns.some(pattern => {
// Exact match
if (pathName === pattern) {
return true;
}
// Directory match
if (pathName.startsWith(pattern + '/')) {
return true;
}
// Handle glob patterns safely
try {
// Escape special regex characters except * which we convert to .*
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
.replace(/\*/g, '.*'); // Convert * to .*
return new RegExp(`^${regexPattern}$`).test(pathName);
}
catch (error) {
this.log.warn(`Invalid pattern: ${pattern}`, error);
return false;
}
});
}
/**
* Detect code patterns in the project
*/
async detectCodePatterns(projectPath, frameworks) {
const patterns = [];
try {
// Add framework-specific pattern detection here
if (frameworks.includes('react') || frameworks.includes('nextjs')) {
// React component patterns
patterns.push(await this.detectReactPatterns(projectPath));
}
if (frameworks.includes('express')) {
// Express route patterns
patterns.push(await this.detectExpressPatterns(projectPath));
}
// Generic patterns (functions, classes, etc.)
patterns.push(...await this.detectGenericPatterns(projectPath));
}
catch (error) {
this.log.warn('Error detecting code patterns', error);
}
return patterns.filter(Boolean);
}
/**
* Detect React component patterns
*/
async detectReactPatterns(projectPath) {
const pattern = {
type: 'component',
pattern: 'React component definition',
examples: []
};
try {
// Find React component files
const files = await (0, glob_1.glob)('**/*.{jsx,tsx}', {
cwd: projectPath,
ignore: this.config.ignorePatterns
});
// Sample a few files
const sampleFiles = files.slice(0, 3);
for (const file of sampleFiles) {
const filePath = path.join(projectPath, file);
const content = await fs.readFile(filePath, 'utf-8');
// Extract component definition
const componentMatch = content.match(/function\s+(\w+)\s*\([^)]*\)\s*{|const\s+(\w+)\s*=\s*\([^)]*\)\s*=>/);
if (componentMatch) {
const snippet = this.extractCodeSnippet(content, componentMatch.index || 0);
if (snippet) {
pattern.examples.push(snippet);
}
}
}
}
catch (error) {
this.log.warn('Error detecting React patterns', error);
}
return pattern;
}
/**
* Detect Express route patterns
*/
async detectExpressPatterns(projectPath) {
const pattern = {
type: 'route',
pattern: 'Express route definition',
examples: []
};
try {
// Find Express route files
const files = await (0, glob_1.glob)('**/*.{js,ts}', {
cwd: projectPath,
ignore: this.config.ignorePatterns
});
// Sample a few files
const sampleFiles = files.slice(0, 3);
for (const file of sampleFiles) {
const filePath = path.join(projectPath, file);
const content = await fs.readFile(filePath, 'utf-8');
// Extract route definition
const routeMatch = content.match(/app\.(get|post|put|delete)\s*\(['"]\//);
if (routeMatch) {
const snippet = this.extractCodeSnippet(content, routeMatch.index || 0);
if (snippet) {
pattern.examples.push(snippet);
}
}
}
}
catch (error) {
this.log.warn('Error detecting Express patterns', error);
}
return pattern;
}
/**
* Detect generic code patterns
*/
async detectGenericPatterns(projectPath) {
const patterns = [];
try {
// Function pattern
const functionPattern = {
type: 'function',
pattern: 'Function definition',
examples: []
};
// Class pattern
const classPattern = {
type: 'class',
pattern: 'Class definition',
examples: []
};
// Find all code files
const files = await (0, glob_1.glob)('**/*.{js,ts,jsx,tsx}', {
cwd: projectPath,
ignore: this.config.ignorePatterns
});
// Sample a few files
const sampleFiles = files.slice(0, 3);
for (const file of sampleFiles) {
const filePath = path.join(projectPath, file);
const content = await fs.readFile(filePath, 'utf-8');
// Find function definitions
const functionMatches = content.matchAll(/function\s+(\w+)\s*\([^)]*\)\s*{/g);
for (const match of functionMatches) {
const snippet = this.extractCodeSnippet(content, match.index || 0);
if (snippet && functionPattern.examples.length < 3) {
functionPattern.examples.push(snippet);
}
}
// Find class definitions
const classMatches = content.matchAll(/class\s+(\w+)(\s+extends\s+\w+)?\s*{/g);
for (const match of classMatches) {
const snippet = this.extractCodeSnippet(content, match.index || 0);
if (snippet && classPattern.examples.length < 3) {
classPattern.examples.push(snippet);
}
}
}
if (functionPattern.examples.length > 0) {
patterns.push(functionPattern);
}
if (classPattern.examples.length > 0) {
patterns.push(classPattern);
}
}
catch (error) {
this.log.warn('Error detecting generic patterns', error);
}
return patterns;
}
/**
* Extract a code snippet from the content starting at the given index
*/
extractCodeSnippet(content, startIndex) {
try {
// Get up to 10 lines
const endOfContent = content.indexOf('\n', startIndex + 200) || content.length;
const snippet = content.substring(startIndex, endOfContent);
// Get a few lines
const lines = snippet.split('\n').slice(0, 10);
return lines.join('\n').trim();
}
catch (error) {
return null;
}
}
/**
* Set up file watcher to trigger context updates
*/
setupFileWatcher() {
try {
// Create debounced update function
const debouncedUpdate = (0, lodash_1.debounce)(() => {
this.log.debug('Files changed, invalidating context cache');
this.lastUpdateTime = 0; // Invalidate cache
}, 5000); // Debounce for 5 seconds
// Set up file watcher
const ignored = this.config.ignorePatterns.map(pattern => new RegExp(`(^|/)${pattern.replace(/\*/g, '.*')}(/|$)`));
this.fileWatcher = chokidar.watch(this.config.projectPath, {
ignored,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 1000,
pollInterval: 100
}
});
this.fileWatcher
.on('add', debouncedUpdate)
.on('change', debouncedUpdate)
.on('unlink', debouncedUpdate)
.on('ready', () => {
this.log.debug('File watcher initialized');
})
.on('error', (error) => {
this.log.error('File watcher error', error);
});
}
catch (error) {
this.log.warn('Error setting up file watcher', error);
}
}
/**
* Clean up resources
*/
dispose() {
if (this.fileWatcher) {
this.fileWatcher.close().catch(error => {
this.log.warn('Error closing file watcher', error);
});
}
}
}
exports.ProjectContextManager = ProjectContextManager;
exports.default = ProjectContextManager;
//# sourceMappingURL=ProjectContextManager.js.map