@vibecheckai/cli 3.2.6 → 3.3.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.
Files changed (84) hide show
  1. package/bin/registry.js +192 -5
  2. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  3. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  4. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  5. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  6. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  7. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  8. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  11. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  12. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  13. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  14. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  15. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  16. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  17. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  18. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  19. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  20. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  21. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  22. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  23. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  24. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  25. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  26. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  27. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  28. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  29. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  30. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  31. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  32. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  35. package/bin/runners/lib/analyzers.js +81 -18
  36. package/bin/runners/lib/authority-badge.js +425 -0
  37. package/bin/runners/lib/cli-output.js +7 -1
  38. package/bin/runners/lib/error-handler.js +16 -9
  39. package/bin/runners/lib/exit-codes.js +275 -0
  40. package/bin/runners/lib/global-flags.js +37 -0
  41. package/bin/runners/lib/help-formatter.js +413 -0
  42. package/bin/runners/lib/logger.js +38 -0
  43. package/bin/runners/lib/unified-cli-output.js +604 -0
  44. package/bin/runners/lib/upsell.js +148 -0
  45. package/bin/runners/runApprove.js +1200 -0
  46. package/bin/runners/runAuth.js +324 -95
  47. package/bin/runners/runCheckpoint.js +39 -21
  48. package/bin/runners/runClassify.js +859 -0
  49. package/bin/runners/runContext.js +136 -24
  50. package/bin/runners/runDoctor.js +108 -68
  51. package/bin/runners/runFix.js +6 -5
  52. package/bin/runners/runGuard.js +212 -118
  53. package/bin/runners/runInit.js +3 -2
  54. package/bin/runners/runMcp.js +130 -52
  55. package/bin/runners/runPolish.js +43 -20
  56. package/bin/runners/runProve.js +1 -2
  57. package/bin/runners/runReport.js +3 -2
  58. package/bin/runners/runScan.js +63 -44
  59. package/bin/runners/runShip.js +3 -4
  60. package/bin/runners/runValidate.js +19 -2
  61. package/bin/runners/runWatch.js +104 -53
  62. package/bin/vibecheck.js +106 -19
  63. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  64. package/mcp-server/agent-firewall-interceptor.js +367 -31
  65. package/mcp-server/authority-tools.js +569 -0
  66. package/mcp-server/conductor/conflict-resolver.js +588 -0
  67. package/mcp-server/conductor/execution-planner.js +544 -0
  68. package/mcp-server/conductor/index.js +377 -0
  69. package/mcp-server/conductor/lock-manager.js +615 -0
  70. package/mcp-server/conductor/request-queue.js +550 -0
  71. package/mcp-server/conductor/session-manager.js +500 -0
  72. package/mcp-server/conductor/tools.js +510 -0
  73. package/mcp-server/index.js +1149 -243
  74. package/mcp-server/lib/{api-client.js → api-client.cjs} +40 -4
  75. package/mcp-server/lib/logger.cjs +30 -0
  76. package/mcp-server/logger.js +173 -0
  77. package/mcp-server/package.json +2 -2
  78. package/mcp-server/premium-tools.js +2 -2
  79. package/mcp-server/tier-auth.js +245 -35
  80. package/mcp-server/truth-firewall-tools.js +145 -15
  81. package/mcp-server/vibecheck-tools.js +2 -2
  82. package/package.json +2 -3
  83. package/mcp-server/index.old.js +0 -4137
  84. package/mcp-server/package-lock.json +0 -165
@@ -24,6 +24,8 @@
24
24
 
25
25
  const fs = require("fs");
26
26
  const path = require("path");
27
+ const { EXIT } = require("./lib/exit-codes");
28
+ const { parseGlobalFlags, shouldSuppressOutput, isJsonMode } = require("./lib/global-flags");
27
29
 
28
30
  // ═══════════════════════════════════════════════════════════════════════════════
29
31
  // TERMINAL STYLING
@@ -2856,26 +2858,35 @@ function getCategoryIcon(category) {
2856
2858
 
2857
2859
  async function runPolish(args) {
2858
2860
  const opts = parseArgs(args);
2861
+ const { flags: globalFlags } = parseGlobalFlags(args);
2862
+ const quiet = shouldSuppressOutput(globalFlags);
2863
+ const json = isJsonMode(globalFlags) || opts.json;
2859
2864
 
2860
2865
  if (opts.help) {
2861
2866
  printHelp();
2862
- return 0;
2867
+ return EXIT.SUCCESS;
2863
2868
  }
2864
2869
 
2865
2870
  const projectPath = path.resolve(opts.path);
2866
2871
 
2867
2872
  // Verify project exists
2868
2873
  if (!await pathExists(projectPath)) {
2869
- console.error(`${c.red}${icons.cross} Project path does not exist: ${projectPath}${c.reset}`);
2870
- return 1;
2874
+ if (json) {
2875
+ console.log(JSON.stringify({ success: false, error: `Project path does not exist: ${projectPath}` }));
2876
+ } else {
2877
+ console.error(`${c.red}${icons.cross} Project path does not exist: ${projectPath}${c.reset}`);
2878
+ console.error(` Verify the path and try again.`);
2879
+ }
2880
+ return EXIT.NOT_FOUND;
2871
2881
  }
2872
2882
 
2873
- // JSON mode
2874
- if (opts.json) {
2875
- const report = await analyzeProject(projectPath, opts);
2876
- console.log(JSON.stringify(report, null, 2));
2877
- return report.critical > 0 ? 1 : 0;
2878
- }
2883
+ try {
2884
+ // JSON mode
2885
+ if (json) {
2886
+ const report = await analyzeProject(projectPath, opts);
2887
+ console.log(JSON.stringify(report, null, 2));
2888
+ return report.critical > 0 ? EXIT.BLOCKING : EXIT.SUCCESS;
2889
+ }
2879
2890
 
2880
2891
  // Interactive mode
2881
2892
  console.log(`
@@ -2914,8 +2925,10 @@ ${c.bold}╔══════════════════════
2914
2925
  console.log(` ${icons.critical} ${report.critical} critical ${icons.high} ${report.high} high ${icons.medium} ${report.medium} medium ${icons.low} ${report.low} low\n`);
2915
2926
 
2916
2927
  if (report.issues.length === 0) {
2917
- console.log(`\n${c.green}${c.bold}${icons.check} Perfect!${c.reset} No polish issues found. Your project is production-ready! ${icons.rocket}\n`);
2918
- return 0;
2928
+ if (!quiet) {
2929
+ console.log(`\n${c.green}${c.bold}${icons.check} Perfect!${c.reset} No polish issues found. Your project is production-ready! ${icons.rocket}\n`);
2930
+ }
2931
+ return EXIT.SUCCESS;
2919
2932
  }
2920
2933
 
2921
2934
  // Group by category
@@ -3016,16 +3029,26 @@ ${c.bold}╔══════════════════════
3016
3029
  }
3017
3030
 
3018
3031
  // Next steps
3019
- console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
3020
- console.log(`${c.bold}${icons.rocket} NEXT STEPS${c.reset}`);
3021
- console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
3022
- console.log(` 1. Fix ${c.red}critical${c.reset} and ${c.yellow}high${c.reset} severity issues first`);
3023
- console.log(` 2. Use ${c.cyan}vibecheck polish --prompts${c.reset} to get AI-ready fixes`);
3024
- console.log(` 3. Copy prompts to your AI assistant (Cursor, Copilot, Claude)`);
3025
- console.log(` 4. Re-run ${c.cyan}vibecheck polish${c.reset} to verify fixes`);
3026
- console.log(` 5. Run ${c.cyan}vibecheck ship${c.reset} when score is 80+\n`);
3032
+ if (!quiet) {
3033
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}`);
3034
+ console.log(`${c.bold}${icons.rocket} NEXT STEPS${c.reset}`);
3035
+ console.log(`${c.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${c.reset}\n`);
3036
+ console.log(` 1. Fix ${c.red}critical${c.reset} and ${c.yellow}high${c.reset} severity issues first`);
3037
+ console.log(` 2. Use ${c.cyan}vibecheck polish --prompts${c.reset} to get AI-ready fixes`);
3038
+ console.log(` 3. Copy prompts to your AI assistant (Cursor, Copilot, Claude)`);
3039
+ console.log(` 4. Re-run ${c.cyan}vibecheck polish${c.reset} to verify fixes`);
3040
+ console.log(` 5. Run ${c.cyan}vibecheck ship${c.reset} when score is 80+\n`);
3041
+ }
3027
3042
 
3028
- return report.critical > 0 ? 1 : 0;
3043
+ return report.critical > 0 ? EXIT.BLOCKING : EXIT.SUCCESS;
3044
+ } catch (error) {
3045
+ if (json) {
3046
+ console.log(JSON.stringify({ success: false, error: error.message }));
3047
+ } else {
3048
+ console.error(`${c.red}${icons.cross} Polish analysis failed: ${error.message}${c.reset}`);
3049
+ }
3050
+ return EXIT.INTERNAL_ERROR;
3051
+ }
3029
3052
  }
3030
3053
 
3031
3054
  module.exports = { runPolish };
@@ -26,10 +26,9 @@ const {
26
26
  generateRunId,
27
27
  createJsonOutput,
28
28
  writeJsonOutput,
29
- exitCodeToVerdict,
30
- verdictToExitCode,
31
29
  saveArtifact
32
30
  } = require("./lib/cli-output");
31
+ const { EXIT, verdictToExitCode, exitCodeToVerdict } = require("./lib/exit-codes");
33
32
  const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
34
33
  const upsell = require("./lib/upsell");
35
34
 
@@ -18,6 +18,7 @@
18
18
  const path = require("path");
19
19
  const fs = require("fs");
20
20
  const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
21
+ const { EXIT } = require("./lib/exit-codes");
21
22
 
22
23
  // Entitlements enforcement
23
24
  let entitlements;
@@ -264,11 +265,11 @@ async function runReport(args) {
264
265
  default:
265
266
  spinner.fail(`Unknown format: ${format}`);
266
267
  console.log(` ${ansi.dim}Supported: html, md, json, sarif, csv, pdf${ansi.reset}`);
267
- return 1;
268
+ return EXIT.USER_ERROR;
268
269
  }
269
270
  } catch (err) {
270
271
  spinner.fail(`Failed to generate report: ${err.message}`);
271
- return 1;
272
+ return EXIT.INTERNAL_ERROR;
272
273
  }
273
274
 
274
275
  // Determine output path
@@ -25,6 +25,7 @@ const {
25
25
  reportScanError,
26
26
  isApiAvailable
27
27
  } = require("./lib/api-client");
28
+ const { EXIT, verdictToExitCode } = require("./lib/exit-codes");
28
29
 
29
30
  // ═══════════════════════════════════════════════════════════════════════════════
30
31
  // ENHANCED TERMINAL UI & OUTPUT MODULES
@@ -170,57 +171,75 @@ function printHelp(showBanner = true) {
170
171
  console.log(BANNER);
171
172
  }
172
173
  console.log(`
173
- ${ansi.bold}Usage:${ansi.reset} vibecheck scan ${ansi.dim}(s)${ansi.reset} [path] [options]
174
+ ${ansi.bold}USAGE${ansi.reset}
175
+ ${colors.accent}vibecheck scan${ansi.reset} [path] [options]
174
176
 
175
- ${ansi.bold}Aliases:${ansi.reset} ${ansi.dim}s${ansi.reset}
176
-
177
- ${ansi.bold}Scan Modes:${ansi.reset}
178
- ${colors.accent}(default)${ansi.reset} Layer 1: AST static analysis ${ansi.dim}(fast)${ansi.reset}
179
- ${colors.accent}--truth, -t${ansi.reset} Layer 1+2: Include build manifest verification ${ansi.dim}(CI/ship)${ansi.reset}
180
- ${colors.accent}--reality, -r${ansi.reset} Layer 1+2+3: Include Playwright runtime proof ${ansi.dim}(full)${ansi.reset}
181
- ${colors.accent}--reality-sniff${ansi.reset} Include Reality Sniff AI artifact detection ${ansi.dim}(recommended)${ansi.reset}
182
-
183
- ${ansi.bold}Fix Mode:${ansi.reset}
184
- ${colors.accent}--autofix, -f${ansi.reset} Apply safe fixes + generate AI missions ${ansi.rgb(0, 200, 255)}[STARTER]${ansi.reset}
185
-
186
- ${ansi.bold}Allowlist (suppress false positives):${ansi.reset}
187
- ${colors.accent}--allowlist list${ansi.reset} Show all allowlist entries
188
- ${colors.accent}--allowlist add${ansi.reset} Add entry to allowlist
189
- ${ansi.dim}--id <finding-id>${ansi.reset} Finding ID to allowlist
190
- ${ansi.dim}--pattern <regex>${ansi.reset} Pattern to match
191
- ${ansi.dim}--reason <text>${ansi.reset} Reason (required)
192
- ${ansi.dim}--scope <global|file|line>${ansi.reset} Scope (default: global)
193
- ${ansi.dim}--expires <days>${ansi.reset} Auto-expire after N days
194
- ${colors.accent}--allowlist remove --id <id>${ansi.reset} Remove entry from allowlist
195
- ${colors.accent}--allowlist check --id <id>${ansi.reset} Check if finding is allowlisted
196
-
197
- ${ansi.bold}Options:${ansi.reset}
198
- ${colors.accent}--url, -u${ansi.reset} Base URL for reality testing (e.g., http://localhost:3000)
199
- ${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
200
- ${colors.accent}--json${ansi.reset} Output results as JSON
201
- ${colors.accent}--sarif${ansi.reset} Output in SARIF format (GitHub code scanning)
202
- ${colors.accent}--no-save${ansi.reset} Don't save results to .vibecheck/results/
203
- ${colors.accent}--help, -h${ansi.reset} Show this help
204
-
205
- ${ansi.bold}Examples:${ansi.reset}
206
- ${ansi.dim}# Quick scan (AST only)${ansi.reset}
177
+ ${ansi.dim}Aliases: s, check${ansi.reset}
178
+
179
+ The core analysis engine. Scans your codebase for route integrity issues,
180
+ security vulnerabilities, code quality problems, and more.
181
+
182
+ ${ansi.bold}SCAN LAYERS${ansi.reset}
183
+ ${colors.accent}(default)${ansi.reset} Layer 1: AST static analysis ${ansi.dim}(fast, ~2s)${ansi.reset}
184
+ ${colors.accent}--truth, -t${ansi.reset} Layer 1+2: + build manifest verification ${ansi.dim}(CI/ship)${ansi.reset}
185
+ ${colors.accent}--reality, -r${ansi.reset} Layer 1+2+3: + Playwright runtime proof ${ansi.dim}[PRO]${ansi.reset}
186
+ ${colors.accent}--reality-sniff${ansi.reset} Include Reality Sniff AI artifact detection
187
+
188
+ ${ansi.bold}FIX MODE${ansi.reset} ${ansi.cyan}[STARTER]${ansi.reset}
189
+ ${colors.accent}--autofix, -f${ansi.reset} Generate AI fix missions for detected issues
190
+
191
+ ${ansi.bold}BASELINE TRACKING${ansi.reset}
192
+ ${colors.accent}--baseline${ansi.reset} Compare against previous scan ${ansi.dim}(default: on)${ansi.reset}
193
+ ${colors.accent}--no-baseline${ansi.reset} Skip baseline comparison
194
+ ${colors.accent}--update-baseline${ansi.reset} Save current findings as new baseline
195
+
196
+ ${ansi.bold}ALLOWLIST (suppress false positives)${ansi.reset}
197
+ ${colors.accent}--allowlist list${ansi.reset} Show all suppressed findings
198
+ ${colors.accent}--allowlist add --id <id> --reason "..."${ansi.reset}
199
+ ${colors.accent}--allowlist remove --id <id>${ansi.reset} Remove suppression
200
+ ${colors.accent}--allowlist check --id <id>${ansi.reset} Check if suppressed
201
+
202
+ ${ansi.bold}OUTPUT OPTIONS${ansi.reset}
203
+ ${colors.accent}--json${ansi.reset} Output as JSON ${ansi.dim}(machine-readable)${ansi.reset}
204
+ ${colors.accent}--sarif${ansi.reset} SARIF format ${ansi.dim}(GitHub code scanning)${ansi.reset}
205
+ ${colors.accent}--no-save${ansi.reset} Don't save results to .vibecheck/results/
206
+ ${colors.accent}--verbose, -v${ansi.reset} Show detailed progress
207
+
208
+ ${ansi.bold}GLOBAL OPTIONS${ansi.reset}
209
+ ${colors.accent}--path, -p <dir>${ansi.reset} Run in specified directory
210
+ ${colors.accent}--quiet, -q${ansi.reset} Suppress non-essential output
211
+ ${colors.accent}--ci${ansi.reset} CI mode ${ansi.dim}(quiet + no-banner)${ansi.reset}
212
+ ${colors.accent}--help, -h${ansi.reset} Show this help
213
+
214
+ ${ansi.bold}💡 EXAMPLES${ansi.reset}
215
+
216
+ ${ansi.dim}# Quick scan (most common)${ansi.reset}
207
217
  vibecheck scan
208
218
 
209
- ${ansi.dim}# Scan + autofix with missions${ansi.reset}
219
+ ${ansi.dim}# Scan with AI fix missions${ansi.reset}
210
220
  vibecheck scan --autofix
211
221
 
212
- ${ansi.dim}# Add false positive to allowlist${ansi.reset}
213
- vibecheck scan --allowlist add --id R_DEAD_abc123 --reason "Known toggle"
222
+ ${ansi.dim}# Suppress a false positive${ansi.reset}
223
+ vibecheck scan --allowlist add --id R_DEAD_abc123 --reason "Feature toggle"
214
224
 
215
- ${ansi.dim}# List allowlist entries${ansi.reset}
216
- vibecheck scan --allowlist list
225
+ ${ansi.dim}# CI pipeline (JSON output, strict)${ansi.reset}
226
+ vibecheck scan --ci --json > results.json
217
227
 
218
- ${ansi.dim}# Full proof with Playwright${ansi.reset}
228
+ ${ansi.dim}# Full reality proof (requires running app)${ansi.reset}
219
229
  vibecheck scan --reality --url http://localhost:3000
220
230
 
221
- ${ansi.bold}Output:${ansi.reset}
222
- Results saved to: .vibecheck/results/latest.json
223
- Missions saved to: .vibecheck/missions/ ${ansi.dim}(with --autofix)${ansi.reset}
231
+ ${ansi.bold}📄 OUTPUT${ansi.reset}
232
+ Results: .vibecheck/results/latest.json
233
+ Missions: .vibecheck/missions/ ${ansi.dim}(with --autofix)${ansi.reset}
234
+ History: .vibecheck/results/history/
235
+
236
+ ${ansi.bold}🔗 RELATED COMMANDS${ansi.reset}
237
+ ${colors.accent}vibecheck ship${ansi.reset} Get final SHIP/WARN/BLOCK verdict
238
+ ${colors.accent}vibecheck fix${ansi.reset} Apply AI-generated fixes ${ansi.cyan}[STARTER]${ansi.reset}
239
+ ${colors.accent}vibecheck prove${ansi.reset} Full proof pipeline with evidence ${ansi.magenta}[PRO]${ansi.reset}
240
+
241
+ ${ansi.dim}─────────────────────────────────────────────────────────────${ansi.reset}
242
+ ${ansi.dim}Documentation: https://docs.vibecheckai.dev/cli/scan${ansi.reset}
224
243
  `);
225
244
  }
226
245
 
@@ -761,7 +780,7 @@ async function runScan(args) {
761
780
  console.log(` ${colors.warning}⚠${ansi.reset} ${ansi.bold}Reality layer requires --url${ansi.reset}`);
762
781
  console.log(` ${ansi.dim}Example: vibecheck scan --reality --url http://localhost:3000${ansi.reset}`);
763
782
  console.log();
764
- return 1;
783
+ return EXIT.USER_ERROR;
765
784
  }
766
785
 
767
786
  // Initialize spinner outside try block for error handling
@@ -1389,7 +1408,7 @@ async function runScan(args) {
1389
1408
  }
1390
1409
  }
1391
1410
 
1392
- return verdict === 'SHIP' ? 0 : verdict === 'WARN' ? 1 : 2;
1411
+ return verdictToExitCode(verdict);
1393
1412
  }
1394
1413
 
1395
1414
  } catch (error) {
@@ -18,10 +18,9 @@ const {
18
18
  generateRunId,
19
19
  createJsonOutput,
20
20
  writeJsonOutput,
21
- exitCodeToVerdict,
22
- verdictToExitCode,
23
21
  saveArtifact
24
22
  } = require("./lib/cli-output");
23
+ const { EXIT, verdictToExitCode, exitCodeToVerdict } = require("./lib/exit-codes");
25
24
 
26
25
  // Route Truth v1 - Fake endpoint detection
27
26
  const { buildTruthpack, writeTruthpack, detectFastifyEntry } = require("./lib/truth");
@@ -828,7 +827,7 @@ async function runShip(args, context = {}) {
828
827
  } catch (err) {
829
828
  if (err.code === 'LIMIT_EXCEEDED' || err.code === 'FEATURE_NOT_AVAILABLE') {
830
829
  console.error(`\n ${colors.error}${icons.error}${ansi.reset} ${err.upgradePrompt || err.message}\n`);
831
- return EXIT_CODES.WARN;
830
+ return EXIT.TIER_REQUIRED;
832
831
  }
833
832
  throw err;
834
833
  }
@@ -1131,7 +1130,7 @@ async function runShip(args, context = {}) {
1131
1130
  console.error(` ${ansi.dim}${error.stack}${ansi.reset}`);
1132
1131
  }
1133
1132
 
1134
- return EXIT_CODES.ERROR;
1133
+ return EXIT.INTERNAL_ERROR;
1135
1134
  }
1136
1135
  }
1137
1136
 
@@ -6,6 +6,9 @@ const fs = require("fs");
6
6
  const path = require("path");
7
7
  const { buildTruthpack } = require("./lib/truth");
8
8
  const { routeMatches } = require("./lib/claims");
9
+ const { EXIT } = require("./lib/exit-codes");
10
+ const { formatSoftUpsell, formatWorkflowUpsell, PRICING_URL } = require("./lib/upsell");
11
+ const { getApiKey } = require("./lib/auth");
9
12
 
10
13
  const c = {
11
14
  reset: "\x1b[0m",
@@ -133,12 +136,19 @@ async function runValidate(args) {
133
136
  }
134
137
 
135
138
  // Output results
139
+ const { key } = getApiKey();
140
+ const currentTier = key ? "starter" : "free";
141
+
136
142
  if (opts.json) {
137
143
  console.log(JSON.stringify({ issues, valid: issues.length === 0 }, null, 2));
138
144
  } else {
139
145
  if (issues.length === 0) {
140
146
  console.log(`${c.green}✓${c.reset} No hallucinations detected!\n`);
141
147
  console.log(` ${c.dim}All routes and env vars are properly defined.${c.reset}\n`);
148
+
149
+ // Workflow upsell
150
+ const workflow = formatWorkflowUpsell("validate", currentTier);
151
+ if (workflow) console.log(` ${workflow}\n`);
142
152
  } else {
143
153
  console.log(`${c.yellow}⚠${c.reset} Found ${issues.length} potential issues:\n`);
144
154
 
@@ -148,14 +158,21 @@ async function runValidate(args) {
148
158
  console.log(` ${c.dim}${issue.file}:${issue.line}${c.reset}`);
149
159
  }
150
160
  console.log();
161
+
162
+ // Upsell for auto-fix
163
+ if (currentTier === "free") {
164
+ console.log(` ${c.dim}─────────────────────────────────────────────────────────────${c.reset}`);
165
+ console.log(` ${c.cyan}★ STARTER${c.reset}${c.dim}: auto-fix hallucinations with vibecheck fix${c.reset}`);
166
+ console.log(` ${c.dim} Upgrade → ${PRICING_URL}${c.reset}\n`);
167
+ }
151
168
  }
152
169
  }
153
170
 
154
- return issues.some(i => i.severity === "error") ? 1 : 0;
171
+ return issues.some(i => i.severity === "error") ? EXIT.BLOCKING : EXIT.SUCCESS;
155
172
 
156
173
  } catch (error) {
157
174
  console.error(`${c.red}✗${c.reset} Validation failed: ${error.message}`);
158
- return 1;
175
+ return EXIT.INTERNAL_ERROR;
159
176
  }
160
177
  }
161
178
 
@@ -14,7 +14,8 @@
14
14
 
15
15
  const fs = require("fs");
16
16
  const path = require("path");
17
- const { parseGlobalFlags, shouldShowBanner } = require("./lib/global-flags");
17
+ const { parseGlobalFlags, shouldShowBanner, shouldSuppressOutput } = require("./lib/global-flags");
18
+ const { EXIT } = require("./lib/exit-codes");
18
19
  const { shipCore } = require("./runShip");
19
20
 
20
21
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -331,12 +332,15 @@ function printHelp(opts = {}) {
331
332
  async function runWatch(argsOrOpts = {}) {
332
333
  // Handle array args from CLI
333
334
  let globalOpts = { noBanner: false, json: false, quiet: false, ci: false };
335
+ let rawArgs = [];
336
+
334
337
  if (Array.isArray(argsOrOpts)) {
338
+ rawArgs = argsOrOpts;
335
339
  const { flags } = parseGlobalFlags(argsOrOpts);
336
340
  globalOpts = { ...globalOpts, ...flags };
337
341
  if (globalOpts.help) {
338
342
  printHelp(globalOpts);
339
- return 0;
343
+ return EXIT.SUCCESS;
340
344
  }
341
345
  const getArg = (flags) => {
342
346
  for (const f of flags) {
@@ -346,10 +350,10 @@ async function runWatch(argsOrOpts = {}) {
346
350
  return undefined;
347
351
  };
348
352
  argsOrOpts = {
349
- repoRoot: process.cwd(),
353
+ repoRoot: globalOpts.path || process.cwd(),
350
354
  fastifyEntry: getArg(["--fastify-entry"]),
351
355
  debounceMs: parseInt(getArg(["--debounce"]) || "500", 10),
352
- clearScreen: !argsOrOpts.includes("--no-clear"),
356
+ clearScreen: !rawArgs.includes("--no-clear"),
353
357
  ...globalOpts,
354
358
  };
355
359
  }
@@ -358,70 +362,117 @@ async function runWatch(argsOrOpts = {}) {
358
362
  repoRoot,
359
363
  fastifyEntry,
360
364
  debounceMs = 500,
361
- clearScreen = true
365
+ clearScreen = true,
366
+ quiet,
362
367
  } = argsOrOpts;
363
368
 
364
369
  const root = repoRoot || process.cwd();
370
+ const suppress = shouldSuppressOutput(argsOrOpts);
371
+
372
+ // Validate project path exists
373
+ if (!fs.existsSync(root)) {
374
+ console.error(`${c.red}✗${c.reset} Project path does not exist: ${root}`);
375
+ console.log(` ${c.dim}Verify the path and try again.${c.reset}`);
376
+ return EXIT.NOT_FOUND;
377
+ }
378
+
379
+ // Validate debounce is a valid number
380
+ if (isNaN(debounceMs) || debounceMs < 0) {
381
+ console.error(`${c.red}✗${c.reset} Invalid debounce value: ${debounceMs}`);
382
+ console.log(` ${c.dim}Debounce must be a positive number in milliseconds.${c.reset}`);
383
+ return EXIT.USER_ERROR;
384
+ }
385
+
365
386
  let runCount = 0;
366
387
  let lastFile = null;
388
+ let watcher = null;
389
+ let exitCode = EXIT.SUCCESS;
367
390
 
368
- console.log(`${c.cyan}Starting vibecheck watch...${c.reset}\n`);
369
-
370
- async function runAndDisplay() {
371
- runCount++;
372
-
373
- if (clearScreen) {
374
- process.stdout.write(c.clear);
391
+ try {
392
+ if (!suppress) {
393
+ console.log(`${c.cyan}Starting vibecheck watch...${c.reset}\n`);
375
394
  }
376
395
 
377
- const result = await runShipQuiet(root, fastifyEntry);
396
+ async function runAndDisplay() {
397
+ runCount++;
398
+
399
+ if (clearScreen && !suppress) {
400
+ process.stdout.write(c.clear);
401
+ }
402
+
403
+ try {
404
+ const result = await runShipQuiet(root, fastifyEntry);
378
405
 
379
- if (result.error) {
380
- console.log(`${c.red}Error: ${result.error}${c.reset}`);
381
- return;
406
+ if (result.error) {
407
+ if (!suppress) {
408
+ console.log(`${c.red}Error: ${result.error}${c.reset}`);
409
+ console.log(` ${c.dim}Will retry on next file change.${c.reset}`);
410
+ }
411
+ return;
412
+ }
413
+
414
+ if (!suppress) {
415
+ printStatus({
416
+ verdict: result.verdict,
417
+ findings: result.findings,
418
+ duration: result.duration,
419
+ lastFile,
420
+ runCount
421
+ });
422
+
423
+ printTopFindings(result.findings);
424
+ }
425
+ } catch (e) {
426
+ if (!suppress) {
427
+ console.log(`${c.red}Analysis error: ${e.message}${c.reset}`);
428
+ }
429
+ }
382
430
  }
383
431
 
384
- printStatus({
385
- verdict: result.verdict,
386
- findings: result.findings,
387
- duration: result.duration,
388
- lastFile,
389
- runCount
432
+ // Initial run
433
+ await runAndDisplay();
434
+
435
+ // Watch for changes
436
+ const debouncedRun = debounce(async (filePath) => {
437
+ lastFile = path.relative(root, filePath);
438
+ await runAndDisplay();
439
+ }, debounceMs);
440
+
441
+ watcher = watchDirectory(root, (filePath) => {
442
+ // Only watch source files
443
+ if (!/\.(ts|tsx|js|jsx|json|env|md|yml|yaml)$/.test(filePath)) return;
444
+ if (shouldIgnore(filePath)) return;
445
+ debouncedRun(filePath);
390
446
  });
391
447
 
392
- printTopFindings(result.findings);
393
- }
448
+ // Handle cleanup - store cleanup function for signal handlers
449
+ const cleanup = () => {
450
+ if (watcher) {
451
+ watcher.close();
452
+ watcher = null;
453
+ }
454
+ };
394
455
 
395
- // Initial run
396
- await runAndDisplay();
456
+ process.on("SIGINT", () => {
457
+ if (!suppress) {
458
+ console.log(`\n${c.dim}Watch stopped.${c.reset}`);
459
+ }
460
+ cleanup();
461
+ process.exit(EXIT.SUCCESS);
462
+ });
397
463
 
398
- // Watch for changes
399
- const debouncedRun = debounce(async (filePath) => {
400
- lastFile = path.relative(root, filePath);
401
- await runAndDisplay();
402
- }, debounceMs);
403
-
404
- const watcher = watchDirectory(root, (filePath) => {
405
- // Only watch source files
406
- if (!/\.(ts|tsx|js|jsx|json|env|md|yml|yaml)$/.test(filePath)) return;
407
- if (shouldIgnore(filePath)) return;
408
- debouncedRun(filePath);
409
- });
410
-
411
- // Handle cleanup
412
- process.on("SIGINT", () => {
413
- console.log(`\n${c.dim}Watch stopped.${c.reset}`);
414
- watcher.close();
415
- process.exit(0);
416
- });
417
-
418
- process.on("SIGTERM", () => {
419
- watcher.close();
420
- process.exit(0);
421
- });
422
-
423
- // Keep process alive
424
- await new Promise(() => {});
464
+ process.on("SIGTERM", () => {
465
+ cleanup();
466
+ process.exit(EXIT.SUCCESS);
467
+ });
468
+
469
+ // Keep process alive
470
+ await new Promise(() => {});
471
+ } catch (error) {
472
+ console.error(`${c.red}✗${c.reset} Watch failed: ${error.message}`);
473
+ if (watcher) watcher.close();
474
+ return EXIT.INTERNAL_ERROR;
475
+ }
425
476
  }
426
477
 
427
478
  module.exports = { runWatch };