@vizzly-testing/cli 0.10.3 → 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 (47) 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/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,351 @@
1
+ /**
2
+ * Project management commands
3
+ * Select, list, and manage project tokens
4
+ */
5
+
6
+ import { ConsoleUI } from '../utils/console-ui.js';
7
+ import { AuthService } from '../services/auth-service.js';
8
+ import { getApiUrl } from '../utils/environment-config.js';
9
+ import { getAuthTokens, saveProjectMapping, getProjectMapping, getProjectMappings, deleteProjectMapping } from '../utils/global-config.js';
10
+ import { resolve } from 'path';
11
+ import readline from 'readline';
12
+
13
+ /**
14
+ * Project select command - configure project for current directory
15
+ * @param {Object} options - Command options
16
+ * @param {Object} globalOptions - Global CLI options
17
+ */
18
+ export async function projectSelectCommand(options = {}, globalOptions = {}) {
19
+ let ui = new ConsoleUI({
20
+ json: globalOptions.json,
21
+ verbose: globalOptions.verbose,
22
+ color: !globalOptions.noColor
23
+ });
24
+ try {
25
+ // Check authentication
26
+ let auth = await getAuthTokens();
27
+ if (!auth || !auth.accessToken) {
28
+ ui.error('Not authenticated', null, 0);
29
+ console.log(''); // Empty line for spacing
30
+ ui.info('Run "vizzly login" to authenticate first');
31
+ process.exit(1);
32
+ }
33
+ let authService = new AuthService({
34
+ baseUrl: options.apiUrl || getApiUrl()
35
+ });
36
+
37
+ // Get user info to show organizations
38
+ ui.startSpinner('Fetching organizations...');
39
+ let userInfo = await authService.whoami();
40
+ ui.stopSpinner();
41
+ if (!userInfo.organizations || userInfo.organizations.length === 0) {
42
+ ui.error('No organizations found', null, 0);
43
+ console.log(''); // Empty line for spacing
44
+ ui.info('Create an organization at https://vizzly.dev');
45
+ process.exit(1);
46
+ }
47
+
48
+ // Select organization
49
+ console.log(''); // Empty line for spacing
50
+ ui.info('Select an organization:');
51
+ console.log(''); // Empty line for spacing
52
+
53
+ userInfo.organizations.forEach((org, index) => {
54
+ console.log(` ${index + 1}. ${org.name} (@${org.slug})`);
55
+ });
56
+ console.log(''); // Empty line for spacing
57
+ let orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
58
+ let selectedOrg = userInfo.organizations[orgChoice - 1];
59
+
60
+ // List projects for organization
61
+ ui.startSpinner(`Fetching projects for ${selectedOrg.name}...`);
62
+ let response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
63
+ headers: {
64
+ Authorization: `Bearer ${auth.accessToken}`,
65
+ 'X-Organization': selectedOrg.slug
66
+ }
67
+ });
68
+ ui.stopSpinner();
69
+
70
+ // Handle both array response and object with projects property
71
+ let projects = Array.isArray(response) ? response : response.projects || [];
72
+ if (projects.length === 0) {
73
+ ui.error('No projects found', null, 0);
74
+ console.log(''); // Empty line for spacing
75
+ ui.info(`Create a project in ${selectedOrg.name} at https://vizzly.dev`);
76
+ process.exit(1);
77
+ }
78
+
79
+ // Select project
80
+ console.log(''); // Empty line for spacing
81
+ ui.info('Select a project:');
82
+ console.log(''); // Empty line for spacing
83
+
84
+ projects.forEach((project, index) => {
85
+ console.log(` ${index + 1}. ${project.name} (${project.slug})`);
86
+ });
87
+ console.log(''); // Empty line for spacing
88
+ let projectChoice = await promptNumber('Enter number', 1, projects.length);
89
+ let selectedProject = projects[projectChoice - 1];
90
+
91
+ // Create API token for project
92
+ ui.startSpinner(`Creating API token for ${selectedProject.name}...`);
93
+ let tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
94
+ method: 'POST',
95
+ headers: {
96
+ Authorization: `Bearer ${auth.accessToken}`,
97
+ 'X-Organization': selectedOrg.slug,
98
+ 'Content-Type': 'application/json'
99
+ },
100
+ body: JSON.stringify({
101
+ name: `CLI Token - ${new Date().toLocaleDateString()}`,
102
+ description: `Generated by vizzly CLI for ${process.cwd()}`
103
+ })
104
+ });
105
+ ui.stopSpinner();
106
+
107
+ // Save project mapping
108
+ let currentDir = resolve(process.cwd());
109
+ await saveProjectMapping(currentDir, {
110
+ token: tokenResponse.token,
111
+ projectSlug: selectedProject.slug,
112
+ projectName: selectedProject.name,
113
+ organizationSlug: selectedOrg.slug
114
+ });
115
+ ui.success('Project configured!');
116
+ console.log(''); // Empty line for spacing
117
+ ui.info(`Project: ${selectedProject.name}`);
118
+ ui.info(`Organization: ${selectedOrg.name}`);
119
+ ui.info(`Directory: ${currentDir}`);
120
+ ui.cleanup();
121
+ } catch (error) {
122
+ ui.stopSpinner();
123
+ ui.error('Failed to configure project', error, 0);
124
+ if (globalOptions.verbose && error.stack) {
125
+ console.error(''); // Empty line for spacing
126
+ console.error(error.stack);
127
+ }
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Project list command - show all configured projects
134
+ * @param {Object} _options - Command options (unused)
135
+ * @param {Object} globalOptions - Global CLI options
136
+ */
137
+ export async function projectListCommand(_options = {}, globalOptions = {}) {
138
+ let ui = new ConsoleUI({
139
+ json: globalOptions.json,
140
+ verbose: globalOptions.verbose,
141
+ color: !globalOptions.noColor
142
+ });
143
+ try {
144
+ let mappings = await getProjectMappings();
145
+ let paths = Object.keys(mappings);
146
+ if (paths.length === 0) {
147
+ ui.info('No projects configured');
148
+ console.log(''); // Empty line for spacing
149
+ ui.info('Run "vizzly project:select" to configure a project');
150
+ ui.cleanup();
151
+ return;
152
+ }
153
+ if (globalOptions.json) {
154
+ ui.data(mappings);
155
+ ui.cleanup();
156
+ return;
157
+ }
158
+ ui.info('Configured projects:');
159
+ console.log(''); // Empty line for spacing
160
+
161
+ let currentDir = resolve(process.cwd());
162
+ for (let path of paths) {
163
+ let mapping = mappings[path];
164
+ let isCurrent = path === currentDir;
165
+ let marker = isCurrent ? '→' : ' ';
166
+
167
+ // Extract token string (handle both string and object formats)
168
+ let tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
169
+ console.log(`${marker} ${path}`);
170
+ console.log(` Project: ${mapping.projectName} (${mapping.projectSlug})`);
171
+ console.log(` Organization: ${mapping.organizationSlug}`);
172
+ if (globalOptions.verbose) {
173
+ console.log(` Token: ${tokenStr.substring(0, 20)}...`);
174
+ console.log(` Created: ${new Date(mapping.createdAt).toLocaleString()}`);
175
+ }
176
+ console.log(''); // Empty line for spacing
177
+ }
178
+ ui.cleanup();
179
+ } catch (error) {
180
+ ui.error('Failed to list projects', error, 0);
181
+ if (globalOptions.verbose && error.stack) {
182
+ console.error(''); // Empty line for spacing
183
+ console.error(error.stack);
184
+ }
185
+ process.exit(1);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Project token command - show/regenerate token for current directory
191
+ * @param {Object} _options - Command options (unused)
192
+ * @param {Object} globalOptions - Global CLI options
193
+ */
194
+ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
195
+ let ui = new ConsoleUI({
196
+ json: globalOptions.json,
197
+ verbose: globalOptions.verbose,
198
+ color: !globalOptions.noColor
199
+ });
200
+ try {
201
+ let currentDir = resolve(process.cwd());
202
+ let mapping = await getProjectMapping(currentDir);
203
+ if (!mapping) {
204
+ ui.error('No project configured for this directory', null, 0);
205
+ console.log(''); // Empty line for spacing
206
+ ui.info('Run "vizzly project:select" to configure a project');
207
+ process.exit(1);
208
+ }
209
+
210
+ // Extract token string (handle both string and object formats)
211
+ let tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
212
+ if (globalOptions.json) {
213
+ ui.data({
214
+ token: tokenStr,
215
+ projectSlug: mapping.projectSlug,
216
+ organizationSlug: mapping.organizationSlug
217
+ });
218
+ ui.cleanup();
219
+ return;
220
+ }
221
+ ui.info('Project token:');
222
+ console.log(''); // Empty line for spacing
223
+ console.log(` ${tokenStr}`);
224
+ console.log(''); // Empty line for spacing
225
+ ui.info(`Project: ${mapping.projectName} (${mapping.projectSlug})`);
226
+ ui.info(`Organization: ${mapping.organizationSlug}`);
227
+ ui.cleanup();
228
+ } catch (error) {
229
+ ui.error('Failed to get project token', error, 0);
230
+ if (globalOptions.verbose && error.stack) {
231
+ console.error(''); // Empty line for spacing
232
+ console.error(error.stack);
233
+ }
234
+ process.exit(1);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Helper to make authenticated API request
240
+ */
241
+ async function makeAuthenticatedRequest(url, options = {}) {
242
+ let response = await fetch(url, options);
243
+ if (!response.ok) {
244
+ let errorText = '';
245
+ try {
246
+ let errorData = await response.json();
247
+ errorText = errorData.error || errorData.message || '';
248
+ } catch {
249
+ errorText = await response.text();
250
+ }
251
+ throw new Error(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''}`);
252
+ }
253
+ return response.json();
254
+ }
255
+
256
+ /**
257
+ * Helper to prompt for a number
258
+ */
259
+ function promptNumber(message, min, max) {
260
+ return new Promise(resolve => {
261
+ let rl = readline.createInterface({
262
+ input: process.stdin,
263
+ output: process.stdout
264
+ });
265
+ let ask = () => {
266
+ rl.question(`${message} (${min}-${max}): `, answer => {
267
+ let num = parseInt(answer, 10);
268
+ if (isNaN(num) || num < min || num > max) {
269
+ console.log(`Please enter a number between ${min} and ${max}`);
270
+ ask();
271
+ } else {
272
+ rl.close();
273
+ resolve(num);
274
+ }
275
+ });
276
+ };
277
+ ask();
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Project remove command - remove project configuration for current directory
283
+ * @param {Object} _options - Command options (unused)
284
+ * @param {Object} globalOptions - Global CLI options
285
+ */
286
+ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
287
+ let ui = new ConsoleUI({
288
+ json: globalOptions.json,
289
+ verbose: globalOptions.verbose,
290
+ color: !globalOptions.noColor
291
+ });
292
+ try {
293
+ let currentDir = resolve(process.cwd());
294
+ let mapping = await getProjectMapping(currentDir);
295
+ if (!mapping) {
296
+ ui.info('No project configured for this directory');
297
+ ui.cleanup();
298
+ return;
299
+ }
300
+
301
+ // Confirm removal
302
+ console.log(''); // Empty line for spacing
303
+ ui.info('Current project configuration:');
304
+ console.log(` Project: ${mapping.projectName} (${mapping.projectSlug})`);
305
+ console.log(` Organization: ${mapping.organizationSlug}`);
306
+ console.log(` Directory: ${currentDir}`);
307
+ console.log(''); // Empty line for spacing
308
+
309
+ let confirmed = await promptConfirm('Remove this project configuration?');
310
+ if (!confirmed) {
311
+ ui.info('Cancelled');
312
+ ui.cleanup();
313
+ return;
314
+ }
315
+ await deleteProjectMapping(currentDir);
316
+ ui.success('Project configuration removed');
317
+ console.log(''); // Empty line for spacing
318
+ ui.info('Run "vizzly project:select" to configure a different project');
319
+ ui.cleanup();
320
+ } catch (error) {
321
+ ui.error('Failed to remove project configuration', error, 0);
322
+ if (globalOptions.verbose && error.stack) {
323
+ console.error(''); // Empty line for spacing
324
+ console.error(error.stack);
325
+ }
326
+ process.exit(1);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Helper to prompt for confirmation
332
+ */
333
+ function promptConfirm(message) {
334
+ return new Promise(resolve => {
335
+ let rl = readline.createInterface({
336
+ input: process.stdin,
337
+ output: process.stdout
338
+ });
339
+ rl.question(`${message} (y/n): `, answer => {
340
+ rl.close();
341
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
342
+ });
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Validate project command options
348
+ */
349
+ export function validateProjectOptions() {
350
+ return [];
351
+ }
@@ -57,8 +57,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
57
57
  ...globalOptions,
58
58
  ...options
59
59
  };
60
+
61
+ // Debug: Check options before loadConfig
62
+ if (process.env.DEBUG_CONFIG) {
63
+ console.log('[RUN] allOptions.token:', allOptions.token ? allOptions.token.substring(0, 8) + '***' : 'NONE');
64
+ }
60
65
  const config = await loadConfig(globalOptions.config, allOptions);
61
66
 
67
+ // Debug: Check config immediately after loadConfig
68
+ if (process.env.DEBUG_CONFIG) {
69
+ console.log('[RUN] Config after loadConfig:', {
70
+ hasApiKey: !!config.apiKey,
71
+ apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE'
72
+ });
73
+ }
74
+ if (globalOptions.verbose) {
75
+ ui.info('Token check:', {
76
+ hasApiKey: !!config.apiKey,
77
+ apiKeyType: typeof config.apiKey,
78
+ apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? config.apiKey.substring(0, 10) + '...' : 'none',
79
+ projectSlug: config.projectSlug || 'none',
80
+ organizationSlug: config.organizationSlug || 'none'
81
+ });
82
+ }
83
+
62
84
  // Validate API token (unless --allow-no-token is set)
63
85
  if (!config.apiKey && !config.allowNoToken) {
64
86
  ui.error('API token required. Use --token, set VIZZLY_TOKEN environment variable, or use --allow-no-token to run without uploading');
@@ -92,6 +114,14 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
92
114
  verbose: globalOptions.verbose,
93
115
  uploadAll: options.uploadAll || false
94
116
  };
117
+
118
+ // Debug: Check config before creating container
119
+ if (process.env.DEBUG_CONFIG) {
120
+ console.log('[RUN] Config before container:', {
121
+ hasApiKey: !!configWithVerbose.apiKey,
122
+ apiKeyPrefix: configWithVerbose.apiKey ? configWithVerbose.apiKey.substring(0, 8) + '***' : 'NONE'
123
+ });
124
+ }
95
125
  const command = 'run';
96
126
  const container = await createServiceContainer(configWithVerbose, command);
97
127
  testRunner = await container.get('testRunner'); // Assign to outer scope variable
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Whoami command implementation
3
+ * Shows current user and authentication status
4
+ */
5
+
6
+ import { ConsoleUI } from '../utils/console-ui.js';
7
+ import { AuthService } from '../services/auth-service.js';
8
+ import { getApiUrl } from '../utils/environment-config.js';
9
+ import { getAuthTokens } from '../utils/global-config.js';
10
+
11
+ /**
12
+ * Whoami command implementation
13
+ * @param {Object} options - Command options
14
+ * @param {Object} globalOptions - Global CLI options
15
+ */
16
+ export async function whoamiCommand(options = {}, globalOptions = {}) {
17
+ // Create UI handler
18
+ let ui = new ConsoleUI({
19
+ json: globalOptions.json,
20
+ verbose: globalOptions.verbose,
21
+ color: !globalOptions.noColor
22
+ });
23
+ try {
24
+ // Check if user is logged in
25
+ let auth = await getAuthTokens();
26
+ if (!auth || !auth.accessToken) {
27
+ if (globalOptions.json) {
28
+ ui.data({
29
+ authenticated: false
30
+ });
31
+ } else {
32
+ ui.info('You are not logged in');
33
+ console.log(''); // Empty line for spacing
34
+ ui.info('Run "vizzly login" to authenticate');
35
+ }
36
+ ui.cleanup();
37
+ return;
38
+ }
39
+
40
+ // Get current user info
41
+ ui.startSpinner('Fetching user information...');
42
+ let authService = new AuthService({
43
+ baseUrl: options.apiUrl || getApiUrl()
44
+ });
45
+ let response = await authService.whoami();
46
+ ui.stopSpinner();
47
+
48
+ // Output in JSON mode
49
+ if (globalOptions.json) {
50
+ ui.data({
51
+ authenticated: true,
52
+ user: response.user,
53
+ organizations: response.organizations || [],
54
+ tokenExpiresAt: auth.expiresAt
55
+ });
56
+ ui.cleanup();
57
+ return;
58
+ }
59
+
60
+ // Human-readable output
61
+ ui.success('Authenticated');
62
+ console.log(''); // Empty line for spacing
63
+
64
+ // Show user info
65
+ if (response.user) {
66
+ ui.info(`User: ${response.user.name || response.user.username}`);
67
+ ui.info(`Email: ${response.user.email}`);
68
+ if (response.user.username) {
69
+ ui.info(`Username: ${response.user.username}`);
70
+ }
71
+ if (globalOptions.verbose && response.user.id) {
72
+ ui.info(`User ID: ${response.user.id}`);
73
+ }
74
+ }
75
+
76
+ // Show organizations
77
+ if (response.organizations && response.organizations.length > 0) {
78
+ console.log(''); // Empty line for spacing
79
+ ui.info('Organizations:');
80
+ for (let org of response.organizations) {
81
+ let orgInfo = ` - ${org.name}`;
82
+ if (org.slug) {
83
+ orgInfo += ` (@${org.slug})`;
84
+ }
85
+ if (org.role) {
86
+ orgInfo += ` [${org.role}]`;
87
+ }
88
+ console.log(orgInfo);
89
+ if (globalOptions.verbose && org.id) {
90
+ console.log(` ID: ${org.id}`);
91
+ }
92
+ }
93
+ }
94
+
95
+ // Show token expiry info
96
+ if (auth.expiresAt) {
97
+ console.log(''); // Empty line for spacing
98
+ let expiresAt = new Date(auth.expiresAt);
99
+ let now = new Date();
100
+ let msUntilExpiry = expiresAt.getTime() - now.getTime();
101
+ let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
102
+ let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
103
+ let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
104
+ if (msUntilExpiry <= 0) {
105
+ ui.warning('Token has expired');
106
+ console.log(''); // Empty line for spacing
107
+ ui.info('Run "vizzly login" to refresh your authentication');
108
+ } else if (daysUntilExpiry > 0) {
109
+ ui.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
110
+ } else if (hoursUntilExpiry > 0) {
111
+ ui.info(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleString()})`);
112
+ } else if (minutesUntilExpiry > 0) {
113
+ ui.info(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
114
+ } else {
115
+ ui.warning('Token expires in less than a minute');
116
+ console.log(''); // Empty line for spacing
117
+ ui.info('Run "vizzly login" to refresh your authentication');
118
+ }
119
+ if (globalOptions.verbose) {
120
+ ui.info(`Token expires at: ${expiresAt.toISOString()}`);
121
+ }
122
+ }
123
+ ui.cleanup();
124
+ } catch (error) {
125
+ ui.stopSpinner();
126
+
127
+ // Handle authentication errors with helpful messages
128
+ if (error.name === 'AuthError') {
129
+ if (globalOptions.json) {
130
+ ui.data({
131
+ authenticated: false,
132
+ error: error.message
133
+ });
134
+ } else {
135
+ ui.error('Authentication token is invalid or expired', error, 0);
136
+ console.log(''); // Empty line for spacing
137
+ ui.info('Run "vizzly login" to authenticate again');
138
+ }
139
+ ui.cleanup();
140
+ process.exit(1);
141
+ } else {
142
+ ui.error('Failed to get user information', error, 0);
143
+ if (globalOptions.verbose && error.stack) {
144
+ console.error(''); // Empty line for spacing
145
+ console.error(error.stack);
146
+ }
147
+ process.exit(1);
148
+ }
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Validate whoami options
154
+ * @param {Object} options - Command options
155
+ */
156
+ export function validateWhoamiOptions() {
157
+ let errors = [];
158
+
159
+ // No specific validation needed for whoami command
160
+
161
+ return errors;
162
+ }
@@ -135,7 +135,9 @@ async function loadPlugin(pluginPath) {
135
135
  const pluginSchema = z.object({
136
136
  name: z.string().min(1, 'Plugin name is required'),
137
137
  version: z.string().optional(),
138
- register: z.function(z.tuple([z.any(), z.any()]), z.void()),
138
+ register: z.custom(val => typeof val === 'function', {
139
+ message: 'register must be a function'
140
+ }),
139
141
  configSchema: z.record(z.any()).optional()
140
142
  });
141
143
 
@@ -162,7 +164,7 @@ function validatePluginStructure(plugin) {
162
164
  }
163
165
  } catch (error) {
164
166
  if (error instanceof z.ZodError) {
165
- let messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
167
+ let messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
166
168
  throw new Error(`Invalid plugin structure: ${messages.join(', ')}`);
167
169
  }
168
170
  throw error;
package/dist/sdk/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { EventEmitter } from 'events';
14
+ import { resolveImageBuffer } from '../utils/file-helpers.js';
14
15
  import { createUploader } from '../services/uploader.js';
15
16
  import { createTDDService } from '../services/tdd-service.js';
16
17
  import { ScreenshotServer } from '../services/screenshot-server.js';
@@ -226,21 +227,27 @@ export class VizzlySDK extends EventEmitter {
226
227
  /**
227
228
  * Capture a screenshot
228
229
  * @param {string} name - Screenshot name
229
- * @param {Buffer} imageBuffer - Image data
230
+ * @param {Buffer|string} imageBuffer - Image data as a Buffer, or a file path to an image
230
231
  * @param {import('../types').ScreenshotOptions} [options] - Options
231
232
  * @returns {Promise<void>}
233
+ * @throws {VizzlyError} When server is not running
234
+ * @throws {VizzlyError} When file path is provided but file doesn't exist
235
+ * @throws {VizzlyError} When file cannot be read due to permissions or I/O errors
232
236
  */
233
237
  async screenshot(name, imageBuffer, options = {}) {
234
238
  if (!this.server || !this.server.isRunning()) {
235
239
  throw new VizzlyError('Server not running. Call start() first.', 'SERVER_NOT_RUNNING');
236
240
  }
237
241
 
242
+ // Resolve Buffer or file path using shared utility
243
+ const buffer = resolveImageBuffer(imageBuffer, 'screenshot');
244
+
238
245
  // Generate or use provided build ID
239
246
  const buildId = options.buildId || this.currentBuildId || 'default';
240
247
  this.currentBuildId = buildId;
241
248
 
242
249
  // Convert Buffer to base64 for JSON transport
243
- const imageBase64 = imageBuffer.toString('base64');
250
+ const imageBase64 = buffer.toString('base64');
244
251
  const screenshotData = {
245
252
  buildId,
246
253
  name,
@@ -331,8 +338,10 @@ export class VizzlySDK extends EventEmitter {
331
338
  /**
332
339
  * Run local comparison in TDD mode
333
340
  * @param {string} name - Screenshot name
334
- * @param {Buffer} imageBuffer - Current image
341
+ * @param {Buffer|string} imageBuffer - Current image as a Buffer, or a file path to an image
335
342
  * @returns {Promise<import('../types').ComparisonResult>} Comparison result
343
+ * @throws {VizzlyError} When file path is provided but file doesn't exist
344
+ * @throws {VizzlyError} When file cannot be read due to permissions or I/O errors
336
345
  */
337
346
  async compare(name, imageBuffer) {
338
347
  if (!this.services?.tddService) {
@@ -341,8 +350,11 @@ export class VizzlySDK extends EventEmitter {
341
350
  logger: this.logger
342
351
  });
343
352
  }
353
+
354
+ // Resolve Buffer or file path using shared utility
355
+ const buffer = resolveImageBuffer(imageBuffer, 'compare');
344
356
  try {
345
- const result = await this.services.tddService.compareScreenshot(name, imageBuffer);
357
+ const result = await this.services.tddService.compareScreenshot(name, buffer);
346
358
  this.emit('comparison:completed', result);
347
359
  return result;
348
360
  } catch (error) {