@vizzly-testing/cli 0.14.0 → 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 (140) 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 +74 -41
  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/screenshot-display.d.ts +16 -0
  59. package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
  60. package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
  61. package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
  62. package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
  63. package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
  64. package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
  65. package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
  66. package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
  67. package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
  68. package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
  69. package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
  70. package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
  71. package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
  72. package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
  73. package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
  74. package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
  75. package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +1 -4
  76. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +1 -6
  77. package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
  78. package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
  79. package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
  80. package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
  81. package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
  82. package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
  83. package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
  84. package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
  85. package/dist/types/sdk/index.d.ts +2 -4
  86. package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
  87. package/dist/types/server/http-server.d.ts +1 -1
  88. package/dist/types/server/middleware/cors.d.ts +11 -0
  89. package/dist/types/server/middleware/json-parser.d.ts +10 -0
  90. package/dist/types/server/middleware/response.d.ts +50 -0
  91. package/dist/types/server/routers/assets.d.ts +6 -0
  92. package/dist/types/server/routers/auth.d.ts +9 -0
  93. package/dist/types/server/routers/baseline.d.ts +13 -0
  94. package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
  95. package/dist/types/server/routers/config.d.ts +9 -0
  96. package/dist/types/server/routers/dashboard.d.ts +6 -0
  97. package/dist/types/server/routers/health.d.ts +11 -0
  98. package/dist/types/server/routers/projects.d.ts +9 -0
  99. package/dist/types/server/routers/screenshot.d.ts +11 -0
  100. package/dist/types/services/build-manager.d.ts +4 -3
  101. package/dist/types/services/config-service.d.ts +2 -3
  102. package/dist/types/services/index.d.ts +7 -0
  103. package/dist/types/services/project-service.d.ts +6 -4
  104. package/dist/types/services/screenshot-server.d.ts +5 -5
  105. package/dist/types/services/server-manager.d.ts +5 -3
  106. package/dist/types/services/tdd-service.d.ts +12 -1
  107. package/dist/types/services/test-runner.d.ts +3 -3
  108. package/dist/types/utils/output.d.ts +84 -0
  109. package/dist/utils/config-loader.js +24 -48
  110. package/dist/utils/global-config.js +2 -17
  111. package/dist/utils/output.js +445 -0
  112. package/dist/utils/security.js +3 -4
  113. package/docs/api-reference.md +0 -1
  114. package/docs/plugins.md +22 -22
  115. package/package.json +3 -2
  116. package/dist/container/index.js +0 -215
  117. package/dist/services/base-service.js +0 -154
  118. package/dist/types/container/index.d.ts +0 -59
  119. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
  120. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
  121. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
  122. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
  123. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
  124. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
  125. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
  126. package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
  127. package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
  128. package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
  129. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
  130. package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
  131. package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
  132. package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
  133. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
  134. package/dist/types/services/base-service.d.ts +0 -71
  135. package/dist/types/utils/console-ui.d.ts +0 -61
  136. package/dist/types/utils/logger-factory.d.ts +0 -26
  137. package/dist/types/utils/logger.d.ts +0 -79
  138. package/dist/utils/console-ui.js +0 -241
  139. package/dist/utils/logger-factory.js +0 -76
  140. package/dist/utils/logger.js +0 -231
@@ -1,889 +1,93 @@
1
+ /**
2
+ * HTTP Server
3
+ * Thin dispatcher that routes requests to modular routers
4
+ */
5
+
1
6
  import { createServer } from 'http';
2
- import { readFileSync, existsSync } from 'fs';
3
- import { join, dirname } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { createServiceLogger } from '../utils/logger-factory.js';
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
- const PROJECT_ROOT = join(__dirname, '..', '..');
9
- const logger = createServiceLogger('HTTP-SERVER');
10
- export const createHttpServer = (port, screenshotHandler, services = {}) => {
7
+ import * as output from '../utils/output.js';
8
+
9
+ // Middleware
10
+ import { corsMiddleware } from './middleware/cors.js';
11
+ import { sendError } from './middleware/response.js';
12
+
13
+ // Routers
14
+ import { createHealthRouter } from './routers/health.js';
15
+ import { createAssetsRouter } from './routers/assets.js';
16
+ import { createDashboardRouter } from './routers/dashboard.js';
17
+ import { createScreenshotRouter } from './routers/screenshot.js';
18
+ import { createBaselineRouter } from './routers/baseline.js';
19
+ import { createConfigRouter } from './routers/config.js';
20
+ import { createAuthRouter } from './routers/auth.js';
21
+ import { createProjectsRouter } from './routers/projects.js';
22
+ import { createCloudProxyRouter } from './routers/cloud-proxy.js';
23
+ export let createHttpServer = (port, screenshotHandler, services = {}) => {
11
24
  let server = null;
12
25
  let defaultBuildId = services.buildId || null;
13
26
 
14
- // Extract services for config/auth/project management
15
- let configService = services.configService;
16
- let authService = services.authService;
17
- let projectService = services.projectService;
18
- const parseRequestBody = req => {
19
- return new Promise((resolve, reject) => {
20
- let body = '';
21
- req.on('data', chunk => {
22
- body += chunk.toString();
23
- });
24
- req.on('end', () => {
25
- try {
26
- const data = JSON.parse(body);
27
- resolve(data);
28
- } catch {
29
- reject(new Error('Invalid JSON'));
30
- }
31
- });
32
- req.on('error', reject);
33
- });
27
+ // Extract services
28
+ let {
29
+ configService,
30
+ authService,
31
+ projectService,
32
+ tddService
33
+ } = services;
34
+
35
+ // Create router context
36
+ let routerContext = {
37
+ port,
38
+ screenshotHandler,
39
+ defaultBuildId,
40
+ configService,
41
+ authService,
42
+ projectService,
43
+ tddService,
44
+ apiUrl: 'https://app.vizzly.dev'
34
45
  };
35
- const handleRequest = async (req, res) => {
36
- res.setHeader('Access-Control-Allow-Origin', '*');
37
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
38
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
39
- res.setHeader('Content-Type', 'application/json');
40
- if (req.method === 'OPTIONS') {
41
- res.statusCode = 200;
42
- res.end();
43
- return;
44
- }
45
46
 
46
- // Parse URL to handle query params properly for all routes
47
- const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
48
- if (req.method === 'GET' && parsedUrl.pathname === '/health') {
49
- // Enhanced health endpoint with diagnostics
50
- const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
51
- const baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
52
- let reportData = null;
53
- let baselineInfo = null;
54
- if (existsSync(reportDataPath)) {
55
- try {
56
- reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
57
- } catch {
58
- // Ignore read errors
59
- }
60
- }
61
- if (existsSync(baselineMetadataPath)) {
62
- try {
63
- baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
64
- } catch {
65
- // Ignore read errors
66
- }
67
- }
68
- res.statusCode = 200;
69
- res.end(JSON.stringify({
70
- status: 'ok',
71
- port: port,
72
- uptime: process.uptime(),
73
- mode: screenshotHandler ? 'tdd' : 'upload',
74
- baseline: baselineInfo ? {
75
- buildName: baselineInfo.buildName,
76
- createdAt: baselineInfo.createdAt
77
- } : null,
78
- stats: reportData ? {
79
- total: reportData.summary?.total || 0,
80
- passed: reportData.summary?.passed || 0,
81
- failed: reportData.summary?.failed || 0,
82
- errors: reportData.summary?.errors || 0
83
- } : null
84
- }));
47
+ // Initialize routers
48
+ let routers = [createHealthRouter(routerContext), createAssetsRouter(routerContext), createScreenshotRouter(routerContext), createBaselineRouter(routerContext), createConfigRouter(routerContext), createAuthRouter(routerContext), createProjectsRouter(routerContext), createCloudProxyRouter(routerContext), createDashboardRouter(routerContext) // Catch-all for SPA routes - must be last
49
+ ];
50
+ let handleRequest = async (req, res) => {
51
+ // Apply CORS middleware
52
+ if (corsMiddleware(req, res)) {
85
53
  return;
86
54
  }
87
55
 
88
- // Serve the main React app for all non-API routes
89
- if (req.method === 'GET' && (parsedUrl.pathname === '/' || parsedUrl.pathname === '/dashboard' || parsedUrl.pathname === '/stats' || parsedUrl.pathname === '/settings' || parsedUrl.pathname === '/projects')) {
90
- // Serve React-powered dashboard
91
- const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
92
-
93
- // Try to read existing report data
94
- let reportData = null;
95
- if (existsSync(reportDataPath)) {
96
- try {
97
- const data = readFileSync(reportDataPath, 'utf8');
98
- reportData = JSON.parse(data);
99
- } catch (error) {
100
- logger.debug('Could not read report data:', error.message);
101
- }
102
- }
103
- const dashboardHtml = `
104
- <!DOCTYPE html>
105
- <html>
106
- <head>
107
- <title>Vizzly Dev Dashboard</title>
108
- <meta charset="utf-8">
109
- <meta name="viewport" content="width=device-width, initial-scale=1">
110
- <link rel="stylesheet" href="/reporter-bundle.css">
111
- </head>
112
- <body>
113
- <div id="vizzly-reporter-root">
114
- <div class="reporter-loading">
115
- <div>
116
- <div class="spinner"></div>
117
- <p>Loading Vizzly Dev Dashboard...</p>
118
- </div>
119
- </div>
120
- </div>
121
-
122
- <script>
123
- // Inject report data if available
124
- ${reportData ? `window.VIZZLY_REPORTER_DATA = ${JSON.stringify(reportData)};` : ''}
125
- </script>
126
- <script src="/reporter-bundle.js"></script>
127
- </body>
128
- </html>`;
129
- res.setHeader('Content-Type', 'text/html');
130
- res.statusCode = 200;
131
- res.end(dashboardHtml);
132
- return;
133
- }
134
- if (req.method === 'GET' && parsedUrl.pathname === '/reporter-bundle.js') {
135
- // Serve the React bundle
136
- const bundlePath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.iife.js');
137
- if (existsSync(bundlePath)) {
138
- try {
139
- const bundle = readFileSync(bundlePath, 'utf8');
140
- res.setHeader('Content-Type', 'application/javascript');
141
- res.statusCode = 200;
142
- res.end(bundle);
143
- } catch (error) {
144
- logger.error('Error serving reporter bundle:', error);
145
- res.statusCode = 500;
146
- res.end('Error loading reporter bundle');
147
- }
148
- } else {
149
- res.statusCode = 404;
150
- res.end('Reporter bundle not found');
151
- }
152
- return;
153
- }
154
- if (req.method === 'GET' && parsedUrl.pathname === '/reporter-bundle.css') {
155
- // Serve the React CSS bundle
156
- const cssPath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.css');
157
- if (existsSync(cssPath)) {
158
- try {
159
- const css = readFileSync(cssPath, 'utf8');
160
- res.setHeader('Content-Type', 'text/css');
161
- res.statusCode = 200;
162
- res.end(css);
163
- } catch (error) {
164
- logger.error('Error serving reporter CSS:', error);
165
- res.statusCode = 500;
166
- res.end('Error loading reporter CSS');
167
- }
168
- } else {
169
- res.statusCode = 404;
170
- res.end('Reporter CSS not found');
171
- }
172
- return;
173
- }
174
- if (req.method === 'GET' && parsedUrl.pathname === '/api/report-data') {
175
- // API endpoint for fetching report data
176
- const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
177
- if (existsSync(reportDataPath)) {
178
- try {
179
- const data = readFileSync(reportDataPath, 'utf8');
180
- res.setHeader('Content-Type', 'application/json');
181
- res.statusCode = 200;
182
- res.end(data);
183
- } catch (error) {
184
- logger.error('Error reading report data:', error);
185
- res.statusCode = 500;
186
- res.end(JSON.stringify({
187
- error: 'Failed to read report data'
188
- }));
189
- }
190
- } else {
191
- res.statusCode = 200;
192
- res.end(JSON.stringify(null)); // No data available yet
193
- }
194
- return;
195
- }
196
- if (req.method === 'GET' && parsedUrl.pathname === '/api/status') {
197
- // Real-time status endpoint
198
- const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
199
- const baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
200
- let reportData = null;
201
- let baselineInfo = null;
202
- if (existsSync(reportDataPath)) {
203
- try {
204
- reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
205
- } catch {
206
- // Ignore
207
- }
208
- }
209
- if (existsSync(baselineMetadataPath)) {
210
- try {
211
- baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
212
- } catch {
213
- // Ignore
214
- }
215
- }
216
- res.setHeader('Content-Type', 'application/json');
217
- res.statusCode = 200;
218
- res.end(JSON.stringify({
219
- timestamp: Date.now(),
220
- baseline: baselineInfo,
221
- comparisons: reportData?.comparisons || [],
222
- summary: reportData?.summary || {
223
- total: 0,
224
- passed: 0,
225
- failed: 0,
226
- errors: 0
227
- }
228
- }));
229
- return;
230
- }
231
- if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/accept') {
232
- // Accept a single screenshot as baseline
233
- if (!screenshotHandler?.acceptBaseline) {
234
- res.statusCode = 400;
235
- res.end(JSON.stringify({
236
- error: 'Baseline management not available'
237
- }));
238
- return;
239
- }
240
- try {
241
- const {
242
- id
243
- } = await parseRequestBody(req);
244
- if (!id) {
245
- res.statusCode = 400;
246
- res.end(JSON.stringify({
247
- error: 'Comparison ID required'
248
- }));
249
- return;
250
- }
251
- await screenshotHandler.acceptBaseline(id);
252
- res.setHeader('Content-Type', 'application/json');
253
- res.statusCode = 200;
254
- res.end(JSON.stringify({
255
- success: true,
256
- message: `Baseline accepted for comparison ${id}`
257
- }));
258
- } catch (error) {
259
- logger.error('Error accepting baseline:', error);
260
- res.statusCode = 500;
261
- res.end(JSON.stringify({
262
- error: error.message
263
- }));
264
- }
265
- return;
266
- }
267
- if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/accept-all') {
268
- // Accept all screenshots as baseline
269
- if (!screenshotHandler?.acceptAllBaselines) {
270
- res.statusCode = 400;
271
- res.end(JSON.stringify({
272
- error: 'Baseline management not available'
273
- }));
274
- return;
275
- }
276
- try {
277
- const result = await screenshotHandler.acceptAllBaselines();
278
- res.setHeader('Content-Type', 'application/json');
279
- res.statusCode = 200;
280
- res.end(JSON.stringify({
281
- success: true,
282
- message: `Accepted ${result.count} baselines`,
283
- count: result.count
284
- }));
285
- } catch (error) {
286
- logger.error('Error accepting all baselines:', error);
287
- res.statusCode = 500;
288
- res.end(JSON.stringify({
289
- error: error.message
290
- }));
291
- }
292
- return;
293
- }
294
- if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/reset') {
295
- // Reset baselines to previous state
296
- if (!screenshotHandler?.resetBaselines) {
297
- res.statusCode = 400;
298
- res.end(JSON.stringify({
299
- error: 'Baseline management not available'
300
- }));
301
- return;
302
- }
303
- try {
304
- await screenshotHandler.resetBaselines();
305
- res.setHeader('Content-Type', 'application/json');
306
- res.statusCode = 200;
307
- res.end(JSON.stringify({
308
- success: true,
309
- message: 'Baselines reset to previous state'
310
- }));
311
- } catch (error) {
312
- logger.error('Error resetting baselines:', error);
313
- res.statusCode = 500;
314
- res.end(JSON.stringify({
315
- error: error.message
316
- }));
317
- }
318
- return;
319
- }
320
-
321
- // Serve images from .vizzly directory
322
- if (req.method === 'GET' && parsedUrl.pathname.startsWith('/images/')) {
323
- const imagePath = parsedUrl.pathname.replace('/images/', '');
324
- const fullImagePath = join(process.cwd(), '.vizzly', imagePath);
325
- if (existsSync(fullImagePath)) {
326
- try {
327
- const imageData = readFileSync(fullImagePath);
328
- res.setHeader('Content-Type', 'image/png');
329
- res.statusCode = 200;
330
- res.end(imageData);
331
- } catch (error) {
332
- logger.error('Error serving image:', error);
333
- res.statusCode = 500;
334
- res.end('Error loading image');
335
- }
336
- } else {
337
- res.statusCode = 404;
338
- res.end('Image not found');
339
- }
340
- return;
341
- }
342
- if (req.method === 'POST' && parsedUrl.pathname === '/screenshot') {
343
- try {
344
- const body = await parseRequestBody(req);
345
- const {
346
- buildId,
347
- name,
348
- properties,
349
- image
350
- } = body;
351
- if (!name || !image) {
352
- res.statusCode = 400;
353
- res.end(JSON.stringify({
354
- error: 'name and image are required'
355
- }));
356
- return;
357
- }
358
-
359
- // Use buildId from request body, or fall back to server's buildId (set during server creation)
360
- // If neither is available, this is an error - buildId is required for cloud uploads
361
- const effectiveBuildId = buildId || defaultBuildId;
362
- const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties);
363
- res.statusCode = result.statusCode;
364
- res.end(JSON.stringify(result.body));
365
- } catch (error) {
366
- logger.error('Screenshot processing error:', error);
367
- res.statusCode = 500;
368
- res.end(JSON.stringify({
369
- error: 'Failed to process screenshot'
370
- }));
371
- }
372
- return;
373
- }
374
- if (req.method === 'POST' && parsedUrl.pathname === '/accept-baseline') {
375
- try {
376
- const body = await parseRequestBody(req);
377
- const {
378
- id
379
- } = body;
380
- if (!id) {
381
- res.statusCode = 400;
382
- res.end(JSON.stringify({
383
- error: 'comparison ID is required'
384
- }));
385
- return;
386
- }
387
-
388
- // Call the screenshot handler's accept baseline method if it exists
389
- if (screenshotHandler.acceptBaseline) {
390
- const result = await screenshotHandler.acceptBaseline(id);
391
- res.statusCode = 200;
392
- res.end(JSON.stringify({
393
- success: true,
394
- ...result
395
- }));
396
- } else {
397
- res.statusCode = 501;
398
- res.end(JSON.stringify({
399
- error: 'Accept baseline not implemented'
400
- }));
401
- }
402
- } catch (error) {
403
- logger.error('Accept baseline error:', error);
404
- res.statusCode = 500;
405
- res.end(JSON.stringify({
406
- error: 'Failed to accept baseline'
407
- }));
408
- }
409
- return;
410
- }
411
-
412
- // ===== CONFIG MANAGEMENT ENDPOINTS =====
413
-
414
- if (req.method === 'GET' && parsedUrl.pathname === '/api/config') {
415
- // Get merged config with sources
416
- if (!configService) {
417
- res.statusCode = 503;
418
- res.end(JSON.stringify({
419
- error: 'Config service not available'
420
- }));
421
- return;
422
- }
423
- try {
424
- const configData = await configService.getConfig('merged');
425
- res.statusCode = 200;
426
- res.end(JSON.stringify(configData));
427
- } catch (error) {
428
- logger.error('Error fetching config:', error);
429
- res.statusCode = 500;
430
- res.end(JSON.stringify({
431
- error: error.message
432
- }));
433
- }
434
- return;
435
- }
436
- if (req.method === 'GET' && parsedUrl.pathname === '/api/config/project') {
437
- // Get project-level config
438
- if (!configService) {
439
- res.statusCode = 503;
440
- res.end(JSON.stringify({
441
- error: 'Config service not available'
442
- }));
443
- return;
444
- }
445
- try {
446
- const configData = await configService.getConfig('project');
447
- res.statusCode = 200;
448
- res.end(JSON.stringify(configData));
449
- } catch (error) {
450
- logger.error('Error fetching project config:', error);
451
- res.statusCode = 500;
452
- res.end(JSON.stringify({
453
- error: error.message
454
- }));
455
- }
456
- return;
457
- }
458
- if (req.method === 'GET' && parsedUrl.pathname === '/api/config/global') {
459
- // Get global config
460
- if (!configService) {
461
- res.statusCode = 503;
462
- res.end(JSON.stringify({
463
- error: 'Config service not available'
464
- }));
465
- return;
466
- }
467
- try {
468
- const configData = await configService.getConfig('global');
469
- res.statusCode = 200;
470
- res.end(JSON.stringify(configData));
471
- } catch (error) {
472
- logger.error('Error fetching global config:', error);
473
- res.statusCode = 500;
474
- res.end(JSON.stringify({
475
- error: error.message
476
- }));
477
- }
478
- return;
479
- }
480
- if (req.method === 'POST' && parsedUrl.pathname === '/api/config/project') {
481
- // Update project config
482
- if (!configService) {
483
- res.statusCode = 503;
484
- res.end(JSON.stringify({
485
- error: 'Config service not available'
486
- }));
487
- return;
488
- }
489
- try {
490
- const body = await parseRequestBody(req);
491
- const result = await configService.updateConfig('project', body);
492
- res.statusCode = 200;
493
- res.end(JSON.stringify({
494
- success: true,
495
- ...result
496
- }));
497
- } catch (error) {
498
- logger.error('Error updating project config:', error);
499
- res.statusCode = 500;
500
- res.end(JSON.stringify({
501
- error: error.message
502
- }));
503
- }
504
- return;
505
- }
506
- if (req.method === 'POST' && parsedUrl.pathname === '/api/config/global') {
507
- // Update global config
508
- if (!configService) {
509
- res.statusCode = 503;
510
- res.end(JSON.stringify({
511
- error: 'Config service not available'
512
- }));
513
- return;
514
- }
515
- try {
516
- const body = await parseRequestBody(req);
517
- const result = await configService.updateConfig('global', body);
518
- res.statusCode = 200;
519
- res.end(JSON.stringify({
520
- success: true,
521
- ...result
522
- }));
523
- } catch (error) {
524
- logger.error('Error updating global config:', error);
525
- res.statusCode = 500;
526
- res.end(JSON.stringify({
527
- error: error.message
528
- }));
529
- }
530
- return;
531
- }
532
- if (req.method === 'POST' && parsedUrl.pathname === '/api/config/validate') {
533
- // Validate config
534
- if (!configService) {
535
- res.statusCode = 503;
536
- res.end(JSON.stringify({
537
- error: 'Config service not available'
538
- }));
539
- return;
540
- }
541
- try {
542
- const body = await parseRequestBody(req);
543
- const result = await configService.validateConfig(body);
544
- res.statusCode = 200;
545
- res.end(JSON.stringify(result));
546
- } catch (error) {
547
- logger.error('Error validating config:', error);
548
- res.statusCode = 500;
549
- res.end(JSON.stringify({
550
- error: error.message
551
- }));
552
- }
553
- return;
554
- }
555
-
556
- // ===== AUTH ENDPOINTS =====
56
+ // Set default JSON content type
57
+ res.setHeader('Content-Type', 'application/json');
557
58
 
558
- if (req.method === 'GET' && parsedUrl.pathname === '/api/auth/status') {
559
- // Get auth status and user info
560
- if (!authService) {
561
- res.statusCode = 503;
562
- res.end(JSON.stringify({
563
- error: 'Auth service not available'
564
- }));
565
- return;
566
- }
567
- try {
568
- const isAuthenticated = await authService.isAuthenticated();
569
- let user = null;
570
- if (isAuthenticated) {
571
- const whoami = await authService.whoami();
572
- user = whoami.user;
573
- }
574
- res.statusCode = 200;
575
- res.end(JSON.stringify({
576
- authenticated: isAuthenticated,
577
- user
578
- }));
579
- } catch (error) {
580
- logger.error('Error getting auth status:', error);
581
- res.statusCode = 200;
582
- res.end(JSON.stringify({
583
- authenticated: false,
584
- user: null
585
- }));
586
- }
587
- return;
588
- }
589
- if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/login') {
590
- // Initiate device flow login
591
- if (!authService) {
592
- res.statusCode = 503;
593
- res.end(JSON.stringify({
594
- error: 'Auth service not available'
595
- }));
596
- return;
597
- }
598
- try {
599
- const deviceFlow = await authService.initiateDeviceFlow();
59
+ // Parse URL
60
+ let parsedUrl = new URL(req.url, `http://${req.headers.host}`);
61
+ let pathname = parsedUrl.pathname;
600
62
 
601
- // Transform snake_case to camelCase for frontend
602
- const response = {
603
- deviceCode: deviceFlow.device_code,
604
- userCode: deviceFlow.user_code,
605
- verificationUri: deviceFlow.verification_uri,
606
- verificationUriComplete: deviceFlow.verification_uri_complete,
607
- expiresIn: deviceFlow.expires_in,
608
- interval: deviceFlow.interval
609
- };
610
- res.statusCode = 200;
611
- res.end(JSON.stringify(response));
612
- } catch (error) {
613
- logger.error('Error initiating device flow:', error);
614
- res.statusCode = 500;
615
- res.end(JSON.stringify({
616
- error: error.message
617
- }));
618
- }
619
- return;
620
- }
621
- if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/poll') {
622
- // Poll device authorization status
623
- if (!authService) {
624
- res.statusCode = 503;
625
- res.end(JSON.stringify({
626
- error: 'Auth service not available'
627
- }));
628
- return;
629
- }
63
+ // Try each router in order
64
+ for (let router of routers) {
630
65
  try {
631
- const body = await parseRequestBody(req);
632
- const {
633
- deviceCode
634
- } = body;
635
- if (!deviceCode) {
636
- res.statusCode = 400;
637
- res.end(JSON.stringify({
638
- error: 'deviceCode is required'
639
- }));
66
+ let handled = await router(req, res, pathname, parsedUrl);
67
+ if (handled) {
640
68
  return;
641
69
  }
642
- let result;
643
- try {
644
- result = await authService.pollDeviceAuthorization(deviceCode);
645
- } catch (error) {
646
- // Handle "Authorization pending" as a valid response
647
- if (error.message && error.message.includes('Authorization pending')) {
648
- res.statusCode = 200;
649
- res.end(JSON.stringify({
650
- status: 'pending'
651
- }));
652
- return;
653
- }
654
- // Other errors are actual failures
655
- throw error;
656
- }
657
-
658
- // Check if authorization is complete by looking for tokens
659
- if (result.tokens && result.tokens.accessToken) {
660
- // Handle both snake_case and camelCase for token data
661
- let tokensData = result.tokens;
662
- let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
663
- let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : result.expires_at || result.expiresAt;
664
- let tokens = {
665
- accessToken: tokensData.accessToken || tokensData.access_token,
666
- refreshToken: tokensData.refreshToken || tokensData.refresh_token,
667
- expiresAt: tokenExpiresAt,
668
- user: result.user
669
- };
670
- await authService.completeDeviceFlow(tokens);
671
-
672
- // Return a simplified response to the client
673
- res.statusCode = 200;
674
- res.end(JSON.stringify({
675
- status: 'complete',
676
- user: result.user
677
- }));
678
- } else {
679
- // Still pending or other status
680
- res.statusCode = 200;
681
- res.end(JSON.stringify({
682
- status: 'pending'
683
- }));
684
- }
685
70
  } catch (error) {
686
- logger.error('Error polling device authorization:', error);
687
- res.statusCode = 500;
688
- res.end(JSON.stringify({
71
+ output.debug('server', `router error: ${pathname}`, {
689
72
  error: error.message
690
- }));
691
- }
692
- return;
693
- }
694
- if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/logout') {
695
- // Logout user
696
- if (!authService) {
697
- res.statusCode = 503;
698
- res.end(JSON.stringify({
699
- error: 'Auth service not available'
700
- }));
701
- return;
702
- }
703
- try {
704
- await authService.logout();
705
- res.statusCode = 200;
706
- res.end(JSON.stringify({
707
- success: true,
708
- message: 'Logged out successfully'
709
- }));
710
- } catch (error) {
711
- logger.error('Error logging out:', error);
712
- res.statusCode = 500;
713
- res.end(JSON.stringify({
714
- error: error.message
715
- }));
716
- }
717
- return;
718
- }
719
-
720
- // ===== PROJECT MANAGEMENT ENDPOINTS =====
721
-
722
- if (req.method === 'GET' && parsedUrl.pathname === '/api/projects') {
723
- // List all projects from API
724
- if (!projectService) {
725
- res.statusCode = 503;
726
- res.end(JSON.stringify({
727
- error: 'Project service not available'
728
- }));
729
- return;
730
- }
731
- try {
732
- const projects = await projectService.listProjects();
733
- res.statusCode = 200;
734
- res.end(JSON.stringify({
735
- projects
736
- }));
737
- } catch (error) {
738
- logger.error('Error listing projects:', error);
739
- res.statusCode = 500;
740
- res.end(JSON.stringify({
741
- error: error.message
742
- }));
743
- }
744
- return;
745
- }
746
- if (req.method === 'GET' && parsedUrl.pathname === '/api/projects/mappings') {
747
- // List project directory mappings
748
- if (!projectService) {
749
- res.statusCode = 503;
750
- res.end(JSON.stringify({
751
- error: 'Project service not available'
752
- }));
753
- return;
754
- }
755
- try {
756
- const mappings = await projectService.listMappings();
757
- res.statusCode = 200;
758
- res.end(JSON.stringify({
759
- mappings
760
- }));
761
- } catch (error) {
762
- logger.error('Error listing project mappings:', error);
763
- res.statusCode = 500;
764
- res.end(JSON.stringify({
765
- error: error.message
766
- }));
767
- }
768
- return;
769
- }
770
- if (req.method === 'POST' && parsedUrl.pathname === '/api/projects/mappings') {
771
- // Create or update project mapping
772
- if (!projectService) {
773
- res.statusCode = 503;
774
- res.end(JSON.stringify({
775
- error: 'Project service not available'
776
- }));
777
- return;
778
- }
779
- try {
780
- const body = await parseRequestBody(req);
781
- const {
782
- directory,
783
- projectSlug,
784
- organizationSlug,
785
- token,
786
- projectName
787
- } = body;
788
- const mapping = await projectService.createMapping(directory, {
789
- projectSlug,
790
- organizationSlug,
791
- token,
792
- projectName
793
73
  });
794
- res.statusCode = 200;
795
- res.end(JSON.stringify({
796
- success: true,
797
- mapping
798
- }));
799
- } catch (error) {
800
- logger.error('Error creating project mapping:', error);
801
- res.statusCode = 500;
802
- res.end(JSON.stringify({
803
- error: error.message
804
- }));
805
- }
806
- return;
807
- }
808
- if (req.method === 'DELETE' && parsedUrl.pathname.startsWith('/api/projects/mappings/')) {
809
- // Delete project mapping
810
- if (!projectService) {
811
- res.statusCode = 503;
812
- res.end(JSON.stringify({
813
- error: 'Project service not available'
814
- }));
815
- return;
816
- }
817
- try {
818
- const directory = decodeURIComponent(parsedUrl.pathname.replace('/api/projects/mappings/', ''));
819
- await projectService.removeMapping(directory);
820
- res.statusCode = 200;
821
- res.end(JSON.stringify({
822
- success: true,
823
- message: 'Mapping deleted'
824
- }));
825
- } catch (error) {
826
- logger.error('Error deleting project mapping:', error);
827
- res.statusCode = 500;
828
- res.end(JSON.stringify({
829
- error: error.message
830
- }));
831
- }
832
- return;
833
- }
834
- if (req.method === 'GET' && parsedUrl.pathname === '/api/builds/recent') {
835
- // Get recent builds for current project
836
- if (!projectService || !configService) {
837
- res.statusCode = 503;
838
- res.end(JSON.stringify({
839
- error: 'Required services not available'
840
- }));
74
+ sendError(res, 500, 'Internal server error');
841
75
  return;
842
76
  }
843
- try {
844
- const config = await configService.getConfig('merged');
845
- const {
846
- projectSlug,
847
- organizationSlug
848
- } = config.config;
849
- if (!projectSlug || !organizationSlug) {
850
- res.statusCode = 400;
851
- res.end(JSON.stringify({
852
- error: 'No project configured for this directory'
853
- }));
854
- return;
855
- }
856
- const limit = parseInt(parsedUrl.searchParams.get('limit') || '10', 10);
857
- const branch = parsedUrl.searchParams.get('branch') || undefined;
858
- const builds = await projectService.getRecentBuilds(projectSlug, organizationSlug, {
859
- limit,
860
- branch
861
- });
862
- res.statusCode = 200;
863
- res.end(JSON.stringify({
864
- builds
865
- }));
866
- } catch (error) {
867
- logger.error('Error fetching recent builds:', error);
868
- res.statusCode = 500;
869
- res.end(JSON.stringify({
870
- error: error.message
871
- }));
872
- }
873
- return;
874
77
  }
875
- res.statusCode = 404;
876
- res.end(JSON.stringify({
877
- error: 'Not found'
878
- }));
78
+
79
+ // No router handled the request
80
+ sendError(res, 404, 'Not found');
879
81
  };
880
- const start = () => {
82
+ let start = () => {
881
83
  return new Promise((resolve, reject) => {
882
84
  server = createServer(async (req, res) => {
883
85
  try {
884
86
  await handleRequest(req, res);
885
87
  } catch (error) {
886
- logger.error('Server error:', error);
88
+ output.debug('server', 'error', {
89
+ error: error.message
90
+ });
887
91
  res.statusCode = 500;
888
92
  res.setHeader('Content-Type', 'application/json');
889
93
  res.end(JSON.stringify({
@@ -895,7 +99,7 @@ export const createHttpServer = (port, screenshotHandler, services = {}) => {
895
99
  if (error) {
896
100
  reject(error);
897
101
  } else {
898
- logger.debug(`HTTP server listening on http://127.0.0.1:${port}`);
102
+ output.debug('server', `listening on :${port}`);
899
103
  resolve();
900
104
  }
901
105
  });
@@ -908,12 +112,12 @@ export const createHttpServer = (port, screenshotHandler, services = {}) => {
908
112
  });
909
113
  });
910
114
  };
911
- const stop = () => {
115
+ let stop = () => {
912
116
  if (server) {
913
117
  return new Promise(resolve => {
914
118
  server.close(() => {
915
119
  server = null;
916
- logger.debug('HTTP server stopped');
120
+ output.debug('server', 'stopped');
917
121
  resolve();
918
122
  });
919
123
  });
@@ -925,16 +129,18 @@ export const createHttpServer = (port, screenshotHandler, services = {}) => {
925
129
  * Finish build - flush any pending background operations
926
130
  * Call this before finalizing a build to ensure all uploads complete
927
131
  */
928
- const finishBuild = async buildId => {
929
- logger.debug(`Finishing build ${buildId}...`);
930
-
132
+ let finishBuild = async _buildId => {
931
133
  // Flush screenshot handler if it has a flush method (API mode)
932
134
  if (screenshotHandler?.flush) {
933
- const stats = await screenshotHandler.flush();
934
- logger.debug(`Build ${buildId} uploads complete: ${stats.uploaded} uploaded, ${stats.failed} failed`);
135
+ let stats = await screenshotHandler.flush();
136
+ if (stats.uploaded > 0 || stats.failed > 0) {
137
+ output.debug('upload', 'flushed', {
138
+ uploaded: stats.uploaded,
139
+ failed: stats.failed
140
+ });
141
+ }
935
142
  return stats;
936
143
  }
937
- logger.debug(`Build ${buildId} finished (no flush needed)`);
938
144
  return null;
939
145
  };
940
146
  return {