@vibecheckai/cli 3.5.5 → 3.6.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.
@@ -1,12 +1,10 @@
1
1
  /**
2
- * Ship Output - Premium Ship Command Display
3
- *
4
- * Handles all ship-specific output formatting:
5
- * - Verdict card (the hero moment)
6
- * - Proof graph visualization
7
- * - Findings breakdown by category
8
- * - Fix mode display
9
- * - Badge generation
2
+ * Enterprise Ship Output - V5 "Mission Control" Format
3
+ * Features:
4
+ * - Dynamic "SHIP" vs "NO SHIP" ASCII Art
5
+ * - Deployment Readiness Gauges
6
+ * - AI Hallucination Risk Assessment
7
+ * - Pixel-perfect 80-char alignment
10
8
  */
11
9
 
12
10
  const path = require('path');
@@ -20,6 +18,119 @@ const {
20
18
  truncate,
21
19
  } = require('./terminal-ui');
22
20
 
21
+ // ANSI Color Helpers (direct for compatibility)
22
+ const ESC = '\x1b';
23
+ const chalk = {
24
+ reset: `${ESC}[0m`,
25
+ bold: `${ESC}[1m`,
26
+ dim: `${ESC}[2m`,
27
+ red: `${ESC}[31m`,
28
+ green: `${ESC}[32m`,
29
+ yellow: `${ESC}[33m`,
30
+ cyan: `${ESC}[36m`,
31
+ magenta: `${ESC}[35m`,
32
+ white: `${ESC}[37m`,
33
+ gray: `${ESC}[90m`,
34
+ bgRed: `${ESC}[41m`,
35
+ bgGreen: `${ESC}[42m`,
36
+ bgYellow: `${ESC}[43m`,
37
+ };
38
+
39
+ // ═══════════════════════════════════════════════════════════════════════════════
40
+ // CONFIGURATION
41
+ // ═══════════════════════════════════════════════════════════════════════════════
42
+
43
+ const WIDTH = 80;
44
+
45
+ const BOX_SHIP = {
46
+ topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝',
47
+ horizontal: '═', vertical: '║',
48
+ teeRight: '╠', teeLeft: '╣', teeTop: '╤', teeBottom: '╧',
49
+ cross: '╪',
50
+ lightH: '─', lightV: '│',
51
+ lightTeeLeft: '├', lightTeeRight: '┤', lightCross: '┼'
52
+ };
53
+
54
+ // EXTERNAL HEADER
55
+ const LOGO_VIBECHECK = `
56
+ ██╗ ██╗██╗██████╗ ███████╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗
57
+ ██║ ██║██║██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝
58
+ ██║ ██║██║██████╔╝█████╗ ██║ ███████║█████╗ ██║ █████╔╝
59
+ ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗
60
+ ╚████╔╝ ██║██████╔╝███████╗╚██████╗██║ ██║███████╗╚██████╗██║ ██╗
61
+ ╚═══╝ ╚═╝╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝
62
+ `;
63
+
64
+ // HERO: SHIP (Green)
65
+ const LOGO_SHIP = `
66
+ ███████╗██╗ ██╗██╗██████╗
67
+ ██╔════╝██║ ██║██║██╔══██╗
68
+ ███████╗███████║██║██████╔╝
69
+ ╚════██║██╔══██║██║██╔═══╝
70
+ ███████║██║ ██║██║██║
71
+ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝
72
+ `;
73
+
74
+ // HERO: NO SHIP (Red)
75
+ const LOGO_NOSHIP = `
76
+ ███╗ ██╗ ██████╗ ███████╗██╗ ██╗██╗██████╗
77
+ ████╗ ██║██╔═══██╗ ██╔════╝██║ ██║██║██╔══██╗
78
+ ██╔██╗ ██║██║ ██║ ███████╗███████║██║██████╔╝
79
+ ██║╚██╗██║██║ ██║ ╚════██║██╔══██║██║██╔═══╝
80
+ ██║ ╚████║╚██████╔╝ ███████║██║ ██║██║██║
81
+ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝
82
+ `;
83
+
84
+ // ═══════════════════════════════════════════════════════════════════════════════
85
+ // UTILITIES
86
+ // ═══════════════════════════════════════════════════════════════════════════════
87
+
88
+ function stripAnsi(str) {
89
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
90
+ }
91
+
92
+ function padCenter(str, width) {
93
+ const visibleLen = stripAnsi(str).length;
94
+ const padding = Math.max(0, width - visibleLen);
95
+ const left = Math.floor(padding / 2);
96
+ const right = padding - left;
97
+ return ' '.repeat(left) + str + ' '.repeat(right);
98
+ }
99
+
100
+ function padRight(str, len) {
101
+ const visibleLen = stripAnsi(str).length;
102
+ const truncated = visibleLen > len ? str.substring(0, len - 3) + '...' : str;
103
+ return truncated + ' '.repeat(Math.max(0, len - visibleLen));
104
+ }
105
+
106
+ function renderHealthBar(percentage, width = 20, colorOverride = null) {
107
+ const fillChar = '█';
108
+ const fadeChars = ['▓', '▒', '░'];
109
+ const emptyChar = '░';
110
+
111
+ const fillAmount = Math.floor((percentage / 100) * width);
112
+
113
+ let barColor = colorOverride;
114
+ if (!barColor) {
115
+ barColor = percentage > 80 ? chalk.green : percentage > 50 ? chalk.yellow : chalk.red;
116
+ }
117
+
118
+ let bar = barColor + fillChar.repeat(fillAmount);
119
+
120
+ const remainder = ((percentage / 100) * width) - fillAmount;
121
+ if (fillAmount < width) {
122
+ if (remainder > 0.66) bar += fadeChars[0];
123
+ else if (remainder > 0.33) bar += fadeChars[1];
124
+ else bar += chalk.gray + emptyChar;
125
+ }
126
+
127
+ const currentVisible = fillAmount + (fillAmount < width ? 1 : 0);
128
+ const emptySpace = Math.max(0, width - currentVisible);
129
+ bar += chalk.gray + emptyChar.repeat(emptySpace) + chalk.reset;
130
+
131
+ return bar;
132
+ }
133
+
23
134
  // ═══════════════════════════════════════════════════════════════════════════════
24
135
  // SHIP-SPECIFIC ICONS
25
136
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -765,97 +876,188 @@ function renderUpgradePrompts(findings, verdict) {
765
876
  // ═══════════════════════════════════════════════════════════════════════════════
766
877
 
767
878
  function formatShipOutput(result, options = {}) {
768
- const {
769
- verbose = false,
770
- showFix = false,
771
- showBadge = false,
772
- outputDir = '.vibecheck',
773
- projectPath = '.',
774
- tier = 'free', // User's current tier
775
- isVerified = false, // Whether reality testing was done (for verified badge)
776
- } = options;
777
-
879
+ // 1. Data Prep - Extract from result structure
778
880
  const {
779
881
  verdict,
780
- score,
882
+ score = 0,
781
883
  findings = [],
782
884
  blockers = [],
783
885
  warnings = [],
784
- truthpack,
785
- proofGraph,
786
- fixResults,
787
886
  duration = 0,
788
- cached = false,
789
887
  } = result;
790
888
 
791
- const isPro = tier === 'pro' || tier === 'compliance' || tier === 'starter'; // starter=legacy compat
792
- const isProTier = isPro; // 2-tier model: isPro covers all paid tiers
889
+ // Calculate blockers and warnings if not provided
890
+ const actualBlockers = blockers.length > 0
891
+ ? blockers
892
+ : findings.filter(f => f.severity === 'BLOCK' || f.severity === 'critical');
893
+ const actualWarnings = warnings.length > 0
894
+ ? warnings
895
+ : findings.filter(f => f.severity === 'WARN' || f.severity === 'warning');
896
+
793
897
  const canShip = verdict === 'SHIP';
898
+ const logo = canShip ? LOGO_SHIP : LOGO_NOSHIP;
899
+ const themeColor = canShip ? chalk.green : chalk.red;
900
+ const statusText = canShip ? 'CLEAR FOR LAUNCH' : 'DEPLOYMENT ABORTED';
901
+
902
+ // Calculate AI Hallucination Score
903
+ const aiHallucinationFindings = findings.filter(f =>
904
+ f.category === 'MissingRoute' ||
905
+ f.category === 'GhostAuth' ||
906
+ f.category === 'FakeSuccess' ||
907
+ f.type === 'MissingRoute' ||
908
+ f.type === 'GhostAuth'
909
+ );
910
+ const aiScore = Math.max(0, 100 - (aiHallucinationFindings.length * 15));
911
+
912
+ // Generate launch ID
913
+ const launchId = Math.floor(Math.random() * 9999);
914
+ const projectName = path.basename(process.cwd());
794
915
 
795
916
  const lines = [];
917
+
918
+ // 2. Render External Header
919
+ lines.push(chalk.cyan + LOGO_VIBECHECK.trim() + chalk.reset);
920
+ lines.push('');
921
+
922
+ // 3. Render Box Top
923
+ lines.push(`${chalk.gray}${BOX_SHIP.topLeft}${BOX_SHIP.horizontal.repeat(WIDTH - 2)}${BOX_SHIP.topRight}${chalk.reset}`);
924
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${' '.repeat(WIDTH - 2)}${BOX_SHIP.vertical}${chalk.reset}`);
925
+
926
+ // 4. Render Hero Logo (SHIP vs NO SHIP)
927
+ logo.trim().split('\n').filter(l => l.trim().length > 0).forEach(l => {
928
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}${themeColor}${padCenter(l.trim(), WIDTH - 2)}${chalk.reset}${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
929
+ });
930
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${' '.repeat(WIDTH - 2)}${BOX_SHIP.vertical}${chalk.reset}`);
931
+
932
+ // 5. Info Row
933
+ const version = options.version || 'v3.5.5';
934
+ const integrity = canShip ? `${chalk.green}OPTIMAL${chalk.reset}` : `${chalk.red}COMPROMISED${chalk.reset}`;
935
+ const infoText = `${chalk.bold}${version} SHIP${chalk.reset} :: SYSTEM INTEGRITY: ${integrity}`;
936
+ const metaText = `Launch ID: #${launchId} | T-${duration}ms`;
937
+ const infoLine = ` ${infoText}${' '.repeat(Math.max(1, WIDTH - 6 - stripAnsi(infoText).length - stripAnsi(metaText).length))}${chalk.dim}${metaText}${chalk.reset}`;
938
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}${infoLine} ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
939
+
940
+ const targetLine = ` Target: ${padRight(projectName, 50)}`;
941
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}${targetLine}${' '.repeat(WIDTH - 2 - stripAnsi(targetLine).length)}${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
942
+
943
+ // 6. Split Pane
944
+ lines.push(`${chalk.gray}${BOX_SHIP.teeRight}${BOX_SHIP.horizontal.repeat(44)}${BOX_SHIP.teeTop}${BOX_SHIP.horizontal.repeat(WIDTH - 47)}${BOX_SHIP.teeLeft}${chalk.reset}`);
796
945
 
797
- // Verdict card (hero moment)
798
- lines.push(renderVerdictCard({
799
- verdict,
800
- score,
801
- blockers: blockers.length || findings.filter(f => f.severity === 'BLOCK' || f.severity === 'critical').length,
802
- warnings: warnings.length || findings.filter(f => f.severity === 'WARN' || f.severity === 'warning').length,
803
- duration,
804
- cached,
805
- }));
806
-
807
- // AI Hallucination Score (always show - this is a key selling point)
808
- lines.push(renderAIHallucinationScore(findings));
809
-
810
- // Findings breakdown
811
- lines.push(renderFindingsBreakdown(findings));
812
-
813
- // Blocker details (with AI attribution, tier-gated fix hints)
814
- lines.push(renderBlockerDetails(findings, 8, { tier }));
815
-
816
- // Upgrade prompts for FREE tier users (if there are fixable issues)
817
- if (findings.length > 0 && !isProTier) {
818
- lines.push(renderUpgradePrompts(findings, verdict));
946
+ function printSplitRow(left, right) {
947
+ const leftContent = padRight(left || '', 42);
948
+ const rightContent = padRight(right || '', WIDTH - 48);
949
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} ${leftContent} ${chalk.gray}${BOX_SHIP.lightV}${chalk.reset} ${rightContent} ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
819
950
  }
951
+
952
+ // Row 1: Headers
953
+ printSplitRow(`${chalk.bold}DEPLOYMENT VITALS${chalk.reset}`, `${chalk.bold}BLOCKER MANIFEST${chalk.reset}`);
820
954
 
821
- // Verbose: Route truth map
822
- if (verbose && truthpack) {
823
- lines.push('');
824
- lines.push(renderRouteTruthMap(truthpack));
825
- }
955
+ // Row 2: Divider
956
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} ${BOX_SHIP.lightH.repeat(42)} ${chalk.gray}${BOX_SHIP.lightCross}${chalk.reset} ${BOX_SHIP.lightH.repeat(WIDTH - 48)} ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
957
+
958
+ // Prepare Left Column
959
+ const confidence = canShip ? 99 : Math.max(10, score - 10);
960
+ const leftCol = [
961
+ '',
962
+ ` LAUNCH STATUS [${themeColor}${statusText}${chalk.reset}]`,
963
+ ` [${renderHealthBar(score, 25, themeColor)}] ${score}%`,
964
+ '',
965
+ ` AI REALITY CHECK [${aiScore > 80 ? 'PASSED' : 'RISK'}]`,
966
+ ` [${renderHealthBar(aiScore, 25)}] ${aiScore}%`,
967
+ '',
968
+ ` CONFIDENCE [${confidence > 90 ? 'HIGH' : 'LOW'}]`,
969
+ ` [${renderHealthBar(confidence, 25)}] ${confidence}%`,
970
+ '',
971
+ `${chalk.gray}${BOX_SHIP.lightH.repeat(42)}${chalk.reset}`,
972
+ `${chalk.bold} CERTIFICATION${chalk.reset}`,
973
+ '',
974
+ ];
975
+
976
+ // Certification checks
977
+ const hasRouteIssues = findings.some(f => f.category === 'MissingRoute' || f.type === 'MissingRoute');
978
+ const hasAuthIssues = findings.some(f => f.category === 'GhostAuth' || f.category === 'Auth' || f.type === 'Auth');
979
+ const hasEnvIssues = findings.some(f => f.category === 'EnvContract' || f.category === 'EnvGap');
980
+ const hasDbIssues = findings.some(f => f.category === 'Database' || f.type === 'Database');
981
+
982
+ leftCol.push(` ${!hasRouteIssues ? chalk.green + '✓' : chalk.red + '✖'}${chalk.reset} Routes...............${!hasRouteIssues ? 'READY' : 'FAIL'}`);
983
+ leftCol.push(` ${!hasAuthIssues ? chalk.green + '✓' : chalk.red + '✖'}${chalk.reset} Auth Boundaries......${!hasAuthIssues ? 'READY' : 'FAIL'}`);
984
+ leftCol.push(` ${!hasEnvIssues ? chalk.green + '✓' : chalk.red + '✖'}${chalk.reset} Environment..........${!hasEnvIssues ? 'READY' : 'FAIL'}`);
985
+ leftCol.push(` ${!hasDbIssues ? chalk.green + '✓' : chalk.red + '✖'}${chalk.reset} Database.............${!hasDbIssues ? 'READY' : 'FAIL'}`);
986
+ leftCol.push('');
987
+
988
+ // Prepare Right Column
989
+ const rightCol = [];
990
+ rightCol.push('');
826
991
 
827
- // Verbose: Proof graph
828
- if (verbose && proofGraph) {
829
- lines.push('');
830
- lines.push(renderProofGraph(proofGraph));
992
+ if (canShip) {
993
+ rightCol.push(`${chalk.green} [✓] ALL SYSTEMS GO${chalk.reset}`);
994
+ rightCol.push(` No blockers detected.`);
995
+ rightCol.push('');
996
+ rightCol.push(` Ready for production.`);
997
+ rightCol.push(` 0 Blockers`);
998
+ rightCol.push(` ${actualWarnings.length} Warnings (Non-critical)`);
999
+ rightCol.push('');
1000
+ } else {
1001
+ // Show top 3 Blockers or Warnings
1002
+ const topItems = actualBlockers.length > 0 ? actualBlockers.slice(0, 3) : actualWarnings.slice(0, 3);
1003
+
1004
+ if (topItems.length === 0) {
1005
+ rightCol.push(`${chalk.yellow} [!] REVIEW REQUIRED${chalk.reset}`);
1006
+ rightCol.push(` Check findings.`);
1007
+ rightCol.push('');
1008
+ } else {
1009
+ topItems.forEach((f, i) => {
1010
+ const isBlock = f.severity === 'BLOCK' || f.severity === 'critical' || actualBlockers.includes(f);
1011
+ const sevColor = isBlock ? chalk.red : chalk.yellow;
1012
+ const icon = isBlock ? '🛑' : '⚠️';
1013
+
1014
+ const category = f.category || f.type || 'Issue';
1015
+ rightCol.push(`${sevColor}${icon} ${category}${chalk.reset}`);
1016
+ rightCol.push(`${chalk.gray}${BOX_SHIP.lightH.repeat(28)}${chalk.reset}`);
1017
+
1018
+ let file = f.file ? path.basename(f.file) : 'Project';
1019
+ rightCol.push(` ${i+1}. ${file}${f.line ? ':' + f.line : ''}`);
1020
+
1021
+ let msg = f.message || f.title || f.description || 'Unknown issue';
1022
+ if (msg.length > 25) msg = msg.substring(0, 25) + '...';
1023
+ rightCol.push(` > ${chalk.dim}${msg}${chalk.reset}`);
1024
+ rightCol.push('');
1025
+ });
1026
+
1027
+ const remaining = (actualBlockers.length + actualWarnings.length) - 3;
1028
+ if (remaining > 0) {
1029
+ rightCol.push(` ${chalk.dim}... and ${remaining} more items${chalk.reset}`);
1030
+ }
1031
+ }
831
1032
  }
832
-
833
- // Fix results
834
- if (showFix && fixResults) {
835
- lines.push(renderFixResults(fixResults));
1033
+
1034
+ // Merge Columns
1035
+ const maxRows = Math.max(leftCol.length, rightCol.length);
1036
+ for (let i = 0; i < maxRows; i++) {
1037
+ printSplitRow(leftCol[i] || '', rightCol[i] || '');
836
1038
  }
837
-
838
- // Badge (if requested and tier allows)
839
- if (showBadge && isProTier) {
840
- const badgeResult = renderBadgeOutput(projectPath, verdict, score, { tier, isVerified });
841
- lines.push(badgeResult.output);
842
- } else if (showBadge && !isProTier) {
843
- // Show badge upsell for FREE users
844
- lines.push('');
845
- lines.push(renderSection('BADGE', '📛'));
846
- lines.push('');
847
- lines.push(` ${ansi.dim}🔒 Ship badges require PRO ($69/mo)${ansi.reset}`);
848
- lines.push(` ${colors.accent}vibecheckai.dev/pricing${ansi.reset} ${ansi.dim}to upgrade${ansi.reset}`);
849
- lines.push('');
1039
+
1040
+ // 7. Action Footer
1041
+ lines.push(`${chalk.gray}${BOX_SHIP.teeRight}${BOX_SHIP.horizontal.repeat(WIDTH - 2)}${BOX_SHIP.teeLeft}${chalk.reset}`);
1042
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} ${chalk.bold}MISSION CONTROL PROTOCOLS${chalk.reset}${' '.repeat(WIDTH - 28)}${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
1043
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} ${BOX_SHIP.lightH.repeat(WIDTH - 4)} ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
1044
+
1045
+ if (!canShip) {
1046
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} ${chalk.red}[!] LAUNCH SCRUBBED. ${actualBlockers.length} BLOCKERS FOUND.${chalk.reset}${' '.repeat(WIDTH - 42)}${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
1047
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${' '.repeat(WIDTH - 2)}${BOX_SHIP.vertical}${chalk.reset}`);
1048
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} > ${chalk.cyan}vibecheck ship --fix${chalk.reset} [AUTO-PATCH] Resolve blockers automatically ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
1049
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} > ${chalk.cyan}vibecheck report${chalk.reset} [DEBRIEF] View full HTML mission report ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
1050
+ } else {
1051
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} ${chalk.green}[✓] ORBITAL INSERTION CALCULATED.${chalk.reset}${' '.repeat(WIDTH - 36)}${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
1052
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${' '.repeat(WIDTH - 2)}${BOX_SHIP.vertical}${chalk.reset}`);
1053
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} > ${chalk.cyan}git push origin main${chalk.reset} [ENGAGE] Proceed with deployment ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
1054
+ lines.push(`${chalk.gray}${BOX_SHIP.vertical}${chalk.reset} > ${chalk.cyan}vibecheck prove${chalk.reset} [RECORD] Generate verification proof ${chalk.gray}${BOX_SHIP.vertical}${chalk.reset}`);
850
1055
  }
851
-
852
- // Report links
853
- lines.push(renderReportLinks(outputDir, showFix));
854
-
855
- // Next steps (shows PRO upsell for verified badge if SHIP passed)
856
- lines.push(renderNextSteps(canShip, showFix, { tier, showBadge }));
857
-
858
- return lines.filter(Boolean).join('\n');
1056
+
1057
+ // 8. Box Bottom
1058
+ lines.push(`${chalk.gray}${BOX_SHIP.bottomLeft}${BOX_SHIP.horizontal.repeat(WIDTH - 2)}${BOX_SHIP.bottomRight}${chalk.reset}`);
1059
+
1060
+ return lines.join('\n');
859
1061
  }
860
1062
 
861
1063
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -868,18 +1070,6 @@ function renderProgressBar(percent, width, color) {
868
1070
  return `${color}${'█'.repeat(filled)}${ansi.dim}${'░'.repeat(empty)}${ansi.reset}`;
869
1071
  }
870
1072
 
871
- function padCenter(str, width) {
872
- const visibleLen = stripAnsi(str).length;
873
- const padding = Math.max(0, width - visibleLen);
874
- const left = Math.floor(padding / 2);
875
- const right = padding - left;
876
- return ' '.repeat(left) + str + ' '.repeat(right);
877
- }
878
-
879
- function stripAnsi(str) {
880
- return str.replace(/\x1b\[[0-9;]*m/g, '');
881
- }
882
-
883
1073
  // ═══════════════════════════════════════════════════════════════════════════════
884
1074
  // EXIT CODES
885
1075
  // ═══════════════════════════════════════════════════════════════════════════════