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.
- package/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +12 -7
- package/cli.js +0 -2627
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -758
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -276
- package/packages/scanner/dependency-graph.js +0 -541
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-collector.js +0 -64
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -301
- package/packages/scanner/hallucination-detector.js +0 -822
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -100
- package/packages/scanner/license-manager.js +0 -331
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/monitor-notifier.js +0 -64
- package/packages/scanner/monitor-service.js +0 -103
- package/packages/scanner/monitor-store.js +0 -117
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/scan-diff.js +0 -50
- package/packages/scanner/scan-runner.js +0 -172
- package/packages/scanner/secret-scanner.js +0 -612
- package/packages/scanner/secret-scanner.test.js +0 -103
- package/packages/scanner/sentinel-core.js +0 -616
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -583
- package/packages/scanner/tech-debt-cost.js +0 -194
- 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();
|