Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# Vizzly CLI

> Visual proof that your UI works
> Reviewed UI context for people and LLM agents

[![npm version](https://img.shields.io/npm/v/@vizzly-testing/cli.svg)](https://www.npmjs.com/package/@vizzly-testing/cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Visual bugs slip through code review. They hide in pixel-perfect mockups, sneak past unit tests, and show up right when you're about to ship. Vizzly catches them first.
Vizzly keeps the visual truth behind your UI: approved baselines, meaningful diffs, review state, comments, and preview links in one place. That makes it useful for humans reviewing product changes and for LLM agents that need to understand what the UI is supposed to look like before they edit it.

Unlike tools that re-render components in isolation, Vizzly captures screenshots directly from your functional teststhe *real thing*. Whether you're validating AI-generated code or testing manual changes, you get visual proof before anything hits production.
Unlike tools that re-render components in isolation, Vizzly captures screenshots directly from your functional tests: the real thing. Whether you're validating AI-generated code or testing manual changes, you get reviewed UI context before anything hits production.

## Why Vizzly?

**Local TDD workflow.** See changes as you type, not after CI. The `vizzly tdd` command runs a local dashboard that compares screenshots instantly—no cloud roundtrip, no waiting.
**Local TDD workflow.** See changes as you type, not after CI. The `vizzly tdd` command runs a local dashboard that compares screenshots instantly and exposes the current workspace as local context.

**Smart diffing with Honeydiff.** Our Rust-based comparison engine is 12x faster than alternatives and ignores the noise: timestamps, ads, font rendering differences. It finds real changes.
**Meaningful diff metadata.** Vizzly stores rich diff evidence: changed regions, cluster metadata, fingerprints, hotspots, confirmed regions, and image URLs. Agents can inspect what changed instead of guessing from a pass/fail label.

**Any screenshot source.** Playwright, Cypress, Puppeteer, Selenium, native mobile apps, or even design mockups. If you can capture it, Vizzly can compare it.

**Team-based pricing.** Pay for your team, not your screenshots. Test everything without budget anxiety.
**Approved baselines as truth.** Cloud context carries human review state. Local context carries the downloaded or generated baseline metadata. That is the bridge between TDD locally and collaborative review in Vizzly.

## Quick Start

Expand Down Expand Up @@ -60,7 +60,7 @@ vizzly run "npm test" --wait

### Visual Context For Agents

Use `vizzly context` when you want Vizzly to act more like visual context than a test runner.
Use `vizzly context` when you want Vizzly to act like a visual context store, not just a test runner.

This is especially useful for LLM agents, automation, and quick debugging loops. Instead of
making a bunch of narrow API calls, you can ask for one build, comparison, screenshot, or review
Expand All @@ -73,11 +73,11 @@ vizzly context comparison def456 --json

# Local workspace context from .vizzly/
vizzly context build current --source local
vizzly context build current --source local --agent
vizzly context screenshot build-detail-screenshots --source local --json
```

`--json` is the main automation path. Human-readable output is there for quick terminal use, but
JSON is what you want for scripts, agents, and prompt assembly.
`--json` is the durable automation path. `--agent` gives a compact Markdown handoff for prompt assembly and local dogfooding.

Local context is read-only and file-backed. It reads your existing `.vizzly` workspace state from
TDD runs, including screenshots, diffs, and any saved hotspot or region metadata.
Expand Down
4 changes: 2 additions & 2 deletions clients/ember/test-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions clients/storybook/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export async function run(storybookPath, options = {}, context = {}) {
branch = gitInfo.branch;
commit = gitInfo.commit;
message = gitInfo.message;
buildName = gitInfo.buildName;
buildName = vizzlyConfig?.build?.name || gitInfo.buildName;
pullRequestNumber = gitInfo.prNumber;
} else {
// Fallback for older CLI versions - use environment variables
Expand All @@ -138,7 +138,9 @@ export async function run(storybookPath, options = {}, context = {}) {
branch = process.env.VIZZLY_BRANCH || 'main';
commit = process.env.VIZZLY_COMMIT_SHA || undefined;
message = process.env.VIZZLY_COMMIT_MESSAGE || undefined;
buildName = `Storybook ${new Date().toISOString()}`;
buildName =
vizzlyConfig?.build?.name ||
`Storybook ${new Date().toISOString()}`;
pullRequestNumber = process.env.VIZZLY_PR_NUMBER
? parseInt(process.env.VIZZLY_PR_NUMBER, 10)
: undefined;
Expand Down
46 changes: 40 additions & 6 deletions clients/swift/scripts/run-e2e.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { spawn } from 'node:child_process';
import { spawn, spawnSync } from 'node:child_process';
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
Expand All @@ -16,6 +16,7 @@ if (!existsSync(distCliPath)) {
}

let tempDir = mkdtempSync(join(tmpdir(), 'vizzly-swift-e2e-'));
let vizzlyHome = join(tempDir, '.vizzly-home');
let swiftTestCommand = [
'cd',
JSON.stringify(swiftPackageDir),
Expand All @@ -26,6 +27,36 @@ let swiftTestCommand = [
'VizzlyE2ETests',
].join(' ');

function cleanupAndExit(code = 1) {
rmSync(tempDir, { recursive: true, force: true });
process.exit(code);
}

function printLocalContext() {
let contextResult = spawnSync(
process.execPath,
[cliPath, 'context', 'build', 'current', '--source', 'local', '--agent'],
{
cwd: tempDir,
encoding: 'utf8',
env: {
...process.env,
VIZZLY_HOME: vizzlyHome,
},
}
);

if (contextResult.stdout) {
process.stdout.write(contextResult.stdout);
}

if (contextResult.stderr) {
process.stderr.write(contextResult.stderr);
}

return contextResult.status ?? 1;
}

let child = spawn(
process.execPath,
[cliPath, 'tdd', 'run', swiftTestCommand, '--no-color'],
Expand All @@ -35,18 +66,21 @@ let child = spawn(
env: {
...process.env,
VIZZLY_E2E: '1',
VIZZLY_HOME: join(tempDir, '.vizzly-home'),
VIZZLY_HOME: vizzlyHome,
},
}
);

child.on('exit', code => {
rmSync(tempDir, { recursive: true, force: true });
process.exit(code ?? 1);
if (code !== 0) {
cleanupAndExit(code ?? 1);
}

let contextStatus = printLocalContext();
cleanupAndExit(contextStatus);
});

child.on('error', error => {
rmSync(tempDir, { recursive: true, force: true });
console.error(error);
process.exit(1);
cleanupAndExit(1);
});
67 changes: 47 additions & 20 deletions docs/json-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ vizzly tdd list --json
### `vizzly context`

Use `vizzly context` when you want one machine-friendly bundle instead of several narrow calls.
This is the best fit for automation, agents, and scripts that need visual evidence plus a little
bit of memory.
This is the best fit for automation, agents, and scripts that need approved baselines, visual
evidence, review state, comments, preview links, and diff metadata in one place.

Every context payload includes a `source` field. That tells you whether the bundle came from
cloud data or your local `.vizzly` workspace.
Expand All @@ -204,8 +204,12 @@ cloud data or your local `.vizzly` workspace.
```bash
vizzly context build abc123 --json
vizzly context build current --source local --json
vizzly context build current --source local --agent
```

Use `--json` for durable automation. Use `--agent` when you want a compact Markdown handoff for
prompt assembly.

```json
{
"resource": "build_context",
Expand All @@ -219,6 +223,21 @@ vizzly context build current --source local --json
"status": "completed",
"approval_status": "pending"
},
"baseline": {
"selected": {
"id": "baseline-build",
"name": "Approved Main",
"approval_status": "approved"
},
"selection_reason": "common_ancestor",
"comparison_baseline_build_ids": ["baseline-build"]
},
"status": {
"needs_review": true,
"reasons": ["comparisons_need_review"],
"pending_comparisons": 3,
"unresolved_comments": 0
},
"summary": {
"comparisons": {
"total": 12,
Expand All @@ -231,14 +250,30 @@ vizzly context build current --source local --json
"rejected": 0
}
},
"screenshots": [
{
"name": "Dashboard",
"url": "https://...",
"baseline": { "url": "https://..." }
}
],
"comparisons": [
{
"id": "cmp-1",
"name": "Dashboard",
"screenshot_name": "Dashboard",
"result": "changed",
"diff_percentage": 0.42
"needs_review": true,
"diff": {
"percentage": 0.42,
"image_url": "https://...",
"regions": []
}
}
]
],
"comments": {
"build": [],
"screenshot_count": 0
}
}
```

Expand Down Expand Up @@ -637,27 +672,19 @@ vizzly init --json
}
```

### `vizzly project:select`
### `vizzly project link`

```bash
vizzly project:select --json
vizzly project link my-org/my-project --json
```

Note: In JSON mode, the interactive prompts still appear because project selection requires user input.

```json
{
"status": "configured",
"project": {
"name": "My Project",
"slug": "my-project"
},
"organization": {
"name": "My Org",
"slug": "my-org"
},
"directory": "/path/to/project",
"tokenCreated": true
"linked": true,
"organizationSlug": "my-org",
"projectSlug": "my-project",
"tokenPrefix": "vzt_abc",
"storage": "keychain"
}
```

Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"name": "@vizzly-testing/cli",
"version": "0.30.0",
"description": "Visual review platform for UI developers and designers",
"description": "Reviewed UI context for people and LLM agents",
"keywords": [
"llm-context",
"ui-context",
"visual-context",
"approved-baselines",
"visual-testing",
"screenshot-testing",
"visual-regression",
"visual-review",
"ui-testing",
"collaboration",
Expand Down
12 changes: 9 additions & 3 deletions src/api/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
*/
export const DEFAULT_API_URL = 'https://app.vizzly.dev';

function isProjectToken(token) {
return typeof token === 'string' && token.startsWith('vzt_');
}

/**
* Create an API client with the given configuration
*
Expand Down Expand Up @@ -84,7 +88,7 @@ export function createApiClient(options = {}) {
shouldRetryWithRefresh(
response.status,
isRetry,
await hasRefreshToken()
!isProjectToken(token) && (await hasRefreshToken())
)
) {
let refreshed = await attemptTokenRefresh();
Expand All @@ -97,7 +101,7 @@ export function createApiClient(options = {}) {
// Auth error
if (isAuthError(response.status)) {
throw new AuthError(
'Invalid or expired API token. Link a project via "vizzly project:select" or set VIZZLY_TOKEN.'
'Invalid or expired API token. Run "vizzly project link <org>/<project>" or set VIZZLY_TOKEN.'
);
}

Expand Down Expand Up @@ -146,7 +150,9 @@ export function createApiClient(options = {}) {
await saveAuthTokens({
accessToken: data.accessToken,
refreshToken: data.refreshToken,
expiresAt: data.expiresAt,
expiresAt:
data.expiresAt ||
new Date(Date.now() + data.expiresIn * 1000).toISOString(),
user: auth.user,
});

Expand Down
Loading