@vizzly-testing/cli 0.10.2 → 0.11.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 (48) hide show
  1. package/.claude-plugin/.mcp.json +8 -0
  2. package/.claude-plugin/README.md +114 -0
  3. package/.claude-plugin/commands/debug-diff.md +153 -0
  4. package/.claude-plugin/commands/setup.md +137 -0
  5. package/.claude-plugin/commands/suggest-screenshots.md +111 -0
  6. package/.claude-plugin/commands/tdd-status.md +43 -0
  7. package/.claude-plugin/marketplace.json +28 -0
  8. package/.claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
  9. package/.claude-plugin/mcp/vizzly-server/index.js +861 -0
  10. package/.claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
  11. package/.claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
  12. package/.claude-plugin/plugin.json +14 -0
  13. package/README.md +168 -8
  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 +4 -2
  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/services/tdd-service.js +2 -1
  26. package/dist/types/client/index.d.ts +9 -3
  27. package/dist/types/commands/login.d.ts +11 -0
  28. package/dist/types/commands/logout.d.ts +11 -0
  29. package/dist/types/commands/project.d.ts +28 -0
  30. package/dist/types/commands/whoami.d.ts +11 -0
  31. package/dist/types/sdk/index.d.ts +9 -4
  32. package/dist/types/services/api-service.d.ts +2 -1
  33. package/dist/types/services/auth-service.d.ts +59 -0
  34. package/dist/types/utils/browser.d.ts +6 -0
  35. package/dist/types/utils/config-loader.d.ts +1 -1
  36. package/dist/types/utils/config-schema.d.ts +8 -174
  37. package/dist/types/utils/file-helpers.d.ts +18 -0
  38. package/dist/types/utils/global-config.d.ts +84 -0
  39. package/dist/utils/browser.js +44 -0
  40. package/dist/utils/config-loader.js +69 -3
  41. package/dist/utils/file-helpers.js +64 -0
  42. package/dist/utils/global-config.js +259 -0
  43. package/docs/api-reference.md +177 -6
  44. package/docs/authentication.md +334 -0
  45. package/docs/getting-started.md +21 -2
  46. package/docs/plugins.md +27 -0
  47. package/docs/test-integration.md +60 -10
  48. package/package.json +5 -3
@@ -21,197 +21,31 @@ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
21
21
  server: z.ZodDefault<z.ZodObject<{
22
22
  port: z.ZodDefault<z.ZodNumber>;
23
23
  timeout: z.ZodDefault<z.ZodNumber>;
24
- }, "strip", z.ZodTypeAny, {
25
- port?: number;
26
- timeout?: number;
27
- }, {
28
- port?: number;
29
- timeout?: number;
30
- }>>;
24
+ }, z.core.$strip>>;
31
25
  build: z.ZodDefault<z.ZodObject<{
32
26
  name: z.ZodDefault<z.ZodString>;
33
27
  environment: z.ZodDefault<z.ZodString>;
34
28
  branch: z.ZodOptional<z.ZodString>;
35
29
  commit: z.ZodOptional<z.ZodString>;
36
30
  message: z.ZodOptional<z.ZodString>;
37
- }, "strip", z.ZodTypeAny, {
38
- name?: string;
39
- message?: string;
40
- environment?: string;
41
- branch?: string;
42
- commit?: string;
43
- }, {
44
- name?: string;
45
- message?: string;
46
- environment?: string;
47
- branch?: string;
48
- commit?: string;
49
- }>>;
31
+ }, z.core.$strip>>;
50
32
  upload: z.ZodDefault<z.ZodObject<{
51
- screenshotsDir: z.ZodDefault<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
33
+ screenshotsDir: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
52
34
  batchSize: z.ZodDefault<z.ZodNumber>;
53
35
  timeout: z.ZodDefault<z.ZodNumber>;
54
- }, "strip", z.ZodTypeAny, {
55
- timeout?: number;
56
- screenshotsDir?: string | string[];
57
- batchSize?: number;
58
- }, {
59
- timeout?: number;
60
- screenshotsDir?: string | string[];
61
- batchSize?: number;
62
- }>>;
36
+ }, z.core.$strip>>;
63
37
  comparison: z.ZodDefault<z.ZodObject<{
64
38
  threshold: z.ZodDefault<z.ZodNumber>;
65
- }, "strip", z.ZodTypeAny, {
66
- threshold?: number;
67
- }, {
68
- threshold?: number;
69
- }>>;
39
+ }, z.core.$strip>>;
70
40
  tdd: z.ZodDefault<z.ZodObject<{
71
41
  openReport: z.ZodDefault<z.ZodBoolean>;
72
- }, "strip", z.ZodTypeAny, {
73
- openReport?: boolean;
74
- }, {
75
- openReport?: boolean;
76
- }>>;
77
- plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
42
+ }, z.core.$strip>>;
43
+ plugins: z.ZodDefault<z.ZodArray<z.ZodString>>;
78
44
  parallelId: z.ZodOptional<z.ZodString>;
79
45
  baselineBuildId: z.ZodOptional<z.ZodString>;
80
46
  baselineComparisonId: z.ZodOptional<z.ZodString>;
81
47
  eager: z.ZodOptional<z.ZodBoolean>;
82
48
  wait: z.ZodOptional<z.ZodBoolean>;
83
49
  allowNoToken: z.ZodOptional<z.ZodBoolean>;
84
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
85
- apiKey: z.ZodOptional<z.ZodString>;
86
- apiUrl: z.ZodOptional<z.ZodString>;
87
- server: z.ZodDefault<z.ZodObject<{
88
- port: z.ZodDefault<z.ZodNumber>;
89
- timeout: z.ZodDefault<z.ZodNumber>;
90
- }, "strip", z.ZodTypeAny, {
91
- port?: number;
92
- timeout?: number;
93
- }, {
94
- port?: number;
95
- timeout?: number;
96
- }>>;
97
- build: z.ZodDefault<z.ZodObject<{
98
- name: z.ZodDefault<z.ZodString>;
99
- environment: z.ZodDefault<z.ZodString>;
100
- branch: z.ZodOptional<z.ZodString>;
101
- commit: z.ZodOptional<z.ZodString>;
102
- message: z.ZodOptional<z.ZodString>;
103
- }, "strip", z.ZodTypeAny, {
104
- name?: string;
105
- message?: string;
106
- environment?: string;
107
- branch?: string;
108
- commit?: string;
109
- }, {
110
- name?: string;
111
- message?: string;
112
- environment?: string;
113
- branch?: string;
114
- commit?: string;
115
- }>>;
116
- upload: z.ZodDefault<z.ZodObject<{
117
- screenshotsDir: z.ZodDefault<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
118
- batchSize: z.ZodDefault<z.ZodNumber>;
119
- timeout: z.ZodDefault<z.ZodNumber>;
120
- }, "strip", z.ZodTypeAny, {
121
- timeout?: number;
122
- screenshotsDir?: string | string[];
123
- batchSize?: number;
124
- }, {
125
- timeout?: number;
126
- screenshotsDir?: string | string[];
127
- batchSize?: number;
128
- }>>;
129
- comparison: z.ZodDefault<z.ZodObject<{
130
- threshold: z.ZodDefault<z.ZodNumber>;
131
- }, "strip", z.ZodTypeAny, {
132
- threshold?: number;
133
- }, {
134
- threshold?: number;
135
- }>>;
136
- tdd: z.ZodDefault<z.ZodObject<{
137
- openReport: z.ZodDefault<z.ZodBoolean>;
138
- }, "strip", z.ZodTypeAny, {
139
- openReport?: boolean;
140
- }, {
141
- openReport?: boolean;
142
- }>>;
143
- plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
144
- parallelId: z.ZodOptional<z.ZodString>;
145
- baselineBuildId: z.ZodOptional<z.ZodString>;
146
- baselineComparisonId: z.ZodOptional<z.ZodString>;
147
- eager: z.ZodOptional<z.ZodBoolean>;
148
- wait: z.ZodOptional<z.ZodBoolean>;
149
- allowNoToken: z.ZodOptional<z.ZodBoolean>;
150
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
151
- apiKey: z.ZodOptional<z.ZodString>;
152
- apiUrl: z.ZodOptional<z.ZodString>;
153
- server: z.ZodDefault<z.ZodObject<{
154
- port: z.ZodDefault<z.ZodNumber>;
155
- timeout: z.ZodDefault<z.ZodNumber>;
156
- }, "strip", z.ZodTypeAny, {
157
- port?: number;
158
- timeout?: number;
159
- }, {
160
- port?: number;
161
- timeout?: number;
162
- }>>;
163
- build: z.ZodDefault<z.ZodObject<{
164
- name: z.ZodDefault<z.ZodString>;
165
- environment: z.ZodDefault<z.ZodString>;
166
- branch: z.ZodOptional<z.ZodString>;
167
- commit: z.ZodOptional<z.ZodString>;
168
- message: z.ZodOptional<z.ZodString>;
169
- }, "strip", z.ZodTypeAny, {
170
- name?: string;
171
- message?: string;
172
- environment?: string;
173
- branch?: string;
174
- commit?: string;
175
- }, {
176
- name?: string;
177
- message?: string;
178
- environment?: string;
179
- branch?: string;
180
- commit?: string;
181
- }>>;
182
- upload: z.ZodDefault<z.ZodObject<{
183
- screenshotsDir: z.ZodDefault<z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>>;
184
- batchSize: z.ZodDefault<z.ZodNumber>;
185
- timeout: z.ZodDefault<z.ZodNumber>;
186
- }, "strip", z.ZodTypeAny, {
187
- timeout?: number;
188
- screenshotsDir?: string | string[];
189
- batchSize?: number;
190
- }, {
191
- timeout?: number;
192
- screenshotsDir?: string | string[];
193
- batchSize?: number;
194
- }>>;
195
- comparison: z.ZodDefault<z.ZodObject<{
196
- threshold: z.ZodDefault<z.ZodNumber>;
197
- }, "strip", z.ZodTypeAny, {
198
- threshold?: number;
199
- }, {
200
- threshold?: number;
201
- }>>;
202
- tdd: z.ZodDefault<z.ZodObject<{
203
- openReport: z.ZodDefault<z.ZodBoolean>;
204
- }, "strip", z.ZodTypeAny, {
205
- openReport?: boolean;
206
- }, {
207
- openReport?: boolean;
208
- }>>;
209
- plugins: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
210
- parallelId: z.ZodOptional<z.ZodString>;
211
- baselineBuildId: z.ZodOptional<z.ZodString>;
212
- baselineComparisonId: z.ZodOptional<z.ZodString>;
213
- eager: z.ZodOptional<z.ZodBoolean>;
214
- wait: z.ZodOptional<z.ZodBoolean>;
215
- allowNoToken: z.ZodOptional<z.ZodBoolean>;
216
- }, z.ZodTypeAny, "passthrough">>>;
50
+ }, z.core.$loose>>;
217
51
  import { z } from 'zod';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resolve image buffer from file path or return buffer as-is
3
+ * Handles both Buffer inputs and file path strings, with proper validation and error handling
4
+ *
5
+ * @param {Buffer|string} imageBufferOrPath - Image data as Buffer or file path
6
+ * @param {string} contextName - Context for error messages (e.g., 'screenshot', 'compare')
7
+ * @returns {Buffer} The image buffer
8
+ * @throws {VizzlyError} When file not found, unreadable, or invalid input type
9
+ *
10
+ * @example
11
+ * // With Buffer
12
+ * const buffer = resolveImageBuffer(myBuffer, 'screenshot');
13
+ *
14
+ * @example
15
+ * // With file path
16
+ * const buffer = resolveImageBuffer('./my-image.png', 'screenshot');
17
+ */
18
+ export function resolveImageBuffer(imageBufferOrPath: Buffer | string, contextName: string): Buffer;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Get the path to the global Vizzly directory
3
+ * @returns {string} Path to ~/.vizzly
4
+ */
5
+ export function getGlobalConfigDir(): string;
6
+ /**
7
+ * Get the path to the global config file
8
+ * @returns {string} Path to ~/.vizzly/config.json
9
+ */
10
+ export function getGlobalConfigPath(): string;
11
+ /**
12
+ * Load the global configuration
13
+ * @returns {Promise<Object>} Global config object
14
+ */
15
+ export function loadGlobalConfig(): Promise<any>;
16
+ /**
17
+ * Save the global configuration
18
+ * @param {Object} config - Configuration object to save
19
+ * @returns {Promise<void>}
20
+ */
21
+ export function saveGlobalConfig(config: any): Promise<void>;
22
+ /**
23
+ * Clear all global configuration
24
+ * @returns {Promise<void>}
25
+ */
26
+ export function clearGlobalConfig(): Promise<void>;
27
+ /**
28
+ * Get authentication tokens from global config
29
+ * @returns {Promise<Object|null>} Token object with accessToken, refreshToken, expiresAt, user, or null if not found
30
+ */
31
+ export function getAuthTokens(): Promise<any | null>;
32
+ /**
33
+ * Save authentication tokens to global config
34
+ * @param {Object} auth - Auth object with accessToken, refreshToken, expiresAt, user
35
+ * @returns {Promise<void>}
36
+ */
37
+ export function saveAuthTokens(auth: any): Promise<void>;
38
+ /**
39
+ * Clear authentication tokens from global config
40
+ * @returns {Promise<void>}
41
+ */
42
+ export function clearAuthTokens(): Promise<void>;
43
+ /**
44
+ * Check if authentication tokens exist and are not expired
45
+ * @returns {Promise<boolean>} True if valid tokens exist
46
+ */
47
+ export function hasValidTokens(): Promise<boolean>;
48
+ /**
49
+ * Get the access token from global config if available
50
+ * @returns {Promise<string|null>} Access token or null
51
+ */
52
+ export function getAccessToken(): Promise<string | null>;
53
+ /**
54
+ * Get project mapping for a directory
55
+ * Walks up the directory tree to find the closest mapping
56
+ * @param {string} directoryPath - Absolute path to project directory
57
+ * @returns {Promise<Object|null>} Project data or null
58
+ */
59
+ export function getProjectMapping(directoryPath: string): Promise<any | null>;
60
+ /**
61
+ * Save project mapping for a directory
62
+ * @param {string} directoryPath - Absolute path to project directory
63
+ * @param {Object} projectData - Project configuration
64
+ * @param {string} projectData.token - Project API token (vzt_...)
65
+ * @param {string} projectData.projectSlug - Project slug
66
+ * @param {string} projectData.organizationSlug - Organization slug
67
+ * @param {string} projectData.projectName - Project name
68
+ */
69
+ export function saveProjectMapping(directoryPath: string, projectData: {
70
+ token: string;
71
+ projectSlug: string;
72
+ organizationSlug: string;
73
+ projectName: string;
74
+ }): Promise<void>;
75
+ /**
76
+ * Get all project mappings
77
+ * @returns {Promise<Object>} Map of directory paths to project data
78
+ */
79
+ export function getProjectMappings(): Promise<any>;
80
+ /**
81
+ * Delete project mapping for a directory
82
+ * @param {string} directoryPath - Absolute path to project directory
83
+ */
84
+ export function deleteProjectMapping(directoryPath: string): Promise<void>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Browser utilities for opening URLs
3
+ */
4
+
5
+ import { execFile } from 'child_process';
6
+ import { platform } from 'os';
7
+
8
+ /**
9
+ * Open a URL in the default browser
10
+ * @param {string} url - URL to open
11
+ * @returns {Promise<boolean>} True if successful
12
+ */
13
+ export async function openBrowser(url) {
14
+ return new Promise(resolve => {
15
+ let command;
16
+ let args;
17
+ let os = platform();
18
+ switch (os) {
19
+ case 'darwin':
20
+ // macOS
21
+ command = 'open';
22
+ args = [url];
23
+ break;
24
+ case 'win32':
25
+ // Windows
26
+ command = 'cmd.exe';
27
+ args = ['/c', 'start', '""', url];
28
+ break;
29
+ default:
30
+ // Linux and others
31
+ command = 'xdg-open';
32
+ args = [url];
33
+ break;
34
+ }
35
+ execFile(command, args, error => {
36
+ if (error) {
37
+ // Browser opening failed, but don't throw - user can manually open
38
+ resolve(false);
39
+ } else {
40
+ resolve(true);
41
+ }
42
+ });
43
+ });
44
+ }
@@ -2,9 +2,11 @@ import { cosmiconfigSync } from 'cosmiconfig';
2
2
  import { resolve } from 'path';
3
3
  import { getApiToken, getApiUrl, getParallelId } from './environment-config.js';
4
4
  import { validateVizzlyConfigWithDefaults } from './config-schema.js';
5
+ import { getAccessToken, getProjectMapping } from './global-config.js';
5
6
  const DEFAULT_CONFIG = {
6
7
  // API Configuration
7
- apiKey: getApiToken(),
8
+ apiKey: undefined,
9
+ // Will be set from env, global config, or CLI overrides
8
10
  apiUrl: getApiUrl(),
9
11
  // Server Configuration (for run command)
10
12
  server: {
@@ -70,16 +72,80 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
70
72
  // Merge validated file config
71
73
  mergeConfig(config, validatedFileConfig);
72
74
 
73
- // 3. Override with environment variables
75
+ // 3. Check project mapping for current directory (if no CLI flag)
76
+ if (!cliOverrides.token) {
77
+ const currentDir = process.cwd();
78
+ if (process.env.DEBUG_CONFIG) {
79
+ console.log('[CONFIG] Looking up project mapping for:', currentDir);
80
+ }
81
+ const projectMapping = await getProjectMapping(currentDir);
82
+ if (projectMapping && projectMapping.token) {
83
+ // Handle both string tokens and token objects (backward compatibility)
84
+ let token;
85
+ if (typeof projectMapping.token === 'string') {
86
+ token = projectMapping.token;
87
+ } else if (typeof projectMapping.token === 'object' && projectMapping.token.token) {
88
+ // Handle nested token object from old API responses
89
+ token = projectMapping.token.token;
90
+ } else {
91
+ token = String(projectMapping.token);
92
+ }
93
+ config.apiKey = token;
94
+ config.projectSlug = projectMapping.projectSlug;
95
+ config.organizationSlug = projectMapping.organizationSlug;
96
+
97
+ // Debug logging
98
+ if (process.env.DEBUG_CONFIG) {
99
+ console.log('[CONFIG] Found project mapping:', {
100
+ dir: currentDir,
101
+ projectSlug: projectMapping.projectSlug,
102
+ hasToken: !!projectMapping.token,
103
+ tokenType: typeof projectMapping.token,
104
+ tokenPrefix: token ? token.substring(0, 8) + '***' : 'none'
105
+ });
106
+ console.log('[CONFIG] Set config.apiKey to:', config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE');
107
+ }
108
+ } else if (process.env.DEBUG_CONFIG) {
109
+ console.log('[CONFIG] No project mapping found for:', currentDir);
110
+ }
111
+ }
112
+
113
+ // 3.5. Check global config for user access token (if no CLI flag)
114
+ if (!config.apiKey && !cliOverrides.token) {
115
+ const globalToken = await getAccessToken();
116
+ if (globalToken) {
117
+ config.apiKey = globalToken;
118
+ }
119
+ }
120
+
121
+ // 4. Override with environment variables (higher priority than fallbacks)
74
122
  const envApiKey = getApiToken();
75
123
  const envApiUrl = getApiUrl();
76
124
  const envParallelId = getParallelId();
125
+ if (process.env.DEBUG_CONFIG) {
126
+ console.log('[CONFIG] Step 4 - env vars:', JSON.stringify({
127
+ hasEnvApiKey: !!envApiKey,
128
+ envApiKeyPrefix: envApiKey ? envApiKey.substring(0, 8) + '***' : 'none',
129
+ configApiKeyBefore: config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE'
130
+ }));
131
+ }
77
132
  if (envApiKey) config.apiKey = envApiKey;
78
133
  if (envApiUrl !== 'https://app.vizzly.dev') config.apiUrl = envApiUrl;
79
134
  if (envParallelId) config.parallelId = envParallelId;
80
135
 
81
- // 4. Apply CLI overrides (highest priority)
136
+ // 5. Apply CLI overrides (highest priority)
137
+ if (process.env.DEBUG_CONFIG) {
138
+ console.log('[CONFIG] Step 5 - before CLI overrides:', {
139
+ configApiKey: config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE',
140
+ cliToken: cliOverrides.token ? cliOverrides.token.substring(0, 8) + '***' : 'none'
141
+ });
142
+ }
82
143
  applyCLIOverrides(config, cliOverrides);
144
+ if (process.env.DEBUG_CONFIG) {
145
+ console.log('[CONFIG] Step 6 - after CLI overrides:', {
146
+ configApiKey: config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE'
147
+ });
148
+ }
83
149
  return config;
84
150
  }
85
151
 
@@ -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
+ }