UNPKG

mira-consciousness

Version:

Memory & Intelligence Retention Archive - Preserving The Spark

681 lines โ€ข 30.1 kB
/** * ๐Ÿงน Cleanup Analyzer - Digital Marie Kondo for Your Codebase * ========================================================== * * This analyzer helps identify "loose" files that may need cleanup - those files * that accumulate over time like digital dust bunnies. It uses intelligent pattern * recognition and context awareness to suggest potential cleanup candidates. * * Categories of files detected: * - Scattered documentation (README_old.md, NOTES.md in wrong places) * - Test artifacts and temporary test files * - Backup files (*.bak, *_backup.*, file_copy.*) * - Editor temporary files (.swp, ~, .DS_Store) * - Version iteration files (v1, v2, final_final) * - Build artifacts in wrong locations * - Log files outside designated directories * - Development experiments (scratch.*, playground.*) * - Screenshots and media files in wrong places * * The analyzer is respectful and cautious - it only suggests, never deletes. * It understands that one person's clutter might be another's treasure. */ import fs from 'fs-extra'; import * as path from 'path'; import { glob } from 'glob'; import * as micromatch from 'micromatch'; import chalk from 'chalk'; export var CleanupCategory; (function (CleanupCategory) { CleanupCategory["SCATTERED_DOCS"] = "scattered_documentation"; CleanupCategory["TEST_ARTIFACTS"] = "test_artifacts"; CleanupCategory["BACKUP_FILES"] = "backup_files"; CleanupCategory["TEMP_FILES"] = "temporary_files"; CleanupCategory["VERSION_ITERATIONS"] = "version_iterations"; CleanupCategory["EDITOR_FILES"] = "editor_files"; CleanupCategory["BUILD_ARTIFACTS"] = "build_artifacts_misplaced"; CleanupCategory["LOG_FILES"] = "log_files_misplaced"; CleanupCategory["EXPERIMENTS"] = "development_experiments"; CleanupCategory["MEDIA_FILES"] = "media_files_misplaced"; CleanupCategory["ARCHIVE_FILES"] = "archive_files"; CleanupCategory["DUPLICATE_CONFIGS"] = "duplicate_configurations"; CleanupCategory["ABANDONED_FILES"] = "abandoned_files"; CleanupCategory["EMPTY_FILES"] = "empty_files"; })(CleanupCategory || (CleanupCategory = {})); export class CleanupAnalyzer { projectRoot; projectStructure; issues = []; // Intelligent pattern definitions with context patterns = { scatteredDocs: { patterns: [ 'NOTES.md', 'TODO.md', 'IDEAS.md', 'CHANGELOG*.md', 'CONTRIBUTING*.md', 'API.md', 'SETUP.md', 'INSTALL*.md', 'DEPLOYMENT.md', 'ARCHITECTURE.md' ], excludeIfInPath: ['docs', 'documentation', '.github', 'DOCS'], category: CleanupCategory.SCATTERED_DOCS, severity: 'medium', message: 'Documentation file found outside docs folder' }, backupFiles: { patterns: [ '*.bak', '*.backup', '*.old', '*_old.*', '*_backup.*', '*_copy.*', '*.orig', 'Copy of *', 'Copy (?) of *', '*_bkp.*', '*.save', '*.previous', '*.archived' ], category: CleanupCategory.BACKUP_FILES, severity: 'high', message: 'Backup file that should be in version control or removed' }, tempFiles: { patterns: [ '*.tmp', '*.temp', 'tmp_*', 'temp_*', '.*.swp', '.*.swo', '*~', '*.cache', '.#*', '#*#', '.DS_Store', 'Thumbs.db', 'desktop.ini', '*.pyc', '__pycache__', '.sass-cache', '.npm', '.yarn-cache' ], category: CleanupCategory.TEMP_FILES, severity: 'high', message: 'Temporary file that should be cleaned up' }, versionIterations: { patterns: [ '*_v[0-9]*.*', '*_V[0-9]*.*', '*_final.*', '*_FINAL.*', '*_final_final.*', '*_draft.*', '*_Draft.*', '*_rev[0-9]*.*', '*.[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9].*', '*_[0-9][0-9][0-9][0-9]_[0-9][0-9]_[0-9][0-9].*' ], category: CleanupCategory.VERSION_ITERATIONS, severity: 'medium', message: 'Version iteration file - consider using version control instead' }, testArtifacts: { patterns: [ 'test.*', 'test-*', '*-test.*', '*.test', 'temp-test-*', 'test_output*', 'test-results*', '*.spec.js', '*.test.js', 'mock_*', 'fixture_*', 'sample_*' ], excludeIfInPath: ['test', 'tests', '__tests__', 'e2e', 'spec'], category: CleanupCategory.TEST_ARTIFACTS, severity: 'medium', message: 'Test file found outside test directory' }, experiments: { patterns: [ 'scratch.*', 'playground.*', 'experiment*', 'demo.*', 'example.*', 'draft_*', 'wip_*', 'temp_*', 'deleteme*', 'ignore*', 'foo.*', 'bar.*', 'baz.*', 'asdf.*', 'zzz*' ], category: CleanupCategory.EXPERIMENTS, severity: 'low', message: 'Experimental or temporary development file' }, buildArtifacts: { patterns: [ '*.min.js', '*.min.css', '*.bundle.js', '*.bundle.css', '*.compiled.*', '*.built.*', '*.transpiled.*' ], excludeIfInPath: ['dist', 'build', 'out', 'public', 'static'], category: CleanupCategory.BUILD_ARTIFACTS, severity: 'medium', message: 'Build artifact found outside build directory' }, logFiles: { patterns: [ '*.log', 'npm-debug.log*', 'yarn-debug.log*', 'yarn-error.log*', 'lerna-debug.log*', 'debug.log', 'error.log', 'access.log' ], excludeIfInPath: ['logs', 'log', '.logs'], category: CleanupCategory.LOG_FILES, severity: 'medium', message: 'Log file found outside logs directory' }, mediaFiles: { patterns: [ 'Screenshot*.png', 'Screen Shot*.png', 'Capture*.png', 'Recording*.mp4', 'Video*.mp4', '*.jpg', '*.jpeg', '*.gif', '*.bmp', '*.tiff', '*.mov', '*.avi' ], excludeIfInPath: ['assets', 'images', 'media', 'public', 'static', 'docs'], category: CleanupCategory.MEDIA_FILES, severity: 'low', message: 'Media file possibly in wrong location' }, archiveFiles: { patterns: [ '*.zip', '*.tar', '*.tar.gz', '*.tgz', '*.rar', '*.7z', '*.bz2', '*.gz', '*.xz' ], excludeIfInPath: ['releases', 'archives', 'backups', 'node_modules'], category: CleanupCategory.ARCHIVE_FILES, severity: 'medium', message: 'Archive file that might be outdated or misplaced' } }; constructor(projectRoot) { this.projectRoot = projectRoot; this.projectStructure = this.analyzeProjectStructure(); } async analyze() { console.log(chalk.cyan('๐Ÿงน Analyzing for cleanup opportunities...')); // Reset issues this.issues = []; // Analyze project structure first this.projectStructure = await this.analyzeProjectStructureAsync(); // Show gitignore status if (this.projectStructure.gitignorePatterns.length > 0) { console.log(chalk.gray(`๐Ÿ“‹ Respecting ${this.projectStructure.gitignorePatterns.length} .gitignore patterns`)); } // Get all files in the project const allFiles = await this.getAllFiles(); // Check each file against our patterns for (const file of allFiles) { await this.analyzeFile(file); } // Look for additional patterns await this.findEmptyFiles(); await this.findAbandonedFiles(); await this.findDuplicateConfigs(); // Calculate metrics const result = this.calculateResults(); return result; } analyzeProjectStructure() { const structure = { hasDocsFolder: false, hasTestsFolder: false, hasBuildFolder: false, hasLogsFolder: false, projectType: 'general', gitignorePatterns: [] }; // Quick synchronous check for basic structure try { if (fs.existsSync(path.join(this.projectRoot, 'package.json'))) { structure.projectType = 'node'; } else if (fs.existsSync(path.join(this.projectRoot, 'requirements.txt'))) { structure.projectType = 'python'; } else if (fs.existsSync(path.join(this.projectRoot, 'pom.xml'))) { structure.projectType = 'java'; } } catch { // Ignore errors in quick check } return structure; } async analyzeProjectStructureAsync() { const structure = this.analyzeProjectStructure(); // Check for common directories const commonDirs = [ { names: ['docs', 'documentation', 'doc'], key: 'hasDocsFolder', pathKey: 'docsPath' }, { names: ['test', 'tests', '__tests__', 'e2e', 'spec'], key: 'hasTestsFolder', pathKey: 'testsPath' }, { names: ['dist', 'build', 'out', 'output'], key: 'hasBuildFolder', pathKey: 'buildPath' }, { names: ['logs', 'log', '.logs'], key: 'hasLogsFolder', pathKey: 'logsPath' } ]; for (const dirConfig of commonDirs) { for (const dirName of dirConfig.names) { const dirPath = path.join(this.projectRoot, dirName); if (await fs.pathExists(dirPath)) { structure[dirConfig.key] = true; structure[dirConfig.pathKey] = dirName; break; } } } // Read .gitignore patterns try { const gitignorePath = path.join(this.projectRoot, '.gitignore'); if (await fs.pathExists(gitignorePath)) { const gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); structure.gitignorePatterns = gitignoreContent .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } } catch { // Ignore .gitignore read errors } return structure; } async getAllFiles() { try { // Start with default ignores const ignorePatterns = [ '**/node_modules/**', // Match node_modules anywhere '.git/**', '**/dist/**', '**/build/**', '**/coverage/**', '.next/**', '.nuxt/**', '**/vendor/**' ]; // Add gitignore patterns if they exist if (this.projectStructure.gitignorePatterns.length > 0) { // Convert gitignore patterns to glob patterns const gitignoreGlobPatterns = this.projectStructure.gitignorePatterns.map(pattern => { // Remove leading slash for glob if (pattern.startsWith('/')) { pattern = pattern.slice(1); } // Handle negations (we'll skip them for now) if (pattern.startsWith('!')) { return null; } // Convert directory patterns - these should match anywhere if (pattern.endsWith('/')) { // For directories like node_modules/, match anywhere const dirName = pattern.slice(0, -1); return '**/' + dirName + '/**'; } // Add ** for extension patterns if (pattern.startsWith('*.')) { return '**/' + pattern; } // Handle paths that should match anywhere if (!pattern.includes('/')) { return '**/' + pattern; } return pattern; }).filter(p => p !== null); ignorePatterns.push(...gitignoreGlobPatterns); // Also add common directory patterns without the ** prefix for root level this.projectStructure.gitignorePatterns.forEach(pattern => { if (pattern.endsWith('/') && !pattern.startsWith('/')) { const dirName = pattern.slice(0, -1); ignorePatterns.push(dirName + '/**'); } }); } const files = await glob('**/*', { cwd: this.projectRoot, ignore: ignorePatterns, nodir: true, dot: true }); return files; } catch (error) { console.error(chalk.red('Error scanning files:'), error); return []; } } async analyzeFile(filePath) { const fullPath = path.join(this.projectRoot, filePath); try { const stats = await fs.stat(fullPath); const fileName = path.basename(filePath); const dirName = path.dirname(filePath); // Special handling for README files if (fileName.toLowerCase() === 'readme.md' || fileName.toLowerCase() === 'readme.txt') { // README files are expected in: // 1. Project root // 2. Package/module roots (like mira-memory/) // 3. Test directories // 4. Any directory that represents a logical module // Skip if it's in a standard location if (dirName === '.' || // Root directory dirName.endsWith('/tests') || dirName.endsWith('/test') || dirName.includes('/packages/') || dirName.includes('/modules/') || dirName.includes('/examples/') || dirName.includes('/docs') || dirName.includes('/DOCS') || // Check if it's a package root (has package.json) await this.isPackageRoot(dirName)) { return; // This is a legitimate README location } // Only flag README files that are truly scattered (e.g., in src/ directories) if (dirName.includes('/src/') || dirName.includes('/lib/') || dirName.includes('/bin/')) { this.issues.push({ file: filePath, reason: 'README file in source code directory - consider moving to parent directory', category: CleanupCategory.SCATTERED_DOCS, severity: 'low', suggestion: `Move to ${path.dirname(dirName)}/README.md`, safeToDelete: false, lastModified: stats.mtime, size: stats.size, properLocation: path.join(path.dirname(dirName), fileName) }); } return; // Don't process README files with other patterns } // Check against each pattern category for (const [key, config] of Object.entries(this.patterns)) { if (this.matchesPattern(filePath, config)) { // Check if it should be excluded based on its path if ('excludeIfInPath' in config && config.excludeIfInPath) { const shouldExclude = config.excludeIfInPath.some((excludePath) => filePath.includes(excludePath)); if (shouldExclude) continue; } const issue = { file: filePath, reason: config.message, category: config.category, severity: config.severity, suggestion: this.generateSuggestion(filePath, config.category), safeToDelete: this.isSafeToDelete(filePath, config.category), lastModified: stats.mtime, size: stats.size, properLocation: this.suggestProperLocation(filePath, config.category) }; // Find related files (e.g., README.md and README_old.md) issue.relatedFiles = await this.findRelatedFiles(filePath); this.issues.push(issue); } } } catch (error) { // Skip files we can't read } } matchesPattern(filePath, config) { const fileName = path.basename(filePath); return micromatch.isMatch(fileName, config.patterns, { nocase: true, dot: true }); } isGitIgnored(filePath) { if (this.projectStructure.gitignorePatterns.length === 0) return false; return micromatch.isMatch(filePath, this.projectStructure.gitignorePatterns, { dot: true }); } async isPackageRoot(dirPath) { try { const fullDirPath = path.join(this.projectRoot, dirPath); // Check for package.json, Cargo.toml, setup.py, etc. const packageFiles = ['package.json', 'Cargo.toml', 'setup.py', 'pyproject.toml', 'go.mod']; for (const packageFile of packageFiles) { if (await fs.pathExists(path.join(fullDirPath, packageFile))) { return true; } } return false; } catch { return false; } } generateSuggestion(filePath, category) { const fileName = path.basename(filePath); const dirName = path.dirname(filePath); switch (category) { case CleanupCategory.SCATTERED_DOCS: if (this.projectStructure.hasDocsFolder) { return `Move to ${this.projectStructure.docsPath}/ directory`; } return 'Consider creating a docs/ directory for documentation'; case CleanupCategory.TEST_ARTIFACTS: if (this.projectStructure.hasTestsFolder) { return `Move to ${this.projectStructure.testsPath}/ directory`; } return 'Consider creating a tests/ directory'; case CleanupCategory.BACKUP_FILES: return 'Remove if no longer needed (version control maintains history)'; case CleanupCategory.TEMP_FILES: return 'Delete temporary file'; case CleanupCategory.VERSION_ITERATIONS: return 'Use git branches/tags instead of version suffixes'; case CleanupCategory.BUILD_ARTIFACTS: if (this.projectStructure.hasBuildFolder) { return `Move to ${this.projectStructure.buildPath}/ directory or add to .gitignore`; } return 'Move to build output directory'; case CleanupCategory.LOG_FILES: if (this.projectStructure.hasLogsFolder) { return `Move to ${this.projectStructure.logsPath}/ directory`; } return 'Create logs/ directory or add to .gitignore'; case CleanupCategory.EXPERIMENTS: return 'Move to playground/ or examples/ directory, or remove if obsolete'; case CleanupCategory.MEDIA_FILES: return 'Move to assets/ or docs/ directory'; case CleanupCategory.ARCHIVE_FILES: return 'Move to releases/ directory or remove if outdated'; default: return 'Review and organize appropriately'; } } isSafeToDelete(filePath, category) { // Never mark these as safe to delete const neverDelete = [ 'README.md', 'LICENSE', 'CONTRIBUTING.md', '.env', '.env.example', 'package.json', 'package-lock.json', 'yarn.lock', 'requirements.txt', 'Makefile', 'Dockerfile', 'docker-compose.yml' ]; const fileName = path.basename(filePath); if (neverDelete.includes(fileName)) return false; // Categories generally safe to delete const safeCategories = [ CleanupCategory.TEMP_FILES, CleanupCategory.EDITOR_FILES, CleanupCategory.EMPTY_FILES ]; return safeCategories.includes(category); } suggestProperLocation(filePath, category) { const fileName = path.basename(filePath); switch (category) { case CleanupCategory.SCATTERED_DOCS: return this.projectStructure.docsPath ? path.join(this.projectStructure.docsPath, fileName) : path.join('docs', fileName); case CleanupCategory.TEST_ARTIFACTS: return this.projectStructure.testsPath ? path.join(this.projectStructure.testsPath, fileName) : path.join('tests', fileName); case CleanupCategory.BUILD_ARTIFACTS: return this.projectStructure.buildPath ? path.join(this.projectStructure.buildPath, fileName) : path.join('dist', fileName); case CleanupCategory.LOG_FILES: return this.projectStructure.logsPath ? path.join(this.projectStructure.logsPath, fileName) : path.join('logs', fileName); default: return undefined; } } async findRelatedFiles(filePath) { const related = []; const dir = path.dirname(filePath); const baseName = path.basename(filePath); const nameWithoutExt = path.parse(baseName).name; const ext = path.parse(baseName).ext; try { const filesInDir = await fs.readdir(path.join(this.projectRoot, dir)); for (const file of filesInDir) { if (file === baseName) continue; // Check for similar names if (file.startsWith(nameWithoutExt) || file.includes(nameWithoutExt)) { related.push(path.join(dir, file)); } } } catch { // Ignore errors } return related; } async findEmptyFiles() { const allFiles = await this.getAllFiles(); for (const file of allFiles) { try { const fullPath = path.join(this.projectRoot, file); const stats = await fs.stat(fullPath); if (stats.size === 0) { this.issues.push({ file, reason: 'Empty file with no content', category: CleanupCategory.EMPTY_FILES, severity: 'low', suggestion: 'Delete empty file or add content', safeToDelete: true, lastModified: stats.mtime, size: 0 }); } } catch { // Skip files we can't read } } } async findAbandonedFiles() { const allFiles = await this.getAllFiles(); const sixMonthsAgo = new Date(); sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); for (const file of allFiles) { try { const fullPath = path.join(this.projectRoot, file); const stats = await fs.stat(fullPath); // Check if file hasn't been modified in 6 months and has certain patterns if (stats.mtime < sixMonthsAgo) { const fileName = path.basename(file); const abandonedPatterns = ['old', 'deprecated', 'obsolete', 'unused', 'legacy']; const isAbandoned = abandonedPatterns.some(pattern => fileName.toLowerCase().includes(pattern)); if (isAbandoned) { this.issues.push({ file, reason: `File hasn't been modified since ${stats.mtime.toLocaleDateString()} and has suspicious name`, category: CleanupCategory.ABANDONED_FILES, severity: 'medium', suggestion: 'Review if still needed or archive/delete', safeToDelete: false, lastModified: stats.mtime, size: stats.size }); } } } catch { // Skip files we can't read } } } async findDuplicateConfigs() { const configPatterns = [ ['config.json', 'config.js', 'config.ts'], ['settings.json', 'settings.js', 'settings.ts'], ['.env', '.env.local', '.env.development', '.env.production'] ]; for (const pattern of configPatterns) { const found = []; for (const configFile of pattern) { const filePath = path.join(this.projectRoot, configFile); if (await fs.pathExists(filePath)) { found.push(configFile); } } if (found.length > 1) { for (const file of found) { const stats = await fs.stat(path.join(this.projectRoot, file)); this.issues.push({ file, reason: `Multiple configuration files of same type: ${found.join(', ')}`, category: CleanupCategory.DUPLICATE_CONFIGS, severity: 'medium', suggestion: 'Consolidate configuration files', safeToDelete: false, lastModified: stats.mtime, size: stats.size, relatedFiles: found.filter(f => f !== file) }); } } } } calculateResults() { const categorySummary = new Map(); let totalSizeWasted = 0; // Count issues by category for (const issue of this.issues) { const count = categorySummary.get(issue.category) || 0; categorySummary.set(issue.category, count + 1); totalSizeWasted += issue.size; } // Calculate score (0-100, where 100 is cleanest) const score = Math.max(0, 100 - this.issues.length * 2); // Generate recommendations const recommendations = this.generateRecommendations(categorySummary); // Generate safe cleanup commands const safeCleanupCommands = this.generateCleanupCommands(); return { score, issues: this.issues, totalSizeWasted, categorySummary, recommendations, safeCleanupCommands }; } generateRecommendations(categorySummary) { const recommendations = []; if (categorySummary.get(CleanupCategory.SCATTERED_DOCS) || 0 > 3) { recommendations.push('๐Ÿ“š Create a centralized docs/ directory for all documentation'); } if (categorySummary.get(CleanupCategory.BACKUP_FILES) || 0 > 0) { recommendations.push('๐Ÿ”„ Use version control instead of backup files'); } if (categorySummary.get(CleanupCategory.TEST_ARTIFACTS) || 0 > 5) { recommendations.push('๐Ÿงช Organize test files in a dedicated test directory'); } if (categorySummary.get(CleanupCategory.TEMP_FILES) || 0 > 0) { recommendations.push('๐Ÿ—‘๏ธ Add temporary file patterns to .gitignore'); } if (this.issues.length > 20) { recommendations.push('๐Ÿงน Consider a major cleanup sprint to organize the codebase'); } if (!this.projectStructure.gitignorePatterns.length) { recommendations.push('๐Ÿ“ Create a .gitignore file to prevent committing temporary files'); } return recommendations; } generateCleanupCommands() { const commands = []; // Safe temp file cleanup const tempFiles = this.issues .filter(i => i.safeToDelete && i.category === CleanupCategory.TEMP_FILES) .map(i => i.file); if (tempFiles.length > 0) { commands.push(`# Remove temporary files:\nrm ${tempFiles.join(' ')}`); } // Editor files cleanup const editorFiles = this.issues .filter(i => i.category === CleanupCategory.EDITOR_FILES) .map(i => i.file); if (editorFiles.length > 0) { commands.push(`# Remove editor temporary files:\nrm ${editorFiles.join(' ')}`); } // Move documentation files if (this.projectStructure.hasDocsFolder) { const docsToMove = this.issues .filter(i => i.category === CleanupCategory.SCATTERED_DOCS) .map(i => `mv ${i.file} ${this.projectStructure.docsPath}/`); if (docsToMove.length > 0) { commands.push(`# Move documentation to docs folder:\n${docsToMove.join('\n')}`); } } return commands; } // Quick scan for CLI usage async quickScan() { return this.analyze(); } } //# sourceMappingURL=CleanupAnalyzer.js.map