@vizzly-testing/cli 0.38.0 → 0.39.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 (40) hide show
  1. package/README.md +222 -183
  2. package/dist/auth/client.js +4 -1
  3. package/dist/auth/core.js +64 -3
  4. package/dist/auth/operations.js +21 -11
  5. package/dist/cli.js +45 -53
  6. package/dist/commands/init.js +156 -483
  7. package/dist/commands/login.js +71 -137
  8. package/dist/commands/project.js +128 -44
  9. package/dist/commands/run.js +2 -2
  10. package/dist/commands/tdd.js +89 -38
  11. package/dist/commands/upload.js +2 -2
  12. package/dist/init/agents.js +99 -0
  13. package/dist/init/core.js +31 -0
  14. package/dist/init/prompts.js +44 -0
  15. package/dist/reporter/reporter-bundle.css +1 -1
  16. package/dist/reporter/reporter-bundle.iife.js +31 -127
  17. package/dist/server/handlers/tdd-handler.js +33 -2
  18. package/dist/server/routers/auth.js +32 -42
  19. package/dist/services/index.js +8 -2
  20. package/dist/tdd/services/comparison-service.js +4 -1
  21. package/dist/tdd/tdd-service.js +14 -2
  22. package/dist/test-runner/core.js +28 -3
  23. package/dist/test-runner/operations.js +41 -20
  24. package/dist/types/index.d.ts +8 -0
  25. package/dist/uploader/core.js +1 -0
  26. package/dist/utils/browser.js +7 -1
  27. package/dist/utils/config-discovery.js +73 -0
  28. package/dist/utils/config-loader.js +29 -8
  29. package/dist/utils/config-schema.js +5 -0
  30. package/dist/utils/file-changes.js +105 -0
  31. package/dist/utils/login-poll-wait.js +50 -0
  32. package/dist/utils/project-binding.js +96 -0
  33. package/dist/utils/project-link-store.js +13 -7
  34. package/package.json +27 -27
  35. package/skills/vizzly/SKILL.md +4 -3
  36. package/skills/vizzly/references/ci.md +33 -0
  37. package/skills/vizzly/references/cli-context.md +81 -71
  38. package/skills/vizzly/references/dynamic-content.md +16 -17
  39. package/skills/vizzly/references/sdks.md +22 -22
  40. package/skills/vizzly/references/setup-ci.md +0 -44
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vizzly CLI
2
2
 
3
- > Visual regression testing from your terminal
3
+ > Reviewed UI context for coding agents and teams
4
4
 
5
5
  [![package version](https://img.shields.io/npm/v/@vizzly-testing/cli.svg)](https://www.npmjs.com/package/@vizzly-testing/cli)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -9,259 +9,298 @@
9
9
  <img src="./docs/assets/vizzly-mascot-inspector.png" alt="Vizzly bear inspecting a screenshot" width="220" />
10
10
  </p>
11
11
 
12
- Vizzly is a visual testing and regression platform for teams that ship UI. It
13
- captures screenshots from your real tests, compares them to approved baselines,
14
- and gives you useful review data: meaningful diffs, comments, approvals, build
15
- status, and preview links.
12
+ Vizzly helps your team and coding agents see what the UI should look like,
13
+ what changed, and what was approved. It keeps screenshots, diffs, and review
14
+ decisions together so you can use that history the next time you change the UI.
16
15
 
17
- This package is the CLI and local SDK surface. Use it to run local visual TDD,
18
- upload screenshots from CI, generate static reports, and fetch machine-readable
19
- build or diff data for scripts, automation, and coding agents.
16
+ ## Get started
20
17
 
21
- ## Quick Start
22
-
23
- You need Node.js 22+.
18
+ You need Node.js 22.18+ on Node 22, or Node.js 24+. From your test project:
24
19
 
25
20
  ```bash
26
- pnpm install -g @vizzly-testing/cli
27
-
28
- vizzly init
21
+ pnpm add --save-dev @vizzly-testing/cli
22
+ pnpm exec vizzly init
29
23
  ```
30
24
 
31
- For agent-friendly repos, install the Vizzly skill and add a short project
32
- `AGENTS.md` note:
25
+ The wizard creates a config and offers agent guidance and cloud linking.
26
+ It previews file changes before applying them. If Vizzly is already configured,
27
+ init leaves it alone. Edit the config directly to change your setup.
33
28
 
34
- ```bash
35
- vizzly init --agent-guidance
36
- ```
29
+ To create a small config and edit it yourself, use `pnpm exec vizzly init --quick`.
30
+ It keeps existing config and doesn't install dependencies or run tests.
37
31
 
38
- This installs the portable Agent Skills bundle in `.agents/skills/vizzly` and
39
- adds a small managed block to `AGENTS.md`. Run the same command again after a
40
- CLI upgrade to refresh both without replacing the rest of `AGENTS.md` or an
41
- existing Vizzly config.
32
+ Choose a Vizzly client for your test setup, then add screenshot capture.
33
+ This example uses Playwright with the Node.js client:
42
34
 
43
- Use `vizzly init --agent-skill` to install or refresh only the local skill, or
44
- `vizzly init --skip-agent-skill` when you want config without the agent prompt.
35
+ ```javascript
36
+ import { test } from '@playwright/test';
37
+ import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
45
38
 
46
- ### Start Local TDD
39
+ test('homepage looks correct', async ({ page }) => {
40
+ await page.goto('/');
41
+ await vizzlyScreenshot('homepage', await page.screenshot());
42
+ });
43
+ ```
47
44
 
48
- Start the TDD server, run your tests, and open the dashboard at the URL the
49
- command prints. Add `--open` when you want Vizzly to open the dashboard for
50
- you. If the default port is busy, Vizzly picks the next available port; use the
51
- printed `--port` value with `vizzly tdd status` or `vizzly tdd stop`.
45
+ The client runs in Node.js and works with any Node-based test runner. You can
46
+ also pass a file path:
52
47
 
53
- ```bash
54
- vizzly tdd start --open
55
- pnpm test -- --watch
48
+ ```javascript
49
+ await vizzlyScreenshot('homepage', './screenshots/homepage.png');
56
50
  ```
57
51
 
58
- The dashboard shows screenshots, baselines, and diffs as they arrive. Accept or
59
- reject changes right from the UI.
52
+ ### Review locally
60
53
 
61
- For a one-off local check, wrap the test command with `tdd run`:
54
+ Start the local TDD server, then run your tests in another terminal:
62
55
 
63
56
  ```bash
64
- vizzly tdd run "pnpm test" --no-open
65
- vizzly context build current --source local --json
57
+ pnpm exec vizzly tdd start --open
58
+ pnpm test -- --watch # Replace this with your test command
66
59
  ```
67
60
 
68
- That run writes review data under `.vizzly/` and prints a context command you
69
- can use for follow-up inspection. If screenshots were captured, Vizzly also
70
- generates `.vizzly/report/index.html`; omit `--no-open` when you want that
71
- report opened automatically.
61
+ The dashboard shows screenshots and diffs as they arrive. Accept or reject
62
+ changes there. If the default port is busy, use the printed port with
63
+ `pnpm exec vizzly tdd status --port <port>` or
64
+ `pnpm exec vizzly tdd stop --port <port>`.
72
65
 
73
- ### Run With Cloud Review
74
-
75
- Use cloud builds when you want shared baselines, team review, and CI status:
66
+ For a single run:
76
67
 
77
68
  ```bash
78
- vizzly login
79
- vizzly project link your-org/your-project
80
- vizzly run "pnpm test" --wait
69
+ pnpm exec vizzly tdd run "pnpm test" --no-open
81
70
  ```
82
71
 
83
- `vizzly login` authenticates your user account. `vizzly project link` creates a
84
- project-scoped upload credential for this checkout, which `vizzly run` uses for
85
- cloud uploads.
86
-
87
- `--wait` blocks until Vizzly finishes processing the build. It exits with code
88
- `1` when visual differences need review.
72
+ Review data goes in `.vizzly/`. When the run captures screenshots, it also
73
+ creates `.vizzly/report/index.html`. Omit `--no-open` to open the report.
74
+ If the configured port is busy, `tdd run` picks an available one. An explicit
75
+ `--port` uses only that port.
89
76
 
90
- In CI, use a project token:
77
+ ### Review in the cloud
91
78
 
92
79
  ```bash
93
- export VIZZLY_TOKEN=your-project-token
94
- vizzly run "pnpm test" --wait
80
+ pnpm exec vizzly login
81
+ pnpm exec vizzly project link your-org/your-project
82
+ pnpm exec vizzly run "pnpm test"
95
83
  ```
96
84
 
97
- ### Inspect Builds And Diffs
98
-
99
- Use `vizzly context` for human summaries and local `.vizzly` evidence. For cloud
100
- reviews, agents should discover and call the public API through its schema:
85
+ `vizzly login` signs you in. Linking saves the project name in your config and
86
+ keeps the upload token in user storage. Run it again to reuse a saved token.
87
+ Replace `"pnpm test"` with your test command.
101
88
 
102
- ```bash
103
- vizzly api schema --json
104
- vizzly api schema sdk.listBuilds --json
105
- ```
89
+ If you linked with an older CLI, run `project link` once in each repository to
90
+ save its project selection. Handwritten config may need a manual edit; the
91
+ command prints what to add. Explicit `--token` and `VIZZLY_TOKEN` still work.
106
92
 
107
- The `context build --agent` and `context comparison --agent` formats are
108
- deprecated and scheduled for removal in v0.38.0. Use the schema-driven API for
109
- cloud evidence; use `vizzly context ... --json` for local evidence.
93
+ To upload screenshots you already have:
110
94
 
111
95
  ```bash
112
- # Human-readable cloud build context
113
- vizzly context build abc123 --source cloud
114
-
115
- # Discover cloud evidence operations for an agent workflow
116
- vizzly api schema sdk.getBuildContext --json
117
- vizzly api schema getComparisonContext --json
118
-
119
- # Local workspace context from .vizzly/
120
- vizzly context build current --source local
121
- vizzly context build current --source local --json
122
- vizzly context screenshot build-detail-screenshots --source local --json
123
- vizzly context review-queue --source local --json
96
+ pnpm exec vizzly upload ./screenshots
124
97
  ```
125
98
 
126
- Use `vizzly api schema` to inspect cloud operation inputs, output fields, and
127
- pagination before making requests. The old compact `--agent` handoff remains
128
- available during deprecation, but new cloud workflows should use the schema.
129
- Use `--json` for machine-readable local context.
99
+ ### Run in CI
130
100
 
131
- Local context is read-only and file-backed. It reads your existing `.vizzly`
132
- workspace state from TDD runs, including screenshots, diffs, and saved hotspot
133
- or region metadata.
101
+ Store a project token as a CI secret and pass it as `VIZZLY_TOKEN`. Add `--wait`
102
+ when you want CI to wait for comparison results and fail on visual differences:
134
103
 
135
- The `context` commands are read-only. The schema-discovered review API also
136
- supports explicit decisions when a task asks for them.
104
+ ```bash
105
+ export VIZZLY_TOKEN=your-project-token
106
+ pnpm exec vizzly run "pnpm test" --wait
107
+ ```
137
108
 
138
- ## Capture Screenshots
109
+ With `--wait`, failed comparisons make the command exit with code `1`.
139
110
 
140
- Add screenshots to your existing tests:
111
+ ## Screenshot options
141
112
 
142
113
  ```javascript
143
- import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
144
-
145
- test('homepage looks correct', async ({ page }) => {
146
- await page.goto('/');
147
-
148
- let screenshot = await page.screenshot();
149
- await vizzlyScreenshot('homepage', screenshot, {
150
- fullPage: true,
151
- requestTimeout: 5000,
152
- properties: {
153
- theme: 'dark',
154
- locale: 'en-US',
155
- },
156
- });
114
+ let screenshot = await page.screenshot({ fullPage: true });
115
+ await vizzlyScreenshot('checkout', screenshot, {
116
+ threshold: 2,
117
+ minClusterSize: 4,
118
+ fullPage: true,
119
+ requestTimeout: 5000,
120
+ properties: { theme: 'dark', locale: 'en-US' },
157
121
  });
158
122
  ```
159
123
 
160
- `properties` is your key/value metadata bag for baseline grouping, filtering,
161
- and debugging. Vizzly passes every property through as user metadata; property
162
- names are never interpreted as options. The supported top-level options are
124
+ Put details like theme or locale in `properties` for baseline grouping and
125
+ filtering. Vizzly keeps these separate from screenshot options and reads image
126
+ dimensions from the screenshot. For local baseline matching, list custom keys
127
+ in `signatureProperties`. For cloud builds, set matching properties in your
128
+ Vizzly project settings. Common top-level options are
163
129
  `threshold`, `minClusterSize`, `fullPage`, `requestTimeout`, and `buildId`.
164
- Vizzly reads width and height from the captured image, so viewport dimensions do
165
- not need to be included in `properties`.
166
130
 
167
- The client SDK is lightweight. It posts screenshots to the local Vizzly server
168
- or the cloud build wrapper. It works with any test runner.
131
+ For setup help, see the [Storybook](clients/storybook/README.md),
132
+ [static site](clients/static-site/README.md), [Vitest](clients/vitest/README.md),
133
+ and [Ember](clients/ember/README.md) guides. There are also
134
+ [Ruby](clients/ruby/README.md) and [Swift](clients/swift/README.md) SDKs.
169
135
 
170
- SDKs are available for
171
- [JavaScript](https://docs.vizzly.dev/integration/sdk/javascript),
172
- [Ruby](https://docs.vizzly.dev/integration/sdk/ruby),
173
- [Swift](https://docs.vizzly.dev/integration/sdk/swift), and
174
- [more](https://docs.vizzly.dev/integration/sdk/overview).
136
+ ## Configuration
175
137
 
176
- Already saving screenshots to disk? Pass the file path instead:
138
+ `vizzly init --quick` creates a config with commented examples. Put settings
139
+ there when you want to change a default, or write `vizzly.config.js` yourself:
177
140
 
178
141
  ```javascript
179
- await page.screenshot({ path: './screenshots/homepage.png' });
180
- await vizzlyScreenshot('homepage', './screenshots/homepage.png');
142
+ export default {
143
+ comparison: {
144
+ threshold: 2, // CIEDE2000 Delta E color tolerance.
145
+ },
146
+ };
181
147
  ```
182
148
 
183
- Or upload an existing folder of screenshots:
149
+ Cloud comparisons use your project's settings unless you override them in
150
+ config, environment variables, or command options. Local comparisons use the
151
+ CLI defaults.
152
+
153
+ Existing config is kept, including TypeScript, rc files, and `package.json`.
154
+ Use `--config <path>` to choose a config or a new destination. `--force`
155
+ replaces the selected config; in `package.json`, it replaces only `vizzly`.
156
+
157
+ For TypeScript, use `vizzly.config.ts`. Node strips the types, so you don't need
158
+ a separate compiler. Use `import type` for types and include extensions in local
159
+ imports, like `./helpers.ts`. Enums, namespaces with runtime code, constructor
160
+ parameter properties, and `tsconfig.json` path aliases won't work in the config
161
+ or files it imports.
162
+
163
+ ### Coding agents and scripts
164
+
165
+ Install the Vizzly skill and a short `AGENTS.md` note:
184
166
 
185
167
  ```bash
186
- vizzly upload ./screenshots --threshold 2 --min-cluster-size 4 --batch-size 10 --upload-timeout 60000
168
+ pnpm exec vizzly init --agent-guidance
187
169
  ```
188
170
 
189
- `--batch-size` controls how many screenshots are uploaded per request.
190
- `--upload-timeout` controls the upload client's timeout, including how long
191
- `--wait` polls for build processing.
171
+ This adds `.agents/skills/vizzly` and a note in `AGENTS.md`, keeping other
172
+ guidance. Use `--agent-skill` to install only the skill during setup.
173
+ These flags don't open the wizard or update an existing Vizzly setup.
174
+ `--skip-agent-skill` skips agent setup unless you also request `--agent-guidance`.
192
175
 
193
- When `vizzly run` sends screenshots to Vizzly, the CLI first asks the API to
194
- resolve each screenshot by SHA. If Vizzly already has the image bytes, the CLI
195
- creates the screenshot record without re-uploading the image. CI workflows can
196
- force every screenshot through with `--upload-all`. Parallel jobs should use the
197
- same stable ID, then finalize that ID after every shard completes:
176
+ Use `context` to read local results. For cloud results, check the API schema
177
+ to find the available requests:
198
178
 
199
179
  ```bash
200
- vizzly run "pnpm test" --upload-all --parallel-id "$GITHUB_RUN_ID"
201
- vizzly finalize "$GITHUB_RUN_ID"
180
+ pnpm exec vizzly context build current --source local --json
181
+ pnpm exec vizzly context review-queue --source local --json
182
+ pnpm exec vizzly api schema --json
183
+ pnpm exec vizzly api schema sdk.getBuildContext --json
202
184
  ```
203
185
 
204
- For smoke jobs where cloud credentials are intentionally unavailable, add
205
- `--allow-no-token` to `vizzly run` and Vizzly will keep the local screenshot
206
- server path working without creating a cloud build.
186
+ The schema lists inputs, output fields, and how to fetch more results. The old
187
+ `context build --agent` and `context comparison --agent` formats are deprecated.
188
+ Use the API schema for new cloud workflows and `context --json` for local data.
189
+ See the [JSON reference](docs/json-output.md) for output formats and scripts.
207
190
 
208
- ## Configuration
191
+ ### Parallel CI and uploads
209
192
 
210
- Generate a config file:
193
+ Vizzly reuses images it already has by SHA. Add `--upload-all` to send every
194
+ image. For parallel jobs, use the same ID for every shard and finalize it when
195
+ they all finish:
211
196
 
212
197
  ```bash
213
- vizzly init
198
+ pnpm exec vizzly run "pnpm test" --parallel-id "$GITHUB_RUN_ID"
199
+ pnpm exec vizzly finalize "$GITHUB_RUN_ID"
214
200
  ```
215
201
 
216
- To teach project agents the Vizzly evidence workflow, add the repo-local skill
217
- and AGENTS.md guidance:
202
+ Use `--batch-size` to set how many screenshots upload per request.
203
+ `--upload-timeout` limits how long `--wait` waits for build processing; it
204
+ doesn't set the HTTP upload timeout. You can also set
205
+ `--threshold` and `--min-cluster-size`. If CI has no cloud token,
206
+ `vizzly run --allow-no-token` lets your test command run without starting Vizzly.
218
207
 
219
- ```bash
220
- vizzly init --agent-guidance
221
- ```
208
+ ## Commands
222
209
 
223
- Or create `vizzly.config.js` manually:
210
+ Use `pnpm exec vizzly --help` to see what's available in your project, or add
211
+ `--help` to a command for its options. These are the built-in commands; run them
212
+ with `pnpm exec` when Vizzly is installed locally.
224
213
 
225
- ```javascript
226
- export default {
227
- comparison: {
228
- // CIEDE2000 Delta E. 0 is exact. 2 is a good default.
229
- threshold: 2.0,
230
- },
231
- };
232
- ```
214
+ <details>
215
+ <summary>Setup and accounts</summary>
233
216
 
234
- ## Commands
217
+ | Command | What it does |
218
+ | --- | --- |
219
+ | `vizzly init` | Walk through setup. Use `--quick` to create config without questions. |
220
+ | `vizzly config [key]` | Show your current config. |
221
+ | `vizzly project link <org>/<project>` | Link this checkout to a cloud project. |
222
+ | `vizzly orgs` | List your organizations. |
223
+ | `vizzly projects` | List projects you can access. |
224
+ | `vizzly login` | Sign in through the browser. |
225
+ | `vizzly logout` | Clear your saved login. |
226
+ | `vizzly whoami` | Check your account and authentication status. |
227
+ | `vizzly doctor` | Check your setup. |
228
+
229
+ </details>
230
+
231
+ <details>
232
+ <summary>Local review</summary>
235
233
 
236
234
  | Command | What it does |
237
235
  | --- | --- |
238
- | `vizzly tdd start` | Start the local TDD server and dashboard. |
239
- | `vizzly tdd status` | Check the local TDD server for this project. |
240
- | `vizzly tdd list` | List running local TDD servers. |
241
- | `vizzly tdd stop` | Stop the local TDD server for this project. |
242
- | `vizzly tdd run "cmd"` | Run tests once and write local review data under `.vizzly/`. |
243
- | `vizzly run "cmd"` | Run tests with cloud build and review integration. |
244
- | `vizzly context ...` | Fetch visual context for builds, comparisons, screenshots, and review queues. |
245
- | `vizzly upload <dir>` | Upload an existing folder of screenshots. |
246
- | `vizzly preview <dir>` | Upload static build output for in-context review. |
247
- | `vizzly approve <comparison-id>` | Approve a visual comparison. |
248
- | `vizzly reject <comparison-id>` | Reject a visual comparison with a reason. |
249
- | `vizzly comment <build-id>` | Add a build comment. |
250
- | `vizzly config [key]` | Inspect resolved configuration values. |
251
- | `vizzly login` | Authenticate through the browser. |
252
- | `vizzly doctor` | Validate your local setup. |
253
-
254
- ## Documentation
255
-
256
- Full documentation lives at **[docs.vizzly.dev](https://docs.vizzly.dev)**.
257
- Start there for framework guides, CI setup, SDK examples, and the configuration
258
- reference.
259
-
260
- ## Contributing
261
-
262
- Found a bug or have an idea?
263
- [Open an issue](https://github.com/vizzly-testing/cli/issues) or send a PR.
264
-
265
- ## License
266
-
267
- MIT
236
+ | `vizzly tdd start` | Start the local server and dashboard. |
237
+ | `vizzly tdd status` | Check this project's local server. |
238
+ | `vizzly tdd list` | List running local servers. |
239
+ | `vizzly tdd stop` | Stop this project's local server. |
240
+ | `vizzly tdd run "command"` | Run tests once and save local review data. |
241
+ | `vizzly tdd screenshots list` | List captures from the latest local run. |
242
+ | `vizzly tdd screenshots latest [name]` | Print the latest capture's image path. |
243
+ | `vizzly tdd screenshots show <id-or-name>` | Show local capture details and the current image path. |
244
+ | `vizzly tdd screenshots accept <id-or-name>` | Save a local capture as its baseline. |
245
+ | `vizzly baselines` | List local baselines, or inspect one with `--info <name>`. |
246
+
247
+ </details>
248
+
249
+ <details>
250
+ <summary>Cloud builds</summary>
251
+
252
+ | Command | What it does |
253
+ | --- | --- |
254
+ | `vizzly run "command"` | Run tests and upload for cloud review. |
255
+ | `vizzly upload <dir>` | Upload screenshots. |
256
+ | `vizzly finalize <parallel-id>` | Finish a cloud build after all shards complete. |
257
+ | `vizzly preview <path>` | Upload a static preview for a cloud build; private projects need `--public-link`. |
258
+ | `vizzly status <build-id>` | Check cloud build status. |
259
+ | `vizzly builds` | List builds, or fetch one with `--build <id>`. |
260
+ | `vizzly comparisons --build <build-id>` | List a build's comparisons; also supports `--id` or `--name` queries. |
261
+
262
+ </details>
263
+
264
+ <details>
265
+ <summary>Build and screenshot context</summary>
266
+
267
+ | Command | What it does |
268
+ | --- | --- |
269
+ | `vizzly context build <build-id>` | Read build context. |
270
+ | `vizzly context comparison <comparison-id>` | Read comparison context. |
271
+ | `vizzly context screenshot <name>` | Read screenshot history. |
272
+ | `vizzly context similar <fingerprint-hash>` | Find cloud comparisons with similar diffs. |
273
+ | `vizzly context review-queue` | Read comparisons waiting for review. |
274
+
275
+ </details>
276
+
277
+ <details>
278
+ <summary>Review decisions</summary>
279
+
280
+ | Command | What it does |
281
+ | --- | --- |
282
+ | `vizzly approve <comparison-id>` | Approve a comparison. |
283
+ | `vizzly reject <comparison-id> --reason "message"` | Reject a comparison with a reason. |
284
+ | `vizzly comment <build-id> "message"` | Comment on a build. |
285
+
286
+ </details>
287
+
288
+ <details>
289
+ <summary>API</summary>
290
+
291
+ | Command | What it does |
292
+ | --- | --- |
293
+ | `vizzly api schema [operation-id]` | Discover cloud API requests, fields, and response formats. |
294
+ | `vizzly api <endpoint>` | Make API requests; use `--output <file>` for images. |
295
+
296
+ </details>
297
+
298
+ Installed plugins add commands such as `storybook`, `static-site`, and `previews`.
299
+ See their setup guides above for details.
300
+
301
+ ## More
302
+
303
+ [Full docs](https://docs.vizzly.dev) ·
304
+ [Report a bug or suggest a change](https://github.com/vizzly-testing/cli/issues)
305
+
306
+ PRs welcome. MIT licensed.
@@ -56,7 +56,10 @@ export function createAuthClient(options = {}) {
56
56
  });
57
57
  if (!response.ok) {
58
58
  let body = await parseResponseBody(response);
59
- throw parseAuthError(response.status, body, endpoint);
59
+ let error = parseAuthError(response.status, body, endpoint);
60
+ let retryAfter = Number(response.headers?.get('retry-after'));
61
+ if (retryAfter > 0) error.context.retryAfter = retryAfter;
62
+ throw error;
60
63
  }
61
64
  return response.json();
62
65
  }
package/dist/auth/core.js CHANGED
@@ -70,13 +70,19 @@ export function parseAuthError(status, body, _endpoint) {
70
70
  } else if (typeof body === 'string') {
71
71
  errorText = body;
72
72
  }
73
+ let retryAfter = Number(body?.retryAfter);
74
+ let context = {
75
+ status,
76
+ serverCode: body?.code,
77
+ retryAfter: retryAfter > 0 ? retryAfter : undefined
78
+ };
73
79
  if (status === 401) {
74
- return new AuthError(errorText || 'Invalid credentials. Please check your email/username and password.');
80
+ return new AuthError(errorText || 'Invalid credentials. Please check your email/username and password.', context);
75
81
  }
76
82
  if (status === 429) {
77
- return new VizzlyError('Too many login attempts. Please try again later.', 'RATE_LIMIT_ERROR');
83
+ return new VizzlyError('Too many login attempts. Please try again later.', 'RATE_LIMIT_ERROR', context);
78
84
  }
79
- return new VizzlyError(`Authentication request failed: ${status}${errorText ? ` - ${errorText}` : ''}`, 'AUTH_REQUEST_ERROR');
85
+ return new VizzlyError(`Authentication request failed: ${status}${errorText ? ` - ${errorText}` : ''}`, 'AUTH_REQUEST_ERROR', context);
80
86
  }
81
87
 
82
88
  /**
@@ -173,4 +179,59 @@ export function validateTokens(auth, requiredField = 'accessToken') {
173
179
  valid: true,
174
180
  error: null
175
181
  };
182
+ }
183
+ export function normalizeDeviceFlowResponse(deviceFlow) {
184
+ let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
185
+ let userCode = deviceFlow.user_code || deviceFlow.userCode;
186
+ let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
187
+ if (!verificationUri || !userCode || !deviceCode) {
188
+ throw new Error('Invalid device flow response from server');
189
+ }
190
+ return {
191
+ verificationUri,
192
+ userCode,
193
+ deviceCode,
194
+ urlWithCode: deviceFlow.verification_uri_complete || deviceFlow.verificationUriComplete || `${verificationUri}?code=${encodeURIComponent(userCode)}`
195
+ };
196
+ }
197
+ export function resolveAuthorizedTokenData(pollResponse) {
198
+ let accessToken = pollResponse.tokens?.accessToken || pollResponse.tokens?.access_token;
199
+ if (accessToken) {
200
+ return pollResponse;
201
+ }
202
+ if (pollResponse.status === 'pending') {
203
+ throw new Error('Authorization not complete yet. Please complete the authorization in your browser and try running "vizzly login" again.');
204
+ }
205
+ if (pollResponse.status === 'expired') {
206
+ throw new Error('Device code expired. Please try logging in again.');
207
+ }
208
+ if (pollResponse.status === 'invalid') {
209
+ throw new Error('Device code is invalid or expired. Please try logging in again.');
210
+ }
211
+ if (pollResponse.status === 'denied') {
212
+ throw new Error('Authorization denied. Please try logging in again.');
213
+ }
214
+ throw new Error('Unexpected response from authorization server. Please try logging in again.');
215
+ }
216
+ export function buildTokens(tokenData, now = Date.now()) {
217
+ let tokensData = tokenData.tokens || tokenData;
218
+ let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
219
+ let tokenExpiresAt = tokenExpiresIn ? new Date(now + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
220
+ return {
221
+ accessToken: tokensData.accessToken || tokensData.access_token,
222
+ refreshToken: tokensData.refreshToken || tokensData.refresh_token,
223
+ expiresAt: tokenExpiresAt,
224
+ user: tokenData.user,
225
+ organizations: tokenData.organizations
226
+ };
227
+ }
228
+
229
+ /** Map protocol codes without depending on the server's human-readable text. */
230
+ export function deviceAuthorizationStatus(error) {
231
+ return {
232
+ AUTHORIZATION_PENDING: 'pending',
233
+ ACCESS_DENIED: 'denied',
234
+ EXPIRED_TOKEN: 'expired',
235
+ INVALID_DEVICE_CODE: 'invalid'
236
+ }[error.context?.serverCode];
176
237
  }
@@ -8,7 +8,8 @@
8
8
  * This makes them trivially testable without mocking modules.
9
9
  */
10
10
 
11
- import { buildDevicePollPayload, buildLogoutPayload, buildRefreshPayload, buildTokenData, validateTokens } from './core.js';
11
+ import * as output from '../utils/output.js';
12
+ import { buildDevicePollPayload, buildLogoutPayload, buildRefreshPayload, buildTokenData, deviceAuthorizationStatus, validateTokens } from './core.js';
12
13
 
13
14
  // ============================================================================
14
15
  // Device Flow Operations
@@ -34,14 +35,23 @@ export async function initiateDeviceFlow(httpClient) {
34
35
  * @param {string} deviceCode - Device code from initiate
35
36
  * @returns {Promise<Object>} Token data or pending status
36
37
  */
37
- export async function pollDeviceAuthorization(httpClient, deviceCode) {
38
- return httpClient.request('/api/auth/cli/device/poll', {
39
- method: 'POST',
40
- headers: {
41
- 'Content-Type': 'application/json'
42
- },
43
- body: JSON.stringify(buildDevicePollPayload(deviceCode))
44
- });
38
+ export async function pollDeviceAuthorization(httpClient, deviceCode, options = {}) {
39
+ try {
40
+ return await httpClient.request('/api/auth/cli/device/poll', {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json'
44
+ },
45
+ body: JSON.stringify(buildDevicePollPayload(deviceCode)),
46
+ signal: options.signal
47
+ });
48
+ } catch (error) {
49
+ let status = deviceAuthorizationStatus(error);
50
+ if (status) return {
51
+ status
52
+ };
53
+ throw error;
54
+ }
45
55
  }
46
56
 
47
57
  /**
@@ -98,7 +108,7 @@ export async function logout(httpClient, tokenStore) {
98
108
  let auth = await tokenStore.getTokens();
99
109
  if (auth?.refreshToken) {
100
110
  try {
101
- await httpClient.request('/api/auth/cli/logout', {
111
+ await httpClient.authenticatedRequest('/api/auth/cli/logout', auth.accessToken, {
102
112
  method: 'POST',
103
113
  headers: {
104
114
  'Content-Type': 'application/json'
@@ -107,7 +117,7 @@ export async function logout(httpClient, tokenStore) {
107
117
  });
108
118
  } catch (error) {
109
119
  // If server request fails, still clear local tokens
110
- console.warn('Warning: Failed to revoke tokens on server:', error.message);
120
+ output.warn(`Failed to revoke tokens on server: ${error.message}`);
111
121
  }
112
122
  }
113
123
  await tokenStore.clearTokens();