@vizzly-testing/cli 0.35.2 → 0.35.3-beta.1

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.
Files changed (43) hide show
  1. package/README.md +8 -5
  2. package/dist/api/client.js +20 -5
  3. package/dist/api/core.js +17 -3
  4. package/dist/api/endpoints.js +24 -3
  5. package/dist/api/index.js +1 -1
  6. package/dist/cli.js +25 -65
  7. package/dist/commands/baselines.js +0 -7
  8. package/dist/commands/comparisons.js +52 -45
  9. package/dist/commands/config-cmd.js +0 -7
  10. package/dist/commands/context.js +444 -238
  11. package/dist/commands/doctor.js +0 -8
  12. package/dist/commands/init.js +7 -12
  13. package/dist/commands/login.js +0 -13
  14. package/dist/commands/logout.js +0 -12
  15. package/dist/commands/orgs.js +0 -9
  16. package/dist/commands/run.js +84 -29
  17. package/dist/commands/status.js +276 -59
  18. package/dist/commands/tdd.js +19 -3
  19. package/dist/commands/whoami.js +0 -12
  20. package/dist/context/local-workspace-provider.js +293 -37
  21. package/dist/plugin-loader.js +1 -1
  22. package/dist/reporter/reporter-bundle.css +1 -1
  23. package/dist/reporter/reporter-bundle.iife.js +17 -17
  24. package/dist/server/routers/events.js +4 -4
  25. package/dist/test-runner/operations.js +70 -28
  26. package/dist/types/index.d.ts +4 -2
  27. package/dist/uploader/core.js +6 -6
  28. package/dist/uploader/operations.js +4 -1
  29. package/dist/utils/visual-context-normalizers.js +455 -0
  30. package/package.json +13 -13
  31. package/skills/vizzly/SKILL.md +68 -101
  32. package/skills/vizzly/agents/openai.yaml +2 -2
  33. package/skills/vizzly/references/cli-context.md +79 -27
  34. package/skills/vizzly/references/dynamic-content.md +28 -33
  35. package/skills/vizzly/references/sdks.md +26 -39
  36. package/skills/vizzly/references/setup-ci.md +44 -20
  37. package/dist/config/index.js +0 -13
  38. package/dist/project/index.js +0 -13
  39. package/dist/tdd/index.js +0 -19
  40. package/dist/tdd/services/baseline-downloader.js +0 -151
  41. package/dist/tdd/services/hotspot-service.js +0 -58
  42. package/dist/utils/environment.js +0 -109
  43. package/skills/vizzly/references/public-screenshots.md +0 -44
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 --agent --json
105
105
 
106
106
  # Local workspace context from .vizzly/
107
107
  vizzly context build current --source local
@@ -110,9 +110,12 @@ vizzly context screenshot build-detail-screenshots --source local --json
110
110
  vizzly context review-queue --source local --json
111
111
  ```
112
112
 
113
- `--json` is the durable automation path. `--agent` gives a compact handoff for
114
- prompt assembly. Add `--full` when you need the whole payload, or
115
- `--include screenshots,diffs,comments` when compact JSON needs selected detail.
113
+ `--json` is the durable automation path. `--agent` gives you the compact API
114
+ handoff for prompt assembly. Build handoffs contain up to 10 records; use the
115
+ exact next-page command returned in `suggested_commands` to continue safely.
116
+ That command carries the API's opaque `--cursor`. Add
117
+ `--include diffs` for raw diff diagnostics, or `--full` when you need the whole
118
+ payload.
116
119
 
117
120
  Local context is read-only and file-backed. It reads your existing `.vizzly`
118
121
  workspace state from TDD runs, including screenshots, diffs, and saved hotspot
@@ -18,6 +18,11 @@ function isProjectToken(token) {
18
18
  return typeof token === 'string' && token.startsWith('vzt_');
19
19
  }
20
20
 
21
+ /** Prefer the transport cause because Node's top-level fetch message is generic. */
22
+ function getNetworkFailureReason(error) {
23
+ return error?.cause?.code || error?.cause?.message || error?.message || 'request failed';
24
+ }
25
+
21
26
  /**
22
27
  * Create an API client with the given configuration
23
28
  *
@@ -58,10 +63,19 @@ export function createApiClient(options = {}) {
58
63
  contentType: fetchOptions.headers?.['Content-Type'],
59
64
  extra: fetchOptions.headers || {}
60
65
  });
61
- let response = await fetch(url, {
62
- ...fetchOptions,
63
- headers
64
- });
66
+ let response;
67
+ try {
68
+ response = await fetch(url, {
69
+ ...fetchOptions,
70
+ headers
71
+ });
72
+ } catch (error) {
73
+ let apiOrigin = new URL(url).origin;
74
+ let reason = getNetworkFailureReason(error);
75
+ throw new VizzlyError(`Unable to reach Vizzly at ${apiOrigin}: ${reason}`, 'NETWORK_ERROR', {
76
+ apiOrigin
77
+ });
78
+ }
65
79
  if (!response.ok) {
66
80
  let errorBody = await extractErrorBody(response);
67
81
 
@@ -82,7 +96,8 @@ export function createApiClient(options = {}) {
82
96
  // Other errors
83
97
  let error = parseApiError(response.status, errorBody, url);
84
98
  throw new VizzlyError(error.message, error.code, {
85
- status: error.status
99
+ status: error.status,
100
+ details: error.details
86
101
  });
87
102
  }
88
103
  return response.json();
package/dist/api/core.js CHANGED
@@ -232,8 +232,21 @@ export function shouldRetryWithRefresh(status, isRetry, hasRefreshToken) {
232
232
  */
233
233
  export function parseApiError(status, body, url) {
234
234
  let message = `API request failed: ${status}`;
235
+ let responseDetails = null;
236
+ let responseCode = null;
237
+ let responseMessage = body;
235
238
  if (body) {
236
- message += ` - ${body}`;
239
+ try {
240
+ let parsedBody = JSON.parse(body);
241
+ responseDetails = parsedBody.details ?? null;
242
+ responseCode = parsedBody.code ?? parsedBody.details?.code ?? null;
243
+ responseMessage = parsedBody.error ?? parsedBody.message ?? body;
244
+ } catch {
245
+ // Plain-text API errors remain valid and are surfaced unchanged.
246
+ }
247
+ }
248
+ if (responseMessage) {
249
+ message += ` - ${responseMessage}`;
237
250
  }
238
251
  message += ` (URL: ${url})`;
239
252
  let code = 'API_ERROR';
@@ -244,8 +257,9 @@ export function parseApiError(status, body, url) {
244
257
  if (status >= 500) code = 'SERVER_ERROR';
245
258
  return {
246
259
  message,
247
- code,
248
- status
260
+ code: responseCode || code,
261
+ status,
262
+ details: responseDetails
249
263
  };
250
264
  }
251
265
 
@@ -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
@@ -3,17 +3,17 @@ import 'dotenv/config';
3
3
  import { existsSync, statSync } from 'node:fs';
4
4
  import { Option, program } from 'commander';
5
5
  import { apiCommand, validateApiOptions } from './commands/api.js';
6
- import { baselinesCommand, validateBaselinesOptions } from './commands/baselines.js';
6
+ import { baselinesCommand } from './commands/baselines.js';
7
7
  import { buildsCommand, validateBuildsOptions } from './commands/builds.js';
8
8
  import { comparisonsCommand, validateComparisonsOptions } from './commands/comparisons.js';
9
- import { configCommand, validateConfigOptions } from './commands/config-cmd.js';
9
+ import { configCommand } from './commands/config-cmd.js';
10
10
  import { contextBuildCommand, contextComparisonCommand, contextReviewQueueCommand, contextScreenshotCommand, contextSimilarCommand, validateContextBuildOptions, validateContextComparisonOptions, validateContextReviewQueueOptions, validateContextScreenshotOptions, validateContextSimilarOptions } from './commands/context.js';
11
- import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
11
+ import { doctorCommand } from './commands/doctor.js';
12
12
  import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
13
13
  import { init } from './commands/init.js';
14
- import { loginCommand, validateLoginOptions } from './commands/login.js';
15
- import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
16
- import { orgsCommand, validateOrgsOptions } from './commands/orgs.js';
14
+ import { loginCommand } from './commands/login.js';
15
+ import { logoutCommand } from './commands/logout.js';
16
+ import { orgsCommand } from './commands/orgs.js';
17
17
  import { previewCommand, validatePreviewOptions } from './commands/preview.js';
18
18
  import { projectLinkCommand, validateProjectLinkOptions } from './commands/project.js';
19
19
  import { projectsCommand, validateProjectsOptions } from './commands/projects.js';
@@ -23,7 +23,7 @@ import { statusCommand, validateStatusOptions } from './commands/status.js';
23
23
  import { tddCommand, validateTddOptions } from './commands/tdd.js';
24
24
  import { runDaemonChild, tddListCommand, tddStartCommand, tddStatusCommand, tddStopCommand, validateTddStartOptions } from './commands/tdd-daemon.js';
25
25
  import { uploadCommand, validateUploadOptions } from './commands/upload.js';
26
- import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
26
+ import { whoamiCommand } from './commands/whoami.js';
27
27
  import { createPluginServices } from './plugin-api.js';
28
28
  import { loadPlugins } from './plugin-loader.js';
29
29
  import { createServices } from './services/index.js';
@@ -651,14 +651,14 @@ Examples:
651
651
  await comparisonsCommand(options, globalOptions);
652
652
  });
653
653
  let contextCmd = program.command('context').description('Fetch build and diff context').showHelpAfterError('(Run vizzly context --help for available commands)').showSuggestionAfterError();
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', `
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 instead of compact context').option('--cursor <cursor>', 'Continue compact evidence from an opaque API cursor').option('--include <items>', 'Add raw diff diagnostics to compact context: diffs').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
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);
@@ -667,12 +667,12 @@ Examples:
667
667
  }
668
668
  await contextBuildCommand(buildId, options, globalOptions);
669
669
  });
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', `
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('--agent', 'Output compact context for LLM agents').option('--full', 'Return the full comparison context instead of compact context').option('--cursor <cursor>', 'Continue compact history from an opaque API cursor').option('--include <items>', 'Add detail to compact context: diffs').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 --json
675
+ $ vizzly context comparison def456 --source cloud --agent --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);
@@ -724,12 +724,6 @@ Examples:
724
724
  });
725
725
  program.command('config').description('Display current configuration').argument('[key]', 'Specific config key to get (dot notation, e.g., comparison.threshold)').action(async (key, options) => {
726
726
  let globalOptions = getGlobalOptions();
727
-
728
- // Validate options
729
- const validationErrors = validateConfigOptions(options);
730
- if (validationErrors.length > 0) {
731
- reportValidationErrors(validationErrors);
732
- }
733
727
  await configCommand(key, options, globalOptions);
734
728
  });
735
729
  program.command('baselines').description('List and query local TDD baselines').option('--name <pattern>', 'Filter baselines by name (supports wildcards)').option('--info <name>', 'Get detailed info for a specific baseline').addHelpText('after', `
@@ -742,12 +736,6 @@ Examples:
742
736
  Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.
743
737
  `).action(async options => {
744
738
  let globalOptions = getGlobalOptions();
745
-
746
- // Validate options
747
- const validationErrors = validateBaselinesOptions(options);
748
- if (validationErrors.length > 0) {
749
- reportValidationErrors(validationErrors);
750
- }
751
739
  await baselinesCommand(options, globalOptions);
752
740
  });
753
741
  program.command('api').description('Make raw API requests (for power users)').argument('<endpoint>', 'API endpoint (e.g., /api/sdk/builds)').option('-X, --method <method>', 'HTTP method (GET or POST for approve/reject/comment)', 'GET').option('-d, --data <json>', 'Request body (JSON)').option('-H, --header <header>', 'Add header (key:value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).option('-q, --query <param>', 'Add query param (key=value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).addHelpText('after', `
@@ -833,10 +821,6 @@ Note: Shows organizations from your user account (via vizzly login)
833
821
  or the single organization for a project token.
834
822
  `).action(async options => {
835
823
  let globalOptions = getGlobalOptions();
836
- const validationErrors = validateOrgsOptions(options);
837
- if (validationErrors.length > 0) {
838
- reportValidationErrors(validationErrors);
839
- }
840
824
  await orgsCommand(options, globalOptions);
841
825
  });
842
826
  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', `
@@ -910,42 +894,18 @@ program.command('preview').description('Upload static files as a preview for a b
910
894
  });
911
895
  program.command('doctor').description('Run diagnostics to check your environment and configuration').option('--api', 'Include API connectivity checks').action(async options => {
912
896
  let globalOptions = getGlobalOptions();
913
-
914
- // Validate options
915
- const validationErrors = validateDoctorOptions(options);
916
- if (validationErrors.length > 0) {
917
- reportValidationErrors(validationErrors);
918
- }
919
897
  await doctorCommand(options, globalOptions);
920
898
  });
921
899
  program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
922
900
  let globalOptions = getGlobalOptions();
923
-
924
- // Validate options
925
- const validationErrors = validateLoginOptions(options);
926
- if (validationErrors.length > 0) {
927
- reportValidationErrors(validationErrors);
928
- }
929
901
  await loginCommand(options, globalOptions);
930
902
  });
931
903
  program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
932
904
  let globalOptions = getGlobalOptions();
933
-
934
- // Validate options
935
- const validationErrors = validateLogoutOptions(options);
936
- if (validationErrors.length > 0) {
937
- reportValidationErrors(validationErrors);
938
- }
939
905
  await logoutCommand(options, globalOptions);
940
906
  });
941
907
  program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
942
908
  let globalOptions = getGlobalOptions();
943
-
944
- // Validate options
945
- const validationErrors = validateWhoamiOptions(options);
946
- if (validationErrors.length > 0) {
947
- reportValidationErrors(validationErrors);
948
- }
949
909
  await whoamiCommand(options, globalOptions);
950
910
  });
951
911
 
@@ -286,11 +286,4 @@ export function formatBytes(bytes) {
286
286
  if (bytes < 1024) return `${bytes} B`;
287
287
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
288
288
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
289
- }
290
-
291
- /**
292
- * Validate baselines command options
293
- */
294
- export function validateBaselinesOptions() {
295
- return [];
296
289
  }
@@ -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`)}`);
@@ -163,11 +163,4 @@ function getNestedValue(obj, path) {
163
163
  current = current[part];
164
164
  }
165
165
  return current;
166
- }
167
-
168
- /**
169
- * Validate config command options
170
- */
171
- export function validateConfigOptions() {
172
- return [];
173
166
  }