@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,64 @@
1
+ /**
2
+ * @module file-helpers
3
+ * @description Utilities for handling file-based screenshot inputs
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import { VizzlyError } from '../errors/vizzly-error.js';
9
+
10
+ /**
11
+ * Resolve image buffer from file path or return buffer as-is
12
+ * Handles both Buffer inputs and file path strings, with proper validation and error handling
13
+ *
14
+ * @param {Buffer|string} imageBufferOrPath - Image data as Buffer or file path
15
+ * @param {string} contextName - Context for error messages (e.g., 'screenshot', 'compare')
16
+ * @returns {Buffer} The image buffer
17
+ * @throws {VizzlyError} When file not found, unreadable, or invalid input type
18
+ *
19
+ * @example
20
+ * // With Buffer
21
+ * const buffer = resolveImageBuffer(myBuffer, 'screenshot');
22
+ *
23
+ * @example
24
+ * // With file path
25
+ * const buffer = resolveImageBuffer('./my-image.png', 'screenshot');
26
+ */
27
+ export function resolveImageBuffer(imageBufferOrPath, contextName) {
28
+ // Return Buffer as-is
29
+ if (Buffer.isBuffer(imageBufferOrPath)) {
30
+ return imageBufferOrPath;
31
+ }
32
+
33
+ // Validate input type
34
+ if (typeof imageBufferOrPath !== 'string') {
35
+ throw new VizzlyError(`Invalid image input: expected Buffer or file path string`, 'INVALID_INPUT', {
36
+ contextName,
37
+ type: typeof imageBufferOrPath
38
+ });
39
+ }
40
+
41
+ // Resolve to absolute path for consistent behavior
42
+ const filePath = resolve(imageBufferOrPath);
43
+
44
+ // Check file exists
45
+ if (!existsSync(filePath)) {
46
+ throw new VizzlyError(`Screenshot file not found: ${imageBufferOrPath}`, 'FILE_NOT_FOUND', {
47
+ contextName,
48
+ filePath,
49
+ originalPath: imageBufferOrPath
50
+ });
51
+ }
52
+
53
+ // Read file with error handling
54
+ try {
55
+ return readFileSync(filePath);
56
+ } catch (error) {
57
+ throw new VizzlyError(`Failed to read screenshot file: ${imageBufferOrPath} - ${error.message}`, 'FILE_READ_ERROR', {
58
+ contextName,
59
+ filePath,
60
+ originalPath: imageBufferOrPath,
61
+ originalError: error.message
62
+ });
63
+ }
64
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Global User Configuration Utilities
3
+ * Manages ~/.vizzly/config.json for storing authentication tokens
4
+ */
5
+
6
+ import { homedir } from 'os';
7
+ import { join, dirname, parse } from 'path';
8
+ import { readFile, writeFile, mkdir, chmod } from 'fs/promises';
9
+ import { existsSync } from 'fs';
10
+
11
+ /**
12
+ * Get the path to the global Vizzly directory
13
+ * @returns {string} Path to ~/.vizzly
14
+ */
15
+ export function getGlobalConfigDir() {
16
+ return join(homedir(), '.vizzly');
17
+ }
18
+
19
+ /**
20
+ * Get the path to the global config file
21
+ * @returns {string} Path to ~/.vizzly/config.json
22
+ */
23
+ export function getGlobalConfigPath() {
24
+ return join(getGlobalConfigDir(), 'config.json');
25
+ }
26
+
27
+ /**
28
+ * Ensure the global config directory exists with proper permissions
29
+ * @returns {Promise<void>}
30
+ */
31
+ async function ensureGlobalConfigDir() {
32
+ let dir = getGlobalConfigDir();
33
+ if (!existsSync(dir)) {
34
+ await mkdir(dir, {
35
+ recursive: true,
36
+ mode: 0o700
37
+ });
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Load the global configuration
43
+ * @returns {Promise<Object>} Global config object
44
+ */
45
+ export async function loadGlobalConfig() {
46
+ try {
47
+ let configPath = getGlobalConfigPath();
48
+ if (!existsSync(configPath)) {
49
+ return {};
50
+ }
51
+ let content = await readFile(configPath, 'utf-8');
52
+ return JSON.parse(content);
53
+ } catch (error) {
54
+ // If file doesn't exist or is corrupted, return empty config
55
+ if (error.code === 'ENOENT') {
56
+ return {};
57
+ }
58
+
59
+ // Log warning about corrupted config but don't crash
60
+ console.warn('Warning: Global config file is corrupted, ignoring');
61
+ return {};
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Save the global configuration
67
+ * @param {Object} config - Configuration object to save
68
+ * @returns {Promise<void>}
69
+ */
70
+ export async function saveGlobalConfig(config) {
71
+ await ensureGlobalConfigDir();
72
+ let configPath = getGlobalConfigPath();
73
+ let content = JSON.stringify(config, null, 2);
74
+
75
+ // Write file with secure permissions (owner read/write only)
76
+ await writeFile(configPath, content, {
77
+ mode: 0o600
78
+ });
79
+
80
+ // Ensure permissions are set correctly (in case umask interfered)
81
+ try {
82
+ await chmod(configPath, 0o600);
83
+ } catch (error) {
84
+ // On Windows, chmod may not work as expected, but that's okay
85
+ if (process.platform !== 'win32') {
86
+ throw error;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Clear all global configuration
93
+ * @returns {Promise<void>}
94
+ */
95
+ export async function clearGlobalConfig() {
96
+ await saveGlobalConfig({});
97
+ }
98
+
99
+ /**
100
+ * Get authentication tokens from global config
101
+ * @returns {Promise<Object|null>} Token object with accessToken, refreshToken, expiresAt, user, or null if not found
102
+ */
103
+ export async function getAuthTokens() {
104
+ let config = await loadGlobalConfig();
105
+ if (!config.auth || !config.auth.accessToken) {
106
+ return null;
107
+ }
108
+ return config.auth;
109
+ }
110
+
111
+ /**
112
+ * Save authentication tokens to global config
113
+ * @param {Object} auth - Auth object with accessToken, refreshToken, expiresAt, user
114
+ * @returns {Promise<void>}
115
+ */
116
+ export async function saveAuthTokens(auth) {
117
+ let config = await loadGlobalConfig();
118
+ config.auth = {
119
+ accessToken: auth.accessToken,
120
+ refreshToken: auth.refreshToken,
121
+ expiresAt: auth.expiresAt,
122
+ user: auth.user
123
+ };
124
+ await saveGlobalConfig(config);
125
+ }
126
+
127
+ /**
128
+ * Clear authentication tokens from global config
129
+ * @returns {Promise<void>}
130
+ */
131
+ export async function clearAuthTokens() {
132
+ let config = await loadGlobalConfig();
133
+ delete config.auth;
134
+ await saveGlobalConfig(config);
135
+ }
136
+
137
+ /**
138
+ * Check if authentication tokens exist and are not expired
139
+ * @returns {Promise<boolean>} True if valid tokens exist
140
+ */
141
+ export async function hasValidTokens() {
142
+ let auth = await getAuthTokens();
143
+ if (!auth || !auth.accessToken) {
144
+ return false;
145
+ }
146
+
147
+ // Check if token is expired
148
+ if (auth.expiresAt) {
149
+ let expiresAt = new Date(auth.expiresAt);
150
+ let now = new Date();
151
+
152
+ // Consider expired if within 5 minutes of expiry
153
+ let bufferMs = 5 * 60 * 1000;
154
+ if (now.getTime() >= expiresAt.getTime() - bufferMs) {
155
+ return false;
156
+ }
157
+ }
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Get the access token from global config if available
163
+ * @returns {Promise<string|null>} Access token or null
164
+ */
165
+ export async function getAccessToken() {
166
+ let auth = await getAuthTokens();
167
+ return auth?.accessToken || null;
168
+ }
169
+
170
+ /**
171
+ * Get project mapping for a directory
172
+ * Walks up the directory tree to find the closest mapping
173
+ * @param {string} directoryPath - Absolute path to project directory
174
+ * @returns {Promise<Object|null>} Project data or null
175
+ */
176
+ export async function getProjectMapping(directoryPath) {
177
+ let config = await loadGlobalConfig();
178
+ if (!config.projects) {
179
+ if (process.env.DEBUG_CONFIG) {
180
+ console.log('[MAPPING] No projects in global config');
181
+ }
182
+ return null;
183
+ }
184
+
185
+ // Walk up the directory tree looking for a mapping
186
+ let currentPath = directoryPath;
187
+ let {
188
+ root
189
+ } = parse(currentPath);
190
+ if (process.env.DEBUG_CONFIG) {
191
+ console.log('[MAPPING] Starting lookup from:', currentPath);
192
+ console.log('[MAPPING] Available mappings:', Object.keys(config.projects));
193
+ }
194
+ while (currentPath !== root) {
195
+ if (process.env.DEBUG_CONFIG) {
196
+ console.log('[MAPPING] Checking:', currentPath);
197
+ }
198
+ if (config.projects[currentPath]) {
199
+ if (process.env.DEBUG_CONFIG) {
200
+ console.log('[MAPPING] Found match at:', currentPath);
201
+ }
202
+ return config.projects[currentPath];
203
+ }
204
+
205
+ // Move to parent directory
206
+ let parentPath = dirname(currentPath);
207
+ if (parentPath === currentPath) {
208
+ // We've reached the root
209
+ break;
210
+ }
211
+ currentPath = parentPath;
212
+ }
213
+ if (process.env.DEBUG_CONFIG) {
214
+ console.log('[MAPPING] No mapping found');
215
+ }
216
+ return null;
217
+ }
218
+
219
+ /**
220
+ * Save project mapping for a directory
221
+ * @param {string} directoryPath - Absolute path to project directory
222
+ * @param {Object} projectData - Project configuration
223
+ * @param {string} projectData.token - Project API token (vzt_...)
224
+ * @param {string} projectData.projectSlug - Project slug
225
+ * @param {string} projectData.organizationSlug - Organization slug
226
+ * @param {string} projectData.projectName - Project name
227
+ */
228
+ export async function saveProjectMapping(directoryPath, projectData) {
229
+ let config = await loadGlobalConfig();
230
+ if (!config.projects) {
231
+ config.projects = {};
232
+ }
233
+ config.projects[directoryPath] = {
234
+ ...projectData,
235
+ createdAt: new Date().toISOString()
236
+ };
237
+ await saveGlobalConfig(config);
238
+ }
239
+
240
+ /**
241
+ * Get all project mappings
242
+ * @returns {Promise<Object>} Map of directory paths to project data
243
+ */
244
+ export async function getProjectMappings() {
245
+ let config = await loadGlobalConfig();
246
+ return config.projects || {};
247
+ }
248
+
249
+ /**
250
+ * Delete project mapping for a directory
251
+ * @param {string} directoryPath - Absolute path to project directory
252
+ */
253
+ export async function deleteProjectMapping(directoryPath) {
254
+ let config = await loadGlobalConfig();
255
+ if (config.projects && config.projects[directoryPath]) {
256
+ delete config.projects[directoryPath];
257
+ await saveGlobalConfig(config);
258
+ }
259
+ }
@@ -12,7 +12,7 @@ Capture a screenshot for visual regression testing.
12
12
 
13
13
  **Parameters:**
14
14
  - `name` (string) - Unique screenshot identifier
15
- - `imageBuffer` (Buffer) - PNG image data as Buffer
15
+ - `imageBuffer` (Buffer | string) - PNG image data as Buffer, or file path to an image
16
16
  - `options` (object, optional) - Configuration and metadata
17
17
 
18
18
  **Options:**
@@ -20,7 +20,7 @@ Capture a screenshot for visual regression testing.
20
20
  {
21
21
  // Comparison settings
22
22
  threshold: 0.01, // Pixel difference threshold (0-1)
23
-
23
+
24
24
  // Metadata for organization (all optional)
25
25
  properties: {
26
26
  browser: 'chrome', // Browser name
@@ -39,7 +39,10 @@ Capture a screenshot for visual regression testing.
39
39
 
40
40
  **Returns:** `Promise<void>`
41
41
 
42
- **Example:**
42
+ **Examples:**
43
+
44
+ Using a Buffer:
45
+
43
46
  ```javascript
44
47
  import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
45
48
 
@@ -54,6 +57,31 @@ await vizzlyScreenshot('homepage', screenshot, {
54
57
  });
55
58
  ```
56
59
 
60
+ Using a file path:
61
+
62
+ ```javascript
63
+ import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
64
+
65
+ // Save screenshot to file
66
+ await page.screenshot({ path: './screenshots/homepage.png' });
67
+
68
+ // Send to Vizzly using file path
69
+ await vizzlyScreenshot('homepage', './screenshots/homepage.png', {
70
+ threshold: 0.02,
71
+ properties: {
72
+ browser: 'chrome',
73
+ viewport: '1920x1080',
74
+ component: 'hero-section'
75
+ }
76
+ });
77
+ ```
78
+
79
+ **File Path Support:**
80
+ - Accepts both absolute and relative paths
81
+ - Automatically reads the file and converts to Buffer internally
82
+ - Throws error if file doesn't exist or cannot be read
83
+ - Works with any PNG image file
84
+
57
85
  ### `vizzlyFlush()`
58
86
 
59
87
  Wait for all queued screenshots to be processed.
@@ -201,10 +229,24 @@ Stop the Vizzly server and cleanup resources.
201
229
  **Returns:** `Promise<void>`
202
230
 
203
231
  ##### `screenshot(name, imageBuffer, options)`
204
- Capture a screenshot (same as client API).
232
+ Capture a screenshot.
233
+
234
+ **Parameters:**
235
+ - `name` (string) - Unique screenshot identifier
236
+ - `imageBuffer` (Buffer | string) - PNG image data as Buffer, or file path to an image
237
+ - `options` (object, optional) - Configuration and metadata
205
238
 
206
239
  **Returns:** `Promise<void>`
207
240
 
241
+ **Example:**
242
+ ```javascript
243
+ // Using a Buffer
244
+ await vizzly.screenshot('homepage', buffer);
245
+
246
+ // Using a file path
247
+ await vizzly.screenshot('homepage', './screenshots/homepage.png');
248
+ ```
249
+
208
250
  ##### `upload(options)`
209
251
  Upload screenshots to Vizzly.
210
252
 
@@ -224,8 +266,21 @@ Upload screenshots to Vizzly.
224
266
  ##### `compare(name, imageBuffer)`
225
267
  Run local comparison (TDD mode).
226
268
 
269
+ **Parameters:**
270
+ - `name` (string) - Screenshot name
271
+ - `imageBuffer` (Buffer | string) - PNG image data as Buffer, or file path to an image
272
+
227
273
  **Returns:** `Promise<ComparisonResult>`
228
274
 
275
+ **Example:**
276
+ ```javascript
277
+ // Using a Buffer
278
+ const result = await vizzly.compare('homepage', buffer);
279
+
280
+ // Using a file path
281
+ const result = await vizzly.compare('homepage', './screenshots/homepage.png');
282
+ ```
283
+
229
284
  ##### `getConfig()`
230
285
  Get current SDK configuration.
231
286
 
@@ -271,6 +326,117 @@ vizzly.on('comparison:failed', (error) => {
271
326
 
272
327
  ## CLI Commands
273
328
 
329
+ ### Authentication Commands
330
+
331
+ #### `vizzly login`
332
+
333
+ Authenticate using OAuth 2.0 device flow.
334
+
335
+ **Options:**
336
+ - `--json` - Machine-readable JSON output
337
+ - `--verbose` - Verbose output
338
+
339
+ **Exit Codes:**
340
+ - `0` - Login successful
341
+ - `1` - Login failed
342
+
343
+ **Example:**
344
+ ```bash
345
+ vizzly login
346
+ ```
347
+
348
+ #### `vizzly logout`
349
+
350
+ Clear stored authentication tokens.
351
+
352
+ **Options:**
353
+ - `--json` - Machine-readable JSON output
354
+ - `--verbose` - Verbose output
355
+
356
+ **Exit Codes:**
357
+ - `0` - Logout successful
358
+ - `1` - Logout failed
359
+
360
+ **Example:**
361
+ ```bash
362
+ vizzly logout
363
+ ```
364
+
365
+ #### `vizzly whoami`
366
+
367
+ Display current user and authentication status.
368
+
369
+ **Options:**
370
+ - `--json` - Machine-readable JSON output
371
+
372
+ **Exit Codes:**
373
+ - `0` - Success
374
+ - `1` - Not authenticated or error
375
+
376
+ **Example:**
377
+ ```bash
378
+ vizzly whoami
379
+ ```
380
+
381
+ #### `vizzly project:select`
382
+
383
+ Configure project-specific token for current directory.
384
+
385
+ **Options:**
386
+ - `--json` - Machine-readable JSON output
387
+
388
+ **Exit Codes:**
389
+ - `0` - Project configured successfully
390
+ - `1` - Configuration failed
391
+
392
+ **Example:**
393
+ ```bash
394
+ cd /path/to/project
395
+ vizzly project:select
396
+ ```
397
+
398
+ #### `vizzly project:list`
399
+
400
+ Show all configured projects.
401
+
402
+ **Exit Codes:**
403
+ - `0` - Success
404
+ - `1` - Error
405
+
406
+ **Example:**
407
+ ```bash
408
+ vizzly project:list
409
+ ```
410
+
411
+ #### `vizzly project:token`
412
+
413
+ Display project token for current directory.
414
+
415
+ **Options:**
416
+ - `--json` - Machine-readable JSON output
417
+
418
+ **Exit Codes:**
419
+ - `0` - Success
420
+ - `1` - No project configured or error
421
+
422
+ **Example:**
423
+ ```bash
424
+ vizzly project:token
425
+ ```
426
+
427
+ #### `vizzly project:remove`
428
+
429
+ Remove project configuration for current directory.
430
+
431
+ **Exit Codes:**
432
+ - `0` - Success
433
+ - `1` - No project configured or error
434
+
435
+ **Example:**
436
+ ```bash
437
+ vizzly project:remove
438
+ ```
439
+
274
440
  ### `vizzly upload <path>`
275
441
 
276
442
  Upload screenshots from a directory.
@@ -502,9 +668,14 @@ Configuration loaded via cosmiconfig in this order:
502
668
 
503
669
  ### Environment Variables
504
670
 
671
+ **Authentication:**
672
+ - `VIZZLY_TOKEN` - API authentication token (project token or access token)
673
+ - For local development: Use `vizzly login` instead of manually managing tokens
674
+ - For CI/CD: Use project tokens from environment variables
675
+ - Token priority: CLI flag → env var → project mapping → user access token
676
+
505
677
  **Core Configuration:**
506
- - `VIZZLY_TOKEN` - API authentication token
507
- - `VIZZLY_API_URL` - API base URL override
678
+ - `VIZZLY_API_URL` - API base URL override (default: `https://app.vizzly.dev`)
508
679
  - `VIZZLY_LOG_LEVEL` - Logger level (`debug`, `info`, `warn`, `error`)
509
680
 
510
681
  **Parallel Builds:**