@vizzly-testing/cli 0.29.6 → 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 +34 -6
- package/dist/api/client.js +6 -3
- package/dist/api/endpoints.js +59 -0
- package/dist/api/index.js +1 -1
- package/dist/auth/core.js +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/cli.js +206 -51
- 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 +870 -0
- 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 +18 -7
- 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 +64 -26
- package/dist/config/core.js +37 -36
- package/dist/context/local-workspace-provider.js +532 -0
- package/dist/context/provider-resolver.js +36 -0
- 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 +2 -1
- package/dist/reporter/reporter-bundle.iife.js +119 -119
- package/dist/reporter-ssr/ssr-entry.js +605 -509
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +226 -148
- 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/core/hotspot-coverage.js +1 -1
- 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 +1 -14
- 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 +47 -7
- 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/colors.js +46 -44
- 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 +22 -3
- 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/report-data.js +31 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/dist/utils/security.js +3 -2
- package/package.json +14 -11
- 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/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { baselinesCommand, validateBaselinesOptions } from './commands/baselines
|
|
|
6
6
|
import { buildsCommand, validateBuildsOptions } from './commands/builds.js';
|
|
7
7
|
import { comparisonsCommand, validateComparisonsOptions } from './commands/comparisons.js';
|
|
8
8
|
import { configCommand, validateConfigOptions } from './commands/config-cmd.js';
|
|
9
|
+
import { contextBuildCommand, contextComparisonCommand, contextReviewQueueCommand, contextScreenshotCommand, contextSimilarCommand, validateContextBuildOptions, validateContextComparisonOptions, validateContextReviewQueueOptions, validateContextScreenshotOptions, validateContextSimilarOptions } from './commands/context.js';
|
|
9
10
|
import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
|
|
10
11
|
import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
|
|
11
12
|
import { init } from './commands/init.js';
|
|
@@ -13,18 +14,20 @@ import { loginCommand, validateLoginOptions } from './commands/login.js';
|
|
|
13
14
|
import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
|
|
14
15
|
import { orgsCommand, validateOrgsOptions } from './commands/orgs.js';
|
|
15
16
|
import { previewCommand, validatePreviewOptions } from './commands/preview.js';
|
|
17
|
+
import { projectLinkCommand, validateProjectLinkOptions } from './commands/project.js';
|
|
16
18
|
import { projectsCommand, validateProjectsOptions } from './commands/projects.js';
|
|
17
19
|
import { approveCommand, commentCommand, rejectCommand, validateApproveOptions, validateCommentOptions, validateRejectOptions } from './commands/review.js';
|
|
18
20
|
import { runCommand, validateRunOptions } from './commands/run.js';
|
|
19
21
|
import { statusCommand, validateStatusOptions } from './commands/status.js';
|
|
20
22
|
import { tddCommand, validateTddOptions } from './commands/tdd.js';
|
|
21
|
-
import { runDaemonChild, tddListCommand, tddStartCommand, tddStatusCommand, tddStopCommand } from './commands/tdd-daemon.js';
|
|
23
|
+
import { runDaemonChild, tddListCommand, tddStartCommand, tddStatusCommand, tddStopCommand, validateTddStartOptions } from './commands/tdd-daemon.js';
|
|
22
24
|
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
23
25
|
import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
|
|
24
26
|
import { createPluginServices } from './plugin-api.js';
|
|
25
27
|
import { loadPlugins } from './plugin-loader.js';
|
|
26
28
|
import { createServices } from './services/index.js';
|
|
27
29
|
import { generateStaticReport, getReportFileUrl } from './services/static-report-generator.js';
|
|
30
|
+
import { withTimeout } from './utils/async-utils.js';
|
|
28
31
|
import { openBrowser } from './utils/browser.js';
|
|
29
32
|
import { colors } from './utils/colors.js';
|
|
30
33
|
import { loadConfig } from './utils/config-loader.js';
|
|
@@ -46,7 +49,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
46
49
|
// Cute grizzly bear mascot with square eyes (like the Vizzly logo!)
|
|
47
50
|
lines.push(c.brand.amber(' ʕ□ᴥ□ʔ'));
|
|
48
51
|
lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.dim(`v${version}`)}`);
|
|
49
|
-
lines.push(` ${c.gray('Visual
|
|
52
|
+
lines.push(` ${c.gray('Visual review for agents and teams')}`);
|
|
50
53
|
} else {
|
|
51
54
|
// Compact header for subcommands
|
|
52
55
|
lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.white(cmd.name())}`);
|
|
@@ -71,7 +74,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
71
74
|
key: 'core',
|
|
72
75
|
icon: '▸',
|
|
73
76
|
title: 'Core',
|
|
74
|
-
names: ['run', 'tdd', 'upload', 'status', 'finalize', 'preview', 'builds', 'comparisons']
|
|
77
|
+
names: ['run', 'tdd', 'upload', 'status', 'context', 'finalize', 'preview', 'builds', 'comparisons']
|
|
75
78
|
}, {
|
|
76
79
|
key: 'review',
|
|
77
80
|
icon: '▸',
|
|
@@ -170,7 +173,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
170
173
|
if (isRootCommand) {
|
|
171
174
|
lines.push(` ${c.brand.amber('▸')} ${c.bold('Quick Start')}`);
|
|
172
175
|
lines.push('');
|
|
173
|
-
lines.push(` ${c.dim('# Local visual
|
|
176
|
+
lines.push(` ${c.dim('# Local visual review')}`);
|
|
174
177
|
lines.push(` ${c.gray('$')} ${c.white('vizzly tdd start')}`);
|
|
175
178
|
lines.push('');
|
|
176
179
|
lines.push(` ${c.dim('# CI pipeline')}`);
|
|
@@ -202,40 +205,65 @@ const formatHelp = (cmd, helper) => {
|
|
|
202
205
|
lines.push('');
|
|
203
206
|
return lines.join('\n');
|
|
204
207
|
};
|
|
205
|
-
|
|
208
|
+
function extractGlobalOptionsFromArgv(argv, commandNames = null) {
|
|
209
|
+
let configPath = null;
|
|
210
|
+
let verboseMode = false;
|
|
211
|
+
let logLevelArg = null;
|
|
212
|
+
let jsonArg = null;
|
|
213
|
+
for (let i = 0; i < argv.length; i++) {
|
|
214
|
+
if ((argv[i] === '-c' || argv[i] === '--config') && argv[i + 1]) {
|
|
215
|
+
configPath = argv[i + 1];
|
|
216
|
+
}
|
|
217
|
+
if (argv[i] === '-v' || argv[i] === '--verbose') {
|
|
218
|
+
verboseMode = true;
|
|
219
|
+
}
|
|
220
|
+
if (argv[i] === '--log-level' && argv[i + 1]) {
|
|
221
|
+
logLevelArg = argv[i + 1];
|
|
222
|
+
}
|
|
223
|
+
if (argv[i] === '--json') {
|
|
224
|
+
let nextArg = argv[i + 1];
|
|
225
|
+
let nextArgIsCommand = commandNames?.has(nextArg);
|
|
226
|
+
if (nextArg && !nextArg.startsWith('-') && !nextArgIsCommand) {
|
|
227
|
+
jsonArg = nextArg;
|
|
228
|
+
} else {
|
|
229
|
+
jsonArg = true;
|
|
230
|
+
}
|
|
231
|
+
} else if (argv[i].startsWith('--json=')) {
|
|
232
|
+
jsonArg = argv[i].substring('--json='.length);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
configPath,
|
|
237
|
+
verboseMode,
|
|
238
|
+
logLevelArg,
|
|
239
|
+
jsonArg
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function normalizeJsonArgv(argv, commandNames) {
|
|
243
|
+
let normalizedArgv = [...argv];
|
|
244
|
+
for (let i = 0; i < normalizedArgv.length; i++) {
|
|
245
|
+
if (normalizedArgv[i] !== '--json') {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
let nextArg = normalizedArgv[i + 1];
|
|
249
|
+
if (nextArg && !nextArg.startsWith('-') && commandNames.has(nextArg)) {
|
|
250
|
+
normalizedArgv[i] = '--json=true';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return normalizedArgv;
|
|
254
|
+
}
|
|
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({
|
|
206
256
|
formatHelp
|
|
207
257
|
});
|
|
208
258
|
|
|
209
259
|
// Load plugins before defining commands
|
|
210
260
|
// We need to manually parse to get the config option early
|
|
211
|
-
let
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
configPath = process.argv[i + 1];
|
|
218
|
-
}
|
|
219
|
-
if (process.argv[i] === '-v' || process.argv[i] === '--verbose') {
|
|
220
|
-
verboseMode = true;
|
|
221
|
-
}
|
|
222
|
-
if (process.argv[i] === '--log-level' && process.argv[i + 1]) {
|
|
223
|
-
logLevelArg = process.argv[i + 1];
|
|
224
|
-
}
|
|
225
|
-
// Handle --json with optional field selection
|
|
226
|
-
// --json (no value) = true, --json=fields or --json fields = "fields"
|
|
227
|
-
if (process.argv[i] === '--json') {
|
|
228
|
-
let nextArg = process.argv[i + 1];
|
|
229
|
-
// If next arg exists and doesn't start with -, it's the fields value
|
|
230
|
-
if (nextArg && !nextArg.startsWith('-')) {
|
|
231
|
-
jsonArg = nextArg;
|
|
232
|
-
} else {
|
|
233
|
-
jsonArg = true;
|
|
234
|
-
}
|
|
235
|
-
} else if (process.argv[i].startsWith('--json=')) {
|
|
236
|
-
jsonArg = process.argv[i].substring('--json='.length);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
261
|
+
let {
|
|
262
|
+
configPath,
|
|
263
|
+
verboseMode,
|
|
264
|
+
logLevelArg,
|
|
265
|
+
jsonArg
|
|
266
|
+
} = extractGlobalOptionsFromArgv(process.argv);
|
|
239
267
|
|
|
240
268
|
// Configure output early
|
|
241
269
|
// Priority: --log-level > --verbose > VIZZLY_LOG_LEVEL env var > default ('info')
|
|
@@ -258,18 +286,15 @@ const pluginServices = createPluginServices(services);
|
|
|
258
286
|
let plugins = [];
|
|
259
287
|
try {
|
|
260
288
|
plugins = await loadPlugins(configPath, config);
|
|
261
|
-
for (
|
|
289
|
+
for (let plugin of plugins) {
|
|
262
290
|
try {
|
|
263
291
|
// Add timeout protection for plugin registration (5 seconds)
|
|
264
|
-
|
|
292
|
+
let registerPromise = plugin.register(program, {
|
|
265
293
|
config,
|
|
266
294
|
services: pluginServices,
|
|
267
|
-
output
|
|
268
|
-
// Backwards compatibility alias for plugins using old API
|
|
269
|
-
logger: output
|
|
295
|
+
output
|
|
270
296
|
});
|
|
271
|
-
|
|
272
|
-
await Promise.race([registerPromise, timeoutPromise]);
|
|
297
|
+
await withTimeout(registerPromise, 5000, 'Plugin registration timeout (5s)');
|
|
273
298
|
output.debug(`Registered plugin: ${plugin.name}`);
|
|
274
299
|
} catch (error) {
|
|
275
300
|
output.warn(`Failed to register plugin ${plugin.name}: ${error.message}`);
|
|
@@ -286,7 +311,7 @@ program.command('init').description('Initialize Vizzly in your project').option(
|
|
|
286
311
|
plugins
|
|
287
312
|
});
|
|
288
313
|
});
|
|
289
|
-
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) => {
|
|
290
315
|
const globalOptions = program.opts();
|
|
291
316
|
|
|
292
317
|
// Validate options
|
|
@@ -301,11 +326,11 @@ program.command('upload').description('Upload screenshots to Vizzly').argument('
|
|
|
301
326
|
await uploadCommand(path, options, globalOptions);
|
|
302
327
|
});
|
|
303
328
|
|
|
304
|
-
// TDD command with subcommands -
|
|
305
|
-
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');
|
|
306
331
|
|
|
307
332
|
// TDD Start - Background server
|
|
308
|
-
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 => {
|
|
309
334
|
const globalOptions = program.opts();
|
|
310
335
|
|
|
311
336
|
// If this is a daemon child process, run the server directly
|
|
@@ -313,6 +338,14 @@ tddCmd.command('start').description('Start background TDD server with dashboard'
|
|
|
313
338
|
await runDaemonChild(options, globalOptions);
|
|
314
339
|
return;
|
|
315
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
|
+
}
|
|
316
349
|
await tddStartCommand(options, globalOptions);
|
|
317
350
|
});
|
|
318
351
|
|
|
@@ -335,7 +368,7 @@ tddCmd.command('list').description('List all running TDD servers').action(async
|
|
|
335
368
|
});
|
|
336
369
|
|
|
337
370
|
// TDD Run - One-off test run with ephemeral server (generates static report)
|
|
338
|
-
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) => {
|
|
339
372
|
const globalOptions = program.opts();
|
|
340
373
|
|
|
341
374
|
// Validate options
|
|
@@ -396,7 +429,7 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
|
|
|
396
429
|
await handleCleanup();
|
|
397
430
|
process.exit(result?.exitCode || 0);
|
|
398
431
|
});
|
|
399
|
-
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) => {
|
|
400
433
|
const globalOptions = program.opts();
|
|
401
434
|
|
|
402
435
|
// Validate options
|
|
@@ -432,7 +465,7 @@ program.command('status').description('Check the status of a build').argument('<
|
|
|
432
465
|
}
|
|
433
466
|
await statusCommand(buildId, options, globalOptions);
|
|
434
467
|
});
|
|
435
|
-
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', `
|
|
436
469
|
Examples:
|
|
437
470
|
$ vizzly builds # List recent builds
|
|
438
471
|
$ vizzly builds --branch main # Filter by branch
|
|
@@ -456,7 +489,7 @@ Examples:
|
|
|
456
489
|
}
|
|
457
490
|
await buildsCommand(options, globalOptions);
|
|
458
491
|
});
|
|
459
|
-
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', `
|
|
460
493
|
Examples:
|
|
461
494
|
$ vizzly comparisons -b abc123 # List comparisons for a build
|
|
462
495
|
$ vizzly comparisons --id def456 # Get specific comparison by ID
|
|
@@ -479,6 +512,98 @@ Examples:
|
|
|
479
512
|
}
|
|
480
513
|
await comparisonsCommand(options, globalOptions);
|
|
481
514
|
});
|
|
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', `
|
|
517
|
+
Examples:
|
|
518
|
+
$ vizzly context build abc123
|
|
519
|
+
$ vizzly context build current --source local
|
|
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
|
|
524
|
+
`).action(async (buildId, options) => {
|
|
525
|
+
const globalOptions = program.opts();
|
|
526
|
+
const validationErrors = validateContextBuildOptions(options);
|
|
527
|
+
if (validationErrors.length > 0) {
|
|
528
|
+
output.error('Validation errors:');
|
|
529
|
+
for (let error of validationErrors) {
|
|
530
|
+
output.printErr(` - ${error}`);
|
|
531
|
+
}
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
await contextBuildCommand(buildId, options, globalOptions);
|
|
535
|
+
});
|
|
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', `
|
|
537
|
+
Examples:
|
|
538
|
+
$ vizzly context comparison def456
|
|
539
|
+
$ vizzly context comparison def456 --source local
|
|
540
|
+
$ vizzly context comparison def456 --similar-limit 5 --recent-limit 5
|
|
541
|
+
$ vizzly context comparison def456 --json
|
|
542
|
+
`).action(async (comparisonId, options) => {
|
|
543
|
+
const globalOptions = program.opts();
|
|
544
|
+
const validationErrors = validateContextComparisonOptions(options);
|
|
545
|
+
if (validationErrors.length > 0) {
|
|
546
|
+
output.error('Validation errors:');
|
|
547
|
+
for (let error of validationErrors) {
|
|
548
|
+
output.printErr(` - ${error}`);
|
|
549
|
+
}
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
await contextComparisonCommand(comparisonId, options, globalOptions);
|
|
553
|
+
});
|
|
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', `
|
|
555
|
+
Examples:
|
|
556
|
+
$ vizzly context screenshot Dashboard
|
|
557
|
+
$ vizzly context screenshot Dashboard --source local
|
|
558
|
+
$ vizzly context screenshot Dashboard --project storybook --org acme
|
|
559
|
+
$ vizzly context screenshot Dashboard --json
|
|
560
|
+
`).action(async (name, options) => {
|
|
561
|
+
const globalOptions = program.opts();
|
|
562
|
+
const validationErrors = validateContextScreenshotOptions(options);
|
|
563
|
+
if (validationErrors.length > 0) {
|
|
564
|
+
output.error('Validation errors:');
|
|
565
|
+
for (let error of validationErrors) {
|
|
566
|
+
output.printErr(` - ${error}`);
|
|
567
|
+
}
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
await contextScreenshotCommand(name, options, globalOptions);
|
|
571
|
+
});
|
|
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', `
|
|
573
|
+
Examples:
|
|
574
|
+
$ vizzly context similar fp-dashboard
|
|
575
|
+
$ vizzly context similar fp-dashboard --project storybook --org acme
|
|
576
|
+
$ vizzly context similar fp-dashboard --json
|
|
577
|
+
`).action(async (fingerprintHash, options) => {
|
|
578
|
+
const globalOptions = program.opts();
|
|
579
|
+
const validationErrors = validateContextSimilarOptions(options);
|
|
580
|
+
if (validationErrors.length > 0) {
|
|
581
|
+
output.error('Validation errors:');
|
|
582
|
+
for (let error of validationErrors) {
|
|
583
|
+
output.printErr(` - ${error}`);
|
|
584
|
+
}
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
await contextSimilarCommand(fingerprintHash, options, globalOptions);
|
|
588
|
+
});
|
|
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', `
|
|
590
|
+
Examples:
|
|
591
|
+
$ vizzly context review-queue --project storybook --org acme
|
|
592
|
+
$ vizzly context review-queue --source local
|
|
593
|
+
$ vizzly context review-queue --limit 10
|
|
594
|
+
$ vizzly context review-queue --json
|
|
595
|
+
`).action(async options => {
|
|
596
|
+
const globalOptions = program.opts();
|
|
597
|
+
const validationErrors = validateContextReviewQueueOptions(options);
|
|
598
|
+
if (validationErrors.length > 0) {
|
|
599
|
+
output.error('Validation errors:');
|
|
600
|
+
for (let error of validationErrors) {
|
|
601
|
+
output.printErr(` - ${error}`);
|
|
602
|
+
}
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
await contextReviewQueueCommand(options, globalOptions);
|
|
606
|
+
});
|
|
482
607
|
program.command('config').description('Display current configuration').argument('[key]', 'Specific config key to get (dot notation, e.g., comparison.threshold)').action(async (key, options) => {
|
|
483
608
|
const globalOptions = program.opts();
|
|
484
609
|
|
|
@@ -624,7 +749,7 @@ or the single organization for a project token.
|
|
|
624
749
|
}
|
|
625
750
|
await orgsCommand(options, globalOptions);
|
|
626
751
|
});
|
|
627
|
-
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', `
|
|
628
753
|
Examples:
|
|
629
754
|
$ vizzly projects # List all projects
|
|
630
755
|
$ vizzly projects --org my-company # Filter by organization
|
|
@@ -646,6 +771,26 @@ Workflow:
|
|
|
646
771
|
}
|
|
647
772
|
await projectsCommand(options, globalOptions);
|
|
648
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
|
+
});
|
|
649
794
|
program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
|
|
650
795
|
const globalOptions = program.opts();
|
|
651
796
|
|
|
@@ -660,7 +805,7 @@ program.command('finalize').description('Finalize a parallel build after all sha
|
|
|
660
805
|
}
|
|
661
806
|
await finalizeCommand(parallelId, options, globalOptions);
|
|
662
807
|
});
|
|
663
|
-
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) => {
|
|
664
809
|
const globalOptions = program.opts();
|
|
665
810
|
|
|
666
811
|
// Show helpful error if path is missing
|
|
@@ -747,4 +892,14 @@ program.command('whoami').description('Show current authentication status and us
|
|
|
747
892
|
// Save user's PATH for menubar app (non-blocking, runs in background)
|
|
748
893
|
// This auto-configures the menubar app so it can find npx/node
|
|
749
894
|
saveUserPath().catch(() => {});
|
|
750
|
-
program.
|
|
895
|
+
let commandNames = new Set(program.commands.map(command => command.name()));
|
|
896
|
+
let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
|
|
897
|
+
let normalizedGlobals = extractGlobalOptionsFromArgv(normalizedArgv, commandNames);
|
|
898
|
+
output.configure({
|
|
899
|
+
logLevel: normalizedGlobals.logLevelArg,
|
|
900
|
+
verbose: normalizedGlobals.verboseMode,
|
|
901
|
+
color: colorOverride,
|
|
902
|
+
json: normalizedGlobals.jsonArg,
|
|
903
|
+
resetTimer: false
|
|
904
|
+
});
|
|
905
|
+
program.parse(normalizedArgv);
|
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(),
|