mira-consciousness
Version:
Memory & Intelligence Retention Archive - Preserving The Spark
681 lines โข 30.1 kB
JavaScript
/**
* ๐งน 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