@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
@@ -0,0 +1,92 @@
1
+ /**
2
+ * vibecheck security - AuthZ + IDOR + Sensitive Security Proofs
3
+ *
4
+ * Subcommands:
5
+ * security model = learn (extract auth model)
6
+ * security matrix = build AuthZ matrix
7
+ * security idor = detect IDOR candidates
8
+ * security prove --url ... = runtime verification
9
+ *
10
+ * Replaces: permissions
11
+ */
12
+
13
+ "use strict";
14
+
15
+ const c = {
16
+ reset: '\x1b[0m',
17
+ bold: '\x1b[1m',
18
+ dim: '\x1b[2m',
19
+ cyan: '\x1b[36m',
20
+ yellow: '\x1b[33m',
21
+ red: '\x1b[31m',
22
+ };
23
+
24
+ function printHelp() {
25
+ console.log(`
26
+ ${c.cyan}${c.bold}🔒 vibecheck security${c.reset} - Authorization & Security Verification
27
+
28
+ AuthZ matrix & IDOR detection for sensitive security proofs.
29
+
30
+ ${c.bold}SUBCOMMANDS${c.reset}
31
+ ${c.cyan}model${c.reset} ${c.dim}Extract auth model from codebase (replaces 'permissions --learn')${c.reset}
32
+ ${c.cyan}matrix${c.reset} ${c.dim}Build AuthZ matrix (replaces 'permissions --matrix')${c.reset}
33
+ ${c.cyan}idor${c.reset} ${c.dim}Detect IDOR candidates (replaces 'permissions --idor')${c.reset}
34
+ ${c.cyan}prove${c.reset} --url <url> ${c.dim}Runtime verification (replaces 'permissions --prove')${c.reset}
35
+
36
+ ${c.bold}EXAMPLES${c.reset}
37
+ vibecheck security model
38
+ vibecheck security matrix
39
+ vibecheck security idor
40
+ vibecheck security prove --url http://localhost:3000
41
+
42
+ ${c.dim}Note: Old command still works as alias:
43
+ vibecheck permissions → vibecheck security model${c.reset}
44
+ `);
45
+ }
46
+
47
+ async function runSecurity(args) {
48
+ if (!args || args.length === 0 || args[0] === "--help" || args[0] === "-h") {
49
+ printHelp();
50
+ return 0;
51
+ }
52
+
53
+ const subcommand = args[0];
54
+ const subArgs = args.slice(1);
55
+
56
+ // Map subcommands to permissions flags
57
+ let permissionsArgs = [];
58
+
59
+ switch (subcommand) {
60
+ case "model":
61
+ permissionsArgs = ["--learn", ...subArgs];
62
+ break;
63
+
64
+ case "matrix":
65
+ permissionsArgs = ["--matrix", ...subArgs];
66
+ break;
67
+
68
+ case "idor":
69
+ permissionsArgs = ["--idor", ...subArgs];
70
+ break;
71
+
72
+ case "prove":
73
+ permissionsArgs = ["--prove", ...subArgs];
74
+ break;
75
+
76
+ default:
77
+ console.error(`${c.red}Unknown subcommand:${c.reset} ${subcommand}`);
78
+ console.log(`\n${c.dim}Run 'vibecheck security --help' for available subcommands.${c.reset}\n`);
79
+ return 1;
80
+ }
81
+
82
+ // Delegate to runPermissions
83
+ try {
84
+ const { runPermissions } = require("./runPermissions");
85
+ return await runPermissions(permissionsArgs);
86
+ } catch (e) {
87
+ console.error(`${c.red}Error:${c.reset} Security command unavailable: ${e.message}`);
88
+ return 1;
89
+ }
90
+ }
91
+
92
+ module.exports = { runSecurity };
@@ -39,120 +39,45 @@ const upsell = require("./lib/upsell");
39
39
  const entitlements = require("./lib/entitlements-v2");
40
40
 
41
41
  // ═══════════════════════════════════════════════════════════════════════════════
42
- // ADVANCED TERMINAL - ANSI CODES & UTILITIES
42
+ // ENHANCED TERMINAL UI & OUTPUT MODULES
43
43
  // ═══════════════════════════════════════════════════════════════════════════════
44
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
- };
45
+ const {
46
+ ansi,
47
+ colors,
48
+ icons,
49
+ Spinner,
50
+ renderBanner,
51
+ renderSection,
52
+ formatDuration,
53
+ } = require("./lib/terminal-ui");
98
54
 
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
- };
55
+ const {
56
+ formatShipOutput,
57
+ renderVerdictCard,
58
+ renderFixModeHeader,
59
+ renderFixResults,
60
+ renderBadgeOutput,
61
+ getExitCode,
62
+ EXIT_CODES,
63
+ shipIcons,
64
+ } = require("./lib/ship-output");
131
65
 
132
66
  // ═══════════════════════════════════════════════════════════════════════════════
133
67
  // PREMIUM BANNER
134
68
  // ═══════════════════════════════════════════════════════════════════════════════
135
69
 
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}
70
+ const BANNER = `
71
+ ${ansi.rgb(0, 255, 200)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${ansi.reset}
72
+ ${ansi.rgb(0, 230, 220)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${ansi.reset}
73
+ ${ansi.rgb(0, 200, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${ansi.reset}
74
+ ${ansi.rgb(50, 150, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${ansi.reset}
75
+ ${ansi.rgb(100, 100, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${ansi.reset}
76
+ ${ansi.rgb(150, 50, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${ansi.reset}
77
+
78
+ ${ansi.dim} ┌─────────────────────────────────────────────────────────────────────┐${ansi.reset}
79
+ ${ansi.dim} │${ansi.reset} ${ansi.rgb(0, 255, 200)}🚀${ansi.reset} ${ansi.bold}SHIP${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(200, 200, 200)}The One Command${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(150, 150, 150)}Zero Config${ansi.reset} ${ansi.dim}•${ansi.reset} ${ansi.rgb(100, 100, 100)}Plain English${ansi.reset} ${ansi.dim}│${ansi.reset}
80
+ ${ansi.dim} └─────────────────────────────────────────────────────────────────────┘${ansi.reset}
156
81
  `;
157
82
 
158
83
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -211,11 +136,7 @@ let spinnerIndex = 0;
211
136
  let spinnerInterval = null;
212
137
  let spinnerStartTime = null;
213
138
 
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
- }
139
+ // formatDuration is imported from terminal-ui.js
219
140
 
220
141
  function formatNumber(num) {
221
142
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
@@ -280,8 +201,18 @@ function stopSpinner(message, success = true) {
280
201
  spinnerStartTime = null;
281
202
  }
282
203
 
204
+ // Compact banner for fix mode
205
+ const SHIP_BANNER = `
206
+ ${ansi.rgb(0, 255, 200)} ███████╗██╗ ██╗██╗██████╗ ${ansi.reset}
207
+ ${ansi.rgb(0, 230, 220)} ██╔════╝██║ ██║██║██╔══██╗${ansi.reset}
208
+ ${ansi.rgb(0, 200, 255)} ███████╗███████║██║██████╔╝${ansi.reset}
209
+ ${ansi.rgb(50, 150, 255)} ╚════██║██╔══██║██║██╔═══╝ ${ansi.reset}
210
+ ${ansi.rgb(100, 100, 255)} ███████║██║ ██║██║██║ ${ansi.reset}
211
+ ${ansi.rgb(150, 50, 255)} ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ${ansi.reset}
212
+ `;
213
+
283
214
  function printBanner(compact = false) {
284
- console.log(compact ? SHIP_BANNER : BANNER_FULL);
215
+ console.log(compact ? SHIP_BANNER : BANNER);
285
216
  }
286
217
 
287
218
  function printDivider(char = '─', width = 69, color = c.dim) {
@@ -677,41 +608,41 @@ function printFixResults(fixResults) {
677
608
  // ═══════════════════════════════════════════════════════════════════════════════
678
609
 
679
610
  function printHelp() {
680
- console.log(BANNER_FULL);
611
+ console.log(BANNER);
681
612
  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}
613
+ ${ansi.bold}Usage:${ansi.reset} vibecheck ship [options]
614
+
615
+ ${ansi.bold}The One Command${ansi.reset} — Get a ship verdict: ${colors.success}SHIP${ansi.reset} | ${colors.warning}WARN${ansi.reset} | ${colors.error}BLOCK${ansi.reset}
616
+
617
+ ${ansi.bold}Options:${ansi.reset}
618
+ ${colors.accent}--fix, -f${ansi.reset} Try safe mechanical fixes ${ansi.dim}(shows plan first)${ansi.reset}
619
+ ${colors.accent}--assist${ansi.reset} Generate AI mission prompts for complex issues
620
+ ${colors.accent}--badge, -b${ansi.reset} Generate embeddable badge for README
621
+ ${colors.accent}--strict${ansi.reset} Treat warnings as blockers
622
+ ${colors.accent}--ci${ansi.reset} Machine output for CI/CD pipelines
623
+ ${colors.accent}--json${ansi.reset} Output results as JSON
624
+ ${colors.accent}--path, -p${ansi.reset} Project path ${ansi.dim}(default: current directory)${ansi.reset}
625
+ ${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
626
+ ${colors.accent}--help, -h${ansi.reset} Show this help
627
+
628
+ ${ansi.bold}Exit Codes:${ansi.reset}
629
+ ${colors.success}0${ansi.reset} SHIP — Ready to ship
630
+ ${colors.warning}1${ansi.reset} WARN — Warnings found, review recommended
631
+ ${colors.error}2${ansi.reset} BLOCK — Blockers found, must fix before shipping
632
+
633
+ ${ansi.bold}Examples:${ansi.reset}
634
+ ${ansi.dim}# Quick ship check${ansi.reset}
704
635
  vibecheck ship
705
636
 
706
- ${c.dim}# Auto-fix what can be fixed${c.reset}
637
+ ${ansi.dim}# Auto-fix what can be fixed${ansi.reset}
707
638
  vibecheck ship --fix
708
639
 
709
- ${c.dim}# Generate README badge${c.reset}
640
+ ${ansi.dim}# Generate README badge${ansi.reset}
710
641
  vibecheck ship --badge
711
642
 
712
- ${c.dim}# Strict CI mode (warnings = failure)${c.reset}
643
+ ${ansi.dim}# Strict CI mode (warnings = failure)${ansi.reset}
713
644
  vibecheck ship --strict --ci
714
- `);
645
+ `);
715
646
  }
716
647
 
717
648
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -832,6 +763,8 @@ function parseArgs(args) {
832
763
  assist: false,
833
764
  strict: false,
834
765
  ci: false,
766
+ mode: null, // "scan" for scan mode
767
+ withRuntime: false, // merge runtime results
835
768
  help: false,
836
769
  };
837
770
 
@@ -844,6 +777,11 @@ function parseArgs(args) {
844
777
  else if (a === "--assist") opts.assist = true;
845
778
  else if (a === "--strict") opts.strict = true;
846
779
  else if (a === "--ci") opts.ci = true;
780
+ else if (a === "--mode") opts.mode = args[++i];
781
+ else if (a === "--with") {
782
+ const next = args[++i];
783
+ if (next === "runtime") opts.withRuntime = true;
784
+ }
847
785
  else if (a === "--help" || a === "-h") opts.help = true;
848
786
  else if (a.startsWith("--path=")) opts.path = a.split("=")[1];
849
787
  else if (a === "--path" || a === "-p") opts.path = args[++i];
@@ -879,8 +817,8 @@ async function runShip(args, context = {}) {
879
817
  }
880
818
  } catch (err) {
881
819
  if (err.code === 'LIMIT_EXCEEDED' || err.code === 'FEATURE_NOT_AVAILABLE') {
882
- console.error(`\n ${colors.blockRed}${ICONS.cross}${c.reset} ${err.upgradePrompt || err.message}\n`);
883
- return 1;
820
+ console.error(`\n ${colors.error}${icons.error}${ansi.reset} ${err.upgradePrompt || err.message}\n`);
821
+ return EXIT_CODES.WARN;
884
822
  }
885
823
  throw err;
886
824
  }
@@ -891,11 +829,11 @@ async function runShip(args, context = {}) {
891
829
  const outputDir = path.join(projectPath, ".vibecheck");
892
830
  const projectName = path.basename(projectPath);
893
831
 
894
- // Print banner (compact for CI)
832
+ // Print banner (unless quiet/json/ci)
895
833
  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}`);
834
+ printBanner();
835
+ console.log(` ${ansi.dim}Project:${ansi.reset} ${ansi.bold}${projectName}${ansi.reset}`);
836
+ console.log(` ${ansi.dim}Path:${ansi.reset} ${projectPath}`);
899
837
  console.log();
900
838
  }
901
839
 
@@ -912,8 +850,14 @@ async function runShip(args, context = {}) {
912
850
  };
913
851
 
914
852
  try {
853
+ // Initialize spinner
854
+ let spinner;
855
+ if (!opts.json && !opts.ci) {
856
+ spinner = new Spinner({ color: colors.accent });
857
+ }
858
+
915
859
  // Phase 1: Production Integrity Check
916
- if (!opts.json) startSpinner('Checking production integrity...');
860
+ if (spinner) spinner.start('Checking production integrity...');
917
861
 
918
862
  try {
919
863
  const { auditProductionIntegrity } = require(
@@ -926,13 +870,13 @@ async function runShip(args, context = {}) {
926
870
  results.deductions = integrity.deductions;
927
871
  results.integrity = integrityResults;
928
872
  } catch (err) {
929
- if (opts.verbose) console.warn(` ${c.dim}Integrity check skipped: ${err.message}${c.reset}`);
873
+ if (opts.verbose) console.warn(` ${ansi.dim}Integrity check skipped: ${err.message}${ansi.reset}`);
930
874
  }
931
875
 
932
- if (!opts.json) stopSpinner('Production integrity checked', true);
876
+ if (spinner) spinner.succeed('Production integrity checked');
933
877
 
934
878
  // Phase 2: Route Truth Analysis
935
- if (!opts.json) startSpinner('Building route truth map...');
879
+ if (spinner) spinner.start('Building route truth map...');
936
880
 
937
881
  const fastifyEntry = detectFastifyEntry(projectPath);
938
882
  const truthpack = await buildTruthpack({ repoRoot: projectPath, fastifyEntry });
@@ -948,7 +892,8 @@ async function runShip(args, context = {}) {
948
892
  ...findStripeWebhookViolations(truthpack),
949
893
  ...findPaidSurfaceNotEnforced(truthpack),
950
894
  ...findOwnerModeBypass(projectPath),
951
- ...findingsFromReality(projectPath)
895
+ // Merge runtime findings if --with runtime is specified
896
+ ...(opts.withRuntime ? findingsFromReality(projectPath) : [])
952
897
  ];
953
898
 
954
899
  // Contract drift detection
@@ -960,15 +905,15 @@ async function runShip(args, context = {}) {
960
905
 
961
906
  results.findings = allFindings;
962
907
 
963
- if (!opts.json) stopSpinner(`Route truth mapped (${truthpack.routes?.server?.length || 0} routes)`, true);
908
+ if (spinner) spinner.succeed(`Route truth mapped (${truthpack.routes?.server?.length || 0} routes)`);
964
909
 
965
910
  // Phase 3: Build Proof Graph
966
- if (!opts.json) startSpinner('Building proof graph...');
911
+ if (spinner) spinner.start('Building proof graph...');
967
912
 
968
913
  const proofGraph = buildProofGraph(allFindings, truthpack, projectPath);
969
914
  results.proofGraph = proofGraph;
970
915
 
971
- if (!opts.json) stopSpinner(`Proof graph built (${proofGraph.summary.totalClaims} claims)`, true);
916
+ if (spinner) spinner.succeed(`Proof graph built (${proofGraph.summary.totalClaims} claims)`);
972
917
 
973
918
  // Calculate final verdict
974
919
  const blockers = allFindings.filter(f => f.severity === 'BLOCK' || f.severity === 'critical');
@@ -1008,7 +953,7 @@ async function runShip(args, context = {}) {
1008
953
  runId,
1009
954
  command: "ship",
1010
955
  startTime,
1011
- exitCode: verdictToExitCode(verdict),
956
+ exitCode: getExitCode(verdict),
1012
957
  verdict,
1013
958
  result: {
1014
959
  verdict,
@@ -1050,7 +995,7 @@ async function runShip(args, context = {}) {
1050
995
  });
1051
996
  const proofPath = saveArtifact(runId, "proof-graph", proofGraph);
1052
997
 
1053
- return verdictToExitCode(verdict);
998
+ return getExitCode(verdict);
1054
999
  }
1055
1000
 
1056
1001
  // CI output mode (minimal)
@@ -1069,69 +1014,52 @@ async function runShip(args, context = {}) {
1069
1014
  timestamp: new Date().toISOString()
1070
1015
  });
1071
1016
 
1072
- return verdictToExitCode(verdict);
1017
+ return getExitCode(verdict);
1073
1018
  }
1074
1019
 
1075
1020
  // Fix mode
1021
+ let fixResults = null;
1076
1022
  if (opts.fix) {
1077
- printFixModeHeader();
1078
- const fixResults = await runAutoFix(projectPath, results, outputDir, allFindings);
1079
- printFixResults(fixResults);
1023
+ if (spinner) spinner.start('Applying safe fixes...');
1024
+ fixResults = await runAutoFix(projectPath, results, outputDir, allFindings);
1025
+ if (spinner) spinner.succeed('Safe fixes applied');
1080
1026
  }
1081
1027
 
1082
- // Main verdict card
1083
- printVerdictCard(verdict, results.score, blockers.length, warnings.length, duration);
1084
-
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);
1094
- }
1028
+ // Human-readable output using ship-output module
1029
+ const result = {
1030
+ verdict,
1031
+ score: results.score,
1032
+ findings: allFindings,
1033
+ blockers,
1034
+ warnings,
1035
+ truthpack,
1036
+ proofGraph,
1037
+ fixResults,
1038
+ duration: Date.now() - executionStart,
1039
+ };
1095
1040
 
1096
- // Proof graph (verbose)
1097
- if (opts.verbose) {
1098
- printProofGraph(proofGraph);
1099
- }
1041
+ console.log(formatShipOutput(result, {
1042
+ verbose: opts.verbose,
1043
+ showFix: opts.fix,
1044
+ showBadge: opts.badge,
1045
+ outputDir,
1046
+ projectPath,
1047
+ }));
1100
1048
 
1101
- // Badge generation
1049
+ // Badge file generation
1102
1050
  if (opts.badge) {
1103
- const badgeInfo = printBadgeOutput(projectPath, verdict, results.score);
1051
+ const { data: badgeData } = renderBadgeOutput(projectPath, verdict, results.score);
1104
1052
 
1105
1053
  // Save badge info
1106
1054
  fs.mkdirSync(outputDir, { recursive: true });
1107
1055
  fs.writeFileSync(
1108
1056
  path.join(outputDir, 'badge.json'),
1109
- JSON.stringify({ ...badgeInfo, verdict, score: results.score, generatedAt: new Date().toISOString() }, null, 2)
1057
+ JSON.stringify({ ...badgeData, verdict, score: results.score, generatedAt: new Date().toISOString() }, null, 2)
1110
1058
  );
1111
1059
  }
1112
1060
 
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
1061
+ // Earned upsell: Badge withheld when verdict != SHIP
1125
1062
  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
1063
  const currentTier = context?.authInfo?.access?.tier || "free";
1136
1064
  if (entitlements.tierMeetsMinimum(currentTier, "starter")) {
1137
1065
  // User has badge access but verdict prevents it
@@ -1154,14 +1082,14 @@ async function runShip(args, context = {}) {
1154
1082
  issueCount: allFindings.length,
1155
1083
  });
1156
1084
 
1157
- // Write artifacts
1085
+ // Write artifacts (no HTML report - use 'vibecheck report' command instead)
1158
1086
  try {
1159
1087
  const { writeArtifacts } = require("./utils");
1160
- writeArtifacts(outputDir, results);
1088
+ writeArtifacts(outputDir, results, { skipHtmlReport: true });
1161
1089
  } catch {}
1162
1090
 
1163
1091
  // Exit code: 0=SHIP, 1=WARN, 2=BLOCK
1164
- const exitCode = verdictToExitCode(verdict);
1092
+ const exitCode = getExitCode(verdict);
1165
1093
 
1166
1094
  // Save final results
1167
1095
  saveArtifact(runId, "summary", {
@@ -1175,14 +1103,14 @@ async function runShip(args, context = {}) {
1175
1103
  return exitCode;
1176
1104
 
1177
1105
  } catch (error) {
1178
- if (!opts.json) stopSpinner(`Ship check failed: ${error.message}`, false);
1106
+ if (spinner) spinner.fail(`Ship check failed: ${error.message}`);
1179
1107
 
1180
- console.error(`\n ${colors.blockRed}${ICONS.cross}${c.reset} ${c.bold}Error:${c.reset} ${error.message}`);
1108
+ console.error(`\n ${colors.error}${icons.error}${ansi.reset} ${ansi.bold}Error:${ansi.reset} ${error.message}`);
1181
1109
  if (opts.verbose) {
1182
- console.error(` ${c.dim}${error.stack}${c.reset}`);
1110
+ console.error(` ${ansi.dim}${error.stack}${ansi.reset}`);
1183
1111
  }
1184
1112
 
1185
- return 2;
1113
+ return EXIT_CODES.ERROR;
1186
1114
  }
1187
1115
  }
1188
1116
 
@@ -1272,6 +1200,8 @@ async function shipCore({ repoRoot, fastifyEntry, jsonOut, noWrite } = {}) {
1272
1200
  fs.writeFileSync(path.join(outDir, "last_ship.json"), JSON.stringify(report, null, 2));
1273
1201
  fs.writeFileSync(path.join(outDir, "proof-graph.json"), JSON.stringify(proofGraph, null, 2));
1274
1202
 
1203
+ // Note: HTML reports are generated by 'vibecheck report' command, not here
1204
+
1275
1205
  if (jsonOut) {
1276
1206
  fs.writeFileSync(path.isAbsolute(jsonOut) ? jsonOut : path.join(root, jsonOut), JSON.stringify(report, null, 2));
1277
1207
  }