@vizzly-testing/cli 0.13.4 → 0.15.0

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 (143) hide show
  1. package/dist/cli.js +68 -68
  2. package/dist/commands/doctor.js +30 -34
  3. package/dist/commands/finalize.js +24 -23
  4. package/dist/commands/init.js +30 -28
  5. package/dist/commands/login.js +49 -55
  6. package/dist/commands/logout.js +14 -19
  7. package/dist/commands/project.js +83 -103
  8. package/dist/commands/run.js +77 -89
  9. package/dist/commands/status.js +48 -49
  10. package/dist/commands/tdd-daemon.js +90 -86
  11. package/dist/commands/tdd.js +59 -88
  12. package/dist/commands/upload.js +57 -57
  13. package/dist/commands/whoami.js +40 -45
  14. package/dist/index.js +2 -5
  15. package/dist/plugin-loader.js +15 -17
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +78 -32
  18. package/dist/sdk/index.js +36 -45
  19. package/dist/server/handlers/api-handler.js +14 -15
  20. package/dist/server/handlers/tdd-handler.js +34 -37
  21. package/dist/server/http-server.js +75 -869
  22. package/dist/server/middleware/cors.js +22 -0
  23. package/dist/server/middleware/json-parser.js +35 -0
  24. package/dist/server/middleware/response.js +79 -0
  25. package/dist/server/routers/assets.js +91 -0
  26. package/dist/server/routers/auth.js +144 -0
  27. package/dist/server/routers/baseline.js +163 -0
  28. package/dist/server/routers/cloud-proxy.js +146 -0
  29. package/dist/server/routers/config.js +126 -0
  30. package/dist/server/routers/dashboard.js +130 -0
  31. package/dist/server/routers/health.js +61 -0
  32. package/dist/server/routers/projects.js +168 -0
  33. package/dist/server/routers/screenshot.js +86 -0
  34. package/dist/services/auth-service.js +1 -1
  35. package/dist/services/build-manager.js +13 -40
  36. package/dist/services/config-service.js +2 -4
  37. package/dist/services/html-report-generator.js +6 -5
  38. package/dist/services/index.js +64 -0
  39. package/dist/services/project-service.js +121 -40
  40. package/dist/services/screenshot-server.js +9 -9
  41. package/dist/services/server-manager.js +11 -18
  42. package/dist/services/static-report-generator.js +3 -4
  43. package/dist/services/tdd-service.js +246 -103
  44. package/dist/services/test-runner.js +24 -25
  45. package/dist/services/uploader.js +5 -4
  46. package/dist/types/commands/init.d.ts +1 -2
  47. package/dist/types/index.d.ts +2 -3
  48. package/dist/types/plugin-loader.d.ts +1 -2
  49. package/dist/types/reporter/src/api/client.d.ts +178 -0
  50. package/dist/types/reporter/src/components/app-router.d.ts +1 -3
  51. package/dist/types/reporter/src/components/code-block.d.ts +4 -0
  52. package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +10 -0
  53. package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +11 -0
  54. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +14 -0
  55. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +30 -0
  56. package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +8 -0
  57. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  58. package/dist/types/reporter/src/components/comparison/fullscreen-viewer.d.ts +13 -0
  59. package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
  60. package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +9 -0
  61. package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +1 -1
  62. package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
  63. package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
  64. package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
  65. package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
  66. package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
  67. package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
  68. package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
  69. package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
  70. package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
  71. package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
  72. package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
  73. package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
  74. package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
  75. package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
  76. package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
  77. package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
  78. package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +5 -0
  79. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +5 -6
  80. package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
  81. package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
  82. package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
  83. package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
  84. package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
  85. package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
  86. package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
  87. package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
  88. package/dist/types/sdk/index.d.ts +2 -4
  89. package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
  90. package/dist/types/server/http-server.d.ts +1 -1
  91. package/dist/types/server/middleware/cors.d.ts +11 -0
  92. package/dist/types/server/middleware/json-parser.d.ts +10 -0
  93. package/dist/types/server/middleware/response.d.ts +50 -0
  94. package/dist/types/server/routers/assets.d.ts +6 -0
  95. package/dist/types/server/routers/auth.d.ts +9 -0
  96. package/dist/types/server/routers/baseline.d.ts +13 -0
  97. package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
  98. package/dist/types/server/routers/config.d.ts +9 -0
  99. package/dist/types/server/routers/dashboard.d.ts +6 -0
  100. package/dist/types/server/routers/health.d.ts +11 -0
  101. package/dist/types/server/routers/projects.d.ts +9 -0
  102. package/dist/types/server/routers/screenshot.d.ts +11 -0
  103. package/dist/types/services/build-manager.d.ts +4 -3
  104. package/dist/types/services/config-service.d.ts +2 -3
  105. package/dist/types/services/index.d.ts +7 -0
  106. package/dist/types/services/project-service.d.ts +6 -4
  107. package/dist/types/services/screenshot-server.d.ts +5 -5
  108. package/dist/types/services/server-manager.d.ts +5 -3
  109. package/dist/types/services/tdd-service.d.ts +12 -1
  110. package/dist/types/services/test-runner.d.ts +3 -3
  111. package/dist/types/utils/output.d.ts +84 -0
  112. package/dist/utils/config-loader.js +24 -48
  113. package/dist/utils/global-config.js +2 -17
  114. package/dist/utils/output.js +445 -0
  115. package/dist/utils/security.js +3 -4
  116. package/docs/api-reference.md +0 -1
  117. package/docs/plugins.md +22 -22
  118. package/package.json +3 -2
  119. package/dist/container/index.js +0 -215
  120. package/dist/services/base-service.js +0 -154
  121. package/dist/types/container/index.d.ts +0 -59
  122. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
  123. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
  124. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
  125. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
  126. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
  127. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
  128. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
  129. package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
  130. package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
  131. package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
  132. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
  133. package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
  134. package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
  135. package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
  136. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
  137. package/dist/types/services/base-service.d.ts +0 -71
  138. package/dist/types/utils/console-ui.d.ts +0 -61
  139. package/dist/types/utils/logger-factory.d.ts +0 -26
  140. package/dist/types/utils/logger.d.ts +0 -79
  141. package/dist/utils/console-ui.js +0 -241
  142. package/dist/utils/logger-factory.js +0 -76
  143. package/dist/utils/logger.js +0 -231
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Config Router
3
+ * Handles configuration management endpoints
4
+ */
5
+
6
+ import { parseJsonBody } from '../middleware/json-parser.js';
7
+ import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
8
+ import * as output from '../../utils/output.js';
9
+
10
+ /**
11
+ * Create config router
12
+ * @param {Object} context - Router context
13
+ * @param {Object} context.configService - Config service
14
+ * @returns {Function} Route handler
15
+ */
16
+ export function createConfigRouter({
17
+ configService
18
+ }) {
19
+ return async function handleConfigRoute(req, res, pathname) {
20
+ // Check if config service is available for all config routes
21
+ if (pathname.startsWith('/api/config') && !configService) {
22
+ sendServiceUnavailable(res, 'Config service');
23
+ return true;
24
+ }
25
+
26
+ // Get merged config with sources
27
+ if (req.method === 'GET' && pathname === '/api/config') {
28
+ try {
29
+ let configData = await configService.getConfig('merged');
30
+ sendSuccess(res, configData);
31
+ return true;
32
+ } catch (error) {
33
+ output.debug('Error fetching config:', {
34
+ error: error.message
35
+ });
36
+ sendError(res, 500, error.message);
37
+ return true;
38
+ }
39
+ }
40
+
41
+ // Get project-level config
42
+ if (req.method === 'GET' && pathname === '/api/config/project') {
43
+ try {
44
+ let configData = await configService.getConfig('project');
45
+ sendSuccess(res, configData);
46
+ return true;
47
+ } catch (error) {
48
+ output.debug('Error fetching project config:', {
49
+ error: error.message
50
+ });
51
+ sendError(res, 500, error.message);
52
+ return true;
53
+ }
54
+ }
55
+
56
+ // Get global config
57
+ if (req.method === 'GET' && pathname === '/api/config/global') {
58
+ try {
59
+ let configData = await configService.getConfig('global');
60
+ sendSuccess(res, configData);
61
+ return true;
62
+ } catch (error) {
63
+ output.debug('Error fetching global config:', {
64
+ error: error.message
65
+ });
66
+ sendError(res, 500, error.message);
67
+ return true;
68
+ }
69
+ }
70
+
71
+ // Update project config
72
+ if (req.method === 'POST' && pathname === '/api/config/project') {
73
+ try {
74
+ let body = await parseJsonBody(req);
75
+ let result = await configService.updateConfig('project', body);
76
+ sendSuccess(res, {
77
+ success: true,
78
+ ...result
79
+ });
80
+ return true;
81
+ } catch (error) {
82
+ output.debug('Error updating project config:', {
83
+ error: error.message
84
+ });
85
+ sendError(res, 500, error.message);
86
+ return true;
87
+ }
88
+ }
89
+
90
+ // Update global config
91
+ if (req.method === 'POST' && pathname === '/api/config/global') {
92
+ try {
93
+ let body = await parseJsonBody(req);
94
+ let result = await configService.updateConfig('global', body);
95
+ sendSuccess(res, {
96
+ success: true,
97
+ ...result
98
+ });
99
+ return true;
100
+ } catch (error) {
101
+ output.debug('Error updating global config:', {
102
+ error: error.message
103
+ });
104
+ sendError(res, 500, error.message);
105
+ return true;
106
+ }
107
+ }
108
+
109
+ // Validate config
110
+ if (req.method === 'POST' && pathname === '/api/config/validate') {
111
+ try {
112
+ let body = await parseJsonBody(req);
113
+ let result = await configService.validateConfig(body);
114
+ sendSuccess(res, result);
115
+ return true;
116
+ } catch (error) {
117
+ output.debug('Error validating config:', {
118
+ error: error.message
119
+ });
120
+ sendError(res, 500, error.message);
121
+ return true;
122
+ }
123
+ }
124
+ return false;
125
+ };
126
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Dashboard Router
3
+ * Serves the React SPA for all dashboard routes
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { sendHtml, sendSuccess } from '../middleware/response.js';
9
+ import * as output from '../../utils/output.js';
10
+
11
+ // SPA routes that should serve the dashboard HTML
12
+ let SPA_ROUTES = ['/', '/dashboard', '/stats', '/settings', '/projects', '/builds'];
13
+
14
+ /**
15
+ * Create dashboard router
16
+ * @param {Object} context - Router context
17
+ * @returns {Function} Route handler
18
+ */
19
+ export function createDashboardRouter() {
20
+ return async function handleDashboardRoute(req, res, pathname) {
21
+ if (req.method !== 'GET') {
22
+ return false;
23
+ }
24
+
25
+ // API endpoint for fetching report data
26
+ if (pathname === '/api/report-data') {
27
+ let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
28
+ if (existsSync(reportDataPath)) {
29
+ try {
30
+ let data = readFileSync(reportDataPath, 'utf8');
31
+ res.setHeader('Content-Type', 'application/json');
32
+ res.statusCode = 200;
33
+ res.end(data);
34
+ return true;
35
+ } catch (error) {
36
+ output.debug('Error reading report data:', {
37
+ error: error.message
38
+ });
39
+ res.statusCode = 500;
40
+ res.end(JSON.stringify({
41
+ error: 'Failed to read report data'
42
+ }));
43
+ return true;
44
+ }
45
+ } else {
46
+ sendSuccess(res, null);
47
+ return true;
48
+ }
49
+ }
50
+
51
+ // API endpoint for real-time status
52
+ if (pathname === '/api/status') {
53
+ let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
54
+ let baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
55
+ let reportData = null;
56
+ let baselineInfo = null;
57
+ if (existsSync(reportDataPath)) {
58
+ try {
59
+ reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
60
+ } catch {
61
+ // Ignore
62
+ }
63
+ }
64
+ if (existsSync(baselineMetadataPath)) {
65
+ try {
66
+ baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
67
+ } catch {
68
+ // Ignore
69
+ }
70
+ }
71
+ sendSuccess(res, {
72
+ timestamp: Date.now(),
73
+ baseline: baselineInfo,
74
+ comparisons: reportData?.comparisons || [],
75
+ summary: reportData?.summary || {
76
+ total: 0,
77
+ passed: 0,
78
+ failed: 0,
79
+ errors: 0
80
+ }
81
+ });
82
+ return true;
83
+ }
84
+
85
+ // Serve React SPA for dashboard routes
86
+ if (SPA_ROUTES.includes(pathname) || pathname.startsWith('/comparison/')) {
87
+ let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
88
+ let reportData = null;
89
+ if (existsSync(reportDataPath)) {
90
+ try {
91
+ let data = readFileSync(reportDataPath, 'utf8');
92
+ reportData = JSON.parse(data);
93
+ } catch (error) {
94
+ output.debug('Could not read report data:', {
95
+ error: error.message
96
+ });
97
+ }
98
+ }
99
+ let dashboardHtml = `
100
+ <!DOCTYPE html>
101
+ <html>
102
+ <head>
103
+ <title>Vizzly Dev Dashboard</title>
104
+ <meta charset="utf-8">
105
+ <meta name="viewport" content="width=device-width, initial-scale=1">
106
+ <link rel="stylesheet" href="/reporter-bundle.css">
107
+ </head>
108
+ <body>
109
+ <div id="vizzly-reporter-root">
110
+ <div class="reporter-loading">
111
+ <div>
112
+ <div class="spinner"></div>
113
+ <p>Loading Vizzly Dev Dashboard...</p>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <script>
119
+ // Inject report data if available
120
+ ${reportData ? `window.VIZZLY_REPORTER_DATA = ${JSON.stringify(reportData)};` : ''}
121
+ </script>
122
+ <script src="/reporter-bundle.js"></script>
123
+ </body>
124
+ </html>`;
125
+ sendHtml(res, 200, dashboardHtml);
126
+ return true;
127
+ }
128
+ return false;
129
+ };
130
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Health Router
3
+ * Health check endpoint with diagnostics
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { sendSuccess } from '../middleware/response.js';
9
+
10
+ /**
11
+ * Create health router
12
+ * @param {Object} context - Router context
13
+ * @param {number} context.port - Server port
14
+ * @param {Object} context.screenshotHandler - Screenshot handler
15
+ * @returns {Function} Route handler
16
+ */
17
+ export function createHealthRouter({
18
+ port,
19
+ screenshotHandler
20
+ }) {
21
+ return async function handleHealthRoute(req, res, pathname) {
22
+ if (req.method !== 'GET' || pathname !== '/health') {
23
+ return false;
24
+ }
25
+ let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
26
+ let baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
27
+ let reportData = null;
28
+ let baselineInfo = null;
29
+ if (existsSync(reportDataPath)) {
30
+ try {
31
+ reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
32
+ } catch {
33
+ // Ignore read errors
34
+ }
35
+ }
36
+ if (existsSync(baselineMetadataPath)) {
37
+ try {
38
+ baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
39
+ } catch {
40
+ // Ignore read errors
41
+ }
42
+ }
43
+ sendSuccess(res, {
44
+ status: 'ok',
45
+ port,
46
+ uptime: process.uptime(),
47
+ mode: screenshotHandler ? 'tdd' : 'upload',
48
+ baseline: baselineInfo ? {
49
+ buildName: baselineInfo.buildName,
50
+ createdAt: baselineInfo.createdAt
51
+ } : null,
52
+ stats: reportData ? {
53
+ total: reportData.summary?.total || 0,
54
+ passed: reportData.summary?.passed || 0,
55
+ failed: reportData.summary?.failed || 0,
56
+ errors: reportData.summary?.errors || 0
57
+ } : null
58
+ });
59
+ return true;
60
+ };
61
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Projects Router
3
+ * Handles project management and builds endpoints
4
+ */
5
+
6
+ import { parseJsonBody } from '../middleware/json-parser.js';
7
+ import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
8
+ import * as output from '../../utils/output.js';
9
+
10
+ /**
11
+ * Create projects router
12
+ * @param {Object} context - Router context
13
+ * @param {Object} context.projectService - Project service
14
+ * @returns {Function} Route handler
15
+ */
16
+ export function createProjectsRouter({
17
+ projectService
18
+ }) {
19
+ return async function handleProjectsRoute(req, res, pathname, parsedUrl) {
20
+ // Check if project service is available for all project routes
21
+ if (pathname.startsWith('/api/projects') && !projectService) {
22
+ sendServiceUnavailable(res, 'Project service');
23
+ return true;
24
+ }
25
+
26
+ // List all projects from API
27
+ if (req.method === 'GET' && pathname === '/api/projects') {
28
+ try {
29
+ let projects = await projectService.listProjects();
30
+ sendSuccess(res, {
31
+ projects
32
+ });
33
+ return true;
34
+ } catch (error) {
35
+ output.debug('Error listing projects:', {
36
+ error: error.message
37
+ });
38
+ sendError(res, 500, error.message);
39
+ return true;
40
+ }
41
+ }
42
+
43
+ // List project directory mappings
44
+ if (req.method === 'GET' && pathname === '/api/projects/mappings') {
45
+ try {
46
+ let mappings = await projectService.listMappings();
47
+ sendSuccess(res, {
48
+ mappings
49
+ });
50
+ return true;
51
+ } catch (error) {
52
+ output.debug('Error listing project mappings:', {
53
+ error: error.message
54
+ });
55
+ sendError(res, 500, error.message);
56
+ return true;
57
+ }
58
+ }
59
+
60
+ // Create or update project mapping
61
+ if (req.method === 'POST' && pathname === '/api/projects/mappings') {
62
+ try {
63
+ let body = await parseJsonBody(req);
64
+ let {
65
+ directory,
66
+ projectSlug,
67
+ organizationSlug,
68
+ token,
69
+ projectName
70
+ } = body;
71
+ let mapping = await projectService.createMapping(directory, {
72
+ projectSlug,
73
+ organizationSlug,
74
+ token,
75
+ projectName
76
+ });
77
+ sendSuccess(res, {
78
+ success: true,
79
+ mapping
80
+ });
81
+ return true;
82
+ } catch (error) {
83
+ output.debug('Error creating project mapping:', {
84
+ error: error.message
85
+ });
86
+ sendError(res, 500, error.message);
87
+ return true;
88
+ }
89
+ }
90
+
91
+ // Delete project mapping
92
+ if (req.method === 'DELETE' && pathname.startsWith('/api/projects/mappings/')) {
93
+ try {
94
+ let directory = decodeURIComponent(pathname.replace('/api/projects/mappings/', ''));
95
+ await projectService.removeMapping(directory);
96
+ sendSuccess(res, {
97
+ success: true,
98
+ message: 'Mapping deleted'
99
+ });
100
+ return true;
101
+ } catch (error) {
102
+ output.debug('Error deleting project mapping:', {
103
+ error: error.message
104
+ });
105
+ sendError(res, 500, error.message);
106
+ return true;
107
+ }
108
+ }
109
+
110
+ // Get recent builds for current project
111
+ if (req.method === 'GET' && pathname === '/api/builds/recent') {
112
+ if (!projectService) {
113
+ sendServiceUnavailable(res, 'Project service');
114
+ return true;
115
+ }
116
+ try {
117
+ let currentDir = process.cwd();
118
+ let mapping = await projectService.getMapping(currentDir);
119
+ if (!mapping || !mapping.projectSlug || !mapping.organizationSlug) {
120
+ sendError(res, 400, 'No project configured for this directory');
121
+ return true;
122
+ }
123
+ let limit = parseInt(parsedUrl.searchParams.get('limit') || '10', 10);
124
+ let branch = parsedUrl.searchParams.get('branch') || undefined;
125
+ let builds = await projectService.getRecentBuilds(mapping.projectSlug, mapping.organizationSlug, {
126
+ limit,
127
+ branch
128
+ });
129
+ sendSuccess(res, {
130
+ builds
131
+ });
132
+ return true;
133
+ } catch (error) {
134
+ output.debug('Error fetching recent builds:', {
135
+ error: error.message
136
+ });
137
+ sendError(res, 500, error.message);
138
+ return true;
139
+ }
140
+ }
141
+
142
+ // Get builds for a specific project (used by /builds page)
143
+ let projectBuildsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/([^/]+)\/builds$/);
144
+ if (req.method === 'GET' && projectBuildsMatch) {
145
+ try {
146
+ let organizationSlug = decodeURIComponent(projectBuildsMatch[1]);
147
+ let projectSlug = decodeURIComponent(projectBuildsMatch[2]);
148
+ let limit = parseInt(parsedUrl.searchParams.get('limit') || '20', 10);
149
+ let branch = parsedUrl.searchParams.get('branch') || undefined;
150
+ let builds = await projectService.getRecentBuilds(projectSlug, organizationSlug, {
151
+ limit,
152
+ branch
153
+ });
154
+ sendSuccess(res, {
155
+ builds
156
+ });
157
+ return true;
158
+ } catch (error) {
159
+ output.debug('Error fetching project builds:', {
160
+ error: error.message
161
+ });
162
+ sendError(res, 500, error.message);
163
+ return true;
164
+ }
165
+ }
166
+ return false;
167
+ };
168
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Screenshot Router
3
+ * Handles screenshot uploads and legacy baseline accept
4
+ */
5
+
6
+ import { parseJsonBody } from '../middleware/json-parser.js';
7
+ import { sendJson, sendError } from '../middleware/response.js';
8
+ import * as output from '../../utils/output.js';
9
+
10
+ /**
11
+ * Create screenshot router
12
+ * @param {Object} context - Router context
13
+ * @param {Object} context.screenshotHandler - Screenshot handler
14
+ * @param {string|null} context.defaultBuildId - Default build ID
15
+ * @returns {Function} Route handler
16
+ */
17
+ export function createScreenshotRouter({
18
+ screenshotHandler,
19
+ defaultBuildId
20
+ }) {
21
+ return async function handleScreenshotRoute(req, res, pathname) {
22
+ if (req.method !== 'POST') {
23
+ return false;
24
+ }
25
+
26
+ // Main screenshot upload endpoint
27
+ if (pathname === '/screenshot') {
28
+ try {
29
+ let body = await parseJsonBody(req);
30
+ let {
31
+ buildId,
32
+ name,
33
+ properties,
34
+ image
35
+ } = body;
36
+ if (!name || !image) {
37
+ sendError(res, 400, 'name and image are required');
38
+ return true;
39
+ }
40
+
41
+ // Use buildId from request body, or fall back to server's buildId
42
+ let effectiveBuildId = buildId || defaultBuildId;
43
+ let result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties);
44
+ sendJson(res, result.statusCode, result.body);
45
+ return true;
46
+ } catch (error) {
47
+ output.debug('Screenshot processing error:', {
48
+ error: error.message
49
+ });
50
+ sendError(res, 500, 'Failed to process screenshot');
51
+ return true;
52
+ }
53
+ }
54
+
55
+ // Legacy accept-baseline endpoint
56
+ if (pathname === '/accept-baseline') {
57
+ try {
58
+ let body = await parseJsonBody(req);
59
+ let {
60
+ id
61
+ } = body;
62
+ if (!id) {
63
+ sendError(res, 400, 'comparison ID is required');
64
+ return true;
65
+ }
66
+ if (screenshotHandler.acceptBaseline) {
67
+ let result = await screenshotHandler.acceptBaseline(id);
68
+ sendJson(res, 200, {
69
+ success: true,
70
+ ...result
71
+ });
72
+ } else {
73
+ sendError(res, 501, 'Accept baseline not implemented');
74
+ }
75
+ return true;
76
+ } catch (error) {
77
+ output.debug('Accept baseline error:', {
78
+ error: error.message
79
+ });
80
+ sendError(res, 500, 'Failed to accept baseline');
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ };
86
+ }
@@ -95,7 +95,7 @@ export class AuthService {
95
95
  if (response.status === 401) {
96
96
  throw new AuthError('Authentication token is invalid or expired. Please run "vizzly login" again.');
97
97
  }
98
- throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''}`, 'API_REQUEST_ERROR');
98
+ throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (${endpoint})`, 'API_REQUEST_ERROR');
99
99
  }
100
100
  return response.json();
101
101
  }
@@ -5,7 +5,6 @@
5
5
 
6
6
  import crypto from 'crypto';
7
7
  import { VizzlyError } from '../errors/vizzly-error.js';
8
- import { BaseService } from './base-service.js';
9
8
 
10
9
  /**
11
10
  * Generate unique build ID for local build management only.
@@ -121,31 +120,15 @@ export function validateBuildOptions(buildOptions) {
121
120
  errors
122
121
  };
123
122
  }
124
- export class BuildManager extends BaseService {
125
- constructor(config, logger) {
126
- super(config, {
127
- logger
128
- });
123
+ export class BuildManager {
124
+ constructor(config) {
125
+ this.config = config;
129
126
  this.currentBuild = null;
130
127
  this.buildQueue = [];
131
128
  }
132
- async onStart() {
133
- this.emitProgress('initializing', 0);
134
- }
135
- async onStop() {
136
- if (this.currentBuild && this.currentBuild.status === 'pending') {
137
- await this.updateBuildStatus(this.currentBuild.id, 'cancelled');
138
- }
139
- this.buildQueue.length = 0;
140
- this.currentBuild = null;
141
- }
142
129
  async createBuild(buildOptions) {
143
- this.emitProgress('creating', 'Creating new build...');
144
- const build = createBuildObject(buildOptions);
130
+ let build = createBuildObject(buildOptions);
145
131
  this.currentBuild = build;
146
- this.emitProgress('created', `Build created: ${build.name}`, {
147
- build
148
- });
149
132
  return build;
150
133
  }
151
134
  async updateBuildStatus(buildId, status, updates = {}) {
@@ -153,11 +136,6 @@ export class BuildManager extends BaseService {
153
136
  throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
154
137
  }
155
138
  this.currentBuild = updateBuild(this.currentBuild, status, updates);
156
- this.emitProgress('updated', `Build status: ${status}`, {
157
- buildId,
158
- status,
159
- build: this.currentBuild
160
- });
161
139
  return this.currentBuild;
162
140
  }
163
141
  async addScreenshot(buildId, screenshot) {
@@ -165,11 +143,6 @@ export class BuildManager extends BaseService {
165
143
  throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
166
144
  }
167
145
  this.currentBuild = addScreenshotToBuild(this.currentBuild, screenshot);
168
- this.emitProgress('screenshot-added', 'Screenshot added to build', {
169
- buildId,
170
- screenshotCount: this.currentBuild.screenshots.length,
171
- screenshot
172
- });
173
146
  return this.currentBuild;
174
147
  }
175
148
  async finalizeBuild(buildId, result = {}) {
@@ -177,22 +150,22 @@ export class BuildManager extends BaseService {
177
150
  throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
178
151
  }
179
152
  this.currentBuild = finalizeBuildObject(this.currentBuild, result);
180
- this.emitProgress('finalized', `Build ${this.currentBuild.status}`, {
181
- buildId,
182
- build: this.currentBuild,
183
- result
184
- });
185
153
  return this.currentBuild;
186
154
  }
187
155
  getCurrentBuild() {
188
156
  return this.currentBuild;
189
157
  }
190
158
  queueBuild(buildOptions) {
191
- const queuedBuild = createQueuedBuild(buildOptions);
159
+ let queuedBuild = createQueuedBuild(buildOptions);
192
160
  this.buildQueue.push(queuedBuild);
193
- this.emitProgress('queued', 'Build queued for processing', {
194
- queueLength: this.buildQueue.length
195
- });
161
+ }
162
+ async clear() {
163
+ // Cancel pending build if exists
164
+ if (this.currentBuild && this.currentBuild.status === 'pending') {
165
+ await this.updateBuildStatus(this.currentBuild.id, 'cancelled');
166
+ }
167
+ this.buildQueue.length = 0;
168
+ this.currentBuild = null;
196
169
  }
197
170
  async processNextBuild() {
198
171
  if (this.buildQueue.length === 0) {