@vibecheckai/cli 3.2.4 → 3.2.6

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 (123) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  4. package/bin/runners/lib/api-client.js +269 -0
  5. package/bin/runners/lib/auth-truth.js +193 -193
  6. package/bin/runners/lib/backup.js +62 -62
  7. package/bin/runners/lib/billing.js +107 -107
  8. package/bin/runners/lib/claims.js +118 -118
  9. package/bin/runners/lib/cli-ui.js +540 -540
  10. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  11. package/bin/runners/lib/contracts/env-contract.js +181 -181
  12. package/bin/runners/lib/contracts/external-contract.js +206 -206
  13. package/bin/runners/lib/contracts/guard.js +168 -168
  14. package/bin/runners/lib/contracts/index.js +89 -89
  15. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  16. package/bin/runners/lib/contracts/route-contract.js +199 -199
  17. package/bin/runners/lib/contracts.js +804 -804
  18. package/bin/runners/lib/detect.js +89 -89
  19. package/bin/runners/lib/doctor/autofix.js +254 -254
  20. package/bin/runners/lib/doctor/index.js +37 -37
  21. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  22. package/bin/runners/lib/doctor/modules/index.js +46 -46
  23. package/bin/runners/lib/doctor/modules/network.js +250 -250
  24. package/bin/runners/lib/doctor/modules/project.js +312 -312
  25. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  26. package/bin/runners/lib/doctor/modules/security.js +348 -348
  27. package/bin/runners/lib/doctor/modules/system.js +213 -213
  28. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  29. package/bin/runners/lib/doctor/reporter.js +262 -262
  30. package/bin/runners/lib/doctor/service.js +262 -262
  31. package/bin/runners/lib/doctor/types.js +113 -113
  32. package/bin/runners/lib/doctor/ui.js +263 -263
  33. package/bin/runners/lib/doctor-v2.js +608 -608
  34. package/bin/runners/lib/drift.js +425 -425
  35. package/bin/runners/lib/enforcement.js +72 -72
  36. package/bin/runners/lib/enterprise-detect.js +603 -603
  37. package/bin/runners/lib/enterprise-init.js +942 -942
  38. package/bin/runners/lib/env-resolver.js +417 -417
  39. package/bin/runners/lib/env-template.js +66 -66
  40. package/bin/runners/lib/env.js +189 -189
  41. package/bin/runners/lib/extractors/client-calls.js +990 -990
  42. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  43. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  44. package/bin/runners/lib/extractors/index.js +363 -363
  45. package/bin/runners/lib/extractors/next-routes.js +524 -524
  46. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  47. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  48. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  49. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  50. package/bin/runners/lib/findings-schema.js +281 -281
  51. package/bin/runners/lib/firewall-prompt.js +50 -50
  52. package/bin/runners/lib/graph/graph-builder.js +265 -265
  53. package/bin/runners/lib/graph/html-renderer.js +413 -413
  54. package/bin/runners/lib/graph/index.js +32 -32
  55. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  56. package/bin/runners/lib/graph/static-extractor.js +518 -518
  57. package/bin/runners/lib/html-report.js +650 -650
  58. package/bin/runners/lib/llm.js +75 -75
  59. package/bin/runners/lib/meter.js +61 -61
  60. package/bin/runners/lib/missions/evidence.js +126 -126
  61. package/bin/runners/lib/patch.js +40 -40
  62. package/bin/runners/lib/permissions/auth-model.js +213 -213
  63. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  64. package/bin/runners/lib/permissions/index.js +45 -45
  65. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  66. package/bin/runners/lib/pkgjson.js +28 -28
  67. package/bin/runners/lib/policy.js +295 -295
  68. package/bin/runners/lib/preflight.js +142 -142
  69. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  70. package/bin/runners/lib/reality/index.js +318 -318
  71. package/bin/runners/lib/reality/request-hashing.js +416 -416
  72. package/bin/runners/lib/reality/request-mapper.js +453 -453
  73. package/bin/runners/lib/reality/safety-rails.js +463 -463
  74. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  75. package/bin/runners/lib/reality/toast-detector.js +393 -393
  76. package/bin/runners/lib/reality-findings.js +84 -84
  77. package/bin/runners/lib/receipts.js +179 -179
  78. package/bin/runners/lib/redact.js +29 -29
  79. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  80. package/bin/runners/lib/replay/index.js +263 -263
  81. package/bin/runners/lib/replay/player.js +348 -348
  82. package/bin/runners/lib/replay/recorder.js +331 -331
  83. package/bin/runners/lib/report.js +135 -135
  84. package/bin/runners/lib/route-detection.js +1140 -1140
  85. package/bin/runners/lib/sandbox/index.js +59 -59
  86. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  87. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  88. package/bin/runners/lib/sandbox/worktree.js +174 -174
  89. package/bin/runners/lib/schema-validator.js +350 -350
  90. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  91. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  92. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  93. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  94. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  95. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  96. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  97. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  98. package/bin/runners/lib/schemas/validator.js +438 -438
  99. package/bin/runners/lib/score-history.js +282 -282
  100. package/bin/runners/lib/share-pack.js +239 -239
  101. package/bin/runners/lib/snippets.js +67 -67
  102. package/bin/runners/lib/upsell.js +510 -510
  103. package/bin/runners/lib/usage.js +153 -153
  104. package/bin/runners/lib/validate-patch.js +156 -156
  105. package/bin/runners/lib/verdict-engine.js +628 -628
  106. package/bin/runners/reality/engine.js +917 -917
  107. package/bin/runners/reality/flows.js +122 -122
  108. package/bin/runners/reality/report.js +378 -378
  109. package/bin/runners/reality/session.js +193 -193
  110. package/bin/runners/runAgent.d.ts +5 -0
  111. package/bin/runners/runFirewall.d.ts +5 -0
  112. package/bin/runners/runFirewallHook.d.ts +5 -0
  113. package/bin/runners/runGuard.js +168 -168
  114. package/bin/runners/runScan.js +82 -0
  115. package/bin/runners/runTruth.d.ts +5 -0
  116. package/bin/vibecheck.js +45 -20
  117. package/mcp-server/index.js +85 -0
  118. package/mcp-server/lib/api-client.js +269 -0
  119. package/mcp-server/package.json +1 -1
  120. package/mcp-server/tier-auth.js +173 -113
  121. package/mcp-server/tools/index.js +72 -72
  122. package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
  123. package/package.json +1 -1
@@ -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";
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";
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
+ };