UNPKG

devibe

Version:

Intelligent repository cleanup with auto mode, AI learning, markdown consolidation, auto-consolidate workflow, context-aware classification, and cost optimization

370 lines 15.2 kB
import * as fs from 'fs/promises'; import * as path from 'path'; export class OperationPlanner { gitDetector; fileClassifier; usageDetector; testOrganizer; constructor(gitDetector, fileClassifier, usageDetector, testOrganizer) { this.gitDetector = gitDetector; this.fileClassifier = fileClassifier; this.usageDetector = usageDetector; this.testOrganizer = testOrganizer; } async planRootFileDistribution(rootPath, onProgress) { const operations = []; const warnings = []; const gitResult = await this.gitDetector.detectRepositories(rootPath); if (!gitResult.rootRepo) { return { operations: [], backupRequired: false, estimatedDuration: 0, warnings: [], }; } // Find root-level files (excluding common system files) const entries = await fs.readdir(rootPath, { withFileTypes: true }); const rootFiles = entries.filter((e) => e.isFile() && !e.name.startsWith('.') && e.name !== 'package.json' && e.name !== 'package-lock.json' && e.name !== 'tsconfig.json' && e.name !== 'README.md' && // Keep main README at root e.name !== 'LICENSE' && // Keep license at root e.name !== '.gitignore'); const totalFiles = rootFiles.length; let currentFile = 0; for (const file of rootFiles) { currentFile++; if (onProgress) { onProgress(currentFile, totalFiles, file.name); } const filePath = path.join(rootPath, file.name); const ext = path.extname(file.name); // Fast path for markdown files - they almost always go to documents/ if (ext === '.md') { const targetRepo = gitResult.rootRepo; operations.push({ type: 'move', sourcePath: filePath, targetPath: path.join(targetRepo.path, 'documents', file.name), reason: 'Markdown documentation file', isReferenced: false, }); continue; // Skip AI analysis } // Fast path for JSON files - categorize and handle appropriately if (ext === '.json') { const lowerName = file.name.toLowerCase(); // Keep essential config files at root if (lowerName === 'version.json' || lowerName === 'package.json' || lowerName === 'tsconfig.json' || lowerName.endsWith('config.json')) { continue; // Skip - keep at root } // Test output files (reports, results, dumps) - delete by default if (lowerName.includes('test') || lowerName.includes('report') || lowerName.includes('result') || lowerName.includes('dump') || lowerName.includes('diagnostic') || lowerName.includes('analysis') || lowerName.endsWith('-output.json')) { operations.push({ type: 'delete', sourcePath: filePath, reason: `Test output/report file (pattern: *${lowerName.match(/(test|report|result|dump|diagnostic|analysis)/)?.[0]}*.json)`, isReferenced: false, }); continue; // Skip AI analysis } // Other JSON files - might be fixtures or data, move to tests/fixtures/ operations.push({ type: 'move', sourcePath: filePath, targetPath: path.join(gitResult.rootRepo.path, 'tests', 'fixtures', file.name), reason: 'JSON data/fixture file', isReferenced: false, }); continue; // Skip AI analysis } // Fast path for obvious test/utility scripts const lowerName = file.name.toLowerCase(); if ((ext === '.js' || ext === '.ts') && (lowerName.startsWith('test-') || lowerName.startsWith('check-') || lowerName.startsWith('debug-') || lowerName.startsWith('cleanup-') || lowerName.startsWith('verify-') || lowerName.startsWith('cache-') || lowerName.startsWith('query-'))) { // These are utility/test scripts - offer to delete if not referenced let isReferenced = false; if (this.usageDetector) { try { const usageResult = await this.usageDetector.checkFileUsage(filePath, [rootPath]); isReferenced = usageResult.isReferenced; if (isReferenced) { warnings.push(`⚠️ ${file.name} is still referenced - keeping`); } } catch { } } if (!isReferenced) { operations.push({ type: 'delete', sourcePath: filePath, reason: `Utility script (pattern: ${lowerName.split('-')[0]}-*)`, isReferenced: false, }); } else { // Still referenced, move to tests/ const targetRepo = gitResult.rootRepo; operations.push({ type: 'move', sourcePath: filePath, targetPath: path.join(targetRepo.path, 'tests', file.name), reason: `Test/utility script still in use`, warning: 'Still referenced', isReferenced: true, }); } continue; // Skip AI analysis } // For everything else, use normal classification let content; try { const stats = await fs.stat(filePath); if (stats.size < 100000) { // Only read files < 100KB content = await fs.readFile(filePath, 'utf-8'); } } catch { // Can't read file, continue without content } const classification = await this.fileClassifier.classify(filePath, content); // Check if file is still being used (if usage detector is available) // Only check for files that might be deleted (utility scripts) let isReferenced = false; let usageWarning; if (this.usageDetector && this.isUtilityFile(file.name, classification)) { try { const startTime = Date.now(); if (onProgress) { // Show we're checking usage const tempProgress = `(checking usage...)`; } const usageResult = await this.usageDetector.checkFileUsage(filePath, [rootPath] // Search entire repository ); const checkTime = Date.now() - startTime; isReferenced = usageResult.isReferenced; if (isReferenced) { usageWarning = `Still referenced in ${usageResult.references.length} file(s)`; warnings.push(`⚠️ ${file.name} is still referenced - recommend keeping (check took ${checkTime}ms)`); } } catch { // Usage detection failed, continue } } // Suggest location based on file category and content (AI-powered for monorepos) const suggestedLocation = await this.fileClassifier.suggestLocation(classification, gitResult.repositories, content); // If we can suggest a location, plan the move (or delete if no location and utility) if (suggestedLocation && suggestedLocation !== filePath) { operations.push({ type: 'move', sourcePath: filePath, targetPath: suggestedLocation, reason: `${classification.category} file (${classification.reasoning})`, warning: usageWarning, isReferenced, }); } else if (!isReferenced && this.isUtilityFile(file.name, classification)) { // Utility files that aren't referenced can be deleted operations.push({ type: 'delete', sourcePath: filePath, reason: `Unused ${classification.category} file (${classification.reasoning})`, isReferenced: false, }); } } return { operations, backupRequired: operations.length > 0, estimatedDuration: operations.length * 50, // 50ms per operation estimate warnings, }; } isUtilityFile(fileName, classification) { const lower = fileName.toLowerCase(); // Utility patterns that suggest file might be temporary/debugging const utilityPatterns = [ 'temp-', 'tmp-', 'debug-', 'test-', 'check-', 'cache-', 'cleanup-', 'verify-', 'query-', ]; return utilityPatterns.some(pattern => lower.startsWith(pattern)) || (classification.category === 'script' && classification.confidence < 0.7); } async planFolderEnforcement(repoPath) { const operations = []; const warnings = []; const requiredFolders = ['scripts', 'documents']; // Check which folders need to be created for (const folder of requiredFolders) { const folderPath = path.join(repoPath, folder); const exists = await this.pathExists(folderPath); if (!exists) { operations.push({ type: 'create', sourcePath: folderPath, reason: `Enforce repository structure: ${folder}/ folder`, }); } } // Find scripts in root and plan to move them const entries = await fs.readdir(repoPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && this.isScript(entry.name)) { const sourcePath = path.join(repoPath, entry.name); const targetPath = path.join(repoPath, 'scripts', entry.name); operations.push({ type: 'move', sourcePath, targetPath, reason: 'Move script to scripts/ folder', }); } } return { operations, backupRequired: operations.some((op) => op.type === 'move'), estimatedDuration: operations.length * 50, warnings, }; } async pathExists(p) { try { await fs.access(p); return true; } catch { return false; } } isScript(filename) { return (filename.endsWith('.sh') || filename.endsWith('.bash') || filename.endsWith('.py') || filename.endsWith('.rb')); } /** * Plan test file organization based on configuration */ async planTestOrganization(rootPath) { if (!this.testOrganizer) { return { operations: [], backupRequired: false, estimatedDuration: 0, warnings: [], }; } return this.testOrganizer.planTestOrganization(rootPath); } } export class OperationExecutor { backupManager; constructor(backupManager) { this.backupManager = backupManager; } async execute(plan, dryRun) { const result = { success: true, operationsCompleted: 0, operationsFailed: 0, errors: [], }; if (dryRun) { // In dry-run, just report what would be done result.operationsCompleted = plan.operations.length; return result; } // Create backups if needed const backupEntries = []; if (plan.backupRequired) { for (const op of plan.operations) { if (op.type === 'move' || op.type === 'delete') { try { const exists = await this.fileExists(op.sourcePath); if (exists) { const entry = await this.backupManager.backupFile(op.sourcePath, op.type); backupEntries.push(entry); } } catch (error) { result.errors.push(`Backup failed for ${op.sourcePath}: ${error.message}`); } } } if (backupEntries.length > 0) { const manifest = await this.backupManager.createManifest(backupEntries); result.backupManifestId = manifest.id; } } // Execute operations for (const op of plan.operations) { try { await this.executeOperation(op); result.operationsCompleted++; } catch (error) { result.operationsFailed++; result.errors.push(`${op.type} ${op.sourcePath}: ${error.message}`); result.success = false; } } return result; } async executeOperation(op) { switch (op.type) { case 'move': if (!op.targetPath) { throw new Error('Target path required for move operation'); } await this.moveFile(op.sourcePath, op.targetPath); break; case 'delete': await fs.unlink(op.sourcePath); break; case 'create': await fs.mkdir(op.sourcePath, { recursive: true }); break; } } async moveFile(source, target) { // Ensure target directory exists const targetDir = path.dirname(target); await fs.mkdir(targetDir, { recursive: true }); // Move the file await fs.rename(source, target); } async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } } //# sourceMappingURL=operation-executor.js.map