@vibecheckai/cli 3.1.2 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +60 -33
  2. package/bin/registry.js +319 -34
  3. package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
  4. package/bin/runners/REPORT_AUDIT.md +64 -0
  5. package/bin/runners/lib/entitlements-v2.js +97 -28
  6. package/bin/runners/lib/entitlements.js +3 -6
  7. package/bin/runners/lib/init-wizard.js +1 -1
  8. package/bin/runners/lib/report-engine.js +459 -280
  9. package/bin/runners/lib/report-html.js +1154 -1423
  10. package/bin/runners/lib/report-output.js +187 -0
  11. package/bin/runners/lib/report-templates.js +848 -850
  12. package/bin/runners/lib/scan-output.js +545 -0
  13. package/bin/runners/lib/server-usage.js +0 -12
  14. package/bin/runners/lib/ship-output.js +641 -0
  15. package/bin/runners/lib/status-output.js +253 -0
  16. package/bin/runners/lib/terminal-ui.js +853 -0
  17. package/bin/runners/runCheckpoint.js +502 -0
  18. package/bin/runners/runContracts.js +105 -0
  19. package/bin/runners/runExport.js +93 -0
  20. package/bin/runners/runFix.js +31 -24
  21. package/bin/runners/runInit.js +377 -112
  22. package/bin/runners/runInstall.js +1 -5
  23. package/bin/runners/runLabs.js +3 -3
  24. package/bin/runners/runPolish.js +2452 -0
  25. package/bin/runners/runProve.js +2 -2
  26. package/bin/runners/runReport.js +251 -200
  27. package/bin/runners/runRuntime.js +110 -0
  28. package/bin/runners/runScan.js +477 -379
  29. package/bin/runners/runSecurity.js +92 -0
  30. package/bin/runners/runShip.js +137 -207
  31. package/bin/runners/runStatus.js +16 -68
  32. package/bin/runners/utils.js +5 -5
  33. package/bin/vibecheck.js +25 -11
  34. package/mcp-server/index.js +150 -18
  35. package/mcp-server/package.json +2 -2
  36. package/mcp-server/premium-tools.js +13 -13
  37. package/mcp-server/tier-auth.js +292 -27
  38. package/mcp-server/vibecheck-tools.js +9 -9
  39. package/package.json +1 -1
  40. package/bin/runners/runClaimVerifier.js +0 -483
  41. package/bin/runners/runContextCompiler.js +0 -385
  42. package/bin/runners/runGate.js +0 -17
  43. package/bin/runners/runInitGha.js +0 -164
  44. package/bin/runners/runInteractive.js +0 -388
  45. package/bin/runners/runMdc.js +0 -204
  46. package/bin/runners/runMissionGenerator.js +0 -282
  47. package/bin/runners/runTruthpack.js +0 -636
@@ -19,321 +19,64 @@ const { enforceLimit, trackUsage } = require("./lib/entitlements");
19
19
  const { emitScanStart, emitScanComplete } = require("./lib/audit-bridge");
20
20
 
21
21
  // ═══════════════════════════════════════════════════════════════════════════════
22
- // ADVANCED TERMINAL - ANSI CODES & UTILITIES
22
+ // ENHANCED TERMINAL UI & OUTPUT MODULES
23
23
  // ═══════════════════════════════════════════════════════════════════════════════
24
24
 
25
- const c = {
26
- reset: '\x1b[0m',
27
- bold: '\x1b[1m',
28
- dim: '\x1b[2m',
29
- italic: '\x1b[3m',
30
- underline: '\x1b[4m',
31
- blink: '\x1b[5m',
32
- inverse: '\x1b[7m',
33
- hidden: '\x1b[8m',
34
- strike: '\x1b[9m',
35
- // Colors
36
- black: '\x1b[30m',
37
- red: '\x1b[31m',
38
- green: '\x1b[32m',
39
- yellow: '\x1b[33m',
40
- blue: '\x1b[34m',
41
- magenta: '\x1b[35m',
42
- cyan: '\x1b[36m',
43
- white: '\x1b[37m',
44
- // Bright colors
45
- gray: '\x1b[90m',
46
- brightRed: '\x1b[91m',
47
- brightGreen: '\x1b[92m',
48
- brightYellow: '\x1b[93m',
49
- brightBlue: '\x1b[94m',
50
- brightMagenta: '\x1b[95m',
51
- brightCyan: '\x1b[96m',
52
- brightWhite: '\x1b[97m',
53
- // Background
54
- bgBlack: '\x1b[40m',
55
- bgRed: '\x1b[41m',
56
- bgGreen: '\x1b[42m',
57
- bgYellow: '\x1b[43m',
58
- bgBlue: '\x1b[44m',
59
- bgMagenta: '\x1b[45m',
60
- bgCyan: '\x1b[46m',
61
- bgWhite: '\x1b[47m',
62
- bgBrightBlack: '\x1b[100m',
63
- bgBrightRed: '\x1b[101m',
64
- bgBrightGreen: '\x1b[102m',
65
- bgBrightYellow: '\x1b[103m',
66
- // Cursor
67
- cursorUp: (n = 1) => `\x1b[${n}A`,
68
- cursorDown: (n = 1) => `\x1b[${n}B`,
69
- cursorRight: (n = 1) => `\x1b[${n}C`,
70
- cursorLeft: (n = 1) => `\x1b[${n}D`,
71
- clearLine: '\x1b[2K',
72
- clearScreen: '\x1b[2J',
73
- saveCursor: '\x1b[s',
74
- restoreCursor: '\x1b[u',
75
- hideCursor: '\x1b[?25l',
76
- showCursor: '\x1b[?25h',
77
- };
78
-
79
- // 256-color support
80
- const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
81
- const bgRgb = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
82
-
83
- // Gradient colors for the banner
84
- const gradientCyan = rgb(0, 255, 255);
85
- const gradientBlue = rgb(100, 149, 237);
86
- const gradientPurple = rgb(138, 43, 226);
87
- const gradientPink = rgb(255, 105, 180);
88
- const gradientOrange = rgb(255, 165, 0);
25
+ const {
26
+ ansi,
27
+ colors,
28
+ Spinner,
29
+ PhaseProgress,
30
+ renderBanner,
31
+ renderSection,
32
+ formatDuration,
33
+ } = require("./lib/terminal-ui");
34
+
35
+ const {
36
+ formatScanOutput,
37
+ formatSARIF,
38
+ getExitCode,
39
+ printError,
40
+ EXIT_CODES,
41
+ calculateScore,
42
+ } = require("./lib/scan-output");
89
43
 
90
44
  const BANNER = `
91
- ${rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${c.reset}
92
- ${rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${c.reset}
93
- ${rgb(60, 160, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${c.reset}
94
- ${rgb(90, 140, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${c.reset}
95
- ${rgb(120, 120, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${c.reset}
96
- ${rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${c.reset}
97
-
98
- ${c.dim} ┌─────────────────────────────────────────────────────────────────────┐${c.reset}
99
- ${c.dim} │${c.reset} ${rgb(255, 255, 255)}${c.bold}Route Integrity${c.reset} ${c.dim}•${c.reset} ${rgb(200, 200, 200)}Security${c.reset} ${c.dim}•${c.reset} ${rgb(150, 150, 150)}Quality${c.reset} ${c.dim}•${c.reset} ${rgb(100, 100, 100)}Ship with Confidence${c.reset} ${c.dim}│${c.reset}
100
- ${c.dim} └─────────────────────────────────────────────────────────────────────┘${c.reset}
45
+ ${ansi.rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
46
+ ${ansi.rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
47
+ ${ansi.rgb(60, 160, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${ansi.reset}
48
+ ${ansi.rgb(90, 140, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${ansi.reset}
49
+ ${ansi.rgb(120, 120, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${ansi.reset}
50
+ ${ansi.rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
51
+
52
+ ${ansi.dim} ┌─────────────────────────────────────────────────────────────────────┐${ansi.reset}
53
+ ${ansi.dim} │${ansi.reset} ${ansi.rgb(255, 255, 255)}${ansi.bold}Route Integrity${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}Security${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}Quality${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(100, 100, 100)}Ship with Confidence${ansi.reset} ${ansi.dim}│${ansi.reset}
54
+ ${ansi.dim} └─────────────────────────────────────────────────────────────────────┘${ansi.reset}
101
55
  `;
102
56
 
103
- // ═══════════════════════════════════════════════════════════════════════════════
104
- // TERMINAL UTILITIES
105
- // ═══════════════════════════════════════════════════════════════════════════════
106
-
107
- const BOX_CHARS = {
108
- topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
109
- horizontal: '─', vertical: '│',
110
- teeRight: '├', teeLeft: '┤', teeDown: '┬', teeUp: '┴',
111
- cross: '┼',
112
- };
113
-
114
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
115
- let spinnerIndex = 0;
116
- let spinnerInterval = null;
117
-
118
- function formatNumber(num) {
119
- return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
120
- }
121
-
122
- function truncate(str, len) {
123
- if (str.length <= len) return str;
124
- return str.slice(0, len - 3) + '...';
125
- }
126
-
127
- function progressBar(percent, width = 30) {
128
- const filled = Math.round((percent / 100) * width);
129
- const empty = width - filled;
130
- const filledColor = percent >= 80 ? rgb(0, 255, 100) : percent >= 50 ? rgb(255, 200, 0) : rgb(255, 80, 80);
131
- return `${filledColor}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
132
- }
133
-
134
- function startSpinner(message) {
135
- process.stdout.write(c.hideCursor);
136
- spinnerInterval = setInterval(() => {
137
- process.stdout.write(`\r ${c.cyan}${SPINNER_FRAMES[spinnerIndex]}${c.reset} ${message} `);
138
- spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
139
- }, 80);
140
- }
141
-
142
- function stopSpinner(message, success = true) {
143
- if (spinnerInterval) {
144
- clearInterval(spinnerInterval);
145
- spinnerInterval = null;
146
- }
147
- const icon = success ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
148
- process.stdout.write(`\r${c.clearLine} ${icon} ${message}\n`);
149
- process.stdout.write(c.showCursor);
150
- }
151
-
152
57
  function printBanner() {
153
58
  console.log(BANNER);
154
59
  }
155
60
 
156
- function printDivider(char = '─', color = c.dim) {
157
- console.log(`${color} ${char.repeat(69)}${c.reset}`);
158
- }
159
-
160
- function printSection(title, icon = '◆') {
161
- console.log();
162
- console.log(` ${rgb(100, 200, 255)}${icon}${c.reset} ${c.bold}${title}${c.reset}`);
163
- printDivider();
164
- }
165
-
166
- // ═══════════════════════════════════════════════════════════════════════════════
167
- // SCORE DISPLAY
168
- // ═══════════════════════════════════════════════════════════════════════════════
169
-
170
- function getScoreColor(score) {
171
- if (score >= 90) return rgb(0, 255, 100);
172
- if (score >= 80) return rgb(100, 255, 100);
173
- if (score >= 70) return rgb(200, 255, 0);
174
- if (score >= 60) return rgb(255, 200, 0);
175
- if (score >= 50) return rgb(255, 150, 0);
176
- return rgb(255, 80, 80);
177
- }
178
-
179
- function getGradeColor(grade) {
180
- const colors = {
181
- 'A': rgb(0, 255, 100),
182
- 'B': rgb(100, 255, 100),
183
- 'C': rgb(255, 200, 0),
184
- 'D': rgb(255, 150, 0),
185
- 'F': rgb(255, 80, 80),
186
- };
187
- return colors[grade] || c.white;
188
- }
189
-
190
- function printScoreCard(score, grade, canShip) {
191
- const scoreColor = getScoreColor(score);
192
- const gradeColor = getGradeColor(grade);
193
-
194
- console.log();
195
- console.log(` ${c.dim}╭────────────────────────────────────────────────────────────────╮${c.reset}`);
196
- console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
197
-
198
- const scoreStr = `${score}`;
199
- const scorePadding = ' '.repeat(Math.max(0, 3 - scoreStr.length));
200
- console.log(` ${c.dim}│${c.reset} ${c.dim}INTEGRITY SCORE${c.reset} ${scoreColor}${c.bold}${scorePadding}${scoreStr}${c.reset}${c.dim}/100${c.reset} ${c.dim}GRADE${c.reset} ${gradeColor}${c.bold}${grade}${c.reset} ${c.dim}│${c.reset}`);
201
- console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
202
- console.log(` ${c.dim}│${c.reset} ${progressBar(score, 40)} ${c.dim}│${c.reset}`);
203
- console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
204
-
205
- if (canShip) {
206
- console.log(` ${c.dim}│${c.reset} ${bgRgb(0, 150, 80)}${c.bold} ✓ CLEAR TO SHIP ${c.reset} ${c.dim}│${c.reset}`);
207
- } else {
208
- console.log(` ${c.dim}│${c.reset} ${bgRgb(200, 50, 50)}${c.bold} ✗ NOT SHIP READY ${c.reset} ${c.dim}│${c.reset}`);
209
- }
210
-
211
- console.log(` ${c.dim}│${c.reset} ${c.dim}│${c.reset}`);
212
- console.log(` ${c.dim}╰────────────────────────────────────────────────────────────────╯${c.reset}`);
213
- }
214
-
215
- // ═══════════════════════════════════════════════════════════════════════════════
216
- // COVERAGE MAP VISUALIZATION
217
- // ═══════════════════════════════════════════════════════════════════════════════
218
-
61
+ // Legacy compatibility functions - now use enhanced modules
219
62
  function printCoverageMap(coverageMap) {
220
- printSection('NAVIGATION COVERAGE', '🗺️');
221
-
222
- const pct = coverageMap.coveragePercent;
223
- const color = pct >= 80 ? rgb(0, 255, 100) : pct >= 60 ? rgb(255, 200, 0) : rgb(255, 80, 80);
224
-
225
- console.log();
226
- console.log(` ${color}${c.bold}${pct}%${c.reset} ${c.dim}of shipped routes reachable from${c.reset} ${c.cyan}/${c.reset}`);
227
- console.log(` ${progressBar(pct, 50)}`);
228
- console.log();
229
- console.log(` ${c.dim}Routes:${c.reset} ${coverageMap.reachableFromRoot}${c.dim}/${c.reset}${coverageMap.totalShippedRoutes} ${c.dim}reachable${c.reset}`);
230
-
231
- if (coverageMap.isolatedClusters && coverageMap.isolatedClusters.length > 0) {
232
- console.log();
233
- console.log(` ${c.yellow}⚠${c.reset} ${c.dim}Isolated clusters:${c.reset}`);
234
- for (const cluster of coverageMap.isolatedClusters.slice(0, 3)) {
235
- const auth = cluster.requiresAuth ? ` ${c.dim}(auth)${c.reset}` : '';
236
- console.log(` ${c.dim}├─${c.reset} ${c.bold}${cluster.name}${c.reset}${auth} ${c.dim}(${cluster.nodeIds.length} routes)${c.reset}`);
237
- }
238
- }
239
-
240
- if (coverageMap.unreachableRoutes && coverageMap.unreachableRoutes.length > 0) {
241
- console.log();
242
- console.log(` ${c.red}✗${c.reset} ${c.dim}Unreachable routes:${c.reset}`);
243
- for (const route of coverageMap.unreachableRoutes.slice(0, 5)) {
244
- console.log(` ${c.dim}├─${c.reset} ${c.red}${route}${c.reset}`);
245
- }
246
- if (coverageMap.unreachableRoutes.length > 5) {
247
- console.log(` ${c.dim}└─ ... and ${coverageMap.unreachableRoutes.length - 5} more${c.reset}`);
248
- }
249
- }
63
+ const { renderCoverageMap } = require("./lib/scan-output");
64
+ console.log(renderCoverageMap(coverageMap));
250
65
  }
251
66
 
252
- // ═══════════════════════════════════════════════════════════════════════════════
253
- // BREAKDOWN DISPLAY
254
- // ═══════════════════════════════════════════════════════════════════════════════
255
-
256
67
  function printBreakdown(breakdown) {
257
- printSection('BREAKDOWN', '📊');
258
- console.log();
259
-
260
- const items = [
261
- { key: 'deadLinks', label: 'Dead Links', icon: '🔗', color: rgb(255, 100, 100) },
262
- { key: 'orphanRoutes', label: 'Orphan Routes', icon: '👻', color: rgb(200, 150, 255) },
263
- { key: 'runtimeFailures', label: 'Runtime 404s', icon: '💥', color: rgb(255, 80, 80) },
264
- { key: 'unresolvedDynamic', label: 'Unresolved Dynamic', icon: '❓', color: rgb(255, 200, 100) },
265
- { key: 'placeholders', label: 'Placeholders', icon: '📝', color: rgb(255, 180, 100) },
266
- ];
267
-
268
- for (const item of items) {
269
- const data = breakdown[item.key] || { count: 0, penalty: 0 };
270
- const status = data.count === 0 ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
271
- const countColor = data.count === 0 ? c.green : item.color;
272
- const countStr = String(data.count).padStart(3);
273
- const penaltyStr = data.penalty > 0 ? `${c.dim}-${data.penalty} pts${c.reset}` : `${c.dim} ---${c.reset}`;
274
-
275
- console.log(` ${status} ${item.icon} ${item.label.padEnd(22)} ${countColor}${c.bold}${countStr}${c.reset} ${penaltyStr}`);
276
- }
68
+ const { renderBreakdown } = require("./lib/scan-output");
69
+ console.log(renderBreakdown(breakdown));
277
70
  }
278
71
 
279
- // ═══════════════════════════════════════════════════════════════════════════════
280
- // BLOCKERS DISPLAY
281
- // ═══════════════════════════════════════════════════════════════════════════════
282
-
283
72
  function printBlockers(blockers) {
284
- if (!blockers || blockers.length === 0) {
285
- printSection('SHIP BLOCKERS', '🚀');
286
- console.log();
287
- console.log(` ${c.green}${c.bold}✓ No blockers! You're clear to ship.${c.reset}`);
288
- return;
289
- }
290
-
291
- printSection(`SHIP BLOCKERS (${blockers.length})`, '🚨');
292
- console.log();
293
-
294
- for (const blocker of blockers.slice(0, 8)) {
295
- const sevColor = blocker.severity === 'critical' ? bgRgb(180, 40, 40) : bgRgb(180, 120, 0);
296
- const sevLabel = blocker.severity === 'critical' ? 'CRITICAL' : ' HIGH ';
297
-
298
- console.log(` ${sevColor}${c.bold} ${sevLabel} ${c.reset} ${c.bold}${truncate(blocker.title, 45)}${c.reset}`);
299
- console.log(` ${c.dim} ${truncate(blocker.description, 55)}${c.reset}`);
300
- if (blocker.file) {
301
- const fileDisplay = path.basename(blocker.file) + (blocker.line ? `:${blocker.line}` : '');
302
- console.log(` ${c.dim} ${c.reset}${c.cyan}${fileDisplay}${c.reset}`);
303
- }
304
- if (blocker.fixSuggestion) {
305
- console.log(` ${c.dim} ${c.green}→ ${blocker.fixSuggestion}${c.reset}`);
306
- }
307
- console.log();
308
- }
309
-
310
- if (blockers.length > 8) {
311
- console.log(` ${c.dim}... and ${blockers.length - 8} more blockers (see full report)${c.reset}`);
312
- }
73
+ const { renderBlockers } = require("./lib/scan-output");
74
+ console.log(renderBlockers(blockers));
313
75
  }
314
76
 
315
- // ═══════════════════════════════════════════════════════════════════════════════
316
- // LAYERS DISPLAY
317
- // ═══════════════════════════════════════════════════════════════════════════════
318
-
319
77
  function printLayers(layers) {
320
- printSection('ANALYSIS LAYERS', '⚡');
321
- console.log();
322
-
323
- const layerInfo = {
324
- ast: { name: 'AST Analysis', icon: '🔍', desc: 'Static code analysis' },
325
- truth: { name: 'Build Truth', icon: '📦', desc: 'Manifest verification' },
326
- reality: { name: 'Reality Proof', icon: '🎭', desc: 'Playwright crawl' },
327
- };
328
-
329
- for (const layer of layers) {
330
- const info = layerInfo[layer.layer] || { name: layer.layer, icon: '○', desc: '' };
331
- const status = layer.executed ? `${c.green}✓${c.reset}` : `${c.dim}○${c.reset}`;
332
- const duration = layer.executed ? `${c.dim}${layer.duration}ms${c.reset}` : `${c.dim}skipped${c.reset}`;
333
- const findings = layer.executed ? `${c.cyan}${layer.findings}${c.reset} ${c.dim}findings${c.reset}` : '';
334
-
335
- console.log(` ${status} ${info.icon} ${c.bold}${info.name.padEnd(15)}${c.reset} ${duration.padEnd(20)} ${findings}`);
336
- }
78
+ const { renderLayers } = require("./lib/scan-output");
79
+ console.log(renderLayers(layers));
337
80
  }
338
81
 
339
82
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -351,6 +94,8 @@ function parseArgs(args) {
351
94
  sarif: false,
352
95
  verbose: false,
353
96
  help: false,
97
+ autofix: false,
98
+ save: true, // Always save results by default
354
99
  };
355
100
 
356
101
  for (let i = 0; i < args.length; i++) {
@@ -364,6 +109,8 @@ function parseArgs(args) {
364
109
  else if (arg === '--sarif') opts.sarif = true;
365
110
  else if (arg === '--verbose' || arg === '-v') opts.verbose = true;
366
111
  else if (arg === '--help' || arg === '-h') opts.help = true;
112
+ else if (arg === '--autofix' || arg === '--fix' || arg === '-f') opts.autofix = true;
113
+ else if (arg === '--no-save') opts.save = false;
367
114
  else if (arg === '--path' || arg === '-p') opts.path = args[++i];
368
115
  else if (arg.startsWith('--path=')) opts.path = arg.split('=')[1];
369
116
  else if (!arg.startsWith('-')) opts.path = path.resolve(arg);
@@ -375,33 +122,315 @@ function parseArgs(args) {
375
122
  function printHelp() {
376
123
  console.log(BANNER);
377
124
  console.log(`
378
- ${c.bold}Usage:${c.reset} vibecheck scan [path] [options]
379
-
380
- ${c.bold}Scan Modes:${c.reset}
381
- ${c.cyan}(default)${c.reset} Layer 1: AST static analysis ${c.dim}(fast)${c.reset}
382
- ${c.cyan}--truth, -t${c.reset} Layer 1+2: Include build manifest verification ${c.dim}(CI/ship)${c.reset}
383
- ${c.cyan}--reality, -r${c.reset} Layer 1+2+3: Include Playwright runtime proof ${c.dim}(full)${c.reset}
384
- ${c.cyan}--reality-sniff${c.reset} Include Reality Sniff AI artifact detection ${c.dim}(recommended)${c.reset}
385
-
386
- ${c.bold}Options:${c.reset}
387
- ${c.cyan}--url, -u${c.reset} Base URL for reality testing (e.g., http://localhost:3000)
388
- ${c.cyan}--verbose, -v${c.reset} Show detailed progress
389
- ${c.cyan}--json${c.reset} Output results as JSON
390
- ${c.cyan}--sarif${c.reset} Output in SARIF format (GitHub code scanning)
391
- ${c.cyan}--help, -h${c.reset} Show this help
392
-
393
- ${c.bold}Examples:${c.reset}
394
- ${c.dim}# Quick scan (AST only)${c.reset}
125
+ ${ansi.bold}Usage:${ansi.reset} vibecheck scan [path] [options]
126
+
127
+ ${ansi.bold}Scan Modes:${ansi.reset}
128
+ ${colors.accent}(default)${ansi.reset} Layer 1: AST static analysis ${ansi.dim}(fast)${ansi.reset}
129
+ ${colors.accent}--truth, -t${ansi.reset} Layer 1+2: Include build manifest verification ${ansi.dim}(CI/ship)${ansi.reset}
130
+ ${colors.accent}--reality, -r${ansi.reset} Layer 1+2+3: Include Playwright runtime proof ${ansi.dim}(full)${ansi.reset}
131
+ ${colors.accent}--reality-sniff${ansi.reset} Include Reality Sniff AI artifact detection ${ansi.dim}(recommended)${ansi.reset}
132
+
133
+ ${ansi.bold}Fix Mode:${ansi.reset}
134
+ ${colors.accent}--autofix, -f${ansi.reset} Apply safe fixes + generate AI missions ${ansi.rgb(0, 200, 255)}[STARTER]${ansi.reset}
135
+
136
+ ${ansi.bold}Options:${ansi.reset}
137
+ ${colors.accent}--url, -u${ansi.reset} Base URL for reality testing (e.g., http://localhost:3000)
138
+ ${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
139
+ ${colors.accent}--json${ansi.reset} Output results as JSON
140
+ ${colors.accent}--sarif${ansi.reset} Output in SARIF format (GitHub code scanning)
141
+ ${colors.accent}--no-save${ansi.reset} Don't save results to .vibecheck/results/
142
+ ${colors.accent}--help, -h${ansi.reset} Show this help
143
+
144
+ ${ansi.bold}Examples:${ansi.reset}
145
+ ${ansi.dim}# Quick scan (AST only)${ansi.reset}
395
146
  vibecheck scan
396
147
 
397
- ${c.dim}# CI/CD scan with manifest verification${c.reset}
148
+ ${ansi.dim}# Scan + autofix with missions${ansi.reset}
149
+ vibecheck scan --autofix
150
+
151
+ ${ansi.dim}# CI/CD scan with manifest verification${ansi.reset}
398
152
  vibecheck scan --truth
399
153
 
400
- ${c.dim}# Full proof with Playwright${c.reset}
154
+ ${ansi.dim}# Full proof with Playwright${ansi.reset}
401
155
  vibecheck scan --reality --url http://localhost:3000
156
+
157
+ ${ansi.bold}Output:${ansi.reset}
158
+ Results saved to: .vibecheck/results/latest.json
159
+ Missions saved to: .vibecheck/missions/ ${ansi.dim}(with --autofix)${ansi.reset}
402
160
  `);
403
161
  }
404
162
 
163
+ // ═══════════════════════════════════════════════════════════════════════════════
164
+ // AUTOFIX & MISSION GENERATION
165
+ // ═══════════════════════════════════════════════════════════════════════════════
166
+
167
+ /**
168
+ * Normalize severity values to standard format
169
+ */
170
+ function normalizeSeverity(severity) {
171
+ if (!severity) return 'medium';
172
+ const sev = String(severity).toLowerCase();
173
+ if (sev === 'block' || sev === 'critical') return 'critical';
174
+ if (sev === 'high') return 'high';
175
+ if (sev === 'warn' || sev === 'warning' || sev === 'medium') return 'medium';
176
+ if (sev === 'info' || sev === 'low') return 'low';
177
+ return 'medium'; // Default fallback
178
+ }
179
+
180
+ async function generateMissions(findings, projectPath, opts) {
181
+ const missionsDir = path.join(projectPath, '.vibecheck', 'missions');
182
+
183
+ // Ensure missions directory exists
184
+ if (!fs.existsSync(missionsDir)) {
185
+ fs.mkdirSync(missionsDir, { recursive: true });
186
+ }
187
+
188
+ const missions = [];
189
+ const missionIndex = [];
190
+
191
+ for (let i = 0; i < findings.length; i++) {
192
+ const finding = findings[i];
193
+ const missionId = `mission-${String(i + 1).padStart(3, '0')}`;
194
+ const normalizedSeverity = normalizeSeverity(finding.severity);
195
+
196
+ // Generate AI-ready mission prompt
197
+ const mission = {
198
+ id: missionId,
199
+ createdAt: new Date().toISOString(),
200
+ finding: {
201
+ id: finding.id || `finding-${i + 1}`,
202
+ severity: normalizedSeverity,
203
+ originalSeverity: finding.severity, // Keep original for reference
204
+ category: finding.category,
205
+ title: finding.title || finding.message,
206
+ file: finding.file,
207
+ line: finding.line,
208
+ },
209
+ prompt: generateMissionPrompt(finding),
210
+ constraints: generateConstraints(finding),
211
+ verification: generateVerificationSteps(finding),
212
+ status: 'pending',
213
+ };
214
+
215
+ missions.push(mission);
216
+ missionIndex.push({
217
+ id: missionId,
218
+ severity: normalizedSeverity,
219
+ title: finding.title || finding.message,
220
+ file: finding.file,
221
+ status: 'pending',
222
+ });
223
+
224
+ // Save individual mission JSON
225
+ const missionPath = path.join(missionsDir, `${missionId}.json`);
226
+ fs.writeFileSync(missionPath, JSON.stringify(mission, null, 2));
227
+ }
228
+
229
+ // Generate MISSIONS.md index
230
+ const missionsMarkdown = generateMissionsMarkdown(missionIndex, projectPath);
231
+ fs.writeFileSync(path.join(missionsDir, 'MISSIONS.md'), missionsMarkdown);
232
+
233
+ // Save missions summary
234
+ const summary = {
235
+ generatedAt: new Date().toISOString(),
236
+ totalMissions: missions.length,
237
+ bySeverity: {
238
+ critical: missionIndex.filter(m => m.severity === 'critical').length,
239
+ high: missionIndex.filter(m => m.severity === 'high').length,
240
+ medium: missionIndex.filter(m => m.severity === 'medium').length,
241
+ low: missionIndex.filter(m => m.severity === 'low').length,
242
+ },
243
+ missions: missionIndex,
244
+ };
245
+ fs.writeFileSync(path.join(missionsDir, 'summary.json'), JSON.stringify(summary, null, 2));
246
+
247
+ return { missions, summary };
248
+ }
249
+
250
+ function generateMissionPrompt(finding) {
251
+ const file = finding.file || 'unknown file';
252
+ const line = finding.line ? ` at line ${finding.line}` : '';
253
+ const title = finding.title || finding.message || 'Unknown issue';
254
+ const category = finding.category || 'general';
255
+
256
+ let prompt = `## Mission: Fix ${title}
257
+
258
+ **File:** \`${file}\`${line}
259
+ **Category:** ${category}
260
+ **Severity:** ${finding.severity || 'medium'}
261
+
262
+ ### Problem
263
+ ${finding.message || title}
264
+
265
+ ### Context
266
+ `;
267
+
268
+ // Add category-specific context
269
+ if (category === 'ROUTE' || category === 'routes') {
270
+ prompt += `This is a route integrity issue. The route may be:
271
+ - Referenced but not defined
272
+ - Defined but not reachable
273
+ - Missing proper error handling
274
+ `;
275
+ } else if (category === 'ENV' || category === 'env') {
276
+ prompt += `This is an environment variable issue. The variable may be:
277
+ - Used but not defined in .env
278
+ - Missing from .env.example
279
+ - Not documented
280
+ `;
281
+ } else if (category === 'AUTH' || category === 'auth' || category === 'security') {
282
+ prompt += `This is a security/authentication issue. Check for:
283
+ - Proper authentication middleware
284
+ - Authorization checks
285
+ - Secure defaults
286
+ `;
287
+ } else if (category === 'MOCK' || category === 'mock') {
288
+ prompt += `This is a mock/placeholder issue. The code may contain:
289
+ - TODO comments that need implementation
290
+ - Placeholder data that should be real
291
+ - Fake success responses
292
+ `;
293
+ }
294
+
295
+ prompt += `
296
+ ### Requirements
297
+ 1. Fix the issue while maintaining existing functionality
298
+ 2. Follow the project's coding style and patterns
299
+ 3. Add appropriate error handling
300
+ 4. Update tests if they exist
301
+
302
+ ### Verification
303
+ After making changes:
304
+ 1. Run \`vibecheck scan\` to verify the issue is resolved
305
+ 2. Run any existing tests for the affected file
306
+ 3. Manually verify the functionality if applicable
307
+ `;
308
+
309
+ if (finding.fix || finding.fixSuggestion) {
310
+ prompt += `
311
+ ### Suggested Fix
312
+ ${finding.fix || finding.fixSuggestion}
313
+ `;
314
+ }
315
+
316
+ return prompt;
317
+ }
318
+
319
+ function generateConstraints(finding) {
320
+ const constraints = [
321
+ 'Do not break existing functionality',
322
+ 'Follow existing code patterns in the project',
323
+ 'Add error handling where appropriate',
324
+ ];
325
+
326
+ if (finding.category === 'security' || finding.category === 'AUTH') {
327
+ constraints.push('Ensure no security regressions');
328
+ constraints.push('Follow OWASP security guidelines');
329
+ }
330
+
331
+ if (finding.category === 'ROUTE' || finding.category === 'routes') {
332
+ constraints.push('Maintain API backwards compatibility');
333
+ constraints.push('Update route documentation if changed');
334
+ }
335
+
336
+ return constraints;
337
+ }
338
+
339
+ function generateVerificationSteps(finding) {
340
+ const steps = [
341
+ 'Run `vibecheck scan` and confirm this issue no longer appears',
342
+ 'Run `vibecheck checkpoint` to ensure no regressions',
343
+ ];
344
+
345
+ if (finding.file) {
346
+ steps.push(`Review changes in \`${finding.file}\``);
347
+ }
348
+
349
+ if (finding.category === 'ROUTE' || finding.category === 'routes') {
350
+ steps.push('Test the affected route(s) manually or with automated tests');
351
+ }
352
+
353
+ if (finding.category === 'ENV' || finding.category === 'env') {
354
+ steps.push('Verify environment variable is properly documented');
355
+ steps.push('Check .env.example is updated');
356
+ }
357
+
358
+ return steps;
359
+ }
360
+
361
+ function generateMissionsMarkdown(missionIndex, projectPath) {
362
+ const projectName = path.basename(projectPath);
363
+ const now = new Date().toISOString();
364
+
365
+ let md = `# Vibecheck Missions
366
+
367
+ > Generated: ${now}
368
+ > Project: ${projectName}
369
+
370
+ ## Summary
371
+
372
+ | Severity | Count |
373
+ |----------|-------|
374
+ | Critical | ${missionIndex.filter(m => m.severity === 'critical').length} |
375
+ | High | ${missionIndex.filter(m => m.severity === 'high').length} |
376
+ | Medium | ${missionIndex.filter(m => m.severity === 'medium').length} |
377
+ | Low | ${missionIndex.filter(m => m.severity === 'low').length} |
378
+ | **Total** | **${missionIndex.length}** |
379
+
380
+ ## Missions
381
+
382
+ `;
383
+
384
+ // Group by severity
385
+ const bySeverity = {
386
+ critical: missionIndex.filter(m => m.severity === 'critical'),
387
+ high: missionIndex.filter(m => m.severity === 'high'),
388
+ medium: missionIndex.filter(m => m.severity === 'medium'),
389
+ low: missionIndex.filter(m => m.severity === 'low'),
390
+ };
391
+
392
+ for (const [severity, missions] of Object.entries(bySeverity)) {
393
+ if (missions.length === 0) continue;
394
+
395
+ const emoji = severity === 'critical' ? '🔴' : severity === 'high' ? '🟠' : severity === 'medium' ? '🟡' : '🔵';
396
+ md += `### ${emoji} ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${missions.length})\n\n`;
397
+
398
+ for (const mission of missions) {
399
+ const checkbox = mission.status === 'completed' ? '[x]' : '[ ]';
400
+ md += `- ${checkbox} **${mission.id}**: ${mission.title}\n`;
401
+ if (mission.file) {
402
+ md += ` - File: \`${mission.file}\`\n`;
403
+ }
404
+ }
405
+ md += '\n';
406
+ }
407
+
408
+ md += `---
409
+
410
+ ## How to Use
411
+
412
+ 1. **Review missions**: Read each mission file in \`.vibecheck/missions/\`
413
+ 2. **Copy prompts**: Use the prompt in each mission file with your AI assistant
414
+ 3. **Verify fixes**: Run \`vibecheck scan\` after each fix
415
+ 4. **Track progress**: Update mission status in this file
416
+
417
+ ## Commands
418
+
419
+ \`\`\`bash
420
+ # Re-scan after fixes
421
+ vibecheck scan
422
+
423
+ # Check progress
424
+ vibecheck checkpoint
425
+
426
+ # Final verification
427
+ vibecheck ship
428
+ \`\`\`
429
+ `;
430
+
431
+ return md;
432
+ }
433
+
405
434
  // ═══════════════════════════════════════════════════════════════════════════════
406
435
  // MAIN SCAN FUNCTION - ROUTE INTEGRITY SYSTEM
407
436
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -426,8 +455,8 @@ async function runScan(args) {
426
455
  }
427
456
  // Network error - fall back to free tier only (SECURITY: never grant paid features offline)
428
457
  if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND' || err.name === 'NetworkError') {
429
- console.warn(` ${c.yellow}⚠${c.reset} API unavailable, running in ${c.green}FREE${c.reset} tier mode`);
430
- console.warn(` ${c.dim}Paid features require API connection. Continuing with free features only.${c.reset}\n`);
458
+ console.warn(` ${colors.warning}⚠${ansi.reset} API unavailable, running in ${colors.success}FREE${ansi.reset} tier mode`);
459
+ console.warn(` ${ansi.dim}Paid features require API connection. Continuing with free features only.${ansi.reset}\n`);
431
460
  // Continue with free tier features only - scan command is free tier
432
461
  } else {
433
462
  throw err; // Re-throw unexpected errors
@@ -464,19 +493,22 @@ async function runScan(args) {
464
493
  if (layers.reality) layerNames.push('Reality');
465
494
  if (layers.realitySniff) layerNames.push('Reality Sniff');
466
495
 
467
- console.log(` ${c.dim}Project:${c.reset} ${c.bold}${projectName}${c.reset}`);
468
- console.log(` ${c.dim}Path:${c.reset} ${projectPath}`);
469
- console.log(` ${c.dim}Layers:${c.reset} ${c.cyan}${layerNames.join(' → ')}${c.reset}`);
496
+ console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${projectName}${ansi.reset}`);
497
+ console.log(` ${ansi.dim}Path:${ansi.reset} ${projectPath}`);
498
+ console.log(` ${ansi.dim}Layers:${ansi.reset} ${colors.accent}${layerNames.join(' → ')}${ansi.reset}`);
470
499
  console.log();
471
500
 
472
501
  // Reality layer requires URL
473
502
  if (opts.reality && !opts.baseUrl) {
474
- console.log(` ${c.yellow}⚠${c.reset} ${c.bold}Reality layer requires --url${c.reset}`);
475
- console.log(` ${c.dim}Example: vibecheck scan --reality --url http://localhost:3000${c.reset}`);
503
+ console.log(` ${colors.warning}⚠${ansi.reset} ${ansi.bold}Reality layer requires --url${ansi.reset}`);
504
+ console.log(` ${ansi.dim}Example: vibecheck scan --reality --url http://localhost:3000${ansi.reset}`);
476
505
  console.log();
477
506
  return 1;
478
507
  }
479
508
 
509
+ // Initialize spinner outside try block for error handling
510
+ let spinner = null;
511
+
480
512
  try {
481
513
  // Import systems - try TypeScript compiled first, fallback to JS runtime
482
514
  let scanRouteIntegrity;
@@ -539,7 +571,7 @@ async function runScan(args) {
539
571
  }
540
572
 
541
573
  // Try to import new unified output system (may not be compiled yet)
542
- let buildVerdictOutput, normalizeFinding, formatStandardOutput, formatScanOutput, getExitCode, CacheManager;
574
+ let buildVerdictOutput, normalizeFinding, formatStandardOutput, formatScanOutputFromUnified, getExitCodeFromUnified, CacheManager;
543
575
  let useUnifiedOutput = false;
544
576
 
545
577
  try {
@@ -549,8 +581,8 @@ async function runScan(args) {
549
581
  formatStandardOutput = outputContract.formatStandardOutput;
550
582
 
551
583
  const unifiedOutput = require('./lib/unified-output');
552
- formatScanOutput = unifiedOutput.formatScanOutput;
553
- getExitCode = unifiedOutput.getExitCode;
584
+ formatScanOutputFromUnified = unifiedOutput.formatScanOutput;
585
+ getExitCodeFromUnified = unifiedOutput.getExitCode;
554
586
 
555
587
  const cacheModule = require('../../dist/lib/cli/cache-manager');
556
588
  CacheManager = cacheModule.CacheManager;
@@ -558,7 +590,7 @@ async function runScan(args) {
558
590
  } catch (error) {
559
591
  // Fallback to old system if new one not available
560
592
  if (opts.verbose) {
561
- console.warn('Unified output system not available, using legacy format');
593
+ console.warn('Unified output system not available, using enhanced format');
562
594
  }
563
595
  useUnifiedOutput = false;
564
596
  }
@@ -594,17 +626,18 @@ async function runScan(args) {
594
626
  return getExitCode(verdict);
595
627
  }
596
628
 
597
- console.log(formatScanOutput({ verdict, findings: cachedResult.findings }, { verbose: opts.verbose, json: opts.json }));
629
+ console.log(formatScanOutput({ verdict, findings: cachedResult.findings, cached }, { verbose: opts.verbose }));
598
630
  return getExitCode(verdict);
599
631
  }
600
632
  }
601
633
  }
602
634
 
603
- // Start scanning with spinner
635
+ // Start scanning with enhanced spinner
604
636
  const timings = { discovery: 0, analysis: 0, verification: 0, detection: 0, total: 0 };
605
637
  timings.discovery = Date.now();
606
638
 
607
- startSpinner('Analyzing codebase...');
639
+ spinner = new Spinner({ color: colors.primary });
640
+ spinner.start('Analyzing codebase...');
608
641
 
609
642
  const result = await scanRouteIntegrity({
610
643
  projectPath,
@@ -612,8 +645,8 @@ async function runScan(args) {
612
645
  baseUrl: opts.baseUrl,
613
646
  verbose: opts.verbose,
614
647
  onProgress: opts.verbose ? (phase, progress) => {
615
- stopSpinner(`${phase}: ${Math.round(progress)}%`, true);
616
- if (progress < 100) startSpinner(`Running ${phase}...`);
648
+ spinner.succeed(`${phase}: ${Math.round(progress)}%`);
649
+ if (progress < 100) spinner.start(`Running ${phase}...`);
617
650
  } : undefined,
618
651
  });
619
652
 
@@ -689,24 +722,24 @@ async function runScan(args) {
689
722
  }
690
723
 
691
724
  timings.detection = Date.now() - detectionStart;
692
- stopSpinner(`Detection complete (${detectionFindings.length} findings)`, true);
725
+ spinner.succeed(`Detection complete (${detectionFindings.length} findings)`);
693
726
  } catch (detectionError) {
694
727
  // Detection engines not compiled yet - continue without them
695
728
  if (opts.verbose) {
696
- console.log(` ${c.dim}Detection engines not available: ${detectionError.message}${c.reset}`);
729
+ console.log(` ${ansi.dim}Detection engines not available: ${detectionError.message}${ansi.reset}`);
697
730
  }
698
- stopSpinner('Detection skipped (not compiled)', true);
731
+ spinner.warn('Detection skipped (not compiled)');
699
732
  }
700
733
 
701
734
  timings.verification = Date.now() - timings.analysis - timings.discovery;
702
735
  timings.total = Date.now() - startTime;
703
736
 
704
- stopSpinner('Analysis complete', true);
737
+ spinner.succeed('Analysis complete');
705
738
 
706
739
  const { report, outputPaths } = result;
707
740
 
708
- // Use new unified output if available, otherwise fallback to old format
709
- if (useUnifiedOutput && buildVerdictOutput && normalizeFinding) {
741
+ // Use new unified output if available, otherwise fallback to enhanced format
742
+ if (useUnifiedOutput && buildVerdictOutput && normalizeFinding && formatScanOutputFromUnified) {
710
743
  // Normalize findings with stable IDs
711
744
  const existingIDs = new Set();
712
745
  const normalizedFindings = [];
@@ -765,22 +798,34 @@ async function runScan(args) {
765
798
  // JSON output mode
766
799
  if (opts.json) {
767
800
  console.log(JSON.stringify(standardOutput, null, 2));
768
- return getExitCode(verdict);
801
+ return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
769
802
  }
770
803
 
771
804
  // SARIF output mode
772
805
  if (opts.sarif) {
773
- const sarifContent = fs.readFileSync(outputPaths.sarif, 'utf8');
774
- console.log(sarifContent);
775
- return report.score.overall >= 70 ? 0 : 1;
806
+ const sarif = formatSARIF(normalizedFindings, {
807
+ projectPath,
808
+ version: require('../../package.json').version || '1.0.0'
809
+ });
810
+ console.log(JSON.stringify(sarif, null, 2));
811
+ return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
776
812
  }
777
813
 
778
814
  // ═══════════════════════════════════════════════════════════════════════════
779
- // UNIFIED OUTPUT
815
+ // ENHANCED OUTPUT
780
816
  // ═══════════════════════════════════════════════════════════════════════════
781
817
 
782
- // Use unified output formatter
783
- console.log(formatScanOutput({ verdict, findings: normalizedFindings }, { verbose: opts.verbose, json: false }));
818
+ // Use enhanced output formatter (from scan-output.js)
819
+ const resultForOutput = {
820
+ verdict,
821
+ findings: normalizedFindings,
822
+ layers: report.layers || [],
823
+ coverage: report.coverageMap,
824
+ breakdown: report.score?.breakdown,
825
+ timings,
826
+ cached,
827
+ };
828
+ console.log(formatScanOutput(resultForOutput, { verbose: opts.verbose }));
784
829
 
785
830
  // Additional details if verbose
786
831
  if (opts.verbose) {
@@ -797,6 +842,66 @@ async function runScan(args) {
797
842
  }
798
843
  }
799
844
 
845
+ // ═══════════════════════════════════════════════════════════════════════════
846
+ // SAVE RESULTS
847
+ // ═══════════════════════════════════════════════════════════════════════════
848
+
849
+ if (opts.save) {
850
+ const resultsDir = path.join(projectPath, '.vibecheck', 'results');
851
+ if (!fs.existsSync(resultsDir)) {
852
+ fs.mkdirSync(resultsDir, { recursive: true });
853
+ }
854
+
855
+ // Save latest.json
856
+ const latestPath = path.join(resultsDir, 'latest.json');
857
+ fs.writeFileSync(latestPath, JSON.stringify(standardOutput, null, 2));
858
+
859
+ // Save timestamped copy
860
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
861
+ const historyDir = path.join(resultsDir, 'history');
862
+ if (!fs.existsSync(historyDir)) {
863
+ fs.mkdirSync(historyDir, { recursive: true });
864
+ }
865
+ fs.writeFileSync(path.join(historyDir, `scan-${timestamp}.json`), JSON.stringify(standardOutput, null, 2));
866
+
867
+ if (!opts.json) {
868
+ console.log(`\n ${ansi.dim}Results saved to: ${latestPath}${ansi.reset}`);
869
+ }
870
+ }
871
+
872
+ // ═══════════════════════════════════════════════════════════════════════════
873
+ // AUTOFIX MODE - Generate missions
874
+ // ═══════════════════════════════════════════════════════════════════════════
875
+
876
+ if (opts.autofix && normalizedFindings.length > 0) {
877
+ // Check entitlement
878
+ const entitlementsV2 = require("./lib/entitlements-v2");
879
+ const access = await entitlementsV2.enforce("scan.autofix", {
880
+ projectPath,
881
+ silent: false,
882
+ });
883
+
884
+ if (!access.allowed) {
885
+ console.log(`\n ${colors.warning}${icons.warning}${ansi.reset} ${ansi.bold}--autofix requires STARTER plan${ansi.reset}`);
886
+ console.log(` ${ansi.dim}Upgrade at: https://vibecheckai.dev/pricing${ansi.reset}`);
887
+ console.log(` ${ansi.dim}Scan results saved. Run 'vibecheck fix' for manual mission generation.${ansi.reset}\n`);
888
+ } else {
889
+ console.log(`\n ${colors.accent}${icons.lightning}${ansi.reset} ${ansi.bold}Generating AI missions...${ansi.reset}\n`);
890
+
891
+ const { missions, summary } = await generateMissions(normalizedFindings, projectPath, opts);
892
+
893
+ console.log(` ${colors.success}✓${ansi.reset} Generated ${missions.length} missions`);
894
+ console.log(` ${ansi.dim}Critical: ${summary.bySeverity.critical}${ansi.reset}`);
895
+ console.log(` ${ansi.dim}High: ${summary.bySeverity.high}${ansi.reset}`);
896
+ console.log(` ${ansi.dim}Medium: ${summary.bySeverity.medium}${ansi.reset}`);
897
+ console.log(` ${ansi.dim}Low: ${summary.bySeverity.low}${ansi.reset}`);
898
+ console.log();
899
+ console.log(` ${ansi.dim}Missions saved to: .vibecheck/missions/${ansi.reset}`);
900
+ console.log(` ${ansi.dim}Open MISSIONS.md for prompts to use with your AI assistant.${ansi.reset}`);
901
+ console.log();
902
+ }
903
+ }
904
+
800
905
  // Emit audit event for scan complete
801
906
  emitScanComplete(projectPath, verdict.verdict === 'PASS' ? 'success' : 'failure', {
802
907
  score: report.score?.overall || (verdict.verdict === 'PASS' ? 100 : 50),
@@ -805,7 +910,7 @@ async function runScan(args) {
805
910
  durationMs: timings.total,
806
911
  });
807
912
 
808
- return getExitCode(verdict);
913
+ return getExitCodeFromUnified ? getExitCodeFromUnified(verdict) : getExitCode(verdict);
809
914
  } else {
810
915
  // Legacy fallback output when unified output system isn't available
811
916
  const findings = [...(report.shipBlockers || []), ...detectionFindings];
@@ -814,40 +919,32 @@ async function runScan(args) {
814
919
 
815
920
  const verdict = criticalCount > 0 ? 'BLOCK' : warningCount > 0 ? 'WARN' : 'SHIP';
816
921
 
817
- // Print simple output
818
- console.log();
819
- console.log(` ${c.bold}═══════════════════════════════════════════════════════════════════${c.reset}`);
820
-
821
- if (verdict === 'SHIP') {
822
- console.log(` ${c.bgGreen}${c.white}${c.bold} ✓ SHIP ${c.reset} ${c.green}Ready to ship!${c.reset}`);
823
- } else if (verdict === 'WARN') {
824
- console.log(` ${c.bgYellow}${c.black}${c.bold} ⚠ WARN ${c.reset} ${c.yellow}Review recommended before shipping${c.reset}`);
825
- } else {
826
- console.log(` ${c.bgRed}${c.white}${c.bold} ✗ BLOCK ${c.reset} ${c.red}Issues must be fixed before shipping${c.reset}`);
827
- }
922
+ // Use enhanced output formatter for legacy fallback
923
+ const severityCounts = {
924
+ critical: criticalCount,
925
+ high: 0,
926
+ medium: warningCount,
927
+ low: findings.length - criticalCount - warningCount,
928
+ };
929
+ const score = calculateScore(severityCounts);
828
930
 
829
- console.log(` ${c.bold}═══════════════════════════════════════════════════════════════════${c.reset}`);
830
- console.log();
931
+ const result = {
932
+ verdict: { verdict, score },
933
+ findings: findings.map(f => ({
934
+ severity: f.severity === 'critical' || f.severity === 'BLOCK' ? 'critical' :
935
+ f.severity === 'warning' || f.severity === 'WARN' ? 'medium' : 'low',
936
+ category: f.category || 'ROUTE',
937
+ title: f.title || f.message,
938
+ message: f.message || f.title,
939
+ file: f.file,
940
+ line: f.line,
941
+ fix: f.fixSuggestion,
942
+ })),
943
+ layers: [],
944
+ timings,
945
+ };
831
946
 
832
- if (findings.length > 0) {
833
- console.log(` ${c.bold}Findings (${findings.length})${c.reset}`);
834
- console.log();
835
-
836
- for (const finding of findings.slice(0, 10)) {
837
- const severityIcon = finding.severity === 'critical' || finding.severity === 'BLOCK'
838
- ? `${c.red}✗${c.reset}`
839
- : `${c.yellow}⚠${c.reset}`;
840
- console.log(` ${severityIcon} ${finding.title || finding.message}`);
841
- if (finding.file) {
842
- console.log(` ${c.dim}${finding.file}${finding.line ? `:${finding.line}` : ''}${c.reset}`);
843
- }
844
- }
845
-
846
- if (findings.length > 10) {
847
- console.log(` ${c.dim}... and ${findings.length - 10} more findings${c.reset}`);
848
- }
849
- console.log();
850
- }
947
+ console.log(formatScanOutput(result, { verbose: opts.verbose }));
851
948
 
852
949
  // Emit audit event
853
950
  emitScanComplete(projectPath, verdict === 'SHIP' ? 'success' : 'failure', {
@@ -860,10 +957,11 @@ async function runScan(args) {
860
957
  }
861
958
 
862
959
  } catch (error) {
863
- stopSpinner(`Scan failed: ${error.message}`, false);
960
+ if (spinner) {
961
+ spinner.fail(`Scan failed: ${error.message}`);
962
+ }
864
963
 
865
- // Use unified error handling
866
- const { printError, EXIT_CODES } = require('./lib/unified-output');
964
+ // Use enhanced error handling
867
965
  const exitCode = printError(error, 'Scan');
868
966
 
869
967
  // Emit audit event for scan error