thuban 0.3.0
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/LICENSE +21 -0
- package/README.md +150 -0
- package/cli.js +1902 -0
- package/package.json +49 -0
- package/packages/scanner/ai-confidence-scorer.js +260 -0
- package/packages/scanner/alert-manager.js +398 -0
- package/packages/scanner/baseline-manager.js +109 -0
- package/packages/scanner/code-scanner.js +712 -0
- package/packages/scanner/codebase-passport.js +299 -0
- package/packages/scanner/copy-paste-detector.js +250 -0
- package/packages/scanner/dependency-graph.js +539 -0
- package/packages/scanner/drift-detector.js +425 -0
- package/packages/scanner/executive-report.js +774 -0
- package/packages/scanner/file-watcher.js +323 -0
- package/packages/scanner/ghost-code-detector.js +226 -0
- package/packages/scanner/hallucination-detector.js +507 -0
- package/packages/scanner/health-checker.js +586 -0
- package/packages/scanner/html-report.js +634 -0
- package/packages/scanner/index.js +92 -0
- package/packages/scanner/license-manager.js +318 -0
- package/packages/scanner/master-health-checker.js +513 -0
- package/packages/scanner/pre-commit-gate.js +216 -0
- package/packages/scanner/sentinel-core.js +608 -0
- package/packages/scanner/sentinel-knowledge.js +322 -0
- package/packages/scanner/tech-debt-analyzer.js +483 -0
- package/packages/scanner/tech-debt-cost.js +194 -0
- package/packages/scanner/widget-generator.js +415 -0
package/cli.js
ADDED
|
@@ -0,0 +1,1902 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thuban CLI
|
|
5
|
+
*
|
|
6
|
+
* Code Health Engine — scan, map, fix.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx thuban scan [path] Scan a codebase for issues
|
|
10
|
+
* npx thuban deps [path] Map dependencies and detect circular imports
|
|
11
|
+
* npx thuban drift [path] Detect drift from Mother Code annotations
|
|
12
|
+
* npx thuban inject [path] Inject Mother Code DNA into files
|
|
13
|
+
* npx thuban health [path] Run health check on codebase
|
|
14
|
+
* npx thuban report [path] Full scan + deps + drift combined report
|
|
15
|
+
* npx thuban hallucinate [path] Detect AI hallucinations and phantom imports
|
|
16
|
+
* npx thuban watch [path] Live sentinel — continuous file monitoring
|
|
17
|
+
* npx thuban dashboard [path] Generate visual HTML dashboard report
|
|
18
|
+
* npx thuban diff [path] Scan only git-changed files
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
|
|
24
|
+
// Import engine components
|
|
25
|
+
const CodeScanner = require('./packages/scanner/code-scanner.js');
|
|
26
|
+
const DependencyGraph = require('./packages/scanner/dependency-graph.js');
|
|
27
|
+
const DriftDetector = require('./packages/scanner/drift-detector.js');
|
|
28
|
+
const WidgetGenerator = require('./packages/scanner/widget-generator.js');
|
|
29
|
+
const AlertManager = require('./packages/scanner/alert-manager.js');
|
|
30
|
+
const TechDebtAnalyzer = require('./packages/scanner/tech-debt-analyzer.js');
|
|
31
|
+
const HallucinationDetector = require('./packages/scanner/hallucination-detector.js');
|
|
32
|
+
const HtmlReportGenerator = require('./packages/scanner/html-report.js');
|
|
33
|
+
const FileWatcher = require('./packages/scanner/file-watcher.js');
|
|
34
|
+
const LicenseManager = require('./packages/scanner/license-manager.js');
|
|
35
|
+
const PreCommitGate = require('./packages/scanner/pre-commit-gate.js');
|
|
36
|
+
const TechDebtCostCalculator = require('./packages/scanner/tech-debt-cost.js');
|
|
37
|
+
const GhostCodeDetector = require('./packages/scanner/ghost-code-detector.js');
|
|
38
|
+
const AIConfidenceScorer = require('./packages/scanner/ai-confidence-scorer.js');
|
|
39
|
+
const CopyPasteDriftDetector = require('./packages/scanner/copy-paste-detector.js');
|
|
40
|
+
const CodebasePassport = require('./packages/scanner/codebase-passport.js');
|
|
41
|
+
const ExecutiveReport = require('./packages/scanner/executive-report.js');
|
|
42
|
+
const BaselineManager = require('./packages/scanner/baseline-manager.js');
|
|
43
|
+
|
|
44
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const COLORS = {
|
|
47
|
+
reset: '\x1b[0m',
|
|
48
|
+
bold: '\x1b[1m',
|
|
49
|
+
red: '\x1b[31m',
|
|
50
|
+
green: '\x1b[32m',
|
|
51
|
+
yellow: '\x1b[33m',
|
|
52
|
+
blue: '\x1b[34m',
|
|
53
|
+
cyan: '\x1b[36m',
|
|
54
|
+
gray: '\x1b[90m',
|
|
55
|
+
white: '\x1b[37m',
|
|
56
|
+
bgRed: '\x1b[41m',
|
|
57
|
+
bgGreen: '\x1b[42m',
|
|
58
|
+
bgYellow: '\x1b[43m',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function c(color, text) {
|
|
62
|
+
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function banner() {
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(c('cyan', ' ╔══════════════════════════════════════╗'));
|
|
68
|
+
console.log(c('cyan', ' ║') + c('bold', ' THUBAN Code Health Engine ') + c('cyan', '║'));
|
|
69
|
+
console.log(c('cyan', ' ║') + c('gray', ' Scan · Map · Detect · Fix ') + c('cyan', '║'));
|
|
70
|
+
console.log(c('cyan', ' ╚══════════════════════════════════════╝'));
|
|
71
|
+
console.log('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function printHelp() {
|
|
75
|
+
banner();
|
|
76
|
+
const lm = new LicenseManager();
|
|
77
|
+
const status = lm.getStatus();
|
|
78
|
+
console.log(c('gray', ` License: ${status.tierName} tier`) + (status.licensed ? c('green', ' ✓') : ''));
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(c('bold', ' USAGE'));
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(` ${c('cyan', 'thuban scan')} [path] Scan for code issues`);
|
|
83
|
+
console.log(` ${c('cyan', 'thuban deps')} [path] Map dependencies`);
|
|
84
|
+
console.log(` ${c('cyan', 'thuban drift')} [path] Detect drift from annotations`);
|
|
85
|
+
console.log(` ${c('cyan', 'thuban inject')} [path] Inject Mother Code DNA`);
|
|
86
|
+
console.log(` ${c('cyan', 'thuban health')} [path] Codebase health check`);
|
|
87
|
+
console.log(` ${c('cyan', 'thuban report')} [path] Full combined report`);
|
|
88
|
+
console.log(` ${c('cyan', 'thuban debt')} [path] Tech debt analysis with scores`);
|
|
89
|
+
console.log(` ${c('cyan', 'thuban fix')} [path] Auto-fix all fixable tech debt`);
|
|
90
|
+
console.log(` ${c('cyan', 'thuban hallucinate')} [path] Detect AI hallucinations`);
|
|
91
|
+
console.log(` ${c('cyan', 'thuban watch')} [path] Live sentinel monitoring`);
|
|
92
|
+
console.log(` ${c('cyan', 'thuban dashboard')} [path] Generate HTML report`);
|
|
93
|
+
console.log(` ${c('cyan', 'thuban diff')} [path] Scan only git-changed files`);
|
|
94
|
+
console.log(` ${c('cyan', 'thuban gate')} Pre-commit hallucination blocker`);
|
|
95
|
+
console.log(` ${c('cyan', 'thuban cost')} [path] Tech debt cost calculator`);
|
|
96
|
+
console.log(` ${c('cyan', 'thuban ghosts')} [path] Find dead AI-pasted code`);
|
|
97
|
+
console.log(` ${c('cyan', 'thuban ai-score')} [path] AI confidence score per function`);
|
|
98
|
+
console.log(` ${c('cyan', 'thuban clones')} [path] Copy-paste drift detection`);
|
|
99
|
+
console.log(` ${c('cyan', 'thuban passport')} [path] Generate codebase identity file`);
|
|
100
|
+
console.log(` ${c('cyan', 'thuban executive')} [path] CTO-ready PDF report (print from browser)`);
|
|
101
|
+
console.log(` ${c('cyan', 'thuban baseline')} [path] Create/check baseline (only fail on NEW issues)`);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(c('bold', ' LICENSE'));
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(` ${c('cyan', 'thuban activate')} <key> Activate a license key`);
|
|
106
|
+
console.log(` ${c('cyan', 'thuban status')} Show license and usage status`);
|
|
107
|
+
console.log(` ${c('cyan', 'thuban upgrade')} Get a license key`);
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(c('bold', ' OPTIONS'));
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(` ${c('gray', '--json')} Output as JSON`);
|
|
112
|
+
console.log(` ${c('gray', '--fix')} Apply fixes (default: dry-run preview)`);
|
|
113
|
+
console.log(` ${c('gray', '--dry-run')} Preview changes without applying`);
|
|
114
|
+
console.log(` ${c('gray', '--unsafe')} Allow runtime-changing fixes`);
|
|
115
|
+
console.log(` ${c('gray', '--commit')} Auto-commit each fix for easy rollback`);
|
|
116
|
+
console.log(` ${c('gray', '--baseline')} Only report NEW issues (vs baseline)`);
|
|
117
|
+
console.log(` ${c('gray', '--baseline-create')} Snapshot current issues as baseline`);
|
|
118
|
+
console.log(` ${c('gray', '--verbose')} Show detailed output`);
|
|
119
|
+
console.log(` ${c('gray', '--ignore <pattern>')} Ignore files matching pattern`);
|
|
120
|
+
console.log(` ${c('gray', '--max-issues <n>')} Limit issues shown (default: 50)`);
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(c('gray', ' https://thuban.dev'));
|
|
123
|
+
console.log('');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseArgs(args) {
|
|
127
|
+
const opts = {
|
|
128
|
+
command: null,
|
|
129
|
+
targetPath: process.cwd(),
|
|
130
|
+
json: false,
|
|
131
|
+
fix: false,
|
|
132
|
+
dryRun: false,
|
|
133
|
+
safe: true,
|
|
134
|
+
gitCommit: false,
|
|
135
|
+
verbose: false,
|
|
136
|
+
ignore: [],
|
|
137
|
+
maxIssues: 50,
|
|
138
|
+
licenseKey: null,
|
|
139
|
+
baseline: false,
|
|
140
|
+
baselineCreate: false,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const noPathCommands = ['activate', 'status', 'upgrade', 'help', 'version'];
|
|
144
|
+
|
|
145
|
+
let i = 0;
|
|
146
|
+
while (i < args.length) {
|
|
147
|
+
const arg = args[i];
|
|
148
|
+
if (arg === '--json') { opts.json = true; }
|
|
149
|
+
else if (arg === '--fix') { opts.fix = true; }
|
|
150
|
+
else if (arg === '--dry-run' || arg === '--dryrun') { opts.dryRun = true; }
|
|
151
|
+
else if (arg === '--unsafe') { opts.safe = false; }
|
|
152
|
+
else if (arg === '--git-commit' || arg === '--commit') { opts.gitCommit = true; }
|
|
153
|
+
else if (arg === '--baseline') { opts.baseline = true; }
|
|
154
|
+
else if (arg === '--baseline-create') { opts.baselineCreate = true; }
|
|
155
|
+
else if (arg === '--verbose') { opts.verbose = true; }
|
|
156
|
+
else if (arg === '--ignore' && args[i + 1]) { opts.ignore.push(args[++i]); }
|
|
157
|
+
else if (arg === '--max-issues' && args[i + 1]) { opts.maxIssues = parseInt(args[++i], 10); }
|
|
158
|
+
else if (arg === '--help' || arg === '-h') { opts.command = 'help'; }
|
|
159
|
+
else if (arg === '--version' || arg === '-v') { opts.command = 'version'; }
|
|
160
|
+
else if (!opts.command) { opts.command = arg; }
|
|
161
|
+
else if (opts.command === 'activate' && !opts.licenseKey) { opts.licenseKey = arg; }
|
|
162
|
+
else if (!noPathCommands.includes(opts.command)) { opts.targetPath = path.resolve(arg); }
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return opts;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Collect files recursively ───────────────────────────────
|
|
170
|
+
|
|
171
|
+
function collectFiles(dir, ignorePatterns = []) {
|
|
172
|
+
const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv', 'coverage', '.cache', '.turbo'];
|
|
173
|
+
const allIgnore = [...defaultIgnore, ...ignorePatterns];
|
|
174
|
+
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.json', '.mjs', '.cjs'];
|
|
175
|
+
const MAX_FILE_SIZE = 1024 * 1024; // 1MB per file — skip anything larger
|
|
176
|
+
const MAX_FILES = 50000; // Hard cap to prevent memory issues
|
|
177
|
+
const files = [];
|
|
178
|
+
|
|
179
|
+
function walk(currentDir) {
|
|
180
|
+
if (files.length >= MAX_FILES) return;
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (files.length >= MAX_FILES) return;
|
|
189
|
+
if (allIgnore.some(p => entry.name === p || entry.name.match(p))) continue;
|
|
190
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
191
|
+
if (entry.isDirectory()) {
|
|
192
|
+
walk(fullPath);
|
|
193
|
+
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
194
|
+
try {
|
|
195
|
+
const stat = fs.statSync(fullPath);
|
|
196
|
+
if (stat.size <= MAX_FILE_SIZE) {
|
|
197
|
+
files.push(fullPath);
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// Skip files we can't stat
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
walk(dir);
|
|
207
|
+
return files;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Commands ───────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async function cmdScan(opts) {
|
|
213
|
+
const startTime = Date.now();
|
|
214
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
215
|
+
|
|
216
|
+
// ─── License check ───────────────────────────────────────
|
|
217
|
+
const lm = new LicenseManager();
|
|
218
|
+
const tier = lm.getTierConfig();
|
|
219
|
+
const scanCheck = lm.canScan();
|
|
220
|
+
|
|
221
|
+
if (!scanCheck.allowed && !opts.json) {
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(c('red', ' ╔══════════════════════════════════════════════════╗'));
|
|
224
|
+
console.log(c('red', ' ║') + c('bold', ' Monthly scan limit reached ') + c('red', '║'));
|
|
225
|
+
console.log(c('red', ' ╚══════════════════════════════════════════════════╝'));
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log(` You've used ${c('bold', scanCheck.used + '/' + scanCheck.limit)} free scans this month.`);
|
|
228
|
+
console.log(` Resets on ${c('cyan', scanCheck.resetsOn)}.`);
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(` ${c('cyan', 'Upgrade to Pro')} for unlimited scans, full reports, and auto-fix:`);
|
|
231
|
+
console.log(` ${c('bold', 'https://thuban.dev/pricing')}`);
|
|
232
|
+
console.log('');
|
|
233
|
+
console.log(` Or activate a key: ${c('cyan', 'thuban activate <your-key>')}`);
|
|
234
|
+
console.log('');
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!opts.json) {
|
|
239
|
+
banner();
|
|
240
|
+
console.log(c('bold', ' SCANNING: ') + c('cyan', targetPath));
|
|
241
|
+
console.log('');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const allFiles = collectFiles(targetPath, opts.ignore);
|
|
245
|
+
const totalFileCount = allFiles.length;
|
|
246
|
+
|
|
247
|
+
// File limit gating
|
|
248
|
+
const files = tier.maxFiles < Infinity ? allFiles.slice(0, tier.maxFiles) : allFiles;
|
|
249
|
+
const isFileCapped = totalFileCount > tier.maxFiles;
|
|
250
|
+
|
|
251
|
+
if (!opts.json && isFileCapped) {
|
|
252
|
+
console.log(c('yellow', ` Free tier: scanning ${tier.maxFiles} of ${totalFileCount} files`));
|
|
253
|
+
console.log(c('gray', ` Upgrade to Pro to scan your entire codebase`));
|
|
254
|
+
console.log('');
|
|
255
|
+
} else if (!opts.json) {
|
|
256
|
+
console.log(c('gray', ` Found ${files.length} files to scan...`));
|
|
257
|
+
console.log('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Run all analyses in parallel for the gut-punch report
|
|
261
|
+
const scanner = new CodeScanner({
|
|
262
|
+
rootPath: targetPath,
|
|
263
|
+
ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
|
|
264
|
+
});
|
|
265
|
+
const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
266
|
+
|
|
267
|
+
const [scanResults, hallResults] = await Promise.all([
|
|
268
|
+
scanner.scanFiles(files),
|
|
269
|
+
hallucinationDetector.scan(files),
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
const elapsed = Date.now() - startTime;
|
|
273
|
+
|
|
274
|
+
// Record this scan
|
|
275
|
+
lm.recordScan();
|
|
276
|
+
|
|
277
|
+
// ─── Collect all issues ──────────────────────────────────
|
|
278
|
+
const allIssues = [];
|
|
279
|
+
for (const [file, fileResults] of Object.entries(scanResults)) {
|
|
280
|
+
if (fileResults.issues) {
|
|
281
|
+
for (const issue of fileResults.issues) {
|
|
282
|
+
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Add hallucination results as issues
|
|
288
|
+
for (const item of hallResults.phantomAPIs) {
|
|
289
|
+
allIssues.push({
|
|
290
|
+
file: item.file, line: item.line, severity: 'critical',
|
|
291
|
+
category: 'hallucination', id: 'HALL_API',
|
|
292
|
+
message: `${item.name} — this API does not exist`,
|
|
293
|
+
suggestion: item.suggestion,
|
|
294
|
+
crash: true,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
for (const item of hallResults.phantomImports) {
|
|
298
|
+
allIssues.push({
|
|
299
|
+
file: item.file, line: item.line, severity: 'critical',
|
|
300
|
+
category: 'hallucination', id: 'HALL_IMPORT',
|
|
301
|
+
message: `${item.module} — ${item.reason}`,
|
|
302
|
+
crash: true,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
for (const item of hallResults.deprecatedAPIs) {
|
|
306
|
+
allIssues.push({
|
|
307
|
+
file: item.file, line: item.line, severity: 'high',
|
|
308
|
+
category: 'deprecated', id: 'DEPR_API',
|
|
309
|
+
message: `${item.name} — deprecated since ${item.since}`,
|
|
310
|
+
suggestion: item.suggestion,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
for (const item of hallResults.aiSmells) {
|
|
314
|
+
allIssues.push({
|
|
315
|
+
file: item.file, line: item.line, severity: 'warning',
|
|
316
|
+
category: 'ai_smell', id: item.id,
|
|
317
|
+
message: item.message,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (opts.json) {
|
|
322
|
+
console.log(JSON.stringify({
|
|
323
|
+
files: totalFileCount,
|
|
324
|
+
filesScanned: files.length,
|
|
325
|
+
elapsed_ms: elapsed,
|
|
326
|
+
tier: lm.getTier(),
|
|
327
|
+
issues: tier.showFullDetails ? allIssues : allIssues.slice(0, tier.teaserIssues),
|
|
328
|
+
totalIssues: allIssues.length,
|
|
329
|
+
gated: !tier.showFullDetails,
|
|
330
|
+
}, null, 2));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─── Sort by severity ────────────────────────────────────
|
|
335
|
+
const severityOrder = { critical: 0, high: 1, error: 2, medium: 3, warning: 4, low: 5, info: 6 };
|
|
336
|
+
allIssues.sort((a, b) => (severityOrder[a.severity] || 99) - (severityOrder[b.severity] || 99));
|
|
337
|
+
|
|
338
|
+
const criticals = allIssues.filter(i => i.severity === 'critical');
|
|
339
|
+
const highs = allIssues.filter(i => i.severity === 'high' || i.severity === 'error');
|
|
340
|
+
const mediums = allIssues.filter(i => i.severity === 'medium' || i.severity === 'warning');
|
|
341
|
+
const lows = allIssues.filter(i => i.severity === 'low' || i.severity === 'info');
|
|
342
|
+
const hallucinations = allIssues.filter(i => i.category === 'hallucination');
|
|
343
|
+
const deprecated = allIssues.filter(i => i.category === 'deprecated');
|
|
344
|
+
|
|
345
|
+
// ─── Health score ────────────────────────────────────────
|
|
346
|
+
// Weight by category: hallucinations are critical, AI smells are minor
|
|
347
|
+
const aiSmellIssues = allIssues.filter(i => i.category === 'ai_smell');
|
|
348
|
+
const nonSmellMediums = mediums.filter(i => i.category !== 'ai_smell');
|
|
349
|
+
const score = Math.max(0, 100
|
|
350
|
+
- (criticals.length * 15)
|
|
351
|
+
- (highs.length * 5)
|
|
352
|
+
- (nonSmellMediums.length * 2)
|
|
353
|
+
- (aiSmellIssues.length * 0.5)
|
|
354
|
+
- (lows.length * 0.25)
|
|
355
|
+
);
|
|
356
|
+
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C+' : score >= 60 ? 'C' : score >= 50 ? 'D+' : score >= 40 ? 'D' : 'F';
|
|
357
|
+
const scoreColor = score >= 80 ? 'green' : score >= 60 ? 'yellow' : 'red';
|
|
358
|
+
|
|
359
|
+
// ═══════════════════════════════════════════════════════════
|
|
360
|
+
// THE GUT-PUNCH OUTPUT
|
|
361
|
+
// ═══════════════════════════════════════════════════════════
|
|
362
|
+
|
|
363
|
+
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
364
|
+
console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN ') + c('cyan', '║'));
|
|
365
|
+
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
366
|
+
console.log(c('cyan', ' ║') + ` Files scanned: ${c('bold', String(files.length).padEnd(6))}` + (isFileCapped ? c('yellow', ` of ${totalFileCount}`) : ' ') + c('cyan', ' ║'));
|
|
367
|
+
console.log(c('cyan', ' ║') + ` Health score: ${c(scoreColor, (grade + ' (' + Math.round(score) + '/100)').padEnd(20))}` + c('cyan', ' ║'));
|
|
368
|
+
console.log(c('cyan', ' ║') + ` Scan time: ${c('gray', (elapsed + 'ms').padEnd(20))}` + c('cyan', ' ║'));
|
|
369
|
+
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
370
|
+
console.log('');
|
|
371
|
+
|
|
372
|
+
// ─── CRITICAL: Hallucinated APIs (will crash) ────────────
|
|
373
|
+
if (hallucinations.length > 0) {
|
|
374
|
+
console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)) + c('red', ' (will crash at runtime)'));
|
|
375
|
+
console.log('');
|
|
376
|
+
|
|
377
|
+
const showCount = tier.showFullDetails ? hallucinations.length : Math.min(tier.teaserIssues, hallucinations.length);
|
|
378
|
+
for (let i = 0; i < showCount; i++) {
|
|
379
|
+
const item = hallucinations[i];
|
|
380
|
+
const lineRef = item.line ? `:${item.line}` : '';
|
|
381
|
+
console.log(` ${c('cyan', item.file + lineRef)}`);
|
|
382
|
+
// Try to read the actual source line for maximum impact
|
|
383
|
+
try {
|
|
384
|
+
const fullPath = path.resolve(targetPath, item.file);
|
|
385
|
+
const lines = fs.readFileSync(fullPath, 'utf-8').split('\n');
|
|
386
|
+
if (item.line && lines[item.line - 1]) {
|
|
387
|
+
console.log(` ${c('red', lines[item.line - 1].trim())}`);
|
|
388
|
+
}
|
|
389
|
+
} catch (e) { /* skip */ }
|
|
390
|
+
console.log(` ${c('yellow', '^^^')} ${item.message}`);
|
|
391
|
+
if (item.suggestion) {
|
|
392
|
+
console.log(` ${c('green', 'Fix:')} ${item.suggestion}`);
|
|
393
|
+
}
|
|
394
|
+
console.log('');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (hallucinations.length > showCount) {
|
|
398
|
+
console.log(c('gray', ` ... ${hallucinations.length - showCount} more hallucinations hidden`));
|
|
399
|
+
console.log(c('cyan', ` Upgrade to Pro to see all: `) + c('bold', 'thuban upgrade'));
|
|
400
|
+
console.log('');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ─── HIGH: Deprecated APIs ───────────────────────────────
|
|
405
|
+
if (deprecated.length > 0) {
|
|
406
|
+
console.log(c('red', c('bold', ` HIGH: ${deprecated.length} Deprecated API${deprecated.length > 1 ? 's' : ''}`)) + c('gray', ' (will break on Node upgrade)'));
|
|
407
|
+
console.log('');
|
|
408
|
+
|
|
409
|
+
const showCount = tier.showFullDetails ? deprecated.length : Math.min(tier.teaserIssues, deprecated.length);
|
|
410
|
+
for (let i = 0; i < showCount; i++) {
|
|
411
|
+
const item = deprecated[i];
|
|
412
|
+
const lineRef = item.line ? `:${item.line}` : '';
|
|
413
|
+
console.log(` ${c('cyan', item.file + lineRef)}`);
|
|
414
|
+
console.log(` ${c('yellow', item.message)}`);
|
|
415
|
+
if (item.suggestion) {
|
|
416
|
+
console.log(` ${c('green', '→')} ${item.suggestion}`);
|
|
417
|
+
}
|
|
418
|
+
console.log('');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (deprecated.length > showCount) {
|
|
422
|
+
console.log(c('gray', ` ... ${deprecated.length - showCount} more hidden`));
|
|
423
|
+
console.log('');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─── Security & quality issues ───────────────────────────
|
|
428
|
+
const securityIssues = allIssues.filter(i => i.category === 'security');
|
|
429
|
+
const qualityIssues = allIssues.filter(i => i.category === 'quality' || i.category === 'performance' || i.category === 'ai' || i.category === 'ai_smell');
|
|
430
|
+
|
|
431
|
+
if (securityIssues.length > 0) {
|
|
432
|
+
console.log(c('red', c('bold', ` SECURITY: ${securityIssues.length} issue${securityIssues.length > 1 ? 's' : ''}`)));
|
|
433
|
+
const showCount = tier.showFullDetails ? Math.min(securityIssues.length, 10) : Math.min(tier.teaserIssues, securityIssues.length);
|
|
434
|
+
for (let i = 0; i < showCount; i++) {
|
|
435
|
+
const item = securityIssues[i];
|
|
436
|
+
const lineRef = item.line ? `:${item.line}` : '';
|
|
437
|
+
console.log(` ${c('red', '✗')} ${c('cyan', item.file + lineRef)} ${c('gray', item.message)}`);
|
|
438
|
+
}
|
|
439
|
+
if (securityIssues.length > showCount) {
|
|
440
|
+
console.log(c('gray', ` ... ${securityIssues.length - showCount} more hidden`));
|
|
441
|
+
}
|
|
442
|
+
console.log('');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (qualityIssues.length > 0) {
|
|
446
|
+
console.log(c('yellow', c('bold', ` CODE QUALITY: ${qualityIssues.length} issue${qualityIssues.length > 1 ? 's' : ''}`)));
|
|
447
|
+
const showCount = tier.showFullDetails ? Math.min(qualityIssues.length, 10) : Math.min(3, qualityIssues.length);
|
|
448
|
+
for (const item of qualityIssues.slice(0, showCount)) {
|
|
449
|
+
const lineRef = item.line ? `:${item.line}` : '';
|
|
450
|
+
const sevColor = item.severity === 'high' || item.severity === 'error' ? 'red' : 'yellow';
|
|
451
|
+
console.log(` ${c(sevColor, '!')} ${c('cyan', item.file + lineRef)} ${c('gray', item.message)}`);
|
|
452
|
+
}
|
|
453
|
+
if (qualityIssues.length > showCount) {
|
|
454
|
+
console.log(c('gray', ` ... ${qualityIssues.length - showCount} more`));
|
|
455
|
+
}
|
|
456
|
+
console.log('');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ─── THE VERDICT ─────────────────────────────────────────
|
|
460
|
+
console.log(c('bold', ' ┌──────────────────────────────────────────────────────┐'));
|
|
461
|
+
|
|
462
|
+
if (score < 60) {
|
|
463
|
+
console.log(c('bold', ' │ ') + c('red', 'YOUR CODEBASE HAS A SERIOUS HEALTH PROBLEM ') + c('bold', ' │'));
|
|
464
|
+
} else if (score < 80) {
|
|
465
|
+
console.log(c('bold', ' │ ') + c('yellow', 'YOUR CODEBASE NEEDS ATTENTION ') + c('bold', ' │'));
|
|
466
|
+
} else {
|
|
467
|
+
console.log(c('bold', ' │ ') + c('green', 'YOUR CODEBASE IS IN GOOD SHAPE ') + c('bold', ' │'));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log(c('bold', ' │') + ` Score: ${c(scoreColor, grade + ' (' + Math.round(score) + '/100)')} ` + c('bold', '│'));
|
|
471
|
+
console.log(c('bold', ' └──────────────────────────────────────────────────────┘'));
|
|
472
|
+
console.log('');
|
|
473
|
+
|
|
474
|
+
// ─── Impact statement ────────────────────────────────────
|
|
475
|
+
if (criticals.length > 0) {
|
|
476
|
+
console.log(c('red', ` ${criticals.length} function${criticals.length > 1 ? 's' : ''} will crash in production.`));
|
|
477
|
+
const fixTime = criticals.length <= 3 ? '30 seconds' : criticals.length <= 10 ? '2 minutes' : '10 minutes';
|
|
478
|
+
console.log(c('gray', ` Estimated cleanup: ${fixTime} with Thuban auto-fix.`));
|
|
479
|
+
console.log(c('gray', ` Estimated cost if shipped: your weekend.`));
|
|
480
|
+
console.log('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ─── FREE TIER: Upgrade CTA ──────────────────────────────
|
|
484
|
+
if (!tier.showFullDetails) {
|
|
485
|
+
const hidden = allIssues.length - tier.teaserIssues;
|
|
486
|
+
if (hidden > 0) {
|
|
487
|
+
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
488
|
+
console.log(c('cyan', ' ║') + c('bold', ` ${hidden} more issues hidden `) + c('cyan', '║'));
|
|
489
|
+
console.log(c('cyan', ' ║') + c('gray', ' Upgrade to see every issue, fix them automatically,') + c('cyan', '║'));
|
|
490
|
+
console.log(c('cyan', ' ║') + c('gray', ' and get the full HTML dashboard report. ') + c('cyan', '║'));
|
|
491
|
+
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
492
|
+
console.log(c('cyan', ' ║') + ` ${c('bold', 'thuban upgrade')} Open pricing page ` + c('cyan', '║'));
|
|
493
|
+
console.log(c('cyan', ' ║') + ` ${c('bold', 'thuban activate')} <key> Activate a license key ` + c('cyan', '║'));
|
|
494
|
+
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
495
|
+
console.log('');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Show scan counter
|
|
499
|
+
const scanStatus = lm.canScan();
|
|
500
|
+
const remaining = scanStatus.remaining;
|
|
501
|
+
console.log(c('gray', ` Free scans remaining this month: ${remaining}/${scanStatus.limit}`));
|
|
502
|
+
console.log('');
|
|
503
|
+
} else {
|
|
504
|
+
// Pro/Team: show fix command
|
|
505
|
+
if (allIssues.length > 0) {
|
|
506
|
+
console.log(` ${c('cyan', 'thuban fix . --fix')} Auto-fix all fixable issues`);
|
|
507
|
+
console.log(` ${c('cyan', 'thuban dashboard .')} Generate full HTML report`);
|
|
508
|
+
console.log('');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (isFileCapped) {
|
|
513
|
+
console.log(c('yellow', ` ⚠ ${totalFileCount - tier.maxFiles} files were not scanned (free tier limit: ${tier.maxFiles} files)`));
|
|
514
|
+
console.log(c('gray', ` Your actual score may be worse. Upgrade to scan everything.`));
|
|
515
|
+
console.log('');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function cmdDeps(opts) {
|
|
520
|
+
const startTime = Date.now();
|
|
521
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
522
|
+
|
|
523
|
+
if (!opts.json) {
|
|
524
|
+
banner();
|
|
525
|
+
console.log(c('bold', ' MAPPING DEPENDENCIES: ') + c('cyan', targetPath));
|
|
526
|
+
console.log('');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
530
|
+
const graph = new DependencyGraph({ rootPath: targetPath });
|
|
531
|
+
await graph.build(files);
|
|
532
|
+
const elapsed = Date.now() - startTime;
|
|
533
|
+
|
|
534
|
+
const summary = graph.getModuleSummary();
|
|
535
|
+
const critical = graph.getMostCriticalFiles(10);
|
|
536
|
+
const orphans = graph.getOrphanFiles();
|
|
537
|
+
const circular = graph.circularDeps || [];
|
|
538
|
+
|
|
539
|
+
if (opts.json) {
|
|
540
|
+
console.log(JSON.stringify({ elapsed_ms: elapsed, summary, critical, orphans, circular, graph: graph.toJSON() }, null, 2));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
console.log(c('gray', ` Analysed ${files.length} files in ${elapsed}ms`));
|
|
545
|
+
console.log('');
|
|
546
|
+
|
|
547
|
+
console.log(c('bold', ' DEPENDENCY SUMMARY'));
|
|
548
|
+
console.log(` Files: ${summary.totalFiles || files.length}`);
|
|
549
|
+
console.log(` Import links: ${summary.totalImports || 0}`);
|
|
550
|
+
console.log(` Circular deps: ${circular.length > 0 ? c('red', circular.length) : c('green', '0')}`);
|
|
551
|
+
console.log(` Orphan files: ${orphans.length > 0 ? c('yellow', orphans.length) : c('green', '0')}`);
|
|
552
|
+
console.log('');
|
|
553
|
+
|
|
554
|
+
if (critical.length > 0) {
|
|
555
|
+
console.log(c('bold', ' MOST CRITICAL FILES') + c('gray', ' (most dependents)'));
|
|
556
|
+
for (const f of critical) {
|
|
557
|
+
const filePath = typeof f === 'string' ? f : (f.file || f.path || String(f));
|
|
558
|
+
const rel = path.relative(targetPath, filePath);
|
|
559
|
+
const deps = typeof f === 'object' ? (f.dependents || f.score || f.count || '?') : '?';
|
|
560
|
+
console.log(` ${c('cyan', rel)} ${c('gray', '→ ' + deps + ' dependents')}`);
|
|
561
|
+
}
|
|
562
|
+
console.log('');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (circular.length > 0) {
|
|
566
|
+
console.log(c('red', ' CIRCULAR DEPENDENCIES'));
|
|
567
|
+
for (const cycle of circular.slice(0, 5)) {
|
|
568
|
+
const cycleStr = Array.isArray(cycle) ? cycle.map(f => path.relative(targetPath, f)).join(' → ') : String(cycle);
|
|
569
|
+
console.log(` ${c('yellow', '⟲')} ${cycleStr}`);
|
|
570
|
+
}
|
|
571
|
+
console.log('');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (orphans.length > 0) {
|
|
575
|
+
console.log(c('yellow', ' ORPHAN FILES') + c('gray', ' (no imports, no exports consumed)'));
|
|
576
|
+
for (const orphan of orphans.slice(0, 10)) {
|
|
577
|
+
const orphanPath = typeof orphan === 'string' ? orphan : (orphan.file || orphan.path || String(orphan));
|
|
578
|
+
console.log(` ${c('gray', '○')} ${path.relative(targetPath, orphanPath)}`);
|
|
579
|
+
}
|
|
580
|
+
if (orphans.length > 10) {
|
|
581
|
+
console.log(c('gray', ` ... and ${orphans.length - 10} more`));
|
|
582
|
+
}
|
|
583
|
+
console.log('');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function cmdDrift(opts) {
|
|
588
|
+
const startTime = Date.now();
|
|
589
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
590
|
+
|
|
591
|
+
if (!opts.json) {
|
|
592
|
+
banner();
|
|
593
|
+
console.log(c('bold', ' DETECTING DRIFT: ') + c('cyan', targetPath));
|
|
594
|
+
console.log('');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
598
|
+
const detector = new DriftDetector({ rootPath: targetPath });
|
|
599
|
+
|
|
600
|
+
const results = [];
|
|
601
|
+
let withWidget = 0;
|
|
602
|
+
let withDrift = 0;
|
|
603
|
+
|
|
604
|
+
for (const file of files) {
|
|
605
|
+
try {
|
|
606
|
+
const result = await detector.analyzeFile(file);
|
|
607
|
+
if (result && result.hasWidget) {
|
|
608
|
+
withWidget++;
|
|
609
|
+
if (result.drifts && result.drifts.length > 0) {
|
|
610
|
+
withDrift++;
|
|
611
|
+
results.push({ file: path.relative(targetPath, file), ...result });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch (e) { /* skip unreadable files */ }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const elapsed = Date.now() - startTime;
|
|
618
|
+
|
|
619
|
+
if (opts.json) {
|
|
620
|
+
console.log(JSON.stringify({ elapsed_ms: elapsed, total: files.length, annotated: withWidget, drifted: withDrift, results }, null, 2));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
console.log(c('gray', ` Checked ${files.length} files in ${elapsed}ms`));
|
|
625
|
+
console.log('');
|
|
626
|
+
console.log(` Files with Mother Code: ${c('cyan', withWidget)}`);
|
|
627
|
+
console.log(` Files without: ${c('yellow', files.length - withWidget)}`);
|
|
628
|
+
console.log(` Files with drift: ${withDrift > 0 ? c('red', withDrift) : c('green', '0')}`);
|
|
629
|
+
console.log('');
|
|
630
|
+
|
|
631
|
+
if (results.length > 0) {
|
|
632
|
+
console.log(c('bold', ' DRIFT DETECTED'));
|
|
633
|
+
for (const r of results.slice(0, 20)) {
|
|
634
|
+
console.log(` ${c('yellow', '⚠')} ${c('cyan', r.file)}`);
|
|
635
|
+
for (const d of r.drifts) {
|
|
636
|
+
console.log(` ${c('gray', '→')} ${d.type}: ${d.message || d.detail || JSON.stringify(d)}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
console.log('');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const coverage = files.length > 0 ? Math.round((withWidget / files.length) * 100) : 0;
|
|
643
|
+
const covColor = coverage >= 80 ? 'green' : coverage >= 40 ? 'yellow' : 'red';
|
|
644
|
+
console.log(` Mother Code coverage: ${c(covColor, coverage + '%')}`);
|
|
645
|
+
console.log('');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async function cmdInject(opts) {
|
|
649
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
650
|
+
|
|
651
|
+
if (!opts.json) {
|
|
652
|
+
banner();
|
|
653
|
+
console.log(c('bold', ' INJECTING MOTHER CODE: ') + c('cyan', targetPath));
|
|
654
|
+
console.log('');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const files = collectFiles(targetPath, opts.ignore).filter(f => f.endsWith('.js') || f.endsWith('.ts'));
|
|
658
|
+
const graph = new DependencyGraph({ rootPath: targetPath });
|
|
659
|
+
await graph.build(files);
|
|
660
|
+
|
|
661
|
+
const generator = new WidgetGenerator({ rootPath: targetPath, dependencyGraph: graph });
|
|
662
|
+
|
|
663
|
+
let injected = 0;
|
|
664
|
+
let skipped = 0;
|
|
665
|
+
const previews = [];
|
|
666
|
+
|
|
667
|
+
for (const file of files) {
|
|
668
|
+
try {
|
|
669
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
670
|
+
// Check if already has a widget
|
|
671
|
+
if (content.includes('@purpose') && content.includes('@module')) {
|
|
672
|
+
skipped++;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const widget = generator.generateWidget(file, content);
|
|
677
|
+
if (!widget) { skipped++; continue; }
|
|
678
|
+
|
|
679
|
+
if (opts.fix) {
|
|
680
|
+
// Actually inject
|
|
681
|
+
const newContent = widget + '\n\n' + content;
|
|
682
|
+
fs.writeFileSync(file, newContent, 'utf-8');
|
|
683
|
+
injected++;
|
|
684
|
+
if (!opts.json) {
|
|
685
|
+
console.log(` ${c('green', '✓')} ${path.relative(targetPath, file)}`);
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
previews.push({ file: path.relative(targetPath, file), widget });
|
|
689
|
+
injected++;
|
|
690
|
+
}
|
|
691
|
+
} catch (e) { skipped++; }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (opts.json) {
|
|
695
|
+
console.log(JSON.stringify({ injected, skipped, dryRun: !opts.fix, previews }, null, 2));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
console.log('');
|
|
700
|
+
if (!opts.fix) {
|
|
701
|
+
console.log(c('yellow', ` DRY RUN — ${injected} files would be annotated. Use --fix to apply.`));
|
|
702
|
+
if (opts.verbose && previews.length > 0) {
|
|
703
|
+
console.log('');
|
|
704
|
+
for (const p of previews.slice(0, 5)) {
|
|
705
|
+
console.log(` ${c('cyan', p.file)}:`);
|
|
706
|
+
console.log(c('gray', p.widget.split('\n').map(l => ' ' + l).join('\n')));
|
|
707
|
+
console.log('');
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
console.log(c('green', ` ✓ Injected Mother Code into ${injected} files`));
|
|
712
|
+
}
|
|
713
|
+
console.log(` ${c('gray', `Skipped: ${skipped} (already annotated or non-JS/TS)`)}`);
|
|
714
|
+
console.log('');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function cmdReport(opts) {
|
|
718
|
+
const startTime = Date.now();
|
|
719
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
720
|
+
|
|
721
|
+
if (!opts.json) {
|
|
722
|
+
banner();
|
|
723
|
+
console.log(c('bold', ' FULL REPORT: ') + c('cyan', targetPath));
|
|
724
|
+
console.log('');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
728
|
+
|
|
729
|
+
// Run all analyses in parallel
|
|
730
|
+
const scanner = new CodeScanner({ rootPath: targetPath });
|
|
731
|
+
const graph = new DependencyGraph({ rootPath: targetPath });
|
|
732
|
+
const detector = new DriftDetector({ rootPath: targetPath });
|
|
733
|
+
|
|
734
|
+
const [scanResults] = await Promise.all([
|
|
735
|
+
scanner.scanFiles(files),
|
|
736
|
+
graph.build(files),
|
|
737
|
+
]);
|
|
738
|
+
|
|
739
|
+
// Drift check
|
|
740
|
+
let driftCount = 0;
|
|
741
|
+
let annotated = 0;
|
|
742
|
+
for (const file of files) {
|
|
743
|
+
try {
|
|
744
|
+
const result = await detector.analyzeFile(file);
|
|
745
|
+
if (result && result.hasWidget) {
|
|
746
|
+
annotated++;
|
|
747
|
+
if (result.drifts && result.drifts.length > 0) driftCount++;
|
|
748
|
+
}
|
|
749
|
+
} catch (e) { /* skip */ }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const elapsed = Date.now() - startTime;
|
|
753
|
+
|
|
754
|
+
// Count issues
|
|
755
|
+
let totalIssues = 0;
|
|
756
|
+
let criticalCount = 0;
|
|
757
|
+
let highCount = 0;
|
|
758
|
+
for (const [, r] of Object.entries(scanResults)) {
|
|
759
|
+
if (r.issues) {
|
|
760
|
+
totalIssues += r.issues.length;
|
|
761
|
+
criticalCount += r.issues.filter(i => i.severity === 'critical').length;
|
|
762
|
+
highCount += r.issues.filter(i => i.severity === 'high').length;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const orphans = graph.getOrphanFiles();
|
|
767
|
+
const circular = graph.circularDeps || [];
|
|
768
|
+
const coverage = files.length > 0 ? Math.round((annotated / files.length) * 100) : 0;
|
|
769
|
+
|
|
770
|
+
// Health score
|
|
771
|
+
const score = Math.max(0, Math.round(
|
|
772
|
+
100
|
|
773
|
+
- (criticalCount * 15)
|
|
774
|
+
- (highCount * 5)
|
|
775
|
+
- (circular.length * 10)
|
|
776
|
+
- (orphans.length * 1)
|
|
777
|
+
- ((100 - coverage) * 0.1)
|
|
778
|
+
- (driftCount * 3)
|
|
779
|
+
));
|
|
780
|
+
|
|
781
|
+
if (opts.json) {
|
|
782
|
+
console.log(JSON.stringify({
|
|
783
|
+
elapsed_ms: elapsed,
|
|
784
|
+
files: files.length,
|
|
785
|
+
health_score: score,
|
|
786
|
+
issues: { total: totalIssues, critical: criticalCount, high: highCount },
|
|
787
|
+
dependencies: { circular: circular.length, orphans: orphans.length },
|
|
788
|
+
mother_code: { annotated, coverage_pct: coverage, drifted: driftCount },
|
|
789
|
+
}, null, 2));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const scoreColor = score >= 80 ? 'green' : score >= 60 ? 'yellow' : 'red';
|
|
794
|
+
|
|
795
|
+
console.log(c('bold', ' ╔══════════════════════════════════════╗'));
|
|
796
|
+
console.log(c('bold', ' ║ THUBAN HEALTH REPORT ║'));
|
|
797
|
+
console.log(c('bold', ' ╚══════════════════════════════════════╝'));
|
|
798
|
+
console.log('');
|
|
799
|
+
console.log(` ${c('bold', 'Health Score:')} ${c(scoreColor, score + '/100')}`);
|
|
800
|
+
console.log(` ${c('bold', 'Files Scanned:')} ${files.length}`);
|
|
801
|
+
console.log(` ${c('bold', 'Time:')} ${elapsed}ms`);
|
|
802
|
+
console.log('');
|
|
803
|
+
console.log(c('bold', ' ── Code Quality ──────────────────────'));
|
|
804
|
+
console.log(` Total issues: ${totalIssues}`);
|
|
805
|
+
console.log(` Critical: ${criticalCount > 0 ? c('red', criticalCount) : c('green', '0')}`);
|
|
806
|
+
console.log(` High: ${highCount > 0 ? c('red', highCount) : c('green', '0')}`);
|
|
807
|
+
console.log('');
|
|
808
|
+
console.log(c('bold', ' ── Dependencies ─────────────────────'));
|
|
809
|
+
console.log(` Circular deps: ${circular.length > 0 ? c('red', circular.length) : c('green', '0')}`);
|
|
810
|
+
console.log(` Orphan files: ${orphans.length > 0 ? c('yellow', orphans.length) : c('green', '0')}`);
|
|
811
|
+
console.log('');
|
|
812
|
+
console.log(c('bold', ' ── Mother Code ──────────────────────'));
|
|
813
|
+
console.log(` Annotated files: ${annotated}`);
|
|
814
|
+
console.log(` Coverage: ${c(coverage >= 80 ? 'green' : coverage >= 40 ? 'yellow' : 'red', coverage + '%')}`);
|
|
815
|
+
console.log(` Drifted files: ${driftCount > 0 ? c('yellow', driftCount) : c('green', '0')}`);
|
|
816
|
+
console.log('');
|
|
817
|
+
|
|
818
|
+
// Grade
|
|
819
|
+
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
820
|
+
const gradeColor = score >= 80 ? 'green' : score >= 60 ? 'yellow' : 'red';
|
|
821
|
+
console.log(` ${c('bold', 'Grade:')} ${c(gradeColor, grade)}`);
|
|
822
|
+
console.log('');
|
|
823
|
+
|
|
824
|
+
if (score < 80) {
|
|
825
|
+
console.log(c('bold', ' RECOMMENDATIONS:'));
|
|
826
|
+
if (criticalCount > 0) console.log(` ${c('red', '→')} Fix ${criticalCount} critical issues immediately`);
|
|
827
|
+
if (circular.length > 0) console.log(` ${c('yellow', '→')} Resolve ${circular.length} circular dependencies`);
|
|
828
|
+
if (coverage < 50) console.log(` ${c('yellow', '→')} Run ${c('cyan', 'thuban inject --fix')} to improve Mother Code coverage`);
|
|
829
|
+
if (driftCount > 0) console.log(` ${c('yellow', '→')} ${driftCount} files have drifted from their annotations`);
|
|
830
|
+
if (orphans.length > 5) console.log(` ${c('gray', '→')} Consider removing ${orphans.length} orphan files`);
|
|
831
|
+
console.log('');
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ─── Tech Debt & Fix Commands ────────────────────────────
|
|
836
|
+
|
|
837
|
+
async function cmdDebt(opts) {
|
|
838
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
839
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
840
|
+
|
|
841
|
+
if (!opts.json) {
|
|
842
|
+
banner();
|
|
843
|
+
console.log(c('bold', ' TECH DEBT ANALYSIS: ') + c('cyan', targetPath));
|
|
844
|
+
console.log(c('gray', ` Scanning ${files.length} files...`));
|
|
845
|
+
console.log('');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const analyzer = new TechDebtAnalyzer({ rootPath: targetPath });
|
|
849
|
+
const debt = await analyzer.analyze(files);
|
|
850
|
+
|
|
851
|
+
if (opts.json) {
|
|
852
|
+
console.log(JSON.stringify(analyzer.toJSON(debt), null, 2));
|
|
853
|
+
} else {
|
|
854
|
+
console.log(analyzer.formatReport(debt));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function cmdFix(opts) {
|
|
859
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
860
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
861
|
+
const { execSync } = require('child_process');
|
|
862
|
+
|
|
863
|
+
if (!opts.json) {
|
|
864
|
+
banner();
|
|
865
|
+
console.log(c('bold', ' AUTO-FIX TECH DEBT: ') + c('cyan', targetPath));
|
|
866
|
+
console.log('');
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const analyzer = new TechDebtAnalyzer({ rootPath: targetPath });
|
|
870
|
+
|
|
871
|
+
// ─── DRY RUN is the default (Senate recommendation #1) ────
|
|
872
|
+
if (!opts.fix) {
|
|
873
|
+
const result = await analyzer.fix(files, { dryRun: true });
|
|
874
|
+
|
|
875
|
+
if (!opts.json) {
|
|
876
|
+
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
877
|
+
console.log(c('cyan', ' ║') + c('bold', ' DRY RUN — no files will be changed ') + c('cyan', '║'));
|
|
878
|
+
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
879
|
+
console.log('');
|
|
880
|
+
console.log(` Fixable items: ${c('bold', result.totalFixable)}`);
|
|
881
|
+
console.log('');
|
|
882
|
+
|
|
883
|
+
const grouped = {};
|
|
884
|
+
const safeActions = ['inject_widget', 'regenerate_widget', 'remove_console_log', 'flag_for_removal'];
|
|
885
|
+
const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api'];
|
|
886
|
+
let safeCount = 0;
|
|
887
|
+
let unsafeCount = 0;
|
|
888
|
+
|
|
889
|
+
for (const f of result.fixes) {
|
|
890
|
+
const action = f.fixAction || 'unknown';
|
|
891
|
+
if (!grouped[action]) grouped[action] = 0;
|
|
892
|
+
grouped[action]++;
|
|
893
|
+
if (unsafeActions.includes(action)) unsafeCount++;
|
|
894
|
+
else safeCount++;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
console.log(c('green', c('bold', ` SAFE FIXES (${safeCount})`)) + c('gray', ' — no runtime behaviour change'));
|
|
898
|
+
for (const [action, count] of Object.entries(grouped).sort((a, b) => b[1] - a[1])) {
|
|
899
|
+
if (unsafeActions.includes(action)) continue;
|
|
900
|
+
const label = {
|
|
901
|
+
'inject_widget': 'Inject Mother Code DNA annotations',
|
|
902
|
+
'regenerate_widget': 'Update stale annotations',
|
|
903
|
+
'remove_console_log': 'Remove debug console.logs',
|
|
904
|
+
'flag_for_removal': 'Confirm orphan files for removal',
|
|
905
|
+
}[action] || action;
|
|
906
|
+
console.log(` ${c('green', '→')} ${label}: ${c('bold', count)}`);
|
|
907
|
+
}
|
|
908
|
+
console.log('');
|
|
909
|
+
|
|
910
|
+
if (unsafeCount > 0) {
|
|
911
|
+
console.log(c('yellow', c('bold', ` UNSAFE FIXES (${unsafeCount})`)) + c('gray', ' — may change runtime behaviour'));
|
|
912
|
+
for (const [action, count] of Object.entries(grouped).sort((a, b) => b[1] - a[1])) {
|
|
913
|
+
if (!unsafeActions.includes(action)) continue;
|
|
914
|
+
const label = {
|
|
915
|
+
'break_circular': 'Restructure circular dependencies',
|
|
916
|
+
'extract_to_env': 'Extract hardcoded secrets to env vars',
|
|
917
|
+
'update_deprecated_api': 'Replace deprecated API calls',
|
|
918
|
+
}[action] || action;
|
|
919
|
+
console.log(` ${c('yellow', '!')} ${label}: ${c('bold', count)}`);
|
|
920
|
+
}
|
|
921
|
+
console.log('');
|
|
922
|
+
console.log(c('gray', ' Unsafe fixes require --unsafe flag:'));
|
|
923
|
+
console.log(` ${c('cyan', 'thuban fix [path] --fix --unsafe')}`);
|
|
924
|
+
console.log('');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
console.log(c('bold', ' TO APPLY:'));
|
|
928
|
+
console.log(` ${c('cyan', 'thuban fix [path] --fix')} Apply safe fixes only`);
|
|
929
|
+
console.log(` ${c('cyan', 'thuban fix [path] --fix --unsafe')} Apply all fixes`);
|
|
930
|
+
console.log(` ${c('cyan', 'thuban fix [path] --fix --commit')} Each fix = separate git commit`);
|
|
931
|
+
console.log('');
|
|
932
|
+
} else {
|
|
933
|
+
console.log(JSON.stringify(result, null, 2));
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// ─── ACTUAL FIX MODE ──────────────────────────────────────
|
|
939
|
+
if (!opts.json) {
|
|
940
|
+
if (opts.safe) {
|
|
941
|
+
console.log(c('green', ' SAFE MODE') + c('gray', ' — only non-runtime-changing fixes will be applied'));
|
|
942
|
+
} else {
|
|
943
|
+
console.log(c('yellow', ' UNSAFE MODE') + c('gray', ' — all fixes including runtime changes'));
|
|
944
|
+
}
|
|
945
|
+
console.log('');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const result = await analyzer.fix(files, {
|
|
949
|
+
dryRun: false,
|
|
950
|
+
safeOnly: opts.safe,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// ─── GIT COMMIT MODE (Senate recommendation: each fix = revertable commit) ─
|
|
954
|
+
if (opts.gitCommit && result.fixed > 0) {
|
|
955
|
+
try {
|
|
956
|
+
execSync('git rev-parse --git-dir', { cwd: targetPath, stdio: 'pipe' });
|
|
957
|
+
execSync('git add -A', { cwd: targetPath, stdio: 'pipe' });
|
|
958
|
+
const commitMsg = `fix(thuban): auto-fix ${result.fixed} issues [safe=${opts.safe}]`;
|
|
959
|
+
execSync(`git commit -m "${commitMsg}"`, { cwd: targetPath, stdio: 'pipe' });
|
|
960
|
+
if (!opts.json) {
|
|
961
|
+
console.log(c('green', ` ✓ Changes committed: "${commitMsg}"`));
|
|
962
|
+
console.log(c('gray', ' Revert with: git revert HEAD'));
|
|
963
|
+
console.log('');
|
|
964
|
+
}
|
|
965
|
+
} catch (e) {
|
|
966
|
+
if (!opts.json) {
|
|
967
|
+
console.log(c('yellow', ' ⚠ Could not auto-commit: ' + (e.message || 'not a git repo')));
|
|
968
|
+
console.log('');
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (opts.json) {
|
|
974
|
+
console.log(JSON.stringify(result, null, 2));
|
|
975
|
+
} else {
|
|
976
|
+
console.log(c('green', ` ✓ Fixed ${result.fixed} items`));
|
|
977
|
+
console.log(c('yellow', ` ⚠ Flagged ${result.flagged} items for manual review`));
|
|
978
|
+
if (result.failed > 0) console.log(c('red', ` ✗ Failed: ${result.failed}`));
|
|
979
|
+
if (result.skippedUnsafe > 0) {
|
|
980
|
+
console.log(c('gray', ` ○ Skipped ${result.skippedUnsafe} unsafe fixes (use --unsafe to apply)`));
|
|
981
|
+
}
|
|
982
|
+
console.log('');
|
|
983
|
+
console.log(c('gray', ` Run 'thuban report' to see updated scores`));
|
|
984
|
+
if (!opts.gitCommit) {
|
|
985
|
+
console.log(c('gray', ` Tip: use --commit to auto-commit fixes for easy rollback`));
|
|
986
|
+
}
|
|
987
|
+
console.log('');
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ─── Main ───────────────────────────────────────────────────
|
|
992
|
+
|
|
993
|
+
async function cmdHallucinate(opts) {
|
|
994
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
995
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
996
|
+
|
|
997
|
+
if (!opts.json) {
|
|
998
|
+
banner();
|
|
999
|
+
console.log(c('bold', ' HALLUCINATION SCAN: ') + c('cyan', targetPath));
|
|
1000
|
+
console.log(c('gray', ` Scanning ${files.length} files for AI hallucinations...`));
|
|
1001
|
+
console.log('');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const detector = new HallucinationDetector({ rootPath: targetPath });
|
|
1005
|
+
const results = await detector.scan(files);
|
|
1006
|
+
|
|
1007
|
+
if (opts.json) {
|
|
1008
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1009
|
+
} else {
|
|
1010
|
+
console.log(detector.formatReport(results));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async function cmdWatch(opts) {
|
|
1015
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1016
|
+
|
|
1017
|
+
const scanner = new CodeScanner({
|
|
1018
|
+
rootPath: targetPath,
|
|
1019
|
+
ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
1023
|
+
|
|
1024
|
+
// Create a quick scan function for the watcher
|
|
1025
|
+
const quickScan = async (filePath) => {
|
|
1026
|
+
const issues = [];
|
|
1027
|
+
|
|
1028
|
+
// Code scan
|
|
1029
|
+
const scanIssues = await scanner.scanFile(filePath);
|
|
1030
|
+
if (scanIssues) issues.push(...scanIssues);
|
|
1031
|
+
|
|
1032
|
+
// Hallucination check
|
|
1033
|
+
const hallResults = await hallucinationDetector.scan([filePath]);
|
|
1034
|
+
if (hallResults.phantomAPIs.length > 0) {
|
|
1035
|
+
for (const api of hallResults.phantomAPIs) {
|
|
1036
|
+
issues.push({ severity: 'high', message: `Phantom API: ${api.name} — ${api.suggestion}`, line: api.line });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (hallResults.phantomImports.length > 0) {
|
|
1040
|
+
for (const imp of hallResults.phantomImports) {
|
|
1041
|
+
issues.push({ severity: 'critical', message: `Phantom import: ${imp.module} — ${imp.reason}`, line: imp.line });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (hallResults.aiSmells.length > 0) {
|
|
1045
|
+
for (const smell of hallResults.aiSmells) {
|
|
1046
|
+
issues.push({ severity: 'warning', message: `AI smell: ${smell.message}`, line: smell.line });
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return issues;
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
const watcher = new FileWatcher({
|
|
1054
|
+
rootPath: targetPath,
|
|
1055
|
+
ignorePatterns: ['node_modules', '.git', 'dist', '.next', '__pycache__', ...opts.ignore],
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// Handle graceful shutdown
|
|
1059
|
+
process.on('SIGINT', () => {
|
|
1060
|
+
watcher.stop();
|
|
1061
|
+
process.exit(0);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
watcher.start(quickScan);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function cmdDashboard(opts) {
|
|
1068
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1069
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1070
|
+
|
|
1071
|
+
if (!opts.json) {
|
|
1072
|
+
banner();
|
|
1073
|
+
console.log(c('bold', ' GENERATING DASHBOARD: ') + c('cyan', targetPath));
|
|
1074
|
+
console.log(c('gray', ` Scanning ${files.length} files...`));
|
|
1075
|
+
console.log('');
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Run full analysis
|
|
1079
|
+
const analyzer = new TechDebtAnalyzer({ rootPath: targetPath });
|
|
1080
|
+
const debt = await analyzer.analyze(files);
|
|
1081
|
+
|
|
1082
|
+
// Get dependency data
|
|
1083
|
+
const graph = new DependencyGraph({ rootPath: targetPath });
|
|
1084
|
+
await graph.build(files);
|
|
1085
|
+
const depData = {
|
|
1086
|
+
circular: graph.circularDeps || [],
|
|
1087
|
+
orphans: graph.getOrphanFiles() || [],
|
|
1088
|
+
critical: graph.getMostCriticalFiles(5) || [],
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
// Run hallucination scan
|
|
1092
|
+
const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
1093
|
+
const hallResults = await hallucinationDetector.scan(files);
|
|
1094
|
+
|
|
1095
|
+
// Generate HTML report
|
|
1096
|
+
const reportGen = new HtmlReportGenerator({
|
|
1097
|
+
rootPath: targetPath,
|
|
1098
|
+
outputDir: targetPath,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
const projectName = path.basename(targetPath);
|
|
1102
|
+
const outputPath = await reportGen.writeReport(debt, depData, {
|
|
1103
|
+
projectName,
|
|
1104
|
+
hallucinations: hallResults,
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
if (!opts.json) {
|
|
1108
|
+
console.log(c('green', ` ✓ Dashboard generated:`));
|
|
1109
|
+
console.log(` ${c('cyan', outputPath)}`);
|
|
1110
|
+
console.log('');
|
|
1111
|
+
console.log(c('gray', ` Open in your browser to view the interactive report`));
|
|
1112
|
+
console.log('');
|
|
1113
|
+
} else {
|
|
1114
|
+
console.log(JSON.stringify({ outputPath, scores: debt.scores, stats: debt.stats }, null, 2));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async function cmdDiff(opts) {
|
|
1119
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1120
|
+
const { execSync } = require('child_process');
|
|
1121
|
+
|
|
1122
|
+
if (!opts.json) {
|
|
1123
|
+
banner();
|
|
1124
|
+
console.log(c('bold', ' DIFF SCAN: ') + c('cyan', targetPath));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Get changed files from git
|
|
1128
|
+
let changedFiles = [];
|
|
1129
|
+
try {
|
|
1130
|
+
// Files changed vs main/master branch
|
|
1131
|
+
let baseBranch = 'main';
|
|
1132
|
+
try {
|
|
1133
|
+
execSync('git rev-parse --verify main', { cwd: targetPath, stdio: 'pipe' });
|
|
1134
|
+
} catch {
|
|
1135
|
+
try {
|
|
1136
|
+
execSync('git rev-parse --verify master', { cwd: targetPath, stdio: 'pipe' });
|
|
1137
|
+
baseBranch = 'master';
|
|
1138
|
+
} catch {
|
|
1139
|
+
// Fall back to HEAD~1
|
|
1140
|
+
baseBranch = 'HEAD~1';
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const diffOutput = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
|
|
1145
|
+
cwd: targetPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
1146
|
+
}).trim();
|
|
1147
|
+
|
|
1148
|
+
// Also get uncommitted changes
|
|
1149
|
+
const statusOutput = execSync('git diff --name-only HEAD', {
|
|
1150
|
+
cwd: targetPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
1151
|
+
}).trim();
|
|
1152
|
+
|
|
1153
|
+
const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
|
|
1154
|
+
cwd: targetPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
1155
|
+
}).trim();
|
|
1156
|
+
|
|
1157
|
+
const allChanged = new Set([
|
|
1158
|
+
...diffOutput.split('\n').filter(Boolean),
|
|
1159
|
+
...statusOutput.split('\n').filter(Boolean),
|
|
1160
|
+
...untrackedOutput.split('\n').filter(Boolean),
|
|
1161
|
+
]);
|
|
1162
|
+
|
|
1163
|
+
// Resolve to full paths and filter to JS/TS files
|
|
1164
|
+
for (const relFile of allChanged) {
|
|
1165
|
+
const fullPath = path.resolve(targetPath, relFile);
|
|
1166
|
+
const ext = path.extname(relFile).toLowerCase();
|
|
1167
|
+
if (['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.json'].includes(ext)) {
|
|
1168
|
+
if (fs.existsSync(fullPath)) {
|
|
1169
|
+
changedFiles.push(fullPath);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
if (!opts.json) {
|
|
1175
|
+
console.log(c('red', ` ✗ Not a git repository or git error: ${e.message}`));
|
|
1176
|
+
}
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (changedFiles.length === 0) {
|
|
1181
|
+
if (!opts.json) {
|
|
1182
|
+
console.log(c('green', ' ✓ No changed files to scan'));
|
|
1183
|
+
} else {
|
|
1184
|
+
console.log(JSON.stringify({ changedFiles: 0, issues: 0 }));
|
|
1185
|
+
}
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (!opts.json) {
|
|
1190
|
+
console.log(c('gray', ` Scanning ${changedFiles.length} changed file${changedFiles.length === 1 ? '' : 's'}...\n`));
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Run scans on changed files only
|
|
1194
|
+
const scanner = new CodeScanner({ rootPath: targetPath });
|
|
1195
|
+
const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
1196
|
+
|
|
1197
|
+
const scanResults = await scanner.scanFiles(changedFiles);
|
|
1198
|
+
const hallResults = await hallucinationDetector.scan(changedFiles);
|
|
1199
|
+
|
|
1200
|
+
// Collect all issues
|
|
1201
|
+
let totalIssues = 0;
|
|
1202
|
+
const fileIssues = {};
|
|
1203
|
+
|
|
1204
|
+
for (const [file, result] of Object.entries(scanResults)) {
|
|
1205
|
+
if (result.issues && result.issues.length > 0) {
|
|
1206
|
+
const relFile = path.relative(targetPath, file);
|
|
1207
|
+
fileIssues[relFile] = result.issues;
|
|
1208
|
+
totalIssues += result.issues.length;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
totalIssues += hallResults.stats.totalIssues;
|
|
1213
|
+
|
|
1214
|
+
if (opts.json) {
|
|
1215
|
+
console.log(JSON.stringify({
|
|
1216
|
+
changedFiles: changedFiles.length,
|
|
1217
|
+
totalIssues,
|
|
1218
|
+
codeIssues: fileIssues,
|
|
1219
|
+
hallucinations: hallResults,
|
|
1220
|
+
}, null, 2));
|
|
1221
|
+
} else {
|
|
1222
|
+
// Display results per file
|
|
1223
|
+
for (const [relFile, issues] of Object.entries(fileIssues)) {
|
|
1224
|
+
console.log(` ${c('cyan', relFile)}`);
|
|
1225
|
+
for (const issue of issues.slice(0, 5)) {
|
|
1226
|
+
const sevColor = issue.severity === 'critical' || issue.severity === 'high' ? 'red' : issue.severity === 'warning' ? 'yellow' : 'gray';
|
|
1227
|
+
console.log(` ${c(sevColor, '✗')} ${c('gray', issue.message || issue.id || '')}`);
|
|
1228
|
+
}
|
|
1229
|
+
if (issues.length > 5) {
|
|
1230
|
+
console.log(` ${c('gray', `... and ${issues.length - 5} more`)}`);
|
|
1231
|
+
}
|
|
1232
|
+
console.log('');
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Hallucination results
|
|
1236
|
+
if (hallResults.stats.totalIssues > 0) {
|
|
1237
|
+
console.log(hallucinationDetector.formatReport(hallResults));
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Summary
|
|
1241
|
+
const color = totalIssues === 0 ? 'green' : 'yellow';
|
|
1242
|
+
console.log(` ${c(color, `${changedFiles.length} files changed, ${totalIssues} new issues introduced`)}\n`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// ─── License Commands ───────────────────────────────────────
|
|
1247
|
+
|
|
1248
|
+
async function cmdActivate(opts) {
|
|
1249
|
+
banner();
|
|
1250
|
+
const lm = new LicenseManager();
|
|
1251
|
+
|
|
1252
|
+
const key = opts.licenseKey;
|
|
1253
|
+
|
|
1254
|
+
if (!key) {
|
|
1255
|
+
console.log(c('red', ' Usage: thuban activate <license-key>'));
|
|
1256
|
+
console.log('');
|
|
1257
|
+
console.log(c('gray', ' Get a key at: ') + c('cyan', 'https://thuban.dev/pricing'));
|
|
1258
|
+
console.log('');
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const result = lm.activate(key);
|
|
1263
|
+
|
|
1264
|
+
if (result.success) {
|
|
1265
|
+
console.log(c('green', ' ╔══════════════════════════════════════╗'));
|
|
1266
|
+
console.log(c('green', ' ║') + c('bold', ' License activated! ') + c('green', '║'));
|
|
1267
|
+
console.log(c('green', ' ╚══════════════════════════════════════╝'));
|
|
1268
|
+
console.log('');
|
|
1269
|
+
console.log(` Tier: ${c('bold', result.tierName)}`);
|
|
1270
|
+
console.log(` Status: ${c('green', 'Active')}`);
|
|
1271
|
+
console.log('');
|
|
1272
|
+
console.log(c('gray', ' All features unlocked. Run ') + c('cyan', 'thuban scan .') + c('gray', ' to get started.'));
|
|
1273
|
+
console.log('');
|
|
1274
|
+
} else {
|
|
1275
|
+
console.log(c('red', ' Invalid license key.'));
|
|
1276
|
+
console.log('');
|
|
1277
|
+
console.log(c('gray', ' Check for typos or get a new key at: ') + c('cyan', 'https://thuban.dev/pricing'));
|
|
1278
|
+
console.log('');
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
async function cmdStatus(opts) {
|
|
1283
|
+
banner();
|
|
1284
|
+
const lm = new LicenseManager();
|
|
1285
|
+
const status = lm.getStatus();
|
|
1286
|
+
|
|
1287
|
+
console.log(c('bold', ' LICENSE STATUS'));
|
|
1288
|
+
console.log('');
|
|
1289
|
+
console.log(` Tier: ${c('bold', status.tierName)}${status.licensed ? c('green', ' (active)') : ''}`);
|
|
1290
|
+
console.log(` Machine ID: ${c('gray', status.machineId)}`);
|
|
1291
|
+
console.log(` Scans this month: ${c('bold', status.scansUsed)}${status.scansLimit < Infinity ? c('gray', ' / ' + status.scansLimit) : ''}`);
|
|
1292
|
+
console.log(` Lifetime scans: ${c('gray', status.totalScans)}`);
|
|
1293
|
+
console.log('');
|
|
1294
|
+
console.log(c('bold', ' FEATURES'));
|
|
1295
|
+
console.log(` Full scan details: ${status.features.fullDetails ? c('green', 'Yes') : c('red', 'No')}`);
|
|
1296
|
+
console.log(` HTML dashboard: ${status.features.dashboard ? c('green', 'Yes') : c('red', 'No')}`);
|
|
1297
|
+
console.log(` Auto-fix: ${status.features.autoFix ? c('green', 'Yes') : c('red', 'No')}`);
|
|
1298
|
+
console.log(` CI Action: ${status.features.ciAction ? c('green', 'Yes') : c('red', 'No')}`);
|
|
1299
|
+
console.log('');
|
|
1300
|
+
|
|
1301
|
+
if (!status.licensed) {
|
|
1302
|
+
console.log(c('cyan', ' Upgrade at: ') + c('bold', 'https://thuban.dev/pricing'));
|
|
1303
|
+
console.log('');
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function cmdUpgrade() {
|
|
1308
|
+
banner();
|
|
1309
|
+
console.log(c('bold', ' THUBAN PRICING'));
|
|
1310
|
+
console.log('');
|
|
1311
|
+
console.log(` ${c('gray', 'FREE')} ${c('bold', '£0/mo')}`);
|
|
1312
|
+
console.log(` 3 scans/month, 100 files, summary only`);
|
|
1313
|
+
console.log('');
|
|
1314
|
+
console.log(` ${c('cyan', 'PRO')} ${c('bold', '£19/mo')} ${c('gray', '(or £149/year — save 35%)')}`);
|
|
1315
|
+
console.log(` Unlimited scans, full details, auto-fix, HTML dashboard`);
|
|
1316
|
+
console.log('');
|
|
1317
|
+
console.log(` ${c('cyan', 'TEAM')} ${c('bold', '£99/mo')}`);
|
|
1318
|
+
console.log(` Everything in Pro + 5 seats, CI Action, team reports`);
|
|
1319
|
+
console.log('');
|
|
1320
|
+
console.log(` ${c('cyan', 'ENTERPRISE')} ${c('bold', '£299/mo')}`);
|
|
1321
|
+
console.log(` Everything in Team + unlimited seats, priority support, SLA`);
|
|
1322
|
+
console.log('');
|
|
1323
|
+
console.log(c('bold', ' Subscribe at: ') + c('cyan', 'https://thuban.dev/pricing'));
|
|
1324
|
+
console.log('');
|
|
1325
|
+
|
|
1326
|
+
// Try to open the URL
|
|
1327
|
+
try {
|
|
1328
|
+
const { exec } = require('child_process');
|
|
1329
|
+
const platform = process.platform;
|
|
1330
|
+
const cmd = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1331
|
+
exec(`${cmd} https://thuban.dev/pricing`);
|
|
1332
|
+
} catch (e) { /* non-fatal */ }
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// ─── New Feature Commands ───────────────────────────────────
|
|
1336
|
+
|
|
1337
|
+
async function cmdGate(opts) {
|
|
1338
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1339
|
+
|
|
1340
|
+
if (opts.fix) {
|
|
1341
|
+
// --fix = install the hook
|
|
1342
|
+
const result = PreCommitGate.install(targetPath);
|
|
1343
|
+
banner();
|
|
1344
|
+
if (result.success) {
|
|
1345
|
+
console.log(c('green', ' ✓ Pre-commit gate installed!'));
|
|
1346
|
+
console.log('');
|
|
1347
|
+
console.log(c('gray', ' Every commit will now be checked for hallucinated APIs.'));
|
|
1348
|
+
console.log(c('gray', ' Commits with phantom APIs will be blocked automatically.'));
|
|
1349
|
+
console.log('');
|
|
1350
|
+
console.log(` To remove: ${c('cyan', 'thuban gate --uninstall')}`);
|
|
1351
|
+
} else {
|
|
1352
|
+
console.log(c('red', ` Error: ${result.error}`));
|
|
1353
|
+
}
|
|
1354
|
+
console.log('');
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (process.argv.includes('--uninstall')) {
|
|
1359
|
+
const result = PreCommitGate.uninstall(targetPath);
|
|
1360
|
+
banner();
|
|
1361
|
+
console.log(c('green', ` ✓ ${result.message}`));
|
|
1362
|
+
console.log('');
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Run the gate check (used by the pre-commit hook and manual runs)
|
|
1367
|
+
const gate = new PreCommitGate({ rootPath: targetPath, strict: !process.argv.includes('--warn') });
|
|
1368
|
+
const result = gate.run();
|
|
1369
|
+
|
|
1370
|
+
if (opts.json) {
|
|
1371
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const isCi = process.argv.includes('--ci');
|
|
1376
|
+
|
|
1377
|
+
if (!isCi) banner();
|
|
1378
|
+
|
|
1379
|
+
if (result.files === 0) {
|
|
1380
|
+
console.log(c('gray', ' No staged files to check.'));
|
|
1381
|
+
console.log('');
|
|
1382
|
+
console.log(` Install the gate: ${c('cyan', 'thuban gate --fix')}`);
|
|
1383
|
+
console.log('');
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if (result.passed) {
|
|
1388
|
+
console.log(c('green', ` ✓ ${result.message}`));
|
|
1389
|
+
console.log(c('gray', ` ${result.elapsed}ms`));
|
|
1390
|
+
if (!isCi) console.log('');
|
|
1391
|
+
process.exit(0);
|
|
1392
|
+
} else {
|
|
1393
|
+
console.log(c('red', ` ✗ ${result.message}`));
|
|
1394
|
+
console.log('');
|
|
1395
|
+
for (const issue of result.issues) {
|
|
1396
|
+
console.log(` ${c('red', issue.file)}:${issue.line}`);
|
|
1397
|
+
console.log(` ${c('yellow', issue.code)}`);
|
|
1398
|
+
console.log(` ${c('red', '^^^')} ${issue.message}`);
|
|
1399
|
+
if (issue.fix) console.log(` ${c('green', 'Fix:')} ${issue.fix}`);
|
|
1400
|
+
console.log('');
|
|
1401
|
+
}
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async function cmdCost(opts) {
|
|
1407
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1408
|
+
banner();
|
|
1409
|
+
console.log(c('bold', ' SCANNING: ') + c('cyan', targetPath));
|
|
1410
|
+
console.log('');
|
|
1411
|
+
|
|
1412
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1413
|
+
const scanner = new CodeScanner({
|
|
1414
|
+
rootPath: targetPath,
|
|
1415
|
+
ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
|
|
1416
|
+
});
|
|
1417
|
+
const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
1418
|
+
|
|
1419
|
+
const [scanResults, hallResults] = await Promise.all([
|
|
1420
|
+
scanner.scanFiles(files),
|
|
1421
|
+
hallucinationDetector.scan(files),
|
|
1422
|
+
]);
|
|
1423
|
+
|
|
1424
|
+
// Collect all issues
|
|
1425
|
+
const allIssues = [];
|
|
1426
|
+
for (const [file, fileResults] of Object.entries(scanResults)) {
|
|
1427
|
+
if (fileResults.issues) {
|
|
1428
|
+
for (const issue of fileResults.issues) {
|
|
1429
|
+
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
for (const item of hallResults.phantomAPIs) {
|
|
1434
|
+
allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
|
|
1435
|
+
}
|
|
1436
|
+
for (const item of hallResults.deprecatedAPIs) {
|
|
1437
|
+
allIssues.push({ category: 'deprecated', severity: 'high', message: item.name });
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const calculator = new TechDebtCostCalculator();
|
|
1441
|
+
const result = calculator.calculate(allIssues, files.length);
|
|
1442
|
+
|
|
1443
|
+
if (opts.json) {
|
|
1444
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
console.log(calculator.formatCLI(result));
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function cmdGhosts(opts) {
|
|
1452
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1453
|
+
banner();
|
|
1454
|
+
console.log(c('bold', ' SCANNING FOR GHOST CODE: ') + c('cyan', targetPath));
|
|
1455
|
+
console.log('');
|
|
1456
|
+
|
|
1457
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1458
|
+
const detector = new GhostCodeDetector({ rootPath: targetPath });
|
|
1459
|
+
const result = detector.scan(files);
|
|
1460
|
+
|
|
1461
|
+
if (opts.json) {
|
|
1462
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (result.ghosts.length === 0) {
|
|
1467
|
+
console.log(c('green', ' No ghost code detected. Every function is referenced.'));
|
|
1468
|
+
console.log('');
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
console.log(c('yellow', c('bold', ` GHOST CODE: ${result.totalGhosts} function${result.totalGhosts > 1 ? 's' : ''} exist but are never called`)));
|
|
1473
|
+
console.log('');
|
|
1474
|
+
|
|
1475
|
+
for (const ghost of result.ghosts.slice(0, opts.maxIssues)) {
|
|
1476
|
+
const sevColor = ghost.severity === 'high' ? 'red' : 'yellow';
|
|
1477
|
+
console.log(` ${c(sevColor, '⊘')} ${c('cyan', ghost.file)}:${ghost.line}`);
|
|
1478
|
+
console.log(` ${c('bold', ghost.name + '()')} — ${ghost.wastedLines} lines, ${ghost.references} references`);
|
|
1479
|
+
console.log('');
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
console.log(c('bold', ' ┌──────────────────────────────────────────────────────┐'));
|
|
1483
|
+
console.log(c('bold', ' │') + ` Wasted code: ${c('red', result.totalWastedLines + ' lines')} (${result.wastedPercent}% of codebase) ` + c('bold', '│'));
|
|
1484
|
+
console.log(c('bold', ' └──────────────────────────────────────────────────────┘'));
|
|
1485
|
+
console.log('');
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async function cmdAIScore(opts) {
|
|
1489
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1490
|
+
banner();
|
|
1491
|
+
console.log(c('bold', ' AI CONFIDENCE SCORING: ') + c('cyan', targetPath));
|
|
1492
|
+
console.log('');
|
|
1493
|
+
|
|
1494
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1495
|
+
const scorer = new AIConfidenceScorer({ rootPath: targetPath });
|
|
1496
|
+
const result = scorer.scan(files);
|
|
1497
|
+
|
|
1498
|
+
if (opts.json) {
|
|
1499
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
if (result.highConfidence.length === 0 && result.mediumConfidence.length === 0) {
|
|
1504
|
+
console.log(c('green', ' No AI-generated code patterns detected.'));
|
|
1505
|
+
console.log('');
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
console.log(c('red', c('bold', ` HIGH CONFIDENCE AI-GENERATED (${result.highConfidence.length} functions)`)));
|
|
1510
|
+
console.log('');
|
|
1511
|
+
|
|
1512
|
+
for (const fn of result.highConfidence.slice(0, 15)) {
|
|
1513
|
+
console.log(` ${c('red', fn.confidence + '%')} AI ${c('cyan', fn.file)}:${fn.line}`);
|
|
1514
|
+
console.log(` ${c('bold', fn.name + '()')} — ${fn.lineCount} lines | Test coverage: ${fn.testCoverage}`);
|
|
1515
|
+
for (const signal of fn.signals.slice(0, 2)) {
|
|
1516
|
+
console.log(` ${c('gray', '• ' + signal)}`);
|
|
1517
|
+
}
|
|
1518
|
+
console.log('');
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (result.mediumConfidence.length > 0) {
|
|
1522
|
+
console.log(c('yellow', c('bold', ` POSSIBLE AI-GENERATED (${result.mediumConfidence.length} functions)`)));
|
|
1523
|
+
console.log('');
|
|
1524
|
+
for (const fn of result.mediumConfidence.slice(0, 10)) {
|
|
1525
|
+
console.log(` ${c('yellow', fn.confidence + '%')} AI ${c('cyan', fn.file)}:${fn.line} ${c('gray', fn.name + '()')}`);
|
|
1526
|
+
}
|
|
1527
|
+
console.log('');
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
console.log(c('bold', ' ┌──────────────────────────────────────────────────────┐'));
|
|
1531
|
+
console.log(c('bold', ' │') + ` ${c('red', result.aiPercentage + '%')} of functions are likely AI-generated ` + c('bold', '│'));
|
|
1532
|
+
console.log(c('bold', ' │') + ` ${c('yellow', result.aiLikelyCount)} high confidence | ${result.aiPossibleCount} possible ` + c('bold', '│'));
|
|
1533
|
+
console.log(c('bold', ' └──────────────────────────────────────────────────────┘'));
|
|
1534
|
+
console.log('');
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
async function cmdClones(opts) {
|
|
1538
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1539
|
+
banner();
|
|
1540
|
+
console.log(c('bold', ' COPY-PASTE DRIFT DETECTION: ') + c('cyan', targetPath));
|
|
1541
|
+
console.log('');
|
|
1542
|
+
|
|
1543
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1544
|
+
const detector = new CopyPasteDriftDetector({ rootPath: targetPath });
|
|
1545
|
+
const result = detector.scan(files);
|
|
1546
|
+
|
|
1547
|
+
if (opts.json) {
|
|
1548
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (result.clusters.length === 0) {
|
|
1553
|
+
console.log(c('green', ' No copy-paste drift detected.'));
|
|
1554
|
+
console.log('');
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
console.log(c('yellow', c('bold', ` COPY-PASTE CLUSTERS: ${result.totalClusters} found`)));
|
|
1559
|
+
console.log('');
|
|
1560
|
+
|
|
1561
|
+
for (const cluster of result.clusters.slice(0, 10)) {
|
|
1562
|
+
const sevColor = cluster.severity === 'high' ? 'red' : 'yellow';
|
|
1563
|
+
console.log(` ${c(sevColor, '◈')} ${c('bold', cluster.pattern + ' pattern')} — ${cluster.totalInstances} instances`);
|
|
1564
|
+
for (const member of cluster.members) {
|
|
1565
|
+
const sim = member.similarity < 100 ? c('gray', ` (${member.similarity}% similar)`) : '';
|
|
1566
|
+
console.log(` ${c('cyan', member.file)}:${member.line} ${c('gray', member.name + '()')}${sim}`);
|
|
1567
|
+
}
|
|
1568
|
+
console.log(` ${c('green', '→')} ${cluster.recommendation}`);
|
|
1569
|
+
console.log('');
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
console.log(c('bold', ` Total duplicate lines: ${c('red', result.totalDuplicateLines)}`));
|
|
1573
|
+
console.log(c('gray', ` ${result.recommendation}`));
|
|
1574
|
+
console.log('');
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
async function cmdPassport(opts) {
|
|
1578
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1579
|
+
banner();
|
|
1580
|
+
console.log(c('bold', ' GENERATING CODEBASE PASSPORT: ') + c('cyan', targetPath));
|
|
1581
|
+
console.log('');
|
|
1582
|
+
|
|
1583
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1584
|
+
const generator = new CodebasePassport({ rootPath: targetPath });
|
|
1585
|
+
const passport = generator.generate(files);
|
|
1586
|
+
|
|
1587
|
+
if (opts.json) {
|
|
1588
|
+
console.log(JSON.stringify(passport, null, 2));
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const outputPath = generator.save(passport);
|
|
1593
|
+
const p = passport.project;
|
|
1594
|
+
const o = passport.onboarding;
|
|
1595
|
+
|
|
1596
|
+
console.log(c('green', ` ✓ Passport generated: ${c('bold', path.relative(targetPath, outputPath))}`));
|
|
1597
|
+
console.log('');
|
|
1598
|
+
console.log(c('bold', ' PROJECT IDENTITY'));
|
|
1599
|
+
console.log(` Name: ${c('cyan', p.name)}`);
|
|
1600
|
+
console.log(` Files: ${c('bold', p.totalFiles)}`);
|
|
1601
|
+
console.log(` Lines: ${c('bold', p.totalLines.toLocaleString())}`);
|
|
1602
|
+
console.log(` Architecture: ${c('bold', p.architecture)}`);
|
|
1603
|
+
console.log(` Frameworks: ${p.frameworks.join(', ') || 'vanilla'}`);
|
|
1604
|
+
console.log('');
|
|
1605
|
+
console.log(c('bold', ' LANGUAGES'));
|
|
1606
|
+
for (const lang of p.languages.slice(0, 5)) {
|
|
1607
|
+
const bar = '█'.repeat(Math.max(1, Math.round(lang.percentage / 5)));
|
|
1608
|
+
console.log(` ${lang.extension.padEnd(6)} ${c('cyan', bar)} ${lang.percentage}% (${lang.lines} lines)`);
|
|
1609
|
+
}
|
|
1610
|
+
console.log('');
|
|
1611
|
+
console.log(c('bold', ' ONBOARDING'));
|
|
1612
|
+
console.log(` Read time: ~${o.estimatedReadTimeMinutes} minutes`);
|
|
1613
|
+
console.log(` Start here: ${o.startHere.slice(0, 3).join(', ')}`);
|
|
1614
|
+
if (o.doNotTouch.length > 0) {
|
|
1615
|
+
console.log(` Do not touch: ${c('red', o.doNotTouch.join(', '))}`);
|
|
1616
|
+
}
|
|
1617
|
+
console.log(` Summary: ${o.quickSummary}`);
|
|
1618
|
+
console.log('');
|
|
1619
|
+
|
|
1620
|
+
if (passport.sacred.length > 0) {
|
|
1621
|
+
console.log(c('bold', ' SACRED FILES (high impact if changed)'));
|
|
1622
|
+
for (const s of passport.sacred.slice(0, 5)) {
|
|
1623
|
+
console.log(` ${c('red', '!')} ${c('cyan', s.file)} — imported by ${s.importedBy} files`);
|
|
1624
|
+
}
|
|
1625
|
+
console.log('');
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
console.log(c('gray', ` Full passport: ${outputPath}`));
|
|
1629
|
+
console.log(c('gray', ` Share with new team members for instant onboarding.`));
|
|
1630
|
+
console.log('');
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async function cmdExecutive(opts) {
|
|
1634
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1635
|
+
banner();
|
|
1636
|
+
console.log(c('bold', ' GENERATING EXECUTIVE REPORT: ') + c('cyan', targetPath));
|
|
1637
|
+
console.log('');
|
|
1638
|
+
|
|
1639
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1640
|
+
|
|
1641
|
+
console.log(c('gray', ` Scanning ${files.length} files...`));
|
|
1642
|
+
|
|
1643
|
+
const scanner = new CodeScanner({
|
|
1644
|
+
rootPath: targetPath,
|
|
1645
|
+
ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
|
|
1646
|
+
});
|
|
1647
|
+
const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
1648
|
+
const ghostDetector = new GhostCodeDetector({ rootPath: targetPath });
|
|
1649
|
+
const aiScorer = new AIConfidenceScorer({ rootPath: targetPath });
|
|
1650
|
+
const cloneDetector = new CopyPasteDriftDetector({ rootPath: targetPath });
|
|
1651
|
+
|
|
1652
|
+
// Run all scans in parallel
|
|
1653
|
+
const [scanResults, hallResults, ghostResults, aiScoreResults, cloneResults] = await Promise.all([
|
|
1654
|
+
scanner.scanFiles(files),
|
|
1655
|
+
hallucinationDetector.scan(files),
|
|
1656
|
+
ghostDetector.scan(files),
|
|
1657
|
+
aiScorer.scan(files),
|
|
1658
|
+
cloneDetector.scan(files),
|
|
1659
|
+
]);
|
|
1660
|
+
|
|
1661
|
+
// Calculate total lines
|
|
1662
|
+
let lineCount = 0;
|
|
1663
|
+
for (const file of files) {
|
|
1664
|
+
try {
|
|
1665
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
1666
|
+
lineCount += content.split('\n').length;
|
|
1667
|
+
} catch { /* skip */ }
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Collect all issues for cost calculator
|
|
1671
|
+
const allIssues = [];
|
|
1672
|
+
for (const [file, fileResults] of Object.entries(scanResults)) {
|
|
1673
|
+
if (fileResults.issues) {
|
|
1674
|
+
for (const issue of fileResults.issues) {
|
|
1675
|
+
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
for (const item of hallResults.phantomAPIs) {
|
|
1680
|
+
allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
|
|
1681
|
+
}
|
|
1682
|
+
for (const item of hallResults.deprecatedAPIs) {
|
|
1683
|
+
allIssues.push({ category: 'deprecated', severity: 'high', message: item.name });
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const costCalc = new TechDebtCostCalculator();
|
|
1687
|
+
const debtCost = costCalc.calculate(allIssues, files.length);
|
|
1688
|
+
|
|
1689
|
+
// Generate report
|
|
1690
|
+
const report = new ExecutiveReport({
|
|
1691
|
+
rootPath: targetPath,
|
|
1692
|
+
companyName: opts.companyName || undefined,
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
const html = report.generate({
|
|
1696
|
+
scanResults,
|
|
1697
|
+
hallResults,
|
|
1698
|
+
debtCost,
|
|
1699
|
+
ghostResults,
|
|
1700
|
+
aiScoreResults,
|
|
1701
|
+
cloneResults,
|
|
1702
|
+
fileCount: files.length,
|
|
1703
|
+
lineCount,
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
const outputPath = report.save(html, targetPath);
|
|
1707
|
+
|
|
1708
|
+
console.log(c('green', ` ✓ Executive report generated!`));
|
|
1709
|
+
console.log('');
|
|
1710
|
+
console.log(` ${c('bold', 'Report:')} ${c('cyan', outputPath)}`);
|
|
1711
|
+
console.log('');
|
|
1712
|
+
console.log(c('gray', ' Open in browser → Ctrl+P → Save as PDF'));
|
|
1713
|
+
console.log(c('gray', ' Or click the "Save as PDF" button in the report.'));
|
|
1714
|
+
console.log('');
|
|
1715
|
+
|
|
1716
|
+
// Try to open in browser
|
|
1717
|
+
try {
|
|
1718
|
+
const { exec } = require('child_process');
|
|
1719
|
+
const platform = process.platform;
|
|
1720
|
+
const cmd = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1721
|
+
exec(`${cmd} "${outputPath}"`);
|
|
1722
|
+
console.log(c('green', ' ✓ Opened in your default browser.'));
|
|
1723
|
+
console.log('');
|
|
1724
|
+
} catch (e) { /* non-fatal */ }
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
async function cmdBaseline(opts) {
|
|
1728
|
+
const targetPath = path.resolve(opts.targetPath);
|
|
1729
|
+
const baseline = new BaselineManager(targetPath);
|
|
1730
|
+
|
|
1731
|
+
banner();
|
|
1732
|
+
|
|
1733
|
+
if (opts.baselineCreate || !baseline.exists()) {
|
|
1734
|
+
// Create baseline from current scan
|
|
1735
|
+
console.log(c('bold', ' CREATING BASELINE: ') + c('cyan', targetPath));
|
|
1736
|
+
console.log('');
|
|
1737
|
+
|
|
1738
|
+
const files = collectFiles(targetPath, opts.ignore);
|
|
1739
|
+
const scanner = new CodeScanner({
|
|
1740
|
+
rootPath: targetPath,
|
|
1741
|
+
ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
|
|
1742
|
+
});
|
|
1743
|
+
const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
1744
|
+
|
|
1745
|
+
const [scanResults, hallResults] = await Promise.all([
|
|
1746
|
+
scanner.scanFiles(files),
|
|
1747
|
+
hallucinationDetector.scan(files),
|
|
1748
|
+
]);
|
|
1749
|
+
|
|
1750
|
+
const allIssues = [];
|
|
1751
|
+
for (const [file, fileResults] of Object.entries(scanResults)) {
|
|
1752
|
+
if (fileResults.issues) {
|
|
1753
|
+
for (const issue of fileResults.issues) {
|
|
1754
|
+
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
for (const item of hallResults.phantomAPIs) {
|
|
1759
|
+
allIssues.push({ file: item.file, category: 'hallucination', severity: 'critical', id: item.id, message: item.name });
|
|
1760
|
+
}
|
|
1761
|
+
for (const item of hallResults.deprecatedAPIs) {
|
|
1762
|
+
allIssues.push({ file: item.file, category: 'deprecated', severity: 'high', id: item.id, message: item.name });
|
|
1763
|
+
}
|
|
1764
|
+
for (const item of hallResults.aiSmells) {
|
|
1765
|
+
allIssues.push({ file: item.file, category: 'ai_smell', severity: 'warning', id: item.id, message: item.message });
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
const result = baseline.create(allIssues);
|
|
1769
|
+
console.log(c('green', ` ✓ Baseline created: ${c('bold', result.count)} known issues recorded`));
|
|
1770
|
+
console.log(` ${c('cyan', result.path)}`);
|
|
1771
|
+
console.log('');
|
|
1772
|
+
console.log(c('gray', ' Future scans with --baseline will only report NEW issues.'));
|
|
1773
|
+
console.log(c('gray', ' Commit .thuban-baseline.json to share baseline with your team.'));
|
|
1774
|
+
console.log('');
|
|
1775
|
+
} else {
|
|
1776
|
+
// Show baseline status
|
|
1777
|
+
const existing = baseline.load();
|
|
1778
|
+
console.log(c('bold', ' BASELINE STATUS'));
|
|
1779
|
+
console.log('');
|
|
1780
|
+
console.log(` Created: ${c('cyan', existing.created)}`);
|
|
1781
|
+
console.log(` Known issues: ${c('bold', existing.issueCount)}`);
|
|
1782
|
+
console.log('');
|
|
1783
|
+
console.log(c('gray', ' Use --baseline-create to update the baseline'));
|
|
1784
|
+
console.log(c('gray', ' Use --baseline with scan to only see new issues'));
|
|
1785
|
+
console.log('');
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
async function main() {
|
|
1790
|
+
const args = process.argv.slice(2);
|
|
1791
|
+
const opts = parseArgs(args);
|
|
1792
|
+
|
|
1793
|
+
if (!opts.command || opts.command === 'help') {
|
|
1794
|
+
printHelp();
|
|
1795
|
+
process.exit(0);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
if (opts.command === 'version') {
|
|
1799
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
1800
|
+
console.log(`thuban v${pkg.version}`);
|
|
1801
|
+
process.exit(0);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// Verify target path exists
|
|
1805
|
+
const noPathCommands2 = ['activate', 'status', 'upgrade'];
|
|
1806
|
+
|
|
1807
|
+
if (!noPathCommands2.includes(opts.command) && !fs.existsSync(opts.targetPath)) {
|
|
1808
|
+
console.error(c('red', ` Error: Path not found: ${opts.targetPath}`));
|
|
1809
|
+
process.exit(1);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
try {
|
|
1813
|
+
switch (opts.command) {
|
|
1814
|
+
case 'scan':
|
|
1815
|
+
await cmdScan(opts);
|
|
1816
|
+
break;
|
|
1817
|
+
case 'deps':
|
|
1818
|
+
await cmdDeps(opts);
|
|
1819
|
+
break;
|
|
1820
|
+
case 'drift':
|
|
1821
|
+
await cmdDrift(opts);
|
|
1822
|
+
break;
|
|
1823
|
+
case 'inject':
|
|
1824
|
+
await cmdInject(opts);
|
|
1825
|
+
break;
|
|
1826
|
+
case 'health':
|
|
1827
|
+
// Alias for report
|
|
1828
|
+
await cmdReport(opts);
|
|
1829
|
+
break;
|
|
1830
|
+
case 'report':
|
|
1831
|
+
await cmdReport(opts);
|
|
1832
|
+
break;
|
|
1833
|
+
case 'debt':
|
|
1834
|
+
await cmdDebt(opts);
|
|
1835
|
+
break;
|
|
1836
|
+
case 'fix':
|
|
1837
|
+
await cmdFix(opts);
|
|
1838
|
+
break;
|
|
1839
|
+
case 'hallucinate':
|
|
1840
|
+
case 'hal':
|
|
1841
|
+
await cmdHallucinate(opts);
|
|
1842
|
+
break;
|
|
1843
|
+
case 'watch':
|
|
1844
|
+
await cmdWatch(opts);
|
|
1845
|
+
break;
|
|
1846
|
+
case 'dashboard':
|
|
1847
|
+
case 'dash':
|
|
1848
|
+
await cmdDashboard(opts);
|
|
1849
|
+
break;
|
|
1850
|
+
case 'diff':
|
|
1851
|
+
await cmdDiff(opts);
|
|
1852
|
+
break;
|
|
1853
|
+
case 'activate':
|
|
1854
|
+
await cmdActivate(opts);
|
|
1855
|
+
break;
|
|
1856
|
+
case 'status':
|
|
1857
|
+
await cmdStatus(opts);
|
|
1858
|
+
break;
|
|
1859
|
+
case 'upgrade':
|
|
1860
|
+
cmdUpgrade();
|
|
1861
|
+
break;
|
|
1862
|
+
case 'gate':
|
|
1863
|
+
await cmdGate(opts);
|
|
1864
|
+
break;
|
|
1865
|
+
case 'cost':
|
|
1866
|
+
await cmdCost(opts);
|
|
1867
|
+
break;
|
|
1868
|
+
case 'ghosts':
|
|
1869
|
+
await cmdGhosts(opts);
|
|
1870
|
+
break;
|
|
1871
|
+
case 'ai-score':
|
|
1872
|
+
case 'aiscore':
|
|
1873
|
+
await cmdAIScore(opts);
|
|
1874
|
+
break;
|
|
1875
|
+
case 'clones':
|
|
1876
|
+
case 'duplicates':
|
|
1877
|
+
await cmdClones(opts);
|
|
1878
|
+
break;
|
|
1879
|
+
case 'passport':
|
|
1880
|
+
await cmdPassport(opts);
|
|
1881
|
+
break;
|
|
1882
|
+
case 'executive':
|
|
1883
|
+
case 'exec-report':
|
|
1884
|
+
case 'pdf':
|
|
1885
|
+
await cmdExecutive(opts);
|
|
1886
|
+
break;
|
|
1887
|
+
case 'baseline':
|
|
1888
|
+
await cmdBaseline(opts);
|
|
1889
|
+
break;
|
|
1890
|
+
default:
|
|
1891
|
+
console.error(c('red', ` Unknown command: ${opts.command}`));
|
|
1892
|
+
console.error(c('gray', ` Run 'thuban --help' for usage`));
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
} catch (err) {
|
|
1896
|
+
console.error(c('red', ` Error: ${err.message}`));
|
|
1897
|
+
if (opts.verbose) console.error(err.stack);
|
|
1898
|
+
process.exit(1);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
main();
|