@vibecheckai/cli 3.0.4 → 3.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.
Files changed (108) hide show
  1. package/bin/dev/run-v2-torture.js +30 -0
  2. package/bin/runners/context/index.js +1 -1
  3. package/bin/runners/lib/analyzers.js +38 -0
  4. package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
  5. package/bin/runners/lib/contracts/auth-contract.js +8 -0
  6. package/bin/runners/lib/contracts/env-contract.js +3 -0
  7. package/bin/runners/lib/contracts/external-contract.js +10 -2
  8. package/bin/runners/lib/contracts/route-contract.js +7 -0
  9. package/bin/runners/lib/contracts.js +804 -0
  10. package/bin/runners/lib/detectors-v2.js +703 -0
  11. package/bin/runners/lib/drift.js +425 -0
  12. package/bin/runners/lib/entitlements-v2.js +3 -1
  13. package/bin/runners/lib/entitlements.js +11 -3
  14. package/bin/runners/lib/env-resolver.js +417 -0
  15. package/bin/runners/lib/extractors/client-calls.js +990 -0
  16. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
  17. package/bin/runners/lib/extractors/fastify-routes.js +426 -0
  18. package/bin/runners/lib/extractors/index.js +363 -0
  19. package/bin/runners/lib/extractors/next-routes.js +524 -0
  20. package/bin/runners/lib/extractors/proof-graph.js +431 -0
  21. package/bin/runners/lib/extractors/route-matcher.js +451 -0
  22. package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
  23. package/bin/runners/lib/extractors/ui-bindings.js +547 -0
  24. package/bin/runners/lib/findings-schema.js +281 -0
  25. package/bin/runners/lib/html-report.js +650 -0
  26. package/bin/runners/lib/missions/templates.js +45 -0
  27. package/bin/runners/lib/policy.js +295 -0
  28. package/bin/runners/lib/reality/correlation-detectors.js +359 -0
  29. package/bin/runners/lib/reality/index.js +318 -0
  30. package/bin/runners/lib/reality/request-hashing.js +416 -0
  31. package/bin/runners/lib/reality/request-mapper.js +453 -0
  32. package/bin/runners/lib/reality/safety-rails.js +463 -0
  33. package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
  34. package/bin/runners/lib/reality/toast-detector.js +393 -0
  35. package/bin/runners/lib/report-html.js +5 -0
  36. package/bin/runners/lib/report-templates.js +5 -0
  37. package/bin/runners/lib/report.js +135 -0
  38. package/bin/runners/lib/route-truth.js +10 -10
  39. package/bin/runners/lib/schema-validator.js +350 -0
  40. package/bin/runners/lib/schemas/contracts.schema.json +160 -0
  41. package/bin/runners/lib/schemas/finding.schema.json +100 -0
  42. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
  43. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
  44. package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
  45. package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
  46. package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
  47. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
  48. package/bin/runners/lib/schemas/validator.js +438 -0
  49. package/bin/runners/lib/ui.js +562 -0
  50. package/bin/runners/lib/verdict-engine.js +628 -0
  51. package/bin/runners/runAIAgent.js +228 -1
  52. package/bin/runners/runBadge.js +181 -1
  53. package/bin/runners/runCtx.js +7 -2
  54. package/bin/runners/runCtxDiff.js +301 -0
  55. package/bin/runners/runGuard.js +168 -0
  56. package/bin/runners/runInitGha.js +78 -15
  57. package/bin/runners/runLabs.js +341 -0
  58. package/bin/runners/runLaunch.js +180 -1
  59. package/bin/runners/runMdc.js +203 -1
  60. package/bin/runners/runProof.zip +0 -0
  61. package/bin/runners/runProve.js +23 -0
  62. package/bin/runners/runReplay.js +114 -84
  63. package/bin/runners/runScan.js +111 -32
  64. package/bin/runners/runShip.js +23 -2
  65. package/bin/runners/runTruthpack.js +9 -7
  66. package/bin/runners/runValidate.js +161 -1
  67. package/bin/vibecheck.js +416 -770
  68. package/mcp-server/.guardrail/audit/audit.log.jsonl +2 -0
  69. package/mcp-server/.specs/architecture.mdc +90 -0
  70. package/mcp-server/.specs/security.mdc +30 -0
  71. package/mcp-server/README.md +252 -0
  72. package/mcp-server/agent-checkpoint.js +364 -0
  73. package/mcp-server/architect-tools.js +707 -0
  74. package/mcp-server/audit-mcp.js +206 -0
  75. package/mcp-server/codebase-architect-tools.js +838 -0
  76. package/mcp-server/consolidated-tools.js +804 -0
  77. package/mcp-server/hygiene-tools.js +428 -0
  78. package/mcp-server/index-v1.js +698 -0
  79. package/mcp-server/index.js +2092 -0
  80. package/mcp-server/index.old.js +4137 -0
  81. package/mcp-server/intelligence-tools.js +664 -0
  82. package/mcp-server/intent-drift-tools.js +873 -0
  83. package/mcp-server/mdc-generator.js +298 -0
  84. package/mcp-server/package-lock.json +165 -0
  85. package/mcp-server/package.json +47 -0
  86. package/mcp-server/premium-tools.js +1275 -0
  87. package/mcp-server/test-mcp.js +108 -0
  88. package/mcp-server/test-tools.js +36 -0
  89. package/mcp-server/tier-auth.js +147 -0
  90. package/mcp-server/tools/index.js +72 -0
  91. package/mcp-server/tools-reorganized.ts +244 -0
  92. package/mcp-server/truth-context.js +581 -0
  93. package/mcp-server/truth-firewall-tools.js +1500 -0
  94. package/mcp-server/vibecheck-2.0-tools.js +748 -0
  95. package/mcp-server/vibecheck-tools.js +1075 -0
  96. package/package.json +10 -8
  97. package/bin/guardrail.js +0 -834
  98. package/bin/runners/runAudit.js +0 -2
  99. package/bin/runners/runAutopilot.js +0 -2
  100. package/bin/runners/runCertify.js +0 -2
  101. package/bin/runners/runDashboard.js +0 -10
  102. package/bin/runners/runEnhancedShip.js +0 -2
  103. package/bin/runners/runFixPacks.js +0 -2
  104. package/bin/runners/runNaturalLanguage.js +0 -3
  105. package/bin/runners/runProof.js +0 -2
  106. package/bin/runners/runRealitySniff.js +0 -2
  107. package/bin/runners/runUpgrade.js +0 -2
  108. package/bin/runners/runVerifyAgentOutput.js +0 -2
@@ -0,0 +1,562 @@
1
+ /**
2
+ * lib/ui.js - Shared UI utilities for VibeCheck CLI
3
+ *
4
+ * Premium terminal styling, spinners, progress bars, and formatting.
5
+ * Import this instead of duplicating ANSI codes across runners.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ // ═══════════════════════════════════════════════════════════════════════════════
11
+ // ENVIRONMENT DETECTION
12
+ // ═══════════════════════════════════════════════════════════════════════════════
13
+ const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
14
+ const SUPPORTS_TRUECOLOR = SUPPORTS_COLOR && (
15
+ process.env.COLORTERM === "truecolor" ||
16
+ process.env.TERM_PROGRAM === "iTerm.app" ||
17
+ process.env.TERM_PROGRAM === "Apple_Terminal" ||
18
+ process.env.WT_SESSION
19
+ );
20
+ const SUPPORTS_UNICODE = process.platform !== "win32" ||
21
+ process.env.WT_SESSION ||
22
+ process.env.TERM_PROGRAM;
23
+
24
+ function isCI() {
25
+ return !!(
26
+ process.env.CI ||
27
+ process.env.CONTINUOUS_INTEGRATION ||
28
+ process.env.GITHUB_ACTIONS ||
29
+ process.env.GITLAB_CI ||
30
+ process.env.CIRCLECI ||
31
+ process.env.VERCEL ||
32
+ process.env.NETLIFY ||
33
+ !process.stdin.isTTY
34
+ );
35
+ }
36
+
37
+ // ═══════════════════════════════════════════════════════════════════════════════
38
+ // ANSI COLORS
39
+ // ═══════════════════════════════════════════════════════════════════════════════
40
+ const c = SUPPORTS_COLOR ? {
41
+ reset: "\x1b[0m",
42
+ bold: "\x1b[1m",
43
+ dim: "\x1b[2m",
44
+ italic: "\x1b[3m",
45
+ underline: "\x1b[4m",
46
+ inverse: "\x1b[7m",
47
+
48
+ black: "\x1b[30m",
49
+ red: "\x1b[31m",
50
+ green: "\x1b[32m",
51
+ yellow: "\x1b[33m",
52
+ blue: "\x1b[34m",
53
+ magenta: "\x1b[35m",
54
+ cyan: "\x1b[36m",
55
+ white: "\x1b[37m",
56
+ gray: "\x1b[90m",
57
+
58
+ brightRed: "\x1b[91m",
59
+ brightGreen: "\x1b[92m",
60
+ brightYellow: "\x1b[93m",
61
+ brightBlue: "\x1b[94m",
62
+ brightMagenta: "\x1b[95m",
63
+ brightCyan: "\x1b[96m",
64
+
65
+ bgRed: "\x1b[41m",
66
+ bgGreen: "\x1b[42m",
67
+ bgYellow: "\x1b[43m",
68
+ bgBlue: "\x1b[44m",
69
+ bgMagenta: "\x1b[45m",
70
+ bgCyan: "\x1b[46m",
71
+ bgGray: "\x1b[100m",
72
+
73
+ // Terminal control
74
+ clear: "\x1b[2J\x1b[H",
75
+ clearLine: "\x1b[2K",
76
+ cursorUp: "\x1b[1A",
77
+ cursorDown: "\x1b[1B",
78
+ cursorHide: "\x1b[?25l",
79
+ cursorShow: "\x1b[?25h",
80
+ saveCursor: "\x1b7",
81
+ restoreCursor: "\x1b8",
82
+
83
+ // RGB (truecolor only)
84
+ rgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `\x1b[38;2;${r};${g};${b}m` : "",
85
+ bgRgb: (r, g, b) => SUPPORTS_TRUECOLOR ? `\x1b[48;2;${r};${g};${b}m` : "",
86
+ } : Object.fromEntries([
87
+ "reset", "bold", "dim", "italic", "underline", "inverse",
88
+ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray",
89
+ "brightRed", "brightGreen", "brightYellow", "brightBlue", "brightMagenta", "brightCyan",
90
+ "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", "bgCyan", "bgGray",
91
+ "clear", "clearLine", "cursorUp", "cursorDown", "cursorHide", "cursorShow",
92
+ "saveCursor", "restoreCursor"
93
+ ].map(k => [k, ""]));
94
+
95
+ // ═══════════════════════════════════════════════════════════════════════════════
96
+ // UNICODE SYMBOLS
97
+ // ═══════════════════════════════════════════════════════════════════════════════
98
+ const sym = SUPPORTS_UNICODE ? {
99
+ // Status
100
+ success: "✓",
101
+ error: "✗",
102
+ warning: "⚠",
103
+ info: "ℹ",
104
+ pending: "○",
105
+ active: "●",
106
+ skip: "↷",
107
+
108
+ // Arrows
109
+ arrow: "→",
110
+ arrowRight: "▸",
111
+ arrowDown: "▾",
112
+ arrowUp: "▴",
113
+
114
+ // Misc
115
+ bullet: "•",
116
+ star: "★",
117
+ shield: "🛡️",
118
+ rocket: "🚀",
119
+ fire: "🔥",
120
+ sparkles: "✨",
121
+ lock: "🔒",
122
+ key: "🔑",
123
+ lightning: "⚡",
124
+ clock: "⏱",
125
+ folder: "📁",
126
+ file: "📄",
127
+ gear: "⚙",
128
+ chart: "📊",
129
+ robot: "🤖",
130
+ eye: "👁️",
131
+ check: "☑",
132
+ box: "☐",
133
+
134
+ // Box drawing
135
+ boxTopLeft: "╭",
136
+ boxTopRight: "╮",
137
+ boxBottomLeft: "╰",
138
+ boxBottomRight: "╯",
139
+ boxHorizontal: "─",
140
+ boxVertical: "│",
141
+
142
+ // Double box
143
+ dblTopLeft: "╔",
144
+ dblTopRight: "╗",
145
+ dblBottomLeft: "╚",
146
+ dblBottomRight: "╝",
147
+ dblHorizontal: "═",
148
+ dblVertical: "║",
149
+
150
+ // Progress
151
+ progressFull: "█",
152
+ progressEmpty: "░",
153
+ progressHalf: "▓",
154
+
155
+ // Spinner frames
156
+ spinner: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
157
+ } : {
158
+ success: "√",
159
+ error: "×",
160
+ warning: "!",
161
+ info: "i",
162
+ pending: "o",
163
+ active: "*",
164
+ skip: "~",
165
+ arrow: "->",
166
+ arrowRight: ">",
167
+ arrowDown: "v",
168
+ arrowUp: "^",
169
+ bullet: "*",
170
+ star: "*",
171
+ shield: "[#]",
172
+ rocket: ">>",
173
+ fire: "(!)",
174
+ sparkles: "*",
175
+ lock: "[L]",
176
+ key: "[K]",
177
+ lightning: "!",
178
+ clock: "[T]",
179
+ folder: "[D]",
180
+ file: "[F]",
181
+ gear: "[G]",
182
+ chart: "[C]",
183
+ robot: "[R]",
184
+ eye: "[E]",
185
+ check: "[x]",
186
+ box: "[ ]",
187
+ boxTopLeft: "+",
188
+ boxTopRight: "+",
189
+ boxBottomLeft: "+",
190
+ boxBottomRight: "+",
191
+ boxHorizontal: "-",
192
+ boxVertical: "|",
193
+ dblTopLeft: "+",
194
+ dblTopRight: "+",
195
+ dblBottomLeft: "+",
196
+ dblBottomRight: "+",
197
+ dblHorizontal: "=",
198
+ dblVertical: "||",
199
+ progressFull: "#",
200
+ progressEmpty: ".",
201
+ progressHalf: ":",
202
+ spinner: ["-", "\\", "|", "/"],
203
+ };
204
+
205
+ // ═══════════════════════════════════════════════════════════════════════════════
206
+ // GRADIENT TEXT
207
+ // ═══════════════════════════════════════════════════════════════════════════════
208
+ function gradient(text, colors = [[0, 255, 255], [255, 0, 255]]) {
209
+ if (!SUPPORTS_TRUECOLOR) return `${c.cyan}${text}${c.reset}`;
210
+
211
+ const chars = [...text];
212
+ const len = chars.length;
213
+ if (len === 0) return text;
214
+
215
+ return chars.map((char, i) => {
216
+ const t = i / Math.max(len - 1, 1);
217
+ const segmentLen = colors.length - 1;
218
+ const segment = Math.min(Math.floor(t * segmentLen), segmentLen - 1);
219
+ const localT = (t * segmentLen) - segment;
220
+
221
+ const c1 = colors[segment];
222
+ const c2 = colors[segment + 1] || c1;
223
+
224
+ const r = Math.round(c1[0] + (c2[0] - c1[0]) * localT);
225
+ const g = Math.round(c1[1] + (c2[1] - c1[1]) * localT);
226
+ const b = Math.round(c1[2] + (c2[2] - c1[2]) * localT);
227
+
228
+ return `${c.rgb(r, g, b)}${char}`;
229
+ }).join("") + c.reset;
230
+ }
231
+
232
+ // ═══════════════════════════════════════════════════════════════════════════════
233
+ // SPINNER
234
+ // ═══════════════════════════════════════════════════════════════════════════════
235
+ class Spinner {
236
+ constructor(text = "") {
237
+ this.text = text;
238
+ this.frame = 0;
239
+ this.interval = null;
240
+ this.stream = process.stderr;
241
+ this._isCI = isCI();
242
+ }
243
+
244
+ start(text) {
245
+ if (text) this.text = text;
246
+ if (this._isCI) {
247
+ this.stream.write(`${c.dim}${sym.pending}${c.reset} ${this.text}\n`);
248
+ return this;
249
+ }
250
+
251
+ this.stream.write(c.cursorHide);
252
+ this.interval = setInterval(() => {
253
+ const spinner = sym.spinner[this.frame % sym.spinner.length];
254
+ this.stream.write(`\r${c.clearLine}${c.cyan}${spinner}${c.reset} ${this.text}`);
255
+ this.frame++;
256
+ }, 80);
257
+
258
+ return this;
259
+ }
260
+
261
+ update(text) {
262
+ this.text = text;
263
+ if (this._isCI) {
264
+ this.stream.write(` ${c.dim}${sym.arrowRight}${c.reset} ${text}\n`);
265
+ }
266
+ return this;
267
+ }
268
+
269
+ succeed(text) {
270
+ this.stop();
271
+ this.stream.write(`\r${c.clearLine}${c.green}${sym.success}${c.reset} ${text || this.text}\n`);
272
+ return this;
273
+ }
274
+
275
+ fail(text) {
276
+ this.stop();
277
+ this.stream.write(`\r${c.clearLine}${c.red}${sym.error}${c.reset} ${text || this.text}\n`);
278
+ return this;
279
+ }
280
+
281
+ warn(text) {
282
+ this.stop();
283
+ this.stream.write(`\r${c.clearLine}${c.yellow}${sym.warning}${c.reset} ${text || this.text}\n`);
284
+ return this;
285
+ }
286
+
287
+ info(text) {
288
+ this.stop();
289
+ this.stream.write(`\r${c.clearLine}${c.blue}${sym.info}${c.reset} ${text || this.text}\n`);
290
+ return this;
291
+ }
292
+
293
+ stop() {
294
+ if (this.interval) {
295
+ clearInterval(this.interval);
296
+ this.interval = null;
297
+ this.stream.write(c.cursorShow);
298
+ this.stream.write(`\r${c.clearLine}`);
299
+ }
300
+ return this;
301
+ }
302
+ }
303
+
304
+ // ═══════════════════════════════════════════════════════════════════════════════
305
+ // PROGRESS BAR
306
+ // ═══════════════════════════════════════════════════════════════════════════════
307
+ class ProgressBar {
308
+ constructor(total, options = {}) {
309
+ this.total = total;
310
+ this.current = 0;
311
+ this.width = options.width || 30;
312
+ this.complete = options.complete || sym.progressFull;
313
+ this.incomplete = options.incomplete || sym.progressEmpty;
314
+ this.stream = process.stderr;
315
+ this._isCI = isCI();
316
+ }
317
+
318
+ tick(delta = 1) {
319
+ this.current = Math.min(this.current + delta, this.total);
320
+ this.render();
321
+ return this;
322
+ }
323
+
324
+ update(current) {
325
+ this.current = Math.min(current, this.total);
326
+ this.render();
327
+ return this;
328
+ }
329
+
330
+ render() {
331
+ const ratio = Math.min(this.current / this.total, 1);
332
+ const percent = Math.floor(ratio * 100);
333
+ const filled = Math.round(this.width * ratio);
334
+ const empty = this.width - filled;
335
+
336
+ const bar = c.green + this.complete.repeat(filled) + c.dim + this.incomplete.repeat(empty) + c.reset;
337
+ const output = `${bar} ${percent.toString().padStart(3)}% (${this.current}/${this.total})`;
338
+
339
+ if (this._isCI) {
340
+ if (this.current === this.total || this.current % 5 === 0) {
341
+ this.stream.write(` ${output}\n`);
342
+ }
343
+ } else {
344
+ this.stream.write(`\r${c.clearLine}${output}`);
345
+ if (this.current >= this.total) {
346
+ this.stream.write("\n");
347
+ }
348
+ }
349
+
350
+ return this;
351
+ }
352
+ }
353
+
354
+ // ═══════════════════════════════════════════════════════════════════════════════
355
+ // BOX DRAWING
356
+ // ═══════════════════════════════════════════════════════════════════════════════
357
+ function stripAnsi(str) {
358
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
359
+ }
360
+
361
+ function box(content, options = {}) {
362
+ const {
363
+ title = "",
364
+ padding = 1,
365
+ borderColor = c.cyan,
366
+ titleColor = c.bold + c.cyan,
367
+ width = null,
368
+ style = "single", // single, double, rounded
369
+ } = options;
370
+
371
+ const chars = style === "double"
372
+ ? { tl: sym.dblTopLeft, tr: sym.dblTopRight, bl: sym.dblBottomLeft, br: sym.dblBottomRight, h: sym.dblHorizontal, v: sym.dblVertical }
373
+ : { tl: sym.boxTopLeft, tr: sym.boxTopRight, bl: sym.boxBottomLeft, br: sym.boxBottomRight, h: sym.boxHorizontal, v: sym.boxVertical };
374
+
375
+ const lines = content.split("\n");
376
+ const maxLen = Math.max(...lines.map(l => stripAnsi(l).length), stripAnsi(title).length);
377
+ const boxWidth = width || maxLen + padding * 2;
378
+
379
+ const paddingStr = " ".repeat(padding);
380
+ const horizontal = chars.h.repeat(boxWidth);
381
+
382
+ const titlePadded = title ? ` ${title} ` : "";
383
+ const titleLine = titlePadded
384
+ ? horizontal.slice(0, 2) + titleColor + titlePadded + borderColor + horizontal.slice(2 + stripAnsi(titlePadded).length)
385
+ : horizontal;
386
+
387
+ const top = `${borderColor}${chars.tl}${titleLine}${chars.tr}${c.reset}`;
388
+ const bottom = `${borderColor}${chars.bl}${horizontal}${chars.br}${c.reset}`;
389
+
390
+ const paddedLines = lines.map(line => {
391
+ const strippedLen = stripAnsi(line).length;
392
+ const rightPad = boxWidth - strippedLen - padding;
393
+ return `${borderColor}${chars.v}${c.reset}${paddingStr}${line}${" ".repeat(Math.max(0, rightPad))}${borderColor}${chars.v}${c.reset}`;
394
+ });
395
+
396
+ return [top, ...paddedLines, bottom].join("\n");
397
+ }
398
+
399
+ // ═══════════════════════════════════════════════════════════════════════════════
400
+ // TABLE FORMATTING
401
+ // ═══════════════════════════════════════════════════════════════════════════════
402
+ function table(data, options = {}) {
403
+ const { headers = [], align = [], indent = 0 } = options;
404
+
405
+ if (data.length === 0) return "";
406
+
407
+ const columns = Math.max(data[0]?.length || 0, headers.length);
408
+ const allRows = headers.length ? [headers, ...data] : data;
409
+
410
+ const widths = Array(columns).fill(0);
411
+ for (const row of allRows) {
412
+ for (let i = 0; i < columns; i++) {
413
+ widths[i] = Math.max(widths[i], stripAnsi(String(row[i] || "")).length);
414
+ }
415
+ }
416
+
417
+ const lines = [];
418
+ const indentStr = " ".repeat(indent);
419
+
420
+ for (let r = 0; r < allRows.length; r++) {
421
+ const row = allRows[r];
422
+ const cells = row.map((cell, i) => {
423
+ const str = String(cell || "");
424
+ const len = stripAnsi(str).length;
425
+ const pad = widths[i] - len;
426
+ const alignment = align[i] || "left";
427
+
428
+ if (alignment === "right") return " ".repeat(pad) + str;
429
+ if (alignment === "center") return " ".repeat(Math.floor(pad / 2)) + str + " ".repeat(Math.ceil(pad / 2));
430
+ return str + " ".repeat(pad);
431
+ });
432
+
433
+ lines.push(indentStr + cells.join(c.dim + " │ " + c.reset));
434
+
435
+ if (r === 0 && headers.length) {
436
+ const separator = widths.map(w => sym.boxHorizontal.repeat(w)).join(c.dim + "─┼─" + c.reset);
437
+ lines.push(indentStr + c.dim + separator + c.reset);
438
+ }
439
+ }
440
+
441
+ return lines.join("\n");
442
+ }
443
+
444
+ // ═══════════════════════════════════════════════════════════════════════════════
445
+ // TIME FORMATTING
446
+ // ═══════════════════════════════════════════════════════════════════════════════
447
+ function formatDuration(ms) {
448
+ if (ms < 1000) return `${ms}ms`;
449
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
450
+ const mins = Math.floor(ms / 60000);
451
+ const secs = Math.round((ms % 60000) / 1000);
452
+ return `${mins}m ${secs}s`;
453
+ }
454
+
455
+ function formatTime() {
456
+ const d = new Date();
457
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
458
+ }
459
+
460
+ function timeAgo(dateStr) {
461
+ if (!dateStr) return "never";
462
+ const d = new Date(dateStr);
463
+ const now = Date.now();
464
+ const diff = now - d.getTime();
465
+
466
+ if (diff < 60000) return "just now";
467
+ if (diff < 3600000) return `${Math.round(diff / 60000)}m ago`;
468
+ if (diff < 86400000) return `${Math.round(diff / 3600000)}h ago`;
469
+ if (diff < 604800000) return `${Math.round(diff / 86400000)}d ago`;
470
+ return `${Math.round(diff / 604800000)}w ago`;
471
+ }
472
+
473
+ // ═══════════════════════════════════════════════════════════════════════════════
474
+ // VERDICT FORMATTING
475
+ // ═══════════════════════════════════════════════════════════════════════════════
476
+ function verdictColor(verdict) {
477
+ switch (verdict) {
478
+ case "SHIP": return c.green;
479
+ case "WARN": return c.yellow;
480
+ case "BLOCK": return c.red;
481
+ default: return c.dim;
482
+ }
483
+ }
484
+
485
+ function verdictIcon(verdict) {
486
+ switch (verdict) {
487
+ case "SHIP": return `${c.green}${sym.success} SHIP${c.reset}`;
488
+ case "WARN": return `${c.yellow}${sym.warning} WARN${c.reset}`;
489
+ case "BLOCK": return `${c.red}${sym.error} BLOCK${c.reset}`;
490
+ default: return `${c.dim}—${c.reset}`;
491
+ }
492
+ }
493
+
494
+ function verdictBadge(verdict) {
495
+ const colors = {
496
+ SHIP: { bg: c.bgGreen, fg: c.white },
497
+ WARN: { bg: c.bgYellow, fg: c.black },
498
+ BLOCK: { bg: c.bgRed, fg: c.white },
499
+ };
500
+ const { bg, fg } = colors[verdict] || { bg: c.bgGray, fg: c.white };
501
+ return `${bg}${fg}${c.bold} ${verdict} ${c.reset}`;
502
+ }
503
+
504
+ // ═══════════════════════════════════════════════════════════════════════════════
505
+ // HEADER PRINTING
506
+ // ═══════════════════════════════════════════════════════════════════════════════
507
+ function printHeader(title, options = {}) {
508
+ const { icon = "", subtitle = "", width = 60 } = options;
509
+ const iconStr = icon ? `${icon} ` : "";
510
+
511
+ console.log(`
512
+ ${c.cyan}${sym.dblTopLeft}${sym.dblHorizontal.repeat(width)}${sym.dblTopRight}${c.reset}
513
+ ${c.cyan}${sym.dblVertical}${c.reset} ${iconStr}${c.bold}${title}${c.reset}${" ".repeat(Math.max(0, width - stripAnsi(iconStr + title).length - 2))}${c.cyan}${sym.dblVertical}${c.reset}
514
+ ${c.cyan}${sym.dblBottomLeft}${sym.dblHorizontal.repeat(width)}${sym.dblBottomRight}${c.reset}
515
+ ${subtitle ? `\n${c.dim}${subtitle}${c.reset}\n` : ""}`);
516
+ }
517
+
518
+ function printSection(title) {
519
+ console.log(`\n${c.bold}${title}${c.reset}`);
520
+ }
521
+
522
+ function printDivider(width = 60) {
523
+ console.log(`${c.dim}${sym.boxHorizontal.repeat(width)}${c.reset}`);
524
+ }
525
+
526
+ // ═══════════════════════════════════════════════════════════════════════════════
527
+ // EXPORTS
528
+ // ═══════════════════════════════════════════════════════════════════════════════
529
+ module.exports = {
530
+ // Environment
531
+ SUPPORTS_COLOR,
532
+ SUPPORTS_TRUECOLOR,
533
+ SUPPORTS_UNICODE,
534
+ isCI,
535
+
536
+ // Colors & Symbols
537
+ c,
538
+ sym,
539
+ gradient,
540
+ stripAnsi,
541
+
542
+ // Components
543
+ Spinner,
544
+ ProgressBar,
545
+ box,
546
+ table,
547
+
548
+ // Formatting
549
+ formatDuration,
550
+ formatTime,
551
+ timeAgo,
552
+
553
+ // Verdict
554
+ verdictColor,
555
+ verdictIcon,
556
+ verdictBadge,
557
+
558
+ // Printing
559
+ printHeader,
560
+ printSection,
561
+ printDivider,
562
+ };