ghost-import-hunter 3.0.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/dist/analyzer.js +86 -7
- package/dist/index.js +148 -111
- package/media/header_v4.png +0 -0
- package/package.json +1 -1
- package/media/header.png +0 -0
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/01Developer95/Ghost-Import-Hunter/main/media/
|
|
2
|
+
<img src="https://raw.githubusercontent.com/01Developer95/Ghost-Import-Hunter/main/media/header_v4.png" alt="Ghost Import Hunter v4.0.0" width="100%">
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
|
+
# Ghost Import Hunter v4.0.0
|
|
6
|
+
|
|
5
7
|
<div align="center">
|
|
6
8
|
|
|
7
|
-

|
|
9
|
+

|
|
8
10
|

|
|
9
11
|

|
|
10
12
|
|
|
@@ -17,16 +19,16 @@
|
|
|
17
19
|
- **Deterministic Validation** - Verify every import against your actual installed modules. No guessing or regex.
|
|
18
20
|
- **Deep Export Analysis** - Uses TypeScript Compiler API to correctly resolve `export *`, re-exports, and aliases.
|
|
19
21
|
- **Local File Scanning** - Validates imports from your own local files, not just external packages.
|
|
20
|
-
- **Zero Configuration** - Works out of the box. Just run `npx ghost-hunter` in your project root.
|
|
22
|
+
- **Zero Configuration** - Works out of the box. Just run `npx ghost-import-hunter` in your project root.
|
|
21
23
|
- **CI/CD Ready** - Fails the build if hallucinations are detected. Preventing bad code from merging.
|
|
22
24
|
|
|
23
25
|
---
|
|
24
26
|
|
|
25
27
|
## 🤔 What is a Hallucination?
|
|
26
28
|
|
|
27
|
-
AI coding assistants often suggest imports that **look real but don't exist**. Ghost Hunter catches these bugs before they break your app.
|
|
29
|
+
AI coding assistants often suggest imports that **look real but don't exist**. Ghost Import Hunter catches these bugs before they break your app.
|
|
28
30
|
|
|
29
|
-
### Examples of what Ghost Hunter catches:
|
|
31
|
+
### Examples of what Ghost Import Hunter catches:
|
|
30
32
|
|
|
31
33
|
**1. The "Fake Function" Hallucination**
|
|
32
34
|
```typescript
|
|
@@ -50,22 +52,22 @@ import { utils } from 'dependency-i-never-installed';
|
|
|
50
52
|
|
|
51
53
|
## 🧠 How it Works (Under the Hood)
|
|
52
54
|
|
|
53
|
-
Ghost Hunter uses three core technologies to ensure your code is safe:
|
|
55
|
+
Ghost Import Hunter uses three core technologies to ensure your code is safe:
|
|
54
56
|
|
|
55
57
|
### 1. `glob` - The Scanner
|
|
56
58
|
**Role:** Finding your files.
|
|
57
|
-
Just like your terminal finds files when you type `ls *.ts`, Ghost Hunter uses `glob` to scan your entire project's TypeScript and JavaScript files, ignoring junk like `node_modules`.
|
|
59
|
+
Just like your terminal finds files when you type `ls *.ts`, Ghost Import Hunter uses `glob` to scan your entire project's TypeScript and JavaScript files, ignoring junk like `node_modules`.
|
|
58
60
|
|
|
59
61
|
### 2. TypeScript Compiler API - The Brain
|
|
60
62
|
**Role:** Understanding your code.
|
|
61
|
-
Unlike
|
|
63
|
+
Unlike early versions that relied on Regex, v4.0.0 uses the real **TypeScript Compiler API** to parse your code, resolve symbols, and track exports across files. This allows it to:
|
|
62
64
|
- Follow `export * from ...` chains.
|
|
63
65
|
- Understand aliased imports (`import { foo as bar }`).
|
|
64
66
|
- Detect missing exports in local files.
|
|
65
67
|
|
|
66
68
|
### 3. `chalk` - The Reporter
|
|
67
69
|
**Role:** Making sense of the output.
|
|
68
|
-
When a hallucination is found, Ghost Hunter uses `chalk` to highlight the error in **red** and the file path in **bold**, making it impossible to miss critical bugs in your terminal.
|
|
70
|
+
When a hallucination is found, Ghost Import Hunter uses `chalk` to highlight the error in **red** and the file path in **bold**, making it impossible to miss critical bugs in your terminal.
|
|
69
71
|
|
|
70
72
|
---
|
|
71
73
|
|
|
@@ -137,7 +139,7 @@ Options:
|
|
|
137
139
|
|
|
138
140
|
## ⚙️ Configuration
|
|
139
141
|
|
|
140
|
-
Ghost Hunter supports a `.ghostrc` file:
|
|
142
|
+
Ghost Import Hunter supports a `.ghostrc` file:
|
|
141
143
|
|
|
142
144
|
```json
|
|
143
145
|
{
|
package/dist/analyzer.js
CHANGED
|
@@ -37,14 +37,26 @@ exports.analyzeProject = analyzeProject;
|
|
|
37
37
|
const ts = __importStar(require("typescript"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const glob_1 = require("glob");
|
|
40
|
-
async function analyzeProject(directory) {
|
|
40
|
+
async function analyzeProject(directory, options = {}) {
|
|
41
41
|
const report = { hallucinations: [], unused: [], usedModules: [] };
|
|
42
42
|
// 1. Find all files in the project
|
|
43
|
-
|
|
43
|
+
let files = await (0, glob_1.glob)('**/*.{ts,tsx,js,jsx}', {
|
|
44
44
|
cwd: directory,
|
|
45
45
|
ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'],
|
|
46
46
|
absolute: true
|
|
47
47
|
});
|
|
48
|
+
if (options.changedOnly) {
|
|
49
|
+
try {
|
|
50
|
+
const { execSync } = require('child_process');
|
|
51
|
+
// Get modified, deleted, and untracked files
|
|
52
|
+
const gitDiff = execSync('git diff --name-only HEAD && git ls-files --others --exclude-standard', { cwd: directory, encoding: 'utf8' });
|
|
53
|
+
const changedFiles = new Set(gitDiff.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(directory, f)));
|
|
54
|
+
files = files.filter(f => changedFiles.has(path.resolve(f)));
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.warn('⚠️ Could not determine git changed files. Falling back to scanning all files.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
48
60
|
if (files.length === 0) {
|
|
49
61
|
return report;
|
|
50
62
|
}
|
|
@@ -74,11 +86,19 @@ async function analyzeProject(directory) {
|
|
|
74
86
|
// Map<Symbol, UnusedItem>
|
|
75
87
|
const trackedImports = new Map();
|
|
76
88
|
// Pass 1: Collect Imports & Check Hallucinations
|
|
77
|
-
|
|
89
|
+
const visitPass1 = (node) => {
|
|
78
90
|
if (ts.isImportDeclaration(node)) {
|
|
79
91
|
visitImportDeclaration(node, sourceFile, checker, report, trackedImports, allUsedModules);
|
|
80
92
|
}
|
|
81
|
-
|
|
93
|
+
else if (ts.isCallExpression(node)) {
|
|
94
|
+
visitCallExpression(node, sourceFile, checker, report, allUsedModules);
|
|
95
|
+
ts.forEachChild(node, visitPass1);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
ts.forEachChild(node, visitPass1);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
visitPass1(sourceFile);
|
|
82
102
|
// Pass 2: Check Usage
|
|
83
103
|
// We visit all nodes EXCEPT ImportDeclarations (which we already processed)
|
|
84
104
|
// If we find an identifier that resolves to a symbol in 'trackedImports', delete it from the map.
|
|
@@ -147,7 +167,24 @@ function visitImportDeclaration(node, sourceFile, checker, report, trackedImport
|
|
|
147
167
|
if (node.importClause?.name) {
|
|
148
168
|
const defaultImport = node.importClause.name;
|
|
149
169
|
const defaultSymbol = checker.getSymbolAtLocation(defaultImport);
|
|
150
|
-
if
|
|
170
|
+
// Check if module actually has a default export
|
|
171
|
+
const moduleSymbol = checker.getSymbolAtLocation(moduleSpecifier);
|
|
172
|
+
let hasDefaultExport = true;
|
|
173
|
+
if (moduleSymbol && moduleSymbol.exports) {
|
|
174
|
+
hasDefaultExport = moduleSymbol.exports.has(ts.escapeLeadingUnderscores('default'));
|
|
175
|
+
}
|
|
176
|
+
if (moduleSymbol && !hasDefaultExport) {
|
|
177
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(defaultImport.getStart());
|
|
178
|
+
report.hallucinations.push({
|
|
179
|
+
file: sourceFile.fileName,
|
|
180
|
+
line: line + 1,
|
|
181
|
+
start: defaultImport.getStart(),
|
|
182
|
+
end: defaultImport.getEnd(),
|
|
183
|
+
module: moduleName,
|
|
184
|
+
member: node.importClause.isTypeOnly ? 'default (type)' : 'default'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else if (defaultSymbol) {
|
|
151
188
|
const { line } = sourceFile.getLineAndCharacterOfPosition(defaultImport.getStart());
|
|
152
189
|
trackedImports.set(defaultSymbol, {
|
|
153
190
|
file: sourceFile.fileName,
|
|
@@ -155,7 +192,7 @@ function visitImportDeclaration(node, sourceFile, checker, report, trackedImport
|
|
|
155
192
|
start: defaultImport.getStart(),
|
|
156
193
|
end: defaultImport.getEnd(),
|
|
157
194
|
module: moduleName,
|
|
158
|
-
member: 'default'
|
|
195
|
+
member: node.importClause.isTypeOnly ? 'default (type)' : 'default'
|
|
159
196
|
});
|
|
160
197
|
}
|
|
161
198
|
}
|
|
@@ -219,9 +256,51 @@ function visitImportDeclaration(node, sourceFile, checker, report, trackedImport
|
|
|
219
256
|
start: element.getStart(),
|
|
220
257
|
end: element.getEnd(),
|
|
221
258
|
module: moduleName,
|
|
222
|
-
member: importName
|
|
259
|
+
member: (node.importClause?.isTypeOnly || element.isTypeOnly) ? `${importName} (type)` : importName
|
|
223
260
|
});
|
|
224
261
|
}
|
|
225
262
|
});
|
|
226
263
|
}
|
|
227
264
|
}
|
|
265
|
+
function visitCallExpression(node, sourceFile, checker, report, allUsedModules) {
|
|
266
|
+
// Check for require('module')
|
|
267
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
268
|
+
if (node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0])) {
|
|
269
|
+
const moduleName = node.arguments[0].text;
|
|
270
|
+
allUsedModules.add(moduleName);
|
|
271
|
+
// Hallucination Check
|
|
272
|
+
const symbol = checker.getSymbolAtLocation(node.arguments[0]);
|
|
273
|
+
if (!symbol) {
|
|
274
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
275
|
+
report.hallucinations.push({
|
|
276
|
+
file: sourceFile.fileName,
|
|
277
|
+
line: line + 1,
|
|
278
|
+
start: node.getStart(),
|
|
279
|
+
end: node.getEnd(),
|
|
280
|
+
module: moduleName,
|
|
281
|
+
member: 'require()'
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Check for import('module')
|
|
287
|
+
else if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
288
|
+
if (node.arguments.length >= 1 && ts.isStringLiteral(node.arguments[0])) {
|
|
289
|
+
const moduleName = node.arguments[0].text;
|
|
290
|
+
allUsedModules.add(moduleName);
|
|
291
|
+
// Hallucination Check
|
|
292
|
+
const symbol = checker.getSymbolAtLocation(node.arguments[0]);
|
|
293
|
+
if (!symbol) {
|
|
294
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
295
|
+
report.hallucinations.push({
|
|
296
|
+
file: sourceFile.fileName,
|
|
297
|
+
line: line + 1,
|
|
298
|
+
start: node.getStart(),
|
|
299
|
+
end: node.getEnd(),
|
|
300
|
+
module: moduleName,
|
|
301
|
+
member: 'import()'
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,9 @@ program
|
|
|
54
54
|
.option('--interactive', 'Interactively fix unused imports and hallucinations')
|
|
55
55
|
.option('--prune', 'Uninstall completely unused dependencies from package.json')
|
|
56
56
|
.option('--uninstall-self', 'Uninstall ghost-import-hunter globally from your system')
|
|
57
|
+
.option('--output <file>', 'Output report to JSON or HTML file')
|
|
58
|
+
.option('--changed', 'Only scan files changed in git (staged or unstaged)')
|
|
59
|
+
.option('--watch', 'Watch mode: auto re-scan on file save')
|
|
57
60
|
.action(async (directory, options) => {
|
|
58
61
|
if (options.uninstallSelf) {
|
|
59
62
|
console.log(chalk_1.default.red('\n⚠️ WARNING: This will completely remove ghost-import-hunter from your system.'));
|
|
@@ -81,138 +84,172 @@ program
|
|
|
81
84
|
}
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
hasError = true;
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
console.log(chalk_1.default.green('\n✅ No Hallucinations detected.'));
|
|
99
|
-
}
|
|
100
|
-
if (report.unused.length > 0) {
|
|
101
|
-
console.log(chalk_1.default.yellow('\n⚠️ Unused Imports (Bloat):'));
|
|
102
|
-
report.unused.forEach(u => {
|
|
103
|
-
console.log(` - ${chalk_1.default.bold(u.module)}: Imported but never used.`);
|
|
104
|
-
console.log(` File: ${u.file}:${u.line}`);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
if (options.interactive) {
|
|
108
|
-
const fixes = [];
|
|
109
|
-
const allIssues = [...report.hallucinations, ...report.unused];
|
|
110
|
-
if (allIssues.length === 0) {
|
|
111
|
-
console.log(chalk_1.default.green('\n✨ No issues to fix!'));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
console.log(chalk_1.default.blue(`\n🕵️ Interactive Mode: Found ${allIssues.length} issues.`));
|
|
115
|
-
const rl = readline.createInterface({
|
|
116
|
-
input: process.stdin,
|
|
117
|
-
output: process.stdout
|
|
118
|
-
});
|
|
119
|
-
for (const issue of allIssues) {
|
|
120
|
-
const isHallucination = 'member' in issue && report.hallucinations.includes(issue);
|
|
121
|
-
const type = isHallucination ? chalk_1.default.red('Hallucination') : chalk_1.default.yellow('Unused');
|
|
122
|
-
console.log(chalk_1.default.gray('--------------------------------------------------'));
|
|
123
|
-
console.log(`${type}: ${chalk_1.default.bold(issue.module)} (${issue.member})`);
|
|
124
|
-
console.log(` File: ${issue.file}:${issue.line}`);
|
|
125
|
-
const answer = await new Promise(resolve => {
|
|
126
|
-
rl.question(chalk_1.default.cyan(' Action? [d]elete, [s]kip (default: skip): '), resolve);
|
|
87
|
+
const runAnalysis = async () => {
|
|
88
|
+
console.log(chalk_1.default.blue(`👻 Ghost Import Hunter scanning: ${directory}...`));
|
|
89
|
+
try {
|
|
90
|
+
// New v4 Engine using TS Compiler API
|
|
91
|
+
const report = await (0, analyzer_1.analyzeProject)(directory, { changedOnly: options.changed });
|
|
92
|
+
let hasError = false;
|
|
93
|
+
if (report.hallucinations.length > 0) {
|
|
94
|
+
console.log(chalk_1.default.red('\n🚨 Hallucinations Detected (AI Lied!):'));
|
|
95
|
+
report.hallucinations.forEach(h => {
|
|
96
|
+
console.log(` - ${chalk_1.default.bold(h.module)}: Used member ${chalk_1.default.bold(h.member)} does not exist.`);
|
|
97
|
+
console.log(` File: ${h.file}:${h.line}`);
|
|
127
98
|
});
|
|
128
|
-
|
|
129
|
-
fixes.push(issue);
|
|
130
|
-
console.log(chalk_1.default.green(' -> Marked for deletion.'));
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
console.log(chalk_1.default.gray(' -> Skipped.'));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
rl.close();
|
|
137
|
-
if (fixes.length > 0) {
|
|
138
|
-
console.log(chalk_1.default.blue(`\n🔧 Applying ${fixes.length} fixes...`));
|
|
139
|
-
await fixImports(fixes);
|
|
140
|
-
console.log(chalk_1.default.green('✨ Fixes applied!'));
|
|
99
|
+
hasError = true;
|
|
141
100
|
}
|
|
142
101
|
else {
|
|
143
|
-
console.log(chalk_1.default.
|
|
102
|
+
console.log(chalk_1.default.green('\n✅ No Hallucinations detected.'));
|
|
144
103
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
output: process.stdout
|
|
152
|
-
});
|
|
153
|
-
const answer = await new Promise(resolve => {
|
|
154
|
-
rl.question(chalk_1.default.yellow(`\n❓ Found ${report.unused.length} unused imports. Do you want to fix them? (y/N) `), resolve);
|
|
155
|
-
});
|
|
156
|
-
rl.close();
|
|
157
|
-
if (answer.toLowerCase() === 'y') {
|
|
158
|
-
console.log(chalk_1.default.blue('\n🔧 Fixing unused imports...'));
|
|
159
|
-
await fixImports(report.unused);
|
|
160
|
-
console.log(chalk_1.default.green('✨ Auto-fix complete!'));
|
|
104
|
+
if (report.unused.length > 0) {
|
|
105
|
+
console.log(chalk_1.default.yellow('\n⚠️ Unused Imports (Bloat):'));
|
|
106
|
+
report.unused.forEach(u => {
|
|
107
|
+
console.log(` - ${chalk_1.default.bold(u.module)}: Imported but never used.`);
|
|
108
|
+
console.log(` File: ${u.file}:${u.line}`);
|
|
109
|
+
});
|
|
161
110
|
}
|
|
162
|
-
|
|
163
|
-
|
|
111
|
+
if (options.output) {
|
|
112
|
+
const outPath = path.resolve(directory, options.output);
|
|
113
|
+
const ext = path.extname(outPath).toLowerCase();
|
|
114
|
+
if (ext === '.html') {
|
|
115
|
+
const html = `<html>
|
|
116
|
+
<head><title>Ghost Import Hunter Report</title><style>body{font-family:sans-serif;padding:20px;}</style></head>
|
|
117
|
+
<body><h1>Ghost Import Hunter Report</h1><pre>${JSON.stringify(report, null, 2)}</pre></body>
|
|
118
|
+
</html>`;
|
|
119
|
+
fs.writeFileSync(outPath, html);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
123
|
+
}
|
|
124
|
+
console.log(chalk_1.default.green(`\n📄 Report saved to ${options.output}`));
|
|
164
125
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
126
|
+
if (options.interactive) {
|
|
127
|
+
const fixes = [];
|
|
128
|
+
const allIssues = [...report.hallucinations, ...report.unused];
|
|
129
|
+
if (allIssues.length === 0) {
|
|
130
|
+
console.log(chalk_1.default.green('\n✨ No issues to fix!'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.log(chalk_1.default.blue(`\n🕵️ Interactive Mode: Found ${allIssues.length} issues.`));
|
|
134
|
+
const rl = readline.createInterface({
|
|
135
|
+
input: process.stdin,
|
|
136
|
+
output: process.stdout
|
|
137
|
+
});
|
|
138
|
+
for (const issue of allIssues) {
|
|
139
|
+
const isHallucination = 'member' in issue && report.hallucinations.includes(issue);
|
|
140
|
+
const type = isHallucination ? chalk_1.default.red('Hallucination') : chalk_1.default.yellow('Unused');
|
|
141
|
+
console.log(chalk_1.default.gray('--------------------------------------------------'));
|
|
142
|
+
console.log(`${type}: ${chalk_1.default.bold(issue.module)} (${issue.member})`);
|
|
143
|
+
console.log(` File: ${issue.file}:${issue.line}`);
|
|
182
144
|
const answer = await new Promise(resolve => {
|
|
183
|
-
rl.question(chalk_1.default.
|
|
145
|
+
rl.question(chalk_1.default.cyan(' Action? [d]elete, [s]kip (default: skip): '), resolve);
|
|
184
146
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
console.log(chalk_1.default.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
147
|
+
if (answer.toLowerCase() === 'd') {
|
|
148
|
+
fixes.push(issue);
|
|
149
|
+
console.log(chalk_1.default.green(' -> Marked for deletion.'));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.log(chalk_1.default.gray(' -> Skipped.'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
rl.close();
|
|
156
|
+
if (fixes.length > 0) {
|
|
157
|
+
console.log(chalk_1.default.blue(`\n🔧 Applying ${fixes.length} fixes...`));
|
|
158
|
+
await fixImports(fixes);
|
|
159
|
+
console.log(chalk_1.default.green('✨ Fixes applied!'));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log(chalk_1.default.yellow('\nℹ️ No changes made.'));
|
|
163
|
+
}
|
|
164
|
+
// Skip the batch block below if we ran interactive
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (options.fix && report.unused.length > 0) {
|
|
168
|
+
const rl = readline.createInterface({
|
|
169
|
+
input: process.stdin,
|
|
170
|
+
output: process.stdout
|
|
171
|
+
});
|
|
172
|
+
const answer = await new Promise(resolve => {
|
|
173
|
+
rl.question(chalk_1.default.yellow(`\n❓ Found ${report.unused.length} unused imports. Do you want to fix them? (y/N) `), resolve);
|
|
174
|
+
});
|
|
175
|
+
rl.close();
|
|
176
|
+
if (answer.toLowerCase() === 'y') {
|
|
177
|
+
console.log(chalk_1.default.blue('\n🔧 Fixing unused imports...'));
|
|
178
|
+
await fixImports(report.unused);
|
|
179
|
+
console.log(chalk_1.default.green('✨ Auto-fix complete!'));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(chalk_1.default.yellow('ℹ️ Auto-fix cancelled.'));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (options.prune) {
|
|
186
|
+
const packageJsonPath = path.join(directory, 'package.json');
|
|
187
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
188
|
+
console.log(chalk_1.default.blue('\n🔍 Checking for completely unused dependencies...'));
|
|
189
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
190
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
191
|
+
// Filter out types and our own tool just in case
|
|
192
|
+
const projectDeps = deps.filter(d => !d.startsWith('@types/') && d !== 'ghost-import-hunter');
|
|
193
|
+
const toRemove = projectDeps.filter(dep => !report.usedModules.includes(dep));
|
|
194
|
+
if (toRemove.length > 0) {
|
|
195
|
+
console.log(chalk_1.default.yellow(`\n🗑️ Found ${toRemove.length} unused dependencies:`));
|
|
196
|
+
toRemove.forEach(d => console.log(` - ${chalk_1.default.bold(d)}`));
|
|
197
|
+
const rl = readline.createInterface({
|
|
198
|
+
input: process.stdin,
|
|
199
|
+
output: process.stdout
|
|
200
|
+
});
|
|
201
|
+
const answer = await new Promise(resolve => {
|
|
202
|
+
rl.question(chalk_1.default.red(`\n❓ Are you sure you want to uninstall these packages? (y/N) `), resolve);
|
|
203
|
+
});
|
|
204
|
+
rl.close();
|
|
205
|
+
if (answer.toLowerCase() === 'y') {
|
|
206
|
+
console.log(chalk_1.default.blue(`\n📦 Uninstalling ${toRemove.join(', ')}...`));
|
|
207
|
+
try {
|
|
208
|
+
const { execSync } = require('child_process');
|
|
209
|
+
execSync(`npm uninstall ${toRemove.join(' ')}`, { stdio: 'inherit', cwd: directory });
|
|
210
|
+
console.log(chalk_1.default.green('✨ Pruning complete!'));
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
console.error(chalk_1.default.red('❌ Failed to uninstall packages:'), err);
|
|
214
|
+
}
|
|
192
215
|
}
|
|
193
|
-
|
|
194
|
-
console.
|
|
216
|
+
else {
|
|
217
|
+
console.log(chalk_1.default.yellow('ℹ️ Pruning cancelled.'));
|
|
195
218
|
}
|
|
196
219
|
}
|
|
197
220
|
else {
|
|
198
|
-
console.log(chalk_1.default.
|
|
221
|
+
console.log(chalk_1.default.green('\n✨ No unused dependencies found in package.json!'));
|
|
199
222
|
}
|
|
200
223
|
}
|
|
201
224
|
else {
|
|
202
|
-
console.log(chalk_1.default.
|
|
225
|
+
console.log(chalk_1.default.yellow('\n⚠️ No package.json found in the specified directory. Cannot prune dependencies.'));
|
|
203
226
|
}
|
|
204
227
|
}
|
|
205
|
-
|
|
206
|
-
|
|
228
|
+
if (!options.watch && hasError) {
|
|
229
|
+
process.exit(1);
|
|
207
230
|
}
|
|
208
231
|
}
|
|
209
|
-
|
|
210
|
-
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.error(chalk_1.default.red('Error scanning project:'), error);
|
|
234
|
+
if (!options.watch) {
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
211
237
|
}
|
|
238
|
+
};
|
|
239
|
+
if (options.watch) {
|
|
240
|
+
console.log(chalk_1.default.cyan(`\n👀 Watch mode enabled. Scanning on file changes...`));
|
|
241
|
+
await runAnalysis();
|
|
242
|
+
// Watch specific folder
|
|
243
|
+
fs.watch(directory, { recursive: true }, async (eventType, filename) => {
|
|
244
|
+
if (filename && (filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.js') || filename.endsWith('.jsx'))) {
|
|
245
|
+
console.clear();
|
|
246
|
+
console.log(chalk_1.default.cyan(`\n🔄 File changed: ${filename}. Re-scanning...`));
|
|
247
|
+
await runAnalysis();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
212
250
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
process.exit(1);
|
|
251
|
+
else {
|
|
252
|
+
await runAnalysis();
|
|
216
253
|
}
|
|
217
254
|
});
|
|
218
255
|
async function fixImports(unused) {
|
|
Binary file
|
package/package.json
CHANGED
package/media/header.png
DELETED
|
Binary file
|