thuban 0.3.2 → 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 +310 -95
- 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,6 +14,9 @@
|
|
|
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
|
*/
|
|
@@ -41,6 +44,9 @@ const CopyPasteDriftDetector = require('./packages/scanner/copy-paste-detector.j
|
|
|
41
44
|
const CodebasePassport = require('./packages/scanner/codebase-passport.js');
|
|
42
45
|
const ExecutiveReport = require('./packages/scanner/executive-report.js');
|
|
43
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');
|
|
44
50
|
|
|
45
51
|
// ─── Helpers ───────────────────────────────────────────────
|
|
46
52
|
|
|
@@ -67,7 +73,7 @@ function banner() {
|
|
|
67
73
|
console.log('');
|
|
68
74
|
console.log(c('cyan', ' ╔══════════════════════════════════════╗'));
|
|
69
75
|
console.log(c('cyan', ' ║') + c('bold', ' THUBAN Code Health Engine ') + c('cyan', '║'));
|
|
70
|
-
console.log(c('cyan', ' ║') + c('gray', '
|
|
76
|
+
console.log(c('cyan', ' ║') + c('gray', ' v0.3.3 · Public Beta · thuban.dev ') + c('cyan', '║'));
|
|
71
77
|
console.log(c('cyan', ' ╚══════════════════════════════════════╝'));
|
|
72
78
|
console.log('');
|
|
73
79
|
}
|
|
@@ -116,6 +122,9 @@ function printHelp() {
|
|
|
116
122
|
console.log(` ${c('cyan', 'inject')} [path] Add Mother Code DNA to every file`);
|
|
117
123
|
console.log(` ${c('cyan', 'gate')} Install pre-commit hook ${c('gray', '(blocks hallucinations)')}`);
|
|
118
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)')}`);
|
|
119
128
|
console.log(` ${c('cyan', 'baseline')} [path] Snapshot issues so future scans show only NEW ones`);
|
|
120
129
|
console.log('');
|
|
121
130
|
|
|
@@ -188,6 +197,10 @@ function parseArgs(args) {
|
|
|
188
197
|
licenseKey: null,
|
|
189
198
|
baseline: false,
|
|
190
199
|
baselineCreate: false,
|
|
200
|
+
frequency: 'daily',
|
|
201
|
+
intervalMs: null,
|
|
202
|
+
notify: 'inbox',
|
|
203
|
+
once: false,
|
|
191
204
|
};
|
|
192
205
|
|
|
193
206
|
const noPathCommands = ['activate', 'status', 'upgrade', 'help', 'version'];
|
|
@@ -202,6 +215,10 @@ function parseArgs(args) {
|
|
|
202
215
|
else if (arg === '--git-commit' || arg === '--commit') { opts.gitCommit = true; }
|
|
203
216
|
else if (arg === '--baseline') { opts.baseline = true; }
|
|
204
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; }
|
|
205
222
|
else if (arg === '--verbose') { opts.verbose = true; }
|
|
206
223
|
else if (arg === '--ignore' && args[i + 1]) { opts.ignore.push(args[++i]); }
|
|
207
224
|
else if (arg === '--max-issues' && args[i + 1]) { opts.maxIssues = parseInt(args[++i], 10); }
|
|
@@ -225,43 +242,98 @@ function parseArgs(args) {
|
|
|
225
242
|
|
|
226
243
|
// ─── Collect files recursively ───────────────────────────────
|
|
227
244
|
|
|
228
|
-
function
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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)`)}`);
|
|
243
327
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
walk(fullPath);
|
|
250
|
-
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
251
|
-
try {
|
|
252
|
-
const stat = fs.statSync(fullPath);
|
|
253
|
-
if (stat.size <= MAX_FILE_SIZE) {
|
|
254
|
-
files.push(fullPath);
|
|
255
|
-
}
|
|
256
|
-
} catch (e) {
|
|
257
|
-
// Skip files we can't stat
|
|
258
|
-
}
|
|
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}`);
|
|
259
333
|
}
|
|
260
334
|
}
|
|
335
|
+
console.log('');
|
|
261
336
|
}
|
|
262
|
-
|
|
263
|
-
walk(dir);
|
|
264
|
-
return files;
|
|
265
337
|
}
|
|
266
338
|
|
|
267
339
|
// ─── Commands ───────────────────────────────────────────────
|
|
@@ -281,9 +353,9 @@ async function cmdScan(opts) {
|
|
|
281
353
|
console.log(c('gray', ` ${opts.targetPath}`));
|
|
282
354
|
}
|
|
283
355
|
const tmpDir = path.join(os.tmpdir(), 'thuban-scan-' + Date.now());
|
|
284
|
-
const {
|
|
356
|
+
const { execFileSync } = require('child_process');
|
|
285
357
|
try {
|
|
286
|
-
|
|
358
|
+
execFileSync('git', ['clone', '--depth', '1', opts.targetPath, tmpDir], { stdio: 'pipe', timeout: 60000 });
|
|
287
359
|
targetPath = tmpDir;
|
|
288
360
|
clonedRepo = tmpDir;
|
|
289
361
|
if (!opts.json) {
|
|
@@ -301,6 +373,7 @@ async function cmdScan(opts) {
|
|
|
301
373
|
const lm = new LicenseManager();
|
|
302
374
|
const tier = lm.getTierConfig();
|
|
303
375
|
const scanCheck = lm.canScan();
|
|
376
|
+
const upgradeUrl = 'https://thuban.dev/pricing';
|
|
304
377
|
|
|
305
378
|
if (!scanCheck.allowed && !opts.json) {
|
|
306
379
|
console.log('');
|
|
@@ -311,8 +384,8 @@ async function cmdScan(opts) {
|
|
|
311
384
|
console.log(` You've used ${c('bold', scanCheck.used + '/' + scanCheck.limit)} free scans this month.`);
|
|
312
385
|
console.log(` Resets on ${c('cyan', scanCheck.resetsOn)}.`);
|
|
313
386
|
console.log('');
|
|
314
|
-
console.log(` ${c('cyan', 'Upgrade to Pro')} for unlimited scans, full reports, and
|
|
315
|
-
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)}`);
|
|
316
389
|
console.log('');
|
|
317
390
|
console.log(` Or activate a key: ${c('cyan', 'thuban activate <your-key>')}`);
|
|
318
391
|
console.log('');
|
|
@@ -348,10 +421,11 @@ async function cmdScan(opts) {
|
|
|
348
421
|
// File limit gating
|
|
349
422
|
const files = tier.maxFiles < Infinity ? allFiles.slice(0, tier.maxFiles) : allFiles;
|
|
350
423
|
const isFileCapped = totalFileCount > tier.maxFiles;
|
|
424
|
+
const remainingSummary = isFileCapped ? summarizeRemainingFiles(files, allFiles, targetPath) : null;
|
|
351
425
|
|
|
352
426
|
if (!opts.json && isFileCapped) {
|
|
353
427
|
console.log(c('yellow', ` Free tier: scanning ${tier.maxFiles} of ${totalFileCount} files`));
|
|
354
|
-
console.log(c('gray', `
|
|
428
|
+
console.log(c('gray', ` Partial results below. Upgrade to scan your entire codebase.`));
|
|
355
429
|
console.log('');
|
|
356
430
|
} else if (!opts.json) {
|
|
357
431
|
console.log(c('gray', ` Found ${files.length} files to scan...`));
|
|
@@ -377,12 +451,8 @@ async function cmdScan(opts) {
|
|
|
377
451
|
|
|
378
452
|
// ─── Collect all issues ──────────────────────────────────
|
|
379
453
|
const allIssues = [];
|
|
380
|
-
for (const
|
|
381
|
-
|
|
382
|
-
for (const issue of fileResults.issues) {
|
|
383
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
384
|
-
}
|
|
385
|
-
}
|
|
454
|
+
for (const issue of (scanResults.issues || [])) {
|
|
455
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
386
456
|
}
|
|
387
457
|
|
|
388
458
|
// Add hallucination results as issues
|
|
@@ -428,6 +498,11 @@ async function cmdScan(opts) {
|
|
|
428
498
|
issues: tier.showFullDetails ? allIssues : allIssues.slice(0, tier.teaserIssues),
|
|
429
499
|
totalIssues: allIssues.length,
|
|
430
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 : [],
|
|
431
506
|
}, null, 2));
|
|
432
507
|
return;
|
|
433
508
|
}
|
|
@@ -466,6 +541,7 @@ async function cmdScan(opts) {
|
|
|
466
541
|
|
|
467
542
|
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
468
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', '║'));
|
|
469
545
|
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
470
546
|
console.log(c('cyan', ' ║') + ` Files scanned: ${c('bold', String(files.length).padEnd(6))}` + (isFileCapped ? c('yellow', ` of ${totalFileCount}`) : ' ') + c('cyan', ' ║'));
|
|
471
547
|
console.log(c('cyan', ' ║') + ` Health score: ${c(scoreColor, (grade + ' (' + Math.round(score) + '/100)').padEnd(20))}` + c('cyan', ' ║'));
|
|
@@ -475,6 +551,15 @@ async function cmdScan(opts) {
|
|
|
475
551
|
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
476
552
|
console.log('');
|
|
477
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
|
+
|
|
478
563
|
// ─── CRITICAL: Hallucinated APIs (will crash) ────────────
|
|
479
564
|
if (hallucinations.length > 0) {
|
|
480
565
|
console.log(c('bgRed', ' CRITICAL ') + c('red', c('bold', ` ${hallucinations.length} Hallucinated API${hallucinations.length > 1 ? 's' : ''}`)));
|
|
@@ -672,9 +757,12 @@ async function cmdScan(opts) {
|
|
|
672
757
|
}
|
|
673
758
|
|
|
674
759
|
if (isFileCapped) {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
760
|
+
printFileCapUpgradePrompt({
|
|
761
|
+
scannedCount: files.length,
|
|
762
|
+
totalCount: totalFileCount,
|
|
763
|
+
remainingSummary,
|
|
764
|
+
upgradeUrl,
|
|
765
|
+
});
|
|
678
766
|
}
|
|
679
767
|
|
|
680
768
|
// ─── Continuous monitoring pitch ────────────────────────
|
|
@@ -759,6 +847,109 @@ async function cmdDeps(opts) {
|
|
|
759
847
|
}
|
|
760
848
|
}
|
|
761
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
|
+
|
|
762
953
|
async function cmdDrift(opts) {
|
|
763
954
|
const startTime = Date.now();
|
|
764
955
|
const targetPath = path.resolve(opts.targetPath);
|
|
@@ -829,7 +1020,11 @@ async function cmdInject(opts) {
|
|
|
829
1020
|
console.log('');
|
|
830
1021
|
}
|
|
831
1022
|
|
|
832
|
-
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
|
+
);
|
|
833
1028
|
const graph = new DependencyGraph({ rootPath: targetPath });
|
|
834
1029
|
await graph.build(files);
|
|
835
1030
|
|
|
@@ -848,8 +1043,26 @@ async function cmdInject(opts) {
|
|
|
848
1043
|
continue;
|
|
849
1044
|
}
|
|
850
1045
|
|
|
851
|
-
const
|
|
852
|
-
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
|
+
}
|
|
853
1066
|
|
|
854
1067
|
if (opts.fix) {
|
|
855
1068
|
// Actually inject
|
|
@@ -885,7 +1098,7 @@ async function cmdInject(opts) {
|
|
|
885
1098
|
} else {
|
|
886
1099
|
console.log(c('green', ` ✓ Injected Mother Code into ${injected} files`));
|
|
887
1100
|
}
|
|
888
|
-
console.log(` ${c('gray', `Skipped: ${skipped} (already annotated or
|
|
1101
|
+
console.log(` ${c('gray', `Skipped: ${skipped} (already annotated or unsupported)`)}`);
|
|
889
1102
|
console.log('');
|
|
890
1103
|
}
|
|
891
1104
|
|
|
@@ -930,12 +1143,10 @@ async function cmdReport(opts) {
|
|
|
930
1143
|
let totalIssues = 0;
|
|
931
1144
|
let criticalCount = 0;
|
|
932
1145
|
let highCount = 0;
|
|
933
|
-
for (const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
highCount += r.issues.filter(i => i.severity === 'high').length;
|
|
938
|
-
}
|
|
1146
|
+
for (const i of (scanResults.issues || [])) {
|
|
1147
|
+
totalIssues++;
|
|
1148
|
+
if (i.severity === 'critical') criticalCount++;
|
|
1149
|
+
if (i.severity === 'high') highCount++;
|
|
939
1150
|
}
|
|
940
1151
|
|
|
941
1152
|
const orphans = graph.getOrphanFiles();
|
|
@@ -1006,10 +1217,7 @@ async function cmdReport(opts) {
|
|
|
1006
1217
|
console.log('');
|
|
1007
1218
|
}
|
|
1008
1219
|
|
|
1009
|
-
// Clean up cloned repo
|
|
1010
|
-
if (clonedRepo) {
|
|
1011
|
-
try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
|
|
1012
|
-
}
|
|
1220
|
+
// Clean up cloned repo (clonedRepo only exists in cmdScan scope)
|
|
1013
1221
|
}
|
|
1014
1222
|
|
|
1015
1223
|
// ─── Tech Debt & Fix Commands ────────────────────────────
|
|
@@ -1157,13 +1365,24 @@ async function cmdFix(opts) {
|
|
|
1157
1365
|
console.log(c('cyan', ' ╔══════════════════════════════════════════════════════╗'));
|
|
1158
1366
|
console.log(c('cyan', ' ║') + c('bold', ' FIX RESULTS ') + c('cyan', '║'));
|
|
1159
1367
|
console.log(c('cyan', ' ╠══════════════════════════════════════════════════════╣'));
|
|
1160
|
-
|
|
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
|
+
}
|
|
1161
1380
|
console.log(c('cyan', ' ║') + ` Flagged for review: ${c('yellow', String(result.flagged).padEnd(19))}` + c('cyan', ' ║'));
|
|
1162
1381
|
if (result.failed > 0) {
|
|
1163
1382
|
console.log(c('cyan', ' ║') + ` Failed: ${c('red', String(result.failed).padEnd(20))}` + c('cyan', ' ║'));
|
|
1164
1383
|
}
|
|
1165
|
-
if (result.
|
|
1166
|
-
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', ' ║'));
|
|
1167
1386
|
}
|
|
1168
1387
|
console.log(c('cyan', ' ╚══════════════════════════════════════════════════════╝'));
|
|
1169
1388
|
console.log('');
|
|
@@ -1180,9 +1399,9 @@ async function cmdFix(opts) {
|
|
|
1180
1399
|
console.log('');
|
|
1181
1400
|
}
|
|
1182
1401
|
|
|
1183
|
-
if (result.
|
|
1184
|
-
console.log(c('gray', ` ${result.
|
|
1185
|
-
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 .'));
|
|
1186
1405
|
console.log('');
|
|
1187
1406
|
}
|
|
1188
1407
|
|
|
@@ -1407,12 +1626,11 @@ async function cmdDiff(opts) {
|
|
|
1407
1626
|
let totalIssues = 0;
|
|
1408
1627
|
const fileIssues = {};
|
|
1409
1628
|
|
|
1410
|
-
for (const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
}
|
|
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++;
|
|
1416
1634
|
}
|
|
1417
1635
|
|
|
1418
1636
|
totalIssues += hallResults.stats.totalIssues;
|
|
@@ -1515,7 +1733,7 @@ function cmdUpgrade() {
|
|
|
1515
1733
|
console.log(c('bold', ' THUBAN PRICING'));
|
|
1516
1734
|
console.log('');
|
|
1517
1735
|
console.log(` ${c('gray', 'FREE')} ${c('bold', '£0/mo')}`);
|
|
1518
|
-
console.log(`
|
|
1736
|
+
console.log(` 5 scans/month, 100 files, summary only`);
|
|
1519
1737
|
console.log('');
|
|
1520
1738
|
console.log(` ${c('cyan', 'PRO')} ${c('bold', '£19/mo')} ${c('gray', '(or £149/year — save 35%)')}`);
|
|
1521
1739
|
console.log(` Unlimited scans, full details, auto-fix, HTML dashboard`);
|
|
@@ -1629,12 +1847,8 @@ async function cmdCost(opts) {
|
|
|
1629
1847
|
|
|
1630
1848
|
// Collect all issues
|
|
1631
1849
|
const allIssues = [];
|
|
1632
|
-
for (const
|
|
1633
|
-
|
|
1634
|
-
for (const issue of fileResults.issues) {
|
|
1635
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1850
|
+
for (const issue of (scanResults.issues || [])) {
|
|
1851
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
1638
1852
|
}
|
|
1639
1853
|
for (const item of hallResults.phantomAPIs) {
|
|
1640
1854
|
allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
|
|
@@ -1875,12 +2089,8 @@ async function cmdExecutive(opts) {
|
|
|
1875
2089
|
|
|
1876
2090
|
// Collect all issues for cost calculator
|
|
1877
2091
|
const allIssues = [];
|
|
1878
|
-
for (const
|
|
1879
|
-
|
|
1880
|
-
for (const issue of fileResults.issues) {
|
|
1881
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
2092
|
+
for (const issue of (scanResults.issues || [])) {
|
|
2093
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
1884
2094
|
}
|
|
1885
2095
|
for (const item of hallResults.phantomAPIs) {
|
|
1886
2096
|
allIssues.push({ category: 'hallucination', severity: 'critical', message: item.name });
|
|
@@ -1954,12 +2164,8 @@ async function cmdBaseline(opts) {
|
|
|
1954
2164
|
]);
|
|
1955
2165
|
|
|
1956
2166
|
const allIssues = [];
|
|
1957
|
-
for (const
|
|
1958
|
-
|
|
1959
|
-
for (const issue of fileResults.issues) {
|
|
1960
|
-
allIssues.push({ file: path.relative(targetPath, file), ...issue });
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
2167
|
+
for (const issue of (scanResults.issues || [])) {
|
|
2168
|
+
allIssues.push({ file: path.relative(targetPath, issue.file), ...issue });
|
|
1963
2169
|
}
|
|
1964
2170
|
for (const item of hallResults.phantomAPIs) {
|
|
1965
2171
|
allIssues.push({ file: item.file, category: 'hallucination', severity: 'critical', id: item.id, message: item.name });
|
|
@@ -2082,8 +2288,8 @@ async function runScanSilent(targetInput) {
|
|
|
2082
2288
|
|
|
2083
2289
|
if (isGitUrl) {
|
|
2084
2290
|
const tmpDir = path.join(os.tmpdir(), 'thuban-compare-' + Date.now() + '-' + Math.random().toString(36).slice(2,6));
|
|
2085
|
-
const {
|
|
2086
|
-
|
|
2291
|
+
const { execFileSync } = require('child_process');
|
|
2292
|
+
execFileSync('git', ['clone', '--depth', '1', targetInput, tmpDir], { stdio: 'pipe', timeout: 60000 });
|
|
2087
2293
|
targetPath = tmpDir;
|
|
2088
2294
|
clonedRepo = tmpDir;
|
|
2089
2295
|
// Extract repo name for label
|
|
@@ -2335,6 +2541,15 @@ async function main() {
|
|
|
2335
2541
|
case 'watch':
|
|
2336
2542
|
await cmdWatch(opts);
|
|
2337
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;
|
|
2338
2553
|
case 'dashboard':
|
|
2339
2554
|
case 'dash':
|
|
2340
2555
|
await cmdDashboard(opts);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thuban",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "The safety layer for AI-coded software. Detect hallucinated APIs, ghost code, tech debt, and architecture drift. One command. Zero dependencies.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"thuban": "./cli.js"
|
|
@@ -45,5 +45,8 @@
|
|
|
45
45
|
"packages/scanner/*.js",
|
|
46
46
|
"README.md",
|
|
47
47
|
"LICENSE"
|
|
48
|
-
]
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"docx": "^9.7.1"
|
|
51
|
+
}
|
|
49
52
|
}
|