UNPKG

superaugment

Version:

Enterprise-grade MCP server with world-class C++ analysis, robust error handling, and production-ready architecture for VS Code Augment

515 lines 18.9 kB
import { readFile, readdir, stat, access, writeFile, mkdir } from 'fs/promises'; import { join, relative, extname, basename, dirname, normalize } from 'path'; import { glob } from 'glob'; import { logger } from './logger.js'; import { FileCache } from './FileCache.js'; import { FileSystemError, ErrorCode, } from '../errors/ErrorTypes.js'; /** * Enhanced file system manager with caching, security, and performance optimizations */ export class FileSystemManager { maxFileSize = 10 * 1024 * 1024; // 10MB fileCache; securityEnabled = true; performanceMonitoring = true; allowedExtensions = new Set([ '.js', '.ts', '.jsx', '.tsx', '.vue', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h', '.css', '.scss', '.sass', '.less', '.html', '.xml', '.json', '.yaml', '.yml', '.md', '.sql', '.sh', '.bat', '.ps1', '.dockerfile', '.gitignore', '.env' ]); constructor(options = {}) { this.maxFileSize = options.maxFileSize || this.maxFileSize; this.securityEnabled = options.enableSecurity ?? true; this.performanceMonitoring = options.enablePerformanceMonitoring ?? true; // Initialize file cache if enabled if (options.enableCache !== false) { this.fileCache = new FileCache(options.cacheOptions); logger.info('File system manager initialized with caching enabled'); } else { // Create a no-op cache for consistency this.fileCache = new FileCache({ maxMemoryUsage: 0, maxEntries: 0 }); logger.info('File system manager initialized without caching'); } } /** * Enhanced file reading with caching and security checks */ async readFileContent(filePath) { const startTime = Date.now(); try { // Security check if (this.securityEnabled) { await this.validateFilePath(filePath); } // Use cache for file reading const content = await this.fileCache.getFile(filePath); // Performance monitoring if (this.performanceMonitoring) { const duration = Date.now() - startTime; logger.debug(`File read completed: ${filePath}`, { duration, size: Buffer.byteLength(content, 'utf-8'), cached: this.fileCache.has(filePath), }); } return content; } catch (error) { throw new FileSystemError(`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`, filePath, ErrorCode.FILE_READ_ERROR, { additionalInfo: { duration: Date.now() - startTime } }, error instanceof Error ? error : undefined); } } /** * Secure file writing with directory creation */ async writeFileContent(filePath, content) { const startTime = Date.now(); try { // Security check if (this.securityEnabled) { await this.validateFilePath(filePath); } // Ensure directory exists const dir = dirname(filePath); await mkdir(dir, { recursive: true }); // Write file await writeFile(filePath, content, 'utf-8'); // Invalidate cache entry if it exists this.fileCache.invalidate(filePath); // Performance monitoring if (this.performanceMonitoring) { const duration = Date.now() - startTime; logger.debug(`File write completed: ${filePath}`, { duration, size: Buffer.byteLength(content, 'utf-8'), }); } } catch (error) { throw new FileSystemError(`Failed to write file: ${error instanceof Error ? error.message : 'Unknown error'}`, filePath, ErrorCode.FILE_WRITE_ERROR, { additionalInfo: { duration: Date.now() - startTime } }, error instanceof Error ? error : undefined); } } /** * Validate file path for security */ async validateFilePath(filePath) { const normalizedPath = normalize(filePath); // Check for path traversal attempts if (normalizedPath.includes('..')) { throw new FileSystemError('Path traversal detected in file path', filePath, ErrorCode.PERMISSION_DENIED, { additionalInfo: { normalizedPath } }); } // Check file extension const ext = extname(normalizedPath).toLowerCase(); if (ext && !this.allowedExtensions.has(ext)) { throw new FileSystemError(`File extension not allowed: ${ext}`, filePath, ErrorCode.PERMISSION_DENIED, { additionalInfo: { extension: ext, allowedExtensions: Array.from(this.allowedExtensions) } }); } // Check if file exists and get stats try { const stats = await stat(normalizedPath); // Check file size if (stats.size > this.maxFileSize) { throw new FileSystemError(`File too large: ${stats.size} bytes (max: ${this.maxFileSize})`, filePath, ErrorCode.FILE_TOO_LARGE, { additionalInfo: { fileSize: stats.size, maxSize: this.maxFileSize } }); } } catch (error) { if (error.code === 'ENOENT') { // File doesn't exist, which is okay for write operations return; } throw error; } } /** * Read files matching glob patterns */ async readFiles(patterns, rootPath = process.cwd()) { try { logger.info('Reading files with patterns:', { patterns, rootPath }); const files = []; for (const pattern of patterns) { const matchedFiles = await glob(pattern, { cwd: rootPath, ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**', 'coverage/**'], absolute: false, }); for (const filePath of matchedFiles) { const fullPath = join(rootPath, filePath); const fileInfo = await this.getFileInfo(fullPath, rootPath); if (fileInfo && this.isAllowedFile(fileInfo)) { files.push(fileInfo); } } } logger.info(`Read ${files.length} files`); return files; } catch (error) { logger.error('Failed to read files:', error); throw error; } } /** * Read a single file with enhanced caching and error handling */ async readFile(filePath, rootPath = process.cwd()) { try { const fullPath = join(rootPath, filePath); return await this.getFileInfo(fullPath, rootPath); } catch (error) { logger.error(`Failed to read file ${filePath}:`, error); return null; } } /** * Analyze project structure */ async analyzeProjectStructure(rootPath = process.cwd()) { try { logger.info('Analyzing project structure:', { rootPath }); const structure = { rootPath, files: [], directories: [], }; // Read package.json if exists try { const packageJsonPath = join(rootPath, 'package.json'); await access(packageJsonPath); const packageContent = await readFile(packageJsonPath, 'utf-8'); structure.packageJson = JSON.parse(packageContent); } catch { // package.json doesn't exist, that's fine } // Detect framework and language structure.framework = await this.detectFramework(rootPath, structure.packageJson); structure.language = await this.detectPrimaryLanguage(rootPath); // Get project files const patterns = ['**/*']; structure.files = await this.readFiles(patterns, rootPath); // Get directories structure.directories = await this.getDirectories(rootPath); logger.info('Project structure analyzed:', { framework: structure.framework, language: structure.language, fileCount: structure.files.length, dirCount: structure.directories.length, }); return structure; } catch (error) { logger.error('Failed to analyze project structure:', error); throw error; } } /** * Get file information */ async getFileInfo(fullPath, rootPath) { try { const stats = await stat(fullPath); if (stats.isDirectory()) { return null; } if (stats.size > this.maxFileSize) { logger.warn(`File too large, skipping: ${fullPath} (${stats.size} bytes)`); return null; } const relativePath = relative(rootPath, fullPath); const extension = extname(fullPath); const fileInfo = { path: fullPath, relativePath, size: stats.size, isDirectory: false, extension, }; // Read content for text files using enhanced file reading if (this.isTextFile(extension)) { try { fileInfo.content = await this.readFileContent(fullPath); } catch (error) { logger.warn(`Failed to read file content: ${fullPath}`, error); } } return fileInfo; } catch (error) { logger.error(`Failed to get file info for ${fullPath}:`, error); return null; } } /** * Check if file is allowed */ isAllowedFile(fileInfo) { // Skip hidden files and directories if (basename(fileInfo.relativePath).startsWith('.') && !fileInfo.relativePath.includes('.env') && !fileInfo.relativePath.includes('.gitignore')) { return false; } // Check extension return this.allowedExtensions.has(fileInfo.extension) || fileInfo.extension === ''; } /** * Check if file is text file */ isTextFile(extension) { const textExtensions = new Set([ '.js', '.ts', '.jsx', '.tsx', '.vue', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h', '.css', '.scss', '.sass', '.less', '.html', '.xml', '.json', '.yaml', '.yml', '.md', '.sql', '.sh', '.bat', '.ps1', '.txt', '.env', '.gitignore' ]); return textExtensions.has(extension) || extension === ''; } /** * Detect project framework */ async detectFramework(rootPath, packageJson) { if (packageJson?.dependencies || packageJson?.devDependencies) { const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; if (deps.react) return 'React'; if (deps.vue) return 'Vue.js'; if (deps.angular || deps['@angular/core']) return 'Angular'; if (deps.next) return 'Next.js'; if (deps.nuxt) return 'Nuxt.js'; if (deps.express) return 'Express.js'; if (deps.fastify) return 'Fastify'; if (deps.nestjs || deps['@nestjs/core']) return 'NestJS'; } // Check for framework-specific files try { await access(join(rootPath, 'angular.json')); return 'Angular'; } catch { } try { await access(join(rootPath, 'nuxt.config.js')); return 'Nuxt.js'; } catch { } try { await access(join(rootPath, 'next.config.js')); return 'Next.js'; } catch { } return undefined; } /** * Detect primary programming language */ async detectPrimaryLanguage(rootPath) { try { const files = await glob('**/*.{js,ts,py,java,go,rs,cpp,c}', { cwd: rootPath, ignore: ['node_modules/**', 'dist/**', 'build/**'], }); const langCounts = {}; for (const file of files) { const ext = extname(file); const lang = this.extensionToLanguage(ext); if (lang) { langCounts[lang] = (langCounts[lang] || 0) + 1; } } // Return the most common language const sortedLangs = Object.entries(langCounts).sort(([, a], [, b]) => b - a); return sortedLangs[0]?.[0]; } catch { return undefined; } } /** * Map file extension to language */ extensionToLanguage(ext) { const mapping = { '.js': 'JavaScript', '.ts': 'TypeScript', '.jsx': 'JavaScript', '.tsx': 'TypeScript', '.py': 'Python', '.java': 'Java', '.go': 'Go', '.rs': 'Rust', '.cpp': 'C++', '.c': 'C', }; return mapping[ext]; } /** * Get directories in project */ async getDirectories(rootPath) { try { const items = await readdir(rootPath, { withFileTypes: true }); const directories = items .filter(item => item.isDirectory()) .map(item => item.name) .filter(name => !name.startsWith('.') && !['node_modules', 'dist', 'build', 'coverage'].includes(name)); return directories; } catch { return []; } } /** * Get cache statistics */ getCacheStats() { return this.fileCache.getStats(); } /** * Clear file cache */ clearCache() { this.fileCache.clear(); logger.info('File system cache cleared'); } /** * Invalidate specific file in cache */ invalidateFile(filePath) { return this.fileCache.invalidate(filePath); } /** * Check if file exists */ async fileExists(filePath) { try { await access(filePath); return true; } catch { return false; } } /** * Get file stats safely */ async getFileStats(filePath) { try { const stats = await stat(filePath); return { size: stats.size, mtime: stats.mtime, }; } catch { return null; } } /** * Find files by pattern with caching */ async findFiles(pattern, rootPath = process.cwd()) { try { const files = await glob(pattern, { cwd: rootPath, ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'], absolute: true, }); return files.filter(file => { const ext = extname(file).toLowerCase(); return this.allowedExtensions.has(ext) || ext === ''; }); } catch (error) { throw new FileSystemError(`Failed to find files with pattern: ${pattern}`, pattern, ErrorCode.FILE_READ_ERROR, { additionalInfo: { rootPath } }, error instanceof Error ? error : undefined); } } /** * Get file content with specific encoding */ async readFileWithEncoding(filePath, encoding = 'utf-8') { try { if (this.securityEnabled) { await this.validateFilePath(filePath); } // For non-UTF8 encodings, bypass cache and read directly if (encoding !== 'utf-8') { const content = await readFile(filePath, encoding); return content; } // Use cache for UTF-8 files return await this.readFileContent(filePath); } catch (error) { throw new FileSystemError(`Failed to read file with encoding ${encoding}: ${error instanceof Error ? error.message : 'Unknown error'}`, filePath, ErrorCode.FILE_READ_ERROR, { additionalInfo: { encoding } }, error instanceof Error ? error : undefined); } } /** * Batch read multiple files efficiently */ async readMultipleFiles(filePaths) { const results = new Map(); const errors = []; // Process files in parallel with concurrency limit const concurrency = 10; for (let i = 0; i < filePaths.length; i += concurrency) { const batch = filePaths.slice(i, i + concurrency); const batchPromises = batch.map(async (filePath) => { try { const content = await this.readFileContent(filePath); results.set(filePath, content); } catch (error) { errors.push(`${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`); } }); await Promise.all(batchPromises); } if (errors.length > 0) { logger.warn(`Failed to read ${errors.length} files:`, { errors: errors.slice(0, 5) }); } logger.info(`Successfully read ${results.size}/${filePaths.length} files`); return results; } /** * Update file system manager options */ updateOptions(options) { if (options.maxFileSize !== undefined) { this.maxFileSize = options.maxFileSize; } if (options.enableSecurity !== undefined) { this.securityEnabled = options.enableSecurity; } if (options.enablePerformanceMonitoring !== undefined) { this.performanceMonitoring = options.enablePerformanceMonitoring; } logger.info('File system manager options updated', { options }); } /** * Get health status of file system manager */ getHealthStatus() { return { cacheEnabled: this.fileCache.getStats().maxMemoryUsage > 0, securityEnabled: this.securityEnabled, performanceMonitoring: this.performanceMonitoring, maxFileSize: this.maxFileSize, cacheStats: this.fileCache.getStats(), }; } /** * Cleanup resources */ cleanup() { this.fileCache.cleanup(); logger.info('File system manager cleaned up'); } } //# sourceMappingURL=FileSystemManager.js.map