@vizzly-testing/cli 0.29.6 → 0.31.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 (87) hide show
  1. package/README.md +34 -6
  2. package/dist/api/client.js +6 -3
  3. package/dist/api/endpoints.js +59 -0
  4. package/dist/api/index.js +1 -1
  5. package/dist/auth/core.js +1 -1
  6. package/dist/auth/index.js +2 -1
  7. package/dist/cli.js +206 -51
  8. package/dist/client/index.js +21 -29
  9. package/dist/commands/api.js +145 -90
  10. package/dist/commands/baselines.js +78 -47
  11. package/dist/commands/builds.js +9 -8
  12. package/dist/commands/comparisons.js +7 -8
  13. package/dist/commands/config-cmd.js +15 -7
  14. package/dist/commands/context.js +870 -0
  15. package/dist/commands/doctor.js +116 -81
  16. package/dist/commands/init.js +202 -201
  17. package/dist/commands/login.js +134 -65
  18. package/dist/commands/logout.js +18 -7
  19. package/dist/commands/preview.js +29 -57
  20. package/dist/commands/project.js +120 -0
  21. package/dist/commands/projects.js +9 -2
  22. package/dist/commands/review.js +236 -202
  23. package/dist/commands/run.js +75 -72
  24. package/dist/commands/status.js +228 -150
  25. package/dist/commands/tdd-daemon-process.js +98 -0
  26. package/dist/commands/tdd-daemon.js +318 -212
  27. package/dist/commands/tdd.js +23 -8
  28. package/dist/commands/upload.js +10 -9
  29. package/dist/commands/whoami.js +64 -26
  30. package/dist/config/core.js +37 -36
  31. package/dist/context/local-workspace-provider.js +532 -0
  32. package/dist/context/provider-resolver.js +36 -0
  33. package/dist/index.js +5 -4
  34. package/dist/plugin-loader.js +55 -49
  35. package/dist/project/core.js +2 -2
  36. package/dist/project/operations.js +6 -3
  37. package/dist/reporter/reporter-bundle.css +2 -1
  38. package/dist/reporter/reporter-bundle.iife.js +119 -119
  39. package/dist/reporter-ssr/ssr-entry.js +605 -509
  40. package/dist/sdk/index.js +162 -107
  41. package/dist/server/handlers/tdd-handler.js +30 -54
  42. package/dist/server/routers/assets.js +4 -0
  43. package/dist/server/routers/baseline.js +195 -178
  44. package/dist/server/routers/events.js +226 -148
  45. package/dist/server/routers/projects.js +14 -5
  46. package/dist/server/routers/screenshot.js +1 -31
  47. package/dist/services/build-manager.js +19 -97
  48. package/dist/services/config-service.js +26 -34
  49. package/dist/services/index.js +2 -4
  50. package/dist/services/static-report-generator.js +11 -12
  51. package/dist/services/test-runner.js +21 -27
  52. package/dist/tdd/core/hotspot-coverage.js +1 -1
  53. package/dist/tdd/index.js +1 -1
  54. package/dist/tdd/metadata/region-metadata.js +0 -37
  55. package/dist/tdd/runtime-context.js +78 -0
  56. package/dist/tdd/server-registry.js +38 -17
  57. package/dist/tdd/services/baseline-manager.js +23 -0
  58. package/dist/tdd/services/hotspot-service.js +1 -14
  59. package/dist/tdd/tdd-service.js +82 -113
  60. package/dist/types/client.d.ts +36 -1
  61. package/dist/types/index.d.ts +114 -22
  62. package/dist/types/sdk.d.ts +47 -7
  63. package/dist/uploader/core.js +5 -4
  64. package/dist/uploader/index.js +79 -2
  65. package/dist/uploader/operations.js +40 -5
  66. package/dist/utils/api-url.js +14 -0
  67. package/dist/utils/async-utils.js +26 -0
  68. package/dist/utils/ci-env.js +19 -12
  69. package/dist/utils/colors.js +46 -44
  70. package/dist/utils/config-loader.js +39 -65
  71. package/dist/utils/config-schema.js +19 -55
  72. package/dist/utils/fetch-utils.js +12 -5
  73. package/dist/utils/git.js +35 -72
  74. package/dist/utils/global-config.js +22 -3
  75. package/dist/utils/output.js +19 -7
  76. package/dist/utils/patterns.js +14 -0
  77. package/dist/utils/project-link-store.js +105 -0
  78. package/dist/utils/report-data.js +31 -0
  79. package/dist/utils/screenshot-options.js +28 -0
  80. package/dist/utils/secret-store.js +65 -0
  81. package/dist/utils/security.js +3 -2
  82. package/package.json +14 -11
  83. package/dist/services/screenshot-server.js +0 -65
  84. package/dist/services/server-manager.js +0 -81
  85. package/dist/services/uploader.js +0 -100
  86. package/dist/tdd/services/region-service.js +0 -75
  87. package/dist/utils/build-history.js +0 -103
@@ -6,6 +6,169 @@
6
6
  import * as output from '../../utils/output.js';
7
7
  import { parseJsonBody } from '../middleware/json-parser.js';
8
8
  import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
9
+ function sendBaselineUnavailable(res) {
10
+ sendError(res, 400, 'Baseline management not available');
11
+ }
12
+ async function parseRequiredComparisonId(req, res) {
13
+ let {
14
+ id
15
+ } = await parseJsonBody(req);
16
+ if (!id) {
17
+ sendError(res, 400, 'Comparison ID required');
18
+ return null;
19
+ }
20
+ return id;
21
+ }
22
+ async function handleComparisonAction({
23
+ req,
24
+ res,
25
+ screenshotHandler,
26
+ actionName,
27
+ action,
28
+ successMessage
29
+ }) {
30
+ if (!screenshotHandler?.[actionName]) {
31
+ sendBaselineUnavailable(res);
32
+ return true;
33
+ }
34
+ try {
35
+ let id = await parseRequiredComparisonId(req, res);
36
+ if (!id) {
37
+ return true;
38
+ }
39
+ await screenshotHandler[actionName](id);
40
+ sendSuccess(res, {
41
+ success: true,
42
+ message: successMessage(id)
43
+ });
44
+ return true;
45
+ } catch (error) {
46
+ if (error.code === 'NOT_FOUND') {
47
+ sendError(res, 404, error.message);
48
+ } else {
49
+ output.error(`Error ${action}:`, error);
50
+ sendError(res, 500, error.message);
51
+ }
52
+ return true;
53
+ }
54
+ }
55
+ async function handleAcceptAllBaselines(res, screenshotHandler) {
56
+ if (!screenshotHandler?.acceptAllBaselines) {
57
+ sendBaselineUnavailable(res);
58
+ return true;
59
+ }
60
+ try {
61
+ let result = await screenshotHandler.acceptAllBaselines();
62
+ sendSuccess(res, {
63
+ success: true,
64
+ message: `Accepted ${result.count} baselines`,
65
+ count: result.count
66
+ });
67
+ return true;
68
+ } catch (error) {
69
+ output.error('Error accepting all baselines:', error);
70
+ sendError(res, 500, error.message);
71
+ return true;
72
+ }
73
+ }
74
+ async function handleResetBaselines(res, screenshotHandler) {
75
+ if (!screenshotHandler?.resetBaselines) {
76
+ sendBaselineUnavailable(res);
77
+ return true;
78
+ }
79
+ try {
80
+ await screenshotHandler.resetBaselines();
81
+ sendSuccess(res, {
82
+ success: true,
83
+ message: 'Baselines reset to previous state'
84
+ });
85
+ return true;
86
+ } catch (error) {
87
+ output.error('Error resetting baselines:', error);
88
+ sendError(res, 500, error.message);
89
+ return true;
90
+ }
91
+ }
92
+ function shouldFallbackFromOAuth(error) {
93
+ return error.message?.includes('Not authenticated') || error.message?.includes('401');
94
+ }
95
+ async function downloadBaselinesWithOAuth({
96
+ authService,
97
+ tddService,
98
+ buildId,
99
+ organizationSlug,
100
+ projectSlug
101
+ }) {
102
+ output.info(`Downloading baselines from build ${buildId}...`);
103
+ output.debug('baseline', `Using OAuth for ${organizationSlug}/${projectSlug}`);
104
+ let apiResponse = await authService.authenticatedRequest(`/api/cli/${projectSlug}/builds/${buildId}/tdd-baselines`, {
105
+ method: 'GET',
106
+ headers: {
107
+ 'X-Organization': organizationSlug
108
+ }
109
+ });
110
+ if (!apiResponse) {
111
+ throw new Error(`Build ${buildId} not found or API returned null`);
112
+ }
113
+ return tddService.processDownloadedBaselines(apiResponse, buildId);
114
+ }
115
+ async function downloadBaselinesWithToken(tddService, buildId) {
116
+ output.info(`Downloading baselines from build ${buildId}...`);
117
+ return tddService.downloadBaselines('test', null, buildId, null);
118
+ }
119
+ async function handleBaselineDownload({
120
+ req,
121
+ res,
122
+ tddService,
123
+ authService
124
+ }) {
125
+ if (!tddService) {
126
+ sendServiceUnavailable(res, 'TDD service not available (only available in TDD mode)');
127
+ return true;
128
+ }
129
+ try {
130
+ let body = await parseJsonBody(req);
131
+ let {
132
+ buildId,
133
+ organizationSlug,
134
+ projectSlug
135
+ } = body;
136
+ if (!buildId) {
137
+ sendError(res, 400, 'buildId is required');
138
+ return true;
139
+ }
140
+ let result;
141
+ if (authService && organizationSlug && projectSlug) {
142
+ try {
143
+ result = await downloadBaselinesWithOAuth({
144
+ authService,
145
+ tddService,
146
+ buildId,
147
+ organizationSlug,
148
+ projectSlug
149
+ });
150
+ } catch (oauthError) {
151
+ if (!shouldFallbackFromOAuth(oauthError)) {
152
+ throw oauthError;
153
+ }
154
+ output.debug('baseline', `OAuth failed, trying other auth methods: ${oauthError.message}`);
155
+ }
156
+ }
157
+ if (!result) {
158
+ result = await downloadBaselinesWithToken(tddService, buildId);
159
+ }
160
+ sendSuccess(res, {
161
+ success: true,
162
+ message: `Baselines downloaded from build ${buildId}`,
163
+ ...result
164
+ });
165
+ return true;
166
+ } catch (error) {
167
+ output.error('Error downloading baselines:', error);
168
+ sendError(res, 500, error.message);
169
+ return true;
170
+ }
171
+ }
9
172
 
10
173
  /**
11
174
  * Create baseline router
@@ -23,204 +186,58 @@ export function createBaselineRouter({
23
186
  return async function handleBaselineRoute(req, res, pathname) {
24
187
  // Accept a single screenshot as baseline
25
188
  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
- const {
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
- }
189
+ return handleComparisonAction({
190
+ req,
191
+ res,
192
+ screenshotHandler,
193
+ actionName: 'acceptBaseline',
194
+ action: 'accepting baseline',
195
+ successMessage: id => `Baseline accepted for comparison ${id}`
196
+ });
49
197
  }
50
198
 
51
199
  // Accept all screenshots as baseline
52
200
  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
- const 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
- }
201
+ return handleAcceptAllBaselines(res, screenshotHandler);
70
202
  }
71
203
 
72
204
  // Reject a single comparison (keep current baseline, discard changes)
73
205
  if (req.method === 'POST' && pathname === '/api/baseline/reject') {
74
- if (!screenshotHandler?.rejectBaseline) {
75
- sendError(res, 400, 'Baseline management not available');
76
- return true;
77
- }
78
- try {
79
- const {
80
- id
81
- } = await parseJsonBody(req);
82
- if (!id) {
83
- sendError(res, 400, 'Comparison ID required');
84
- return true;
85
- }
86
- await screenshotHandler.rejectBaseline(id);
87
- sendSuccess(res, {
88
- success: true,
89
- message: `Changes rejected for comparison ${id}`
90
- });
91
- return true;
92
- } catch (error) {
93
- output.error('Error rejecting baseline:', error);
94
- sendError(res, 500, error.message);
95
- return true;
96
- }
206
+ return handleComparisonAction({
207
+ req,
208
+ res,
209
+ screenshotHandler,
210
+ actionName: 'rejectBaseline',
211
+ action: 'rejecting baseline',
212
+ successMessage: id => `Changes rejected for comparison ${id}`
213
+ });
97
214
  }
98
215
 
99
216
  // Delete a comparison entirely (removes from report and deletes files)
100
217
  if (req.method === 'POST' && pathname === '/api/baseline/delete') {
101
- if (!screenshotHandler?.deleteComparison) {
102
- sendError(res, 400, 'Baseline management not available');
103
- return true;
104
- }
105
- try {
106
- const {
107
- id
108
- } = await parseJsonBody(req);
109
- if (!id) {
110
- sendError(res, 400, 'Comparison ID required');
111
- return true;
112
- }
113
- await screenshotHandler.deleteComparison(id);
114
- sendSuccess(res, {
115
- success: true,
116
- message: `Comparison ${id} deleted`
117
- });
118
- return true;
119
- } catch (error) {
120
- if (error.code === 'NOT_FOUND') {
121
- sendError(res, 404, error.message);
122
- } else {
123
- output.error('Error deleting comparison:', error);
124
- sendError(res, 500, error.message);
125
- }
126
- return true;
127
- }
218
+ return handleComparisonAction({
219
+ req,
220
+ res,
221
+ screenshotHandler,
222
+ actionName: 'deleteComparison',
223
+ action: 'deleting comparison',
224
+ successMessage: id => `Comparison ${id} deleted`
225
+ });
128
226
  }
129
227
 
130
228
  // Reset baselines to previous state
131
229
  if (req.method === 'POST' && pathname === '/api/baseline/reset') {
132
- if (!screenshotHandler?.resetBaselines) {
133
- sendError(res, 400, 'Baseline management not available');
134
- return true;
135
- }
136
- try {
137
- await screenshotHandler.resetBaselines();
138
- sendSuccess(res, {
139
- success: true,
140
- message: 'Baselines reset to previous state'
141
- });
142
- return true;
143
- } catch (error) {
144
- output.error('Error resetting baselines:', error);
145
- sendError(res, 500, error.message);
146
- return true;
147
- }
230
+ return handleResetBaselines(res, screenshotHandler);
148
231
  }
149
232
 
150
233
  // Download baselines from a remote build
151
234
  if (req.method === 'POST' && pathname === '/api/baselines/download') {
152
- if (!tddService) {
153
- sendServiceUnavailable(res, 'TDD service not available (only available in TDD mode)');
154
- return true;
155
- }
156
- try {
157
- let body = await parseJsonBody(req);
158
- let {
159
- buildId,
160
- organizationSlug,
161
- projectSlug
162
- } = body;
163
- if (!buildId) {
164
- sendError(res, 400, 'buildId is required');
165
- return true;
166
- }
167
-
168
- // Try OAuth authentication first (allows access to any project user has membership to)
169
- // This is the preferred method when user is logged in via the dashboard
170
- if (authService && organizationSlug && projectSlug) {
171
- try {
172
- output.info(`Downloading baselines from build ${buildId}...`);
173
- output.debug('baseline', `Using OAuth for ${organizationSlug}/${projectSlug}`);
174
-
175
- // Use the CLI endpoint which accepts OAuth and checks project membership
176
- let apiResponse = await authService.authenticatedRequest(`/api/cli/${projectSlug}/builds/${buildId}/tdd-baselines`, {
177
- method: 'GET',
178
- headers: {
179
- 'X-Organization': organizationSlug
180
- }
181
- });
182
- if (!apiResponse) {
183
- throw new Error(`Build ${buildId} not found or API returned null`);
184
- }
185
-
186
- // Process the baselines through tddService
187
- let result = await tddService.processDownloadedBaselines(apiResponse, buildId);
188
- sendSuccess(res, {
189
- success: true,
190
- message: `Baselines downloaded from build ${buildId}`,
191
- ...result
192
- });
193
- return true;
194
- } catch (oauthError) {
195
- // If OAuth fails with auth error, fall through to other methods
196
- if (!oauthError.message?.includes('Not authenticated') && !oauthError.message?.includes('401')) {
197
- throw oauthError;
198
- }
199
- output.debug('baseline', `OAuth failed, trying other auth methods: ${oauthError.message}`);
200
- }
201
- }
202
-
203
- // Fall back to using tddService directly (requires VIZZLY_TOKEN env var)
204
- output.info(`Downloading baselines from build ${buildId}...`);
205
- let result = await tddService.downloadBaselines('test',
206
- // environment
207
- null,
208
- // branch (not needed when buildId is specified)
209
- buildId,
210
- // specific build to download from
211
- null // comparisonId (not needed)
212
- );
213
- sendSuccess(res, {
214
- success: true,
215
- message: `Baselines downloaded from build ${buildId}`,
216
- ...result
217
- });
218
- return true;
219
- } catch (error) {
220
- output.error('Error downloading baselines:', error);
221
- sendError(res, 500, error.message);
222
- return true;
223
- }
235
+ return handleBaselineDownload({
236
+ req,
237
+ res,
238
+ tddService,
239
+ authService
240
+ });
224
241
  }
225
242
  return false;
226
243
  };