@vizzly-testing/cli 0.27.1 → 0.28.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/dist/api/endpoints.js +2 -0
- package/dist/cli.js +3 -35
- package/dist/commands/builds.js +1 -0
- package/dist/commands/comparisons.js +1 -0
- package/dist/commands/config-cmd.js +1 -20
- package/dist/project/core.js +0 -84
- package/dist/project/operations.js +1 -77
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +55 -55
- package/dist/server/routers/projects.js +1 -101
- package/dist/services/project-service.js +1 -49
- package/dist/utils/config-loader.js +4 -27
- package/dist/utils/context.js +4 -38
- package/dist/utils/global-config.js +1 -76
- package/package.json +1 -1
- package/dist/commands/project.js +0 -414
package/dist/commands/project.js
DELETED
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project management commands
|
|
3
|
-
* Select, list, and manage project tokens
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { resolve } from 'node:path';
|
|
7
|
-
import readline from 'node:readline';
|
|
8
|
-
import { createAuthClient, createTokenStore, getAuthTokens, whoami } from '../auth/index.js';
|
|
9
|
-
import { getApiUrl } from '../utils/environment-config.js';
|
|
10
|
-
import { deleteProjectMapping, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
|
|
11
|
-
import * as output from '../utils/output.js';
|
|
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
|
-
output.configure({
|
|
20
|
-
json: globalOptions.json,
|
|
21
|
-
verbose: globalOptions.verbose,
|
|
22
|
-
color: !globalOptions.noColor
|
|
23
|
-
});
|
|
24
|
-
try {
|
|
25
|
-
output.header('project:select');
|
|
26
|
-
|
|
27
|
-
// Check authentication
|
|
28
|
-
let auth = await getAuthTokens();
|
|
29
|
-
if (!auth || !auth.accessToken) {
|
|
30
|
-
output.error('Not authenticated');
|
|
31
|
-
output.hint('Run "vizzly login" to authenticate first');
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
let client = createAuthClient({
|
|
35
|
-
baseUrl: options.apiUrl || getApiUrl()
|
|
36
|
-
});
|
|
37
|
-
let tokenStore = createTokenStore();
|
|
38
|
-
|
|
39
|
-
// Get user info to show organizations
|
|
40
|
-
output.startSpinner('Fetching organizations...');
|
|
41
|
-
let userInfo = await whoami(client, tokenStore);
|
|
42
|
-
output.stopSpinner();
|
|
43
|
-
if (!userInfo.organizations || userInfo.organizations.length === 0) {
|
|
44
|
-
output.error('No organizations found');
|
|
45
|
-
output.hint('Create an organization at https://vizzly.dev');
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Select organization
|
|
50
|
-
output.labelValue('Organizations', '');
|
|
51
|
-
userInfo.organizations.forEach((org, index) => {
|
|
52
|
-
output.print(` ${index + 1}. ${org.name} (@${org.slug})`);
|
|
53
|
-
});
|
|
54
|
-
output.blank();
|
|
55
|
-
let orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
|
|
56
|
-
let selectedOrg = userInfo.organizations[orgChoice - 1];
|
|
57
|
-
|
|
58
|
-
// List projects for organization
|
|
59
|
-
output.startSpinner(`Fetching projects for ${selectedOrg.name}...`);
|
|
60
|
-
let response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
|
|
61
|
-
headers: {
|
|
62
|
-
Authorization: `Bearer ${auth.accessToken}`,
|
|
63
|
-
'X-Organization': selectedOrg.slug
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
output.stopSpinner();
|
|
67
|
-
|
|
68
|
-
// Handle both array response and object with projects property
|
|
69
|
-
let projects = Array.isArray(response) ? response : response.projects || [];
|
|
70
|
-
if (projects.length === 0) {
|
|
71
|
-
output.error('No projects found');
|
|
72
|
-
output.hint(`Create a project in ${selectedOrg.name} at https://vizzly.dev`);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Select project
|
|
77
|
-
output.blank();
|
|
78
|
-
output.labelValue('Projects', '');
|
|
79
|
-
projects.forEach((project, index) => {
|
|
80
|
-
output.print(` ${index + 1}. ${project.name} (${project.slug})`);
|
|
81
|
-
});
|
|
82
|
-
output.blank();
|
|
83
|
-
let projectChoice = await promptNumber('Enter number', 1, projects.length);
|
|
84
|
-
let selectedProject = projects[projectChoice - 1];
|
|
85
|
-
|
|
86
|
-
// Create API token for project
|
|
87
|
-
output.startSpinner(`Creating API token for ${selectedProject.name}...`);
|
|
88
|
-
let tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
headers: {
|
|
91
|
-
Authorization: `Bearer ${auth.accessToken}`,
|
|
92
|
-
'X-Organization': selectedOrg.slug,
|
|
93
|
-
'Content-Type': 'application/json'
|
|
94
|
-
},
|
|
95
|
-
body: JSON.stringify({
|
|
96
|
-
name: `CLI Token - ${new Date().toLocaleDateString()}`,
|
|
97
|
-
description: `Generated by vizzly CLI for ${process.cwd()}`
|
|
98
|
-
})
|
|
99
|
-
});
|
|
100
|
-
output.stopSpinner();
|
|
101
|
-
|
|
102
|
-
// Save project mapping
|
|
103
|
-
let currentDir = resolve(process.cwd());
|
|
104
|
-
await saveProjectMapping(currentDir, {
|
|
105
|
-
token: tokenResponse.token,
|
|
106
|
-
projectSlug: selectedProject.slug,
|
|
107
|
-
projectName: selectedProject.name,
|
|
108
|
-
organizationSlug: selectedOrg.slug
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// JSON output for success
|
|
112
|
-
if (globalOptions.json) {
|
|
113
|
-
output.data({
|
|
114
|
-
status: 'configured',
|
|
115
|
-
project: {
|
|
116
|
-
name: selectedProject.name,
|
|
117
|
-
slug: selectedProject.slug
|
|
118
|
-
},
|
|
119
|
-
organization: {
|
|
120
|
-
name: selectedOrg.name,
|
|
121
|
-
slug: selectedOrg.slug
|
|
122
|
-
},
|
|
123
|
-
directory: currentDir,
|
|
124
|
-
tokenCreated: true
|
|
125
|
-
});
|
|
126
|
-
output.cleanup();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
output.complete('Project configured');
|
|
130
|
-
output.blank();
|
|
131
|
-
output.keyValue({
|
|
132
|
-
Project: selectedProject.name,
|
|
133
|
-
Organization: selectedOrg.name,
|
|
134
|
-
Directory: currentDir
|
|
135
|
-
});
|
|
136
|
-
output.cleanup();
|
|
137
|
-
} catch (error) {
|
|
138
|
-
output.stopSpinner();
|
|
139
|
-
output.error('Failed to configure project', error);
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Project list command - show all configured projects
|
|
146
|
-
* @param {Object} _options - Command options (unused)
|
|
147
|
-
* @param {Object} globalOptions - Global CLI options
|
|
148
|
-
*/
|
|
149
|
-
export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
150
|
-
output.configure({
|
|
151
|
-
json: globalOptions.json,
|
|
152
|
-
verbose: globalOptions.verbose,
|
|
153
|
-
color: !globalOptions.noColor
|
|
154
|
-
});
|
|
155
|
-
try {
|
|
156
|
-
let mappings = await getProjectMappings();
|
|
157
|
-
let paths = Object.keys(mappings);
|
|
158
|
-
let currentDir = resolve(process.cwd());
|
|
159
|
-
if (paths.length === 0) {
|
|
160
|
-
if (globalOptions.json) {
|
|
161
|
-
output.data({
|
|
162
|
-
projects: [],
|
|
163
|
-
current: null
|
|
164
|
-
});
|
|
165
|
-
} else {
|
|
166
|
-
output.header('project:list');
|
|
167
|
-
output.print(' No projects configured');
|
|
168
|
-
output.blank();
|
|
169
|
-
output.hint('Run "vizzly project:select" to configure a project');
|
|
170
|
-
}
|
|
171
|
-
output.cleanup();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (globalOptions.json) {
|
|
175
|
-
let projects = paths.map(path => {
|
|
176
|
-
let mapping = mappings[path];
|
|
177
|
-
return {
|
|
178
|
-
directory: path,
|
|
179
|
-
isCurrent: path === currentDir,
|
|
180
|
-
project: {
|
|
181
|
-
name: mapping.projectName,
|
|
182
|
-
slug: mapping.projectSlug
|
|
183
|
-
},
|
|
184
|
-
organization: mapping.organizationSlug,
|
|
185
|
-
createdAt: mapping.createdAt
|
|
186
|
-
};
|
|
187
|
-
});
|
|
188
|
-
let current = projects.find(p => p.isCurrent) || null;
|
|
189
|
-
output.data({
|
|
190
|
-
projects,
|
|
191
|
-
current
|
|
192
|
-
});
|
|
193
|
-
output.cleanup();
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
output.header('project:list');
|
|
197
|
-
let colors = output.getColors();
|
|
198
|
-
for (let path of paths) {
|
|
199
|
-
let mapping = mappings[path];
|
|
200
|
-
let isCurrent = path === currentDir;
|
|
201
|
-
let marker = isCurrent ? colors.brand.amber('→') : ' ';
|
|
202
|
-
output.print(`${marker} ${isCurrent ? colors.bold(path) : path}`);
|
|
203
|
-
output.keyValue({
|
|
204
|
-
Project: `${mapping.projectName} (${mapping.projectSlug})`,
|
|
205
|
-
Org: mapping.organizationSlug
|
|
206
|
-
}, {
|
|
207
|
-
indent: 4
|
|
208
|
-
});
|
|
209
|
-
if (globalOptions.verbose) {
|
|
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
|
-
output.hint(`Token: ${tokenStr.substring(0, 20)}...`, {
|
|
213
|
-
indent: 4
|
|
214
|
-
});
|
|
215
|
-
output.hint(`Created: ${new Date(mapping.createdAt).toLocaleString()}`, {
|
|
216
|
-
indent: 4
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
output.blank();
|
|
220
|
-
}
|
|
221
|
-
output.cleanup();
|
|
222
|
-
} catch (error) {
|
|
223
|
-
output.error('Failed to list projects', error);
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Project token command - show/regenerate token for current directory
|
|
230
|
-
* @param {Object} _options - Command options (unused)
|
|
231
|
-
* @param {Object} globalOptions - Global CLI options
|
|
232
|
-
*/
|
|
233
|
-
export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
234
|
-
output.configure({
|
|
235
|
-
json: globalOptions.json,
|
|
236
|
-
verbose: globalOptions.verbose,
|
|
237
|
-
color: !globalOptions.noColor
|
|
238
|
-
});
|
|
239
|
-
try {
|
|
240
|
-
let currentDir = resolve(process.cwd());
|
|
241
|
-
let mapping = await getProjectMapping(currentDir);
|
|
242
|
-
if (!mapping) {
|
|
243
|
-
output.error('No project configured for this directory');
|
|
244
|
-
output.hint('Run "vizzly project:select" to configure a project');
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Extract token string (handle both string and object formats)
|
|
249
|
-
let tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
|
|
250
|
-
if (globalOptions.json) {
|
|
251
|
-
output.data({
|
|
252
|
-
token: tokenStr,
|
|
253
|
-
directory: currentDir,
|
|
254
|
-
project: {
|
|
255
|
-
name: mapping.projectName,
|
|
256
|
-
slug: mapping.projectSlug
|
|
257
|
-
},
|
|
258
|
-
organization: mapping.organizationSlug,
|
|
259
|
-
createdAt: mapping.createdAt
|
|
260
|
-
});
|
|
261
|
-
output.cleanup();
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
output.header('project:token');
|
|
265
|
-
output.printBox(tokenStr, {
|
|
266
|
-
title: 'Token'
|
|
267
|
-
});
|
|
268
|
-
output.blank();
|
|
269
|
-
output.keyValue({
|
|
270
|
-
Project: `${mapping.projectName} (${mapping.projectSlug})`,
|
|
271
|
-
Org: mapping.organizationSlug
|
|
272
|
-
});
|
|
273
|
-
output.cleanup();
|
|
274
|
-
} catch (error) {
|
|
275
|
-
output.error('Failed to get project token', error);
|
|
276
|
-
process.exit(1);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Helper to make authenticated API request
|
|
282
|
-
*/
|
|
283
|
-
async function makeAuthenticatedRequest(url, options = {}) {
|
|
284
|
-
const response = await fetch(url, options);
|
|
285
|
-
if (!response.ok) {
|
|
286
|
-
let errorText = '';
|
|
287
|
-
try {
|
|
288
|
-
const errorData = await response.json();
|
|
289
|
-
errorText = errorData.error || errorData.message || '';
|
|
290
|
-
} catch {
|
|
291
|
-
errorText = await response.text();
|
|
292
|
-
}
|
|
293
|
-
throw new Error(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''}`);
|
|
294
|
-
}
|
|
295
|
-
return response.json();
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Helper to prompt for a number
|
|
300
|
-
*/
|
|
301
|
-
function promptNumber(message, min, max) {
|
|
302
|
-
return new Promise(resolve => {
|
|
303
|
-
const rl = readline.createInterface({
|
|
304
|
-
input: process.stdin,
|
|
305
|
-
output: process.stdout
|
|
306
|
-
});
|
|
307
|
-
const ask = () => {
|
|
308
|
-
rl.question(`${message} (${min}-${max}): `, answer => {
|
|
309
|
-
const num = parseInt(answer, 10);
|
|
310
|
-
if (Number.isNaN(num) || num < min || num > max) {
|
|
311
|
-
output.print(`Please enter a number between ${min} and ${max}`);
|
|
312
|
-
ask();
|
|
313
|
-
} else {
|
|
314
|
-
rl.close();
|
|
315
|
-
resolve(num);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
};
|
|
319
|
-
ask();
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Project remove command - remove project configuration for current directory
|
|
325
|
-
* @param {Object} _options - Command options (unused)
|
|
326
|
-
* @param {Object} globalOptions - Global CLI options
|
|
327
|
-
*/
|
|
328
|
-
export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
329
|
-
output.configure({
|
|
330
|
-
json: globalOptions.json,
|
|
331
|
-
verbose: globalOptions.verbose,
|
|
332
|
-
color: !globalOptions.noColor
|
|
333
|
-
});
|
|
334
|
-
try {
|
|
335
|
-
let currentDir = resolve(process.cwd());
|
|
336
|
-
let mapping = await getProjectMapping(currentDir);
|
|
337
|
-
if (!mapping) {
|
|
338
|
-
if (globalOptions.json) {
|
|
339
|
-
output.data({
|
|
340
|
-
removed: false,
|
|
341
|
-
reason: 'not_configured'
|
|
342
|
-
});
|
|
343
|
-
} else {
|
|
344
|
-
output.header('project:remove');
|
|
345
|
-
output.print(' No project configured for this directory');
|
|
346
|
-
}
|
|
347
|
-
output.cleanup();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// In JSON mode, skip confirmation (for scripting)
|
|
352
|
-
if (globalOptions.json) {
|
|
353
|
-
await deleteProjectMapping(currentDir);
|
|
354
|
-
output.data({
|
|
355
|
-
removed: true,
|
|
356
|
-
directory: currentDir,
|
|
357
|
-
project: {
|
|
358
|
-
name: mapping.projectName,
|
|
359
|
-
slug: mapping.projectSlug
|
|
360
|
-
},
|
|
361
|
-
organization: mapping.organizationSlug
|
|
362
|
-
});
|
|
363
|
-
output.cleanup();
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Confirm removal (interactive mode only)
|
|
368
|
-
output.header('project:remove');
|
|
369
|
-
output.labelValue('Current configuration', '');
|
|
370
|
-
output.keyValue({
|
|
371
|
-
Project: `${mapping.projectName} (${mapping.projectSlug})`,
|
|
372
|
-
Org: mapping.organizationSlug,
|
|
373
|
-
Directory: currentDir
|
|
374
|
-
});
|
|
375
|
-
output.blank();
|
|
376
|
-
let confirmed = await promptConfirm('Remove this project configuration?');
|
|
377
|
-
if (!confirmed) {
|
|
378
|
-
output.print(' Cancelled');
|
|
379
|
-
output.cleanup();
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
await deleteProjectMapping(currentDir);
|
|
383
|
-
output.complete('Project configuration removed');
|
|
384
|
-
output.blank();
|
|
385
|
-
output.hint('Run "vizzly project:select" to configure a different project');
|
|
386
|
-
output.cleanup();
|
|
387
|
-
} catch (error) {
|
|
388
|
-
output.error('Failed to remove project configuration', error);
|
|
389
|
-
process.exit(1);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Helper to prompt for confirmation
|
|
395
|
-
*/
|
|
396
|
-
function promptConfirm(message) {
|
|
397
|
-
return new Promise(resolve => {
|
|
398
|
-
const rl = readline.createInterface({
|
|
399
|
-
input: process.stdin,
|
|
400
|
-
output: process.stdout
|
|
401
|
-
});
|
|
402
|
-
rl.question(`${message} (y/n): `, answer => {
|
|
403
|
-
rl.close();
|
|
404
|
-
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Validate project command options
|
|
411
|
-
*/
|
|
412
|
-
export function validateProjectOptions() {
|
|
413
|
-
return [];
|
|
414
|
-
}
|