@maverick006/vibeguard 1.0.4 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/formatter.js CHANGED
@@ -10,6 +10,31 @@ const chalk_1 = __importDefault(require("chalk"));
10
10
  const child_process_1 = require("child_process");
11
11
  const types_1 = require("@maverick006/types");
12
12
  const path_1 = __importDefault(require("path"));
13
+ function stripAnsi(str) {
14
+ return str.replace(/\u001b\[[0-9;]*m/g, '');
15
+ }
16
+ function visibleWidth(str) {
17
+ const plain = stripAnsi(str);
18
+ let width = 0;
19
+ for (const char of plain) {
20
+ const code = char.codePointAt(0) || 0;
21
+ if (code === 0xFE0F || code === 0xFE0E)
22
+ continue; // ignore variation selectors
23
+ // Emojis and certain symbols take 2 terminal columns
24
+ if (code > 0x1F000 || (code >= 0x2600 && code <= 0x27BF)) {
25
+ width += 2;
26
+ }
27
+ else {
28
+ width += 1;
29
+ }
30
+ }
31
+ return width;
32
+ }
33
+ function padVisible(str, targetWidth) {
34
+ const current = visibleWidth(str);
35
+ const diff = Math.max(0, targetWidth - current);
36
+ return str + ' '.repeat(diff);
37
+ }
13
38
  function getGitInfo(cwd = process.cwd()) {
14
39
  let name = path_1.default.basename(cwd);
15
40
  let branch = 'main';
@@ -95,39 +120,44 @@ function renderDashboard(options) {
95
120
  ' \\ \\ / / ',
96
121
  ' `-------\' '
97
122
  ];
98
- // Title ASCII Art
123
+ // Title ASCII Art (VIBEGUARD)
99
124
  const title = [
100
- ' _ _ _____ ____ _____ ____ _ _ _ ____ ____ ',
101
- '\\ \\ / /_ _| __ )| ____/ ___| | | | / \\ | _ \\| _ \\ ',
102
- ' \\ V / | || _ \\| _|| | _| | | |/ _ \\| |_) | | | |',
103
- ' | | | || |_) | |___| |_| |_| / ___ \\ _ <| |_| |',
104
- ' |_| |___|____/|_____|\\____|\\___/_/ \\_\\_| \\_\\____/'
125
+ ' __ __ ___ ____ _____ ____ _ _ _ ____ ____ ',
126
+ '\\ \\ / /|_ _| __ )| ____/ ___| | | | / \\ | _ \\| _ \\ ',
127
+ ' \\ \\ / / | || _ \\| _|| | _| | | |/ _ \\| |_) | | | |',
128
+ ' \\ V / | || |_) | |___| |_| |_| / ___ \\ _ <| |_| |',
129
+ ' \\_/ |___|____/|_____|\\____|\\___/_/ \\_\\_| \\_\\____/'
105
130
  ];
106
131
  const scoreColor = stats.score >= 85 ? green : stats.score >= 65 ? yellow : red;
107
132
  const riskColor = stats.riskLevel === 'LOW RISK' ? green : stats.riskLevel === 'MEDIUM RISK' ? yellow : red;
108
133
  console.log('\n');
109
134
  // Print Top Row: Clock aligned right
110
135
  console.log(' '.repeat(65) + cyan.bold(timeStr));
111
- // Header Row with Logo and Overall Risk Score Box
112
- const headerLines = [
113
- `${cyan(shield[0])} ${cyan.bold(title[0])} ${darkBorder('┌───────────────────────────────────────┐')}`,
114
- `${cyan(shield[1])} ${cyan.bold(title[1])} ${darkBorder('│')} ${cyan('OVERALL RISK SCORE')} ${darkBorder('│')}`,
115
- `${cyan(shield[2])} ${cyan.bold(title[2])} ${darkBorder('│')} ${red('CRITICAL')} ${String(stats.critical).padStart(2, ' ')} ${darkBorder('│')}`,
116
- `${cyan(shield[3])} ${cyan.bold(title[3])} ${darkBorder('│')} ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${orange('HIGH')} ${String(stats.high).padStart(2, ' ')} ${darkBorder('│')}`,
117
- `${cyan(shield[4])} ${cyan.bold(title[4])} ${darkBorder('│')} ${yellow('MEDIUM')} ${String(stats.medium).padStart(2, ' ')} ${darkBorder('│')}`,
118
- `${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')} ${darkBorder('│')} ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${cyan('LOW')} ${String(stats.low).padStart(2, ' ')} ${darkBorder('│')}`,
119
- ` ${cyan('Scanning. Analyzing. Protecting.')} ${darkBorder('└───────────────────────────────────────┘')}`
120
- ];
121
- for (const line of headerLines) {
122
- console.log(line);
123
- }
136
+ // 1. Header (Shield + VIBEGUARD)
137
+ console.log(`${cyan(shield[0])} ${cyan.bold(title[0])}`);
138
+ console.log(`${cyan(shield[1])} ${cyan.bold(title[1])}`);
139
+ console.log(`${cyan(shield[2])} ${cyan.bold(title[2])}`);
140
+ console.log(`${cyan(shield[3])} ${cyan.bold(title[3])}`);
141
+ console.log(`${cyan(shield[4])} ${cyan.bold(title[4])}`);
142
+ console.log(`${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')}`);
143
+ console.log(` ${cyan('Scanning. Analyzing. Protecting.')}\n`);
144
+ // 2. Risk Score Box (Placed cleanly BELOW VIBEGUARD Header)
145
+ const boxWidth = 74;
146
+ console.log(darkBorder(`┌${'─'.repeat(boxWidth)}┐`));
147
+ console.log(`${darkBorder('│')} ${cyan.bold('OVERALL RISK SCORE')}${' '.repeat(boxWidth - 20)}${darkBorder('│')}`);
148
+ console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
149
+ const scoreLine1 = ` ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${red('CRITICAL')} ${String(stats.critical).padEnd(2, ' ')} ${orange('HIGH')} ${String(stats.high).padEnd(2, ' ')}`;
150
+ const scoreLine2 = ` ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${yellow('MEDIUM')} ${String(stats.medium).padEnd(2, ' ')} ${cyan('LOW')} ${String(stats.low).padEnd(2, ' ')}`;
151
+ console.log(`${darkBorder('│')}${padVisible(scoreLine1, boxWidth)}${darkBorder('│')}`);
152
+ console.log(`${darkBorder('│')}${padVisible(scoreLine2, boxWidth)}${darkBorder('│')}`);
153
+ console.log(darkBorder(`└${'─'.repeat(boxWidth)}┘`));
124
154
  console.log('');
125
- // 3-Column / Multi-panel Grid
155
+ // 3. Multi-panel Grid (Pipeline & Findings)
126
156
  const pipelineRows = [
127
- `${gray('</>')} SAST (Semgrep) ${green('✓ OK')}`,
157
+ `</> SAST (Semgrep) ${green('✓ OK')}`,
128
158
  `📦 Dependency Check (Trivy) ${green('✓ OK')}`,
129
159
  `🔑 Secrets Scan (Gitleaks) ${green('✓ OK')}`,
130
- `☁️ IaC Scan (Checkov) ${green('✓ OK')}`,
160
+ `🔒 IaC Scan (Checkov) ${green('✓ OK')}`,
131
161
  `🐳 Container Scan (Trivy) ${green('✓ OK')}`,
132
162
  `📑 Code Quality (ESLint) ${green('✓ OK')}`,
133
163
  ``,
@@ -140,7 +170,7 @@ function renderDashboard(options) {
140
170
  ];
141
171
  // Table of findings (top 10)
142
172
  const topFindings = findings.slice(0, 10);
143
- const tableHeader = `${dimGray('ID'.padEnd(12))} ${dimGray('SEVERITY'.padEnd(10))} ${dimGray('TITLE'.padEnd(28))} ${dimGray('FILE:LINE')}`;
173
+ const tableHeader = `${dimGray(padVisible('ID', 13))} ${dimGray(padVisible('SEVERITY', 10))} ${dimGray(padVisible('TITLE', 28))} ${dimGray('FILE:LINE')}`;
144
174
  const findingRows = [tableHeader];
145
175
  if (topFindings.length === 0) {
146
176
  findingRows.push(green(' ✓ No security vulnerabilities detected. Codebase is clean!'));
@@ -156,22 +186,25 @@ function renderDashboard(options) {
156
186
  sevFormatted = orange.bold('HIGH ');
157
187
  else if (sev === 'MEDIUM')
158
188
  sevFormatted = yellow('MEDIUM ');
159
- const title = (f.title || 'Security Finding').length > 26
160
- ? (f.title || '').slice(0, 24) + '..'
161
- : (f.title || '').padEnd(27, ' ');
189
+ const rawTitle = f.title || 'Security Finding';
190
+ const titleTruncated = rawTitle.length > 26 ? rawTitle.slice(0, 24) + '..' : rawTitle;
191
+ const titleFormatted = padVisible(white(titleTruncated), 28);
162
192
  const fileLine = `${f.file || 'unknown'}:${f.line || 1}`;
163
- findingRows.push(`${cyan(id.padEnd(12))} ${sevFormatted} ${white(title)} ${dimGray(fileLine)}`);
193
+ const idFormatted = padVisible(cyan(id), 13);
194
+ findingRows.push(`${idFormatted} ${sevFormatted} ${titleFormatted} ${dimGray(fileLine)}`);
164
195
  }
165
196
  }
166
- // Print Section Headers
167
- console.log(`\n${cyan('▶ SCANNER PIPELINE')} ${cyan('▶ TOP FINDINGS')}`);
197
+ // Print Section Headers with ANSI-safe padding
198
+ const headerCol1 = padVisible(cyan('▶ SCANNER PIPELINE'), 36);
199
+ const headerCol2 = cyan('▶ TOP FINDINGS');
200
+ console.log(`${headerCol1} ${headerCol2}`);
168
201
  const maxRows = Math.max(pipelineRows.length, findingRows.length);
169
202
  for (let i = 0; i < maxRows; i++) {
170
- const left = (pipelineRows[i] || '').padEnd(38, ' ');
203
+ const left = padVisible(pipelineRows[i] || '', 36);
171
204
  const right = findingRows[i] || '';
172
205
  console.log(`${left} ${right}`);
173
206
  }
174
- // AI Remediation Section
207
+ // 4. AI Remediation Section
175
208
  if (remediation) {
176
209
  console.log(`\n${cyan('▶ AI REMEDIATION (VG-AI)')}\n`);
177
210
  console.log(`${cyan('Finding:')} ${red.bold(remediation.findingId)}\n`);
@@ -191,20 +224,18 @@ function renderDashboard(options) {
191
224
  styled = dimGray(d);
192
225
  else
193
226
  styled = white(d);
194
- // Calculate visible length without ANSI codes for proper border padding
195
- const plain = d.replace(/\u001b\[[0-9;]*m/g, '');
196
- const padLen = Math.max(0, 58 - plain.length);
197
- console.log(`${darkBorder('│')} ${styled}${' '.repeat(padLen)} ${darkBorder('│')}`);
227
+ const paddedLine = padVisible(styled, 58);
228
+ console.log(`${darkBorder('│')} ${paddedLine} ${darkBorder('')}`);
198
229
  }
199
230
  console.log(darkBorder('└────────────────────────────────────────────────────────────┘'));
200
231
  console.log(`\n${cyan('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
201
232
  }
202
- // Summary Bar (bottom capsule)
233
+ // 5. Summary Bar (bottom capsule)
203
234
  console.log(`\n${cyan('▶ SUMMARY')}`);
204
235
  const summaryText = `🛡️ Scan completed in ${duration} │ ${stats.total} findings │ 6 passed`;
205
- const barTop = `┌${'─'.repeat(summaryText.length - 2)}┐`;
236
+ const barTop = `┌${'─'.repeat(visibleWidth(summaryText) + 4)}┐`;
206
237
  const barMid = `│ ${summaryText} │`;
207
- const barBot = `└${'─'.repeat(summaryText.length - 2)}┘`;
238
+ const barBot = `└${'─'.repeat(visibleWidth(summaryText) + 4)}┘`;
208
239
  console.log(cyan(barTop));
209
240
  console.log(cyan(barMid));
210
241
  console.log(cyan(barBot));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maverick006/vibeguard",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "VibeGuard CLI",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/formatter.ts CHANGED
@@ -13,6 +13,32 @@ export interface ScanStats {
13
13
  riskLevel: 'LOW RISK' | 'MEDIUM RISK' | 'HIGH RISK' | 'CRITICAL RISK';
14
14
  }
15
15
 
16
+ function stripAnsi(str: string): string {
17
+ return str.replace(/\u001b\[[0-9;]*m/g, '');
18
+ }
19
+
20
+ function visibleWidth(str: string): number {
21
+ const plain = stripAnsi(str);
22
+ let width = 0;
23
+ for (const char of plain) {
24
+ const code = char.codePointAt(0) || 0;
25
+ if (code === 0xFE0F || code === 0xFE0E) continue; // ignore variation selectors
26
+ // Emojis and certain symbols take 2 terminal columns
27
+ if (code > 0x1F000 || (code >= 0x2600 && code <= 0x27BF)) {
28
+ width += 2;
29
+ } else {
30
+ width += 1;
31
+ }
32
+ }
33
+ return width;
34
+ }
35
+
36
+ function padVisible(str: string, targetWidth: number): string {
37
+ const current = visibleWidth(str);
38
+ const diff = Math.max(0, targetWidth - current);
39
+ return str + ' '.repeat(diff);
40
+ }
41
+
16
42
  export function getGitInfo(cwd: string = process.cwd()) {
17
43
  let name = path.basename(cwd);
18
44
  let branch = 'main';
@@ -112,13 +138,13 @@ export function renderDashboard(options: {
112
138
  ' `-------\' '
113
139
  ];
114
140
 
115
- // Title ASCII Art
141
+ // Title ASCII Art (VIBEGUARD)
116
142
  const title = [
117
- ' _ _ _____ ____ _____ ____ _ _ _ ____ ____ ',
118
- '\\ \\ / /_ _| __ )| ____/ ___| | | | / \\ | _ \\| _ \\ ',
119
- ' \\ V / | || _ \\| _|| | _| | | |/ _ \\| |_) | | | |',
120
- ' | | | || |_) | |___| |_| |_| / ___ \\ _ <| |_| |',
121
- ' |_| |___|____/|_____|\\____|\\___/_/ \\_\\_| \\_\\____/'
143
+ ' __ __ ___ ____ _____ ____ _ _ _ ____ ____ ',
144
+ '\\ \\ / /|_ _| __ )| ____/ ___| | | | / \\ | _ \\| _ \\ ',
145
+ ' \\ \\ / / | || _ \\| _|| | _| | | |/ _ \\| |_) | | | |',
146
+ ' \\ V / | || |_) | |___| |_| |_| / ___ \\ _ <| |_| |',
147
+ ' \\_/ |___|____/|_____|\\____|\\___/_/ \\_\\_| \\_\\____/'
122
148
  ];
123
149
 
124
150
  const scoreColor = stats.score >= 85 ? green : stats.score >= 65 ? yellow : red;
@@ -129,28 +155,35 @@ export function renderDashboard(options: {
129
155
  // Print Top Row: Clock aligned right
130
156
  console.log(' '.repeat(65) + cyan.bold(timeStr));
131
157
 
132
- // Header Row with Logo and Overall Risk Score Box
133
- const headerLines = [
134
- `${cyan(shield[0])} ${cyan.bold(title[0])} ${darkBorder('┌───────────────────────────────────────┐')}`,
135
- `${cyan(shield[1])} ${cyan.bold(title[1])} ${darkBorder('│')} ${cyan('OVERALL RISK SCORE')} ${darkBorder('│')}`,
136
- `${cyan(shield[2])} ${cyan.bold(title[2])} ${darkBorder('│')} ${red('CRITICAL')} ${String(stats.critical).padStart(2, ' ')} ${darkBorder('│')}`,
137
- `${cyan(shield[3])} ${cyan.bold(title[3])} ${darkBorder('│')} ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${orange('HIGH')} ${String(stats.high).padStart(2, ' ')} ${darkBorder('│')}`,
138
- `${cyan(shield[4])} ${cyan.bold(title[4])} ${darkBorder('│')} ${yellow('MEDIUM')} ${String(stats.medium).padStart(2, ' ')} ${darkBorder('│')}`,
139
- `${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')} ${darkBorder('│')} ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${cyan('LOW')} ${String(stats.low).padStart(2, ' ')} ${darkBorder('│')}`,
140
- ` ${cyan('Scanning. Analyzing. Protecting.')} ${darkBorder('└───────────────────────────────────────┘')}`
141
- ];
158
+ // 1. Header (Shield + VIBEGUARD)
159
+ console.log(`${cyan(shield[0])} ${cyan.bold(title[0])}`);
160
+ console.log(`${cyan(shield[1])} ${cyan.bold(title[1])}`);
161
+ console.log(`${cyan(shield[2])} ${cyan.bold(title[2])}`);
162
+ console.log(`${cyan(shield[3])} ${cyan.bold(title[3])}`);
163
+ console.log(`${cyan(shield[4])} ${cyan.bold(title[4])}`);
164
+ console.log(`${cyan(shield[5])} ${gray('AI-Powered DevSecOps Orchestrator')}`);
165
+ console.log(` ${cyan('Scanning. Analyzing. Protecting.')}\n`);
142
166
 
143
- for (const line of headerLines) {
144
- console.log(line);
145
- }
167
+ // 2. Risk Score Box (Placed cleanly BELOW VIBEGUARD Header)
168
+ const boxWidth = 74;
169
+ console.log(darkBorder(`┌${'─'.repeat(boxWidth)}┐`));
170
+ console.log(`${darkBorder('│')} ${cyan.bold('OVERALL RISK SCORE')}${' '.repeat(boxWidth - 20)}${darkBorder('│')}`);
171
+ console.log(`${darkBorder('│')}${' '.repeat(boxWidth)}${darkBorder('│')}`);
172
+
173
+ const scoreLine1 = ` ${scoreColor.bold(String(stats.score))} ${gray('/100')} ${red('CRITICAL')} ${String(stats.critical).padEnd(2, ' ')} ${orange('HIGH')} ${String(stats.high).padEnd(2, ' ')}`;
174
+ const scoreLine2 = ` ${riskColor.bold(stats.riskLevel.padEnd(13, ' '))} ${yellow('MEDIUM')} ${String(stats.medium).padEnd(2, ' ')} ${cyan('LOW')} ${String(stats.low).padEnd(2, ' ')}`;
175
+
176
+ console.log(`${darkBorder('│')}${padVisible(scoreLine1, boxWidth)}${darkBorder('│')}`);
177
+ console.log(`${darkBorder('│')}${padVisible(scoreLine2, boxWidth)}${darkBorder('│')}`);
178
+ console.log(darkBorder(`└${'─'.repeat(boxWidth)}┘`));
146
179
  console.log('');
147
180
 
148
- // 3-Column / Multi-panel Grid
181
+ // 3. Multi-panel Grid (Pipeline & Findings)
149
182
  const pipelineRows = [
150
- `${gray('</>')} SAST (Semgrep) ${green('✓ OK')}`,
183
+ `</> SAST (Semgrep) ${green('✓ OK')}`,
151
184
  `📦 Dependency Check (Trivy) ${green('✓ OK')}`,
152
185
  `🔑 Secrets Scan (Gitleaks) ${green('✓ OK')}`,
153
- `☁️ IaC Scan (Checkov) ${green('✓ OK')}`,
186
+ `🔒 IaC Scan (Checkov) ${green('✓ OK')}`,
154
187
  `🐳 Container Scan (Trivy) ${green('✓ OK')}`,
155
188
  `📑 Code Quality (ESLint) ${green('✓ OK')}`,
156
189
  ``,
@@ -164,7 +197,7 @@ export function renderDashboard(options: {
164
197
 
165
198
  // Table of findings (top 10)
166
199
  const topFindings = findings.slice(0, 10);
167
- const tableHeader = `${dimGray('ID'.padEnd(12))} ${dimGray('SEVERITY'.padEnd(10))} ${dimGray('TITLE'.padEnd(28))} ${dimGray('FILE:LINE')}`;
200
+ const tableHeader = `${dimGray(padVisible('ID', 13))} ${dimGray(padVisible('SEVERITY', 10))} ${dimGray(padVisible('TITLE', 28))} ${dimGray('FILE:LINE')}`;
168
201
 
169
202
  const findingRows: string[] = [tableHeader];
170
203
 
@@ -179,26 +212,29 @@ export function renderDashboard(options: {
179
212
  else if (sev === 'HIGH') sevFormatted = orange.bold('HIGH ');
180
213
  else if (sev === 'MEDIUM') sevFormatted = yellow('MEDIUM ');
181
214
 
182
- const title = (f.title || 'Security Finding').length > 26
183
- ? (f.title || '').slice(0, 24) + '..'
184
- : (f.title || '').padEnd(27, ' ');
215
+ const rawTitle = f.title || 'Security Finding';
216
+ const titleTruncated = rawTitle.length > 26 ? rawTitle.slice(0, 24) + '..' : rawTitle;
217
+ const titleFormatted = padVisible(white(titleTruncated), 28);
185
218
 
186
219
  const fileLine = `${f.file || 'unknown'}:${f.line || 1}`;
187
- findingRows.push(`${cyan(id.padEnd(12))} ${sevFormatted} ${white(title)} ${dimGray(fileLine)}`);
220
+ const idFormatted = padVisible(cyan(id), 13);
221
+ findingRows.push(`${idFormatted} ${sevFormatted} ${titleFormatted} ${dimGray(fileLine)}`);
188
222
  }
189
223
  }
190
224
 
191
- // Print Section Headers
192
- console.log(`\n${cyan('▶ SCANNER PIPELINE')} ${cyan('▶ TOP FINDINGS')}`);
225
+ // Print Section Headers with ANSI-safe padding
226
+ const headerCol1 = padVisible(cyan('▶ SCANNER PIPELINE'), 36);
227
+ const headerCol2 = cyan('▶ TOP FINDINGS');
228
+ console.log(`${headerCol1} ${headerCol2}`);
193
229
 
194
230
  const maxRows = Math.max(pipelineRows.length, findingRows.length);
195
231
  for (let i = 0; i < maxRows; i++) {
196
- const left = (pipelineRows[i] || '').padEnd(38, ' ');
232
+ const left = padVisible(pipelineRows[i] || '', 36);
197
233
  const right = findingRows[i] || '';
198
234
  console.log(`${left} ${right}`);
199
235
  }
200
236
 
201
- // AI Remediation Section
237
+ // 4. AI Remediation Section
202
238
  if (remediation) {
203
239
  console.log(`\n${cyan('▶ AI REMEDIATION (VG-AI)')}\n`);
204
240
  console.log(`${cyan('Finding:')} ${red.bold(remediation.findingId)}\n`);
@@ -216,22 +252,20 @@ export function renderDashboard(options: {
216
252
  else if (d.trim().startsWith('#')) styled = dimGray(d);
217
253
  else styled = white(d);
218
254
 
219
- // Calculate visible length without ANSI codes for proper border padding
220
- const plain = d.replace(/\u001b\[[0-9;]*m/g, '');
221
- const padLen = Math.max(0, 58 - plain.length);
222
- console.log(`${darkBorder('│')} ${styled}${' '.repeat(padLen)} ${darkBorder('│')}`);
255
+ const paddedLine = padVisible(styled, 58);
256
+ console.log(`${darkBorder('│')} ${paddedLine} ${darkBorder('')}`);
223
257
  }
224
258
  console.log(darkBorder('└────────────────────────────────────────────────────────────┘'));
225
259
 
226
260
  console.log(`\n${cyan('Confidence:')} ${green.bold(remediation.confidence + '%')}`);
227
261
  }
228
262
 
229
- // Summary Bar (bottom capsule)
263
+ // 5. Summary Bar (bottom capsule)
230
264
  console.log(`\n${cyan('▶ SUMMARY')}`);
231
265
  const summaryText = `🛡️ Scan completed in ${duration} │ ${stats.total} findings │ 6 passed`;
232
- const barTop = `┌${'─'.repeat(summaryText.length - 2)}┐`;
266
+ const barTop = `┌${'─'.repeat(visibleWidth(summaryText) + 4)}┐`;
233
267
  const barMid = `│ ${summaryText} │`;
234
- const barBot = `└${'─'.repeat(summaryText.length - 2)}┘`;
268
+ const barBot = `└${'─'.repeat(visibleWidth(summaryText) + 4)}┘`;
235
269
  console.log(cyan(barTop));
236
270
  console.log(cyan(barMid));
237
271
  console.log(cyan(barBot));