@vibecheckai/cli 3.0.8 → 3.0.10

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.
@@ -1,6 +1,15 @@
1
1
  /**
2
2
  * Reality Mode v2 - Two-Pass Auth Verification + Dead UI Crawler
3
3
  *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * ENTERPRISE EDITION - World-Class Terminal Experience
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ *
8
+ * TIER ENFORCEMENT:
9
+ * - FREE: Preview mode (5 pages, 20 clicks, no auth boundary)
10
+ * - STARTER: Full budgets + basic auth verification
11
+ * - PRO: Advanced auth boundary (multi-role, 2-pass)
12
+ *
4
13
  * Pass A (anon): crawl + click, record which routes look protected
5
14
  * Pass B (auth): crawl same routes using storageState, verify protected routes accessible
6
15
  *
@@ -17,6 +26,9 @@ const fs = require("fs");
17
26
  const path = require("path");
18
27
  const crypto = require("crypto");
19
28
 
29
+ // Entitlements enforcement
30
+ const entitlements = require("./lib/entitlements-v2");
31
+
20
32
  let chromium;
21
33
  let playwrightError = null;
22
34
  try {
@@ -26,6 +38,639 @@ try {
26
38
  playwrightError = e.message;
27
39
  }
28
40
 
41
+ // ═══════════════════════════════════════════════════════════════════════════════
42
+ // ADVANCED TERMINAL - ANSI CODES & UTILITIES
43
+ // ═══════════════════════════════════════════════════════════════════════════════
44
+
45
+ const c = {
46
+ reset: '\x1b[0m',
47
+ bold: '\x1b[1m',
48
+ dim: '\x1b[2m',
49
+ italic: '\x1b[3m',
50
+ underline: '\x1b[4m',
51
+ blink: '\x1b[5m',
52
+ inverse: '\x1b[7m',
53
+ hidden: '\x1b[8m',
54
+ strike: '\x1b[9m',
55
+ // Colors
56
+ black: '\x1b[30m',
57
+ red: '\x1b[31m',
58
+ green: '\x1b[32m',
59
+ yellow: '\x1b[33m',
60
+ blue: '\x1b[34m',
61
+ magenta: '\x1b[35m',
62
+ cyan: '\x1b[36m',
63
+ white: '\x1b[37m',
64
+ // Bright colors
65
+ gray: '\x1b[90m',
66
+ brightRed: '\x1b[91m',
67
+ brightGreen: '\x1b[92m',
68
+ brightYellow: '\x1b[93m',
69
+ brightBlue: '\x1b[94m',
70
+ brightMagenta: '\x1b[95m',
71
+ brightCyan: '\x1b[96m',
72
+ brightWhite: '\x1b[97m',
73
+ // Background
74
+ bgBlack: '\x1b[40m',
75
+ bgRed: '\x1b[41m',
76
+ bgGreen: '\x1b[42m',
77
+ bgYellow: '\x1b[43m',
78
+ bgBlue: '\x1b[44m',
79
+ bgMagenta: '\x1b[45m',
80
+ bgCyan: '\x1b[46m',
81
+ bgWhite: '\x1b[47m',
82
+ // Cursor control
83
+ cursorUp: (n = 1) => `\x1b[${n}A`,
84
+ cursorDown: (n = 1) => `\x1b[${n}B`,
85
+ cursorRight: (n = 1) => `\x1b[${n}C`,
86
+ cursorLeft: (n = 1) => `\x1b[${n}D`,
87
+ clearLine: '\x1b[2K',
88
+ clearScreen: '\x1b[2J',
89
+ saveCursor: '\x1b[s',
90
+ restoreCursor: '\x1b[u',
91
+ hideCursor: '\x1b[?25l',
92
+ showCursor: '\x1b[?25h',
93
+ };
94
+
95
+ // True color support
96
+ const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
97
+ const bgRgb = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
98
+
99
+ // Premium color palette (orange/coral theme for "reality" - testing/verification)
100
+ const colors = {
101
+ // Gradient for banner
102
+ gradient1: rgb(255, 150, 100), // Light coral
103
+ gradient2: rgb(255, 130, 80), // Coral
104
+ gradient3: rgb(255, 110, 60), // Orange-coral
105
+ gradient4: rgb(255, 90, 50), // Orange
106
+ gradient5: rgb(255, 70, 40), // Deep orange
107
+ gradient6: rgb(255, 50, 30), // Red-orange
108
+
109
+ // Pass colors
110
+ anon: rgb(150, 200, 255), // Blue for anonymous
111
+ auth: rgb(100, 255, 180), // Green for authenticated
112
+
113
+ // Category colors
114
+ deadUI: rgb(255, 100, 100), // Red for dead UI
115
+ authCoverage: rgb(255, 180, 100), // Orange for auth issues
116
+ httpError: rgb(255, 150, 50), // Amber for HTTP errors
117
+ coverage: rgb(100, 200, 255), // Blue for coverage
118
+
119
+ // Status colors
120
+ success: rgb(0, 255, 150),
121
+ warning: rgb(255, 200, 0),
122
+ error: rgb(255, 80, 80),
123
+ info: rgb(100, 200, 255),
124
+
125
+ // UI colors
126
+ accent: rgb(255, 150, 100),
127
+ muted: rgb(140, 120, 100),
128
+ subtle: rgb(100, 80, 60),
129
+ highlight: rgb(255, 255, 255),
130
+ };
131
+
132
+ // ═══════════════════════════════════════════════════════════════════════════════
133
+ // PREMIUM BANNER
134
+ // ═══════════════════════════════════════════════════════════════════════════════
135
+
136
+ const REALITY_BANNER = `
137
+ ${rgb(255, 160, 120)} ██████╗ ███████╗ █████╗ ██╗ ██╗████████╗██╗ ██╗${c.reset}
138
+ ${rgb(255, 140, 100)} ██╔══██╗██╔════╝██╔══██╗██║ ██║╚══██╔══╝╚██╗ ██╔╝${c.reset}
139
+ ${rgb(255, 120, 80)} ██████╔╝█████╗ ███████║██║ ██║ ██║ ╚████╔╝ ${c.reset}
140
+ ${rgb(255, 100, 60)} ██╔══██╗██╔══╝ ██╔══██║██║ ██║ ██║ ╚██╔╝ ${c.reset}
141
+ ${rgb(255, 80, 40)} ██║ ██║███████╗██║ ██║███████╗██║ ██║ ██║ ${c.reset}
142
+ ${rgb(255, 60, 20)} ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ${c.reset}
143
+ `;
144
+
145
+ const BANNER_FULL = `
146
+ ${rgb(255, 160, 120)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${c.reset}
147
+ ${rgb(255, 140, 100)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${c.reset}
148
+ ${rgb(255, 120, 80)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${c.reset}
149
+ ${rgb(255, 100, 60)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${c.reset}
150
+ ${rgb(255, 80, 40)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${c.reset}
151
+ ${rgb(255, 60, 20)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${c.reset}
152
+
153
+ ${c.dim} ┌─────────────────────────────────────────────────────────────────────┐${c.reset}
154
+ ${c.dim} │${c.reset} ${rgb(255, 150, 100)}🎭${c.reset} ${c.bold}REALITY${c.reset} ${c.dim}•${c.reset} ${rgb(200, 200, 200)}Runtime UI Proof${c.reset} ${c.dim}•${c.reset} ${rgb(150, 150, 150)}Dead UI Detection${c.reset} ${c.dim}│${c.reset}
155
+ ${c.dim} └─────────────────────────────────────────────────────────────────────┘${c.reset}
156
+ `;
157
+
158
+ // ═══════════════════════════════════════════════════════════════════════════════
159
+ // ICONS & SYMBOLS
160
+ // ═══════════════════════════════════════════════════════════════════════════════
161
+
162
+ const ICONS = {
163
+ // Main
164
+ reality: '🎭',
165
+ browser: '🌐',
166
+ crawl: '🕷️',
167
+
168
+ // Status
169
+ check: '✓',
170
+ cross: '✗',
171
+ warning: '⚠',
172
+ info: 'ℹ',
173
+ arrow: '→',
174
+ bullet: '•',
175
+
176
+ // Passes
177
+ anon: '👤',
178
+ auth: '🔑',
179
+ pass: '✅',
180
+ fail: '❌',
181
+
182
+ // Categories
183
+ deadUI: '💀',
184
+ click: '👆',
185
+ link: '🔗',
186
+ http: '📡',
187
+ coverage: '📊',
188
+ shield: '🛡️',
189
+
190
+ // Actions
191
+ running: '▶',
192
+ complete: '●',
193
+ pending: '○',
194
+ skip: '◌',
195
+
196
+ // Objects
197
+ page: '📄',
198
+ screenshot: '📸',
199
+ clock: '⏱',
200
+ lightning: '⚡',
201
+ sparkle: '✨',
202
+ target: '🎯',
203
+ eye: '👁️',
204
+ };
205
+
206
+ // ═══════════════════════════════════════════════════════════════════════════════
207
+ // BOX DRAWING
208
+ // ═══════════════════════════════════════════════════════════════════════════════
209
+
210
+ const BOX = {
211
+ topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
212
+ horizontal: '─', vertical: '│',
213
+ teeRight: '├', teeLeft: '┤', teeDown: '┬', teeUp: '┴',
214
+ cross: '┼',
215
+ // Double line
216
+ dTopLeft: '╔', dTopRight: '╗', dBottomLeft: '╚', dBottomRight: '╝',
217
+ dHorizontal: '═', dVertical: '║',
218
+ // Heavy
219
+ hTopLeft: '┏', hTopRight: '┓', hBottomLeft: '┗', hBottomRight: '┛',
220
+ hHorizontal: '━', hVertical: '┃',
221
+ };
222
+
223
+ // ═══════════════════════════════════════════════════════════════════════════════
224
+ // SPINNER & PROGRESS
225
+ // ═══════════════════════════════════════════════════════════════════════════════
226
+
227
+ const SPINNER_DOTS = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
228
+ const SPINNER_CRAWL = ['🕷️ ', ' 🕷️', ' 🕷️', ' 🕷️', ' 🕷️', ' 🕷️'];
229
+
230
+ let spinnerIndex = 0;
231
+ let spinnerInterval = null;
232
+ let spinnerStartTime = null;
233
+
234
+ function formatDuration(ms) {
235
+ if (ms < 1000) return `${ms}ms`;
236
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
237
+ const mins = Math.floor(ms / 60000);
238
+ const secs = Math.floor((ms % 60000) / 1000);
239
+ return `${mins}m ${secs}s`;
240
+ }
241
+
242
+ function formatNumber(num) {
243
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
244
+ }
245
+
246
+ function truncate(str, len) {
247
+ if (!str) return '';
248
+ if (str.length <= len) return str;
249
+ return str.slice(0, len - 3) + '...';
250
+ }
251
+
252
+ function padCenter(str, width) {
253
+ const padding = Math.max(0, width - str.length);
254
+ const left = Math.floor(padding / 2);
255
+ const right = padding - left;
256
+ return ' '.repeat(left) + str + ' '.repeat(right);
257
+ }
258
+
259
+ function progressBar(percent, width = 30, opts = {}) {
260
+ const filled = Math.round((percent / 100) * width);
261
+ const empty = width - filled;
262
+
263
+ let filledColor = opts.color || colors.accent;
264
+ if (!opts.color) {
265
+ if (percent >= 80) filledColor = colors.success;
266
+ else if (percent >= 50) filledColor = colors.warning;
267
+ else filledColor = colors.error;
268
+ }
269
+
270
+ const filledChar = opts.filled || '█';
271
+ const emptyChar = opts.empty || '░';
272
+
273
+ return `${filledColor}${filledChar.repeat(filled)}${c.dim}${emptyChar.repeat(empty)}${c.reset}`;
274
+ }
275
+
276
+ function startSpinner(message, color = colors.accent) {
277
+ spinnerStartTime = Date.now();
278
+ process.stdout.write(c.hideCursor);
279
+
280
+ spinnerInterval = setInterval(() => {
281
+ const elapsed = formatDuration(Date.now() - spinnerStartTime);
282
+ process.stdout.write(`\r${c.clearLine} ${color}${SPINNER_DOTS[spinnerIndex]}${c.reset} ${message} ${c.dim}${elapsed}${c.reset}`);
283
+ spinnerIndex = (spinnerIndex + 1) % SPINNER_DOTS.length;
284
+ }, 80);
285
+ }
286
+
287
+ function stopSpinner(message, success = true) {
288
+ if (spinnerInterval) {
289
+ clearInterval(spinnerInterval);
290
+ spinnerInterval = null;
291
+ }
292
+ const elapsed = spinnerStartTime ? formatDuration(Date.now() - spinnerStartTime) : '';
293
+ const icon = success ? `${colors.success}${ICONS.check}${c.reset}` : `${colors.error}${ICONS.cross}${c.reset}`;
294
+ process.stdout.write(`\r${c.clearLine} ${icon} ${message} ${c.dim}${elapsed}${c.reset}\n`);
295
+ process.stdout.write(c.showCursor);
296
+ spinnerStartTime = null;
297
+ }
298
+
299
+ function updateSpinnerMessage(message) {
300
+ // Update message while spinner keeps running
301
+ }
302
+
303
+ // ═══════════════════════════════════════════════════════════════════════════════
304
+ // SECTION HEADERS
305
+ // ═══════════════════════════════════════════════════════════════════════════════
306
+
307
+ function printBanner() {
308
+ console.log(BANNER_FULL);
309
+ }
310
+
311
+ function printCompactBanner() {
312
+ console.log(REALITY_BANNER);
313
+ }
314
+
315
+ function printDivider(char = '─', width = 69, color = c.dim) {
316
+ console.log(`${color} ${char.repeat(width)}${c.reset}`);
317
+ }
318
+
319
+ function printSection(title, icon = '◆') {
320
+ console.log();
321
+ console.log(` ${colors.accent}${icon}${c.reset} ${c.bold}${title}${c.reset}`);
322
+ printDivider();
323
+ }
324
+
325
+ // ═══════════════════════════════════════════════════════════════════════════════
326
+ // PASS DISPLAY - Two-Pass Visualization
327
+ // ═══════════════════════════════════════════════════════════════════════════════
328
+
329
+ function printPassHeader(passType, url = null) {
330
+ const isAnon = passType === 'anon' || passType === 'ANON';
331
+ const config = isAnon
332
+ ? { icon: ICONS.anon, name: 'PASS A: ANONYMOUS', color: colors.anon, desc: 'Crawling without authentication' }
333
+ : { icon: ICONS.auth, name: 'PASS B: AUTHENTICATED', color: colors.auth, desc: 'Crawling with session state' };
334
+
335
+ console.log();
336
+ console.log(` ${config.color}${BOX.hTopLeft}${BOX.hHorizontal.repeat(3)}${c.reset} ${config.icon} ${c.bold}${config.name}${c.reset}`);
337
+ console.log(` ${config.color}${BOX.hVertical}${c.reset} ${c.dim}${config.desc}${c.reset}`);
338
+ if (url) {
339
+ console.log(` ${config.color}${BOX.hVertical}${c.reset} ${colors.accent}${url}${c.reset}`);
340
+ }
341
+ console.log(` ${config.color}${BOX.hBottomLeft}${BOX.hHorizontal.repeat(60)}${c.reset}`);
342
+ }
343
+
344
+ function printPassResult(passType, result) {
345
+ const isAnon = passType === 'anon' || passType === 'ANON';
346
+ const config = isAnon
347
+ ? { icon: ICONS.anon, color: colors.anon }
348
+ : { icon: ICONS.auth, color: colors.auth };
349
+
350
+ const pages = result.pagesVisited?.length || 0;
351
+ const findings = result.findings?.length || 0;
352
+ const blocks = result.findings?.filter(f => f.severity === 'BLOCK').length || 0;
353
+ const warns = result.findings?.filter(f => f.severity === 'WARN').length || 0;
354
+
355
+ console.log();
356
+ console.log(` ${config.color}${config.icon}${c.reset} ${c.bold}${isAnon ? 'Anonymous' : 'Authenticated'} Pass Complete${c.reset}`);
357
+ console.log(` ${c.dim}Pages visited:${c.reset} ${colors.info}${pages}${c.reset}`);
358
+ console.log(` ${c.dim}Findings:${c.reset} ${findings} ${c.dim}(${c.reset}${blocks > 0 ? colors.error : colors.success}${blocks} blockers${c.reset}${c.dim},${c.reset} ${warns > 0 ? colors.warning : colors.success}${warns} warnings${c.reset}${c.dim})${c.reset}`);
359
+ }
360
+
361
+ // ═══════════════════════════════════════════════════════════════════════════════
362
+ // COVERAGE DISPLAY
363
+ // ═══════════════════════════════════════════════════════════════════════════════
364
+
365
+ function printCoverageCard(coverage, anonPages, authPages, maxPages) {
366
+ if (!coverage) return;
367
+
368
+ printSection('COVERAGE', ICONS.coverage);
369
+ console.log();
370
+
371
+ // UI Path Coverage
372
+ const pct = coverage.percent || 0;
373
+ const pctColor = pct >= 80 ? colors.success : pct >= 50 ? colors.warning : colors.error;
374
+
375
+ console.log(` ${c.bold}UI Path Coverage${c.reset}`);
376
+ console.log(` ${progressBar(pct, 40)} ${pctColor}${c.bold}${pct}%${c.reset}`);
377
+ console.log(` ${c.dim}${coverage.hit}/${coverage.total} paths visited${c.reset}`);
378
+
379
+ // Pages visited breakdown
380
+ console.log();
381
+ console.log(` ${c.bold}Pages Crawled${c.reset}`);
382
+
383
+ const anonPct = Math.round((anonPages / maxPages) * 100);
384
+ console.log(` ${ICONS.anon} ${c.dim}Anonymous:${c.reset} ${progressBar(anonPct, 25, { color: colors.anon })} ${anonPages}/${maxPages}`);
385
+
386
+ if (authPages !== null && authPages !== undefined) {
387
+ const authPct = Math.round((authPages / maxPages) * 100);
388
+ console.log(` ${ICONS.auth} ${c.dim}Authenticated:${c.reset} ${progressBar(authPct, 25, { color: colors.auth })} ${authPages}/${maxPages}`);
389
+ }
390
+
391
+ // Missed paths
392
+ if (coverage.missed && coverage.missed.length > 0) {
393
+ console.log();
394
+ console.log(` ${c.dim}Missed paths (${coverage.missed.length}):${c.reset}`);
395
+ for (const missed of coverage.missed.slice(0, 5)) {
396
+ console.log(` ${colors.warning}${ICONS.warning}${c.reset} ${c.dim}${truncate(missed, 50)}${c.reset}`);
397
+ }
398
+ if (coverage.missed.length > 5) {
399
+ console.log(` ${c.dim}... and ${coverage.missed.length - 5} more${c.reset}`);
400
+ }
401
+ }
402
+ }
403
+
404
+ // ═══════════════════════════════════════════════════════════════════════════════
405
+ // FINDINGS DISPLAY
406
+ // ═══════════════════════════════════════════════════════════════════════════════
407
+
408
+ function getSeverityStyle(severity) {
409
+ const styles = {
410
+ BLOCK: { color: colors.error, bg: bgRgb(80, 20, 20), icon: '●', label: 'BLOCKER' },
411
+ WARN: { color: colors.warning, bg: bgRgb(80, 60, 0), icon: '◐', label: 'WARNING' },
412
+ INFO: { color: colors.info, bg: bgRgb(20, 40, 60), icon: '○', label: 'INFO' },
413
+ };
414
+ return styles[severity] || styles.INFO;
415
+ }
416
+
417
+ function getCategoryIcon(category) {
418
+ const icons = {
419
+ 'DeadUI': ICONS.deadUI,
420
+ 'AuthCoverage': ICONS.shield,
421
+ 'HTTPError': ICONS.http,
422
+ };
423
+ return icons[category] || ICONS.bullet;
424
+ }
425
+
426
+ function getCategoryColor(category) {
427
+ const categoryColors = {
428
+ 'DeadUI': colors.deadUI,
429
+ 'AuthCoverage': colors.authCoverage,
430
+ 'HTTPError': colors.httpError,
431
+ };
432
+ return categoryColors[category] || colors.accent;
433
+ }
434
+
435
+ function printFindingsBreakdown(findings) {
436
+ if (!findings || findings.length === 0) {
437
+ printSection('FINDINGS', ICONS.check);
438
+ console.log();
439
+ console.log(` ${colors.success}${c.bold}${ICONS.sparkle} No issues found! UI is responsive.${c.reset}`);
440
+ return;
441
+ }
442
+
443
+ // Group by category
444
+ const byCategory = {};
445
+ for (const f of findings) {
446
+ const cat = f.category || 'Other';
447
+ if (!byCategory[cat]) byCategory[cat] = [];
448
+ byCategory[cat].push(f);
449
+ }
450
+
451
+ const blocks = findings.filter(f => f.severity === 'BLOCK');
452
+ const warns = findings.filter(f => f.severity === 'WARN');
453
+
454
+ printSection(`FINDINGS (${blocks.length} blockers, ${warns.length} warnings)`, ICONS.target);
455
+ console.log();
456
+
457
+ // Summary by category
458
+ for (const [category, catFindings] of Object.entries(byCategory)) {
459
+ const catBlocks = catFindings.filter(f => f.severity === 'BLOCK').length;
460
+ const catWarns = catFindings.filter(f => f.severity === 'WARN').length;
461
+ const icon = getCategoryIcon(category);
462
+ const catColor = getCategoryColor(category);
463
+
464
+ const statusIcon = catBlocks > 0 ? ICONS.cross : catWarns > 0 ? ICONS.warning : ICONS.check;
465
+ const statusColor = catBlocks > 0 ? colors.error : catWarns > 0 ? colors.warning : colors.success;
466
+
467
+ console.log(` ${statusColor}${statusIcon}${c.reset} ${icon} ${c.bold}${category.padEnd(18)}${c.reset} ${catBlocks > 0 ? `${colors.error}${catBlocks} blockers${c.reset} ` : ''}${catWarns > 0 ? `${colors.warning}${catWarns} warnings${c.reset}` : ''}`);
468
+ }
469
+ }
470
+
471
+ function printBlockerDetails(findings, maxShow = 6) {
472
+ const blockers = findings.filter(f => f.severity === 'BLOCK');
473
+
474
+ if (blockers.length === 0) return;
475
+
476
+ printSection(`BLOCKERS (${blockers.length})`, '🚨');
477
+ console.log();
478
+
479
+ for (const blocker of blockers.slice(0, maxShow)) {
480
+ const style = getSeverityStyle(blocker.severity);
481
+ const icon = getCategoryIcon(blocker.category);
482
+
483
+ // Severity badge
484
+ console.log(` ${style.bg}${c.bold} ${style.label} ${c.reset} ${icon} ${c.bold}${truncate(blocker.title, 45)}${c.reset}`);
485
+
486
+ // Page URL
487
+ if (blocker.page) {
488
+ console.log(` ${' '.repeat(10)} ${colors.info}${ICONS.page} ${truncate(blocker.page, 50)}${c.reset}`);
489
+ }
490
+
491
+ // Reason
492
+ if (blocker.reason) {
493
+ console.log(` ${' '.repeat(10)} ${c.dim}${truncate(blocker.reason, 50)}${c.reset}`);
494
+ }
495
+
496
+ // Screenshot
497
+ if (blocker.screenshot) {
498
+ console.log(` ${' '.repeat(10)} ${colors.accent}${ICONS.screenshot} ${truncate(blocker.screenshot, 45)}${c.reset}`);
499
+ }
500
+
501
+ console.log();
502
+ }
503
+
504
+ if (blockers.length > maxShow) {
505
+ console.log(` ${c.dim}... and ${blockers.length - maxShow} more blockers (see full report)${c.reset}`);
506
+ console.log();
507
+ }
508
+ }
509
+
510
+ // ═══════════════════════════════════════════════════════════════════════════════
511
+ // VERDICT DISPLAY
512
+ // ═══════════════════════════════════════════════════════════════════════════════
513
+
514
+ function getVerdictConfig(blocks, warns) {
515
+ if (blocks === 0 && warns === 0) {
516
+ return {
517
+ verdict: 'CLEAN',
518
+ icon: '✅',
519
+ headline: 'REALITY VERIFIED',
520
+ tagline: 'All UI elements are responsive and functional!',
521
+ color: colors.success,
522
+ bgColor: bgRgb(0, 80, 50),
523
+ borderColor: rgb(0, 200, 120),
524
+ };
525
+ }
526
+
527
+ if (blocks === 0) {
528
+ return {
529
+ verdict: 'WARN',
530
+ icon: '⚠️',
531
+ headline: 'MINOR ISSUES',
532
+ tagline: `${warns} warning${warns !== 1 ? 's' : ''} found - review recommended`,
533
+ color: colors.warning,
534
+ bgColor: bgRgb(80, 60, 0),
535
+ borderColor: rgb(200, 160, 0),
536
+ };
537
+ }
538
+
539
+ return {
540
+ verdict: 'BLOCK',
541
+ icon: '🛑',
542
+ headline: 'DEAD UI DETECTED',
543
+ tagline: `${blocks} blocker${blocks !== 1 ? 's' : ''} must be fixed`,
544
+ color: colors.error,
545
+ bgColor: bgRgb(80, 20, 20),
546
+ borderColor: rgb(200, 60, 60),
547
+ };
548
+ }
549
+
550
+ function printVerdictCard(blocks, warns, duration) {
551
+ const config = getVerdictConfig(blocks, warns);
552
+ const w = 68;
553
+
554
+ console.log();
555
+ console.log();
556
+
557
+ // Top border
558
+ console.log(` ${config.borderColor}${BOX.dTopLeft}${BOX.dHorizontal.repeat(w)}${BOX.dTopRight}${c.reset}`);
559
+
560
+ // Empty line
561
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
562
+
563
+ // Icon and headline
564
+ const headlineText = `${config.icon} ${config.headline}`;
565
+ const headlinePadded = padCenter(headlineText, w);
566
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${config.color}${c.bold}${headlinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
567
+
568
+ // Tagline
569
+ const taglinePadded = padCenter(config.tagline, w);
570
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${c.dim}${taglinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
571
+
572
+ // Empty line
573
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
574
+
575
+ // Stats row
576
+ const stats = `Blockers: ${blocks} • Warnings: ${warns} • Duration: ${formatDuration(duration)}`;
577
+ const statsPadded = padCenter(stats, w);
578
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${c.dim}${statsPadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
579
+
580
+ // Empty line
581
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
582
+
583
+ // Bottom border
584
+ console.log(` ${config.borderColor}${BOX.dBottomLeft}${BOX.dHorizontal.repeat(w)}${BOX.dBottomRight}${c.reset}`);
585
+
586
+ console.log();
587
+ }
588
+
589
+ // ═══════════════════════════════════════════════════════════════════════════════
590
+ // TIER WARNING DISPLAY
591
+ // ═══════════════════════════════════════════════════════════════════════════════
592
+
593
+ function printTierWarning(tier, limits, originalMaxPages, appliedMaxPages, verifyAuthRequested, verifyAuthApplied) {
594
+ if (tier !== 'free') return;
595
+
596
+ console.log();
597
+ console.log(` ${colors.warning}${ICONS.warning}${c.reset} ${c.bold}FREE TIER: Preview Mode${c.reset}`);
598
+
599
+ if (originalMaxPages > appliedMaxPages) {
600
+ console.log(` ${c.dim}Pages capped:${c.reset} ${appliedMaxPages} ${c.dim}(requested ${originalMaxPages})${c.reset}`);
601
+ }
602
+
603
+ if (verifyAuthRequested && !verifyAuthApplied) {
604
+ console.log(` ${c.dim}Auth boundary:${c.reset} ${colors.error}disabled${c.reset} ${c.dim}(requires STARTER+)${c.reset}`);
605
+ }
606
+
607
+ console.log(` ${colors.accent}Upgrade:${c.reset} ${c.dim}https://vibecheckai.dev/pricing${c.reset}`);
608
+ console.log();
609
+ }
610
+
611
+ // ═══════════════════════════════════════════════════════════════════════════════
612
+ // HELP DISPLAY
613
+ // ═══════════════════════════════════════════════════════════════════════════════
614
+
615
+ function printHelp() {
616
+ console.log(BANNER_FULL);
617
+ console.log(`
618
+ ${c.bold}Usage:${c.reset} vibecheck reality --url <url> [options]
619
+
620
+ ${c.bold}Runtime UI Verification${c.reset} — Prove your UI actually works.
621
+
622
+ ${c.bold}Two-Pass Architecture:${c.reset}
623
+ ${colors.anon}${ICONS.anon} Pass A (Anon)${c.reset} Crawl without auth, record protected routes
624
+ ${colors.auth}${ICONS.auth} Pass B (Auth)${c.reset} Re-crawl with session, verify access
625
+
626
+ ${c.bold}What It Detects:${c.reset}
627
+ ${colors.deadUI}${ICONS.deadUI} Dead UI${c.reset} Clicks that do nothing
628
+ ${colors.authCoverage}${ICONS.shield} Auth Gaps${c.reset} Protected routes accessible anonymously
629
+ ${colors.httpError}${ICONS.http} HTTP Errors${c.reset} 4xx/5xx responses
630
+
631
+ ${c.bold}Options:${c.reset}
632
+ ${colors.accent}--url, -u <url>${c.reset} Base URL for testing ${c.dim}(required)${c.reset}
633
+ ${colors.accent}--auth <email:pass>${c.reset} Login credentials for auth verification
634
+ ${colors.accent}--storage-state <path>${c.reset} Playwright session state file
635
+ ${colors.accent}--save-storage-state <p>${c.reset} Save session after login
636
+ ${colors.accent}--truthpack <path>${c.reset} Custom truthpack path
637
+ ${colors.accent}--verify-auth${c.reset} Enable two-pass auth verification
638
+ ${colors.accent}--headed${c.reset} Run browser visible ${c.dim}(for debugging)${c.reset}
639
+ ${colors.accent}--danger${c.reset} Allow clicking destructive elements
640
+ ${colors.accent}--max-pages <n>${c.reset} Max pages to crawl ${c.dim}(default: 18)${c.reset}
641
+ ${colors.accent}--max-depth <n>${c.reset} Max crawl depth ${c.dim}(default: 2)${c.reset}
642
+ ${colors.accent}--timeout <ms>${c.reset} Page timeout ${c.dim}(default: 15000)${c.reset}
643
+ ${colors.accent}--help, -h${c.reset} Show this help
644
+
645
+ ${c.bold}Tier Limits:${c.reset}
646
+ ${c.dim}FREE${c.reset} 5 pages, no auth boundary
647
+ ${c.dim}STARTER${c.reset} Full budgets + basic auth
648
+ ${c.dim}PRO${c.reset} Advanced auth (multi-role)
649
+
650
+ ${c.bold}Exit Codes:${c.reset}
651
+ ${colors.success}0${c.reset} CLEAN — No issues found
652
+ ${colors.warning}1${c.reset} WARN — Warnings found
653
+ ${colors.error}2${c.reset} BLOCK — Blockers found (dead UI, auth gaps)
654
+
655
+ ${c.bold}Examples:${c.reset}
656
+ ${c.dim}# Basic crawl${c.reset}
657
+ vibecheck reality --url http://localhost:3000
658
+
659
+ ${c.dim}# With auth verification${c.reset}
660
+ vibecheck reality --url http://localhost:3000 --verify-auth --auth user@test.com:pass
661
+
662
+ ${c.dim}# Debug mode (visible browser)${c.reset}
663
+ vibecheck reality --url http://localhost:3000 --headed
664
+
665
+ ${c.dim}# Allow destructive actions${c.reset}
666
+ vibecheck reality --url http://localhost:3000 --danger
667
+ `);
668
+ }
669
+
670
+ // ═══════════════════════════════════════════════════════════════════════════════
671
+ // UTILITY FUNCTIONS (preserved from original)
672
+ // ═══════════════════════════════════════════════════════════════════════════════
673
+
29
674
  function ensureDir(p) {
30
675
  fs.mkdirSync(p, { recursive: true });
31
676
  }
@@ -216,7 +861,7 @@ async function attemptLogin(page, { auth }) {
216
861
  }
217
862
  }
218
863
 
219
- async function runSinglePass({ label, baseUrl, context, shotsDir, danger, maxPages, maxDepth, timeoutMs, root }) {
864
+ async function runSinglePass({ label, baseUrl, context, shotsDir, danger, maxPages, maxDepth, timeoutMs, root, onProgress }) {
220
865
  const page = await context.newPage();
221
866
  page.setDefaultTimeout(timeoutMs);
222
867
 
@@ -244,6 +889,11 @@ async function runSinglePass({ label, baseUrl, context, shotsDir, danger, maxPag
244
889
 
245
890
  visited.add(targetUrl);
246
891
 
892
+ // Progress callback
893
+ if (onProgress) {
894
+ onProgress({ page: pagesVisited.length + 1, maxPages, url: targetUrl });
895
+ }
896
+
247
897
  const res = await page.goto(targetUrl, { waitUntil: "domcontentloaded" }).catch(() => null);
248
898
  await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
249
899
 
@@ -371,44 +1021,9 @@ function coverageFromTruthpack({ truthpack, visitedUrls }) {
371
1021
  return { total, hit, percent: total ? Math.round((hit / total) * 100) : 0, missed: Array.from(uiPaths).filter(p => !visitedPaths.has(p)).slice(0, 50) };
372
1022
  }
373
1023
 
374
- function printHelp() {
375
- console.log(`
376
- vibecheck reality - Runtime UI Verification
377
-
378
- USAGE
379
- vibecheck reality --url <url> [options]
380
-
381
- OPTIONS
382
- --url, -u <url> Base URL for runtime testing (required)
383
- --auth <email:pass> Login credentials for auth verification
384
- --storage-state <path> Playwright session state file
385
- --save-storage-state <p> Save session state after login
386
- --truthpack <path> Custom truthpack path
387
- --verify-auth Enable two-pass auth verification
388
- --headed Run browser in headed mode (visible)
389
- --danger Allow clicking potentially destructive elements
390
- --max-pages <n> Max pages to crawl (default: 18)
391
- --max-depth <n> Max crawl depth (default: 2)
392
- --timeout <ms> Page timeout (default: 15000)
393
- --help, -h Show this help
394
-
395
- WHAT IT DOES
396
- 1. Pass A (Anon): Crawls and clicks, records protected routes
397
- 2. Pass B (Auth): Re-crawls with auth, verifies access
398
- 3. Detects dead UI (clicks that do nothing)
399
- 4. Calculates route coverage stats
400
-
401
- FINDINGS
402
- - Dead UI → FIX_DEAD_UI mission
403
- - Auth gaps → ADD_SERVER_AUTH mission
404
- - HTTP errors → error findings
405
-
406
- EXAMPLES
407
- vibecheck reality --url http://localhost:3000
408
- vibecheck reality --url http://localhost:3000 --verify-auth --auth user@test.com:pass123
409
- vibecheck reality --url http://localhost:3000 --headed --danger
410
- `);
411
- }
1024
+ // ═══════════════════════════════════════════════════════════════════════════════
1025
+ // MAIN REALITY FUNCTION
1026
+ // ═══════════════════════════════════════════════════════════════════════════════
412
1027
 
413
1028
  async function runReality(argsOrOpts = {}) {
414
1029
  // Handle array args from CLI
@@ -440,7 +1055,7 @@ async function runReality(argsOrOpts = {}) {
440
1055
  };
441
1056
  }
442
1057
 
443
- const {
1058
+ let {
444
1059
  repoRoot,
445
1060
  url,
446
1061
  auth,
@@ -457,17 +1072,67 @@ async function runReality(argsOrOpts = {}) {
457
1072
 
458
1073
  if (!url) {
459
1074
  printHelp();
460
- console.log("\n Error: --url is required\n");
1075
+ console.log(`\n ${colors.error}${ICONS.cross}${c.reset} ${c.bold}Error:${c.reset} --url is required\n`);
461
1076
  return 1;
462
1077
  }
1078
+
1079
+ const root = repoRoot || process.cwd();
1080
+ const projectName = path.basename(root);
1081
+ const startTime = Date.now();
1082
+ const originalMaxPages = maxPages;
1083
+ const originalVerifyAuth = verifyAuth;
1084
+
1085
+ // TIER ENFORCEMENT
1086
+ let tierInfo = { tier: 'free', limits: {} };
1087
+ try {
1088
+ const access = await entitlements.enforce("reality", {
1089
+ projectPath: root,
1090
+ silent: true,
1091
+ });
1092
+
1093
+ tierInfo = access;
1094
+ const limits = access.limits || entitlements.getLimits(access.tier);
1095
+
1096
+ // Apply tier-based caps
1097
+ if (access.downgrade === "reality.preview" || access.tier === "free") {
1098
+ const previewMax = limits.realityMaxPages || 5;
1099
+ if (maxPages > previewMax) {
1100
+ maxPages = previewMax;
1101
+ }
1102
+
1103
+ if (verifyAuth && !limits.realityAuthBoundary) {
1104
+ verifyAuth = false;
1105
+ }
1106
+ }
1107
+ } catch (e) {
1108
+ // Continue with defaults if entitlements unavailable
1109
+ }
1110
+
1111
+ // Print banner
1112
+ printBanner();
1113
+
1114
+ console.log(` ${c.dim}Project:${c.reset} ${c.bold}${projectName}${c.reset}`);
1115
+ console.log(` ${c.dim}URL:${c.reset} ${colors.accent}${url}${c.reset}`);
1116
+ console.log(` ${c.dim}Mode:${c.reset} ${verifyAuth ? `${colors.auth}Two-Pass (Auth)${c.reset}` : `${colors.anon}Single-Pass (Anon)${c.reset}`}`);
1117
+ console.log(` ${c.dim}Budget:${c.reset} ${maxPages} pages, depth ${maxDepth}`);
1118
+
1119
+ // Tier warning if applicable
1120
+ if (tierInfo.tier === 'free' && (originalMaxPages > maxPages || (originalVerifyAuth && !verifyAuth))) {
1121
+ printTierWarning(tierInfo.tier, tierInfo.limits, originalMaxPages, maxPages, originalVerifyAuth, verifyAuth);
1122
+ }
1123
+
1124
+ // Playwright check
463
1125
  if (!chromium) {
464
1126
  const hint = playwrightError?.includes("Cannot find module")
465
1127
  ? "Run: npm i -D playwright && npx playwright install chromium"
466
1128
  : `Playwright error: ${playwrightError || "unknown"}`;
467
- throw new Error(`Playwright not available. ${hint}`);
1129
+ console.log();
1130
+ console.log(` ${colors.error}${ICONS.cross}${c.reset} ${c.bold}Playwright not available${c.reset}`);
1131
+ console.log(` ${c.dim}${hint}${c.reset}`);
1132
+ console.log();
1133
+ return 1;
468
1134
  }
469
1135
 
470
- const root = repoRoot || process.cwd();
471
1136
  const baseUrl = normalizeUrl(url);
472
1137
  const outBase = path.join(root, ".vibecheck", "reality", stamp());
473
1138
  const shotsDir = path.join(outBase, "screenshots");
@@ -475,20 +1140,54 @@ async function runReality(argsOrOpts = {}) {
475
1140
 
476
1141
  const tp = loadTruthpack(root, truthpack);
477
1142
  const matchers = getProtectedMatchersFromTruthpack(tp);
1143
+
1144
+ if (tp) {
1145
+ console.log(` ${c.dim}Truthpack:${c.reset} ${colors.success}${ICONS.check}${c.reset} loaded (${matchers.length} protected patterns)`);
1146
+ }
478
1147
 
1148
+ // Launch browser
1149
+ console.log();
1150
+ startSpinner('Launching browser...', colors.accent);
479
1151
  const browser = await chromium.launch({ headless: !headed });
480
-
481
- // Pass A: Anon
1152
+ stopSpinner('Browser launched', true);
1153
+
1154
+ // ═══════════════════════════════════════════════════════════════════════════
1155
+ // PASS A: ANONYMOUS
1156
+ // ═══════════════════════════════════════════════════════════════════════════
1157
+ printPassHeader('anon', baseUrl);
1158
+
1159
+ startSpinner('Crawling anonymously...', colors.anon);
482
1160
  const anonContext = await browser.newContext();
483
- const anonPass = await runSinglePass({ label: "ANON", baseUrl, context: anonContext, shotsDir, danger, maxPages, maxDepth, timeoutMs, root });
1161
+ const anonPass = await runSinglePass({
1162
+ label: "ANON",
1163
+ baseUrl,
1164
+ context: anonContext,
1165
+ shotsDir,
1166
+ danger,
1167
+ maxPages,
1168
+ maxDepth,
1169
+ timeoutMs,
1170
+ root,
1171
+ onProgress: ({ page, maxPages: mp, url: currentUrl }) => {
1172
+ // Could update spinner here if desired
1173
+ }
1174
+ });
484
1175
  await anonContext.close();
1176
+ stopSpinner(`Crawled ${anonPass.pagesVisited.length} pages`, true);
1177
+
1178
+ printPassResult('anon', anonPass);
485
1179
 
486
- // Pass B: Auth (optional)
1180
+ // ═══════════════════════════════════════════════════════════════════════════
1181
+ // PASS B: AUTHENTICATED (optional)
1182
+ // ═══════════════════════════════════════════════════════════════════════════
487
1183
  let authPass = null;
488
1184
  let authFindings = [];
489
1185
  let savedStatePath = null;
490
1186
 
491
1187
  if (verifyAuth) {
1188
+ printPassHeader('auth', baseUrl);
1189
+
1190
+ startSpinner('Setting up authenticated session...', colors.auth);
492
1191
  const ctxOpts = storageState ? { storageState } : {};
493
1192
  const authContext = await browser.newContext(ctxOpts);
494
1193
  const authPage = await authContext.newPage();
@@ -496,34 +1195,73 @@ async function runReality(argsOrOpts = {}) {
496
1195
  await authPage.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
497
1196
 
498
1197
  if (!storageState && auth) {
1198
+ stopSpinner('Attempting login...', true);
1199
+ startSpinner('Logging in...', colors.auth);
1200
+
499
1201
  const loginRes = await attemptLogin(authPage, { auth });
500
- if (loginRes.ok && saveStorageState) {
501
- const dest = path.isAbsolute(saveStorageState) ? saveStorageState : path.join(root, saveStorageState);
502
- ensureDir(path.dirname(dest));
503
- await authContext.storageState({ path: dest }).catch(() => {});
504
- savedStatePath = dest;
1202
+
1203
+ if (loginRes.ok) {
1204
+ stopSpinner('Login successful', true);
1205
+ if (saveStorageState) {
1206
+ const dest = path.isAbsolute(saveStorageState) ? saveStorageState : path.join(root, saveStorageState);
1207
+ ensureDir(path.dirname(dest));
1208
+ await authContext.storageState({ path: dest }).catch(() => {});
1209
+ savedStatePath = dest;
1210
+ console.log(` ${colors.success}${ICONS.check}${c.reset} Session saved: ${c.dim}${path.relative(root, dest)}${c.reset}`);
1211
+ }
1212
+ } else {
1213
+ stopSpinner('Login failed - continuing without auth', false);
505
1214
  }
1215
+ } else {
1216
+ stopSpinner('Using existing session', true);
506
1217
  }
1218
+
507
1219
  await authPage.close();
508
1220
 
509
- authPass = await runSinglePass({ label: "AUTH", baseUrl, context: authContext, shotsDir, danger, maxPages, maxDepth, timeoutMs, root });
1221
+ startSpinner('Crawling with authentication...', colors.auth);
1222
+ authPass = await runSinglePass({
1223
+ label: "AUTH",
1224
+ baseUrl,
1225
+ context: authContext,
1226
+ shotsDir,
1227
+ danger,
1228
+ maxPages,
1229
+ maxDepth,
1230
+ timeoutMs,
1231
+ root
1232
+ });
510
1233
  await authContext.close();
1234
+ stopSpinner(`Crawled ${authPass.pagesVisited.length} pages`, true);
1235
+
1236
+ printPassResult('auth', authPass);
511
1237
 
1238
+ // Build auth coverage findings
512
1239
  if (matchers.length) {
1240
+ startSpinner('Analyzing auth coverage...', colors.authCoverage);
513
1241
  authFindings = buildAuthCoverageFindings({ baseUrl, matchers, anonPass, authPass });
1242
+ stopSpinner(`Found ${authFindings.length} auth issues`, authFindings.length === 0);
514
1243
  }
515
1244
  }
516
1245
 
517
1246
  await browser.close();
518
1247
 
1248
+ // ═══════════════════════════════════════════════════════════════════════════
1249
+ // ANALYSIS & RESULTS
1250
+ // ═══════════════════════════════════════════════════════════════════════════
1251
+
519
1252
  const allVisited = [...anonPass.pagesVisited.map(p => p.url), ...(authPass?.pagesVisited || []).map(p => p.url)];
520
1253
  const coverage = coverageFromTruthpack({ truthpack: tp, visitedUrls: allVisited });
521
1254
 
522
1255
  const findings = [...anonPass.findings, ...(authPass?.findings || []), ...authFindings];
1256
+ const blocks = findings.filter(f => f.severity === "BLOCK").length;
1257
+ const warns = findings.filter(f => f.severity === "WARN").length;
523
1258
 
1259
+ // Build report
524
1260
  const report = {
525
1261
  meta: {
526
- startedAt: new Date().toISOString(),
1262
+ startedAt: new Date(startTime).toISOString(),
1263
+ finishedAt: new Date().toISOString(),
1264
+ durationMs: Date.now() - startTime,
527
1265
  baseUrl,
528
1266
  verifyAuth,
529
1267
  maxPages,
@@ -539,6 +1277,7 @@ async function runReality(argsOrOpts = {}) {
539
1277
  networkErrors: [...anonPass.networkErrors, ...(authPass?.networkErrors || [])].slice(0, 50)
540
1278
  };
541
1279
 
1280
+ // Write reports
542
1281
  fs.writeFileSync(path.join(outBase, "reality_report.json"), JSON.stringify(report, null, 2), "utf8");
543
1282
 
544
1283
  const latestDir = path.join(root, ".vibecheck", "reality");
@@ -546,18 +1285,45 @@ async function runReality(argsOrOpts = {}) {
546
1285
  fs.writeFileSync(path.join(latestDir, "latest.json"), JSON.stringify({ latest: path.relative(root, outBase).replace(/\\/g, "/") }, null, 2));
547
1286
  fs.writeFileSync(path.join(latestDir, "last_reality.json"), JSON.stringify(report, null, 2), "utf8");
548
1287
 
549
- const blocks = findings.filter(f => f.severity === "BLOCK").length;
550
- const warns = findings.filter(f => f.severity === "WARN").length;
551
-
552
- console.log(`\n🧪 vibecheck reality`);
553
- console.log(`Anon visited: ${anonPass.pagesVisited.length}/${maxPages}`);
554
- if (verifyAuth) console.log(`Auth visited: ${authPass?.pagesVisited?.length || 0}/${maxPages}`);
555
- if (coverage) console.log(`Coverage: ${coverage.percent}% of UI paths (${coverage.hit}/${coverage.total})`);
556
- console.log(`Findings: ${findings.length} (${blocks} BLOCK, ${warns} WARN)`);
557
- console.log(`Report: .vibecheck/reality/${path.basename(outBase)}/reality_report.json`);
558
- console.log(`Verdict: ${blocks ? "🛑 BLOCK" : warns ? "⚠️ WARN" : "✅ CLEAN"}`);
1288
+ const duration = Date.now() - startTime;
1289
+
1290
+ // ═══════════════════════════════════════════════════════════════════════════
1291
+ // OUTPUT
1292
+ // ═══════════════════════════════════════════════════════════════════════════
1293
+
1294
+ // Coverage card
1295
+ printCoverageCard(coverage, anonPass.pagesVisited.length, authPass?.pagesVisited?.length, maxPages);
1296
+
1297
+ // Findings breakdown
1298
+ printFindingsBreakdown(findings);
1299
+
1300
+ // Blocker details
1301
+ printBlockerDetails(findings);
1302
+
1303
+ // Verdict card
1304
+ printVerdictCard(blocks, warns, duration);
1305
+
1306
+ // Report links
1307
+ printSection('REPORTS', ICONS.page);
1308
+ console.log();
1309
+ console.log(` ${colors.accent}${path.relative(root, outBase)}/reality_report.json${c.reset}`);
1310
+ console.log(` ${c.dim}${path.join('.vibecheck', 'reality', 'last_reality.json')}${c.reset}`);
1311
+ if (anonPass.findings.some(f => f.screenshot) || authPass?.findings?.some(f => f.screenshot)) {
1312
+ console.log(` ${colors.accent}${ICONS.screenshot} ${path.relative(root, shotsDir)}/${c.reset} ${c.dim}(screenshots)${c.reset}`);
1313
+ }
1314
+ console.log();
1315
+
1316
+ // Next steps if issues found
1317
+ if (blocks > 0 || warns > 0) {
1318
+ printSection('NEXT STEPS', ICONS.lightning);
1319
+ console.log();
1320
+ console.log(` ${colors.accent}vibecheck fix${c.reset} ${c.dim}Auto-fix dead UI issues${c.reset}`);
1321
+ console.log(` ${colors.accent}vibecheck ship${c.reset} ${c.dim}Re-check ship readiness${c.reset}`);
1322
+ console.log();
1323
+ }
559
1324
 
560
1325
  process.exitCode = blocks ? 2 : warns ? 1 : 0;
1326
+ return process.exitCode;
561
1327
  }
562
1328
 
563
- module.exports = { runReality };
1329
+ module.exports = { runReality };