@vibecheckai/cli 3.0.10 → 3.1.0

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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Usage Logger - Track metered units per command
3
+ *
4
+ * Records usage for monetization and billing.
5
+ * All paid commands must log their usage.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const os = require("os");
13
+
14
+ // ═══════════════════════════════════════════════════════════════════════════════
15
+ // USAGE LOGGING
16
+ // ═══════════════════════════════════════════════════════════════════════════════
17
+
18
+ function getUsageLogPath() {
19
+ const home = os.homedir();
20
+ const vibecheckDir = path.join(home, ".vibecheck");
21
+ const usageDir = path.join(vibecheckDir, "usage");
22
+
23
+ // Ensure directories exist
24
+ if (!fs.existsSync(vibecheckDir)) fs.mkdirSync(vibecheckDir, { recursive: true });
25
+ if (!fs.existsSync(usageDir)) fs.mkdirSync(usageDir, { recursive: true });
26
+
27
+ return path.join(usageDir, "usage.jsonl");
28
+ }
29
+
30
+ /**
31
+ * Log usage for a command run
32
+ * @param {Object} usage - Usage data
33
+ * @param {string} usage.runId - Unique run identifier
34
+ * @param {string} usage.command - Command name
35
+ * @param {string} usage.tier - User tier (free/pro/complete)
36
+ * @param {Object} usage.units - Units consumed
37
+ * @param {number} usage.units.pages - Pages crawled (reality)
38
+ * @param {number} usage.units.clicks - UI interactions (reality)
39
+ * @param {number} usage.units.endpoints - API endpoints tested (permissions)
40
+ * @param {number} usage.units.roles - Roles tested (permissions)
41
+ * @param {number} usage.units.files - Files modified (fix)
42
+ * @param {number} usage.units.hunks - Hunks applied (fix)
43
+ * @param {number} usage.units.exports - Reports generated
44
+ * @param {string} usage.timestamp - ISO timestamp
45
+ * @param {string} usage.projectPath - Project directory
46
+ */
47
+ function logUsage(usage) {
48
+ const logPath = getUsageLogPath();
49
+ const entry = {
50
+ ...usage,
51
+ timestamp: usage.timestamp || new Date().toISOString(),
52
+ hostname: os.hostname(),
53
+ platform: process.platform,
54
+ nodeVersion: process.version,
55
+ };
56
+
57
+ // Append to log file (JSONL format - one JSON per line)
58
+ const line = JSON.stringify(entry) + "\n";
59
+ fs.appendFileSync(logPath, line);
60
+
61
+ // Also store in run-specific directory for receipts
62
+ if (usage.runId) {
63
+ const runDir = path.join(process.cwd(), ".vibecheck", "runs", usage.runId);
64
+ if (fs.existsSync(runDir)) {
65
+ fs.writeFileSync(path.join(runDir, "usage.json"), JSON.stringify(entry, null, 2));
66
+ }
67
+ }
68
+
69
+ return entry;
70
+ }
71
+
72
+ /**
73
+ * Get usage statistics for a time period
74
+ * @param {Object} options - Query options
75
+ * @param {string} options.since - ISO date to start from
76
+ * @param {string} options.until - ISO date to end at
77
+ * @param {string} options.command - Filter by command
78
+ * @param {string} options.tier - Filter by tier
79
+ * @returns {Array} Array of usage entries
80
+ */
81
+ function getUsage(options = {}) {
82
+ const logPath = getUsageLogPath();
83
+
84
+ if (!fs.existsSync(logPath)) return [];
85
+
86
+ const lines = fs.readFileSync(logPath, "utf8").split("\n").filter(Boolean);
87
+ const entries = lines.map(line => {
88
+ try { return JSON.parse(line); } catch { return null; }
89
+ }).filter(Boolean);
90
+
91
+ // Apply filters
92
+ let filtered = entries;
93
+
94
+ if (options.since) {
95
+ filtered = filtered.filter(e => e.timestamp >= options.since);
96
+ }
97
+
98
+ if (options.until) {
99
+ filtered = filtered.filter(e => e.timestamp <= options.until);
100
+ }
101
+
102
+ if (options.command) {
103
+ filtered = filtered.filter(e => e.command === options.command);
104
+ }
105
+
106
+ if (options.tier) {
107
+ filtered = filtered.filter(e => e.tier === options.tier);
108
+ }
109
+
110
+ return filtered;
111
+ }
112
+
113
+ /**
114
+ * Get aggregate usage by unit type
115
+ * @param {Object} options - Query options (same as getUsage)
116
+ * @returns {Object} Aggregated totals
117
+ */
118
+ function getUsageTotals(options = {}) {
119
+ const entries = getUsage(options);
120
+ const totals = {
121
+ runs: entries.length,
122
+ pages: 0,
123
+ clicks: 0,
124
+ endpoints: 0,
125
+ roles: 0,
126
+ files: 0,
127
+ hunks: 0,
128
+ exports: 0,
129
+ };
130
+
131
+ for (const entry of entries) {
132
+ if (entry.units) {
133
+ totals.pages += entry.units.pages || 0;
134
+ totals.clicks += entry.units.clicks || 0;
135
+ totals.endpoints += entry.units.endpoints || 0;
136
+ totals.roles += entry.units.roles || 0;
137
+ totals.files += entry.units.files || 0;
138
+ totals.hunks += entry.units.hunks || 0;
139
+ totals.exports += entry.units.exports || 0;
140
+ }
141
+ }
142
+
143
+ return totals;
144
+ }
145
+
146
+ // ═══════════════════════════════════════════════════════════════════════════════
147
+ // EXPORTS
148
+ // ═══════════════════════════════════════════════════════════════════════════════
149
+ module.exports = {
150
+ logUsage,
151
+ getUsage,
152
+ getUsageTotals,
153
+ };
@@ -12,6 +12,9 @@
12
12
  const fs = require("fs");
13
13
  const path = require("path");
14
14
 
15
+ const upsell = require("./lib/upsell");
16
+ const entitlements = require("./lib/entitlements-v2");
17
+
15
18
  // ═══════════════════════════════════════════════════════════════════════════════
16
19
  // ADVANCED TERMINAL - ANSI CODES & UTILITIES
17
20
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -726,7 +729,7 @@ function printHelp() {
726
729
  // MAIN BADGE FUNCTION
727
730
  // ═══════════════════════════════════════════════════════════════════════════════
728
731
 
729
- async function runBadge(args) {
732
+ async function runBadge(args, context = {}) {
730
733
  const opts = parseArgs(args);
731
734
 
732
735
  if (opts.help) {
@@ -740,10 +743,15 @@ async function runBadge(args) {
740
743
  return 0;
741
744
  }
742
745
 
746
+ // Get current tier for policy enforcement
747
+ const authInfo = context.authInfo || {};
748
+ const tier = authInfo.access?.tier || await entitlements.getTier({ apiKey: authInfo.key });
749
+
743
750
  // Load last scan result
744
751
  let verdict = opts.verdict || 'SHIP';
745
752
  let score = opts.score || 100;
746
753
  let stats = { blockers: 0, warnings: 0 };
754
+ let findings = [];
747
755
 
748
756
  const lastScanPath = path.join(process.cwd(), '.vibecheck', 'last_scan.json');
749
757
  const lastShipPath = path.join(process.cwd(), '.vibecheck', 'last_ship.json');
@@ -755,11 +763,12 @@ async function runBadge(args) {
755
763
  if (fs.existsSync(reportPath)) {
756
764
  try {
757
765
  const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
758
- verdict = report.verdict || 'SHIP';
766
+ verdict = report.verdict || report.meta?.verdict || 'SHIP';
767
+ findings = report.findings || [];
759
768
  score = report.score ?? report.vibeScore ?? 100;
760
769
  stats = {
761
- blockers: report.counts?.BLOCK || report.findings?.filter(f => f.severity === 'BLOCK').length || 0,
762
- warnings: report.counts?.WARN || report.findings?.filter(f => f.severity === 'WARN').length || 0,
770
+ blockers: report.counts?.BLOCK || findings.filter(f => f.severity === 'BLOCK').length || 0,
771
+ warnings: report.counts?.WARN || findings.filter(f => f.severity === 'WARN').length || 0,
763
772
  };
764
773
  } catch (e) {
765
774
  // Use defaults
@@ -767,6 +776,24 @@ async function runBadge(args) {
767
776
  }
768
777
  }
769
778
 
779
+ // ═══════════════════════════════════════════════════════════════════════════════
780
+ // BADGE POLICY ENFORCEMENT - SHIP ONLY
781
+ // ═══════════════════════════════════════════════════════════════════════════════
782
+ // Badge is only generated when:
783
+ // 1. User has STARTER+ tier (enforced at CLI level)
784
+ // 2. Last verdict = SHIP
785
+ //
786
+ // If verdict != SHIP, show withheld message with top issues
787
+ if (verdict !== 'SHIP' && !opts.verdict) {
788
+ console.log(BANNER_FULL);
789
+
790
+ // Show badge withheld message with top issues
791
+ console.log(upsell.formatBadgeWithheld(verdict, findings, tier));
792
+
793
+ // Do NOT write badge file
794
+ return verdict === 'BLOCK' ? 2 : 1;
795
+ }
796
+
770
797
  const projectName = path.basename(process.cwd());
771
798
 
772
799
  // Print banner
@@ -10,6 +10,14 @@
10
10
 
11
11
  const fs = require("fs");
12
12
  const path = require("path");
13
+ const {
14
+ generateRunId,
15
+ createJsonOutput,
16
+ writeJsonOutput,
17
+ exitCodeToVerdict,
18
+ verdictToExitCode,
19
+ saveArtifact
20
+ } = require("./lib/cli-output");
13
21
 
14
22
  // ═══════════════════════════════════════════════════════════════════════════════
15
23
  // ADVANCED TERMINAL - ANSI CODES & UTILITIES
@@ -283,9 +291,13 @@ try {
283
291
  // MAIN DOCTOR FUNCTION
284
292
  // ═══════════════════════════════════════════════════════════════════════════════
285
293
 
286
- async function runDoctor(args) {
294
+ async function runDoctor(args, context = {}) {
295
+ // Extract runId from context or generate new one
296
+ const runId = context.runId || generateRunId();
297
+ const startTime = context.startTime || new Date().toISOString();
298
+
287
299
  const opts = parseArgs(args);
288
- const startTime = Date.now();
300
+ const executionStart = Date.now();
289
301
 
290
302
  if (opts.help) {
291
303
  printHelp();
@@ -315,8 +327,51 @@ async function runDoctor(args) {
315
327
  skipNetwork: opts.skipNetwork,
316
328
  saveReport: opts.saveReport,
317
329
  failOnWarn: opts.failOnWarn,
330
+ runId,
318
331
  });
319
- return await doctor.run();
332
+
333
+ const results = await doctor.run();
334
+
335
+ // Apply CLI output conventions
336
+ if (opts.json) {
337
+ const output = createJsonOutput({
338
+ runId,
339
+ command: "doctor",
340
+ startTime,
341
+ exitCode: results.exitCode || 0,
342
+ verdict: exitCodeToVerdict(results.exitCode || 0),
343
+ result: {
344
+ health: results.health || "unknown",
345
+ checks: results.checks || [],
346
+ fixes: results.fixes || [],
347
+ summary: {
348
+ total: results.totalChecks || 0,
349
+ passed: results.passedChecks || 0,
350
+ warnings: results.warnings || 0,
351
+ errors: results.errors || 0,
352
+ }
353
+ },
354
+ tier: "free",
355
+ version: require("../../package.json").version,
356
+ artifacts: results.reportPath ? [{
357
+ type: "report",
358
+ path: results.reportPath,
359
+ description: "Doctor report"
360
+ }] : []
361
+ });
362
+
363
+ writeJsonOutput(output, opts.output);
364
+ }
365
+
366
+ // Save artifacts
367
+ if (results.checks) {
368
+ saveArtifact(runId, "checks", results.checks);
369
+ }
370
+ if (results.fixes) {
371
+ saveArtifact(runId, "fixes", results.fixes);
372
+ }
373
+
374
+ return results.exitCode || 0;
320
375
  }
321
376
 
322
377
  // Legacy fallback
@@ -333,6 +388,9 @@ function parseArgs(args) {
333
388
  dryRun: false,
334
389
  quiet: false,
335
390
  verbose: false,
391
+ strict: false,
392
+ ci: false,
393
+ output: null,
336
394
  categories: null,
337
395
  skipNetwork: false,
338
396
  saveReport: true,
@@ -381,6 +439,17 @@ function parseArgs(args) {
381
439
  case '--fail-on-warn':
382
440
  opts.failOnWarn = true;
383
441
  break;
442
+ case '--strict':
443
+ opts.strict = true;
444
+ break;
445
+ case '--ci':
446
+ opts.ci = true;
447
+ opts.quiet = true;
448
+ break;
449
+ case '--output':
450
+ case '-o':
451
+ opts.output = args[++i];
452
+ break;
384
453
  default:
385
454
  if (!arg.startsWith('-')) {
386
455
  opts.path = path.resolve(arg);
@@ -35,6 +35,7 @@ const { buildSharePack } = require('./lib/share-pack');
35
35
 
36
36
  // Entitlements enforcement
37
37
  const entitlements = require('./lib/entitlements-v2');
38
+ const upsell = require('./lib/upsell');
38
39
 
39
40
  // ═══════════════════════════════════════════════════════════════════════════════
40
41
  // ADVANCED TERMINAL - ANSI CODES & UTILITIES
@@ -549,6 +550,18 @@ async function runFix(args) {
549
550
  console.log(` ${colors.mission}${ICONS.robot}${c.reset} ${c.bold}vibecheck fix --autopilot --apply${c.reset}`);
550
551
  console.log(` ${c.dim}Loop until SHIP or stuck${c.reset}`);
551
552
  console.log();
553
+
554
+ // Earned upsell: Show if --apply is locked for their tier
555
+ const applyAccess = await entitlements.checkCommand("fix.apply_patches", { projectPath: root });
556
+ if (!applyAccess.allowed) {
557
+ console.log(upsell.formatEarnedUpsell({
558
+ cmd: "fix",
559
+ withheldArtifact: "apply",
560
+ upgradeTier: applyAccess.requiredTier || "complete",
561
+ why: "Apply patches automatically",
562
+ }));
563
+ }
564
+
552
565
  return 0;
553
566
  }
554
567
 
@@ -31,6 +31,9 @@ const {
31
31
  mergeRuntimeResults
32
32
  } = require("./lib/graph");
33
33
 
34
+ // Entitlements enforcement
35
+ const entitlements = require("./lib/entitlements-v2");
36
+
34
37
  // ═══════════════════════════════════════════════════════════════════════════════
35
38
  // ADVANCED TERMINAL - ANSI CODES & UTILITIES
36
39
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -174,6 +177,17 @@ async function runGraph(args) {
174
177
  return 0;
175
178
  }
176
179
 
180
+ // TIER ENFORCEMENT: COMPLETE only
181
+ const access = await entitlements.enforce("graph", {
182
+ projectPath: opts.path || process.cwd(),
183
+ silent: false,
184
+ });
185
+
186
+ if (!access.allowed) {
187
+ console.log(`\n${c.yellow}Tip:${c.reset} The graph command requires COMPLETE tier for advanced proof graph visualization.`);
188
+ return entitlements.EXIT_FEATURE_NOT_ALLOWED;
189
+ }
190
+
177
191
  const root = path.resolve(opts.path || process.cwd());
178
192
  const projectName = path.basename(root);
179
193
  const outDir = path.join(root, ".vibecheck", "graph");