@vibecheckai/cli 3.0.5 → 3.0.8

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 (63) hide show
  1. package/bin/runners/cli-utils.js +6 -6
  2. package/bin/runners/context/index.js +1 -1
  3. package/bin/runners/lib/entitlements-v2.js +3 -1
  4. package/bin/runners/lib/entitlements.js +3 -0
  5. package/bin/runners/lib/firewall-prompt.js +1 -1
  6. package/bin/runners/lib/report-html.js +5 -0
  7. package/bin/runners/lib/report-templates.js +5 -0
  8. package/bin/runners/lib/report.js +135 -0
  9. package/bin/runners/lib/sandbox/proof-chain.js +3 -3
  10. package/bin/runners/lib/ui.js +562 -0
  11. package/bin/runners/runCtx.js +7 -2
  12. package/bin/runners/runGuard.js +168 -0
  13. package/bin/runners/runInstall.js +41 -1
  14. package/bin/runners/runLabs.js +341 -0
  15. package/bin/runners/runMdc.js +203 -1
  16. package/bin/runners/runProof.zip +0 -0
  17. package/bin/runners/runProve.js +85 -27
  18. package/bin/runners/runReality.js +89 -15
  19. package/bin/runners/runScan.js +6 -6
  20. package/bin/runners/runShare.js +64 -4
  21. package/bin/runners/runStatus.js +3 -1
  22. package/bin/vibecheck.js +415 -774
  23. package/mcp-server/.guardrail/audit/audit.log.jsonl +2 -0
  24. package/mcp-server/.specs/architecture.mdc +90 -0
  25. package/mcp-server/.specs/security.mdc +30 -0
  26. package/mcp-server/README.md +252 -0
  27. package/mcp-server/agent-checkpoint.js +364 -0
  28. package/mcp-server/architect-tools.js +707 -0
  29. package/mcp-server/audit-mcp.js +206 -0
  30. package/mcp-server/codebase-architect-tools.js +838 -0
  31. package/mcp-server/consolidated-tools.js +804 -0
  32. package/mcp-server/hygiene-tools.js +428 -0
  33. package/mcp-server/index-v1.js +698 -0
  34. package/mcp-server/index.js +2092 -0
  35. package/mcp-server/index.old.js +4137 -0
  36. package/mcp-server/intelligence-tools.js +664 -0
  37. package/mcp-server/intent-drift-tools.js +873 -0
  38. package/mcp-server/mdc-generator.js +298 -0
  39. package/mcp-server/package-lock.json +165 -0
  40. package/mcp-server/package.json +47 -0
  41. package/mcp-server/premium-tools.js +1275 -0
  42. package/mcp-server/test-mcp.js +108 -0
  43. package/mcp-server/test-tools.js +36 -0
  44. package/mcp-server/tier-auth.js +147 -0
  45. package/mcp-server/tools/index.js +72 -0
  46. package/mcp-server/tools-reorganized.ts +244 -0
  47. package/mcp-server/truth-context.js +581 -0
  48. package/mcp-server/truth-firewall-tools.js +1500 -0
  49. package/mcp-server/vibecheck-2.0-tools.js +748 -0
  50. package/mcp-server/vibecheck-tools.js +1075 -0
  51. package/package.json +7 -2
  52. package/bin/guardrail.js +0 -843
  53. package/bin/runners/runAudit.js +0 -2
  54. package/bin/runners/runAutopilot.js +0 -2
  55. package/bin/runners/runCertify.js +0 -2
  56. package/bin/runners/runDashboard.js +0 -10
  57. package/bin/runners/runEnhancedShip.js +0 -2
  58. package/bin/runners/runFixPacks.js +0 -2
  59. package/bin/runners/runNaturalLanguage.js +0 -3
  60. package/bin/runners/runProof.js +0 -2
  61. package/bin/runners/runRealitySniff.js +0 -2
  62. package/bin/runners/runUpgrade.js +0 -2
  63. 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
+ };
@@ -131,9 +131,14 @@ async function runCtx(args) {
131
131
  const nextRoutes = (truthpack.routes?.server || []).filter(r =>
132
132
  r.handler?.includes("app/api") || r.handler?.includes("pages/api")
133
133
  ).length;
134
- const fastifyRoutes = (truthpack.routes?.server || []).filter(r =>
134
+ const otherRoutes = (truthpack.routes?.server || []).filter(r =>
135
135
  !r.handler?.includes("app/api") && !r.handler?.includes("pages/api")
136
136
  ).length;
137
+
138
+ // Detect framework from routes evidence or truthpack meta
139
+ const detectedFramework = truthpack.meta?.framework ||
140
+ (truthpack.routes?.server || []).find(r => r.evidence?.[0]?.reason)?.evidence?.[0]?.reason?.split(' ')[0] ||
141
+ 'Express';
137
142
  const clientRefs = truthpack.routes?.clientRefs?.length || 0;
138
143
  const gaps = truthpack.routes?.gaps?.length || 0;
139
144
 
@@ -153,7 +158,7 @@ async function runCtx(args) {
153
158
  console.log('');
154
159
  console.log(`${c.cyan}┌─ Routes ──────────────────────────────────────────────────────────────┐${c.reset}`);
155
160
  console.log(`${c.cyan}│${c.reset} ${c.bold}▲ Next.js:${c.reset} ${nextRoutes} routes`);
156
- console.log(`${c.cyan}│${c.reset} ${c.bold}⚡ Fastify:${c.reset} ${fastifyRoutes} routes`);
161
+ console.log(`${c.cyan}│${c.reset} ${c.bold}⚡ ${detectedFramework}:${c.reset} ${otherRoutes} routes`);
157
162
  console.log(`${c.cyan}│${c.reset} ${c.dim}Client refs:${c.reset} ${clientRefs} ${gaps > 0 ? `${c.yellow}⚠ ${gaps} gaps${c.reset}` : `${c.green}✓ no gaps${c.reset}`}`);
158
163
  console.log(`${c.cyan}└───────────────────────────────────────────────────────────────────────┘${c.reset}`);
159
164