thuban 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -20,6 +20,7 @@
20
20
 
21
21
  const path = require('path');
22
22
  const fs = require('fs');
23
+ const os = require('os');
23
24
 
24
25
  // Import engine components
25
26
  const CodeScanner = require('./packages/scanner/code-scanner.js');
@@ -75,51 +76,100 @@ function printHelp() {
75
76
  banner();
76
77
  const lm = new LicenseManager();
77
78
  const status = lm.getStatus();
78
- console.log(c('gray', ` License: ${status.tierName} tier`) + (status.licensed ? c('green', '') : ''));
79
- console.log('');
80
- console.log(c('bold', ' USAGE'));
81
- console.log('');
82
- console.log(` ${c('cyan', 'thuban scan')} [path] Scan for code issues`);
83
- console.log(` ${c('cyan', 'thuban deps')} [path] Map dependencies`);
84
- console.log(` ${c('cyan', 'thuban drift')} [path] Detect drift from annotations`);
85
- console.log(` ${c('cyan', 'thuban inject')} [path] Inject Mother Code DNA`);
86
- console.log(` ${c('cyan', 'thuban health')} [path] Codebase health check`);
87
- console.log(` ${c('cyan', 'thuban report')} [path] Full combined report`);
88
- console.log(` ${c('cyan', 'thuban debt')} [path] Tech debt analysis with scores`);
89
- console.log(` ${c('cyan', 'thuban fix')} [path] Auto-fix all fixable tech debt`);
90
- console.log(` ${c('cyan', 'thuban hallucinate')} [path] Detect AI hallucinations`);
91
- console.log(` ${c('cyan', 'thuban watch')} [path] Live sentinel monitoring`);
92
- console.log(` ${c('cyan', 'thuban dashboard')} [path] Generate HTML report`);
93
- console.log(` ${c('cyan', 'thuban diff')} [path] Scan only git-changed files`);
94
- console.log(` ${c('cyan', 'thuban gate')} Pre-commit hallucination blocker`);
95
- console.log(` ${c('cyan', 'thuban cost')} [path] Tech debt cost calculator`);
96
- console.log(` ${c('cyan', 'thuban ghosts')} [path] Find dead AI-pasted code`);
97
- console.log(` ${c('cyan', 'thuban ai-score')} [path] AI confidence score per function`);
98
- console.log(` ${c('cyan', 'thuban clones')} [path] Copy-paste drift detection`);
99
- console.log(` ${c('cyan', 'thuban passport')} [path] Generate codebase identity file`);
100
- console.log(` ${c('cyan', 'thuban executive')} [path] CTO-ready PDF report (print from browser)`);
101
- console.log(` ${c('cyan', 'thuban baseline')} [path] Create/check baseline (only fail on NEW issues)`);
79
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
80
+ console.log(c('gray', ` v${pkg.version} · ${status.tierName} tier`) + (status.licensed ? c('green', ' ✓') : '') + c('gray', ' · https://thuban.dev'));
102
81
  console.log('');
82
+
83
+ // ─── QUICK START ──────────────────────────────────────────
84
+ console.log(c('bold', ' QUICK START'));
85
+ console.log('');
86
+ console.log(c('gray', ' Run these in order to see what Thuban does:'));
87
+ console.log('');
88
+ console.log(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
89
+ console.log(` ${c('cyan', 'npx thuban fix .')} Preview what can be fixed`);
90
+ console.log(` ${c('cyan', 'npx thuban fix . --fix')} Apply all safe fixes`);
91
+ console.log(` ${c('cyan', 'npx thuban dashboard .')} Open visual HTML report`);
92
+ console.log('');
93
+
94
+ // ─── SCAN & DETECT ───────────────────────────────────────
95
+ console.log(c('bold', ' SCAN & DETECT'));
96
+ console.log('');
97
+ console.log(` ${c('cyan', 'scan')} [path|url] Full health scan — score, grade, all issues`);
98
+ console.log(` ${c('cyan', 'compare')} [a] [b] Compare two codebases side by side`);
99
+ console.log(` ${c('cyan', 'hallucinate')} [path] Find phantom APIs and invented imports`);
100
+ console.log(` ${c('cyan', 'ghosts')} [path] Dead functions nobody calls`);
101
+ console.log(` ${c('cyan', 'ai-score')} [path] Probability each function is AI-generated`);
102
+ console.log(` ${c('cyan', 'clones')} [path] Copy-pasted code that has drifted apart`);
103
+ console.log(` ${c('cyan', 'drift')} [path] Files that changed since Mother Code was set`);
104
+ console.log(` ${c('cyan', 'deps')} [path] Dependency map — circular, orphan, critical`);
105
+ console.log(` ${c('cyan', 'diff')} [path] Scan only git-changed files`);
106
+ console.log(` ${c('cyan', 'debt')} [path] Detailed tech debt breakdown`);
107
+ console.log(` ${c('cyan', 'cost')} [path] Tech debt translated to £ and hours`);
108
+ console.log('');
109
+
110
+ // ─── FIX & PROTECT ───────────────────────────────────────
111
+ console.log(c('bold', ' FIX & PROTECT'));
112
+ console.log('');
113
+ console.log(` ${c('cyan', 'fix')} [path] Preview fixable issues ${c('gray', '(safe, no changes)')}`);
114
+ console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix')} Apply all safe fixes`);
115
+ console.log(` ${c('cyan', 'fix')} [path] ${c('gray', '--fix --commit')} Each fix = 1 git commit ${c('gray', '(revert any)')}`);
116
+ console.log(` ${c('cyan', 'inject')} [path] Add Mother Code DNA to every file`);
117
+ console.log(` ${c('cyan', 'gate')} Install pre-commit hook ${c('gray', '(blocks hallucinations)')}`);
118
+ console.log(` ${c('cyan', 'watch')} [path] Live sentinel — scans files as you save`);
119
+ console.log(` ${c('cyan', 'baseline')} [path] Snapshot issues so future scans show only NEW ones`);
120
+ console.log('');
121
+
122
+ // ─── REPORTS ──────────────────────────────────────────────
123
+ console.log(c('bold', ' REPORTS'));
124
+ console.log('');
125
+ console.log(` ${c('cyan', 'dashboard')} [path] Interactive HTML report ${c('gray', '(share with your team)')}`);
126
+ console.log(` ${c('cyan', 'executive')} [path] CTO-ready report ${c('gray', '(print to PDF from browser)')}`);
127
+ console.log(` ${c('cyan', 'report')} [path] Combined scan + deps + drift summary`);
128
+ console.log(` ${c('cyan', 'passport')} [path] Codebase identity card ${c('gray', '(for new team members)')}`);
129
+ console.log('');
130
+
131
+ // ─── LICENSE ──────────────────────────────────────────────
103
132
  console.log(c('bold', ' LICENSE'));
104
133
  console.log('');
105
- console.log(` ${c('cyan', 'thuban activate')} <key> Activate a license key`);
106
- console.log(` ${c('cyan', 'thuban status')} Show license and usage status`);
107
- console.log(` ${c('cyan', 'thuban upgrade')} Get a license key`);
134
+ console.log(` ${c('cyan', 'activate')} <key> Activate a license key`);
135
+ console.log(` ${c('cyan', 'status')} Show license, usage, and features`);
136
+ console.log(` ${c('cyan', 'upgrade')} Open pricing page`);
108
137
  console.log('');
138
+
139
+ // ─── OPTIONS ──────────────────────────────────────────────
109
140
  console.log(c('bold', ' OPTIONS'));
110
141
  console.log('');
111
- console.log(` ${c('gray', '--json')} Output as JSON`);
112
- console.log(` ${c('gray', '--fix')} Apply fixes (default: dry-run preview)`);
113
- console.log(` ${c('gray', '--dry-run')} Preview changes without applying`);
114
- console.log(` ${c('gray', '--unsafe')} Allow runtime-changing fixes`);
115
- console.log(` ${c('gray', '--commit')} Auto-commit each fix for easy rollback`);
116
- console.log(` ${c('gray', '--baseline')} Only report NEW issues (vs baseline)`);
117
- console.log(` ${c('gray', '--baseline-create')} Snapshot current issues as baseline`);
118
- console.log(` ${c('gray', '--verbose')} Show detailed output`);
119
- console.log(` ${c('gray', '--ignore <pattern>')} Ignore files matching pattern`);
120
- console.log(` ${c('gray', '--max-issues <n>')} Limit issues shown (default: 50)`);
142
+ console.log(` ${c('gray', '--fix')} Apply changes ${c('gray', '(default is dry-run preview)')}`);
143
+ console.log(` ${c('gray', '--commit')} Auto-commit each fix for easy rollback`);
144
+ console.log(` ${c('gray', '--unsafe')} Include fixes that change runtime behaviour`);
145
+ console.log(` ${c('gray', '--json')} Machine-readable JSON output`);
146
+ console.log(` ${c('gray', '--baseline')} Only show NEW issues vs saved baseline`);
147
+ console.log(` ${c('gray', '--verbose')} Show detailed output`);
148
+ console.log(` ${c('gray', '--ignore <pat>')} Skip files matching pattern`);
149
+ console.log(` ${c('gray', '--max-issues <n>')} Limit displayed issues ${c('gray', '(default: 50)')}`);
150
+ console.log('');
151
+
152
+ // ─── EXAMPLES ─────────────────────────────────────────────
153
+ console.log(c('bold', ' EXAMPLES'));
154
+ console.log('');
155
+ console.log(c('gray', ' # Scan the current project'));
156
+ console.log(` ${c('white', 'npx thuban scan .')}`);
157
+ console.log('');
158
+ console.log(c('gray', ' # Find and fix all AI hallucinations'));
159
+ console.log(` ${c('white', 'npx thuban hallucinate .')}`);
160
+ console.log(` ${c('white', 'npx thuban fix . --fix')}`);
161
+ console.log('');
162
+ console.log(c('gray', ' # Generate a report for your CTO'));
163
+ console.log(` ${c('white', 'npx thuban executive .')}`);
164
+ console.log('');
165
+ console.log(c('gray', ' # Block hallucinated code on every commit'));
166
+ console.log(` ${c('white', 'npx thuban gate --fix')}`);
167
+ console.log('');
168
+ console.log(c('gray', ' # Scan a specific folder, ignore tests'));
169
+ console.log(` ${c('white', 'npx thuban scan ./src --ignore "**/*.test.js"')}`);
121
170
  console.log('');
122
- console.log(c('gray', ' https://thuban.dev'));
171
+ console.log(c('gray', ' Report a bug or request a feature:'));
172
+ console.log(` ${c('cyan', 'https://github.com/SilverwingsBenefitsGit/thuban/issues')}`);
123
173
  console.log('');
124
174
  }
125
175
 
@@ -159,7 +209,14 @@ function parseArgs(args) {
159
209
  else if (arg === '--version' || arg === '-v') { opts.command = 'version'; }
160
210
  else if (!opts.command) { opts.command = arg; }
161
211
  else if (opts.command === 'activate' && !opts.licenseKey) { opts.licenseKey = arg; }
162
- else if (!noPathCommands.includes(opts.command)) { opts.targetPath = path.resolve(arg); }
212
+ else if (!noPathCommands.includes(opts.command)) {
213
+ // Don't resolve git URLs as local paths
214
+ if (arg.match(/^(https?:\/\/|git@)/)) {
215
+ opts.targetPath = arg;
216
+ } else {
217
+ opts.targetPath = path.resolve(arg);
218
+ }
219
+ }
163
220
  i++;
164
221
  }
165
222
 
@@ -171,7 +228,7 @@ function parseArgs(args) {
171
228
  function collectFiles(dir, ignorePatterns = []) {
172
229
  const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv', 'coverage', '.cache', '.turbo'];
173
230
  const allIgnore = [...defaultIgnore, ...ignorePatterns];
174
- const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.json', '.mjs', '.cjs'];
231
+ const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs', '.json'];
175
232
  const MAX_FILE_SIZE = 1024 * 1024; // 1MB per file — skip anything larger
176
233
  const MAX_FILES = 50000; // Hard cap to prevent memory issues
177
234
  const files = [];
@@ -211,7 +268,34 @@ function collectFiles(dir, ignorePatterns = []) {
211
268
 
212
269
  async function cmdScan(opts) {
213
270
  const startTime = Date.now();
214
- const targetPath = path.resolve(opts.targetPath);
271
+ const gitUrlPattern = /^(https?:\/\/|git@)/;
272
+ const isGitUrl = gitUrlPattern.test(opts.targetPath);
273
+ let targetPath = isGitUrl ? opts.targetPath : path.resolve(opts.targetPath);
274
+ let clonedRepo = null;
275
+
276
+ // ─── Git URL detection — clone and scan ─────────────────
277
+ if (isGitUrl) {
278
+ if (!opts.json) {
279
+ console.log('');
280
+ console.log(c('cyan', ' Cloning repository...'));
281
+ console.log(c('gray', ` ${opts.targetPath}`));
282
+ }
283
+ const tmpDir = path.join(os.tmpdir(), 'thuban-scan-' + Date.now());
284
+ const { execSync } = require('child_process');
285
+ try {
286
+ execSync(`git clone --depth 1 "${opts.targetPath}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
287
+ targetPath = tmpDir;
288
+ clonedRepo = tmpDir;
289
+ if (!opts.json) {
290
+ console.log(c('cyan', ' Cloned successfully'));
291
+ console.log('');
292
+ }
293
+ } catch (e) {
294
+ console.error(c('red', ` Failed to clone repository: ${e.message}`));
295
+ console.error(c('gray', ' Check the URL is correct and the repo is accessible'));
296
+ process.exit(1);
297
+ }
298
+ }
215
299
 
216
300
  // ─── License check ───────────────────────────────────────
217
301
  const lm = new LicenseManager();
@@ -237,6 +321,23 @@ async function cmdScan(opts) {
237
321
 
238
322
  if (!opts.json) {
239
323
  banner();
324
+
325
+ // First-run welcome
326
+ if (lm.usage.totalScans === 0) {
327
+ console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
328
+ console.log(c('cyan', ' │') + c('bold', ' Welcome to Thuban! ') + c('cyan', '│'));
329
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
330
+ console.log(c('cyan', ' │') + ' ' + c('gray', 'Thuban scans your codebase for AI hallucinations,') + ' ' + c('cyan', '│'));
331
+ console.log(c('cyan', ' │') + ' ' + c('gray', 'dead code, tech debt, and architecture drift.') + ' ' + c('cyan', '│'));
332
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
333
+ console.log(c('cyan', ' │') + ' ' + c('gray', 'After this scan, try:') + ' ' + c('cyan', '│'));
334
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix .') + ' ' + c('gray', 'Preview fixes') + ' ' + c('cyan', '│'));
335
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('gray', 'Visual report') + ' ' + c('cyan', '│'));
336
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban --help') + ' ' + c('gray', 'All commands') + ' ' + c('cyan', '│'));
337
+ console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
338
+ console.log('');
339
+ }
340
+
240
341
  console.log(c('bold', ' SCANNING: ') + c('cyan', targetPath));
241
342
  console.log('');
242
343
  }
@@ -360,18 +461,24 @@ async function cmdScan(opts) {
360
461
  // THE GUT-PUNCH OUTPUT
361
462
  // ═══════════════════════════════════════════════════════════
362
463
 
464
+ // ─── Count fixable items ─────────────────────────────────
465
+ const fixableCount = allIssues.filter(i => i.fixable !== false).length;
466
+
363
467
  console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
364
- console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN ') + c('cyan', '║'));
468
+ console.log(c('cyan', ' ║') + c('bold', ' THUBAN SCAN RESULTS ') + c('cyan', '║'));
365
469
  console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
366
470
  console.log(c('cyan', ' ║') + ` Files scanned: ${c('bold', String(files.length).padEnd(6))}` + (isFileCapped ? c('yellow', ` of ${totalFileCount}`) : ' ') + c('cyan', ' ║'));
367
471
  console.log(c('cyan', ' ║') + ` Health score: ${c(scoreColor, (grade + ' (' + Math.round(score) + '/100)').padEnd(20))}` + c('cyan', ' ║'));
472
+ console.log(c('cyan', ' ║') + ` Issues found: ${c(allIssues.length > 0 ? 'yellow' : 'green', String(allIssues.length).padEnd(20))}` + c('cyan', ' ║'));
473
+ console.log(c('cyan', ' ║') + ` Auto-fixable: ${c('green', String(fixableCount).padEnd(20))}` + c('cyan', ' ║'));
368
474
  console.log(c('cyan', ' ║') + ` Scan time: ${c('gray', (elapsed + 'ms').padEnd(20))}` + c('cyan', ' ║'));
369
475
  console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
370
476
  console.log('');
371
477
 
372
478
  // ─── CRITICAL: Hallucinated APIs (will crash) ────────────
373
479
  if (hallucinations.length > 0) {
374
- console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)) + c('red', ' (will crash at runtime)'));
480
+ console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)));
481
+ console.log(c('gray', ' These imports or method calls do not exist. Your code WILL crash when it hits them.'));
375
482
  console.log('');
376
483
 
377
484
  const showCount = tier.showFullDetails ? hallucinations.length : Math.min(tier.teaserIssues, hallucinations.length);
@@ -403,7 +510,9 @@ async function cmdScan(opts) {
403
510
 
404
511
  // ─── HIGH: Deprecated APIs ───────────────────────────────
405
512
  if (deprecated.length > 0) {
406
- console.log(c('red', c('bold', ` HIGH: ${deprecated.length} Deprecated API${deprecated.length > 1 ? 's' : ''}`)) + c('gray', ' (will break on Node upgrade)'));
513
+ console.log(c('red', c('bold', ` DEPRECATED: ${deprecated.length} API${deprecated.length > 1 ? 's' : ''} that will break on upgrade`)));
514
+ console.log(c('gray', ' These work NOW but will stop working when you upgrade Node.js or your dependencies.'));
515
+ console.log(c('gray', ' Fix: ') + c('cyan', 'npx thuban fix . --fix --unsafe'));
407
516
  console.log('');
408
517
 
409
518
  const showCount = tier.showFullDetails ? deprecated.length : Math.min(tier.teaserIssues, deprecated.length);
@@ -443,17 +552,44 @@ async function cmdScan(opts) {
443
552
  }
444
553
 
445
554
  if (qualityIssues.length > 0) {
555
+ // Group quality issues by type for clearer output
556
+ const qualityByType = {};
557
+ for (const item of qualityIssues) {
558
+ const key = item.message || item.type || 'other';
559
+ // Normalize similar messages
560
+ const normalizedKey = key.replace(/\s*—.*$/, '').trim();
561
+ if (!qualityByType[normalizedKey]) qualityByType[normalizedKey] = [];
562
+ qualityByType[normalizedKey].push(item);
563
+ }
564
+ const typeEntries = Object.entries(qualityByType).sort((a, b) => b[1].length - a[1].length);
565
+
446
566
  console.log(c('yellow', c('bold', ` CODE QUALITY: ${qualityIssues.length} issue${qualityIssues.length > 1 ? 's' : ''}`)));
447
- const showCount = tier.showFullDetails ? Math.min(qualityIssues.length, 10) : Math.min(3, qualityIssues.length);
448
- for (const item of qualityIssues.slice(0, showCount)) {
449
- const lineRef = item.line ? `:${item.line}` : '';
450
- const sevColor = item.severity === 'high' || item.severity === 'error' ? 'red' : 'yellow';
451
- console.log(` ${c(sevColor, '!')} ${c('cyan', item.file + lineRef)} ${c('gray', item.message)}`);
567
+ console.log(c('gray', ' Patterns that reduce code quality, maintainability, or production readiness.'));
568
+ console.log('');
569
+
570
+ // Show grouped summary
571
+ const showTypes = tier.showFullDetails ? typeEntries.length : Math.min(3, typeEntries.length);
572
+ for (let t = 0; t < showTypes; t++) {
573
+ const [typeName, items] = typeEntries[t];
574
+ const sevColor = items[0].severity === 'high' || items[0].severity === 'error' ? 'red' : 'yellow';
575
+ console.log(` ${c(sevColor, '!')} ${c('bold', String(items.length))} x ${typeName}`);
576
+ // Show first example file
577
+ if (items[0].file) {
578
+ const lineRef = items[0].line ? `:${items[0].line}` : '';
579
+ console.log(` ${c('gray', 'e.g.')} ${c('cyan', items[0].file + lineRef)}`);
580
+ }
452
581
  }
453
- if (qualityIssues.length > showCount) {
454
- console.log(c('gray', ` ... ${qualityIssues.length - showCount} more`));
582
+ if (typeEntries.length > showTypes) {
583
+ console.log(c('gray', ` ... ${typeEntries.length - showTypes} more issue types`));
455
584
  }
456
585
  console.log('');
586
+
587
+ // Show fix command if fixable
588
+ const fixableQuality = qualityIssues.filter(i => i.fixable !== false).length;
589
+ if (fixableQuality > 0) {
590
+ console.log(c('gray', ` ${fixableQuality} of these can be auto-fixed: `) + c('cyan', 'npx thuban fix . --fix --unsafe'));
591
+ console.log('');
592
+ }
457
593
  }
458
594
 
459
595
  // ─── THE VERDICT ─────────────────────────────────────────
@@ -501,10 +637,36 @@ async function cmdScan(opts) {
501
637
  console.log(c('gray', ` Free scans remaining this month: ${remaining}/${scanStatus.limit}`));
502
638
  console.log('');
503
639
  } else {
504
- // Pro/Team: show fix command
640
+ // Pro/Team: show clear next steps
505
641
  if (allIssues.length > 0) {
506
- console.log(` ${c('cyan', 'thuban fix . --fix')} Auto-fix all fixable issues`);
507
- console.log(` ${c('cyan', 'thuban dashboard .')} Generate full HTML report`);
642
+ const fixable = allIssues.filter(i => i.fixable !== false).length;
643
+ console.log(c('cyan', ' ┌──────────────────────────────────────────────────────┐'));
644
+ console.log(c('cyan', ' │') + c('bold', ' WHAT TO DO NEXT ') + c('cyan', '│'));
645
+ console.log(c('cyan', ' ├──────────────────────────────────────────────────────┤'));
646
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
647
+ console.log(c('cyan', ' │') + ' ' + c('bold', '1. See what would be fixed (safe preview):') + ' ' + c('cyan', '│'));
648
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . ') + ' ' + c('cyan', '│'));
649
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
650
+ console.log(c('cyan', ' │') + ' ' + c('bold', '2. Apply all safe fixes:') + ' ' + c('cyan', '│'));
651
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix') + ' ' + c('cyan', '│'));
652
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
653
+ console.log(c('cyan', ' │') + ' ' + c('bold', '3. Apply fixes with git rollback:') + ' ' + c('cyan', '│'));
654
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban fix . --fix --commit') + ' ' + c('cyan', '│'));
655
+ console.log(c('cyan', ' │') + ' ' + c('gray', 'Each fix = 1 git commit. Undo any with git revert.') + c('cyan', '│'));
656
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
657
+ console.log(c('cyan', ' │') + ' ' + c('bold', '4. Generate visual report (HTML):') + ' ' + c('cyan', '│'));
658
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban dashboard .') + ' ' + c('cyan', '│'));
659
+ console.log(c('cyan', ' │') + ' ' + c('gray', 'Opens a dashboard you can share with your team.') + ' ' + c('cyan', '│'));
660
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
661
+ console.log(c('cyan', ' │') + ' ' + c('bold', '5. Show tech debt cost in £:') + ' ' + c('cyan', '│'));
662
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban cost .') + ' ' + c('cyan', '│'));
663
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
664
+ console.log(c('cyan', ' │') + ' ' + c('bold', '6. Block bad code on every commit:') + ' ' + c('cyan', '│'));
665
+ console.log(c('cyan', ' │') + ' ' + c('cyan', 'npx thuban gate') + ' ' + c('cyan', '│'));
666
+ console.log(c('cyan', ' │') + ' ' + c('gray', 'Installs a pre-commit hook. Set once, forget.') + ' ' + c('cyan', '│'));
667
+ console.log(c('cyan', ' │') + ' ' + c('cyan', '│'));
668
+ console.log(c('cyan', ' │') + ' ' + c('gray', 'All commands: npx thuban --help') + ' ' + c('cyan', '│'));
669
+ console.log(c('cyan', ' └──────────────────────────────────────────────────────┘'));
508
670
  console.log('');
509
671
  }
510
672
  }
@@ -514,6 +676,19 @@ async function cmdScan(opts) {
514
676
  console.log(c('gray', ` Your actual score may be worse. Upgrade to scan everything.`));
515
677
  console.log('');
516
678
  }
679
+
680
+ // ─── Continuous monitoring pitch ────────────────────────
681
+ if (allIssues.length > 0 && !isFileCapped) {
682
+ console.log(c('gray', ' ─────────────────────────────────────────────────────'));
683
+ console.log(c('gray', ' Thuban runs 100% on your machine. No data leaves.'));
684
+ console.log(c('gray', ' No cloud costs. As your codebase grows, Thuban grows with it.'));
685
+ console.log('');
686
+ console.log(c('gray', ' Keep your codebase healthy continuously:'));
687
+ console.log(c('gray', ' ') + c('cyan', 'npx thuban gate --fix') + c('gray', ' Block bad code on every commit'));
688
+ console.log(c('gray', ' ') + c('cyan', 'npx thuban watch .') + c('gray', ' Scan files as you save them'));
689
+ console.log(c('gray', ' ') + c('cyan', 'npx thuban baseline .') + c('gray', ' Only alert on NEW issues'));
690
+ console.log('');
691
+ }
517
692
  }
518
693
 
519
694
  async function cmdDeps(opts) {
@@ -830,6 +1005,11 @@ async function cmdReport(opts) {
830
1005
  if (orphans.length > 5) console.log(` ${c('gray', '→')} Consider removing ${orphans.length} orphan files`);
831
1006
  console.log('');
832
1007
  }
1008
+
1009
+ // Clean up cloned repo
1010
+ if (clonedRepo) {
1011
+ try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
1012
+ }
833
1013
  }
834
1014
 
835
1015
  // ─── Tech Debt & Fix Commands ────────────────────────────
@@ -882,7 +1062,7 @@ async function cmdFix(opts) {
882
1062
 
883
1063
  const grouped = {};
884
1064
  const safeActions = ['inject_widget', 'regenerate_widget', 'remove_console_log', 'flag_for_removal'];
885
- const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api'];
1065
+ const unsafeActions = ['break_circular', 'extract_to_env', 'update_deprecated_api', 'wrap_localhost_url'];
886
1066
  let safeCount = 0;
887
1067
  let unsafeCount = 0;
888
1068
 
@@ -914,7 +1094,8 @@ async function cmdFix(opts) {
914
1094
  const label = {
915
1095
  'break_circular': 'Restructure circular dependencies',
916
1096
  'extract_to_env': 'Extract hardcoded secrets to env vars',
917
- 'update_deprecated_api': 'Replace deprecated API calls',
1097
+ 'update_deprecated_api': 'Replace deprecated API calls with modern equivalents',
1098
+ 'wrap_localhost_url': 'Wrap hardcoded localhost URLs in env var fallback',
918
1099
  }[action] || action;
919
1100
  console.log(` ${c('yellow', '!')} ${label}: ${c('bold', count)}`);
920
1101
  }
@@ -973,18 +1154,43 @@ async function cmdFix(opts) {
973
1154
  if (opts.json) {
974
1155
  console.log(JSON.stringify(result, null, 2));
975
1156
  } else {
976
- console.log(c('green', ` ✓ Fixed ${result.fixed} items`));
977
- console.log(c('yellow', ` Flagged ${result.flagged} items for manual review`));
978
- if (result.failed > 0) console.log(c('red', ` ✗ Failed: ${result.failed}`));
1157
+ console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
1158
+ console.log(c('cyan', ' ║') + c('bold', ' FIX RESULTS ') + c('cyan', '║'));
1159
+ console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
1160
+ console.log(c('cyan', ' ║') + ` Fixed: ${c('green', String(result.fixed).padEnd(20))}` + c('cyan', ' ║'));
1161
+ console.log(c('cyan', ' ║') + ` Flagged for review: ${c('yellow', String(result.flagged).padEnd(19))}` + c('cyan', ' ║'));
1162
+ if (result.failed > 0) {
1163
+ console.log(c('cyan', ' ║') + ` Failed: ${c('red', String(result.failed).padEnd(20))}` + c('cyan', ' ║'));
1164
+ }
979
1165
  if (result.skippedUnsafe > 0) {
980
- console.log(c('gray', ` Skipped ${result.skippedUnsafe} unsafe fixes (use --unsafe to apply)`));
1166
+ console.log(c('cyan', ' ║') + ` Skipped (unsafe): ${c('gray', String(result.skippedUnsafe).padEnd(20))}` + c('cyan', ' ║'));
981
1167
  }
1168
+ console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
982
1169
  console.log('');
983
- console.log(c('gray', ` Run 'thuban report' to see updated scores`));
984
- if (!opts.gitCommit) {
985
- console.log(c('gray', ` Tip: use --commit to auto-commit fixes for easy rollback`));
1170
+
1171
+ if (result.fixed > 0) {
1172
+ console.log(c('green', ' Your files have been updated. Changes are saved to disk.'));
1173
+ if (!opts.gitCommit) {
1174
+ console.log(c('gray', ' Tip: use --commit next time to auto-commit each fix for easy rollback.'));
1175
+ }
1176
+ console.log('');
1177
+ console.log(c('gray', ' Verify the changes:'));
1178
+ console.log(c('gray', ' ') + c('cyan', 'npx thuban scan .') + c('gray', ' Re-scan to see your new score'));
1179
+ console.log(c('gray', ' ') + c('cyan', 'npx thuban dashboard .') + c('gray', ' Generate updated report'));
1180
+ console.log('');
1181
+ }
1182
+
1183
+ if (result.skippedUnsafe > 0) {
1184
+ console.log(c('gray', ` ${result.skippedUnsafe} unsafe fixes were skipped. These change runtime behaviour.`));
1185
+ console.log(c('gray', ' Review them with: ') + c('cyan', 'npx thuban fix . --fix --unsafe'));
1186
+ console.log('');
1187
+ }
1188
+
1189
+ if (result.flagged > 0) {
1190
+ console.log(c('gray', ` ${result.flagged} items need human review (secrets, orphan files, complex patterns).`));
1191
+ console.log(c('gray', ' See details: ') + c('cyan', 'npx thuban dashboard .'));
1192
+ console.log('');
986
1193
  }
987
- console.log('');
988
1194
  }
989
1195
  }
990
1196
 
@@ -1786,6 +1992,292 @@ async function cmdBaseline(opts) {
1786
1992
  }
1787
1993
  }
1788
1994
 
1995
+ // ─── Telemetry (opt-in anonymous usage stats) ───────────────
1996
+
1997
+ function getTelemetryConfigPath() {
1998
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
1999
+ return path.join(home, '.thuban-telemetry.json');
2000
+ }
2001
+
2002
+ function isTelemetryEnabled() {
2003
+ try {
2004
+ const config = JSON.parse(fs.readFileSync(getTelemetryConfigPath(), 'utf-8'));
2005
+ return config.enabled === true;
2006
+ } catch { return false; }
2007
+ }
2008
+
2009
+ function sendAnonymousTelemetry(data) {
2010
+ if (!isTelemetryEnabled()) return;
2011
+ try {
2012
+ const payload = JSON.stringify({
2013
+ v: data.version || '0.3.1',
2014
+ cmd: data.command,
2015
+ files: data.fileCount,
2016
+ score: data.score,
2017
+ grade: data.grade,
2018
+ langs: data.languages,
2019
+ issues: data.issueCount,
2020
+ duration: data.duration,
2021
+ os: process.platform,
2022
+ node: process.version,
2023
+ ts: new Date().toISOString()
2024
+ });
2025
+ // Fire and forget — never blocks the CLI
2026
+ const https = require('https');
2027
+ const req = https.request({
2028
+ hostname: 'thuban-telemetry.silverwingsbenefits.workers.dev',
2029
+ path: '/report',
2030
+ method: 'POST',
2031
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
2032
+ timeout: 3000
2033
+ });
2034
+ req.on('error', () => {}); // silently ignore
2035
+ req.write(payload);
2036
+ req.end();
2037
+ } catch { /* never fail the CLI for telemetry */ }
2038
+ }
2039
+
2040
+ async function cmdTelemetry(opts) {
2041
+ const configPath = getTelemetryConfigPath();
2042
+ const args = process.argv.slice(2);
2043
+
2044
+ if (args.includes('--on')) {
2045
+ fs.writeFileSync(configPath, JSON.stringify({ enabled: true, updated: new Date().toISOString() }));
2046
+ console.log('');
2047
+ console.log(c('cyan', ' Telemetry enabled'));
2048
+ console.log(c('gray', ' Anonymous usage stats will be sent (no code, no paths, no PII)'));
2049
+ console.log(c('gray', ' Turn off anytime: npx thuban telemetry --off'));
2050
+ console.log('');
2051
+ } else if (args.includes('--off')) {
2052
+ fs.writeFileSync(configPath, JSON.stringify({ enabled: false, updated: new Date().toISOString() }));
2053
+ console.log('');
2054
+ console.log(c('cyan', ' Telemetry disabled'));
2055
+ console.log(c('gray', ' No data will be collected'));
2056
+ console.log('');
2057
+ } else {
2058
+ const enabled = isTelemetryEnabled();
2059
+ console.log('');
2060
+ console.log(c('bold', ' TELEMETRY STATUS'));
2061
+ console.log('');
2062
+ console.log(` Status: ${enabled ? c('cyan', 'Enabled') : c('gray', 'Disabled (default)')}`);
2063
+ console.log('');
2064
+ console.log(c('gray', ' Thuban telemetry is 100% opt-in and anonymous.'));
2065
+ console.log(c('gray', ' We collect: file count, score, grade, languages, OS, scan duration.'));
2066
+ console.log(c('gray', ' We never collect: code, file names, paths, project names, or PII.'));
2067
+ console.log('');
2068
+ console.log(` Enable: ${c('cyan', 'npx thuban telemetry --on')}`);
2069
+ console.log(` Disable: ${c('cyan', 'npx thuban telemetry --off')}`);
2070
+ console.log('');
2071
+ }
2072
+ }
2073
+
2074
+ // ─── Internal scan (returns data without printing) ──────────
2075
+
2076
+ async function runScanSilent(targetInput) {
2077
+ const gitUrlPattern = /^(https?:\/\/|git@)/;
2078
+ const isGitUrl = gitUrlPattern.test(targetInput);
2079
+ let targetPath = isGitUrl ? targetInput : path.resolve(targetInput);
2080
+ let clonedRepo = null;
2081
+ let label = targetInput;
2082
+
2083
+ if (isGitUrl) {
2084
+ const tmpDir = path.join(os.tmpdir(), 'thuban-compare-' + Date.now() + '-' + Math.random().toString(36).slice(2,6));
2085
+ const { execSync } = require('child_process');
2086
+ execSync(`git clone --depth 1 "${targetInput}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
2087
+ targetPath = tmpDir;
2088
+ clonedRepo = tmpDir;
2089
+ // Extract repo name for label
2090
+ const parts = targetInput.replace(/\.git$/, '').split('/');
2091
+ label = parts[parts.length - 1] || targetInput;
2092
+ } else {
2093
+ label = path.basename(path.resolve(targetInput)) || targetInput;
2094
+ }
2095
+
2096
+ const startTime = Date.now();
2097
+ const scanner = new CodeScanner();
2098
+ const graph = new DependencyGraph();
2099
+ const hallDetector = new HallucinationDetector({ rootPath: targetPath });
2100
+
2101
+ const files = collectFiles(targetPath, []);
2102
+ const scanResults = {};
2103
+ let annotated = 0;
2104
+ let driftCount = 0;
2105
+
2106
+ for (const file of files) {
2107
+ try {
2108
+ const result = scanner.scanFile(file);
2109
+ const relPath = path.relative(targetPath, file);
2110
+ scanResults[file] = result;
2111
+ graph.addFile(file, targetPath);
2112
+ if (result.motherCode && result.motherCode.hasDNA) annotated++;
2113
+ if (result.drifts && result.drifts.length > 0) driftCount++;
2114
+ } catch (e) { /* skip */ }
2115
+ }
2116
+
2117
+ // Run hallucination detection (same as main scan)
2118
+ const hallResults = hallDetector.scan(files, targetPath);
2119
+
2120
+ const elapsed = Date.now() - startTime;
2121
+ let totalIssues = 0, criticalCount = 0, highCount = 0;
2122
+ const issueByCat = {};
2123
+
2124
+ // Count code scanner issues
2125
+ for (const [, r] of Object.entries(scanResults)) {
2126
+ if (r.issues) {
2127
+ totalIssues += r.issues.length;
2128
+ for (const iss of r.issues) {
2129
+ if (iss.severity === 'critical') criticalCount++;
2130
+ if (iss.severity === 'high') highCount++;
2131
+ const cat = iss.category || iss.type || 'other';
2132
+ issueByCat[cat] = (issueByCat[cat] || 0) + 1;
2133
+ }
2134
+ }
2135
+ }
2136
+
2137
+ // Count hallucination issues
2138
+ const hallIssueCount = (hallResults.phantomAPIs || []).length
2139
+ + (hallResults.phantomImports || []).length
2140
+ + (hallResults.deprecatedAPIs || []).length;
2141
+ totalIssues += hallIssueCount;
2142
+ criticalCount += (hallResults.phantomAPIs || []).length + (hallResults.phantomImports || []).length;
2143
+ highCount += (hallResults.deprecatedAPIs || []).length;
2144
+ if (hallIssueCount > 0) issueByCat['hallucination'] = hallIssueCount;
2145
+
2146
+ const orphans = graph.getOrphanFiles();
2147
+ const circular = graph.circularDeps || [];
2148
+ const coverage = files.length > 0 ? Math.round((annotated / files.length) * 100) : 0;
2149
+
2150
+ const score = Math.max(0, Math.round(
2151
+ 100 - (criticalCount * 15) - (highCount * 5) - (circular.length * 10)
2152
+ - (orphans.length * 1) - ((100 - coverage) * 0.1) - (driftCount * 3)
2153
+ ));
2154
+
2155
+ const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
2156
+
2157
+ if (clonedRepo) {
2158
+ try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
2159
+ }
2160
+
2161
+ return {
2162
+ label,
2163
+ files: files.length,
2164
+ score,
2165
+ grade,
2166
+ elapsed,
2167
+ issues: { total: totalIssues, critical: criticalCount, high: highCount },
2168
+ categories: issueByCat,
2169
+ dependencies: { circular: circular.length, orphans: orphans.length },
2170
+ motherCode: { annotated, coverage, drifted: driftCount }
2171
+ };
2172
+ }
2173
+
2174
+ // ─── Compare Command ────────────────────────────────────────
2175
+
2176
+ async function cmdCompare(opts) {
2177
+ const args = process.argv.slice(2);
2178
+ // Find the two paths after 'compare'
2179
+ const paths = args.filter(a => a !== 'compare' && !a.startsWith('--'));
2180
+
2181
+ if (paths.length < 2) {
2182
+ console.log('');
2183
+ console.log(c('red', ' Compare requires two paths or URLs'));
2184
+ console.log('');
2185
+ console.log(c('gray', ' Usage:'));
2186
+ console.log(` ${c('cyan', 'npx thuban compare ./project-a ./project-b')}`);
2187
+ console.log(` ${c('cyan', 'npx thuban compare . https://github.com/user/repo')}`);
2188
+ console.log(` ${c('cyan', 'npx thuban compare https://github.com/a/repo https://github.com/b/repo')}`);
2189
+ console.log('');
2190
+ process.exit(1);
2191
+ }
2192
+
2193
+ console.log('');
2194
+ console.log(c('bold', ' ╔══════════════════════════════════════╗'));
2195
+ console.log(c('bold', ' ║ THUBAN — Codebase Comparison ║'));
2196
+ console.log(c('bold', ' ╚══════════════════════════════════════╝'));
2197
+ console.log('');
2198
+
2199
+ console.log(c('cyan', ` Scanning: ${paths[0]}`));
2200
+ const resultA = await runScanSilent(paths[0]);
2201
+ console.log(c('cyan', ` Scanning: ${paths[1]}`));
2202
+ const resultB = await runScanSilent(paths[1]);
2203
+
2204
+ console.log('');
2205
+
2206
+ // ─── Side by side comparison ────────────────────────────
2207
+ const pad = (str, len) => String(str).padEnd(len);
2208
+ const padL = (str, len) => String(str).padStart(len);
2209
+ const colW = 20;
2210
+ const labelA = resultA.label.length > colW ? resultA.label.slice(0, colW - 2) + '..' : resultA.label;
2211
+ const labelB = resultB.label.length > colW ? resultB.label.slice(0, colW - 2) + '..' : resultB.label;
2212
+
2213
+ const scoreColorA = resultA.score >= 80 ? 'green' : resultA.score >= 60 ? 'yellow' : 'red';
2214
+ const scoreColorB = resultB.score >= 80 ? 'green' : resultB.score >= 60 ? 'yellow' : 'red';
2215
+
2216
+ const arrow = (a, b) => {
2217
+ if (a === b) return c('gray', ' =');
2218
+ return a > b ? c('green', ' ↑') : c('red', ' ↓');
2219
+ };
2220
+
2221
+ const better = (a, b, lowerIsBetter) => {
2222
+ if (a === b) return c('gray', ' draw');
2223
+ if (lowerIsBetter) return a < b ? c('green', ' ← winner') : c('red', '');
2224
+ return a > b ? c('green', ' ← winner') : c('red', '');
2225
+ };
2226
+
2227
+ console.log(c('bold', ' ┌──────────────────────┬──────────────────────┬──────────────────────┐'));
2228
+ console.log(c('bold', ` │ ${pad('METRIC', colW)} │ ${pad(labelA, colW)} │ ${pad(labelB, colW)} │`));
2229
+ console.log(c('bold', ' ├──────────────────────┼──────────────────────┼──────────────────────┤'));
2230
+
2231
+ const row = (label, valA, valB) => {
2232
+ console.log(` │ ${c('gray', pad(label, colW))} │ ${pad(valA, colW)} │ ${pad(valB, colW)} │`);
2233
+ };
2234
+
2235
+ row('Health Score', `${c(scoreColorA, resultA.score + '/100')}`, `${c(scoreColorB, resultB.score + '/100')}`);
2236
+ row('Grade', `${c(scoreColorA, resultA.grade)}`, `${c(scoreColorB, resultB.grade)}`);
2237
+ row('Files Scanned', String(resultA.files), String(resultB.files));
2238
+ row('Total Issues', String(resultA.issues.total), String(resultB.issues.total));
2239
+ row('Critical Issues', String(resultA.issues.critical), String(resultB.issues.critical));
2240
+ row('High Issues', String(resultA.issues.high), String(resultB.issues.high));
2241
+ row('Circular Deps', String(resultA.dependencies.circular), String(resultB.dependencies.circular));
2242
+ row('Orphan Files', String(resultA.dependencies.orphans), String(resultB.dependencies.orphans));
2243
+ row('Mother Code DNA', `${resultA.motherCode.coverage}%`, `${resultB.motherCode.coverage}%`);
2244
+ row('Drift Issues', String(resultA.motherCode.drifted), String(resultB.motherCode.drifted));
2245
+ row('Scan Time', `${resultA.elapsed}ms`, `${resultB.elapsed}ms`);
2246
+
2247
+ console.log(c('bold', ' └──────────────────────┴──────────────────────┴──────────────────────┘'));
2248
+ console.log('');
2249
+
2250
+ // ─── Verdict ────────────────────────────────────────────
2251
+ const diff = resultA.score - resultB.score;
2252
+ if (diff === 0) {
2253
+ console.log(c('cyan', ` VERDICT: Both codebases score identically (${resultA.score}/100)`));
2254
+ } else {
2255
+ const winner = diff > 0 ? resultA : resultB;
2256
+ const loser = diff > 0 ? resultB : resultA;
2257
+ console.log(c('cyan', ` VERDICT: ${winner.label} is healthier`));
2258
+ console.log(c('gray', ` ${winner.label} scores ${winner.score}/100 (${winner.grade}) vs ${loser.label} at ${loser.score}/100 (${loser.grade})`));
2259
+ console.log(c('gray', ` ${Math.abs(diff)} point difference — ${Math.abs(diff) > 20 ? 'significant gap' : Math.abs(diff) > 10 ? 'moderate gap' : 'close race'}`));
2260
+ }
2261
+
2262
+ // ─── Issue breakdown comparison ─────────────────────────
2263
+ const allCats = new Set([...Object.keys(resultA.categories), ...Object.keys(resultB.categories)]);
2264
+ if (allCats.size > 0) {
2265
+ console.log('');
2266
+ console.log(c('bold', ' ISSUE BREAKDOWN'));
2267
+ for (const cat of allCats) {
2268
+ const countA = resultA.categories[cat] || 0;
2269
+ const countB = resultB.categories[cat] || 0;
2270
+ const catLabel = cat.charAt(0).toUpperCase() + cat.slice(1);
2271
+ 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')}`));
2272
+ }
2273
+ }
2274
+
2275
+ console.log('');
2276
+ console.log(c('gray', ` Full scan: npx thuban scan ${paths[0]}`));
2277
+ console.log(c('gray', ` Full scan: npx thuban scan ${paths[1]}`));
2278
+ console.log('');
2279
+ }
2280
+
1789
2281
  async function main() {
1790
2282
  const args = process.argv.slice(2);
1791
2283
  const opts = parseArgs(args);
@@ -1802,9 +2294,9 @@ async function main() {
1802
2294
  }
1803
2295
 
1804
2296
  // Verify target path exists
1805
- const noPathCommands2 = ['activate', 'status', 'upgrade'];
2297
+ const noPathCommands2 = ['activate', 'status', 'upgrade', 'telemetry', 'compare'];
1806
2298
 
1807
- if (!noPathCommands2.includes(opts.command) && !fs.existsSync(opts.targetPath)) {
2299
+ if (!noPathCommands2.includes(opts.command) && !opts.targetPath.match(/^(https?:\/\/|git@)/) && !fs.existsSync(opts.targetPath)) {
1808
2300
  console.error(c('red', ` Error: Path not found: ${opts.targetPath}`));
1809
2301
  process.exit(1);
1810
2302
  }
@@ -1887,9 +2379,27 @@ async function main() {
1887
2379
  case 'baseline':
1888
2380
  await cmdBaseline(opts);
1889
2381
  break;
2382
+ case 'telemetry':
2383
+ await cmdTelemetry(opts);
2384
+ break;
2385
+ case 'compare':
2386
+ await cmdCompare(opts);
2387
+ break;
1890
2388
  default:
2389
+ console.error('');
1891
2390
  console.error(c('red', ` Unknown command: ${opts.command}`));
1892
- console.error(c('gray', ` Run 'thuban --help' for usage`));
2391
+ console.error('');
2392
+ // Suggest closest match
2393
+ 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'];
2394
+ const suggestions = allCmds.filter(cmd => cmd.startsWith(opts.command.slice(0,2)) || cmd.includes(opts.command));
2395
+ if (suggestions.length > 0) {
2396
+ console.error(c('gray', ` Did you mean: `) + c('cyan', suggestions.join(', ')) + c('gray', '?'));
2397
+ console.error('');
2398
+ }
2399
+ console.error(c('gray', ' Quick start:'));
2400
+ console.error(` ${c('cyan', 'npx thuban scan .')} Scan your project`);
2401
+ console.error(` ${c('cyan', 'npx thuban --help')} See all commands`);
2402
+ console.error('');
1893
2403
  process.exit(1);
1894
2404
  }
1895
2405
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thuban",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "The safety layer for AI-coded software. Detect hallucinated APIs, ghost code, tech debt, and architecture drift. One command. Zero dependencies.",
5
5
  "bin": {
6
6
  "thuban": "./cli.js"
@@ -333,9 +333,10 @@ class CodeScanner extends EventEmitter {
333
333
  const content = fs.readFileSync(filePath, 'utf8');
334
334
  const lines = content.split('\n');
335
335
  const ext = path.extname(filePath).toLowerCase();
336
+ const supportedExts = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs'];
336
337
 
337
- // Only scan JS/TS files with pattern matching
338
- if (['.js', '.ts', '.jsx', '.tsx'].includes(ext)) {
338
+ // Scan supported source files with pattern matching
339
+ if (supportedExts.includes(ext)) {
339
340
  // Security scan
340
341
  issues.push(...this._runPatternScan(filePath, content, lines, this.securityPatterns, 'security'));
341
342
 
@@ -47,7 +47,7 @@ class DependencyGraph extends EventEmitter {
47
47
  * Build the full dependency graph
48
48
  */
49
49
  async build() {
50
- console.log('[DEPENDENCY-GRAPH] Building dependency graph...');
50
+ // debug logging silenced for clean output
51
51
  const startTime = Date.now();
52
52
 
53
53
  // Clear existing graph
@@ -62,7 +62,6 @@ class DependencyGraph extends EventEmitter {
62
62
 
63
63
  // Discover all files
64
64
  const files = await this._discoverFiles(this.config.rootPath);
65
- console.log(`[DEPENDENCY-GRAPH] Found ${files.length} files to analyze`);
66
65
 
67
66
  // First pass: extract all imports
68
67
  for (const file of files) {
@@ -76,8 +75,6 @@ class DependencyGraph extends EventEmitter {
76
75
  this._detectCircularDependencies();
77
76
 
78
77
  const duration = Date.now() - startTime;
79
- console.log(`[DEPENDENCY-GRAPH] Graph built in ${duration}ms`);
80
- console.log(`[DEPENDENCY-GRAPH] ${this.stats.filesAnalyzed} files, ${this.stats.totalImports} imports, ${this.stats.unresolvedImports} unresolved`);
81
78
 
82
79
  return this.getStats();
83
80
  }
@@ -36,7 +36,6 @@ class DriftDetector extends EventEmitter {
36
36
  * Detect drift across all files
37
37
  */
38
38
  async detectAll() {
39
- console.log('[DRIFT-DETECTOR] Starting drift detection...');
40
39
  const startTime = Date.now();
41
40
 
42
41
  const result = {
@@ -86,7 +85,6 @@ class DriftDetector extends EventEmitter {
86
85
  result.issues.push(...this._analyzeCrossFile(result.moduleMap));
87
86
 
88
87
  result.duration = Date.now() - startTime;
89
- console.log(`[DRIFT-DETECTOR] Detection complete: ${result.filesAnalyzed} files, ${result.issues.length} drift issues`);
90
88
 
91
89
  return result;
92
90
  } catch (error) {
@@ -69,6 +69,81 @@ class HallucinationDetector {
69
69
  { pattern: /process\.exit\s*\(\s*['"]/, suggestion: 'process.exit() takes a number, not a string', name: 'process.exit(string)', id: 'HALL_API018' },
70
70
  ];
71
71
 
72
+ // ─── Python phantom APIs ────────────────────────────────
73
+ this.pythonPhantomAPIs = [
74
+ { pattern: /os\.path\.exists_sync\s*\(/, suggestion: 'Use os.path.exists() — exists_sync does not exist in Python', name: 'os.path.exists_sync', id: 'PY_HALL001' },
75
+ { pattern: /json\.tryParse\s*\(/, suggestion: 'Use json.loads() with try/except', name: 'json.tryParse', id: 'PY_HALL002' },
76
+ { pattern: /list\.flatMap\s*\(/, suggestion: 'Python lists have no flatMap — use list comprehension', name: 'list.flatMap', id: 'PY_HALL003' },
77
+ { pattern: /dict\.merge\s*\(/, suggestion: 'Use {**dict1, **dict2} or dict1 | dict2 (3.9+)', name: 'dict.merge', id: 'PY_HALL004' },
78
+ { pattern: /string\.format_map\s*\((?!.*\bself\b)/, suggestion: 'str.format_map exists but is rarely correct — did you mean .format()?', name: 'string.format_map misuse', id: 'PY_HALL005' },
79
+ { pattern: /from\s+collections\s+import\s+OrderedDict.*#.*maintain\s+order/i, suggestion: 'Regular dict maintains order since Python 3.7 — OrderedDict is unnecessary', name: 'Unnecessary OrderedDict', id: 'PY_HALL006' },
80
+ { pattern: /async\s+def\s+\w+.*asyncio\.sleep\s*\(\s*0\s*\)\s*#.*yield/i, suggestion: 'asyncio.sleep(0) to yield is a code smell — review async design', name: 'asyncio.sleep(0) hack', id: 'PY_HALL007' },
81
+ { pattern: /import\s+tensorflow\.v2/, suggestion: 'tensorflow.v2 is not a real module — use import tensorflow', name: 'tensorflow.v2', id: 'PY_HALL008' },
82
+ { pattern: /from\s+sklearn\.model_selection\s+import\s+train_test_split_v2/, suggestion: 'train_test_split_v2 does not exist — use train_test_split', name: 'sklearn v2 hallucination', id: 'PY_HALL009' },
83
+ { pattern: /requests\.async_get\s*\(/, suggestion: 'requests has no async_get — use aiohttp or httpx', name: 'requests.async_get', id: 'PY_HALL010' },
84
+ ];
85
+
86
+ this.pythonDeprecated = [
87
+ { pattern: /from\s+distutils/, suggestion: 'distutils removed in Python 3.12 — use setuptools', since: 'Python 3.12', name: 'distutils', id: 'PY_DEPR001' },
88
+ { pattern: /from\s+imp\s+import/, suggestion: 'imp removed in Python 3.12 — use importlib', since: 'Python 3.12', name: 'imp module', id: 'PY_DEPR002' },
89
+ { pattern: /asyncio\.get_event_loop\(\)/, suggestion: 'Deprecated in 3.10+ — use asyncio.run() or get_running_loop()', since: 'Python 3.10', name: 'asyncio.get_event_loop()', id: 'PY_DEPR003' },
90
+ { pattern: /collections\.MutableMapping/, suggestion: 'Moved to collections.abc.MutableMapping in Python 3.3+', since: 'Python 3.9', name: 'collections.MutableMapping', id: 'PY_DEPR004' },
91
+ { pattern: /optparse\.OptionParser/, suggestion: 'optparse deprecated since Python 3.2 — use argparse', since: 'Python 3.2', name: 'optparse', id: 'PY_DEPR005' },
92
+ { pattern: /cgi\.parse_header\s*\(/, suggestion: 'cgi module deprecated in Python 3.11, removed in 3.13', since: 'Python 3.11', name: 'cgi module', id: 'PY_DEPR006' },
93
+ { pattern: /from\s+typing\s+import\s+(List|Dict|Tuple|Set)\b/, suggestion: 'Use built-in list, dict, tuple, set for type hints (Python 3.9+)', since: 'Python 3.9', name: 'typing.List/Dict/Tuple', id: 'PY_DEPR007' },
94
+ { pattern: /unittest\.makeSuite\s*\(/, suggestion: 'makeSuite deprecated — use TestLoader.loadTestsFromTestCase', since: 'Python 3.11', name: 'unittest.makeSuite', id: 'PY_DEPR008' },
95
+ ];
96
+
97
+ this.pythonSmells = [
98
+ { pattern: /print\s*\(\s*f?['"]\s*debug/i, id: 'PY_SMELL001', name: 'Debug Print', message: 'Debug print statement left in code' },
99
+ { pattern: /except\s*:\s*$|except\s+Exception\s*:\s*\n\s*pass/m, id: 'PY_SMELL002', name: 'Bare Except', message: 'Bare except or swallowed exception — hides real errors' },
100
+ { pattern: /#\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'PY_SMELL003', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
101
+ { pattern: /raise\s+NotImplementedError\s*\(?\s*\)?/, id: 'PY_SMELL004', name: 'Not Implemented', message: 'Stub implementation — function body was not generated' },
102
+ { pattern: /['"]your.api.key.here['"]|['"]sk-[.]{3,}['"]/i, id: 'PY_SMELL005', name: 'Dummy Credential', message: 'Placeholder credential — will fail at runtime' },
103
+ { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'PY_SMELL006', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
104
+ { pattern: /import\s+\*/, id: 'PY_SMELL007', name: 'Wildcard Import', message: 'Wildcard import — pollutes namespace, hides dependencies' },
105
+ ];
106
+
107
+ // ─── Go phantom APIs ────────────────────────────────────
108
+ this.goPhantomAPIs = [
109
+ { pattern: /ioutil\.ReadFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.ReadFile()', name: 'ioutil.ReadFile', id: 'GO_HALL001' },
110
+ { pattern: /ioutil\.WriteFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.WriteFile()', name: 'ioutil.WriteFile', id: 'GO_HALL002' },
111
+ { pattern: /ioutil\.TempDir\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.MkdirTemp()', name: 'ioutil.TempDir', id: 'GO_HALL003' },
112
+ { pattern: /ioutil\.ReadAll\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use io.ReadAll()', name: 'ioutil.ReadAll', id: 'GO_HALL004' },
113
+ { pattern: /strings\.Title\s*\(/, suggestion: 'strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text', name: 'strings.Title', id: 'GO_HALL005' },
114
+ ];
115
+
116
+ this.goSmells = [
117
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'GO_SMELL001', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
118
+ { pattern: /panic\s*\(\s*["']not implemented["']\s*\)/, id: 'GO_SMELL002', name: 'Panic Not Implemented', message: 'Stub implementation — will panic at runtime' },
119
+ { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'GO_SMELL003', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
120
+ ];
121
+
122
+ // ─── Rust phantom APIs ──────────────────────────────────
123
+ this.rustSmells = [
124
+ { pattern: /todo!\s*\(\s*\)/, id: 'RS_SMELL001', name: 'todo! macro', message: 'todo!() macro — will panic at runtime' },
125
+ { pattern: /unimplemented!\s*\(\s*\)/, id: 'RS_SMELL002', name: 'unimplemented! macro', message: 'unimplemented!() macro — will panic at runtime' },
126
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'RS_SMELL003', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
127
+ { pattern: /unwrap\(\).*unwrap\(\)/, id: 'RS_SMELL004', name: 'Double Unwrap', message: 'Chained unwrap() — will panic on None/Err' },
128
+ { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'RS_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
129
+ ];
130
+
131
+ // ─── Java/C# common issues ──────────────────────────────
132
+ this.javaSmells = [
133
+ { pattern: /System\.out\.println\s*\(.*debug/i, id: 'JAVA_SMELL001', name: 'Debug Println', message: 'Debug System.out.println — use a logger' },
134
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'JAVA_SMELL002', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
135
+ { pattern: /throw\s+new\s+(?:Runtime)?Exception\s*\(\s*["']Not\s+implemented["']/i, id: 'JAVA_SMELL003', name: 'Not Implemented', message: 'Stub implementation — will throw at runtime' },
136
+ { pattern: /catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/, id: 'JAVA_SMELL004', name: 'Empty Catch', message: 'Empty catch block — swallows all exceptions' },
137
+ { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'JAVA_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
138
+ ];
139
+
140
+ // Language extensions mapping
141
+ this.jsExtensions = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']);
142
+ this.pyExtensions = new Set(['.py', '.pyw']);
143
+ this.goExtensions = new Set(['.go']);
144
+ this.rustExtensions = new Set(['.rs']);
145
+ this.javaExtensions = new Set(['.java', '.kt', '.cs']);
146
+
72
147
  // Patterns suggesting AI-generated code with common issues
73
148
  this.aiCodeSmells = [
74
149
  { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)\s*(this|here|later)/i, id: 'AI_SMELL001', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished implementation' },
@@ -122,7 +197,13 @@ class HallucinationDetector {
122
197
  for (const file of files) {
123
198
  try {
124
199
  const ext = path.extname(file).toLowerCase();
125
- if (!['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'].includes(ext)) continue;
200
+ const isJS = this.jsExtensions.has(ext);
201
+ const isPython = this.pyExtensions.has(ext);
202
+ const isGo = this.goExtensions.has(ext);
203
+ const isRust = this.rustExtensions.has(ext);
204
+ const isJava = this.javaExtensions.has(ext);
205
+
206
+ if (!isJS && !isPython && !isGo && !isRust && !isJava) continue;
126
207
 
127
208
  const relPath = path.relative(this.config.rootPath, file);
128
209
 
@@ -147,20 +228,31 @@ class HallucinationDetector {
147
228
 
148
229
  results.stats.filesScanned++;
149
230
 
150
- // 1. Phantom imports — packages that don't exist
151
- this._checkPhantomImports(relPath, content, lines, declaredDeps, results, ignoredLines);
152
-
153
- // 2. Phantom APIs methods that don't exist on known objects
154
- if (!isPatternFile) {
155
- this._checkPhantomAPIs(relPath, content, lines, results, ignoredLines);
156
- }
157
-
158
- // 3. AI code smells — patterns typical of AI-generated code
159
- this._checkAISmells(relPath, content, lines, results, ignoredLines);
160
-
161
- // 4. Deprecated APIs — APIs that LLMs still suggest
162
- if (!isPatternFile) {
163
- this._checkDeprecatedAPIs(relPath, content, lines, results, ignoredLines);
231
+ // ─── Language-specific checks ──────────────────────
232
+ if (isJS) {
233
+ // 1. Phantom imports — packages that don't exist
234
+ this._checkPhantomImports(relPath, content, lines, declaredDeps, results, ignoredLines);
235
+ // 2. Phantom APIs — methods that don't exist on known objects
236
+ if (!isPatternFile) {
237
+ this._checkPhantomAPIs(relPath, content, lines, results, ignoredLines);
238
+ }
239
+ // 3. AI code smells
240
+ this._checkAISmells(relPath, content, lines, results, ignoredLines);
241
+ // 4. Deprecated APIs
242
+ if (!isPatternFile) {
243
+ this._checkDeprecatedAPIs(relPath, content, lines, results, ignoredLines);
244
+ }
245
+ } else if (isPython) {
246
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonPhantomAPIs, 'phantomAPIs');
247
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonDeprecated, 'deprecatedAPIs');
248
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.pythonSmells);
249
+ } else if (isGo) {
250
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.goPhantomAPIs, 'phantomAPIs');
251
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.goSmells);
252
+ } else if (isRust) {
253
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.rustSmells);
254
+ } else if (isJava) {
255
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.javaSmells);
164
256
  }
165
257
 
166
258
  } catch (e) { /* skip unreadable */ }
@@ -178,6 +270,59 @@ class HallucinationDetector {
178
270
  return results;
179
271
  }
180
272
 
273
+ /**
274
+ * Generic language pattern checker (for phantom APIs and deprecated APIs)
275
+ */
276
+ _checkLanguagePatterns(relPath, content, lines, results, ignoredLines, patterns, resultKey) {
277
+ for (let i = 0; i < lines.length; i++) {
278
+ if (ignoredLines.has(i + 1)) continue;
279
+ const line = lines[i];
280
+ for (const check of patterns) {
281
+ if (check.pattern.test(line)) {
282
+ const entry = {
283
+ file: relPath,
284
+ line: i + 1,
285
+ name: check.name,
286
+ id: check.id,
287
+ };
288
+ if (check.suggestion) entry.suggestion = check.suggestion;
289
+ if (check.since) {
290
+ entry.since = check.since;
291
+ entry.fix = check.suggestion;
292
+ }
293
+ if (resultKey === 'deprecatedAPIs') {
294
+ results.deprecatedAPIs.push(entry);
295
+ } else {
296
+ results.phantomAPIs.push(entry);
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Generic language smell checker
305
+ */
306
+ _checkLanguageSmells(relPath, content, lines, results, ignoredLines, smells) {
307
+ for (let i = 0; i < lines.length; i++) {
308
+ if (ignoredLines.has(i + 1)) continue;
309
+ const line = lines[i];
310
+ for (const smell of smells) {
311
+ if (smell.pattern.test(line)) {
312
+ results.aiSmells.push({
313
+ file: relPath,
314
+ line: i + 1,
315
+ id: smell.id,
316
+ name: smell.name,
317
+ message: smell.message,
318
+ type: 'ai_smell',
319
+ severity: 'medium',
320
+ });
321
+ }
322
+ }
323
+ }
324
+ }
325
+
181
326
  /**
182
327
  * Format results for terminal output
183
328
  */
@@ -150,6 +150,38 @@ class TechDebtAnalyzer {
150
150
 
151
151
  // ─── 3. Code Quality Debt ───────────────────────────────
152
152
  const qualityIssues = [];
153
+ const HallucinationDetector = require('./hallucination-detector');
154
+ const hallDetector = new HallucinationDetector(this.config);
155
+ const hallResults = await hallDetector.scan(files);
156
+
157
+ // Add deprecated APIs as fixable items
158
+ for (const item of (hallResults.deprecatedAPIs || [])) {
159
+ qualityIssues.push({
160
+ file: path.relative(this.config.rootPath, item.file),
161
+ type: 'deprecated_api',
162
+ severity: 'high',
163
+ message: `${item.name} — deprecated since ${item.since}`,
164
+ fixable: true,
165
+ fixAction: 'update_deprecated_api',
166
+ suggestion: item.fix,
167
+ });
168
+ }
169
+
170
+ // Add hardcoded localhost URLs as fixable items
171
+ for (const item of (hallResults.aiSmells || [])) {
172
+ if (item.message && item.message.includes('localhost')) {
173
+ qualityIssues.push({
174
+ file: path.relative(this.config.rootPath, item.file),
175
+ type: 'hardcoded_localhost',
176
+ severity: 'medium',
177
+ message: item.message,
178
+ fixable: true,
179
+ fixAction: 'wrap_localhost_url',
180
+ suggestion: 'Use environment variable with localhost fallback',
181
+ });
182
+ }
183
+ }
184
+
153
185
  for (const [file, result] of Object.entries(scanResults)) {
154
186
  if (result.issues) {
155
187
  for (const issue of result.issues) {
@@ -320,6 +352,56 @@ class TechDebtAnalyzer {
320
352
  break;
321
353
  }
322
354
 
355
+ case 'update_deprecated_api': {
356
+ const fullPath = path.resolve(this.config.rootPath, item.file);
357
+ let content = fs.readFileSync(fullPath, 'utf-8');
358
+ const before = content;
359
+ // url.parse() → new URL()
360
+ content = content.replace(/url\.parse\(([^)]+)\)/g, 'new URL($1)');
361
+ // new Buffer() → Buffer.from()
362
+ content = content.replace(/new Buffer\(([^)]+)\)/g, 'Buffer.from($1)');
363
+ // require('sys') → require('util')
364
+ content = content.replace(/require\(['"]sys['"]\)/g, "require('util')");
365
+ // util.isArray() → Array.isArray()
366
+ content = content.replace(/util\.isArray\(/g, 'Array.isArray(');
367
+ // util.isFunction() → typeof x === 'function'
368
+ content = content.replace(/util\.isFunction\(([^)]+)\)/g, "typeof $1 === 'function'");
369
+ // fs.exists() callback → fs.existsSync or fs.access
370
+ content = content.replace(/fs\.exists\(([^,]+),\s*/g, 'fs.access($1, ');
371
+ if (content !== before) {
372
+ fs.writeFileSync(fullPath, content, 'utf-8');
373
+ fixes.push({ ...item, status: 'fixed', detail: 'Replaced deprecated API with modern equivalent' });
374
+ } else {
375
+ fixes.push({ ...item, status: 'skipped', reason: 'Pattern not matched for auto-replacement' });
376
+ }
377
+ break;
378
+ }
379
+
380
+ case 'wrap_localhost_url': {
381
+ const fullPath = path.resolve(this.config.rootPath, item.file);
382
+ let content = fs.readFileSync(fullPath, 'utf-8');
383
+ const before = content;
384
+ // Wrap hardcoded localhost URLs in env var fallback
385
+ // Match: 'http://localhost:NNNN' or "http://localhost:NNNN" or `http://localhost:NNNN`
386
+ content = content.replace(
387
+ /(['"`])(https?:\/\/localhost:\d{2,5}(?:\/[^'"`]*)?)\1/g,
388
+ (match, quote, url) => {
389
+ // Generate an env var name from the URL
390
+ const portMatch = url.match(/:(\d+)/);
391
+ const port = portMatch ? portMatch[1] : 'PORT';
392
+ const envVar = `SERVICE_URL_${port}`;
393
+ return `(process.env.${envVar} || ${quote}${url}${quote})`;
394
+ }
395
+ );
396
+ if (content !== before) {
397
+ fs.writeFileSync(fullPath, content, 'utf-8');
398
+ fixes.push({ ...item, status: 'fixed', detail: 'Wrapped localhost URL in env var fallback' });
399
+ } else {
400
+ fixes.push({ ...item, status: 'skipped', reason: 'Pattern not matched for auto-replacement' });
401
+ }
402
+ break;
403
+ }
404
+
323
405
  case 'extract_to_env': {
324
406
  // Don't auto-fix secrets — too risky. Flag for manual review.
325
407
  fixes.push({ ...item, status: 'flagged_for_review', reason: 'Secrets require manual extraction to .env' });