@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.
- package/README.md +77 -484
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -0
- package/bin/runners/lib/entitlements-v2.js +409 -299
- package/bin/runners/lib/report-html.js +378 -1
- package/bin/runners/runBadge.js +823 -116
- package/bin/runners/runCtx.js +602 -119
- package/bin/runners/runDoctor.js +329 -42
- package/bin/runners/runFix.js +562 -83
- package/bin/runners/runGraph.js +231 -74
- package/bin/runners/runInit.js +647 -88
- package/bin/runners/runInstall.js +207 -46
- package/bin/runners/runMcp.js +58 -0
- package/bin/runners/runPR.js +172 -13
- package/bin/runners/runProve.js +818 -97
- package/bin/runners/runReality.js +831 -65
- package/bin/runners/runReport.js +108 -2
- package/bin/runners/runShare.js +156 -38
- package/bin/runners/runShip.js +919 -792
- package/bin/runners/runWatch.js +215 -38
- package/bin/vibecheck.js +158 -59
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
-
//
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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 };
|