UNPKG

devghost

Version:

šŸ‘» Find dead code, dead imports, and dead dependencies before they haunt your project

417 lines • 18.1 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("node:fs")); const path = __importStar(require("node:path")); const commander_1 = require("commander"); const inquirer_1 = __importDefault(require("inquirer")); const core_1 = require("../core"); const fixer_1 = require("../fixer"); const dependencyFixer_1 = require("../fixer/dependencyFixer"); const reporter_1 = require("../reporter"); const logger_1 = require("../utils/logger"); const packageJson = require('../../package.json'); /** * Check if user is running the latest version */ async function checkVersion() { try { const response = await fetch('https://registry.npmjs.org/devghost/latest'); const data = (await response.json()); const latestVersion = data.version; const currentVersion = packageJson.version; if (currentVersion !== latestVersion) { console.log((0, logger_1.warning)(`\nāš ļø Update available! ${currentVersion} → ${latestVersion}`)); console.log((0, logger_1.info)(' Run: npm install -g devghost@latest\n')); } } catch { // Silently ignore version check errors } } const program = new commander_1.Command(); program .name('devghost') .description('šŸ‘» Find dead code, dead imports, and dead dependencies') .version(packageJson.version) .argument('[path]', 'Path to analyze', process.cwd()) .option('--json', 'Output results as JSON') .option('--fix', 'Automatically remove unused imports') .option('--dry-run', 'Preview fixes without applying (use with --fix)') .option('--interactive', 'Review each issue interactively') .option('--ci', 'CI mode (minimal output, exit code 1 if issues found)') .option('--config <path>', 'Path to config file') .option('--include-dev', 'Include devDependencies in analysis') .option('--fix-deps', 'Automatically remove unused dependencies') .option('--fix-functions', 'Automatically remove unused functions') .option('--fix-types', 'Automatically remove unused types') .option('--fix-variables', 'Automatically remove unused variables') .option('-y, --yes', 'Skip confirmation prompts (auto-confirm)') .option('-q, --quiet', 'Minimal output (errors and summary only)') .option('--report <format>', 'Generate report (html or json)') .option('--output <path>', 'Custom output path for report') .action(async (targetPath, options) => { try { // Check for updates (non-blocking) checkVersion().catch(() => { /* ignore */ }); // Change to target directory process.chdir(targetPath); // Load config let config = { includeDev: options.includeDev || false, fix: options.fix || false, fixDeps: options.fixDeps || false, fixFunctions: options.fixFunctions || false, deps: options.deps || false, interactive: options.interactive || false, ci: options.ci || false, dryRun: options.dryRun || false, yes: options.yes || false, quiet: options.quiet || false, report: options.report, output: options.output, }; // Load config file if specified if (options.config) { const configPath = path.resolve(options.config); if (fs.existsSync(configPath)) { const fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); config = { ...config, ...fileConfig }; } } else { // Try to load devghost.config.json from current directory const defaultConfigPath = path.join(process.cwd(), 'devghost.config.json'); if (fs.existsSync(defaultConfigPath)) { const fileConfig = JSON.parse(fs.readFileSync(defaultConfigPath, 'utf-8')); config = { ...config, ...fileConfig }; } } // Run analysis if (!config.ci && !config.quiet) { (0, logger_1.info)('Scanning project...'); } const results = await (0, core_1.analyze)(config); // Handle CI mode if (config.ci) { const totalIssues = results.unusedImports.length + results.unusedFiles.length + results.unusedDependencies.length + results.unusedExports.length; if (totalIssues > 0) { console.log(`Found ${totalIssues} issues`); process.exit(1); } else { console.log('No issues found'); process.exit(0); } } // Handle JSON output if (options.json) { console.log(JSON.stringify(results, null, 2)); return; } // Handle interactive mode if (config.interactive && (results.unusedImports.length > 0 || results.unusedDependencies.length > 0)) { await handleInteractiveMode(results); return; } // Handle auto-fix if (config.fix && results.unusedImports.length > 0) { if (config.dryRun) { if (!config.quiet) (0, logger_1.info)('DRY RUN MODE - No files will be modified'); console.log((0, fixer_1.getFixPreview)(results.unusedImports)); } else { if (!config.quiet) (0, logger_1.warning)(`About to remove ${results.unusedImports.length} unused imports`); const fixResults = await (0, fixer_1.fixUnusedImports)(results.unusedImports, { dryRun: false, createBackup: false, }); const successful = fixResults.filter((r) => r.success).length; const failed = fixResults.filter((r) => !r.success).length; (0, logger_1.success)(`Fixed ${successful} files`); if (failed > 0) { (0, logger_1.error)(`Failed to fix ${failed} files`); } } return; } // Handle dependency fixing const shouldFixDeps = config.fixDeps || (config.fix && config.deps); if (shouldFixDeps && results.unusedDependencies.length > 0) { if (config.dryRun) { if (!config.quiet) (0, logger_1.info)('DRY RUN MODE - No dependencies will be removed'); if (!config.quiet) (0, logger_1.warning)(`Would remove ${results.unusedDependencies.length} unused dependencies:`); results.unusedDependencies.forEach((dep) => { console.log(` - ${dep.name}`); }); } else { if (!config.quiet) (0, logger_1.warning)(`About to remove ${results.unusedDependencies.length} unused dependencies:`); // Show the list of dependencies to be removed console.log(''); results.unusedDependencies.forEach((dep) => { console.log(` šŸ“¦ ${dep.name} (${dep.type}) - ${(dep.size / 1024).toFixed(2)} KB`); }); console.log(''); if (!config.yes) { const { confirm } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'confirm', message: `Are you sure you want to remove these ${results.unusedDependencies.length} dependencies?`, default: false, }, ]); if (!confirm) { (0, logger_1.info)('Cancelled'); return; } } if (!config.quiet) { (0, logger_1.info)('Removing dependencies (this may take a moment)...'); } const projectRoot = process.cwd(); const depResults = await (0, dependencyFixer_1.removeDependencies)(results.unusedDependencies.map((d) => d.name), projectRoot, config.includeDev || false, false); const successful = depResults.filter((r) => r.success).length; const failed = depResults.filter((r) => !r.success); (0, logger_1.success)(`Removed ${successful} dependencies`); if (failed.length > 0) { (0, logger_1.error)(`Failed to remove ${failed.length} dependencies:`); failed.forEach((f) => { console.log(` - ${f.packageName}: ${f.error}`); }); } } return; } // Handle unused functions fixing if (config.fixFunctions && results.unusedFunctions.length > 0) { if (config.dryRun) { if (!config.quiet) (0, logger_1.info)('DRY RUN MODE - No functions will be removed'); if (!config.quiet) (0, logger_1.warning)(`Would remove ${results.unusedFunctions.length} unused functions:`); results.unusedFunctions.forEach((func) => { console.log(` - ${func.functionName} (${func.file}:${func.line + 1})`); }); } else { if (!config.quiet) (0, logger_1.warning)(`About to remove ${results.unusedFunctions.length} unused functions`); if (!config.yes) { const { confirm } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'confirm', message: `Are you sure you want to remove these ${results.unusedFunctions.length} functions?`, default: false, }, ]); if (!confirm) { (0, logger_1.info)('Cancelled'); return; } } const fixResults = await (0, fixer_1.fixUnusedFunctions)(results.unusedFunctions, { dryRun: false, createBackup: false, // TODO: Add backup option }); const successful = fixResults.filter((r) => r.success).length; const failed = fixResults.filter((r) => !r.success).length; (0, logger_1.success)(`Removed ${successful} functions`); if (failed > 0) { (0, logger_1.error)(`Failed to remove ${failed} functions`); } } return; } // Handle HTML report generation if (config.report === 'html') { const reportPath = await (0, reporter_1.generateHtmlReport)(results, config.output); if (!config.quiet) { (0, logger_1.success)(`HTML report generated: ${reportPath}`); } // Ask to open in browser if (!config.quiet && !config.yes) { const { shouldOpen } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'shouldOpen', message: 'Open report in browser?', default: true, }, ]); if (shouldOpen) { (0, reporter_1.openInBrowser)(reportPath); (0, logger_1.success)('Report opened in browser'); } } else if (config.yes) { // Auto-open when --yes flag is used (0, reporter_1.openInBrowser)(reportPath); } return; } // Display results console.log((0, logger_1.formatResults)(results, true)); // Exit with error code if issues found (for CI integration) const totalIssues = results.unusedImports.length + results.unusedFiles.length + results.unusedDependencies.length + results.unusedExports.length; if (totalIssues > 0) { process.exit(0); // Don't exit with error in normal mode, only in CI mode } } catch (err) { (0, logger_1.error)(`Error: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } }); /** * Handle interactive mode */ async function handleInteractiveMode(results) { const unusedImports = results.unusedImports || []; const unusedDependencies = results.unusedDependencies || []; // Handle unused imports if (unusedImports.length > 0) { (0, logger_1.info)(`Found ${unusedImports.length} unused imports. Let's review them...`); const toFix = []; let skipAll = false; for (const imp of unusedImports) { if (skipAll) { break; } console.log(`\n${imp.file}:${imp.line + 1}`); console.log(` ${imp.entireLine.trim()}`); const { action } = await inquirer_1.default.prompt([ { type: 'list', name: 'action', message: 'What do you want to do?', choices: [ { name: 'Fix (remove this import)', value: 'fix' }, { name: 'Skip this one', value: 'skip' }, { name: 'Skip all remaining', value: 'skip-all' }, { name: 'Cancel', value: 'cancel' }, ], }, ]); if (action === 'fix') { toFix.push(imp); } else if (action === 'skip-all') { skipAll = true; } else if (action === 'cancel') { (0, logger_1.info)('Cancelled'); return; } } if (toFix.length > 0) { (0, logger_1.info)(`Fixing ${toFix.length} imports...`); const fixResults = await (0, fixer_1.fixUnusedImports)(toFix); const successful = fixResults.filter((r) => r.success).length; (0, logger_1.success)(`Fixed ${successful} imports`); } else { (0, logger_1.info)('No import changes made'); } } // Handle unused dependencies if (unusedDependencies.length > 0) { (0, logger_1.info)(`\nFound ${unusedDependencies.length} unused dependencies. Let's review them...`); const depsToFix = []; let skipAllDeps = false; for (const dep of unusedDependencies) { if (skipAllDeps) { break; } console.log(`\nšŸ“¦ ${dep.name}`); console.log(` Type: ${dep.type}`); console.log(` Size: ${(dep.size / 1024).toFixed(2)} KB`); const { action } = await inquirer_1.default.prompt([ { type: 'list', name: 'action', message: 'What do you want to do?', choices: [ { name: 'āœ“ Remove this dependency', value: 'fix' }, { name: 'āŠ— Skip this one', value: 'skip' }, { name: 'āŠ— Skip all remaining', value: 'skip-all' }, { name: 'āœ• Cancel', value: 'cancel' }, ], }, ]); if (action === 'fix') { depsToFix.push(dep.name); } else if (action === 'skip-all') { skipAllDeps = true; } else if (action === 'cancel') { (0, logger_1.info)('Cancelled'); return; } } if (depsToFix.length > 0) { (0, logger_1.info)(`\nRemoving ${depsToFix.length} dependencies...`); const depResults = await (0, dependencyFixer_1.removeDependencies)(depsToFix, process.cwd(), false, false); const successful = depResults.filter((r) => r.success).length; (0, logger_1.success)(`Removed ${successful} dependencies`); } else { (0, logger_1.info)('No dependency changes made'); } } } program.parse(); //# sourceMappingURL=index.js.map