@vizzly-testing/cli 0.13.0 → 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 +28 -63
  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
@@ -1,54 +1,16 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import { writeFileSync, readFileSync, existsSync } from 'fs';
3
3
  import { join, resolve } from 'path';
4
+ import honeydiff from '@vizzly-testing/honeydiff';
4
5
  import { createServiceLogger } from '../../utils/logger-factory.js';
5
6
  import { TddService } from '../../services/tdd-service.js';
6
7
  import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
7
8
  import { detectImageInputType } from '../../utils/image-input-detector.js';
9
+ let {
10
+ getDimensionsSync
11
+ } = honeydiff;
8
12
  const logger = createServiceLogger('TDD-HANDLER');
9
13
 
10
- /**
11
- * Detect PNG dimensions by reading the IHDR chunk header
12
- * PNG spec (ISO/IEC 15948:2004) guarantees width/height at bytes 16-23
13
- * @param {Buffer} buffer - PNG image buffer
14
- * @returns {{ width: number, height: number } | null} Dimensions or null if not a valid PNG
15
- */
16
- const detectPNGDimensions = buffer => {
17
- // Full PNG signature (8 bytes): 89 50 4E 47 0D 0A 1A 0A
18
- const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
19
-
20
- // Need at least 24 bytes (8 signature + 4 length + 4 type + 8 width/height)
21
- if (!buffer || buffer.length < 24) {
22
- return null;
23
- }
24
-
25
- // Validate full 8-byte PNG signature
26
- for (let i = 0; i < PNG_SIGNATURE.length; i++) {
27
- if (buffer[i] !== PNG_SIGNATURE[i]) {
28
- return null;
29
- }
30
- }
31
-
32
- // Validate IHDR chunk type at bytes 12-15 (should be 'IHDR')
33
- // 0x49484452 = 'IHDR' in ASCII
34
- if (buffer[12] !== 0x49 || buffer[13] !== 0x48 || buffer[14] !== 0x44 || buffer[15] !== 0x52) {
35
- return null;
36
- }
37
-
38
- // Read width and height from IHDR chunk (guaranteed positions per PNG spec)
39
- const width = buffer.readUInt32BE(16); // Bytes 16-19
40
- const height = buffer.readUInt32BE(20); // Bytes 20-23
41
-
42
- // Sanity check: dimensions should be positive and reasonable
43
- if (width <= 0 || height <= 0 || width > 65535 || height > 65535) {
44
- return null;
45
- }
46
- return {
47
- width,
48
- height
49
- };
50
- };
51
-
52
14
  /**
53
15
  * Group comparisons by screenshot name with variant structure
54
16
  * Matches cloud product's grouping logic from comparison.js
@@ -349,18 +311,19 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
349
311
  };
350
312
  }
351
313
 
352
- // Auto-detect image dimensions from PNG header if viewport not provided
353
- // This matches cloud API behavior but without requiring Sharp
314
+ // Auto-detect image dimensions if viewport not provided
354
315
  if (!extractedProperties.viewport_width || !extractedProperties.viewport_height) {
355
- const dimensions = detectPNGDimensions(imageBuffer);
356
- if (dimensions) {
316
+ try {
317
+ const dimensions = getDimensionsSync(imageBuffer);
357
318
  if (!extractedProperties.viewport_width) {
358
319
  extractedProperties.viewport_width = dimensions.width;
359
320
  }
360
321
  if (!extractedProperties.viewport_height) {
361
322
  extractedProperties.viewport_height = dimensions.height;
362
323
  }
363
- logger.debug(`Auto-detected dimensions from PNG: ${dimensions.width}x${dimensions.height}`);
324
+ logger.debug(`Auto-detected dimensions: ${dimensions.width}x${dimensions.height}`);
325
+ } catch (err) {
326
+ logger.debug(`Failed to auto-detect dimensions: ${err.message}`);
364
327
  }
365
328
  }
366
329
 
@@ -464,24 +427,24 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
464
427
  };
465
428
  const acceptBaseline = async comparisonId => {
466
429
  try {
467
- // Use TDD service to accept the baseline
468
- const result = await tddService.acceptBaseline(comparisonId);
469
-
470
- // Read current report data and update the comparison status
430
+ // Read current report data to get the comparison
471
431
  const reportData = readReportData();
472
432
  const comparison = reportData.comparisons.find(c => c.id === comparisonId);
473
- if (comparison) {
474
- // Update the comparison to passed status
475
- const updatedComparison = {
476
- ...comparison,
477
- status: 'passed',
478
- diffPercentage: 0,
479
- diff: null
480
- };
481
- updateComparison(updatedComparison);
482
- } else {
483
- logger.error(`Comparison not found in report data for ID: ${comparisonId}`);
433
+ if (!comparison) {
434
+ throw new Error(`Comparison not found with ID: ${comparisonId}`);
484
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);
485
448
  logger.info(`Baseline accepted for comparison ${comparisonId}`);
486
449
  return result;
487
450
  } catch (error) {
@@ -498,7 +461,9 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
498
461
  // Accept all failed or new comparisons
499
462
  for (const comparison of reportData.comparisons) {
500
463
  if (comparison.status === 'failed' || comparison.status === 'new') {
501
- 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);
502
467
 
503
468
  // Update the comparison to passed status
504
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'