@vizzly-testing/cli 0.10.3 → 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/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
package/README.md
CHANGED
|
@@ -36,16 +36,44 @@ npm install -g @vizzly-testing/cli
|
|
|
36
36
|
vizzly init
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
###
|
|
39
|
+
### Authentication
|
|
40
40
|
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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();
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|