@vizzly-testing/cli 0.20.1-beta.0 → 0.20.1-beta.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.
- package/dist/cli.js +177 -2
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +118 -33
- package/dist/commands/finalize.js +8 -3
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +42 -49
- package/dist/commands/logout.js +13 -5
- package/dist/commands/project.js +95 -67
- package/dist/commands/run.js +32 -6
- package/dist/commands/status.js +81 -50
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +4 -25
- package/dist/commands/upload.js +18 -9
- package/dist/commands/whoami.js +40 -38
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +11 -11
- package/dist/server/handlers/tdd-handler.js +113 -7
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +5 -2
- package/dist/server-manager/operations.js +2 -1
- package/dist/tdd/tdd-service.js +190 -126
- package/dist/types/client.d.ts +25 -2
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +12 -7
- package/dist/report-generator/core.js +0 -315
- package/dist/report-generator/index.js +0 -8
- package/dist/report-generator/operations.js +0 -196
- package/dist/services/static-report-generator.js +0 -65
package/dist/utils/output.js
CHANGED
|
@@ -30,13 +30,14 @@ const config = {
|
|
|
30
30
|
json: false,
|
|
31
31
|
logLevel: null,
|
|
32
32
|
// null = not yet initialized, will check env var on first configure
|
|
33
|
-
color:
|
|
33
|
+
color: undefined,
|
|
34
|
+
// undefined = auto-detect, true = force on, false = force off
|
|
34
35
|
silent: false,
|
|
35
36
|
logFile: null
|
|
36
37
|
};
|
|
37
38
|
let colors = createColors({
|
|
38
39
|
useColor: config.color
|
|
39
|
-
});
|
|
40
|
+
}); // undefined triggers auto-detect
|
|
40
41
|
let spinnerInterval = null;
|
|
41
42
|
let spinnerMessage = '';
|
|
42
43
|
let lastSpinnerLine = '';
|
|
@@ -142,16 +143,29 @@ export function isVerbose() {
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/**
|
|
145
|
-
* Show command header
|
|
146
|
+
* Show command header with distinctive branding
|
|
147
|
+
* Uses Observatory's signature amber color for "vizzly"
|
|
146
148
|
* Only shows once per command execution
|
|
149
|
+
* @param {string} command - Command name (e.g., 'tdd', 'run')
|
|
150
|
+
* @param {string} [mode] - Optional mode (e.g., 'local', 'cloud')
|
|
147
151
|
*/
|
|
148
152
|
export function header(command, mode = null) {
|
|
149
153
|
if (config.json || config.silent || headerShown) return;
|
|
150
154
|
headerShown = true;
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
let parts = [];
|
|
156
|
+
|
|
157
|
+
// Brand "vizzly" with Observatory's signature amber
|
|
158
|
+
parts.push(colors.brand.amber(colors.bold('vizzly')));
|
|
159
|
+
|
|
160
|
+
// Command in info blue (processing, active)
|
|
161
|
+
parts.push(colors.brand.info(command));
|
|
162
|
+
|
|
163
|
+
// Mode (if provided) in muted text
|
|
164
|
+
if (mode) {
|
|
165
|
+
parts.push(colors.brand.textTertiary(mode));
|
|
166
|
+
}
|
|
153
167
|
console.error('');
|
|
154
|
-
console.error(
|
|
168
|
+
console.error(parts.join(colors.brand.textMuted(' · ')));
|
|
155
169
|
console.error('');
|
|
156
170
|
}
|
|
157
171
|
|
|
@@ -334,16 +348,20 @@ export function data(obj) {
|
|
|
334
348
|
|
|
335
349
|
/**
|
|
336
350
|
* Start a spinner with message
|
|
351
|
+
* Uses Observatory amber for the spinner animation
|
|
337
352
|
*/
|
|
338
353
|
export function startSpinner(message) {
|
|
339
354
|
if (config.json || config.silent || !process.stderr.isTTY) return;
|
|
340
355
|
stopSpinner();
|
|
341
356
|
spinnerMessage = message;
|
|
342
|
-
|
|
357
|
+
|
|
358
|
+
// Braille dots spinner - smooth animation
|
|
359
|
+
let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
343
360
|
let i = 0;
|
|
344
361
|
spinnerInterval = setInterval(() => {
|
|
345
|
-
|
|
346
|
-
|
|
362
|
+
let frame = frames[i++ % frames.length];
|
|
363
|
+
// Use amber brand color for spinner, plain text for message (better readability)
|
|
364
|
+
let line = ` ${colors.brand.amber(frame)} ${spinnerMessage}`;
|
|
347
365
|
|
|
348
366
|
// Clear previous line and write new one
|
|
349
367
|
process.stderr.write(`\r${' '.repeat(lastSpinnerLine.length)}\r`);
|
|
@@ -357,7 +375,7 @@ export function startSpinner(message) {
|
|
|
357
375
|
*/
|
|
358
376
|
export function updateSpinner(message, current = 0, total = 0) {
|
|
359
377
|
if (config.json || config.silent || !process.stderr.isTTY) return;
|
|
360
|
-
|
|
378
|
+
let progressText = total > 0 ? ` ${colors.brand.textMuted(`(${current}/${total})`)}` : '';
|
|
361
379
|
spinnerMessage = `${message}${progressText}`;
|
|
362
380
|
if (!spinnerInterval) {
|
|
363
381
|
startSpinner(spinnerMessage);
|
|
@@ -450,6 +468,33 @@ function formatData(data) {
|
|
|
450
468
|
* @param {string} message - Debug message
|
|
451
469
|
* @param {Object} data - Optional data object to display inline
|
|
452
470
|
*/
|
|
471
|
+
/**
|
|
472
|
+
* Get a distinctive color for a component name
|
|
473
|
+
* Uses Observatory design system colors for consistent styling
|
|
474
|
+
* @param {string} component - Component name
|
|
475
|
+
* @returns {Function} Color function
|
|
476
|
+
*/
|
|
477
|
+
function getComponentColor(component) {
|
|
478
|
+
// Map components to Observatory semantic colors
|
|
479
|
+
let componentColors = {
|
|
480
|
+
// Server/infrastructure - success green (active, running)
|
|
481
|
+
server: colors.brand.success,
|
|
482
|
+
baseline: colors.brand.success,
|
|
483
|
+
// TDD/comparison - info blue (processing, informational)
|
|
484
|
+
tdd: colors.brand.info,
|
|
485
|
+
compare: colors.brand.info,
|
|
486
|
+
// Config/auth - warning amber (attention, configuration)
|
|
487
|
+
config: colors.brand.warning,
|
|
488
|
+
build: colors.brand.warning,
|
|
489
|
+
auth: colors.brand.warning,
|
|
490
|
+
// Upload/API - info blue (processing)
|
|
491
|
+
upload: colors.brand.info,
|
|
492
|
+
api: colors.brand.info,
|
|
493
|
+
// Run - amber (primary action)
|
|
494
|
+
run: colors.brand.amber
|
|
495
|
+
};
|
|
496
|
+
return componentColors[component] || colors.brand.info;
|
|
497
|
+
}
|
|
453
498
|
export function debug(component, message, data = {}) {
|
|
454
499
|
if (!shouldLog('debug')) return;
|
|
455
500
|
|
|
@@ -472,12 +517,15 @@ export function debug(component, message, data = {}) {
|
|
|
472
517
|
let formattedData = formatData(data);
|
|
473
518
|
let dataStr = formattedData ? ` ${colors.dim(formattedData)}` : '';
|
|
474
519
|
if (component) {
|
|
475
|
-
// Component-based format
|
|
520
|
+
// Component-based format with distinctive colors
|
|
521
|
+
// " server ready on :47392"
|
|
476
522
|
let paddedComponent = component.padEnd(8);
|
|
477
|
-
|
|
523
|
+
let componentColor = getComponentColor(component);
|
|
524
|
+
// Use plain text for message (better readability on dark backgrounds)
|
|
525
|
+
console.error(` ${componentColor(paddedComponent)} ${message}${dataStr}`);
|
|
478
526
|
} else {
|
|
479
527
|
// Simple format for legacy calls
|
|
480
|
-
console.error(` ${colors.dim('•')} ${
|
|
528
|
+
console.error(` ${colors.dim('•')} ${message}${dataStr}`);
|
|
481
529
|
}
|
|
482
530
|
}
|
|
483
531
|
writeLog('debug', message, {
|
|
@@ -523,6 +571,393 @@ function writeLog(level, message, data = {}) {
|
|
|
523
571
|
}
|
|
524
572
|
}
|
|
525
573
|
|
|
574
|
+
// ============================================================================
|
|
575
|
+
// Visual formatting helpers
|
|
576
|
+
// ============================================================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Generate a visual diff bar with color coding
|
|
580
|
+
* Shows percentage as a filled/empty bar with color based on severity
|
|
581
|
+
* Uses Observatory semantic colors (success → warning → danger)
|
|
582
|
+
* @param {number} percentage - Diff percentage (0-100)
|
|
583
|
+
* @param {number} [width=10] - Bar width in characters
|
|
584
|
+
* @returns {string} Colored diff bar string
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* diffBar(4.2) // Returns "████░░░░░░" in warning amber
|
|
588
|
+
* diffBar(0.5) // Returns "█░░░░░░░░░" in success green
|
|
589
|
+
* diffBar(15.0) // Returns "██░░░░░░░░" in danger red
|
|
590
|
+
*/
|
|
591
|
+
export function diffBar(percentage, width = 10) {
|
|
592
|
+
if (config.json || config.silent) return '';
|
|
593
|
+
|
|
594
|
+
// Calculate filled blocks - ensure at least 1 filled for non-zero percentages
|
|
595
|
+
let filled = Math.round(percentage / 100 * width);
|
|
596
|
+
if (percentage > 0 && filled === 0) filled = 1;
|
|
597
|
+
let empty = width - filled;
|
|
598
|
+
|
|
599
|
+
// Color based on severity using Observatory semantic colors
|
|
600
|
+
let barColor;
|
|
601
|
+
if (percentage < 1) {
|
|
602
|
+
barColor = colors.brand.success; // Green - minimal change
|
|
603
|
+
} else if (percentage < 5) {
|
|
604
|
+
barColor = colors.brand.warning; // Amber - attention needed
|
|
605
|
+
} else {
|
|
606
|
+
barColor = colors.brand.danger; // Red - significant change
|
|
607
|
+
}
|
|
608
|
+
let filledPart = barColor('█'.repeat(filled));
|
|
609
|
+
let emptyPart = colors.brand.textMuted('░'.repeat(empty));
|
|
610
|
+
return `${filledPart}${emptyPart}`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Generate a gradient progress bar
|
|
615
|
+
* Creates a visually appealing progress indicator with color gradient
|
|
616
|
+
* Default gradient uses Observatory amber → amber-light (signature brand gradient)
|
|
617
|
+
* @param {number} current - Current progress value
|
|
618
|
+
* @param {number} total - Total value
|
|
619
|
+
* @param {number} [width=20] - Bar width in characters
|
|
620
|
+
* @param {Object} [options] - Gradient options
|
|
621
|
+
* @param {string} [options.from='#F59E0B'] - Start color (hex) - default: amber
|
|
622
|
+
* @param {string} [options.to='#FBBF24'] - End color (hex) - default: amber-light
|
|
623
|
+
* @returns {string} Gradient progress bar string
|
|
624
|
+
*/
|
|
625
|
+
export function progressBar(current, total, width = 20, options = {}) {
|
|
626
|
+
if (config.json || config.silent) return '';
|
|
627
|
+
|
|
628
|
+
// Default to Observatory's signature amber gradient
|
|
629
|
+
let {
|
|
630
|
+
from = '#F59E0B',
|
|
631
|
+
to = '#FBBF24'
|
|
632
|
+
} = options;
|
|
633
|
+
let percent = Math.min(100, Math.max(0, current / total * 100));
|
|
634
|
+
let filled = Math.round(percent / 100 * width);
|
|
635
|
+
let empty = width - filled;
|
|
636
|
+
|
|
637
|
+
// Parse hex colors
|
|
638
|
+
let fromRgb = hexToRgb(from);
|
|
639
|
+
let toRgb = hexToRgb(to);
|
|
640
|
+
|
|
641
|
+
// Build gradient
|
|
642
|
+
let bar = '';
|
|
643
|
+
for (let i = 0; i < filled; i++) {
|
|
644
|
+
let ratio = filled > 1 ? i / (filled - 1) : 0;
|
|
645
|
+
let r = Math.round(fromRgb.r + (toRgb.r - fromRgb.r) * ratio);
|
|
646
|
+
let g = Math.round(fromRgb.g + (toRgb.g - fromRgb.g) * ratio);
|
|
647
|
+
let b = Math.round(fromRgb.b + (toRgb.b - fromRgb.b) * ratio);
|
|
648
|
+
bar += colors.rgb(r, g, b)('█');
|
|
649
|
+
}
|
|
650
|
+
bar += colors.dim('░'.repeat(empty));
|
|
651
|
+
return bar;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Parse hex color to RGB object
|
|
656
|
+
* @param {string} hex - Hex color string (e.g., '#FF0000' or 'FF0000')
|
|
657
|
+
* @returns {{r: number, g: number, b: number}} RGB values
|
|
658
|
+
*/
|
|
659
|
+
function hexToRgb(hex) {
|
|
660
|
+
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
661
|
+
return result ? {
|
|
662
|
+
r: parseInt(result[1], 16),
|
|
663
|
+
g: parseInt(result[2], 16),
|
|
664
|
+
b: parseInt(result[3], 16)
|
|
665
|
+
} : {
|
|
666
|
+
r: 128,
|
|
667
|
+
g: 128,
|
|
668
|
+
b: 128
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Create a colored badge/pill for status indicators
|
|
674
|
+
* Uses Observatory semantic colors for consistent meaning
|
|
675
|
+
* @param {string} text - Badge text
|
|
676
|
+
* @param {string} [type='info'] - Badge type: 'success', 'warning', 'error', 'info'
|
|
677
|
+
* @returns {string} Formatted badge string
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* badge('READY', 'success') // Success green background
|
|
681
|
+
* badge('FAIL', 'error') // Danger red background
|
|
682
|
+
* badge('SYNC', 'warning') // Warning amber background
|
|
683
|
+
*/
|
|
684
|
+
export function badge(text, type = 'info') {
|
|
685
|
+
if (config.json || config.silent) return text;
|
|
686
|
+
let bgColor;
|
|
687
|
+
let fgColor = colors.black;
|
|
688
|
+
switch (type) {
|
|
689
|
+
case 'success':
|
|
690
|
+
bgColor = colors.brand.bgSuccess;
|
|
691
|
+
break;
|
|
692
|
+
case 'warning':
|
|
693
|
+
bgColor = colors.brand.bgWarning;
|
|
694
|
+
break;
|
|
695
|
+
case 'error':
|
|
696
|
+
bgColor = colors.brand.bgDanger;
|
|
697
|
+
fgColor = colors.white;
|
|
698
|
+
break;
|
|
699
|
+
default:
|
|
700
|
+
bgColor = colors.brand.bgInfo;
|
|
701
|
+
fgColor = colors.white;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
return bgColor(fgColor(` ${text} `));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Create a colored status dot
|
|
709
|
+
* Uses Observatory semantic colors for consistent meaning
|
|
710
|
+
* @param {string} [status='info'] - Status type: 'success', 'warning', 'error', 'info'
|
|
711
|
+
* @returns {string} Colored dot character
|
|
712
|
+
*/
|
|
713
|
+
export function statusDot(status = 'info') {
|
|
714
|
+
if (config.json || config.silent) return '●';
|
|
715
|
+
switch (status) {
|
|
716
|
+
case 'success':
|
|
717
|
+
return colors.brand.success('●');
|
|
718
|
+
case 'warning':
|
|
719
|
+
return colors.brand.warning('●');
|
|
720
|
+
case 'error':
|
|
721
|
+
return colors.brand.danger('●');
|
|
722
|
+
default:
|
|
723
|
+
return colors.brand.info('●');
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Format a link with styling
|
|
729
|
+
* @param {string} label - Link label (not currently used, for future OSC 8 support)
|
|
730
|
+
* @param {string} url - URL to display
|
|
731
|
+
* @returns {string} Styled URL string
|
|
732
|
+
*/
|
|
733
|
+
export function link(_label, url) {
|
|
734
|
+
if (config.json) return url;
|
|
735
|
+
if (config.silent) return '';
|
|
736
|
+
|
|
737
|
+
// Style the URL with underline and info blue
|
|
738
|
+
return colors.brand.info(colors.underline(url));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Print a labeled value with consistent formatting
|
|
743
|
+
* Useful for displaying key-value pairs in verbose output
|
|
744
|
+
* @param {string} label - The label (will be styled as tertiary text)
|
|
745
|
+
* @param {string} value - The value to display
|
|
746
|
+
* @param {Object} [options] - Display options
|
|
747
|
+
* @param {number} [options.indent=2] - Number of spaces to indent
|
|
748
|
+
*/
|
|
749
|
+
export function labelValue(label, value, options = {}) {
|
|
750
|
+
if (config.json || config.silent) return;
|
|
751
|
+
let {
|
|
752
|
+
indent = 2
|
|
753
|
+
} = options;
|
|
754
|
+
let padding = ' '.repeat(indent);
|
|
755
|
+
console.log(`${padding}${colors.brand.textTertiary(`${label}:`)} ${value}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Print a hint/tip with muted styling
|
|
760
|
+
* @param {string} text - The hint text
|
|
761
|
+
* @param {Object} [options] - Display options
|
|
762
|
+
* @param {number} [options.indent=2] - Number of spaces to indent
|
|
763
|
+
*/
|
|
764
|
+
export function hint(text, options = {}) {
|
|
765
|
+
if (config.json || config.silent) return;
|
|
766
|
+
let {
|
|
767
|
+
indent = 2
|
|
768
|
+
} = options;
|
|
769
|
+
let padding = ' '.repeat(indent);
|
|
770
|
+
console.log(`${padding}${colors.brand.textMuted(text)}`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Print a list of items with bullet points
|
|
775
|
+
* @param {string[]} items - Array of items to display
|
|
776
|
+
* @param {Object} [options] - Display options
|
|
777
|
+
* @param {number} [options.indent=2] - Number of spaces to indent
|
|
778
|
+
* @param {string} [options.bullet='•'] - Bullet character
|
|
779
|
+
* @param {string} [options.style='default'] - Style: 'default', 'success', 'warning', 'error'
|
|
780
|
+
*/
|
|
781
|
+
export function list(items, options = {}) {
|
|
782
|
+
if (config.json || config.silent) return;
|
|
783
|
+
let {
|
|
784
|
+
indent = 2,
|
|
785
|
+
bullet = '•',
|
|
786
|
+
style = 'default'
|
|
787
|
+
} = options;
|
|
788
|
+
let padding = ' '.repeat(indent);
|
|
789
|
+
let bulletColor;
|
|
790
|
+
switch (style) {
|
|
791
|
+
case 'success':
|
|
792
|
+
bulletColor = colors.brand.success;
|
|
793
|
+
bullet = '✓';
|
|
794
|
+
break;
|
|
795
|
+
case 'warning':
|
|
796
|
+
bulletColor = colors.brand.warning;
|
|
797
|
+
bullet = '!';
|
|
798
|
+
break;
|
|
799
|
+
case 'error':
|
|
800
|
+
bulletColor = colors.brand.danger;
|
|
801
|
+
bullet = '✗';
|
|
802
|
+
break;
|
|
803
|
+
default:
|
|
804
|
+
bulletColor = colors.brand.textMuted;
|
|
805
|
+
}
|
|
806
|
+
for (let item of items) {
|
|
807
|
+
console.log(`${padding}${bulletColor(bullet)} ${item}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Print a success/completion message with checkmark
|
|
813
|
+
* @param {string} message - The success message
|
|
814
|
+
* @param {Object} [options] - Display options
|
|
815
|
+
* @param {string} [options.detail] - Optional detail text (shown dimmed)
|
|
816
|
+
*/
|
|
817
|
+
export function complete(message, options = {}) {
|
|
818
|
+
if (config.silent) return;
|
|
819
|
+
let {
|
|
820
|
+
detail
|
|
821
|
+
} = options;
|
|
822
|
+
let detailStr = detail ? ` ${colors.brand.textMuted(detail)}` : '';
|
|
823
|
+
if (config.json) {
|
|
824
|
+
console.log(JSON.stringify({
|
|
825
|
+
status: 'complete',
|
|
826
|
+
message,
|
|
827
|
+
detail
|
|
828
|
+
}));
|
|
829
|
+
} else {
|
|
830
|
+
console.log(` ${colors.brand.success('✓')} ${message}${detailStr}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Print a simple key-value table
|
|
836
|
+
* @param {Object} data - Object with key-value pairs to display
|
|
837
|
+
* @param {Object} [options] - Display options
|
|
838
|
+
* @param {number} [options.indent=2] - Number of spaces to indent
|
|
839
|
+
* @param {number} [options.keyWidth=12] - Width for key column
|
|
840
|
+
*/
|
|
841
|
+
export function keyValue(data, options = {}) {
|
|
842
|
+
if (config.json || config.silent) return;
|
|
843
|
+
let {
|
|
844
|
+
indent = 2,
|
|
845
|
+
keyWidth = 12
|
|
846
|
+
} = options;
|
|
847
|
+
let padding = ' '.repeat(indent);
|
|
848
|
+
for (let [key, value] of Object.entries(data)) {
|
|
849
|
+
if (value === undefined || value === null) continue;
|
|
850
|
+
let paddedKey = key.padEnd(keyWidth);
|
|
851
|
+
console.log(`${padding}${colors.brand.textTertiary(paddedKey)} ${value}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Print a divider line
|
|
857
|
+
* @param {Object} [options] - Display options
|
|
858
|
+
* @param {number} [options.width=40] - Width of the divider
|
|
859
|
+
* @param {string} [options.char='─'] - Character to use for divider
|
|
860
|
+
*/
|
|
861
|
+
export function divider(options = {}) {
|
|
862
|
+
if (config.json || config.silent) return;
|
|
863
|
+
let {
|
|
864
|
+
width = 40,
|
|
865
|
+
char = '─'
|
|
866
|
+
} = options;
|
|
867
|
+
console.log(colors.brand.textMuted(char.repeat(width)));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Create a styled box around content
|
|
872
|
+
* Uses Unicode box-drawing characters for clean terminal rendering
|
|
873
|
+
* Features brand-colored borders and titles
|
|
874
|
+
*
|
|
875
|
+
* @param {string|string[]} content - Content to display (string or array of lines)
|
|
876
|
+
* @param {Object} [options] - Box options
|
|
877
|
+
* @param {string} [options.title] - Optional title for the box
|
|
878
|
+
* @param {number} [options.padding=1] - Horizontal padding inside the box
|
|
879
|
+
* @param {Function} [options.borderColor] - Color function for the border
|
|
880
|
+
* @param {string} [options.style='default'] - Box style: 'default', 'branded'
|
|
881
|
+
* @returns {string} Formatted box string
|
|
882
|
+
*
|
|
883
|
+
* @example
|
|
884
|
+
* box('Dashboard: http://localhost:47392')
|
|
885
|
+
* // ╭───────────────────────────────────────╮
|
|
886
|
+
* // │ Dashboard: http://localhost:47392 │
|
|
887
|
+
* // ╰───────────────────────────────────────╯
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* box(['Line 1', 'Line 2'], { title: 'Info', style: 'branded' })
|
|
891
|
+
*/
|
|
892
|
+
export function box(content, options = {}) {
|
|
893
|
+
if (config.json || config.silent) return '';
|
|
894
|
+
let {
|
|
895
|
+
title = null,
|
|
896
|
+
padding = 1,
|
|
897
|
+
borderColor = null,
|
|
898
|
+
style = 'default'
|
|
899
|
+
} = options;
|
|
900
|
+
let lines = Array.isArray(content) ? content : [content];
|
|
901
|
+
|
|
902
|
+
// Strip ANSI codes for width calculation
|
|
903
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape sequence matching
|
|
904
|
+
let stripAnsi = str => str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
905
|
+
|
|
906
|
+
// Calculate max width (content + padding on each side)
|
|
907
|
+
let maxContentWidth = Math.max(...lines.map(line => stripAnsi(line).length));
|
|
908
|
+
let innerWidth = maxContentWidth + padding * 2;
|
|
909
|
+
|
|
910
|
+
// If title provided, ensure box is wide enough
|
|
911
|
+
if (title) {
|
|
912
|
+
let titleWidth = stripAnsi(title).length + 4; // " title " with spaces
|
|
913
|
+
innerWidth = Math.max(innerWidth, titleWidth);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Border styling - use Observatory amber for 'branded' style
|
|
917
|
+
let border = borderColor || (style === 'branded' ? colors.brand.amber : colors.dim);
|
|
918
|
+
let titleColor = style === 'branded' ? colors.bold : s => s;
|
|
919
|
+
|
|
920
|
+
// Build the box
|
|
921
|
+
let result = [];
|
|
922
|
+
|
|
923
|
+
// Top border with optional title
|
|
924
|
+
if (title) {
|
|
925
|
+
let titleStr = ` ${titleColor(title)} `;
|
|
926
|
+
let leftDash = '─'.repeat(1);
|
|
927
|
+
let rightDash = '─'.repeat(innerWidth - stripAnsi(title).length - 3);
|
|
928
|
+
result.push(border(`╭${leftDash}`) + titleStr + border(`${rightDash}╮`));
|
|
929
|
+
} else {
|
|
930
|
+
result.push(border(`╭${'─'.repeat(innerWidth)}╮`));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Content lines
|
|
934
|
+
let paddingStr = ' '.repeat(padding);
|
|
935
|
+
for (let line of lines) {
|
|
936
|
+
let lineWidth = stripAnsi(line).length;
|
|
937
|
+
let rightPad = ' '.repeat(innerWidth - lineWidth - padding * 2);
|
|
938
|
+
result.push(border('│') + paddingStr + line + rightPad + paddingStr + border('│'));
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Bottom border
|
|
942
|
+
result.push(border(`╰${'─'.repeat(innerWidth)}╯`));
|
|
943
|
+
return result.join('\n');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Print a box to stderr
|
|
948
|
+
* Convenience wrapper around box() that prints directly
|
|
949
|
+
*
|
|
950
|
+
* @param {string|string[]} content - Content to display
|
|
951
|
+
* @param {Object} [options] - Box options (see box())
|
|
952
|
+
*/
|
|
953
|
+
export function printBox(content, options = {}) {
|
|
954
|
+
if (config.json || config.silent) return;
|
|
955
|
+
let boxStr = box(content, options);
|
|
956
|
+
if (boxStr) {
|
|
957
|
+
console.error(boxStr);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
526
961
|
// ============================================================================
|
|
527
962
|
// Cleanup
|
|
528
963
|
// ============================================================================
|
|
@@ -542,7 +977,7 @@ export function reset() {
|
|
|
542
977
|
stopSpinner();
|
|
543
978
|
config.json = false;
|
|
544
979
|
config.logLevel = null;
|
|
545
|
-
config.color =
|
|
980
|
+
config.color = undefined; // Reset to auto-detect
|
|
546
981
|
config.silent = false;
|
|
547
982
|
config.logFile = null;
|
|
548
983
|
colors = createColors({
|