@vizzly-testing/cli 0.13.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/cli.js +68 -68
  2. package/dist/commands/doctor.js +30 -34
  3. package/dist/commands/finalize.js +24 -23
  4. package/dist/commands/init.js +30 -28
  5. package/dist/commands/login.js +49 -55
  6. package/dist/commands/logout.js +14 -19
  7. package/dist/commands/project.js +83 -103
  8. package/dist/commands/run.js +77 -89
  9. package/dist/commands/status.js +48 -49
  10. package/dist/commands/tdd-daemon.js +90 -86
  11. package/dist/commands/tdd.js +59 -88
  12. package/dist/commands/upload.js +57 -57
  13. package/dist/commands/whoami.js +40 -45
  14. package/dist/index.js +2 -5
  15. package/dist/plugin-loader.js +15 -17
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +78 -32
  18. package/dist/sdk/index.js +36 -45
  19. package/dist/server/handlers/api-handler.js +14 -15
  20. package/dist/server/handlers/tdd-handler.js +34 -37
  21. package/dist/server/http-server.js +75 -869
  22. package/dist/server/middleware/cors.js +22 -0
  23. package/dist/server/middleware/json-parser.js +35 -0
  24. package/dist/server/middleware/response.js +79 -0
  25. package/dist/server/routers/assets.js +91 -0
  26. package/dist/server/routers/auth.js +144 -0
  27. package/dist/server/routers/baseline.js +163 -0
  28. package/dist/server/routers/cloud-proxy.js +146 -0
  29. package/dist/server/routers/config.js +126 -0
  30. package/dist/server/routers/dashboard.js +130 -0
  31. package/dist/server/routers/health.js +61 -0
  32. package/dist/server/routers/projects.js +168 -0
  33. package/dist/server/routers/screenshot.js +86 -0
  34. package/dist/services/auth-service.js +1 -1
  35. package/dist/services/build-manager.js +13 -40
  36. package/dist/services/config-service.js +2 -4
  37. package/dist/services/html-report-generator.js +6 -5
  38. package/dist/services/index.js +64 -0
  39. package/dist/services/project-service.js +121 -40
  40. package/dist/services/screenshot-server.js +9 -9
  41. package/dist/services/server-manager.js +11 -18
  42. package/dist/services/static-report-generator.js +3 -4
  43. package/dist/services/tdd-service.js +246 -103
  44. package/dist/services/test-runner.js +24 -25
  45. package/dist/services/uploader.js +5 -4
  46. package/dist/types/commands/init.d.ts +1 -2
  47. package/dist/types/index.d.ts +2 -3
  48. package/dist/types/plugin-loader.d.ts +1 -2
  49. package/dist/types/reporter/src/api/client.d.ts +178 -0
  50. package/dist/types/reporter/src/components/app-router.d.ts +1 -3
  51. package/dist/types/reporter/src/components/code-block.d.ts +4 -0
  52. package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +10 -0
  53. package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +11 -0
  54. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +14 -0
  55. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +30 -0
  56. package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +8 -0
  57. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  58. package/dist/types/reporter/src/components/comparison/fullscreen-viewer.d.ts +13 -0
  59. package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
  60. package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +9 -0
  61. package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +1 -1
  62. package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
  63. package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
  64. package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
  65. package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
  66. package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
  67. package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
  68. package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
  69. package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
  70. package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
  71. package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
  72. package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
  73. package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
  74. package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
  75. package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
  76. package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
  77. package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
  78. package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +5 -0
  79. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +5 -6
  80. package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
  81. package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
  82. package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
  83. package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
  84. package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
  85. package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
  86. package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
  87. package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
  88. package/dist/types/sdk/index.d.ts +2 -4
  89. package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
  90. package/dist/types/server/http-server.d.ts +1 -1
  91. package/dist/types/server/middleware/cors.d.ts +11 -0
  92. package/dist/types/server/middleware/json-parser.d.ts +10 -0
  93. package/dist/types/server/middleware/response.d.ts +50 -0
  94. package/dist/types/server/routers/assets.d.ts +6 -0
  95. package/dist/types/server/routers/auth.d.ts +9 -0
  96. package/dist/types/server/routers/baseline.d.ts +13 -0
  97. package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
  98. package/dist/types/server/routers/config.d.ts +9 -0
  99. package/dist/types/server/routers/dashboard.d.ts +6 -0
  100. package/dist/types/server/routers/health.d.ts +11 -0
  101. package/dist/types/server/routers/projects.d.ts +9 -0
  102. package/dist/types/server/routers/screenshot.d.ts +11 -0
  103. package/dist/types/services/build-manager.d.ts +4 -3
  104. package/dist/types/services/config-service.d.ts +2 -3
  105. package/dist/types/services/index.d.ts +7 -0
  106. package/dist/types/services/project-service.d.ts +6 -4
  107. package/dist/types/services/screenshot-server.d.ts +5 -5
  108. package/dist/types/services/server-manager.d.ts +5 -3
  109. package/dist/types/services/tdd-service.d.ts +12 -1
  110. package/dist/types/services/test-runner.d.ts +3 -3
  111. package/dist/types/utils/output.d.ts +84 -0
  112. package/dist/utils/config-loader.js +24 -48
  113. package/dist/utils/global-config.js +2 -17
  114. package/dist/utils/output.js +445 -0
  115. package/dist/utils/security.js +3 -4
  116. package/docs/api-reference.md +0 -1
  117. package/docs/plugins.md +22 -22
  118. package/package.json +3 -2
  119. package/dist/container/index.js +0 -215
  120. package/dist/services/base-service.js +0 -154
  121. package/dist/types/container/index.d.ts +0 -59
  122. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
  123. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
  124. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
  125. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
  126. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
  127. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
  128. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
  129. package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
  130. package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
  131. package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
  132. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
  133. package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
  134. package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
  135. package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
  136. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
  137. package/dist/types/services/base-service.d.ts +0 -71
  138. package/dist/types/utils/console-ui.d.ts +0 -61
  139. package/dist/types/utils/logger-factory.d.ts +0 -26
  140. package/dist/types/utils/logger.d.ts +0 -79
  141. package/dist/utils/console-ui.js +0 -241
  142. package/dist/utils/logger-factory.js +0 -76
  143. package/dist/utils/logger.js +0 -231
@@ -0,0 +1,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
+ }