@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.
@@ -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
- }