@vizzly-testing/cli 0.23.1 → 0.23.2

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 (36) hide show
  1. package/README.md +54 -586
  2. package/dist/api/client.js +3 -1
  3. package/dist/api/endpoints.js +6 -7
  4. package/dist/cli.js +15 -2
  5. package/dist/commands/finalize.js +12 -0
  6. package/dist/commands/preview.js +210 -28
  7. package/dist/commands/run.js +15 -0
  8. package/dist/commands/status.js +34 -8
  9. package/dist/commands/upload.js +13 -0
  10. package/package.json +1 -2
  11. package/claude-plugin/.claude-plugin/README.md +0 -270
  12. package/claude-plugin/.claude-plugin/marketplace.json +0 -28
  13. package/claude-plugin/.claude-plugin/plugin.json +0 -14
  14. package/claude-plugin/.mcp.json +0 -12
  15. package/claude-plugin/CHANGELOG.md +0 -85
  16. package/claude-plugin/commands/setup.md +0 -137
  17. package/claude-plugin/commands/suggest-screenshots.md +0 -111
  18. package/claude-plugin/mcp/vizzly-docs-server/README.md +0 -95
  19. package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +0 -110
  20. package/claude-plugin/mcp/vizzly-docs-server/index.js +0 -283
  21. package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +0 -399
  22. package/claude-plugin/mcp/vizzly-server/index.js +0 -927
  23. package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +0 -455
  24. package/claude-plugin/mcp/vizzly-server/token-resolver.js +0 -185
  25. package/claude-plugin/skills/check-visual-tests/SKILL.md +0 -158
  26. package/claude-plugin/skills/debug-visual-regression/SKILL.md +0 -269
  27. package/docs/api-reference.md +0 -1003
  28. package/docs/authentication.md +0 -334
  29. package/docs/doctor-command.md +0 -44
  30. package/docs/getting-started.md +0 -131
  31. package/docs/internal/SDK-API.md +0 -1018
  32. package/docs/plugins.md +0 -557
  33. package/docs/tdd-mode.md +0 -594
  34. package/docs/test-integration.md +0 -523
  35. package/docs/tui-elements.md +0 -560
  36. package/docs/upload-command.md +0 -196
@@ -1,399 +0,0 @@
1
- /**
2
- * Provider for Vizzly Cloud API integration
3
- */
4
- export class CloudAPIProvider {
5
- constructor() {
6
- this.defaultApiUrl = process.env.VIZZLY_API_URL || 'https://app.vizzly.dev';
7
- }
8
-
9
- /**
10
- * Make API request to Vizzly
11
- */
12
- async makeRequest(path, apiToken, apiUrl = this.defaultApiUrl) {
13
- if (!apiToken) {
14
- throw new Error(
15
- 'API token required. Set VIZZLY_TOKEN environment variable or provide via apiToken parameter.'
16
- );
17
- }
18
-
19
- let url = `${apiUrl}${path}`;
20
- let response = await fetch(url, {
21
- headers: {
22
- Authorization: `Bearer ${apiToken}`,
23
- 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0'
24
- }
25
- });
26
-
27
- if (!response.ok) {
28
- let error = await response.text();
29
- throw new Error(`API request failed (${response.status}): ${error}`);
30
- }
31
-
32
- return response.json();
33
- }
34
-
35
- /**
36
- * Get build status and details
37
- */
38
- async getBuildStatus(buildId, apiToken, apiUrl) {
39
- if (!buildId || typeof buildId !== 'string') {
40
- throw new Error('buildId is required and must be a non-empty string');
41
- }
42
-
43
- let data = await this.makeRequest(
44
- `/api/sdk/builds/${buildId}?include=comparisons`,
45
- apiToken,
46
- apiUrl
47
- );
48
-
49
- let { build } = data;
50
-
51
- // Calculate comparison summary
52
- // Note: API returns 'result' field (not 'status'), and doesn't have 'has_diff'
53
- // result can be: 'identical', 'changed', 'new', 'removed', 'error', 'missing', 'returning'
54
- let comparisons = build.comparisons || [];
55
-
56
- // Calculate basic comparison summary
57
- let changedComparisons = comparisons.filter((c) => c.result === 'changed' || (c.diff_percentage && c.diff_percentage > 0));
58
-
59
- let summary = {
60
- total: comparisons.length,
61
- new: comparisons.filter((c) => c.result === 'new').length,
62
- changed: changedComparisons.length,
63
- identical: comparisons.filter((c) => c.result === 'identical' || (c.result !== 'new' && c.result !== 'changed' && (!c.diff_percentage || c.diff_percentage === 0))).length,
64
- // Approval status breakdown
65
- approval: {
66
- pending: comparisons.filter((c) => c.approval_status === 'pending').length,
67
- approved: comparisons.filter((c) => c.approval_status === 'approved').length,
68
- rejected: comparisons.filter((c) => c.approval_status === 'rejected').length,
69
- autoApproved: comparisons.filter((c) => c.approval_status === 'auto_approved').length
70
- },
71
- // Flaky screenshot count
72
- flaky: comparisons.filter((c) => c.is_flaky).length
73
- };
74
-
75
- // Keep minimal for token efficiency - use read_comparison_details for full metadata
76
- let failedComparisons = comparisons
77
- .filter((c) => c.result === 'changed' || (c.diff_percentage && c.diff_percentage > 0))
78
- .map((c) => ({
79
- id: c.id,
80
- name: c.current_name || c.name,
81
- diffPercentage: c.diff_percentage,
82
- approvalStatus: c.approval_status,
83
- // Include hot spot coverage % for quick triage (single number)
84
- hotSpotCoverage: c.analysis_metadata?.hot_spot_coverage || null
85
- }));
86
-
87
- let newComparisons = comparisons
88
- .filter((c) => c.result === 'new')
89
- .map((c) => ({
90
- name: c.name,
91
- currentUrl: c.current_screenshot?.original_url
92
- }));
93
-
94
- return {
95
- build: {
96
- id: build.id,
97
- name: build.name,
98
- branch: build.branch,
99
- status: build.status,
100
- url: build.url,
101
- organizationSlug: build.organizationSlug,
102
- projectSlug: build.projectSlug,
103
- createdAt: build.created_at,
104
- // Include commit details for debugging
105
- commitSha: build.commit_sha,
106
- commitMessage: build.commit_message,
107
- commonAncestorSha: build.common_ancestor_sha
108
- },
109
- summary,
110
- failedComparisons,
111
- newComparisons
112
- };
113
- }
114
-
115
- /**
116
- * List recent builds
117
- */
118
- async listRecentBuilds(apiToken, options = {}) {
119
- let { limit = 10, branch, apiUrl } = options;
120
-
121
- let queryParams = new URLSearchParams({
122
- limit: limit.toString()
123
- });
124
-
125
- if (branch) {
126
- queryParams.append('branch', branch);
127
- }
128
-
129
- let data = await this.makeRequest(`/api/sdk/builds?${queryParams}`, apiToken, apiUrl);
130
-
131
- return {
132
- builds: data.builds.map((b) => ({
133
- id: b.id,
134
- name: b.name,
135
- branch: b.branch,
136
- status: b.status,
137
- environment: b.environment,
138
- createdAt: b.created_at
139
- })),
140
- pagination: data.pagination
141
- };
142
- }
143
-
144
- /**
145
- * Get token context (organization and project info)
146
- */
147
- async getTokenContext(apiToken, apiUrl) {
148
- return await this.makeRequest('/api/sdk/token/context', apiToken, apiUrl);
149
- }
150
-
151
- /**
152
- * Get comparison details
153
- */
154
- async getComparison(comparisonId, apiToken, apiUrl) {
155
- if (!comparisonId || typeof comparisonId !== 'string') {
156
- throw new Error('comparisonId is required and must be a non-empty string');
157
- }
158
-
159
- let data = await this.makeRequest(`/api/sdk/comparisons/${comparisonId}`, apiToken, apiUrl);
160
-
161
- return data.comparison;
162
- }
163
-
164
- /**
165
- * Search for comparisons by name across builds
166
- */
167
- async searchComparisons(name, apiToken, options = {}) {
168
- if (!name || typeof name !== 'string') {
169
- throw new Error('name is required and must be a non-empty string');
170
- }
171
-
172
- let { branch, limit = 50, offset = 0, apiUrl } = options;
173
-
174
- let queryParams = new URLSearchParams({
175
- name,
176
- limit: limit.toString(),
177
- offset: offset.toString()
178
- });
179
-
180
- if (branch) {
181
- queryParams.append('branch', branch);
182
- }
183
-
184
- let data = await this.makeRequest(
185
- `/api/sdk/comparisons/search?${queryParams}`,
186
- apiToken,
187
- apiUrl
188
- );
189
-
190
- return data;
191
- }
192
-
193
- // ==================================================================
194
- // BUILD COMMENTS
195
- // ==================================================================
196
-
197
- /**
198
- * Create a comment on a build
199
- */
200
- async createBuildComment(buildId, content, type, apiToken, apiUrl) {
201
- if (!buildId || typeof buildId !== 'string') {
202
- throw new Error('buildId is required and must be a non-empty string');
203
- }
204
- if (!content || typeof content !== 'string') {
205
- throw new Error('content is required and must be a non-empty string');
206
- }
207
-
208
- let url = `${apiUrl || this.defaultApiUrl}/api/sdk/builds/${buildId}/comments`;
209
- let response = await fetch(url, {
210
- method: 'POST',
211
- headers: {
212
- Authorization: `Bearer ${apiToken}`,
213
- 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
214
- 'Content-Type': 'application/json'
215
- },
216
- body: JSON.stringify({ content, type })
217
- });
218
-
219
- if (!response.ok) {
220
- let error = await response.text();
221
- throw new Error(`Failed to create comment (${response.status}): ${error}`);
222
- }
223
-
224
- return response.json();
225
- }
226
-
227
- /**
228
- * List comments for a build
229
- */
230
- async listBuildComments(buildId, apiToken, apiUrl) {
231
- if (!buildId || typeof buildId !== 'string') {
232
- throw new Error('buildId is required and must be a non-empty string');
233
- }
234
-
235
- let data = await this.makeRequest(`/api/sdk/builds/${buildId}/comments`, apiToken, apiUrl);
236
-
237
- // Filter out unnecessary fields from comments for MCP
238
- let filterComment = (comment) => {
239
- // eslint-disable-next-line no-unused-vars
240
- let { profile_photo_url, email, ...filtered } = comment;
241
- // Recursively filter replies if they exist
242
- if (filtered.replies && Array.isArray(filtered.replies)) {
243
- filtered.replies = filtered.replies.map(filterComment);
244
- }
245
- return filtered;
246
- };
247
-
248
- return {
249
- ...data,
250
- comments: data.comments ? data.comments.map(filterComment) : []
251
- };
252
- }
253
-
254
- // ==================================================================
255
- // COMPARISON APPROVALS
256
- // ==================================================================
257
-
258
- /**
259
- * Approve a comparison
260
- */
261
- async approveComparison(comparisonId, comment, apiToken, apiUrl) {
262
- if (!comparisonId || typeof comparisonId !== 'string') {
263
- throw new Error('comparisonId is required and must be a non-empty string');
264
- }
265
-
266
- let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approve`;
267
- let response = await fetch(url, {
268
- method: 'POST',
269
- headers: {
270
- Authorization: `Bearer ${apiToken}`,
271
- 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
272
- 'Content-Type': 'application/json'
273
- },
274
- body: JSON.stringify({ comment })
275
- });
276
-
277
- if (!response.ok) {
278
- let error = await response.text();
279
- throw new Error(`Failed to approve comparison (${response.status}): ${error}`);
280
- }
281
-
282
- return response.json();
283
- }
284
-
285
- /**
286
- * Reject a comparison
287
- */
288
- async rejectComparison(comparisonId, reason, apiToken, apiUrl) {
289
- if (!comparisonId || typeof comparisonId !== 'string') {
290
- throw new Error('comparisonId is required and must be a non-empty string');
291
- }
292
- if (!reason || typeof reason !== 'string') {
293
- throw new Error('reason is required and must be a non-empty string');
294
- }
295
-
296
- let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/reject`;
297
- let response = await fetch(url, {
298
- method: 'POST',
299
- headers: {
300
- Authorization: `Bearer ${apiToken}`,
301
- 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
302
- 'Content-Type': 'application/json'
303
- },
304
- body: JSON.stringify({ reason })
305
- });
306
-
307
- if (!response.ok) {
308
- let error = await response.text();
309
- throw new Error(`Failed to reject comparison (${response.status}): ${error}`);
310
- }
311
-
312
- return response.json();
313
- }
314
-
315
- /**
316
- * Update comparison approval status
317
- */
318
- async updateComparisonApproval(comparisonId, approvalStatus, comment, apiToken, apiUrl) {
319
- let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approval`;
320
- let response = await fetch(url, {
321
- method: 'PUT',
322
- headers: {
323
- Authorization: `Bearer ${apiToken}`,
324
- 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
325
- 'Content-Type': 'application/json'
326
- },
327
- body: JSON.stringify({ approval_status: approvalStatus, comment })
328
- });
329
-
330
- if (!response.ok) {
331
- let error = await response.text();
332
- throw new Error(`Failed to update comparison approval (${response.status}): ${error}`);
333
- }
334
-
335
- return response.json();
336
- }
337
-
338
- // ==================================================================
339
- // REVIEW STATUS
340
- // ==================================================================
341
-
342
- /**
343
- * Get review summary for a build
344
- */
345
- async getReviewSummary(buildId, apiToken, apiUrl) {
346
- if (!buildId || typeof buildId !== 'string') {
347
- throw new Error('buildId is required and must be a non-empty string');
348
- }
349
-
350
- let data = await this.makeRequest(
351
- `/api/sdk/builds/${buildId}/review-summary`,
352
- apiToken,
353
- apiUrl
354
- );
355
-
356
- return data;
357
- }
358
-
359
- // ==================================================================
360
- // TDD WORKFLOW
361
- // ==================================================================
362
-
363
- /**
364
- * Download baseline screenshots from a cloud build
365
- * Returns screenshot data that can be saved locally
366
- */
367
- async downloadBaselines(buildId, screenshotNames, apiToken, apiUrl) {
368
- if (!buildId || typeof buildId !== 'string') {
369
- throw new Error('buildId is required and must be a non-empty string');
370
- }
371
-
372
- let data = await this.makeRequest(
373
- `/api/sdk/builds/${buildId}?include=screenshots`,
374
- apiToken,
375
- apiUrl
376
- );
377
-
378
- let { build } = data;
379
- let screenshots = build.screenshots || [];
380
-
381
- // Filter by screenshot names if provided
382
- if (screenshotNames && screenshotNames.length > 0) {
383
- screenshots = screenshots.filter((s) => screenshotNames.includes(s.name));
384
- }
385
-
386
- return {
387
- buildId: build.id,
388
- buildName: build.name,
389
- screenshots: screenshots.map((s) => ({
390
- name: s.name,
391
- url: s.original_url,
392
- sha256: s.sha256,
393
- width: s.viewport_width,
394
- height: s.viewport_height,
395
- browser: s.browser
396
- }))
397
- };
398
- }
399
- }