llm-checker 3.4.2 → 3.5.1

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.
@@ -0,0 +1,733 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ // Adapted from /Users/pchmirenko/Downloads/ascii-motion-cli.tsx frame model.
9
+ const THEME_DARK = {
10
+ border: '#6b7280',
11
+ scan: '#56606e',
12
+ outline: '#e2e8f0',
13
+ accent: '#67e8f9',
14
+ logo: '#67e8f9',
15
+ byline: '#facc15',
16
+ subtitle: '#94a3b8',
17
+ muted: '#222a34'
18
+ };
19
+
20
+ const THEME_LIGHT = {
21
+ border: '#475569',
22
+ scan: '#64748b',
23
+ outline: '#0f172a',
24
+ accent: '#0891b2',
25
+ logo: '#0f172a',
26
+ byline: '#854d0e',
27
+ subtitle: '#334155',
28
+ muted: '#cbd5e1'
29
+ };
30
+
31
+ const LOGO_LINES = [
32
+ ' _ _ __ __ ____ _ _ ',
33
+ '| | | | | \\/ | / ___| |__ ___ ___| | _____ _ __ ',
34
+ "| | | | | |\\/| | | | | '_ \\ / _ \\/ __| |/ / _ \\ '__|",
35
+ '| |___ | |___ | | | | | |___| | | | __/ (__| < __/ | ',
36
+ '|_____||_____||_| |_| \\____|_| |_|\\___|\\___|_|\\_\\___|_| '
37
+ ];
38
+
39
+ const MASCOT_MASK = [
40
+ ' /\\_/\\ ',
41
+ ' / o o \\ ',
42
+ ' ( ^ ) ',
43
+ ' \\ _ / ',
44
+ ' /___\\ ',
45
+ ' / \\ ',
46
+ ' (_/ \\_) '
47
+ ];
48
+
49
+ const DEFAULT_LOOP = true;
50
+ const FRAMES_PER_SECOND = 14;
51
+ const DEFAULT_BANNER_SOURCE = path.join(os.homedir(), 'Downloads', 'ascii-motion-cli.tsx');
52
+ const DEFAULT_TEXT_BANNER_SOURCE = path.join(
53
+ os.homedir(),
54
+ 'Desktop',
55
+ 'llm-checker',
56
+ 'banner-profesional-v2.txt'
57
+ );
58
+ let cachedExternalBanner = null;
59
+ let cachedTextBanner = null;
60
+
61
+ function sleep(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+
65
+ function clearTerminal() {
66
+ process.stdout.write('\x1b[2J\x1b[0f');
67
+ }
68
+
69
+ function fitLine(line, width) {
70
+ const value = String(line || '');
71
+ if (value.length <= width) return value;
72
+ if (value.trim().length === 0) return ' '.repeat(width);
73
+ if (width <= 3) return value.slice(0, width);
74
+ return `${value.slice(0, width - 3)}...`;
75
+ }
76
+
77
+ function extractBalanced(source, startIndex, openChar, closeChar) {
78
+ if (startIndex < 0 || source[startIndex] !== openChar) return null;
79
+
80
+ let depth = 0;
81
+ let inString = null;
82
+ let escape = false;
83
+
84
+ for (let index = startIndex; index < source.length; index += 1) {
85
+ const char = source[index];
86
+
87
+ if (inString) {
88
+ if (escape) {
89
+ escape = false;
90
+ continue;
91
+ }
92
+
93
+ if (char === '\\') {
94
+ escape = true;
95
+ continue;
96
+ }
97
+
98
+ if (char === inString) {
99
+ inString = null;
100
+ }
101
+
102
+ continue;
103
+ }
104
+
105
+ if (char === '"' || char === '\'' || char === '`') {
106
+ inString = char;
107
+ continue;
108
+ }
109
+
110
+ if (char === openChar) {
111
+ depth += 1;
112
+ } else if (char === closeChar) {
113
+ depth -= 1;
114
+ if (depth === 0) {
115
+ return source.slice(startIndex, index + 1);
116
+ }
117
+ }
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ function extractAssignedLiteral(source, constName, openChar, closeChar) {
124
+ const marker = `const ${constName}`;
125
+ const markerIndex = source.indexOf(marker);
126
+ if (markerIndex < 0) return null;
127
+
128
+ const equalsIndex = source.indexOf('=', markerIndex);
129
+ if (equalsIndex < 0) return null;
130
+
131
+ const startIndex = source.indexOf(openChar, equalsIndex);
132
+ if (startIndex < 0) return null;
133
+
134
+ return extractBalanced(source, startIndex, openChar, closeChar);
135
+ }
136
+
137
+ function evaluateLiteral(literal) {
138
+ if (!literal) return null;
139
+ try {
140
+ return Function(`"use strict"; return (${literal});`)();
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function parseNumericConstant(source, constName) {
147
+ const match = source.match(new RegExp(`const\\s+${constName}\\s*=\\s*(\\d+(?:\\.\\d+)?)`));
148
+ if (!match) return null;
149
+ const parsed = Number.parseFloat(match[1]);
150
+ return Number.isFinite(parsed) ? parsed : null;
151
+ }
152
+
153
+ function getLongestFrameLine(frames) {
154
+ let longest = 0;
155
+ for (const frame of frames) {
156
+ const rows = Array.isArray(frame.content) ? frame.content : [];
157
+ for (const row of rows) {
158
+ longest = Math.max(longest, String(row || '').length);
159
+ }
160
+ }
161
+ return longest;
162
+ }
163
+
164
+ function normalizeExternalFrame(frame, contentWidth, defaultDuration) {
165
+ const sourceRows = Array.isArray(frame.content) ? frame.content : [];
166
+ const content = sourceRows.map((line) => fitLine(line, contentWidth).padEnd(contentWidth, ' '));
167
+ const duration = Number.isFinite(frame.duration) ? frame.duration : defaultDuration;
168
+
169
+ return {
170
+ duration,
171
+ content,
172
+ fgColors: frame.fgColors && typeof frame.fgColors === 'object' ? frame.fgColors : {},
173
+ bgColors: frame.bgColors && typeof frame.bgColors === 'object' ? frame.bgColors : {}
174
+ };
175
+ }
176
+
177
+ function loadExternalBanner(sourceFile) {
178
+ const filePath = sourceFile || process.env.LLM_CHECKER_BANNER_SOURCE || DEFAULT_BANNER_SOURCE;
179
+ let mtimeMs = -1;
180
+
181
+ try {
182
+ const stat = fs.statSync(filePath);
183
+ mtimeMs = stat.mtimeMs;
184
+ } catch {
185
+ cachedExternalBanner = {
186
+ filePath,
187
+ mtimeMs: -1,
188
+ payload: null
189
+ };
190
+ return null;
191
+ }
192
+
193
+ if (
194
+ cachedExternalBanner &&
195
+ cachedExternalBanner.filePath === filePath &&
196
+ cachedExternalBanner.mtimeMs === mtimeMs
197
+ ) {
198
+ return cachedExternalBanner.payload;
199
+ }
200
+
201
+ try {
202
+ const source = fs.readFileSync(filePath, 'utf8');
203
+ const framesLiteral = extractAssignedLiteral(source, 'FRAMES', '[', ']');
204
+ const darkThemeLiteral = extractAssignedLiteral(source, 'THEME_DARK', '{', '}');
205
+ const lightThemeLiteral = extractAssignedLiteral(source, 'THEME_LIGHT', '{', '}');
206
+
207
+ const frames = evaluateLiteral(framesLiteral);
208
+ const themeDark = evaluateLiteral(darkThemeLiteral);
209
+ const themeLight = evaluateLiteral(lightThemeLiteral);
210
+ const canvasWidth = parseNumericConstant(source, 'CANVAS_WIDTH');
211
+
212
+ if (!Array.isArray(frames) || frames.length === 0) {
213
+ cachedExternalBanner = {
214
+ filePath,
215
+ mtimeMs,
216
+ payload: null
217
+ };
218
+ return null;
219
+ }
220
+
221
+ const payload = {
222
+ frames,
223
+ themeDark: themeDark && typeof themeDark === 'object' ? themeDark : {},
224
+ themeLight: themeLight && typeof themeLight === 'object' ? themeLight : {},
225
+ canvasWidth: Number.isFinite(canvasWidth) ? canvasWidth : null
226
+ };
227
+
228
+ cachedExternalBanner = {
229
+ filePath,
230
+ mtimeMs,
231
+ payload
232
+ };
233
+
234
+ return payload;
235
+ } catch {
236
+ cachedExternalBanner = {
237
+ filePath,
238
+ mtimeMs,
239
+ payload: null
240
+ };
241
+ return null;
242
+ }
243
+ }
244
+
245
+ function loadTextBanner(sourceFile) {
246
+ const filePath =
247
+ sourceFile ||
248
+ process.env.LLM_CHECKER_TEXT_BANNER_SOURCE ||
249
+ DEFAULT_TEXT_BANNER_SOURCE;
250
+ let mtimeMs = -1;
251
+
252
+ try {
253
+ const stat = fs.statSync(filePath);
254
+ mtimeMs = stat.mtimeMs;
255
+ } catch {
256
+ cachedTextBanner = {
257
+ filePath,
258
+ mtimeMs: -1,
259
+ lines: null
260
+ };
261
+ return null;
262
+ }
263
+
264
+ if (
265
+ cachedTextBanner &&
266
+ cachedTextBanner.filePath === filePath &&
267
+ cachedTextBanner.mtimeMs === mtimeMs
268
+ ) {
269
+ return cachedTextBanner.lines;
270
+ }
271
+
272
+ try {
273
+ const raw = fs.readFileSync(filePath, 'utf8');
274
+ const lines = String(raw).split(/\r?\n/);
275
+ cachedTextBanner = {
276
+ filePath,
277
+ mtimeMs,
278
+ lines
279
+ };
280
+ return lines;
281
+ } catch {
282
+ cachedTextBanner = {
283
+ filePath,
284
+ mtimeMs,
285
+ lines: null
286
+ };
287
+ return null;
288
+ }
289
+ }
290
+
291
+ function drawTextBanner(lines, options = {}) {
292
+ const colorPhase = Number.isFinite(options.colorPhase)
293
+ ? Math.max(0, Math.floor(options.colorPhase))
294
+ : 0;
295
+ const terminalWidth = Number.isFinite(process.stdout.columns) && process.stdout.columns > 0
296
+ ? process.stdout.columns
297
+ : null;
298
+
299
+ const centerToWidth = (text, width) => {
300
+ const value = String(text || '').replace(/\s+$/g, '');
301
+ if (!Number.isFinite(width) || width <= 0) return value;
302
+ if (value.length >= width) return value.slice(0, width);
303
+ const left = Math.floor((width - value.length) / 2);
304
+ const right = width - value.length - left;
305
+ return `${' '.repeat(left)}${value}${' '.repeat(right)}`;
306
+ };
307
+
308
+ const fitLogoToWidth = (text, width) => {
309
+ if (!Number.isFinite(width) || width <= 0) return String(text || '');
310
+
311
+ const base = String(text || '').replace(/\s+$/g, '');
312
+ if (base.length <= width) {
313
+ return centerToWidth(base, width);
314
+ }
315
+
316
+ // Do not distort glyphs. If still too wide, degrade to readable fallback.
317
+ return centerToWidth(width >= 24 ? 'LLM-CHECKER' : 'LLM', width);
318
+ };
319
+
320
+ const colorizeDosRebelLine = (text) => {
321
+ const solidPalette = ['#F8FAFC', '#E2ECFF', '#DBEAFE', '#E2ECFF'];
322
+ const shadePalette = ['#93C5FD', '#60A5FA', '#38BDF8', '#22D3EE', '#38BDF8', '#60A5FA'];
323
+ let out = '';
324
+ for (let index = 0; index < text.length; index += 1) {
325
+ const ch = text[index];
326
+ if (ch === '█') {
327
+ const tone = solidPalette[(index + colorPhase) % solidPalette.length];
328
+ out += chalk.hex(tone)(ch);
329
+ } else if (ch === '░' || ch === '▒' || ch === '▓') {
330
+ const tone = shadePalette[(index + colorPhase) % shadePalette.length];
331
+ out += chalk.hex(tone)(ch);
332
+ } else {
333
+ out += ch;
334
+ }
335
+ }
336
+ return out;
337
+ };
338
+
339
+ for (const line of lines) {
340
+ if (!line) {
341
+ console.log('');
342
+ continue;
343
+ }
344
+
345
+ if (/^\s*\+[-+]+\+\s*$/.test(line)) {
346
+ if (terminalWidth && terminalWidth >= 10) {
347
+ const inner = Math.max(6, terminalWidth - 4);
348
+ console.log(chalk.hex('#0066FF')(` +${'-'.repeat(inner)}+ `));
349
+ } else {
350
+ console.log(chalk.hex('#0066FF')(line));
351
+ }
352
+ continue;
353
+ }
354
+
355
+ const frameMatch = line.match(/^(\s*\|)(.*)(\|\s*)$/);
356
+ if (!frameMatch) {
357
+ console.log(line);
358
+ continue;
359
+ }
360
+
361
+ const left = chalk.hex('#0066FF')(frameMatch[1]);
362
+ const right = chalk.hex('#0066FF')(frameMatch[3]);
363
+ const content = frameMatch[2];
364
+ const maxInnerWidth = terminalWidth
365
+ ? Math.max(0, terminalWidth - (frameMatch[1].length + frameMatch[3].length))
366
+ : null;
367
+ const isDosRebelLike =
368
+ content.includes('█') ||
369
+ content.includes('░') ||
370
+ content.includes('▒') ||
371
+ content.includes('▓');
372
+
373
+ let fittedContent = content;
374
+ if (Number.isFinite(maxInnerWidth)) {
375
+ if (isDosRebelLike) {
376
+ fittedContent = fitLogoToWidth(content, maxInnerWidth);
377
+ } else {
378
+ const trimmed = content.trim();
379
+ fittedContent = trimmed.length === 0
380
+ ? ' '.repeat(maxInnerWidth)
381
+ : centerToWidth(trimmed, maxInnerWidth);
382
+ }
383
+ }
384
+
385
+ let inner = fittedContent;
386
+
387
+ if (
388
+ fittedContent.includes('INTELLIGENT OLLAMA MODEL SELECTOR') ||
389
+ fittedContent.includes('Deterministic scoring across') ||
390
+ fittedContent.includes('Run: llm-checker recommend')
391
+ ) {
392
+ inner = chalk.hex('#60A5FA')(fittedContent);
393
+ } else if (
394
+ fittedContent.includes('[200+ DYNAMIC MODELS]') ||
395
+ fittedContent.includes('[35+ FALLBACK]') ||
396
+ fittedContent.includes('[4D SCORING]') ||
397
+ fittedContent.includes('[MULTI-GPU]') ||
398
+ fittedContent.includes('[MCP SERVER]')
399
+ ) {
400
+ inner = chalk.hex('#A7F3D0')(fittedContent);
401
+ } else if (
402
+ fittedContent.includes('AI-powered CLI for hardware-aware local LLM recommendations')
403
+ ) {
404
+ inner = chalk.hex('#C7D2FE')(fittedContent);
405
+ } else if (
406
+ fittedContent.includes('github.com/Pavelevich/llm-checker') ||
407
+ fittedContent.includes('npmjs.com/package/llm-checker')
408
+ ) {
409
+ inner = chalk.hex('#3B82F6')(fittedContent);
410
+ } else if (fittedContent.includes('Install: npm install -g llm-checker')) {
411
+ inner = chalk.hex('#F8FAFC')(fittedContent);
412
+ } else if (
413
+ fittedContent.includes('█') ||
414
+ fittedContent.includes('░') ||
415
+ fittedContent.includes('▒') ||
416
+ fittedContent.includes('▓')
417
+ ) {
418
+ inner = colorizeDosRebelLine(fittedContent);
419
+ } else if (
420
+ /[_\\\/|]/.test(fittedContent) ||
421
+ fittedContent.includes('____') ||
422
+ fittedContent.includes('▀') ||
423
+ fittedContent.includes('▄')
424
+ ) {
425
+ inner = chalk.hex('#F8FAFC')(fittedContent);
426
+ }
427
+
428
+ console.log(left + inner + right);
429
+ }
430
+ }
431
+
432
+ function buildScanline(width, row, phase) {
433
+ const stripe = (row + phase) % 2 === 0 ? '=' : '-';
434
+ return stripe.repeat(width);
435
+ }
436
+
437
+ function applyMask(baseLine, maskLine) {
438
+ if (!maskLine) return baseLine;
439
+
440
+ const result = baseLine.split('');
441
+ const limit = Math.min(baseLine.length, maskLine.length);
442
+ for (let index = 0; index < limit; index += 1) {
443
+ const symbol = maskLine[index];
444
+ if (symbol !== ' ') result[index] = symbol;
445
+ }
446
+
447
+ return result.join('');
448
+ }
449
+
450
+ function buildMascotLines(phase) {
451
+ const width = 34;
452
+ const rows = 11;
453
+ const maskOffset = 2;
454
+ const lines = [];
455
+
456
+ for (let row = 0; row < rows; row += 1) {
457
+ const maskLine = MASCOT_MASK[row - maskOffset];
458
+ lines.push(applyMask(buildScanline(width, row, phase), maskLine));
459
+ }
460
+
461
+ return lines;
462
+ }
463
+
464
+ function buildRows(phase) {
465
+ return [
466
+ ...buildMascotLines(phase).map((text) => ({ kind: 'mascot', text })),
467
+ { kind: 'blank', text: '' },
468
+ ...LOGO_LINES.map((text) => ({ kind: 'logo', text })),
469
+ { kind: 'blank', text: '' },
470
+ { kind: 'byline', text: 'by Pavelevich' },
471
+ { kind: 'subtitle', text: 'Interactive command panel' }
472
+ ];
473
+ }
474
+
475
+ function classifyMascotColor(char) {
476
+ if (char === '=' || char === '-') return 'scan';
477
+ if (char === 'o' || char === '^') return 'accent';
478
+ if (char === '/' || char === '\\' || char === '(' || char === ')' || char === '_') {
479
+ return 'outline';
480
+ }
481
+ return 'outline';
482
+ }
483
+
484
+ function colorKeyForChar(kind, char, visible) {
485
+ if (!visible) return 'muted';
486
+
487
+ if (kind === 'blank') return 'muted';
488
+ if (kind === 'logo') return 'logo';
489
+ if (kind === 'byline') return 'byline';
490
+ if (kind === 'subtitle') return 'subtitle';
491
+ if (kind === 'mascot') return classifyMascotColor(char);
492
+ return 'logo';
493
+ }
494
+
495
+ function createFrameData(progress, phase, contentWidth, frameDuration) {
496
+ const sourceRows = buildRows(phase);
497
+ const content = [];
498
+ const fgColors = {};
499
+
500
+ for (let y = 0; y < sourceRows.length; y += 1) {
501
+ const row = sourceRows[y];
502
+ const fitted = fitLine(row.text, contentWidth).padEnd(contentWidth, ' ');
503
+ const visibleChars = Math.floor(fitted.length * progress);
504
+ content.push(fitted);
505
+
506
+ for (let x = 0; x < fitted.length; x += 1) {
507
+ const char = fitted[x];
508
+ const visible = x < visibleChars;
509
+ const colorKey = colorKeyForChar(row.kind, char, visible);
510
+ if (colorKey) {
511
+ fgColors[`${x},${y}`] = colorKey;
512
+ }
513
+ }
514
+ }
515
+
516
+ return {
517
+ duration: frameDuration,
518
+ content,
519
+ fgColors,
520
+ bgColors: {}
521
+ };
522
+ }
523
+
524
+ function resolveTheme(hasDarkBackground, externalTheme = null) {
525
+ const base = hasDarkBackground ? THEME_DARK : THEME_LIGHT;
526
+ if (!externalTheme || typeof externalTheme !== 'object') return base;
527
+ return { ...base, ...externalTheme };
528
+ }
529
+
530
+ function resolveTerminalWidth(preferredWidth, maxContentWidth) {
531
+ const terminalWidth = process.stdout.columns || preferredWidth;
532
+ const maxWidth = Math.max(24, terminalWidth - 2);
533
+ const fallbackLongest = buildRows(0).reduce((max, row) => Math.max(max, row.text.length), 0);
534
+ const longestLine = Math.max(0, maxContentWidth || fallbackLongest);
535
+ const minWidth = longestLine + 4;
536
+
537
+ return Math.min(Math.max(preferredWidth, minWidth), maxWidth);
538
+ }
539
+
540
+ function makeFrames(options = {}) {
541
+ const {
542
+ frameCount = 16,
543
+ width = 74,
544
+ hasDarkBackground = true,
545
+ frameDurationMs = Math.round(1000 / FRAMES_PER_SECOND),
546
+ sourceFile
547
+ } = options;
548
+
549
+ const externalBanner = loadExternalBanner(sourceFile);
550
+ if (externalBanner) {
551
+ const longestExternalLine = Math.max(
552
+ getLongestFrameLine(externalBanner.frames),
553
+ externalBanner.canvasWidth || 0
554
+ );
555
+ const resolvedWidth = resolveTerminalWidth(width, longestExternalLine);
556
+ const contentWidth = resolvedWidth - 4;
557
+ const externalTheme = hasDarkBackground
558
+ ? externalBanner.themeDark
559
+ : externalBanner.themeLight;
560
+ const theme = resolveTheme(hasDarkBackground, externalTheme);
561
+
562
+ const sourceFrames = externalBanner.frames.length > 0
563
+ ? externalBanner.frames
564
+ : [{ content: [''], fgColors: {}, bgColors: {}, duration: frameDurationMs }];
565
+
566
+ const externalFrames = sourceFrames.map((frame) =>
567
+ normalizeExternalFrame(frame, contentWidth, frameDurationMs)
568
+ );
569
+
570
+ if (frameCount <= 1) {
571
+ return {
572
+ width: resolvedWidth,
573
+ theme,
574
+ frames: [externalFrames[externalFrames.length - 1]]
575
+ };
576
+ }
577
+
578
+ return {
579
+ width: resolvedWidth,
580
+ theme,
581
+ frames: externalFrames
582
+ };
583
+ }
584
+
585
+ const resolvedWidth = resolveTerminalWidth(width);
586
+ const contentWidth = resolvedWidth - 4;
587
+ const frames = [];
588
+
589
+ for (let frameIndex = 0; frameIndex < Math.max(1, frameCount); frameIndex += 1) {
590
+ const progress = frameCount <= 1 ? 1 : frameIndex / (frameCount - 1);
591
+ const phase = frameIndex % 2;
592
+ frames.push(createFrameData(progress, phase, contentWidth, frameDurationMs));
593
+ }
594
+
595
+ return {
596
+ width: resolvedWidth,
597
+ theme: resolveTheme(hasDarkBackground),
598
+ frames
599
+ };
600
+ }
601
+
602
+ function applyFg(text, color) {
603
+ if (!color) return text;
604
+ if (color.startsWith('#')) return chalk.hex(color)(text);
605
+ if (typeof chalk[color] === 'function') return chalk[color](text);
606
+ return text;
607
+ }
608
+
609
+ function applyBg(text, color) {
610
+ if (!color) return text;
611
+ if (color.startsWith('#')) return chalk.bgHex(color)(text);
612
+ const key = `bg${color[0].toUpperCase()}${color.slice(1)}`;
613
+ if (typeof chalk[key] === 'function') return chalk[key](text);
614
+ return text;
615
+ }
616
+
617
+ function drawFrame(frame, width, theme) {
618
+ const top = `+${'-'.repeat(width - 2)}+`;
619
+ const bottom = `+${'-'.repeat(width - 2)}+`;
620
+ const contentWidth = width - 4;
621
+
622
+ console.log(applyFg(top, theme.border));
623
+
624
+ for (let y = 0; y < frame.content.length; y += 1) {
625
+ const row = fitLine(frame.content[y] || '', contentWidth).padEnd(contentWidth, ' ');
626
+ let renderedRow = '';
627
+
628
+ for (let x = 0; x < row.length; x += 1) {
629
+ const char = row[x];
630
+ const key = `${x},${y}`;
631
+ const fgColorKey = frame.fgColors[key];
632
+ const bgColorKey = frame.bgColors[key];
633
+
634
+ const fgColor = fgColorKey
635
+ ? (theme[fgColorKey] || fgColorKey)
636
+ : (theme.logo || 'white');
637
+ const bgColor = bgColorKey
638
+ ? (theme[bgColorKey] || bgColorKey)
639
+ : undefined;
640
+
641
+ let styled = applyFg(char, fgColor);
642
+ if (bgColor) styled = applyBg(styled, bgColor);
643
+ renderedRow += styled;
644
+ }
645
+
646
+ const left = applyFg('| ', theme.border);
647
+ const right = applyFg(' |', theme.border);
648
+ console.log(left + renderedRow + right);
649
+ }
650
+
651
+ console.log(applyFg(bottom, theme.border));
652
+ }
653
+
654
+ async function animateBanner(options = {}) {
655
+ const {
656
+ hasDarkBackground = true,
657
+ autoPlay = true,
658
+ loop: _loop = DEFAULT_LOOP,
659
+ frameDelayMs,
660
+ frames = 16,
661
+ enabled = true
662
+ } = options;
663
+
664
+ const shouldAnimate =
665
+ enabled &&
666
+ autoPlay &&
667
+ process.stdout.isTTY &&
668
+ process.env.LLM_CHECKER_DISABLE_ANIMATION !== '1';
669
+
670
+ const frameDurationMs = frameDelayMs || Math.round(1000 / FRAMES_PER_SECOND);
671
+ const prepared = makeFrames({
672
+ frameCount: Math.max(1, frames),
673
+ hasDarkBackground,
674
+ frameDurationMs
675
+ });
676
+ const textBanner = loadTextBanner();
677
+
678
+ if (textBanner && textBanner.length > 0) {
679
+ if (!shouldAnimate) {
680
+ clearTerminal();
681
+ drawTextBanner(textBanner);
682
+ return;
683
+ }
684
+
685
+ const textFrames = Math.max(10, Math.min(24, frames));
686
+ for (let frameIndex = 0; frameIndex < textFrames; frameIndex += 1) {
687
+ clearTerminal();
688
+ drawTextBanner(textBanner, { colorPhase: frameIndex });
689
+ await sleep(frameDurationMs);
690
+ }
691
+ return;
692
+ }
693
+
694
+ if (!shouldAnimate || prepared.frames.length <= 1) {
695
+ clearTerminal();
696
+ drawFrame(prepared.frames[prepared.frames.length - 1], prepared.width, prepared.theme);
697
+ return;
698
+ }
699
+
700
+ for (const frame of prepared.frames) {
701
+ clearTerminal();
702
+ drawFrame(frame, prepared.width, prepared.theme);
703
+ await sleep(frame.duration);
704
+ }
705
+ }
706
+
707
+ function renderPersistentBanner(width = 74, options = {}) {
708
+ const textBanner = loadTextBanner();
709
+ if (textBanner && textBanner.length > 0) {
710
+ drawTextBanner(textBanner, options);
711
+ return;
712
+ }
713
+
714
+ const prepared = makeFrames({ frameCount: 1, width });
715
+ drawFrame(prepared.frames[0], prepared.width, prepared.theme);
716
+ }
717
+
718
+ function renderCommandHeader(commandLabel) {
719
+ const label = String(commandLabel || 'command');
720
+ const line = '-'.repeat(Math.min(64, Math.max(28, label.length + 24)));
721
+ console.log(chalk.cyan.bold(`\nllm-checker | ${label}`));
722
+ console.log(chalk.gray(line));
723
+ }
724
+
725
+ module.exports = {
726
+ animateBanner,
727
+ renderPersistentBanner,
728
+ renderCommandHeader,
729
+ __private: {
730
+ makeFrames,
731
+ drawFrame
732
+ }
733
+ };