@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.
- package/dist/cli.js +68 -68
- package/dist/commands/doctor.js +30 -34
- package/dist/commands/finalize.js +24 -23
- package/dist/commands/init.js +30 -28
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +14 -19
- package/dist/commands/project.js +83 -103
- package/dist/commands/run.js +77 -89
- package/dist/commands/status.js +48 -49
- package/dist/commands/tdd-daemon.js +90 -86
- package/dist/commands/tdd.js +59 -88
- package/dist/commands/upload.js +57 -57
- package/dist/commands/whoami.js +40 -45
- package/dist/index.js +2 -5
- package/dist/plugin-loader.js +15 -17
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +74 -41
- package/dist/sdk/index.js +36 -45
- package/dist/server/handlers/api-handler.js +14 -15
- package/dist/server/handlers/tdd-handler.js +34 -37
- package/dist/server/http-server.js +75 -869
- package/dist/server/middleware/cors.js +22 -0
- package/dist/server/middleware/json-parser.js +35 -0
- package/dist/server/middleware/response.js +79 -0
- package/dist/server/routers/assets.js +91 -0
- package/dist/server/routers/auth.js +144 -0
- package/dist/server/routers/baseline.js +163 -0
- package/dist/server/routers/cloud-proxy.js +146 -0
- package/dist/server/routers/config.js +126 -0
- package/dist/server/routers/dashboard.js +130 -0
- package/dist/server/routers/health.js +61 -0
- package/dist/server/routers/projects.js +168 -0
- package/dist/server/routers/screenshot.js +86 -0
- package/dist/services/auth-service.js +1 -1
- package/dist/services/build-manager.js +13 -40
- package/dist/services/config-service.js +2 -4
- package/dist/services/html-report-generator.js +6 -5
- package/dist/services/index.js +64 -0
- package/dist/services/project-service.js +121 -40
- package/dist/services/screenshot-server.js +9 -9
- package/dist/services/server-manager.js +11 -18
- package/dist/services/static-report-generator.js +3 -4
- package/dist/services/tdd-service.js +246 -103
- package/dist/services/test-runner.js +24 -25
- package/dist/services/uploader.js +5 -4
- package/dist/types/commands/init.d.ts +1 -2
- package/dist/types/index.d.ts +2 -3
- package/dist/types/plugin-loader.d.ts +1 -2
- package/dist/types/reporter/src/api/client.d.ts +178 -0
- package/dist/types/reporter/src/components/app-router.d.ts +1 -3
- package/dist/types/reporter/src/components/code-block.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +10 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +11 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +14 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +30 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +8 -0
- package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
- package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
- package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
- package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
- package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
- package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
- package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
- package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
- package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
- package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
- package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
- package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
- package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
- package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
- package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
- package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
- package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
- package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +1 -4
- package/dist/types/reporter/src/components/views/comparisons-view.d.ts +1 -6
- package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
- package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
- package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
- package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
- package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
- package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
- package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
- package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
- package/dist/types/sdk/index.d.ts +2 -4
- package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
- package/dist/types/server/http-server.d.ts +1 -1
- package/dist/types/server/middleware/cors.d.ts +11 -0
- package/dist/types/server/middleware/json-parser.d.ts +10 -0
- package/dist/types/server/middleware/response.d.ts +50 -0
- package/dist/types/server/routers/assets.d.ts +6 -0
- package/dist/types/server/routers/auth.d.ts +9 -0
- package/dist/types/server/routers/baseline.d.ts +13 -0
- package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
- package/dist/types/server/routers/config.d.ts +9 -0
- package/dist/types/server/routers/dashboard.d.ts +6 -0
- package/dist/types/server/routers/health.d.ts +11 -0
- package/dist/types/server/routers/projects.d.ts +9 -0
- package/dist/types/server/routers/screenshot.d.ts +11 -0
- package/dist/types/services/build-manager.d.ts +4 -3
- package/dist/types/services/config-service.d.ts +2 -3
- package/dist/types/services/index.d.ts +7 -0
- package/dist/types/services/project-service.d.ts +6 -4
- package/dist/types/services/screenshot-server.d.ts +5 -5
- package/dist/types/services/server-manager.d.ts +5 -3
- package/dist/types/services/tdd-service.d.ts +12 -1
- package/dist/types/services/test-runner.d.ts +3 -3
- package/dist/types/utils/output.d.ts +84 -0
- package/dist/utils/config-loader.js +24 -48
- package/dist/utils/global-config.js +2 -17
- package/dist/utils/output.js +445 -0
- package/dist/utils/security.js +3 -4
- package/docs/api-reference.md +0 -1
- package/docs/plugins.md +22 -22
- package/package.json +3 -2
- package/dist/container/index.js +0 -215
- package/dist/services/base-service.js +0 -154
- package/dist/types/container/index.d.ts +0 -59
- package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
- package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
- package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
- package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
- package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
- package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
- package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
- package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
- package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
- package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
- package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
- package/dist/types/services/base-service.d.ts +0 -71
- package/dist/types/utils/console-ui.d.ts +0 -61
- package/dist/types/utils/logger-factory.d.ts +0 -26
- package/dist/types/utils/logger.d.ts +0 -79
- package/dist/utils/console-ui.js +0 -241
- package/dist/utils/logger-factory.js +0 -76
- 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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
89
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}));
|
|
78
|
+
|
|
79
|
+
// No router handled the request
|
|
80
|
+
sendError(res, 404, 'Not found');
|
|
879
81
|
};
|
|
880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
let stop = () => {
|
|
912
116
|
if (server) {
|
|
913
117
|
return new Promise(resolve => {
|
|
914
118
|
server.close(() => {
|
|
915
119
|
server = null;
|
|
916
|
-
|
|
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
|
-
|
|
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
|
-
|
|
934
|
-
|
|
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 {
|