@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.
- package/README.md +8 -5
- package/dist/api/client.js +20 -5
- package/dist/api/core.js +17 -3
- package/dist/api/endpoints.js +24 -3
- package/dist/api/index.js +1 -1
- package/dist/cli.js +25 -65
- package/dist/commands/baselines.js +0 -7
- package/dist/commands/comparisons.js +52 -45
- package/dist/commands/config-cmd.js +0 -7
- package/dist/commands/context.js +444 -238
- package/dist/commands/doctor.js +0 -8
- package/dist/commands/init.js +7 -12
- package/dist/commands/login.js +0 -13
- package/dist/commands/logout.js +0 -12
- package/dist/commands/orgs.js +0 -9
- package/dist/commands/run.js +84 -29
- package/dist/commands/status.js +276 -59
- package/dist/commands/tdd.js +19 -3
- package/dist/commands/whoami.js +0 -12
- package/dist/context/local-workspace-provider.js +293 -37
- package/dist/plugin-loader.js +1 -1
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +17 -17
- package/dist/server/routers/events.js +4 -4
- package/dist/test-runner/operations.js +70 -28
- package/dist/types/index.d.ts +4 -2
- package/dist/uploader/core.js +6 -6
- package/dist/uploader/operations.js +4 -1
- package/dist/utils/visual-context-normalizers.js +455 -0
- package/package.json +13 -13
- package/skills/vizzly/SKILL.md +68 -101
- package/skills/vizzly/agents/openai.yaml +2 -2
- package/skills/vizzly/references/cli-context.md +79 -27
- package/skills/vizzly/references/dynamic-content.md +28 -33
- package/skills/vizzly/references/sdks.md +26 -39
- package/skills/vizzly/references/setup-ci.md +44 -20
- package/dist/config/index.js +0 -13
- package/dist/project/index.js +0 -13
- package/dist/tdd/index.js +0 -19
- package/dist/tdd/services/baseline-downloader.js +0 -151
- package/dist/tdd/services/hotspot-service.js +0 -58
- package/dist/utils/environment.js +0 -109
- 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
|
|
114
|
-
prompt assembly.
|
|
115
|
-
|
|
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
|
package/dist/api/client.js
CHANGED
|
@@ -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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/api/endpoints.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
15
|
-
import { logoutCommand
|
|
16
|
-
import { orgsCommand
|
|
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 {
|
|
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
|
|
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
|
|
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('--
|
|
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 --
|
|
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(
|
|
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(
|
|
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(
|
|
92
|
-
failed: comparisons.filter(
|
|
93
|
-
new: comparisons.filter(
|
|
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
|
|
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
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
let formatted = formatComparisonForJson(comparison);
|
|
212
|
+
output.header('comparison', formatted.result);
|
|
206
213
|
output.keyValue({
|
|
207
|
-
Name:
|
|
208
|
-
Status:
|
|
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
|
-
|
|
217
|
+
Review: formatted.reviewState || 'unknown'
|
|
211
218
|
});
|
|
212
219
|
output.blank();
|
|
213
|
-
if (
|
|
214
|
-
output.labelValue('Viewport', `${
|
|
220
|
+
if (formatted.viewport) {
|
|
221
|
+
output.labelValue('Viewport', `${formatted.viewport.width ?? '?'}×${formatted.viewport.height ?? '?'}`);
|
|
215
222
|
}
|
|
216
|
-
if (
|
|
217
|
-
output.labelValue('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
|
|
235
|
-
|
|
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 (
|
|
241
|
-
output.labelValue('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 (
|
|
244
|
-
output.labelValue('
|
|
250
|
+
if (honeydiff.gmsdScore != null) {
|
|
251
|
+
output.labelValue('GMSD', honeydiff.gmsdScore.toFixed(4));
|
|
245
252
|
}
|
|
246
|
-
if (
|
|
247
|
-
output.labelValue('
|
|
253
|
+
if (honeydiff.fingerprintHash) {
|
|
254
|
+
output.labelValue('Fingerprint', honeydiff.fingerprintHash);
|
|
248
255
|
}
|
|
249
|
-
if (
|
|
250
|
-
output.labelValue('
|
|
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 =
|
|
258
|
-
let currentUrl =
|
|
259
|
-
let diffUrl =
|
|
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(
|
|
290
|
-
let failed = comparisons.filter(
|
|
291
|
-
let newCount = comparisons.filter(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)}`);
|