@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.
- package/bin/.generated +25 -0
- package/bin/registry.js +105 -0
- package/bin/runners/lib/cli-output.js +368 -0
- package/bin/runners/lib/entitlements-v2.js +26 -30
- package/bin/runners/lib/receipts.js +179 -0
- package/bin/runners/lib/report-html.js +378 -1
- package/bin/runners/lib/upsell.js +510 -0
- package/bin/runners/lib/usage.js +153 -0
- package/bin/runners/runBadge.js +850 -116
- package/bin/runners/runCtx.js +602 -119
- package/bin/runners/runDoctor.js +400 -44
- package/bin/runners/runFix.js +557 -85
- package/bin/runners/runGraph.js +245 -74
- package/bin/runners/runInit.js +647 -88
- package/bin/runners/runInstall.js +207 -46
- package/bin/runners/runMcp.js +865 -42
- package/bin/runners/runPR.js +123 -32
- package/bin/runners/runPermissions.js +14 -0
- package/bin/runners/runPreflight.js +553 -0
- package/bin/runners/runProve.js +884 -104
- package/bin/runners/runReality.js +812 -92
- package/bin/runners/runReport.js +68 -2
- package/bin/runners/runShare.js +156 -38
- package/bin/runners/runShip.js +999 -889
- package/bin/runners/runVerify.js +272 -0
- package/bin/runners/runWatch.js +175 -55
- package/bin/vibecheck.js +108 -94
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
package/bin/runners/runShip.js
CHANGED
|
@@ -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 = `[](${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
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
372
|
-
if (a
|
|
373
|
-
if (a === "--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
908
|
+
warnings: [],
|
|
909
|
+
findings: [],
|
|
910
|
+
truthpack: null,
|
|
911
|
+
proofGraph: null,
|
|
457
912
|
};
|
|
458
913
|
|
|
459
914
|
try {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
//
|
|
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 ===
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
1082
|
+
// Main verdict card
|
|
1083
|
+
printVerdictCard(verdict, results.score, blockers.length, warnings.length, duration);
|
|
580
1084
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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 = `[](${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
|
-
|
|
644
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
+
};
|