@vizzly-testing/cli 0.16.4 → 0.18.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/README.md +4 -4
- package/claude-plugin/skills/debug-visual-regression/SKILL.md +2 -2
- package/dist/cli.js +84 -58
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +18 -17
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +30 -30
- package/dist/commands/login.js +23 -23
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +36 -36
- package/dist/commands/run.js +33 -33
- package/dist/commands/status.js +14 -14
- package/dist/commands/tdd-daemon.js +43 -43
- package/dist/commands/tdd.js +27 -27
- package/dist/commands/upload.js +33 -33
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-loader.js +28 -28
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/sdk/index.js +33 -35
- package/dist/server/handlers/api-handler.js +4 -4
- package/dist/server/handlers/tdd-handler.js +12 -12
- package/dist/server/http-server.js +21 -22
- package/dist/server/middleware/json-parser.js +1 -1
- package/dist/server/routers/assets.js +14 -14
- package/dist/server/routers/auth.js +14 -14
- package/dist/server/routers/baseline.js +8 -8
- package/dist/server/routers/cloud-proxy.js +15 -15
- package/dist/server/routers/config.js +11 -11
- package/dist/server/routers/dashboard.js +11 -11
- package/dist/server/routers/health.js +4 -4
- package/dist/server/routers/projects.js +19 -19
- package/dist/server/routers/screenshot.js +9 -9
- package/dist/services/api-service.js +16 -16
- package/dist/services/auth-service.js +17 -17
- package/dist/services/build-manager.js +3 -3
- package/dist/services/config-service.js +33 -33
- package/dist/services/html-report-generator.js +8 -8
- package/dist/services/index.js +11 -11
- package/dist/services/project-service.js +19 -19
- package/dist/services/report-generator/report.css +3 -3
- package/dist/services/report-generator/viewer.js +25 -23
- package/dist/services/screenshot-server.js +1 -1
- package/dist/services/server-manager.js +5 -5
- package/dist/services/static-report-generator.js +14 -14
- package/dist/services/tdd-service.js +101 -95
- package/dist/services/test-runner.js +14 -4
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/utils/browser.js +3 -3
- package/dist/utils/build-history.js +12 -12
- package/dist/utils/config-loader.js +19 -19
- package/dist/utils/config-schema.js +10 -9
- package/dist/utils/environment-config.js +11 -0
- package/dist/utils/fetch-utils.js +2 -2
- package/dist/utils/file-helpers.js +2 -2
- package/dist/utils/git.js +3 -6
- package/dist/utils/global-config.js +28 -25
- package/dist/utils/output.js +136 -28
- package/dist/utils/package-info.js +3 -3
- package/dist/utils/security.js +12 -12
- package/docs/api-reference.md +56 -27
- package/docs/doctor-command.md +1 -1
- package/docs/tdd-mode.md +3 -3
- package/package.json +9 -13
package/dist/sdk/index.js
CHANGED
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
* @description Full SDK for custom integrations and advanced usage
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { EventEmitter } from 'events';
|
|
14
|
-
import {
|
|
15
|
-
import { createUploader } from '../services/uploader.js';
|
|
16
|
-
import { createTDDService } from '../services/tdd-service.js';
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
17
15
|
import { ScreenshotServer } from '../services/screenshot-server.js';
|
|
16
|
+
import { createTDDService } from '../services/tdd-service.js';
|
|
17
|
+
import { createUploader } from '../services/uploader.js';
|
|
18
18
|
import { loadConfig } from '../utils/config-loader.js';
|
|
19
|
+
import { resolveImageBuffer } from '../utils/file-helpers.js';
|
|
19
20
|
import * as output from '../utils/output.js';
|
|
20
|
-
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Create a new Vizzly instance with custom configuration
|
|
@@ -59,15 +59,15 @@ export function createVizzly(config = {}, options = {}) {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
// Merge with loaded config
|
|
62
|
-
|
|
62
|
+
const resolvedConfig = {
|
|
63
63
|
...config
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Initialize SDK with config loading
|
|
68
68
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const init = async () => {
|
|
70
|
+
const fileConfig = await loadConfig();
|
|
71
71
|
Object.assign(resolvedConfig, fileConfig, config); // CLI config takes precedence
|
|
72
72
|
return resolvedConfig;
|
|
73
73
|
};
|
|
@@ -75,7 +75,7 @@ export function createVizzly(config = {}, options = {}) {
|
|
|
75
75
|
/**
|
|
76
76
|
* Create uploader service
|
|
77
77
|
*/
|
|
78
|
-
|
|
78
|
+
const createUploaderService = (uploaderOptions = {}) => {
|
|
79
79
|
return createUploader({
|
|
80
80
|
apiKey: resolvedConfig.apiKey,
|
|
81
81
|
apiUrl: resolvedConfig.apiUrl
|
|
@@ -88,7 +88,7 @@ export function createVizzly(config = {}, options = {}) {
|
|
|
88
88
|
/**
|
|
89
89
|
* Create TDD service
|
|
90
90
|
*/
|
|
91
|
-
|
|
91
|
+
const createTDDServiceInstance = (tddOptions = {}) => {
|
|
92
92
|
return createTDDService(resolvedConfig, {
|
|
93
93
|
...options,
|
|
94
94
|
...tddOptions
|
|
@@ -98,16 +98,16 @@ export function createVizzly(config = {}, options = {}) {
|
|
|
98
98
|
/**
|
|
99
99
|
* Upload screenshots (convenience method)
|
|
100
100
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const upload = async uploadOptions => {
|
|
102
|
+
const uploader = createUploaderService();
|
|
103
103
|
return uploader.upload(uploadOptions);
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* Start TDD mode (convenience method)
|
|
108
108
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
const startTDD = async (tddOptions = {}) => {
|
|
110
|
+
const tddService = createTDDServiceInstance();
|
|
111
111
|
return tddService.start(tddOptions);
|
|
112
112
|
};
|
|
113
113
|
return {
|
|
@@ -195,7 +195,7 @@ export class VizzlySDK extends EventEmitter {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
// Create a simple build manager for screenshot collection
|
|
198
|
-
|
|
198
|
+
const buildManager = {
|
|
199
199
|
screenshots: new Map(),
|
|
200
200
|
currentBuildId: null,
|
|
201
201
|
async addScreenshot(buildId, screenshot) {
|
|
@@ -210,8 +210,8 @@ export class VizzlySDK extends EventEmitter {
|
|
|
210
210
|
};
|
|
211
211
|
this.server = new ScreenshotServer(this.config, buildManager);
|
|
212
212
|
await this.server.start();
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const port = this.config.server?.port || 3000;
|
|
214
|
+
const serverInfo = {
|
|
215
215
|
port,
|
|
216
216
|
url: `http://localhost:${port}`
|
|
217
217
|
};
|
|
@@ -235,15 +235,15 @@ export class VizzlySDK extends EventEmitter {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// Resolve Buffer or file path using shared utility
|
|
238
|
-
|
|
238
|
+
const buffer = resolveImageBuffer(imageBuffer, 'screenshot');
|
|
239
239
|
|
|
240
240
|
// Generate or use provided build ID
|
|
241
|
-
|
|
241
|
+
const buildId = options.buildId || this.currentBuildId || 'default';
|
|
242
242
|
this.currentBuildId = buildId;
|
|
243
243
|
|
|
244
244
|
// Convert Buffer to base64 for JSON transport
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
const imageBase64 = buffer.toString('base64');
|
|
246
|
+
const screenshotData = {
|
|
247
247
|
buildId,
|
|
248
248
|
name,
|
|
249
249
|
image: imageBase64,
|
|
@@ -251,9 +251,9 @@ export class VizzlySDK extends EventEmitter {
|
|
|
251
251
|
};
|
|
252
252
|
|
|
253
253
|
// POST to the local screenshot server
|
|
254
|
-
|
|
254
|
+
const serverUrl = `http://localhost:${this.config.server?.port || 3000}`;
|
|
255
255
|
try {
|
|
256
|
-
|
|
256
|
+
const response = await fetch(`${serverUrl}/screenshot`, {
|
|
257
257
|
method: 'POST',
|
|
258
258
|
headers: {
|
|
259
259
|
'Content-Type': 'application/json'
|
|
@@ -261,7 +261,7 @@ export class VizzlySDK extends EventEmitter {
|
|
|
261
261
|
body: JSON.stringify(screenshotData)
|
|
262
262
|
});
|
|
263
263
|
if (!response.ok) {
|
|
264
|
-
|
|
264
|
+
const errorData = await response.json().catch(() => ({
|
|
265
265
|
error: 'Unknown error'
|
|
266
266
|
}));
|
|
267
267
|
throw new VizzlyError(`Screenshot capture failed: ${errorData.error}`, 'SCREENSHOT_FAILED', {
|
|
@@ -302,8 +302,8 @@ export class VizzlySDK extends EventEmitter {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// Get the screenshots directory from config or default
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
const screenshotsDir = options.screenshotsDir || this.config?.upload?.screenshotsDir || './screenshots';
|
|
306
|
+
const uploadOptions = {
|
|
307
307
|
screenshotsDir,
|
|
308
308
|
buildName: options.buildName || this.config.buildName,
|
|
309
309
|
branch: options.branch || this.config.branch,
|
|
@@ -319,7 +319,7 @@ export class VizzlySDK extends EventEmitter {
|
|
|
319
319
|
}
|
|
320
320
|
};
|
|
321
321
|
try {
|
|
322
|
-
|
|
322
|
+
const result = await this.services.uploader.upload(uploadOptions);
|
|
323
323
|
this.emit('upload:completed', result);
|
|
324
324
|
return result;
|
|
325
325
|
} catch (error) {
|
|
@@ -343,9 +343,9 @@ export class VizzlySDK extends EventEmitter {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
// Resolve Buffer or file path using shared utility
|
|
346
|
-
|
|
346
|
+
const buffer = resolveImageBuffer(imageBuffer, 'compare');
|
|
347
347
|
try {
|
|
348
|
-
|
|
348
|
+
const result = await this.services.tddService.compareScreenshot(name, buffer);
|
|
349
349
|
this.emit('comparison:completed', result);
|
|
350
350
|
return result;
|
|
351
351
|
} catch (error) {
|
|
@@ -357,11 +357,9 @@ export class VizzlySDK extends EventEmitter {
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
|
|
361
|
-
// Re-export key utilities and errors
|
|
362
|
-
export { loadConfig } from '../utils/config-loader.js';
|
|
363
|
-
export * as output from '../utils/output.js';
|
|
364
|
-
|
|
360
|
+
export { createTDDService } from '../services/tdd-service.js';
|
|
365
361
|
// Export service creators for advanced usage
|
|
366
362
|
export { createUploader } from '../services/uploader.js';
|
|
367
|
-
export
|
|
363
|
+
// Re-export key utilities and errors
|
|
364
|
+
export { loadConfig } from '../utils/config-loader.js';
|
|
365
|
+
export * as output from '../utils/output.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Buffer } from 'buffer';
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { resolve } from 'path';
|
|
4
|
-
import * as output from '../../utils/output.js';
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
5
4
|
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
5
|
+
import * as output from '../../utils/output.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* API Handler - Non-blocking screenshot upload
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Buffer } from 'buffer';
|
|
2
|
-
import {
|
|
3
|
-
import { join, resolve } from 'path';
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
4
|
import { getDimensionsSync } from '@vizzly-testing/honeydiff';
|
|
5
|
-
import * as output from '../../utils/output.js';
|
|
6
5
|
import { TddService } from '../../services/tdd-service.js';
|
|
7
|
-
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
8
6
|
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
7
|
+
import * as output from '../../utils/output.js';
|
|
8
|
+
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Group comparisons by screenshot name with variant structure
|
|
@@ -141,7 +141,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
141
141
|
if (existingIndex >= 0) {
|
|
142
142
|
// Preserve initialStatus from the original comparison
|
|
143
143
|
// This keeps sort order stable when status changes (e.g., after approval)
|
|
144
|
-
|
|
144
|
+
const initialStatus = reportData.comparisons[existingIndex].initialStatus;
|
|
145
145
|
reportData.comparisons[existingIndex] = {
|
|
146
146
|
...newComparison,
|
|
147
147
|
initialStatus: initialStatus || newComparison.status
|
|
@@ -200,7 +200,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
200
200
|
output.debug('tdd', `using baseline: ${baseline.buildName}`);
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
|
-
const handleScreenshot = async (
|
|
203
|
+
const handleScreenshot = async (_buildId, name, image, properties = {}) => {
|
|
204
204
|
// Validate and sanitize screenshot name
|
|
205
205
|
let sanitizedName;
|
|
206
206
|
try {
|
|
@@ -217,7 +217,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
// Unwrap double-nested properties if needed (client SDK wraps options in properties field)
|
|
220
|
-
// This happens when test helper passes { properties: {...}, threshold: 0
|
|
220
|
+
// This happens when test helper passes { properties: {...}, threshold: 2.0 }
|
|
221
221
|
// and client SDK wraps it as { properties: options }
|
|
222
222
|
let unwrappedProperties = properties;
|
|
223
223
|
if (properties.properties && typeof properties.properties === 'object') {
|
|
@@ -504,7 +504,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
504
504
|
try {
|
|
505
505
|
const {
|
|
506
506
|
unlinkSync
|
|
507
|
-
} = await import('fs');
|
|
507
|
+
} = await import('node:fs');
|
|
508
508
|
unlinkSync(baselinePath);
|
|
509
509
|
deletedBaselines++;
|
|
510
510
|
// Silent deletion
|
|
@@ -521,7 +521,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
521
521
|
try {
|
|
522
522
|
const {
|
|
523
523
|
unlinkSync
|
|
524
|
-
} = await import('fs');
|
|
524
|
+
} = await import('node:fs');
|
|
525
525
|
unlinkSync(currentPath);
|
|
526
526
|
deletedCurrents++;
|
|
527
527
|
// Silent deletion
|
|
@@ -538,7 +538,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
538
538
|
try {
|
|
539
539
|
const {
|
|
540
540
|
unlinkSync
|
|
541
|
-
} = await import('fs');
|
|
541
|
+
} = await import('node:fs');
|
|
542
542
|
unlinkSync(diffPath);
|
|
543
543
|
deletedDiffs++;
|
|
544
544
|
output.debug(`Deleted diff for ${comparison.name}`);
|
|
@@ -555,7 +555,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
555
555
|
try {
|
|
556
556
|
const {
|
|
557
557
|
unlinkSync
|
|
558
|
-
} = await import('fs');
|
|
558
|
+
} = await import('node:fs');
|
|
559
559
|
unlinkSync(metadataPath);
|
|
560
560
|
output.debug('Deleted baseline metadata');
|
|
561
561
|
} catch (error) {
|
|
@@ -3,29 +3,28 @@
|
|
|
3
3
|
* Thin dispatcher that routes requests to modular routers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createServer } from 'http';
|
|
6
|
+
import { createServer } from 'node:http';
|
|
7
7
|
import * as output from '../utils/output.js';
|
|
8
8
|
|
|
9
9
|
// Middleware
|
|
10
10
|
import { corsMiddleware } from './middleware/cors.js';
|
|
11
11
|
import { sendError } from './middleware/response.js';
|
|
12
|
-
|
|
13
|
-
// Routers
|
|
14
|
-
import { createHealthRouter } from './routers/health.js';
|
|
15
12
|
import { createAssetsRouter } from './routers/assets.js';
|
|
16
|
-
import {
|
|
17
|
-
import { createScreenshotRouter } from './routers/screenshot.js';
|
|
13
|
+
import { createAuthRouter } from './routers/auth.js';
|
|
18
14
|
import { createBaselineRouter } from './routers/baseline.js';
|
|
15
|
+
import { createCloudProxyRouter } from './routers/cloud-proxy.js';
|
|
19
16
|
import { createConfigRouter } from './routers/config.js';
|
|
20
|
-
import {
|
|
17
|
+
import { createDashboardRouter } from './routers/dashboard.js';
|
|
18
|
+
// Routers
|
|
19
|
+
import { createHealthRouter } from './routers/health.js';
|
|
21
20
|
import { createProjectsRouter } from './routers/projects.js';
|
|
22
|
-
import {
|
|
23
|
-
export
|
|
21
|
+
import { createScreenshotRouter } from './routers/screenshot.js';
|
|
22
|
+
export const createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
24
23
|
let server = null;
|
|
25
|
-
|
|
24
|
+
const defaultBuildId = services.buildId || null;
|
|
26
25
|
|
|
27
26
|
// Extract services
|
|
28
|
-
|
|
27
|
+
const {
|
|
29
28
|
configService,
|
|
30
29
|
authService,
|
|
31
30
|
projectService,
|
|
@@ -33,7 +32,7 @@ export let createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
33
32
|
} = services;
|
|
34
33
|
|
|
35
34
|
// Create router context
|
|
36
|
-
|
|
35
|
+
const routerContext = {
|
|
37
36
|
port,
|
|
38
37
|
screenshotHandler,
|
|
39
38
|
defaultBuildId,
|
|
@@ -45,9 +44,9 @@ export let createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
45
44
|
};
|
|
46
45
|
|
|
47
46
|
// Initialize routers
|
|
48
|
-
|
|
47
|
+
const 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
48
|
];
|
|
50
|
-
|
|
49
|
+
const handleRequest = async (req, res) => {
|
|
51
50
|
// Apply CORS middleware
|
|
52
51
|
if (corsMiddleware(req, res)) {
|
|
53
52
|
return;
|
|
@@ -57,13 +56,13 @@ export let createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
57
56
|
res.setHeader('Content-Type', 'application/json');
|
|
58
57
|
|
|
59
58
|
// Parse URL
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
|
60
|
+
const pathname = parsedUrl.pathname;
|
|
62
61
|
|
|
63
62
|
// Try each router in order
|
|
64
|
-
for (
|
|
63
|
+
for (const router of routers) {
|
|
65
64
|
try {
|
|
66
|
-
|
|
65
|
+
const handled = await router(req, res, pathname, parsedUrl);
|
|
67
66
|
if (handled) {
|
|
68
67
|
return;
|
|
69
68
|
}
|
|
@@ -79,7 +78,7 @@ export let createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
79
78
|
// No router handled the request
|
|
80
79
|
sendError(res, 404, 'Not found');
|
|
81
80
|
};
|
|
82
|
-
|
|
81
|
+
const start = () => {
|
|
83
82
|
return new Promise((resolve, reject) => {
|
|
84
83
|
server = createServer(async (req, res) => {
|
|
85
84
|
try {
|
|
@@ -112,7 +111,7 @@ export let createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
112
111
|
});
|
|
113
112
|
});
|
|
114
113
|
};
|
|
115
|
-
|
|
114
|
+
const stop = () => {
|
|
116
115
|
if (server) {
|
|
117
116
|
return new Promise(resolve => {
|
|
118
117
|
server.close(() => {
|
|
@@ -129,10 +128,10 @@ export let createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
129
128
|
* Finish build - flush any pending background operations
|
|
130
129
|
* Call this before finalizing a build to ensure all uploads complete
|
|
131
130
|
*/
|
|
132
|
-
|
|
131
|
+
const finishBuild = async _buildId => {
|
|
133
132
|
// Flush screenshot handler if it has a flush method (API mode)
|
|
134
133
|
if (screenshotHandler?.flush) {
|
|
135
|
-
|
|
134
|
+
const stats = await screenshotHandler.flush();
|
|
136
135
|
if (stats.uploaded > 0 || stats.failed > 0) {
|
|
137
136
|
output.debug('upload', 'flushed', {
|
|
138
137
|
uploaded: stats.uploaded,
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* Serves static assets (bundle files, images)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
9
|
-
import { sendFile, sendError, sendNotFound } from '../middleware/response.js';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
10
9
|
import * as output from '../../utils/output.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import { sendError, sendFile, sendNotFound } from '../middleware/response.js';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const PROJECT_ROOT = join(__dirname, '..', '..', '..');
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Create assets router
|
|
@@ -25,10 +25,10 @@ export function createAssetsRouter() {
|
|
|
25
25
|
|
|
26
26
|
// Serve React bundle JS
|
|
27
27
|
if (pathname === '/reporter-bundle.js') {
|
|
28
|
-
|
|
28
|
+
const bundlePath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.iife.js');
|
|
29
29
|
if (existsSync(bundlePath)) {
|
|
30
30
|
try {
|
|
31
|
-
|
|
31
|
+
const bundle = readFileSync(bundlePath, 'utf8');
|
|
32
32
|
sendFile(res, bundle, 'application/javascript');
|
|
33
33
|
return true;
|
|
34
34
|
} catch (error) {
|
|
@@ -46,10 +46,10 @@ export function createAssetsRouter() {
|
|
|
46
46
|
|
|
47
47
|
// Serve React bundle CSS
|
|
48
48
|
if (pathname === '/reporter-bundle.css') {
|
|
49
|
-
|
|
49
|
+
const cssPath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.css');
|
|
50
50
|
if (existsSync(cssPath)) {
|
|
51
51
|
try {
|
|
52
|
-
|
|
52
|
+
const css = readFileSync(cssPath, 'utf8');
|
|
53
53
|
sendFile(res, css, 'text/css');
|
|
54
54
|
return true;
|
|
55
55
|
} catch (error) {
|
|
@@ -67,11 +67,11 @@ export function createAssetsRouter() {
|
|
|
67
67
|
|
|
68
68
|
// Serve images from .vizzly directory
|
|
69
69
|
if (pathname.startsWith('/images/')) {
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const imagePath = pathname.replace('/images/', '');
|
|
71
|
+
const fullImagePath = join(process.cwd(), '.vizzly', imagePath);
|
|
72
72
|
if (existsSync(fullImagePath)) {
|
|
73
73
|
try {
|
|
74
|
-
|
|
74
|
+
const imageData = readFileSync(fullImagePath);
|
|
75
75
|
sendFile(res, imageData, 'image/png');
|
|
76
76
|
return true;
|
|
77
77
|
} catch (error) {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Handles authentication endpoints (device flow login, logout, status)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
7
|
-
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
8
6
|
import * as output from '../../utils/output.js';
|
|
7
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
8
|
+
import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Create auth router
|
|
@@ -26,10 +26,10 @@ export function createAuthRouter({
|
|
|
26
26
|
// Get auth status and user info
|
|
27
27
|
if (req.method === 'GET' && pathname === '/api/auth/status') {
|
|
28
28
|
try {
|
|
29
|
-
|
|
29
|
+
const isAuthenticated = await authService.isAuthenticated();
|
|
30
30
|
let user = null;
|
|
31
31
|
if (isAuthenticated) {
|
|
32
|
-
|
|
32
|
+
const whoami = await authService.whoami();
|
|
33
33
|
user = whoami.user;
|
|
34
34
|
}
|
|
35
35
|
sendSuccess(res, {
|
|
@@ -50,10 +50,10 @@ export function createAuthRouter({
|
|
|
50
50
|
// Initiate device flow login
|
|
51
51
|
if (req.method === 'POST' && pathname === '/api/auth/login') {
|
|
52
52
|
try {
|
|
53
|
-
|
|
53
|
+
const deviceFlow = await authService.initiateDeviceFlow();
|
|
54
54
|
|
|
55
55
|
// Transform snake_case to camelCase for frontend
|
|
56
|
-
|
|
56
|
+
const response = {
|
|
57
57
|
deviceCode: deviceFlow.device_code,
|
|
58
58
|
userCode: deviceFlow.user_code,
|
|
59
59
|
verificationUri: deviceFlow.verification_uri,
|
|
@@ -73,8 +73,8 @@ export function createAuthRouter({
|
|
|
73
73
|
// Poll device authorization status
|
|
74
74
|
if (req.method === 'POST' && pathname === '/api/auth/poll') {
|
|
75
75
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const body = await parseJsonBody(req);
|
|
77
|
+
const {
|
|
78
78
|
deviceCode
|
|
79
79
|
} = body;
|
|
80
80
|
if (!deviceCode) {
|
|
@@ -86,7 +86,7 @@ export function createAuthRouter({
|
|
|
86
86
|
result = await authService.pollDeviceAuthorization(deviceCode);
|
|
87
87
|
} catch (error) {
|
|
88
88
|
// Handle "Authorization pending" as a valid response
|
|
89
|
-
if (error.message
|
|
89
|
+
if (error.message?.includes('Authorization pending')) {
|
|
90
90
|
sendSuccess(res, {
|
|
91
91
|
status: 'pending'
|
|
92
92
|
});
|
|
@@ -96,11 +96,11 @@ export function createAuthRouter({
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Check if authorization is complete by looking for tokens
|
|
99
|
-
if (result.tokens
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
if (result.tokens?.accessToken) {
|
|
100
|
+
const tokensData = result.tokens;
|
|
101
|
+
const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
102
|
+
const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : result.expires_at || result.expiresAt;
|
|
103
|
+
const tokens = {
|
|
104
104
|
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
105
105
|
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
106
106
|
expiresAt: tokenExpiresAt,
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Handles baseline management (accept, accept-all, reset)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
7
|
-
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
8
6
|
import * as output from '../../utils/output.js';
|
|
7
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
8
|
+
import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Create baseline router
|
|
@@ -28,7 +28,7 @@ export function createBaselineRouter({
|
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
30
|
try {
|
|
31
|
-
|
|
31
|
+
const {
|
|
32
32
|
id
|
|
33
33
|
} = await parseJsonBody(req);
|
|
34
34
|
if (!id) {
|
|
@@ -55,7 +55,7 @@ export function createBaselineRouter({
|
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
try {
|
|
58
|
-
|
|
58
|
+
const result = await screenshotHandler.acceptAllBaselines();
|
|
59
59
|
sendSuccess(res, {
|
|
60
60
|
success: true,
|
|
61
61
|
message: `Accepted ${result.count} baselines`,
|
|
@@ -96,8 +96,8 @@ export function createBaselineRouter({
|
|
|
96
96
|
return true;
|
|
97
97
|
}
|
|
98
98
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const body = await parseJsonBody(req);
|
|
100
|
+
const {
|
|
101
101
|
buildId,
|
|
102
102
|
organizationSlug,
|
|
103
103
|
projectSlug
|
|
@@ -111,7 +111,7 @@ export function createBaselineRouter({
|
|
|
111
111
|
// If organizationSlug and projectSlug are provided, use OAuth-based download
|
|
112
112
|
if (organizationSlug && projectSlug && authService) {
|
|
113
113
|
try {
|
|
114
|
-
|
|
114
|
+
const result = await tddService.downloadBaselinesWithAuth(buildId, organizationSlug, projectSlug, authService);
|
|
115
115
|
sendSuccess(res, {
|
|
116
116
|
success: true,
|
|
117
117
|
message: `Baselines downloaded from build ${buildId}`,
|
|
@@ -138,7 +138,7 @@ export function createBaselineRouter({
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// Fall back to API token-based download (when no OAuth info or OAuth auth failed)
|
|
141
|
-
|
|
141
|
+
const result = await tddService.downloadBaselines('test',
|
|
142
142
|
// environment
|
|
143
143
|
null,
|
|
144
144
|
// branch (not needed when buildId is specified)
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - Returns proxied response to React app
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
13
|
-
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
14
12
|
import * as output from '../../utils/output.js';
|
|
13
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
14
|
+
import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Create cloud proxy router
|
|
@@ -53,7 +53,7 @@ export function createCloudProxyRouter({
|
|
|
53
53
|
// Route: GET /api/cloud/projects - List user's projects
|
|
54
54
|
if (req.method === 'GET' && pathname === '/api/cloud/projects') {
|
|
55
55
|
try {
|
|
56
|
-
|
|
56
|
+
const response = await proxyRequest('/api/cli/projects', {
|
|
57
57
|
method: 'GET'
|
|
58
58
|
});
|
|
59
59
|
sendSuccess(res, {
|
|
@@ -76,19 +76,19 @@ export function createCloudProxyRouter({
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// Route: GET /api/cloud/organizations/:org/projects/:project/builds
|
|
79
|
-
|
|
79
|
+
const buildsMatch = pathname.match(/^\/api\/cloud\/organizations\/([^/]+)\/projects\/([^/]+)\/builds$/);
|
|
80
80
|
if (req.method === 'GET' && buildsMatch) {
|
|
81
81
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
const organizationSlug = decodeURIComponent(buildsMatch[1]);
|
|
83
|
+
const projectSlug = decodeURIComponent(buildsMatch[2]);
|
|
84
|
+
const limit = parsedUrl.searchParams.get('limit') || '20';
|
|
85
|
+
const branch = parsedUrl.searchParams.get('branch');
|
|
86
|
+
const queryParams = new URLSearchParams();
|
|
87
87
|
if (limit) queryParams.append('limit', limit);
|
|
88
88
|
if (branch) queryParams.append('branch', branch);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
const query = queryParams.toString();
|
|
90
|
+
const endpoint = `/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/builds${query ? `?${query}` : ''}`;
|
|
91
|
+
const response = await proxyRequest(endpoint, {
|
|
92
92
|
method: 'GET'
|
|
93
93
|
});
|
|
94
94
|
sendSuccess(res, {
|
|
@@ -105,8 +105,8 @@ export function createCloudProxyRouter({
|
|
|
105
105
|
// Route: POST /api/cloud/baselines/download - Download baselines from build
|
|
106
106
|
if (req.method === 'POST' && pathname === '/api/cloud/baselines/download') {
|
|
107
107
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
const body = await parseJsonBody(req);
|
|
109
|
+
const {
|
|
110
110
|
buildId,
|
|
111
111
|
screenshotNames
|
|
112
112
|
} = body;
|
|
@@ -116,7 +116,7 @@ export function createCloudProxyRouter({
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Download baselines from the specified build
|
|
119
|
-
|
|
119
|
+
const response = await proxyRequest('/api/cli/baselines/download', {
|
|
120
120
|
method: 'POST',
|
|
121
121
|
headers: {
|
|
122
122
|
'Content-Type': 'application/json'
|