@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.
- package/.claude-plugin/.mcp.json +8 -0
- package/.claude-plugin/README.md +114 -0
- package/.claude-plugin/commands/debug-diff.md +153 -0
- package/.claude-plugin/commands/setup.md +137 -0
- package/.claude-plugin/commands/suggest-screenshots.md +111 -0
- package/.claude-plugin/commands/tdd-status.md +43 -0
- package/.claude-plugin/marketplace.json +28 -0
- package/.claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
- package/.claude-plugin/mcp/vizzly-server/index.js +861 -0
- package/.claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
- package/.claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
- package/.claude-plugin/plugin.json +14 -0
- package/README.md +168 -8
- package/dist/cli.js +64 -0
- package/dist/client/index.js +13 -3
- package/dist/commands/login.js +195 -0
- package/dist/commands/logout.js +71 -0
- package/dist/commands/project.js +351 -0
- package/dist/commands/run.js +30 -0
- package/dist/commands/whoami.js +162 -0
- package/dist/plugin-loader.js +4 -2
- package/dist/sdk/index.js +16 -4
- package/dist/services/api-service.js +50 -7
- package/dist/services/auth-service.js +226 -0
- package/dist/services/tdd-service.js +2 -1
- package/dist/types/client/index.d.ts +9 -3
- package/dist/types/commands/login.d.ts +11 -0
- package/dist/types/commands/logout.d.ts +11 -0
- package/dist/types/commands/project.d.ts +28 -0
- package/dist/types/commands/whoami.d.ts +11 -0
- package/dist/types/sdk/index.d.ts +9 -4
- package/dist/types/services/api-service.d.ts +2 -1
- package/dist/types/services/auth-service.d.ts +59 -0
- package/dist/types/utils/browser.d.ts +6 -0
- package/dist/types/utils/config-loader.d.ts +1 -1
- package/dist/types/utils/config-schema.d.ts +8 -174
- package/dist/types/utils/file-helpers.d.ts +18 -0
- package/dist/types/utils/global-config.d.ts +84 -0
- package/dist/utils/browser.js +44 -0
- package/dist/utils/config-loader.js +69 -3
- package/dist/utils/file-helpers.js +64 -0
- package/dist/utils/global-config.js +259 -0
- package/docs/api-reference.md +177 -6
- package/docs/authentication.md +334 -0
- package/docs/getting-started.md +21 -2
- package/docs/plugins.md +27 -0
- package/docs/test-integration.md +60 -10
- 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
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/plugin-loader.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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 =
|
|
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,
|
|
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) {
|