@vibecheckai/cli 3.1.2 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +60 -33
  2. package/bin/registry.js +319 -34
  3. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  4. package/bin/runners/REPORT_AUDIT.md +64 -0
  5. package/bin/runners/lib/entitlements-v2.js +97 -28
  6. package/bin/runners/lib/entitlements.js +3 -6
  7. package/bin/runners/lib/init-wizard.js +1 -1
  8. package/bin/runners/lib/report-engine.js +459 -280
  9. package/bin/runners/lib/report-html.js +1154 -1423
  10. package/bin/runners/lib/report-output.js +187 -0
  11. package/bin/runners/lib/report-templates.js +848 -850
  12. package/bin/runners/lib/scan-output.js +545 -0
  13. package/bin/runners/lib/server-usage.js +0 -12
  14. package/bin/runners/lib/ship-output.js +641 -0
  15. package/bin/runners/lib/status-output.js +253 -0
  16. package/bin/runners/lib/terminal-ui.js +853 -0
  17. package/bin/runners/runCheckpoint.js +502 -0
  18. package/bin/runners/runContracts.js +105 -0
  19. package/bin/runners/runExport.js +93 -0
  20. package/bin/runners/runFix.js +31 -24
  21. package/bin/runners/runInit.js +377 -112
  22. package/bin/runners/runInstall.js +1 -5
  23. package/bin/runners/runLabs.js +3 -3
  24. package/bin/runners/runPolish.js +2452 -0
  25. package/bin/runners/runProve.js +2 -2
  26. package/bin/runners/runReport.js +251 -200
  27. package/bin/runners/runRuntime.js +110 -0
  28. package/bin/runners/runScan.js +477 -379
  29. package/bin/runners/runSecurity.js +92 -0
  30. package/bin/runners/runShip.js +137 -207
  31. package/bin/runners/runStatus.js +16 -68
  32. package/bin/runners/utils.js +5 -5
  33. package/bin/vibecheck.js +25 -11
  34. package/mcp-server/index.js +150 -18
  35. package/mcp-server/package.json +2 -2
  36. package/mcp-server/premium-tools.js +13 -13
  37. package/mcp-server/tier-auth.js +292 -27
  38. package/mcp-server/vibecheck-tools.js +9 -9
  39. package/package.json +1 -1
  40. package/bin/runners/runClaimVerifier.js +0 -483
  41. package/bin/runners/runContextCompiler.js +0 -385
  42. package/bin/runners/runGate.js +0 -17
  43. package/bin/runners/runInitGha.js +0 -164
  44. package/bin/runners/runInteractive.js +0 -388
  45. package/bin/runners/runMdc.js +0 -204
  46. package/bin/runners/runMissionGenerator.js +0 -282
  47. package/bin/runners/runTruthpack.js +0 -636
@@ -0,0 +1,545 @@
1
+ /**
2
+ * Scan Output - Premium Scan Results Display
3
+ *
4
+ * Handles all scan output formatting:
5
+ * - Layer status display
6
+ * - Findings grouped by category
7
+ * - Coverage maps
8
+ * - JSON/SARIF export
9
+ */
10
+
11
+ const {
12
+ ansi,
13
+ colors,
14
+ box,
15
+ icons,
16
+ renderScoreCard,
17
+ renderFindingsList,
18
+ renderSection,
19
+ renderDivider,
20
+ renderTable,
21
+ formatDuration,
22
+ formatNumber,
23
+ truncate,
24
+ } = require('./terminal-ui');
25
+
26
+ // ═══════════════════════════════════════════════════════════════════════════════
27
+ // LAYER STATUS DISPLAY
28
+ // ═══════════════════════════════════════════════════════════════════════════════
29
+
30
+ function renderLayers(layers) {
31
+ const lines = [];
32
+ lines.push(renderSection('ANALYSIS LAYERS', '⚡'));
33
+ lines.push('');
34
+
35
+ const layerConfig = {
36
+ ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code parsing' },
37
+ truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
38
+ reality: { name: 'Reality Check', icon: '🎭', desc: 'Playwright runtime' },
39
+ realitySniff: { name: 'Reality Sniff', icon: '🔬', desc: 'AI artifact detection' },
40
+ detection: { name: 'Detection Engines', icon: '🛡️', desc: 'Security patterns' },
41
+ };
42
+
43
+ for (const layer of layers) {
44
+ const config = layerConfig[layer.name] || { name: layer.name, icon: '○', desc: '' };
45
+
46
+ let status, statusColor;
47
+ if (layer.skipped) {
48
+ status = `${ansi.dim}○ skipped${ansi.reset}`;
49
+ } else if (layer.error) {
50
+ status = `${colors.error}${icons.error} error${ansi.reset}`;
51
+ } else {
52
+ status = `${colors.success}${icons.success}${ansi.reset}`;
53
+ }
54
+
55
+ const duration = layer.duration ? `${ansi.dim}${layer.duration}ms${ansi.reset}` : '';
56
+ const findings = layer.findings !== undefined ? `${colors.accent}${layer.findings}${ansi.reset} ${ansi.dim}findings${ansi.reset}` : '';
57
+
58
+ lines.push(` ${status} ${config.icon} ${config.name.padEnd(20)} ${duration.padEnd(15)} ${findings}`);
59
+ }
60
+
61
+ return lines.join('\n');
62
+ }
63
+
64
+ // ═══════════════════════════════════════════════════════════════════════════════
65
+ // COVERAGE MAP DISPLAY
66
+ // ═══════════════════════════════════════════════════════════════════════════════
67
+
68
+ function renderCoverageMap(coverage) {
69
+ if (!coverage) return '';
70
+
71
+ const lines = [];
72
+ lines.push(renderSection('ROUTE COVERAGE', '🗺️'));
73
+ lines.push('');
74
+
75
+ const pct = coverage.coveragePercent || 0;
76
+ const color = pct >= 80 ? colors.success : pct >= 60 ? colors.warning : colors.error;
77
+
78
+ // Coverage bar
79
+ const barWidth = 50;
80
+ const filled = Math.round((pct / 100) * barWidth);
81
+ const bar = `${color}${'█'.repeat(filled)}${ansi.dim}${'░'.repeat(barWidth - filled)}${ansi.reset}`;
82
+
83
+ lines.push(` ${color}${ansi.bold}${pct}%${ansi.reset} ${ansi.dim}of routes reachable from${ansi.reset} ${colors.accent}/${ansi.reset}`);
84
+ lines.push(` ${bar}`);
85
+ lines.push('');
86
+
87
+ // Stats
88
+ const stats = [
89
+ ['Total Routes', coverage.totalRoutes || 0],
90
+ ['Reachable', coverage.reachableFromRoot || 0],
91
+ ['Orphaned', coverage.orphanedRoutes || 0],
92
+ ['Dead Links', coverage.deadLinks || 0],
93
+ ];
94
+
95
+ for (const [label, value] of stats) {
96
+ const valueColor = label === 'Orphaned' || label === 'Dead Links'
97
+ ? (value > 0 ? colors.error : colors.success)
98
+ : ansi.reset;
99
+ lines.push(` ${ansi.dim}${label}:${ansi.reset} ${valueColor}${ansi.bold}${value}${ansi.reset}`);
100
+ }
101
+
102
+ // Isolated clusters
103
+ if (coverage.isolatedClusters?.length > 0) {
104
+ lines.push('');
105
+ lines.push(` ${colors.warning}${icons.warning}${ansi.reset} ${ansi.dim}Isolated clusters:${ansi.reset}`);
106
+ for (const cluster of coverage.isolatedClusters.slice(0, 3)) {
107
+ const auth = cluster.requiresAuth ? ` ${ansi.dim}(auth required)${ansi.reset}` : '';
108
+ lines.push(` ${ansi.dim}├─${ansi.reset} ${ansi.bold}${cluster.name}${ansi.reset}${auth} ${ansi.dim}(${cluster.nodeIds?.length || 0} routes)${ansi.reset}`);
109
+ }
110
+ if (coverage.isolatedClusters.length > 3) {
111
+ lines.push(` ${ansi.dim}└─ ... and ${coverage.isolatedClusters.length - 3} more clusters${ansi.reset}`);
112
+ }
113
+ }
114
+
115
+ // Unreachable routes
116
+ if (coverage.unreachableRoutes?.length > 0) {
117
+ lines.push('');
118
+ lines.push(` ${colors.error}${icons.error}${ansi.reset} ${ansi.dim}Unreachable routes:${ansi.reset}`);
119
+ for (const route of coverage.unreachableRoutes.slice(0, 5)) {
120
+ lines.push(` ${ansi.dim}├─${ansi.reset} ${colors.error}${route}${ansi.reset}`);
121
+ }
122
+ if (coverage.unreachableRoutes.length > 5) {
123
+ lines.push(` ${ansi.dim}└─ ... and ${coverage.unreachableRoutes.length - 5} more${ansi.reset}`);
124
+ }
125
+ }
126
+
127
+ return lines.join('\n');
128
+ }
129
+
130
+ // ═══════════════════════════════════════════════════════════════════════════════
131
+ // BREAKDOWN DISPLAY
132
+ // ═══════════════════════════════════════════════════════════════════════════════
133
+
134
+ function renderBreakdown(breakdown) {
135
+ if (!breakdown) return '';
136
+
137
+ const lines = [];
138
+ lines.push(renderSection('SCORE BREAKDOWN', '📊'));
139
+ lines.push('');
140
+
141
+ const items = [
142
+ { key: 'deadLinks', label: 'Dead Links', icon: '🔗' },
143
+ { key: 'orphanRoutes', label: 'Orphan Routes', icon: '👻' },
144
+ { key: 'runtimeFailures', label: 'Runtime 404s', icon: '💥' },
145
+ { key: 'unresolvedDynamic', label: 'Unresolved Dynamic', icon: '❓' },
146
+ { key: 'placeholders', label: 'Placeholders', icon: '📝' },
147
+ { key: 'secretsExposed', label: 'Secrets Exposed', icon: '🔐' },
148
+ { key: 'authBypass', label: 'Auth Bypass', icon: '🚪' },
149
+ { key: 'mockData', label: 'Mock Data', icon: '🎭' },
150
+ ];
151
+
152
+ for (const item of items) {
153
+ const data = breakdown[item.key];
154
+ if (!data && data !== 0) continue;
155
+
156
+ const count = typeof data === 'object' ? data.count : data;
157
+ const penalty = typeof data === 'object' ? data.penalty : 0;
158
+
159
+ const status = count === 0 ? `${colors.success}${icons.success}${ansi.reset}` : `${colors.error}${icons.error}${ansi.reset}`;
160
+ const countColor = count === 0 ? colors.success : colors.error;
161
+ const penaltyStr = penalty > 0 ? `${ansi.dim}-${penalty} pts${ansi.reset}` : `${ansi.dim} ---${ansi.reset}`;
162
+
163
+ lines.push(` ${status} ${item.icon} ${item.label.padEnd(22)} ${countColor}${ansi.bold}${String(count).padStart(3)}${ansi.reset} ${penaltyStr}`);
164
+ }
165
+
166
+ return lines.join('\n');
167
+ }
168
+
169
+ // ═══════════════════════════════════════════════════════════════════════════════
170
+ // BLOCKERS DISPLAY
171
+ // ═══════════════════════════════════════════════════════════════════════════════
172
+
173
+ function renderBlockers(blockers, options = {}) {
174
+ const { maxItems = 8 } = options;
175
+
176
+ if (!blockers || blockers.length === 0) {
177
+ const lines = [];
178
+ lines.push(renderSection('SHIP BLOCKERS', '🚀'));
179
+ lines.push('');
180
+ lines.push(` ${colors.success}${ansi.bold}${icons.success} No blockers! You're clear to ship.${ansi.reset}`);
181
+ return lines.join('\n');
182
+ }
183
+
184
+ const lines = [];
185
+ lines.push(renderSection(`SHIP BLOCKERS (${blockers.length})`, '🚨'));
186
+ lines.push('');
187
+
188
+ for (const blocker of blockers.slice(0, maxItems)) {
189
+ const sevColor = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
190
+ ? colors.bg.error
191
+ : colors.bg.warning;
192
+ const sevLabel = blocker.severity === 'critical' || blocker.severity === 'BLOCK'
193
+ ? 'CRITICAL'
194
+ : ' HIGH ';
195
+
196
+ lines.push(` ${sevColor}${ansi.bold} ${sevLabel} ${ansi.reset} ${ansi.bold}${truncate(blocker.title || blocker.message, 45)}${ansi.reset}`);
197
+
198
+ if (blocker.description) {
199
+ lines.push(` ${ansi.dim}${truncate(blocker.description, 55)}${ansi.reset}`);
200
+ }
201
+
202
+ if (blocker.file) {
203
+ const fileDisplay = blocker.file + (blocker.line ? `:${blocker.line}` : '');
204
+ lines.push(` ${colors.accent}${truncate(fileDisplay, 50)}${ansi.reset}`);
205
+ }
206
+
207
+ if (blocker.fix || blocker.fixSuggestion) {
208
+ lines.push(` ${colors.success}→ ${truncate(blocker.fix || blocker.fixSuggestion, 50)}${ansi.reset}`);
209
+ }
210
+
211
+ lines.push('');
212
+ }
213
+
214
+ if (blockers.length > maxItems) {
215
+ lines.push(` ${ansi.dim}... and ${blockers.length - maxItems} more blockers${ansi.reset}`);
216
+ lines.push('');
217
+ }
218
+
219
+ return lines.join('\n');
220
+ }
221
+
222
+ // ═══════════════════════════════════════════════════════════════════════════════
223
+ // CATEGORY SUMMARY
224
+ // ═══════════════════════════════════════════════════════════════════════════════
225
+
226
+ function renderCategorySummary(findings) {
227
+ if (!findings || findings.length === 0) return '';
228
+
229
+ // Group by category
230
+ const categories = {};
231
+ for (const f of findings) {
232
+ const cat = f.category || f.ruleId?.split('/')[0] || 'other';
233
+ if (!categories[cat]) {
234
+ categories[cat] = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
235
+ }
236
+ categories[cat].total++;
237
+
238
+ const sev = (f.severity || '').toLowerCase();
239
+ if (sev === 'critical' || sev === 'block') categories[cat].critical++;
240
+ else if (sev === 'high') categories[cat].high++;
241
+ else if (sev === 'medium' || sev === 'warn' || sev === 'warning') categories[cat].medium++;
242
+ else categories[cat].low++;
243
+ }
244
+
245
+ const lines = [];
246
+ lines.push(renderSection('FINDINGS BY CATEGORY', '📁'));
247
+ lines.push('');
248
+
249
+ const categoryIcons = {
250
+ ROUTE: '🔗',
251
+ AUTH: '🔐',
252
+ SECRET: '🔑',
253
+ BILLING: '💳',
254
+ MOCK: '🎭',
255
+ DEAD_UI: '👻',
256
+ FAKE_SUCCESS: '✨',
257
+ REALITY: '🔬',
258
+ QUALITY: '📋',
259
+ CONFIG: '⚙️',
260
+ };
261
+
262
+ const sortedCategories = Object.entries(categories)
263
+ .sort((a, b) => {
264
+ // Sort by criticality: critical > high > medium > low
265
+ const aCrit = a[1].critical * 1000 + a[1].high * 100 + a[1].medium * 10;
266
+ const bCrit = b[1].critical * 1000 + b[1].high * 100 + b[1].medium * 10;
267
+ return bCrit - aCrit;
268
+ });
269
+
270
+ for (const [cat, counts] of sortedCategories) {
271
+ const icon = categoryIcons[cat.toUpperCase()] || '•';
272
+ const critStr = counts.critical > 0 ? `${colors.critical}${counts.critical}C${ansi.reset} ` : '';
273
+ const highStr = counts.high > 0 ? `${colors.high}${counts.high}H${ansi.reset} ` : '';
274
+ const medStr = counts.medium > 0 ? `${colors.medium}${counts.medium}M${ansi.reset} ` : '';
275
+ const lowStr = counts.low > 0 ? `${colors.low}${counts.low}L${ansi.reset}` : '';
276
+
277
+ lines.push(` ${icon} ${cat.padEnd(15)} ${ansi.dim}${String(counts.total).padStart(3)} total${ansi.reset} ${critStr}${highStr}${medStr}${lowStr}`);
278
+ }
279
+
280
+ return lines.join('\n');
281
+ }
282
+
283
+ // ═══════════════════════════════════════════════════════════════════════════════
284
+ // FULL SCAN OUTPUT
285
+ // ═══════════════════════════════════════════════════════════════════════════════
286
+
287
+ function formatScanOutput(result, options = {}) {
288
+ const { verbose = false, json = false } = options;
289
+
290
+ if (json) {
291
+ return JSON.stringify(result, null, 2);
292
+ }
293
+
294
+ const { verdict, findings = [], layers = [], coverage, breakdown, timings = {} } = result;
295
+
296
+ // Count findings by severity
297
+ const severityCounts = {
298
+ critical: findings.filter(f => f.severity === 'critical' || f.severity === 'BLOCK').length,
299
+ high: findings.filter(f => f.severity === 'high').length,
300
+ medium: findings.filter(f => f.severity === 'medium' || f.severity === 'WARN' || f.severity === 'warning').length,
301
+ low: findings.filter(f => f.severity === 'low' || f.severity === 'INFO' || f.severity === 'info').length,
302
+ };
303
+
304
+ // Calculate score
305
+ const score = verdict?.score ?? calculateScore(severityCounts);
306
+ const verdictStatus = verdict?.verdict || (severityCounts.critical > 0 ? 'BLOCK' : severityCounts.high > 0 ? 'WARN' : 'SHIP');
307
+
308
+ const lines = [];
309
+
310
+ // Score card
311
+ lines.push(renderScoreCard(score, {
312
+ verdict: verdictStatus,
313
+ findings: severityCounts,
314
+ duration: timings.total,
315
+ cached: result.cached,
316
+ }));
317
+
318
+ // Blockers (critical + high severity findings)
319
+ const blockers = findings.filter(f =>
320
+ f.severity === 'critical' || f.severity === 'BLOCK' || f.severity === 'high'
321
+ );
322
+ lines.push(renderBlockers(blockers));
323
+
324
+ // Category summary
325
+ if (findings.length > 0) {
326
+ lines.push(renderCategorySummary(findings));
327
+ }
328
+
329
+ // Verbose output
330
+ if (verbose) {
331
+ // Coverage map
332
+ if (coverage) {
333
+ lines.push('');
334
+ lines.push(renderCoverageMap(coverage));
335
+ }
336
+
337
+ // Breakdown
338
+ if (breakdown) {
339
+ lines.push('');
340
+ lines.push(renderBreakdown(breakdown));
341
+ }
342
+
343
+ // Layers
344
+ if (layers.length > 0) {
345
+ lines.push('');
346
+ lines.push(renderLayers(layers));
347
+ }
348
+
349
+ // All findings
350
+ if (findings.length > 0) {
351
+ lines.push('');
352
+ lines.push(renderFindingsList(findings, { maxItems: 20, groupBySeverity: true }));
353
+ }
354
+ }
355
+
356
+ // Timing summary
357
+ if (timings.total) {
358
+ lines.push('');
359
+ lines.push(` ${ansi.dim}Completed in ${formatDuration(timings.total)}${ansi.reset}`);
360
+ }
361
+
362
+ lines.push('');
363
+ return lines.join('\n');
364
+ }
365
+
366
+ // ═══════════════════════════════════════════════════════════════════════════════
367
+ // SCORE CALCULATION
368
+ // ═══════════════════════════════════════════════════════════════════════════════
369
+
370
+ function calculateScore(severityCounts) {
371
+ const deductions =
372
+ (severityCounts.critical || 0) * 25 +
373
+ (severityCounts.high || 0) * 15 +
374
+ (severityCounts.medium || 0) * 5 +
375
+ (severityCounts.low || 0) * 1;
376
+
377
+ return Math.max(0, 100 - deductions);
378
+ }
379
+
380
+ // ═══════════════════════════════════════════════════════════════════════════════
381
+ // EXIT CODE DETERMINATION
382
+ // ═══════════════════════════════════════════════════════════════════════════════
383
+
384
+ const EXIT_CODES = {
385
+ SUCCESS: 0,
386
+ WARNING: 1,
387
+ FAILURE: 2,
388
+ ERROR: 3,
389
+ };
390
+
391
+ function getExitCode(verdict) {
392
+ if (!verdict) return EXIT_CODES.ERROR;
393
+
394
+ const status = verdict.verdict || verdict;
395
+
396
+ switch (status) {
397
+ case 'PASS':
398
+ case 'SHIP':
399
+ return EXIT_CODES.SUCCESS;
400
+ case 'WARN':
401
+ return EXIT_CODES.WARNING;
402
+ case 'FAIL':
403
+ case 'BLOCK':
404
+ return EXIT_CODES.FAILURE;
405
+ default:
406
+ return EXIT_CODES.ERROR;
407
+ }
408
+ }
409
+
410
+ // ═══════════════════════════════════════════════════════════════════════════════
411
+ // ERROR DISPLAY
412
+ // ═══════════════════════════════════════════════════════════════════════════════
413
+
414
+ function printError(error, context = '') {
415
+ const prefix = context ? `${context}: ` : '';
416
+
417
+ console.error('');
418
+ console.error(` ${colors.error}${icons.error}${ansi.reset} ${ansi.bold}${prefix}${error.message || error}${ansi.reset}`);
419
+
420
+ if (error.code) {
421
+ console.error(` ${ansi.dim}Error code: ${error.code}${ansi.reset}`);
422
+ }
423
+
424
+ if (error.suggestion || error.fix) {
425
+ console.error(` ${colors.success}${icons.arrowRight}${ansi.reset} ${error.suggestion || error.fix}`);
426
+ }
427
+
428
+ if (error.docs || error.helpUrl) {
429
+ console.error(` ${ansi.dim}See: ${error.docs || error.helpUrl}${ansi.reset}`);
430
+ }
431
+
432
+ console.error('');
433
+
434
+ // Return appropriate exit code
435
+ if (error.code === 'VALIDATION_ERROR') return EXIT_CODES.FAILURE;
436
+ if (error.code === 'LIMIT_EXCEEDED') return EXIT_CODES.WARNING;
437
+ return EXIT_CODES.ERROR;
438
+ }
439
+
440
+ // ═══════════════════════════════════════════════════════════════════════════════
441
+ // SARIF OUTPUT
442
+ // ═══════════════════════════════════════════════════════════════════════════════
443
+
444
+ function formatSARIF(findings, options = {}) {
445
+ const { projectPath = '.', version = '1.0.0' } = options;
446
+
447
+ const rules = new Map();
448
+ const results = [];
449
+
450
+ for (const f of findings) {
451
+ const ruleId = f.ruleId || f.id || `vibecheck/${f.category || 'general'}`;
452
+
453
+ if (!rules.has(ruleId)) {
454
+ rules.set(ruleId, {
455
+ id: ruleId,
456
+ name: f.category || 'general',
457
+ shortDescription: { text: f.title || f.message },
458
+ defaultConfiguration: {
459
+ level: sarifLevel(f.severity),
460
+ },
461
+ helpUri: 'https://vibecheck.dev/docs/rules/' + ruleId,
462
+ });
463
+ }
464
+
465
+ const result = {
466
+ ruleId,
467
+ level: sarifLevel(f.severity),
468
+ message: { text: f.message || f.title },
469
+ locations: [],
470
+ };
471
+
472
+ if (f.file) {
473
+ result.locations.push({
474
+ physicalLocation: {
475
+ artifactLocation: { uri: f.file },
476
+ region: f.line ? { startLine: f.line, startColumn: f.column || 1 } : undefined,
477
+ },
478
+ });
479
+ }
480
+
481
+ if (f.fix) {
482
+ result.fixes = [{ description: { text: f.fix } }];
483
+ }
484
+
485
+ results.push(result);
486
+ }
487
+
488
+ return {
489
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
490
+ version: '2.1.0',
491
+ runs: [{
492
+ tool: {
493
+ driver: {
494
+ name: 'vibecheck',
495
+ version,
496
+ informationUri: 'https://vibecheck.dev',
497
+ rules: Array.from(rules.values()),
498
+ },
499
+ },
500
+ results,
501
+ invocations: [{
502
+ executionSuccessful: true,
503
+ endTimeUtc: new Date().toISOString(),
504
+ }],
505
+ }],
506
+ };
507
+ }
508
+
509
+ function sarifLevel(severity) {
510
+ const levels = {
511
+ critical: 'error',
512
+ BLOCK: 'error',
513
+ high: 'error',
514
+ medium: 'warning',
515
+ WARN: 'warning',
516
+ warning: 'warning',
517
+ low: 'note',
518
+ INFO: 'note',
519
+ info: 'none',
520
+ };
521
+ return levels[severity] || 'warning';
522
+ }
523
+
524
+ // ═══════════════════════════════════════════════════════════════════════════════
525
+ // EXPORTS
526
+ // ═══════════════════════════════════════════════════════════════════════════════
527
+
528
+ module.exports = {
529
+ // Main formatters
530
+ formatScanOutput,
531
+ formatSARIF,
532
+
533
+ // Component renderers
534
+ renderLayers,
535
+ renderCoverageMap,
536
+ renderBreakdown,
537
+ renderBlockers,
538
+ renderCategorySummary,
539
+
540
+ // Utilities
541
+ calculateScore,
542
+ getExitCode,
543
+ printError,
544
+ EXIT_CODES,
545
+ };
@@ -269,18 +269,6 @@ class ServerUsageEnforcement {
269
269
  return { synced: 0, pending: 0 };
270
270
  }
271
271
 
272
- // Check if API key is configured before attempting sync
273
- const token = getAuthToken();
274
- if (!token) {
275
- // No API key - silently allow offline mode (no warning needed for missing config)
276
- return {
277
- synced: 0,
278
- pending: offline.queue.length,
279
- error: 'No API key configured',
280
- offline: true,
281
- };
282
- }
283
-
284
272
  const result = await apiRequest('/sync', 'POST');
285
273
 
286
274
  if (result.offline || !result.success) {