@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.
Files changed (48) 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/services/tdd-service.js +2 -1
  26. package/dist/types/client/index.d.ts +9 -3
  27. package/dist/types/commands/login.d.ts +11 -0
  28. package/dist/types/commands/logout.d.ts +11 -0
  29. package/dist/types/commands/project.d.ts +28 -0
  30. package/dist/types/commands/whoami.d.ts +11 -0
  31. package/dist/types/sdk/index.d.ts +9 -4
  32. package/dist/types/services/api-service.d.ts +2 -1
  33. package/dist/types/services/auth-service.d.ts +59 -0
  34. package/dist/types/utils/browser.d.ts +6 -0
  35. package/dist/types/utils/config-loader.d.ts +1 -1
  36. package/dist/types/utils/config-schema.d.ts +8 -174
  37. package/dist/types/utils/file-helpers.d.ts +18 -0
  38. package/dist/types/utils/global-config.d.ts +84 -0
  39. package/dist/utils/browser.js +44 -0
  40. package/dist/utils/config-loader.js +69 -3
  41. package/dist/utils/file-helpers.js +64 -0
  42. package/dist/utils/global-config.js +259 -0
  43. package/docs/api-reference.md +177 -6
  44. package/docs/authentication.md +334 -0
  45. package/docs/getting-started.md +21 -2
  46. package/docs/plugins.md +27 -0
  47. package/docs/test-integration.md +60 -10
  48. package/package.json +5 -3
package/README.md CHANGED
@@ -36,16 +36,44 @@ npm install -g @vizzly-testing/cli
36
36
  vizzly init
37
37
  ```
38
38
 
39
- ### Set up your API token
39
+ ### Authentication
40
40
 
41
- For local development, create a `.env` file in your project root and add your token:
41
+ Vizzly supports two authentication methods:
42
42
 
43
+ **Option 1: User Authentication (Recommended for local development)**
44
+ ```bash
45
+ # Authenticate with your Vizzly account
46
+ vizzly login
47
+
48
+ # Optional: Configure project-specific token
49
+ vizzly project:select
50
+
51
+ # Run tests
52
+ vizzly run "npm test"
43
53
  ```
44
- VIZZLY_TOKEN=your-api-token
54
+
55
+ **Option 2: API Token (Recommended for CI/CD)**
56
+ ```bash
57
+ # Set via environment variable
58
+ export VIZZLY_TOKEN=your-project-token
59
+
60
+ # Run tests
61
+ vizzly run "npm test"
62
+ ```
63
+
64
+ For local development with `.env` files:
65
+ ```
66
+ VIZZLY_TOKEN=your-project-token
45
67
  ```
46
68
 
47
69
  Then add `.env` to your `.gitignore` file. For CI/CD, use your provider's secret management system.
48
70
 
71
+ **Token Priority:**
72
+ 1. CLI flag (`--token`)
73
+ 2. Environment variable (`VIZZLY_TOKEN`)
74
+ 3. Project mapping (configured via `vizzly project:select`)
75
+ 4. User access token (from `vizzly login`)
76
+
49
77
  ### Upload existing screenshots
50
78
 
51
79
  ```bash
@@ -67,14 +95,19 @@ vizzly tdd run "npm test"
67
95
  ```javascript
68
96
  import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
69
97
 
70
- // Your test framework takes the screenshot
98
+ // Option 1: Using a Buffer
71
99
  const screenshot = await page.screenshot();
72
-
73
- // Send to Vizzly for review
74
100
  await vizzlyScreenshot('homepage', screenshot, {
75
101
  browser: 'chrome',
76
102
  viewport: '1920x1080'
77
103
  });
104
+
105
+ // Option 2: Using a file path
106
+ await page.screenshot({ path: './screenshots/homepage.png' });
107
+ await vizzlyScreenshot('homepage', './screenshots/homepage.png', {
108
+ browser: 'chrome',
109
+ viewport: '1920x1080'
110
+ });
78
111
  ```
79
112
 
80
113
  > **Multi-Language Support**: Currently available as a JavaScript/Node.js SDK with Python, Ruby, and
@@ -83,6 +116,84 @@ await vizzlyScreenshot('homepage', screenshot, {
83
116
 
84
117
  ## Commands
85
118
 
119
+ ### Authentication Commands
120
+
121
+ ```bash
122
+ vizzly login # Authenticate with your Vizzly account
123
+ vizzly logout # Clear stored authentication tokens
124
+ vizzly whoami # Show current user and authentication status
125
+ vizzly project:select # Configure project-specific token
126
+ vizzly project:list # Show all configured projects
127
+ vizzly project:token # Display project token for current directory
128
+ vizzly project:remove # Remove project configuration
129
+ ```
130
+
131
+ #### Login Command
132
+ Authenticate using OAuth 2.0 device flow. Opens your browser to authorize the CLI with your Vizzly account.
133
+
134
+ ```bash
135
+ # Interactive browser-based login
136
+ vizzly login
137
+
138
+ # JSON output for scripting
139
+ vizzly login --json
140
+ ```
141
+
142
+ **Features:**
143
+ - Browser auto-opens with pre-filled device code
144
+ - Secure OAuth 2.0 device authorization flow
145
+ - 30-day token expiry with automatic refresh
146
+ - Tokens stored securely in `~/.vizzly/config.json` with 0600 permissions
147
+
148
+ #### Logout Command
149
+ Clear all stored authentication tokens from your machine.
150
+
151
+ ```bash
152
+ # Clear all tokens
153
+ vizzly logout
154
+
155
+ # JSON output
156
+ vizzly logout --json
157
+ ```
158
+
159
+ Revokes tokens on the server and removes them from local storage.
160
+
161
+ #### Whoami Command
162
+ Display current authentication status, user information, and organizations.
163
+
164
+ ```bash
165
+ # Show user and authentication info
166
+ vizzly whoami
167
+
168
+ # JSON output for scripting
169
+ vizzly whoami --json
170
+ ```
171
+
172
+ Shows:
173
+ - Current user email and name
174
+ - Organizations you belong to
175
+ - Token status and expiry
176
+ - Project mappings (if any)
177
+
178
+ #### Project Commands
179
+ Configure directory-specific project tokens for multi-project workflows.
180
+
181
+ ```bash
182
+ # Select a project for current directory
183
+ vizzly project:select
184
+
185
+ # List all configured projects
186
+ vizzly project:list
187
+
188
+ # Show token for current directory
189
+ vizzly project:token
190
+
191
+ # Remove project configuration
192
+ vizzly project:remove
193
+ ```
194
+
195
+ **Use case:** Working on multiple Vizzly projects? Configure each project directory with its specific token. The CLI automatically uses the right token based on your current directory.
196
+
86
197
  ### Upload Screenshots
87
198
  ```bash
88
199
  vizzly upload <directory> # Upload screenshots from directory
@@ -360,12 +471,52 @@ The `--wait` flag ensures the process:
360
471
  ### `vizzlyScreenshot(name, imageBuffer, properties)`
361
472
  Send a screenshot to Vizzly.
362
473
  - `name` (string): Screenshot identifier
363
- - `imageBuffer` (Buffer): Image data
474
+ - `imageBuffer` (Buffer | string): Image data as Buffer, or file path to an image
364
475
  - `properties` (object): Metadata for organization
365
476
 
477
+ **File Path Support:**
478
+ - Accepts both absolute and relative paths
479
+ - Automatically reads the file and converts to Buffer internally
480
+ - Works with any PNG image file
481
+
366
482
  ### `isVizzlyEnabled()`
367
483
  Check if Vizzly is enabled in the current environment.
368
484
 
485
+ ## AI & Editor Integrations
486
+
487
+ ### Claude Code Plugin
488
+
489
+ Vizzly includes built-in support for [Claude Code](https://claude.com/code), Anthropic's official CLI tool. The integration brings AI-powered visual testing workflows directly into your development environment.
490
+
491
+ **Features:**
492
+ - 🤖 **AI-assisted debugging** - Get intelligent analysis of visual regressions
493
+ - 📊 **TDD status insights** - Check dashboard status with contextual suggestions
494
+ - 🔍 **Smart diff analysis** - AI helps determine if changes should be accepted or fixed
495
+ - ✨ **Test coverage suggestions** - Get framework-specific screenshot recommendations
496
+ - 🛠️ **Interactive setup** - Guided configuration and CI/CD integration help
497
+
498
+ **Getting Started with Claude Code:**
499
+
500
+ 1. **Install Claude Code** (if you haven't already):
501
+ ```bash
502
+ npm install -g @anthropic-ai/claude-code
503
+ ```
504
+
505
+ 2. **Install the Vizzly plugin** via Claude Code marketplace:
506
+ ```
507
+ /plugin marketplace add vizzly-testing/cli
508
+ ```
509
+
510
+ 3. **Use AI-powered workflows** with slash commands:
511
+ ```
512
+ /vizzly:tdd-status # Check TDD dashboard with AI insights
513
+ /vizzly:debug-diff homepage # Analyze visual failures with AI
514
+ /vizzly:suggest-screenshots # Find test coverage gaps
515
+ /vizzly:setup # Interactive setup wizard
516
+ ```
517
+
518
+ The plugin works seamlessly with both local TDD mode and cloud builds, providing contextual help based on your current workflow.
519
+
369
520
  ## Plugin Ecosystem
370
521
 
371
522
  Vizzly supports a powerful plugin system that allows you to extend the CLI with custom
@@ -374,6 +525,7 @@ explicitly configured.
374
525
 
375
526
  ### Official Plugins
376
527
 
528
+ - **Claude Code Integration** *(built-in)* - AI-powered visual testing workflows for Claude Code
377
529
  - **[@vizzly-testing/storybook](https://npmjs.com/package/@vizzly-testing/storybook)** *(coming
378
530
  soon)* - Capture screenshots from Storybook builds
379
531
 
@@ -425,6 +577,7 @@ See the [Plugin Development Guide](./docs/plugins.md) for complete documentation
425
577
  ## Documentation
426
578
 
427
579
  - [Getting Started](./docs/getting-started.md)
580
+ - [Authentication Guide](./docs/authentication.md)
428
581
  - [Upload Command Guide](./docs/upload-command.md)
429
582
  - [Test Integration Guide](./docs/test-integration.md)
430
583
  - [TDD Mode Guide](./docs/tdd-mode.md)
@@ -432,10 +585,17 @@ See the [Plugin Development Guide](./docs/plugins.md) for complete documentation
432
585
  - [API Reference](./docs/api-reference.md)
433
586
  - [Doctor Command](./docs/doctor-command.md)
434
587
 
588
+ **AI & Editor Integrations:**
589
+ - Claude Code Plugin - Built-in support (see [AI & Editor Integrations](#ai--editor-integrations) above)
590
+
435
591
  ## Environment Variables
436
592
 
593
+ ### Authentication
594
+ - `VIZZLY_TOKEN`: API authentication token (project token or access token). Example: `export VIZZLY_TOKEN=your-token`.
595
+ - For local development: Use `vizzly login` instead of manually managing tokens
596
+ - For CI/CD: Use project tokens from environment variables
597
+
437
598
  ### Core Configuration
438
- - `VIZZLY_TOKEN`: API authentication token. Example: `export VIZZLY_TOKEN=your-token`.
439
599
  - `VIZZLY_API_URL`: Override API base URL. Default: `https://app.vizzly.dev`.
440
600
  - `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
441
601
 
package/dist/cli.js CHANGED
@@ -9,6 +9,10 @@ import { tddStartCommand, tddStopCommand, tddStatusCommand, runDaemonChild } fro
9
9
  import { statusCommand, validateStatusOptions } from './commands/status.js';
10
10
  import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
11
11
  import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
12
+ import { loginCommand, validateLoginOptions } from './commands/login.js';
13
+ import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
14
+ import { whoamiCommand, validateWhoamiOptions } from './commands/whoami.js';
15
+ import { projectSelectCommand, projectListCommand, projectTokenCommand, projectRemoveCommand, validateProjectOptions } from './commands/project.js';
12
16
  import { getPackageVersion } from './utils/package-info.js';
13
17
  import { loadPlugins } from './plugin-loader.js';
14
18
  import { loadConfig } from './utils/config-loader.js';
@@ -194,4 +198,64 @@ program.command('doctor').description('Run diagnostics to check your environment
194
198
  }
195
199
  await doctorCommand(options, globalOptions);
196
200
  });
201
+ program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
202
+ const globalOptions = program.opts();
203
+
204
+ // Validate options
205
+ const validationErrors = validateLoginOptions(options);
206
+ if (validationErrors.length > 0) {
207
+ console.error('Validation errors:');
208
+ validationErrors.forEach(error => console.error(` - ${error}`));
209
+ process.exit(1);
210
+ }
211
+ await loginCommand(options, globalOptions);
212
+ });
213
+ program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
214
+ const globalOptions = program.opts();
215
+
216
+ // Validate options
217
+ const validationErrors = validateLogoutOptions(options);
218
+ if (validationErrors.length > 0) {
219
+ console.error('Validation errors:');
220
+ validationErrors.forEach(error => console.error(` - ${error}`));
221
+ process.exit(1);
222
+ }
223
+ await logoutCommand(options, globalOptions);
224
+ });
225
+ program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
226
+ const globalOptions = program.opts();
227
+
228
+ // Validate options
229
+ const validationErrors = validateWhoamiOptions(options);
230
+ if (validationErrors.length > 0) {
231
+ console.error('Validation errors:');
232
+ validationErrors.forEach(error => console.error(` - ${error}`));
233
+ process.exit(1);
234
+ }
235
+ await whoamiCommand(options, globalOptions);
236
+ });
237
+ program.command('project:select').description('Configure project for current directory').option('--api-url <url>', 'API URL override').action(async options => {
238
+ const globalOptions = program.opts();
239
+
240
+ // Validate options
241
+ const validationErrors = validateProjectOptions(options);
242
+ if (validationErrors.length > 0) {
243
+ console.error('Validation errors:');
244
+ validationErrors.forEach(error => console.error(` - ${error}`));
245
+ process.exit(1);
246
+ }
247
+ await projectSelectCommand(options, globalOptions);
248
+ });
249
+ program.command('project:list').description('Show all configured projects').action(async options => {
250
+ const globalOptions = program.opts();
251
+ await projectListCommand(options, globalOptions);
252
+ });
253
+ program.command('project:token').description('Show project token for current directory').action(async options => {
254
+ const globalOptions = program.opts();
255
+ await projectTokenCommand(options, globalOptions);
256
+ });
257
+ program.command('project:remove').description('Remove project configuration for current directory').action(async options => {
258
+ const globalOptions = program.opts();
259
+ await projectRemoveCommand(options, globalOptions);
260
+ });
197
261
  program.parse();
@@ -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
+ }