@vibecheckai/cli 3.1.5 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/vibecheck.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // bin/vibecheck.js - World-Class CLI
2
+ // bin/vibecheck.js - World-Class CLI (Refactored)
3
3
  // ═══════════════════════════════════════════════════════════════════════════════
4
4
  // VibeCheck - Proves your app is real
5
5
  // Ship with confidence. Catch fake features before your users do.
@@ -7,38 +7,71 @@
7
7
 
8
8
  "use strict";
9
9
 
10
- const readline = require("readline");
11
- const path = require("path");
12
- const fs = require("fs");
13
- const os = require("os");
14
10
  const { performance } = require("perf_hooks");
15
- const crypto = require("crypto");
11
+ const STARTUP_TIME = performance.now();
16
12
 
17
13
  // ═══════════════════════════════════════════════════════════════════════════════
18
- // PERFORMANCE: Track startup time
14
+ // LAZY LOADING - Defer all requires until needed for fast startup
19
15
  // ═══════════════════════════════════════════════════════════════════════════════
20
- const STARTUP_TIME = performance.now();
16
+ const lazyModules = {};
17
+
18
+ function lazy(name, loader) {
19
+ return () => {
20
+ if (!lazyModules[name]) {
21
+ lazyModules[name] = loader();
22
+ }
23
+ return lazyModules[name];
24
+ };
25
+ }
26
+
27
+ const getFs = lazy("fs", () => require("fs"));
28
+ const getPath = lazy("path", () => require("path"));
29
+ const getOs = lazy("os", () => require("os"));
30
+ const getCrypto = lazy("crypto", () => require("crypto"));
31
+ const getHttps = lazy("https", () => require("https"));
32
+ const getReadline = lazy("readline", () => require("readline"));
21
33
 
22
34
  // ═══════════════════════════════════════════════════════════════════════════════
23
- // VERSION & METADATA
35
+ // VERSION & METADATA (lazy loaded)
24
36
  // ═══════════════════════════════════════════════════════════════════════════════
37
+ let _version = null;
25
38
  function getVersion() {
39
+ if (_version) return _version;
26
40
  try {
41
+ const fs = getFs();
42
+ const path = getPath();
27
43
  const pkgPath = path.join(__dirname, "..", "package.json");
28
44
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
29
- return pkg.version || "0.0.0";
45
+ _version = pkg.version || "0.0.0";
30
46
  } catch {
31
- return "0.0.0";
47
+ _version = "0.0.0";
32
48
  }
49
+ return _version;
33
50
  }
34
51
 
35
- const VERSION = getVersion();
36
52
  const CLI_NAME = "vibecheck";
37
53
  const CONFIG_FILE = ".vibecheckrc";
38
- const CACHE_DIR = path.join(os.homedir(), ".vibecheck");
39
- const STATE_FILE = path.join(CACHE_DIR, "state.json");
40
54
  const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
41
55
 
56
+ // Cache/state paths (computed lazily)
57
+ let _cacheDir = null;
58
+ let _stateFile = null;
59
+ function getCacheDir() {
60
+ if (!_cacheDir) {
61
+ const os = getOs();
62
+ const path = getPath();
63
+ _cacheDir = path.join(os.homedir(), ".vibecheck");
64
+ }
65
+ return _cacheDir;
66
+ }
67
+ function getStateFile() {
68
+ if (!_stateFile) {
69
+ const path = getPath();
70
+ _stateFile = path.join(getCacheDir(), "state.json");
71
+ }
72
+ return _stateFile;
73
+ }
74
+
42
75
  // ═══════════════════════════════════════════════════════════════════════════════
43
76
  // ANSI STYLES - Premium terminal styling with gradient support
44
77
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -47,7 +80,7 @@ const SUPPORTS_TRUECOLOR = SUPPORTS_COLOR && (
47
80
  process.env.COLORTERM === "truecolor" ||
48
81
  process.env.TERM_PROGRAM === "iTerm.app" ||
49
82
  process.env.TERM_PROGRAM === "Apple_Terminal" ||
50
- process.env.WT_SESSION // Windows Terminal
83
+ process.env.WT_SESSION
51
84
  );
52
85
 
53
86
  const c = SUPPORTS_COLOR ? {
@@ -93,7 +126,12 @@ const c = SUPPORTS_COLOR ? {
93
126
  "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", "bgCyan", "bgWhite", "bgGray"
94
127
  ].map(k => [k, ""]));
95
128
 
96
- // Gradient text generator (cyan magenta → yellow)
129
+ // Add no-op rgb functions for non-color mode
130
+ if (!SUPPORTS_COLOR) {
131
+ c.rgb = () => "";
132
+ c.bgRgb = () => "";
133
+ }
134
+
97
135
  function gradient(text, colors = [[0, 255, 255], [255, 0, 255], [255, 255, 0]]) {
98
136
  if (!SUPPORTS_TRUECOLOR) return `${c.cyan}${text}${c.reset}`;
99
137
  const chars = [...text];
@@ -114,7 +152,7 @@ function gradient(text, colors = [[0, 255, 255], [255, 0, 255], [255, 255, 0]])
114
152
  }
115
153
 
116
154
  // ═══════════════════════════════════════════════════════════════════════════════
117
- // UNICODE SYMBOLS - Rich visual indicators
155
+ // UNICODE SYMBOLS
118
156
  // ═══════════════════════════════════════════════════════════════════════════════
119
157
  const SUPPORTS_UNICODE = process.platform !== "win32" || process.env.WT_SESSION || process.env.TERM_PROGRAM;
120
158
 
@@ -150,24 +188,48 @@ const sym = SUPPORTS_UNICODE ? {
150
188
  };
151
189
 
152
190
  // ═══════════════════════════════════════════════════════════════════════════════
153
- // SPINNER & PROGRESS
191
+ // CI DETECTION
154
192
  // ═══════════════════════════════════════════════════════════════════════════════
155
193
  function isCI() {
156
- return !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.RAILWAY_ENVIRONMENT ||
157
- process.env.VERCEL || process.env.NETLIFY || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI ||
158
- process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.RENDER ||
159
- process.env.HEROKU || process.env.CODEBUILD_BUILD_ID || process.env.JENKINS_URL ||
160
- process.env.TEAMCITY_VERSION || process.env.TF_BUILD || !process.stdin.isTTY);
194
+ return !!(
195
+ process.env.CI ||
196
+ process.env.CONTINUOUS_INTEGRATION ||
197
+ process.env.RAILWAY_ENVIRONMENT ||
198
+ process.env.VERCEL ||
199
+ process.env.NETLIFY ||
200
+ process.env.GITHUB_ACTIONS ||
201
+ process.env.GITLAB_CI ||
202
+ process.env.CIRCLECI ||
203
+ process.env.TRAVIS ||
204
+ process.env.BUILDKITE ||
205
+ process.env.RENDER ||
206
+ process.env.HEROKU ||
207
+ process.env.CODEBUILD_BUILD_ID ||
208
+ process.env.JENKINS_URL ||
209
+ process.env.TEAMCITY_VERSION ||
210
+ process.env.TF_BUILD ||
211
+ !process.stdin.isTTY
212
+ );
161
213
  }
162
214
 
215
+ // ═══════════════════════════════════════════════════════════════════════════════
216
+ // SPINNER CLASS
217
+ // ═══════════════════════════════════════════════════════════════════════════════
163
218
  class Spinner {
164
219
  constructor(text = "") {
165
- this.text = text; this.frame = 0; this.interval = null;
166
- this.stream = process.stderr; this.isCI = isCI();
220
+ this.text = text;
221
+ this.frame = 0;
222
+ this.interval = null;
223
+ this.stream = process.stderr;
224
+ this.isCI = isCI();
167
225
  }
226
+
168
227
  start(text) {
169
228
  if (text) this.text = text;
170
- if (this.isCI) { this.stream.write(`${c.dim}${sym.pending}${c.reset} ${this.text}\n`); return this; }
229
+ if (this.isCI) {
230
+ this.stream.write(`${c.dim}${sym.pending}${c.reset} ${this.text}\n`);
231
+ return this;
232
+ }
171
233
  this.interval = setInterval(() => {
172
234
  const spinner = sym.spinner[this.frame % sym.spinner.length];
173
235
  this.stream.write(`\r${c.cyan}${spinner}${c.reset} ${this.text}`);
@@ -175,35 +237,183 @@ class Spinner {
175
237
  }, 80);
176
238
  return this;
177
239
  }
178
- update(text) { this.text = text; if (this.isCI) this.stream.write(` ${c.dim}${sym.arrowRight}${c.reset} ${text}\n`); return this; }
179
- succeed(text) { this.stop(); this.stream.write(`\r${c.green}${sym.success}${c.reset} ${text || this.text}\n`); return this; }
180
- fail(text) { this.stop(); this.stream.write(`\r${c.red}${sym.error}${c.reset} ${text || this.text}\n`); return this; }
181
- warn(text) { this.stop(); this.stream.write(`\r${c.yellow}${sym.warning}${c.reset} ${text || this.text}\n`); return this; }
182
- info(text) { this.stop(); this.stream.write(`\r${c.blue}${sym.info}${c.reset} ${text || this.text}\n`); return this; }
183
- stop() { if (this.interval) { clearInterval(this.interval); this.interval = null; this.stream.write("\r\x1b[K"); } return this; }
184
- }
185
240
 
186
- function stripAnsi(str) { return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, ""); }
241
+ update(text) {
242
+ this.text = text;
243
+ if (this.isCI) this.stream.write(` ${c.dim}${sym.arrowRight}${c.reset} ${text}\n`);
244
+ return this;
245
+ }
246
+
247
+ succeed(text) {
248
+ this.stop();
249
+ this.stream.write(`\r${c.green}${sym.success}${c.reset} ${text || this.text}\n`);
250
+ return this;
251
+ }
252
+
253
+ fail(text) {
254
+ this.stop();
255
+ this.stream.write(`\r${c.red}${sym.error}${c.reset} ${text || this.text}\n`);
256
+ return this;
257
+ }
258
+
259
+ warn(text) {
260
+ this.stop();
261
+ this.stream.write(`\r${c.yellow}${sym.warning}${c.reset} ${text || this.text}\n`);
262
+ return this;
263
+ }
264
+
265
+ info(text) {
266
+ this.stop();
267
+ this.stream.write(`\r${c.blue}${sym.info}${c.reset} ${text || this.text}\n`);
268
+ return this;
269
+ }
270
+
271
+ stop() {
272
+ if (this.interval) {
273
+ clearInterval(this.interval);
274
+ this.interval = null;
275
+ this.stream.write("\r\x1b[K");
276
+ }
277
+ return this;
278
+ }
279
+ }
187
280
 
188
- function ensureCacheDir() { if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true }); }
281
+ // ═══════════════════════════════════════════════════════════════════════════════
282
+ // STATE MANAGEMENT (lazy file I/O)
283
+ // ═══════════════════════════════════════════════════════════════════════════════
284
+ function ensureCacheDir() {
285
+ const fs = getFs();
286
+ const cacheDir = getCacheDir();
287
+ if (!fs.existsSync(cacheDir)) {
288
+ fs.mkdirSync(cacheDir, { recursive: true });
289
+ }
290
+ }
189
291
 
190
292
  function loadState() {
191
- try { if (fs.existsSync(STATE_FILE)) return JSON.parse(fs.readFileSync(STATE_FILE, "utf-8")); } catch {}
192
- return { firstRun: Date.now(), lastRun: null, runCount: 0, lastUpdateCheck: null, latestVersion: null, commandHistory: [], favorites: [] };
293
+ try {
294
+ const fs = getFs();
295
+ const stateFile = getStateFile();
296
+ if (fs.existsSync(stateFile)) {
297
+ return JSON.parse(fs.readFileSync(stateFile, "utf-8"));
298
+ }
299
+ } catch {}
300
+ return {
301
+ firstRun: Date.now(),
302
+ lastRun: null,
303
+ runCount: 0,
304
+ lastUpdateCheck: null,
305
+ latestVersion: null,
306
+ commandHistory: [],
307
+ favorites: [],
308
+ };
193
309
  }
194
310
 
195
- function saveState(state) { try { ensureCacheDir(); fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); } catch {} }
311
+ function saveState(state) {
312
+ try {
313
+ const fs = getFs();
314
+ ensureCacheDir();
315
+ fs.writeFileSync(getStateFile(), JSON.stringify(state, null, 2));
316
+ } catch {}
317
+ }
196
318
 
197
319
  function loadConfig() {
198
- const config = { debug: false, verbose: false, quiet: false, color: true, analytics: true, updateCheck: true, timeout: 30000, maxRetries: 3 };
320
+ const fs = getFs();
321
+ const path = getPath();
322
+
323
+ const config = {
324
+ debug: false,
325
+ verbose: false,
326
+ quiet: false,
327
+ color: true,
328
+ analytics: true,
329
+ updateCheck: true,
330
+ timeout: 30000,
331
+ maxRetries: 3,
332
+ noBanner: false,
333
+ };
334
+
199
335
  const projectConfigPath = path.join(process.cwd(), CONFIG_FILE);
200
- if (fs.existsSync(projectConfigPath)) { try { Object.assign(config, JSON.parse(fs.readFileSync(projectConfigPath, "utf-8"))); } catch {} }
336
+ if (fs.existsSync(projectConfigPath)) {
337
+ try {
338
+ Object.assign(config, JSON.parse(fs.readFileSync(projectConfigPath, "utf-8")));
339
+ } catch {}
340
+ }
341
+
342
+ // Environment overrides
201
343
  if (process.env.VIBECHECK_DEBUG === "true") config.debug = true;
202
344
  if (process.env.VIBECHECK_VERBOSE === "true") config.verbose = true;
345
+ if (process.env.VIBECHECK_QUIET === "true") config.quiet = true;
346
+ if (process.env.VIBECHECK_NO_BANNER === "true") config.noBanner = true;
203
347
  if (process.env.NO_COLOR || process.env.VIBECHECK_NO_COLOR) config.color = false;
348
+
204
349
  return config;
205
350
  }
206
351
 
352
+ // ═══════════════════════════════════════════════════════════════════════════════
353
+ // UPDATE CHECKER - Actually implemented
354
+ // ═══════════════════════════════════════════════════════════════════════════════
355
+ async function checkForUpdates(state, config) {
356
+ if (!config.updateCheck) return null;
357
+ if (isCI()) return null;
358
+
359
+ const now = Date.now();
360
+ if (state.lastUpdateCheck && (now - state.lastUpdateCheck) < UPDATE_CHECK_INTERVAL) {
361
+ // Use cached version if recent
362
+ if (state.latestVersion && state.latestVersion !== getVersion()) {
363
+ return state.latestVersion;
364
+ }
365
+ return null;
366
+ }
367
+
368
+ // Async check - don't block CLI startup
369
+ return new Promise((resolve) => {
370
+ const https = getHttps();
371
+ const timeout = setTimeout(() => resolve(null), 3000); // 3s timeout
372
+
373
+ const req = https.get(
374
+ "https://registry.npmjs.org/@vibecheckai/cli/latest",
375
+ { headers: { "Accept": "application/json", "User-Agent": `vibecheck-cli/${getVersion()}` } },
376
+ (res) => {
377
+ let data = "";
378
+ res.on("data", (chunk) => { data += chunk; });
379
+ res.on("end", () => {
380
+ clearTimeout(timeout);
381
+ try {
382
+ const pkg = JSON.parse(data);
383
+ const latest = pkg.version;
384
+ state.lastUpdateCheck = now;
385
+ state.latestVersion = latest;
386
+ saveState(state);
387
+
388
+ if (latest && latest !== getVersion()) {
389
+ resolve(latest);
390
+ } else {
391
+ resolve(null);
392
+ }
393
+ } catch {
394
+ resolve(null);
395
+ }
396
+ });
397
+ }
398
+ );
399
+
400
+ req.on("error", () => {
401
+ clearTimeout(timeout);
402
+ resolve(null);
403
+ });
404
+
405
+ req.end();
406
+ });
407
+ }
408
+
409
+ function printUpdateNotice(latestVersion) {
410
+ const currentVersion = getVersion();
411
+ console.log();
412
+ console.log(`${c.yellow}${sym.sparkles}${c.reset} Update available: ${c.dim}${currentVersion}${c.reset} ${sym.arrow} ${c.green}${latestVersion}${c.reset}`);
413
+ console.log(` Run ${c.cyan}npm update -g @vibecheckai/cli${c.reset} to update`);
414
+ console.log();
415
+ }
416
+
207
417
  // ═══════════════════════════════════════════════════════════════════════════════
208
418
  // FUZZY MATCHING
209
419
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -213,8 +423,15 @@ function levenshtein(a, b) {
213
423
  for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
214
424
  for (let i = 1; i <= b.length; i++) {
215
425
  for (let j = 1; j <= a.length; j++) {
216
- if (b.charAt(i - 1) === a.charAt(j - 1)) matrix[i][j] = matrix[i - 1][j - 1];
217
- else matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
426
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
427
+ matrix[i][j] = matrix[i - 1][j - 1];
428
+ } else {
429
+ matrix[i][j] = Math.min(
430
+ matrix[i - 1][j - 1] + 1,
431
+ matrix[i][j - 1] + 1,
432
+ matrix[i - 1][j] + 1
433
+ );
434
+ }
218
435
  }
219
436
  }
220
437
  return matrix[b.length][a.length];
@@ -224,63 +441,89 @@ function findSimilarCommands(input, commands, maxDistance = 3) {
224
441
  const matches = [];
225
442
  for (const cmd of commands) {
226
443
  const distance = levenshtein(input.toLowerCase(), cmd.toLowerCase());
227
- if (distance <= maxDistance) matches.push({ cmd, distance });
228
- if (cmd.toLowerCase().startsWith(input.toLowerCase()) && input.length >= 2) matches.push({ cmd, distance: 0.5 });
444
+ if (distance <= maxDistance) {
445
+ matches.push({ cmd, distance });
446
+ }
447
+ if (cmd.toLowerCase().startsWith(input.toLowerCase()) && input.length >= 2) {
448
+ matches.push({ cmd, distance: 0.5 });
449
+ }
229
450
  }
230
- return matches.sort((a, b) => a.distance - b.distance).map(m => m.cmd).slice(0, 3);
451
+ return [...new Set(matches.sort((a, b) => a.distance - b.distance).map(m => m.cmd))].slice(0, 3);
231
452
  }
232
453
 
233
454
  // ═══════════════════════════════════════════════════════════════════════════════
234
- // ENTITLEMENTS (v2 - Single Source of Truth)
455
+ // COMMAND REGISTRY (lazy loaded)
235
456
  // ═══════════════════════════════════════════════════════════════════════════════
236
- const entitlements = require("./runners/lib/entitlements-v2");
457
+ let _registry = null;
458
+ function getRegistry() {
459
+ if (!_registry) {
460
+ _registry = require("./registry");
461
+ }
462
+ return _registry;
463
+ }
464
+
465
+ function getRunner(cmd) {
466
+ const registry = getRegistry();
467
+ return registry.getRunner(cmd, { red: c.red, reset: c.reset, errorSymbol: sym.error });
468
+ }
237
469
 
238
470
  // ═══════════════════════════════════════════════════════════════════════════════
239
- // UPSELL COPY MODULE - Central copy generator for all upgrade messaging
471
+ // ENTITLEMENTS & UPSELL (lazy loaded)
240
472
  // ═══════════════════════════════════════════════════════════════════════════════
241
- const upsell = require("./runners/lib/upsell");
473
+ let _entitlements = null;
474
+ let _upsell = null;
475
+
476
+ function getEntitlements() {
477
+ if (!_entitlements) {
478
+ _entitlements = require("./runners/lib/entitlements-v2");
479
+ }
480
+ return _entitlements;
481
+ }
482
+
483
+ function getUpsell() {
484
+ if (!_upsell) {
485
+ _upsell = require("./runners/lib/upsell");
486
+ }
487
+ return _upsell;
488
+ }
242
489
 
243
490
  // ═══════════════════════════════════════════════════════════════════════════════
244
- // CLI OUTPUT UTILITIES
491
+ // CLI OUTPUT UTILITIES (lazy loaded)
245
492
  // ═══════════════════════════════════════════════════════════════════════════════
246
- const {
247
- generateRunId,
248
- withStandardOutput,
249
- parseStandardFlags,
250
- exitCodeToVerdict
251
- } = require("./runners/lib/cli-output");
493
+ let _cliOutput = null;
494
+ function getCliOutput() {
495
+ if (!_cliOutput) {
496
+ _cliOutput = require("./runners/lib/cli-output");
497
+ }
498
+ return _cliOutput;
499
+ }
252
500
 
253
501
  // ═══════════════════════════════════════════════════════════════════════════════
254
- // COMMAND REGISTRY - Imported from bin/registry.js (single source of truth)
502
+ // AUTH MODULE (lazy loaded)
255
503
  // ═══════════════════════════════════════════════════════════════════════════════
256
- const { COMMANDS, ALIAS_MAP, ALL_COMMANDS, getRunner: _getRunner } = require("./registry");
257
-
258
- // Wrap getRunner to pass styling
259
- function getRunner(cmd) {
260
- return _getRunner(cmd, { red: c.red, reset: c.reset, errorSymbol: sym.error });
504
+ let _authModule = null;
505
+ function getAuthModule() {
506
+ if (!_authModule) {
507
+ _authModule = require("./runners/lib/auth");
508
+ }
509
+ return _authModule;
261
510
  }
262
511
 
263
512
  // ═══════════════════════════════════════════════════════════════════════════════
264
- // AUTH & ACCESS CONTROL (uses entitlements-v2 - NO BYPASS ALLOWED)
513
+ // ACCESS CONTROL
265
514
  // ═══════════════════════════════════════════════════════════════════════════════
266
- let authModule = null;
267
- function getAuthModule() { if (!authModule) authModule = require("./runners/lib/auth"); return authModule; }
268
-
269
- /**
270
- * Check command access using entitlements-v2 module.
271
- * NO OWNER MODE. NO ENV VAR BYPASS. NO OFFLINE ESCALATION.
272
- */
273
515
  async function checkCommandAccess(cmd, args, authInfo) {
274
- const def = COMMANDS[cmd];
516
+ const registry = getRegistry();
517
+ const def = registry.COMMANDS[cmd];
275
518
  if (!def) return { allowed: true };
276
-
277
- // Use centralized entitlements enforcement
519
+
520
+ const entitlements = getEntitlements();
278
521
  const result = await entitlements.enforce(cmd, {
279
522
  apiKey: authInfo?.key,
280
523
  projectPath: process.cwd(),
281
- silent: true, // We'll handle messaging ourselves
524
+ silent: true,
282
525
  });
283
-
526
+
284
527
  if (result.allowed) {
285
528
  return {
286
529
  allowed: true,
@@ -290,29 +533,272 @@ async function checkCommandAccess(cmd, args, authInfo) {
290
533
  caps: result.caps,
291
534
  };
292
535
  }
293
-
294
- // Not allowed - return with proper exit code
536
+
537
+ const upsell = getUpsell();
295
538
  return {
296
539
  allowed: false,
297
540
  tier: result.tier,
298
541
  requiredTier: result.requiredTier,
299
542
  exitCode: result.exitCode,
300
- reason: formatAccessDenied(cmd, result.requiredTier, result.tier),
543
+ reason: upsell.formatDenied(cmd, {
544
+ currentTier: result.tier,
545
+ requiredTier: result.requiredTier,
546
+ }),
301
547
  };
302
548
  }
303
549
 
304
- function formatAccessDenied(cmd, requiredTier, currentTier) {
305
- // Use centralized upsell copy module
306
- return upsell.formatDenied(cmd, {
307
- currentTier,
308
- requiredTier,
309
- });
550
+ // ═══════════════════════════════════════════════════════════════════════════════
551
+ // SHELL COMPLETIONS
552
+ // ═══════════════════════════════════════════════════════════════════════════════
553
+ function generateBashCompletion() {
554
+ const registry = getRegistry();
555
+ const commands = Object.keys(registry.COMMANDS);
556
+ const aliases = Object.keys(registry.ALIAS_MAP);
557
+ const allCmds = [...commands, ...aliases].join(" ");
558
+
559
+ return `# vibecheck bash completion
560
+ # Add to ~/.bashrc or ~/.bash_profile:
561
+ # eval "$(vibecheck completion bash)"
562
+
563
+ _vibecheck_completions() {
564
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
565
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
566
+
567
+ # Main commands
568
+ local commands="${allCmds}"
569
+
570
+ # Global flags
571
+ local global_flags="--help --version --json --ci --quiet --verbose --debug --path --output --no-banner"
572
+
573
+ case "\${prev}" in
574
+ vibecheck|vc)
575
+ COMPREPLY=($(compgen -W "\${commands} \${global_flags}" -- "\${cur}"))
576
+ return 0
577
+ ;;
578
+ --path|--output|-p|-o)
579
+ COMPREPLY=($(compgen -d -- "\${cur}"))
580
+ return 0
581
+ ;;
582
+ init)
583
+ COMPREPLY=($(compgen -W "--local --connect --quick --enterprise --ci --soc2 --hipaa --gdpr --team --mcp" -- "\${cur}"))
584
+ return 0
585
+ ;;
586
+ scan)
587
+ COMPREPLY=($(compgen -W "--fix --autofix --json --strict --path" -- "\${cur}"))
588
+ return 0
589
+ ;;
590
+ report)
591
+ COMPREPLY=($(compgen -W "--format --output html md json sarif csv" -- "\${cur}"))
592
+ return 0
593
+ ;;
594
+ completion)
595
+ COMPREPLY=($(compgen -W "bash zsh fish" -- "\${cur}"))
596
+ return 0
597
+ ;;
598
+ esac
599
+
600
+ if [[ "\${cur}" == -* ]]; then
601
+ COMPREPLY=($(compgen -W "\${global_flags}" -- "\${cur}"))
602
+ fi
603
+ }
604
+
605
+ complete -F _vibecheck_completions vibecheck
606
+ complete -F _vibecheck_completions vc
607
+ `;
608
+ }
609
+
610
+ function generateZshCompletion() {
611
+ const registry = getRegistry();
612
+ const commands = Object.entries(registry.COMMANDS);
613
+
614
+ let cmdList = commands.map(([cmd, def]) => {
615
+ const desc = (def.description || "").replace(/'/g, "\\'").replace(/"/g, '\\"');
616
+ return ` '${cmd}:${desc}'`;
617
+ }).join(" \\\n");
618
+
619
+ return `#compdef vibecheck vc
620
+ # vibecheck zsh completion
621
+ # Add to ~/.zshrc:
622
+ # eval "$(vibecheck completion zsh)"
623
+
624
+ _vibecheck() {
625
+ local -a commands
626
+ commands=(
627
+ ${cmdList}
628
+ )
629
+
630
+ local -a global_opts
631
+ global_opts=(
632
+ '--help[Show help]'
633
+ '--version[Show version]'
634
+ '--json[Output as JSON]'
635
+ '--ci[CI mode]'
636
+ '--quiet[Suppress output]'
637
+ '--verbose[Verbose output]'
638
+ '--debug[Debug mode]'
639
+ '--path[Project path]:directory:_directories'
640
+ '--output[Output directory]:directory:_directories'
641
+ '--no-banner[Hide banner]'
642
+ )
643
+
644
+ _arguments -C \\
645
+ "1: :->command" \\
646
+ "*::arg:->args"
647
+
648
+ case "\$state" in
649
+ command)
650
+ _describe -t commands 'vibecheck commands' commands
651
+ _describe -t options 'options' global_opts
652
+ ;;
653
+ args)
654
+ case "\$words[1]" in
655
+ init)
656
+ _arguments \\
657
+ '--local[Local setup only]' \\
658
+ '--connect[GitHub integration]' \\
659
+ '--quick[Quick setup]' \\
660
+ '--enterprise[Enterprise mode]' \\
661
+ '--ci[Setup CI/CD]' \\
662
+ '--soc2[SOC2 compliance]' \\
663
+ '--hipaa[HIPAA compliance]'
664
+ ;;
665
+ scan)
666
+ _arguments \\
667
+ '--fix[Apply fixes]' \\
668
+ '--autofix[Auto-fix issues]' \\
669
+ '--json[JSON output]' \\
670
+ '--strict[Strict mode]'
671
+ ;;
672
+ report)
673
+ _arguments \\
674
+ '--format[Output format]:format:(html md json sarif csv)' \\
675
+ '--output[Output file]:file:_files'
676
+ ;;
677
+ completion)
678
+ _arguments '1:shell:(bash zsh fish)'
679
+ ;;
680
+ esac
681
+ ;;
682
+ esac
683
+ }
684
+
685
+ compdef _vibecheck vibecheck
686
+ compdef _vibecheck vc
687
+ `;
688
+ }
689
+
690
+ function generateFishCompletion() {
691
+ const registry = getRegistry();
692
+ const commands = Object.entries(registry.COMMANDS);
693
+
694
+ let completions = `# vibecheck fish completion
695
+ # Add to ~/.config/fish/completions/vibecheck.fish
696
+ # Or run: vibecheck completion fish > ~/.config/fish/completions/vibecheck.fish
697
+
698
+ # Disable file completion by default
699
+ complete -c vibecheck -f
700
+ complete -c vc -f
701
+
702
+ # Global flags
703
+ complete -c vibecheck -l help -d 'Show help'
704
+ complete -c vibecheck -l version -d 'Show version'
705
+ complete -c vibecheck -l json -d 'Output as JSON'
706
+ complete -c vibecheck -l ci -d 'CI mode'
707
+ complete -c vibecheck -l quiet -d 'Suppress output'
708
+ complete -c vibecheck -l verbose -d 'Verbose output'
709
+ complete -c vibecheck -l debug -d 'Debug mode'
710
+ complete -c vibecheck -l path -d 'Project path' -r -a '(__fish_complete_directories)'
711
+ complete -c vibecheck -l output -d 'Output directory' -r -a '(__fish_complete_directories)'
712
+ complete -c vibecheck -l no-banner -d 'Hide banner'
713
+
714
+ # Commands
715
+ `;
716
+
717
+ for (const [cmd, def] of commands) {
718
+ const desc = (def.description || "").replace(/'/g, "\\'");
719
+ completions += `complete -c vibecheck -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'\n`;
720
+ completions += `complete -c vc -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'\n`;
721
+ }
722
+
723
+ // Add aliases
724
+ for (const [alias, target] of Object.entries(registry.ALIAS_MAP)) {
725
+ const def = registry.COMMANDS[target];
726
+ if (def) {
727
+ const desc = `Alias for ${target}`;
728
+ completions += `complete -c vibecheck -n '__fish_use_subcommand' -a '${alias}' -d '${desc}'\n`;
729
+ completions += `complete -c vc -n '__fish_use_subcommand' -a '${alias}' -d '${desc}'\n`;
730
+ }
731
+ }
732
+
733
+ completions += `
734
+ # init subcommand options
735
+ complete -c vibecheck -n '__fish_seen_subcommand_from init' -l local -d 'Local setup only'
736
+ complete -c vibecheck -n '__fish_seen_subcommand_from init' -l connect -d 'GitHub integration'
737
+ complete -c vibecheck -n '__fish_seen_subcommand_from init' -l quick -d 'Quick setup'
738
+ complete -c vibecheck -n '__fish_seen_subcommand_from init' -l enterprise -d 'Enterprise mode'
739
+
740
+ # scan subcommand options
741
+ complete -c vibecheck -n '__fish_seen_subcommand_from scan' -l fix -d 'Apply fixes'
742
+ complete -c vibecheck -n '__fish_seen_subcommand_from scan' -l autofix -d 'Auto-fix issues'
743
+ complete -c vibecheck -n '__fish_seen_subcommand_from scan' -l strict -d 'Strict mode'
744
+
745
+ # report subcommand options
746
+ complete -c vibecheck -n '__fish_seen_subcommand_from report' -l format -d 'Output format' -r -a 'html md json sarif csv'
747
+
748
+ # completion subcommand
749
+ complete -c vibecheck -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell type'
750
+ `;
751
+
752
+ return completions;
753
+ }
754
+
755
+ function runCompletion(args) {
756
+ const shell = args[0];
757
+
758
+ if (!shell || shell === "--help" || shell === "-h") {
759
+ console.log(`
760
+ ${c.bold}Usage:${c.reset} vibecheck completion <shell>
761
+
762
+ ${c.bold}Shells:${c.reset}
763
+ bash Generate bash completions
764
+ zsh Generate zsh completions
765
+ fish Generate fish completions
766
+
767
+ ${c.bold}Installation:${c.reset}
768
+ ${c.dim}# Bash (add to ~/.bashrc)${c.reset}
769
+ eval "$(vibecheck completion bash)"
770
+
771
+ ${c.dim}# Zsh (add to ~/.zshrc)${c.reset}
772
+ eval "$(vibecheck completion zsh)"
773
+
774
+ ${c.dim}# Fish${c.reset}
775
+ vibecheck completion fish > ~/.config/fish/completions/vibecheck.fish
776
+ `);
777
+ return 0;
778
+ }
779
+
780
+ switch (shell.toLowerCase()) {
781
+ case "bash":
782
+ console.log(generateBashCompletion());
783
+ return 0;
784
+ case "zsh":
785
+ console.log(generateZshCompletion());
786
+ return 0;
787
+ case "fish":
788
+ console.log(generateFishCompletion());
789
+ return 0;
790
+ default:
791
+ console.error(`${c.red}${sym.error}${c.reset} Unknown shell: ${shell}`);
792
+ console.error(`Supported shells: bash, zsh, fish`);
793
+ return 1;
794
+ }
310
795
  }
311
796
 
312
797
  // ═══════════════════════════════════════════════════════════════════════════════
313
798
  // HELP SYSTEM
314
799
  // ═══════════════════════════════════════════════════════════════════════════════
315
800
  function printBanner() {
801
+ const VERSION = getVersion();
316
802
  console.log(`
317
803
  ${c.dim}${sym.boxTopLeft}${sym.boxHorizontal.repeat(60)}${sym.boxTopRight}${c.reset}
318
804
  ${c.dim}${sym.boxVertical}${c.reset} ${gradient("VIBECHECK", [[0, 255, 255], [138, 43, 226], [255, 20, 147]])} ${c.dim}v${VERSION}${c.reset}${" ".repeat(60 - 13 - VERSION.length - 4)}${c.dim}${sym.boxVertical}${c.reset}
@@ -321,9 +807,19 @@ ${c.dim}${sym.boxBottomLeft}${sym.boxHorizontal.repeat(60)}${sym.boxBottomRight}
321
807
  `);
322
808
  }
323
809
 
324
- function printHelp() {
325
- printBanner();
326
-
810
+ function printHelp(showBanner = true) {
811
+ if (showBanner) printBanner();
812
+
813
+ const registry = getRegistry();
814
+ const { COMMANDS, ALIAS_MAP } = registry;
815
+
816
+ // Build reverse alias map for display
817
+ const reverseAliases = {};
818
+ for (const [alias, target] of Object.entries(ALIAS_MAP)) {
819
+ if (!reverseAliases[target]) reverseAliases[target] = [];
820
+ reverseAliases[target].push(alias);
821
+ }
822
+
327
823
  // Categories ordered as specified
328
824
  const categoryOrder = ["setup", "analysis", "proof", "quality", "truth", "output", "ci", "automation", "account", "extras"];
329
825
  const categories = {
@@ -338,7 +834,7 @@ function printHelp() {
338
834
  account: { name: "ACCOUNT", color: c.dim, icon: sym.key },
339
835
  extras: { name: "EXTRAS", color: c.dim, icon: sym.starEmpty },
340
836
  };
341
-
837
+
342
838
  // Group commands
343
839
  const grouped = {};
344
840
  for (const [cmd, def] of Object.entries(COMMANDS)) {
@@ -346,33 +842,35 @@ function printHelp() {
346
842
  if (!grouped[cat]) grouped[cat] = [];
347
843
  grouped[cat].push({ cmd, ...def });
348
844
  }
349
-
845
+
350
846
  // Print in order
351
847
  for (const catKey of categoryOrder) {
352
848
  const commands = grouped[catKey];
353
849
  if (!commands || commands.length === 0) continue;
354
-
850
+
355
851
  const cat = categories[catKey];
356
852
  console.log(`\n${cat.color}${cat.icon} ${cat.name}${c.reset}\n`);
357
-
358
- for (const { cmd, description, tier, aliases, caps } of commands) {
359
- // Tier badge with color
853
+
854
+ for (const { cmd, description, tier, caps } of commands) {
855
+ // Tier badge
360
856
  let tierBadge = "";
361
- if (tier === "free") {
362
- tierBadge = `${c.green}[FREE]${c.reset} `;
363
- } else if (tier === "starter") {
364
- tierBadge = `${c.cyan}[STARTER]${c.reset} `;
365
- } else if (tier === "pro") {
366
- tierBadge = `${c.magenta}[PRO]${c.reset} `;
367
- }
368
-
369
- // Caps info (e.g., "preview mode on FREE")
857
+ if (tier === "free") tierBadge = `${c.green}[FREE]${c.reset} `;
858
+ else if (tier === "starter") tierBadge = `${c.cyan}[STARTER]${c.reset} `;
859
+ else if (tier === "pro") tierBadge = `${c.magenta}[PRO]${c.reset} `;
860
+
861
+ // Aliases (from reverseAliases map built earlier)
862
+ const aliasList = reverseAliases[cmd] || [];
863
+ const aliasStr = aliasList.length > 0
864
+ ? `${c.dim}(${aliasList.slice(0, 3).join(", ")})${c.reset} `
865
+ : "";
866
+
867
+ // Caps info
370
868
  const capsStr = caps ? `${c.dim}(${caps})${c.reset}` : "";
371
-
372
- console.log(` ${c.cyan}${cmd.padEnd(12)}${c.reset} ${tierBadge}${description} ${capsStr}`);
869
+
870
+ console.log(` ${c.cyan}${cmd.padEnd(12)}${c.reset} ${aliasStr}${tierBadge}${description} ${capsStr}`);
373
871
  }
374
872
  }
375
-
873
+
376
874
  console.log(`
377
875
  ${c.dim}${sym.boxHorizontal.repeat(64)}${c.reset}
378
876
 
@@ -390,6 +888,12 @@ ${c.green}QUICK START - The 5-Step Journey${c.reset}
390
888
  4. ${c.bold}Prove${c.reset} ${c.cyan}vibecheck prove${c.reset} ${c.magenta}[PRO]${c.reset}
391
889
  5. ${c.bold}Ship${c.reset} ${c.cyan}vibecheck ship${c.reset}
392
890
 
891
+ ${c.bold}SHELL COMPLETIONS${c.reset}
892
+
893
+ ${c.cyan}vibecheck completion bash${c.reset} ${c.dim}# Add to ~/.bashrc${c.reset}
894
+ ${c.cyan}vibecheck completion zsh${c.reset} ${c.dim}# Add to ~/.zshrc${c.reset}
895
+ ${c.cyan}vibecheck completion fish${c.reset} ${c.dim}# Save to completions dir${c.reset}
896
+
393
897
  ${c.dim}Run 'vibecheck <command> --help' for command-specific help.${c.reset}
394
898
  ${c.dim}Pricing: https://vibecheckai.dev/pricing${c.reset}
395
899
  `);
@@ -406,58 +910,165 @@ function getArgValue(args, flags) {
406
910
  return undefined;
407
911
  }
408
912
 
913
+ function hasFlag(args, flags) {
914
+ for (const flag of flags) {
915
+ if (args.includes(flag)) return true;
916
+ }
917
+ return false;
918
+ }
919
+
409
920
  function formatError(error, config) {
410
921
  const lines = [`${c.red}${sym.error} Error:${c.reset} ${error.message}`];
411
- if (config.debug && error.stack) { lines.push("", `${c.dim}Stack trace:${c.reset}`, c.dim + error.stack.split("\n").slice(1).join("\n") + c.reset); }
922
+ if (config.debug && error.stack) {
923
+ lines.push("", `${c.dim}Stack trace:${c.reset}`, c.dim + error.stack.split("\n").slice(1).join("\n") + c.reset);
924
+ }
412
925
  return lines.join("\n");
413
926
  }
414
927
 
928
+ // ═══════════════════════════════════════════════════════════════════════════════
929
+ // FLAG PARSING
930
+ // ═══════════════════════════════════════════════════════════════════════════════
931
+ function parseGlobalFlags(rawArgs) {
932
+ const flags = {
933
+ help: false,
934
+ version: false,
935
+ json: false,
936
+ ci: false,
937
+ quiet: false,
938
+ verbose: false,
939
+ debug: false,
940
+ strict: false,
941
+ noBanner: false,
942
+ path: process.cwd(),
943
+ output: null,
944
+ };
945
+
946
+ const cleanArgs = [];
947
+ let i = 0;
948
+
949
+ while (i < rawArgs.length) {
950
+ const arg = rawArgs[i];
951
+
952
+ switch (arg) {
953
+ case "--help":
954
+ case "-h":
955
+ flags.help = true;
956
+ break;
957
+ case "--version":
958
+ case "-v":
959
+ flags.version = true;
960
+ break;
961
+ case "--json":
962
+ flags.json = true;
963
+ break;
964
+ case "--ci":
965
+ flags.ci = true;
966
+ break;
967
+ case "--quiet":
968
+ case "-q":
969
+ flags.quiet = true;
970
+ break;
971
+ case "--verbose":
972
+ flags.verbose = true;
973
+ break;
974
+ case "--debug":
975
+ flags.debug = true;
976
+ break;
977
+ case "--strict":
978
+ flags.strict = true;
979
+ break;
980
+ case "--no-banner":
981
+ flags.noBanner = true;
982
+ break;
983
+ case "--path":
984
+ case "-p":
985
+ flags.path = rawArgs[++i] || process.cwd();
986
+ break;
987
+ case "--output":
988
+ case "-o":
989
+ flags.output = rawArgs[++i] || null;
990
+ break;
991
+ default:
992
+ if (arg.startsWith("--path=")) {
993
+ flags.path = arg.split("=")[1];
994
+ } else if (arg.startsWith("--output=")) {
995
+ flags.output = arg.split("=")[1];
996
+ } else {
997
+ cleanArgs.push(arg);
998
+ }
999
+ }
1000
+ i++;
1001
+ }
1002
+
1003
+ return { flags, cleanArgs };
1004
+ }
1005
+
415
1006
  // ═══════════════════════════════════════════════════════════════════════════════
416
1007
  // MAIN ENTRY POINT
417
1008
  // ═══════════════════════════════════════════════════════════════════════════════
418
1009
  async function main() {
419
1010
  const startTime = performance.now();
420
1011
  const rawArgs = process.argv.slice(2);
1012
+
1013
+ // Parse global flags
1014
+ const { flags: globalFlags, cleanArgs } = parseGlobalFlags(rawArgs);
1015
+
1016
+ // Handle version (fast path - no config loading)
1017
+ if (globalFlags.version) {
1018
+ console.log(`vibecheck v${getVersion()}`);
1019
+ process.exit(0);
1020
+ }
1021
+
1022
+ // Load config and state
421
1023
  const config = loadConfig();
422
1024
  const state = loadState();
423
-
424
- // Parse standard flags
425
- const { flags: globalFlags, parsed: cleanArgs } = parseStandardFlags(rawArgs);
426
-
427
- // Update config based on flags
1025
+
1026
+ // Apply flag overrides to config
428
1027
  if (globalFlags.debug) config.debug = true;
429
1028
  if (globalFlags.verbose) config.verbose = true;
430
1029
  if (globalFlags.quiet) config.quiet = true;
431
- if (globalFlags.ci) config.quiet = true; // CI mode implies quiet
432
-
433
- // Handle version
434
- if (globalFlags.version) {
435
- console.log(`vibecheck v${VERSION}`);
436
- process.exit(0);
1030
+ if (globalFlags.noBanner) config.noBanner = true;
1031
+ if (globalFlags.ci) {
1032
+ config.quiet = true;
1033
+ config.noBanner = true;
437
1034
  }
438
-
1035
+
439
1036
  // Handle no command
440
- if (!cleanArgs[0]) { printHelp(); process.exit(0); }
441
-
442
- // Handle command-specific help (vibecheck <cmd> --help)
443
- if (globalFlags.help && cleanArgs[0] && COMMANDS[cleanArgs[0]]) {
444
- // Pass --help to the command runner
445
- } else if (globalFlags.help && !cleanArgs[0]) {
446
- printHelp(); process.exit(0);
1037
+ if (!cleanArgs[0]) {
1038
+ printHelp(!config.noBanner);
1039
+ process.exit(0);
447
1040
  }
448
-
1041
+
1042
+ // Handle global help
1043
+ if (globalFlags.help && !cleanArgs[0]) {
1044
+ printHelp(!config.noBanner);
1045
+ process.exit(0);
1046
+ }
1047
+
1048
+ // Handle completion command (special - doesn't need registry loaded normally)
1049
+ if (cleanArgs[0] === "completion") {
1050
+ process.exit(runCompletion(cleanArgs.slice(1)));
1051
+ }
1052
+
1053
+ // Load registry for command resolution
1054
+ const registry = getRegistry();
1055
+ const { COMMANDS, ALIAS_MAP, ALL_COMMANDS } = registry;
1056
+
449
1057
  let cmd = cleanArgs[0];
450
- // Only use alias if exact command doesn't exist (prefer exact matches)
451
- if (!COMMANDS[cmd] && ALIAS_MAP[cmd]) { cmd = ALIAS_MAP[cmd]; }
1058
+ // Prefer exact match, then alias
1059
+ if (!COMMANDS[cmd] && ALIAS_MAP[cmd]) {
1060
+ cmd = ALIAS_MAP[cmd];
1061
+ }
452
1062
  let cmdArgs = cleanArgs.slice(1);
453
-
454
- // Add --help back to cmdArgs if it was passed with a command
1063
+
1064
+ // Pass --help to runner if specified with command
455
1065
  if (globalFlags.help) cmdArgs = ["--help", ...cmdArgs];
456
-
1066
+
457
1067
  // Pass standard flags to runners
458
- cmdArgs = [...cmdArgs];
459
1068
  if (globalFlags.json) cmdArgs.push("--json");
460
1069
  if (globalFlags.ci) cmdArgs.push("--ci");
1070
+ if (globalFlags.noBanner) cmdArgs.push("--no-banner");
1071
+ if (globalFlags.quiet) cmdArgs.push("--quiet");
461
1072
  if (globalFlags.path && globalFlags.path !== process.cwd()) {
462
1073
  cmdArgs.push("--path", globalFlags.path);
463
1074
  }
@@ -466,114 +1077,149 @@ async function main() {
466
1077
  }
467
1078
  if (globalFlags.verbose) cmdArgs.push("--verbose");
468
1079
  if (globalFlags.strict) cmdArgs.push("--strict");
469
-
1080
+
1081
+ // Unknown command
470
1082
  if (!COMMANDS[cmd]) {
471
1083
  const suggestions = findSimilarCommands(cmd, ALL_COMMANDS);
472
1084
  console.log(`\n${c.red}${sym.error}${c.reset} Unknown command: ${c.yellow}${cmd}${c.reset}`);
473
-
474
- // Check for specific common misses
1085
+
475
1086
  if (cmd === "replay" || cmd === "record") {
476
1087
  console.log(`\n${c.dim}replay is a PRO feature for session recording.${c.reset}`);
477
1088
  console.log(`${c.dim}Free alternative:${c.reset} ${c.cyan}vibecheck reality${c.reset} ${c.dim}(one-time runtime proof)${c.reset}`);
478
1089
  } else if (suggestions.length > 0) {
479
1090
  console.log(`\n${c.dim}Did you mean:${c.reset}`);
480
- suggestions.forEach(s => { const actual = ALIAS_MAP[s] || s; const def = COMMANDS[actual]; console.log(` ${c.cyan}vibecheck ${s}${c.reset} ${c.dim}${def?.description || ""}${c.reset}`); });
1091
+ suggestions.forEach((s) => {
1092
+ const actual = ALIAS_MAP[s] || s;
1093
+ const def = COMMANDS[actual];
1094
+ console.log(` ${c.cyan}vibecheck ${s}${c.reset} ${c.dim}${def?.description || ""}${c.reset}`);
1095
+ });
481
1096
  }
482
1097
  console.log(`\n${c.dim}Run 'vibecheck --help' for available commands.${c.reset}\n`);
483
- process.exit(4); // INVALID_INPUT
1098
+ process.exit(4);
484
1099
  }
485
-
486
- // Generate runId for tracking
487
- const runId = generateRunId();
1100
+
1101
+ // Generate runId
1102
+ const cliOutput = getCliOutput();
1103
+ const runId = cliOutput.generateRunId();
488
1104
  const runStart = new Date().toISOString();
489
-
1105
+
490
1106
  const cmdDef = COMMANDS[cmd];
491
1107
  let authInfo = { key: null };
492
-
1108
+
1109
+ // Auth check (unless skipAuth)
493
1110
  if (!cmdDef.skipAuth) {
494
1111
  const auth = getAuthModule();
495
1112
  const { key } = auth.getApiKey();
496
1113
  authInfo.key = key;
497
-
498
- // Use entitlements-v2 for access control (NO BYPASS)
1114
+
499
1115
  const access = await checkCommandAccess(cmd, cmdArgs, authInfo);
500
-
1116
+
501
1117
  if (!access.allowed) {
502
1118
  console.log(access.reason);
503
- // Use proper exit code: 3 = feature not allowed
504
1119
  process.exit(access.exitCode || 3);
505
1120
  }
506
-
507
- // Show downgrade notice if applicable (single-line at start)
1121
+
1122
+ // Downgrade notice
508
1123
  if (access.downgrade && !config.quiet) {
1124
+ const upsell = getUpsell();
509
1125
  console.log(upsell.formatDowngrade(cmd, {
510
1126
  currentTier: access.tier,
511
1127
  effectiveMode: access.downgrade,
512
1128
  caps: access.caps,
513
1129
  }));
514
1130
  }
515
-
516
- // Show tier badge
517
- if (!config.quiet) {
518
- if (access.tier === "pro") console.log(`${c.magenta}${sym.arrowRight} PRO${c.reset} ${c.dim}feature${c.reset}`);
519
- else if (access.tier === "complete") console.log(`${c.yellow}${sym.arrowRight} COMPLETE${c.reset} ${c.dim}feature${c.reset}`);
1131
+
1132
+ // Tier badge
1133
+ if (!config.quiet && !config.noBanner) {
1134
+ if (access.tier === "pro") {
1135
+ console.log(`${c.magenta}${sym.arrowRight} PRO${c.reset} ${c.dim}feature${c.reset}`);
1136
+ } else if (access.tier === "complete") {
1137
+ console.log(`${c.yellow}${sym.arrowRight} COMPLETE${c.reset} ${c.dim}feature${c.reset}`);
1138
+ }
520
1139
  }
521
-
522
- // Attach access info for runners to use
1140
+
523
1141
  authInfo.access = access;
524
1142
  }
525
-
526
- state.runCount++; state.lastRun = Date.now();
1143
+
1144
+ // Update state
1145
+ state.runCount++;
1146
+ state.lastRun = Date.now();
527
1147
  state.commandHistory = [...(state.commandHistory || []).slice(-99), { cmd, timestamp: Date.now() }];
528
1148
  saveState(state);
529
-
1149
+
1150
+ // Check for updates (async, non-blocking for startup)
1151
+ let updatePromise = null;
1152
+ if (!config.quiet && !config.noBanner && !isCI()) {
1153
+ updatePromise = checkForUpdates(state, config);
1154
+ }
1155
+
530
1156
  let exitCode = 0;
1157
+
531
1158
  try {
532
1159
  const runner = getRunner(cmd);
533
- if (!runner) { console.error(`${c.red}${sym.error}${c.reset} Failed to load runner for: ${cmd}`); process.exit(1); }
534
-
535
- const context = {
536
- repoRoot: process.cwd(),
537
- config,
538
- state,
539
- authInfo,
540
- version: VERSION,
1160
+ if (!runner) {
1161
+ console.error(`${c.red}${sym.error}${c.reset} Failed to load runner for: ${cmd}`);
1162
+ process.exit(1);
1163
+ }
1164
+
1165
+ const context = {
1166
+ repoRoot: globalFlags.path,
1167
+ config,
1168
+ state,
1169
+ authInfo,
1170
+ version: getVersion(),
541
1171
  isCI: isCI(),
542
1172
  runId,
543
- runStart
1173
+ runStart,
544
1174
  };
545
-
546
- // Pass context to runners that support it
1175
+
1176
+ // Execute command
547
1177
  switch (cmd) {
548
- case "prove": exitCode = await runner(cmdArgs, context); break;
549
- case "reality": exitCode = await runner(cmdArgs, context); break;
550
- case "watch": exitCode = await runner(cmdArgs, context); break;
551
- case "ship": exitCode = await runner(cmdArgs, context); break;
552
- case "ctx": case "truthpack":
553
- if (cmdArgs[0] === "sync") { const { runCtxSync } = require("./runners/runCtxSync"); exitCode = await runCtxSync({ ...context, fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]) }); }
554
- else if (cmdArgs[0] === "guard") { const { runCtxGuard } = require("./runners/runCtxGuard"); exitCode = await runCtxGuard.main(cmdArgs.slice(1)); }
555
- else if (cmdArgs[0] === "diff") { const { main: ctxDiffMain } = require("./runners/runCtxDiff"); exitCode = await ctxDiffMain(cmdArgs.slice(1)); }
556
- else if (cmdArgs[0] === "search") { const { runContext } = require("./runners/runContext"); exitCode = await runContext(["--search", ...cmdArgs.slice(1)]); }
557
- else { exitCode = await runner(cmdArgs, context); }
558
- break;
1178
+ case "prove":
1179
+ case "reality":
1180
+ case "watch":
1181
+ case "ship":
559
1182
  case "runtime":
560
- exitCode = await runner(cmdArgs, context);
561
- break;
562
1183
  case "export":
563
- exitCode = await runner(cmdArgs, context);
564
- break;
565
1184
  case "security":
1185
+ case "install":
1186
+ case "pr":
1187
+ case "share":
566
1188
  exitCode = await runner(cmdArgs, context);
567
1189
  break;
568
- case "install": exitCode = await runner(cmdArgs, context); break;
569
- case "status": exitCode = await runner({ ...context, json: cmdArgs.includes("--json") }); break;
570
- case "pr": exitCode = await runner(cmdArgs, context); break;
571
- case "share": exitCode = await runner(cmdArgs, context); break;
572
- default: exitCode = await runner(cmdArgs);
1190
+
1191
+ case "ctx":
1192
+ case "truthpack":
1193
+ if (cmdArgs[0] === "sync") {
1194
+ const { runCtxSync } = require("./runners/runCtxSync");
1195
+ exitCode = await runCtxSync({ ...context, fastifyEntry: getArgValue(cmdArgs, ["--fastify-entry"]) });
1196
+ } else if (cmdArgs[0] === "guard") {
1197
+ const { runCtxGuard } = require("./runners/runCtxGuard");
1198
+ exitCode = await runCtxGuard.main(cmdArgs.slice(1));
1199
+ } else if (cmdArgs[0] === "diff") {
1200
+ const { main: ctxDiffMain } = require("./runners/runCtxDiff");
1201
+ exitCode = await ctxDiffMain(cmdArgs.slice(1));
1202
+ } else if (cmdArgs[0] === "search") {
1203
+ const { runContext } = require("./runners/runContext");
1204
+ exitCode = await runContext(["--search", ...cmdArgs.slice(1)]);
1205
+ } else {
1206
+ exitCode = await runner(cmdArgs, context);
1207
+ }
1208
+ break;
1209
+
1210
+ case "status":
1211
+ exitCode = await runner({ ...context, json: cmdArgs.includes("--json") });
1212
+ break;
1213
+
1214
+ default:
1215
+ exitCode = await runner(cmdArgs);
573
1216
  }
574
- } catch (error) { console.error(formatError(error, config)); exitCode = 1; }
575
-
576
- // Create manifest and receipt for paid commands
1217
+ } catch (error) {
1218
+ console.error(formatError(error, config));
1219
+ exitCode = 1;
1220
+ }
1221
+
1222
+ // Create receipt for paid commands
577
1223
  if (cmdDef.tier !== "free" && runId) {
578
1224
  try {
579
1225
  const { createManifestAndReceipt } = require("./runners/lib/receipts");
@@ -585,24 +1231,59 @@ async function main() {
585
1231
  exitCode,
586
1232
  startTime: runStart,
587
1233
  endTime: new Date().toISOString(),
588
- projectPath: process.cwd(),
1234
+ projectPath: globalFlags.path,
589
1235
  });
590
1236
  } catch (e) {
591
- // Don't fail the command if receipt creation fails
592
1237
  if (config.debug) console.error(`Failed to create receipt: ${e.message}`);
593
1238
  }
594
1239
  }
595
-
596
- if (config.debug) console.log(`\n${c.dim}${sym.clock} Total: ${(performance.now() - startTime).toFixed(0)}ms${c.reset}`);
1240
+
1241
+ // Show update notice after command (if available)
1242
+ if (updatePromise) {
1243
+ const latestVersion = await updatePromise;
1244
+ if (latestVersion) {
1245
+ printUpdateNotice(latestVersion);
1246
+ }
1247
+ }
1248
+
1249
+ // Debug timing
1250
+ if (config.debug) {
1251
+ console.log(`\n${c.dim}${sym.clock} Total: ${(performance.now() - startTime).toFixed(0)}ms${c.reset}`);
1252
+ }
1253
+
597
1254
  process.exit(exitCode);
598
1255
  }
599
1256
 
600
1257
  // ═══════════════════════════════════════════════════════════════════════════════
601
1258
  // GRACEFUL SHUTDOWN
602
1259
  // ═══════════════════════════════════════════════════════════════════════════════
603
- process.on("SIGINT", () => { console.log(`\n${c.yellow}${sym.warning}${c.reset} Interrupted`); process.exit(130); });
604
- process.on("SIGTERM", () => { console.log(`\n${c.yellow}${sym.warning}${c.reset} Terminated`); process.exit(143); });
605
- process.on("uncaughtException", (error) => { console.error(`\n${c.red}${sym.error} Uncaught Exception:${c.reset} ${error.message}`); if (process.env.VIBECHECK_DEBUG === "true") console.error(c.dim + error.stack + c.reset); process.exit(1); });
606
- process.on("unhandledRejection", (reason) => { console.error(`\n${c.red}${sym.error} Unhandled Rejection:${c.reset} ${reason}`); process.exit(1); });
1260
+ process.on("SIGINT", () => {
1261
+ console.log(`\n${c.yellow}${sym.warning}${c.reset} Interrupted`);
1262
+ process.exit(130);
1263
+ });
1264
+
1265
+ process.on("SIGTERM", () => {
1266
+ console.log(`\n${c.yellow}${sym.warning}${c.reset} Terminated`);
1267
+ process.exit(143);
1268
+ });
1269
+
1270
+ process.on("uncaughtException", (error) => {
1271
+ console.error(`\n${c.red}${sym.error} Uncaught Exception:${c.reset} ${error.message}`);
1272
+ if (process.env.VIBECHECK_DEBUG === "true") {
1273
+ console.error(c.dim + error.stack + c.reset);
1274
+ }
1275
+ process.exit(1);
1276
+ });
1277
+
1278
+ process.on("unhandledRejection", (reason) => {
1279
+ console.error(`\n${c.red}${sym.error} Unhandled Rejection:${c.reset} ${reason}`);
1280
+ process.exit(1);
1281
+ });
607
1282
 
608
- main().catch((error) => { console.error(`\n${c.red}${sym.error} Fatal:${c.reset} ${error.message}`); process.exit(1); });
1283
+ // ═══════════════════════════════════════════════════════════════════════════════
1284
+ // RUN
1285
+ // ═══════════════════════════════════════════════════════════════════════════════
1286
+ main().catch((error) => {
1287
+ console.error(`\n${c.red}${sym.error} Fatal:${c.reset} ${error.message}`);
1288
+ process.exit(1);
1289
+ });