@vibecheckai/cli 3.0.8 → 3.0.10

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.
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * vibecheck watch - Continuous Dev Mode
3
3
  *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * ENTERPRISE EDITION - World-Class Terminal Experience
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ *
4
8
  * Watches for file changes and re-runs ship automatically.
5
9
  * Shows a persistent status line with current verdict.
6
10
  * Perfect for development workflow.
@@ -12,16 +16,92 @@ const fs = require("fs");
12
16
  const path = require("path");
13
17
  const { shipCore } = require("./runShip");
14
18
 
19
+ // ═══════════════════════════════════════════════════════════════════════════════
20
+ // ADVANCED TERMINAL - ANSI CODES & UTILITIES
21
+ // ═══════════════════════════════════════════════════════════════════════════════
22
+
15
23
  const c = {
16
- reset: "\x1b[0m",
17
- bold: "\x1b[1m",
18
- dim: "\x1b[2m",
19
- red: "\x1b[31m",
20
- green: "\x1b[32m",
21
- yellow: "\x1b[33m",
22
- cyan: "\x1b[36m",
23
- blue: "\x1b[34m",
24
- clear: "\x1b[2J\x1b[H",
24
+ reset: '\x1b[0m',
25
+ bold: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ italic: '\x1b[3m',
28
+ underline: '\x1b[4m',
29
+ red: '\x1b[31m',
30
+ green: '\x1b[32m',
31
+ yellow: '\x1b[33m',
32
+ blue: '\x1b[34m',
33
+ magenta: '\x1b[35m',
34
+ cyan: '\x1b[36m',
35
+ white: '\x1b[37m',
36
+ gray: '\x1b[90m',
37
+ clear: '\x1b[2J\x1b[H',
38
+ clearLine: '\x1b[2K',
39
+ hideCursor: '\x1b[?25l',
40
+ showCursor: '\x1b[?25h',
41
+ };
42
+
43
+ const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
44
+ const bgRgb = (r, g, b) => `\x1b[48;2;${r};${g};${b}m`;
45
+
46
+ const colors = {
47
+ gradient1: rgb(150, 100, 255),
48
+ gradient2: rgb(130, 80, 255),
49
+ gradient3: rgb(110, 60, 255),
50
+ shipGreen: rgb(0, 255, 150),
51
+ warnAmber: rgb(255, 200, 0),
52
+ blockRed: rgb(255, 80, 80),
53
+ accent: rgb(150, 100, 255),
54
+ muted: rgb(120, 120, 140),
55
+ watching: rgb(100, 200, 255),
56
+ };
57
+
58
+ // ═══════════════════════════════════════════════════════════════════════════════
59
+ // PREMIUM BANNER
60
+ // ═══════════════════════════════════════════════════════════════════════════════
61
+
62
+ const WATCH_BANNER = `
63
+ ${rgb(150, 100, 255)} ██╗ ██╗ █████╗ ████████╗ ██████╗██╗ ██╗${c.reset}
64
+ ${rgb(130, 80, 255)} ██║ ██║██╔══██╗╚══██╔══╝██╔════╝██║ ██║${c.reset}
65
+ ${rgb(110, 60, 255)} ██║ █╗ ██║███████║ ██║ ██║ ███████║${c.reset}
66
+ ${rgb(90, 40, 255)} ██║███╗██║██╔══██║ ██║ ██║ ██╔══██║${c.reset}
67
+ ${rgb(70, 20, 255)} ╚███╔███╔╝██║ ██║ ██║ ╚██████╗██║ ██║${c.reset}
68
+ ${rgb(50, 0, 255)} ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝${c.reset}
69
+ `;
70
+
71
+ const BANNER_FULL = `
72
+ ${rgb(150, 100, 255)} ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗${c.reset}
73
+ ${rgb(140, 90, 255)} ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝${c.reset}
74
+ ${rgb(130, 80, 255)} ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝ ${c.reset}
75
+ ${rgb(110, 60, 255)} ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ${c.reset}
76
+ ${rgb(90, 40, 255)} ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗${c.reset}
77
+ ${rgb(70, 20, 255)} ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝${c.reset}
78
+
79
+ ${c.dim} ┌─────────────────────────────────────────────────────────────────────┐${c.reset}
80
+ ${c.dim} │${c.reset} ${rgb(150, 100, 255)}👁️${c.reset} ${c.bold}WATCH${c.reset} ${c.dim}•${c.reset} ${rgb(200, 200, 200)}Live Reload${c.reset} ${c.dim}•${c.reset} ${rgb(150, 150, 150)}Auto Analysis${c.reset} ${c.dim}•${c.reset} ${rgb(100, 100, 100)}Dev Mode${c.reset} ${c.dim}│${c.reset}
81
+ ${c.dim} └─────────────────────────────────────────────────────────────────────┘${c.reset}
82
+ `;
83
+
84
+ const BOX = {
85
+ topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯',
86
+ horizontal: '─', vertical: '│',
87
+ dTopLeft: '╔', dTopRight: '╗', dBottomLeft: '╚', dBottomRight: '╝',
88
+ dHorizontal: '═', dVertical: '║',
89
+ };
90
+
91
+ const ICONS = {
92
+ eye: '👁️',
93
+ check: '✓',
94
+ cross: '✗',
95
+ warning: '⚠',
96
+ arrow: '→',
97
+ bullet: '•',
98
+ ship: '🚀',
99
+ clock: '⏱',
100
+ file: '📄',
101
+ sparkle: '✨',
102
+ lightning: '⚡',
103
+ refresh: '🔄',
104
+ watching: '◉',
25
105
  };
26
106
 
27
107
  const IGNORE_PATTERNS = [
@@ -58,29 +138,63 @@ function formatDuration(ms) {
58
138
  return `${(ms / 1000).toFixed(1)}s`;
59
139
  }
60
140
 
141
+ function progressBar(percent, width = 20) {
142
+ const filled = Math.round((percent / 100) * width);
143
+ const empty = width - filled;
144
+ const color = percent >= 80 ? colors.shipGreen : percent >= 50 ? colors.warnAmber : colors.blockRed;
145
+ return `${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
146
+ }
147
+
148
+ function printDivider(char = '─', width = 62) {
149
+ console.log(` ${c.dim}${char.repeat(width)}${c.reset}`);
150
+ }
151
+
61
152
  function printStatus({ verdict, findings, duration, lastFile, runCount }) {
62
- const verdictColor = verdict === "SHIP" ? c.green : verdict === "WARN" ? c.yellow : c.red;
153
+ const verdictColor = verdict === "SHIP" ? colors.shipGreen : verdict === "WARN" ? colors.warnAmber : colors.blockRed;
63
154
  const blocks = findings.filter(f => f.severity === "BLOCK").length;
64
155
  const warns = findings.filter(f => f.severity === "WARN").length;
65
-
66
- console.log(`
67
- ${c.cyan}${c.bold}╔════════════════════════════════════════════════════════════╗
68
- 👁️ vibecheck watch ║
69
- ╚════════════════════════════════════════════════════════════╝${c.reset}
70
-
71
- ${c.dim}Time:${c.reset} ${formatTime()}
72
- ${c.dim}Runs:${c.reset} ${runCount}
73
- ${c.dim}Duration:${c.reset} ${formatDuration(duration)}
74
- ${c.dim}Changed:${c.reset} ${lastFile || "(initial)"}
75
-
76
- ${c.bold} VERDICT: ${verdictColor}${verdict}${c.reset}
156
+ const total = blocks + warns;
157
+ const health = total === 0 ? 100 : Math.max(0, 100 - (blocks * 20) - (warns * 5));
158
+
159
+ console.log();
160
+ console.log(` ${c.dim}${BOX.dTopLeft}${BOX.dHorizontal.repeat(62)}${BOX.dTopRight}${c.reset}`);
161
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${colors.watching}${ICONS.eye}${c.reset} ${c.bold}VIBECHECK WATCH${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
162
+ console.log(` ${c.dim}${BOX.dVertical}${BOX.dHorizontal.repeat(62)}${BOX.dVertical}${c.reset}`);
163
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
77
164
 
78
- ${c.dim}Findings:${c.reset} ${blocks} BLOCK, ${warns} WARN
79
-
80
- ${verdict === "SHIP" ? ` ${c.green}✓ Ready to ship${c.reset}` : ` ${c.yellow}⚠ Run "vibecheck fix" to resolve${c.reset}`}
81
-
82
- ${c.dim} Watching for changes... (Ctrl+C to stop)${c.reset}
83
- `);
165
+ // Stats row
166
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${ICONS.clock} Time:${c.reset} ${formatTime()} ${c.dim}${ICONS.refresh} Runs:${c.reset} ${runCount} ${c.dim}${ICONS.lightning} Duration:${c.reset} ${formatDuration(duration)} ${c.dim}${BOX.dVertical}${c.reset}`);
167
+
168
+ // Changed file
169
+ const changedDisplay = lastFile ? lastFile.slice(0, 40) : '(initial scan)';
170
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${ICONS.file} Changed:${c.reset} ${colors.accent}${changedDisplay}${c.reset}${' '.repeat(Math.max(0, 45 - changedDisplay.length))}${c.dim}${BOX.dVertical}${c.reset}`);
171
+
172
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
173
+ console.log(` ${c.dim}${BOX.dVertical}${BOX.dHorizontal.repeat(62)}${BOX.dVertical}${c.reset}`);
174
+
175
+ // Verdict
176
+ const verdictIcon = verdict === "SHIP" ? ICONS.ship : verdict === "WARN" ? ICONS.warning : ICONS.cross;
177
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
178
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.bold}VERDICT:${c.reset} ${verdictColor}${c.bold}${verdictIcon} ${verdict}${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
179
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
180
+
181
+ // Health bar
182
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}Health:${c.reset} ${progressBar(health)} ${health}% ${c.dim}${BOX.dVertical}${c.reset}`);
183
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}Findings:${c.reset} ${colors.blockRed}${blocks}${c.reset} ${c.dim}BLOCK${c.reset} ${colors.warnAmber}${warns}${c.reset} ${c.dim}WARN${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
184
+
185
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
186
+
187
+ // Status message
188
+ const statusMsg = verdict === "SHIP"
189
+ ? `${colors.shipGreen}${ICONS.check}${c.reset} Ready to ship`
190
+ : `${colors.warnAmber}${ICONS.arrow}${c.reset} Run ${colors.accent}vibecheck fix${c.reset} to resolve`;
191
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${statusMsg} ${c.dim}${BOX.dVertical}${c.reset}`);
192
+
193
+ console.log(` ${c.dim}${BOX.dVertical}${c.reset} ${c.dim}${BOX.dVertical}${c.reset}`);
194
+ console.log(` ${c.dim}${BOX.dBottomLeft}${BOX.dHorizontal.repeat(62)}${BOX.dBottomRight}${c.reset}`);
195
+ console.log();
196
+ console.log(` ${c.dim}${ICONS.watching} Watching for changes... (Ctrl+C to stop)${c.reset}`);
197
+ console.log();
84
198
  }
85
199
 
86
200
  function printTopFindings(findings, max = 5) {
@@ -90,12 +204,13 @@ function printTopFindings(findings, max = 5) {
90
204
 
91
205
  if (!top.length) return;
92
206
 
93
- console.log(`${c.dim} Top findings:${c.reset}`);
207
+ console.log(` ${c.bold}Top findings:${c.reset}`);
94
208
  for (const f of top) {
95
- const icon = f.severity === "BLOCK" ? c.red + "●" : c.yellow + "●";
96
- console.log(` ${icon}${c.reset} ${f.title}`);
209
+ const icon = f.severity === "BLOCK" ? `${colors.blockRed}●` : `${colors.warnAmber}●`;
210
+ const title = f.title?.length > 50 ? f.title.slice(0, 47) + '...' : f.title;
211
+ console.log(` ${icon}${c.reset} ${title}`);
97
212
  }
98
- console.log("");
213
+ console.log();
99
214
  }
100
215
 
101
216
  async function runShipQuiet(root, fastifyEntry) {
@@ -158,12 +273,74 @@ function watchDirectory(dir, callback) {
158
273
  };
159
274
  }
160
275
 
161
- async function runWatch({
162
- repoRoot,
163
- fastifyEntry,
164
- debounceMs = 500,
165
- clearScreen = true
166
- } = {}) {
276
+ function printHelp() {
277
+ console.log(BANNER_FULL);
278
+ console.log(`
279
+ ${c.bold}Usage:${c.reset} vibecheck watch [options]
280
+
281
+ ${c.bold}Continuous Dev Mode${c.reset} — Live reload with automatic ship analysis.
282
+
283
+ ${c.bold}Options:${c.reset}
284
+ ${colors.accent}--fastify-entry <path>${c.reset} Fastify entry file for route extraction
285
+ ${colors.accent}--debounce <ms>${c.reset} Debounce delay in ms ${c.dim}(default: 500)${c.reset}
286
+ ${colors.accent}--no-clear${c.reset} Don't clear screen between runs
287
+ ${colors.accent}--help, -h${c.reset} Show this help
288
+
289
+ ${c.bold}What It Does:${c.reset}
290
+ ${colors.shipGreen}1.${c.reset} Runs initial ship analysis
291
+ ${colors.shipGreen}2.${c.reset} Watches for file changes in your project
292
+ ${colors.shipGreen}3.${c.reset} Re-runs analysis on each change
293
+ ${colors.shipGreen}4.${c.reset} Shows live verdict dashboard
294
+
295
+ ${c.bold}Watched Files:${c.reset}
296
+ ${colors.accent}.ts${c.reset} ${colors.accent}.tsx${c.reset} ${colors.accent}.js${c.reset} ${colors.accent}.jsx${c.reset} ${colors.accent}.json${c.reset} ${colors.accent}.env${c.reset} ${colors.accent}.md${c.reset} ${colors.accent}.yml${c.reset} ${colors.accent}.yaml${c.reset}
297
+
298
+ ${c.bold}Ignored:${c.reset}
299
+ ${c.dim}node_modules, .next, .vibecheck, dist, build, .git${c.reset}
300
+
301
+ ${c.bold}Examples:${c.reset}
302
+ ${c.dim}# Start watching${c.reset}
303
+ vibecheck watch
304
+
305
+ ${c.dim}# 1 second debounce${c.reset}
306
+ vibecheck watch --debounce 1000
307
+
308
+ ${c.dim}# Keep history (don't clear)${c.reset}
309
+ vibecheck watch --no-clear
310
+
311
+ ${c.dim}Press Ctrl+C to stop watching${c.reset}
312
+ `);
313
+ }
314
+
315
+ async function runWatch(argsOrOpts = {}) {
316
+ // Handle array args from CLI
317
+ if (Array.isArray(argsOrOpts)) {
318
+ if (argsOrOpts.includes("--help") || argsOrOpts.includes("-h")) {
319
+ printHelp();
320
+ return 0;
321
+ }
322
+ const getArg = (flags) => {
323
+ for (const f of flags) {
324
+ const idx = argsOrOpts.indexOf(f);
325
+ if (idx !== -1 && idx < argsOrOpts.length - 1) return argsOrOpts[idx + 1];
326
+ }
327
+ return undefined;
328
+ };
329
+ argsOrOpts = {
330
+ repoRoot: process.cwd(),
331
+ fastifyEntry: getArg(["--fastify-entry"]),
332
+ debounceMs: parseInt(getArg(["--debounce"]) || "500", 10),
333
+ clearScreen: !argsOrOpts.includes("--no-clear"),
334
+ };
335
+ }
336
+
337
+ const {
338
+ repoRoot,
339
+ fastifyEntry,
340
+ debounceMs = 500,
341
+ clearScreen = true
342
+ } = argsOrOpts;
343
+
167
344
  const root = repoRoot || process.cwd();
168
345
  let runCount = 0;
169
346
  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();
@@ -448,18 +547,18 @@ async function main() {
448
547
 
449
548
  switch (cmd) {
450
549
  case "prove": exitCode = await runner(cmdArgs); 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;
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
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;
561
+ case "pr": exitCode = await runner(cmdArgs); break;
463
562
  case "share": exitCode = await runner(cmdArgs); break;
464
563
  default: exitCode = await runner(cmdArgs);
465
564
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibecheck-mcp-server",
3
- "version": "2.1.0",
3
+ "version": "3.0.9",
4
4
  "description": "Professional MCP server for vibecheck - Intelligent development environment vibechecks",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecheckai/cli",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
4
4
  "description": "Vibecheck CLI - Ship with confidence. One verdict: SHIP | WARN | BLOCK.",
5
5
  "main": "bin/vibecheck.js",
6
6
  "bin": {