thuban 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -102
- package/cli.js +644 -93
- package/package.json +5 -2
- package/packages/scanner/alert-manager.js +398 -398
- package/packages/scanner/code-scanner.js +758 -713
- package/packages/scanner/copy-paste-detector.js +33 -7
- package/packages/scanner/dependency-graph.js +541 -536
- package/packages/scanner/drift-detector.js +423 -423
- package/packages/scanner/file-collector.js +64 -0
- package/packages/scanner/file-watcher.js +323 -323
- package/packages/scanner/ghost-code-detector.js +80 -5
- package/packages/scanner/hallucination-detector.js +189 -19
- package/packages/scanner/health-checker.js +586 -586
- package/packages/scanner/index.js +100 -92
- package/packages/scanner/license-manager.js +15 -2
- package/packages/scanner/master-health-checker.js +513 -513
- package/packages/scanner/monitor-notifier.js +64 -0
- package/packages/scanner/monitor-service.js +103 -0
- package/packages/scanner/monitor-store.js +117 -0
- package/packages/scanner/pre-commit-gate.js +1 -1
- package/packages/scanner/scan-diff.js +50 -0
- package/packages/scanner/scan-runner.js +172 -0
- package/packages/scanner/secret-scanner.js +612 -0
- package/packages/scanner/secret-scanner.test.js +103 -0
- package/packages/scanner/sentinel-core.js +616 -608
- package/packages/scanner/sentinel-knowledge.js +322 -322
- package/packages/scanner/tech-debt-analyzer.js +46 -28
- package/packages/scanner/widget-generator.js +415 -415
package/cli.js
CHANGED
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
* npx thuban report [path] Full scan + deps + drift combined report
|
|
15
15
|
* npx thuban hallucinate [path] Detect AI hallucinations and phantom imports
|
|
16
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
|
|
17
20
|
* npx thuban dashboard [path] Generate visual HTML dashboard report
|
|
18
21
|
* npx thuban diff [path] Scan only git-changed files
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
const path = require('path');
|
|
22
25
|
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
23
27
|
|
|
24
28
|
// Import engine components
|
|
25
29
|
const CodeScanner = require('./packages/scanner/code-scanner.js');
|
|
@@ -40,6 +44,9 @@ const CopyPasteDriftDetector = require('./packages/scanner/copy-paste-detector.j
|
|
|
40
44
|
const CodebasePassport = require('./packages/scanner/codebase-passport.js');
|
|
41
45
|
const ExecutiveReport = require('./packages/scanner/executive-report.js');
|
|
42
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');
|
|
43
50
|
|
|
44
51
|
// ─── Helpers ───────────────────────────────────────────────
|
|
45
52
|
|
|
@@ -66,7 +73,7 @@ function banner() {
|
|
|
66
73
|
console.log('');
|
|
67
74
|
console.log(c('cyan', ' ╔══════════════════════════════════════╗'));
|
|
68
75
|
console.log(c('cyan', ' ║') + c('bold', ' THUBAN Code Health Engine ') + c('cyan', '║'));
|
|
69
|
-
console.log(c('cyan', ' ║') + c('gray', '
|
|
76
|
+
console.log(c('cyan', ' ║') + c('gray', ' v0.3.3 · Public Beta · thuban.dev ') + c('cyan', '║'));
|
|
70
77
|
console.log(c('cyan', ' ╚══════════════════════════════════════╝'));
|
|
71
78
|
console.log('');
|
|
72
79
|
}
|
|
@@ -93,7 +100,8 @@ function printHelp() {
|
|
|
93
100
|
// ─── SCAN & DETECT ───────────────────────────────────────
|
|
94
101
|
console.log(c('bold', ' SCAN & DETECT'));
|
|
95
102
|
console.log('');
|
|
96
|
-
console.log(` ${c('cyan', 'scan')} [path]
|
|
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`);
|
|
97
105
|
console.log(` ${c('cyan', 'hallucinate')} [path] Find phantom APIs and invented imports`);
|
|
98
106
|
console.log(` ${c('cyan', 'ghosts')} [path] Dead functions nobody calls`);
|
|
99
107
|
console.log(` ${c('cyan', 'ai-score')} [path] Probability each function is AI-generated`);
|
|
@@ -114,6 +122,9 @@ function printHelp() {
|
|
|
114
122
|
console.log(` ${c('cyan', 'inject')} [path] Add Mother Code DNA to every file`);
|
|
115
123
|
console.log(` ${c('cyan', 'gate')} Install pre-commit hook ${c('gray', '(blocks hallucinations)')}`);
|
|
116
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)')}`);
|
|
117
128
|
console.log(` ${c('cyan', 'baseline')} [path] Snapshot issues so future scans show only NEW ones`);
|
|
118
129
|
console.log('');
|
|
119
130
|
|
|
@@ -166,6 +177,9 @@ function printHelp() {
|
|
|
166
177
|
console.log(c('gray', ' # Scan a specific folder, ignore tests'));
|
|
167
178
|
console.log(` ${c('white', 'npx thuban scan ./src --ignore "**/*.test.js"')}`);
|
|
168
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('');
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
function parseArgs(args) {
|
|
@@ -183,6 +197,10 @@ function parseArgs(args) {
|
|
|
183
197
|
licenseKey: null,
|
|
184
198
|
baseline: false,
|
|
185
199
|
baselineCreate: false,
|
|
200
|
+
frequency: 'daily',
|
|
201
|
+
intervalMs: null,
|
|
202
|
+
notify: 'inbox',
|
|
203
|
+
once: false,
|
|
186
204
|
};
|
|
187
205
|
|
|
188
206
|
const noPathCommands = ['activate', 'status', 'upgrade', 'help', 'version'];
|
|
@@ -197,6 +215,10 @@ function parseArgs(args) {
|
|
|
197
215
|
else if (arg === '--git-commit' || arg === '--commit') { opts.gitCommit = true; }
|
|
198
216
|
else if (arg === '--baseline') { opts.baseline = true; }
|
|
199
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; }
|
|
200
222
|
else if (arg === '--verbose') { opts.verbose = true; }
|
|
201
223
|
else if (arg === '--ignore' && args[i + 1]) { opts.ignore.push(args[++i]); }
|
|
202
224
|
else if (arg === '--max-issues' && args[i + 1]) { opts.maxIssues = parseInt(args[++i], 10); }
|
|
@@ -204,7 +226,14 @@ function parseArgs(args) {
|
|
|
204
226
|
else if (arg === '--version' || arg === '-v') { opts.command = 'version'; }
|
|
205
227
|
else if (!opts.command) { opts.command = arg; }
|
|
206
228
|
else if (opts.command === 'activate' && !opts.licenseKey) { opts.licenseKey = arg; }
|
|
207
|
-
else if (!noPathCommands.includes(opts.command)) {
|
|
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
|
+
}
|
|
208
237
|
i++;
|
|
209
238
|
}
|
|
210
239
|
|
|
@@ -213,55 +242,138 @@ function parseArgs(args) {
|
|
|
213
242
|
|
|
214
243
|
// ─── Collect files recursively ───────────────────────────────
|
|
215
244
|
|
|
216
|
-
function
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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)`)}`);
|
|
231
327
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
walk(fullPath);
|
|
238
|
-
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
239
|
-
try {
|
|
240
|
-
const stat = fs.statSync(fullPath);
|
|
241
|
-
if (stat.size <= MAX_FILE_SIZE) {
|
|
242
|
-
files.push(fullPath);
|
|
243
|
-
}
|
|
244
|
-
} catch (e) {
|
|
245
|
-
// Skip files we can't stat
|
|
246
|
-
}
|
|
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}`);
|
|
247
333
|
}
|
|
248
334
|
}
|
|
335
|
+
console.log('');
|
|
249
336
|
}
|
|
250
|
-
|
|
251
|
-
walk(dir);
|
|
252
|
-
return files;
|
|
253
337
|
}
|
|
254
338
|
|
|
255
339
|
// ─── Commands ───────────────────────────────────────────────
|
|
256
340
|
|
|
257
341
|
async function cmdScan(opts) {
|
|
258
342
|
const startTime = Date.now();
|
|
259
|
-
const
|
|
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
|
+
}
|
|
260
371
|
|
|
261
372
|
// ─── License check ───────────────────────────────────────
|
|
262
373
|
const lm = new LicenseManager();
|
|
263
374
|
const tier = lm.getTierConfig();
|
|
264
375
|
const scanCheck = lm.canScan();
|
|
376
|
+
const upgradeUrl = 'https://thuban.dev/pricing';
|
|
265
377
|
|
|
266
378
|
if (!scanCheck.allowed && !opts.json) {
|
|
267
379
|
console.log('');
|
|
@@ -272,8 +384,8 @@ async function cmdScan(opts) {
|
|
|
272
384
|
console.log(` You've used ${c('bold', scanCheck.used + '/' + scanCheck.limit)} free scans this month.`);
|
|
273
385
|
console.log(` Resets on ${c('cyan', scanCheck.resetsOn)}.`);
|
|
274
386
|
console.log('');
|
|
275
|
-
console.log(` ${c('cyan', 'Upgrade to Pro')} for unlimited scans, full reports, and
|
|
276
|
-
console.log(` ${c('bold',
|
|
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)}`);
|
|
277
389
|
console.log('');
|
|
278
390
|
console.log(` Or activate a key: ${c('cyan', 'thuban activate <your-key>')}`);
|
|
279
391
|
console.log('');
|
|
@@ -309,10 +421,11 @@ async function cmdScan(opts) {
|
|
|
309
421
|
// File limit gating
|
|
310
422
|
const files = tier.maxFiles < Infinity ? allFiles.slice(0, tier.maxFiles) : allFiles;
|
|
311
423
|
const isFileCapped = totalFileCount > tier.maxFiles;
|
|
424
|
+
const remainingSummary = isFileCapped ? summarizeRemainingFiles(files, allFiles, targetPath) : null;
|
|
312
425
|
|
|
313
426
|
if (!opts.json && isFileCapped) {
|
|
314
427
|
console.log(c('yellow', ` Free tier: scanning ${tier.maxFiles} of ${totalFileCount} files`));
|
|
315
|
-
console.log(c('gray', `
|
|
428
|
+
console.log(c('gray', ` Partial results below. Upgrade to scan your entire codebase.`));
|
|
316
429
|
console.log('');
|
|
317
430
|
} else if (!opts.json) {
|
|
318
431
|
console.log(c('gray', ` Found ${files.length} files to scan...`));
|
|
@@ -338,12 +451,8 @@ async function cmdScan(opts) {
|
|
|
338
451
|
|
|
339
452
|
// ─── Collect all issues ──────────────────────────────────
|
|
340
453
|
const allIssues = [];
|
|
341
|
-
for (const
|
|
342
|
-
|
|
343
|
-
for (const issue of fileResults.issues) {
|
|
344
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
345
|
-
}
|
|
346
|
-
}
|
|
454
|
+
for (const issue of (scanResults.issues || [])) {
|
|
455
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
347
456
|
}
|
|
348
457
|
|
|
349
458
|
// Add hallucination results as issues
|
|
@@ -389,6 +498,11 @@ async function cmdScan(opts) {
|
|
|
389
498
|
issues: tier.showFullDetails ? allIssues : allIssues.slice(0, tier.teaserIssues),
|
|
390
499
|
totalIssues: allIssues.length,
|
|
391
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 : [],
|
|
392
506
|
}, null, 2));
|
|
393
507
|
return;
|
|
394
508
|
}
|
|
@@ -427,6 +541,7 @@ async function cmdScan(opts) {
|
|
|
427
541
|
|
|
428
542
|
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
429
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', '║'));
|
|
430
545
|
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
431
546
|
console.log(c('cyan', ' ║') + ` Files scanned: ${c('bold', String(files.length).padEnd(6))}` + (isFileCapped ? c('yellow', ` of ${totalFileCount}`) : ' ') + c('cyan', ' ║'));
|
|
432
547
|
console.log(c('cyan', ' ║') + ` Health score: ${c(scoreColor, (grade + ' (' + Math.round(score) + '/100)').padEnd(20))}` + c('cyan', ' ║'));
|
|
@@ -436,6 +551,15 @@ async function cmdScan(opts) {
|
|
|
436
551
|
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
437
552
|
console.log('');
|
|
438
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
|
+
|
|
439
563
|
// ─── CRITICAL: Hallucinated APIs (will crash) ────────────
|
|
440
564
|
if (hallucinations.length > 0) {
|
|
441
565
|
console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)));
|
|
@@ -633,9 +757,12 @@ async function cmdScan(opts) {
|
|
|
633
757
|
}
|
|
634
758
|
|
|
635
759
|
if (isFileCapped) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
760
|
+
printFileCapUpgradePrompt({
|
|
761
|
+
scannedCount: files.length,
|
|
762
|
+
totalCount: totalFileCount,
|
|
763
|
+
remainingSummary,
|
|
764
|
+
upgradeUrl,
|
|
765
|
+
});
|
|
639
766
|
}
|
|
640
767
|
|
|
641
768
|
// ─── Continuous monitoring pitch ────────────────────────
|
|
@@ -720,6 +847,109 @@ async function cmdDeps(opts) {
|
|
|
720
847
|
}
|
|
721
848
|
}
|
|
722
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
|
+
|
|
723
953
|
async function cmdDrift(opts) {
|
|
724
954
|
const startTime = Date.now();
|
|
725
955
|
const targetPath = path.resolve(opts.targetPath);
|
|
@@ -790,7 +1020,11 @@ async function cmdInject(opts) {
|
|
|
790
1020
|
console.log('');
|
|
791
1021
|
}
|
|
792
1022
|
|
|
793
|
-
const files = collectFiles(targetPath, opts.ignore).filter(f =>
|
|
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
|
+
);
|
|
794
1028
|
const graph = new DependencyGraph({ rootPath: targetPath });
|
|
795
1029
|
await graph.build(files);
|
|
796
1030
|
|
|
@@ -809,8 +1043,26 @@ async function cmdInject(opts) {
|
|
|
809
1043
|
continue;
|
|
810
1044
|
}
|
|
811
1045
|
|
|
812
|
-
const
|
|
813
|
-
if (!
|
|
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
|
+
}
|
|
814
1066
|
|
|
815
1067
|
if (opts.fix) {
|
|
816
1068
|
// Actually inject
|
|
@@ -846,7 +1098,7 @@ async function cmdInject(opts) {
|
|
|
846
1098
|
} else {
|
|
847
1099
|
console.log(c('green', ` ✓ Injected Mother Code into ${injected} files`));
|
|
848
1100
|
}
|
|
849
|
-
console.log(` ${c('gray', `Skipped: ${skipped} (already annotated or
|
|
1101
|
+
console.log(` ${c('gray', `Skipped: ${skipped} (already annotated or unsupported)`)}`);
|
|
850
1102
|
console.log('');
|
|
851
1103
|
}
|
|
852
1104
|
|
|
@@ -891,12 +1143,10 @@ async function cmdReport(opts) {
|
|
|
891
1143
|
let totalIssues = 0;
|
|
892
1144
|
let criticalCount = 0;
|
|
893
1145
|
let highCount = 0;
|
|
894
|
-
for (const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
highCount += r.issues.filter(i => i.severity === 'high').length;
|
|
899
|
-
}
|
|
1146
|
+
for (const i of (scanResults.issues || [])) {
|
|
1147
|
+
totalIssues++;
|
|
1148
|
+
if (i.severity === 'critical') criticalCount++;
|
|
1149
|
+
if (i.severity === 'high') highCount++;
|
|
900
1150
|
}
|
|
901
1151
|
|
|
902
1152
|
const orphans = graph.getOrphanFiles();
|
|
@@ -966,6 +1216,8 @@ async function cmdReport(opts) {
|
|
|
966
1216
|
if (orphans.length > 5) console.log(` ${c('gray', '→')} Consider removing ${orphans.length} orphan files`);
|
|
967
1217
|
console.log('');
|
|
968
1218
|
}
|
|
1219
|
+
|
|
1220
|
+
// Clean up cloned repo (clonedRepo only exists in cmdScan scope)
|
|
969
1221
|
}
|
|
970
1222
|
|
|
971
1223
|
// ─── Tech Debt & Fix Commands ────────────────────────────
|
|
@@ -1113,13 +1365,24 @@ async function cmdFix(opts) {
|
|
|
1113
1365
|
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
1114
1366
|
console.log(c('cyan', ' ║') + c('bold', ' FIX RESULTS ') + c('cyan', '║'));
|
|
1115
1367
|
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
1116
|
-
|
|
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
|
+
}
|
|
1117
1380
|
console.log(c('cyan', ' ║') + ` Flagged for review: ${c('yellow', String(result.flagged).padEnd(19))}` + c('cyan', ' ║'));
|
|
1118
1381
|
if (result.failed > 0) {
|
|
1119
1382
|
console.log(c('cyan', ' ║') + ` Failed: ${c('red', String(result.failed).padEnd(20))}` + c('cyan', ' ║'));
|
|
1120
1383
|
}
|
|
1121
|
-
if (result.
|
|
1122
|
-
console.log(c('cyan', ' ║') + ` Skipped
|
|
1384
|
+
if (result.skipped > 0) {
|
|
1385
|
+
console.log(c('cyan', ' ║') + ` Skipped: ${c('gray', String(result.skipped).padEnd(20))}` + c('cyan', ' ║'));
|
|
1123
1386
|
}
|
|
1124
1387
|
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
1125
1388
|
console.log('');
|
|
@@ -1136,9 +1399,9 @@ async function cmdFix(opts) {
|
|
|
1136
1399
|
console.log('');
|
|
1137
1400
|
}
|
|
1138
1401
|
|
|
1139
|
-
if (result.
|
|
1140
|
-
console.log(c('gray', ` ${result.
|
|
1141
|
-
console.log(c('gray', ' Review them
|
|
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 .'));
|
|
1142
1405
|
console.log('');
|
|
1143
1406
|
}
|
|
1144
1407
|
|
|
@@ -1363,12 +1626,11 @@ async function cmdDiff(opts) {
|
|
|
1363
1626
|
let totalIssues = 0;
|
|
1364
1627
|
const fileIssues = {};
|
|
1365
1628
|
|
|
1366
|
-
for (const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
}
|
|
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++;
|
|
1372
1634
|
}
|
|
1373
1635
|
|
|
1374
1636
|
totalIssues += hallResults.stats.totalIssues;
|
|
@@ -1471,7 +1733,7 @@ function cmdUpgrade() {
|
|
|
1471
1733
|
console.log(c('bold', ' THUBAN PRICING'));
|
|
1472
1734
|
console.log('');
|
|
1473
1735
|
console.log(` ${c('gray', 'FREE')} ${c('bold', '£0/mo')}`);
|
|
1474
|
-
console.log(`
|
|
1736
|
+
console.log(` 5 scans/month, 100 files, summary only`);
|
|
1475
1737
|
console.log('');
|
|
1476
1738
|
console.log(` ${c('cyan', 'PRO')} ${c('bold', '£19/mo')} ${c('gray', '(or £149/year — save 35%)')}`);
|
|
1477
1739
|
console.log(` Unlimited scans, full details, auto-fix, HTML dashboard`);
|
|
@@ -1585,12 +1847,8 @@ async function cmdCost(opts) {
|
|
|
1585
1847
|
|
|
1586
1848
|
// Collect all issues
|
|
1587
1849
|
const allIssues = [];
|
|
1588
|
-
for (const
|
|
1589
|
-
|
|
1590
|
-
for (const issue of fileResults.issues) {
|
|
1591
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1850
|
+
for (const issue of (scanResults.issues || [])) {
|
|
1851
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
1594
1852
|
}
|
|
1595
1853
|
for (const item of hallResults.phantomAPIs) {
|
|
1596
1854
|
allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
|
|
@@ -1831,12 +2089,8 @@ async function cmdExecutive(opts) {
|
|
|
1831
2089
|
|
|
1832
2090
|
// Collect all issues for cost calculator
|
|
1833
2091
|
const allIssues = [];
|
|
1834
|
-
for (const
|
|
1835
|
-
|
|
1836
|
-
for (const issue of fileResults.issues) {
|
|
1837
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
2092
|
+
for (const issue of (scanResults.issues || [])) {
|
|
2093
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
1840
2094
|
}
|
|
1841
2095
|
for (const item of hallResults.phantomAPIs) {
|
|
1842
2096
|
allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
|
|
@@ -1910,12 +2164,8 @@ async function cmdBaseline(opts) {
|
|
|
1910
2164
|
]);
|
|
1911
2165
|
|
|
1912
2166
|
const allIssues = [];
|
|
1913
|
-
for (const
|
|
1914
|
-
|
|
1915
|
-
for (const issue of fileResults.issues) {
|
|
1916
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
2167
|
+
for (const issue of (scanResults.issues || [])) {
|
|
2168
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
1919
2169
|
}
|
|
1920
2170
|
for (const item of hallResults.phantomAPIs) {
|
|
1921
2171
|
allIssues.push({ file: item.file, category: 'hallucination', severity: 'critical', id: item.id, message: item.name });
|
|
@@ -1948,6 +2198,292 @@ async function cmdBaseline(opts) {
|
|
|
1948
2198
|
}
|
|
1949
2199
|
}
|
|
1950
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
|
+
|
|
1951
2487
|
async function main() {
|
|
1952
2488
|
const args = process.argv.slice(2);
|
|
1953
2489
|
const opts = parseArgs(args);
|
|
@@ -1964,9 +2500,9 @@ async function main() {
|
|
|
1964
2500
|
}
|
|
1965
2501
|
|
|
1966
2502
|
// Verify target path exists
|
|
1967
|
-
const noPathCommands2 = ['activate', 'status', 'upgrade'];
|
|
2503
|
+
const noPathCommands2 = ['activate', 'status', 'upgrade', 'telemetry', 'compare'];
|
|
1968
2504
|
|
|
1969
|
-
if (!noPathCommands2.includes(opts.command) && !fs.existsSync(opts.targetPath)) {
|
|
2505
|
+
if (!noPathCommands2.includes(opts.command) && !opts.targetPath.match(/^(https?:\/\/|git@)/) && !fs.existsSync(opts.targetPath)) {
|
|
1970
2506
|
console.error(c('red', ` Error: Path not found: ${opts.targetPath}`));
|
|
1971
2507
|
process.exit(1);
|
|
1972
2508
|
}
|
|
@@ -2005,6 +2541,15 @@ async function main() {
|
|
|
2005
2541
|
case 'watch':
|
|
2006
2542
|
await cmdWatch(opts);
|
|
2007
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;
|
|
2008
2553
|
case 'dashboard':
|
|
2009
2554
|
case 'dash':
|
|
2010
2555
|
await cmdDashboard(opts);
|
|
@@ -2049,12 +2594,18 @@ async function main() {
|
|
|
2049
2594
|
case 'baseline':
|
|
2050
2595
|
await cmdBaseline(opts);
|
|
2051
2596
|
break;
|
|
2597
|
+
case 'telemetry':
|
|
2598
|
+
await cmdTelemetry(opts);
|
|
2599
|
+
break;
|
|
2600
|
+
case 'compare':
|
|
2601
|
+
await cmdCompare(opts);
|
|
2602
|
+
break;
|
|
2052
2603
|
default:
|
|
2053
2604
|
console.error('');
|
|
2054
2605
|
console.error(c('red', ` Unknown command: ${opts.command}`));
|
|
2055
2606
|
console.error('');
|
|
2056
2607
|
// Suggest closest match
|
|
2057
|
-
const allCmds = ['scan','deps','drift','inject','health','report','debt','fix','hallucinate','watch','dashboard','diff','gate','cost','ghosts','ai-score','clones','passport','executive','baseline','activate','status','upgrade'];
|
|
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'];
|
|
2058
2609
|
const suggestions = allCmds.filter(cmd => cmd.startsWith(opts.command.slice(0,2)) || cmd.includes(opts.command));
|
|
2059
2610
|
if (suggestions.length > 0) {
|
|
2060
2611
|
console.error(c('gray', ` Did you mean: `) + c('cyan', suggestions.join(', ')) + c('gray', '?'));
|