@vizzly-testing/cli 0.29.5 → 0.30.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 CHANGED
@@ -58,6 +58,33 @@ export VIZZLY_TOKEN=your-project-token
58
58
  vizzly run "npm test" --wait
59
59
  ```
60
60
 
61
+ ### Visual Context For Agents
62
+
63
+ Use `vizzly context` when you want Vizzly to act more like visual context than a test runner.
64
+
65
+ This is especially useful for LLM agents, automation, and quick debugging loops. Instead of
66
+ making a bunch of narrow API calls, you can ask for one build, comparison, screenshot, or review
67
+ queue bundle and get the evidence in one place.
68
+
69
+ ```bash
70
+ # Cloud context for a build or comparison
71
+ vizzly context build abc123
72
+ vizzly context comparison def456 --json
73
+
74
+ # Local workspace context from .vizzly/
75
+ vizzly context build current --source local
76
+ vizzly context screenshot build-detail-screenshots --source local --json
77
+ ```
78
+
79
+ `--json` is the main automation path. Human-readable output is there for quick terminal use, but
80
+ JSON is what you want for scripts, agents, and prompt assembly.
81
+
82
+ Local context is read-only and file-backed. It reads your existing `.vizzly` workspace state from
83
+ TDD runs, including screenshots, diffs, and any saved hotspot or region metadata.
84
+
85
+ Cloud context is also read-only right now. That is intentional. It keeps the trust model simple:
86
+ Vizzly helps you see and inspect visual changes, while people still decide what gets approved.
87
+
61
88
  ## Capture Screenshots
62
89
 
63
90
  Add screenshots to your existing tests:
@@ -116,6 +143,7 @@ export default {
116
143
  | `vizzly tdd start` | Start local TDD server with dashboard |
117
144
  | `vizzly tdd run "cmd"` | Run tests once, generate static report |
118
145
  | `vizzly run "cmd"` | Run tests with cloud integration |
146
+ | `vizzly context ...` | Fetch visual context bundles for builds, comparisons, and screenshots |
119
147
  | `vizzly upload <dir>` | Upload existing screenshots |
120
148
  | `vizzly login` | Authenticate via browser |
121
149
  | `vizzly doctor` | Validate local setup |
@@ -210,6 +210,65 @@ export async function getComparison(client, comparisonId) {
210
210
  return response.comparison;
211
211
  }
212
212
 
213
+ /**
214
+ * Get build context bundle for agent and reviewer workflows
215
+ * @param {Object} client - API client
216
+ * @param {string} buildId - Build ID
217
+ * @returns {Promise<Object>} Build context bundle
218
+ */
219
+ export async function getBuildContext(client, buildId) {
220
+ return client.request(`/api/sdk/context/builds/${buildId}`);
221
+ }
222
+
223
+ /**
224
+ * Get comparison context bundle with similarity and history
225
+ * @param {Object} client - API client
226
+ * @param {string} comparisonId - Comparison ID
227
+ * @param {Object} options - Optional query params
228
+ * @returns {Promise<Object>} Comparison context bundle
229
+ */
230
+ export async function getComparisonContext(client, comparisonId, options = {}) {
231
+ let endpoint = buildEndpointWithParams(`/api/sdk/context/comparisons/${comparisonId}`, options);
232
+ return client.request(endpoint);
233
+ }
234
+
235
+ /**
236
+ * Get screenshot context bundle for a screenshot name
237
+ * @param {Object} client - API client
238
+ * @param {string} screenshotName - Screenshot name
239
+ * @param {Object} options - Optional query params
240
+ * @returns {Promise<Object>} Screenshot context bundle
241
+ */
242
+ export async function getScreenshotContext(client, screenshotName, options = {}) {
243
+ let encodedName = encodeURIComponent(screenshotName);
244
+ let endpoint = buildEndpointWithParams(`/api/sdk/context/screenshots/${encodedName}`, options);
245
+ return client.request(endpoint);
246
+ }
247
+
248
+ /**
249
+ * Get project-scoped similar comparisons for a fingerprint hash
250
+ * @param {Object} client - API client
251
+ * @param {string} fingerprintHash - Honeydiff fingerprint hash
252
+ * @param {Object} options - Optional query params
253
+ * @returns {Promise<Object>} Fingerprint context bundle
254
+ */
255
+ export async function getSimilarFingerprintContext(client, fingerprintHash, options = {}) {
256
+ let encodedFingerprint = encodeURIComponent(fingerprintHash);
257
+ let endpoint = buildEndpointWithParams(`/api/sdk/context/fingerprints/${encodedFingerprint}/similar`, options);
258
+ return client.request(endpoint);
259
+ }
260
+
261
+ /**
262
+ * Get pending review queue context for a project
263
+ * @param {Object} client - API client
264
+ * @param {Object} options - Optional query params
265
+ * @returns {Promise<Object>} Review queue context bundle
266
+ */
267
+ export async function getReviewQueueContext(client, options = {}) {
268
+ let endpoint = buildEndpointWithParams('/api/sdk/context/review-queue', options);
269
+ return client.request(endpoint);
270
+ }
271
+
213
272
  /**
214
273
  * Search for comparisons by name
215
274
  * @param {Object} client - API client
package/dist/api/index.js CHANGED
@@ -16,4 +16,4 @@ export { createApiClient, DEFAULT_API_URL } from './client.js';
16
16
  export { buildApiUrl, buildAuthHeader, buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildRequestHeaders, buildScreenshotCheckObject, buildScreenshotPayload, buildShaCheckPayload, buildUserAgent, computeSha256, extractErrorBody, findScreenshotBySha, isAuthError, isRateLimited, parseApiError, partitionByShaExistence, shaExists, shouldRetryWithRefresh } from './core.js';
17
17
 
18
18
  // Endpoint functions
19
- export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBatchHotspots, getBuild, getBuilds, getComparison, getPreviewInfo, getScreenshotHotspots, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';
19
+ export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBatchHotspots, getBuild, getBuildContext, getBuilds, getComparison, getComparisonContext, getPreviewInfo, getReviewQueueContext, getScreenshotContext, getScreenshotHotspots, getSimilarFingerprintContext, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';
package/dist/auth/core.js CHANGED
@@ -162,7 +162,7 @@ export function buildTokenData(response, existingUser = null) {
162
162
  * @returns {{ valid: boolean, error: Error|null }}
163
163
  */
164
164
  export function validateTokens(auth, requiredField = 'accessToken') {
165
- if (!auth || !auth[requiredField]) {
165
+ if (!auth?.[requiredField]) {
166
166
  let message = requiredField === 'refreshToken' ? 'No refresh token found. Please run "vizzly login" first.' : 'No authentication token found. Please run "vizzly login" first.';
167
167
  return {
168
168
  valid: false,
@@ -9,13 +9,14 @@
9
9
 
10
10
  // Re-export token store utilities for convenience
11
11
  import { clearAuthTokens, getAuthTokens, saveAuthTokens } from '../utils/global-config.js';
12
- export { clearAuthTokens, getAuthTokens, saveAuthTokens };
12
+
13
13
  // HTTP client factory
14
14
  export { createAuthClient } from './client.js';
15
15
  // Core pure functions
16
16
  export { buildAuthHeader, buildAuthUserAgent, buildDevicePollPayload, buildLogoutPayload, buildRefreshPayload, buildRequestHeaders, buildTokenData, parseAuthError, parseAuthenticatedError, validateTokens } from './core.js';
17
17
  // Auth operations (take dependencies as parameters)
18
18
  export { completeDeviceFlow, initiateDeviceFlow, isAuthenticated, logout, pollDeviceAuthorization, refresh, whoami } from './operations.js';
19
+ export { clearAuthTokens, getAuthTokens, saveAuthTokens };
19
20
 
20
21
  /**
21
22
  * Create a token store adapter from global-config functions
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';
@@ -71,7 +72,7 @@ const formatHelp = (cmd, helper) => {
71
72
  key: 'core',
72
73
  icon: '▸',
73
74
  title: 'Core',
74
- names: ['run', 'tdd', 'upload', 'status', 'finalize', 'preview', 'builds', 'comparisons']
75
+ names: ['run', 'tdd', 'upload', 'status', 'context', 'finalize', 'preview', 'builds', 'comparisons']
75
76
  }, {
76
77
  key: 'review',
77
78
  icon: '▸',
@@ -202,40 +203,65 @@ const formatHelp = (cmd, helper) => {
202
203
  lines.push('');
203
204
  return lines.join('\n');
204
205
  };
206
+ function extractGlobalOptionsFromArgv(argv, commandNames = null) {
207
+ let configPath = null;
208
+ let verboseMode = false;
209
+ let logLevelArg = null;
210
+ let jsonArg = null;
211
+ for (let i = 0; i < argv.length; i++) {
212
+ if ((argv[i] === '-c' || argv[i] === '--config') && argv[i + 1]) {
213
+ configPath = argv[i + 1];
214
+ }
215
+ if (argv[i] === '-v' || argv[i] === '--verbose') {
216
+ verboseMode = true;
217
+ }
218
+ if (argv[i] === '--log-level' && argv[i + 1]) {
219
+ logLevelArg = argv[i + 1];
220
+ }
221
+ if (argv[i] === '--json') {
222
+ let nextArg = argv[i + 1];
223
+ let nextArgIsCommand = commandNames?.has(nextArg);
224
+ if (nextArg && !nextArg.startsWith('-') && !nextArgIsCommand) {
225
+ jsonArg = nextArg;
226
+ } else {
227
+ jsonArg = true;
228
+ }
229
+ } else if (argv[i].startsWith('--json=')) {
230
+ jsonArg = argv[i].substring('--json='.length);
231
+ }
232
+ }
233
+ return {
234
+ configPath,
235
+ verboseMode,
236
+ logLevelArg,
237
+ jsonArg
238
+ };
239
+ }
240
+ function normalizeJsonArgv(argv, commandNames) {
241
+ let normalizedArgv = [...argv];
242
+ for (let i = 0; i < normalizedArgv.length; i++) {
243
+ if (normalizedArgv[i] !== '--json') {
244
+ continue;
245
+ }
246
+ let nextArg = normalizedArgv[i + 1];
247
+ if (nextArg && !nextArg.startsWith('-') && commandNames.has(nextArg)) {
248
+ normalizedArgv[i] = '--json=true';
249
+ }
250
+ }
251
+ return normalizedArgv;
252
+ }
205
253
  program.name('vizzly').description('Vizzly CLI for visual regression testing').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
254
  formatHelp
207
255
  });
208
256
 
209
257
  // Load plugins before defining commands
210
258
  // We need to manually parse to get the config option early
211
- let configPath = null;
212
- let verboseMode = false;
213
- let logLevelArg = null;
214
- let jsonArg = null;
215
- for (let i = 0; i < process.argv.length; i++) {
216
- if ((process.argv[i] === '-c' || process.argv[i] === '--config') && process.argv[i + 1]) {
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
- }
259
+ let {
260
+ configPath,
261
+ verboseMode,
262
+ logLevelArg,
263
+ jsonArg
264
+ } = extractGlobalOptionsFromArgv(process.argv);
239
265
 
240
266
  // Configure output early
241
267
  // Priority: --log-level > --verbose > VIZZLY_LOG_LEVEL env var > default ('info')
@@ -479,6 +505,96 @@ Examples:
479
505
  }
480
506
  await comparisonsCommand(options, globalOptions);
481
507
  });
508
+ let contextCmd = program.command('context').description('Fetch visual context bundles for agents and reviewers');
509
+ contextCmd.command('build').description('Fetch a build context bundle').argument('<build-id>', 'Build ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').addHelpText('after', `
510
+ Examples:
511
+ $ vizzly context build abc123
512
+ $ vizzly context build current --source local
513
+ $ vizzly context build abc123 --json
514
+ $ vizzly context build abc123 --json build.id,summary.comparisons
515
+ `).action(async (buildId, options) => {
516
+ const globalOptions = program.opts();
517
+ const validationErrors = validateContextBuildOptions(options);
518
+ if (validationErrors.length > 0) {
519
+ output.error('Validation errors:');
520
+ for (let error of validationErrors) {
521
+ output.printErr(` - ${error}`);
522
+ }
523
+ process.exit(1);
524
+ }
525
+ await contextBuildCommand(buildId, options, globalOptions);
526
+ });
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)', val => parseInt(val, 10)).option('--recent-limit <n>', 'Maximum recent same-name comparisons to return (1-50)', val => parseInt(val, 10)).option('--window-size <n>', 'Historical hotspot analysis window size (1-50)', val => parseInt(val, 10)).addHelpText('after', `
528
+ Examples:
529
+ $ vizzly context comparison def456
530
+ $ vizzly context comparison def456 --source local
531
+ $ vizzly context comparison def456 --similar-limit 5 --recent-limit 5
532
+ $ vizzly context comparison def456 --json
533
+ `).action(async (comparisonId, options) => {
534
+ const globalOptions = program.opts();
535
+ const validationErrors = validateContextComparisonOptions(options);
536
+ if (validationErrors.length > 0) {
537
+ output.error('Validation errors:');
538
+ for (let error of validationErrors) {
539
+ output.printErr(` - ${error}`);
540
+ }
541
+ process.exit(1);
542
+ }
543
+ await contextComparisonCommand(comparisonId, options, globalOptions);
544
+ });
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)', val => parseInt(val, 10)).option('--window-size <n>', 'Historical hotspot analysis window size (1-50)', val => parseInt(val, 10)).addHelpText('after', `
546
+ Examples:
547
+ $ vizzly context screenshot Dashboard
548
+ $ vizzly context screenshot Dashboard --source local
549
+ $ vizzly context screenshot Dashboard --project storybook --org acme
550
+ $ vizzly context screenshot Dashboard --json
551
+ `).action(async (name, options) => {
552
+ const globalOptions = program.opts();
553
+ const validationErrors = validateContextScreenshotOptions(options);
554
+ if (validationErrors.length > 0) {
555
+ output.error('Validation errors:');
556
+ for (let error of validationErrors) {
557
+ output.printErr(` - ${error}`);
558
+ }
559
+ process.exit(1);
560
+ }
561
+ await contextScreenshotCommand(name, options, globalOptions);
562
+ });
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)', val => parseInt(val, 10)).addHelpText('after', `
564
+ Examples:
565
+ $ vizzly context similar fp-dashboard
566
+ $ vizzly context similar fp-dashboard --project storybook --org acme
567
+ $ vizzly context similar fp-dashboard --json
568
+ `).action(async (fingerprintHash, options) => {
569
+ const globalOptions = program.opts();
570
+ const validationErrors = validateContextSimilarOptions(options);
571
+ if (validationErrors.length > 0) {
572
+ output.error('Validation errors:');
573
+ for (let error of validationErrors) {
574
+ output.printErr(` - ${error}`);
575
+ }
576
+ process.exit(1);
577
+ }
578
+ await contextSimilarCommand(fingerprintHash, options, globalOptions);
579
+ });
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)', val => parseInt(val, 10)).option('--offset <n>', 'Skip first N comparisons', val => parseInt(val, 10)).addHelpText('after', `
581
+ Examples:
582
+ $ vizzly context review-queue --project storybook --org acme
583
+ $ vizzly context review-queue --source local
584
+ $ vizzly context review-queue --limit 10
585
+ $ vizzly context review-queue --json
586
+ `).action(async options => {
587
+ const globalOptions = program.opts();
588
+ const validationErrors = validateContextReviewQueueOptions(options);
589
+ if (validationErrors.length > 0) {
590
+ output.error('Validation errors:');
591
+ for (let error of validationErrors) {
592
+ output.printErr(` - ${error}`);
593
+ }
594
+ process.exit(1);
595
+ }
596
+ await contextReviewQueueCommand(options, globalOptions);
597
+ });
482
598
  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
599
  const globalOptions = program.opts();
484
600
 
@@ -747,4 +863,14 @@ program.command('whoami').description('Show current authentication status and us
747
863
  // Save user's PATH for menubar app (non-blocking, runs in background)
748
864
  // This auto-configures the menubar app so it can find npx/node
749
865
  saveUserPath().catch(() => {});
750
- program.parse();
866
+ let commandNames = new Set(program.commands.map(command => command.name()));
867
+ let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
868
+ let normalizedGlobals = extractGlobalOptionsFromArgv(normalizedArgv, commandNames);
869
+ output.configure({
870
+ logLevel: normalizedGlobals.logLevelArg,
871
+ verbose: normalizedGlobals.verboseMode,
872
+ color: colorOverride,
873
+ json: normalizedGlobals.jsonArg,
874
+ resetTimer: false
875
+ });
876
+ program.parse(normalizedArgv);