@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.
- package/README.md +77 -484
- package/bin/runners/cli-utils.js +6 -6
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -0
- package/bin/runners/lib/entitlements-v2.js +409 -299
- package/bin/runners/lib/firewall-prompt.js +1 -1
- package/bin/runners/lib/sandbox/proof-chain.js +3 -3
- package/bin/runners/runFix.js +20 -0
- package/bin/runners/runInstall.js +41 -1
- package/bin/runners/runMcp.js +58 -0
- package/bin/runners/runPR.js +80 -12
- package/bin/runners/runProve.js +85 -27
- package/bin/runners/runReality.js +136 -16
- package/bin/runners/runReport.js +40 -0
- package/bin/runners/runScan.js +6 -6
- package/bin/runners/runShare.js +64 -4
- package/bin/runners/runShip.js +97 -1
- package/bin/runners/runStatus.js +3 -1
- package/bin/runners/runWatch.js +63 -6
- package/bin/vibecheck.js +161 -62
- package/package.json +6 -2
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Entitlements v2 -
|
|
2
|
+
* Entitlements v2 - CANONICAL Tier Enforcement
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* SINGLE SOURCE OF TRUTH for all tier gating in vibecheck CLI.
|
|
5
|
+
* Every command runner MUST use this module for access control.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 4. Offline grace: 24h using last valid receipt
|
|
7
|
+
* NO BYPASS ALLOWED:
|
|
8
|
+
* - No owner-mode env vars
|
|
9
|
+
* - No offline fallback that grants paid features
|
|
10
|
+
* - No silent feature access
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
12
|
+
* Exit Codes:
|
|
13
|
+
* - 0: Success
|
|
14
|
+
* - 2: BLOCK verdict (CI failure)
|
|
15
|
+
* - 3: Feature not allowed (upgrade required)
|
|
16
|
+
* - 4: Misconfiguration/env error
|
|
17
|
+
*
|
|
18
|
+
* Tiers:
|
|
19
|
+
* - FREE ($0): Basic scanning and validation
|
|
20
|
+
* - STARTER ($29/repo/mo): CI gates, launch, PR, badge, MCP
|
|
21
|
+
* - PRO ($99/repo/mo): Full fix, prove, ai-test, share, advanced reality
|
|
22
|
+
* - ENTERPRISE: Custom (placeholder only)
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
"use strict";
|
|
@@ -22,362 +29,465 @@ const path = require("path");
|
|
|
22
29
|
const os = require("os");
|
|
23
30
|
const crypto = require("crypto");
|
|
24
31
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
// EXIT CODES
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
35
|
+
const EXIT_SUCCESS = 0;
|
|
36
|
+
const EXIT_BLOCK_VERDICT = 2;
|
|
37
|
+
const EXIT_FEATURE_NOT_ALLOWED = 3;
|
|
38
|
+
const EXIT_MISCONFIG = 4;
|
|
29
39
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// TIER DEFINITIONS - SOURCE OF TRUTH
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
const TIERS = {
|
|
44
|
+
free: { name: "FREE", price: 0, order: 0 },
|
|
45
|
+
starter: { name: "STARTER", price: 29, order: 1 },
|
|
46
|
+
pro: { name: "PRO", price: 99, order: 2 },
|
|
47
|
+
enterprise: { name: "ENTERPRISE", price: 499, order: 3 },
|
|
48
|
+
};
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
51
|
+
// ENTITLEMENTS MATRIX - SOURCE OF TRUTH
|
|
52
|
+
// Format: feature -> { minTier, caps?, downgrade? }
|
|
53
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
54
|
+
const ENTITLEMENTS = {
|
|
55
|
+
// Core commands
|
|
56
|
+
"scan": { minTier: "free" },
|
|
57
|
+
"ship": { minTier: "free", caps: { free: "static-only" } },
|
|
58
|
+
"ship.static": { minTier: "free" },
|
|
59
|
+
"ship.full": { minTier: "starter" },
|
|
60
|
+
|
|
61
|
+
// Reality testing
|
|
62
|
+
"reality": { minTier: "free", downgrade: "reality.preview" },
|
|
63
|
+
"reality.preview": { minTier: "free", caps: { free: { maxPages: 5, maxClicks: 20, noAuthBoundary: true } } },
|
|
64
|
+
"reality.full": { minTier: "starter" },
|
|
65
|
+
"reality.advanced_auth_boundary": { minTier: "pro" },
|
|
66
|
+
|
|
67
|
+
// Prove command
|
|
68
|
+
"prove": { minTier: "pro" },
|
|
69
|
+
|
|
70
|
+
// Fix command
|
|
71
|
+
"fix": { minTier: "free", downgrade: "fix.plan_only" },
|
|
72
|
+
"fix.plan_only": { minTier: "free" },
|
|
73
|
+
"fix.apply_patches": { minTier: "pro" },
|
|
74
|
+
|
|
75
|
+
// Report formats
|
|
76
|
+
"report": { minTier: "free", downgrade: "report.html_md" },
|
|
77
|
+
"report.html_md": { minTier: "free" },
|
|
78
|
+
"report.sarif_csv": { minTier: "starter" },
|
|
79
|
+
"report.compliance_packs": { minTier: "pro" },
|
|
80
|
+
|
|
81
|
+
// Setup & DX
|
|
82
|
+
"install": { minTier: "free" },
|
|
83
|
+
"init": { minTier: "free" },
|
|
84
|
+
"doctor": { minTier: "free" },
|
|
85
|
+
"status": { minTier: "free" },
|
|
86
|
+
"watch": { minTier: "free" },
|
|
87
|
+
|
|
88
|
+
// AI Truth
|
|
89
|
+
"ctx": { minTier: "free" },
|
|
90
|
+
"guard": { minTier: "free" },
|
|
91
|
+
"context": { minTier: "free" },
|
|
92
|
+
"mdc": { minTier: "free" },
|
|
93
|
+
|
|
94
|
+
// CI & Collaboration (STARTER+)
|
|
95
|
+
"gate": { minTier: "starter" },
|
|
96
|
+
"launch": { minTier: "starter" },
|
|
97
|
+
"pr": { minTier: "starter" },
|
|
98
|
+
"badge": { minTier: "starter" },
|
|
99
|
+
"mcp": { minTier: "starter" },
|
|
100
|
+
|
|
101
|
+
// PRO only
|
|
102
|
+
"share": { minTier: "pro" },
|
|
103
|
+
"ai-test": { minTier: "pro" },
|
|
104
|
+
|
|
105
|
+
// Account (always free)
|
|
106
|
+
"login": { minTier: "free" },
|
|
107
|
+
"logout": { minTier: "free" },
|
|
108
|
+
"whoami": { minTier: "free" },
|
|
109
|
+
|
|
110
|
+
// Labs/experimental
|
|
111
|
+
"labs": { minTier: "free" },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
115
|
+
// LIMITS BY TIER
|
|
116
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
117
|
+
const LIMITS = {
|
|
118
|
+
free: {
|
|
119
|
+
realityMaxPages: 5,
|
|
120
|
+
realityMaxClicks: 20,
|
|
121
|
+
realityAuthBoundary: false,
|
|
122
|
+
reportFormats: ["html", "md"],
|
|
123
|
+
fixApplyPatches: false,
|
|
124
|
+
scansPerMonth: 50,
|
|
125
|
+
shipChecksPerMonth: 20,
|
|
126
|
+
},
|
|
127
|
+
starter: {
|
|
128
|
+
realityMaxPages: 50,
|
|
129
|
+
realityMaxClicks: 500,
|
|
130
|
+
realityAuthBoundary: true,
|
|
131
|
+
reportFormats: ["html", "md", "sarif", "csv"],
|
|
132
|
+
fixApplyPatches: false,
|
|
133
|
+
scansPerMonth: -1, // unlimited
|
|
134
|
+
shipChecksPerMonth: -1,
|
|
135
|
+
},
|
|
136
|
+
pro: {
|
|
137
|
+
realityMaxPages: -1, // unlimited
|
|
138
|
+
realityMaxClicks: -1,
|
|
139
|
+
realityAuthBoundary: true,
|
|
140
|
+
realityAdvancedAuth: true,
|
|
141
|
+
reportFormats: ["html", "md", "sarif", "csv", "compliance"],
|
|
142
|
+
fixApplyPatches: true,
|
|
143
|
+
scansPerMonth: -1,
|
|
144
|
+
shipChecksPerMonth: -1,
|
|
145
|
+
},
|
|
146
|
+
enterprise: {
|
|
147
|
+
realityMaxPages: -1,
|
|
148
|
+
realityMaxClicks: -1,
|
|
149
|
+
realityAuthBoundary: true,
|
|
150
|
+
realityAdvancedAuth: true,
|
|
151
|
+
reportFormats: ["html", "md", "sarif", "csv", "compliance", "custom"],
|
|
152
|
+
fixApplyPatches: true,
|
|
153
|
+
scansPerMonth: -1,
|
|
154
|
+
shipChecksPerMonth: -1,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const API_BASE_URL = process.env.VIBECHECK_API_URL || "https://api.vibecheckai.dev";
|
|
159
|
+
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
// CACHE PATHS
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
41
163
|
function getEntitlementsCachePath(projectPath) {
|
|
42
164
|
return path.join(projectPath || process.cwd(), ".vibecheck", ".entitlements.json");
|
|
43
165
|
}
|
|
44
166
|
|
|
45
|
-
/**
|
|
46
|
-
* Get global config path (user home)
|
|
47
|
-
*/
|
|
48
167
|
function getGlobalConfigPath() {
|
|
49
168
|
const home = os.homedir();
|
|
50
169
|
if (process.platform === "win32") {
|
|
51
|
-
return path.join(
|
|
52
|
-
process.env.APPDATA || path.join(home, "AppData", "Roaming"),
|
|
53
|
-
"vibecheck",
|
|
54
|
-
"config.json"
|
|
55
|
-
);
|
|
170
|
+
return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "vibecheck", "config.json");
|
|
56
171
|
}
|
|
57
172
|
return path.join(home, ".config", "vibecheck", "config.json");
|
|
58
173
|
}
|
|
59
174
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
68
|
-
}
|
|
69
|
-
} catch {}
|
|
70
|
-
return null;
|
|
175
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
176
|
+
// TIER COMPARISON
|
|
177
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
178
|
+
function tierMeetsMinimum(currentTier, requiredTier) {
|
|
179
|
+
const current = TIERS[currentTier]?.order ?? -1;
|
|
180
|
+
const required = TIERS[requiredTier]?.order ?? 999;
|
|
181
|
+
return current >= required;
|
|
71
182
|
}
|
|
72
183
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*/
|
|
76
|
-
function saveCachedReceipt(projectPath, receipt) {
|
|
77
|
-
const cachePath = getEntitlementsCachePath(projectPath);
|
|
78
|
-
const dir = path.dirname(cachePath);
|
|
79
|
-
if (!fs.existsSync(dir)) {
|
|
80
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
fs.writeFileSync(cachePath, JSON.stringify(receipt, null, 2), { mode: 0o600 });
|
|
184
|
+
function getTierLabel(tier) {
|
|
185
|
+
return TIERS[tier]?.name || tier.toUpperCase();
|
|
83
186
|
}
|
|
84
187
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// In production: use RSA signature verification with public key
|
|
94
|
-
// For now: use HMAC with a shared secret (server would use same secret)
|
|
95
|
-
const secret = process.env.VIBECHECK_RECEIPT_SECRET || "vibecheck-receipt-v1";
|
|
96
|
-
const dataStr = JSON.stringify(receipt.data);
|
|
97
|
-
const expectedSig = crypto.createHmac("sha256", secret).update(dataStr).digest("hex");
|
|
98
|
-
|
|
99
|
-
return receipt.signature === expectedSig;
|
|
100
|
-
}
|
|
188
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
189
|
+
// CORE API: getTier()
|
|
190
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
191
|
+
let _cachedTier = null;
|
|
192
|
+
let _cachedTierExpiry = 0;
|
|
101
193
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!
|
|
107
|
-
return
|
|
194
|
+
async function getTier(options = {}) {
|
|
195
|
+
const { apiKey, projectPath, forceRefresh = false } = options;
|
|
196
|
+
|
|
197
|
+
// Check cache (5 minute TTL)
|
|
198
|
+
if (!forceRefresh && _cachedTier && Date.now() < _cachedTierExpiry) {
|
|
199
|
+
return _cachedTier;
|
|
108
200
|
}
|
|
109
201
|
|
|
110
|
-
|
|
111
|
-
const graceMs = RECEIPT_GRACE_HOURS * 3600 * 1000;
|
|
112
|
-
return Date.now() - issuedAt < graceMs;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Get current month key (YYYY-MM)
|
|
117
|
-
*/
|
|
118
|
-
function getCurrentMonthKey() {
|
|
119
|
-
const now = new Date();
|
|
120
|
-
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Request ship usage from server
|
|
125
|
-
* Returns signed receipt with updated usage
|
|
126
|
-
*/
|
|
127
|
-
async function requestShipUsage(apiKey, projectPath) {
|
|
202
|
+
// No API key = free tier
|
|
128
203
|
if (!apiKey) {
|
|
129
|
-
|
|
204
|
+
_cachedTier = "free";
|
|
205
|
+
_cachedTierExpiry = Date.now() + 300000;
|
|
206
|
+
return "free";
|
|
130
207
|
}
|
|
131
208
|
|
|
209
|
+
// Fetch from API
|
|
132
210
|
try {
|
|
133
|
-
const res = await fetch(`${API_BASE_URL}/v1/
|
|
134
|
-
method: "
|
|
135
|
-
headers: {
|
|
136
|
-
|
|
137
|
-
"Content-Type": "application/json"
|
|
138
|
-
},
|
|
139
|
-
body: JSON.stringify({
|
|
140
|
-
projectPath: projectPath ? path.basename(projectPath) : "unknown",
|
|
141
|
-
timestamp: new Date().toISOString()
|
|
142
|
-
}),
|
|
143
|
-
signal: AbortSignal.timeout(10000)
|
|
211
|
+
const res = await fetch(`${API_BASE_URL}/v1/entitlements`, {
|
|
212
|
+
method: "GET",
|
|
213
|
+
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
214
|
+
signal: AbortSignal.timeout(5000),
|
|
144
215
|
});
|
|
145
216
|
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
console.warn("Warning: Receipt signature invalid, proceeding with caution");
|
|
217
|
+
if (res.ok) {
|
|
218
|
+
const data = await res.json();
|
|
219
|
+
_cachedTier = data.tier || "free";
|
|
220
|
+
_cachedTierExpiry = Date.now() + 300000;
|
|
221
|
+
|
|
222
|
+
// Cache locally
|
|
223
|
+
try {
|
|
224
|
+
const cachePath = getEntitlementsCachePath(projectPath);
|
|
225
|
+
const dir = path.dirname(cachePath);
|
|
226
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
227
|
+
fs.writeFileSync(cachePath, JSON.stringify({ tier: _cachedTier, fetchedAt: new Date().toISOString() }, null, 2));
|
|
228
|
+
} catch {}
|
|
229
|
+
|
|
230
|
+
return _cachedTier;
|
|
161
231
|
}
|
|
162
232
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return receipt;
|
|
167
|
-
} catch (error) {
|
|
168
|
-
// Network error - try to use cached receipt
|
|
169
|
-
const cached = loadCachedReceipt(projectPath);
|
|
170
|
-
if (cached && isReceiptInGrace(cached)) {
|
|
171
|
-
console.log(" (offline mode - using cached entitlements)");
|
|
172
|
-
return { ...cached, offline: true };
|
|
233
|
+
if (res.status === 401) {
|
|
234
|
+
return "free"; // Invalid key = free tier
|
|
173
235
|
}
|
|
174
|
-
|
|
175
|
-
|
|
236
|
+
} catch {
|
|
237
|
+
// Network error - check local cache
|
|
238
|
+
try {
|
|
239
|
+
const cachePath = getEntitlementsCachePath(projectPath);
|
|
240
|
+
if (fs.existsSync(cachePath)) {
|
|
241
|
+
const cached = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
242
|
+
// Only use cache if less than 24 hours old
|
|
243
|
+
const fetchedAt = new Date(cached.fetchedAt).getTime();
|
|
244
|
+
if (Date.now() - fetchedAt < 24 * 3600 * 1000) {
|
|
245
|
+
return cached.tier || "free";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
176
249
|
}
|
|
250
|
+
|
|
251
|
+
// Default to free (no offline bypass to paid features)
|
|
252
|
+
return "free";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
256
|
+
// CORE API: getLimits()
|
|
257
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
258
|
+
function getLimits(tier) {
|
|
259
|
+
return LIMITS[tier] || LIMITS.free;
|
|
177
260
|
}
|
|
178
261
|
|
|
262
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
263
|
+
// CORE API: enforce() - THE GATEKEEPER
|
|
264
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
179
265
|
/**
|
|
180
|
-
*
|
|
266
|
+
* Enforce feature access. Returns enforcement result.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} feature - Feature key (e.g., "prove", "fix.apply_patches")
|
|
269
|
+
* @param {object} options - { apiKey?, projectPath?, silent? }
|
|
270
|
+
* @returns {object} - { allowed, tier, downgrade?, exitCode, message }
|
|
181
271
|
*/
|
|
182
|
-
async function
|
|
183
|
-
const { apiKey,
|
|
272
|
+
async function enforce(feature, options = {}) {
|
|
273
|
+
const { apiKey, projectPath, silent = false } = options;
|
|
184
274
|
|
|
185
|
-
|
|
186
|
-
|
|
275
|
+
const tier = await getTier({ apiKey, projectPath });
|
|
276
|
+
const entitlement = ENTITLEMENTS[feature];
|
|
277
|
+
|
|
278
|
+
if (!entitlement) {
|
|
279
|
+
// Unknown feature - block by default
|
|
187
280
|
return {
|
|
188
|
-
allowed:
|
|
189
|
-
tier
|
|
190
|
-
|
|
191
|
-
|
|
281
|
+
allowed: false,
|
|
282
|
+
tier,
|
|
283
|
+
exitCode: EXIT_MISCONFIG,
|
|
284
|
+
message: `Unknown feature: ${feature}`,
|
|
192
285
|
};
|
|
193
286
|
}
|
|
194
287
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
288
|
+
const hasAccess = tierMeetsMinimum(tier, entitlement.minTier);
|
|
289
|
+
|
|
290
|
+
if (hasAccess) {
|
|
291
|
+
// Full access
|
|
292
|
+
return {
|
|
293
|
+
allowed: true,
|
|
294
|
+
tier,
|
|
295
|
+
limits: getLimits(tier),
|
|
296
|
+
caps: entitlement.caps?.[tier] || null,
|
|
297
|
+
};
|
|
198
298
|
}
|
|
199
299
|
|
|
200
|
-
//
|
|
201
|
-
if (
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
allowed: true,
|
|
209
|
-
tier: receipt.data.tier || "free",
|
|
210
|
-
reason: "Offline grace period",
|
|
211
|
-
remaining: receipt.data.remainingShipChecks || 0,
|
|
212
|
-
offline: true
|
|
213
|
-
};
|
|
214
|
-
}
|
|
300
|
+
// Check for downgrade option
|
|
301
|
+
if (entitlement.downgrade) {
|
|
302
|
+
const downgradeEntitlement = ENTITLEMENTS[entitlement.downgrade];
|
|
303
|
+
if (downgradeEntitlement && tierMeetsMinimum(tier, downgradeEntitlement.minTier)) {
|
|
304
|
+
// Downgrade allowed
|
|
305
|
+
const caps = downgradeEntitlement.caps?.[tier] || null;
|
|
306
|
+
const message = formatDowngradeMessage(feature, entitlement.downgrade, tier, entitlement.minTier, caps);
|
|
215
307
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
allowed: false,
|
|
220
|
-
tier: "free",
|
|
221
|
-
reason: "Ship check limit reached for this month",
|
|
222
|
-
remaining: 0
|
|
223
|
-
};
|
|
308
|
+
if (!silent) {
|
|
309
|
+
console.log(message);
|
|
224
310
|
}
|
|
225
311
|
|
|
226
|
-
|
|
227
|
-
|
|
312
|
+
return {
|
|
313
|
+
allowed: true,
|
|
314
|
+
tier,
|
|
315
|
+
downgrade: entitlement.downgrade,
|
|
316
|
+
limits: getLimits(tier),
|
|
317
|
+
caps,
|
|
318
|
+
message,
|
|
319
|
+
};
|
|
228
320
|
}
|
|
229
|
-
|
|
230
|
-
// Valid receipt from server
|
|
231
|
-
const data = receipt.data || receipt;
|
|
232
|
-
return {
|
|
233
|
-
allowed: true,
|
|
234
|
-
tier: data.tier || "pro",
|
|
235
|
-
reason: "Authenticated",
|
|
236
|
-
remaining: data.remainingShipChecks,
|
|
237
|
-
receipt
|
|
238
|
-
};
|
|
239
321
|
}
|
|
240
322
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
tier: cached.data.tier || "pro",
|
|
247
|
-
reason: "Cached entitlements",
|
|
248
|
-
remaining: cached.data.remainingShipChecks
|
|
249
|
-
};
|
|
323
|
+
// Not allowed - generate upgrade message
|
|
324
|
+
const message = formatUpgradeMessage(feature, tier, entitlement.minTier);
|
|
325
|
+
|
|
326
|
+
if (!silent) {
|
|
327
|
+
console.error(message);
|
|
250
328
|
}
|
|
251
329
|
|
|
252
|
-
return {
|
|
330
|
+
return {
|
|
331
|
+
allowed: false,
|
|
332
|
+
tier,
|
|
333
|
+
requiredTier: entitlement.minTier,
|
|
334
|
+
exitCode: EXIT_FEATURE_NOT_ALLOWED,
|
|
335
|
+
message,
|
|
336
|
+
};
|
|
253
337
|
}
|
|
254
338
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
339
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
340
|
+
// MESSAGING
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
342
|
+
const c = {
|
|
343
|
+
reset: "\x1b[0m",
|
|
344
|
+
bold: "\x1b[1m",
|
|
345
|
+
dim: "\x1b[2m",
|
|
346
|
+
red: "\x1b[31m",
|
|
347
|
+
green: "\x1b[32m",
|
|
348
|
+
yellow: "\x1b[33m",
|
|
349
|
+
cyan: "\x1b[36m",
|
|
350
|
+
magenta: "\x1b[35m",
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
function formatUpgradeMessage(feature, currentTier, requiredTier) {
|
|
354
|
+
const tierColors = { starter: c.cyan, pro: c.magenta, enterprise: c.yellow };
|
|
355
|
+
const reqColor = tierColors[requiredTier] || c.yellow;
|
|
261
356
|
|
|
262
|
-
|
|
357
|
+
return `
|
|
358
|
+
${c.red}${c.bold}⛔ Feature Not Available${c.reset}
|
|
359
|
+
|
|
360
|
+
${c.yellow}${feature}${c.reset} requires ${reqColor}${getTierLabel(requiredTier)}${c.reset} plan.
|
|
361
|
+
Your current plan: ${c.dim}${getTierLabel(currentTier)}${c.reset}
|
|
362
|
+
|
|
363
|
+
${c.cyan}Upgrade at:${c.reset} https://vibecheckai.dev/pricing
|
|
364
|
+
|
|
365
|
+
${c.dim}Exit code: ${EXIT_FEATURE_NOT_ALLOWED}${c.reset}
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function formatDowngradeMessage(feature, downgradeTo, currentTier, requiredTier, caps) {
|
|
370
|
+
const tierColors = { starter: c.cyan, pro: c.magenta, enterprise: c.yellow };
|
|
371
|
+
const reqColor = tierColors[requiredTier] || c.yellow;
|
|
263
372
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
373
|
+
let capsStr = "";
|
|
374
|
+
if (caps) {
|
|
375
|
+
if (typeof caps === "string") {
|
|
376
|
+
capsStr = ` ${c.dim}Mode: ${caps}${c.reset}\n`;
|
|
377
|
+
} else if (typeof caps === "object") {
|
|
378
|
+
const entries = Object.entries(caps).map(([k, v]) => `${k}: ${v}`).join(", ");
|
|
379
|
+
capsStr = ` ${c.dim}Limits: ${entries}${c.reset}\n`;
|
|
268
380
|
}
|
|
269
|
-
} catch {}
|
|
270
|
-
|
|
271
|
-
const remaining = FREE_SHIP_CHECKS_PER_MONTH - usage.shipChecks;
|
|
272
|
-
|
|
273
|
-
if (remaining <= 0) {
|
|
274
|
-
return {
|
|
275
|
-
allowed: false,
|
|
276
|
-
tier: "free",
|
|
277
|
-
reason: `Free tier limit reached (${FREE_SHIP_CHECKS_PER_MONTH}/month)`,
|
|
278
|
-
remaining: 0,
|
|
279
|
-
upgradeUrl: "https://vibecheckai.dev/pricing"
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Increment usage if not skipping meter
|
|
284
|
-
if (!skipMeter) {
|
|
285
|
-
usage.shipChecks++;
|
|
286
|
-
try {
|
|
287
|
-
saveCachedReceipt(projectPath, {
|
|
288
|
-
freeUsage: usage,
|
|
289
|
-
data: { tier: "free", remainingShipChecks: remaining - 1 },
|
|
290
|
-
issuedAt: new Date().toISOString()
|
|
291
|
-
});
|
|
292
|
-
} catch {}
|
|
293
381
|
}
|
|
294
382
|
|
|
295
|
-
return
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
383
|
+
return `
|
|
384
|
+
${c.yellow}${c.bold}⚠ Running in Preview Mode${c.reset}
|
|
385
|
+
|
|
386
|
+
Full ${c.yellow}${feature}${c.reset} requires ${reqColor}${getTierLabel(requiredTier)}${c.reset} plan.
|
|
387
|
+
Running ${c.green}${downgradeTo}${c.reset} instead.
|
|
388
|
+
${capsStr}
|
|
389
|
+
${c.cyan}Upgrade for full access:${c.reset} https://vibecheckai.dev/pricing
|
|
390
|
+
`;
|
|
301
391
|
}
|
|
302
392
|
|
|
393
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
394
|
+
// CONVENIENCE HELPERS
|
|
395
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
396
|
+
|
|
303
397
|
/**
|
|
304
|
-
* Check if
|
|
398
|
+
* Check if a command is allowed (does not print messages)
|
|
305
399
|
*/
|
|
306
|
-
async function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const featureTiers = {
|
|
310
|
-
// Free features
|
|
311
|
-
ship: ["free", "pro", "team", "enterprise"],
|
|
312
|
-
scan: ["free", "pro", "team", "enterprise"],
|
|
313
|
-
ctx: ["free", "pro", "team", "enterprise"],
|
|
314
|
-
install: ["free", "pro", "team", "enterprise"],
|
|
315
|
-
|
|
316
|
-
// Pro features
|
|
317
|
-
reality: ["pro", "team", "enterprise"],
|
|
318
|
-
fix_autopilot: ["pro", "team", "enterprise"],
|
|
319
|
-
share: ["pro", "team", "enterprise"],
|
|
320
|
-
|
|
321
|
-
// Team features
|
|
322
|
-
pr_gate: ["team", "enterprise"],
|
|
323
|
-
ci_automation: ["team", "enterprise"]
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const allowedTiers = featureTiers[feature] || ["enterprise"];
|
|
327
|
-
const hasAccess = allowedTiers.includes(entitlement.tier);
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
allowed: hasAccess,
|
|
331
|
-
tier: entitlement.tier,
|
|
332
|
-
requiredTier: allowedTiers[0],
|
|
333
|
-
upgradeUrl: hasAccess ? null : "https://vibecheckai.dev/pricing"
|
|
334
|
-
};
|
|
400
|
+
async function checkCommand(command, options = {}) {
|
|
401
|
+
return enforce(command, { ...options, silent: true });
|
|
335
402
|
}
|
|
336
403
|
|
|
337
404
|
/**
|
|
338
|
-
*
|
|
405
|
+
* Enforce and exit if not allowed
|
|
339
406
|
*/
|
|
340
|
-
function
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!cached) {
|
|
345
|
-
return {
|
|
346
|
-
tier: "free",
|
|
347
|
-
shipChecksUsed: 0,
|
|
348
|
-
shipChecksRemaining: FREE_SHIP_CHECKS_PER_MONTH,
|
|
349
|
-
month: currentMonth
|
|
350
|
-
};
|
|
407
|
+
async function enforceOrExit(feature, options = {}) {
|
|
408
|
+
const result = await enforce(feature, options);
|
|
409
|
+
if (!result.allowed) {
|
|
410
|
+
process.exit(result.exitCode);
|
|
351
411
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get the minimum tier required for a feature
|
|
417
|
+
*/
|
|
418
|
+
function getMinTierForFeature(feature) {
|
|
419
|
+
return ENTITLEMENTS[feature]?.minTier || "enterprise";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Check if tier has access to feature (sync, for help display)
|
|
424
|
+
*/
|
|
425
|
+
function tierHasFeature(tier, feature) {
|
|
426
|
+
const entitlement = ENTITLEMENTS[feature];
|
|
427
|
+
if (!entitlement) return false;
|
|
428
|
+
return tierMeetsMinimum(tier, entitlement.minTier);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get all features for a tier
|
|
433
|
+
*/
|
|
434
|
+
function getFeaturesForTier(tier) {
|
|
435
|
+
const features = [];
|
|
436
|
+
for (const [feature, def] of Object.entries(ENTITLEMENTS)) {
|
|
437
|
+
if (tierMeetsMinimum(tier, def.minTier)) {
|
|
438
|
+
features.push(feature);
|
|
439
|
+
}
|
|
360
440
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
441
|
+
return features;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
445
|
+
// COMMAND GROUPING FOR HELP DISPLAY
|
|
446
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
447
|
+
const COMMAND_GROUPS = {
|
|
448
|
+
"Proof Loop": ["scan", "ship", "reality", "prove", "fix", "report"],
|
|
449
|
+
"Setup & DX": ["install", "init", "doctor", "status", "watch", "launch"],
|
|
450
|
+
"AI Truth": ["ctx", "guard", "context", "mdc"],
|
|
451
|
+
"CI & Collaboration": ["gate", "pr", "badge"],
|
|
452
|
+
"Reporting": ["report"],
|
|
453
|
+
"Automation": ["ai-test", "mcp", "share"],
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
function getCommandGroup(command) {
|
|
457
|
+
for (const [group, commands] of Object.entries(COMMAND_GROUPS)) {
|
|
458
|
+
if (commands.includes(command)) return group;
|
|
369
459
|
}
|
|
370
|
-
|
|
371
|
-
return { tier: "unknown", month: currentMonth };
|
|
460
|
+
return "Other";
|
|
372
461
|
}
|
|
373
462
|
|
|
463
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
464
|
+
// EXPORTS
|
|
465
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
374
466
|
module.exports = {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
467
|
+
// Core API
|
|
468
|
+
getTier,
|
|
469
|
+
getLimits,
|
|
470
|
+
enforce,
|
|
471
|
+
enforceOrExit,
|
|
472
|
+
checkCommand,
|
|
473
|
+
|
|
474
|
+
// Tier helpers
|
|
475
|
+
tierMeetsMinimum,
|
|
476
|
+
getTierLabel,
|
|
477
|
+
getMinTierForFeature,
|
|
478
|
+
tierHasFeature,
|
|
479
|
+
getFeaturesForTier,
|
|
480
|
+
|
|
481
|
+
// Command grouping
|
|
482
|
+
COMMAND_GROUPS,
|
|
483
|
+
getCommandGroup,
|
|
484
|
+
|
|
485
|
+
// Constants
|
|
486
|
+
TIERS,
|
|
487
|
+
ENTITLEMENTS,
|
|
488
|
+
LIMITS,
|
|
489
|
+
EXIT_SUCCESS,
|
|
490
|
+
EXIT_BLOCK_VERDICT,
|
|
491
|
+
EXIT_FEATURE_NOT_ALLOWED,
|
|
492
|
+
EXIT_MISCONFIG,
|
|
383
493
|
};
|