@vizzly-testing/cli 0.10.3 → 0.11.1

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 (47) hide show
  1. package/README.md +168 -8
  2. package/claude-plugin/.claude-plugin/.mcp.json +8 -0
  3. package/claude-plugin/.claude-plugin/README.md +114 -0
  4. package/claude-plugin/.claude-plugin/marketplace.json +28 -0
  5. package/claude-plugin/.claude-plugin/plugin.json +14 -0
  6. package/claude-plugin/commands/debug-diff.md +153 -0
  7. package/claude-plugin/commands/setup.md +137 -0
  8. package/claude-plugin/commands/suggest-screenshots.md +111 -0
  9. package/claude-plugin/commands/tdd-status.md +43 -0
  10. package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
  11. package/claude-plugin/mcp/vizzly-server/index.js +861 -0
  12. package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
  13. package/claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
  14. package/dist/cli.js +64 -0
  15. package/dist/client/index.js +13 -3
  16. package/dist/commands/login.js +195 -0
  17. package/dist/commands/logout.js +71 -0
  18. package/dist/commands/project.js +351 -0
  19. package/dist/commands/run.js +30 -0
  20. package/dist/commands/whoami.js +162 -0
  21. package/dist/plugin-loader.js +9 -15
  22. package/dist/sdk/index.js +16 -4
  23. package/dist/services/api-service.js +50 -7
  24. package/dist/services/auth-service.js +226 -0
  25. package/dist/types/client/index.d.ts +9 -3
  26. package/dist/types/commands/login.d.ts +11 -0
  27. package/dist/types/commands/logout.d.ts +11 -0
  28. package/dist/types/commands/project.d.ts +28 -0
  29. package/dist/types/commands/whoami.d.ts +11 -0
  30. package/dist/types/sdk/index.d.ts +9 -4
  31. package/dist/types/services/api-service.d.ts +2 -1
  32. package/dist/types/services/auth-service.d.ts +59 -0
  33. package/dist/types/utils/browser.d.ts +6 -0
  34. package/dist/types/utils/config-loader.d.ts +1 -1
  35. package/dist/types/utils/config-schema.d.ts +8 -174
  36. package/dist/types/utils/file-helpers.d.ts +18 -0
  37. package/dist/types/utils/global-config.d.ts +84 -0
  38. package/dist/utils/browser.js +44 -0
  39. package/dist/utils/config-loader.js +69 -3
  40. package/dist/utils/file-helpers.js +64 -0
  41. package/dist/utils/global-config.js +259 -0
  42. package/docs/api-reference.md +177 -6
  43. package/docs/authentication.md +334 -0
  44. package/docs/getting-started.md +21 -2
  45. package/docs/plugins.md +27 -0
  46. package/docs/test-integration.md +60 -10
  47. package/package.json +5 -3
@@ -0,0 +1,111 @@
1
+ ---
2
+ description: Analyze test files and suggest where visual screenshots would be valuable
3
+ ---
4
+
5
+ # Suggest Screenshot Opportunities
6
+
7
+ Analyze existing test files to identify ideal locations for visual regression testing.
8
+
9
+ ## Process
10
+
11
+ 1. **Detect SDK type** (same logic as setup command)
12
+
13
+ **If Storybook detected** (`@storybook/*` in package.json or `.storybook/` directory):
14
+ - Inform user that Storybook SDK auto-discovers all stories
15
+ - No manual screenshot calls needed
16
+ - Exit early
17
+
18
+ **If Static Site detected** (build directories like `dist/`, `build/`, `.next/out/` or static site generators):
19
+ - Inform user that Static Site SDK auto-discovers all pages
20
+ - No manual screenshot calls needed
21
+ - Exit early
22
+
23
+ **Otherwise continue** with test file analysis below.
24
+
25
+ 2. **Ask user for test directory** if not obvious (e.g., `tests/`, `e2e/`, `__tests__/`, `spec/`)
26
+
27
+ 3. **Find test files** using glob patterns:
28
+ - `**/*.test.{js,ts,jsx,tsx}`
29
+ - `**/*.spec.{js,ts,jsx,tsx}`
30
+ - `**/e2e/**/*.{js,ts}`
31
+
32
+ **IMPORTANT: Exclude these directories:**
33
+ - `node_modules/`
34
+ - `dist/`, `build/`, `out/`
35
+ - `.next/`, `.nuxt/`, `.vite/`
36
+ - `coverage/`, `.nyc_output/`
37
+ - `vendor/`
38
+ - Any hidden directories (`.*/`)
39
+
40
+ Use the Glob tool with explicit exclusion or filter results to avoid these directories.
41
+
42
+ 4. **Analyze test files** looking for:
43
+ - Page navigations (`.goto()`, `.visit()`, `navigate()`)
44
+ - User interactions before assertions (`.click()`, `.type()`, `.fill()`)
45
+ - Component rendering (React Testing Library, etc.)
46
+ - Visual assertions or wait conditions
47
+
48
+ 5. **Suggest screenshot points** where:
49
+ - After page loads or navigations
50
+ - After key user interactions
51
+ - Before visual assertions
52
+ - At different viewport sizes
53
+ - For different user states (logged in/out, etc.)
54
+
55
+ 6. **Provide code examples** specific to their test framework:
56
+
57
+ **Playwright:**
58
+
59
+ ```javascript
60
+ import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
61
+
62
+ // After page navigation
63
+ await page.goto('/dashboard');
64
+ await vizzlyScreenshot('dashboard-logged-in', await page.screenshot(), {
65
+ browser: 'chrome',
66
+ viewport: '1920x1080'
67
+ });
68
+ ```
69
+
70
+ **Cypress:**
71
+
72
+ ```javascript
73
+ cy.visit('/login');
74
+ cy.screenshot('login-page');
75
+ // Then add vizzlyScreenshot in custom command
76
+ ```
77
+
78
+ ## Output Format
79
+
80
+ ```
81
+ Found 8 potential screenshot opportunities in your tests:
82
+
83
+ tests/e2e/auth.spec.js:
84
+ Line 15: After login page navigation
85
+ Suggested screenshot: 'login-page'
86
+ Reason: Page load after navigation
87
+
88
+ Line 28: After successful login
89
+ Suggested screenshot: 'dashboard-authenticated'
90
+ Reason: User state change (logged in)
91
+
92
+ tests/e2e/checkout.spec.js:
93
+ Line 42: Shopping cart with items
94
+ Suggested screenshot: 'cart-with-items'
95
+ Reason: Visual state after user interaction
96
+
97
+ Line 67: Checkout confirmation page
98
+ Suggested screenshot: 'order-confirmation'
99
+ Reason: Final state of user flow
100
+
101
+ Example integration for Playwright:
102
+ [Provide code example specific to their test]
103
+ ```
104
+
105
+ ## Important Notes
106
+
107
+ - **Do NOT modify test files**
108
+ - **Do NOT create new test files**
109
+ - Only provide suggestions and examples
110
+ - Let the user decide where to add screenshots
111
+ - Respect their test framework and structure
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: Check TDD dashboard status and view visual regression test results
3
+ ---
4
+
5
+ # Check Vizzly TDD Status
6
+
7
+ Use the Vizzly MCP server to check the current TDD status:
8
+
9
+ 1. Call the `get_tdd_status` tool from the vizzly MCP server
10
+ 2. Analyze the comparison results
11
+ 3. Show a summary of:
12
+ - Total screenshots tested
13
+ - Passed, failed, and new screenshot counts
14
+ - List of failed comparisons with diff percentages
15
+ - Available diff images to inspect
16
+ 4. If TDD server is running, provide the dashboard URL
17
+ 5. For failed comparisons, provide guidance on next steps
18
+
19
+ ## Example Output Format
20
+
21
+ ```
22
+ Vizzly TDD Status:
23
+ ✅ Total: 15 screenshots
24
+ ✅ Passed: 12
25
+ ❌ Failed: 2 (exceeded threshold)
26
+ 🆕 New: 1 (no baseline)
27
+
28
+ Failed Comparisons:
29
+ - homepage (2.3% diff) - Check .vizzly/diffs/homepage.png
30
+ - login-form (1.8% diff) - Check .vizzly/diffs/login-form.png
31
+
32
+ New Screenshots:
33
+ - dashboard (no baseline for comparison)
34
+
35
+ Dashboard: http://localhost:47392
36
+
37
+ Next Steps:
38
+ - Review diff images to understand what changed
39
+ - Accept baselines from dashboard if changes are intentional
40
+ - Fix visual issues if changes are unintentional
41
+ ```
42
+
43
+ Focus on providing actionable information to help the developer understand what's failing and why.
@@ -0,0 +1,354 @@
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
+ let comparisons = build.comparisons || [];
53
+ let summary = {
54
+ total: comparisons.length,
55
+ new: comparisons.filter((c) => c.status === 'new').length,
56
+ changed: comparisons.filter((c) => c.has_diff).length,
57
+ identical: comparisons.filter((c) => !c.has_diff && c.status !== 'new').length
58
+ };
59
+
60
+ // Group comparisons by status
61
+ let failedComparisons = comparisons
62
+ .filter((c) => c.has_diff)
63
+ .map((c) => ({
64
+ name: c.name,
65
+ diffPercentage: c.diff_percentage,
66
+ currentUrl: c.current_screenshot?.original_url,
67
+ baselineUrl: c.baseline_screenshot?.original_url,
68
+ diffUrl: c.diff_image?.url
69
+ }));
70
+
71
+ let newComparisons = comparisons
72
+ .filter((c) => c.status === 'new')
73
+ .map((c) => ({
74
+ name: c.name,
75
+ currentUrl: c.current_screenshot?.original_url
76
+ }));
77
+
78
+ return {
79
+ build: {
80
+ id: build.id,
81
+ name: build.name,
82
+ branch: build.branch,
83
+ status: build.status,
84
+ url: build.url,
85
+ organizationSlug: build.organizationSlug,
86
+ projectSlug: build.projectSlug,
87
+ createdAt: build.created_at,
88
+ // Include commit details for debugging
89
+ commitSha: build.commit_sha,
90
+ commitMessage: build.commit_message,
91
+ commonAncestorSha: build.common_ancestor_sha
92
+ },
93
+ summary,
94
+ failedComparisons,
95
+ newComparisons
96
+ };
97
+ }
98
+
99
+ /**
100
+ * List recent builds
101
+ */
102
+ async listRecentBuilds(apiToken, options = {}) {
103
+ let { limit = 10, branch, apiUrl } = options;
104
+
105
+ let queryParams = new URLSearchParams({
106
+ limit: limit.toString()
107
+ });
108
+
109
+ if (branch) {
110
+ queryParams.append('branch', branch);
111
+ }
112
+
113
+ let data = await this.makeRequest(`/api/sdk/builds?${queryParams}`, apiToken, apiUrl);
114
+
115
+ return {
116
+ builds: data.builds.map((b) => ({
117
+ id: b.id,
118
+ name: b.name,
119
+ branch: b.branch,
120
+ status: b.status,
121
+ environment: b.environment,
122
+ createdAt: b.created_at
123
+ })),
124
+ pagination: data.pagination
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Get token context (organization and project info)
130
+ */
131
+ async getTokenContext(apiToken, apiUrl) {
132
+ return await this.makeRequest('/api/sdk/token/context', apiToken, apiUrl);
133
+ }
134
+
135
+ /**
136
+ * Get comparison details
137
+ */
138
+ async getComparison(comparisonId, apiToken, apiUrl) {
139
+ if (!comparisonId || typeof comparisonId !== 'string') {
140
+ throw new Error('comparisonId is required and must be a non-empty string');
141
+ }
142
+
143
+ let data = await this.makeRequest(`/api/sdk/comparisons/${comparisonId}`, apiToken, apiUrl);
144
+
145
+ return data.comparison;
146
+ }
147
+
148
+ // ==================================================================
149
+ // BUILD COMMENTS
150
+ // ==================================================================
151
+
152
+ /**
153
+ * Create a comment on a build
154
+ */
155
+ async createBuildComment(buildId, content, type, apiToken, apiUrl) {
156
+ if (!buildId || typeof buildId !== 'string') {
157
+ throw new Error('buildId is required and must be a non-empty string');
158
+ }
159
+ if (!content || typeof content !== 'string') {
160
+ throw new Error('content is required and must be a non-empty string');
161
+ }
162
+
163
+ let url = `${apiUrl || this.defaultApiUrl}/api/sdk/builds/${buildId}/comments`;
164
+ let response = await fetch(url, {
165
+ method: 'POST',
166
+ headers: {
167
+ Authorization: `Bearer ${apiToken}`,
168
+ 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
169
+ 'Content-Type': 'application/json'
170
+ },
171
+ body: JSON.stringify({ content, type })
172
+ });
173
+
174
+ if (!response.ok) {
175
+ let error = await response.text();
176
+ throw new Error(`Failed to create comment (${response.status}): ${error}`);
177
+ }
178
+
179
+ return response.json();
180
+ }
181
+
182
+ /**
183
+ * List comments for a build
184
+ */
185
+ async listBuildComments(buildId, apiToken, apiUrl) {
186
+ if (!buildId || typeof buildId !== 'string') {
187
+ throw new Error('buildId is required and must be a non-empty string');
188
+ }
189
+
190
+ let data = await this.makeRequest(`/api/sdk/builds/${buildId}/comments`, apiToken, apiUrl);
191
+
192
+ // Filter out unnecessary fields from comments for MCP
193
+ let filterComment = (comment) => {
194
+ // eslint-disable-next-line no-unused-vars
195
+ let { profile_photo_url, email, ...filtered } = comment;
196
+ // Recursively filter replies if they exist
197
+ if (filtered.replies && Array.isArray(filtered.replies)) {
198
+ filtered.replies = filtered.replies.map(filterComment);
199
+ }
200
+ return filtered;
201
+ };
202
+
203
+ return {
204
+ ...data,
205
+ comments: data.comments ? data.comments.map(filterComment) : []
206
+ };
207
+ }
208
+
209
+ // ==================================================================
210
+ // COMPARISON APPROVALS
211
+ // ==================================================================
212
+
213
+ /**
214
+ * Approve a comparison
215
+ */
216
+ async approveComparison(comparisonId, comment, apiToken, apiUrl) {
217
+ if (!comparisonId || typeof comparisonId !== 'string') {
218
+ throw new Error('comparisonId is required and must be a non-empty string');
219
+ }
220
+
221
+ let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approve`;
222
+ let response = await fetch(url, {
223
+ method: 'POST',
224
+ headers: {
225
+ Authorization: `Bearer ${apiToken}`,
226
+ 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
227
+ 'Content-Type': 'application/json'
228
+ },
229
+ body: JSON.stringify({ comment })
230
+ });
231
+
232
+ if (!response.ok) {
233
+ let error = await response.text();
234
+ throw new Error(`Failed to approve comparison (${response.status}): ${error}`);
235
+ }
236
+
237
+ return response.json();
238
+ }
239
+
240
+ /**
241
+ * Reject a comparison
242
+ */
243
+ async rejectComparison(comparisonId, reason, apiToken, apiUrl) {
244
+ if (!comparisonId || typeof comparisonId !== 'string') {
245
+ throw new Error('comparisonId is required and must be a non-empty string');
246
+ }
247
+ if (!reason || typeof reason !== 'string') {
248
+ throw new Error('reason is required and must be a non-empty string');
249
+ }
250
+
251
+ let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/reject`;
252
+ let response = await fetch(url, {
253
+ method: 'POST',
254
+ headers: {
255
+ Authorization: `Bearer ${apiToken}`,
256
+ 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
257
+ 'Content-Type': 'application/json'
258
+ },
259
+ body: JSON.stringify({ reason })
260
+ });
261
+
262
+ if (!response.ok) {
263
+ let error = await response.text();
264
+ throw new Error(`Failed to reject comparison (${response.status}): ${error}`);
265
+ }
266
+
267
+ return response.json();
268
+ }
269
+
270
+ /**
271
+ * Update comparison approval status
272
+ */
273
+ async updateComparisonApproval(comparisonId, approvalStatus, comment, apiToken, apiUrl) {
274
+ let url = `${apiUrl || this.defaultApiUrl}/api/sdk/comparisons/${comparisonId}/approval`;
275
+ let response = await fetch(url, {
276
+ method: 'PUT',
277
+ headers: {
278
+ Authorization: `Bearer ${apiToken}`,
279
+ 'User-Agent': 'Vizzly-Claude-Plugin/0.1.0',
280
+ 'Content-Type': 'application/json'
281
+ },
282
+ body: JSON.stringify({ approval_status: approvalStatus, comment })
283
+ });
284
+
285
+ if (!response.ok) {
286
+ let error = await response.text();
287
+ throw new Error(`Failed to update comparison approval (${response.status}): ${error}`);
288
+ }
289
+
290
+ return response.json();
291
+ }
292
+
293
+ // ==================================================================
294
+ // REVIEW STATUS
295
+ // ==================================================================
296
+
297
+ /**
298
+ * Get review summary for a build
299
+ */
300
+ async getReviewSummary(buildId, apiToken, apiUrl) {
301
+ if (!buildId || typeof buildId !== 'string') {
302
+ throw new Error('buildId is required and must be a non-empty string');
303
+ }
304
+
305
+ let data = await this.makeRequest(
306
+ `/api/sdk/builds/${buildId}/review-summary`,
307
+ apiToken,
308
+ apiUrl
309
+ );
310
+
311
+ return data;
312
+ }
313
+
314
+ // ==================================================================
315
+ // TDD WORKFLOW
316
+ // ==================================================================
317
+
318
+ /**
319
+ * Download baseline screenshots from a cloud build
320
+ * Returns screenshot data that can be saved locally
321
+ */
322
+ async downloadBaselines(buildId, screenshotNames, apiToken, apiUrl) {
323
+ if (!buildId || typeof buildId !== 'string') {
324
+ throw new Error('buildId is required and must be a non-empty string');
325
+ }
326
+
327
+ let data = await this.makeRequest(
328
+ `/api/sdk/builds/${buildId}?include=screenshots`,
329
+ apiToken,
330
+ apiUrl
331
+ );
332
+
333
+ let { build } = data;
334
+ let screenshots = build.screenshots || [];
335
+
336
+ // Filter by screenshot names if provided
337
+ if (screenshotNames && screenshotNames.length > 0) {
338
+ screenshots = screenshots.filter((s) => screenshotNames.includes(s.name));
339
+ }
340
+
341
+ return {
342
+ buildId: build.id,
343
+ buildName: build.name,
344
+ screenshots: screenshots.map((s) => ({
345
+ name: s.name,
346
+ url: s.original_url,
347
+ sha256: s.sha256,
348
+ width: s.viewport_width,
349
+ height: s.viewport_height,
350
+ browser: s.browser
351
+ }))
352
+ };
353
+ }
354
+ }