@vizzly-testing/cli 0.13.1 → 0.13.2

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 (40) hide show
  1. package/README.md +552 -88
  2. package/claude-plugin/.claude-plugin/README.md +4 -0
  3. package/claude-plugin/.mcp.json +4 -0
  4. package/claude-plugin/CHANGELOG.md +27 -0
  5. package/claude-plugin/mcp/vizzly-docs-server/README.md +95 -0
  6. package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +110 -0
  7. package/claude-plugin/mcp/vizzly-docs-server/index.js +283 -0
  8. package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +26 -10
  9. package/claude-plugin/mcp/vizzly-server/index.js +14 -1
  10. package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +61 -28
  11. package/dist/cli.js +4 -4
  12. package/dist/commands/run.js +1 -1
  13. package/dist/commands/tdd-daemon.js +54 -8
  14. package/dist/commands/tdd.js +8 -8
  15. package/dist/container/index.js +34 -3
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +29 -59
  18. package/dist/server/handlers/tdd-handler.js +18 -16
  19. package/dist/server/http-server.js +473 -4
  20. package/dist/services/config-service.js +371 -0
  21. package/dist/services/project-service.js +245 -0
  22. package/dist/services/server-manager.js +4 -5
  23. package/dist/services/static-report-generator.js +208 -0
  24. package/dist/services/tdd-service.js +14 -6
  25. package/dist/types/reporter/src/components/ui/form-field.d.ts +16 -0
  26. package/dist/types/reporter/src/components/views/projects-view.d.ts +1 -0
  27. package/dist/types/reporter/src/components/views/settings-view.d.ts +1 -0
  28. package/dist/types/reporter/src/hooks/use-auth.d.ts +10 -0
  29. package/dist/types/reporter/src/hooks/use-config.d.ts +9 -0
  30. package/dist/types/reporter/src/hooks/use-projects.d.ts +10 -0
  31. package/dist/types/reporter/src/services/api-client.d.ts +7 -0
  32. package/dist/types/server/http-server.d.ts +1 -1
  33. package/dist/types/services/config-service.d.ts +98 -0
  34. package/dist/types/services/project-service.d.ts +103 -0
  35. package/dist/types/services/server-manager.d.ts +2 -1
  36. package/dist/types/services/static-report-generator.d.ts +25 -0
  37. package/dist/types/services/tdd-service.d.ts +2 -2
  38. package/dist/utils/console-ui.js +26 -2
  39. package/docs/tdd-mode.md +31 -15
  40. package/package.json +4 -4
@@ -427,24 +427,24 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
427
427
  };
428
428
  const acceptBaseline = async comparisonId => {
429
429
  try {
430
- // Use TDD service to accept the baseline
431
- const result = await tddService.acceptBaseline(comparisonId);
432
-
433
- // Read current report data and update the comparison status
430
+ // Read current report data to get the comparison
434
431
  const reportData = readReportData();
435
432
  const comparison = reportData.comparisons.find(c => c.id === comparisonId);
436
- if (comparison) {
437
- // Update the comparison to passed status
438
- const updatedComparison = {
439
- ...comparison,
440
- status: 'passed',
441
- diffPercentage: 0,
442
- diff: null
443
- };
444
- updateComparison(updatedComparison);
445
- } else {
446
- logger.error(`Comparison not found in report data for ID: ${comparisonId}`);
433
+ if (!comparison) {
434
+ throw new Error(`Comparison not found with ID: ${comparisonId}`);
447
435
  }
436
+
437
+ // Pass the comparison object to tddService instead of just the ID
438
+ const result = await tddService.acceptBaseline(comparison);
439
+
440
+ // Update the comparison to passed status
441
+ const updatedComparison = {
442
+ ...comparison,
443
+ status: 'passed',
444
+ diffPercentage: 0,
445
+ diff: null
446
+ };
447
+ updateComparison(updatedComparison);
448
448
  logger.info(`Baseline accepted for comparison ${comparisonId}`);
449
449
  return result;
450
450
  } catch (error) {
@@ -461,7 +461,9 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
461
461
  // Accept all failed or new comparisons
462
462
  for (const comparison of reportData.comparisons) {
463
463
  if (comparison.status === 'failed' || comparison.status === 'new') {
464
- await tddService.acceptBaseline(comparison.id);
464
+ // Pass the comparison object directly instead of just the ID
465
+ // This allows tddService to work with comparisons from report-data.json
466
+ await tddService.acceptBaseline(comparison);
465
467
 
466
468
  // Update the comparison to passed status
467
469
  updateComparison({
@@ -7,8 +7,13 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
8
  const PROJECT_ROOT = join(__dirname, '..', '..');
9
9
  const logger = createServiceLogger('HTTP-SERVER');
10
- export const createHttpServer = (port, screenshotHandler) => {
10
+ export const createHttpServer = (port, screenshotHandler, services = {}) => {
11
11
  let server = null;
12
+
13
+ // Extract services for config/auth/project management
14
+ let configService = services.configService;
15
+ let authService = services.authService;
16
+ let projectService = services.projectService;
12
17
  const parseRequestBody = req => {
13
18
  return new Promise((resolve, reject) => {
14
19
  let body = '';
@@ -80,7 +85,7 @@ export const createHttpServer = (port, screenshotHandler) => {
80
85
  }
81
86
 
82
87
  // Serve the main React app for all non-API routes
83
- if (req.method === 'GET' && (parsedUrl.pathname === '/' || parsedUrl.pathname === '/dashboard' || parsedUrl.pathname === '/stats')) {
88
+ if (req.method === 'GET' && (parsedUrl.pathname === '/' || parsedUrl.pathname === '/dashboard' || parsedUrl.pathname === '/stats' || parsedUrl.pathname === '/settings' || parsedUrl.pathname === '/projects')) {
84
89
  // Serve React-powered dashboard
85
90
  const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
86
91
 
@@ -98,7 +103,7 @@ export const createHttpServer = (port, screenshotHandler) => {
98
103
  <!DOCTYPE html>
99
104
  <html>
100
105
  <head>
101
- <title>Vizzly TDD Dashboard</title>
106
+ <title>Vizzly Dev Dashboard</title>
102
107
  <meta charset="utf-8">
103
108
  <meta name="viewport" content="width=device-width, initial-scale=1">
104
109
  <link rel="stylesheet" href="/reporter-bundle.css">
@@ -108,7 +113,7 @@ export const createHttpServer = (port, screenshotHandler) => {
108
113
  <div class="reporter-loading">
109
114
  <div>
110
115
  <div class="spinner"></div>
111
- <p>Loading Vizzly TDD Dashboard...</p>
116
+ <p>Loading Vizzly Dev Dashboard...</p>
112
117
  </div>
113
118
  </div>
114
119
  </div>
@@ -401,6 +406,470 @@ export const createHttpServer = (port, screenshotHandler) => {
401
406
  }
402
407
  return;
403
408
  }
409
+
410
+ // ===== CONFIG MANAGEMENT ENDPOINTS =====
411
+
412
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/config') {
413
+ // Get merged config with sources
414
+ if (!configService) {
415
+ res.statusCode = 503;
416
+ res.end(JSON.stringify({
417
+ error: 'Config service not available'
418
+ }));
419
+ return;
420
+ }
421
+ try {
422
+ const configData = await configService.getConfig('merged');
423
+ res.statusCode = 200;
424
+ res.end(JSON.stringify(configData));
425
+ } catch (error) {
426
+ logger.error('Error fetching config:', error);
427
+ res.statusCode = 500;
428
+ res.end(JSON.stringify({
429
+ error: error.message
430
+ }));
431
+ }
432
+ return;
433
+ }
434
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/config/project') {
435
+ // Get project-level config
436
+ if (!configService) {
437
+ res.statusCode = 503;
438
+ res.end(JSON.stringify({
439
+ error: 'Config service not available'
440
+ }));
441
+ return;
442
+ }
443
+ try {
444
+ const configData = await configService.getConfig('project');
445
+ res.statusCode = 200;
446
+ res.end(JSON.stringify(configData));
447
+ } catch (error) {
448
+ logger.error('Error fetching project config:', error);
449
+ res.statusCode = 500;
450
+ res.end(JSON.stringify({
451
+ error: error.message
452
+ }));
453
+ }
454
+ return;
455
+ }
456
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/config/global') {
457
+ // Get global config
458
+ if (!configService) {
459
+ res.statusCode = 503;
460
+ res.end(JSON.stringify({
461
+ error: 'Config service not available'
462
+ }));
463
+ return;
464
+ }
465
+ try {
466
+ const configData = await configService.getConfig('global');
467
+ res.statusCode = 200;
468
+ res.end(JSON.stringify(configData));
469
+ } catch (error) {
470
+ logger.error('Error fetching global config:', error);
471
+ res.statusCode = 500;
472
+ res.end(JSON.stringify({
473
+ error: error.message
474
+ }));
475
+ }
476
+ return;
477
+ }
478
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/config/project') {
479
+ // Update project config
480
+ if (!configService) {
481
+ res.statusCode = 503;
482
+ res.end(JSON.stringify({
483
+ error: 'Config service not available'
484
+ }));
485
+ return;
486
+ }
487
+ try {
488
+ const body = await parseRequestBody(req);
489
+ const result = await configService.updateConfig('project', body);
490
+ res.statusCode = 200;
491
+ res.end(JSON.stringify({
492
+ success: true,
493
+ ...result
494
+ }));
495
+ } catch (error) {
496
+ logger.error('Error updating project config:', error);
497
+ res.statusCode = 500;
498
+ res.end(JSON.stringify({
499
+ error: error.message
500
+ }));
501
+ }
502
+ return;
503
+ }
504
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/config/global') {
505
+ // Update global config
506
+ if (!configService) {
507
+ res.statusCode = 503;
508
+ res.end(JSON.stringify({
509
+ error: 'Config service not available'
510
+ }));
511
+ return;
512
+ }
513
+ try {
514
+ const body = await parseRequestBody(req);
515
+ const result = await configService.updateConfig('global', body);
516
+ res.statusCode = 200;
517
+ res.end(JSON.stringify({
518
+ success: true,
519
+ ...result
520
+ }));
521
+ } catch (error) {
522
+ logger.error('Error updating global config:', error);
523
+ res.statusCode = 500;
524
+ res.end(JSON.stringify({
525
+ error: error.message
526
+ }));
527
+ }
528
+ return;
529
+ }
530
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/config/validate') {
531
+ // Validate config
532
+ if (!configService) {
533
+ res.statusCode = 503;
534
+ res.end(JSON.stringify({
535
+ error: 'Config service not available'
536
+ }));
537
+ return;
538
+ }
539
+ try {
540
+ const body = await parseRequestBody(req);
541
+ const result = await configService.validateConfig(body);
542
+ res.statusCode = 200;
543
+ res.end(JSON.stringify(result));
544
+ } catch (error) {
545
+ logger.error('Error validating config:', error);
546
+ res.statusCode = 500;
547
+ res.end(JSON.stringify({
548
+ error: error.message
549
+ }));
550
+ }
551
+ return;
552
+ }
553
+
554
+ // ===== AUTH ENDPOINTS =====
555
+
556
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/auth/status') {
557
+ // Get auth status and user info
558
+ if (!authService) {
559
+ res.statusCode = 503;
560
+ res.end(JSON.stringify({
561
+ error: 'Auth service not available'
562
+ }));
563
+ return;
564
+ }
565
+ try {
566
+ const isAuthenticated = await authService.isAuthenticated();
567
+ let user = null;
568
+ if (isAuthenticated) {
569
+ const whoami = await authService.whoami();
570
+ user = whoami.user;
571
+ }
572
+ res.statusCode = 200;
573
+ res.end(JSON.stringify({
574
+ authenticated: isAuthenticated,
575
+ user
576
+ }));
577
+ } catch (error) {
578
+ logger.error('Error getting auth status:', error);
579
+ res.statusCode = 200;
580
+ res.end(JSON.stringify({
581
+ authenticated: false,
582
+ user: null
583
+ }));
584
+ }
585
+ return;
586
+ }
587
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/login') {
588
+ // Initiate device flow login
589
+ if (!authService) {
590
+ res.statusCode = 503;
591
+ res.end(JSON.stringify({
592
+ error: 'Auth service not available'
593
+ }));
594
+ return;
595
+ }
596
+ try {
597
+ const deviceFlow = await authService.initiateDeviceFlow();
598
+
599
+ // Transform snake_case to camelCase for frontend
600
+ const response = {
601
+ deviceCode: deviceFlow.device_code,
602
+ userCode: deviceFlow.user_code,
603
+ verificationUri: deviceFlow.verification_uri,
604
+ verificationUriComplete: deviceFlow.verification_uri_complete,
605
+ expiresIn: deviceFlow.expires_in,
606
+ interval: deviceFlow.interval
607
+ };
608
+ res.statusCode = 200;
609
+ res.end(JSON.stringify(response));
610
+ } catch (error) {
611
+ logger.error('Error initiating device flow:', error);
612
+ res.statusCode = 500;
613
+ res.end(JSON.stringify({
614
+ error: error.message
615
+ }));
616
+ }
617
+ return;
618
+ }
619
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/poll') {
620
+ // Poll device authorization status
621
+ if (!authService) {
622
+ res.statusCode = 503;
623
+ res.end(JSON.stringify({
624
+ error: 'Auth service not available'
625
+ }));
626
+ return;
627
+ }
628
+ try {
629
+ const body = await parseRequestBody(req);
630
+ const {
631
+ deviceCode
632
+ } = body;
633
+ if (!deviceCode) {
634
+ res.statusCode = 400;
635
+ res.end(JSON.stringify({
636
+ error: 'deviceCode is required'
637
+ }));
638
+ return;
639
+ }
640
+ let result;
641
+ try {
642
+ result = await authService.pollDeviceAuthorization(deviceCode);
643
+ } catch (error) {
644
+ // Handle "Authorization pending" as a valid response
645
+ if (error.message && error.message.includes('Authorization pending')) {
646
+ res.statusCode = 200;
647
+ res.end(JSON.stringify({
648
+ status: 'pending'
649
+ }));
650
+ return;
651
+ }
652
+ // Other errors are actual failures
653
+ throw error;
654
+ }
655
+
656
+ // Check if authorization is complete by looking for tokens
657
+ if (result.tokens && result.tokens.accessToken) {
658
+ // Handle both snake_case and camelCase for token data
659
+ let tokensData = result.tokens;
660
+ let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
661
+ let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : result.expires_at || result.expiresAt;
662
+ let tokens = {
663
+ accessToken: tokensData.accessToken || tokensData.access_token,
664
+ refreshToken: tokensData.refreshToken || tokensData.refresh_token,
665
+ expiresAt: tokenExpiresAt,
666
+ user: result.user
667
+ };
668
+ await authService.completeDeviceFlow(tokens);
669
+
670
+ // Return a simplified response to the client
671
+ res.statusCode = 200;
672
+ res.end(JSON.stringify({
673
+ status: 'complete',
674
+ user: result.user
675
+ }));
676
+ } else {
677
+ // Still pending or other status
678
+ res.statusCode = 200;
679
+ res.end(JSON.stringify({
680
+ status: 'pending'
681
+ }));
682
+ }
683
+ } catch (error) {
684
+ logger.error('Error polling device authorization:', error);
685
+ res.statusCode = 500;
686
+ res.end(JSON.stringify({
687
+ error: error.message
688
+ }));
689
+ }
690
+ return;
691
+ }
692
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/logout') {
693
+ // Logout user
694
+ if (!authService) {
695
+ res.statusCode = 503;
696
+ res.end(JSON.stringify({
697
+ error: 'Auth service not available'
698
+ }));
699
+ return;
700
+ }
701
+ try {
702
+ await authService.logout();
703
+ res.statusCode = 200;
704
+ res.end(JSON.stringify({
705
+ success: true,
706
+ message: 'Logged out successfully'
707
+ }));
708
+ } catch (error) {
709
+ logger.error('Error logging out:', error);
710
+ res.statusCode = 500;
711
+ res.end(JSON.stringify({
712
+ error: error.message
713
+ }));
714
+ }
715
+ return;
716
+ }
717
+
718
+ // ===== PROJECT MANAGEMENT ENDPOINTS =====
719
+
720
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/projects') {
721
+ // List all projects from API
722
+ if (!projectService) {
723
+ res.statusCode = 503;
724
+ res.end(JSON.stringify({
725
+ error: 'Project service not available'
726
+ }));
727
+ return;
728
+ }
729
+ try {
730
+ const projects = await projectService.listProjects();
731
+ res.statusCode = 200;
732
+ res.end(JSON.stringify({
733
+ projects
734
+ }));
735
+ } catch (error) {
736
+ logger.error('Error listing projects:', error);
737
+ res.statusCode = 500;
738
+ res.end(JSON.stringify({
739
+ error: error.message
740
+ }));
741
+ }
742
+ return;
743
+ }
744
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/projects/mappings') {
745
+ // List project directory mappings
746
+ if (!projectService) {
747
+ res.statusCode = 503;
748
+ res.end(JSON.stringify({
749
+ error: 'Project service not available'
750
+ }));
751
+ return;
752
+ }
753
+ try {
754
+ const mappings = await projectService.listMappings();
755
+ res.statusCode = 200;
756
+ res.end(JSON.stringify({
757
+ mappings
758
+ }));
759
+ } catch (error) {
760
+ logger.error('Error listing project mappings:', error);
761
+ res.statusCode = 500;
762
+ res.end(JSON.stringify({
763
+ error: error.message
764
+ }));
765
+ }
766
+ return;
767
+ }
768
+ if (req.method === 'POST' && parsedUrl.pathname === '/api/projects/mappings') {
769
+ // Create or update project mapping
770
+ if (!projectService) {
771
+ res.statusCode = 503;
772
+ res.end(JSON.stringify({
773
+ error: 'Project service not available'
774
+ }));
775
+ return;
776
+ }
777
+ try {
778
+ const body = await parseRequestBody(req);
779
+ const {
780
+ directory,
781
+ projectSlug,
782
+ organizationSlug,
783
+ token,
784
+ projectName
785
+ } = body;
786
+ const mapping = await projectService.createMapping(directory, {
787
+ projectSlug,
788
+ organizationSlug,
789
+ token,
790
+ projectName
791
+ });
792
+ res.statusCode = 200;
793
+ res.end(JSON.stringify({
794
+ success: true,
795
+ mapping
796
+ }));
797
+ } catch (error) {
798
+ logger.error('Error creating project mapping:', error);
799
+ res.statusCode = 500;
800
+ res.end(JSON.stringify({
801
+ error: error.message
802
+ }));
803
+ }
804
+ return;
805
+ }
806
+ if (req.method === 'DELETE' && parsedUrl.pathname.startsWith('/api/projects/mappings/')) {
807
+ // Delete project mapping
808
+ if (!projectService) {
809
+ res.statusCode = 503;
810
+ res.end(JSON.stringify({
811
+ error: 'Project service not available'
812
+ }));
813
+ return;
814
+ }
815
+ try {
816
+ const directory = decodeURIComponent(parsedUrl.pathname.replace('/api/projects/mappings/', ''));
817
+ await projectService.removeMapping(directory);
818
+ res.statusCode = 200;
819
+ res.end(JSON.stringify({
820
+ success: true,
821
+ message: 'Mapping deleted'
822
+ }));
823
+ } catch (error) {
824
+ logger.error('Error deleting project mapping:', error);
825
+ res.statusCode = 500;
826
+ res.end(JSON.stringify({
827
+ error: error.message
828
+ }));
829
+ }
830
+ return;
831
+ }
832
+ if (req.method === 'GET' && parsedUrl.pathname === '/api/builds/recent') {
833
+ // Get recent builds for current project
834
+ if (!projectService || !configService) {
835
+ res.statusCode = 503;
836
+ res.end(JSON.stringify({
837
+ error: 'Required services not available'
838
+ }));
839
+ return;
840
+ }
841
+ try {
842
+ const config = await configService.getConfig('merged');
843
+ const {
844
+ projectSlug,
845
+ organizationSlug
846
+ } = config.config;
847
+ if (!projectSlug || !organizationSlug) {
848
+ res.statusCode = 400;
849
+ res.end(JSON.stringify({
850
+ error: 'No project configured for this directory'
851
+ }));
852
+ return;
853
+ }
854
+ const limit = parseInt(parsedUrl.searchParams.get('limit') || '10', 10);
855
+ const branch = parsedUrl.searchParams.get('branch') || undefined;
856
+ const builds = await projectService.getRecentBuilds(projectSlug, organizationSlug, {
857
+ limit,
858
+ branch
859
+ });
860
+ res.statusCode = 200;
861
+ res.end(JSON.stringify({
862
+ builds
863
+ }));
864
+ } catch (error) {
865
+ logger.error('Error fetching recent builds:', error);
866
+ res.statusCode = 500;
867
+ res.end(JSON.stringify({
868
+ error: error.message
869
+ }));
870
+ }
871
+ return;
872
+ }
404
873
  res.statusCode = 404;
405
874
  res.end(JSON.stringify({
406
875
  error: 'Not found'