@vizzly-testing/cli 0.30.0 → 0.31.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/README.md +9 -9
- package/dist/api/client.js +6 -3
- package/dist/cli.js +58 -29
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +329 -18
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +17 -6
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +63 -25
- package/dist/config/core.js +36 -35
- package/dist/context/local-workspace-provider.js +132 -12
- package/dist/context/provider-resolver.js +5 -1
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +7 -7
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +212 -174
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +40 -0
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +20 -1
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/package.json +11 -8
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# Vizzly CLI
|
|
2
2
|
|
|
3
|
-
> Visual
|
|
3
|
+
> Visual review for agents and teams
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@vizzly-testing/cli)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
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.
|
|
9
9
|
|
|
10
|
-
Unlike tools that re-render components in isolation, Vizzly captures screenshots directly from your functional tests
|
|
10
|
+
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 visual evidence before anything hits production.
|
|
11
11
|
|
|
12
12
|
## Why Vizzly?
|
|
13
13
|
|
|
14
|
-
**Local TDD workflow.** See changes as you type, not after CI. The `vizzly tdd` command runs a local dashboard that compares screenshots instantly
|
|
14
|
+
**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.
|
|
15
15
|
|
|
16
|
-
**
|
|
16
|
+
**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.
|
|
17
17
|
|
|
18
18
|
**Any screenshot source.** Playwright, Cypress, Puppeteer, Selenium, native mobile apps, or even design mockups. If you can capture it, Vizzly can compare it.
|
|
19
19
|
|
|
20
|
-
**
|
|
20
|
+
**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.
|
|
21
21
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
@@ -60,7 +60,7 @@ vizzly run "npm test" --wait
|
|
|
60
60
|
|
|
61
61
|
### Visual Context For Agents
|
|
62
62
|
|
|
63
|
-
Use `vizzly context` when you want Vizzly to act
|
|
63
|
+
Use `vizzly context` when you want Vizzly to act like a visual context store, not just a test runner.
|
|
64
64
|
|
|
65
65
|
This is especially useful for LLM agents, automation, and quick debugging loops. Instead of
|
|
66
66
|
making a bunch of narrow API calls, you can ask for one build, comparison, screenshot, or review
|
|
@@ -73,11 +73,11 @@ vizzly context comparison def456 --json
|
|
|
73
73
|
|
|
74
74
|
# Local workspace context from .vizzly/
|
|
75
75
|
vizzly context build current --source local
|
|
76
|
+
vizzly context build current --source local --agent
|
|
76
77
|
vizzly context screenshot build-detail-screenshots --source local --json
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
`--json` is the
|
|
80
|
-
JSON is what you want for scripts, agents, and prompt assembly.
|
|
80
|
+
`--json` is the durable automation path. `--agent` gives a compact handoff for prompt assembly and local dogfooding. Add `--full` when an agent really needs the complete build context payload, or `--include screenshots,diffs,comments` when the compact JSON needs selected detail.
|
|
81
81
|
|
|
82
82
|
Local context is read-only and file-backed. It reads your existing `.vizzly` workspace state from
|
|
83
83
|
TDD runs, including screenshots, diffs, and any saved hotspot or region metadata.
|
package/dist/api/client.js
CHANGED
|
@@ -14,6 +14,9 @@ import { buildApiUrl, buildRequestHeaders, buildUserAgent, extractErrorBody, isA
|
|
|
14
14
|
* Default API URL
|
|
15
15
|
*/
|
|
16
16
|
export const DEFAULT_API_URL = 'https://app.vizzly.dev';
|
|
17
|
+
function isProjectToken(token) {
|
|
18
|
+
return typeof token === 'string' && token.startsWith('vzt_');
|
|
19
|
+
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Create an API client with the given configuration
|
|
@@ -63,7 +66,7 @@ export function createApiClient(options = {}) {
|
|
|
63
66
|
let errorBody = await extractErrorBody(response);
|
|
64
67
|
|
|
65
68
|
// Handle 401 with token refresh
|
|
66
|
-
if (shouldRetryWithRefresh(response.status, isRetry, await hasRefreshToken())) {
|
|
69
|
+
if (shouldRetryWithRefresh(response.status, isRetry, !isProjectToken(token) && (await hasRefreshToken()))) {
|
|
67
70
|
let refreshed = await attemptTokenRefresh();
|
|
68
71
|
if (refreshed) {
|
|
69
72
|
token = refreshed;
|
|
@@ -73,7 +76,7 @@ export function createApiClient(options = {}) {
|
|
|
73
76
|
|
|
74
77
|
// Auth error
|
|
75
78
|
if (isAuthError(response.status)) {
|
|
76
|
-
throw new AuthError('Invalid or expired API token.
|
|
79
|
+
throw new AuthError('Invalid or expired API token. Run "vizzly project link <org>/<project>" or set VIZZLY_TOKEN.');
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// Other errors
|
|
@@ -119,7 +122,7 @@ export function createApiClient(options = {}) {
|
|
|
119
122
|
await saveAuthTokens({
|
|
120
123
|
accessToken: data.accessToken,
|
|
121
124
|
refreshToken: data.refreshToken,
|
|
122
|
-
expiresAt: data.expiresAt,
|
|
125
|
+
expiresAt: data.expiresAt || new Date(Date.now() + data.expiresIn * 1000).toISOString(),
|
|
123
126
|
user: auth.user
|
|
124
127
|
});
|
|
125
128
|
return data.accessToken;
|
package/dist/cli.js
CHANGED
|
@@ -14,18 +14,20 @@ import { loginCommand, validateLoginOptions } from './commands/login.js';
|
|
|
14
14
|
import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
|
|
15
15
|
import { orgsCommand, validateOrgsOptions } from './commands/orgs.js';
|
|
16
16
|
import { previewCommand, validatePreviewOptions } from './commands/preview.js';
|
|
17
|
+
import { projectLinkCommand, validateProjectLinkOptions } from './commands/project.js';
|
|
17
18
|
import { projectsCommand, validateProjectsOptions } from './commands/projects.js';
|
|
18
19
|
import { approveCommand, commentCommand, rejectCommand, validateApproveOptions, validateCommentOptions, validateRejectOptions } from './commands/review.js';
|
|
19
20
|
import { runCommand, validateRunOptions } from './commands/run.js';
|
|
20
21
|
import { statusCommand, validateStatusOptions } from './commands/status.js';
|
|
21
22
|
import { tddCommand, validateTddOptions } from './commands/tdd.js';
|
|
22
|
-
import { runDaemonChild, tddListCommand, tddStartCommand, tddStatusCommand, tddStopCommand } from './commands/tdd-daemon.js';
|
|
23
|
+
import { runDaemonChild, tddListCommand, tddStartCommand, tddStatusCommand, tddStopCommand, validateTddStartOptions } from './commands/tdd-daemon.js';
|
|
23
24
|
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
24
25
|
import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
|
|
25
26
|
import { createPluginServices } from './plugin-api.js';
|
|
26
27
|
import { loadPlugins } from './plugin-loader.js';
|
|
27
28
|
import { createServices } from './services/index.js';
|
|
28
29
|
import { generateStaticReport, getReportFileUrl } from './services/static-report-generator.js';
|
|
30
|
+
import { withTimeout } from './utils/async-utils.js';
|
|
29
31
|
import { openBrowser } from './utils/browser.js';
|
|
30
32
|
import { colors } from './utils/colors.js';
|
|
31
33
|
import { loadConfig } from './utils/config-loader.js';
|
|
@@ -47,7 +49,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
47
49
|
// Cute grizzly bear mascot with square eyes (like the Vizzly logo!)
|
|
48
50
|
lines.push(c.brand.amber(' ʕ□ᴥ□ʔ'));
|
|
49
51
|
lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.dim(`v${version}`)}`);
|
|
50
|
-
lines.push(` ${c.gray('Visual
|
|
52
|
+
lines.push(` ${c.gray('Visual review for agents and teams')}`);
|
|
51
53
|
} else {
|
|
52
54
|
// Compact header for subcommands
|
|
53
55
|
lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.white(cmd.name())}`);
|
|
@@ -171,7 +173,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
171
173
|
if (isRootCommand) {
|
|
172
174
|
lines.push(` ${c.brand.amber('▸')} ${c.bold('Quick Start')}`);
|
|
173
175
|
lines.push('');
|
|
174
|
-
lines.push(` ${c.dim('# Local visual
|
|
176
|
+
lines.push(` ${c.dim('# Local visual review')}`);
|
|
175
177
|
lines.push(` ${c.gray('$')} ${c.white('vizzly tdd start')}`);
|
|
176
178
|
lines.push('');
|
|
177
179
|
lines.push(` ${c.dim('# CI pipeline')}`);
|
|
@@ -250,7 +252,7 @@ function normalizeJsonArgv(argv, commandNames) {
|
|
|
250
252
|
}
|
|
251
253
|
return normalizedArgv;
|
|
252
254
|
}
|
|
253
|
-
program.name('vizzly').description('Vizzly CLI for visual
|
|
255
|
+
program.name('vizzly').description('Vizzly CLI for visual review').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json [fields]', 'JSON output, optionally specify fields (e.g., --json id,status,branch)').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)').configureHelp({
|
|
254
256
|
formatHelp
|
|
255
257
|
});
|
|
256
258
|
|
|
@@ -284,18 +286,15 @@ const pluginServices = createPluginServices(services);
|
|
|
284
286
|
let plugins = [];
|
|
285
287
|
try {
|
|
286
288
|
plugins = await loadPlugins(configPath, config);
|
|
287
|
-
for (
|
|
289
|
+
for (let plugin of plugins) {
|
|
288
290
|
try {
|
|
289
291
|
// Add timeout protection for plugin registration (5 seconds)
|
|
290
|
-
|
|
292
|
+
let registerPromise = plugin.register(program, {
|
|
291
293
|
config,
|
|
292
294
|
services: pluginServices,
|
|
293
|
-
output
|
|
294
|
-
// Backwards compatibility alias for plugins using old API
|
|
295
|
-
logger: output
|
|
295
|
+
output
|
|
296
296
|
});
|
|
297
|
-
|
|
298
|
-
await Promise.race([registerPromise, timeoutPromise]);
|
|
297
|
+
await withTimeout(registerPromise, 5000, 'Plugin registration timeout (5s)');
|
|
299
298
|
output.debug(`Registered plugin: ${plugin.name}`);
|
|
300
299
|
} catch (error) {
|
|
301
300
|
output.warn(`Failed to register plugin ${plugin.name}: ${error.message}`);
|
|
@@ -312,7 +311,7 @@ program.command('init').description('Initialize Vizzly in your project').option(
|
|
|
312
311
|
plugins
|
|
313
312
|
});
|
|
314
313
|
});
|
|
315
|
-
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size',
|
|
314
|
+
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', Number).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', Number).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (path, options) => {
|
|
316
315
|
const globalOptions = program.opts();
|
|
317
316
|
|
|
318
317
|
// Validate options
|
|
@@ -327,11 +326,11 @@ program.command('upload').description('Upload screenshots to Vizzly').argument('
|
|
|
327
326
|
await uploadCommand(path, options, globalOptions);
|
|
328
327
|
});
|
|
329
328
|
|
|
330
|
-
// TDD command with subcommands -
|
|
331
|
-
const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual
|
|
329
|
+
// TDD command with subcommands - local visual review with an interactive dashboard
|
|
330
|
+
const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual review');
|
|
332
331
|
|
|
333
332
|
// TDD Start - Background server
|
|
334
|
-
tddCmd.command('start').description('Start background TDD server with dashboard').option('--port <port>', 'Port for TDD server', '47392').option('--open', 'Open dashboard in browser').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold',
|
|
333
|
+
tddCmd.command('start').description('Start background TDD server with dashboard').option('--port <port>', 'Port for TDD server', '47392').option('--open', 'Open dashboard in browser').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--fail-on-diff', 'Fail tests when visual differences are detected').option('--token <token>', 'API token override').option('--daemon-child', 'Internal: run as daemon child process').action(async options => {
|
|
335
334
|
const globalOptions = program.opts();
|
|
336
335
|
|
|
337
336
|
// If this is a daemon child process, run the server directly
|
|
@@ -339,6 +338,14 @@ tddCmd.command('start').description('Start background TDD server with dashboard'
|
|
|
339
338
|
await runDaemonChild(options, globalOptions);
|
|
340
339
|
return;
|
|
341
340
|
}
|
|
341
|
+
let validationErrors = validateTddStartOptions(options);
|
|
342
|
+
if (validationErrors.length > 0) {
|
|
343
|
+
output.error('Validation errors:');
|
|
344
|
+
for (let error of validationErrors) {
|
|
345
|
+
output.printErr(` - ${error}`);
|
|
346
|
+
}
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
342
349
|
await tddStartCommand(options, globalOptions);
|
|
343
350
|
});
|
|
344
351
|
|
|
@@ -361,7 +368,7 @@ tddCmd.command('list').description('List all running TDD servers').action(async
|
|
|
361
368
|
});
|
|
362
369
|
|
|
363
370
|
// TDD Run - One-off test run with ephemeral server (generates static report)
|
|
364
|
-
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual
|
|
371
|
+
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual review').option('--port <port>', 'Port for TDD server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').option('--fail-on-diff', 'Fail tests when visual differences are detected').option('--no-open', 'Skip opening report in browser').action(async (command, options) => {
|
|
365
372
|
const globalOptions = program.opts();
|
|
366
373
|
|
|
367
374
|
// Validate options
|
|
@@ -422,7 +429,7 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
|
|
|
422
429
|
await handleCleanup();
|
|
423
430
|
process.exit(result?.exitCode || 0);
|
|
424
431
|
});
|
|
425
|
-
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (command, options) => {
|
|
432
|
+
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--batch-size <n>', 'Upload batch size', Number).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', Number).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (command, options) => {
|
|
426
433
|
const globalOptions = program.opts();
|
|
427
434
|
|
|
428
435
|
// Validate options
|
|
@@ -458,7 +465,7 @@ program.command('status').description('Check the status of a build').argument('<
|
|
|
458
465
|
}
|
|
459
466
|
await statusCommand(buildId, options, globalOptions);
|
|
460
467
|
});
|
|
461
|
-
program.command('builds').description('List and query builds').option('-b, --build <id>', 'Get a specific build by ID').option('--branch <branch>', 'Filter by branch').option('--status <status>', 'Filter by status (created, pending, processing, completed, failed)').option('--environment <env>', 'Filter by environment').option('-p, --project <slug>', 'Filter by project slug').option('--org <slug>', 'Filter by organization slug').option('--limit <n>', 'Maximum results to return (1-250)',
|
|
468
|
+
program.command('builds').description('List and query builds').option('-b, --build <id>', 'Get a specific build by ID').option('--branch <branch>', 'Filter by branch').option('--status <status>', 'Filter by status (created, pending, processing, completed, failed)').option('--environment <env>', 'Filter by environment').option('-p, --project <slug>', 'Filter by project slug').option('--org <slug>', 'Filter by organization slug').option('--limit <n>', 'Maximum results to return (1-250)', Number, 20).option('--offset <n>', 'Skip first N results', Number, 0).option('--comparisons', 'Include comparisons when fetching a specific build').addHelpText('after', `
|
|
462
469
|
Examples:
|
|
463
470
|
$ vizzly builds # List recent builds
|
|
464
471
|
$ vizzly builds --branch main # Filter by branch
|
|
@@ -482,7 +489,7 @@ Examples:
|
|
|
482
489
|
}
|
|
483
490
|
await buildsCommand(options, globalOptions);
|
|
484
491
|
});
|
|
485
|
-
program.command('comparisons').description('Query and search comparisons').option('-b, --build <id>', 'Get comparisons for a specific build').option('--id <id>', 'Get a specific comparison by ID').option('--name <pattern>', 'Search comparisons by name (supports wildcards)').option('--status <status>', 'Filter by status (identical, new, changed)').option('--branch <branch>', 'Filter by branch (for name search)').option('--limit <n>', 'Maximum results to return (1-250)',
|
|
492
|
+
program.command('comparisons').description('Query and search comparisons').option('-b, --build <id>', 'Get comparisons for a specific build').option('--id <id>', 'Get a specific comparison by ID').option('--name <pattern>', 'Search comparisons by name (supports wildcards)').option('--status <status>', 'Filter by status (identical, new, changed)').option('--branch <branch>', 'Filter by branch (for name search)').option('--limit <n>', 'Maximum results to return (1-250)', Number, 50).option('--offset <n>', 'Skip first N results', Number, 0).option('-p, --project <slug>', 'Filter by project slug').option('--org <slug>', 'Filter by organization slug').addHelpText('after', `
|
|
486
493
|
Examples:
|
|
487
494
|
$ vizzly comparisons -b abc123 # List comparisons for a build
|
|
488
495
|
$ vizzly comparisons --id def456 # Get specific comparison by ID
|
|
@@ -505,13 +512,15 @@ Examples:
|
|
|
505
512
|
}
|
|
506
513
|
await comparisonsCommand(options, globalOptions);
|
|
507
514
|
});
|
|
508
|
-
let contextCmd = program.command('context').description('Fetch
|
|
509
|
-
contextCmd.command('build').description('Fetch
|
|
515
|
+
let contextCmd = program.command('context').description('Fetch build context for agents and reviewers');
|
|
516
|
+
contextCmd.command('build').description('Fetch build context for agents and reviewers').argument('<build-id>', 'Build ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('--agent', 'Output compact context for LLM agents').option('--full', 'Return the full build context payload with --agent --json').option('--include <items>', 'Add detail to compact agent JSON: screenshots,diffs,comments').addHelpText('after', `
|
|
510
517
|
Examples:
|
|
511
518
|
$ vizzly context build abc123
|
|
512
519
|
$ vizzly context build current --source local
|
|
513
|
-
$ vizzly context build
|
|
514
|
-
$ vizzly context build abc123 --json
|
|
520
|
+
$ vizzly context build current --source local --agent
|
|
521
|
+
$ vizzly context build abc123 --agent --json
|
|
522
|
+
$ vizzly context build abc123 --agent --json --include diffs,comments
|
|
523
|
+
$ vizzly context build abc123 --agent --json --full
|
|
515
524
|
`).action(async (buildId, options) => {
|
|
516
525
|
const globalOptions = program.opts();
|
|
517
526
|
const validationErrors = validateContextBuildOptions(options);
|
|
@@ -524,7 +533,7 @@ Examples:
|
|
|
524
533
|
}
|
|
525
534
|
await contextBuildCommand(buildId, options, globalOptions);
|
|
526
535
|
});
|
|
527
|
-
contextCmd.command('comparison').description('Fetch a comparison context bundle').argument('<comparison-id>', 'Comparison ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('--similar-limit <n>', 'Maximum similar fingerprint matches to return (1-50)',
|
|
536
|
+
contextCmd.command('comparison').description('Fetch a comparison context bundle').argument('<comparison-id>', 'Comparison ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('--similar-limit <n>', 'Maximum similar fingerprint matches to return (1-50)', Number).option('--recent-limit <n>', 'Maximum recent same-name comparisons to return (1-50)', Number).option('--window-size <n>', 'Historical hotspot analysis window size (1-50)', Number).addHelpText('after', `
|
|
528
537
|
Examples:
|
|
529
538
|
$ vizzly context comparison def456
|
|
530
539
|
$ vizzly context comparison def456 --source local
|
|
@@ -542,7 +551,7 @@ Examples:
|
|
|
542
551
|
}
|
|
543
552
|
await contextComparisonCommand(comparisonId, options, globalOptions);
|
|
544
553
|
});
|
|
545
|
-
contextCmd.command('screenshot').description('Fetch screenshot context and historical memory').argument('<name>', 'Screenshot name').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--recent-limit <n>', 'Maximum recent comparisons to return (1-50)',
|
|
554
|
+
contextCmd.command('screenshot').description('Fetch screenshot context and historical memory').argument('<name>', 'Screenshot name').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--recent-limit <n>', 'Maximum recent comparisons to return (1-50)', Number).option('--window-size <n>', 'Historical hotspot analysis window size (1-50)', Number).addHelpText('after', `
|
|
546
555
|
Examples:
|
|
547
556
|
$ vizzly context screenshot Dashboard
|
|
548
557
|
$ vizzly context screenshot Dashboard --source local
|
|
@@ -560,7 +569,7 @@ Examples:
|
|
|
560
569
|
}
|
|
561
570
|
await contextScreenshotCommand(name, options, globalOptions);
|
|
562
571
|
});
|
|
563
|
-
contextCmd.command('similar').description('Fetch project-scoped matches for a fingerprint hash').argument('<fingerprint-hash>', 'Fingerprint hash to search for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--limit <n>', 'Maximum matches to return (1-50)',
|
|
572
|
+
contextCmd.command('similar').description('Fetch project-scoped matches for a fingerprint hash').argument('<fingerprint-hash>', 'Fingerprint hash to search for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--limit <n>', 'Maximum matches to return (1-50)', Number).addHelpText('after', `
|
|
564
573
|
Examples:
|
|
565
574
|
$ vizzly context similar fp-dashboard
|
|
566
575
|
$ vizzly context similar fp-dashboard --project storybook --org acme
|
|
@@ -577,7 +586,7 @@ Examples:
|
|
|
577
586
|
}
|
|
578
587
|
await contextSimilarCommand(fingerprintHash, options, globalOptions);
|
|
579
588
|
});
|
|
580
|
-
contextCmd.command('review-queue').description('Fetch pending review context for a project').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--limit <n>', 'Maximum comparisons to return (1-100)',
|
|
589
|
+
contextCmd.command('review-queue').description('Fetch pending review context for a project').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--limit <n>', 'Maximum comparisons to return (1-100)', Number).option('--offset <n>', 'Skip first N comparisons', Number).addHelpText('after', `
|
|
581
590
|
Examples:
|
|
582
591
|
$ vizzly context review-queue --project storybook --org acme
|
|
583
592
|
$ vizzly context review-queue --source local
|
|
@@ -740,7 +749,7 @@ or the single organization for a project token.
|
|
|
740
749
|
}
|
|
741
750
|
await orgsCommand(options, globalOptions);
|
|
742
751
|
});
|
|
743
|
-
program.command('projects').description('List projects you have access to').option('--org <slug>', 'Filter by organization slug').option('--limit <n>', 'Maximum results to return (1-250)',
|
|
752
|
+
program.command('projects').description('List projects you have access to').option('--org <slug>', 'Filter by organization slug').option('--limit <n>', 'Maximum results to return (1-250)', Number, 50).option('--offset <n>', 'Skip first N results', Number, 0).addHelpText('after', `
|
|
744
753
|
Examples:
|
|
745
754
|
$ vizzly projects # List all projects
|
|
746
755
|
$ vizzly projects --org my-company # Filter by organization
|
|
@@ -762,6 +771,26 @@ Workflow:
|
|
|
762
771
|
}
|
|
763
772
|
await projectsCommand(options, globalOptions);
|
|
764
773
|
});
|
|
774
|
+
let projectCommand = program.command('project').description('Manage the project linked to this local checkout');
|
|
775
|
+
projectCommand.command('link [selector]').description('Link a Vizzly project for cloud uploads').option('--org <slug>', 'Organization slug').option('--project <slug>', 'Project slug').option('--name <name>', 'Credential name shown in Vizzly').option('--expires-at <iso>', 'Optional token expiration timestamp').addHelpText('after', `
|
|
776
|
+
Examples:
|
|
777
|
+
$ vizzly project link vizzly/storybook
|
|
778
|
+
$ vizzly project link --org vizzly --project storybook
|
|
779
|
+
|
|
780
|
+
Note: run "vizzly login" first. The linked credential is project-scoped and is
|
|
781
|
+
used for cloud uploads; your user login remains separate for review actions.
|
|
782
|
+
`).action(async (selector, options) => {
|
|
783
|
+
const globalOptions = program.opts();
|
|
784
|
+
const validationErrors = validateProjectLinkOptions(selector, options);
|
|
785
|
+
if (validationErrors.length > 0) {
|
|
786
|
+
output.error('Validation errors:');
|
|
787
|
+
for (let error of validationErrors) {
|
|
788
|
+
output.printErr(` - ${error}`);
|
|
789
|
+
}
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
await projectLinkCommand(selector, options, globalOptions);
|
|
793
|
+
});
|
|
765
794
|
program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
|
|
766
795
|
const globalOptions = program.opts();
|
|
767
796
|
|
|
@@ -776,7 +805,7 @@ program.command('finalize').description('Finalize a parallel build after all sha
|
|
|
776
805
|
}
|
|
777
806
|
await finalizeCommand(parallelId, options, globalOptions);
|
|
778
807
|
});
|
|
779
|
-
program.command('preview').description('Upload static files as a preview for a build').argument('[path]', 'Path to static files (dist/, build/, out/)').option('-b, --build <id>', 'Build ID to attach preview to').option('
|
|
808
|
+
program.command('preview').description('Upload static files as a preview for a build').argument('[path]', 'Path to static files (dist/, build/, out/)').option('-b, --build <id>', 'Build ID to attach preview to').option('--base <path>', 'Override auto-detected base path').option('--open', 'Open preview URL in browser after upload').option('--dry-run', 'Show what would be uploaded without uploading').option('--public-link', 'Acknowledge that preview URL grants access to anyone with the link (required for private projects)').option('-x, --exclude <pattern>', 'Exclude files/dirs (repeatable, e.g. -x "*.log" -x "temp/")', (val, prev) => prev ? [...prev, val] : [val]).option('-i, --include <pattern>', 'Override default exclusions (repeatable, e.g. -i package.json -i tests/)', (val, prev) => prev ? [...prev, val] : [val]).action(async (path, options) => {
|
|
780
809
|
const globalOptions = program.opts();
|
|
781
810
|
|
|
782
811
|
// Show helpful error if path is missing
|
package/dist/client/index.js
CHANGED
|
@@ -8,16 +8,17 @@ import { request as httpRequest } from 'node:http';
|
|
|
8
8
|
import { request as httpsRequest } from 'node:https';
|
|
9
9
|
import { dirname, join, parse } from 'node:path';
|
|
10
10
|
import { getBuildId, getServerUrl, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
|
|
11
|
+
import { createScreenshotProperties } from '../utils/screenshot-options.js';
|
|
11
12
|
|
|
12
13
|
// Internal client state
|
|
13
14
|
let currentClient = null;
|
|
14
15
|
let isDisabled = false;
|
|
15
16
|
|
|
16
17
|
// Default timeout for screenshot requests (30 seconds)
|
|
17
|
-
|
|
18
|
+
let DEFAULT_TIMEOUT_MS = 30000;
|
|
18
19
|
|
|
19
20
|
// Log levels for client SDK output control
|
|
20
|
-
export
|
|
21
|
+
export let LOG_LEVELS = {
|
|
21
22
|
debug: 0,
|
|
22
23
|
info: 1,
|
|
23
24
|
warn: 2,
|
|
@@ -71,14 +72,14 @@ export function autoDiscoverTddServer(startDir, deps = {}) {
|
|
|
71
72
|
try {
|
|
72
73
|
// Look for .vizzly/server.json in current directory and parent directories
|
|
73
74
|
let currentDir = startDir || process.cwd();
|
|
74
|
-
|
|
75
|
+
let root = parse(currentDir).root;
|
|
75
76
|
while (currentDir !== root) {
|
|
76
|
-
|
|
77
|
+
let serverJsonPath = join(currentDir, '.vizzly', 'server.json');
|
|
77
78
|
if (exists(serverJsonPath)) {
|
|
78
79
|
try {
|
|
79
|
-
|
|
80
|
+
let serverInfo = JSON.parse(readFile(serverJsonPath, 'utf8'));
|
|
80
81
|
if (serverInfo.port) {
|
|
81
|
-
|
|
82
|
+
let url = `http://localhost:${serverInfo.port}`;
|
|
82
83
|
return url;
|
|
83
84
|
}
|
|
84
85
|
} catch {
|
|
@@ -191,14 +192,9 @@ function createSimpleClient(serverUrl) {
|
|
|
191
192
|
let isFilePath = typeof imageBuffer === 'string';
|
|
192
193
|
let image = isFilePath ? imageBuffer : imageBuffer.toString('base64');
|
|
193
194
|
let type = isFilePath ? 'file-path' : 'base64';
|
|
194
|
-
let
|
|
195
|
-
fullPage,
|
|
196
|
-
threshold,
|
|
197
|
-
properties: userProperties,
|
|
198
|
-
...rest
|
|
199
|
-
} = options;
|
|
195
|
+
let properties = createScreenshotProperties(options);
|
|
200
196
|
let httpStart = Date.now();
|
|
201
|
-
|
|
197
|
+
let {
|
|
202
198
|
status,
|
|
203
199
|
json
|
|
204
200
|
} = await httpPost(`${serverUrl}/screenshot`, {
|
|
@@ -206,11 +202,7 @@ function createSimpleClient(serverUrl) {
|
|
|
206
202
|
name,
|
|
207
203
|
image,
|
|
208
204
|
type,
|
|
209
|
-
properties
|
|
210
|
-
...rest,
|
|
211
|
-
...userProperties
|
|
212
|
-
},
|
|
213
|
-
fullPage: fullPage || false
|
|
205
|
+
properties
|
|
214
206
|
}, DEFAULT_TIMEOUT_MS);
|
|
215
207
|
let httpMs = Date.now() - httpStart;
|
|
216
208
|
if (shouldLogClient('debug')) {
|
|
@@ -293,10 +285,11 @@ function createSimpleClient(serverUrl) {
|
|
|
293
285
|
* @param {Buffer|string} imageBuffer - PNG image data as a Buffer, or a file path to an image
|
|
294
286
|
* @param {Object} [options] - Optional configuration
|
|
295
287
|
* @param {Record<string, any>} [options.properties] - Additional properties to attach to the screenshot
|
|
296
|
-
* @param {number} [options.threshold
|
|
297
|
-
* @param {
|
|
288
|
+
* @param {number} [options.threshold] - CIEDE2000 Delta E threshold for this screenshot
|
|
289
|
+
* @param {number} [options.minClusterSize] - Minimum changed cluster size for this screenshot
|
|
290
|
+
* @param {boolean} [options.fullPage] - Whether this is a full page screenshot
|
|
298
291
|
*
|
|
299
|
-
* @returns {Promise<
|
|
292
|
+
* @returns {Promise<Object|null>} Screenshot result from the server, or null when capture is skipped
|
|
300
293
|
*
|
|
301
294
|
* @example
|
|
302
295
|
* // Basic usage with Buffer
|
|
@@ -319,19 +312,17 @@ function createSimpleClient(serverUrl) {
|
|
|
319
312
|
* threshold: 5
|
|
320
313
|
* });
|
|
321
314
|
*
|
|
322
|
-
*
|
|
323
|
-
* @throws {VizzlyError} When file path is provided but file doesn't exist
|
|
324
|
-
* @throws {VizzlyError} When file cannot be read due to permissions or I/O errors
|
|
315
|
+
* Capture failures are logged and return null so test suites can continue.
|
|
325
316
|
*/
|
|
326
317
|
export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
|
|
327
318
|
if (isVizzlyDisabled()) {
|
|
328
|
-
return; // Silently skip when disabled
|
|
319
|
+
return null; // Silently skip when disabled
|
|
329
320
|
}
|
|
330
321
|
let client = getClient();
|
|
331
322
|
if (!client) {
|
|
332
323
|
// Silently disable - no server running, nothing to do
|
|
333
324
|
disableVizzly();
|
|
334
|
-
return;
|
|
325
|
+
return null;
|
|
335
326
|
}
|
|
336
327
|
|
|
337
328
|
// Pass through the original value (Buffer or file path)
|
|
@@ -342,7 +333,7 @@ export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
|
|
|
342
333
|
/**
|
|
343
334
|
* Wait for all queued screenshots to be processed
|
|
344
335
|
*
|
|
345
|
-
* @returns {Promise<
|
|
336
|
+
* @returns {Promise<Object|null>} Flush summary, or null if no server is connected
|
|
346
337
|
*
|
|
347
338
|
* @example
|
|
348
339
|
* afterAll(async () => {
|
|
@@ -350,10 +341,11 @@ export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
|
|
|
350
341
|
* });
|
|
351
342
|
*/
|
|
352
343
|
export async function vizzlyFlush() {
|
|
353
|
-
|
|
344
|
+
let client = getClient();
|
|
354
345
|
if (client) {
|
|
355
346
|
return client.flush();
|
|
356
347
|
}
|
|
348
|
+
return null;
|
|
357
349
|
}
|
|
358
350
|
|
|
359
351
|
/**
|
|
@@ -401,7 +393,7 @@ export function setEnabled(enabled) {
|
|
|
401
393
|
* @returns {Object} Client information
|
|
402
394
|
*/
|
|
403
395
|
export function getVizzlyInfo() {
|
|
404
|
-
|
|
396
|
+
let client = getClient();
|
|
405
397
|
return {
|
|
406
398
|
enabled: !isVizzlyDisabled(),
|
|
407
399
|
serverUrl: getServerUrl(),
|