@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
@@ -4,91 +4,199 @@
4
4
 
5
5
  import { createApiClient as defaultCreateApiClient } from '../api/index.js';
6
6
  import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
+ import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
7
8
  import * as defaultOutput from '../utils/output.js';
8
-
9
- /**
10
- * Approve a comparison
11
- * @param {string} comparisonId - Comparison ID to approve
12
- * @param {Object} options - Command options
13
- * @param {Object} globalOptions - Global CLI options
14
- * @param {Object} deps - Dependencies for testing
15
- */
16
- export async function approveCommand(comparisonId, options = {}, globalOptions = {}, deps = {}) {
17
- let {
18
- loadConfig = defaultLoadConfig,
19
- createApiClient = defaultCreateApiClient,
20
- output = defaultOutput,
21
- exit = code => process.exit(code)
22
- } = deps;
9
+ function createReviewDeps(deps = {}) {
10
+ return {
11
+ loadConfig: deps.loadConfig || defaultLoadConfig,
12
+ createApiClient: deps.createApiClient || defaultCreateApiClient,
13
+ getAccessToken: deps.getAccessToken || defaultGetAccessToken,
14
+ output: deps.output || defaultOutput,
15
+ exit: deps.exit || (code => process.exit(code))
16
+ };
17
+ }
18
+ function configureOutput(output, globalOptions) {
23
19
  output.configure({
24
20
  json: globalOptions.json,
25
21
  verbose: globalOptions.verbose,
26
22
  color: !globalOptions.noColor
27
23
  });
24
+ }
25
+ async function loadReviewConfig({
26
+ loadConfig,
27
+ options,
28
+ globalOptions
29
+ }) {
30
+ let allOptions = {
31
+ ...globalOptions,
32
+ ...options
33
+ };
34
+ return await loadConfig(globalOptions.config, allOptions);
35
+ }
36
+ function isProjectToken(token) {
37
+ return typeof token === 'string' && token.startsWith('vzt_');
38
+ }
39
+ async function getReviewToken(config, getAccessToken) {
40
+ if (config.userToken) {
41
+ return config.userToken;
42
+ }
43
+ let userToken = await getAccessToken();
44
+ if (userToken) {
45
+ return userToken;
46
+ }
47
+ if (config.apiKey && !isProjectToken(config.apiKey)) {
48
+ return config.apiKey;
49
+ }
50
+ return null;
51
+ }
52
+ function createReviewClient({
53
+ createApiClient,
54
+ config,
55
+ command,
56
+ token
57
+ }) {
58
+ return createApiClient({
59
+ baseUrl: config.apiUrl,
60
+ token,
61
+ command
62
+ });
63
+ }
64
+ function jsonBody(body) {
65
+ if (!body || Object.keys(body).length === 0) {
66
+ return {};
67
+ }
68
+ return {
69
+ body: JSON.stringify(body),
70
+ headers: {
71
+ 'Content-Type': 'application/json'
72
+ }
73
+ };
74
+ }
75
+ async function runReviewMutation({
76
+ command,
77
+ endpoint,
78
+ failureMessage,
79
+ globalOptions,
80
+ options,
81
+ requestBody,
82
+ spinnerMessage,
83
+ writeJsonError,
84
+ writeJsonSuccess,
85
+ writeHumanSuccess,
86
+ deps,
87
+ configure = true
88
+ }) {
89
+ let {
90
+ loadConfig,
91
+ createApiClient,
92
+ getAccessToken,
93
+ output,
94
+ exit
95
+ } = createReviewDeps(deps);
96
+ if (configure) {
97
+ configureOutput(output, globalOptions);
98
+ }
28
99
  try {
29
- let allOptions = {
30
- ...globalOptions,
31
- ...options
32
- };
33
- let config = await loadConfig(globalOptions.config, allOptions);
34
- if (!config.apiKey) {
35
- output.error('API token required');
36
- output.hint('Use --token or set VIZZLY_TOKEN environment variable');
100
+ let config = await loadReviewConfig({
101
+ loadConfig,
102
+ options,
103
+ globalOptions
104
+ });
105
+ let token = await getReviewToken(config, getAccessToken);
106
+ if (!token) {
107
+ output.error('User login required for review actions');
108
+ output.hint('Run "vizzly login" to approve, reject, or comment');
109
+ output.cleanup();
37
110
  exit(1);
38
111
  return;
39
112
  }
40
- output.startSpinner('Approving comparison...');
41
- let client = createApiClient({
42
- baseUrl: config.apiUrl,
43
- token: config.apiKey,
44
- command: 'approve'
113
+ output.startSpinner(spinnerMessage);
114
+ let client = createReviewClient({
115
+ createApiClient,
116
+ config,
117
+ command,
118
+ token
45
119
  });
46
- let body = {};
47
- if (options.comment) {
48
- body.comment = options.comment;
49
- }
50
- let response = await client.request(`/api/sdk/comparisons/${comparisonId}/approve`, {
120
+ let response = await client.request(endpoint, {
51
121
  method: 'POST',
52
- body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
53
- headers: Object.keys(body).length > 0 ? {
54
- 'Content-Type': 'application/json'
55
- } : undefined
122
+ ...jsonBody(requestBody)
56
123
  });
57
124
  output.stopSpinner();
58
125
  if (globalOptions.json) {
59
- output.data({
60
- approved: true,
61
- comparisonId,
62
- comparison: response.comparison
63
- });
126
+ writeJsonSuccess(output, response);
64
127
  output.cleanup();
65
128
  return;
66
129
  }
67
- output.complete(`Comparison ${comparisonId} approved`);
68
- if (options.comment) {
69
- output.hint(`Comment: "${options.comment}"`);
70
- }
130
+ writeHumanSuccess(output, response);
71
131
  output.cleanup();
72
132
  } catch (error) {
73
133
  output.stopSpinner();
74
134
  if (globalOptions.json) {
75
- output.data({
76
- approved: false,
77
- comparisonId,
78
- error: {
79
- message: error.message,
80
- code: error.code
81
- }
82
- });
135
+ writeJsonError(output, error);
83
136
  output.cleanup();
84
137
  exit(1);
85
138
  return;
86
139
  }
87
- output.error('Failed to approve comparison', error);
140
+ output.error(failureMessage, error);
88
141
  output.cleanup();
89
142
  exit(1);
90
143
  }
91
144
  }
145
+ export function createApprovalBody(options = {}) {
146
+ return options.comment ? {
147
+ comment: options.comment
148
+ } : {};
149
+ }
150
+ export function createRejectionBody(options = {}) {
151
+ return {
152
+ reason: options.reason
153
+ };
154
+ }
155
+ export function createCommentBody(message, options = {}) {
156
+ return {
157
+ content: message,
158
+ type: options.type || 'general'
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Approve a comparison
164
+ * @param {string} comparisonId - Comparison ID to approve
165
+ * @param {Object} options - Command options
166
+ * @param {Object} globalOptions - Global CLI options
167
+ * @param {Object} deps - Dependencies for testing
168
+ */
169
+ export async function approveCommand(comparisonId, options = {}, globalOptions = {}, deps = {}) {
170
+ return await runReviewMutation({
171
+ command: 'approve',
172
+ endpoint: `/api/sdk/comparisons/${comparisonId}/approve`,
173
+ failureMessage: 'Failed to approve comparison',
174
+ globalOptions,
175
+ options,
176
+ requestBody: createApprovalBody(options),
177
+ spinnerMessage: 'Approving comparison...',
178
+ writeJsonError: (output, error) => output.data({
179
+ approved: false,
180
+ comparisonId,
181
+ error: {
182
+ message: error.message,
183
+ code: error.code
184
+ }
185
+ }),
186
+ writeJsonSuccess: (output, response) => output.data({
187
+ approved: true,
188
+ comparisonId,
189
+ comparison: response.comparison
190
+ }),
191
+ writeHumanSuccess: output => {
192
+ output.complete(`Comparison ${comparisonId} approved`);
193
+ if (options.comment) {
194
+ output.hint(`Comment: "${options.comment}"`);
195
+ }
196
+ },
197
+ deps
198
+ });
199
+ }
92
200
 
93
201
  /**
94
202
  * Reject a comparison
@@ -99,82 +207,46 @@ export async function approveCommand(comparisonId, options = {}, globalOptions =
99
207
  */
100
208
  export async function rejectCommand(comparisonId, options = {}, globalOptions = {}, deps = {}) {
101
209
  let {
102
- loadConfig = defaultLoadConfig,
103
- createApiClient = defaultCreateApiClient,
104
- output = defaultOutput,
105
- exit = code => process.exit(code)
106
- } = deps;
107
- output.configure({
108
- json: globalOptions.json,
109
- verbose: globalOptions.verbose,
110
- color: !globalOptions.noColor
111
- });
112
- try {
113
- let allOptions = {
114
- ...globalOptions,
115
- ...options
116
- };
117
- let config = await loadConfig(globalOptions.config, allOptions);
118
- if (!config.apiKey) {
119
- output.error('API token required');
120
- output.hint('Use --token or set VIZZLY_TOKEN environment variable');
121
- exit(1);
122
- return;
123
- }
124
- if (!options.reason) {
125
- output.error('Reason required when rejecting');
126
- output.hint('Use --reason "explanation" to provide a reason');
127
- exit(1);
128
- return;
129
- }
130
- output.startSpinner('Rejecting comparison...');
131
- let client = createApiClient({
132
- baseUrl: config.apiUrl,
133
- token: config.apiKey,
134
- command: 'reject'
135
- });
136
- let response = await client.request(`/api/sdk/comparisons/${comparisonId}/reject`, {
137
- method: 'POST',
138
- body: JSON.stringify({
139
- reason: options.reason
140
- }),
141
- headers: {
142
- 'Content-Type': 'application/json'
143
- }
144
- });
145
- output.stopSpinner();
146
- if (globalOptions.json) {
147
- output.data({
148
- rejected: true,
149
- comparisonId,
150
- reason: options.reason,
151
- comparison: response.comparison
152
- });
153
- output.cleanup();
154
- return;
155
- }
156
- output.complete(`Comparison ${comparisonId} rejected`);
157
- output.hint(`Reason: "${options.reason}"`);
158
- output.cleanup();
159
- } catch (error) {
160
- output.stopSpinner();
161
- if (globalOptions.json) {
162
- output.data({
163
- rejected: false,
164
- comparisonId,
165
- error: {
166
- message: error.message,
167
- code: error.code
168
- }
169
- });
170
- output.cleanup();
171
- exit(1);
172
- return;
173
- }
174
- output.error('Failed to reject comparison', error);
210
+ output,
211
+ exit
212
+ } = createReviewDeps(deps);
213
+ configureOutput(output, globalOptions);
214
+ if (!options.reason) {
215
+ output.error('Reason required when rejecting');
216
+ output.hint('Use --reason "explanation" to provide a reason');
175
217
  output.cleanup();
176
218
  exit(1);
219
+ return;
177
220
  }
221
+ return await runReviewMutation({
222
+ command: 'reject',
223
+ endpoint: `/api/sdk/comparisons/${comparisonId}/reject`,
224
+ failureMessage: 'Failed to reject comparison',
225
+ globalOptions,
226
+ options,
227
+ requestBody: createRejectionBody(options),
228
+ spinnerMessage: 'Rejecting comparison...',
229
+ writeJsonError: (output, error) => output.data({
230
+ rejected: false,
231
+ comparisonId,
232
+ error: {
233
+ message: error.message,
234
+ code: error.code
235
+ }
236
+ }),
237
+ writeJsonSuccess: (output, response) => output.data({
238
+ rejected: true,
239
+ comparisonId,
240
+ reason: options.reason,
241
+ comparison: response.comparison
242
+ }),
243
+ writeHumanSuccess: output => {
244
+ output.complete(`Comparison ${comparisonId} rejected`);
245
+ output.hint(`Reason: "${options.reason}"`);
246
+ },
247
+ deps,
248
+ configure: false
249
+ });
178
250
  }
179
251
 
180
252
  /**
@@ -187,83 +259,45 @@ export async function rejectCommand(comparisonId, options = {}, globalOptions =
187
259
  */
188
260
  export async function commentCommand(buildId, message, options = {}, globalOptions = {}, deps = {}) {
189
261
  let {
190
- loadConfig = defaultLoadConfig,
191
- createApiClient = defaultCreateApiClient,
192
- output = defaultOutput,
193
- exit = code => process.exit(code)
194
- } = deps;
195
- output.configure({
196
- json: globalOptions.json,
197
- verbose: globalOptions.verbose,
198
- color: !globalOptions.noColor
199
- });
200
- try {
201
- let allOptions = {
202
- ...globalOptions,
203
- ...options
204
- };
205
- let config = await loadConfig(globalOptions.config, allOptions);
206
- if (!config.apiKey) {
207
- output.error('API token required');
208
- output.hint('Use --token or set VIZZLY_TOKEN environment variable');
209
- exit(1);
210
- return;
211
- }
212
- if (!message || message.trim() === '') {
213
- output.error('Comment message required');
214
- exit(1);
215
- return;
216
- }
217
- output.startSpinner('Adding comment...');
218
- let client = createApiClient({
219
- baseUrl: config.apiUrl,
220
- token: config.apiKey,
221
- command: 'comment'
222
- });
223
- let body = {
224
- content: message,
225
- type: options.type || 'general'
226
- };
227
- let response = await client.request(`/api/sdk/builds/${buildId}/comments`, {
228
- method: 'POST',
229
- body: JSON.stringify(body),
230
- headers: {
231
- 'Content-Type': 'application/json'
232
- }
233
- });
234
- output.stopSpinner();
235
- if (globalOptions.json) {
236
- output.data({
237
- created: true,
238
- buildId,
239
- comment: response.comment
240
- });
241
- output.cleanup();
242
- return;
243
- }
244
- output.complete('Comment added');
245
- output.labelValue('Build', buildId);
246
- output.labelValue('Message', message);
247
- output.cleanup();
248
- } catch (error) {
249
- output.stopSpinner();
250
- if (globalOptions.json) {
251
- output.data({
252
- created: false,
253
- buildId,
254
- error: {
255
- message: error.message,
256
- code: error.code
257
- }
258
- });
259
- output.cleanup();
260
- exit(1);
261
- return;
262
- }
263
- output.error('Failed to add comment', error);
262
+ output,
263
+ exit
264
+ } = createReviewDeps(deps);
265
+ configureOutput(output, globalOptions);
266
+ if (!message || message.trim() === '') {
267
+ output.error('Comment message required');
264
268
  output.cleanup();
265
269
  exit(1);
270
+ return;
266
271
  }
272
+ return await runReviewMutation({
273
+ command: 'comment',
274
+ endpoint: `/api/sdk/builds/${buildId}/comments`,
275
+ failureMessage: 'Failed to add comment',
276
+ globalOptions,
277
+ options,
278
+ requestBody: createCommentBody(message, options),
279
+ spinnerMessage: 'Adding comment...',
280
+ writeJsonError: (output, error) => output.data({
281
+ created: false,
282
+ buildId,
283
+ error: {
284
+ message: error.message,
285
+ code: error.code
286
+ }
287
+ }),
288
+ writeJsonSuccess: (output, response) => output.data({
289
+ created: true,
290
+ buildId,
291
+ comment: response.comment
292
+ }),
293
+ writeHumanSuccess: output => {
294
+ output.complete('Comment added');
295
+ output.labelValue('Build', buildId);
296
+ output.labelValue('Message', message);
297
+ },
298
+ deps,
299
+ configure: false
300
+ });
267
301
  }
268
302
 
269
303
  /**