@vizzly-testing/cli 0.10.3 → 0.11.1

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/README.md +168 -8
  2. package/claude-plugin/.claude-plugin/.mcp.json +8 -0
  3. package/claude-plugin/.claude-plugin/README.md +114 -0
  4. package/claude-plugin/.claude-plugin/marketplace.json +28 -0
  5. package/claude-plugin/.claude-plugin/plugin.json +14 -0
  6. package/claude-plugin/commands/debug-diff.md +153 -0
  7. package/claude-plugin/commands/setup.md +137 -0
  8. package/claude-plugin/commands/suggest-screenshots.md +111 -0
  9. package/claude-plugin/commands/tdd-status.md +43 -0
  10. package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
  11. package/claude-plugin/mcp/vizzly-server/index.js +861 -0
  12. package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
  13. package/claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
  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 +9 -15
  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
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { getServerUrl, getBuildId, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
7
+ import { resolveImageBuffer } from '../utils/file-helpers.js';
7
8
  import { existsSync, readFileSync } from 'fs';
8
9
  import { join, parse, dirname } from 'path';
9
10
 
@@ -184,7 +185,7 @@ function createSimpleClient(serverUrl) {
184
185
  * Take a screenshot for visual regression testing
185
186
  *
186
187
  * @param {string} name - Unique name for the screenshot
187
- * @param {Buffer} imageBuffer - PNG image data as a Buffer
188
+ * @param {Buffer|string} imageBuffer - PNG image data as a Buffer, or a file path to an image
188
189
  * @param {Object} [options] - Optional configuration
189
190
  * @param {Record<string, any>} [options.properties] - Additional properties to attach to the screenshot
190
191
  * @param {number} [options.threshold=0] - Pixel difference threshold (0-100)
@@ -193,13 +194,17 @@ function createSimpleClient(serverUrl) {
193
194
  * @returns {Promise<void>}
194
195
  *
195
196
  * @example
196
- * // Basic usage
197
+ * // Basic usage with Buffer
197
198
  * import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
198
199
  *
199
200
  * const screenshot = await page.screenshot();
200
201
  * await vizzlyScreenshot('homepage', screenshot);
201
202
  *
202
203
  * @example
204
+ * // Basic usage with file path
205
+ * await vizzlyScreenshot('homepage', './screenshots/homepage.png');
206
+ *
207
+ * @example
203
208
  * // With properties and threshold
204
209
  * await vizzlyScreenshot('checkout-form', screenshot, {
205
210
  * properties: {
@@ -210,6 +215,8 @@ function createSimpleClient(serverUrl) {
210
215
  * });
211
216
  *
212
217
  * @throws {VizzlyError} When screenshot capture fails or client is not initialized
218
+ * @throws {VizzlyError} When file path is provided but file doesn't exist
219
+ * @throws {VizzlyError} When file cannot be read due to permissions or I/O errors
213
220
  */
214
221
  export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
215
222
  if (isVizzlyDisabled()) {
@@ -224,7 +231,10 @@ export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
224
231
  }
225
232
  return;
226
233
  }
227
- return client.screenshot(name, imageBuffer, options);
234
+
235
+ // Resolve Buffer or file path using shared utility
236
+ const buffer = resolveImageBuffer(imageBuffer, 'screenshot');
237
+ return client.screenshot(name, buffer, options);
228
238
  }
229
239
 
230
240
  /**
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Login command implementation
3
+ * Authenticates user via OAuth device flow
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 { openBrowser } from '../utils/browser.js';
10
+
11
+ /**
12
+ * Login command implementation using OAuth device flow
13
+ * @param {Object} options - Command options
14
+ * @param {Object} globalOptions - Global CLI options
15
+ */
16
+ export async function loginCommand(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
+ ui.info('Starting Vizzly authentication...');
25
+ console.log(''); // Empty line for spacing
26
+
27
+ // Create auth service
28
+ let authService = new AuthService({
29
+ baseUrl: options.apiUrl || getApiUrl()
30
+ });
31
+
32
+ // Initiate device flow
33
+ ui.startSpinner('Connecting to Vizzly...');
34
+ let deviceFlow = await authService.initiateDeviceFlow();
35
+ ui.stopSpinner();
36
+
37
+ // Handle both snake_case and camelCase field names
38
+ let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
39
+ let userCode = deviceFlow.user_code || deviceFlow.userCode;
40
+ let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
41
+ if (!verificationUri || !userCode || !deviceCode) {
42
+ throw new Error('Invalid device flow response from server');
43
+ }
44
+
45
+ // Build URL with pre-filled code
46
+ let urlWithCode = `${verificationUri}?code=${userCode}`;
47
+
48
+ // Display user code prominently
49
+ console.log(''); // Empty line for spacing
50
+ console.log('='.repeat(50));
51
+ console.log('');
52
+ console.log(' Please visit the following URL to authorize this device:');
53
+ console.log('');
54
+ console.log(` ${urlWithCode}`);
55
+ console.log('');
56
+ console.log(' Your code (pre-filled):');
57
+ console.log('');
58
+ console.log(` ${ui.colors.bold(ui.colors.cyan(userCode))}`);
59
+ console.log('');
60
+ console.log('='.repeat(50));
61
+ console.log(''); // Empty line for spacing
62
+
63
+ // Try to open browser with pre-filled code
64
+ let browserOpened = await openBrowser(urlWithCode);
65
+ if (browserOpened) {
66
+ ui.info('Opening browser...');
67
+ } else {
68
+ ui.warning('Could not open browser automatically. Please open the URL manually.');
69
+ }
70
+ console.log(''); // Empty line for spacing
71
+ ui.info('After authorizing in your browser, press Enter to continue...');
72
+
73
+ // Wait for user to press Enter
74
+ await new Promise(resolve => {
75
+ process.stdin.setRawMode(true);
76
+ process.stdin.resume();
77
+ process.stdin.once('data', () => {
78
+ process.stdin.setRawMode(false);
79
+ process.stdin.pause();
80
+ resolve();
81
+ });
82
+ });
83
+
84
+ // Check authorization status
85
+ ui.startSpinner('Checking authorization status...');
86
+ let pollResponse = await authService.pollDeviceAuthorization(deviceCode);
87
+ ui.stopSpinner();
88
+ let tokenData = null;
89
+
90
+ // Check if authorization was successful by looking for tokens
91
+ if (pollResponse.tokens && pollResponse.tokens.accessToken) {
92
+ // Success! We got tokens
93
+ tokenData = pollResponse;
94
+ } else if (pollResponse.status === 'pending') {
95
+ throw new Error('Authorization not complete yet. Please complete the authorization in your browser and try running "vizzly login" again.');
96
+ } else if (pollResponse.status === 'expired') {
97
+ throw new Error('Device code expired. Please try logging in again.');
98
+ } else if (pollResponse.status === 'denied') {
99
+ throw new Error('Authorization denied. Please try logging in again.');
100
+ } else {
101
+ throw new Error('Unexpected response from authorization server. Please try logging in again.');
102
+ }
103
+
104
+ // Complete device flow and save tokens
105
+ // Handle both snake_case and camelCase for token data, and nested tokens object
106
+ let tokensData = tokenData.tokens || tokenData;
107
+ let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
108
+ let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
109
+ let tokens = {
110
+ accessToken: tokensData.accessToken || tokensData.access_token,
111
+ refreshToken: tokensData.refreshToken || tokensData.refresh_token,
112
+ expiresAt: tokenExpiresAt,
113
+ user: tokenData.user,
114
+ organizations: tokenData.organizations
115
+ };
116
+ await authService.completeDeviceFlow(tokens);
117
+
118
+ // Display success message
119
+ ui.success('Successfully authenticated!');
120
+ console.log(''); // Empty line for spacing
121
+
122
+ // Show user info
123
+ if (tokens.user) {
124
+ ui.info(`User: ${tokens.user.name || tokens.user.username}`);
125
+ ui.info(`Email: ${tokens.user.email}`);
126
+ }
127
+
128
+ // Show organization info
129
+ if (tokens.organizations && tokens.organizations.length > 0) {
130
+ console.log(''); // Empty line for spacing
131
+ ui.info('Organizations:');
132
+ for (let org of tokens.organizations) {
133
+ console.log(` - ${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
134
+ }
135
+ }
136
+
137
+ // Show token expiry info
138
+ if (tokens.expiresAt) {
139
+ console.log(''); // Empty line for spacing
140
+ let expiresAt = new Date(tokens.expiresAt);
141
+ let msUntilExpiry = expiresAt.getTime() - Date.now();
142
+ let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
143
+ let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
144
+ let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
145
+ if (daysUntilExpiry > 0) {
146
+ ui.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
147
+ } else if (hoursUntilExpiry > 0) {
148
+ ui.info(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
149
+ } else if (minutesUntilExpiry > 0) {
150
+ ui.info(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
151
+ }
152
+ }
153
+ console.log(''); // Empty line for spacing
154
+ ui.info('You can now use Vizzly CLI commands without setting VIZZLY_TOKEN');
155
+ ui.cleanup();
156
+ } catch (error) {
157
+ ui.stopSpinner();
158
+
159
+ // Handle authentication errors with helpful messages
160
+ if (error.name === 'AuthError') {
161
+ ui.error('Authentication failed', error, 0);
162
+ console.log(''); // Empty line for spacing
163
+ console.log('Please try logging in again.');
164
+ console.log("If you don't have an account, sign up at https://vizzly.dev");
165
+ process.exit(1);
166
+ } else if (error.code === 'RATE_LIMIT_ERROR') {
167
+ ui.error('Too many login attempts', error, 0);
168
+ console.log(''); // Empty line for spacing
169
+ console.log('Please wait a few minutes before trying again.');
170
+ process.exit(1);
171
+ } else {
172
+ ui.error('Login failed', error, 0);
173
+ console.log(''); // Empty line for spacing
174
+ console.log('Error details:', error.message);
175
+ if (globalOptions.verbose && error.stack) {
176
+ console.error(''); // Empty line for spacing
177
+ console.error(error.stack);
178
+ }
179
+ process.exit(1);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Validate login options
186
+ * @param {Object} options - Command options
187
+ */
188
+ export function validateLoginOptions() {
189
+ let errors = [];
190
+
191
+ // No specific validation needed for login command
192
+ // OAuth device flow handles everything via browser
193
+
194
+ return errors;
195
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Logout command implementation
3
+ * Clears stored authentication 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 } from '../utils/global-config.js';
10
+
11
+ /**
12
+ * Logout command implementation
13
+ * @param {Object} options - Command options
14
+ * @param {Object} globalOptions - Global CLI options
15
+ */
16
+ export async function logoutCommand(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
+ ui.info('You are not logged in');
28
+ ui.cleanup();
29
+ return;
30
+ }
31
+
32
+ // Logout
33
+ ui.startSpinner('Logging out...');
34
+ let authService = new AuthService({
35
+ baseUrl: options.apiUrl || getApiUrl()
36
+ });
37
+ await authService.logout();
38
+ ui.stopSpinner();
39
+ ui.success('Successfully logged out');
40
+ if (globalOptions.json) {
41
+ ui.data({
42
+ loggedOut: true
43
+ });
44
+ } else {
45
+ console.log(''); // Empty line for spacing
46
+ ui.info('Your authentication tokens have been cleared');
47
+ ui.info('Run "vizzly login" to authenticate again');
48
+ }
49
+ ui.cleanup();
50
+ } catch (error) {
51
+ ui.stopSpinner();
52
+ ui.error('Logout failed', error, 0);
53
+ if (globalOptions.verbose && error.stack) {
54
+ console.error(''); // Empty line for spacing
55
+ console.error(error.stack);
56
+ }
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Validate logout options
63
+ * @param {Object} options - Command options
64
+ */
65
+ export function validateLogoutOptions() {
66
+ let errors = [];
67
+
68
+ // No specific validation needed for logout command
69
+
70
+ return errors;
71
+ }
@@ -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
+ }