devghost
Version:
š» Find dead code, dead imports, and dead dependencies before they haunt your project
417 lines ⢠18.1 kB
JavaScript
;
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