@vibecheckai/cli 3.5.0 → 3.5.2

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 (224) hide show
  1. package/bin/registry.js +214 -237
  2. package/bin/runners/cli-utils.js +33 -2
  3. package/bin/runners/context/analyzer.js +52 -1
  4. package/bin/runners/context/generators/cursor.js +2 -49
  5. package/bin/runners/context/git-context.js +3 -1
  6. package/bin/runners/context/team-conventions.js +33 -7
  7. package/bin/runners/lib/analysis-core.js +25 -5
  8. package/bin/runners/lib/analyzers.js +431 -481
  9. package/bin/runners/lib/default-config.js +127 -0
  10. package/bin/runners/lib/doctor/modules/security.js +3 -1
  11. package/bin/runners/lib/engine/ast-cache.js +210 -0
  12. package/bin/runners/lib/engine/auth-extractor.js +211 -0
  13. package/bin/runners/lib/engine/billing-extractor.js +112 -0
  14. package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
  15. package/bin/runners/lib/engine/env-extractor.js +207 -0
  16. package/bin/runners/lib/engine/express-extractor.js +208 -0
  17. package/bin/runners/lib/engine/extractors.js +849 -0
  18. package/bin/runners/lib/engine/index.js +207 -0
  19. package/bin/runners/lib/engine/repo-index.js +514 -0
  20. package/bin/runners/lib/engine/types.js +124 -0
  21. package/bin/runners/lib/engines/accessibility-engine.js +18 -218
  22. package/bin/runners/lib/engines/api-consistency-engine.js +30 -335
  23. package/bin/runners/lib/engines/cross-file-analysis-engine.js +27 -292
  24. package/bin/runners/lib/engines/empty-catch-engine.js +17 -127
  25. package/bin/runners/lib/engines/mock-data-engine.js +10 -53
  26. package/bin/runners/lib/engines/performance-issues-engine.js +36 -176
  27. package/bin/runners/lib/engines/security-vulnerabilities-engine.js +54 -382
  28. package/bin/runners/lib/engines/type-aware-engine.js +39 -263
  29. package/bin/runners/lib/engines/vibecheck-engines/index.js +13 -122
  30. package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
  31. package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
  32. package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
  33. package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
  34. package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
  35. package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
  36. package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
  37. package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +73 -373
  38. package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
  39. package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
  40. package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
  41. package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
  42. package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
  43. package/bin/runners/lib/entitlements-v2.js +73 -97
  44. package/bin/runners/lib/error-handler.js +44 -3
  45. package/bin/runners/lib/error-messages.js +289 -0
  46. package/bin/runners/lib/evidence-pack.js +7 -1
  47. package/bin/runners/lib/finding-id.js +69 -0
  48. package/bin/runners/lib/finding-sorter.js +89 -0
  49. package/bin/runners/lib/html-proof-report.js +700 -350
  50. package/bin/runners/lib/missions/plan.js +6 -46
  51. package/bin/runners/lib/missions/templates.js +0 -232
  52. package/bin/runners/lib/next-action.js +560 -0
  53. package/bin/runners/lib/prerequisites.js +149 -0
  54. package/bin/runners/lib/route-detection.js +137 -68
  55. package/bin/runners/lib/scan-output.js +91 -76
  56. package/bin/runners/lib/scan-runner.js +135 -0
  57. package/bin/runners/lib/schemas/ajv-validator.js +464 -0
  58. package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
  59. package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
  60. package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
  61. package/bin/runners/lib/schemas/run-request.schema.json +108 -0
  62. package/bin/runners/lib/schemas/validator.js +27 -0
  63. package/bin/runners/lib/schemas/verdict.schema.json +140 -0
  64. package/bin/runners/lib/ship-output-enterprise.js +23 -23
  65. package/bin/runners/lib/ship-output.js +75 -31
  66. package/bin/runners/lib/terminal-ui.js +6 -113
  67. package/bin/runners/lib/truth.js +351 -10
  68. package/bin/runners/lib/unified-cli-output.js +430 -603
  69. package/bin/runners/lib/unified-output.js +13 -9
  70. package/bin/runners/runAIAgent.js +10 -5
  71. package/bin/runners/runAgent.js +0 -3
  72. package/bin/runners/runAllowlist.js +389 -0
  73. package/bin/runners/runApprove.js +0 -33
  74. package/bin/runners/runAuth.js +73 -45
  75. package/bin/runners/runCheckpoint.js +51 -11
  76. package/bin/runners/runClassify.js +85 -21
  77. package/bin/runners/runContext.js +0 -3
  78. package/bin/runners/runDoctor.js +41 -28
  79. package/bin/runners/runEvidencePack.js +362 -0
  80. package/bin/runners/runFirewall.js +0 -3
  81. package/bin/runners/runFirewallHook.js +0 -3
  82. package/bin/runners/runFix.js +66 -76
  83. package/bin/runners/runGuard.js +18 -411
  84. package/bin/runners/runInit.js +113 -30
  85. package/bin/runners/runLabs.js +424 -0
  86. package/bin/runners/runMcp.js +19 -25
  87. package/bin/runners/runPolish.js +64 -240
  88. package/bin/runners/runPromptFirewall.js +12 -5
  89. package/bin/runners/runProve.js +57 -22
  90. package/bin/runners/runQuickstart.js +531 -0
  91. package/bin/runners/runReality.js +59 -68
  92. package/bin/runners/runReport.js +38 -33
  93. package/bin/runners/runRuntime.js +8 -5
  94. package/bin/runners/runScan.js +1413 -190
  95. package/bin/runners/runShip.js +113 -719
  96. package/bin/runners/runTruth.js +0 -3
  97. package/bin/runners/runValidate.js +13 -9
  98. package/bin/runners/runWatch.js +23 -14
  99. package/bin/scan.js +6 -1
  100. package/bin/vibecheck.js +204 -185
  101. package/mcp-server/deprecation-middleware.js +282 -0
  102. package/mcp-server/handlers/index.ts +15 -0
  103. package/mcp-server/handlers/tool-handler.ts +554 -0
  104. package/mcp-server/index-v1.js +698 -0
  105. package/mcp-server/index.js +210 -238
  106. package/mcp-server/lib/cache-wrapper.cjs +383 -0
  107. package/mcp-server/lib/error-envelope.js +138 -0
  108. package/mcp-server/lib/executor.ts +499 -0
  109. package/mcp-server/lib/index.ts +19 -0
  110. package/mcp-server/lib/rate-limiter.js +166 -0
  111. package/mcp-server/lib/sandbox.test.ts +519 -0
  112. package/mcp-server/lib/sandbox.ts +395 -0
  113. package/mcp-server/lib/types.ts +267 -0
  114. package/mcp-server/package.json +12 -3
  115. package/mcp-server/registry/tool-registry.js +794 -0
  116. package/mcp-server/registry/tools.json +605 -0
  117. package/mcp-server/registry.test.ts +334 -0
  118. package/mcp-server/tests/tier-gating.test.js +297 -0
  119. package/mcp-server/tier-auth.js +378 -45
  120. package/mcp-server/tools-v3.js +353 -442
  121. package/mcp-server/tsconfig.json +37 -0
  122. package/mcp-server/vibecheck-2.0-tools.js +14 -1
  123. package/package.json +1 -1
  124. package/bin/runners/lib/agent-firewall/learning/learning-engine.js +0 -849
  125. package/bin/runners/lib/audit-logger.js +0 -532
  126. package/bin/runners/lib/authority/authorities/architecture.js +0 -364
  127. package/bin/runners/lib/authority/authorities/compliance.js +0 -341
  128. package/bin/runners/lib/authority/authorities/human.js +0 -343
  129. package/bin/runners/lib/authority/authorities/quality.js +0 -420
  130. package/bin/runners/lib/authority/authorities/security.js +0 -228
  131. package/bin/runners/lib/authority/index.js +0 -293
  132. package/bin/runners/lib/bundle/bundle-intelligence.js +0 -846
  133. package/bin/runners/lib/cli-charts.js +0 -368
  134. package/bin/runners/lib/cli-config-display.js +0 -405
  135. package/bin/runners/lib/cli-demo.js +0 -275
  136. package/bin/runners/lib/cli-errors.js +0 -438
  137. package/bin/runners/lib/cli-help-formatter.js +0 -439
  138. package/bin/runners/lib/cli-interactive-menu.js +0 -509
  139. package/bin/runners/lib/cli-prompts.js +0 -441
  140. package/bin/runners/lib/cli-scan-cards.js +0 -362
  141. package/bin/runners/lib/compliance-reporter.js +0 -710
  142. package/bin/runners/lib/conductor/index.js +0 -671
  143. package/bin/runners/lib/easy/README.md +0 -123
  144. package/bin/runners/lib/easy/index.js +0 -140
  145. package/bin/runners/lib/easy/interactive-wizard.js +0 -788
  146. package/bin/runners/lib/easy/one-click-firewall.js +0 -564
  147. package/bin/runners/lib/easy/zero-config-reality.js +0 -714
  148. package/bin/runners/lib/engines/async-patterns-engine.js +0 -444
  149. package/bin/runners/lib/engines/bundle-size-engine.js +0 -433
  150. package/bin/runners/lib/engines/confidence-scoring.js +0 -276
  151. package/bin/runners/lib/engines/context-detection.js +0 -264
  152. package/bin/runners/lib/engines/database-patterns-engine.js +0 -429
  153. package/bin/runners/lib/engines/duplicate-code-engine.js +0 -354
  154. package/bin/runners/lib/engines/env-variables-engine.js +0 -458
  155. package/bin/runners/lib/engines/error-handling-engine.js +0 -437
  156. package/bin/runners/lib/engines/false-positive-prevention.js +0 -630
  157. package/bin/runners/lib/engines/framework-adapters/index.js +0 -607
  158. package/bin/runners/lib/engines/framework-detection.js +0 -508
  159. package/bin/runners/lib/engines/import-order-engine.js +0 -429
  160. package/bin/runners/lib/engines/naming-conventions-engine.js +0 -544
  161. package/bin/runners/lib/engines/noise-reduction-engine.js +0 -452
  162. package/bin/runners/lib/engines/orchestrator.js +0 -334
  163. package/bin/runners/lib/engines/react-patterns-engine.js +0 -457
  164. package/bin/runners/lib/engines/vibecheck-engines/lib/ai-hallucination-engine.js +0 -806
  165. package/bin/runners/lib/engines/vibecheck-engines/lib/smart-fix-engine.js +0 -577
  166. package/bin/runners/lib/engines/vibecheck-engines/lib/vibe-score-engine.js +0 -543
  167. package/bin/runners/lib/engines/vibecheck-engines.js +0 -514
  168. package/bin/runners/lib/enhanced-features/index.js +0 -305
  169. package/bin/runners/lib/enhanced-output.js +0 -631
  170. package/bin/runners/lib/enterprise.js +0 -300
  171. package/bin/runners/lib/firewall/command-validator.js +0 -351
  172. package/bin/runners/lib/firewall/config.js +0 -341
  173. package/bin/runners/lib/firewall/content-validator.js +0 -519
  174. package/bin/runners/lib/firewall/index.js +0 -101
  175. package/bin/runners/lib/firewall/path-validator.js +0 -256
  176. package/bin/runners/lib/intelligence/cross-repo-intelligence.js +0 -817
  177. package/bin/runners/lib/mcp-utils.js +0 -425
  178. package/bin/runners/lib/output/index.js +0 -1022
  179. package/bin/runners/lib/policy-engine.js +0 -652
  180. package/bin/runners/lib/polish/autofix/accessibility-fixes.js +0 -333
  181. package/bin/runners/lib/polish/autofix/async-handlers.js +0 -273
  182. package/bin/runners/lib/polish/autofix/dead-code.js +0 -280
  183. package/bin/runners/lib/polish/autofix/imports-optimizer.js +0 -344
  184. package/bin/runners/lib/polish/autofix/index.js +0 -200
  185. package/bin/runners/lib/polish/autofix/remove-consoles.js +0 -209
  186. package/bin/runners/lib/polish/autofix/strengthen-types.js +0 -245
  187. package/bin/runners/lib/polish/backend-checks.js +0 -148
  188. package/bin/runners/lib/polish/documentation-checks.js +0 -111
  189. package/bin/runners/lib/polish/frontend-checks.js +0 -168
  190. package/bin/runners/lib/polish/index.js +0 -71
  191. package/bin/runners/lib/polish/infrastructure-checks.js +0 -131
  192. package/bin/runners/lib/polish/library-detection.js +0 -175
  193. package/bin/runners/lib/polish/performance-checks.js +0 -100
  194. package/bin/runners/lib/polish/security-checks.js +0 -148
  195. package/bin/runners/lib/polish/utils.js +0 -203
  196. package/bin/runners/lib/prompt-builder.js +0 -540
  197. package/bin/runners/lib/proof-certificate.js +0 -634
  198. package/bin/runners/lib/reality/accessibility-audit.js +0 -946
  199. package/bin/runners/lib/reality/api-contract-validator.js +0 -1012
  200. package/bin/runners/lib/reality/chaos-engineering.js +0 -1084
  201. package/bin/runners/lib/reality/performance-tracker.js +0 -1077
  202. package/bin/runners/lib/reality/scenario-generator.js +0 -1404
  203. package/bin/runners/lib/reality/visual-regression.js +0 -852
  204. package/bin/runners/lib/reality-profiler.js +0 -717
  205. package/bin/runners/lib/replay/flight-recorder-viewer.js +0 -1160
  206. package/bin/runners/lib/review/ai-code-review.js +0 -832
  207. package/bin/runners/lib/rules/custom-rule-engine.js +0 -985
  208. package/bin/runners/lib/sbom-generator.js +0 -641
  209. package/bin/runners/lib/scan-output-enhanced.js +0 -512
  210. package/bin/runners/lib/security/owasp-scanner.js +0 -939
  211. package/bin/runners/lib/validators/contract-validator.js +0 -283
  212. package/bin/runners/lib/validators/dead-export-detector.js +0 -279
  213. package/bin/runners/lib/validators/dep-audit.js +0 -245
  214. package/bin/runners/lib/validators/env-validator.js +0 -319
  215. package/bin/runners/lib/validators/index.js +0 -120
  216. package/bin/runners/lib/validators/license-checker.js +0 -252
  217. package/bin/runners/lib/validators/route-validator.js +0 -290
  218. package/bin/runners/runAuthority.js +0 -528
  219. package/bin/runners/runConductor.js +0 -772
  220. package/bin/runners/runContainer.js +0 -366
  221. package/bin/runners/runEasy.js +0 -410
  222. package/bin/runners/runIaC.js +0 -372
  223. package/bin/runners/runVibe.js +0 -791
  224. package/mcp-server/tools.js +0 -495
@@ -1,504 +1,235 @@
1
1
  /**
2
- * Unified CLI Output - Consistent, beautiful terminal output for all Vibecheck commands
2
+ * Unified CLI Output - Enterprise-Grade Terminal Experience
3
3
  *
4
4
  * ═══════════════════════════════════════════════════════════════════════════════
5
- * ENTERPRISE EDITION - World-Class Terminal Experience
5
+ * VIBECHECK DESIGN SYSTEM
6
6
  * ═══════════════════════════════════════════════════════════════════════════════
7
7
  *
8
- * Features:
9
- * - Consistent styling across all commands
10
- * - Beautiful tables with proper alignment
11
- * - Progress spinners with timing
12
- * - Status badges and verdict cards
13
- * - Tier-aware upsell messaging
14
- * - JSON output mode for CI
8
+ * This module provides consistent, professional output formatting for all CLI
9
+ * commands. Use these functions to ensure a unified enterprise-grade experience.
15
10
  */
16
11
 
17
12
  "use strict";
18
13
 
14
+ const { getApiKey } = require("./auth");
15
+
19
16
  // ═══════════════════════════════════════════════════════════════════════════════
20
- // CONFIGURATION
17
+ // CONSTANTS
21
18
  // ═══════════════════════════════════════════════════════════════════════════════
22
19
 
23
- const WIDTH = 76;
24
- const ESC = '\x1b';
25
-
26
- const SUPPORTS_COLOR = process.stdout.isTTY &&
27
- !process.env.NO_COLOR &&
28
- process.env.TERM !== 'dumb';
29
-
30
- const SUPPORTS_UNICODE = (() => {
31
- if (process.env.VIBECHECK_NO_UNICODE === '1') return false;
32
- if (process.platform === 'win32') {
33
- return Boolean(
34
- process.env.CI ||
35
- process.env.WT_SESSION ||
36
- process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm' ||
37
- (process.env.TERM || '').includes('256color') ||
38
- (process.env.LANG || '').toLowerCase().includes('utf')
39
- );
40
- }
41
- return process.env.TERM !== 'linux';
42
- })();
20
+ const WIDTH = 72;
21
+ const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
22
+ const SUPPORTS_UNICODE = !process.env.NO_UNICODE && process.platform !== "win32" || process.env.WT_SESSION;
43
23
 
44
24
  // ═══════════════════════════════════════════════════════════════════════════════
45
25
  // ANSI CODES
46
26
  // ═══════════════════════════════════════════════════════════════════════════════
47
27
 
48
- const ansi = {
49
- reset: SUPPORTS_COLOR ? `${ESC}[0m` : '',
50
- bold: SUPPORTS_COLOR ? `${ESC}[1m` : '',
51
- dim: SUPPORTS_COLOR ? `${ESC}[2m` : '',
52
- italic: SUPPORTS_COLOR ? `${ESC}[3m` : '',
53
- underline: SUPPORTS_COLOR ? `${ESC}[4m` : '',
54
-
55
- // Foreground
56
- black: SUPPORTS_COLOR ? `${ESC}[30m` : '',
57
- red: SUPPORTS_COLOR ? `${ESC}[31m` : '',
58
- green: SUPPORTS_COLOR ? `${ESC}[32m` : '',
59
- yellow: SUPPORTS_COLOR ? `${ESC}[33m` : '',
60
- blue: SUPPORTS_COLOR ? `${ESC}[34m` : '',
61
- magenta: SUPPORTS_COLOR ? `${ESC}[35m` : '',
62
- cyan: SUPPORTS_COLOR ? `${ESC}[36m` : '',
63
- white: SUPPORTS_COLOR ? `${ESC}[37m` : '',
64
- gray: SUPPORTS_COLOR ? `${ESC}[90m` : '',
65
-
66
- // Background
67
- bgRed: SUPPORTS_COLOR ? `${ESC}[41m` : '',
68
- bgGreen: SUPPORTS_COLOR ? `${ESC}[42m` : '',
69
- bgYellow: SUPPORTS_COLOR ? `${ESC}[43m` : '',
70
- bgBlue: SUPPORTS_COLOR ? `${ESC}[44m` : '',
71
- bgMagenta: SUPPORTS_COLOR ? `${ESC}[45m` : '',
72
- bgCyan: SUPPORTS_COLOR ? `${ESC}[46m` : '',
73
-
74
- // RGB (truecolor)
75
- rgb: (r, g, b) => SUPPORTS_COLOR ? `${ESC}[38;2;${r};${g};${b}m` : '',
76
- bgRgb: (r, g, b) => SUPPORTS_COLOR ? `${ESC}[48;2;${r};${g};${b}m` : '',
77
-
78
- // Cursor
79
- hideCursor: `${ESC}[?25l`,
80
- showCursor: `${ESC}[?25h`,
81
- clearLine: `${ESC}[2K`,
82
- cursorUp: (n = 1) => `${ESC}[${n}A`,
83
- };
84
-
85
- // ═══════════════════════════════════════════════════════════════════════════════
86
- // SYMBOLS & ICONS
28
+ const ESC = "\x1b";
29
+ const ansi = SUPPORTS_COLOR ? {
30
+ reset: `${ESC}[0m`,
31
+ bold: `${ESC}[1m`,
32
+ dim: `${ESC}[2m`,
33
+ italic: `${ESC}[3m`,
34
+ underline: `${ESC}[4m`,
35
+ red: `${ESC}[31m`,
36
+ green: `${ESC}[32m`,
37
+ yellow: `${ESC}[33m`,
38
+ blue: `${ESC}[34m`,
39
+ magenta: `${ESC}[35m`,
40
+ cyan: `${ESC}[36m`,
41
+ white: `${ESC}[37m`,
42
+ gray: `${ESC}[90m`,
43
+ bgGreen: `${ESC}[42m`,
44
+ bgRed: `${ESC}[41m`,
45
+ bgYellow: `${ESC}[43m`,
46
+ bgCyan: `${ESC}[46m`,
47
+ bgMagenta: `${ESC}[45m`,
48
+ } : Object.fromEntries([
49
+ "reset", "bold", "dim", "italic", "underline",
50
+ "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray",
51
+ "bgGreen", "bgRed", "bgYellow", "bgCyan", "bgMagenta"
52
+ ].map(k => [k, ""]));
53
+
54
+ // ═══════════════════════════════════════════════════════════════════════════════
55
+ // SYMBOLS
87
56
  // ═══════════════════════════════════════════════════════════════════════════════
88
57
 
89
58
  const sym = SUPPORTS_UNICODE ? {
90
- // Status
91
- check: '✓',
92
- cross: '✗',
93
- warning: '⚠',
94
- info: 'ℹ',
95
- bullet: '',
96
- arrow: '→',
97
- arrowDown: '↓',
98
- pointer: '❯',
99
-
100
- // Progress
101
- filled: '█',
102
- empty: '░',
103
-
104
- // Categories
105
- shield: '🛡️',
106
- lock: '🔒',
107
- key: '🔑',
108
- gear: '⚙',
109
- package: '📦',
110
- folder: '📁',
111
- file: '📄',
112
- search: '🔍',
113
- clock: '⏱',
114
- chart: '📊',
115
- target: '🎯',
116
- rocket: '🚀',
117
- star: '★',
118
- sparkles: '✨',
119
- fire: '🔥',
120
- lightning: '⚡',
121
- wrench: '🔧',
122
- doctor: '🩺',
123
-
124
- // Severity
125
- critical: '🛑',
126
- high: '🟠',
127
- medium: '🟡',
128
- low: '🔵',
129
-
130
- // Commands
131
- scan: '🔍',
132
- ship: '🚀',
133
- fix: '🔧',
134
- auth: '🔑',
135
- init: '⚙',
136
- truth: '✓',
137
- guard: '🛡️',
138
- } : {
139
- check: '+',
140
- cross: 'x',
141
- warning: '!',
142
- info: 'i',
143
- bullet: '-',
144
- arrow: '->',
145
- arrowDown: 'v',
146
- pointer: '>',
147
- filled: '#',
148
- empty: '-',
149
- shield: '[S]',
150
- lock: '[L]',
151
- key: '[K]',
152
- gear: '[G]',
153
- package: '[P]',
154
- folder: '[D]',
155
- file: '[F]',
156
- search: '[?]',
157
- clock: '[T]',
158
- chart: '[#]',
159
- target: '[O]',
160
- rocket: '^',
161
- star: '*',
162
- sparkles: '*',
163
- fire: '*',
164
- lightning: '!',
165
- wrench: '[W]',
166
- doctor: '[+]',
167
- critical: '[!]',
168
- high: '[H]',
169
- medium: '[M]',
170
- low: '[L]',
171
- scan: '[?]',
172
- ship: '^',
173
- fix: '[W]',
174
- auth: '[K]',
175
- init: '[G]',
176
- truth: '+',
177
- guard: '[S]',
178
- };
179
-
180
- // ═══════════════════════════════════════════════════════════════════════════════
181
- // BOX DRAWING
182
- // ═══════════════════════════════════════════════════════════════════════════════
183
-
184
- const box = SUPPORTS_UNICODE ? {
185
- topLeft: '┌', topRight: '┐',
186
- bottomLeft: '└', bottomRight: '┘',
187
- horizontal: '─', vertical: '│',
188
- cross: '┼',
189
- teeLeft: '├', teeRight: '┤',
190
- teeUp: '┴', teeDown: '┬',
191
- dTopLeft: '╔', dTopRight: '╗',
192
- dBottomLeft: '╚', dBottomRight: '╝',
193
- dHorizontal: '═', dVertical: '║',
194
- dTeeLeft: '╠', dTeeRight: '╣',
195
- rTopLeft: '╭', rTopRight: '╮',
196
- rBottomLeft: '╰', rBottomRight: '╯',
59
+ check: "✓",
60
+ cross: "✗",
61
+ warning: "⚠",
62
+ info: "ℹ",
63
+ arrow: "→",
64
+ bullet: "",
65
+ star: "★",
66
+ rocket: "🚀",
67
+ shield: "🛡️",
68
+ lock: "🔒",
69
+ key: "🔑",
70
+ gear: "⚙",
71
+ search: "🔍",
72
+ chart: "📊",
73
+ file: "📄",
74
+ folder: "📁",
75
+ clock: "⏱",
76
+ lightning: "⚡",
77
+ sparkle: "✨",
78
+ fire: "🔥",
79
+ target: "🎯",
80
+ trophy: "🏆",
81
+ box: {
82
+ tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║",
83
+ ltl: "┌", ltr: "┐", lbl: "└", lbr: "┘", lh: "─", lv: "│",
84
+ lt: "┬", lb: "┴", lx: "┼", ltrT: "├", ltlT: "┤",
85
+ },
197
86
  } : {
198
- topLeft: '+', topRight: '+',
199
- bottomLeft: '+', bottomRight: '+',
200
- horizontal: '-', vertical: '|',
201
- cross: '+',
202
- teeLeft: '+', teeRight: '+',
203
- teeUp: '+', teeDown: '+',
204
- dTopLeft: '+', dTopRight: '+',
205
- dBottomLeft: '+', dBottomRight: '+',
206
- dHorizontal: '=', dVertical: '|',
207
- dTeeLeft: '+', dTeeRight: '+',
208
- rTopLeft: '+', rTopRight: '+',
209
- rBottomLeft: '+', rBottomRight: '+',
87
+ check: "+",
88
+ cross: "x",
89
+ warning: "!",
90
+ info: "i",
91
+ arrow: "->",
92
+ bullet: "*",
93
+ star: "*",
94
+ rocket: ">",
95
+ shield: "[S]",
96
+ lock: "[L]",
97
+ key: "[K]",
98
+ gear: "[G]",
99
+ search: "[?]",
100
+ chart: "[C]",
101
+ file: "[F]",
102
+ folder: "[D]",
103
+ clock: "[T]",
104
+ lightning: "[!]",
105
+ sparkle: "*",
106
+ fire: "(!)",
107
+ target: "(>)",
108
+ trophy: "[W]",
109
+ box: {
110
+ tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|",
111
+ ltl: "+", ltr: "+", lbl: "+", lbr: "+", lh: "-", lv: "|",
112
+ lt: "+", lb: "+", lx: "+", ltrT: "+", ltlT: "+",
113
+ },
210
114
  };
211
115
 
212
116
  // ═══════════════════════════════════════════════════════════════════════════════
213
- // TIER CONFIGURATION
117
+ // TIER BADGES
214
118
  // ═══════════════════════════════════════════════════════════════════════════════
215
119
 
216
120
  const TIER = {
217
- FREE: 'free',
218
- PRO: 'pro',
219
- };
220
-
221
- const TIER_COLORS = {
222
- free: ansi.gray,
223
- pro: ansi.magenta,
224
- };
225
-
226
- const TIER_LABELS = {
227
- free: 'FREE',
228
- pro: 'PRO',
121
+ free: { label: "FREE", color: ansi.green, bg: ansi.bgGreen },
122
+ starter: { label: "STARTER", color: ansi.cyan, bg: ansi.bgCyan },
123
+ pro: { label: "PRO", color: ansi.magenta, bg: ansi.bgMagenta },
124
+ complete: { label: "COMPLETE", color: ansi.yellow, bg: ansi.bgYellow },
229
125
  };
230
126
 
231
127
  function getTierBadge(tier) {
232
- const color = TIER_COLORS[tier] || ansi.gray;
233
- const label = TIER_LABELS[tier] || tier?.toUpperCase() || 'FREE';
234
- return `${color}${ansi.bold}${label}${ansi.reset}`;
128
+ const t = TIER[tier] || TIER.free;
129
+ return `${t.color}[${t.label}]${ansi.reset}`;
235
130
  }
236
131
 
237
- function getTierColor(tier) {
238
- return TIER_COLORS[tier] || ansi.gray;
132
+ function getTierFromKey() {
133
+ const { key } = getApiKey();
134
+ if (!key) return "free";
135
+ // Simplified tier detection - in real app would check entitlements
136
+ return "starter";
239
137
  }
240
138
 
241
139
  // ═══════════════════════════════════════════════════════════════════════════════
242
- // TEXT UTILITIES
140
+ // UTILITY FUNCTIONS
243
141
  // ═══════════════════════════════════════════════════════════════════════════════
244
142
 
245
- function visibleLength(str) {
246
- return (str || '').replace(/\x1b\[[0-9;]*m/g, '').length;
247
- }
248
-
249
- function padRight(str, width, char = ' ') {
250
- const visible = visibleLength(str);
251
- if (visible >= width) return str;
252
- return str + char.repeat(width - visible);
143
+ function stripAnsi(str) {
144
+ return str.replace(/\x1b\[\d+m/g, "").replace(/\x1b\[38;2;\d+;\d+;\d+m/g, "");
253
145
  }
254
146
 
255
- function padLeft(str, width, char = ' ') {
256
- const visible = visibleLength(str);
257
- if (visible >= width) return str;
258
- return char.repeat(width - visible) + str;
147
+ function padCenter(str, width = WIDTH) {
148
+ const len = stripAnsi(str).length;
149
+ const padding = Math.max(0, width - len);
150
+ const left = Math.floor(padding / 2);
151
+ return " ".repeat(left) + str + " ".repeat(padding - left);
259
152
  }
260
153
 
261
- function center(str, width) {
262
- const visible = visibleLength(str);
263
- if (visible >= width) return str;
264
- const left = Math.floor((width - visible) / 2);
265
- const right = width - visible - left;
266
- return ' '.repeat(left) + str + ' '.repeat(right);
154
+ function padRight(str, width) {
155
+ const len = stripAnsi(str).length;
156
+ return str + " ".repeat(Math.max(0, width - len));
267
157
  }
268
158
 
269
159
  function truncate(str, maxLen) {
270
- if (!str) return '';
271
- const visible = visibleLength(str);
272
- if (visible <= maxLen) return str;
273
- return str.substring(0, maxLen - 3) + '...';
160
+ const clean = stripAnsi(str);
161
+ if (clean.length <= maxLen) return str;
162
+ return clean.slice(0, maxLen - 3) + "...";
274
163
  }
275
164
 
276
165
  function formatDuration(ms) {
277
166
  if (ms < 1000) return `${ms}ms`;
278
167
  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
279
- const mins = Math.floor(ms / 60000);
280
- const secs = Math.floor((ms % 60000) / 1000);
281
- return `${mins}m ${secs}s`;
282
- }
283
-
284
- function formatBytes(bytes) {
285
- if (bytes < 1024) return `${bytes}B`;
286
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
287
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
288
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
289
- }
290
-
291
- // ═══════════════════════════════════════════════════════════════════════════════
292
- // SPINNER CLASS
293
- // ═══════════════════════════════════════════════════════════════════════════════
294
-
295
- const SPINNER_FRAMES = SUPPORTS_UNICODE
296
- ? ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
297
- : ['-', '\\', '|', '/'];
298
-
299
- class Spinner {
300
- constructor(text = '') {
301
- this.text = text;
302
- this.timer = null;
303
- this.frameIndex = 0;
304
- this.startTime = null;
305
- this.stream = process.stderr;
306
- }
307
-
308
- start(text) {
309
- if (text) this.text = text;
310
- this.startTime = Date.now();
311
-
312
- if (!process.stdout.isTTY) {
313
- console.log(` ${sym.info} ${this.text}`);
314
- return this;
315
- }
316
-
317
- this.stream.write(ansi.hideCursor);
318
- this.timer = setInterval(() => {
319
- const frame = SPINNER_FRAMES[this.frameIndex];
320
- const elapsed = formatDuration(Date.now() - this.startTime);
321
- this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
322
- this.stream.write(`\r${ansi.clearLine} ${ansi.cyan}${frame}${ansi.reset} ${this.text} ${ansi.dim}${elapsed}${ansi.reset}`);
323
- }, 80);
324
- return this;
325
- }
326
-
327
- update(text) {
328
- this.text = text;
329
- return this;
330
- }
331
-
332
- stop(finalIcon, finalColor, finalText) {
333
- if (this.timer) {
334
- clearInterval(this.timer);
335
- this.timer = null;
336
- }
337
- const elapsed = this.startTime ? formatDuration(Date.now() - this.startTime) : '';
338
- this.stream.write(`\r${ansi.clearLine}`);
339
-
340
- if (finalIcon !== null && finalText !== undefined && process.stdout.isTTY) {
341
- console.log(` ${finalColor || ''}${finalIcon || ''}${ansi.reset} ${finalText || this.text} ${ansi.dim}${elapsed}${ansi.reset}`);
342
- }
343
- this.stream.write(ansi.showCursor);
344
- this.startTime = null;
345
- return this;
346
- }
347
-
348
- succeed(text) {
349
- return this.stop(sym.check, ansi.green, text || this.text);
350
- }
351
-
352
- fail(text) {
353
- return this.stop(sym.cross, ansi.red, text || this.text);
354
- }
355
-
356
- warn(text) {
357
- return this.stop(sym.warning, ansi.yellow, text || this.text);
358
- }
359
-
360
- info(text) {
361
- return this.stop(sym.info, ansi.cyan, text || this.text);
362
- }
168
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
363
169
  }
364
170
 
365
171
  // ═══════════════════════════════════════════════════════════════════════════════
366
- // PROGRESS BAR
172
+ // HEADER / BANNER
367
173
  // ═══════════════════════════════════════════════════════════════════════════════
368
174
 
369
- function progressBar(percent, width = 30, options = {}) {
370
- const {
371
- filled = sym.filled,
372
- empty = sym.empty,
373
- showPercent = true,
374
- } = options;
375
-
376
- const clamped = Math.max(0, Math.min(100, percent));
377
- const filledCount = Math.round((clamped / 100) * width);
378
-
379
- let color;
380
- if (options.color) {
381
- color = options.color;
382
- } else if (clamped >= 80) {
383
- color = ansi.green;
384
- } else if (clamped >= 50) {
385
- color = ansi.yellow;
386
- } else {
387
- color = ansi.red;
175
+ /**
176
+ * Render a compact enterprise header for any command
177
+ * @param {object} opts - Header options
178
+ * @param {string} opts.command - Command name (e.g., "scan", "ship")
179
+ * @param {string} opts.title - Title text
180
+ * @param {string} opts.tier - Tier requirement (free, starter, pro)
181
+ * @param {string} [opts.subtitle] - Optional subtitle
182
+ */
183
+ function renderHeader(opts) {
184
+ const { command, title, tier = "free", subtitle } = opts;
185
+ const tierBadge = getTierBadge(tier);
186
+ const b = sym.box;
187
+
188
+ const lines = [];
189
+
190
+ // Top border
191
+ lines.push(`${ansi.gray}${b.tl}${b.h.repeat(WIDTH - 2)}${b.tr}${ansi.reset}`);
192
+
193
+ // Title line
194
+ const titleStr = `${ansi.bold}${title}${ansi.reset} ${tierBadge}`;
195
+ lines.push(`${ansi.gray}${b.v}${ansi.reset}${padCenter(titleStr, WIDTH - 2)}${ansi.gray}${b.v}${ansi.reset}`);
196
+
197
+ // Subtitle if provided
198
+ if (subtitle) {
199
+ const subStr = `${ansi.dim}${subtitle}${ansi.reset}`;
200
+ lines.push(`${ansi.gray}${b.v}${ansi.reset}${padCenter(subStr, WIDTH - 2)}${ansi.gray}${b.v}${ansi.reset}`);
388
201
  }
389
202
 
390
- const bar = `${color}${filled.repeat(filledCount)}${ansi.gray}${empty.repeat(width - filledCount)}${ansi.reset}`;
391
- const percentStr = showPercent ? ` ${String(Math.round(clamped)).padStart(3)}%` : '';
203
+ // Bottom border
204
+ lines.push(`${ansi.gray}${b.bl}${b.h.repeat(WIDTH - 2)}${b.br}${ansi.reset}`);
392
205
 
393
- return `${bar}${ansi.gray}${percentStr}${ansi.reset}`;
206
+ console.log(lines.join("\n"));
394
207
  }
395
208
 
396
- // ═══════════════════════════════════════════════════════════════════════════════
397
- // TABLE RENDERING
398
- // ═══════════════════════════════════════════════════════════════════════════════
399
-
400
- function renderTable(options) {
401
- const { columns, data, title, maxRows = 20, showRowNumbers = false } = options;
402
- const lines = [];
403
-
404
- // Calculate column widths
405
- const colWidths = columns.map((col) => {
406
- const headerLen = visibleLength(col.header);
407
- const maxDataLen = data.reduce((max, row) => {
408
- const formatted = col.format ? col.format(row[col.key], row) : String(row[col.key] || '');
409
- return Math.max(max, visibleLength(formatted));
410
- }, 0);
411
- return col.width || Math.min(Math.max(headerLen, maxDataLen) + 2, 40);
412
- });
413
-
414
- if (showRowNumbers) {
415
- colWidths.unshift(4);
416
- }
417
-
418
- // Title
419
- if (title) {
420
- lines.push('');
421
- lines.push(` ${ansi.bold}${title}${ansi.reset}`);
422
- lines.push(` ${ansi.gray}${box.horizontal.repeat(Math.min(WIDTH - 4, 72))}${ansi.reset}`);
423
- }
424
-
425
- // Header row
426
- const headerBorder = ` ${box.topLeft}${colWidths.map(w => box.horizontal.repeat(w)).join(box.teeDown)}${box.topRight}`;
427
- lines.push(ansi.gray + headerBorder + ansi.reset);
428
-
429
- let headerRow = ` ${ansi.gray}${box.vertical}${ansi.reset}`;
430
- if (showRowNumbers) {
431
- headerRow += center('#', colWidths[0]) + `${ansi.gray}${box.vertical}${ansi.reset}`;
432
- }
433
- columns.forEach((col, i) => {
434
- const idx = showRowNumbers ? i + 1 : i;
435
- headerRow += `${ansi.bold}${center(col.header, colWidths[idx])}${ansi.reset}${ansi.gray}${box.vertical}${ansi.reset}`;
436
- });
437
- lines.push(headerRow);
438
-
439
- // Header separator
440
- const headerSep = ` ${box.teeLeft}${colWidths.map(w => box.horizontal.repeat(w)).join(box.cross)}${box.teeRight}`;
441
- lines.push(ansi.gray + headerSep + ansi.reset);
442
-
443
- // Data rows
444
- const displayData = data.slice(0, maxRows);
445
- displayData.forEach((row, rowIndex) => {
446
- let dataRow = ` ${ansi.gray}${box.vertical}${ansi.reset}`;
447
- if (showRowNumbers) {
448
- dataRow += `${ansi.gray}${center(String(rowIndex + 1), colWidths[0])}${box.vertical}${ansi.reset}`;
449
- }
450
- columns.forEach((col, i) => {
451
- const idx = showRowNumbers ? i + 1 : i;
452
- const value = col.format ? col.format(row[col.key], row) : String(row[col.key] || '');
453
- const aligned = col.align === 'right'
454
- ? padLeft(truncate(value, colWidths[idx] - 1), colWidths[idx])
455
- : col.align === 'center'
456
- ? center(truncate(value, colWidths[idx] - 1), colWidths[idx])
457
- : padRight(' ' + truncate(value, colWidths[idx] - 2), colWidths[idx]);
458
- dataRow += `${aligned}${ansi.gray}${box.vertical}${ansi.reset}`;
459
- });
460
- lines.push(dataRow);
461
- });
462
-
463
- // Footer border
464
- const footerBorder = ` ${box.bottomLeft}${colWidths.map(w => box.horizontal.repeat(w)).join(box.teeUp)}${box.bottomRight}`;
465
- lines.push(ansi.gray + footerBorder + ansi.reset);
466
-
467
- // Truncation notice
468
- if (data.length > maxRows) {
469
- lines.push(` ${ansi.gray}... and ${data.length - maxRows} more rows${ansi.reset}`);
470
- }
471
-
472
- return lines.join('\n');
209
+ /**
210
+ * Render a minimal header (single line)
211
+ */
212
+ function renderMinimalHeader(command, tier = "free") {
213
+ const tierBadge = getTierBadge(tier);
214
+ console.log(`\n ${ansi.bold}vibecheck ${command}${ansi.reset} ${tierBadge}\n`);
473
215
  }
474
216
 
475
217
  // ═══════════════════════════════════════════════════════════════════════════════
476
- // HEADERS & SECTIONS
218
+ // SECTION DIVIDERS
477
219
  // ═══════════════════════════════════════════════════════════════════════════════
478
220
 
479
- function renderMinimalHeader(command, tier = 'free') {
480
- const cmdIcon = sym[command] || sym.sparkles;
481
- const tierBadge = getTierBadge(tier);
482
-
483
- console.log();
484
- console.log(` ${ansi.cyan}${cmdIcon}${ansi.reset} ${ansi.bold}VIBECHECK ${command.toUpperCase()}${ansi.reset} ${ansi.gray}•${ansi.reset} ${tierBadge}`);
485
- console.log(` ${ansi.gray}${box.horizontal.repeat(68)}${ansi.reset}`);
486
- console.log();
487
- }
488
-
489
- function renderSectionHeader(title, icon) {
490
- const iconStr = icon ? `${icon} ` : '';
491
- console.log();
492
- console.log(` ${ansi.cyan}${iconStr}${ansi.bold}${title}${ansi.reset}`);
493
- console.log(` ${ansi.gray}${box.horizontal.repeat(WIDTH - 4)}${ansi.reset}`);
221
+ function renderDivider(style = "single") {
222
+ const char = style === "double" ? sym.box.h : sym.box.lh;
223
+ console.log(` ${ansi.gray}${char.repeat(WIDTH - 4)}${ansi.reset}`);
494
224
  }
495
225
 
496
- function renderDivider(char = box.horizontal, width = 68) {
497
- console.log(` ${ansi.gray}${char.repeat(width)}${ansi.reset}`);
226
+ function renderSectionHeader(title, icon = sym.bullet) {
227
+ console.log(`\n ${ansi.cyan}${icon}${ansi.reset} ${ansi.bold}${title}${ansi.reset}`);
228
+ console.log(` ${ansi.gray}${sym.box.lh.repeat(WIDTH - 4)}${ansi.reset}`);
498
229
  }
499
230
 
500
231
  // ═══════════════════════════════════════════════════════════════════════════════
501
- // STATUS MESSAGES
232
+ // STATUS INDICATORS
502
233
  // ═══════════════════════════════════════════════════════════════════════════════
503
234
 
504
235
  function renderSuccess(message) {
@@ -517,193 +248,306 @@ function renderInfo(message) {
517
248
  console.log(` ${ansi.cyan}${sym.info}${ansi.reset} ${message}`);
518
249
  }
519
250
 
520
- function renderBullet(message) {
521
- console.log(` ${ansi.gray}${sym.bullet}${ansi.reset} ${message}`);
251
+ function renderBullet(message, indent = 2) {
252
+ console.log(`${" ".repeat(indent)}${ansi.gray}${sym.bullet}${ansi.reset} ${message}`);
253
+ }
254
+
255
+ function renderStep(number, message, status = "pending") {
256
+ const statusIcon = status === "done" ? `${ansi.green}${sym.check}${ansi.reset}` :
257
+ status === "error" ? `${ansi.red}${sym.cross}${ansi.reset}` :
258
+ status === "running" ? `${ansi.cyan}${sym.gear}${ansi.reset}` :
259
+ `${ansi.gray}${number}${ansi.reset}`;
260
+ console.log(` ${statusIcon} ${message}`);
522
261
  }
523
262
 
524
263
  // ═══════════════════════════════════════════════════════════════════════════════
525
- // KEY-VALUE DISPLAY
264
+ // VERDICT / RESULT BOX
526
265
  // ═══════════════════════════════════════════════════════════════════════════════
527
266
 
528
- function renderKeyValue(pairs, options = {}) {
529
- const { labelWidth = 14, indent = 2 } = options;
530
- const prefix = ' '.repeat(indent);
267
+ /**
268
+ * Render a verdict box (SHIP/WARN/BLOCK)
269
+ * @param {string} verdict - SHIP, WARN, or BLOCK
270
+ * @param {object} [stats] - Optional statistics
271
+ */
272
+ function renderVerdict(verdict, stats = {}) {
273
+ const b = sym.box;
274
+ const v = String(verdict).toUpperCase();
275
+
276
+ let bg, icon, label;
277
+ switch (v) {
278
+ case "SHIP":
279
+ case "PASS":
280
+ case "SUCCESS":
281
+ bg = ansi.bgGreen;
282
+ icon = sym.rocket;
283
+ label = "SHIP";
284
+ break;
285
+ case "WARN":
286
+ case "WARNING":
287
+ bg = ansi.bgYellow;
288
+ icon = sym.warning;
289
+ label = "WARN";
290
+ break;
291
+ case "BLOCK":
292
+ case "FAIL":
293
+ case "ERROR":
294
+ bg = ansi.bgRed;
295
+ icon = sym.cross;
296
+ label = "BLOCK";
297
+ break;
298
+ default:
299
+ bg = ansi.bgCyan;
300
+ icon = sym.info;
301
+ label = v;
302
+ }
531
303
 
532
- pairs.forEach(({ label, value }) => {
533
- console.log(`${prefix}${ansi.gray}${padRight(label + ':', labelWidth)}${ansi.reset} ${value}`);
534
- });
304
+ console.log();
305
+ console.log(` ${ansi.gray}${b.tl}${b.h.repeat(WIDTH - 6)}${b.tr}${ansi.reset}`);
306
+
307
+ // Verdict line
308
+ const verdictStr = `${bg}${ansi.bold}${ansi.white} ${icon} ${label} ${ansi.reset}`;
309
+ console.log(` ${ansi.gray}${b.v}${ansi.reset}${padCenter(verdictStr, WIDTH - 4)}${ansi.gray}${b.v}${ansi.reset}`);
310
+
311
+ // Stats line if provided
312
+ if (stats.findings !== undefined || stats.duration !== undefined) {
313
+ const parts = [];
314
+ if (stats.critical !== undefined) parts.push(`${ansi.red}${stats.critical} critical${ansi.reset}`);
315
+ if (stats.warnings !== undefined) parts.push(`${ansi.yellow}${stats.warnings} warnings${ansi.reset}`);
316
+ if (stats.findings !== undefined && stats.critical === undefined) parts.push(`${stats.findings} findings`);
317
+ if (stats.duration !== undefined) parts.push(`${ansi.dim}${formatDuration(stats.duration)}${ansi.reset}`);
318
+
319
+ const statsStr = parts.join(` ${ansi.gray}${sym.bullet}${ansi.reset} `);
320
+ console.log(` ${ansi.gray}${b.v}${ansi.reset}${padCenter(statsStr, WIDTH - 4)}${ansi.gray}${b.v}${ansi.reset}`);
321
+ }
322
+
323
+ console.log(` ${ansi.gray}${b.bl}${b.h.repeat(WIDTH - 6)}${b.br}${ansi.reset}`);
324
+ console.log();
535
325
  }
536
326
 
537
327
  // ═══════════════════════════════════════════════════════════════════════════════
538
- // VERDICT DISPLAY
328
+ // FINDINGS TABLE
539
329
  // ═══════════════════════════════════════════════════════════════════════════════
540
330
 
541
- function renderVerdict(verdict, score) {
542
- const verdictConfig = {
543
- SHIP: { badge: `${ansi.bgGreen}${ansi.bold}${ansi.white} SHIP ${ansi.reset}`, icon: sym.rocket, message: 'Ready to ship!' },
544
- PASS: { badge: `${ansi.bgGreen}${ansi.bold}${ansi.white} PASS ${ansi.reset}`, icon: sym.check, message: 'All checks passed' },
545
- WARN: { badge: `${ansi.bgYellow}${ansi.bold} WARN ${ansi.reset}`, icon: sym.warning, message: 'Review recommended' },
546
- BLOCK: { badge: `${ansi.bgRed}${ansi.bold}${ansi.white} BLOCK ${ansi.reset}`, icon: sym.cross, message: 'Critical issues found' },
547
- FAIL: { badge: `${ansi.bgRed}${ansi.bold}${ansi.white} FAIL ${ansi.reset}`, icon: sym.cross, message: 'Scan failed' },
548
- };
549
-
550
- const config = verdictConfig[verdict] || verdictConfig.WARN;
331
+ /**
332
+ * Render a findings table
333
+ * @param {Array} findings - Array of finding objects
334
+ * @param {object} opts - Options
335
+ * @param {number} [opts.limit=5] - Max findings to show
336
+ */
337
+ function renderFindingsTable(findings, opts = {}) {
338
+ const { limit = 5 } = opts;
339
+ const b = sym.box;
551
340
 
552
- console.log();
553
- console.log(` ${config.badge} ${config.icon} ${config.message}`);
341
+ if (!findings || findings.length === 0) {
342
+ console.log(` ${ansi.green}${sym.check}${ansi.reset} ${ansi.dim}No issues found${ansi.reset}`);
343
+ return;
344
+ }
554
345
 
555
- if (score !== undefined && score !== null) {
556
- const scoreColor = score >= 80 ? ansi.green : score >= 60 ? ansi.yellow : ansi.red;
557
- console.log(` ${ansi.gray}Health Score:${ansi.reset} ${scoreColor}${ansi.bold}${score}${ansi.reset}${ansi.gray}/100${ansi.reset}`);
558
- console.log(` ${progressBar(score, 40)}`);
346
+ const COL1 = 10; // Severity
347
+ const COL2 = 18; // Category
348
+ const COL3 = WIDTH - COL1 - COL2 - 12; // Message
349
+
350
+ // Header
351
+ console.log(` ${ansi.gray}${b.ltl}${b.lh.repeat(COL1)}${b.lt}${b.lh.repeat(COL2)}${b.lt}${b.lh.repeat(COL3)}${b.ltr}${ansi.reset}`);
352
+ console.log(` ${ansi.gray}${b.lv}${ansi.reset}${ansi.bold}${padRight(" SEVERITY", COL1)}${ansi.reset}${ansi.gray}${b.lv}${ansi.reset}${ansi.bold}${padRight(" CATEGORY", COL2)}${ansi.reset}${ansi.gray}${b.lv}${ansi.reset}${ansi.bold}${padRight(" MESSAGE", COL3)}${ansi.reset}${ansi.gray}${b.lv}${ansi.reset}`);
353
+ console.log(` ${ansi.gray}${b.ltrT}${b.lh.repeat(COL1)}${b.lx}${b.lh.repeat(COL2)}${b.lx}${b.lh.repeat(COL3)}${b.ltlT}${ansi.reset}`);
354
+
355
+ // Rows
356
+ const shown = findings.slice(0, limit);
357
+ for (const f of shown) {
358
+ const sev = f.severity?.toUpperCase() || "INFO";
359
+ let sevStr;
360
+ switch (sev) {
361
+ case "BLOCK":
362
+ case "CRITICAL":
363
+ sevStr = `${ansi.red} ${sym.cross} BLOCK${ansi.reset}`;
364
+ break;
365
+ case "WARN":
366
+ case "WARNING":
367
+ case "HIGH":
368
+ sevStr = `${ansi.yellow} ${sym.warning} WARN${ansi.reset}`;
369
+ break;
370
+ default:
371
+ sevStr = `${ansi.gray} ${sym.info} INFO${ansi.reset}`;
372
+ }
373
+
374
+ const cat = truncate(f.category || f.type || "General", COL2 - 1);
375
+ const msg = truncate(f.message || f.title || f.id || "", COL3 - 1);
376
+
377
+ console.log(` ${ansi.gray}${b.lv}${ansi.reset}${padRight(sevStr, COL1 + 9)}${ansi.gray}${b.lv}${ansi.reset}${padRight(" " + cat, COL2)}${ansi.gray}${b.lv}${ansi.reset}${padRight(" " + msg, COL3)}${ansi.gray}${b.lv}${ansi.reset}`);
378
+ }
379
+
380
+ // Footer
381
+ console.log(` ${ansi.gray}${b.lbl}${b.lh.repeat(COL1)}${b.lb}${b.lh.repeat(COL2)}${b.lb}${b.lh.repeat(COL3)}${b.lbr}${ansi.reset}`);
382
+
383
+ // More indicator
384
+ if (findings.length > limit) {
385
+ console.log(` ${ansi.dim}... and ${findings.length - limit} more findings${ansi.reset}`);
559
386
  }
560
- console.log();
561
387
  }
562
388
 
563
389
  // ═══════════════════════════════════════════════════════════════════════════════
564
- // FINDINGS DISPLAY
390
+ // KEY-VALUE LIST
565
391
  // ═══════════════════════════════════════════════════════════════════════════════
566
392
 
567
- function renderFindings(findings, maxDisplay = 10) {
568
- if (!findings || findings.length === 0) {
569
- console.log(` ${ansi.green}${sym.check}${ansi.reset} No issues found!`);
570
- return;
393
+ /**
394
+ * Render a key-value list
395
+ * @param {Array<{label: string, value: string}>} items
396
+ */
397
+ function renderKeyValue(items) {
398
+ const maxLabel = Math.max(...items.map(i => stripAnsi(i.label).length));
399
+
400
+ for (const item of items) {
401
+ const label = padRight(item.label, maxLabel);
402
+ console.log(` ${ansi.dim}${label}${ansi.reset} ${item.value}`);
571
403
  }
404
+ }
572
405
 
573
- const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
574
- const sorted = [...findings].sort((a, b) =>
575
- (severityOrder[a.severity] || 4) - (severityOrder[b.severity] || 4)
576
- );
577
-
578
- console.log(renderTable({
579
- title: `${sym.warning} Issues Found (${findings.length})`,
580
- columns: [
581
- {
582
- header: 'Sev',
583
- key: 'severity',
584
- width: 10,
585
- format: (v) => `${sym[v] || sym.info} ${(v || 'INFO').toUpperCase().substring(0, 4)}`,
586
- },
587
- { header: 'Category', key: 'category', width: 15 },
588
- { header: 'Message', key: 'message', width: 42 },
589
- ],
590
- data: sorted.slice(0, maxDisplay),
591
- maxRows: maxDisplay,
592
- }));
406
+ // ═══════════════════════════════════════════════════════════════════════════════
407
+ // PROGRESS BAR
408
+ // ═══════════════════════════════════════════════════════════════════════════════
409
+
410
+ function renderProgressBar(percent, width = 20) {
411
+ const filled = Math.round((percent / 100) * width);
412
+ const empty = width - filled;
413
+
414
+ let color = ansi.green;
415
+ if (percent < 50) color = ansi.red;
416
+ else if (percent < 80) color = ansi.yellow;
417
+
418
+ return `${color}${"█".repeat(filled)}${ansi.gray}${"░".repeat(empty)}${ansi.reset}`;
419
+ }
420
+
421
+ function renderScore(score, label = "Score") {
422
+ const bar = renderProgressBar(score);
423
+ console.log(` ${ansi.dim}${label}:${ansi.reset} [${bar}] ${score}/100`);
593
424
  }
594
425
 
595
426
  // ═══════════════════════════════════════════════════════════════════════════════
596
- // FOOTER & NEXT STEPS
427
+ // FOOTER / NEXT STEPS
597
428
  // ═══════════════════════════════════════════════════════════════════════════════
598
429
 
599
- function renderFooter(options = {}) {
600
- const { nextSteps, showUpsell, tier = 'free' } = options;
430
+ /**
431
+ * Render footer with next steps and optional upsell
432
+ * @param {object} opts
433
+ * @param {Array<{cmd: string, desc: string}>} [opts.nextSteps]
434
+ * @param {string} [opts.docsUrl]
435
+ * @param {boolean} [opts.showUpsell]
436
+ */
437
+ function renderFooter(opts = {}) {
438
+ const { nextSteps = [], docsUrl, showUpsell = true } = opts;
439
+ const currentTier = getTierFromKey();
601
440
 
602
441
  console.log();
442
+ renderDivider();
603
443
 
604
- if (nextSteps && nextSteps.length > 0) {
605
- console.log(` ${ansi.gray}Next Steps:${ansi.reset}`);
606
- nextSteps.forEach(({ cmd, desc }) => {
607
- console.log(` ${ansi.cyan}${cmd}${ansi.reset} ${ansi.gray}${desc}${ansi.reset}`);
608
- });
444
+ // Next steps
445
+ if (nextSteps.length > 0) {
446
+ console.log(`\n ${ansi.bold}${sym.arrow} Next steps${ansi.reset}`);
447
+ for (const step of nextSteps) {
448
+ console.log(` ${ansi.cyan}${step.cmd}${ansi.reset} ${ansi.dim}${step.desc}${ansi.reset}`);
449
+ }
609
450
  }
610
451
 
611
- if (showUpsell && tier === 'free') {
452
+ // Upsell for free tier
453
+ if (showUpsell && currentTier === "free") {
612
454
  console.log();
613
- console.log(` ${ansi.gray}${sym.star}${ansi.reset} ${ansi.magenta}PRO${ansi.reset}${ansi.gray}: auto-fix + AI explanations + badge generation${ansi.reset}`);
614
- console.log(` ${ansi.gray} Upgrade ${sym.arrow} https://vibecheckai.dev${ansi.reset}`);
455
+ console.log(` ${ansi.dim}${sym.star}${ansi.reset} ${ansi.cyan}STARTER${ansi.reset}${ansi.dim}: dashboard sync + auto-fix + team features${ansi.reset}`);
456
+ console.log(` ${ansi.dim} Upgrade ${sym.arrow} https://vibecheckai.dev${ansi.reset}`);
457
+ }
458
+
459
+ // Docs link
460
+ if (docsUrl) {
461
+ console.log();
462
+ console.log(` ${ansi.dim}Docs: ${docsUrl}${ansi.reset}`);
615
463
  }
616
464
 
617
465
  console.log();
618
466
  }
619
467
 
620
468
  // ═══════════════════════════════════════════════════════════════════════════════
621
- // DOCTOR CHECK RESULTS
469
+ // SPINNER
622
470
  // ═══════════════════════════════════════════════════════════════════════════════
623
471
 
624
- function renderDoctorCheck(check) {
625
- const statusConfig = {
626
- pass: { icon: sym.check, color: ansi.green },
627
- warn: { icon: sym.warning, color: ansi.yellow },
628
- fail: { icon: sym.cross, color: ansi.red },
629
- info: { icon: sym.info, color: ansi.cyan },
630
- skip: { icon: sym.bullet, color: ansi.gray },
631
- };
632
-
633
- const config = statusConfig[check.status] || statusConfig.info;
634
- const nameStr = padRight(check.name, 20);
635
- const message = check.message || '';
636
-
637
- console.log(` ${config.color}${config.icon}${ansi.reset} ${nameStr} ${ansi.gray}${message}${ansi.reset}`);
638
-
639
- if (check.fix && check.status !== 'pass') {
640
- console.log(` ${ansi.dim}${sym.arrow} Fix: ${check.fix}${ansi.reset}`);
641
- }
642
- }
472
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
643
473
 
644
- function renderDoctorResults(checks) {
645
- const passed = checks.filter(c => c.status === 'pass').length;
646
- const warnings = checks.filter(c => c.status === 'warn').length;
647
- const failed = checks.filter(c => c.status === 'fail').length;
648
-
649
- console.log();
650
-
651
- checks.forEach(check => renderDoctorCheck(check));
652
-
653
- console.log();
654
- console.log(` ${ansi.gray}${box.horizontal.repeat(68)}${ansi.reset}`);
655
-
656
- const summary = [];
657
- if (passed > 0) summary.push(`${ansi.green}${passed} passed${ansi.reset}`);
658
- if (warnings > 0) summary.push(`${ansi.yellow}${warnings} warnings${ansi.reset}`);
659
- if (failed > 0) summary.push(`${ansi.red}${failed} failed${ansi.reset}`);
474
+ class Spinner {
475
+ constructor(text = "") {
476
+ this.text = text;
477
+ this.timer = null;
478
+ this.frame = 0;
479
+ }
660
480
 
661
- console.log(` Summary: ${summary.join(', ')}`);
481
+ start(text) {
482
+ if (text) this.text = text;
483
+ if (!process.stdout.isTTY) {
484
+ console.log(` ${ansi.cyan}${sym.gear}${ansi.reset} ${this.text}...`);
485
+ return this;
486
+ }
487
+
488
+ process.stdout.write("\x1b[?25l"); // Hide cursor
489
+ this.timer = setInterval(() => {
490
+ const f = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
491
+ process.stdout.write(`\r ${ansi.cyan}${f}${ansi.reset} ${this.text}`);
492
+ this.frame++;
493
+ }, 80);
494
+ return this;
495
+ }
662
496
 
663
- if (failed > 0) {
664
- console.log(` ${ansi.red}${sym.cross}${ansi.reset} Issues found - run ${ansi.cyan}vibecheck doctor --fix${ansi.reset} to repair`);
665
- } else if (warnings > 0) {
666
- console.log(` ${ansi.yellow}${sym.warning}${ansi.reset} Warnings found - review recommended`);
667
- } else {
668
- console.log(` ${ansi.green}${sym.check}${ansi.reset} Environment healthy!`);
497
+ stop(finalIcon, finalColor, finalText) {
498
+ if (this.timer) {
499
+ clearInterval(this.timer);
500
+ this.timer = null;
501
+ }
502
+
503
+ if (process.stdout.isTTY) {
504
+ process.stdout.write("\r\x1b[2K"); // Clear line
505
+ process.stdout.write("\x1b[?25h"); // Show cursor
506
+ }
507
+
508
+ if (finalText !== null) {
509
+ const icon = finalIcon || sym.check;
510
+ const color = finalColor || ansi.green;
511
+ console.log(` ${color}${icon}${ansi.reset} ${finalText || this.text}`);
512
+ }
513
+ return this;
669
514
  }
515
+
516
+ succeed(text) { return this.stop(sym.check, ansi.green, text); }
517
+ fail(text) { return this.stop(sym.cross, ansi.red, text); }
518
+ warn(text) { return this.stop(sym.warning, ansi.yellow, text); }
519
+ info(text) { return this.stop(sym.info, ansi.cyan, text); }
670
520
  }
671
521
 
672
522
  // ═══════════════════════════════════════════════════════════════════════════════
673
- // COMMAND HELP FORMATTING
523
+ // COMMAND TEMPLATES
674
524
  // ═══════════════════════════════════════════════════════════════════════════════
675
525
 
676
- function renderCommandHelp(command, options = {}) {
677
- const { usage, aliases, description, sections = [] } = options;
678
- const lines = [];
679
-
680
- lines.push('');
681
- lines.push(` ${ansi.bold}USAGE${ansi.reset}`);
682
- lines.push(` ${ansi.cyan}${usage}${ansi.reset}`);
526
+ /**
527
+ * Standard command wrapper for consistent output
528
+ * @param {object} opts
529
+ * @param {string} opts.command - Command name
530
+ * @param {string} opts.title - Title for header
531
+ * @param {string} [opts.tier] - Required tier
532
+ * @param {boolean} [opts.quiet] - Suppress non-essential output
533
+ * @param {boolean} [opts.json] - JSON output mode
534
+ * @param {Function} opts.run - The command's main logic
535
+ */
536
+ async function runCommand(opts) {
537
+ const { command, title, tier = "free", quiet = false, json = false, run } = opts;
683
538
 
684
- if (aliases) {
685
- lines.push(` ${ansi.dim}Aliases: ${aliases}${ansi.reset}`);
539
+ if (json) {
540
+ // JSON mode - run silently, output only JSON
541
+ return await run();
686
542
  }
687
543
 
688
- if (description) {
689
- lines.push('');
690
- lines.push(` ${description}`);
544
+ if (!quiet) {
545
+ renderMinimalHeader(command, tier);
691
546
  }
692
547
 
693
- sections.forEach(section => {
694
- lines.push('');
695
- lines.push(` ${ansi.bold}${section.title}${ansi.reset}`);
696
- section.items.forEach(item => {
697
- const tierBadge = item.tier && item.tier !== 'free'
698
- ? ` ${ansi.dim}[${TIER_LABELS[item.tier]}]${ansi.reset}`
699
- : '';
700
- lines.push(` ${ansi.cyan}${padRight(item.name, 20)}${ansi.reset}${item.description}${tierBadge}`);
701
- });
702
- });
703
-
704
- lines.push('');
548
+ const result = await run();
705
549
 
706
- return lines.join('\n');
550
+ return result;
707
551
  }
708
552
 
709
553
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -711,67 +555,50 @@ function renderCommandHelp(command, options = {}) {
711
555
  // ═══════════════════════════════════════════════════════════════════════════════
712
556
 
713
557
  module.exports = {
714
- // Config
558
+ // Constants
715
559
  WIDTH,
716
- SUPPORTS_COLOR,
717
- SUPPORTS_UNICODE,
718
-
719
- // ANSI
720
560
  ansi,
721
-
722
- // Symbols & Icons
723
561
  sym,
724
- box,
725
-
726
- // Tier
727
562
  TIER,
728
- TIER_COLORS,
729
- TIER_LABELS,
730
- getTierBadge,
731
- getTierColor,
732
563
 
733
- // Text utilities
734
- visibleLength,
564
+ // Utilities
565
+ stripAnsi,
566
+ padCenter,
735
567
  padRight,
736
- padLeft,
737
- center,
738
568
  truncate,
739
569
  formatDuration,
740
- formatBytes,
741
-
742
- // Components
743
- Spinner,
744
- progressBar,
745
- renderTable,
570
+ getTierBadge,
571
+ getTierFromKey,
746
572
 
747
- // Headers & Sections
573
+ // Headers
574
+ renderHeader,
748
575
  renderMinimalHeader,
749
- renderSectionHeader,
576
+
577
+ // Dividers
750
578
  renderDivider,
579
+ renderSectionHeader,
751
580
 
752
- // Status messages
581
+ // Status
753
582
  renderSuccess,
754
583
  renderError,
755
584
  renderWarning,
756
585
  renderInfo,
757
586
  renderBullet,
587
+ renderStep,
758
588
 
759
- // Key-Value
760
- renderKeyValue,
761
-
762
- // Verdict
589
+ // Results
763
590
  renderVerdict,
764
-
765
- // Findings
766
- renderFindings,
591
+ renderFindingsTable,
592
+ renderKeyValue,
593
+ renderProgressBar,
594
+ renderScore,
767
595
 
768
596
  // Footer
769
597
  renderFooter,
770
598
 
771
- // Doctor
772
- renderDoctorCheck,
773
- renderDoctorResults,
599
+ // Components
600
+ Spinner,
774
601
 
775
- // Help
776
- renderCommandHelp,
602
+ // Templates
603
+ runCommand,
777
604
  };