@vizzly-testing/cli 0.13.4 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +78 -32
- 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/fullscreen-viewer.d.ts +13 -0
- package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
- package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +9 -0
- package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +1 -1
- 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 +5 -0
- package/dist/types/reporter/src/components/views/comparisons-view.d.ts +5 -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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS Middleware
|
|
3
|
+
* Handles cross-origin requests and preflight OPTIONS
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Apply CORS headers and handle OPTIONS preflight
|
|
8
|
+
* @param {http.IncomingMessage} req
|
|
9
|
+
* @param {http.ServerResponse} res
|
|
10
|
+
* @returns {boolean} True if request was handled (OPTIONS), false to continue
|
|
11
|
+
*/
|
|
12
|
+
export function corsMiddleware(req, res) {
|
|
13
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
14
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
15
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
16
|
+
if (req.method === 'OPTIONS') {
|
|
17
|
+
res.statusCode = 200;
|
|
18
|
+
res.end();
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Body Parser Middleware
|
|
3
|
+
* Parses JSON request bodies for POST requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse JSON body from request
|
|
8
|
+
* @param {http.IncomingMessage} req
|
|
9
|
+
* @returns {Promise<Object|null>} Parsed JSON body or null for non-POST
|
|
10
|
+
*/
|
|
11
|
+
export function parseJsonBody(req) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
if (req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'PATCH') {
|
|
14
|
+
resolve(null);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let body = '';
|
|
18
|
+
req.on('data', chunk => {
|
|
19
|
+
body += chunk.toString();
|
|
20
|
+
});
|
|
21
|
+
req.on('end', () => {
|
|
22
|
+
if (!body) {
|
|
23
|
+
resolve({});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
let data = JSON.parse(body);
|
|
28
|
+
resolve(data);
|
|
29
|
+
} catch {
|
|
30
|
+
reject(new Error('Invalid JSON'));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
req.on('error', reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Helpers
|
|
3
|
+
* Standardized response utilities for consistent API responses
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Send JSON response
|
|
8
|
+
* @param {http.ServerResponse} res
|
|
9
|
+
* @param {number} statusCode
|
|
10
|
+
* @param {Object} data
|
|
11
|
+
*/
|
|
12
|
+
export function sendJson(res, statusCode, data) {
|
|
13
|
+
res.setHeader('Content-Type', 'application/json');
|
|
14
|
+
res.statusCode = statusCode;
|
|
15
|
+
res.end(JSON.stringify(data));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Send success response
|
|
20
|
+
* @param {http.ServerResponse} res
|
|
21
|
+
* @param {Object} data
|
|
22
|
+
*/
|
|
23
|
+
export function sendSuccess(res, data = {}) {
|
|
24
|
+
sendJson(res, 200, data);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Send error response
|
|
29
|
+
* @param {http.ServerResponse} res
|
|
30
|
+
* @param {number} statusCode
|
|
31
|
+
* @param {string} message
|
|
32
|
+
*/
|
|
33
|
+
export function sendError(res, statusCode, message) {
|
|
34
|
+
sendJson(res, statusCode, {
|
|
35
|
+
error: message
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Send 404 Not Found
|
|
41
|
+
* @param {http.ServerResponse} res
|
|
42
|
+
* @param {string} message
|
|
43
|
+
*/
|
|
44
|
+
export function sendNotFound(res, message = 'Not found') {
|
|
45
|
+
sendError(res, 404, message);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Send 503 Service Unavailable
|
|
50
|
+
* @param {http.ServerResponse} res
|
|
51
|
+
* @param {string} serviceName
|
|
52
|
+
*/
|
|
53
|
+
export function sendServiceUnavailable(res, serviceName) {
|
|
54
|
+
sendError(res, 503, `${serviceName} not available`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Send HTML response
|
|
59
|
+
* @param {http.ServerResponse} res
|
|
60
|
+
* @param {number} statusCode
|
|
61
|
+
* @param {string} html
|
|
62
|
+
*/
|
|
63
|
+
export function sendHtml(res, statusCode, html) {
|
|
64
|
+
res.setHeader('Content-Type', 'text/html');
|
|
65
|
+
res.statusCode = statusCode;
|
|
66
|
+
res.end(html);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Send file response with specified content type
|
|
71
|
+
* @param {http.ServerResponse} res
|
|
72
|
+
* @param {Buffer|string} content
|
|
73
|
+
* @param {string} contentType
|
|
74
|
+
*/
|
|
75
|
+
export function sendFile(res, content, contentType) {
|
|
76
|
+
res.setHeader('Content-Type', contentType);
|
|
77
|
+
res.statusCode = 200;
|
|
78
|
+
res.end(content);
|
|
79
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assets Router
|
|
3
|
+
* Serves static assets (bundle files, images)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { sendFile, sendError, sendNotFound } from '../middleware/response.js';
|
|
10
|
+
import * as output from '../../utils/output.js';
|
|
11
|
+
let __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
let __dirname = dirname(__filename);
|
|
13
|
+
let PROJECT_ROOT = join(__dirname, '..', '..', '..');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create assets router
|
|
17
|
+
* @param {Object} context - Router context
|
|
18
|
+
* @returns {Function} Route handler
|
|
19
|
+
*/
|
|
20
|
+
export function createAssetsRouter() {
|
|
21
|
+
return async function handleAssetsRoute(req, res, pathname) {
|
|
22
|
+
if (req.method !== 'GET') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Serve React bundle JS
|
|
27
|
+
if (pathname === '/reporter-bundle.js') {
|
|
28
|
+
let bundlePath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.iife.js');
|
|
29
|
+
if (existsSync(bundlePath)) {
|
|
30
|
+
try {
|
|
31
|
+
let bundle = readFileSync(bundlePath, 'utf8');
|
|
32
|
+
sendFile(res, bundle, 'application/javascript');
|
|
33
|
+
return true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
output.debug('Error serving reporter bundle:', {
|
|
36
|
+
error: error.message
|
|
37
|
+
});
|
|
38
|
+
sendError(res, 500, 'Error loading reporter bundle');
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
sendNotFound(res, 'Reporter bundle not found');
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Serve React bundle CSS
|
|
48
|
+
if (pathname === '/reporter-bundle.css') {
|
|
49
|
+
let cssPath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.css');
|
|
50
|
+
if (existsSync(cssPath)) {
|
|
51
|
+
try {
|
|
52
|
+
let css = readFileSync(cssPath, 'utf8');
|
|
53
|
+
sendFile(res, css, 'text/css');
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
output.debug('Error serving reporter CSS:', {
|
|
57
|
+
error: error.message
|
|
58
|
+
});
|
|
59
|
+
sendError(res, 500, 'Error loading reporter CSS');
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
sendNotFound(res, 'Reporter CSS not found');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Serve images from .vizzly directory
|
|
69
|
+
if (pathname.startsWith('/images/')) {
|
|
70
|
+
let imagePath = pathname.replace('/images/', '');
|
|
71
|
+
let fullImagePath = join(process.cwd(), '.vizzly', imagePath);
|
|
72
|
+
if (existsSync(fullImagePath)) {
|
|
73
|
+
try {
|
|
74
|
+
let imageData = readFileSync(fullImagePath);
|
|
75
|
+
sendFile(res, imageData, 'image/png');
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
output.debug('Error serving image:', {
|
|
79
|
+
error: error.message
|
|
80
|
+
});
|
|
81
|
+
sendError(res, 500, 'Error loading image');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
sendNotFound(res, 'Image not found');
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Router
|
|
3
|
+
* Handles authentication endpoints (device flow login, logout, status)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
7
|
+
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
8
|
+
import * as output from '../../utils/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create auth router
|
|
12
|
+
* @param {Object} context - Router context
|
|
13
|
+
* @param {Object} context.authService - Auth service
|
|
14
|
+
* @returns {Function} Route handler
|
|
15
|
+
*/
|
|
16
|
+
export function createAuthRouter({
|
|
17
|
+
authService
|
|
18
|
+
}) {
|
|
19
|
+
return async function handleAuthRoute(req, res, pathname) {
|
|
20
|
+
// Check if auth service is available for all auth routes
|
|
21
|
+
if (pathname.startsWith('/api/auth') && !authService) {
|
|
22
|
+
sendServiceUnavailable(res, 'Auth service');
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get auth status and user info
|
|
27
|
+
if (req.method === 'GET' && pathname === '/api/auth/status') {
|
|
28
|
+
try {
|
|
29
|
+
let isAuthenticated = await authService.isAuthenticated();
|
|
30
|
+
let user = null;
|
|
31
|
+
if (isAuthenticated) {
|
|
32
|
+
let whoami = await authService.whoami();
|
|
33
|
+
user = whoami.user;
|
|
34
|
+
}
|
|
35
|
+
sendSuccess(res, {
|
|
36
|
+
authenticated: isAuthenticated,
|
|
37
|
+
user
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
output.error('Error getting auth status:', error);
|
|
42
|
+
sendSuccess(res, {
|
|
43
|
+
authenticated: false,
|
|
44
|
+
user: null
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initiate device flow login
|
|
51
|
+
if (req.method === 'POST' && pathname === '/api/auth/login') {
|
|
52
|
+
try {
|
|
53
|
+
let deviceFlow = await authService.initiateDeviceFlow();
|
|
54
|
+
|
|
55
|
+
// Transform snake_case to camelCase for frontend
|
|
56
|
+
let response = {
|
|
57
|
+
deviceCode: deviceFlow.device_code,
|
|
58
|
+
userCode: deviceFlow.user_code,
|
|
59
|
+
verificationUri: deviceFlow.verification_uri,
|
|
60
|
+
verificationUriComplete: deviceFlow.verification_uri_complete,
|
|
61
|
+
expiresIn: deviceFlow.expires_in,
|
|
62
|
+
interval: deviceFlow.interval
|
|
63
|
+
};
|
|
64
|
+
sendSuccess(res, response);
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
output.error('Error initiating device flow:', error);
|
|
68
|
+
sendError(res, 500, error.message);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Poll device authorization status
|
|
74
|
+
if (req.method === 'POST' && pathname === '/api/auth/poll') {
|
|
75
|
+
try {
|
|
76
|
+
let body = await parseJsonBody(req);
|
|
77
|
+
let {
|
|
78
|
+
deviceCode
|
|
79
|
+
} = body;
|
|
80
|
+
if (!deviceCode) {
|
|
81
|
+
sendError(res, 400, 'deviceCode is required');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
let result;
|
|
85
|
+
try {
|
|
86
|
+
result = await authService.pollDeviceAuthorization(deviceCode);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// Handle "Authorization pending" as a valid response
|
|
89
|
+
if (error.message && error.message.includes('Authorization pending')) {
|
|
90
|
+
sendSuccess(res, {
|
|
91
|
+
status: 'pending'
|
|
92
|
+
});
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if authorization is complete by looking for tokens
|
|
99
|
+
if (result.tokens && result.tokens.accessToken) {
|
|
100
|
+
let tokensData = result.tokens;
|
|
101
|
+
let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
102
|
+
let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : result.expires_at || result.expiresAt;
|
|
103
|
+
let tokens = {
|
|
104
|
+
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
105
|
+
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
106
|
+
expiresAt: tokenExpiresAt,
|
|
107
|
+
user: result.user
|
|
108
|
+
};
|
|
109
|
+
await authService.completeDeviceFlow(tokens);
|
|
110
|
+
sendSuccess(res, {
|
|
111
|
+
status: 'complete',
|
|
112
|
+
user: result.user
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
sendSuccess(res, {
|
|
116
|
+
status: 'pending'
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
output.error('Error polling device authorization:', error);
|
|
122
|
+
sendError(res, 500, error.message);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Logout user
|
|
128
|
+
if (req.method === 'POST' && pathname === '/api/auth/logout') {
|
|
129
|
+
try {
|
|
130
|
+
await authService.logout();
|
|
131
|
+
sendSuccess(res, {
|
|
132
|
+
success: true,
|
|
133
|
+
message: 'Logged out successfully'
|
|
134
|
+
});
|
|
135
|
+
return true;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
output.error('Error logging out:', error);
|
|
138
|
+
sendError(res, 500, error.message);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline Router
|
|
3
|
+
* Handles baseline management (accept, accept-all, reset)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
7
|
+
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
8
|
+
import * as output from '../../utils/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create baseline router
|
|
12
|
+
* @param {Object} context - Router context
|
|
13
|
+
* @param {Object} context.screenshotHandler - Screenshot handler
|
|
14
|
+
* @param {Object} context.tddService - TDD service for baseline downloads
|
|
15
|
+
* @param {Object} context.authService - Auth service for OAuth requests
|
|
16
|
+
* @returns {Function} Route handler
|
|
17
|
+
*/
|
|
18
|
+
export function createBaselineRouter({
|
|
19
|
+
screenshotHandler,
|
|
20
|
+
tddService,
|
|
21
|
+
authService
|
|
22
|
+
}) {
|
|
23
|
+
return async function handleBaselineRoute(req, res, pathname) {
|
|
24
|
+
// Accept a single screenshot as baseline
|
|
25
|
+
if (req.method === 'POST' && pathname === '/api/baseline/accept') {
|
|
26
|
+
if (!screenshotHandler?.acceptBaseline) {
|
|
27
|
+
sendError(res, 400, 'Baseline management not available');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
let {
|
|
32
|
+
id
|
|
33
|
+
} = await parseJsonBody(req);
|
|
34
|
+
if (!id) {
|
|
35
|
+
sendError(res, 400, 'Comparison ID required');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
await screenshotHandler.acceptBaseline(id);
|
|
39
|
+
sendSuccess(res, {
|
|
40
|
+
success: true,
|
|
41
|
+
message: `Baseline accepted for comparison ${id}`
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
output.error('Error accepting baseline:', error);
|
|
46
|
+
sendError(res, 500, error.message);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Accept all screenshots as baseline
|
|
52
|
+
if (req.method === 'POST' && pathname === '/api/baseline/accept-all') {
|
|
53
|
+
if (!screenshotHandler?.acceptAllBaselines) {
|
|
54
|
+
sendError(res, 400, 'Baseline management not available');
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
let result = await screenshotHandler.acceptAllBaselines();
|
|
59
|
+
sendSuccess(res, {
|
|
60
|
+
success: true,
|
|
61
|
+
message: `Accepted ${result.count} baselines`,
|
|
62
|
+
count: result.count
|
|
63
|
+
});
|
|
64
|
+
return true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
output.error('Error accepting all baselines:', error);
|
|
67
|
+
sendError(res, 500, error.message);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Reset baselines to previous state
|
|
73
|
+
if (req.method === 'POST' && pathname === '/api/baseline/reset') {
|
|
74
|
+
if (!screenshotHandler?.resetBaselines) {
|
|
75
|
+
sendError(res, 400, 'Baseline management not available');
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await screenshotHandler.resetBaselines();
|
|
80
|
+
sendSuccess(res, {
|
|
81
|
+
success: true,
|
|
82
|
+
message: 'Baselines reset to previous state'
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
output.error('Error resetting baselines:', error);
|
|
87
|
+
sendError(res, 500, error.message);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Download baselines from a remote build
|
|
93
|
+
if (req.method === 'POST' && pathname === '/api/baselines/download') {
|
|
94
|
+
if (!tddService) {
|
|
95
|
+
sendServiceUnavailable(res, 'TDD service not available (only available in TDD mode)');
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
let body = await parseJsonBody(req);
|
|
100
|
+
let {
|
|
101
|
+
buildId,
|
|
102
|
+
organizationSlug,
|
|
103
|
+
projectSlug
|
|
104
|
+
} = body;
|
|
105
|
+
if (!buildId) {
|
|
106
|
+
sendError(res, 400, 'buildId is required');
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
output.info(`Downloading baselines from build ${buildId}...`);
|
|
110
|
+
|
|
111
|
+
// If organizationSlug and projectSlug are provided, use OAuth-based download
|
|
112
|
+
if (organizationSlug && projectSlug && authService) {
|
|
113
|
+
try {
|
|
114
|
+
let result = await tddService.downloadBaselinesWithAuth(buildId, organizationSlug, projectSlug, authService);
|
|
115
|
+
sendSuccess(res, {
|
|
116
|
+
success: true,
|
|
117
|
+
message: `Baselines downloaded from build ${buildId}`,
|
|
118
|
+
...result
|
|
119
|
+
});
|
|
120
|
+
return true;
|
|
121
|
+
} catch (authError) {
|
|
122
|
+
// Log the OAuth error with details
|
|
123
|
+
output.warn(`OAuth download failed (org=${organizationSlug}, project=${projectSlug}): ${authError.message}`);
|
|
124
|
+
|
|
125
|
+
// If the error is a 404, it's likely the build doesn't belong to the project
|
|
126
|
+
// or the project/org is incorrect - provide a helpful error
|
|
127
|
+
if (authError.message?.includes('404')) {
|
|
128
|
+
sendError(res, 404, `Build not found or does not belong to project "${projectSlug}" in organization "${organizationSlug}". ` + `Please verify the build exists and you have access to it.`);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// For auth errors, try API token fallback
|
|
133
|
+
if (!authError.message?.includes('401')) {
|
|
134
|
+
// For other errors, don't fall through - report them directly
|
|
135
|
+
throw authError;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fall back to API token-based download (when no OAuth info or OAuth auth failed)
|
|
141
|
+
let result = await tddService.downloadBaselines('test',
|
|
142
|
+
// environment
|
|
143
|
+
null,
|
|
144
|
+
// branch (not needed when buildId is specified)
|
|
145
|
+
buildId,
|
|
146
|
+
// specific build to download from
|
|
147
|
+
null // comparisonId (not needed)
|
|
148
|
+
);
|
|
149
|
+
sendSuccess(res, {
|
|
150
|
+
success: true,
|
|
151
|
+
message: `Baselines downloaded from build ${buildId}`,
|
|
152
|
+
...result
|
|
153
|
+
});
|
|
154
|
+
return true;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
output.error('Error downloading baselines:', error);
|
|
157
|
+
sendError(res, 500, error.message);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Proxy Router
|
|
3
|
+
* Proxies requests to Vizzly cloud API with OAuth authentication
|
|
4
|
+
*
|
|
5
|
+
* This router transparently handles OAuth token management:
|
|
6
|
+
* - Reads tokens from ~/.vizzly/config.json via authService
|
|
7
|
+
* - Adds Authorization header to outgoing requests
|
|
8
|
+
* - Handles token refresh on 401 responses
|
|
9
|
+
* - Returns proxied response to React app
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
13
|
+
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
14
|
+
import * as output from '../../utils/output.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create cloud proxy router
|
|
18
|
+
* @param {Object} context - Router context
|
|
19
|
+
* @param {Object} context.authService - Auth service for token management
|
|
20
|
+
* @param {string} context.apiUrl - Base API URL (default: https://app.vizzly.dev)
|
|
21
|
+
* @returns {Function} Route handler
|
|
22
|
+
*/
|
|
23
|
+
export function createCloudProxyRouter({
|
|
24
|
+
authService,
|
|
25
|
+
apiUrl: _apiUrl = 'https://app.vizzly.dev'
|
|
26
|
+
}) {
|
|
27
|
+
/**
|
|
28
|
+
* Make an authenticated request to the cloud API
|
|
29
|
+
* @param {string} endpoint - API endpoint (e.g., /api/cli/projects)
|
|
30
|
+
* @param {Object} options - Fetch options
|
|
31
|
+
* @returns {Promise<Object>} Response data
|
|
32
|
+
*/
|
|
33
|
+
async function proxyRequest(endpoint, options = {}) {
|
|
34
|
+
if (!authService) {
|
|
35
|
+
throw new Error('Auth service not available');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Use authService.authenticatedRequest which handles token refresh
|
|
39
|
+
return authService.authenticatedRequest(endpoint, options);
|
|
40
|
+
}
|
|
41
|
+
return async function handleCloudProxyRoute(req, res, pathname, parsedUrl) {
|
|
42
|
+
// Only handle /api/cloud/* routes
|
|
43
|
+
if (!pathname.startsWith('/api/cloud')) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check auth service availability
|
|
48
|
+
if (!authService) {
|
|
49
|
+
sendServiceUnavailable(res, 'Auth service');
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Route: GET /api/cloud/projects - List user's projects
|
|
54
|
+
if (req.method === 'GET' && pathname === '/api/cloud/projects') {
|
|
55
|
+
try {
|
|
56
|
+
let response = await proxyRequest('/api/cli/projects', {
|
|
57
|
+
method: 'GET'
|
|
58
|
+
});
|
|
59
|
+
sendSuccess(res, {
|
|
60
|
+
projects: response.projects || []
|
|
61
|
+
});
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
output.error('Error fetching projects from cloud:', error);
|
|
65
|
+
// Return empty array instead of error for better UX when not logged in
|
|
66
|
+
if (error.message?.includes('not authenticated') || error.code === 'AUTH_ERROR') {
|
|
67
|
+
sendSuccess(res, {
|
|
68
|
+
projects: [],
|
|
69
|
+
authenticated: false
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
sendError(res, 500, error.message);
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Route: GET /api/cloud/organizations/:org/projects/:project/builds
|
|
79
|
+
let buildsMatch = pathname.match(/^\/api\/cloud\/organizations\/([^/]+)\/projects\/([^/]+)\/builds$/);
|
|
80
|
+
if (req.method === 'GET' && buildsMatch) {
|
|
81
|
+
try {
|
|
82
|
+
let organizationSlug = decodeURIComponent(buildsMatch[1]);
|
|
83
|
+
let projectSlug = decodeURIComponent(buildsMatch[2]);
|
|
84
|
+
let limit = parsedUrl.searchParams.get('limit') || '20';
|
|
85
|
+
let branch = parsedUrl.searchParams.get('branch');
|
|
86
|
+
let queryParams = new URLSearchParams();
|
|
87
|
+
if (limit) queryParams.append('limit', limit);
|
|
88
|
+
if (branch) queryParams.append('branch', branch);
|
|
89
|
+
let query = queryParams.toString();
|
|
90
|
+
let endpoint = `/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/builds${query ? `?${query}` : ''}`;
|
|
91
|
+
let response = await proxyRequest(endpoint, {
|
|
92
|
+
method: 'GET'
|
|
93
|
+
});
|
|
94
|
+
sendSuccess(res, {
|
|
95
|
+
builds: response.builds || []
|
|
96
|
+
});
|
|
97
|
+
return true;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
output.error('Error fetching builds from cloud:', error);
|
|
100
|
+
sendError(res, 500, error.message);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Route: POST /api/cloud/baselines/download - Download baselines from build
|
|
106
|
+
if (req.method === 'POST' && pathname === '/api/cloud/baselines/download') {
|
|
107
|
+
try {
|
|
108
|
+
let body = await parseJsonBody(req);
|
|
109
|
+
let {
|
|
110
|
+
buildId,
|
|
111
|
+
screenshotNames
|
|
112
|
+
} = body;
|
|
113
|
+
if (!buildId) {
|
|
114
|
+
sendError(res, 400, 'buildId is required');
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Download baselines from the specified build
|
|
119
|
+
let response = await proxyRequest('/api/cli/baselines/download', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json'
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
buildId,
|
|
126
|
+
screenshotNames
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
sendSuccess(res, {
|
|
130
|
+
success: true,
|
|
131
|
+
message: `Baselines downloaded from build ${buildId}`,
|
|
132
|
+
...response
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
output.error('Error downloading baselines from cloud:', error);
|
|
137
|
+
sendError(res, 500, error.message);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Unknown cloud route
|
|
143
|
+
sendError(res, 404, 'Cloud API endpoint not found');
|
|
144
|
+
return true;
|
|
145
|
+
};
|
|
146
|
+
}
|