thuban 0.3.3 → 0.3.4

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.
Files changed (66) hide show
  1. package/dist/LICENSE +21 -0
  2. package/dist/README.md +185 -0
  3. package/dist/cli.js +2 -0
  4. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  5. package/dist/packages/scanner/alert-manager.js +1 -0
  6. package/dist/packages/scanner/baseline-manager.js +1 -0
  7. package/dist/packages/scanner/code-scanner.js +1 -0
  8. package/dist/packages/scanner/codebase-passport.js +1 -0
  9. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  10. package/dist/packages/scanner/dependency-graph.js +1 -0
  11. package/dist/packages/scanner/drift-detector.js +1 -0
  12. package/dist/packages/scanner/executive-report.js +1 -0
  13. package/dist/packages/scanner/file-collector.js +1 -0
  14. package/dist/packages/scanner/file-watcher.js +1 -0
  15. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  16. package/dist/packages/scanner/hallucination-detector.js +1 -0
  17. package/dist/packages/scanner/health-checker.js +1 -0
  18. package/dist/packages/scanner/html-report.js +1 -0
  19. package/dist/packages/scanner/index.js +1 -0
  20. package/dist/packages/scanner/license-manager.js +1 -0
  21. package/dist/packages/scanner/master-health-checker.js +1 -0
  22. package/dist/packages/scanner/monitor-notifier.js +1 -0
  23. package/dist/packages/scanner/monitor-service.js +1 -0
  24. package/dist/packages/scanner/monitor-store.js +1 -0
  25. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  26. package/dist/packages/scanner/scan-diff.js +1 -0
  27. package/dist/packages/scanner/scan-runner.js +1 -0
  28. package/dist/packages/scanner/secret-scanner.js +1 -0
  29. package/dist/packages/scanner/sentinel-core.js +1 -0
  30. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  31. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  32. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  33. package/dist/packages/scanner/widget-generator.js +1 -0
  34. package/package.json +12 -7
  35. package/cli.js +0 -2627
  36. package/packages/scanner/ai-confidence-scorer.js +0 -260
  37. package/packages/scanner/alert-manager.js +0 -398
  38. package/packages/scanner/baseline-manager.js +0 -109
  39. package/packages/scanner/code-scanner.js +0 -758
  40. package/packages/scanner/codebase-passport.js +0 -299
  41. package/packages/scanner/copy-paste-detector.js +0 -276
  42. package/packages/scanner/dependency-graph.js +0 -541
  43. package/packages/scanner/drift-detector.js +0 -423
  44. package/packages/scanner/executive-report.js +0 -774
  45. package/packages/scanner/file-collector.js +0 -64
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -301
  48. package/packages/scanner/hallucination-detector.js +0 -822
  49. package/packages/scanner/health-checker.js +0 -586
  50. package/packages/scanner/html-report.js +0 -634
  51. package/packages/scanner/index.js +0 -100
  52. package/packages/scanner/license-manager.js +0 -331
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/monitor-notifier.js +0 -64
  55. package/packages/scanner/monitor-service.js +0 -103
  56. package/packages/scanner/monitor-store.js +0 -117
  57. package/packages/scanner/pre-commit-gate.js +0 -216
  58. package/packages/scanner/scan-diff.js +0 -50
  59. package/packages/scanner/scan-runner.js +0 -172
  60. package/packages/scanner/secret-scanner.js +0 -612
  61. package/packages/scanner/secret-scanner.test.js +0 -103
  62. package/packages/scanner/sentinel-core.js +0 -616
  63. package/packages/scanner/sentinel-knowledge.js +0 -322
  64. package/packages/scanner/tech-debt-analyzer.js +0 -583
  65. package/packages/scanner/tech-debt-cost.js +0 -194
  66. package/packages/scanner/widget-generator.js +0 -415
package/cli.js DELETED
@@ -1,2627 +0,0 @@
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 monitor [path] Recurring scans with saved history and alerts
18
- * npx thuban history [path] View recurring scan history for a repo
19
- * npx thuban trend [path] Show issue trends across recurring scans
20
- * npx thuban dashboard [path] Generate visual HTML dashboard report
21
- * npx thuban diff [path] Scan only git-changed files
22
- */
23
-
24
- const path = require('path');
25
- const fs = require('fs');
26
- const os = require('os');
27
-
28
- // Import engine components
29
- const CodeScanner = require('./packages/scanner/code-scanner.js');
30
- const DependencyGraph = require('./packages/scanner/dependency-graph.js');
31
- const DriftDetector = require('./packages/scanner/drift-detector.js');
32
- const WidgetGenerator = require('./packages/scanner/widget-generator.js');
33
- const AlertManager = require('./packages/scanner/alert-manager.js');
34
- const TechDebtAnalyzer = require('./packages/scanner/tech-debt-analyzer.js');
35
- const HallucinationDetector = require('./packages/scanner/hallucination-detector.js');
36
- const HtmlReportGenerator = require('./packages/scanner/html-report.js');
37
- const FileWatcher = require('./packages/scanner/file-watcher.js');
38
- const LicenseManager = require('./packages/scanner/license-manager.js');
39
- const PreCommitGate = require('./packages/scanner/pre-commit-gate.js');
40
- const TechDebtCostCalculator = require('./packages/scanner/tech-debt-cost.js');
41
- const GhostCodeDetector = require('./packages/scanner/ghost-code-detector.js');
42
- const AIConfidenceScorer = require('./packages/scanner/ai-confidence-scorer.js');
43
- const CopyPasteDriftDetector = require('./packages/scanner/copy-paste-detector.js');
44
- const CodebasePassport = require('./packages/scanner/codebase-passport.js');
45
- const ExecutiveReport = require('./packages/scanner/executive-report.js');
46
- const BaselineManager = require('./packages/scanner/baseline-manager.js');
47
- const MonitorStore = require('./packages/scanner/monitor-store.js');
48
- const { MonitorService, resolveIntervalMs } = require('./packages/scanner/monitor-service.js');
49
- const { collectFiles } = require('./packages/scanner/file-collector.js');
50
-
51
- // ─── Helpers ───────────────────────────────────────────────
52
-
53
- const COLORS = {
54
- reset: '\x1b[0m',
55
- bold: '\x1b[1m',
56
- red: '\x1b[31m',
57
- green: '\x1b[32m',
58
- yellow: '\x1b[33m',
59
- blue: '\x1b[34m',
60
- cyan: '\x1b[36m',
61
- gray: '\x1b[90m',
62
- white: '\x1b[37m',
63
- bgRed: '\x1b[41m',
64
- bgGreen: '\x1b[42m',
65
- bgYellow: '\x1b[43m',
66
- };
67
-
68
- function c(color, text) {
69
- return `${COLORS[color]}${text}${COLORS.reset}`;
70
- }
71
-
72
- function banner() {
73
- console.log('');
74
- console.log(c('cyan', ' ╔══════════════════════════════════════╗'));
75
- console.log(c('cyan', ' ║') + c('bold', ' THUBAN Code Health Engine ') + c('cyan', '║'));
76
- console.log(c('cyan', ' ║') + c('gray', ' v0.3.3 · Public Beta · thuban.dev ') + c('cyan', '║'));
77
- console.log(c('cyan', ' ╚══════════════════════════════════════╝'));
78
- console.log('');
79
- }
80
-
81
- function printHelp() {
82
- banner();
83
- const lm = new LicenseManager();
84
- const status = lm.getStatus();
85
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
86
- console.log(c('gray', ` v${pkg.version} · ${status.tierName} tier`) + (status.licensed ? c('green', ' ✓') : '') + c('gray', ' · https://thuban.dev'));
87
- console.log('');
88
-
89
- // ─── QUICK START ──────────────────────────────────────────
90
- console.log(c('bold', ' QUICK START'));
91
- console.log('');
92
- console.log(c('gray', ' Run these in order to see what Thuban does:'));
93
- console.log('');
94
- console.log(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
95
- console.log(` ${c('cyan', 'npx thuban fix .')} Preview what can be fixed`);
96
- console.log(` ${c('cyan', 'npx thuban fix . --fix')} Apply all safe fixes`);
97
- console.log(` ${c('cyan', 'npx thuban dashboard .')} Open visual HTML report`);
98
- console.log('');
99
-
100
- // ─── SCAN & DETECT ───────────────────────────────────────
101
- console.log(c('bold', ' SCAN & DETECT'));
102
- console.log('');
103
- console.log(` ${c('cyan', 'scan')} [path|url] Full health scan — score, grade, all issues`);
104
- console.log(` ${c('cyan', 'compare')} [a] [b] Compare two codebases side by side`);
105
- console.log(` ${c('cyan', 'hallucinate')} [path] Find phantom APIs and invented imports`);
106
- console.log(` ${c('cyan', 'ghosts')} [path] Dead functions nobody calls`);
107
- console.log(` ${c('cyan', 'ai-score')} [path] Probability each function is AI-generated`);
108
- console.log(` ${c('cyan', 'clones')} [path] Copy-pasted code that has drifted apart`);
109
- console.log(` ${c('cyan', 'drift')} [path] Files that changed since Mother Code was set`);
110
- console.log(` ${c('cyan', 'deps')} [path] Dependency map — circular, orphan, critical`);
111
- console.log(` ${c('cyan', 'diff')} [path] Scan only git-changed files`);
112
- console.log(` ${c('cyan', 'debt')} [path] Detailed tech debt breakdown`);
113
- console.log(` ${c('cyan', 'cost')} [path] Tech debt translated to £ and hours`);
114
- console.log('');
115
-
116
- // ─── FIX & PROTECT ───────────────────────────────────────
117
- console.log(c('bold', ' FIX & PROTECT'));
118
- console.log('');
119
- console.log(` ${c('cyan', 'fix')} [path] Preview fixable issues ${c('gray', '(safe, no changes)')}`);
120
- console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix')} Apply all safe fixes`);
121
- console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix --commit')} Each fix = 1 git commit ${c('gray', '(revert any)')}`);
122
- console.log(` ${c('cyan', 'inject')} [path] Add Mother Code DNA to every file`);
123
- console.log(` ${c('cyan', 'gate')} Install pre-commit hook ${c('gray', '(blocks hallucinations)')}`);
124
- console.log(` ${c('cyan', 'watch')} [path] Live sentinel — scans files as you save`);
125
- console.log(` ${c('cyan', 'monitor')} [path] Recurring scans with saved history and alerts ${c('gray', '(Pro)')}`);
126
- console.log(` ${c('cyan', 'history')} [path] View recurring scan history for a repo ${c('gray', '(Pro)')}`);
127
- console.log(` ${c('cyan', 'trend')} [path] Show issue trends across recurring scans ${c('gray', '(Pro)')}`);
128
- console.log(` ${c('cyan', 'baseline')} [path] Snapshot issues so future scans show only NEW ones`);
129
- console.log('');
130
-
131
- // ─── REPORTS ──────────────────────────────────────────────
132
- console.log(c('bold', ' REPORTS'));
133
- console.log('');
134
- console.log(` ${c('cyan', 'dashboard')} [path] Interactive HTML report ${c('gray', '(share with your team)')}`);
135
- console.log(` ${c('cyan', 'executive')} [path] CTO-ready report ${c('gray', '(print to PDF from browser)')}`);
136
- console.log(` ${c('cyan', 'report')} [path] Combined scan + deps + drift summary`);
137
- console.log(` ${c('cyan', 'passport')} [path] Codebase identity card ${c('gray', '(for new team members)')}`);
138
- console.log('');
139
-
140
- // ─── LICENSE ──────────────────────────────────────────────
141
- console.log(c('bold', ' LICENSE'));
142
- console.log('');
143
- console.log(` ${c('cyan', 'activate')} <key> Activate a license key`);
144
- console.log(` ${c('cyan', 'status')} Show license, usage, and features`);
145
- console.log(` ${c('cyan', 'upgrade')} Open pricing page`);
146
- console.log('');
147
-
148
- // ─── OPTIONS ──────────────────────────────────────────────
149
- console.log(c('bold', ' OPTIONS'));
150
- console.log('');
151
- console.log(` ${c('gray', '--fix')} Apply changes ${c('gray', '(default is dry-run preview)')}`);
152
- console.log(` ${c('gray', '--commit')} Auto-commit each fix for easy rollback`);
153
- console.log(` ${c('gray', '--unsafe')} Include fixes that change runtime behaviour`);
154
- console.log(` ${c('gray', '--json')} Machine-readable JSON output`);
155
- console.log(` ${c('gray', '--baseline')} Only show NEW issues vs saved baseline`);
156
- console.log(` ${c('gray', '--verbose')} Show detailed output`);
157
- console.log(` ${c('gray', '--ignore <pat>')} Skip files matching pattern`);
158
- console.log(` ${c('gray', '--max-issues <n>')} Limit displayed issues ${c('gray', '(default: 50)')}`);
159
- console.log('');
160
-
161
- // ─── EXAMPLES ─────────────────────────────────────────────
162
- console.log(c('bold', ' EXAMPLES'));
163
- console.log('');
164
- console.log(c('gray', ' # Scan the current project'));
165
- console.log(` ${c('white', 'npx thuban scan .')}`);
166
- console.log('');
167
- console.log(c('gray', ' # Find and fix all AI hallucinations'));
168
- console.log(` ${c('white', 'npx thuban hallucinate .')}`);
169
- console.log(` ${c('white', 'npx thuban fix . --fix')}`);
170
- console.log('');
171
- console.log(c('gray', ' # Generate a report for your CTO'));
172
- console.log(` ${c('white', 'npx thuban executive .')}`);
173
- console.log('');
174
- console.log(c('gray', ' # Block hallucinated code on every commit'));
175
- console.log(` ${c('white', 'npx thuban gate --fix')}`);
176
- console.log('');
177
- console.log(c('gray', ' # Scan a specific folder, ignore tests'));
178
- console.log(` ${c('white', 'npx thuban scan ./src --ignore "**/*.test.js"')}`);
179
- console.log('');
180
- console.log(c('gray', ' Report a bug or request a feature:'));
181
- console.log(` ${c('cyan', 'https://github.com/SilverwingsBenefitsGit/thuban/issues')}`);
182
- console.log('');
183
- }
184
-
185
- function parseArgs(args) {
186
- const opts = {
187
- command: null,
188
- targetPath: process.cwd(),
189
- json: false,
190
- fix: false,
191
- dryRun: false,
192
- safe: true,
193
- gitCommit: false,
194
- verbose: false,
195
- ignore: [],
196
- maxIssues: 50,
197
- licenseKey: null,
198
- baseline: false,
199
- baselineCreate: false,
200
- frequency: 'daily',
201
- intervalMs: null,
202
- notify: 'inbox',
203
- once: false,
204
- };
205
-
206
- const noPathCommands = ['activate', 'status', 'upgrade', 'help', 'version'];
207
-
208
- let i = 0;
209
- while (i < args.length) {
210
- const arg = args[i];
211
- if (arg === '--json') { opts.json = true; }
212
- else if (arg === '--fix') { opts.fix = true; }
213
- else if (arg === '--dry-run' || arg === '--dryrun') { opts.dryRun = true; }
214
- else if (arg === '--unsafe') { opts.safe = false; }
215
- else if (arg === '--git-commit' || arg === '--commit') { opts.gitCommit = true; }
216
- else if (arg === '--baseline') { opts.baseline = true; }
217
- else if (arg === '--baseline-create') { opts.baselineCreate = true; }
218
- else if (arg === '--frequency' && args[i + 1]) { opts.frequency = args[++i]; }
219
- else if (arg === '--interval-ms' && args[i + 1]) { opts.intervalMs = parseInt(args[++i], 10); }
220
- else if (arg === '--notify' && args[i + 1]) { opts.notify = args[++i]; }
221
- else if (arg === '--once') { opts.once = true; }
222
- else if (arg === '--verbose') { opts.verbose = true; }
223
- else if (arg === '--ignore' && args[i + 1]) { opts.ignore.push(args[++i]); }
224
- else if (arg === '--max-issues' && args[i + 1]) { opts.maxIssues = parseInt(args[++i], 10); }
225
- else if (arg === '--help' || arg === '-h') { opts.command = 'help'; }
226
- else if (arg === '--version' || arg === '-v') { opts.command = 'version'; }
227
- else if (!opts.command) { opts.command = arg; }
228
- else if (opts.command === 'activate' && !opts.licenseKey) { opts.licenseKey = arg; }
229
- else if (!noPathCommands.includes(opts.command)) {
230
- // Don't resolve git URLs as local paths
231
- if (arg.match(/^(https?:\/\/|git@)/)) {
232
- opts.targetPath = arg;
233
- } else {
234
- opts.targetPath = path.resolve(arg);
235
- }
236
- }
237
- i++;
238
- }
239
-
240
- return opts;
241
- }
242
-
243
- // ─── Collect files recursively ───────────────────────────────
244
-
245
- function summarizeRemainingFiles(scannedFiles, allFiles, rootPath) {
246
- const scannedSet = new Set(scannedFiles);
247
- const remainingFiles = allFiles.filter(file => !scannedSet.has(file));
248
- const directoryCounts = new Map();
249
-
250
- for (const file of remainingFiles) {
251
- const relativePath = path.relative(rootPath, file);
252
- const segments = relativePath.split(path.sep).filter(Boolean);
253
- const directory = segments.length > 1 ? segments[0] : '(root)';
254
- directoryCounts.set(directory, (directoryCounts.get(directory) || 0) + 1);
255
- }
256
-
257
- const topDirectories = Array.from(directoryCounts.entries())
258
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
259
- .slice(0, 3)
260
- .map(([directory, count]) => ({ directory, count }));
261
-
262
- return {
263
- remainingFiles,
264
- remainingCount: remainingFiles.length,
265
- topDirectories,
266
- previewFiles: remainingFiles.slice(0, 5).map(file => path.relative(rootPath, file)),
267
- };
268
- }
269
-
270
- function requireProFeature(featureName) {
271
- const lm = new LicenseManager();
272
- const access = lm.canUseProFeature(featureName);
273
- if (!access.allowed) {
274
- const upgradeUrl = 'https://thuban.dev/pricing';
275
- console.log('');
276
- console.log(c('yellow', ' ╔══════════════════════════════════════════════════════════════╗'));
277
- console.log(c('yellow', ' ║') + c('bold', ' PRO FEATURE REQUIRED ') + c('yellow', '║'));
278
- console.log(c('yellow', ' ╠══════════════════════════════════════════════════════════════╣'));
279
- console.log(c('yellow', ' ║') + ` ${featureName} is available on Thuban Pro.`.padEnd(62) + c('yellow', '║'));
280
- console.log(c('yellow', ' ║') + ' Free Thuban gives you a one-off checkup. Pro gives you'.padEnd(62) + c('yellow', '║'));
281
- console.log(c('yellow', ' ║') + ' a pair of eyes on your codebase all day every day with'.padEnd(62) + c('yellow', '║'));
282
- console.log(c('yellow', ' ║') + ' recurring scans, saved history, trends, and alerts.'.padEnd(62) + c('yellow', '║'));
283
- console.log(c('yellow', ' ║') + ' ' + c('yellow', '║'));
284
- console.log(c('yellow', ' ║') + ` Upgrade now: ${upgradeUrl}`.padEnd(62) + c('yellow', '║'));
285
- console.log(c('yellow', ' ║') + ' CLI shortcut: thuban upgrade'.padEnd(62) + c('yellow', '║'));
286
- console.log(c('yellow', ' ╚══════════════════════════════════════════════════════════════╝'));
287
- console.log('');
288
- process.exit(0);
289
- }
290
- return lm;
291
- }
292
-
293
- function resolveRepoConfigOrExit(store, targetPath) {
294
- const repo = store.getRepo(targetPath);
295
- if (!repo) {
296
- console.log('');
297
- console.log(c('yellow', ' No monitor configuration found for this repo.'));
298
- console.log(c('gray', ' Run `thuban monitor <path> --once` or `thuban monitor <path>` first.'));
299
- console.log('');
300
- process.exit(0);
301
- }
302
- return repo;
303
- }
304
-
305
- function printFileCapUpgradePrompt({ scannedCount, totalCount, remainingSummary, upgradeUrl }) {
306
- const criticalDirectoryCount = remainingSummary.topDirectories.length;
307
-
308
- console.log(c('yellow', ' ╔══════════════════════════════════════════════════════════════╗'));
309
- console.log(c('yellow', ' ║') + c('bold', ' FREE TIER LIMIT REACHED ') + c('yellow', '║'));
310
- console.log(c('yellow', ' ╠══════════════════════════════════════════════════════════════╣'));
311
- console.log(c('yellow', ' ║') + ` You have scanned ${scannedCount} of ${totalCount} files.`.padEnd(62) + c('yellow', '║'));
312
- console.log(c('yellow', ' ║') + ' Upgrade to Thuban Pro for continuous monitoring —'.padEnd(62) + c('yellow', '║'));
313
- console.log(c('yellow', ' ║') + ' a pair of eyes on your codebase, all day, every day.'.padEnd(62) + c('yellow', '║'));
314
- console.log(c('yellow', ' ║') + ' Your always-on guardian with unlimited scans.'.padEnd(62) + c('yellow', '║'));
315
- console.log(c('yellow', ' ║') + ` ${remainingSummary.remainingCount} more files remaining including ${criticalDirectoryCount}`.padEnd(62) + c('yellow', '║'));
316
- console.log(c('yellow', ' ║') + ` critical director${criticalDirectoryCount === 1 ? 'y' : 'ies'}.`.padEnd(62) + c('yellow', '║'));
317
- console.log(c('yellow', ' ║') + ' ' + c('yellow', '║'));
318
- console.log(c('yellow', ' ║') + ` Upgrade now: ${upgradeUrl}`.padEnd(62) + c('yellow', '║'));
319
- console.log(c('yellow', ' ║') + ` CLI shortcut: thuban upgrade`.padEnd(62) + c('yellow', '║'));
320
- console.log(c('yellow', ' ╚══════════════════════════════════════════════════════════════╝'));
321
- console.log('');
322
-
323
- if (remainingSummary.topDirectories.length > 0) {
324
- console.log(c('bold', ' Remaining scan preview'));
325
- for (const item of remainingSummary.topDirectories) {
326
- console.log(` ${c('yellow', '•')} ${c('cyan', item.directory)} ${c('gray', `(${item.count} files)`)}`);
327
- }
328
- if (remainingSummary.previewFiles.length > 0) {
329
- console.log('');
330
- console.log(c('gray', ' Next files that would be scanned:'));
331
- for (const file of remainingSummary.previewFiles) {
332
- console.log(` ${c('gray', '·')} ${file}`);
333
- }
334
- }
335
- console.log('');
336
- }
337
- }
338
-
339
- // ─── Commands ───────────────────────────────────────────────
340
-
341
- async function cmdScan(opts) {
342
- const startTime = Date.now();
343
- const gitUrlPattern = /^(https?:\/\/|git@)/;
344
- const isGitUrl = gitUrlPattern.test(opts.targetPath);
345
- let targetPath = isGitUrl ? opts.targetPath : path.resolve(opts.targetPath);
346
- let clonedRepo = null;
347
-
348
- // ─── Git URL detection — clone and scan ─────────────────
349
- if (isGitUrl) {
350
- if (!opts.json) {
351
- console.log('');
352
- console.log(c('cyan', ' Cloning repository...'));
353
- console.log(c('gray', ` ${opts.targetPath}`));
354
- }
355
- const tmpDir = path.join(os.tmpdir(), 'thuban-scan-' + Date.now());
356
- const { execFileSync } = require('child_process');
357
- try {
358
- execFileSync('git', ['clone', '--depth', '1', opts.targetPath, tmpDir], { stdio: 'pipe', timeout: 60000 });
359
- targetPath = tmpDir;
360
- clonedRepo = tmpDir;
361
- if (!opts.json) {
362
- console.log(c('cyan', ' Cloned successfully'));
363
- console.log('');
364
- }
365
- } catch (e) {
366
- console.error(c('red', ` Failed to clone repository: ${e.message}`));
367
- console.error(c('gray', ' Check the URL is correct and the repo is accessible'));
368
- process.exit(1);
369
- }
370
- }
371
-
372
- // ─── License check ───────────────────────────────────────
373
- const lm = new LicenseManager();
374
- const tier = lm.getTierConfig();
375
- const scanCheck = lm.canScan();
376
- const upgradeUrl = 'https://thuban.dev/pricing';
377
-
378
- if (!scanCheck.allowed && !opts.json) {
379
- console.log('');
380
- console.log(c('red', ' ╔══════════════════════════════════════════════════╗'));
381
- console.log(c('red', ' ║') + c('bold', ' Monthly scan limit reached ') + c('red', '║'));
382
- console.log(c('red', ' ╚══════════════════════════════════════════════════╝'));
383
- console.log('');
384
- console.log(` You've used ${c('bold', scanCheck.used + '/' + scanCheck.limit)} free scans this month.`);
385
- console.log(` Resets on ${c('cyan', scanCheck.resetsOn)}.`);
386
- console.log('');
387
- console.log(` ${c('cyan', 'Upgrade to Pro')} for unlimited scans, full reports, auto-fix, and an always-on pair of eyes via thuban monitor:`);
388
- console.log(` ${c('bold', upgradeUrl)}`);
389
- console.log('');
390
- console.log(` Or activate a key: ${c('cyan', 'thuban activate <your-key>')}`);
391
- console.log('');
392
- process.exit(0);
393
- }
394
-
395
- if (!opts.json) {
396
- banner();
397
-
398
- // First-run welcome
399
- if (lm.usage.totalScans === 0) {
400
- console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
401
- console.log(c('cyan', ' │') + c('bold', ' Welcome to Thuban! ') + c('cyan', '│'));
402
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
403
- console.log(c('cyan', ' │') + ' ' + c('gray', 'Thuban scans your codebase for AI hallucinations,') + ' ' + c('cyan', '│'));
404
- console.log(c('cyan', ' │') + ' ' + c('gray', 'dead code, tech debt, and architecture drift.') + ' ' + c('cyan', '│'));
405
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
406
- console.log(c('cyan', ' │') + ' ' + c('gray', 'After this scan, try:') + ' ' + c('cyan', '│'));
407
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix .') + ' ' + c('gray', 'Preview fixes') + ' ' + c('cyan', '│'));
408
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('gray', 'Visual report') + ' ' + c('cyan', '│'));
409
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban --help') + ' ' + c('gray', 'All commands') + ' ' + c('cyan', '│'));
410
- console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
411
- console.log('');
412
- }
413
-
414
- console.log(c('bold', ' SCANNING: ') + c('cyan', targetPath));
415
- console.log('');
416
- }
417
-
418
- const allFiles = collectFiles(targetPath, opts.ignore);
419
- const totalFileCount = allFiles.length;
420
-
421
- // File limit gating
422
- const files = tier.maxFiles < Infinity ? allFiles.slice(0, tier.maxFiles) : allFiles;
423
- const isFileCapped = totalFileCount > tier.maxFiles;
424
- const remainingSummary = isFileCapped ? summarizeRemainingFiles(files, allFiles, targetPath) : null;
425
-
426
- if (!opts.json && isFileCapped) {
427
- console.log(c('yellow', ` Free tier: scanning ${tier.maxFiles} of ${totalFileCount} files`));
428
- console.log(c('gray', ` Partial results below. Upgrade to scan your entire codebase.`));
429
- console.log('');
430
- } else if (!opts.json) {
431
- console.log(c('gray', ` Found ${files.length} files to scan...`));
432
- console.log('');
433
- }
434
-
435
- // Run all analyses in parallel for the gut-punch report
436
- const scanner = new CodeScanner({
437
- rootPath: targetPath,
438
- ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
439
- });
440
- const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
441
-
442
- const [scanResults, hallResults] = await Promise.all([
443
- scanner.scanFiles(files),
444
- hallucinationDetector.scan(files),
445
- ]);
446
-
447
- const elapsed = Date.now() - startTime;
448
-
449
- // Record this scan
450
- lm.recordScan();
451
-
452
- // ─── Collect all issues ──────────────────────────────────
453
- const allIssues = [];
454
- for (const issue of (scanResults.issues || [])) {
455
- allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
456
- }
457
-
458
- // Add hallucination results as issues
459
- for (const item of hallResults.phantomAPIs) {
460
- allIssues.push({
461
- file: item.file, line: item.line, severity: 'critical',
462
- category: 'hallucination', id: 'HALL_API',
463
- message: `${item.name} — this API does not exist`,
464
- suggestion: item.suggestion,
465
- crash: true,
466
- });
467
- }
468
- for (const item of hallResults.phantomImports) {
469
- allIssues.push({
470
- file: item.file, line: item.line, severity: 'critical',
471
- category: 'hallucination', id: 'HALL_IMPORT',
472
- message: `${item.module} — ${item.reason}`,
473
- crash: true,
474
- });
475
- }
476
- for (const item of hallResults.deprecatedAPIs) {
477
- allIssues.push({
478
- file: item.file, line: item.line, severity: 'high',
479
- category: 'deprecated', id: 'DEPR_API',
480
- message: `${item.name} — deprecated since ${item.since}`,
481
- suggestion: item.suggestion,
482
- });
483
- }
484
- for (const item of hallResults.aiSmells) {
485
- allIssues.push({
486
- file: item.file, line: item.line, severity: 'warning',
487
- category: 'ai_smell', id: item.id,
488
- message: item.message,
489
- });
490
- }
491
-
492
- if (opts.json) {
493
- console.log(JSON.stringify({
494
- files: totalFileCount,
495
- filesScanned: files.length,
496
- elapsed_ms: elapsed,
497
- tier: lm.getTier(),
498
- issues: tier.showFullDetails ? allIssues : allIssues.slice(0, tier.teaserIssues),
499
- totalIssues: allIssues.length,
500
- gated: !tier.showFullDetails,
501
- fileLimitReached: isFileCapped,
502
- upgradeUrl: isFileCapped ? upgradeUrl : null,
503
- remainingFiles: remainingSummary ? remainingSummary.remainingCount : 0,
504
- remainingDirectories: remainingSummary ? remainingSummary.topDirectories : [],
505
- remainingPreview: remainingSummary ? remainingSummary.previewFiles : [],
506
- }, null, 2));
507
- return;
508
- }
509
-
510
- // ─── Sort by severity ────────────────────────────────────
511
- const severityOrder = { critical: 0, high: 1, error: 2, medium: 3, warning: 4, low: 5, info: 6 };
512
- allIssues.sort((a, b) => (severityOrder[a.severity] || 99) - (severityOrder[b.severity] || 99));
513
-
514
- const criticals = allIssues.filter(i => i.severity === 'critical');
515
- const highs = allIssues.filter(i => i.severity === 'high' || i.severity === 'error');
516
- const mediums = allIssues.filter(i => i.severity === 'medium' || i.severity === 'warning');
517
- const lows = allIssues.filter(i => i.severity === 'low' || i.severity === 'info');
518
- const hallucinations = allIssues.filter(i => i.category === 'hallucination');
519
- const deprecated = allIssues.filter(i => i.category === 'deprecated');
520
-
521
- // ─── Health score ────────────────────────────────────────
522
- // Weight by category: hallucinations are critical, AI smells are minor
523
- const aiSmellIssues = allIssues.filter(i => i.category === 'ai_smell');
524
- const nonSmellMediums = mediums.filter(i => i.category !== 'ai_smell');
525
- const score = Math.max(0, 100
526
- - (criticals.length * 15)
527
- - (highs.length * 5)
528
- - (nonSmellMediums.length * 2)
529
- - (aiSmellIssues.length * 0.5)
530
- - (lows.length * 0.25)
531
- );
532
- const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C+' : score >= 60 ? 'C' : score >= 50 ? 'D+' : score >= 40 ? 'D' : 'F';
533
- const scoreColor = score >= 80 ? 'green' : score >= 60 ? 'yellow' : 'red';
534
-
535
- // ═══════════════════════════════════════════════════════════
536
- // THE GUT-PUNCH OUTPUT
537
- // ═══════════════════════════════════════════════════════════
538
-
539
- // ─── Count fixable items ─────────────────────────────────
540
- const fixableCount = allIssues.filter(i => i.fixable !== false).length;
541
-
542
- console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
543
- console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN RESULTS ') + c('cyan', '║'));
544
- console.log(c('cyan', ' ║') + c('gray', ' v0.3.3 · Public Beta ') + c('cyan', '║'));
545
- console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
546
- console.log(c('cyan', ' ║') + ` Files scanned: ${c('bold', String(files.length).padEnd(6))}` + (isFileCapped ? c('yellow', ` of ${totalFileCount}`) : ' ') + c('cyan', ' ║'));
547
- console.log(c('cyan', ' ║') + ` Health score: ${c(scoreColor, (grade + ' (' + Math.round(score) + '/100)').padEnd(20))}` + c('cyan', ' ║'));
548
- console.log(c('cyan', ' ║') + ` Issues found: ${c(allIssues.length > 0 ? 'yellow' : 'green', String(allIssues.length).padEnd(20))}` + c('cyan', ' ║'));
549
- console.log(c('cyan', ' ║') + ` Auto-fixable: ${c('green', String(fixableCount).padEnd(20))}` + c('cyan', ' ║'));
550
- console.log(c('cyan', ' ║') + ` Scan time: ${c('gray', (elapsed + 'ms').padEnd(20))}` + c('cyan', ' ║'));
551
- console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
552
- console.log('');
553
-
554
- // ─── Python/non-JS scoring disclosure ───────────────────
555
- const hasNonJsFiles = files.some(f => !f.endsWith('.js') && !f.endsWith('.ts') && !f.endsWith('.jsx') && !f.endsWith('.tsx') && !f.endsWith('.mjs'));
556
- if (hasNonJsFiles) {
557
- const nonJsExts = [...new Set(files.filter(f => !/\.(js|ts|jsx|tsx|mjs)$/.test(f)).map(f => path.extname(f)).filter(Boolean))];
558
- console.log(c('gray', ` Note: Score includes Security + Hallucination + Code Quality categories.`));
559
- console.log(c('gray', ` Architecture + Dependency analysis evaluated for JS/TS only. Detected non-JS: ${nonJsExts.join(', ')}`));
560
- console.log('');
561
- }
562
-
563
- // ─── CRITICAL: Hallucinated APIs (will crash) ────────────
564
- if (hallucinations.length > 0) {
565
- console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)));
566
- console.log(c('gray', ' These imports or method calls do not exist. Your code WILL crash when it hits them.'));
567
- console.log('');
568
-
569
- const showCount = tier.showFullDetails ? hallucinations.length : Math.min(tier.teaserIssues, hallucinations.length);
570
- for (let i = 0; i < showCount; i++) {
571
- const item = hallucinations[i];
572
- const lineRef = item.line ? `:${item.line}` : '';
573
- console.log(` ${c('cyan', item.file + lineRef)}`);
574
- // Try to read the actual source line for maximum impact
575
- try {
576
- const fullPath = path.resolve(targetPath, item.file);
577
- const lines = fs.readFileSync(fullPath, 'utf-8').split('\n');
578
- if (item.line && lines[item.line - 1]) {
579
- console.log(` ${c('red', lines[item.line - 1].trim())}`);
580
- }
581
- } catch (e) { /* skip */ }
582
- console.log(` ${c('yellow', '^^^')} ${item.message}`);
583
- if (item.suggestion) {
584
- console.log(` ${c('green', 'Fix:')} ${item.suggestion}`);
585
- }
586
- console.log('');
587
- }
588
-
589
- if (hallucinations.length > showCount) {
590
- console.log(c('gray', ` ... ${hallucinations.length - showCount} more hallucinations hidden`));
591
- console.log(c('cyan', ` Upgrade to Pro to see all: `) + c('bold', 'thuban upgrade'));
592
- console.log('');
593
- }
594
- }
595
-
596
- // ─── HIGH: Deprecated APIs ───────────────────────────────
597
- if (deprecated.length > 0) {
598
- console.log(c('red', c('bold', ` DEPRECATED: ${deprecated.length} API${deprecated.length > 1 ? 's' : ''} that will break on upgrade`)));
599
- console.log(c('gray', ' These work NOW but will stop working when you upgrade Node.js or your dependencies.'));
600
- console.log(c('gray', ' Fix: ') + c('cyan', 'npx thuban fix . --fix --unsafe'));
601
- console.log('');
602
-
603
- const showCount = tier.showFullDetails ? deprecated.length : Math.min(tier.teaserIssues, deprecated.length);
604
- for (let i = 0; i < showCount; i++) {
605
- const item = deprecated[i];
606
- const lineRef = item.line ? `:${item.line}` : '';
607
- console.log(` ${c('cyan', item.file + lineRef)}`);
608
- console.log(` ${c('yellow', item.message)}`);
609
- if (item.suggestion) {
610
- console.log(` ${c('green', '→')} ${item.suggestion}`);
611
- }
612
- console.log('');
613
- }
614
-
615
- if (deprecated.length > showCount) {
616
- console.log(c('gray', ` ... ${deprecated.length - showCount} more hidden`));
617
- console.log('');
618
- }
619
- }
620
-
621
- // ─── Security & quality issues ───────────────────────────
622
- const securityIssues = allIssues.filter(i => i.category === 'security');
623
- const qualityIssues = allIssues.filter(i => i.category === 'quality' || i.category === 'performance' || i.category === 'ai' || i.category === 'ai_smell');
624
-
625
- if (securityIssues.length > 0) {
626
- console.log(c('red', c('bold', ` SECURITY: ${securityIssues.length} issue${securityIssues.length > 1 ? 's' : ''}`)));
627
- const showCount = tier.showFullDetails ? Math.min(securityIssues.length, 10) : Math.min(tier.teaserIssues, securityIssues.length);
628
- for (let i = 0; i < showCount; i++) {
629
- const item = securityIssues[i];
630
- const lineRef = item.line ? `:${item.line}` : '';
631
- console.log(` ${c('red', '✗')} ${c('cyan', item.file + lineRef)} ${c('gray', item.message)}`);
632
- }
633
- if (securityIssues.length > showCount) {
634
- console.log(c('gray', ` ... ${securityIssues.length - showCount} more hidden`));
635
- }
636
- console.log('');
637
- }
638
-
639
- if (qualityIssues.length > 0) {
640
- // Group quality issues by type for clearer output
641
- const qualityByType = {};
642
- for (const item of qualityIssues) {
643
- const key = item.message || item.type || 'other';
644
- // Normalize similar messages
645
- const normalizedKey = key.replace(/\s*—.*$/, '').trim();
646
- if (!qualityByType[normalizedKey]) qualityByType[normalizedKey] = [];
647
- qualityByType[normalizedKey].push(item);
648
- }
649
- const typeEntries = Object.entries(qualityByType).sort((a, b) => b[1].length - a[1].length);
650
-
651
- console.log(c('yellow', c('bold', ` CODE QUALITY: ${qualityIssues.length} issue${qualityIssues.length > 1 ? 's' : ''}`)));
652
- console.log(c('gray', ' Patterns that reduce code quality, maintainability, or production readiness.'));
653
- console.log('');
654
-
655
- // Show grouped summary
656
- const showTypes = tier.showFullDetails ? typeEntries.length : Math.min(3, typeEntries.length);
657
- for (let t = 0; t < showTypes; t++) {
658
- const [typeName, items] = typeEntries[t];
659
- const sevColor = items[0].severity === 'high' || items[0].severity === 'error' ? 'red' : 'yellow';
660
- console.log(` ${c(sevColor, '!')} ${c('bold', String(items.length))} x ${typeName}`);
661
- // Show first example file
662
- if (items[0].file) {
663
- const lineRef = items[0].line ? `:${items[0].line}` : '';
664
- console.log(` ${c('gray', 'e.g.')} ${c('cyan', items[0].file + lineRef)}`);
665
- }
666
- }
667
- if (typeEntries.length > showTypes) {
668
- console.log(c('gray', ` ... ${typeEntries.length - showTypes} more issue types`));
669
- }
670
- console.log('');
671
-
672
- // Show fix command if fixable
673
- const fixableQuality = qualityIssues.filter(i => i.fixable !== false).length;
674
- if (fixableQuality > 0) {
675
- console.log(c('gray', ` ${fixableQuality} of these can be auto-fixed: `) + c('cyan', 'npx thuban fix . --fix --unsafe'));
676
- console.log('');
677
- }
678
- }
679
-
680
- // ─── THE VERDICT ─────────────────────────────────────────
681
- console.log(c('bold', ' ┌──────────────────────────────────────────────────────┐'));
682
-
683
- if (score < 60) {
684
- console.log(c('bold', ' │ ') + c('red', 'YOUR CODEBASE HAS A SERIOUS HEALTH PROBLEM ') + c('bold', ' │'));
685
- } else if (score < 80) {
686
- console.log(c('bold', ' │ ') + c('yellow', 'YOUR CODEBASE NEEDS ATTENTION ') + c('bold', ' │'));
687
- } else {
688
- console.log(c('bold', ' │ ') + c('green', 'YOUR CODEBASE IS IN GOOD SHAPE ') + c('bold', ' │'));
689
- }
690
-
691
- console.log(c('bold', ' │') + ` Score: ${c(scoreColor, grade + ' (' + Math.round(score) + '/100)')} ` + c('bold', '│'));
692
- console.log(c('bold', ' └──────────────────────────────────────────────────────┘'));
693
- console.log('');
694
-
695
- // ─── Impact statement ────────────────────────────────────
696
- if (criticals.length > 0) {
697
- console.log(c('red', ` ${criticals.length} function${criticals.length > 1 ? 's' : ''} will crash in production.`));
698
- const fixTime = criticals.length <= 3 ? '30 seconds' : criticals.length <= 10 ? '2 minutes' : '10 minutes';
699
- console.log(c('gray', ` Estimated cleanup: ${fixTime} with Thuban auto-fix.`));
700
- console.log(c('gray', ` Estimated cost if shipped: your weekend.`));
701
- console.log('');
702
- }
703
-
704
- // ─── FREE TIER: Upgrade CTA ──────────────────────────────
705
- if (!tier.showFullDetails) {
706
- const hidden = allIssues.length - tier.teaserIssues;
707
- if (hidden > 0) {
708
- console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
709
- console.log(c('cyan', ' ║') + c('bold', ` ${hidden} more issues hidden `) + c('cyan', '║'));
710
- console.log(c('cyan', ' ║') + c('gray', ' Upgrade to see every issue, fix them automatically,') + c('cyan', '║'));
711
- console.log(c('cyan', ' ║') + c('gray', ' and get the full HTML dashboard report. ') + c('cyan', '║'));
712
- console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
713
- console.log(c('cyan', ' ║') + ` ${c('bold', 'thuban upgrade')} Open pricing page ` + c('cyan', '║'));
714
- console.log(c('cyan', ' ║') + ` ${c('bold', 'thuban activate')} <key> Activate a license key ` + c('cyan', '║'));
715
- console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
716
- console.log('');
717
- }
718
-
719
- // Show scan counter
720
- const scanStatus = lm.canScan();
721
- const remaining = scanStatus.remaining;
722
- console.log(c('gray', ` Free scans remaining this month: ${remaining}/${scanStatus.limit}`));
723
- console.log('');
724
- } else {
725
- // Pro/Team: show clear next steps
726
- if (allIssues.length > 0) {
727
- const fixable = allIssues.filter(i => i.fixable !== false).length;
728
- console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
729
- console.log(c('cyan', ' │') + c('bold', ' WHAT TO DO NEXT ') + c('cyan', '│'));
730
- console.log(c('cyan', ' ├──────────────────────────────────────────────────────┤'));
731
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
732
- console.log(c('cyan', ' │') + ' ' + c('bold', '1. See what would be fixed (safe preview):') + ' ' + c('cyan', '│'));
733
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . ') + ' ' + c('cyan', '│'));
734
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
735
- console.log(c('cyan', ' │') + ' ' + c('bold', '2. Apply all safe fixes:') + ' ' + c('cyan', '│'));
736
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix') + ' ' + c('cyan', '│'));
737
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
738
- console.log(c('cyan', ' │') + ' ' + c('bold', '3. Apply fixes with git rollback:') + ' ' + c('cyan', '│'));
739
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix --commit') + ' ' + c('cyan', '│'));
740
- console.log(c('cyan', ' │') + ' ' + c('gray', 'Each fix = 1 git commit. Undo any with git revert.') + c('cyan', '│'));
741
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
742
- console.log(c('cyan', ' │') + ' ' + c('bold', '4. Generate visual report (HTML):') + ' ' + c('cyan', '│'));
743
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('cyan', '│'));
744
- console.log(c('cyan', ' │') + ' ' + c('gray', 'Opens a dashboard you can share with your team.') + ' ' + c('cyan', '│'));
745
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
746
- console.log(c('cyan', ' │') + ' ' + c('bold', '5. Show tech debt cost in £:') + ' ' + c('cyan', '│'));
747
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban cost .') + ' ' + c('cyan', '│'));
748
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
749
- console.log(c('cyan', ' │') + ' ' + c('bold', '6. Block bad code on every commit:') + ' ' + c('cyan', '│'));
750
- console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban gate') + ' ' + c('cyan', '│'));
751
- console.log(c('cyan', ' │') + ' ' + c('gray', 'Installs a pre-commit hook. Set once, forget.') + ' ' + c('cyan', '│'));
752
- console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
753
- console.log(c('cyan', ' │') + ' ' + c('gray', 'All commands: npx thuban --help') + ' ' + c('cyan', '│'));
754
- console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
755
- console.log('');
756
- }
757
- }
758
-
759
- if (isFileCapped) {
760
- printFileCapUpgradePrompt({
761
- scannedCount: files.length,
762
- totalCount: totalFileCount,
763
- remainingSummary,
764
- upgradeUrl,
765
- });
766
- }
767
-
768
- // ─── Continuous monitoring pitch ────────────────────────
769
- if (allIssues.length > 0 && !isFileCapped) {
770
- console.log(c('gray', ' ─────────────────────────────────────────────────────'));
771
- console.log(c('gray', ' Thuban runs 100% on your machine. No data leaves.'));
772
- console.log(c('gray', ' No cloud costs. As your codebase grows, Thuban grows with it.'));
773
- console.log('');
774
- console.log(c('gray', ' Keep your codebase healthy continuously:'));
775
- console.log(c('gray', ' ') + c('cyan', 'npx thuban gate --fix') + c('gray', ' Block bad code on every commit'));
776
- console.log(c('gray', ' ') + c('cyan', 'npx thuban watch .') + c('gray', ' Scan files as you save them'));
777
- console.log(c('gray', ' ') + c('cyan', 'npx thuban baseline .') + c('gray', ' Only alert on NEW issues'));
778
- console.log('');
779
- }
780
- }
781
-
782
- async function cmdDeps(opts) {
783
- const startTime = Date.now();
784
- const targetPath = path.resolve(opts.targetPath);
785
-
786
- if (!opts.json) {
787
- banner();
788
- console.log(c('bold', ' MAPPING DEPENDENCIES: ') + c('cyan', targetPath));
789
- console.log('');
790
- }
791
-
792
- const files = collectFiles(targetPath, opts.ignore);
793
- const graph = new DependencyGraph({ rootPath: targetPath });
794
- await graph.build(files);
795
- const elapsed = Date.now() - startTime;
796
-
797
- const summary = graph.getModuleSummary();
798
- const critical = graph.getMostCriticalFiles(10);
799
- const orphans = graph.getOrphanFiles();
800
- const circular = graph.circularDeps || [];
801
-
802
- if (opts.json) {
803
- console.log(JSON.stringify({ elapsed_ms: elapsed, summary, critical, orphans, circular, graph: graph.toJSON() }, null, 2));
804
- return;
805
- }
806
-
807
- console.log(c('gray', ` Analysed ${files.length} files in ${elapsed}ms`));
808
- console.log('');
809
-
810
- console.log(c('bold', ' DEPENDENCY SUMMARY'));
811
- console.log(` Files: ${summary.totalFiles || files.length}`);
812
- console.log(` Import links: ${summary.totalImports || 0}`);
813
- console.log(` Circular deps: ${circular.length > 0 ? c('red', circular.length) : c('green', '0')}`);
814
- console.log(` Orphan files: ${orphans.length > 0 ? c('yellow', orphans.length) : c('green', '0')}`);
815
- console.log('');
816
-
817
- if (critical.length > 0) {
818
- console.log(c('bold', ' MOST CRITICAL FILES') + c('gray', ' (most dependents)'));
819
- for (const f of critical) {
820
- const filePath = typeof f === 'string' ? f : (f.file || f.path || String(f));
821
- const rel = path.relative(targetPath, filePath);
822
- const deps = typeof f === 'object' ? (f.dependents || f.score || f.count || '?') : '?';
823
- console.log(` ${c('cyan', rel)} ${c('gray', '→ ' + deps + ' dependents')}`);
824
- }
825
- console.log('');
826
- }
827
-
828
- if (circular.length > 0) {
829
- console.log(c('red', ' CIRCULAR DEPENDENCIES'));
830
- for (const cycle of circular.slice(0, 5)) {
831
- const cycleStr = Array.isArray(cycle) ? cycle.map(f => path.relative(targetPath, f)).join(' → ') : String(cycle);
832
- console.log(` ${c('yellow', '⟲')} ${cycleStr}`);
833
- }
834
- console.log('');
835
- }
836
-
837
- if (orphans.length > 0) {
838
- console.log(c('yellow', ' ORPHAN FILES') + c('gray', ' (no imports, no exports consumed)'));
839
- for (const orphan of orphans.slice(0, 10)) {
840
- const orphanPath = typeof orphan === 'string' ? orphan : (orphan.file || orphan.path || String(orphan));
841
- console.log(` ${c('gray', '○')} ${path.relative(targetPath, orphanPath)}`);
842
- }
843
- if (orphans.length > 10) {
844
- console.log(c('gray', ` ... and ${orphans.length - 10} more`));
845
- }
846
- console.log('');
847
- }
848
- }
849
-
850
- async function cmdMonitor(opts) {
851
- requireProFeature('Scheduled monitoring');
852
- const targetPath = path.resolve(opts.targetPath);
853
- const service = new MonitorService();
854
- const repoConfig = service.configureRepo({
855
- repoPath: targetPath,
856
- frequency: opts.frequency,
857
- intervalMs: resolveIntervalMs(opts.frequency, opts.intervalMs),
858
- notification: { channels: String(opts.notify || 'inbox').split(',').map(item => item.trim()).filter(Boolean) },
859
- });
860
-
861
- if (opts.once) {
862
- const result = await service.runRepoScan(repoConfig);
863
- if (opts.json) {
864
- console.log(JSON.stringify(result.run, null, 2));
865
- return;
866
- }
867
- console.log('');
868
- console.log(c('bold', ' THUBAN MONITOR RUN'));
869
- console.log(` Repo: ${repoConfig.repoPath}`);
870
- console.log(` New issues: ${result.diff.added.length}`);
871
- console.log(` Resolved issues: ${result.diff.resolved.length}`);
872
- console.log(` History saved: ${result.runPath}`);
873
- console.log('');
874
- return;
875
- }
876
-
877
- console.log('');
878
- console.log(c('bold', ' THUBAN MONITOR ACTIVE'));
879
- console.log(` Repo: ${repoConfig.repoPath}`);
880
- console.log(` Frequency: ${repoConfig.frequency} (${repoConfig.intervalMs}ms)`);
881
- console.log(` Notifications: ${(repoConfig.notification.channels || []).join(', ')}`);
882
- console.log(c('gray', ' Press Ctrl+C to stop.'));
883
- console.log('');
884
-
885
- service.start(repoConfig);
886
- process.on('SIGINT', () => {
887
- service.stopAll();
888
- process.exit(0);
889
- });
890
- }
891
-
892
- async function cmdHistory(opts) {
893
- requireProFeature('Scan history');
894
- const store = new MonitorStore();
895
- const repo = resolveRepoConfigOrExit(store, opts.targetPath);
896
- const runs = store.listRuns(repo.repoId);
897
-
898
- if (opts.json) {
899
- console.log(JSON.stringify({ repo, runs }, null, 2));
900
- return;
901
- }
902
-
903
- console.log('');
904
- console.log(c('bold', ' THUBAN HISTORY'));
905
- console.log(` Repo: ${repo.repoPath}`);
906
- console.log(` Runs: ${runs.length}`);
907
- console.log('');
908
-
909
- for (const run of runs.slice(-10).reverse()) {
910
- console.log(` ${c('cyan', run.timestamp)} issues=${run.metrics.issueCount} new=${run.diff?.added?.length || 0} resolved=${run.diff?.resolved?.length || 0}`);
911
- }
912
-
913
- const notifications = store.getNotifications(10).filter(item => item.repoId === repo.repoId);
914
- if (notifications.length) {
915
- console.log('');
916
- console.log(c('bold', ' RECENT ALERTS'));
917
- for (const notification of notifications) {
918
- console.log(` ${c('yellow', notification.timestamp)} ${notification.summary}`);
919
- }
920
- }
921
- console.log('');
922
- }
923
-
924
- async function cmdTrend(opts) {
925
- requireProFeature('Trend reporting');
926
- const store = new MonitorStore();
927
- const repo = resolveRepoConfigOrExit(store, opts.targetPath);
928
- const runs = store.listRuns(repo.repoId);
929
- const trend = runs.map(run => ({
930
- timestamp: run.timestamp,
931
- issueCount: run.metrics.issueCount,
932
- hallucinations: run.metrics.hallucinations,
933
- secrets: run.metrics.secrets,
934
- techDebtScore: run.metrics.techDebtScore,
935
- techDebtHours: run.metrics.techDebtHours,
936
- }));
937
-
938
- if (opts.json) {
939
- console.log(JSON.stringify({ repo, trend }, null, 2));
940
- return;
941
- }
942
-
943
- console.log('');
944
- console.log(c('bold', ' THUBAN TREND'));
945
- console.log(` Repo: ${repo.repoPath}`);
946
- console.log('');
947
- for (const point of trend.slice(-12)) {
948
- console.log(` ${c('cyan', point.timestamp)} issues=${point.issueCount} hallucinations=${point.hallucinations} secrets=${point.secrets} debtScore=${point.techDebtScore ?? 'n/a'} debtHours=${point.techDebtHours ?? 'n/a'}`);
949
- }
950
- console.log('');
951
- }
952
-
953
- async function cmdDrift(opts) {
954
- const startTime = Date.now();
955
- const targetPath = path.resolve(opts.targetPath);
956
-
957
- if (!opts.json) {
958
- banner();
959
- console.log(c('bold', ' DETECTING DRIFT: ') + c('cyan', targetPath));
960
- console.log('');
961
- }
962
-
963
- const files = collectFiles(targetPath, opts.ignore);
964
- const detector = new DriftDetector({ rootPath: targetPath });
965
-
966
- const results = [];
967
- let withWidget = 0;
968
- let withDrift = 0;
969
-
970
- for (const file of files) {
971
- try {
972
- const result = await detector.analyzeFile(file);
973
- if (result && result.hasWidget) {
974
- withWidget++;
975
- if (result.drifts && result.drifts.length > 0) {
976
- withDrift++;
977
- results.push({ file: path.relative(targetPath, file), ...result });
978
- }
979
- }
980
- } catch (e) { /* skip unreadable files */ }
981
- }
982
-
983
- const elapsed = Date.now() - startTime;
984
-
985
- if (opts.json) {
986
- console.log(JSON.stringify({ elapsed_ms: elapsed, total: files.length, annotated: withWidget, drifted: withDrift, results }, null, 2));
987
- return;
988
- }
989
-
990
- console.log(c('gray', ` Checked ${files.length} files in ${elapsed}ms`));
991
- console.log('');
992
- console.log(` Files with Mother Code: ${c('cyan', withWidget)}`);
993
- console.log(` Files without: ${c('yellow', files.length - withWidget)}`);
994
- console.log(` Files with drift: ${withDrift > 0 ? c('red', withDrift) : c('green', '0')}`);
995
- console.log('');
996
-
997
- if (results.length > 0) {
998
- console.log(c('bold', ' DRIFT DETECTED'));
999
- for (const r of results.slice(0, 20)) {
1000
- console.log(` ${c('yellow', '⚠')} ${c('cyan', r.file)}`);
1001
- for (const d of r.drifts) {
1002
- console.log(` ${c('gray', '→')} ${d.type}: ${d.message || d.detail || JSON.stringify(d)}`);
1003
- }
1004
- }
1005
- console.log('');
1006
- }
1007
-
1008
- const coverage = files.length > 0 ? Math.round((withWidget / files.length) * 100) : 0;
1009
- const covColor = coverage >= 80 ? 'green' : coverage >= 40 ? 'yellow' : 'red';
1010
- console.log(` Mother Code coverage: ${c(covColor, coverage + '%')}`);
1011
- console.log('');
1012
- }
1013
-
1014
- async function cmdInject(opts) {
1015
- const targetPath = path.resolve(opts.targetPath);
1016
-
1017
- if (!opts.json) {
1018
- banner();
1019
- console.log(c('bold', ' INJECTING MOTHER CODE: ') + c('cyan', targetPath));
1020
- console.log('');
1021
- }
1022
-
1023
- const files = collectFiles(targetPath, opts.ignore).filter(f =>
1024
- f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.jsx') || f.endsWith('.tsx') ||
1025
- f.endsWith('.py') || f.endsWith('.java') || f.endsWith('.cs') || f.endsWith('.go') ||
1026
- f.endsWith('.kt') || f.endsWith('.rs') || f.endsWith('.php') || f.endsWith('.rb')
1027
- );
1028
- const graph = new DependencyGraph({ rootPath: targetPath });
1029
- await graph.build(files);
1030
-
1031
- const generator = new WidgetGenerator({ rootPath: targetPath, dependencyGraph: graph });
1032
-
1033
- let injected = 0;
1034
- let skipped = 0;
1035
- const previews = [];
1036
-
1037
- for (const file of files) {
1038
- try {
1039
- const content = fs.readFileSync(file, 'utf-8');
1040
- // Check if already has a widget
1041
- if (content.includes('@purpose') && content.includes('@module')) {
1042
- skipped++;
1043
- continue;
1044
- }
1045
-
1046
- const rawWidget = await generator.generateWidget(file, content);
1047
- if (!rawWidget) { skipped++; continue; }
1048
- // generateWidget returns an object with { widgetString, ... }
1049
- const rawWidgetStr = (rawWidget && rawWidget.widgetString) ? rawWidget.widgetString : String(rawWidget);
1050
-
1051
- // Select correct comment style by file extension
1052
- const ext = path.extname(file).toLowerCase();
1053
- let widget;
1054
- if (ext === '.py' || ext === '.rb') {
1055
- // Python and Ruby use # line comments
1056
- widget = rawWidgetStr.replace(/^\/\*\*\n/, '').replace(/\s*\*\/\s*$/, '')
1057
- .split('\n').map(l => l.replace(/^\s*\*\s?/, '# ')).join('\n');
1058
- } else if (ext === '.go' || ext === '.rs') {
1059
- // Go and Rust use // line comments
1060
- widget = rawWidgetStr.replace(/^\/\*\*\n/, '').replace(/\s*\*\/\s*$/, '')
1061
- .split('\n').map(l => l.replace(/^\s*\*\s?/, '// ')).join('\n');
1062
- } else {
1063
- // Java, Kotlin, C#, PHP all support /** */ block comments natively
1064
- widget = rawWidgetStr;
1065
- }
1066
-
1067
- if (opts.fix) {
1068
- // Actually inject
1069
- const newContent = widget + '\n\n' + content;
1070
- fs.writeFileSync(file, newContent, 'utf-8');
1071
- injected++;
1072
- if (!opts.json) {
1073
- console.log(` ${c('green', '✓')} ${path.relative(targetPath, file)}`);
1074
- }
1075
- } else {
1076
- previews.push({ file: path.relative(targetPath, file), widget });
1077
- injected++;
1078
- }
1079
- } catch (e) { skipped++; }
1080
- }
1081
-
1082
- if (opts.json) {
1083
- console.log(JSON.stringify({ injected, skipped, dryRun: !opts.fix, previews }, null, 2));
1084
- return;
1085
- }
1086
-
1087
- console.log('');
1088
- if (!opts.fix) {
1089
- console.log(c('yellow', ` DRY RUN — ${injected} files would be annotated. Use --fix to apply.`));
1090
- if (opts.verbose && previews.length > 0) {
1091
- console.log('');
1092
- for (const p of previews.slice(0, 5)) {
1093
- console.log(` ${c('cyan', p.file)}:`);
1094
- console.log(c('gray', p.widget.split('\n').map(l => ' ' + l).join('\n')));
1095
- console.log('');
1096
- }
1097
- }
1098
- } else {
1099
- console.log(c('green', ` ✓ Injected Mother Code into ${injected} files`));
1100
- }
1101
- console.log(` ${c('gray', `Skipped: ${skipped} (already annotated or unsupported)`)}`);
1102
- console.log('');
1103
- }
1104
-
1105
- async function cmdReport(opts) {
1106
- const startTime = Date.now();
1107
- const targetPath = path.resolve(opts.targetPath);
1108
-
1109
- if (!opts.json) {
1110
- banner();
1111
- console.log(c('bold', ' FULL REPORT: ') + c('cyan', targetPath));
1112
- console.log('');
1113
- }
1114
-
1115
- const files = collectFiles(targetPath, opts.ignore);
1116
-
1117
- // Run all analyses in parallel
1118
- const scanner = new CodeScanner({ rootPath: targetPath });
1119
- const graph = new DependencyGraph({ rootPath: targetPath });
1120
- const detector = new DriftDetector({ rootPath: targetPath });
1121
-
1122
- const [scanResults] = await Promise.all([
1123
- scanner.scanFiles(files),
1124
- graph.build(files),
1125
- ]);
1126
-
1127
- // Drift check
1128
- let driftCount = 0;
1129
- let annotated = 0;
1130
- for (const file of files) {
1131
- try {
1132
- const result = await detector.analyzeFile(file);
1133
- if (result && result.hasWidget) {
1134
- annotated++;
1135
- if (result.drifts && result.drifts.length > 0) driftCount++;
1136
- }
1137
- } catch (e) { /* skip */ }
1138
- }
1139
-
1140
- const elapsed = Date.now() - startTime;
1141
-
1142
- // Count issues
1143
- let totalIssues = 0;
1144
- let criticalCount = 0;
1145
- let highCount = 0;
1146
- for (const i of (scanResults.issues || [])) {
1147
- totalIssues++;
1148
- if (i.severity === 'critical') criticalCount++;
1149
- if (i.severity === 'high') highCount++;
1150
- }
1151
-
1152
- const orphans = graph.getOrphanFiles();
1153
- const circular = graph.circularDeps || [];
1154
- const coverage = files.length > 0 ? Math.round((annotated / files.length) * 100) : 0;
1155
-
1156
- // Health score
1157
- const score = Math.max(0, Math.round(
1158
- 100
1159
- - (criticalCount * 15)
1160
- - (highCount * 5)
1161
- - (circular.length * 10)
1162
- - (orphans.length * 1)
1163
- - ((100 - coverage) * 0.1)
1164
- - (driftCount * 3)
1165
- ));
1166
-
1167
- if (opts.json) {
1168
- console.log(JSON.stringify({
1169
- elapsed_ms: elapsed,
1170
- files: files.length,
1171
- health_score: score,
1172
- issues: { total: totalIssues, critical: criticalCount, high: highCount },
1173
- dependencies: { circular: circular.length, orphans: orphans.length },
1174
- mother_code: { annotated, coverage_pct: coverage, drifted: driftCount },
1175
- }, null, 2));
1176
- return;
1177
- }
1178
-
1179
- const scoreColor = score >= 80 ? 'green' : score >= 60 ? 'yellow' : 'red';
1180
-
1181
- console.log(c('bold', ' ╔══════════════════════════════════════╗'));
1182
- console.log(c('bold', ' ║ THUBAN HEALTH REPORT ║'));
1183
- console.log(c('bold', ' ╚══════════════════════════════════════╝'));
1184
- console.log('');
1185
- console.log(` ${c('bold', 'Health Score:')} ${c(scoreColor, score + '/100')}`);
1186
- console.log(` ${c('bold', 'Files Scanned:')} ${files.length}`);
1187
- console.log(` ${c('bold', 'Time:')} ${elapsed}ms`);
1188
- console.log('');
1189
- console.log(c('bold', ' ── Code Quality ──────────────────────'));
1190
- console.log(` Total issues: ${totalIssues}`);
1191
- console.log(` Critical: ${criticalCount > 0 ? c('red', criticalCount) : c('green', '0')}`);
1192
- console.log(` High: ${highCount > 0 ? c('red', highCount) : c('green', '0')}`);
1193
- console.log('');
1194
- console.log(c('bold', ' ── Dependencies ─────────────────────'));
1195
- console.log(` Circular deps: ${circular.length > 0 ? c('red', circular.length) : c('green', '0')}`);
1196
- console.log(` Orphan files: ${orphans.length > 0 ? c('yellow', orphans.length) : c('green', '0')}`);
1197
- console.log('');
1198
- console.log(c('bold', ' ── Mother Code ──────────────────────'));
1199
- console.log(` Annotated files: ${annotated}`);
1200
- console.log(` Coverage: ${c(coverage >= 80 ? 'green' : coverage >= 40 ? 'yellow' : 'red', coverage + '%')}`);
1201
- console.log(` Drifted files: ${driftCount > 0 ? c('yellow', driftCount) : c('green', '0')}`);
1202
- console.log('');
1203
-
1204
- // Grade
1205
- const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
1206
- const gradeColor = score >= 80 ? 'green' : score >= 60 ? 'yellow' : 'red';
1207
- console.log(` ${c('bold', 'Grade:')} ${c(gradeColor, grade)}`);
1208
- console.log('');
1209
-
1210
- if (score < 80) {
1211
- console.log(c('bold', ' RECOMMENDATIONS:'));
1212
- if (criticalCount > 0) console.log(` ${c('red', '→')} Fix ${criticalCount} critical issues immediately`);
1213
- if (circular.length > 0) console.log(` ${c('yellow', '→')} Resolve ${circular.length} circular dependencies`);
1214
- if (coverage < 50) console.log(` ${c('yellow', '→')} Run ${c('cyan', 'thuban inject --fix')} to improve Mother Code coverage`);
1215
- if (driftCount > 0) console.log(` ${c('yellow', '→')} ${driftCount} files have drifted from their annotations`);
1216
- if (orphans.length > 5) console.log(` ${c('gray', '→')} Consider removing ${orphans.length} orphan files`);
1217
- console.log('');
1218
- }
1219
-
1220
- // Clean up cloned repo (clonedRepo only exists in cmdScan scope)
1221
- }
1222
-
1223
- // ─── Tech Debt & Fix Commands ────────────────────────────
1224
-
1225
- async function cmdDebt(opts) {
1226
- const targetPath = path.resolve(opts.targetPath);
1227
- const files = collectFiles(targetPath, opts.ignore);
1228
-
1229
- if (!opts.json) {
1230
- banner();
1231
- console.log(c('bold', ' TECH DEBT ANALYSIS: ') + c('cyan', targetPath));
1232
- console.log(c('gray', ` Scanning ${files.length} files...`));
1233
- console.log('');
1234
- }
1235
-
1236
- const analyzer = new TechDebtAnalyzer({ rootPath: targetPath });
1237
- const debt = await analyzer.analyze(files);
1238
-
1239
- if (opts.json) {
1240
- console.log(JSON.stringify(analyzer.toJSON(debt), null, 2));
1241
- } else {
1242
- console.log(analyzer.formatReport(debt));
1243
- }
1244
- }
1245
-
1246
- async function cmdFix(opts) {
1247
- const targetPath = path.resolve(opts.targetPath);
1248
- const files = collectFiles(targetPath, opts.ignore);
1249
- const { execSync } = require('child_process');
1250
-
1251
- if (!opts.json) {
1252
- banner();
1253
- console.log(c('bold', ' AUTO-FIX TECH DEBT: ') + c('cyan', targetPath));
1254
- console.log('');
1255
- }
1256
-
1257
- const analyzer = new TechDebtAnalyzer({ rootPath: targetPath });
1258
-
1259
- // ─── DRY RUN is the default (Senate recommendation #1) ────
1260
- if (!opts.fix) {
1261
- const result = await analyzer.fix(files, { dryRun: true });
1262
-
1263
- if (!opts.json) {
1264
- console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
1265
- console.log(c('cyan', ' ║') + c('bold', ' DRY RUN — no files will be changed ') + c('cyan', '║'));
1266
- console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
1267
- console.log('');
1268
- console.log(` Fixable items: ${c('bold', result.totalFixable)}`);
1269
- console.log('');
1270
-
1271
- const grouped = {};
1272
- const safeActions = ['inject_widget', 'regenerate_widget', 'remove_console_log', 'flag_for_removal'];
1273
- const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api', 'wrap_localhost_url'];
1274
- let safeCount = 0;
1275
- let unsafeCount = 0;
1276
-
1277
- for (const f of result.fixes) {
1278
- const action = f.fixAction || 'unknown';
1279
- if (!grouped[action]) grouped[action] = 0;
1280
- grouped[action]++;
1281
- if (unsafeActions.includes(action)) unsafeCount++;
1282
- else safeCount++;
1283
- }
1284
-
1285
- console.log(c('green', c('bold', ` SAFE FIXES (${safeCount})`)) + c('gray', ' — no runtime behaviour change'));
1286
- for (const [action, count] of Object.entries(grouped).sort((a, b) => b[1] - a[1])) {
1287
- if (unsafeActions.includes(action)) continue;
1288
- const label = {
1289
- 'inject_widget': 'Inject Mother Code DNA annotations',
1290
- 'regenerate_widget': 'Update stale annotations',
1291
- 'remove_console_log': 'Remove debug console.logs',
1292
- 'flag_for_removal': 'Confirm orphan files for removal',
1293
- }[action] || action;
1294
- console.log(` ${c('green', '→')} ${label}: ${c('bold', count)}`);
1295
- }
1296
- console.log('');
1297
-
1298
- if (unsafeCount > 0) {
1299
- console.log(c('yellow', c('bold', ` UNSAFE FIXES (${unsafeCount})`)) + c('gray', ' — may change runtime behaviour'));
1300
- for (const [action, count] of Object.entries(grouped).sort((a, b) => b[1] - a[1])) {
1301
- if (!unsafeActions.includes(action)) continue;
1302
- const label = {
1303
- 'break_circular': 'Restructure circular dependencies',
1304
- 'extract_to_env': 'Extract hardcoded secrets to env vars',
1305
- 'update_deprecated_api': 'Replace deprecated API calls with modern equivalents',
1306
- 'wrap_localhost_url': 'Wrap hardcoded localhost URLs in env var fallback',
1307
- }[action] || action;
1308
- console.log(` ${c('yellow', '!')} ${label}: ${c('bold', count)}`);
1309
- }
1310
- console.log('');
1311
- console.log(c('gray', ' Unsafe fixes require --unsafe flag:'));
1312
- console.log(` ${c('cyan', 'thuban fix [path] --fix --unsafe')}`);
1313
- console.log('');
1314
- }
1315
-
1316
- console.log(c('bold', ' TO APPLY:'));
1317
- console.log(` ${c('cyan', 'thuban fix [path] --fix')} Apply safe fixes only`);
1318
- console.log(` ${c('cyan', 'thuban fix [path] --fix --unsafe')} Apply all fixes`);
1319
- console.log(` ${c('cyan', 'thuban fix [path] --fix --commit')} Each fix = separate git commit`);
1320
- console.log('');
1321
- } else {
1322
- console.log(JSON.stringify(result, null, 2));
1323
- }
1324
- return;
1325
- }
1326
-
1327
- // ─── ACTUAL FIX MODE ──────────────────────────────────────
1328
- if (!opts.json) {
1329
- if (opts.safe) {
1330
- console.log(c('green', ' SAFE MODE') + c('gray', ' — only non-runtime-changing fixes will be applied'));
1331
- } else {
1332
- console.log(c('yellow', ' UNSAFE MODE') + c('gray', ' — all fixes including runtime changes'));
1333
- }
1334
- console.log('');
1335
- }
1336
-
1337
- const result = await analyzer.fix(files, {
1338
- dryRun: false,
1339
- safeOnly: opts.safe,
1340
- });
1341
-
1342
- // ─── GIT COMMIT MODE (Senate recommendation: each fix = revertable commit) ─
1343
- if (opts.gitCommit && result.fixed > 0) {
1344
- try {
1345
- execSync('git rev-parse --git-dir', { cwd: targetPath, stdio: 'pipe' });
1346
- execSync('git add -A', { cwd: targetPath, stdio: 'pipe' });
1347
- const commitMsg = `fix(thuban): auto-fix ${result.fixed} issues [safe=${opts.safe}]`;
1348
- execSync(`git commit -m "${commitMsg}"`, { cwd: targetPath, stdio: 'pipe' });
1349
- if (!opts.json) {
1350
- console.log(c('green', ` ✓ Changes committed: "${commitMsg}"`));
1351
- console.log(c('gray', ' Revert with: git revert HEAD'));
1352
- console.log('');
1353
- }
1354
- } catch (e) {
1355
- if (!opts.json) {
1356
- console.log(c('yellow', ' ⚠ Could not auto-commit: ' + (e.message || 'not a git repo')));
1357
- console.log('');
1358
- }
1359
- }
1360
- }
1361
-
1362
- if (opts.json) {
1363
- console.log(JSON.stringify(result, null, 2));
1364
- } else {
1365
- console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
1366
- console.log(c('cyan', ' ║') + c('bold', ' FIX RESULTS ') + c('cyan', '║'));
1367
- console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
1368
- // Split annotations (inject_widget/regenerate_widget) from real code fixes
1369
- const annotationsAdded = (result.fixes || []).filter(f => f.status === 'fixed' && (f.fixAction === 'inject_widget' || f.fixAction === 'regenerate_widget')).length;
1370
- const issuesFixed = result.fixed - annotationsAdded;
1371
- if (annotationsAdded > 0) {
1372
- console.log(c('cyan', ' ║') + ` Annotations added: ${c('green', String(annotationsAdded).padEnd(19))}` + c('cyan', ' ║'));
1373
- }
1374
- if (issuesFixed > 0) {
1375
- console.log(c('cyan', ' ║') + ` Issues fixed: ${c('green', String(issuesFixed).padEnd(20))}` + c('cyan', ' ║'));
1376
- }
1377
- if (annotationsAdded === 0 && issuesFixed === 0) {
1378
- console.log(c('cyan', ' ║') + ` Fixed: ${c('green', String(result.fixed).padEnd(20))}` + c('cyan', ' ║'));
1379
- }
1380
- console.log(c('cyan', ' ║') + ` Flagged for review: ${c('yellow', String(result.flagged).padEnd(19))}` + c('cyan', ' ║'));
1381
- if (result.failed > 0) {
1382
- console.log(c('cyan', ' ║') + ` Failed: ${c('red', String(result.failed).padEnd(20))}` + c('cyan', ' ║'));
1383
- }
1384
- if (result.skipped > 0) {
1385
- console.log(c('cyan', ' ║') + ` Skipped: ${c('gray', String(result.skipped).padEnd(20))}` + c('cyan', ' ║'));
1386
- }
1387
- console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
1388
- console.log('');
1389
-
1390
- if (result.fixed > 0) {
1391
- console.log(c('green', ' Your files have been updated. Changes are saved to disk.'));
1392
- if (!opts.gitCommit) {
1393
- console.log(c('gray', ' Tip: use --commit next time to auto-commit each fix for easy rollback.'));
1394
- }
1395
- console.log('');
1396
- console.log(c('gray', ' Verify the changes:'));
1397
- console.log(c('gray', ' ') + c('cyan', 'npx thuban scan .') + c('gray', ' Re-scan to see your new score'));
1398
- console.log(c('gray', ' ') + c('cyan', 'npx thuban dashboard .') + c('gray', ' Generate updated report'));
1399
- console.log('');
1400
- }
1401
-
1402
- if (result.skipped > 0) {
1403
- console.log(c('gray', ` ${result.skipped} items were skipped (no auto-fix available or pattern not matched).`));
1404
- console.log(c('gray', ' Review them manually or run: ') + c('cyan', 'npx thuban dashboard .'));
1405
- console.log('');
1406
- }
1407
-
1408
- if (result.flagged > 0) {
1409
- console.log(c('gray', ` ${result.flagged} items need human review (secrets, orphan files, complex patterns).`));
1410
- console.log(c('gray', ' See details: ') + c('cyan', 'npx thuban dashboard .'));
1411
- console.log('');
1412
- }
1413
- }
1414
- }
1415
-
1416
- // ─── Main ───────────────────────────────────────────────────
1417
-
1418
- async function cmdHallucinate(opts) {
1419
- const targetPath = path.resolve(opts.targetPath);
1420
- const files = collectFiles(targetPath, opts.ignore);
1421
-
1422
- if (!opts.json) {
1423
- banner();
1424
- console.log(c('bold', ' HALLUCINATION SCAN: ') + c('cyan', targetPath));
1425
- console.log(c('gray', ` Scanning ${files.length} files for AI hallucinations...`));
1426
- console.log('');
1427
- }
1428
-
1429
- const detector = new HallucinationDetector({ rootPath: targetPath });
1430
- const results = await detector.scan(files);
1431
-
1432
- if (opts.json) {
1433
- console.log(JSON.stringify(results, null, 2));
1434
- } else {
1435
- console.log(detector.formatReport(results));
1436
- }
1437
- }
1438
-
1439
- async function cmdWatch(opts) {
1440
- const targetPath = path.resolve(opts.targetPath);
1441
-
1442
- const scanner = new CodeScanner({
1443
- rootPath: targetPath,
1444
- ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
1445
- });
1446
-
1447
- const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
1448
-
1449
- // Create a quick scan function for the watcher
1450
- const quickScan = async (filePath) => {
1451
- const issues = [];
1452
-
1453
- // Code scan
1454
- const scanIssues = await scanner.scanFile(filePath);
1455
- if (scanIssues) issues.push(...scanIssues);
1456
-
1457
- // Hallucination check
1458
- const hallResults = await hallucinationDetector.scan([filePath]);
1459
- if (hallResults.phantomAPIs.length > 0) {
1460
- for (const api of hallResults.phantomAPIs) {
1461
- issues.push({ severity: 'high', message: `Phantom API: ${api.name} — ${api.suggestion}`, line: api.line });
1462
- }
1463
- }
1464
- if (hallResults.phantomImports.length > 0) {
1465
- for (const imp of hallResults.phantomImports) {
1466
- issues.push({ severity: 'critical', message: `Phantom import: ${imp.module} — ${imp.reason}`, line: imp.line });
1467
- }
1468
- }
1469
- if (hallResults.aiSmells.length > 0) {
1470
- for (const smell of hallResults.aiSmells) {
1471
- issues.push({ severity: 'warning', message: `AI smell: ${smell.message}`, line: smell.line });
1472
- }
1473
- }
1474
-
1475
- return issues;
1476
- };
1477
-
1478
- const watcher = new FileWatcher({
1479
- rootPath: targetPath,
1480
- ignorePatterns: ['node_modules', '.git', 'dist', '.next', '__pycache__', ...opts.ignore],
1481
- });
1482
-
1483
- // Handle graceful shutdown
1484
- process.on('SIGINT', () => {
1485
- watcher.stop();
1486
- process.exit(0);
1487
- });
1488
-
1489
- watcher.start(quickScan);
1490
- }
1491
-
1492
- async function cmdDashboard(opts) {
1493
- const targetPath = path.resolve(opts.targetPath);
1494
- const files = collectFiles(targetPath, opts.ignore);
1495
-
1496
- if (!opts.json) {
1497
- banner();
1498
- console.log(c('bold', ' GENERATING DASHBOARD: ') + c('cyan', targetPath));
1499
- console.log(c('gray', ` Scanning ${files.length} files...`));
1500
- console.log('');
1501
- }
1502
-
1503
- // Run full analysis
1504
- const analyzer = new TechDebtAnalyzer({ rootPath: targetPath });
1505
- const debt = await analyzer.analyze(files);
1506
-
1507
- // Get dependency data
1508
- const graph = new DependencyGraph({ rootPath: targetPath });
1509
- await graph.build(files);
1510
- const depData = {
1511
- circular: graph.circularDeps || [],
1512
- orphans: graph.getOrphanFiles() || [],
1513
- critical: graph.getMostCriticalFiles(5) || [],
1514
- };
1515
-
1516
- // Run hallucination scan
1517
- const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
1518
- const hallResults = await hallucinationDetector.scan(files);
1519
-
1520
- // Generate HTML report
1521
- const reportGen = new HtmlReportGenerator({
1522
- rootPath: targetPath,
1523
- outputDir: targetPath,
1524
- });
1525
-
1526
- const projectName = path.basename(targetPath);
1527
- const outputPath = await reportGen.writeReport(debt, depData, {
1528
- projectName,
1529
- hallucinations: hallResults,
1530
- });
1531
-
1532
- if (!opts.json) {
1533
- console.log(c('green', ` ✓ Dashboard generated:`));
1534
- console.log(` ${c('cyan', outputPath)}`);
1535
- console.log('');
1536
- console.log(c('gray', ` Open in your browser to view the interactive report`));
1537
- console.log('');
1538
- } else {
1539
- console.log(JSON.stringify({ outputPath, scores: debt.scores, stats: debt.stats }, null, 2));
1540
- }
1541
- }
1542
-
1543
- async function cmdDiff(opts) {
1544
- const targetPath = path.resolve(opts.targetPath);
1545
- const { execSync } = require('child_process');
1546
-
1547
- if (!opts.json) {
1548
- banner();
1549
- console.log(c('bold', ' DIFF SCAN: ') + c('cyan', targetPath));
1550
- }
1551
-
1552
- // Get changed files from git
1553
- let changedFiles = [];
1554
- try {
1555
- // Files changed vs main/master branch
1556
- let baseBranch = 'main';
1557
- try {
1558
- execSync('git rev-parse --verify main', { cwd: targetPath, stdio: 'pipe' });
1559
- } catch {
1560
- try {
1561
- execSync('git rev-parse --verify master', { cwd: targetPath, stdio: 'pipe' });
1562
- baseBranch = 'master';
1563
- } catch {
1564
- // Fall back to HEAD~1
1565
- baseBranch = 'HEAD~1';
1566
- }
1567
- }
1568
-
1569
- const diffOutput = execSync(`git diff --name-only ${baseBranch}...HEAD`, {
1570
- cwd: targetPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
1571
- }).trim();
1572
-
1573
- // Also get uncommitted changes
1574
- const statusOutput = execSync('git diff --name-only HEAD', {
1575
- cwd: targetPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
1576
- }).trim();
1577
-
1578
- const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
1579
- cwd: targetPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
1580
- }).trim();
1581
-
1582
- const allChanged = new Set([
1583
- ...diffOutput.split('\n').filter(Boolean),
1584
- ...statusOutput.split('\n').filter(Boolean),
1585
- ...untrackedOutput.split('\n').filter(Boolean),
1586
- ]);
1587
-
1588
- // Resolve to full paths and filter to JS/TS files
1589
- for (const relFile of allChanged) {
1590
- const fullPath = path.resolve(targetPath, relFile);
1591
- const ext = path.extname(relFile).toLowerCase();
1592
- if (['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.json'].includes(ext)) {
1593
- if (fs.existsSync(fullPath)) {
1594
- changedFiles.push(fullPath);
1595
- }
1596
- }
1597
- }
1598
- } catch (e) {
1599
- if (!opts.json) {
1600
- console.log(c('red', ` ✗ Not a git repository or git error: ${e.message}`));
1601
- }
1602
- process.exit(1);
1603
- }
1604
-
1605
- if (changedFiles.length === 0) {
1606
- if (!opts.json) {
1607
- console.log(c('green', ' ✓ No changed files to scan'));
1608
- } else {
1609
- console.log(JSON.stringify({ changedFiles: 0, issues: 0 }));
1610
- }
1611
- return;
1612
- }
1613
-
1614
- if (!opts.json) {
1615
- console.log(c('gray', ` Scanning ${changedFiles.length} changed file${changedFiles.length === 1 ? '' : 's'}...\n`));
1616
- }
1617
-
1618
- // Run scans on changed files only
1619
- const scanner = new CodeScanner({ rootPath: targetPath });
1620
- const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
1621
-
1622
- const scanResults = await scanner.scanFiles(changedFiles);
1623
- const hallResults = await hallucinationDetector.scan(changedFiles);
1624
-
1625
- // Collect all issues
1626
- let totalIssues = 0;
1627
- const fileIssues = {};
1628
-
1629
- for (const issue of (scanResults.issues || [])) {
1630
- const relFile = path.relative(targetPath, issue.file);
1631
- if (!fileIssues[relFile]) fileIssues[relFile] = [];
1632
- fileIssues[relFile].push(issue);
1633
- totalIssues++;
1634
- }
1635
-
1636
- totalIssues += hallResults.stats.totalIssues;
1637
-
1638
- if (opts.json) {
1639
- console.log(JSON.stringify({
1640
- changedFiles: changedFiles.length,
1641
- totalIssues,
1642
- codeIssues: fileIssues,
1643
- hallucinations: hallResults,
1644
- }, null, 2));
1645
- } else {
1646
- // Display results per file
1647
- for (const [relFile, issues] of Object.entries(fileIssues)) {
1648
- console.log(` ${c('cyan', relFile)}`);
1649
- for (const issue of issues.slice(0, 5)) {
1650
- const sevColor = issue.severity === 'critical' || issue.severity === 'high' ? 'red' : issue.severity === 'warning' ? 'yellow' : 'gray';
1651
- console.log(` ${c(sevColor, '✗')} ${c('gray', issue.message || issue.id || '')}`);
1652
- }
1653
- if (issues.length > 5) {
1654
- console.log(` ${c('gray', `... and ${issues.length - 5} more`)}`);
1655
- }
1656
- console.log('');
1657
- }
1658
-
1659
- // Hallucination results
1660
- if (hallResults.stats.totalIssues > 0) {
1661
- console.log(hallucinationDetector.formatReport(hallResults));
1662
- }
1663
-
1664
- // Summary
1665
- const color = totalIssues === 0 ? 'green' : 'yellow';
1666
- console.log(` ${c(color, `${changedFiles.length} files changed, ${totalIssues} new issues introduced`)}\n`);
1667
- }
1668
- }
1669
-
1670
- // ─── License Commands ───────────────────────────────────────
1671
-
1672
- async function cmdActivate(opts) {
1673
- banner();
1674
- const lm = new LicenseManager();
1675
-
1676
- const key = opts.licenseKey;
1677
-
1678
- if (!key) {
1679
- console.log(c('red', ' Usage: thuban activate <license-key>'));
1680
- console.log('');
1681
- console.log(c('gray', ' Get a key at: ') + c('cyan', 'https://thuban.dev/pricing'));
1682
- console.log('');
1683
- return;
1684
- }
1685
-
1686
- const result = lm.activate(key);
1687
-
1688
- if (result.success) {
1689
- console.log(c('green', ' ╔══════════════════════════════════════╗'));
1690
- console.log(c('green', ' ║') + c('bold', ' License activated! ') + c('green', '║'));
1691
- console.log(c('green', ' ╚══════════════════════════════════════╝'));
1692
- console.log('');
1693
- console.log(` Tier: ${c('bold', result.tierName)}`);
1694
- console.log(` Status: ${c('green', 'Active')}`);
1695
- console.log('');
1696
- console.log(c('gray', ' All features unlocked. Run ') + c('cyan', 'thuban scan .') + c('gray', ' to get started.'));
1697
- console.log('');
1698
- } else {
1699
- console.log(c('red', ' Invalid license key.'));
1700
- console.log('');
1701
- console.log(c('gray', ' Check for typos or get a new key at: ') + c('cyan', 'https://thuban.dev/pricing'));
1702
- console.log('');
1703
- }
1704
- }
1705
-
1706
- async function cmdStatus(opts) {
1707
- banner();
1708
- const lm = new LicenseManager();
1709
- const status = lm.getStatus();
1710
-
1711
- console.log(c('bold', ' LICENSE STATUS'));
1712
- console.log('');
1713
- console.log(` Tier: ${c('bold', status.tierName)}${status.licensed ? c('green', ' (active)') : ''}`);
1714
- console.log(` Machine ID: ${c('gray', status.machineId)}`);
1715
- console.log(` Scans this month: ${c('bold', status.scansUsed)}${status.scansLimit < Infinity ? c('gray', ' / ' + status.scansLimit) : ''}`);
1716
- console.log(` Lifetime scans: ${c('gray', status.totalScans)}`);
1717
- console.log('');
1718
- console.log(c('bold', ' FEATURES'));
1719
- console.log(` Full scan details: ${status.features.fullDetails ? c('green', 'Yes') : c('red', 'No')}`);
1720
- console.log(` HTML dashboard: ${status.features.dashboard ? c('green', 'Yes') : c('red', 'No')}`);
1721
- console.log(` Auto-fix: ${status.features.autoFix ? c('green', 'Yes') : c('red', 'No')}`);
1722
- console.log(` CI Action: ${status.features.ciAction ? c('green', 'Yes') : c('red', 'No')}`);
1723
- console.log('');
1724
-
1725
- if (!status.licensed) {
1726
- console.log(c('cyan', ' Upgrade at: ') + c('bold', 'https://thuban.dev/pricing'));
1727
- console.log('');
1728
- }
1729
- }
1730
-
1731
- function cmdUpgrade() {
1732
- banner();
1733
- console.log(c('bold', ' THUBAN PRICING'));
1734
- console.log('');
1735
- console.log(` ${c('gray', 'FREE')} ${c('bold', '£0/mo')}`);
1736
- console.log(` 5 scans/month, 100 files, summary only`);
1737
- console.log('');
1738
- console.log(` ${c('cyan', 'PRO')} ${c('bold', '£19/mo')} ${c('gray', '(or £149/year — save 35%)')}`);
1739
- console.log(` Unlimited scans, full details, auto-fix, HTML dashboard`);
1740
- console.log('');
1741
- console.log(` ${c('cyan', 'TEAM')} ${c('bold', '£99/mo')}`);
1742
- console.log(` Everything in Pro + 5 seats, CI Action, team reports`);
1743
- console.log('');
1744
- console.log(` ${c('cyan', 'ENTERPRISE')} ${c('bold', '£299/mo')}`);
1745
- console.log(` Everything in Team + unlimited seats, priority support, SLA`);
1746
- console.log('');
1747
- console.log(c('bold', ' Subscribe at: ') + c('cyan', 'https://thuban.dev/pricing'));
1748
- console.log('');
1749
-
1750
- // Try to open the URL
1751
- try {
1752
- const { exec } = require('child_process');
1753
- const platform = process.platform;
1754
- const cmd = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
1755
- exec(`${cmd} https://thuban.dev/pricing`);
1756
- } catch (e) { /* non-fatal */ }
1757
- }
1758
-
1759
- // ─── New Feature Commands ───────────────────────────────────
1760
-
1761
- async function cmdGate(opts) {
1762
- const targetPath = path.resolve(opts.targetPath);
1763
-
1764
- if (opts.fix) {
1765
- // --fix = install the hook
1766
- const result = PreCommitGate.install(targetPath);
1767
- banner();
1768
- if (result.success) {
1769
- console.log(c('green', ' ✓ Pre-commit gate installed!'));
1770
- console.log('');
1771
- console.log(c('gray', ' Every commit will now be checked for hallucinated APIs.'));
1772
- console.log(c('gray', ' Commits with phantom APIs will be blocked automatically.'));
1773
- console.log('');
1774
- console.log(` To remove: ${c('cyan', 'thuban gate --uninstall')}`);
1775
- } else {
1776
- console.log(c('red', ` Error: ${result.error}`));
1777
- }
1778
- console.log('');
1779
- return;
1780
- }
1781
-
1782
- if (process.argv.includes('--uninstall')) {
1783
- const result = PreCommitGate.uninstall(targetPath);
1784
- banner();
1785
- console.log(c('green', ` ✓ ${result.message}`));
1786
- console.log('');
1787
- return;
1788
- }
1789
-
1790
- // Run the gate check (used by the pre-commit hook and manual runs)
1791
- const gate = new PreCommitGate({ rootPath: targetPath, strict: !process.argv.includes('--warn') });
1792
- const result = gate.run();
1793
-
1794
- if (opts.json) {
1795
- console.log(JSON.stringify(result, null, 2));
1796
- return;
1797
- }
1798
-
1799
- const isCi = process.argv.includes('--ci');
1800
-
1801
- if (!isCi) banner();
1802
-
1803
- if (result.files === 0) {
1804
- console.log(c('gray', ' No staged files to check.'));
1805
- console.log('');
1806
- console.log(` Install the gate: ${c('cyan', 'thuban gate --fix')}`);
1807
- console.log('');
1808
- return;
1809
- }
1810
-
1811
- if (result.passed) {
1812
- console.log(c('green', ` ✓ ${result.message}`));
1813
- console.log(c('gray', ` ${result.elapsed}ms`));
1814
- if (!isCi) console.log('');
1815
- process.exit(0);
1816
- } else {
1817
- console.log(c('red', ` ✗ ${result.message}`));
1818
- console.log('');
1819
- for (const issue of result.issues) {
1820
- console.log(` ${c('red', issue.file)}:${issue.line}`);
1821
- console.log(` ${c('yellow', issue.code)}`);
1822
- console.log(` ${c('red', '^^^')} ${issue.message}`);
1823
- if (issue.fix) console.log(` ${c('green', 'Fix:')} ${issue.fix}`);
1824
- console.log('');
1825
- }
1826
- process.exit(1);
1827
- }
1828
- }
1829
-
1830
- async function cmdCost(opts) {
1831
- const targetPath = path.resolve(opts.targetPath);
1832
- banner();
1833
- console.log(c('bold', ' SCANNING: ') + c('cyan', targetPath));
1834
- console.log('');
1835
-
1836
- const files = collectFiles(targetPath, opts.ignore);
1837
- const scanner = new CodeScanner({
1838
- rootPath: targetPath,
1839
- ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
1840
- });
1841
- const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
1842
-
1843
- const [scanResults, hallResults] = await Promise.all([
1844
- scanner.scanFiles(files),
1845
- hallucinationDetector.scan(files),
1846
- ]);
1847
-
1848
- // Collect all issues
1849
- const allIssues = [];
1850
- for (const issue of (scanResults.issues || [])) {
1851
- allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
1852
- }
1853
- for (const item of hallResults.phantomAPIs) {
1854
- allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
1855
- }
1856
- for (const item of hallResults.deprecatedAPIs) {
1857
- allIssues.push({ category: 'deprecated', severity: 'high', message: item.name });
1858
- }
1859
-
1860
- const calculator = new TechDebtCostCalculator();
1861
- const result = calculator.calculate(allIssues, files.length);
1862
-
1863
- if (opts.json) {
1864
- console.log(JSON.stringify(result, null, 2));
1865
- return;
1866
- }
1867
-
1868
- console.log(calculator.formatCLI(result));
1869
- }
1870
-
1871
- async function cmdGhosts(opts) {
1872
- const targetPath = path.resolve(opts.targetPath);
1873
- banner();
1874
- console.log(c('bold', ' SCANNING FOR GHOST CODE: ') + c('cyan', targetPath));
1875
- console.log('');
1876
-
1877
- const files = collectFiles(targetPath, opts.ignore);
1878
- const detector = new GhostCodeDetector({ rootPath: targetPath });
1879
- const result = detector.scan(files);
1880
-
1881
- if (opts.json) {
1882
- console.log(JSON.stringify(result, null, 2));
1883
- return;
1884
- }
1885
-
1886
- if (result.ghosts.length === 0) {
1887
- console.log(c('green', ' No ghost code detected. Every function is referenced.'));
1888
- console.log('');
1889
- return;
1890
- }
1891
-
1892
- console.log(c('yellow', c('bold', ` GHOST CODE: ${result.totalGhosts} function${result.totalGhosts > 1 ? 's' : ''} exist but are never called`)));
1893
- console.log('');
1894
-
1895
- for (const ghost of result.ghosts.slice(0, opts.maxIssues)) {
1896
- const sevColor = ghost.severity === 'high' ? 'red' : 'yellow';
1897
- console.log(` ${c(sevColor, '⊘')} ${c('cyan', ghost.file)}:${ghost.line}`);
1898
- console.log(` ${c('bold', ghost.name + '()')} — ${ghost.wastedLines} lines, ${ghost.references} references`);
1899
- console.log('');
1900
- }
1901
-
1902
- console.log(c('bold', ' ┌──────────────────────────────────────────────────────┐'));
1903
- console.log(c('bold', ' │') + ` Wasted code: ${c('red', result.totalWastedLines + ' lines')} (${result.wastedPercent}% of codebase) ` + c('bold', '│'));
1904
- console.log(c('bold', ' └──────────────────────────────────────────────────────┘'));
1905
- console.log('');
1906
- }
1907
-
1908
- async function cmdAIScore(opts) {
1909
- const targetPath = path.resolve(opts.targetPath);
1910
- banner();
1911
- console.log(c('bold', ' AI CONFIDENCE SCORING: ') + c('cyan', targetPath));
1912
- console.log('');
1913
-
1914
- const files = collectFiles(targetPath, opts.ignore);
1915
- const scorer = new AIConfidenceScorer({ rootPath: targetPath });
1916
- const result = scorer.scan(files);
1917
-
1918
- if (opts.json) {
1919
- console.log(JSON.stringify(result, null, 2));
1920
- return;
1921
- }
1922
-
1923
- if (result.highConfidence.length === 0 && result.mediumConfidence.length === 0) {
1924
- console.log(c('green', ' No AI-generated code patterns detected.'));
1925
- console.log('');
1926
- return;
1927
- }
1928
-
1929
- console.log(c('red', c('bold', ` HIGH CONFIDENCE AI-GENERATED (${result.highConfidence.length} functions)`)));
1930
- console.log('');
1931
-
1932
- for (const fn of result.highConfidence.slice(0, 15)) {
1933
- console.log(` ${c('red', fn.confidence + '%')} AI ${c('cyan', fn.file)}:${fn.line}`);
1934
- console.log(` ${c('bold', fn.name + '()')} — ${fn.lineCount} lines | Test coverage: ${fn.testCoverage}`);
1935
- for (const signal of fn.signals.slice(0, 2)) {
1936
- console.log(` ${c('gray', '• ' + signal)}`);
1937
- }
1938
- console.log('');
1939
- }
1940
-
1941
- if (result.mediumConfidence.length > 0) {
1942
- console.log(c('yellow', c('bold', ` POSSIBLE AI-GENERATED (${result.mediumConfidence.length} functions)`)));
1943
- console.log('');
1944
- for (const fn of result.mediumConfidence.slice(0, 10)) {
1945
- console.log(` ${c('yellow', fn.confidence + '%')} AI ${c('cyan', fn.file)}:${fn.line} ${c('gray', fn.name + '()')}`);
1946
- }
1947
- console.log('');
1948
- }
1949
-
1950
- console.log(c('bold', ' ┌──────────────────────────────────────────────────────┐'));
1951
- console.log(c('bold', ' │') + ` ${c('red', result.aiPercentage + '%')} of functions are likely AI-generated ` + c('bold', '│'));
1952
- console.log(c('bold', ' │') + ` ${c('yellow', result.aiLikelyCount)} high confidence | ${result.aiPossibleCount} possible ` + c('bold', '│'));
1953
- console.log(c('bold', ' └──────────────────────────────────────────────────────┘'));
1954
- console.log('');
1955
- }
1956
-
1957
- async function cmdClones(opts) {
1958
- const targetPath = path.resolve(opts.targetPath);
1959
- banner();
1960
- console.log(c('bold', ' COPY-PASTE DRIFT DETECTION: ') + c('cyan', targetPath));
1961
- console.log('');
1962
-
1963
- const files = collectFiles(targetPath, opts.ignore);
1964
- const detector = new CopyPasteDriftDetector({ rootPath: targetPath });
1965
- const result = detector.scan(files);
1966
-
1967
- if (opts.json) {
1968
- console.log(JSON.stringify(result, null, 2));
1969
- return;
1970
- }
1971
-
1972
- if (result.clusters.length === 0) {
1973
- console.log(c('green', ' No copy-paste drift detected.'));
1974
- console.log('');
1975
- return;
1976
- }
1977
-
1978
- console.log(c('yellow', c('bold', ` COPY-PASTE CLUSTERS: ${result.totalClusters} found`)));
1979
- console.log('');
1980
-
1981
- for (const cluster of result.clusters.slice(0, 10)) {
1982
- const sevColor = cluster.severity === 'high' ? 'red' : 'yellow';
1983
- console.log(` ${c(sevColor, '◈')} ${c('bold', cluster.pattern + ' pattern')} — ${cluster.totalInstances} instances`);
1984
- for (const member of cluster.members) {
1985
- const sim = member.similarity < 100 ? c('gray', ` (${member.similarity}% similar)`) : '';
1986
- console.log(` ${c('cyan', member.file)}:${member.line} ${c('gray', member.name + '()')}${sim}`);
1987
- }
1988
- console.log(` ${c('green', '→')} ${cluster.recommendation}`);
1989
- console.log('');
1990
- }
1991
-
1992
- console.log(c('bold', ` Total duplicate lines: ${c('red', result.totalDuplicateLines)}`));
1993
- console.log(c('gray', ` ${result.recommendation}`));
1994
- console.log('');
1995
- }
1996
-
1997
- async function cmdPassport(opts) {
1998
- const targetPath = path.resolve(opts.targetPath);
1999
- banner();
2000
- console.log(c('bold', ' GENERATING CODEBASE PASSPORT: ') + c('cyan', targetPath));
2001
- console.log('');
2002
-
2003
- const files = collectFiles(targetPath, opts.ignore);
2004
- const generator = new CodebasePassport({ rootPath: targetPath });
2005
- const passport = generator.generate(files);
2006
-
2007
- if (opts.json) {
2008
- console.log(JSON.stringify(passport, null, 2));
2009
- return;
2010
- }
2011
-
2012
- const outputPath = generator.save(passport);
2013
- const p = passport.project;
2014
- const o = passport.onboarding;
2015
-
2016
- console.log(c('green', ` ✓ Passport generated: ${c('bold', path.relative(targetPath, outputPath))}`));
2017
- console.log('');
2018
- console.log(c('bold', ' PROJECT IDENTITY'));
2019
- console.log(` Name: ${c('cyan', p.name)}`);
2020
- console.log(` Files: ${c('bold', p.totalFiles)}`);
2021
- console.log(` Lines: ${c('bold', p.totalLines.toLocaleString())}`);
2022
- console.log(` Architecture: ${c('bold', p.architecture)}`);
2023
- console.log(` Frameworks: ${p.frameworks.join(', ') || 'vanilla'}`);
2024
- console.log('');
2025
- console.log(c('bold', ' LANGUAGES'));
2026
- for (const lang of p.languages.slice(0, 5)) {
2027
- const bar = '█'.repeat(Math.max(1, Math.round(lang.percentage / 5)));
2028
- console.log(` ${lang.extension.padEnd(6)} ${c('cyan', bar)} ${lang.percentage}% (${lang.lines} lines)`);
2029
- }
2030
- console.log('');
2031
- console.log(c('bold', ' ONBOARDING'));
2032
- console.log(` Read time: ~${o.estimatedReadTimeMinutes} minutes`);
2033
- console.log(` Start here: ${o.startHere.slice(0, 3).join(', ')}`);
2034
- if (o.doNotTouch.length > 0) {
2035
- console.log(` Do not touch: ${c('red', o.doNotTouch.join(', '))}`);
2036
- }
2037
- console.log(` Summary: ${o.quickSummary}`);
2038
- console.log('');
2039
-
2040
- if (passport.sacred.length > 0) {
2041
- console.log(c('bold', ' SACRED FILES (high impact if changed)'));
2042
- for (const s of passport.sacred.slice(0, 5)) {
2043
- console.log(` ${c('red', '!')} ${c('cyan', s.file)} — imported by ${s.importedBy} files`);
2044
- }
2045
- console.log('');
2046
- }
2047
-
2048
- console.log(c('gray', ` Full passport: ${outputPath}`));
2049
- console.log(c('gray', ` Share with new team members for instant onboarding.`));
2050
- console.log('');
2051
- }
2052
-
2053
- async function cmdExecutive(opts) {
2054
- const targetPath = path.resolve(opts.targetPath);
2055
- banner();
2056
- console.log(c('bold', ' GENERATING EXECUTIVE REPORT: ') + c('cyan', targetPath));
2057
- console.log('');
2058
-
2059
- const files = collectFiles(targetPath, opts.ignore);
2060
-
2061
- console.log(c('gray', ` Scanning ${files.length} files...`));
2062
-
2063
- const scanner = new CodeScanner({
2064
- rootPath: targetPath,
2065
- ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
2066
- });
2067
- const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
2068
- const ghostDetector = new GhostCodeDetector({ rootPath: targetPath });
2069
- const aiScorer = new AIConfidenceScorer({ rootPath: targetPath });
2070
- const cloneDetector = new CopyPasteDriftDetector({ rootPath: targetPath });
2071
-
2072
- // Run all scans in parallel
2073
- const [scanResults, hallResults, ghostResults, aiScoreResults, cloneResults] = await Promise.all([
2074
- scanner.scanFiles(files),
2075
- hallucinationDetector.scan(files),
2076
- ghostDetector.scan(files),
2077
- aiScorer.scan(files),
2078
- cloneDetector.scan(files),
2079
- ]);
2080
-
2081
- // Calculate total lines
2082
- let lineCount = 0;
2083
- for (const file of files) {
2084
- try {
2085
- const content = fs.readFileSync(file, 'utf-8');
2086
- lineCount += content.split('\n').length;
2087
- } catch { /* skip */ }
2088
- }
2089
-
2090
- // Collect all issues for cost calculator
2091
- const allIssues = [];
2092
- for (const issue of (scanResults.issues || [])) {
2093
- allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
2094
- }
2095
- for (const item of hallResults.phantomAPIs) {
2096
- allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
2097
- }
2098
- for (const item of hallResults.deprecatedAPIs) {
2099
- allIssues.push({ category: 'deprecated', severity: 'high', message: item.name });
2100
- }
2101
-
2102
- const costCalc = new TechDebtCostCalculator();
2103
- const debtCost = costCalc.calculate(allIssues, files.length);
2104
-
2105
- // Generate report
2106
- const report = new ExecutiveReport({
2107
- rootPath: targetPath,
2108
- companyName: opts.companyName || undefined,
2109
- });
2110
-
2111
- const html = report.generate({
2112
- scanResults,
2113
- hallResults,
2114
- debtCost,
2115
- ghostResults,
2116
- aiScoreResults,
2117
- cloneResults,
2118
- fileCount: files.length,
2119
- lineCount,
2120
- });
2121
-
2122
- const outputPath = report.save(html, targetPath);
2123
-
2124
- console.log(c('green', ` ✓ Executive report generated!`));
2125
- console.log('');
2126
- console.log(` ${c('bold', 'Report:')} ${c('cyan', outputPath)}`);
2127
- console.log('');
2128
- console.log(c('gray', ' Open in browser → Ctrl+P → Save as PDF'));
2129
- console.log(c('gray', ' Or click the "Save as PDF" button in the report.'));
2130
- console.log('');
2131
-
2132
- // Try to open in browser
2133
- try {
2134
- const { exec } = require('child_process');
2135
- const platform = process.platform;
2136
- const cmd = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
2137
- exec(`${cmd} "${outputPath}"`);
2138
- console.log(c('green', ' ✓ Opened in your default browser.'));
2139
- console.log('');
2140
- } catch (e) { /* non-fatal */ }
2141
- }
2142
-
2143
- async function cmdBaseline(opts) {
2144
- const targetPath = path.resolve(opts.targetPath);
2145
- const baseline = new BaselineManager(targetPath);
2146
-
2147
- banner();
2148
-
2149
- if (opts.baselineCreate || !baseline.exists()) {
2150
- // Create baseline from current scan
2151
- console.log(c('bold', ' CREATING BASELINE: ') + c('cyan', targetPath));
2152
- console.log('');
2153
-
2154
- const files = collectFiles(targetPath, opts.ignore);
2155
- const scanner = new CodeScanner({
2156
- rootPath: targetPath,
2157
- ignorePatterns: ['node_modules/**', '.git/**', 'dist/**', ...opts.ignore],
2158
- });
2159
- const hallucinationDetector = new HallucinationDetector({ rootPath: targetPath });
2160
-
2161
- const [scanResults, hallResults] = await Promise.all([
2162
- scanner.scanFiles(files),
2163
- hallucinationDetector.scan(files),
2164
- ]);
2165
-
2166
- const allIssues = [];
2167
- for (const issue of (scanResults.issues || [])) {
2168
- allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
2169
- }
2170
- for (const item of hallResults.phantomAPIs) {
2171
- allIssues.push({ file: item.file, category: 'hallucination', severity: 'critical', id: item.id, message: item.name });
2172
- }
2173
- for (const item of hallResults.deprecatedAPIs) {
2174
- allIssues.push({ file: item.file, category: 'deprecated', severity: 'high', id: item.id, message: item.name });
2175
- }
2176
- for (const item of hallResults.aiSmells) {
2177
- allIssues.push({ file: item.file, category: 'ai_smell', severity: 'warning', id: item.id, message: item.message });
2178
- }
2179
-
2180
- const result = baseline.create(allIssues);
2181
- console.log(c('green', ` ✓ Baseline created: ${c('bold', result.count)} known issues recorded`));
2182
- console.log(` ${c('cyan', result.path)}`);
2183
- console.log('');
2184
- console.log(c('gray', ' Future scans with --baseline will only report NEW issues.'));
2185
- console.log(c('gray', ' Commit .thuban-baseline.json to share baseline with your team.'));
2186
- console.log('');
2187
- } else {
2188
- // Show baseline status
2189
- const existing = baseline.load();
2190
- console.log(c('bold', ' BASELINE STATUS'));
2191
- console.log('');
2192
- console.log(` Created: ${c('cyan', existing.created)}`);
2193
- console.log(` Known issues: ${c('bold', existing.issueCount)}`);
2194
- console.log('');
2195
- console.log(c('gray', ' Use --baseline-create to update the baseline'));
2196
- console.log(c('gray', ' Use --baseline with scan to only see new issues'));
2197
- console.log('');
2198
- }
2199
- }
2200
-
2201
- // ─── Telemetry (opt-in anonymous usage stats) ───────────────
2202
-
2203
- function getTelemetryConfigPath() {
2204
- const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
2205
- return path.join(home, '.thuban-telemetry.json');
2206
- }
2207
-
2208
- function isTelemetryEnabled() {
2209
- try {
2210
- const config = JSON.parse(fs.readFileSync(getTelemetryConfigPath(), 'utf-8'));
2211
- return config.enabled === true;
2212
- } catch { return false; }
2213
- }
2214
-
2215
- function sendAnonymousTelemetry(data) {
2216
- if (!isTelemetryEnabled()) return;
2217
- try {
2218
- const payload = JSON.stringify({
2219
- v: data.version || '0.3.1',
2220
- cmd: data.command,
2221
- files: data.fileCount,
2222
- score: data.score,
2223
- grade: data.grade,
2224
- langs: data.languages,
2225
- issues: data.issueCount,
2226
- duration: data.duration,
2227
- os: process.platform,
2228
- node: process.version,
2229
- ts: new Date().toISOString()
2230
- });
2231
- // Fire and forget — never blocks the CLI
2232
- const https = require('https');
2233
- const req = https.request({
2234
- hostname: 'thuban-telemetry.silverwingsbenefits.workers.dev',
2235
- path: '/report',
2236
- method: 'POST',
2237
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
2238
- timeout: 3000
2239
- });
2240
- req.on('error', () => {}); // silently ignore
2241
- req.write(payload);
2242
- req.end();
2243
- } catch { /* never fail the CLI for telemetry */ }
2244
- }
2245
-
2246
- async function cmdTelemetry(opts) {
2247
- const configPath = getTelemetryConfigPath();
2248
- const args = process.argv.slice(2);
2249
-
2250
- if (args.includes('--on')) {
2251
- fs.writeFileSync(configPath, JSON.stringify({ enabled: true, updated: new Date().toISOString() }));
2252
- console.log('');
2253
- console.log(c('cyan', ' Telemetry enabled'));
2254
- console.log(c('gray', ' Anonymous usage stats will be sent (no code, no paths, no PII)'));
2255
- console.log(c('gray', ' Turn off anytime: npx thuban telemetry --off'));
2256
- console.log('');
2257
- } else if (args.includes('--off')) {
2258
- fs.writeFileSync(configPath, JSON.stringify({ enabled: false, updated: new Date().toISOString() }));
2259
- console.log('');
2260
- console.log(c('cyan', ' Telemetry disabled'));
2261
- console.log(c('gray', ' No data will be collected'));
2262
- console.log('');
2263
- } else {
2264
- const enabled = isTelemetryEnabled();
2265
- console.log('');
2266
- console.log(c('bold', ' TELEMETRY STATUS'));
2267
- console.log('');
2268
- console.log(` Status: ${enabled ? c('cyan', 'Enabled') : c('gray', 'Disabled (default)')}`);
2269
- console.log('');
2270
- console.log(c('gray', ' Thuban telemetry is 100% opt-in and anonymous.'));
2271
- console.log(c('gray', ' We collect: file count, score, grade, languages, OS, scan duration.'));
2272
- console.log(c('gray', ' We never collect: code, file names, paths, project names, or PII.'));
2273
- console.log('');
2274
- console.log(` Enable: ${c('cyan', 'npx thuban telemetry --on')}`);
2275
- console.log(` Disable: ${c('cyan', 'npx thuban telemetry --off')}`);
2276
- console.log('');
2277
- }
2278
- }
2279
-
2280
- // ─── Internal scan (returns data without printing) ──────────
2281
-
2282
- async function runScanSilent(targetInput) {
2283
- const gitUrlPattern = /^(https?:\/\/|git@)/;
2284
- const isGitUrl = gitUrlPattern.test(targetInput);
2285
- let targetPath = isGitUrl ? targetInput : path.resolve(targetInput);
2286
- let clonedRepo = null;
2287
- let label = targetInput;
2288
-
2289
- if (isGitUrl) {
2290
- const tmpDir = path.join(os.tmpdir(), 'thuban-compare-' + Date.now() + '-' + Math.random().toString(36).slice(2,6));
2291
- const { execFileSync } = require('child_process');
2292
- execFileSync('git', ['clone', '--depth', '1', targetInput, tmpDir], { stdio: 'pipe', timeout: 60000 });
2293
- targetPath = tmpDir;
2294
- clonedRepo = tmpDir;
2295
- // Extract repo name for label
2296
- const parts = targetInput.replace(/\.git$/, '').split('/');
2297
- label = parts[parts.length - 1] || targetInput;
2298
- } else {
2299
- label = path.basename(path.resolve(targetInput)) || targetInput;
2300
- }
2301
-
2302
- const startTime = Date.now();
2303
- const scanner = new CodeScanner();
2304
- const graph = new DependencyGraph();
2305
- const hallDetector = new HallucinationDetector({ rootPath: targetPath });
2306
-
2307
- const files = collectFiles(targetPath, []);
2308
- const scanResults = {};
2309
- let annotated = 0;
2310
- let driftCount = 0;
2311
-
2312
- for (const file of files) {
2313
- try {
2314
- const result = scanner.scanFile(file);
2315
- const relPath = path.relative(targetPath, file);
2316
- scanResults[file] = result;
2317
- graph.addFile(file, targetPath);
2318
- if (result.motherCode && result.motherCode.hasDNA) annotated++;
2319
- if (result.drifts && result.drifts.length > 0) driftCount++;
2320
- } catch (e) { /* skip */ }
2321
- }
2322
-
2323
- // Run hallucination detection (same as main scan)
2324
- const hallResults = hallDetector.scan(files, targetPath);
2325
-
2326
- const elapsed = Date.now() - startTime;
2327
- let totalIssues = 0, criticalCount = 0, highCount = 0;
2328
- const issueByCat = {};
2329
-
2330
- // Count code scanner issues
2331
- for (const [, r] of Object.entries(scanResults)) {
2332
- if (r.issues) {
2333
- totalIssues += r.issues.length;
2334
- for (const iss of r.issues) {
2335
- if (iss.severity === 'critical') criticalCount++;
2336
- if (iss.severity === 'high') highCount++;
2337
- const cat = iss.category || iss.type || 'other';
2338
- issueByCat[cat] = (issueByCat[cat] || 0) + 1;
2339
- }
2340
- }
2341
- }
2342
-
2343
- // Count hallucination issues
2344
- const hallIssueCount = (hallResults.phantomAPIs || []).length
2345
- + (hallResults.phantomImports || []).length
2346
- + (hallResults.deprecatedAPIs || []).length;
2347
- totalIssues += hallIssueCount;
2348
- criticalCount += (hallResults.phantomAPIs || []).length + (hallResults.phantomImports || []).length;
2349
- highCount += (hallResults.deprecatedAPIs || []).length;
2350
- if (hallIssueCount > 0) issueByCat['hallucination'] = hallIssueCount;
2351
-
2352
- const orphans = graph.getOrphanFiles();
2353
- const circular = graph.circularDeps || [];
2354
- const coverage = files.length > 0 ? Math.round((annotated / files.length) * 100) : 0;
2355
-
2356
- const score = Math.max(0, Math.round(
2357
- 100 - (criticalCount * 15) - (highCount * 5) - (circular.length * 10)
2358
- - (orphans.length * 1) - ((100 - coverage) * 0.1) - (driftCount * 3)
2359
- ));
2360
-
2361
- const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
2362
-
2363
- if (clonedRepo) {
2364
- try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
2365
- }
2366
-
2367
- return {
2368
- label,
2369
- files: files.length,
2370
- score,
2371
- grade,
2372
- elapsed,
2373
- issues: { total: totalIssues, critical: criticalCount, high: highCount },
2374
- categories: issueByCat,
2375
- dependencies: { circular: circular.length, orphans: orphans.length },
2376
- motherCode: { annotated, coverage, drifted: driftCount }
2377
- };
2378
- }
2379
-
2380
- // ─── Compare Command ────────────────────────────────────────
2381
-
2382
- async function cmdCompare(opts) {
2383
- const args = process.argv.slice(2);
2384
- // Find the two paths after 'compare'
2385
- const paths = args.filter(a => a !== 'compare' && !a.startsWith('--'));
2386
-
2387
- if (paths.length < 2) {
2388
- console.log('');
2389
- console.log(c('red', ' Compare requires two paths or URLs'));
2390
- console.log('');
2391
- console.log(c('gray', ' Usage:'));
2392
- console.log(` ${c('cyan', 'npx thuban compare ./project-a ./project-b')}`);
2393
- console.log(` ${c('cyan', 'npx thuban compare . https://github.com/user/repo')}`);
2394
- console.log(` ${c('cyan', 'npx thuban compare https://github.com/a/repo https://github.com/b/repo')}`);
2395
- console.log('');
2396
- process.exit(1);
2397
- }
2398
-
2399
- console.log('');
2400
- console.log(c('bold', ' ╔══════════════════════════════════════╗'));
2401
- console.log(c('bold', ' ║ THUBAN — Codebase Comparison ║'));
2402
- console.log(c('bold', ' ╚══════════════════════════════════════╝'));
2403
- console.log('');
2404
-
2405
- console.log(c('cyan', ` Scanning: ${paths[0]}`));
2406
- const resultA = await runScanSilent(paths[0]);
2407
- console.log(c('cyan', ` Scanning: ${paths[1]}`));
2408
- const resultB = await runScanSilent(paths[1]);
2409
-
2410
- console.log('');
2411
-
2412
- // ─── Side by side comparison ────────────────────────────
2413
- const pad = (str, len) => String(str).padEnd(len);
2414
- const padL = (str, len) => String(str).padStart(len);
2415
- const colW = 20;
2416
- const labelA = resultA.label.length > colW ? resultA.label.slice(0, colW - 2) + '..' : resultA.label;
2417
- const labelB = resultB.label.length > colW ? resultB.label.slice(0, colW - 2) + '..' : resultB.label;
2418
-
2419
- const scoreColorA = resultA.score >= 80 ? 'green' : resultA.score >= 60 ? 'yellow' : 'red';
2420
- const scoreColorB = resultB.score >= 80 ? 'green' : resultB.score >= 60 ? 'yellow' : 'red';
2421
-
2422
- const arrow = (a, b) => {
2423
- if (a === b) return c('gray', ' =');
2424
- return a > b ? c('green', ' ↑') : c('red', ' ↓');
2425
- };
2426
-
2427
- const better = (a, b, lowerIsBetter) => {
2428
- if (a === b) return c('gray', ' draw');
2429
- if (lowerIsBetter) return a < b ? c('green', ' ← winner') : c('red', '');
2430
- return a > b ? c('green', ' ← winner') : c('red', '');
2431
- };
2432
-
2433
- console.log(c('bold', ' ┌──────────────────────┬──────────────────────┬──────────────────────┐'));
2434
- console.log(c('bold', ` │ ${pad('METRIC', colW)} │ ${pad(labelA, colW)} │ ${pad(labelB, colW)} │`));
2435
- console.log(c('bold', ' ├──────────────────────┼──────────────────────┼──────────────────────┤'));
2436
-
2437
- const row = (label, valA, valB) => {
2438
- console.log(` │ ${c('gray', pad(label, colW))} │ ${pad(valA, colW)} │ ${pad(valB, colW)} │`);
2439
- };
2440
-
2441
- row('Health Score', `${c(scoreColorA, resultA.score + '/100')}`, `${c(scoreColorB, resultB.score + '/100')}`);
2442
- row('Grade', `${c(scoreColorA, resultA.grade)}`, `${c(scoreColorB, resultB.grade)}`);
2443
- row('Files Scanned', String(resultA.files), String(resultB.files));
2444
- row('Total Issues', String(resultA.issues.total), String(resultB.issues.total));
2445
- row('Critical Issues', String(resultA.issues.critical), String(resultB.issues.critical));
2446
- row('High Issues', String(resultA.issues.high), String(resultB.issues.high));
2447
- row('Circular Deps', String(resultA.dependencies.circular), String(resultB.dependencies.circular));
2448
- row('Orphan Files', String(resultA.dependencies.orphans), String(resultB.dependencies.orphans));
2449
- row('Mother Code DNA', `${resultA.motherCode.coverage}%`, `${resultB.motherCode.coverage}%`);
2450
- row('Drift Issues', String(resultA.motherCode.drifted), String(resultB.motherCode.drifted));
2451
- row('Scan Time', `${resultA.elapsed}ms`, `${resultB.elapsed}ms`);
2452
-
2453
- console.log(c('bold', ' └──────────────────────┴──────────────────────┴──────────────────────┘'));
2454
- console.log('');
2455
-
2456
- // ─── Verdict ────────────────────────────────────────────
2457
- const diff = resultA.score - resultB.score;
2458
- if (diff === 0) {
2459
- console.log(c('cyan', ` VERDICT: Both codebases score identically (${resultA.score}/100)`));
2460
- } else {
2461
- const winner = diff > 0 ? resultA : resultB;
2462
- const loser = diff > 0 ? resultB : resultA;
2463
- console.log(c('cyan', ` VERDICT: ${winner.label} is healthier`));
2464
- console.log(c('gray', ` ${winner.label} scores ${winner.score}/100 (${winner.grade}) vs ${loser.label} at ${loser.score}/100 (${loser.grade})`));
2465
- console.log(c('gray', ` ${Math.abs(diff)} point difference — ${Math.abs(diff) > 20 ? 'significant gap' : Math.abs(diff) > 10 ? 'moderate gap' : 'close race'}`));
2466
- }
2467
-
2468
- // ─── Issue breakdown comparison ─────────────────────────
2469
- const allCats = new Set([...Object.keys(resultA.categories), ...Object.keys(resultB.categories)]);
2470
- if (allCats.size > 0) {
2471
- console.log('');
2472
- console.log(c('bold', ' ISSUE BREAKDOWN'));
2473
- for (const cat of allCats) {
2474
- const countA = resultA.categories[cat] || 0;
2475
- const countB = resultB.categories[cat] || 0;
2476
- const catLabel = cat.charAt(0).toUpperCase() + cat.slice(1);
2477
- console.log(c('gray', ` ${pad(catLabel, 22)} ${padL(countA, 5)} vs ${padL(countB, 5)} ${countA < countB ? c('green', `← ${labelA}`) : countA > countB ? c('green', `→ ${labelB}`) : c('gray', 'tied')}`));
2478
- }
2479
- }
2480
-
2481
- console.log('');
2482
- console.log(c('gray', ` Full scan: npx thuban scan ${paths[0]}`));
2483
- console.log(c('gray', ` Full scan: npx thuban scan ${paths[1]}`));
2484
- console.log('');
2485
- }
2486
-
2487
- async function main() {
2488
- const args = process.argv.slice(2);
2489
- const opts = parseArgs(args);
2490
-
2491
- if (!opts.command || opts.command === 'help') {
2492
- printHelp();
2493
- process.exit(0);
2494
- }
2495
-
2496
- if (opts.command === 'version') {
2497
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
2498
- console.log(`thuban v${pkg.version}`);
2499
- process.exit(0);
2500
- }
2501
-
2502
- // Verify target path exists
2503
- const noPathCommands2 = ['activate', 'status', 'upgrade', 'telemetry', 'compare'];
2504
-
2505
- if (!noPathCommands2.includes(opts.command) && !opts.targetPath.match(/^(https?:\/\/|git@)/) && !fs.existsSync(opts.targetPath)) {
2506
- console.error(c('red', ` Error: Path not found: ${opts.targetPath}`));
2507
- process.exit(1);
2508
- }
2509
-
2510
- try {
2511
- switch (opts.command) {
2512
- case 'scan':
2513
- await cmdScan(opts);
2514
- break;
2515
- case 'deps':
2516
- await cmdDeps(opts);
2517
- break;
2518
- case 'drift':
2519
- await cmdDrift(opts);
2520
- break;
2521
- case 'inject':
2522
- await cmdInject(opts);
2523
- break;
2524
- case 'health':
2525
- // Alias for report
2526
- await cmdReport(opts);
2527
- break;
2528
- case 'report':
2529
- await cmdReport(opts);
2530
- break;
2531
- case 'debt':
2532
- await cmdDebt(opts);
2533
- break;
2534
- case 'fix':
2535
- await cmdFix(opts);
2536
- break;
2537
- case 'hallucinate':
2538
- case 'hal':
2539
- await cmdHallucinate(opts);
2540
- break;
2541
- case 'watch':
2542
- await cmdWatch(opts);
2543
- break;
2544
- case 'monitor':
2545
- await cmdMonitor(opts);
2546
- break;
2547
- case 'history':
2548
- await cmdHistory(opts);
2549
- break;
2550
- case 'trend':
2551
- await cmdTrend(opts);
2552
- break;
2553
- case 'dashboard':
2554
- case 'dash':
2555
- await cmdDashboard(opts);
2556
- break;
2557
- case 'diff':
2558
- await cmdDiff(opts);
2559
- break;
2560
- case 'activate':
2561
- await cmdActivate(opts);
2562
- break;
2563
- case 'status':
2564
- await cmdStatus(opts);
2565
- break;
2566
- case 'upgrade':
2567
- cmdUpgrade();
2568
- break;
2569
- case 'gate':
2570
- await cmdGate(opts);
2571
- break;
2572
- case 'cost':
2573
- await cmdCost(opts);
2574
- break;
2575
- case 'ghosts':
2576
- await cmdGhosts(opts);
2577
- break;
2578
- case 'ai-score':
2579
- case 'aiscore':
2580
- await cmdAIScore(opts);
2581
- break;
2582
- case 'clones':
2583
- case 'duplicates':
2584
- await cmdClones(opts);
2585
- break;
2586
- case 'passport':
2587
- await cmdPassport(opts);
2588
- break;
2589
- case 'executive':
2590
- case 'exec-report':
2591
- case 'pdf':
2592
- await cmdExecutive(opts);
2593
- break;
2594
- case 'baseline':
2595
- await cmdBaseline(opts);
2596
- break;
2597
- case 'telemetry':
2598
- await cmdTelemetry(opts);
2599
- break;
2600
- case 'compare':
2601
- await cmdCompare(opts);
2602
- break;
2603
- default:
2604
- console.error('');
2605
- console.error(c('red', ` Unknown command: ${opts.command}`));
2606
- console.error('');
2607
- // Suggest closest match
2608
- const allCmds = ['scan','compare','deps','drift','inject','health','report','debt','fix','hallucinate','watch','dashboard','diff','gate','cost','ghosts','ai-score','clones','passport','executive','baseline','activate','status','upgrade','telemetry'];
2609
- const suggestions = allCmds.filter(cmd => cmd.startsWith(opts.command.slice(0,2)) || cmd.includes(opts.command));
2610
- if (suggestions.length > 0) {
2611
- console.error(c('gray', ` Did you mean: `) + c('cyan', suggestions.join(', ')) + c('gray', '?'));
2612
- console.error('');
2613
- }
2614
- console.error(c('gray', ' Quick start:'));
2615
- console.error(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
2616
- console.error(` ${c('cyan', 'npx thuban --help')} See all commands`);
2617
- console.error('');
2618
- process.exit(1);
2619
- }
2620
- } catch (err) {
2621
- console.error(c('red', ` Error: ${err.message}`));
2622
- if (opts.verbose) console.error(err.stack);
2623
- process.exit(1);
2624
- }
2625
- }
2626
-
2627
- main();