UNPKG

vibe-janitor

Version:

A CLI tool that cleans AI-generated JavaScript/TypeScript projects efficiently and intelligently

325 lines (324 loc) 15.1 kB
#!/usr/bin/env node import { Command } from 'commander'; import { Logger } from '../utils/logger.js'; import { Reporter } from '../utils/reporter.js'; import { Cleaner } from '../core/cleaner.js'; import { AssetSweeper } from '../core/assetSweeper.js'; import { StyleCleaner } from '../core/styleCleaner.js'; import { DependencyAuditor } from '../core/dependencyAuditor.js'; import { CircularDependencyScanner, } from '../core/circularDependencyScanner.js'; import path from 'path'; import fs from 'fs-extra'; import { fileURLToPath } from 'url'; import prompts from 'prompts'; import chalk from 'chalk'; // Get the directory name of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Get package version from package.json (resolve from project root) const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const program = new Command(); /** * Initialize and configure the CLI */ function initCLI() { program .name('vibe-janitor') .description('Clean up AI-generated JavaScript/TypeScript projects') .version(packageJson.version) .argument('[directory]', 'Target directory to clean', '.') .option('--deep-scrub', 'Run all available cleanup routines') .option('--dry-run', 'Show what would be removed without deleting anything') .option('--remove-unused', 'Remove unused files, components, and imports') .option('--delete-unused-files', 'Delete files that are not imported or used anywhere') .option('--clean-styles', 'Clean unused CSS classes and selectors') .option('--list', 'List detailed information about unused imports and other issues') .option('--report [path]', 'Generate detailed reports (JSON and Markdown)') .option('--analyze-complexity', 'Analyze code complexity') .option('--analyze-dependencies', 'Analyze package dependencies') .option('--check-circular', 'Check for circular dependencies') .option('--generate-graph', 'Generate dependency graph visualization') .option('--log', 'Output detailed cleanup logs') .option('--quiet', 'No console output, just do the job') .option('--no-progress', 'Disable progress bars') .option('--no-interactive', 'Skip interactive prompts') .action(async (directory, options) => { try { // Convert to absolute path const targetDir = path.resolve(directory); // Check if directory exists if (!fs.existsSync(targetDir)) { Logger.error(`Directory not found: ${targetDir}`); process.exit(1); } // Check if we need interactive prompts // Only show interactive prompts when no explicit action flags are provided // and interactive is not disabled const hasExplicitOptions = Boolean(options.removeUnused ?? options.dryRun ?? options.deepScrub ?? options.list ?? options.report ?? options.analyzeComplexity ?? options.analyzeDependencies ?? options.checkCircular ?? options.generateGraph); if (!hasExplicitOptions && options.interactive !== false) { options = await promptForOptions(options, targetDir); // User cancelled the prompts if (!options) { process.exit(0); } } if (!options.quiet) { Logger.welcome(); Logger.info(`Running cleanup in: ${Logger.formatPath(targetDir)}`); if (options.dryRun) { Logger.info('Dry run mode: No files will be modified'); } } // Run the core cleaning modules with progress bars if not disabled const results = options.noProgress || options.quiet ? await runCleanupModules(targetDir, options) : await Logger.runWithProgress(() => runCleanupModules(targetDir, options), 3, [ 'Analyzing project', 'Cleaning code', 'Processing results', ]); if (!options.quiet) { // Generate console summary const reporter = new Reporter({ outputPath: options.report === true ? 'vibe-janitor-report' : options.report, verbose: options.log, }); reporter.generateConsoleSummary(results.cleanerResult, results.assetResult, Boolean(options.list), results.styleResult); // Show dependency analysis if requested if (options.analyzeDependencies && results.dependencyResult) { Logger.log('\n📦 Dependency Analysis:'); Logger.log(` - Unused dependencies: ${results.dependencyResult.unusedDependencies.length}`); Logger.log(` - Missing dependencies: ${results.dependencyResult.missingDependencies.length}`); Logger.log(` - Possible native replacements: ${results.dependencyResult.possibleNativeReplacements.length}`); // Generate dependency cleanup instructions if requested if (options.report) { const dependencyAuditor = new DependencyAuditor(targetDir); const instructions = dependencyAuditor.generateCleanupInstructions(results.dependencyResult); // Always use vibe-janitor-report directory for consistency const reportDirName = 'vibe-janitor-report'; const reportBaseName = path.basename(options.report === true ? 'vibe-janitor-report' : options.report); const depReportFileName = `${reportBaseName}-dependencies.md`; const depReportPath = path.join(reportDirName, depReportFileName); // Ensure the directory exists await fs.ensureDir(reportDirName); await fs.outputFile(depReportPath, instructions); Logger.info(`- Dependency cleanup instructions: ${depReportPath}`); } } // Show circular dependency analysis if requested if (options.checkCircular && results.circularResult) { Logger.log('\n🔄 Circular Dependency Analysis:'); Logger.log(` - Circular dependencies found: ${results.circularResult.circularDependencies.length}`); Logger.log(` - Total files analyzed: ${results.circularResult.fileCount}`); // Generate circular dependency report if requested if (options.report) { const circularScanner = new CircularDependencyScanner(targetDir); const report = circularScanner.generateReport(results.circularResult); // Always use vibe-janitor-report directory for consistency const reportDirName = 'vibe-janitor-report'; const reportBaseName = path.basename(options.report === true ? 'vibe-janitor-report' : options.report); const circReportFileName = `${reportBaseName}-circular.md`; const circReportPath = path.join(reportDirName, circReportFileName); // Ensure the directory exists await fs.ensureDir(reportDirName); await fs.outputFile(circReportPath, report); Logger.info(`- Circular dependency report: ${circReportPath}`); } } if (options.report) { const reportPaths = await reporter.generateReports(results.cleanerResult, results.assetResult, results.styleResult); if (reportPaths.markdownPath || reportPaths.jsonPath) { Logger.info('\nReports generated:'); if (reportPaths.markdownPath) { Logger.info(`- Markdown: ${reportPaths.markdownPath}`); } if (reportPaths.jsonPath) { Logger.info(`- JSON: ${reportPaths.jsonPath}`); } } } Logger.success('Cleanup complete!'); } } catch (error) { Logger.error(`An error occurred: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } }); return program; } /** * Ask the user for cleanup options interactively */ async function promptForOptions(options, targetDir) { Logger.info('\n🧹 Welcome to vibe-janitor interactive setup!'); Logger.info(`\nTarget directory: ${targetDir}\n`); try { const responses = await prompts([ { type: 'confirm', name: 'removeUnused', message: 'Clean up unused imports and code automatically?', initial: true, }, { type: 'confirm', name: 'list', message: 'Show detailed information about issues found?', initial: true, }, { type: 'confirm', name: 'report', message: 'Generate detailed reports (JSON and Markdown)?', initial: false, }, { type: 'confirm', name: 'deepScrub', message: 'Run advanced cleanup (assets, variables, functions)?', initial: false, }, { type: 'confirm', name: 'cleanStyles', message: 'Clean unused CSS classes and selectors?', initial: false, }, { type: 'confirm', name: 'deleteUnusedFiles', message: `${chalk.red('Delete')} files that are not imported or used anywhere?`, initial: true, }, { type: 'confirm', name: 'analyzeDependencies', message: 'Analyze package dependencies?', initial: false, }, { type: 'confirm', name: 'checkCircular', message: 'Check for circular dependencies?', initial: false, }, ], { onCancel: () => { Logger.info('\n🚫 Operation cancelled by user'); return null; }, }); // Merge the responses with the original options return { ...options, ...responses }; } catch (error) { Logger.error(`Error during interactive prompts: ${error instanceof Error ? error.message : String(error)}`); return options; // Return original options if prompts fail } } /** * Run the code and asset cleanup modules */ async function runCleanupModules(targetDir, options) { const results = { cleanerResult: { unusedImports: [], unusedVariables: [], unusedFunctions: [], unusedFiles: [], modifiedFiles: [], deletedFiles: [], unusedFilesSize: 0, }, }; // Initialize the cleaner const cleaner = new Cleaner(targetDir, { dryRun: Boolean(options.dryRun ?? false), removeUnused: Boolean(options.removeUnused ?? false), deepScrub: Boolean(options.deepScrub ?? false), deleteUnusedFiles: Boolean(options.deleteUnusedFiles ?? false), verbose: Boolean(options.log ?? false), }); // Run code cleanup if (!options.quiet && options.log) { Logger.info('Running code cleanup...'); } results.cleanerResult = await cleaner.clean(); // Run asset sweeper if deep scrub is enabled if (options.deepScrub) { const assetSweeper = new AssetSweeper(targetDir, { dryRun: Boolean(options.dryRun ?? false), deleteUnused: Boolean(options.removeUnused ?? false), verbose: Boolean(options.log ?? false), }); if (!options.quiet && options.log) { Logger.info('Running asset sweeping...'); } results.assetResult = await assetSweeper.sweep(); } // Run style cleanup if requested if (options.cleanStyles || options.deepScrub) { const styleCleaner = new StyleCleaner(targetDir, { dryRun: Boolean(options.dryRun ?? false), removeUnused: Boolean(options.removeUnused ?? false), scanComponents: true, verbose: Boolean(options.log ?? false), }); if (!options.quiet && options.log) { Logger.info('Running CSS style cleanup...'); } results.styleResult = await styleCleaner.clean(); if (!options.quiet && options.log && results.styleResult) { Logger.info(`Found ${results.styleResult.totalUnusedSelectors} unused CSS selectors across ${results.styleResult.unusedSelectors.length} files`); if (results.styleResult.modifiedFiles.length > 0) { Logger.success(`Cleaned ${results.styleResult.modifiedFiles.length} CSS files`); } } } // Run complexity analysis if requested if (options.analyzeDependencies || options.deepScrub) { const dependencyAuditor = new DependencyAuditor(targetDir, { verbose: Boolean(options.log ?? false), }); if (!options.quiet && options.log) { Logger.info('Analyzing package dependencies...'); } results.dependencyResult = await dependencyAuditor.audit(); } // Scan for circular dependencies if requested if (options.checkCircular || options.deepScrub) { const circularScanner = new CircularDependencyScanner(targetDir, { verbose: Boolean(options.log ?? false), }); if (!options.quiet && options.log) { Logger.info('Checking for circular dependencies...'); } results.circularResult = await circularScanner.scan(); // Generate dependency graph visualization if requested if (options.generateGraph && !options.dryRun) { const graphPath = await circularScanner.generateGraph(); if (graphPath && !options.quiet) { Logger.success(`Dependency graph generated: ${graphPath}`); } } } return results; } // Execute the program try { initCLI().parse(); } catch (error) { Logger.error(`Failed to initialize CLI: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); }