@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.
Files changed (36) hide show
  1. package/dist/cli.js +177 -2
  2. package/dist/client/index.js +144 -77
  3. package/dist/commands/doctor.js +118 -33
  4. package/dist/commands/finalize.js +8 -3
  5. package/dist/commands/init.js +13 -18
  6. package/dist/commands/login.js +42 -49
  7. package/dist/commands/logout.js +13 -5
  8. package/dist/commands/project.js +95 -67
  9. package/dist/commands/run.js +32 -6
  10. package/dist/commands/status.js +81 -50
  11. package/dist/commands/tdd-daemon.js +61 -32
  12. package/dist/commands/tdd.js +4 -25
  13. package/dist/commands/upload.js +18 -9
  14. package/dist/commands/whoami.js +40 -38
  15. package/dist/reporter/reporter-bundle.css +1 -1
  16. package/dist/reporter/reporter-bundle.iife.js +11 -11
  17. package/dist/server/handlers/tdd-handler.js +113 -7
  18. package/dist/server/http-server.js +9 -3
  19. package/dist/server/routers/baseline.js +58 -0
  20. package/dist/server/routers/dashboard.js +10 -6
  21. package/dist/server/routers/screenshot.js +32 -0
  22. package/dist/server-manager/core.js +5 -2
  23. package/dist/server-manager/operations.js +2 -1
  24. package/dist/tdd/tdd-service.js +190 -126
  25. package/dist/types/client.d.ts +25 -2
  26. package/dist/utils/colors.js +187 -39
  27. package/dist/utils/config-loader.js +3 -6
  28. package/dist/utils/context.js +228 -0
  29. package/dist/utils/output.js +449 -14
  30. package/docs/api-reference.md +173 -8
  31. package/docs/tui-elements.md +560 -0
  32. package/package.json +12 -7
  33. package/dist/report-generator/core.js +0 -315
  34. package/dist/report-generator/index.js +0 -8
  35. package/dist/report-generator/operations.js +0 -196
  36. package/dist/services/static-report-generator.js +0 -65
@@ -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: true,
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 (e.g., "vizzly · tdd · local")
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
- const parts = ['vizzly', command];
152
- if (mode) parts.push(mode);
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(colors.dim(parts.join(' · ')));
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
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
357
+
358
+ // Braille dots spinner - smooth animation
359
+ let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
343
360
  let i = 0;
344
361
  spinnerInterval = setInterval(() => {
345
- const frame = frames[i++ % frames.length];
346
- const line = `${colors.cyan(frame)} ${spinnerMessage}`;
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
- const progressText = total > 0 ? ` (${current}/${total})` : '';
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: " server listening on :47392"
520
+ // Component-based format with distinctive colors
521
+ // " server ready on :47392"
476
522
  let paddedComponent = component.padEnd(8);
477
- console.error(` ${colors.cyan(paddedComponent)} ${message}${dataStr}`);
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('•')} ${colors.dim(message)}${dataStr}`);
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 = true;
980
+ config.color = undefined; // Reset to auto-detect
546
981
  config.silent = false;
547
982
  config.logFile = null;
548
983
  colors = createColors({