@vizzly-testing/cli 0.35.2 → 0.35.3-beta.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
@@ -100,8 +100,8 @@ evidence in one place.
100
100
 
101
101
  ```bash
102
102
  # Cloud context for a build or comparison
103
- vizzly context build abc123
104
- vizzly context comparison def456 --json
103
+ vizzly context build abc123 --source cloud
104
+ vizzly context comparison def456 --source cloud --json
105
105
 
106
106
  # Local workspace context from .vizzly/
107
107
  vizzly context build current --source local
@@ -30,6 +30,20 @@ export async function getBuild(client, buildId, include = null) {
30
30
  return client.request(endpoint);
31
31
  }
32
32
 
33
+ /**
34
+ * Get the server-owned build processing and review status.
35
+ *
36
+ * This endpoint keeps processing, comparison, and review counts separate so
37
+ * callers do not have to reconstruct lifecycle state from legacy build fields.
38
+ *
39
+ * @param {Object} client - API client
40
+ * @param {string} buildId - Build ID
41
+ * @returns {Promise<Object>} Canonical build status bundle
42
+ */
43
+ export async function getBuildStatus(client, buildId) {
44
+ return client.request(`/api/sdk/builds/${buildId}/status`);
45
+ }
46
+
33
47
  /**
34
48
  * Get builds for a project
35
49
  * @param {Object} client - API client
@@ -234,13 +248,20 @@ export async function getComparison(client, comparisonId) {
234
248
  }
235
249
 
236
250
  /**
237
- * Get build context bundle for agent and reviewer workflows
251
+ * Get a build context bundle at the detail level the caller can consume.
252
+ *
253
+ * Passing detail selection to the API keeps compact callers from downloading
254
+ * raw Honeydiff geometry by default while preserving the unfiltered endpoint
255
+ * for raw and full context consumers.
256
+ *
238
257
  * @param {Object} client - API client
239
258
  * @param {string} buildId - Build ID
259
+ * @param {Object} options - Optional query params
240
260
  * @returns {Promise<Object>} Build context bundle
241
261
  */
242
- export async function getBuildContext(client, buildId) {
243
- return client.request(`/api/sdk/context/builds/${buildId}`);
262
+ export async function getBuildContext(client, buildId, options = {}) {
263
+ let endpoint = buildEndpointWithParams(`/api/sdk/context/builds/${buildId}`, options);
264
+ return client.request(endpoint);
244
265
  }
245
266
 
246
267
  /**
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, buildScreenshotResolvePayload, 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, getBuildContext, getBuilds, getComparison, getComparisonContext, getPreviewInfo, getReviewQueueContext, getScreenshotContext, getScreenshotHotspots, getSimilarFingerprintContext, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';
19
+ export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBatchHotspots, getBuild, getBuildContext, getBuildStatus, getBuilds, getComparison, getComparisonContext, getPreviewInfo, getReviewQueueContext, getScreenshotContext, getScreenshotHotspots, getSimilarFingerprintContext, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';
package/dist/cli.js CHANGED
@@ -653,12 +653,12 @@ Examples:
653
653
  let contextCmd = program.command('context').description('Fetch build and diff context').showHelpAfterError('(Run vizzly context --help for available commands)').showSuggestionAfterError();
654
654
  contextCmd.command('build').description('Fetch build context').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', `
655
655
  Examples:
656
- $ vizzly context build abc123
656
+ $ vizzly context build abc123 --source cloud
657
657
  $ vizzly context build current --source local
658
658
  $ vizzly context build current --source local --agent
659
- $ vizzly context build abc123 --agent --json
660
- $ vizzly context build abc123 --agent --json --include diffs,comments
661
- $ vizzly context build abc123 --agent --json --full
659
+ $ vizzly context build abc123 --source cloud --agent --json
660
+ $ vizzly context build abc123 --source cloud --agent --json --include diffs,comments
661
+ $ vizzly context build abc123 --source cloud --agent --json --full
662
662
  `).action(async (buildId, options) => {
663
663
  let globalOptions = getGlobalOptions();
664
664
  const validationErrors = validateContextBuildOptions(options);
@@ -669,10 +669,10 @@ Examples:
669
669
  });
670
670
  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', `
671
671
  Examples:
672
- $ vizzly context comparison def456
672
+ $ vizzly context comparison def456 --source cloud
673
673
  $ vizzly context comparison def456 --source local
674
- $ vizzly context comparison def456 --similar-limit 5 --recent-limit 5
675
- $ vizzly context comparison def456 --json
674
+ $ vizzly context comparison def456 --source cloud --similar-limit 5 --recent-limit 5
675
+ $ vizzly context comparison def456 --source cloud --json
676
676
  `).action(async (comparisonId, options) => {
677
677
  let globalOptions = getGlobalOptions();
678
678
  const validationErrors = validateContextComparisonOptions(options);
@@ -683,10 +683,10 @@ Examples:
683
683
  });
684
684
  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', `
685
685
  Examples:
686
- $ vizzly context screenshot Dashboard
686
+ $ vizzly context screenshot Dashboard --source cloud
687
687
  $ vizzly context screenshot Dashboard --source local
688
- $ vizzly context screenshot Dashboard --project storybook --org acme
689
- $ vizzly context screenshot Dashboard --json
688
+ $ vizzly context screenshot Dashboard --source cloud --project storybook --org acme
689
+ $ vizzly context screenshot Dashboard --source cloud --json
690
690
  `).action(async (name, options) => {
691
691
  let globalOptions = getGlobalOptions();
692
692
  const validationErrors = validateContextScreenshotOptions(options);
@@ -697,9 +697,9 @@ Examples:
697
697
  });
698
698
  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', `
699
699
  Examples:
700
- $ vizzly context similar fp-dashboard
701
- $ vizzly context similar fp-dashboard --project storybook --org acme
702
- $ vizzly context similar fp-dashboard --json
700
+ $ vizzly context similar fp-dashboard --source cloud
701
+ $ vizzly context similar fp-dashboard --source cloud --project storybook --org acme
702
+ $ vizzly context similar fp-dashboard --source cloud --json
703
703
  `).action(async (fingerprintHash, options) => {
704
704
  let globalOptions = getGlobalOptions();
705
705
  const validationErrors = validateContextSimilarOptions(options);
@@ -710,10 +710,10 @@ Examples:
710
710
  });
711
711
  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', `
712
712
  Examples:
713
- $ vizzly context review-queue --project storybook --org acme
713
+ $ vizzly context review-queue --source cloud --project storybook --org acme
714
714
  $ vizzly context review-queue --source local
715
- $ vizzly context review-queue --limit 10
716
- $ vizzly context review-queue --json
715
+ $ vizzly context review-queue --source cloud --limit 10
716
+ $ vizzly context review-queue --source cloud --json
717
717
  `).action(async options => {
718
718
  let globalOptions = getGlobalOptions();
719
719
  const validationErrors = validateContextReviewQueueOptions(options);
@@ -6,6 +6,7 @@ import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild,
6
6
  import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
7
  import * as defaultOutput from '../utils/output.js';
8
8
  import { createWildcardMatcher } from '../utils/patterns.js';
9
+ import { getComparisonBrowser, getComparisonName, getComparisonResult, getComparisonViewport, getVisualReviewState } from '../utils/visual-context-normalizers.js';
9
10
 
10
11
  /**
11
12
  * Comparisons command - query comparisons with various filters
@@ -73,13 +74,13 @@ export async function comparisonsCommand(options = {}, globalOptions = {}, deps
73
74
 
74
75
  // Apply status filter if provided
75
76
  if (options.status) {
76
- comparisons = comparisons.filter(c => c.status === options.status);
77
+ comparisons = comparisons.filter(comparison => getComparisonResult(comparison) === options.status);
77
78
  }
78
79
 
79
80
  // Apply name filter if provided
80
81
  if (options.name) {
81
82
  let matchesName = createWildcardMatcher(options.name);
82
- comparisons = comparisons.filter(c => matchesName(c.name));
83
+ comparisons = comparisons.filter(comparison => matchesName(getComparisonName(comparison)));
83
84
  }
84
85
  if (globalOptions.json) {
85
86
  output.data({
@@ -88,9 +89,9 @@ export async function comparisonsCommand(options = {}, globalOptions = {}, deps
88
89
  comparisons: comparisons.map(formatComparisonForJson),
89
90
  summary: {
90
91
  total: comparisons.length,
91
- passed: comparisons.filter(c => c.status === 'identical').length,
92
- failed: comparisons.filter(c => c.status === 'changed').length,
93
- new: comparisons.filter(c => c.status === 'new').length
92
+ passed: comparisons.filter(comparison => getComparisonResult(comparison) === 'identical').length,
93
+ failed: comparisons.filter(comparison => getComparisonResult(comparison) === 'changed').length,
94
+ new: comparisons.filter(comparison => getComparisonResult(comparison) === 'new').length
94
95
  }
95
96
  });
96
97
  output.cleanup();
@@ -164,18 +165,21 @@ function formatComparisonForJson(comparison) {
164
165
  let ssimScore = comparison.ssim_score ?? diffImage.ssim_score ?? null;
165
166
  let gmsdScore = comparison.gmsd_score ?? diffImage.gmsd_score ?? null;
166
167
  let fingerprintHash = comparison.fingerprint_hash || diffImage.fingerprint_hash || null;
167
- let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash;
168
+ let projection = comparison.analysis_projection || comparison.projection || diffImage.analysis_projection || diffImage.projection || null;
169
+ let diffRegions = comparison.diff_regions ?? diffImage.diff_regions ?? null;
170
+ let regionCount = comparison.region_count ?? diffImage.region_count ?? projection?.clusters?.count ?? (Array.isArray(diffRegions) ? diffRegions.length : null);
171
+ let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash || projection || regionCount != null;
168
172
  return {
169
173
  id: comparison.id,
170
- name: comparison.name,
174
+ name: getComparisonName(comparison),
171
175
  status: comparison.status,
176
+ result: getComparisonResult(comparison),
172
177
  diffPercentage: comparison.diff_percentage ?? null,
173
178
  approvalStatus: comparison.approval_status,
174
- viewport: comparison.viewport_width ? {
175
- width: comparison.viewport_width,
176
- height: comparison.viewport_height
177
- } : null,
178
- browser: comparison.browser || null,
179
+ reviewState: getVisualReviewState(comparison),
180
+ visualReview: comparison.visual_review || null,
181
+ viewport: getComparisonViewport(comparison),
182
+ browser: getComparisonBrowser(comparison),
179
183
  urls: {
180
184
  baseline: comparison.baseline_screenshot?.original_url || comparison.baseline_original_url || comparison.baseline_screenshot_url || null,
181
185
  current: comparison.current_screenshot?.original_url || comparison.current_original_url || comparison.current_screenshot_url || null,
@@ -187,7 +191,9 @@ function formatComparisonForJson(comparison) {
187
191
  clusterClassification: clusterMetadata?.classification || null,
188
192
  clusterMetadata,
189
193
  fingerprintHash,
190
- diffRegions: comparison.diff_regions ?? diffImage.diff_regions ?? null,
194
+ regionCount,
195
+ projection,
196
+ diffRegions,
191
197
  diffLines: comparison.diff_lines ?? diffImage.diff_lines ?? null,
192
198
  fingerprintData: comparison.fingerprint_data ?? diffImage.fingerprint_data ?? null
193
199
  } : null,
@@ -202,19 +208,20 @@ function formatComparisonForJson(comparison) {
202
208
  * Display a single comparison in detail
203
209
  */
204
210
  function displayComparison(output, comparison, verbose) {
205
- output.header('comparison', comparison.status);
211
+ let formatted = formatComparisonForJson(comparison);
212
+ output.header('comparison', formatted.result);
206
213
  output.keyValue({
207
- Name: comparison.name,
208
- Status: comparison.status?.toUpperCase(),
214
+ Name: formatted.name,
215
+ Status: formatted.result?.toUpperCase(),
209
216
  'Diff %': comparison.diff_percentage != null ? `${(comparison.diff_percentage * 100).toFixed(2)}%` : 'N/A',
210
- Approval: comparison.approval_status || 'pending'
217
+ Review: formatted.reviewState || 'unknown'
211
218
  });
212
219
  output.blank();
213
- if (comparison.viewport_width) {
214
- output.labelValue('Viewport', `${comparison.viewport_width}×${comparison.viewport_height}`);
220
+ if (formatted.viewport) {
221
+ output.labelValue('Viewport', `${formatted.viewport.width ?? '?'}×${formatted.viewport.height ?? '?'}`);
215
222
  }
216
- if (comparison.browser) {
217
- output.labelValue('Browser', comparison.browser);
223
+ if (formatted.browser) {
224
+ output.labelValue('Browser', formatted.browser);
218
225
  }
219
226
  output.blank();
220
227
 
@@ -230,33 +237,33 @@ function displayComparison(output, comparison, verbose) {
230
237
  }
231
238
 
232
239
  // Honeydiff analysis in verbose mode
233
- if (verbose) {
234
- let clusterMetadata = comparison.cluster_metadata || comparison.diff_image?.cluster_metadata;
235
- let ssim = comparison.ssim_score ?? comparison.diff_image?.ssim_score;
236
- let gmsd = comparison.gmsd_score ?? comparison.diff_image?.gmsd_score;
237
- let fingerprint = comparison.fingerprint_hash || comparison.diff_image?.fingerprint_hash;
238
- if (clusterMetadata || ssim != null || gmsd != null || fingerprint) {
240
+ if (verbose && formatted.honeydiff) {
241
+ let honeydiff = formatted.honeydiff;
242
+ if (honeydiff.clusterMetadata || honeydiff.ssimScore != null || honeydiff.gmsdScore != null || honeydiff.fingerprintHash || honeydiff.regionCount != null) {
239
243
  output.blank();
240
- if (clusterMetadata?.classification) {
241
- output.labelValue('Classification', clusterMetadata.classification);
244
+ if (honeydiff.clusterClassification) {
245
+ output.labelValue('Classification', honeydiff.clusterClassification);
246
+ }
247
+ if (honeydiff.ssimScore != null) {
248
+ output.labelValue('SSIM', honeydiff.ssimScore.toFixed(4));
242
249
  }
243
- if (ssim != null) {
244
- output.labelValue('SSIM', ssim.toFixed(4));
250
+ if (honeydiff.gmsdScore != null) {
251
+ output.labelValue('GMSD', honeydiff.gmsdScore.toFixed(4));
245
252
  }
246
- if (gmsd != null) {
247
- output.labelValue('GMSD', gmsd.toFixed(4));
253
+ if (honeydiff.fingerprintHash) {
254
+ output.labelValue('Fingerprint', honeydiff.fingerprintHash);
248
255
  }
249
- if (fingerprint) {
250
- output.labelValue('Fingerprint', fingerprint);
256
+ if (honeydiff.regionCount != null) {
257
+ output.labelValue('Regions', String(honeydiff.regionCount));
251
258
  }
252
259
  }
253
260
  }
254
261
 
255
262
  // URLs in verbose mode
256
263
  if (verbose) {
257
- let baselineUrl = comparison.baseline_screenshot?.original_url || comparison.baseline_original_url || comparison.baseline_screenshot_url;
258
- let currentUrl = comparison.current_screenshot?.original_url || comparison.current_original_url || comparison.current_screenshot_url;
259
- let diffUrl = comparison.diff_image?.url || comparison.diff_image_url || comparison.diff_url;
264
+ let baselineUrl = formatted.urls.baseline;
265
+ let currentUrl = formatted.urls.current;
266
+ let diffUrl = formatted.urls.diff;
260
267
  if (baselineUrl || currentUrl || diffUrl) {
261
268
  output.blank();
262
269
  output.labelValue('URLs', '');
@@ -286,9 +293,9 @@ function displayBuildComparisons(output, build, comparisons, verbose) {
286
293
  }
287
294
 
288
295
  // Summary
289
- let passed = comparisons.filter(c => c.status === 'identical').length;
290
- let failed = comparisons.filter(c => c.status === 'changed').length;
291
- let newCount = comparisons.filter(c => c.status === 'new').length;
296
+ let passed = comparisons.filter(comparison => getComparisonResult(comparison) === 'identical').length;
297
+ let failed = comparisons.filter(comparison => getComparisonResult(comparison) === 'changed').length;
298
+ let newCount = comparisons.filter(comparison => getComparisonResult(comparison) === 'new').length;
292
299
  let stats = [];
293
300
  if (passed > 0) stats.push(`${colors.brand.success(passed)} identical`);
294
301
  if (failed > 0) stats.push(`${colors.brand.warning(failed)} changed`);
@@ -298,10 +305,10 @@ function displayBuildComparisons(output, build, comparisons, verbose) {
298
305
 
299
306
  // List comparisons
300
307
  for (let comp of comparisons.slice(0, verbose ? 100 : 20)) {
301
- let icon = getStatusIcon(colors, comp.status);
308
+ let icon = getStatusIcon(colors, getComparisonResult(comp));
302
309
  let diffInfo = comp.diff_percentage != null ? colors.dim(` (${(comp.diff_percentage * 100).toFixed(1)}%)`) : '';
303
310
  let classification = verbose ? getClassificationLabel(colors, comp.cluster_metadata) : '';
304
- output.print(` ${icon} ${comp.name}${diffInfo}${classification}`);
311
+ output.print(` ${icon} ${getComparisonName(comp)}${diffInfo}${classification}`);
305
312
  }
306
313
  if (comparisons.length > (verbose ? 100 : 20)) {
307
314
  output.blank();
@@ -341,9 +348,9 @@ function displaySearchResults(output, comparisons, searchPattern, pagination, ve
341
348
  output.print(` ${colors.dim(group.buildBranch)}`);
342
349
  }
343
350
  for (let comp of group.comparisons.slice(0, verbose ? 10 : 3)) {
344
- let icon = getStatusIcon(colors, comp.status);
351
+ let icon = getStatusIcon(colors, getComparisonResult(comp));
345
352
  let classification = verbose ? getClassificationLabel(colors, comp.cluster_metadata || comp.diff_image?.cluster_metadata) : '';
346
- output.print(` ${icon} ${comp.name}${classification}`);
353
+ output.print(` ${icon} ${getComparisonName(comp)}${classification}`);
347
354
  }
348
355
  if (group.comparisons.length > (verbose ? 10 : 3)) {
349
356
  output.print(` ${colors.dim(`... and ${group.comparisons.length - (verbose ? 10 : 3)} more`)}`);