@vibecheckai/cli 3.2.1 → 3.2.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.
@@ -1,189 +1,193 @@
1
1
  /**
2
- * Unified Output System
3
- *
4
- * Provides consistent, deterministic output across all vibecheck commands.
5
- * This is what makes vibecheck feel "enterprise-grade".
2
+ * Unified Output System - World Class
3
+ * * The central nervous system for Vibecheck's CLI output.
4
+ * Delivers consistent, high-fidelity visualization and strict exit code compliance.
6
5
  */
7
6
 
8
- // Graceful fallback for missing compiled modules
9
- let EXIT_CODES, formatVerdictOutput;
7
+ "use strict";
10
8
 
11
- // Exit codes per product spec:
12
- // 0 = SHIP / no blockers
13
- // 1 = WARN (allowed if you choose)
14
- // 2 = BLOCK / blockers found
15
- EXIT_CODES = {
9
+ const chalk = require('chalk');
10
+ const os = require('os');
11
+
12
+ // ═══════════════════════════════════════════════════════════════════════════════
13
+ // CONFIGURATION & CONSTANTS
14
+ // ═══════════════════════════════════════════════════════════════════════════════
15
+
16
+ const EXIT_CODES = {
16
17
  SHIP: 0, // Ready to ship, no blockers
17
- PASS: 0, // Alias for SHIP
18
- WARN: 1, // Warnings found (allowed)
19
- BLOCK: 2, // Blockers found, do not ship
20
- FAIL: 2, // Alias for BLOCK
21
- MISCONFIG: 2, // Configuration error = block
22
- INTERNAL: 2, // Internal error = block (safe default)
18
+ WARN: 1, // Warnings found (allowed via flag)
19
+ BLOCK: 2, // Blockers found (hard stop)
20
+ INTERNAL: 2, // Internal error (hard stop)
21
+ MISCONFIG: 2, // Configuration error (hard stop)
23
22
  };
24
23
 
25
- try {
26
- const verdictFormatter = require('../../../dist/lib/cli/verdict-formatter');
27
- formatVerdictOutput = verdictFormatter.formatVerdictOutput;
28
- } catch (err) {
29
- // Fallback when dist not built
30
- formatVerdictOutput = (verdict, options) => {
31
- const c = {
32
- reset: '\x1b[0m',
33
- green: '\x1b[32m',
34
- red: '\x1b[31m',
35
- yellow: '\x1b[33m',
36
- cyan: '\x1b[36m',
37
- dim: '\x1b[2m',
38
- bold: '\x1b[1m',
39
- };
40
-
41
- if (!verdict) return 'No verdict available';
42
-
43
- const icon = verdict.verdict === 'PASS' ? `${c.green}✓${c.reset}` :
44
- verdict.verdict === 'FAIL' ? `${c.red}✗${c.reset}` :
45
- `${c.yellow}⚠${c.reset}`;
46
-
47
- let output = `\n${icon} ${c.bold}${verdict.verdict}${c.reset}\n`;
48
-
49
- if (verdict.summary) {
50
- output += `\n${c.dim}Summary:${c.reset}\n`;
51
- output += ` Blockers: ${verdict.summary.blockers || 0}\n`;
52
- output += ` Warnings: ${verdict.summary.warnings || 0}\n`;
53
- }
54
-
55
- if (verdict.findings && verdict.findings.length > 0) {
56
- output += `\n${c.dim}Findings:${c.reset}\n`;
57
- for (const finding of verdict.findings.slice(0, 10)) {
58
- const sevColor = finding.severity === 'critical' ? c.red :
59
- finding.severity === 'high' ? c.red :
60
- finding.severity === 'medium' ? c.yellow : c.dim;
61
- output += ` ${sevColor}[${finding.severity?.toUpperCase() || 'INFO'}]${c.reset} ${finding.title || finding.message || 'Unknown issue'}\n`;
62
- if (finding.file) {
63
- output += ` ${c.cyan}${finding.file}${finding.line ? `:${finding.line}` : ''}${c.reset}\n`;
64
- }
65
- }
66
- if (verdict.findings.length > 10) {
67
- output += ` ${c.dim}... and ${verdict.findings.length - 10} more${c.reset}\n`;
68
- }
69
- }
70
-
71
- return output;
72
- };
24
+ const WIDTH = 76;
25
+
26
+ const BOX = {
27
+ tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║',
28
+ trT: '╠', tlT: '╣',
29
+ // Light (Inner)
30
+ ltl: '┌', ltr: '┐', lbl: '└', lbr: '┘', lh: '─', lv: '│',
31
+ lt: '', lb: '┴', lx: '┼', ltrT: '├', ltlT: '┤'
32
+ };
33
+
34
+ // ═══════════════════════════════════════════════════════════════════════════════
35
+ // VISUAL RENDERING ENGINE
36
+ // ═══════════════════════════════════════════════════════════════════════════════
37
+
38
+ function padCenter(str, width) {
39
+ const visibleLen = str.replace(/\u001b\[\d+m/g, '').length;
40
+ const padding = Math.max(0, width - visibleLen);
41
+ const left = Math.floor(padding / 2);
42
+ return ' '.repeat(left) + str + ' '.repeat(padding - left);
73
43
  }
74
44
 
75
- /**
76
- * Format scan output with unified contract
77
- */
78
- function formatScanOutput(result, options = {}) {
79
- const { verbose = false, json = false } = options;
80
-
81
- if (json) {
82
- return JSON.stringify(result, null, 2);
83
- }
84
-
85
- return formatVerdictOutput(result.verdict, { verbose, json });
45
+ function padRight(str, len) {
46
+ const visibleLen = str.length;
47
+ const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
48
+ return truncated + ' '.repeat(Math.max(0, len - truncated.length));
86
49
  }
87
50
 
88
- /**
89
- * Get exit code from verdict
90
- * Per spec: 0=SHIP, 1=WARN, 2=BLOCK
91
- */
92
- function getExitCode(verdict) {
93
- if (!verdict) return EXIT_CODES.BLOCK; // Safe default
94
-
95
- switch (verdict.verdict) {
96
- case 'SHIP':
97
- case 'PASS':
98
- return EXIT_CODES.SHIP; // 0
99
- case 'WARN':
100
- return EXIT_CODES.WARN; // 1
101
- case 'BLOCK':
102
- case 'FAIL':
103
- return EXIT_CODES.BLOCK; // 2
104
- case 'ERROR':
105
- case 'MISCONFIG':
106
- return EXIT_CODES.BLOCK; // 2 (safe default)
107
- default:
108
- return EXIT_CODES.BLOCK; // 2 (safe default)
109
- }
51
+ function renderProgressBar(score, width = 20) {
52
+ const filled = Math.round((score / 100) * width);
53
+ return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(width - filled));
110
54
  }
111
55
 
112
- /**
113
- * Handle errors with proper exit codes
114
- */
115
- function handleError(error, context = '') {
116
- const errorType = classifyError(error);
56
+ // --- The Core Formatter ---
57
+
58
+ function renderVerdict(verdictData, options = {}) {
59
+ const {
60
+ score = 0,
61
+ findings = [],
62
+ duration = 0,
63
+ scannedFiles = 0
64
+ } = verdictData;
65
+
66
+ const lines = [];
67
+ const hasIssues = findings.length > 0;
117
68
 
118
- let exitCode = EXIT_CODES.INTERNAL;
119
- let message = error.message || 'Unknown error';
120
- let nextStep = 'Run: vibecheck doctor';
69
+ // 1. FRAME HEADER
70
+ lines.push(chalk.gray(BOX.tl + BOX.h.repeat(WIDTH - 2) + BOX.tr));
71
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
72
+
73
+ // 2. CLEAN HEADER (No ASCII Logo)
74
+ const titleColor = hasIssues ? chalk.red.bold : chalk.cyan.bold;
75
+ const subTitle = hasIssues ? 'INTEGRITY SCAN • ISSUES DETECTED' : 'INTEGRITY SCAN • CLEAN';
121
76
 
122
- switch (errorType) {
123
- case 'MISCONFIG':
124
- exitCode = EXIT_CODES.MISCONFIG;
125
- message = `Configuration error: ${message}`;
126
- nextStep = 'Run: vibecheck doctor --fix';
127
- break;
128
- case 'MISSING_DEPS':
129
- exitCode = EXIT_CODES.MISCONFIG;
130
- message = `Missing dependencies: ${message}`;
131
- nextStep = 'Run: vibecheck doctor';
132
- break;
133
- case 'PERMISSION':
134
- exitCode = EXIT_CODES.MISCONFIG;
135
- message = `Permission error: ${message}`;
136
- nextStep = 'Check file permissions and run: vibecheck doctor';
137
- break;
138
- default:
139
- exitCode = EXIT_CODES.INTERNAL;
140
- message = `Internal error: ${message}`;
141
- nextStep = 'This looks like a bug. Run: vibecheck doctor';
77
+ // Add some vertical padding for elegance
78
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
79
+ lines.push(chalk.gray(BOX.v) + padCenter(titleColor(subTitle), WIDTH - 2) + chalk.gray(BOX.v));
80
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
81
+
82
+ // 3. TELEMETRY
83
+ lines.push(chalk.gray(BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT));
84
+ const heapMB = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
85
+ const stats = `📡 TELEMETRY │ ${duration}ms │ 📂 ${scannedFiles || '?'} Files │ 📦 ${heapMB}MB`;
86
+ lines.push(chalk.gray(BOX.v) + padCenter(stats, WIDTH - 2) + chalk.gray(BOX.v));
87
+ lines.push(chalk.gray(BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT));
88
+
89
+ if (!hasIssues) {
90
+ // CLEAN STATE
91
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
92
+ lines.push(chalk.gray(BOX.v) + padCenter(chalk.green('✅ NO STATIC ISSUES FOUND'), WIDTH - 2) + chalk.gray(BOX.v));
93
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
94
+ } else {
95
+ // ISSUES STATE
96
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
97
+ const scoreBar = renderProgressBar(score, 20);
98
+ lines.push(chalk.gray(BOX.v) + padCenter(`HEALTH SCORE [${scoreBar}] ${score} / 100`, WIDTH + 18) + chalk.gray(BOX.v));
99
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
100
+
101
+ // TABLE HEADER
102
+ lines.push(chalk.gray(BOX.v) + padCenter('DETECTED VULNERABILITIES', WIDTH - 2) + chalk.gray(BOX.v));
103
+
104
+ // Hardcoded column widths for perfect alignment
105
+ const C1=10, C2=13, C3=41;
106
+
107
+ const tTop = ` ${BOX.ltl}${BOX.lh.repeat(C1)}${BOX.lt}${BOX.lh.repeat(C2)}${BOX.lt}${BOX.lh.repeat(C3)}${BOX.ltr} `;
108
+ const tMid = ` ${BOX.ltrT}${BOX.lh.repeat(C1)}${BOX.lx}${BOX.lh.repeat(C2)}${BOX.lx}${BOX.lh.repeat(C3)}${BOX.ltlT} `;
109
+ const tBot = ` ${BOX.lbl}${BOX.lh.repeat(C1)}${BOX.lb}${BOX.lh.repeat(C2)}${BOX.lb}${BOX.lh.repeat(C3)}${BOX.lbr} `;
110
+
111
+ lines.push(chalk.gray(BOX.v) + chalk.gray(tTop) + chalk.gray(BOX.v));
112
+ const header = ` ${BOX.lv}${padRight(' SEVERITY', C1)}${BOX.lv}${padRight(' TYPE', C2)}${BOX.lv}${padRight(' FINDING', C3)}${BOX.lv} `;
113
+ lines.push(chalk.gray(BOX.v) + chalk.bold(header) + chalk.gray(BOX.v));
114
+ lines.push(chalk.gray(BOX.v) + chalk.gray(tMid) + chalk.gray(BOX.v));
115
+
116
+ // ROWS
117
+ findings.slice(0, 5).forEach(f => {
118
+ const sev = (f.severity === 'critical' || f.severity === 'BLOCK') ? chalk.red('🛑 CRIT ') : chalk.yellow('🟡 WARN ');
119
+ const row = ` ${chalk.gray(BOX.lv)}${sev}${chalk.gray(BOX.lv)}${padRight(' '+(f.category||'General'), C2)}${chalk.gray(BOX.lv)}${padRight(' '+(f.message||f.title), C3)}${chalk.gray(BOX.lv)} `;
120
+ lines.push(chalk.gray(BOX.v) + row + chalk.gray(BOX.v));
121
+ });
122
+
123
+ lines.push(chalk.gray(BOX.v) + chalk.gray(tBot) + chalk.gray(BOX.v));
124
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
125
+
126
+ // UPSELL (REDACTED STYLE)
127
+ lines.push(chalk.gray(BOX.trT + BOX.h.repeat(WIDTH - 2) + BOX.tlT));
128
+ const lockTitle = chalk.white.bold('🔒 DEEP SCAN ANALYSIS: ') + chalk.gray('UNAVAILABLE');
129
+ lines.push(chalk.gray(BOX.v) + padCenter(lockTitle, WIDTH + 9) + chalk.gray(BOX.v));
130
+ lines.push(chalk.gray(BOX.v) + padCenter(chalk.gray('─'.repeat(60)), WIDTH + 9) + chalk.gray(BOX.v));
131
+
132
+ // Redacted lines
133
+ ['Runtime API Schema Validation', 'Live Auth Boundary Verification', 'Env Config Resolution'].forEach(txt => {
134
+ const line = chalk.dim(`░░ [REDACTED] 🔒 ${txt}`);
135
+ lines.push(chalk.gray(BOX.v) + padCenter(line, WIDTH + 9) + chalk.gray(BOX.v));
136
+ });
137
+
138
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
139
+ lines.push(chalk.gray(BOX.v) + padCenter('Run ' + chalk.cyan('vibecheck upgrade pro') + ' to unlock.', WIDTH + 9) + chalk.gray(BOX.v));
142
140
  }
143
-
144
- return {
145
- exitCode,
146
- message: `${context ? `[${context}] ` : ''}${message}`,
147
- nextStep,
148
- errorType,
149
- };
141
+
142
+ // BOTTOM FRAME
143
+ lines.push(chalk.gray(BOX.v) + ' '.repeat(WIDTH - 2) + chalk.gray(BOX.v));
144
+ lines.push(chalk.gray(BOX.bl + BOX.h.repeat(WIDTH - 2) + BOX.br));
145
+
146
+ return lines.join('\n');
150
147
  }
151
148
 
152
- function classifyError(error) {
153
- const message = (error.message || '').toLowerCase();
154
- const code = error.code || '';
155
-
156
- if (code === 'ENOENT' || message.includes('not found') || message.includes('missing')) {
157
- return 'MISSING_DEPS';
158
- }
159
-
160
- if (code === 'EACCES' || code === 'EPERM' || message.includes('permission')) {
161
- return 'PERMISSION';
162
- }
163
-
164
- if (message.includes('config') || message.includes('environment') || message.includes('env')) {
165
- return 'MISCONFIG';
166
- }
167
-
168
- return 'INTERNAL';
149
+ // ═══════════════════════════════════════════════════════════════════════════════
150
+ // PUBLIC API
151
+ // ═══════════════════════════════════════════════════════════════════════════════
152
+
153
+ function formatScanOutput(result, options = {}) {
154
+ const { json = false } = options;
155
+ if (json) return JSON.stringify(result, null, 2);
156
+ return renderVerdict(result, options);
157
+ }
158
+
159
+ function getExitCode(verdict) {
160
+ if (!verdict) return EXIT_CODES.BLOCK;
161
+ const v = (typeof verdict === 'string' ? verdict : verdict.verdict).toUpperCase();
162
+ if (v === 'SHIP' || v === 'PASS') return EXIT_CODES.SHIP;
163
+ if (v === 'WARN') return EXIT_CODES.WARN;
164
+ return EXIT_CODES.BLOCK;
169
165
  }
170
166
 
171
- /**
172
- * Print error with proper formatting
173
- */
174
167
  function printError(error, context = '') {
175
- const handled = handleError(error, context);
168
+ const isConfig = error.message.toLowerCase().includes('config') || error.code === 'ENOENT';
169
+ const exitCode = isConfig ? EXIT_CODES.MISCONFIG : EXIT_CODES.INTERNAL;
176
170
 
177
- console.error(`\n${handled.message}`);
178
- console.error(`\n${handled.nextStep}\n`);
171
+ const lines = [];
172
+ lines.push('');
173
+ lines.push(chalk.bgRed.white.bold(` 🛑 SYSTEM ALERT: ${context || 'INTERNAL_ERROR'} `));
174
+ lines.push(chalk.red('─'.repeat(WIDTH)));
175
+ lines.push(`${chalk.bold('Error:')} ${error.message}`);
176
+ if (error.code) lines.push(`${chalk.bold('Code:')} ${error.code}`);
177
+ lines.push('');
179
178
 
180
- return handled.exitCode;
179
+ const fixCmd = isConfig ? 'vibecheck doctor --fix' : 'vibecheck doctor';
180
+ lines.push(chalk.gray('Recovery Step:'));
181
+ lines.push(`$ ${chalk.cyan(fixCmd)}`);
182
+ lines.push('');
183
+
184
+ console.error(lines.join('\n'));
185
+ return exitCode;
181
186
  }
182
187
 
183
188
  module.exports = {
184
189
  formatScanOutput,
185
190
  getExitCode,
186
- handleError,
187
191
  printError,
188
192
  EXIT_CODES,
189
193
  };
@@ -54,7 +54,7 @@ const {
54
54
  } = require("./lib/terminal-ui");
55
55
 
56
56
  const {
57
- formatShipOutput,
57
+ formatShipOutput: formatShipOutputLegacy,
58
58
  renderVerdictCard,
59
59
  renderFixModeHeader,
60
60
  renderFixResults,
@@ -64,6 +64,10 @@ const {
64
64
  shipIcons,
65
65
  } = require("./lib/ship-output");
66
66
 
67
+ const {
68
+ formatShipOutput,
69
+ } = require("./lib/ship-output-enterprise");
70
+
67
71
  // ═══════════════════════════════════════════════════════════════════════════════
68
72
  // PREMIUM BANNER
69
73
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1031,7 +1035,7 @@ async function runShip(args, context = {}) {
1031
1035
  if (spinner) spinner.succeed('Safe fixes applied');
1032
1036
  }
1033
1037
 
1034
- // Human-readable output using ship-output module
1038
+ // Human-readable output using enterprise ship-output module
1035
1039
  const result = {
1036
1040
  verdict,
1037
1041
  score: results.score,
@@ -1047,14 +1051,9 @@ async function runShip(args, context = {}) {
1047
1051
  // Get current tier for output formatting
1048
1052
  const currentTier = context?.authInfo?.access?.tier || getCurrentTier() || "free";
1049
1053
 
1054
+ // Use enterprise format
1050
1055
  console.log(formatShipOutput(result, {
1051
- verbose: opts.verbose,
1052
- showFix: opts.fix,
1053
- showBadge: opts.badge,
1054
- outputDir,
1055
- projectPath,
1056
1056
  tier: currentTier,
1057
- isVerified: opts.withRuntime || false, // Reality testing = verified
1058
1057
  }));
1059
1058
 
1060
1059
  // Badge file generation (STARTER+ only)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecheckai/cli",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
4
4
  "description": "Vibecheck CLI - Ship with confidence. One verdict: SHIP | WARN | BLOCK.",
5
5
  "main": "bin/vibecheck.js",
6
6
  "bin": {