@vibecheckai/cli 3.0.7 → 3.0.9

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.
@@ -88,12 +88,12 @@ const gradientPink = rgb(255, 105, 180);
88
88
  const gradientOrange = rgb(255, 165, 0);
89
89
 
90
90
  const BANNER = `
91
- ${rgb(0, 200, 255)} ██████╗ ██╗ ██╗ █████╗ ██████╗ ██████╗ ██████╗ █████╗ ██╗██╗ ${c.reset}
92
- ${rgb(30, 180, 255)} ██╔════╝ ██║ ██║██╔══██╗██╔══██╗██╔══██╗██╔══██╗██╔══██╗██║██║ ${c.reset}
93
- ${rgb(60, 160, 255)} ██║ ███╗██║ ██║███████║██████╔╝██║ ██║██████╔╝███████║██║██║ ${c.reset}
94
- ${rgb(90, 140, 255)} ██║ ██║██║ ██║██╔══██║██╔══██╗██║ ██║██╔══██╗██╔══██║██║██║ ${c.reset}
95
- ${rgb(120, 120, 255)} ╚██████╔╝╚██████╔╝██║ ██║██║ ██║██████╔╝██║ ██║██║ ██║██║███████╗${c.reset}
96
- ${rgb(150, 100, 255)} ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝${c.reset}
91
+ ${rgb(0, 200, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${c.reset}
92
+ ${rgb(30, 180, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${c.reset}
93
+ ${rgb(60, 160, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${c.reset}
94
+ ${rgb(90, 140, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${c.reset}
95
+ ${rgb(120, 120, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${c.reset}
96
+ ${rgb(150, 100, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${c.reset}
97
97
 
98
98
  ${c.dim} ┌─────────────────────────────────────────────────────────────────────┐${c.reset}
99
99
  ${c.dim} │${c.reset} ${rgb(255, 255, 255)}${c.bold}Route Integrity${c.reset} ${c.dim}•${c.reset} ${rgb(200, 200, 200)}Security${c.reset} ${c.dim}•${c.reset} ${rgb(150, 150, 150)}Quality${c.reset} ${c.dim}•${c.reset} ${rgb(100, 100, 100)}Ship with Confidence${c.reset} ${c.dim}│${c.reset}
@@ -3,12 +3,72 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const { buildSharePack, findLatestMissionDir } = require("./lib/share-pack");
5
5
 
6
- async function runShare({ repoRoot, missionDir, outputDir, prComment } = {}) {
6
+ function printHelp() {
7
+ console.log(`
8
+ vibecheck share - Generate shareable proof bundles
9
+
10
+ USAGE
11
+ vibecheck share [options]
12
+
13
+ OPTIONS
14
+ --mission-dir <path> Specific mission directory to share
15
+ --output-dir <path> Output directory for share pack
16
+ --pr-comment Print PR comment to stdout
17
+ --help, -h Show this help
18
+
19
+ WHAT IT DOES
20
+ 1. Finds latest mission pack from .vibecheck/missions/
21
+ 2. Generates share bundle with:
22
+ - share.json (machine readable)
23
+ - share.md (human readable)
24
+ - pr_comment.md (GitHub ready)
25
+
26
+ OUTPUT FILES
27
+ .vibecheck/missions/<timestamp>/share/share.json
28
+ .vibecheck/missions/<timestamp>/share/share.md
29
+ .vibecheck/missions/<timestamp>/share/pr_comment.md
30
+
31
+ EXAMPLES
32
+ vibecheck share # Share latest mission
33
+ vibecheck share --pr-comment # Print PR comment body
34
+ vibecheck fix --autopilot --share # Fix then auto-share
35
+ `);
36
+ }
37
+
38
+ async function runShare(argsOrOpts = {}) {
39
+ // Handle array args from CLI
40
+ if (Array.isArray(argsOrOpts)) {
41
+ if (argsOrOpts.includes("--help") || argsOrOpts.includes("-h")) {
42
+ printHelp();
43
+ return 0;
44
+ }
45
+ const getArg = (flags) => {
46
+ for (const f of flags) {
47
+ const idx = argsOrOpts.indexOf(f);
48
+ if (idx !== -1 && idx < argsOrOpts.length - 1) return argsOrOpts[idx + 1];
49
+ }
50
+ return undefined;
51
+ };
52
+ argsOrOpts = {
53
+ missionDir: getArg(["--mission-dir"]),
54
+ outputDir: getArg(["--output-dir"]),
55
+ prComment: argsOrOpts.includes("--pr-comment"),
56
+ };
57
+ }
58
+
59
+ const { repoRoot, missionDir, outputDir, prComment } = argsOrOpts;
7
60
  const root = repoRoot || process.cwd();
8
61
 
9
- const resolvedMissionDir = missionDir
10
- ? (path.isAbsolute(missionDir) ? missionDir : path.join(root, missionDir))
11
- : findLatestMissionDir(root);
62
+ let resolvedMissionDir;
63
+ try {
64
+ resolvedMissionDir = missionDir
65
+ ? (path.isAbsolute(missionDir) ? missionDir : path.join(root, missionDir))
66
+ : findLatestMissionDir(root);
67
+ } catch (e) {
68
+ console.log(`\n❌ Error: ${e.message}\n`);
69
+ console.log(`Run 'vibecheck fix --autopilot' first to generate mission packs.\n`);
70
+ return 1;
71
+ }
12
72
 
13
73
  const res = buildSharePack({
14
74
  repoRoot: root,
@@ -571,6 +571,7 @@ ${c.bold}EXAMPLES${c.reset}
571
571
  translated,
572
572
  results,
573
573
  outputDir,
574
+ results.routeTruth?.findings || [], // Pass modern analyzer findings
574
575
  );
575
576
  printFixResults(fixResults);
576
577
  }
@@ -699,7 +700,7 @@ ${c.bold}EXAMPLES${c.reset}
699
700
  * 2. Updates .gitignore to protect sensitive files
700
701
  * 3. Generates fixes.md with detailed manual fix instructions
701
702
  */
702
- async function runAutoFix(projectPath, translated, results, outputDir) {
703
+ async function runAutoFix(projectPath, translated, results, outputDir, analyzerFindings = []) {
703
704
  const fixResults = {
704
705
  envExampleCreated: false,
705
706
  gitignoreUpdated: false,
@@ -984,6 +985,101 @@ async function runAutoFix(projectPath, translated, results, outputDir) {
984
985
  }
985
986
  }
986
987
 
988
+ // 5. Add modern analyzer findings to AI prompt
989
+ if (analyzerFindings && analyzerFindings.length > 0) {
990
+ aiPrompt += "---\n\n";
991
+ aiPrompt += "## Analyzer Findings\n\n";
992
+ aiPrompt += "The following issues were detected by static analysis:\n\n";
993
+
994
+ // Group by category
995
+ const findingsByCategory = {};
996
+ for (const f of analyzerFindings) {
997
+ const cat = f.category || "Other";
998
+ if (!findingsByCategory[cat]) findingsByCategory[cat] = [];
999
+ findingsByCategory[cat].push(f);
1000
+ }
1001
+
1002
+ for (const [category, findings] of Object.entries(findingsByCategory)) {
1003
+ const blockers = findings.filter(f => f.severity === "BLOCK");
1004
+ const warnings = findings.filter(f => f.severity === "WARN");
1005
+
1006
+ aiPrompt += `### ${category} (${blockers.length} blockers, ${warnings.length} warnings)\n\n`;
1007
+
1008
+ // Show blockers first (limit to 10 per category to avoid huge prompts)
1009
+ const toShow = [...blockers.slice(0, 10), ...warnings.slice(0, 5)];
1010
+
1011
+ for (const finding of toShow) {
1012
+ aiPrompt += `#### Fix ${fixNum}: ${finding.title}\n\n`;
1013
+ aiPrompt += `**Severity:** ${finding.severity === "BLOCK" ? "🔴 BLOCKER" : "🟡 WARNING"}\n\n`;
1014
+
1015
+ if (finding.evidence && finding.evidence.length > 0) {
1016
+ const ev = finding.evidence[0];
1017
+ aiPrompt += `**File:** \`${ev.file}${ev.lines ? `:${ev.lines}` : ""}\`\n\n`;
1018
+ }
1019
+
1020
+ aiPrompt += `**Problem:** ${finding.title}\n\n`;
1021
+
1022
+ if (finding.why) {
1023
+ aiPrompt += `**Why this matters:** ${finding.why}\n\n`;
1024
+ }
1025
+
1026
+ if (finding.fixHints && finding.fixHints.length > 0) {
1027
+ aiPrompt += "**How to fix:**\n";
1028
+ for (const hint of finding.fixHints) {
1029
+ aiPrompt += `- ${hint}\n`;
1030
+ }
1031
+ aiPrompt += "\n";
1032
+ }
1033
+
1034
+ // Add category-specific fix instructions
1035
+ if (category === "MissingRoute" || finding.title?.includes("route")) {
1036
+ aiPrompt += "**Action:**\n";
1037
+ aiPrompt += "1. Check if this API endpoint should exist\n";
1038
+ aiPrompt += "2. If yes, create the route handler in your backend\n";
1039
+ aiPrompt += "3. If no, update the frontend to use the correct endpoint\n";
1040
+ aiPrompt += "4. Ensure the route is registered with your framework (Express, Fastify, Next.js API)\n\n";
1041
+ } else if (category === "EnvGap" || finding.title?.includes("env")) {
1042
+ aiPrompt += "**Action:**\n";
1043
+ aiPrompt += "1. Add the missing environment variable to `.env.example`\n";
1044
+ aiPrompt += "2. Document what the variable is for\n";
1045
+ aiPrompt += "3. Add to your deployment environment\n\n";
1046
+ } else if (category === "GhostAuth" || finding.title?.includes("auth")) {
1047
+ aiPrompt += "**Action:**\n";
1048
+ aiPrompt += "1. Add authentication middleware to this route\n";
1049
+ aiPrompt += "2. Verify the middleware checks for valid session/token\n";
1050
+ aiPrompt += "3. Return 401/403 for unauthorized requests\n\n";
1051
+ } else if (category === "FakeSuccess" || finding.title?.includes("fake")) {
1052
+ aiPrompt += "**Action:**\n";
1053
+ aiPrompt += "1. Ensure success UI only shows after confirmed API success\n";
1054
+ aiPrompt += "2. Check response.ok or status before showing success\n";
1055
+ aiPrompt += "3. Add proper error handling for failed requests\n\n";
1056
+ } else if (category === "StripeWebhook" || finding.title?.includes("Stripe")) {
1057
+ aiPrompt += "**Action:**\n";
1058
+ aiPrompt += "1. Verify Stripe webhook signature using stripe.webhooks.constructEvent()\n";
1059
+ aiPrompt += "2. Add idempotency key handling to prevent duplicate processing\n";
1060
+ aiPrompt += "3. Return 200 only after successful processing\n\n";
1061
+ } else if (category === "PaidSurface" || finding.title?.includes("paid")) {
1062
+ aiPrompt += "**Action:**\n";
1063
+ aiPrompt += "1. Add server-side entitlement check before allowing access\n";
1064
+ aiPrompt += "2. Verify the user's subscription status from your database\n";
1065
+ aiPrompt += "3. Return 403 if user doesn't have access to this feature\n\n";
1066
+ } else if (category === "OwnerMode" || finding.title?.includes("OWNER")) {
1067
+ aiPrompt += "**Action:**\n";
1068
+ aiPrompt += "1. Remove or disable OWNER_MODE/bypass flags in production\n";
1069
+ aiPrompt += "2. Use proper feature flags that require authentication\n";
1070
+ aiPrompt += "3. Never ship code that bypasses auth checks\n\n";
1071
+ }
1072
+
1073
+ fixNum++;
1074
+ }
1075
+
1076
+ if (blockers.length > 10) {
1077
+ aiPrompt += `\n*...and ${blockers.length - 10} more blockers in this category*\n\n`;
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ aiPrompt += "---\n\n";
987
1083
  aiPrompt += "## Verification\n\n";
988
1084
  aiPrompt += "After applying fixes:\n";
989
1085
  aiPrompt += "1. Run `npm run build` or `pnpm build` to check for errors\n";
@@ -130,7 +130,9 @@ ${c.bold}Quick Actions${c.reset}
130
130
  ${lastShip?.meta?.verdict === "SHIP" ? `${c.green}✓ Ready to deploy!${c.reset}` : ""}
131
131
  `);
132
132
 
133
- return lastShip?.meta?.verdict === "SHIP" ? 0 : lastShip?.meta?.verdict === "BLOCK" ? 2 : 1;
133
+ // Exit 0 when showing status successfully (even if no ship yet)
134
+ // Only return non-zero if explicitly checking for BLOCK
135
+ return 0;
134
136
  }
135
137
 
136
138
  module.exports = { runStatus };
@@ -158,12 +158,69 @@ function watchDirectory(dir, callback) {
158
158
  };
159
159
  }
160
160
 
161
- async function runWatch({
162
- repoRoot,
163
- fastifyEntry,
164
- debounceMs = 500,
165
- clearScreen = true
166
- } = {}) {
161
+ function printHelp() {
162
+ console.log(`
163
+ ${c.cyan}${c.bold}vibecheck watch${c.reset} - Continuous Dev Mode
164
+
165
+ ${c.bold}USAGE${c.reset}
166
+ vibecheck watch [options]
167
+
168
+ ${c.bold}OPTIONS${c.reset}
169
+ --fastify-entry <path> Fastify entry file for route extraction
170
+ --debounce <ms> Debounce delay in ms (default: 500)
171
+ --no-clear Don't clear screen between runs
172
+ --help, -h Show this help
173
+
174
+ ${c.bold}WHAT IT DOES${c.reset}
175
+ 1. Runs initial ship analysis
176
+ 2. Watches for file changes in your project
177
+ 3. Re-runs analysis on each change
178
+ 4. Shows live verdict dashboard
179
+
180
+ ${c.bold}WATCHED FILES${c.reset}
181
+ .ts, .tsx, .js, .jsx, .json, .env, .md, .yml, .yaml
182
+
183
+ ${c.bold}IGNORED${c.reset}
184
+ node_modules, .next, .vibecheck, dist, build, .git
185
+
186
+ ${c.bold}EXAMPLES${c.reset}
187
+ vibecheck watch # Start watching
188
+ vibecheck watch --debounce 1000 # 1s debounce
189
+ vibecheck watch --no-clear # Keep history
190
+
191
+ ${c.dim}Press Ctrl+C to stop watching${c.reset}
192
+ `);
193
+ }
194
+
195
+ async function runWatch(argsOrOpts = {}) {
196
+ // Handle array args from CLI
197
+ if (Array.isArray(argsOrOpts)) {
198
+ if (argsOrOpts.includes("--help") || argsOrOpts.includes("-h")) {
199
+ printHelp();
200
+ return 0;
201
+ }
202
+ const getArg = (flags) => {
203
+ for (const f of flags) {
204
+ const idx = argsOrOpts.indexOf(f);
205
+ if (idx !== -1 && idx < argsOrOpts.length - 1) return argsOrOpts[idx + 1];
206
+ }
207
+ return undefined;
208
+ };
209
+ argsOrOpts = {
210
+ repoRoot: process.cwd(),
211
+ fastifyEntry: getArg(["--fastify-entry"]),
212
+ debounceMs: parseInt(getArg(["--debounce"]) || "500", 10),
213
+ clearScreen: !argsOrOpts.includes("--no-clear"),
214
+ };
215
+ }
216
+
217
+ const {
218
+ repoRoot,
219
+ fastifyEntry,
220
+ debounceMs = 500,
221
+ clearScreen = true
222
+ } = argsOrOpts;
223
+
167
224
  const root = repoRoot || process.cwd();
168
225
  let runCount = 0;
169
226
  let lastFile = null;
package/bin/vibecheck.js CHANGED
@@ -230,34 +230,53 @@ function findSimilarCommands(input, commands, maxDistance = 3) {
230
230
  }
231
231
 
232
232
  // ═══════════════════════════════════════════════════════════════════════════════
233
- // COMMAND REGISTRY
233
+ // ENTITLEMENTS (v2 - Single Source of Truth)
234
+ // ═══════════════════════════════════════════════════════════════════════════════
235
+ const entitlements = require("./runners/lib/entitlements-v2");
236
+
237
+ // ═══════════════════════════════════════════════════════════════════════════════
238
+ // COMMAND REGISTRY - Tiers match entitlements-v2.js EXACTLY
234
239
  // ═══════════════════════════════════════════════════════════════════════════════
235
240
  const COMMANDS = {
236
- scan: { description: "Static truth - routes, contracts, secrets, coverage", tier: "free", category: "proof", aliases: ["s", "check"], runner: () => require("./runners/runScan").runScan },
237
- ship: { description: "Verdict engine - SHIP / WARN / BLOCK decision", tier: "free", category: "proof", aliases: ["verdict"], runner: () => require("./runners/runShip").runShip },
238
- reality: { description: "Runtime proof - Playwright clicks every button", tier: "free", category: "proof", aliases: ["r", "test", "e2e"], runner: () => { try { return require("./runners/runReality").runReality; } catch (e) { return async () => { console.error("Reality runner unavailable:", e.message); return 1; }; } } },
239
- fix: { description: "Mission-based repair - targeted fixes with proof", tier: "pro", category: "proof", freeArgs: ["--plan-only", "--help", "-h"], aliases: ["f", "repair"], runner: () => require("./runners/runFix").runFix },
240
- prove: { description: "One command - runs the full loop, fixes issues, proves SHIP", tier: "free", category: "proof", aliases: ["p", "full", "all"], runner: () => require("./runners/runProve").runProve },
241
- report: { description: "HTML artifact - shareable proof of what shipped", tier: "free", category: "proof", aliases: ["html", "artifact"], runner: () => require("./runners/runReport").runReport },
242
- init: { description: "Set up vibecheck (--gha for GitHub Actions)", tier: "free", category: "setup", aliases: ["setup", "configure"], runner: () => require("./runners/runInit").runInit },
243
- install: { description: "Zero-friction onboarding - auto-detects everything", tier: "free", category: "setup", aliases: ["i", "bootstrap"], runner: () => require("./runners/runInstall").runInstall },
244
- doctor: { description: "Environment + project diagnostics", tier: "free", category: "setup", aliases: ["health", "diag"], runner: () => require("./runners/runDoctor").runDoctor },
245
- watch: { description: "Continuous mode - re-runs on file changes", tier: "free", category: "setup", aliases: ["w", "dev"], runner: () => require("./runners/runWatch").runWatch },
246
- ctx: { description: "Generate truthpack - ground truth for AI agents", tier: "free", category: "truth", aliases: ["truthpack", "tp"], subcommands: ["build", "diff", "guard", "sync", "search"], runner: () => require("./runners/runCtx").runCtx },
247
- guard: { description: "Trust boundaries - validates AI claims + prompt injection", tier: "free", category: "truth", aliases: ["validate", "trust"], runner: () => require("./runners/runGuard").runGuard },
248
- context: { description: "Generate AI rules (.cursorrules, .windsurf/rules, etc.)", tier: "free", category: "truth", aliases: ["rules", "ai-rules"], runner: () => require("./runners/runContext").runContext },
249
- mcp: { description: "Start MCP server for AI coding agents", tier: "free", category: "extras", aliases: [], runner: () => require("./runners/runMcp").runMcp },
250
- badge: { description: "Generate ship badge for README/PR", tier: "free", category: "extras", aliases: ["b"], runner: () => require("./runners/runBadge").runBadge },
251
- pr: { description: "Generate PR comment with findings", tier: "free", category: "extras", aliases: ["pull-request"], runner: () => require("./runners/runPR").runPR },
252
- labs: { description: "Experimental features", tier: "free", category: "extras", aliases: ["experimental", "beta"], runner: () => require("./runners/runLabs").runLabs },
253
- gate: { description: "CI/CD gate - blocks deploys on failures", tier: "starter", category: "ci", aliases: ["ci", "block"], scope: "gate:ci", runner: () => require("./runners/runGate").runGate },
254
- "ai-test": { description: "AI Agent testing - autonomous test generation", tier: "pro", category: "automation", aliases: ["ai", "agent"], scope: "ai:agent", runner: () => require("./runners/runAIAgent").runAIAgent },
241
+ // PROOF LOOP
242
+ scan: { description: "Static analysis - routes, secrets, contracts", tier: "free", category: "proof", aliases: ["s", "check"], runner: () => require("./runners/runScan").runScan },
243
+ ship: { description: "Verdict engine - SHIP / WARN / BLOCK", tier: "free", category: "proof", aliases: ["verdict"], caps: "static-only on FREE", runner: () => require("./runners/runShip").runShip },
244
+ reality: { description: "Runtime proof - Playwright clicks every button", tier: "free", category: "proof", aliases: ["r", "test", "e2e"], caps: "preview mode on FREE (5 pages, no auth)", runner: () => { try { return require("./runners/runReality").runReality; } catch (e) { return async () => { console.error("Reality runner unavailable:", e.message); return 1; }; } } },
245
+ prove: { description: "Full proof loop - ctx reality ship fix", tier: "pro", category: "proof", aliases: ["p", "full", "all"], runner: () => require("./runners/runProve").runProve },
246
+ fix: { description: "AI-powered auto-fix", tier: "free", category: "proof", caps: "--plan-only on FREE/STARTER", aliases: ["f", "repair"], runner: () => require("./runners/runFix").runFix },
247
+ report: { description: "Generate HTML/MD/SARIF reports", tier: "free", category: "proof", caps: "HTML/MD only on FREE", aliases: ["html", "artifact"], runner: () => require("./runners/runReport").runReport },
248
+
249
+ // SETUP & DX
250
+ install: { description: "Zero-friction onboarding", tier: "free", category: "setup", aliases: ["i", "bootstrap"], runner: () => require("./runners/runInstall").runInstall },
251
+ init: { description: "Project setup wizard", tier: "free", category: "setup", aliases: ["setup", "configure"], runner: () => require("./runners/runInit").runInit },
252
+ doctor: { description: "Environment diagnostics", tier: "free", category: "setup", aliases: ["health", "diag"], runner: () => require("./runners/runDoctor").runDoctor },
253
+ status: { description: "Project health dashboard", tier: "free", category: "setup", aliases: ["st"], runner: () => require("./runners/runStatus").runStatus },
254
+ watch: { description: "Continuous mode - re-runs on changes", tier: "free", category: "setup", aliases: ["w", "dev"], runner: () => require("./runners/runWatch").runWatch },
255
+ launch: { description: "Pre-launch checklist wizard", tier: "starter", category: "setup", aliases: ["checklist", "preflight"], runner: () => require("./runners/runLaunch").runLaunch },
256
+
257
+ // AI TRUTH
258
+ ctx: { description: "Generate truthpack for AI agents", tier: "free", category: "truth", aliases: ["truthpack", "tp"], subcommands: ["build", "diff", "guard", "sync", "search"], runner: () => require("./runners/runCtx").runCtx },
259
+ guard: { description: "Validate AI claims against truth", tier: "free", category: "truth", aliases: ["validate", "trust"], runner: () => require("./runners/runGuard").runGuard },
260
+ context: { description: "Generate .cursorrules, .windsurf/rules", tier: "free", category: "truth", aliases: ["rules", "ai-rules"], runner: () => require("./runners/runContext").runContext },
261
+ mdc: { description: "Generate MDC specifications", tier: "free", category: "truth", aliases: [], runner: () => require("./runners/runMdc").runMdc },
262
+
263
+ // CI & COLLABORATION (STARTER+)
264
+ gate: { description: "CI/CD gate - blocks deploys on failures", tier: "starter", category: "ci", aliases: ["ci", "block"], runner: () => require("./runners/runGate").runGate },
265
+ pr: { description: "Generate PR comment with findings", tier: "starter", category: "ci", aliases: ["pull-request"], runner: () => require("./runners/runPR").runPR },
266
+ badge: { description: "Generate ship badge for README", tier: "starter", category: "ci", aliases: ["b"], runner: () => require("./runners/runBadge").runBadge },
267
+
268
+ // AUTOMATION (STARTER+/PRO)
269
+ mcp: { description: "Start MCP server for AI IDEs", tier: "starter", category: "automation", aliases: [], runner: () => require("./runners/runMcp").runMcp },
270
+ share: { description: "Generate share pack for PR/docs", tier: "pro", category: "automation", aliases: [], runner: () => require("./runners/runShare").runShare },
271
+ "ai-test": { description: "AI autonomous test generation", tier: "pro", category: "automation", aliases: ["ai", "agent"], runner: () => require("./runners/runAIAgent").runAIAgent },
272
+
273
+ // ACCOUNT (always free)
255
274
  login: { description: "Authenticate with API key", tier: "free", category: "account", aliases: ["auth", "signin"], runner: () => require("./runners/runAuth").runLogin, skipAuth: true },
256
275
  logout: { description: "Remove stored credentials", tier: "free", category: "account", aliases: ["signout"], runner: () => require("./runners/runAuth").runLogout, skipAuth: true },
257
276
  whoami: { description: "Show current user and plan", tier: "free", category: "account", aliases: ["me", "user"], runner: () => require("./runners/runAuth").runWhoami, skipAuth: true },
258
- mdc: { description: "Generate MDC specifications", tier: "free", category: "truth", aliases: [], runner: () => require("./runners/runMdc").runMdc },
259
- status: { description: "Project status dashboard", tier: "free", category: "setup", aliases: ["st"], runner: () => require("./runners/runStatus").runStatus },
260
- share: { description: "Generate share pack for PR/docs", tier: "free", category: "extras", aliases: [], runner: () => require("./runners/runShare").runShare },
277
+
278
+ // EXTRAS
279
+ labs: { description: "Experimental features", tier: "free", category: "extras", aliases: ["experimental", "beta"], runner: () => require("./runners/runLabs").runLabs },
261
280
  };
262
281
 
263
282
  const ALIAS_MAP = {};
@@ -271,27 +290,62 @@ function getRunner(cmd) {
271
290
  }
272
291
 
273
292
  // ═══════════════════════════════════════════════════════════════════════════════
274
- // AUTH & ACCESS CONTROL
293
+ // AUTH & ACCESS CONTROL (uses entitlements-v2 - NO BYPASS ALLOWED)
275
294
  // ═══════════════════════════════════════════════════════════════════════════════
276
295
  let authModule = null;
277
296
  function getAuthModule() { if (!authModule) authModule = require("./runners/lib/auth"); return authModule; }
278
297
 
279
- async function checkCommandAccess(cmd, args, entitlements) {
298
+ /**
299
+ * Check command access using entitlements-v2 module.
300
+ * NO OWNER MODE. NO ENV VAR BYPASS. NO OFFLINE ESCALATION.
301
+ */
302
+ async function checkCommandAccess(cmd, args, authInfo) {
280
303
  const def = COMMANDS[cmd];
281
304
  if (!def) return { allowed: true };
282
- if (def.tier === "free") return { allowed: true, tier: "free" };
283
- if (!entitlements) return { allowed: false, tier: def.tier, reason: formatAccessDenied(cmd, def.tier, null) };
284
- const hasAccess = entitlements.scopes?.includes(def.scope) || entitlements.scopes?.includes("*");
285
- if (!hasAccess) return { allowed: false, tier: def.tier, reason: formatAccessDenied(cmd, def.tier, entitlements.plan) };
286
- return { allowed: true, tier: def.tier };
305
+
306
+ // Use centralized entitlements enforcement
307
+ const result = await entitlements.enforce(cmd, {
308
+ apiKey: authInfo?.key,
309
+ projectPath: process.cwd(),
310
+ silent: true, // We'll handle messaging ourselves
311
+ });
312
+
313
+ if (result.allowed) {
314
+ return {
315
+ allowed: true,
316
+ tier: result.tier,
317
+ downgrade: result.downgrade,
318
+ limits: result.limits,
319
+ caps: result.caps,
320
+ };
321
+ }
322
+
323
+ // Not allowed - return with proper exit code
324
+ return {
325
+ allowed: false,
326
+ tier: result.tier,
327
+ requiredTier: result.requiredTier,
328
+ exitCode: result.exitCode,
329
+ reason: formatAccessDenied(cmd, result.requiredTier, result.tier),
330
+ };
287
331
  }
288
332
 
289
- function formatAccessDenied(cmd, requiredTier, currentPlan) {
333
+ function formatAccessDenied(cmd, requiredTier, currentTier) {
290
334
  const tierColors = { starter: c.cyan, pro: c.magenta, enterprise: c.yellow };
291
335
  const tierColor = tierColors[requiredTier] || c.white;
292
- let msg = `${c.yellow}${cmd}${c.reset} requires a ${tierColor}${requiredTier.toUpperCase()}${c.reset} plan.\n\n`;
293
- if (!currentPlan) { msg += ` Run ${c.cyan}vibecheck login${c.reset} to authenticate.\n Get your API key at: ${c.cyan}https://vibecheckai.dev/settings/keys${c.reset}`; }
294
- else { msg += ` Your current plan: ${c.yellow}${currentPlan.toUpperCase()}${c.reset}\n Upgrade at: ${c.cyan}https://vibecheckai.dev/pricing${c.reset}`; }
336
+ const tierLabel = entitlements.getTierLabel(requiredTier);
337
+ const currentLabel = entitlements.getTierLabel(currentTier);
338
+
339
+ let msg = `\n${c.red}${c.bold}⛔ Feature Not Available${c.reset}\n\n`;
340
+ msg += ` ${c.yellow}${cmd}${c.reset} requires ${tierColor}${tierLabel}${c.reset} plan.\n`;
341
+ msg += ` Your current plan: ${c.dim}${currentLabel}${c.reset}\n\n`;
342
+
343
+ if (currentTier === "free") {
344
+ msg += ` ${c.cyan}Get started:${c.reset} vibecheck login\n`;
345
+ }
346
+ msg += ` ${c.cyan}Upgrade at:${c.reset} https://vibecheckai.dev/pricing\n`;
347
+ msg += `\n ${c.dim}Exit code: ${entitlements.EXIT_FEATURE_NOT_ALLOWED}${c.reset}\n`;
348
+
295
349
  return msg;
296
350
  }
297
351
 
@@ -309,40 +363,70 @@ ${c.dim}${sym.boxBottomLeft}${sym.boxHorizontal.repeat(60)}${sym.boxBottomRight}
309
363
 
310
364
  function printHelp() {
311
365
  printBanner();
366
+
367
+ // Categories ordered as specified
368
+ const categoryOrder = ["proof", "setup", "truth", "ci", "automation", "account", "extras"];
312
369
  const categories = {
313
- proof: { name: "THE PROOF LOOP", color: c.green, icon: sym.shield },
314
- setup: { name: "SETUP & DIAGNOSTICS", color: c.yellow, icon: sym.gear },
315
- truth: { name: "TRUTH SYSTEM", color: c.magenta, icon: sym.lightning },
316
- extras: { name: "EXTRAS", color: c.dim, icon: sym.star },
317
- ci: { name: "CI/CD", color: c.cyan, icon: sym.rocket },
370
+ proof: { name: "PROOF LOOP", color: c.green, icon: sym.shield },
371
+ setup: { name: "SETUP & DX", color: c.yellow, icon: sym.gear },
372
+ truth: { name: "AI TRUTH", color: c.magenta, icon: sym.lightning },
373
+ ci: { name: "CI & COLLABORATION", color: c.cyan, icon: sym.rocket },
318
374
  automation: { name: "AUTOMATION", color: c.blue, icon: sym.fire },
319
375
  account: { name: "ACCOUNT", color: c.dim, icon: sym.key },
376
+ extras: { name: "EXTRAS", color: c.dim, icon: sym.star },
320
377
  };
378
+
379
+ // Group commands
321
380
  const grouped = {};
322
381
  for (const [cmd, def] of Object.entries(COMMANDS)) {
323
382
  const cat = def.category || "extras";
324
383
  if (!grouped[cat]) grouped[cat] = [];
325
384
  grouped[cat].push({ cmd, ...def });
326
385
  }
327
- for (const [catKey, commands] of Object.entries(grouped)) {
328
- const cat = categories[catKey] || { name: catKey.toUpperCase(), color: c.white, icon: sym.bullet };
386
+
387
+ // Print in order
388
+ for (const catKey of categoryOrder) {
389
+ const commands = grouped[catKey];
390
+ if (!commands || commands.length === 0) continue;
391
+
392
+ const cat = categories[catKey];
329
393
  console.log(`\n${cat.color}${cat.icon} ${cat.name}${c.reset}\n`);
330
- for (const { cmd, description, tier, aliases } of commands) {
331
- const tierBadge = tier === "starter" ? `${c.cyan}[STARTER]${c.reset} ` : tier === "pro" ? `${c.magenta}[PRO]${c.reset} ` : "";
332
- const aliasStr = aliases?.length ? `${c.dim}(${aliases.join(", ")})${c.reset}` : "";
333
- console.log(` ${c.cyan}${cmd.padEnd(12)}${c.reset} ${tierBadge}${description} ${aliasStr}`);
394
+
395
+ for (const { cmd, description, tier, aliases, caps } of commands) {
396
+ // Tier badge with color
397
+ let tierBadge = "";
398
+ if (tier === "free") {
399
+ tierBadge = `${c.green}[FREE]${c.reset} `;
400
+ } else if (tier === "starter") {
401
+ tierBadge = `${c.cyan}[STARTER]${c.reset} `;
402
+ } else if (tier === "pro") {
403
+ tierBadge = `${c.magenta}[PRO]${c.reset} `;
404
+ }
405
+
406
+ // Caps info (e.g., "preview mode on FREE")
407
+ const capsStr = caps ? `${c.dim}(${caps})${c.reset}` : "";
408
+
409
+ console.log(` ${c.cyan}${cmd.padEnd(12)}${c.reset} ${tierBadge}${description} ${capsStr}`);
334
410
  }
335
411
  }
412
+
336
413
  console.log(`
337
414
  ${c.dim}${sym.boxHorizontal.repeat(64)}${c.reset}
338
415
 
416
+ ${c.green}TIERS${c.reset}
417
+
418
+ ${c.green}FREE${c.reset} $0 scan, ship, ctx, doctor, report (HTML/MD)
419
+ ${c.cyan}STARTER${c.reset} $29/mo + gate, launch, pr, badge, mcp, reality full
420
+ ${c.magenta}PRO${c.reset} $99/mo + prove, fix apply, share, ai-test, compliance
421
+
339
422
  ${c.green}QUICK START${c.reset}
340
423
 
341
424
  ${c.bold}"Check my repo"${c.reset} ${c.cyan}vibecheck scan${c.reset}
342
- ${c.bold}"Can I ship?"${c.reset} ${c.cyan}vibecheck prove --url http://localhost:3000${c.reset}
343
- ${c.bold}"Why did it fail?"${c.reset} ${c.cyan}vibecheck report${c.reset} ${c.dim}(then: vibecheck fix)${c.reset}
425
+ ${c.bold}"Can I ship?"${c.reset} ${c.cyan}vibecheck ship${c.reset}
426
+ ${c.bold}"Full proof loop"${c.reset} ${c.cyan}vibecheck prove --url http://localhost:3000${c.reset} ${c.magenta}[PRO]${c.reset}
344
427
 
345
428
  ${c.dim}Run 'vibecheck <command> --help' for command-specific help.${c.reset}
429
+ ${c.dim}Pricing: https://vibecheckai.dev/pricing${c.reset}
346
430
  `);
347
431
  }
348
432
 
@@ -419,20 +503,35 @@ async function main() {
419
503
  }
420
504
 
421
505
  const cmdDef = COMMANDS[cmd];
422
- let authInfo = { key: null, entitlements: null };
506
+ let authInfo = { key: null };
423
507
 
424
508
  if (!cmdDef.skipAuth) {
425
509
  const auth = getAuthModule();
426
510
  const { key } = auth.getApiKey();
511
+ authInfo.key = key;
427
512
 
428
- if (key && cmdDef.tier !== "free") {
429
- try { authInfo.key = key; authInfo.entitlements = await auth.getEntitlements(key); } catch (e) { if (config.verbose) console.log(`${c.yellow}${sym.warning}${c.reset} ${c.dim}Could not verify credentials${c.reset}`); }
430
- } else { authInfo.key = key; }
513
+ // Use entitlements-v2 for access control (NO BYPASS)
514
+ const access = await checkCommandAccess(cmd, cmdArgs, authInfo);
515
+
516
+ if (!access.allowed) {
517
+ console.log(access.reason);
518
+ // Use proper exit code: 3 = feature not allowed
519
+ process.exit(access.exitCode || entitlements.EXIT_FEATURE_NOT_ALLOWED);
520
+ }
521
+
522
+ // Show downgrade notice if applicable
523
+ if (access.downgrade && !config.quiet) {
524
+ console.log(`${c.yellow}${sym.warning}${c.reset} Running in ${c.yellow}${access.downgrade}${c.reset} mode (upgrade for full access)`);
525
+ }
526
+
527
+ // Show tier badge
528
+ if (!config.quiet) {
529
+ if (access.tier === "starter") console.log(`${c.cyan}${sym.arrowRight} STARTER${c.reset} ${c.dim}feature${c.reset}`);
530
+ else if (access.tier === "pro") console.log(`${c.magenta}${sym.arrowRight} PRO${c.reset} ${c.dim}feature${c.reset}`);
531
+ }
431
532
 
432
- const access = await checkCommandAccess(cmd, cmdArgs, authInfo.entitlements);
433
- if (!access.allowed) { console.log(`\n${c.red}${sym.error} Access Denied${c.reset}\n`); console.log(access.reason); console.log(""); process.exit(1); }
434
- if (access.tier === "starter" && !config.quiet) console.log(`${c.cyan}${sym.arrowRight} STARTER${c.reset} ${c.dim}feature${c.reset}`);
435
- else if (access.tier === "pro" && !config.quiet) console.log(`${c.magenta}${sym.arrowRight} PRO${c.reset} ${c.dim}feature${c.reset}`);
533
+ // Attach access info for runners to use
534
+ authInfo.access = access;
436
535
  }
437
536
 
438
537
  state.runCount++; state.lastRun = Date.now();
@@ -447,20 +546,20 @@ async function main() {
447
546
  const context = { repoRoot: process.cwd(), config, state, authInfo, version: VERSION, isCI: isCI() };
448
547
 
449
548
  switch (cmd) {
450
- case "prove": exitCode = await runner({ ...context, url: getArgValue(cmdArgs, ["--url", "-u"]), auth: getArgValue(cmdArgs, ["--auth"]), storageState: getArgValue(cmdArgs, ["--storage-state"]), fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]), maxFixRounds: parseInt(getArgValue(cmdArgs, ["--max-fix-rounds"]) || "3", 10), skipReality: cmdArgs.includes("--skip-reality"), skipFix: cmdArgs.includes("--skip-fix"), headed: cmdArgs.includes("--headed"), danger: cmdArgs.includes("--danger") }); break;
451
- case "reality": exitCode = await runner({ ...context, url: getArgValue(cmdArgs, ["--url", "-u"]), auth: getArgValue(cmdArgs, ["--auth"]), storageState: getArgValue(cmdArgs, ["--storage-state"]), saveStorageState: getArgValue(cmdArgs, ["--save-storage-state"]), truthpack: getArgValue(cmdArgs, ["--truthpack"]), verifyAuth: cmdArgs.includes("--verify-auth"), headed: cmdArgs.includes("--headed"), danger: cmdArgs.includes("--danger") }); break;
452
- case "watch": exitCode = await runner({ ...context, fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]), debounceMs: parseInt(getArgValue(cmdArgs, ["--debounce"]) || "500", 10), clearScreen: !cmdArgs.includes("--no-clear") }); break;
549
+ case "prove": exitCode = await runner(cmdArgs); break;
550
+ case "reality": exitCode = await runner(cmdArgs); break;
551
+ case "watch": exitCode = await runner(cmdArgs); break;
453
552
  case "ctx": case "truthpack":
454
553
  if (cmdArgs[0] === "sync") { const { runCtxSync } = require("./runners/runCtxSync"); exitCode = await runCtxSync({ ...context, fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]) }); }
455
554
  else if (cmdArgs[0] === "guard") { const { runCtxGuard } = require("./runners/runCtxGuard"); exitCode = await runCtxGuard.main(cmdArgs.slice(1)); }
456
555
  else if (cmdArgs[0] === "diff") { const { main: ctxDiffMain } = require("./runners/runCtxDiff"); exitCode = await ctxDiffMain(cmdArgs.slice(1)); }
457
556
  else if (cmdArgs[0] === "search") { const { runContext } = require("./runners/runContext"); exitCode = await runContext(["--search", ...cmdArgs.slice(1)]); }
458
- else { exitCode = await runner({ ...context, fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]), print: cmdArgs.includes("--print") }); }
557
+ else { exitCode = await runner(cmdArgs); }
459
558
  break;
460
- case "install": exitCode = await runner(context); break;
559
+ case "install": exitCode = await runner(cmdArgs); break;
461
560
  case "status": exitCode = await runner({ ...context, json: cmdArgs.includes("--json") }); break;
462
- case "pr": exitCode = await runner({ ...context, fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]), out: getArgValue(cmdArgs, ["--out"]), failOnWarn: cmdArgs.includes("--fail-on-warn"), maxFindings: parseInt(getArgValue(cmdArgs, ["--max-findings"]) || "12", 10) }); break;
463
- case "share": exitCode = await runner({ ...context, missionDir: getArgValue(cmdArgs, ["--mission-dir"]), outputDir: getArgValue(cmdArgs, ["--output-dir"]), prComment: cmdArgs.includes("--pr-comment") }); break;
561
+ case "pr": exitCode = await runner(cmdArgs); break;
562
+ case "share": exitCode = await runner(cmdArgs); break;
464
563
  default: exitCode = await runner(cmdArgs);
465
564
  }
466
565
  } catch (error) { console.error(formatError(error, config)); exitCode = 1; }