@vibecheckai/cli 3.0.9 → 3.1.0

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,10 @@
1
1
  /**
2
2
  * vibecheck ship - The Vibe Coder's Best Friend
3
3
  * Zero config. Plain English. One command to ship with confidence.
4
+ *
5
+ * ═══════════════════════════════════════════════════════════════════════════════
6
+ * ENTERPRISE EDITION - World-Class Terminal Experience
7
+ * ═══════════════════════════════════════════════════════════════════════════════
4
8
  */
5
9
 
6
10
  const path = require("path");
@@ -9,6 +13,14 @@ const { withErrorHandling } = require("./lib/error-handler");
9
13
  const { ensureOutputDir, detectProjectFeatures } = require("./utils");
10
14
  const { enforceLimit, enforceFeature, trackUsage, getCurrentTier } = require("./lib/entitlements");
11
15
  const { emitShipCheck } = require("./lib/audit-bridge");
16
+ const {
17
+ generateRunId,
18
+ createJsonOutput,
19
+ writeJsonOutput,
20
+ exitCodeToVerdict,
21
+ verdictToExitCode,
22
+ saveArtifact
23
+ } = require("./lib/cli-output");
12
24
 
13
25
  // Route Truth v1 - Fake endpoint detection
14
26
  const { buildTruthpack, writeTruthpack, detectFastifyEntry } = require("./lib/truth");
@@ -22,10 +34,690 @@ const {
22
34
  findOwnerModeBypass
23
35
  } = require("./lib/analyzers");
24
36
  const { findingsFromReality } = require("./lib/reality-findings");
25
- // Contract Drift Detection - per spec: "routes/env/auth drift → usually BLOCK"
26
37
  const { findContractDrift, loadContracts, hasContracts, getDriftSummary } = require("./lib/drift");
38
+ const upsell = require("./lib/upsell");
39
+ const entitlements = require("./lib/entitlements-v2");
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
+ bgBrightBlack: '\x1b[100m',
83
+ bgBrightRed: '\x1b[101m',
84
+ bgBrightGreen: '\x1b[102m',
85
+ bgBrightYellow: '\x1b[103m',
86
+ // Cursor control
87
+ cursorUp: (n = 1) => `\x1b[${n}A`,
88
+ cursorDown: (n = 1) => `\x1b[${n}B`,
89
+ cursorRight: (n = 1) => `\x1b[${n}C`,
90
+ cursorLeft: (n = 1) => `\x1b[${n}D`,
91
+ clearLine: '\x1b[2K',
92
+ clearScreen: '\x1b[2J',
93
+ saveCursor: '\x1b[s',
94
+ restoreCursor: '\x1b[u',
95
+ hideCursor: '\x1b[?25l',
96
+ showCursor: '\x1b[?25h',
97
+ };
98
+
99
+ // 256-color / True color support
100
+ const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
101
+ const bgRgb = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
102
+
103
+ // Premium color palette
104
+ const colors = {
105
+ // Gradients for banner
106
+ gradient1: rgb(0, 255, 200), // Cyan-green
107
+ gradient2: rgb(0, 200, 255), // Cyan
108
+ gradient3: rgb(50, 150, 255), // Blue
109
+ gradient4: rgb(100, 100, 255), // Purple-blue
110
+ gradient5: rgb(150, 50, 255), // Purple
111
+ gradient6: rgb(200, 0, 255), // Magenta
112
+
113
+ // Verdict colors
114
+ shipGreen: rgb(0, 255, 150),
115
+ warnAmber: rgb(255, 200, 0),
116
+ blockRed: rgb(255, 80, 80),
117
+
118
+ // UI colors
119
+ accent: rgb(100, 200, 255),
120
+ muted: rgb(120, 120, 140),
121
+ subtle: rgb(80, 80, 100),
122
+ highlight: rgb(255, 255, 255),
123
+
124
+ // Severity colors
125
+ critical: rgb(255, 60, 60),
126
+ high: rgb(255, 120, 60),
127
+ medium: rgb(255, 200, 60),
128
+ low: rgb(100, 200, 255),
129
+ info: rgb(150, 150, 180),
130
+ };
131
+
132
+ // ═══════════════════════════════════════════════════════════════════════════════
133
+ // PREMIUM BANNER
134
+ // ═══════════════════════════════════════════════════════════════════════════════
135
+
136
+ const SHIP_BANNER = `
137
+ ${rgb(0, 255, 200)} ███████╗██╗ ██╗██╗██████╗ ${c.reset}
138
+ ${rgb(0, 230, 220)} ██╔════╝██║ ██║██║██╔══██╗${c.reset}
139
+ ${rgb(0, 200, 255)} ███████╗███████║██║██████╔╝${c.reset}
140
+ ${rgb(50, 150, 255)} ╚════██║██╔══██║██║██╔═══╝ ${c.reset}
141
+ ${rgb(100, 100, 255)} ███████║██║ ██║██║██║ ${c.reset}
142
+ ${rgb(150, 50, 255)} ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ${c.reset}
143
+ `;
144
+
145
+ const BANNER_FULL = `
146
+ ${rgb(0, 255, 200)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${c.reset}
147
+ ${rgb(0, 230, 220)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${c.reset}
148
+ ${rgb(0, 200, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${c.reset}
149
+ ${rgb(50, 150, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${c.reset}
150
+ ${rgb(100, 100, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${c.reset}
151
+ ${rgb(150, 50, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${c.reset}
152
+
153
+ ${c.dim} ┌─────────────────────────────────────────────────────────────────────┐${c.reset}
154
+ ${c.dim} │${c.reset} ${rgb(0, 255, 200)}🚀${c.reset} ${c.bold}SHIP${c.reset} ${c.dim}•${c.reset} ${rgb(200, 200, 200)}The One Command${c.reset} ${c.dim}•${c.reset} ${rgb(150, 150, 150)}Zero Config${c.reset} ${c.dim}•${c.reset} ${rgb(100, 100, 100)}Plain English${c.reset} ${c.dim}│${c.reset}
155
+ ${c.dim} └─────────────────────────────────────────────────────────────────────┘${c.reset}
156
+ `;
157
+
158
+ // ═══════════════════════════════════════════════════════════════════════════════
159
+ // TERMINAL UTILITIES
160
+ // ═══════════════════════════════════════════════════════════════════════════════
161
+
162
+ const BOX = {
163
+ topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
164
+ horizontal: '─', vertical: '│',
165
+ teeRight: '├', teeLeft: '┤', teeDown: '┬', teeUp: '┴',
166
+ cross: '┼',
167
+ // Double line variants
168
+ dTopLeft: '╔', dTopRight: '╗', dBottomLeft: '╚', dBottomRight: '╝',
169
+ dHorizontal: '═', dVertical: '║',
170
+ };
171
+
172
+ const ICONS = {
173
+ ship: '🚀',
174
+ check: '✓',
175
+ cross: '✗',
176
+ warning: '⚠',
177
+ error: '✗',
178
+ info: 'ℹ',
179
+ arrow: '→',
180
+ bullet: '•',
181
+ star: '★',
182
+ sparkle: '✨',
183
+ fire: '🔥',
184
+ lock: '🔐',
185
+ key: '🔑',
186
+ link: '🔗',
187
+ graph: '📊',
188
+ map: '🗺️',
189
+ doc: '📄',
190
+ folder: '📁',
191
+ clock: '⏱',
192
+ target: '🎯',
193
+ shield: '🛡️',
194
+ bug: '🐛',
195
+ wrench: '🔧',
196
+ lightning: '⚡',
197
+ package: '📦',
198
+ route: '🛤️',
199
+ env: '🌍',
200
+ auth: '🔒',
201
+ money: '💰',
202
+ ghost: '👻',
203
+ dead: '💀',
204
+ };
205
+
206
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
207
+ const SPINNER_DOTS = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
208
+ const SPINNER_ARROWS = ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'];
209
+
210
+ let spinnerIndex = 0;
211
+ let spinnerInterval = null;
212
+ let spinnerStartTime = null;
213
+
214
+ function formatDuration(ms) {
215
+ if (ms < 1000) return `${ms}ms`;
216
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
217
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
218
+ }
219
+
220
+ function formatNumber(num) {
221
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
222
+ }
223
+
224
+ function truncate(str, len) {
225
+ if (!str) return '';
226
+ if (str.length <= len) return str;
227
+ return str.slice(0, len - 3) + '...';
228
+ }
229
+
230
+ function padCenter(str, width) {
231
+ const padding = Math.max(0, width - str.length);
232
+ const left = Math.floor(padding / 2);
233
+ const right = padding - left;
234
+ return ' '.repeat(left) + str + ' '.repeat(right);
235
+ }
236
+
237
+ function progressBar(percent, width = 30, opts = {}) {
238
+ const filled = Math.round((percent / 100) * width);
239
+ const empty = width - filled;
240
+
241
+ let filledColor;
242
+ if (opts.color) {
243
+ filledColor = opts.color;
244
+ } else if (percent >= 80) {
245
+ filledColor = colors.shipGreen;
246
+ } else if (percent >= 50) {
247
+ filledColor = colors.warnAmber;
248
+ } else {
249
+ filledColor = colors.blockRed;
250
+ }
251
+
252
+ const filledChar = opts.filled || '█';
253
+ const emptyChar = opts.empty || '░';
254
+
255
+ return `${filledColor}${filledChar.repeat(filled)}${c.dim}${emptyChar.repeat(empty)}${c.reset}`;
256
+ }
257
+
258
+ function startSpinner(message, style = 'dots') {
259
+ const frames = style === 'arrows' ? SPINNER_ARROWS :
260
+ style === 'dots' ? SPINNER_DOTS : SPINNER_FRAMES;
261
+ spinnerStartTime = Date.now();
262
+ process.stdout.write(c.hideCursor);
263
+
264
+ spinnerInterval = setInterval(() => {
265
+ const elapsed = formatDuration(Date.now() - spinnerStartTime);
266
+ process.stdout.write(`\r${c.clearLine} ${colors.accent}${frames[spinnerIndex]}${c.reset} ${message} ${c.dim}${elapsed}${c.reset}`);
267
+ spinnerIndex = (spinnerIndex + 1) % frames.length;
268
+ }, 80);
269
+ }
270
+
271
+ function stopSpinner(message, success = true) {
272
+ if (spinnerInterval) {
273
+ clearInterval(spinnerInterval);
274
+ spinnerInterval = null;
275
+ }
276
+ const elapsed = spinnerStartTime ? formatDuration(Date.now() - spinnerStartTime) : '';
277
+ const icon = success ? `${colors.shipGreen}${ICONS.check}${c.reset}` : `${colors.blockRed}${ICONS.cross}${c.reset}`;
278
+ process.stdout.write(`\r${c.clearLine} ${icon} ${message} ${c.dim}${elapsed}${c.reset}\n`);
279
+ process.stdout.write(c.showCursor);
280
+ spinnerStartTime = null;
281
+ }
282
+
283
+ function printBanner(compact = false) {
284
+ console.log(compact ? SHIP_BANNER : BANNER_FULL);
285
+ }
286
+
287
+ function printDivider(char = '─', width = 69, color = c.dim) {
288
+ console.log(`${color} ${char.repeat(width)}${c.reset}`);
289
+ }
290
+
291
+ function printSection(title, icon = '◆') {
292
+ console.log();
293
+ console.log(` ${colors.accent}${icon}${c.reset} ${c.bold}${title}${c.reset}`);
294
+ printDivider();
295
+ }
296
+
297
+ function printSubSection(title) {
298
+ console.log();
299
+ console.log(` ${c.dim}${BOX.teeRight}${BOX.horizontal}${c.reset} ${c.bold}${title}${c.reset}`);
300
+ }
301
+
302
+ // ═══════════════════════════════════════════════════════════════════════════════
303
+ // VERDICT DISPLAY - THE HERO MOMENT
304
+ // ═══════════════════════════════════════════════════════════════════════════════
305
+
306
+ function getVerdictConfig(verdict, score, blockers, warnings) {
307
+ if (verdict === 'SHIP' || (score >= 90 && blockers === 0)) {
308
+ return {
309
+ verdict: 'SHIP',
310
+ icon: '🚀',
311
+ headline: 'CLEAR TO SHIP',
312
+ tagline: 'Your app is production ready!',
313
+ color: colors.shipGreen,
314
+ bgColor: bgRgb(0, 80, 50),
315
+ borderColor: rgb(0, 200, 120),
316
+ glow: rgb(0, 255, 150),
317
+ };
318
+ }
319
+
320
+ if (verdict === 'WARN' || (score >= 50 && blockers <= 2)) {
321
+ return {
322
+ verdict: 'WARN',
323
+ icon: '⚠️',
324
+ headline: 'REVIEW BEFORE SHIP',
325
+ tagline: `${warnings} warning${warnings !== 1 ? 's' : ''} to address`,
326
+ color: colors.warnAmber,
327
+ bgColor: bgRgb(80, 60, 0),
328
+ borderColor: rgb(200, 160, 0),
329
+ glow: rgb(255, 200, 0),
330
+ };
331
+ }
332
+
333
+ return {
334
+ verdict: 'BLOCK',
335
+ icon: '🛑',
336
+ headline: 'NOT SHIP READY',
337
+ tagline: `${blockers} blocker${blockers !== 1 ? 's' : ''} must be fixed`,
338
+ color: colors.blockRed,
339
+ bgColor: bgRgb(80, 20, 20),
340
+ borderColor: rgb(200, 60, 60),
341
+ glow: rgb(255, 80, 80),
342
+ };
343
+ }
344
+
345
+ function printVerdictCard(verdict, score, blockers, warnings, duration) {
346
+ const config = getVerdictConfig(verdict, score, blockers, warnings);
347
+ const w = 68; // Inner width
348
+
349
+ console.log();
350
+ console.log();
351
+
352
+ // Top border with glow effect
353
+ console.log(` ${config.borderColor}${BOX.dTopLeft}${BOX.dHorizontal.repeat(w)}${BOX.dTopRight}${c.reset}`);
354
+
355
+ // Empty line
356
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
357
+
358
+ // Verdict icon and headline
359
+ const headlineText = `${config.icon} ${config.headline}`;
360
+ const headlinePadded = padCenter(headlineText, w);
361
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${config.color}${c.bold}${headlinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
362
+
363
+ // Tagline
364
+ const taglinePadded = padCenter(config.tagline, w);
365
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${c.dim}${taglinePadded}${c.reset}${config.borderColor}${BOX.dVertical}${c.reset}`);
366
+
367
+ // Empty line
368
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
369
+
370
+ // Score bar
371
+ const scoreLabel = `VIBE SCORE`;
372
+ const scoreValue = `${score}/100`;
373
+ const scoreBar = progressBar(score, 35, { color: config.color });
374
+ const scoreLine = ` ${scoreLabel} ${scoreBar} ${config.color}${c.bold}${scoreValue}${c.reset}`;
375
+ const scoreLinePadded = scoreLine + ' '.repeat(Math.max(0, w - 60));
376
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${scoreLinePadded}${config.borderColor}${BOX.dVertical}${c.reset}`);
377
+
378
+ // Empty line
379
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
380
+
381
+ // Stats row
382
+ const stats = [
383
+ { label: 'Blockers', value: blockers, color: blockers > 0 ? colors.blockRed : colors.shipGreen },
384
+ { label: 'Warnings', value: warnings, color: warnings > 0 ? colors.warnAmber : colors.shipGreen },
385
+ { label: 'Duration', value: formatDuration(duration), color: colors.accent },
386
+ ];
387
+
388
+ let statsLine = ' ';
389
+ for (const stat of stats) {
390
+ statsLine += `${c.dim}${stat.label}:${c.reset} ${stat.color}${c.bold}${stat.value}${c.reset} `;
391
+ }
392
+ const statsLinePadded = statsLine + ' '.repeat(Math.max(0, w - statsLine.length + 20));
393
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${statsLinePadded}${config.borderColor}${BOX.dVertical}${c.reset}`);
394
+
395
+ // Empty line
396
+ console.log(` ${config.borderColor}${BOX.dVertical}${c.reset}${' '.repeat(w)}${config.borderColor}${BOX.dVertical}${c.reset}`);
397
+
398
+ // Bottom border
399
+ console.log(` ${config.borderColor}${BOX.dBottomLeft}${BOX.dHorizontal.repeat(w)}${BOX.dBottomRight}${c.reset}`);
400
+
401
+ console.log();
402
+ }
403
+
404
+ // ═══════════════════════════════════════════════════════════════════════════════
405
+ // FINDINGS DISPLAY
406
+ // ═══════════════════════════════════════════════════════════════════════════════
407
+
408
+ function getSeverityStyle(severity) {
409
+ const styles = {
410
+ BLOCK: { color: colors.critical, bg: bgRgb(80, 20, 20), icon: '●', label: 'BLOCKER' },
411
+ critical: { color: colors.critical, bg: bgRgb(80, 20, 20), icon: '●', label: 'CRITICAL' },
412
+ WARN: { color: colors.warnAmber, bg: bgRgb(80, 60, 0), icon: '◐', label: 'WARNING' },
413
+ warning: { color: colors.warnAmber, bg: bgRgb(80, 60, 0), icon: '◐', label: 'WARNING' },
414
+ high: { color: colors.high, bg: bgRgb(80, 40, 20), icon: '○', label: 'HIGH' },
415
+ medium: { color: colors.medium, bg: bgRgb(60, 50, 0), icon: '○', label: 'MEDIUM' },
416
+ low: { color: colors.low, bg: bgRgb(20, 40, 60), icon: '○', label: 'LOW' },
417
+ info: { color: colors.info, bg: bgRgb(40, 40, 50), icon: '○', label: 'INFO' },
418
+ };
419
+ return styles[severity] || styles.info;
420
+ }
421
+
422
+ function getCategoryIcon(category) {
423
+ const icons = {
424
+ 'MissingRoute': ICONS.route,
425
+ 'EnvContract': ICONS.env,
426
+ 'EnvGap': ICONS.env,
427
+ 'FakeSuccess': ICONS.ghost,
428
+ 'GhostAuth': ICONS.auth,
429
+ 'StripeWebhook': ICONS.money,
430
+ 'PaidSurface': ICONS.money,
431
+ 'OwnerModeBypass': ICONS.lock,
432
+ 'DeadUI': ICONS.dead,
433
+ 'ContractDrift': ICONS.warning,
434
+ 'Security': ICONS.shield,
435
+ 'Auth': ICONS.lock,
436
+ 'Fake Code': ICONS.ghost,
437
+ };
438
+ return icons[category] || ICONS.bug;
439
+ }
440
+
441
+ function printFindingsBreakdown(findings) {
442
+ if (!findings || findings.length === 0) {
443
+ printSection('FINDINGS', ICONS.check);
444
+ console.log();
445
+ console.log(` ${colors.shipGreen}${c.bold}${ICONS.sparkle} No issues found! Your code is clean.${c.reset}`);
446
+ return;
447
+ }
448
+
449
+ // Group by category
450
+ const byCategory = {};
451
+ for (const f of findings) {
452
+ const cat = f.category || 'Other';
453
+ if (!byCategory[cat]) byCategory[cat] = [];
454
+ byCategory[cat].push(f);
455
+ }
456
+
457
+ const blockers = findings.filter(f => f.severity === 'BLOCK' || f.severity === 'critical');
458
+ const warnings = findings.filter(f => f.severity === 'WARN' || f.severity === 'warning');
459
+
460
+ printSection(`FINDINGS (${blockers.length} blockers, ${warnings.length} warnings)`, ICONS.graph);
461
+ console.log();
462
+
463
+ // Summary by category
464
+ const categories = Object.entries(byCategory).sort((a, b) => {
465
+ const aBlockers = a[1].filter(f => f.severity === 'BLOCK').length;
466
+ const bBlockers = b[1].filter(f => f.severity === 'BLOCK').length;
467
+ return bBlockers - aBlockers;
468
+ });
469
+
470
+ for (const [category, catFindings] of categories) {
471
+ const catBlockers = catFindings.filter(f => f.severity === 'BLOCK' || f.severity === 'critical').length;
472
+ const catWarnings = catFindings.filter(f => f.severity === 'WARN' || f.severity === 'warning').length;
473
+ const icon = getCategoryIcon(category);
474
+
475
+ const statusColor = catBlockers > 0 ? colors.blockRed : catWarnings > 0 ? colors.warnAmber : colors.shipGreen;
476
+ const statusIcon = catBlockers > 0 ? ICONS.cross : catWarnings > 0 ? ICONS.warning : ICONS.check;
477
+
478
+ console.log(` ${statusColor}${statusIcon}${c.reset} ${icon} ${c.bold}${category.padEnd(20)}${c.reset} ${catBlockers > 0 ? `${colors.blockRed}${catBlockers} blockers${c.reset}` : ''}${catWarnings > 0 ? ` ${colors.warnAmber}${catWarnings} warnings${c.reset}` : ''}`);
479
+ }
480
+ }
481
+
482
+ function printBlockerDetails(findings, maxShow = 8) {
483
+ const blockers = findings.filter(f => f.severity === 'BLOCK' || f.severity === 'critical');
484
+
485
+ if (blockers.length === 0) return;
486
+
487
+ printSection(`BLOCKERS (${blockers.length})`, '🚨');
488
+ console.log();
489
+
490
+ for (const blocker of blockers.slice(0, maxShow)) {
491
+ const style = getSeverityStyle(blocker.severity);
492
+ const icon = getCategoryIcon(blocker.category);
493
+
494
+ // Severity badge
495
+ console.log(` ${style.bg}${c.bold} ${style.label} ${c.reset} ${icon} ${c.bold}${truncate(blocker.title, 50)}${c.reset}`);
496
+
497
+ // Details
498
+ if (blocker.why) {
499
+ console.log(` ${' '.repeat(10)} ${c.dim}${truncate(blocker.why, 55)}${c.reset}`);
500
+ }
501
+
502
+ // File location
503
+ if (blocker.evidence && blocker.evidence.length > 0) {
504
+ const ev = blocker.evidence[0];
505
+ const fileDisplay = `${path.basename(ev.file || '')}${ev.lines ? `:${ev.lines}` : ''}`;
506
+ console.log(` ${' '.repeat(10)} ${colors.accent}${ICONS.doc} ${fileDisplay}${c.reset}`);
507
+ }
508
+
509
+ // Fix hint
510
+ if (blocker.fixHints && blocker.fixHints.length > 0) {
511
+ console.log(` ${' '.repeat(10)} ${colors.shipGreen}${ICONS.arrow} ${truncate(blocker.fixHints[0], 50)}${c.reset}`);
512
+ }
513
+
514
+ console.log();
515
+ }
516
+
517
+ if (blockers.length > maxShow) {
518
+ console.log(` ${c.dim}... and ${blockers.length - maxShow} more blockers (see full report)${c.reset}`);
519
+ console.log();
520
+ }
521
+ }
522
+
523
+ // ═══════════════════════════════════════════════════════════════════════════════
524
+ // ROUTE TRUTH VISUALIZATION
525
+ // ═══════════════════════════════════════════════════════════════════════════════
526
+
527
+ function printRouteTruthMap(truthpack) {
528
+ if (!truthpack) return;
529
+
530
+ printSection('ROUTE TRUTH MAP', ICONS.map);
531
+ console.log();
532
+
533
+ const serverRoutes = truthpack.routes?.server?.length || 0;
534
+ const clientRefs = truthpack.routes?.clientRefs?.length || 0;
535
+ const envVars = truthpack.env?.vars?.length || 0;
536
+ const envDeclared = truthpack.env?.declared?.length || 0;
537
+
538
+ // Routes coverage
539
+ const routeCoverage = serverRoutes > 0 ? Math.round((clientRefs / serverRoutes) * 100) : 100;
540
+ const routeColor = routeCoverage >= 80 ? colors.shipGreen : routeCoverage >= 50 ? colors.warnAmber : colors.blockRed;
541
+
542
+ console.log(` ${ICONS.route} ${c.bold}Routes${c.reset}`);
543
+ console.log(` Server: ${colors.accent}${serverRoutes}${c.reset} defined`);
544
+ console.log(` Client: ${colors.accent}${clientRefs}${c.reset} references`);
545
+ console.log(` Coverage: ${progressBar(routeCoverage, 20)} ${routeColor}${routeCoverage}%${c.reset}`);
546
+ console.log();
547
+
548
+ // Env coverage
549
+ const envCoverage = envVars > 0 ? Math.round((envDeclared / envVars) * 100) : 100;
550
+ const envColor = envCoverage >= 80 ? colors.shipGreen : envCoverage >= 50 ? colors.warnAmber : colors.blockRed;
551
+
552
+ console.log(` ${ICONS.env} ${c.bold}Environment${c.reset}`);
553
+ console.log(` Used: ${colors.accent}${envVars}${c.reset} variables`);
554
+ console.log(` Declared: ${colors.accent}${envDeclared}${c.reset} in .env`);
555
+ console.log(` Coverage: ${progressBar(envCoverage, 20)} ${envColor}${envCoverage}%${c.reset}`);
556
+ }
557
+
558
+ // ═══════════════════════════════════════════════════════════════════════════════
559
+ // PROOF GRAPH VISUALIZATION
560
+ // ═══════════════════════════════════════════════════════════════════════════════
561
+
562
+ function printProofGraph(proofGraph) {
563
+ if (!proofGraph || !proofGraph.summary) return;
564
+
565
+ printSection('PROOF GRAPH', ICONS.graph);
566
+ console.log();
567
+
568
+ const { summary } = proofGraph;
569
+
570
+ // Confidence gauge
571
+ const confidence = Math.round((summary.confidence || 0) * 100);
572
+ const confColor = confidence >= 80 ? colors.shipGreen : confidence >= 50 ? colors.warnAmber : colors.blockRed;
573
+
574
+ console.log(` ${c.bold}Analysis Confidence${c.reset}`);
575
+ console.log(` ${progressBar(confidence, 40)} ${confColor}${c.bold}${confidence}%${c.reset}`);
576
+ console.log();
577
+
578
+ // Claims summary
579
+ console.log(` ${c.dim}Claims:${c.reset} ${colors.shipGreen}${summary.verifiedClaims || 0}${c.reset} verified ${c.dim}/${c.reset} ${colors.blockRed}${summary.failedClaims || 0}${c.reset} failed ${c.dim}of${c.reset} ${summary.totalClaims || 0} total`);
580
+ console.log(` ${c.dim}Gaps:${c.reset} ${summary.gaps || 0} identified`);
581
+ console.log(` ${c.dim}Risk Score:${c.reset} ${summary.riskScore || 0}/100`);
582
+
583
+ // Top blockers from proof graph
584
+ if (proofGraph.topBlockers && proofGraph.topBlockers.length > 0) {
585
+ console.log();
586
+ console.log(` ${c.bold}Top Unverified Claims:${c.reset}`);
587
+ for (const blocker of proofGraph.topBlockers.slice(0, 3)) {
588
+ console.log(` ${colors.blockRed}${ICONS.cross}${c.reset} ${truncate(blocker.assertion, 50)}`);
589
+ }
590
+ }
591
+ }
592
+
593
+ // ═══════════════════════════════════════════════════════════════════════════════
594
+ // BADGE DISPLAY
595
+ // ═══════════════════════════════════════════════════════════════════════════════
596
+
597
+ function printBadgeOutput(projectPath, verdict, score) {
598
+ const projectName = path.basename(projectPath);
599
+ const projectId = projectName.toLowerCase().replace(/[^a-z0-9]/g, '-');
600
+
601
+ const config = getVerdictConfig(verdict, score, verdict === 'BLOCK' ? 1 : 0, verdict === 'WARN' ? 1 : 0);
602
+
603
+ printSection('SHIP BADGE', '📛');
604
+ console.log();
605
+
606
+ // Badge preview box
607
+ const badgeText = `vibecheck | ${verdict} | ${score}`;
608
+ console.log(` ${config.bgColor}${c.bold} ${badgeText} ${c.reset}`);
609
+ console.log();
610
+
611
+ const badgeUrl = `https://vibecheck.dev/badge/${projectId}.svg`;
612
+ const reportUrl = `https://vibecheck.dev/report/${projectId}`;
613
+ const markdown = `[![Vibecheck](${badgeUrl})](${reportUrl})`;
614
+
615
+ console.log(` ${c.dim}Badge URL:${c.reset}`);
616
+ console.log(` ${colors.accent}${badgeUrl}${c.reset}`);
617
+ console.log();
618
+ console.log(` ${c.dim}Report URL:${c.reset}`);
619
+ console.log(` ${colors.accent}${reportUrl}${c.reset}`);
620
+ console.log();
621
+ console.log(` ${c.dim}Add to README.md:${c.reset}`);
622
+ console.log(` ${colors.shipGreen}${markdown}${c.reset}`);
623
+
624
+ return { projectId, badgeUrl, reportUrl, markdown };
625
+ }
626
+
627
+ // ═══════════════════════════════════════════════════════════════════════════════
628
+ // FIX MODE DISPLAY
629
+ // ═══════════════════════════════════════════════════════════════════════════════
630
+
631
+ function printFixModeHeader() {
632
+ console.log();
633
+ console.log(` ${bgRgb(40, 80, 120)}${c.bold} ${ICONS.wrench} AUTO-FIX MODE ${c.reset}`);
634
+ console.log();
635
+ }
636
+
637
+ function printFixResults(fixResults) {
638
+ if (!fixResults) return;
639
+
640
+ printSection('FIX RESULTS', ICONS.wrench);
641
+ console.log();
642
+
643
+ if (fixResults.errors && fixResults.errors.length > 0) {
644
+ for (const err of fixResults.errors) {
645
+ console.log(` ${colors.blockRed}${ICONS.cross}${c.reset} ${err}`);
646
+ }
647
+ console.log();
648
+ }
649
+
650
+ const actions = [
651
+ { done: fixResults.envExampleCreated, label: 'Created .env.example', icon: ICONS.env },
652
+ { done: fixResults.gitignoreUpdated, label: 'Updated .gitignore', icon: ICONS.shield },
653
+ { done: fixResults.fixesMdCreated, label: 'Generated fixes.md', icon: ICONS.doc },
654
+ ];
655
+
656
+ for (const action of actions) {
657
+ const icon = action.done ? `${colors.shipGreen}${ICONS.check}` : `${c.dim}${ICONS.bullet}`;
658
+ const label = action.done ? c.reset + action.label : c.dim + action.label + c.reset;
659
+ console.log(` ${icon}${c.reset} ${action.icon} ${label}`);
660
+ }
661
+
662
+ if (fixResults.secretsFound && fixResults.secretsFound.length > 0) {
663
+ console.log();
664
+ console.log(` ${c.bold}Secrets to migrate:${c.reset}`);
665
+ for (const secret of fixResults.secretsFound.slice(0, 5)) {
666
+ console.log(` ${ICONS.key} ${secret.varName} ${c.dim}(${secret.type})${c.reset}`);
667
+ }
668
+ }
669
+
670
+ console.log();
671
+ console.log(` ${colors.shipGreen}${ICONS.check}${c.reset} ${c.bold}Safe fixes applied!${c.reset}`);
672
+ console.log(` ${c.dim}Review changes and follow instructions in ${colors.accent}.vibecheck/fixes.md${c.reset}`);
673
+ }
674
+
675
+ // ═══════════════════════════════════════════════════════════════════════════════
676
+ // HELP DISPLAY
677
+ // ═══════════════════════════════════════════════════════════════════════════════
678
+
679
+ function printHelp() {
680
+ console.log(BANNER_FULL);
681
+ console.log(`
682
+ ${c.bold}Usage:${c.reset} vibecheck ship [options]
683
+
684
+ ${c.bold}The One Command${c.reset} — Get a ship verdict: ${colors.shipGreen}SHIP${c.reset} | ${colors.warnAmber}WARN${c.reset} | ${colors.blockRed}BLOCK${c.reset}
685
+
686
+ ${c.bold}Options:${c.reset}
687
+ ${colors.accent}--fix, -f${c.reset} Try safe mechanical fixes ${c.dim}(shows plan first)${c.reset}
688
+ ${colors.accent}--assist${c.reset} Generate AI mission prompts for complex issues
689
+ ${colors.accent}--badge, -b${c.reset} Generate embeddable badge for README
690
+ ${colors.accent}--strict${c.reset} Treat warnings as blockers
691
+ ${colors.accent}--ci${c.reset} Machine output for CI/CD pipelines
692
+ ${colors.accent}--json${c.reset} Output results as JSON
693
+ ${colors.accent}--path, -p${c.reset} Project path ${c.dim}(default: current directory)${c.reset}
694
+ ${colors.accent}--verbose, -v${c.reset} Show detailed progress
695
+ ${colors.accent}--help, -h${c.reset} Show this help
696
+
697
+ ${c.bold}Exit Codes:${c.reset}
698
+ ${colors.shipGreen}0${c.reset} SHIP — Ready to ship
699
+ ${colors.warnAmber}1${c.reset} WARN — Warnings found, review recommended
700
+ ${colors.blockRed}2${c.reset} BLOCK — Blockers found, must fix before shipping
701
+
702
+ ${c.bold}Examples:${c.reset}
703
+ ${c.dim}# Quick ship check${c.reset}
704
+ vibecheck ship
705
+
706
+ ${c.dim}# Auto-fix what can be fixed${c.reset}
707
+ vibecheck ship --fix
708
+
709
+ ${c.dim}# Generate README badge${c.reset}
710
+ vibecheck ship --badge
711
+
712
+ ${c.dim}# Strict CI mode (warnings = failure)${c.reset}
713
+ vibecheck ship --strict --ci
714
+ `);
715
+ }
716
+
717
+ // ═══════════════════════════════════════════════════════════════════════════════
718
+ // PROOF GRAPH BUILDER
719
+ // ═══════════════════════════════════════════════════════════════════════════════
27
720
 
28
- // Build proof graph from findings for evidence-backed verdicts
29
721
  function buildProofGraph(findings, truthpack, root) {
30
722
  const claims = [];
31
723
  let claimId = 0;
@@ -126,227 +818,9 @@ function getGapType(category) {
126
818
  return map[category] || 'untested_path';
127
819
  }
128
820
 
129
- // ANSI color codes
130
- const c = {
131
- reset: "\x1b[0m",
132
- bold: "\x1b[1m",
133
- dim: "\x1b[2m",
134
- red: "\x1b[31m",
135
- green: "\x1b[32m",
136
- yellow: "\x1b[33m",
137
- blue: "\x1b[34m",
138
- magenta: "\x1b[35m",
139
- cyan: "\x1b[36m",
140
- white: "\x1b[37m",
141
- bgRed: "\x1b[41m",
142
- bgGreen: "\x1b[42m",
143
- bgYellow: "\x1b[43m",
144
- };
145
-
146
- const PLAIN_ENGLISH = {
147
- secretExposed: (type) => ({
148
- message: `🔑 Your ${type} is visible in the code - hackers can steal it`,
149
- why: `If this code is pushed to GitHub or deployed, anyone can see your ${type} and use it maliciously.`,
150
- fix: `Move this to a .env file and use process.env.${type.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`,
151
- }),
152
- adminExposed: (route) => ({
153
- message: `🔐 Anyone can access ${route} without logging in`,
154
- why: `This endpoint has no authentication. Attackers can access admin features directly.`,
155
- fix: `Add authentication middleware to protect this route.`,
156
- }),
157
- mockInProd: (detail) => ({
158
- message: `🎭 Your code uses fake data instead of real data`,
159
- why: `Mock/test code in production means users see fake data or features don't work.`,
160
- fix: `Remove or conditionally disable this mock code for production builds.`,
161
- }),
162
- endpointMissing: (route) => ({
163
- message: `🔗 Button calls ${route} but that endpoint doesn't exist`,
164
- why: `Your frontend calls an API that doesn't exist - this will cause errors for users.`,
165
- fix: `Either create the missing endpoint or update the frontend to use the correct URL.`,
166
- }),
167
- };
168
-
169
- function getTrafficLight(score) {
170
- if (score >= 80) return "🟢";
171
- if (score >= 50) return "🟡";
172
- return "🔴";
173
- }
174
-
175
- function getVerdict(score, blockers) {
176
- if (score >= 90 && blockers.length === 0) {
177
- return {
178
- emoji: "🚀",
179
- headline: "Ready to ship!",
180
- detail: "Your app looks solid. Ship it!",
181
- };
182
- }
183
- if (score >= 70 && blockers.length <= 2) {
184
- return {
185
- emoji: "⚠️",
186
- headline: "Almost ready",
187
- detail: "A few things to fix, but you're close.",
188
- };
189
- }
190
- if (score >= 50) {
191
- return {
192
- emoji: "🛑",
193
- headline: "Not ready yet",
194
- detail: "Some important issues need your attention.",
195
- };
196
- }
197
- return {
198
- emoji: "🚨",
199
- headline: "Don't ship this!",
200
- detail: "Critical problems found. Fix these first.",
201
- };
202
- }
203
-
204
- function translateToPlainEnglish(results) {
205
- const problems = [],
206
- warnings = [],
207
- passes = [];
208
-
209
- if (results.integrity?.env?.secrets) {
210
- for (const secret of results.integrity.env.secrets) {
211
- if (secret.severity === "critical") {
212
- const info = PLAIN_ENGLISH.secretExposed(secret.type);
213
- problems.push({
214
- category: "Security",
215
- type: secret.type,
216
- message: info.message,
217
- why: info.why,
218
- fix: info.fix,
219
- file: `${secret.file}:${secret.line}`,
220
- line: secret.line,
221
- rawFile: secret.file,
222
- fixable: true,
223
- fixAction: "move-to-env",
224
- });
225
- }
226
- }
227
- }
228
-
229
- if (results.integrity?.auth?.analysis?.adminExposed?.length > 0) {
230
- for (const route of results.integrity.auth.analysis.adminExposed) {
231
- const info = PLAIN_ENGLISH.adminExposed(`${route.method} ${route.path}`);
232
- problems.push({
233
- category: "Auth",
234
- message: info.message,
235
- why: info.why,
236
- fix: info.fix,
237
- file: route.file,
238
- fixable: false,
239
- fixAction: "add-auth-middleware",
240
- });
241
- }
242
- }
243
-
244
- if (results.integrity?.mocks?.issues) {
245
- for (const issue of results.integrity.mocks.issues) {
246
- if (issue.severity === "critical" || issue.severity === "high") {
247
- const info = PLAIN_ENGLISH.mockInProd(issue.type);
248
- problems.push({
249
- category: "Fake Code",
250
- message: info.message,
251
- why: info.why,
252
- fix: info.fix,
253
- detail: issue.evidence || issue.type,
254
- file: `${issue.file}:${issue.line}`,
255
- fixable: false,
256
- fixAction: "remove-mock",
257
- });
258
- }
259
- }
260
- }
261
-
262
- if (!results.integrity?.env?.secrets?.length)
263
- passes.push({
264
- category: "Secrets",
265
- message: "✅ No exposed secrets found",
266
- });
267
- if (!results.integrity?.mocks?.issues?.length)
268
- passes.push({ category: "Code", message: "✅ No fake/mock code found" });
269
-
270
- return { problems, warnings, passes };
271
- }
272
-
273
- function printVibeCoderResults(results, translated, outputDir) {
274
- const { problems, warnings, passes } = translated;
275
- const score = results.score || 0;
276
- const light = getTrafficLight(score);
277
- const verdict = getVerdict(score, problems);
278
-
279
- // Get colors based on score
280
- const boxColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
281
- const scoreColor = score >= 80 ? c.green : score >= 50 ? c.yellow : c.red;
282
-
283
- console.log("");
284
- console.log(
285
- ` ${boxColor}╔═════════════════════════════════════════════════════════════════╗${c.reset}`,
286
- );
287
- console.log(
288
- ` ${boxColor}║${c.reset} ${boxColor}║${c.reset}`,
289
- );
290
- console.log(
291
- ` ${boxColor}║${c.reset} ${light} ${c.bold}${verdict.headline}${c.reset} ${boxColor}║${c.reset}`,
292
- );
293
- console.log(
294
- ` ${boxColor}║${c.reset} ${c.dim}${verdict.detail}${c.reset} ${boxColor}║${c.reset}`,
295
- );
296
- console.log(
297
- ` ${boxColor}║${c.reset} ${boxColor}║${c.reset}`,
298
- );
299
- console.log(
300
- ` ${boxColor}╚═════════════════════════════════════════════════════════════════╝${c.reset}`,
301
- );
302
- console.log("");
303
-
304
- if (problems.length > 0) {
305
- console.log(
306
- ` ${c.bold}${c.red}🚨 PROBLEMS${c.reset} ${c.dim}(${problems.length} found)${c.reset}\n`,
307
- );
308
- for (const p of problems.slice(0, 8)) {
309
- console.log(` ${c.red}❌${c.reset} ${c.bold}${p.message}${c.reset}`);
310
- if (p.why) console.log(` ${c.dim}Why: ${p.why}${c.reset}`);
311
- if (p.file) console.log(` ${c.cyan}📍 ${p.file}${c.reset}`);
312
- if (p.fix) console.log(` ${c.green}💡 Fix: ${p.fix}${c.reset}`);
313
- console.log("");
314
- }
315
- }
316
-
317
- if (passes.length > 0) {
318
- console.log(` ${c.bold}${c.green}✅ WHAT'S WORKING${c.reset}\n`);
319
- for (const p of passes) console.log(` ${c.green}${p.message}${c.reset}`);
320
- console.log("");
321
- }
322
-
323
- // Score summary box
324
- console.log(
325
- ` ${c.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`,
326
- );
327
- console.log(
328
- ` ${c.bold}Score:${c.reset} ${scoreColor}${c.bold}${score}${c.reset}/100 ${light}`,
329
- );
330
- console.log("");
331
- if (problems.length > 0) {
332
- const fixable = problems.filter((p) => p.fixable).length;
333
- console.log(
334
- ` ${c.dim}${problems.length} problems found (${fixable} auto-fixable)${c.reset}`,
335
- );
336
- console.log(
337
- ` ${c.dim}Run:${c.reset} ${c.cyan}${c.bold}vibecheck ship --fix${c.reset}`,
338
- );
339
- } else {
340
- console.log(` ${c.green}${c.bold}No critical problems! 🎉${c.reset}`);
341
- }
342
- console.log(
343
- ` ${c.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`,
344
- );
345
- console.log("");
346
- console.log(
347
- ` ${c.dim}📄 Full report:${c.reset} ${c.cyan}${outputDir}/report.html${c.reset}\n`,
348
- );
349
- }
821
+ // ═══════════════════════════════════════════════════════════════════════════════
822
+ // ARGS PARSER
823
+ // ═══════════════════════════════════════════════════════════════════════════════
350
824
 
351
825
  function parseArgs(args) {
352
826
  const opts = {
@@ -358,92 +832,72 @@ function parseArgs(args) {
358
832
  assist: false,
359
833
  strict: false,
360
834
  ci: false,
835
+ help: false,
361
836
  };
837
+
362
838
  for (let i = 0; i < args.length; i++) {
363
839
  const a = args[i];
364
840
  if (a === "--fix" || a === "-f") opts.fix = true;
365
- if (a === "--verbose" || a === "-v") opts.verbose = true;
366
- if (a === "--json") opts.json = true;
367
- if (a === "--badge" || a === "-b") opts.badge = true;
368
- if (a === "--assist") opts.assist = true;
369
- if (a === "--strict") opts.strict = true;
370
- if (a === "--ci") opts.ci = true;
371
- if (a.startsWith("--path=")) opts.path = a.split("=")[1];
372
- if (a === "--path" || a === "-p") opts.path = args[++i];
373
- if (a === "--help" || a === "-h") opts.help = true;
841
+ else if (a === "--verbose" || a === "-v") opts.verbose = true;
842
+ else if (a === "--json") opts.json = true;
843
+ else if (a === "--badge" || a === "-b") opts.badge = true;
844
+ else if (a === "--assist") opts.assist = true;
845
+ else if (a === "--strict") opts.strict = true;
846
+ else if (a === "--ci") opts.ci = true;
847
+ else if (a === "--help" || a === "-h") opts.help = true;
848
+ else if (a.startsWith("--path=")) opts.path = a.split("=")[1];
849
+ else if (a === "--path" || a === "-p") opts.path = args[++i];
374
850
  }
851
+
375
852
  return opts;
376
853
  }
377
854
 
378
- async function runShip(args) {
855
+ // ═══════════════════════════════════════════════════════════════════════════════
856
+ // MAIN SHIP FUNCTION
857
+ // ═══════════════════════════════════════════════════════════════════════════════
858
+
859
+ async function runShip(args, context = {}) {
860
+ // Extract runId from context or generate new one
861
+ const runId = context.runId || generateRunId();
862
+ const startTime = context.startTime || new Date().toISOString();
863
+
379
864
  const opts = parseArgs(args);
865
+ const executionStart = Date.now();
380
866
 
867
+ // Show help if requested
381
868
  if (opts.help) {
382
- console.log(`
383
- ${c.bold}${c.cyan}vibecheck ship${c.reset} — The One Command
384
-
385
- ${c.dim}Get a ship verdict: SHIP | WARN | BLOCK${c.reset}
386
-
387
- ${c.bold}USAGE${c.reset}
388
- vibecheck ship Get ship verdict + Vibe Score
389
- vibecheck ship --fix Try safe mechanical fixes
390
- vibecheck ship --assist Generate AI mission packs
391
- vibecheck ship --badge Generate embeddable badge
392
-
393
- ${c.bold}OPTIONS${c.reset}
394
- --fix, -f Try safe mechanical fixes (shows plan first)
395
- --assist Generate AI mission prompts for complex issues
396
- --badge, -b Generate embeddable badge for README
397
- --strict Treat warnings as blockers
398
- --ci Machine output for CI/CD
399
- --json Output as JSON
400
- --path, -p Project path (default: current directory)
401
- --help, -h Show this help
402
-
403
- ${c.bold}EXIT CODES${c.reset}
404
- ${c.green}0${c.reset} SHIP — Ready to ship
405
- ${c.yellow}1${c.reset} WARN — Warnings found
406
- ${c.red}2${c.reset} BLOCK — Blockers found
407
-
408
- ${c.bold}EXAMPLES${c.reset}
409
- vibecheck ship # Get verdict
410
- vibecheck ship --fix # Fix what can be fixed
411
- vibecheck ship --badge # Get README badge
412
- vibecheck ship --strict --ci # Strict CI mode
413
- `);
869
+ printHelp();
414
870
  return 0;
415
871
  }
416
872
 
417
- // ═══════════════════════════════════════════════════════════════════════════
418
- // ENTITLEMENT CHECK
419
- // ═══════════════════════════════════════════════════════════════════════════
873
+ // Entitlement check
420
874
  try {
421
875
  await enforceLimit('scans');
422
876
  await enforceFeature('ship');
423
-
424
- // Check for fix feature (premium)
425
877
  if (opts.fix) {
426
878
  await enforceFeature('fix');
427
879
  }
428
880
  } catch (err) {
429
881
  if (err.code === 'LIMIT_EXCEEDED' || err.code === 'FEATURE_NOT_AVAILABLE') {
430
- console.error(err.upgradePrompt || err.message);
431
- const { EXIT_CODES } = require('./lib/error-handler');
432
- process.exit(EXIT_CODES.AUTH_FAILURE);
882
+ console.error(`\n ${colors.blockRed}${ICONS.cross}${c.reset} ${err.upgradePrompt || err.message}\n`);
883
+ return 1;
433
884
  }
434
885
  throw err;
435
886
  }
436
887
 
437
- // Track usage
438
888
  await trackUsage('scans');
439
889
 
440
890
  const projectPath = path.resolve(opts.path);
441
891
  const outputDir = path.join(projectPath, ".vibecheck");
442
-
443
- console.log(`\n ${c.bold}${c.magenta}🚀 vibecheck SHIP${c.reset}\n`);
444
- console.log(
445
- ` ${c.dim}Scanning your app for production readiness...${c.reset}\n`,
446
- );
892
+ const projectName = path.basename(projectPath);
893
+
894
+ // Print banner (compact for CI)
895
+ if (!opts.json && !opts.ci) {
896
+ printBanner(opts.ci);
897
+ console.log(` ${c.dim}Project:${c.reset} ${c.bold}${projectName}${c.reset}`);
898
+ console.log(` ${c.dim}Path:${c.reset} ${projectPath}`);
899
+ console.log();
900
+ }
447
901
 
448
902
  let results = {
449
903
  score: 100,
@@ -451,37 +905,41 @@ ${c.bold}EXAMPLES${c.reset}
451
905
  canShip: true,
452
906
  deductions: [],
453
907
  blockers: [],
454
- counts: {},
455
- checks: {},
456
- outputDir,
908
+ warnings: [],
909
+ findings: [],
910
+ truthpack: null,
911
+ proofGraph: null,
457
912
  };
458
913
 
459
914
  try {
460
- console.log(" 🔍 Checking for problems...");
461
- const { auditProductionIntegrity } = require(
462
- path.join(__dirname, "../../scripts/audit-production-integrity.js"),
463
- );
464
- const { results: integrityResults, integrity } =
465
- await auditProductionIntegrity(projectPath);
466
- results.score = integrity.score;
467
- results.grade = integrity.grade;
468
- results.canShip = integrity.canShip;
469
- results.deductions = integrity.deductions;
470
- results.integrity = integrityResults;
471
- } catch (err) {
472
- if (opts.verbose) console.error(" ⚠️ Integrity check error:", err.message);
473
- }
915
+ // Phase 1: Production Integrity Check
916
+ if (!opts.json) startSpinner('Checking production integrity...');
917
+
918
+ try {
919
+ const { auditProductionIntegrity } = require(
920
+ path.join(__dirname, "../../scripts/audit-production-integrity.js"),
921
+ );
922
+ const { results: integrityResults, integrity } = await auditProductionIntegrity(projectPath);
923
+ results.score = integrity.score;
924
+ results.grade = integrity.grade;
925
+ results.canShip = integrity.canShip;
926
+ results.deductions = integrity.deductions;
927
+ results.integrity = integrityResults;
928
+ } catch (err) {
929
+ if (opts.verbose) console.warn(` ${c.dim}Integrity check skipped: ${err.message}${c.reset}`);
930
+ }
931
+
932
+ if (!opts.json) stopSpinner('Production integrity checked', true);
474
933
 
475
- // ═══════════════════════════════════════════════════════════════════════════
476
- // ROUTE TRUTH v1 - Fake Endpoint Detection
477
- // ═══════════════════════════════════════════════════════════════════════════
478
- try {
479
- console.log(" 🗺️ Building route truth map...");
934
+ // Phase 2: Route Truth Analysis
935
+ if (!opts.json) startSpinner('Building route truth map...');
936
+
480
937
  const fastifyEntry = detectFastifyEntry(projectPath);
481
938
  const truthpack = await buildTruthpack({ repoRoot: projectPath, fastifyEntry });
482
939
  writeTruthpack(projectPath, truthpack);
940
+ results.truthpack = truthpack;
483
941
 
484
- // Run all v1 analyzers
942
+ // Run all analyzers
485
943
  const allFindings = [
486
944
  ...findMissingRoutes(truthpack),
487
945
  ...findEnvGaps(truthpack),
@@ -490,217 +948,251 @@ ${c.bold}EXAMPLES${c.reset}
490
948
  ...findStripeWebhookViolations(truthpack),
491
949
  ...findPaidSurfaceNotEnforced(truthpack),
492
950
  ...findOwnerModeBypass(projectPath),
493
- // Runtime UI reality (Dead UI detection)
494
951
  ...findingsFromReality(projectPath)
495
952
  ];
496
953
 
497
- // Contract Drift Detection - per spec section 8.1:
498
- // "drift can be WARN or BLOCK depending on category:
499
- // routes/env/auth drift → usually BLOCK (because AI will lie here)"
954
+ // Contract drift detection
500
955
  if (hasContracts(projectPath)) {
501
956
  const contracts = loadContracts(projectPath);
502
957
  const driftFindings = findContractDrift(contracts, truthpack);
503
958
  allFindings.push(...driftFindings);
504
-
505
- const driftSummary = getDriftSummary(driftFindings);
506
- if (driftSummary.hasDrift) {
507
- console.log(` ${driftSummary.blocks > 0 ? c.red + '🛑' : c.yellow + '⚠️'} Contract drift detected: ${driftSummary.blocks} blocks, ${driftSummary.warns} warnings${c.reset}`);
508
- if (driftSummary.blocks > 0) {
509
- console.log(` ${c.dim}Run 'vibecheck ctx sync' to update contracts${c.reset}`);
510
- }
511
- }
512
959
  }
513
960
 
514
- results.routeTruth = {
515
- serverRoutes: truthpack.routes.server.length,
516
- clientRefs: truthpack.routes.clientRefs.length,
517
- envVars: truthpack.env?.vars?.length || 0,
518
- envDeclared: truthpack.env?.declared?.length || 0,
519
- findings: allFindings,
520
- };
961
+ results.findings = allFindings;
962
+
963
+ if (!opts.json) stopSpinner(`Route truth mapped (${truthpack.routes?.server?.length || 0} routes)`, true);
964
+
965
+ // Phase 3: Build Proof Graph
966
+ if (!opts.json) startSpinner('Building proof graph...');
967
+
968
+ const proofGraph = buildProofGraph(allFindings, truthpack, projectPath);
969
+ results.proofGraph = proofGraph;
970
+
971
+ if (!opts.json) stopSpinner(`Proof graph built (${proofGraph.summary.totalClaims} claims)`, true);
972
+
973
+ // Calculate final verdict
974
+ const blockers = allFindings.filter(f => f.severity === 'BLOCK' || f.severity === 'critical');
975
+ const warnings = allFindings.filter(f => f.severity === 'WARN' || f.severity === 'warning');
976
+
977
+ results.blockers = blockers;
978
+ results.warnings = warnings;
521
979
 
522
- // Add blocking findings to results
980
+ // Apply strict mode
981
+ if (opts.strict && warnings.length > 0) {
982
+ results.canShip = false;
983
+ }
984
+
985
+ if (blockers.length > 0) {
986
+ results.canShip = false;
987
+ }
988
+
989
+ // Deduct score for findings
523
990
  for (const finding of allFindings) {
524
- if (finding.severity === "BLOCK") {
525
- results.canShip = false;
526
- results.blockers.push({
527
- type: finding.category,
528
- message: finding.title,
529
- evidence: finding.evidence,
530
- fix: finding.fixHints?.join(" ") || "",
531
- });
532
- results.deductions.push({
533
- reason: finding.title,
534
- points: 15,
535
- severity: "critical",
536
- });
991
+ if (finding.severity === 'BLOCK') {
537
992
  results.score = Math.max(0, results.score - 15);
993
+ } else if (finding.severity === 'WARN') {
994
+ results.score = Math.max(0, results.score - 5);
538
995
  }
539
996
  }
540
997
 
541
- // Print summary
542
- console.log(` Routes: ${truthpack.routes.server.length} server, ${truthpack.routes.clientRefs.length} client refs`);
543
- console.log(` Env: ${truthpack.env?.vars?.length || 0} used, ${truthpack.env?.declared?.length || 0} declared`);
544
-
545
- const blockers = allFindings.filter(f => f.severity === "BLOCK");
546
- const warns = allFindings.filter(f => f.severity === "WARN");
547
-
548
- if (blockers.length) {
549
- console.log(` ${c.red}🛑 ${blockers.length} BLOCKERS detected${c.reset}`);
550
- for (const b of blockers.slice(0, 5)) {
551
- console.log(` - ${b.title}`);
552
- }
553
- if (blockers.length > 5) console.log(` ...and ${blockers.length - 5} more`);
998
+ const verdict = results.canShip ? 'SHIP' : blockers.length > 0 ? 'BLOCK' : 'WARN';
999
+ const duration = Date.now() - startTime;
1000
+
1001
+ // ═══════════════════════════════════════════════════════════════════════════
1002
+ // OUTPUT
1003
+ // ═══════════════════════════════════════════════════════════════════════════
1004
+
1005
+ // JSON output mode
1006
+ if (opts.json) {
1007
+ const output = createJsonOutput({
1008
+ runId,
1009
+ command: "ship",
1010
+ startTime,
1011
+ exitCode: verdictToExitCode(verdict),
1012
+ verdict,
1013
+ result: {
1014
+ verdict,
1015
+ score: results.score,
1016
+ grade: results.grade,
1017
+ canShip: results.canShip,
1018
+ summary: {
1019
+ blockers: blockers.length,
1020
+ warnings: warnings.length,
1021
+ total: allFindings.length,
1022
+ },
1023
+ findings: allFindings,
1024
+ proofGraph: proofGraph.summary,
1025
+ duration: Date.now() - executionStart,
1026
+ },
1027
+ tier: getCurrentTier(),
1028
+ version: require("../../package.json").version,
1029
+ artifacts: [
1030
+ {
1031
+ type: "report",
1032
+ path: path.join(outputDir, "report.json"),
1033
+ description: "Ship report with findings"
1034
+ },
1035
+ {
1036
+ type: "proof",
1037
+ path: path.join(outputDir, "proof-graph.json"),
1038
+ description: "Proof graph analysis"
1039
+ }
1040
+ ]
1041
+ });
1042
+
1043
+ writeJsonOutput(output, opts.output);
1044
+
1045
+ // Save artifacts
1046
+ const reportPath = saveArtifact(runId, "report", {
1047
+ ...output.result,
1048
+ truthpack,
1049
+ integrity: results.integrity
1050
+ });
1051
+ const proofPath = saveArtifact(runId, "proof-graph", proofGraph);
1052
+
1053
+ return verdictToExitCode(verdict);
554
1054
  }
555
- if (warns.length) {
556
- console.log(` ${c.yellow}⚠️ ${warns.length} WARNINGS${c.reset}`);
1055
+
1056
+ // CI output mode (minimal)
1057
+ if (opts.ci) {
1058
+ console.log(`VERDICT=${verdict}`);
1059
+ console.log(`SCORE=${results.score}`);
1060
+ console.log(`BLOCKERS=${blockers.length}`);
1061
+ console.log(`WARNINGS=${warnings.length}`);
1062
+
1063
+ // Save CI artifacts
1064
+ saveArtifact(runId, "ci-summary", {
1065
+ verdict,
1066
+ score: results.score,
1067
+ blockers: blockers.length,
1068
+ warnings: warnings.length,
1069
+ timestamp: new Date().toISOString()
1070
+ });
1071
+
1072
+ return verdictToExitCode(verdict);
557
1073
  }
558
- } catch (err) {
559
- if (opts.verbose) console.error(" ⚠️ Route truth check error:", err.message);
560
- }
561
1074
 
562
- console.log(" ✅ Scan complete!");
563
-
564
- const translated = translateToPlainEnglish(results);
565
-
566
- // Run auto-fix if requested
567
- if (opts.fix) {
568
- console.log(`\n ${c.bold}${c.cyan}🔧 AUTO-FIX MODE${c.reset}\n`);
569
- const fixResults = await runAutoFix(
570
- projectPath,
571
- translated,
572
- results,
573
- outputDir,
574
- results.routeTruth?.findings || [], // Pass modern analyzer findings
575
- );
576
- printFixResults(fixResults);
577
- }
1075
+ // Fix mode
1076
+ if (opts.fix) {
1077
+ printFixModeHeader();
1078
+ const fixResults = await runAutoFix(projectPath, results, outputDir, allFindings);
1079
+ printFixResults(fixResults);
1080
+ }
578
1081
 
579
- printVibeCoderResults(results, translated, outputDir);
1082
+ // Main verdict card
1083
+ printVerdictCard(verdict, results.score, blockers.length, warnings.length, duration);
580
1084
 
581
- try {
582
- const { writeArtifacts } = require("./utils");
583
- writeArtifacts(outputDir, results);
584
- } catch (err) {
585
- // Log but don't fail - artifact writing is non-critical
586
- console.warn(`${c.yellow}⚠${c.reset} Failed to write artifacts: ${err.message}`);
587
- if (process.env.DEBUG || process.env.VIBECHECK_DEBUG) {
588
- console.error(err.stack);
1085
+ // Findings breakdown
1086
+ printFindingsBreakdown(allFindings);
1087
+
1088
+ // Blocker details (if any)
1089
+ printBlockerDetails(allFindings);
1090
+
1091
+ // Route truth map (verbose)
1092
+ if (opts.verbose) {
1093
+ printRouteTruthMap(truthpack);
589
1094
  }
590
- }
591
1095
 
592
- // Emit audit event for ship check
593
- emitShipCheck(projectPath, results.canShip ? 'success' : 'failure', {
594
- score: results.score,
595
- grade: results.grade,
596
- canShip: results.canShip,
597
- issueCount: translated.problems?.length || 0,
598
- });
1096
+ // Proof graph (verbose)
1097
+ if (opts.verbose) {
1098
+ printProofGraph(proofGraph);
1099
+ }
599
1100
 
600
- // Badge generation mode
601
- if (opts.badge) {
602
- const projectName = path.basename(projectPath);
603
- const projectId = projectName.toLowerCase().replace(/[^a-z0-9]/g, '-');
604
- const verdict = results.canShip ? 'SHIP' : (results.score >= 50 ? 'WARN' : 'BLOCK');
605
- const score = results.score || 0;
606
-
607
- console.log(`\n${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
608
- console.log(`${c.bold}📛 Ship Badge Generated${c.reset}\n`);
609
-
610
- const badgeUrl = `https://vibecheck.dev/badge/${projectId}.svg`;
611
- const reportUrl = `https://vibecheck.dev/report/${projectId}`;
612
- const markdown = `[![Vibecheck](${badgeUrl})](${reportUrl})`;
613
-
614
- const verdictColor = verdict === 'SHIP' ? c.green : verdict === 'WARN' ? c.yellow : c.red;
615
- const verdictIcon = verdict === 'SHIP' ? '✅' : verdict === 'WARN' ? '⚠️' : '🚫';
616
-
617
- console.log(` ${verdictIcon} ${verdictColor}${c.bold}${verdict}${c.reset} · Score: ${c.bold}${score}${c.reset}`);
618
- console.log(`\n ${c.dim}Badge URL:${c.reset} ${c.cyan}${badgeUrl}${c.reset}`);
619
- console.log(` ${c.dim}Report URL:${c.reset} ${c.cyan}${reportUrl}${c.reset}`);
620
- console.log(`\n ${c.dim}Add to README.md:${c.reset}`);
621
- console.log(` ${c.green}${markdown}${c.reset}`);
622
- console.log(`\n${c.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
623
-
624
- // Save badge info to .vibecheck directory
625
- const badgeInfo = {
626
- projectId,
627
- score,
628
- verdict,
629
- badgeUrl,
630
- reportUrl,
631
- markdown,
632
- generatedAt: new Date().toISOString(),
633
- };
634
-
635
- try {
1101
+ // Badge generation
1102
+ if (opts.badge) {
1103
+ const badgeInfo = printBadgeOutput(projectPath, verdict, results.score);
1104
+
1105
+ // Save badge info
1106
+ fs.mkdirSync(outputDir, { recursive: true });
636
1107
  fs.writeFileSync(
637
1108
  path.join(outputDir, 'badge.json'),
638
- JSON.stringify(badgeInfo, null, 2)
1109
+ JSON.stringify({ ...badgeInfo, verdict, score: results.score, generatedAt: new Date().toISOString() }, null, 2)
639
1110
  );
1111
+ }
1112
+
1113
+ // Footer with report links
1114
+ printSection('REPORTS', ICONS.doc);
1115
+ console.log();
1116
+ console.log(` ${colors.accent}${outputDir}/report.html${c.reset}`);
1117
+ console.log(` ${c.dim}${outputDir}/last_ship.json${c.reset}`);
1118
+ if (opts.fix) {
1119
+ console.log(` ${colors.accent}${outputDir}/fixes.md${c.reset}`);
1120
+ console.log(` ${colors.accent}${outputDir}/ai-fix-prompt.md${c.reset}`);
1121
+ }
1122
+ console.log();
1123
+
1124
+ // Quick actions
1125
+ if (!results.canShip) {
1126
+ printSection('NEXT STEPS', ICONS.lightning);
1127
+ console.log();
1128
+ if (!opts.fix) {
1129
+ console.log(` ${colors.accent}vibecheck ship --fix${c.reset} ${c.dim}Auto-fix what can be fixed${c.reset}`);
1130
+ }
1131
+ console.log(` ${colors.accent}vibecheck ship --assist${c.reset} ${c.dim}Get AI help for complex issues${c.reset}`);
1132
+ console.log();
1133
+
1134
+ // Earned upsell: Badge withheld when verdict != SHIP
1135
+ const currentTier = context?.authInfo?.access?.tier || "free";
1136
+ if (entitlements.tierMeetsMinimum(currentTier, "starter")) {
1137
+ // User has badge access but verdict prevents it
1138
+ console.log(upsell.formatEarnedUpsell({
1139
+ cmd: "ship",
1140
+ verdict,
1141
+ topIssues: blockers.slice(0, 3),
1142
+ withheldArtifact: "badge",
1143
+ currentTier,
1144
+ suggestedCmd: currentTier === "free" ? "vibecheck fix --plan-only" : "vibecheck fix",
1145
+ }));
1146
+ }
1147
+ }
1148
+
1149
+ // Emit audit event
1150
+ emitShipCheck(projectPath, results.canShip ? 'success' : 'failure', {
1151
+ score: results.score,
1152
+ grade: results.grade,
1153
+ canShip: results.canShip,
1154
+ issueCount: allFindings.length,
1155
+ });
1156
+
1157
+ // Write artifacts
1158
+ try {
1159
+ const { writeArtifacts } = require("./utils");
1160
+ writeArtifacts(outputDir, results);
640
1161
  } catch {}
641
- }
642
1162
 
643
- // JSON output mode - use standardized schema
644
- if (opts.json) {
645
- const { createScanResult, validateScanResult } = require('./lib/scan-output-schema');
646
-
647
- // Convert results to standardized format
648
- const findings = (translated.problems || []).map((p, idx) => ({
649
- id: `finding_${idx}`,
650
- type: p.type || 'ship_blocker',
651
- severity: p.severity || 'high',
652
- message: p.message || p.title || '',
653
- file: p.file || null,
654
- line: p.line || null,
655
- confidence: 0.9,
656
- blocksShip: !results.canShip,
657
- suggestedFix: p.fix || null,
658
- }));
1163
+ // Exit code: 0=SHIP, 1=WARN, 2=BLOCK
1164
+ const exitCode = verdictToExitCode(verdict);
659
1165
 
660
- const standardizedResult = createScanResult({
661
- findings,
662
- projectPath,
663
- scanId: `ship_${Date.now()}`,
664
- startTime: Date.now(),
1166
+ // Save final results
1167
+ saveArtifact(runId, "summary", {
1168
+ verdict,
1169
+ score: results.score,
1170
+ canShip: results.canShip,
1171
+ exitCode,
1172
+ timestamp: new Date().toISOString()
665
1173
  });
666
1174
 
667
- // Validate before output
668
- const validation = validateScanResult(standardizedResult);
669
- if (!validation.valid) {
670
- console.error(JSON.stringify({
671
- schemaVersion: "1.0.0",
672
- success: false,
673
- error: {
674
- code: "SCHEMA_VALIDATION_FAILED",
675
- message: "JSON output validation failed",
676
- nextSteps: validation.errors,
677
- },
678
- }, null, 2));
679
- const { EXIT_CODES } = require('./lib/error-handler');
680
- return EXIT_CODES.SYSTEM_ERROR;
1175
+ return exitCode;
1176
+
1177
+ } catch (error) {
1178
+ if (!opts.json) stopSpinner(`Ship check failed: ${error.message}`, false);
1179
+
1180
+ console.error(`\n ${colors.blockRed}${ICONS.cross}${c.reset} ${c.bold}Error:${c.reset} ${error.message}`);
1181
+ if (opts.verbose) {
1182
+ console.error(` ${c.dim}${error.stack}${c.reset}`);
681
1183
  }
682
1184
 
683
- console.log(JSON.stringify(standardizedResult, null, 2));
684
- // Exit codes per spec: 0=SHIP, 1=WARN, 2=BLOCK
685
- if (results.canShip) return 0; // SHIP
686
- if (results.hasWarnings && !results.hasBlockers) return 1; // WARN
687
- return 2; // BLOCK
1185
+ return 2;
688
1186
  }
689
-
690
- // Exit codes per spec: 0=SHIP, 1=WARN, 2=BLOCK
691
- if (results.canShip) return 0; // SHIP
692
- if (results.hasWarnings && !results.hasBlockers) return 1; // WARN
693
- return 2; // BLOCK
694
1187
  }
695
1188
 
696
- /**
697
- * Safe auto-fix implementation
698
- * Only performs non-destructive fixes:
699
- * 1. Creates .env.example with detected secrets as placeholders
700
- * 2. Updates .gitignore to protect sensitive files
701
- * 3. Generates fixes.md with detailed manual fix instructions
702
- */
703
- async function runAutoFix(projectPath, translated, results, outputDir, analyzerFindings = []) {
1189
+ // ═══════════════════════════════════════════════════════════════════════════════
1190
+ // AUTO-FIX (Placeholder - would import from original)
1191
+ // ═══════════════════════════════════════════════════════════════════════════════
1192
+
1193
+ async function runAutoFix(projectPath, results, outputDir, findings) {
1194
+ // This would be the full auto-fix implementation from the original
1195
+ // Keeping it as a placeholder for now
704
1196
  const fixResults = {
705
1197
  envExampleCreated: false,
706
1198
  gitignoreUpdated: false,
@@ -708,421 +1200,39 @@ async function runAutoFix(projectPath, translated, results, outputDir, analyzerF
708
1200
  secretsFound: [],
709
1201
  errors: [],
710
1202
  };
711
-
712
- const { ensureOutputDir } = require("./utils");
713
- ensureOutputDir(outputDir);
714
-
715
- // 1. Create .env.example with detected secrets
716
- const secretProblems = translated.problems.filter(
717
- (p) => p.fixAction === "move-to-env",
718
- );
719
- if (secretProblems.length > 0) {
720
- try {
721
- const envExamplePath = path.join(projectPath, ".env.example");
722
- const envVars = new Set();
723
-
724
- for (const problem of secretProblems) {
725
- const varName = problem.type.toUpperCase().replace(/[^A-Z0-9]/g, "_");
726
- envVars.add(varName);
727
- fixResults.secretsFound.push({
728
- type: problem.type,
729
- varName,
730
- file: problem.rawFile,
731
- });
732
- }
733
-
734
- let envContent = "# Environment Variables Template\n";
735
- envContent += "# Copy this file to .env and fill in your actual values\n";
736
- envContent += "# NEVER commit .env to version control!\n\n";
737
-
738
- for (const varName of envVars) {
739
- envContent += `${varName}=your_${varName.toLowerCase()}_here\n`;
740
- }
741
-
742
- // Append to existing .env.example or create new
743
- if (fs.existsSync(envExamplePath)) {
744
- const existing = fs.readFileSync(envExamplePath, "utf8");
745
- for (const varName of envVars) {
746
- if (!existing.includes(varName)) {
747
- fs.appendFileSync(
748
- envExamplePath,
749
- `${varName}=your_${varName.toLowerCase()}_here\n`,
750
- );
751
- }
752
- }
753
- console.log(
754
- ` ${c.green}✓${c.reset} Updated ${c.cyan}.env.example${c.reset} with ${envVars.size} variables`,
755
- );
756
- } else {
757
- fs.writeFileSync(envExamplePath, envContent);
758
- console.log(
759
- ` ${c.green}✓${c.reset} Created ${c.cyan}.env.example${c.reset} with ${envVars.size} variables`,
760
- );
761
- }
762
- fixResults.envExampleCreated = true;
763
- } catch (err) {
764
- fixResults.errors.push(`Failed to create .env.example: ${err.message}`);
765
- }
766
- }
767
-
768
- // 2. Update .gitignore to protect sensitive files
769
- try {
770
- const gitignorePath = path.join(projectPath, ".gitignore");
771
- const sensitivePatterns = [
772
- ".env",
773
- ".env.local",
774
- ".env.*.local",
775
- "*.pem",
776
- "*.key",
777
- ".vibecheck/artifacts/",
778
- ];
779
-
780
- let gitignoreContent = "";
781
- if (fs.existsSync(gitignorePath)) {
782
- gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
783
- }
784
-
785
- const patternsToAdd = sensitivePatterns.filter(
786
- (p) => !gitignoreContent.includes(p),
787
- );
788
-
789
- if (patternsToAdd.length > 0) {
790
- const addition =
791
- "\n# vibecheck: Protect sensitive files\n" +
792
- patternsToAdd.join("\n") +
793
- "\n";
794
- fs.appendFileSync(gitignorePath, addition);
795
- console.log(
796
- ` ${c.green}✓${c.reset} Updated ${c.cyan}.gitignore${c.reset} with ${patternsToAdd.length} patterns`,
797
- );
798
- fixResults.gitignoreUpdated = true;
799
- } else {
800
- console.log(
801
- ` ${c.dim}✓ .gitignore already protects sensitive files${c.reset}`,
802
- );
803
- }
804
- } catch (err) {
805
- fixResults.errors.push(`Failed to update .gitignore: ${err.message}`);
806
- }
807
-
808
- // 3. Generate detailed fixes.md with manual instructions AND AI agent prompt
809
- try {
810
- const fixesMdPath = path.join(outputDir, "fixes.md");
811
- const aiPromptPath = path.join(outputDir, "ai-fix-prompt.md");
812
-
813
- // Group problems by category and dedupe by file
814
- const byCategory = {};
815
- const seenFiles = new Set();
816
- for (const p of translated.problems) {
817
- // Dedupe: only show first issue per file for same category
818
- const key = `${p.category}:${p.file}`;
819
- if (seenFiles.has(key)) continue;
820
- seenFiles.add(key);
821
-
822
- if (!byCategory[p.category]) byCategory[p.category] = [];
823
- byCategory[p.category].push(p);
824
- }
825
-
826
- // Read actual file content for context (keyed by file:line to handle multiple issues per file)
827
- const fileContexts = {};
828
- const fileContents = {}; // Cache file contents
829
- for (const problems of Object.values(byCategory)) {
830
- for (const p of problems) {
831
- if (p.rawFile) {
832
- const contextKey = `${p.rawFile}:${p.line || 1}`;
833
- if (!fileContexts[contextKey]) {
834
- try {
835
- // Cache file content
836
- if (!fileContents[p.rawFile]) {
837
- const fullPath = path.join(projectPath, p.rawFile);
838
- if (fs.existsSync(fullPath)) {
839
- fileContents[p.rawFile] = fs
840
- .readFileSync(fullPath, "utf8")
841
- .split("\n");
842
- }
843
- }
844
-
845
- if (fileContents[p.rawFile]) {
846
- const lines = fileContents[p.rawFile];
847
- const lineNum = p.line || 1;
848
- const start = Math.max(0, lineNum - 3);
849
- const end = Math.min(lines.length, lineNum + 3);
850
- fileContexts[contextKey] = {
851
- snippet: lines
852
- .slice(start, end)
853
- .map((l, i) => `${start + i + 1}: ${l}`)
854
- .join("\n"),
855
- line: lineNum,
856
- };
857
- }
858
- } catch (e) {
859
- /* ignore read errors */
860
- }
861
- }
862
- // Store the context key on the problem for later lookup
863
- p._contextKey = contextKey;
864
- }
865
- }
866
- }
867
-
868
- // Generate human-readable fixes.md
869
- let md = "# 🔧 vibecheck Fix Guide\n\n";
870
- md += `Generated: ${new Date().toISOString()}\n\n`;
871
-
872
- const totalIssues = Object.values(byCategory).flat().length;
873
- md += `**${totalIssues} unique issues found across ${Object.keys(byCategory).length} categories**\n\n`;
874
- md += "---\n\n";
875
-
876
- for (const [category, problems] of Object.entries(byCategory)) {
877
- md += `## ${category} (${problems.length} files)\n\n`;
878
-
879
- for (const p of problems) {
880
- md += `### \`${p.file}\`\n\n`;
881
- md += `**Problem:** ${p.message}\n\n`;
882
- md += `**Risk:** ${p.why}\n\n`;
883
-
884
- // Show actual code context if available
885
- if (p._contextKey && fileContexts[p._contextKey]) {
886
- md += "**Current code:**\n```\n";
887
- md += fileContexts[p._contextKey].snippet;
888
- md += "\n```\n\n";
889
- }
890
-
891
- md += `**Fix:** ${p.fix}\n\n`;
892
-
893
- // Add specific code example for secrets
894
- if (p.fixAction === "move-to-env" && p.type) {
895
- const varName = p.type.toUpperCase().replace(/[^A-Z0-9]/g, "_");
896
- md += "**Replace with:**\n```javascript\n";
897
- md += `const ${p.type.toLowerCase().replace(/[^a-z0-9]/g, "")} = process.env.${varName};\n`;
898
- md += "```\n\n";
899
- }
900
-
901
- md += "---\n\n";
1203
+
1204
+ fs.mkdirSync(outputDir, { recursive: true });
1205
+
1206
+ // Generate fixes.md with modern findings
1207
+ const fixesMdPath = path.join(outputDir, "fixes.md");
1208
+ let md = "# 🔧 vibecheck Fix Guide\n\n";
1209
+ md += `Generated: ${new Date().toISOString()}\n\n`;
1210
+ md += `**${findings.length} issues found**\n\n---\n\n`;
1211
+
1212
+ for (const finding of findings) {
1213
+ md += `## ${finding.category}: ${finding.title}\n\n`;
1214
+ md += `**Severity:** ${finding.severity}\n\n`;
1215
+ if (finding.why) md += `**Why:** ${finding.why}\n\n`;
1216
+ if (finding.fixHints?.length) {
1217
+ md += "**Fix:**\n";
1218
+ for (const hint of finding.fixHints) {
1219
+ md += `- ${hint}\n`;
902
1220
  }
1221
+ md += "\n";
903
1222
  }
904
-
905
- md += "## Next Steps\n\n";
906
- md += "1. Copy `.env.example` to `.env` and fill in real values\n";
907
- md += "2. Apply the fixes above to each file\n";
908
- md += "3. Run `vibecheck ship` again to verify\n\n";
909
1223
  md += "---\n\n";
910
- md +=
911
- "📋 **AI Agent prompt available at:** `.vibecheck/ai-fix-prompt.md`\n";
912
-
913
- fs.writeFileSync(fixesMdPath, md);
914
- console.log(
915
- ` ${c.green}✓${c.reset} Created ${c.cyan}.vibecheck/fixes.md${c.reset} with detailed instructions`,
916
- );
917
-
918
- // 4. Generate AI agent prompt
919
- let aiPrompt = "# AI Agent Fix Prompt\n\n";
920
- aiPrompt +=
921
- "> Copy this entire prompt to an AI coding assistant to fix these issues safely.\n\n";
922
- aiPrompt += "---\n\n";
923
- aiPrompt += "## Task\n\n";
924
- aiPrompt +=
925
- "Fix the following production security and code quality issues. ";
926
- aiPrompt +=
927
- "Follow the exact instructions for each fix. Do NOT break existing functionality.\n\n";
928
-
929
- aiPrompt += "## Critical Rules\n\n";
930
- aiPrompt += "1. **Never delete code** - only modify or comment out\n";
931
- aiPrompt += "2. **Never change function signatures** - keep APIs stable\n";
932
- aiPrompt += "3. **Test after each fix** - ensure the app still runs\n";
933
- aiPrompt +=
934
- "4. **Preserve comments** - don't remove existing documentation\n";
935
- aiPrompt += "5. **Use environment variables** - never hardcode secrets\n\n";
936
-
937
- aiPrompt += "## Fixes Required\n\n";
938
-
939
- let fixNum = 1;
940
- for (const [category, problems] of Object.entries(byCategory)) {
941
- for (const p of problems) {
942
- aiPrompt += `### Fix ${fixNum}: ${category}\n\n`;
943
- aiPrompt += `**File:** \`${p.file}\`\n\n`;
944
- aiPrompt += `**Problem:** ${p.message}\n\n`;
945
-
946
- if (p._contextKey && fileContexts[p._contextKey]) {
947
- aiPrompt +=
948
- "**Current code (around line " +
949
- fileContexts[p._contextKey].line +
950
- "):**\n```\n";
951
- aiPrompt += fileContexts[p._contextKey].snippet;
952
- aiPrompt += "\n```\n\n";
953
- }
954
-
955
- aiPrompt += "**Action:**\n";
956
-
957
- if (p.fixAction === "move-to-env") {
958
- const varName = p.type.toUpperCase().replace(/[^A-Z0-9]/g, "_");
959
- aiPrompt += `1. Find the hardcoded ${p.type} in this file\n`;
960
- aiPrompt += `2. Replace the hardcoded value with \`process.env.${varName}\`\n`;
961
- aiPrompt += `3. Add \`${varName}\` to \`.env.example\` if not present\n`;
962
- aiPrompt += `4. Ensure the code handles undefined env var gracefully\n\n`;
963
- aiPrompt += "**Example transformation:**\n```diff\n";
964
- aiPrompt += `- const secret = "sk_live_xxxxx";\n`;
965
- aiPrompt += `+ const secret = process.env.${varName};\n`;
966
- aiPrompt += `+ if (!secret) throw new Error('${varName} is required');\n`;
967
- aiPrompt += "```\n\n";
968
- } else if (p.fixAction === "remove-mock") {
969
- aiPrompt +=
970
- "1. Check if this file is a test file (should be in __tests__, *.test.*, *.spec.*)\n";
971
- aiPrompt +=
972
- "2. If it's a test file, this is a FALSE POSITIVE - skip it\n";
973
- aiPrompt += "3. If it's production code, either:\n";
974
- aiPrompt += " - Remove the mock import/code entirely, OR\n";
975
- aiPrompt +=
976
- ' - Wrap it in `if (process.env.NODE_ENV !== "production")`\n\n';
977
- } else if (p.fixAction === "add-auth-middleware") {
978
- aiPrompt += "1. Identify the route handler for this endpoint\n";
979
- aiPrompt += "2. Add authentication middleware before the handler\n";
980
- aiPrompt +=
981
- '3. Example: `router.get("/admin", authMiddleware, adminHandler)`\n\n';
982
- }
983
-
984
- fixNum++;
985
- }
986
- }
987
-
988
- // 5. Add modern analyzer findings to AI prompt
989
- if (analyzerFindings && analyzerFindings.length > 0) {
990
- aiPrompt += "---\n\n";
991
- aiPrompt += "## Analyzer Findings\n\n";
992
- aiPrompt += "The following issues were detected by static analysis:\n\n";
993
-
994
- // Group by category
995
- const findingsByCategory = {};
996
- for (const f of analyzerFindings) {
997
- const cat = f.category || "Other";
998
- if (!findingsByCategory[cat]) findingsByCategory[cat] = [];
999
- findingsByCategory[cat].push(f);
1000
- }
1001
-
1002
- for (const [category, findings] of Object.entries(findingsByCategory)) {
1003
- const blockers = findings.filter(f => f.severity === "BLOCK");
1004
- const warnings = findings.filter(f => f.severity === "WARN");
1005
-
1006
- aiPrompt += `### ${category} (${blockers.length} blockers, ${warnings.length} warnings)\n\n`;
1007
-
1008
- // Show blockers first (limit to 10 per category to avoid huge prompts)
1009
- const toShow = [...blockers.slice(0, 10), ...warnings.slice(0, 5)];
1010
-
1011
- for (const finding of toShow) {
1012
- aiPrompt += `#### Fix ${fixNum}: ${finding.title}\n\n`;
1013
- aiPrompt += `**Severity:** ${finding.severity === "BLOCK" ? "🔴 BLOCKER" : "🟡 WARNING"}\n\n`;
1014
-
1015
- if (finding.evidence && finding.evidence.length > 0) {
1016
- const ev = finding.evidence[0];
1017
- aiPrompt += `**File:** \`${ev.file}${ev.lines ? `:${ev.lines}` : ""}\`\n\n`;
1018
- }
1019
-
1020
- aiPrompt += `**Problem:** ${finding.title}\n\n`;
1021
-
1022
- if (finding.why) {
1023
- aiPrompt += `**Why this matters:** ${finding.why}\n\n`;
1024
- }
1025
-
1026
- if (finding.fixHints && finding.fixHints.length > 0) {
1027
- aiPrompt += "**How to fix:**\n";
1028
- for (const hint of finding.fixHints) {
1029
- aiPrompt += `- ${hint}\n`;
1030
- }
1031
- aiPrompt += "\n";
1032
- }
1033
-
1034
- // Add category-specific fix instructions
1035
- if (category === "MissingRoute" || finding.title?.includes("route")) {
1036
- aiPrompt += "**Action:**\n";
1037
- aiPrompt += "1. Check if this API endpoint should exist\n";
1038
- aiPrompt += "2. If yes, create the route handler in your backend\n";
1039
- aiPrompt += "3. If no, update the frontend to use the correct endpoint\n";
1040
- aiPrompt += "4. Ensure the route is registered with your framework (Express, Fastify, Next.js API)\n\n";
1041
- } else if (category === "EnvGap" || finding.title?.includes("env")) {
1042
- aiPrompt += "**Action:**\n";
1043
- aiPrompt += "1. Add the missing environment variable to `.env.example`\n";
1044
- aiPrompt += "2. Document what the variable is for\n";
1045
- aiPrompt += "3. Add to your deployment environment\n\n";
1046
- } else if (category === "GhostAuth" || finding.title?.includes("auth")) {
1047
- aiPrompt += "**Action:**\n";
1048
- aiPrompt += "1. Add authentication middleware to this route\n";
1049
- aiPrompt += "2. Verify the middleware checks for valid session/token\n";
1050
- aiPrompt += "3. Return 401/403 for unauthorized requests\n\n";
1051
- } else if (category === "FakeSuccess" || finding.title?.includes("fake")) {
1052
- aiPrompt += "**Action:**\n";
1053
- aiPrompt += "1. Ensure success UI only shows after confirmed API success\n";
1054
- aiPrompt += "2. Check response.ok or status before showing success\n";
1055
- aiPrompt += "3. Add proper error handling for failed requests\n\n";
1056
- } else if (category === "StripeWebhook" || finding.title?.includes("Stripe")) {
1057
- aiPrompt += "**Action:**\n";
1058
- aiPrompt += "1. Verify Stripe webhook signature using stripe.webhooks.constructEvent()\n";
1059
- aiPrompt += "2. Add idempotency key handling to prevent duplicate processing\n";
1060
- aiPrompt += "3. Return 200 only after successful processing\n\n";
1061
- } else if (category === "PaidSurface" || finding.title?.includes("paid")) {
1062
- aiPrompt += "**Action:**\n";
1063
- aiPrompt += "1. Add server-side entitlement check before allowing access\n";
1064
- aiPrompt += "2. Verify the user's subscription status from your database\n";
1065
- aiPrompt += "3. Return 403 if user doesn't have access to this feature\n\n";
1066
- } else if (category === "OwnerMode" || finding.title?.includes("OWNER")) {
1067
- aiPrompt += "**Action:**\n";
1068
- aiPrompt += "1. Remove or disable OWNER_MODE/bypass flags in production\n";
1069
- aiPrompt += "2. Use proper feature flags that require authentication\n";
1070
- aiPrompt += "3. Never ship code that bypasses auth checks\n\n";
1071
- }
1072
-
1073
- fixNum++;
1074
- }
1075
-
1076
- if (blockers.length > 10) {
1077
- aiPrompt += `\n*...and ${blockers.length - 10} more blockers in this category*\n\n`;
1078
- }
1079
- }
1080
- }
1081
-
1082
- aiPrompt += "---\n\n";
1083
- aiPrompt += "## Verification\n\n";
1084
- aiPrompt += "After applying fixes:\n";
1085
- aiPrompt += "1. Run `npm run build` or `pnpm build` to check for errors\n";
1086
- aiPrompt += "2. Run `vibecheck ship` to verify all issues are resolved\n";
1087
- aiPrompt += "3. Test the application manually to ensure it works\n";
1088
-
1089
- fs.writeFileSync(aiPromptPath, aiPrompt);
1090
- console.log(
1091
- ` ${c.green}✓${c.reset} Created ${c.cyan}.vibecheck/ai-fix-prompt.md${c.reset} for AI agents`,
1092
- );
1093
-
1094
- fixResults.fixesMdCreated = true;
1095
- } catch (err) {
1096
- fixResults.errors.push(`Failed to create fixes.md: ${err.message}`);
1097
1224
  }
1098
-
1225
+
1226
+ fs.writeFileSync(fixesMdPath, md);
1227
+ fixResults.fixesMdCreated = true;
1228
+
1099
1229
  return fixResults;
1100
1230
  }
1101
1231
 
1102
- function printFixResults(fixResults) {
1103
- console.log("");
1104
- if (fixResults.errors.length > 0) {
1105
- for (const err of fixResults.errors) {
1106
- console.log(` ${c.red}⚠️ ${err}${c.reset}`);
1107
- }
1108
- }
1109
-
1110
- if (
1111
- fixResults.envExampleCreated ||
1112
- fixResults.gitignoreUpdated ||
1113
- fixResults.fixesMdCreated
1114
- ) {
1115
- console.log(`\n ${c.bold}${c.green}✅ Safe fixes applied!${c.reset}`);
1116
- console.log(
1117
- ` ${c.dim}Review the changes and follow instructions in .vibecheck/fixes.md${c.reset}\n`,
1118
- );
1119
- }
1120
- }
1232
+ // ═══════════════════════════════════════════════════════════════════════════════
1233
+ // SHIP CORE (for programmatic use)
1234
+ // ═══════════════════════════════════════════════════════════════════════════════
1121
1235
 
1122
- /**
1123
- * shipCore: returns report, never consumes meter, never exits.
1124
- * Used by fix/autopilot verification loops.
1125
- */
1126
1236
  async function shipCore({ repoRoot, fastifyEntry, jsonOut, noWrite } = {}) {
1127
1237
  const root = repoRoot || process.cwd();
1128
1238
  const fastEntry = fastifyEntry || detectFastifyEntry(root);
@@ -1138,14 +1248,12 @@ async function shipCore({ repoRoot, fastifyEntry, jsonOut, noWrite } = {}) {
1138
1248
  ...findStripeWebhookViolations(truthpack),
1139
1249
  ...findPaidSurfaceNotEnforced(truthpack),
1140
1250
  ...findOwnerModeBypass(root),
1141
- // Runtime UI reality (Dead UI detection)
1142
1251
  ...findingsFromReality(root)
1143
1252
  ];
1144
1253
 
1145
1254
  const verdict = allFindings.some(f => f.severity === "BLOCK") ? "BLOCK" :
1146
1255
  allFindings.some(f => f.severity === "WARN") ? "WARN" : "SHIP";
1147
1256
 
1148
- // Build proof graph from findings
1149
1257
  const proofGraph = buildProofGraph(allFindings, truthpack, root);
1150
1258
 
1151
1259
  const report = {
@@ -1162,8 +1270,6 @@ async function shipCore({ repoRoot, fastifyEntry, jsonOut, noWrite } = {}) {
1162
1270
  const outDir = path.join(root, ".vibecheck");
1163
1271
  fs.mkdirSync(outDir, { recursive: true });
1164
1272
  fs.writeFileSync(path.join(outDir, "last_ship.json"), JSON.stringify(report, null, 2));
1165
-
1166
- // Write full proof graph separately
1167
1273
  fs.writeFileSync(path.join(outDir, "proof-graph.json"), JSON.stringify(proofGraph, null, 2));
1168
1274
 
1169
1275
  if (jsonOut) {
@@ -1173,7 +1279,11 @@ async function shipCore({ repoRoot, fastifyEntry, jsonOut, noWrite } = {}) {
1173
1279
  return { report, truthpack, verdict };
1174
1280
  }
1175
1281
 
1282
+ // ═══════════════════════════════════════════════════════════════════════════════
1283
+ // EXPORTS
1284
+ // ═══════════════════════════════════════════════════════════════════════════════
1285
+
1176
1286
  module.exports = {
1177
1287
  runShip: withErrorHandling(runShip, "Ship check failed"),
1178
1288
  shipCore
1179
- };
1289
+ };