@vibecheckai/cli 3.1.0 → 3.1.2

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 (160) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +105 -105
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/analysis-core.js +271 -271
  6. package/bin/runners/lib/analyzers.js +579 -579
  7. package/bin/runners/lib/auth-truth.js +193 -193
  8. package/bin/runners/lib/backup.js +62 -62
  9. package/bin/runners/lib/billing.js +107 -107
  10. package/bin/runners/lib/claims.js +118 -118
  11. package/bin/runners/lib/cli-output.js +368 -368
  12. package/bin/runners/lib/cli-ui.js +540 -540
  13. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  14. package/bin/runners/lib/contracts/env-contract.js +181 -181
  15. package/bin/runners/lib/contracts/external-contract.js +206 -206
  16. package/bin/runners/lib/contracts/guard.js +168 -168
  17. package/bin/runners/lib/contracts/index.js +89 -89
  18. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  19. package/bin/runners/lib/contracts/route-contract.js +199 -199
  20. package/bin/runners/lib/contracts.js +804 -804
  21. package/bin/runners/lib/detect.js +89 -89
  22. package/bin/runners/lib/detectors-v2.js +703 -703
  23. package/bin/runners/lib/doctor/autofix.js +254 -254
  24. package/bin/runners/lib/doctor/index.js +37 -37
  25. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  26. package/bin/runners/lib/doctor/modules/index.js +46 -46
  27. package/bin/runners/lib/doctor/modules/network.js +250 -250
  28. package/bin/runners/lib/doctor/modules/project.js +312 -312
  29. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  30. package/bin/runners/lib/doctor/modules/security.js +348 -348
  31. package/bin/runners/lib/doctor/modules/system.js +213 -213
  32. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  33. package/bin/runners/lib/doctor/reporter.js +262 -262
  34. package/bin/runners/lib/doctor/service.js +262 -262
  35. package/bin/runners/lib/doctor/types.js +113 -113
  36. package/bin/runners/lib/doctor/ui.js +263 -263
  37. package/bin/runners/lib/doctor-v2.js +608 -608
  38. package/bin/runners/lib/drift.js +425 -425
  39. package/bin/runners/lib/enforcement.js +72 -72
  40. package/bin/runners/lib/enterprise-detect.js +603 -603
  41. package/bin/runners/lib/enterprise-init.js +942 -942
  42. package/bin/runners/lib/entitlements-v2.js +490 -489
  43. package/bin/runners/lib/entitlements.js +6 -3
  44. package/bin/runners/lib/env-resolver.js +417 -417
  45. package/bin/runners/lib/env-template.js +66 -66
  46. package/bin/runners/lib/env.js +189 -189
  47. package/bin/runners/lib/extractors/client-calls.js +990 -990
  48. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  49. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  50. package/bin/runners/lib/extractors/index.js +363 -363
  51. package/bin/runners/lib/extractors/next-routes.js +524 -524
  52. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  53. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  54. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  55. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  56. package/bin/runners/lib/findings-schema.js +281 -281
  57. package/bin/runners/lib/firewall-prompt.js +50 -50
  58. package/bin/runners/lib/graph/graph-builder.js +265 -265
  59. package/bin/runners/lib/graph/html-renderer.js +413 -413
  60. package/bin/runners/lib/graph/index.js +32 -32
  61. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  62. package/bin/runners/lib/graph/static-extractor.js +518 -518
  63. package/bin/runners/lib/html-report.js +650 -650
  64. package/bin/runners/lib/init-wizard.js +308 -308
  65. package/bin/runners/lib/llm.js +75 -75
  66. package/bin/runners/lib/meter.js +61 -61
  67. package/bin/runners/lib/missions/evidence.js +126 -126
  68. package/bin/runners/lib/missions/plan.js +69 -69
  69. package/bin/runners/lib/missions/templates.js +192 -192
  70. package/bin/runners/lib/patch.js +40 -40
  71. package/bin/runners/lib/permissions/auth-model.js +213 -213
  72. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  73. package/bin/runners/lib/permissions/index.js +45 -45
  74. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  75. package/bin/runners/lib/pkgjson.js +28 -28
  76. package/bin/runners/lib/policy.js +295 -295
  77. package/bin/runners/lib/preflight.js +142 -142
  78. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  79. package/bin/runners/lib/reality/index.js +318 -318
  80. package/bin/runners/lib/reality/request-hashing.js +416 -416
  81. package/bin/runners/lib/reality/request-mapper.js +453 -453
  82. package/bin/runners/lib/reality/safety-rails.js +463 -463
  83. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  84. package/bin/runners/lib/reality/toast-detector.js +393 -393
  85. package/bin/runners/lib/reality-findings.js +84 -84
  86. package/bin/runners/lib/receipts.js +179 -179
  87. package/bin/runners/lib/redact.js +29 -29
  88. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  89. package/bin/runners/lib/replay/index.js +263 -263
  90. package/bin/runners/lib/replay/player.js +348 -348
  91. package/bin/runners/lib/replay/recorder.js +331 -331
  92. package/bin/runners/lib/report-engine.js +447 -447
  93. package/bin/runners/lib/report-html.js +1499 -1499
  94. package/bin/runners/lib/report-templates.js +969 -969
  95. package/bin/runners/lib/report.js +135 -135
  96. package/bin/runners/lib/route-detection.js +1140 -1140
  97. package/bin/runners/lib/route-truth.js +477 -477
  98. package/bin/runners/lib/sandbox/index.js +59 -59
  99. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  100. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  101. package/bin/runners/lib/sandbox/worktree.js +174 -174
  102. package/bin/runners/lib/schema-validator.js +350 -350
  103. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  104. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  105. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  106. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  107. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  108. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  109. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  110. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  111. package/bin/runners/lib/schemas/validator.js +438 -438
  112. package/bin/runners/lib/score-history.js +282 -282
  113. package/bin/runners/lib/server-usage.js +12 -0
  114. package/bin/runners/lib/share-pack.js +239 -239
  115. package/bin/runners/lib/snippets.js +67 -67
  116. package/bin/runners/lib/truth.js +667 -667
  117. package/bin/runners/lib/upsell.js +510 -510
  118. package/bin/runners/lib/usage.js +153 -153
  119. package/bin/runners/lib/validate-patch.js +156 -156
  120. package/bin/runners/lib/verdict-engine.js +628 -628
  121. package/bin/runners/reality/engine.js +917 -917
  122. package/bin/runners/reality/flows.js +122 -122
  123. package/bin/runners/reality/report.js +378 -378
  124. package/bin/runners/reality/session.js +193 -193
  125. package/bin/runners/runAuth.js +51 -0
  126. package/bin/runners/runClaimVerifier.js +483 -483
  127. package/bin/runners/runContext.js +56 -56
  128. package/bin/runners/runContextCompiler.js +385 -385
  129. package/bin/runners/runCtx.js +674 -674
  130. package/bin/runners/runCtxDiff.js +301 -301
  131. package/bin/runners/runCtxGuard.js +176 -176
  132. package/bin/runners/runCtxSync.js +116 -116
  133. package/bin/runners/runGate.js +17 -17
  134. package/bin/runners/runGraph.js +454 -454
  135. package/bin/runners/runGuard.js +168 -168
  136. package/bin/runners/runInitGha.js +164 -164
  137. package/bin/runners/runInstall.js +277 -277
  138. package/bin/runners/runInteractive.js +388 -388
  139. package/bin/runners/runLabs.js +340 -340
  140. package/bin/runners/runMissionGenerator.js +282 -282
  141. package/bin/runners/runPR.js +255 -255
  142. package/bin/runners/runPermissions.js +304 -304
  143. package/bin/runners/runPreflight.js +580 -553
  144. package/bin/runners/runProve.js +1252 -1252
  145. package/bin/runners/runReality.js +1328 -1328
  146. package/bin/runners/runReplay.js +499 -499
  147. package/bin/runners/runReport.js +584 -584
  148. package/bin/runners/runShare.js +212 -212
  149. package/bin/runners/runStatus.js +138 -138
  150. package/bin/runners/runTruthpack.js +636 -636
  151. package/bin/runners/runVerify.js +272 -272
  152. package/bin/runners/runWatch.js +407 -407
  153. package/bin/vibecheck.js +2 -1
  154. package/mcp-server/consolidated-tools.js +804 -804
  155. package/mcp-server/package.json +1 -1
  156. package/mcp-server/tools/index.js +72 -72
  157. package/mcp-server/truth-context.js +581 -581
  158. package/mcp-server/truth-firewall-tools.js +1500 -1500
  159. package/package.json +1 -1
  160. package/bin/runners/runProof.zip +0 -0
@@ -1,510 +1,510 @@
1
- /**
2
- * Upsell Copy Module - Central copy generator for tier upgrades
3
- *
4
- * This is the SINGLE SOURCE OF TRUTH for all upsell/upgrade messaging.
5
- * All upgrade nudges flow through these functions.
6
- *
7
- * Rules:
8
- * - Blunt, confident, minimal tone
9
- * - No cringe language ("please upgrade")
10
- * - Every upsell includes a free path if one exists
11
- * - No web links besides https://vibecheckai.dev/pricing
12
- *
13
- * @module bin/runners/lib/upsell
14
- */
15
-
16
- "use strict";
17
-
18
- // ═══════════════════════════════════════════════════════════════════════════════
19
- // ANSI STYLING
20
- // ═══════════════════════════════════════════════════════════════════════════════
21
- const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
22
-
23
- const c = SUPPORTS_COLOR ? {
24
- reset: "\x1b[0m",
25
- bold: "\x1b[1m",
26
- dim: "\x1b[2m",
27
- red: "\x1b[31m",
28
- green: "\x1b[32m",
29
- yellow: "\x1b[33m",
30
- cyan: "\x1b[36m",
31
- magenta: "\x1b[35m",
32
- white: "\x1b[37m",
33
- gray: "\x1b[90m",
34
- } : {
35
- reset: "", bold: "", dim: "", red: "", green: "", yellow: "",
36
- cyan: "", magenta: "", white: "", gray: "",
37
- };
38
-
39
- const sym = {
40
- lock: "🔒",
41
- arrow: "→",
42
- warning: "⚠",
43
- check: "✓",
44
- cross: "✗",
45
- star: "★",
46
- rocket: "🚀",
47
- badge: "🏆",
48
- lightning: "⚡",
49
- };
50
-
51
- // ═══════════════════════════════════════════════════════════════════════════════
52
- // TIER CONFIGURATION
53
- // ═══════════════════════════════════════════════════════════════════════════════
54
- const TIER_LABELS = {
55
- free: "FREE",
56
- starter: "STARTER",
57
- pro: "PRO",
58
- complete: "COMPLETE",
59
- };
60
-
61
- const TIER_COLORS = {
62
- free: c.green,
63
- starter: c.cyan,
64
- pro: c.magenta,
65
- complete: c.yellow,
66
- };
67
-
68
- const PRICING_URL = "https://vibecheckai.dev/pricing";
69
-
70
- // ═══════════════════════════════════════════════════════════════════════════════
71
- // DENIAL COPY - Command-specific reasons and alternatives
72
- // ═══════════════════════════════════════════════════════════════════════════════
73
- const DENIAL_COPY = {
74
- ship: {
75
- feature: "release gate",
76
- why: "Full ship analysis with runtime verification",
77
- freeAlt: "vibecheck scan",
78
- freeAltDesc: "static analysis",
79
- },
80
- prove: {
81
- feature: "proof loop",
82
- why: "Complete ctx → reality → ship → fix cycle",
83
- freeAlt: "vibecheck ship",
84
- freeAltDesc: "static verdict",
85
- },
86
- permissions: {
87
- feature: "auth boundary / IDOR detection",
88
- why: "Deep authorization matrix and IDOR analysis",
89
- freeAlt: "vibecheck reality --verify-auth",
90
- freeAltDesc: "basic auth boundary check",
91
- },
92
- "fix.apply_patches": {
93
- feature: "patch generator / PR-ready diff",
94
- why: "Apply LLM-generated patches automatically",
95
- freeAlt: "vibecheck fix",
96
- freeAltDesc: "plan-only mode",
97
- },
98
- fix: {
99
- feature: "auto-fix with patches",
100
- why: "Generate and apply fixes to your codebase",
101
- freeAlt: "vibecheck fix --plan-only",
102
- freeAltDesc: "view fix plan without applying",
103
- },
104
- badge: {
105
- feature: "status artifact",
106
- why: "Verified ship badge for README",
107
- freeAlt: "vibecheck report",
108
- freeAltDesc: "HTML/MD report",
109
- },
110
- gate: {
111
- feature: "CI/CD gate",
112
- why: "Block deploys on verification failures",
113
- freeAlt: "vibecheck ship --ci",
114
- freeAltDesc: "exit codes for scripts",
115
- },
116
- pr: {
117
- feature: "PR comment generator",
118
- why: "Auto-generated PR comments with findings",
119
- freeAlt: "vibecheck report --format md",
120
- freeAltDesc: "markdown report",
121
- },
122
- launch: {
123
- feature: "pre-launch checklist",
124
- why: "Guided launch readiness wizard",
125
- freeAlt: "vibecheck ship",
126
- freeAltDesc: "verdict check",
127
- },
128
- mcp: {
129
- feature: "MCP server for AI IDEs",
130
- why: "Real-time AI IDE integration",
131
- freeAlt: "vibecheck ctx",
132
- freeAltDesc: "generate truthpack manually",
133
- },
134
- share: {
135
- feature: "share pack generator",
136
- why: "Shareable proof bundle for PRs/docs",
137
- freeAlt: "vibecheck report",
138
- freeAltDesc: "local report",
139
- },
140
- "ai-test": {
141
- feature: "autonomous test generation",
142
- why: "AI-generated test coverage",
143
- freeAlt: "vibecheck scan",
144
- freeAltDesc: "static analysis",
145
- },
146
- replay: {
147
- feature: "session replay",
148
- why: "Record and replay user sessions",
149
- freeAlt: "vibecheck reality",
150
- freeAltDesc: "one-time runtime proof",
151
- },
152
- graph: {
153
- feature: "reality proof graph",
154
- why: "Visual dependency and proof graph",
155
- freeAlt: "vibecheck ctx",
156
- freeAltDesc: "truthpack",
157
- },
158
- };
159
-
160
- // ═══════════════════════════════════════════════════════════════════════════════
161
- // CAPS COPY - Downgrade mode descriptions
162
- // ═══════════════════════════════════════════════════════════════════════════════
163
- const CAPS_COPY = {
164
- "reality.preview": {
165
- short: "5 pages, no auth boundary",
166
- full: "Preview mode: 5 pages max, 20 clicks, no auth boundary verification",
167
- upgradeBenefit: "Unlimited pages + full auth boundary testing",
168
- },
169
- "fix.plan_only": {
170
- short: "plan-only, no apply",
171
- full: "Plan mode: generates fix missions but cannot apply patches",
172
- upgradeBenefit: "Apply patches automatically with --apply",
173
- },
174
- "report.html_md": {
175
- short: "HTML/MD only",
176
- full: "Basic formats: HTML and Markdown reports only",
177
- upgradeBenefit: "SARIF, CSV, and compliance pack exports",
178
- },
179
- "ship.static": {
180
- short: "static-only",
181
- full: "Static analysis only, no runtime verification",
182
- upgradeBenefit: "Full runtime + static verification",
183
- },
184
- "mcp.help_only": {
185
- short: "help and print-config only",
186
- full: "MCP server limited to help and config commands",
187
- upgradeBenefit: "Full MCP server with all tools",
188
- },
189
- };
190
-
191
- // ═══════════════════════════════════════════════════════════════════════════════
192
- // formatDenied() - Hard denial message
193
- // ═══════════════════════════════════════════════════════════════════════════════
194
- /**
195
- * Format a denial message for a command that requires a higher tier.
196
- *
197
- * @param {string} cmd - The command that was denied
198
- * @param {object} opts - Options
199
- * @param {string} opts.currentTier - User's current tier
200
- * @param {string} opts.requiredTier - Required tier for the command
201
- * @param {string} [opts.reason] - Additional context
202
- * @param {string} [opts.suggestedNext] - Suggested next command
203
- * @param {string} [opts.freeAlternative] - Free alternative command
204
- * @returns {string} Formatted denial message
205
- */
206
- function formatDenied(cmd, opts = {}) {
207
- const { currentTier = "free", requiredTier = "pro" } = opts;
208
-
209
- const copy = DENIAL_COPY[cmd] || {
210
- feature: cmd,
211
- why: `${cmd} command`,
212
- freeAlt: null,
213
- freeAltDesc: null,
214
- };
215
-
216
- const reqColor = TIER_COLORS[requiredTier] || c.yellow;
217
- const reqLabel = TIER_LABELS[requiredTier] || requiredTier.toUpperCase();
218
- const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
219
-
220
- let msg = `
221
- ${c.red}${c.bold}${sym.lock} LOCKED${c.reset}
222
-
223
- ${c.bold}What:${c.reset} ${c.yellow}${cmd}${c.reset} ${c.dim}(${copy.feature})${c.reset}
224
- ${c.bold}Why:${c.reset} ${copy.why}
225
- ${c.bold}Requires:${c.reset} ${reqColor}${reqLabel}${c.reset} plan
226
- ${c.bold}You have:${c.reset} ${c.dim}${curLabel}${c.reset}
227
- `;
228
-
229
- // Free alternative
230
- if (copy.freeAlt) {
231
- msg += `
232
- ${c.green}${sym.arrow} Free path:${c.reset} ${c.cyan}${copy.freeAlt}${c.reset} ${c.dim}(${copy.freeAltDesc})${c.reset}`;
233
- }
234
-
235
- // Upgrade CTA
236
- msg += `
237
- ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
238
- `;
239
-
240
- return msg;
241
- }
242
-
243
- // ═══════════════════════════════════════════════════════════════════════════════
244
- // formatDowngrade() - Caps/downgrade notice
245
- // ═══════════════════════════════════════════════════════════════════════════════
246
- /**
247
- * Format a downgrade notice for a command running in capped mode.
248
- *
249
- * @param {string} cmd - The command being run
250
- * @param {object} opts - Options
251
- * @param {string} opts.currentTier - User's current tier
252
- * @param {string} opts.effectiveMode - The downgraded mode being used
253
- * @param {object} [opts.caps] - Specific caps applied
254
- * @returns {string} Formatted downgrade notice
255
- */
256
- function formatDowngrade(cmd, opts = {}) {
257
- const { currentTier = "free", effectiveMode, caps } = opts;
258
-
259
- const copy = CAPS_COPY[effectiveMode] || {
260
- short: effectiveMode || "limited mode",
261
- full: `Running in ${effectiveMode || "limited"} mode`,
262
- upgradeBenefit: "Full access",
263
- };
264
-
265
- const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
266
-
267
- // Single line notice for start of run
268
- let shortNotice = `${c.yellow}${sym.warning}${c.reset} Running in ${c.yellow}${curLabel}${c.reset} mode: ${c.dim}${copy.short}${c.reset}`;
269
-
270
- return shortNotice;
271
- }
272
-
273
- /**
274
- * Format a cap-hit notice when user reaches a limit during execution.
275
- *
276
- * @param {string} cmd - The command being run
277
- * @param {object} opts - Options
278
- * @param {string} opts.limitType - Type of limit hit (e.g., "pages", "clicks")
279
- * @param {number} opts.limitValue - The limit value
280
- * @param {string} opts.upgradeTier - Tier to upgrade to
281
- * @returns {string} Formatted cap-hit message
282
- */
283
- function formatCapHit(cmd, opts = {}) {
284
- const { limitType = "limit", limitValue, upgradeTier = "pro" } = opts;
285
-
286
- const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
287
- const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
288
-
289
- return `${c.yellow}${sym.warning}${c.reset} Hit FREE ${limitType} cap (${limitValue}). Upgrade to ${tierColor}${tierLabel}${c.reset} ${sym.arrow} ${PRICING_URL}`;
290
- }
291
-
292
- // ═══════════════════════════════════════════════════════════════════════════════
293
- // formatEarnedUpsell() - End-of-run upsell
294
- // ═══════════════════════════════════════════════════════════════════════════════
295
- /**
296
- * Format an earned upsell shown at end of run.
297
- *
298
- * @param {object} opts - Options
299
- * @param {string} opts.cmd - The command that was run
300
- * @param {string} opts.verdict - The verdict (SHIP/WARN/BLOCK)
301
- * @param {Array} [opts.topIssues] - Top 3 issues to show
302
- * @param {string} [opts.withheldArtifact] - Artifact that was withheld (e.g., "badge")
303
- * @param {string} [opts.upgradeTier] - Tier to suggest upgrading to
304
- * @param {string} [opts.why] - Why this upsell is relevant
305
- * @returns {string} Formatted earned upsell message
306
- */
307
- function formatEarnedUpsell(opts = {}) {
308
- const {
309
- cmd,
310
- verdict,
311
- topIssues = [],
312
- withheldArtifact,
313
- upgradeTier = "pro",
314
- why,
315
- currentTier = "free",
316
- suggestedCmd,
317
- } = opts;
318
-
319
- const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
320
- const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
321
-
322
- let msg = "";
323
-
324
- // Badge withheld case
325
- if (withheldArtifact === "badge") {
326
- msg += `
327
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
328
- ${c.yellow}${sym.badge} Badge withheld${c.reset} ${c.dim}(verdict: ${verdict})${c.reset}
329
- `;
330
-
331
- if (topIssues.length > 0) {
332
- msg += `\n${c.bold}Top blockers:${c.reset}\n`;
333
- topIssues.slice(0, 3).forEach((issue, i) => {
334
- const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
335
- msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}${issue.severity}${c.reset} ${issue.message || issue.id || issue.type}\n`;
336
- });
337
- }
338
-
339
- if (suggestedCmd) {
340
- msg += `\n${c.green}${sym.arrow} Fix:${c.reset} ${c.cyan}${suggestedCmd}${c.reset}`;
341
- }
342
-
343
- msg += `\n${c.dim}Badge unlocks when verdict = SHIP${c.reset}\n`;
344
- return msg;
345
- }
346
-
347
- // Fix plan-only case
348
- if (cmd === "fix" && withheldArtifact === "apply") {
349
- msg += `
350
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
351
- ${c.yellow}${sym.lightning} Fix plan generated${c.reset}
352
-
353
- Patches ready but ${c.yellow}--apply${c.reset} requires ${tierColor}${tierLabel}${c.reset} plan.
354
-
355
- ${c.green}${sym.arrow} Review:${c.reset} .vibecheck/missions/
356
- ${c.bold}${sym.arrow} Apply:${c.reset} Upgrade ${sym.arrow} ${PRICING_URL}
357
- `;
358
- return msg;
359
- }
360
-
361
- // Reality cap hit case
362
- if (cmd === "reality" && why === "cap_hit") {
363
- msg += `
364
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
365
- ${c.yellow}${sym.warning} Preview complete${c.reset}
366
-
367
- Crawled ${topIssues.length || 5} pages (FREE limit).
368
- ${tierColor}${tierLabel}${c.reset} unlocks unlimited pages + auth boundary testing.
369
-
370
- ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
371
- `;
372
- return msg;
373
- }
374
-
375
- // Generic earned upsell
376
- if (why) {
377
- msg += `
378
- ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
379
- ${c.cyan}${sym.star} ${why}${c.reset}
380
-
381
- ${tierColor}${tierLabel}${c.reset} unlocks this feature.
382
- ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
383
- `;
384
- }
385
-
386
- return msg;
387
- }
388
-
389
- // ═══════════════════════════════════════════════════════════════════════════════
390
- // formatBadgeWithheld() - Specific badge denial
391
- // ═══════════════════════════════════════════════════════════════════════════════
392
- /**
393
- * Format badge withheld message with top issues.
394
- *
395
- * @param {string} verdict - Current verdict
396
- * @param {Array} findings - All findings
397
- * @param {string} currentTier - User's tier
398
- * @returns {string} Formatted message
399
- */
400
- function formatBadgeWithheld(verdict, findings = [], currentTier = "free") {
401
- const blockers = findings.filter(f => f.severity === "BLOCK").slice(0, 3);
402
- const topIssues = blockers.length > 0 ? blockers : findings.slice(0, 3);
403
-
404
- let msg = `
405
- ${c.yellow}${sym.badge} Badge withheld${c.reset}
406
-
407
- ${c.bold}Verdict:${c.reset} ${verdict === "BLOCK" ? c.red : c.yellow}${verdict}${c.reset}
408
- ${c.dim}Badge requires SHIP verdict${c.reset}
409
- `;
410
-
411
- if (topIssues.length > 0) {
412
- msg += `\n ${c.bold}Fix these first:${c.reset}\n`;
413
- topIssues.forEach((issue, i) => {
414
- const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
415
- const label = issue.id || issue.type || issue.ruleId || "issue";
416
- const desc = issue.message || issue.description || "";
417
- msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}[${issue.severity}]${c.reset} ${label}${desc ? `: ${c.dim}${desc.slice(0, 50)}${c.reset}` : ""}\n`;
418
- });
419
- }
420
-
421
- // Suggest fix command if available
422
- const canFix = currentTier !== "free";
423
- if (canFix) {
424
- msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix${c.reset}\n`;
425
- } else {
426
- msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix --plan-only${c.reset} ${c.dim}(view fix plan)${c.reset}\n`;
427
- }
428
-
429
- return msg;
430
- }
431
-
432
- // ═══════════════════════════════════════════════════════════════════════════════
433
- // formatNextSteps() - Smart suggestions
434
- // ═══════════════════════════════════════════════════════════════════════════════
435
- /**
436
- * Format next step suggestions based on current command and result.
437
- *
438
- * @param {string} cmd - Command just run
439
- * @param {string} verdict - Result/verdict
440
- * @param {string} currentTier - User's tier
441
- * @returns {string} Next step suggestion
442
- */
443
- function formatNextSteps(cmd, verdict, currentTier = "free") {
444
- const steps = [];
445
-
446
- switch (cmd) {
447
- case "scan":
448
- if (currentTier === "free") {
449
- steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
450
- } else {
451
- steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
452
- steps.push({ cmd: "vibecheck prove --url <url>", desc: "full proof loop" });
453
- }
454
- break;
455
-
456
- case "ship":
457
- if (verdict === "SHIP") {
458
- if (currentTier !== "free") {
459
- steps.push({ cmd: "vibecheck badge", desc: "generate badge" });
460
- }
461
- steps.push({ cmd: "vibecheck report", desc: "export report" });
462
- } else {
463
- if (currentTier === "free") {
464
- steps.push({ cmd: "vibecheck fix --plan-only", desc: "view fix plan" });
465
- } else {
466
- steps.push({ cmd: "vibecheck fix", desc: "auto-fix issues" });
467
- }
468
- }
469
- break;
470
-
471
- case "fix":
472
- steps.push({ cmd: "vibecheck ship", desc: "verify fixes" });
473
- break;
474
-
475
- case "reality":
476
- steps.push({ cmd: "vibecheck ship", desc: "get verdict with runtime proof" });
477
- break;
478
- }
479
-
480
- if (steps.length === 0) return "";
481
-
482
- let msg = `\n${c.dim}Next:${c.reset} `;
483
- msg += steps.map(s => `${c.cyan}${s.cmd}${c.reset} ${c.dim}(${s.desc})${c.reset}`).join(" or ");
484
-
485
- return msg;
486
- }
487
-
488
- // ═══════════════════════════════════════════════════════════════════════════════
489
- // EXPORTS
490
- // ═══════════════════════════════════════════════════════════════════════════════
491
- module.exports = {
492
- // Core formatters
493
- formatDenied,
494
- formatDowngrade,
495
- formatCapHit,
496
- formatEarnedUpsell,
497
- formatBadgeWithheld,
498
- formatNextSteps,
499
-
500
- // Copy data (for testing/docs)
501
- DENIAL_COPY,
502
- CAPS_COPY,
503
- TIER_LABELS,
504
- TIER_COLORS,
505
- PRICING_URL,
506
-
507
- // Styling (for consistent use)
508
- c,
509
- sym,
510
- };
1
+ /**
2
+ * Upsell Copy Module - Central copy generator for tier upgrades
3
+ *
4
+ * This is the SINGLE SOURCE OF TRUTH for all upsell/upgrade messaging.
5
+ * All upgrade nudges flow through these functions.
6
+ *
7
+ * Rules:
8
+ * - Blunt, confident, minimal tone
9
+ * - No cringe language ("please upgrade")
10
+ * - Every upsell includes a free path if one exists
11
+ * - No web links besides https://vibecheckai.dev/pricing
12
+ *
13
+ * @module bin/runners/lib/upsell
14
+ */
15
+
16
+ "use strict";
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════════
19
+ // ANSI STYLING
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+ const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
22
+
23
+ const c = SUPPORTS_COLOR ? {
24
+ reset: "\x1b[0m",
25
+ bold: "\x1b[1m",
26
+ dim: "\x1b[2m",
27
+ red: "\x1b[31m",
28
+ green: "\x1b[32m",
29
+ yellow: "\x1b[33m",
30
+ cyan: "\x1b[36m",
31
+ magenta: "\x1b[35m",
32
+ white: "\x1b[37m",
33
+ gray: "\x1b[90m",
34
+ } : {
35
+ reset: "", bold: "", dim: "", red: "", green: "", yellow: "",
36
+ cyan: "", magenta: "", white: "", gray: "",
37
+ };
38
+
39
+ const sym = {
40
+ lock: "🔒",
41
+ arrow: "→",
42
+ warning: "⚠",
43
+ check: "✓",
44
+ cross: "✗",
45
+ star: "★",
46
+ rocket: "🚀",
47
+ badge: "🏆",
48
+ lightning: "⚡",
49
+ };
50
+
51
+ // ═══════════════════════════════════════════════════════════════════════════════
52
+ // TIER CONFIGURATION
53
+ // ═══════════════════════════════════════════════════════════════════════════════
54
+ const TIER_LABELS = {
55
+ free: "FREE",
56
+ starter: "STARTER",
57
+ pro: "PRO",
58
+ complete: "COMPLETE",
59
+ };
60
+
61
+ const TIER_COLORS = {
62
+ free: c.green,
63
+ starter: c.cyan,
64
+ pro: c.magenta,
65
+ complete: c.yellow,
66
+ };
67
+
68
+ const PRICING_URL = "https://vibecheckai.dev/pricing";
69
+
70
+ // ═══════════════════════════════════════════════════════════════════════════════
71
+ // DENIAL COPY - Command-specific reasons and alternatives
72
+ // ═══════════════════════════════════════════════════════════════════════════════
73
+ const DENIAL_COPY = {
74
+ ship: {
75
+ feature: "release gate",
76
+ why: "Full ship analysis with runtime verification",
77
+ freeAlt: "vibecheck scan",
78
+ freeAltDesc: "static analysis",
79
+ },
80
+ prove: {
81
+ feature: "proof loop",
82
+ why: "Complete ctx → reality → ship → fix cycle",
83
+ freeAlt: "vibecheck ship",
84
+ freeAltDesc: "static verdict",
85
+ },
86
+ permissions: {
87
+ feature: "auth boundary / IDOR detection",
88
+ why: "Deep authorization matrix and IDOR analysis",
89
+ freeAlt: "vibecheck reality --verify-auth",
90
+ freeAltDesc: "basic auth boundary check",
91
+ },
92
+ "fix.apply_patches": {
93
+ feature: "patch generator / PR-ready diff",
94
+ why: "Apply LLM-generated patches automatically",
95
+ freeAlt: "vibecheck fix",
96
+ freeAltDesc: "plan-only mode",
97
+ },
98
+ fix: {
99
+ feature: "auto-fix with patches",
100
+ why: "Generate and apply fixes to your codebase",
101
+ freeAlt: "vibecheck fix --plan-only",
102
+ freeAltDesc: "view fix plan without applying",
103
+ },
104
+ badge: {
105
+ feature: "status artifact",
106
+ why: "Verified ship badge for README",
107
+ freeAlt: "vibecheck report",
108
+ freeAltDesc: "HTML/MD report",
109
+ },
110
+ gate: {
111
+ feature: "CI/CD gate",
112
+ why: "Block deploys on verification failures",
113
+ freeAlt: "vibecheck ship --ci",
114
+ freeAltDesc: "exit codes for scripts",
115
+ },
116
+ pr: {
117
+ feature: "PR comment generator",
118
+ why: "Auto-generated PR comments with findings",
119
+ freeAlt: "vibecheck report --format md",
120
+ freeAltDesc: "markdown report",
121
+ },
122
+ launch: {
123
+ feature: "pre-launch checklist",
124
+ why: "Guided launch readiness wizard",
125
+ freeAlt: "vibecheck ship",
126
+ freeAltDesc: "verdict check",
127
+ },
128
+ mcp: {
129
+ feature: "MCP server for AI IDEs",
130
+ why: "Real-time AI IDE integration",
131
+ freeAlt: "vibecheck ctx",
132
+ freeAltDesc: "generate truthpack manually",
133
+ },
134
+ share: {
135
+ feature: "share pack generator",
136
+ why: "Shareable proof bundle for PRs/docs",
137
+ freeAlt: "vibecheck report",
138
+ freeAltDesc: "local report",
139
+ },
140
+ "ai-test": {
141
+ feature: "autonomous test generation",
142
+ why: "AI-generated test coverage",
143
+ freeAlt: "vibecheck scan",
144
+ freeAltDesc: "static analysis",
145
+ },
146
+ replay: {
147
+ feature: "session replay",
148
+ why: "Record and replay user sessions",
149
+ freeAlt: "vibecheck reality",
150
+ freeAltDesc: "one-time runtime proof",
151
+ },
152
+ graph: {
153
+ feature: "reality proof graph",
154
+ why: "Visual dependency and proof graph",
155
+ freeAlt: "vibecheck ctx",
156
+ freeAltDesc: "truthpack",
157
+ },
158
+ };
159
+
160
+ // ═══════════════════════════════════════════════════════════════════════════════
161
+ // CAPS COPY - Downgrade mode descriptions
162
+ // ═══════════════════════════════════════════════════════════════════════════════
163
+ const CAPS_COPY = {
164
+ "reality.preview": {
165
+ short: "5 pages, no auth boundary",
166
+ full: "Preview mode: 5 pages max, 20 clicks, no auth boundary verification",
167
+ upgradeBenefit: "Unlimited pages + full auth boundary testing",
168
+ },
169
+ "fix.plan_only": {
170
+ short: "plan-only, no apply",
171
+ full: "Plan mode: generates fix missions but cannot apply patches",
172
+ upgradeBenefit: "Apply patches automatically with --apply",
173
+ },
174
+ "report.html_md": {
175
+ short: "HTML/MD only",
176
+ full: "Basic formats: HTML and Markdown reports only",
177
+ upgradeBenefit: "SARIF, CSV, and compliance pack exports",
178
+ },
179
+ "ship.static": {
180
+ short: "static-only",
181
+ full: "Static analysis only, no runtime verification",
182
+ upgradeBenefit: "Full runtime + static verification",
183
+ },
184
+ "mcp.help_only": {
185
+ short: "help and print-config only",
186
+ full: "MCP server limited to help and config commands",
187
+ upgradeBenefit: "Full MCP server with all tools",
188
+ },
189
+ };
190
+
191
+ // ═══════════════════════════════════════════════════════════════════════════════
192
+ // formatDenied() - Hard denial message
193
+ // ═══════════════════════════════════════════════════════════════════════════════
194
+ /**
195
+ * Format a denial message for a command that requires a higher tier.
196
+ *
197
+ * @param {string} cmd - The command that was denied
198
+ * @param {object} opts - Options
199
+ * @param {string} opts.currentTier - User's current tier
200
+ * @param {string} opts.requiredTier - Required tier for the command
201
+ * @param {string} [opts.reason] - Additional context
202
+ * @param {string} [opts.suggestedNext] - Suggested next command
203
+ * @param {string} [opts.freeAlternative] - Free alternative command
204
+ * @returns {string} Formatted denial message
205
+ */
206
+ function formatDenied(cmd, opts = {}) {
207
+ const { currentTier = "free", requiredTier = "pro" } = opts;
208
+
209
+ const copy = DENIAL_COPY[cmd] || {
210
+ feature: cmd,
211
+ why: `${cmd} command`,
212
+ freeAlt: null,
213
+ freeAltDesc: null,
214
+ };
215
+
216
+ const reqColor = TIER_COLORS[requiredTier] || c.yellow;
217
+ const reqLabel = TIER_LABELS[requiredTier] || requiredTier.toUpperCase();
218
+ const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
219
+
220
+ let msg = `
221
+ ${c.red}${c.bold}${sym.lock} LOCKED${c.reset}
222
+
223
+ ${c.bold}What:${c.reset} ${c.yellow}${cmd}${c.reset} ${c.dim}(${copy.feature})${c.reset}
224
+ ${c.bold}Why:${c.reset} ${copy.why}
225
+ ${c.bold}Requires:${c.reset} ${reqColor}${reqLabel}${c.reset} plan
226
+ ${c.bold}You have:${c.reset} ${c.dim}${curLabel}${c.reset}
227
+ `;
228
+
229
+ // Free alternative
230
+ if (copy.freeAlt) {
231
+ msg += `
232
+ ${c.green}${sym.arrow} Free path:${c.reset} ${c.cyan}${copy.freeAlt}${c.reset} ${c.dim}(${copy.freeAltDesc})${c.reset}`;
233
+ }
234
+
235
+ // Upgrade CTA
236
+ msg += `
237
+ ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
238
+ `;
239
+
240
+ return msg;
241
+ }
242
+
243
+ // ═══════════════════════════════════════════════════════════════════════════════
244
+ // formatDowngrade() - Caps/downgrade notice
245
+ // ═══════════════════════════════════════════════════════════════════════════════
246
+ /**
247
+ * Format a downgrade notice for a command running in capped mode.
248
+ *
249
+ * @param {string} cmd - The command being run
250
+ * @param {object} opts - Options
251
+ * @param {string} opts.currentTier - User's current tier
252
+ * @param {string} opts.effectiveMode - The downgraded mode being used
253
+ * @param {object} [opts.caps] - Specific caps applied
254
+ * @returns {string} Formatted downgrade notice
255
+ */
256
+ function formatDowngrade(cmd, opts = {}) {
257
+ const { currentTier = "free", effectiveMode, caps } = opts;
258
+
259
+ const copy = CAPS_COPY[effectiveMode] || {
260
+ short: effectiveMode || "limited mode",
261
+ full: `Running in ${effectiveMode || "limited"} mode`,
262
+ upgradeBenefit: "Full access",
263
+ };
264
+
265
+ const curLabel = TIER_LABELS[currentTier] || currentTier.toUpperCase();
266
+
267
+ // Single line notice for start of run
268
+ let shortNotice = `${c.yellow}${sym.warning}${c.reset} Running in ${c.yellow}${curLabel}${c.reset} mode: ${c.dim}${copy.short}${c.reset}`;
269
+
270
+ return shortNotice;
271
+ }
272
+
273
+ /**
274
+ * Format a cap-hit notice when user reaches a limit during execution.
275
+ *
276
+ * @param {string} cmd - The command being run
277
+ * @param {object} opts - Options
278
+ * @param {string} opts.limitType - Type of limit hit (e.g., "pages", "clicks")
279
+ * @param {number} opts.limitValue - The limit value
280
+ * @param {string} opts.upgradeTier - Tier to upgrade to
281
+ * @returns {string} Formatted cap-hit message
282
+ */
283
+ function formatCapHit(cmd, opts = {}) {
284
+ const { limitType = "limit", limitValue, upgradeTier = "pro" } = opts;
285
+
286
+ const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
287
+ const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
288
+
289
+ return `${c.yellow}${sym.warning}${c.reset} Hit FREE ${limitType} cap (${limitValue}). Upgrade to ${tierColor}${tierLabel}${c.reset} ${sym.arrow} ${PRICING_URL}`;
290
+ }
291
+
292
+ // ═══════════════════════════════════════════════════════════════════════════════
293
+ // formatEarnedUpsell() - End-of-run upsell
294
+ // ═══════════════════════════════════════════════════════════════════════════════
295
+ /**
296
+ * Format an earned upsell shown at end of run.
297
+ *
298
+ * @param {object} opts - Options
299
+ * @param {string} opts.cmd - The command that was run
300
+ * @param {string} opts.verdict - The verdict (SHIP/WARN/BLOCK)
301
+ * @param {Array} [opts.topIssues] - Top 3 issues to show
302
+ * @param {string} [opts.withheldArtifact] - Artifact that was withheld (e.g., "badge")
303
+ * @param {string} [opts.upgradeTier] - Tier to suggest upgrading to
304
+ * @param {string} [opts.why] - Why this upsell is relevant
305
+ * @returns {string} Formatted earned upsell message
306
+ */
307
+ function formatEarnedUpsell(opts = {}) {
308
+ const {
309
+ cmd,
310
+ verdict,
311
+ topIssues = [],
312
+ withheldArtifact,
313
+ upgradeTier = "pro",
314
+ why,
315
+ currentTier = "free",
316
+ suggestedCmd,
317
+ } = opts;
318
+
319
+ const tierColor = TIER_COLORS[upgradeTier] || c.magenta;
320
+ const tierLabel = TIER_LABELS[upgradeTier] || upgradeTier.toUpperCase();
321
+
322
+ let msg = "";
323
+
324
+ // Badge withheld case
325
+ if (withheldArtifact === "badge") {
326
+ msg += `
327
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
328
+ ${c.yellow}${sym.badge} Badge withheld${c.reset} ${c.dim}(verdict: ${verdict})${c.reset}
329
+ `;
330
+
331
+ if (topIssues.length > 0) {
332
+ msg += `\n${c.bold}Top blockers:${c.reset}\n`;
333
+ topIssues.slice(0, 3).forEach((issue, i) => {
334
+ const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
335
+ msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}${issue.severity}${c.reset} ${issue.message || issue.id || issue.type}\n`;
336
+ });
337
+ }
338
+
339
+ if (suggestedCmd) {
340
+ msg += `\n${c.green}${sym.arrow} Fix:${c.reset} ${c.cyan}${suggestedCmd}${c.reset}`;
341
+ }
342
+
343
+ msg += `\n${c.dim}Badge unlocks when verdict = SHIP${c.reset}\n`;
344
+ return msg;
345
+ }
346
+
347
+ // Fix plan-only case
348
+ if (cmd === "fix" && withheldArtifact === "apply") {
349
+ msg += `
350
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
351
+ ${c.yellow}${sym.lightning} Fix plan generated${c.reset}
352
+
353
+ Patches ready but ${c.yellow}--apply${c.reset} requires ${tierColor}${tierLabel}${c.reset} plan.
354
+
355
+ ${c.green}${sym.arrow} Review:${c.reset} .vibecheck/missions/
356
+ ${c.bold}${sym.arrow} Apply:${c.reset} Upgrade ${sym.arrow} ${PRICING_URL}
357
+ `;
358
+ return msg;
359
+ }
360
+
361
+ // Reality cap hit case
362
+ if (cmd === "reality" && why === "cap_hit") {
363
+ msg += `
364
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
365
+ ${c.yellow}${sym.warning} Preview complete${c.reset}
366
+
367
+ Crawled ${topIssues.length || 5} pages (FREE limit).
368
+ ${tierColor}${tierLabel}${c.reset} unlocks unlimited pages + auth boundary testing.
369
+
370
+ ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
371
+ `;
372
+ return msg;
373
+ }
374
+
375
+ // Generic earned upsell
376
+ if (why) {
377
+ msg += `
378
+ ${c.dim}─────────────────────────────────────────────────────────────${c.reset}
379
+ ${c.cyan}${sym.star} ${why}${c.reset}
380
+
381
+ ${tierColor}${tierLabel}${c.reset} unlocks this feature.
382
+ ${c.bold}${sym.arrow} Upgrade:${c.reset} ${PRICING_URL}
383
+ `;
384
+ }
385
+
386
+ return msg;
387
+ }
388
+
389
+ // ═══════════════════════════════════════════════════════════════════════════════
390
+ // formatBadgeWithheld() - Specific badge denial
391
+ // ═══════════════════════════════════════════════════════════════════════════════
392
+ /**
393
+ * Format badge withheld message with top issues.
394
+ *
395
+ * @param {string} verdict - Current verdict
396
+ * @param {Array} findings - All findings
397
+ * @param {string} currentTier - User's tier
398
+ * @returns {string} Formatted message
399
+ */
400
+ function formatBadgeWithheld(verdict, findings = [], currentTier = "free") {
401
+ const blockers = findings.filter(f => f.severity === "BLOCK").slice(0, 3);
402
+ const topIssues = blockers.length > 0 ? blockers : findings.slice(0, 3);
403
+
404
+ let msg = `
405
+ ${c.yellow}${sym.badge} Badge withheld${c.reset}
406
+
407
+ ${c.bold}Verdict:${c.reset} ${verdict === "BLOCK" ? c.red : c.yellow}${verdict}${c.reset}
408
+ ${c.dim}Badge requires SHIP verdict${c.reset}
409
+ `;
410
+
411
+ if (topIssues.length > 0) {
412
+ msg += `\n ${c.bold}Fix these first:${c.reset}\n`;
413
+ topIssues.forEach((issue, i) => {
414
+ const sev = issue.severity === "BLOCK" ? c.red : c.yellow;
415
+ const label = issue.id || issue.type || issue.ruleId || "issue";
416
+ const desc = issue.message || issue.description || "";
417
+ msg += ` ${c.dim}${i + 1}.${c.reset} ${sev}[${issue.severity}]${c.reset} ${label}${desc ? `: ${c.dim}${desc.slice(0, 50)}${c.reset}` : ""}\n`;
418
+ });
419
+ }
420
+
421
+ // Suggest fix command if available
422
+ const canFix = currentTier !== "free";
423
+ if (canFix) {
424
+ msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix${c.reset}\n`;
425
+ } else {
426
+ msg += `\n ${c.green}${sym.arrow} Run:${c.reset} ${c.cyan}vibecheck fix --plan-only${c.reset} ${c.dim}(view fix plan)${c.reset}\n`;
427
+ }
428
+
429
+ return msg;
430
+ }
431
+
432
+ // ═══════════════════════════════════════════════════════════════════════════════
433
+ // formatNextSteps() - Smart suggestions
434
+ // ═══════════════════════════════════════════════════════════════════════════════
435
+ /**
436
+ * Format next step suggestions based on current command and result.
437
+ *
438
+ * @param {string} cmd - Command just run
439
+ * @param {string} verdict - Result/verdict
440
+ * @param {string} currentTier - User's tier
441
+ * @returns {string} Next step suggestion
442
+ */
443
+ function formatNextSteps(cmd, verdict, currentTier = "free") {
444
+ const steps = [];
445
+
446
+ switch (cmd) {
447
+ case "scan":
448
+ if (currentTier === "free") {
449
+ steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
450
+ } else {
451
+ steps.push({ cmd: "vibecheck ship", desc: "get verdict" });
452
+ steps.push({ cmd: "vibecheck prove --url <url>", desc: "full proof loop" });
453
+ }
454
+ break;
455
+
456
+ case "ship":
457
+ if (verdict === "SHIP") {
458
+ if (currentTier !== "free") {
459
+ steps.push({ cmd: "vibecheck badge", desc: "generate badge" });
460
+ }
461
+ steps.push({ cmd: "vibecheck report", desc: "export report" });
462
+ } else {
463
+ if (currentTier === "free") {
464
+ steps.push({ cmd: "vibecheck fix --plan-only", desc: "view fix plan" });
465
+ } else {
466
+ steps.push({ cmd: "vibecheck fix", desc: "auto-fix issues" });
467
+ }
468
+ }
469
+ break;
470
+
471
+ case "fix":
472
+ steps.push({ cmd: "vibecheck ship", desc: "verify fixes" });
473
+ break;
474
+
475
+ case "reality":
476
+ steps.push({ cmd: "vibecheck ship", desc: "get verdict with runtime proof" });
477
+ break;
478
+ }
479
+
480
+ if (steps.length === 0) return "";
481
+
482
+ let msg = `\n${c.dim}Next:${c.reset} `;
483
+ msg += steps.map(s => `${c.cyan}${s.cmd}${c.reset} ${c.dim}(${s.desc})${c.reset}`).join(" or ");
484
+
485
+ return msg;
486
+ }
487
+
488
+ // ═══════════════════════════════════════════════════════════════════════════════
489
+ // EXPORTS
490
+ // ═══════════════════════════════════════════════════════════════════════════════
491
+ module.exports = {
492
+ // Core formatters
493
+ formatDenied,
494
+ formatDowngrade,
495
+ formatCapHit,
496
+ formatEarnedUpsell,
497
+ formatBadgeWithheld,
498
+ formatNextSteps,
499
+
500
+ // Copy data (for testing/docs)
501
+ DENIAL_COPY,
502
+ CAPS_COPY,
503
+ TIER_LABELS,
504
+ TIER_COLORS,
505
+ PRICING_URL,
506
+
507
+ // Styling (for consistent use)
508
+ c,
509
+ sym,
510
+ };