@vizzly-testing/cli 0.30.0 → 0.32.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 +9 -9
- package/dist/api/client.js +6 -3
- package/dist/cli.js +58 -29
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +329 -18
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +17 -6
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +63 -25
- package/dist/config/core.js +36 -35
- package/dist/context/local-workspace-provider.js +132 -12
- package/dist/context/provider-resolver.js +5 -1
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +7 -7
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +212 -174
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/region-coverage.js +109 -21
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/comparison-service.js +10 -1
- package/dist/tdd/services/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +144 -138
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +40 -0
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +20 -1
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
package/dist/commands/context.js
CHANGED
|
@@ -7,12 +7,16 @@ import { createLocalWorkspaceContextProvider as defaultCreateLocalWorkspaceConte
|
|
|
7
7
|
import { resolveContextSource as defaultResolveContextSource } from '../context/provider-resolver.js';
|
|
8
8
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
9
9
|
import * as defaultOutput from '../utils/output.js';
|
|
10
|
+
import { readSession as defaultReadSession } from '../utils/session.js';
|
|
10
11
|
function buildAuthErrorMessage() {
|
|
11
|
-
return '
|
|
12
|
+
return 'Authentication required. Use --token, set VIZZLY_TOKEN, run "vizzly login", or link a project.';
|
|
12
13
|
}
|
|
13
14
|
function buildSourceErrorMessage() {
|
|
14
15
|
return '--source must be one of: auto, cloud, local';
|
|
15
16
|
}
|
|
17
|
+
function buildIncludeErrorMessage() {
|
|
18
|
+
return '--include must contain only: screenshots, diffs, comments';
|
|
19
|
+
}
|
|
16
20
|
function validateLimitRange(value, flagName, {
|
|
17
21
|
min = 1,
|
|
18
22
|
max
|
|
@@ -21,7 +25,7 @@ function validateLimitRange(value, flagName, {
|
|
|
21
25
|
return [];
|
|
22
26
|
}
|
|
23
27
|
if (!Number.isInteger(value) || value < min || value > max) {
|
|
24
|
-
return [`${flagName} must be
|
|
28
|
+
return [`${flagName} must be an integer between ${min} and ${max}`];
|
|
25
29
|
}
|
|
26
30
|
return [];
|
|
27
31
|
}
|
|
@@ -30,7 +34,7 @@ function validateOffset(value) {
|
|
|
30
34
|
return [];
|
|
31
35
|
}
|
|
32
36
|
if (!Number.isInteger(value) || value < 0) {
|
|
33
|
-
return ['--offset must be a non-negative
|
|
37
|
+
return ['--offset must be a non-negative integer'];
|
|
34
38
|
}
|
|
35
39
|
return [];
|
|
36
40
|
}
|
|
@@ -43,6 +47,18 @@ function validateSourceOption(value) {
|
|
|
43
47
|
}
|
|
44
48
|
return [];
|
|
45
49
|
}
|
|
50
|
+
function parseIncludeOption(value) {
|
|
51
|
+
if (!value) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
let rawItems = Array.isArray(value) ? value : String(value).split(',');
|
|
55
|
+
return rawItems.map(item => item.trim()).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
function validateIncludeOption(value) {
|
|
58
|
+
let allowed = new Set(['screenshots', 'diffs', 'comments']);
|
|
59
|
+
let invalid = parseIncludeOption(value).filter(item => !allowed.has(item));
|
|
60
|
+
return invalid.length > 0 ? [buildIncludeErrorMessage()] : [];
|
|
61
|
+
}
|
|
46
62
|
function validateScopedProjectOptions(options = {}) {
|
|
47
63
|
let errors = [];
|
|
48
64
|
if (options.org && !options.project) {
|
|
@@ -50,10 +66,13 @@ function validateScopedProjectOptions(options = {}) {
|
|
|
50
66
|
}
|
|
51
67
|
return errors;
|
|
52
68
|
}
|
|
69
|
+
function hasExplicitCloudScope(options = {}, config = {}) {
|
|
70
|
+
return Boolean(options.org || options.project || config.linkedProject?.organizationSlug || config.linkedProject?.projectSlug);
|
|
71
|
+
}
|
|
53
72
|
function createClient(config, createApiClient) {
|
|
54
73
|
return createApiClient({
|
|
55
74
|
baseUrl: config.apiUrl,
|
|
56
|
-
token: config.apiKey,
|
|
75
|
+
token: config.apiKey || config.userToken,
|
|
57
76
|
command: 'context'
|
|
58
77
|
});
|
|
59
78
|
}
|
|
@@ -69,7 +88,7 @@ async function loadContextConfig(globalOptions, options, deps) {
|
|
|
69
88
|
...options
|
|
70
89
|
};
|
|
71
90
|
let config = await loadConfig(globalOptions.config, allOptions);
|
|
72
|
-
if (requireApiKey && !config.apiKey) {
|
|
91
|
+
if (requireApiKey && !config.apiKey && !config.userToken) {
|
|
73
92
|
output.error(buildAuthErrorMessage());
|
|
74
93
|
output.cleanup();
|
|
75
94
|
exit(1);
|
|
@@ -111,6 +130,23 @@ function buildLocalFingerprintCapabilityError() {
|
|
|
111
130
|
error.code = 'LOCAL_WORKSPACE_CONTEXT';
|
|
112
131
|
return error;
|
|
113
132
|
}
|
|
133
|
+
function resolveBuildContextId(buildId, runtime, deps = {}) {
|
|
134
|
+
let {
|
|
135
|
+
readSession = defaultReadSession
|
|
136
|
+
} = deps;
|
|
137
|
+
if (buildId !== 'current' || runtime.source !== 'cloud') {
|
|
138
|
+
return buildId;
|
|
139
|
+
}
|
|
140
|
+
let session = readSession({
|
|
141
|
+
cwd: runtime.projectRoot
|
|
142
|
+
});
|
|
143
|
+
if (session?.buildId && !session.expired) {
|
|
144
|
+
return session.buildId;
|
|
145
|
+
}
|
|
146
|
+
let error = new Error('No current cloud build found. Run "vizzly run" first, or pass a build ID.');
|
|
147
|
+
error.code = 'NO_CURRENT_CLOUD_BUILD';
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
114
150
|
function shouldExplainLocalSimilarityGap(requestedSource, command, localProvider) {
|
|
115
151
|
return requestedSource === 'auto' && command === 'similar' && localProvider.isAvailable();
|
|
116
152
|
}
|
|
@@ -136,11 +172,12 @@ async function loadContextRuntime(command, target, globalOptions, options, deps
|
|
|
136
172
|
requestedSource,
|
|
137
173
|
command,
|
|
138
174
|
target,
|
|
139
|
-
projectRoot
|
|
175
|
+
projectRoot,
|
|
176
|
+
hasCloudScope: hasExplicitCloudScope(options, config)
|
|
140
177
|
}, {
|
|
141
178
|
createLocalWorkspaceContextProvider
|
|
142
179
|
});
|
|
143
|
-
if (source === 'cloud' && !config.apiKey) {
|
|
180
|
+
if (source === 'cloud' && !config.apiKey && !config.userToken) {
|
|
144
181
|
if (shouldExplainLocalSimilarityGap(requestedSource, command, localProvider)) {
|
|
145
182
|
throw buildLocalFingerprintCapabilityError();
|
|
146
183
|
}
|
|
@@ -183,6 +220,189 @@ function getStatusTone(colors, status) {
|
|
|
183
220
|
function getComparisonDisplayState(comparison = {}) {
|
|
184
221
|
return comparison.result || comparison.status || 'unknown';
|
|
185
222
|
}
|
|
223
|
+
function isChangedComparison(comparison = {}) {
|
|
224
|
+
return ['changed', 'failed', 'pending'].includes(getComparisonDisplayState(comparison));
|
|
225
|
+
}
|
|
226
|
+
function isNewComparison(comparison = {}) {
|
|
227
|
+
return getComparisonDisplayState(comparison) === 'new';
|
|
228
|
+
}
|
|
229
|
+
function getComparisonName(comparison = {}) {
|
|
230
|
+
return comparison.screenshot_name || comparison.screenshot?.name || comparison.name || comparison.id || 'unknown screenshot';
|
|
231
|
+
}
|
|
232
|
+
function getComparisonDiffPercentage(comparison = {}) {
|
|
233
|
+
return comparison.diff?.percentage ?? comparison.diff_percentage ?? null;
|
|
234
|
+
}
|
|
235
|
+
function getComparisonFingerprint(comparison = {}) {
|
|
236
|
+
return comparison.diff?.fingerprint_hash || comparison.analysis?.fingerprint_hash || null;
|
|
237
|
+
}
|
|
238
|
+
function getComparisonDiffImageUrl(comparison = {}) {
|
|
239
|
+
return comparison.diff?.image_url || comparison.analysis?.diff_image_url || null;
|
|
240
|
+
}
|
|
241
|
+
function getComparisonRegionCount(comparison = {}) {
|
|
242
|
+
let regions = comparison.diff?.regions || comparison.analysis?.diff_regions;
|
|
243
|
+
return Array.isArray(regions) ? regions.length : 0;
|
|
244
|
+
}
|
|
245
|
+
function getComparisonScreenshot(comparison = {}) {
|
|
246
|
+
return comparison.screenshot || {};
|
|
247
|
+
}
|
|
248
|
+
function getComparisonBaseline(comparison = {}) {
|
|
249
|
+
return comparison.baseline || comparison.screenshot?.baseline || {};
|
|
250
|
+
}
|
|
251
|
+
function buildCompactDiff(comparison = {}, includeDiffs = false) {
|
|
252
|
+
let diff = comparison.diff || comparison.analysis || {};
|
|
253
|
+
let compact = {
|
|
254
|
+
percentage: getComparisonDiffPercentage(comparison),
|
|
255
|
+
changed_pixels: diff.changed_pixels ?? comparison.changed_pixels ?? null,
|
|
256
|
+
total_pixels: diff.total_pixels ?? comparison.total_pixels ?? null,
|
|
257
|
+
threshold: diff.threshold ?? comparison.threshold ?? null,
|
|
258
|
+
fingerprint_hash: getComparisonFingerprint(comparison),
|
|
259
|
+
region_count: getComparisonRegionCount(comparison),
|
|
260
|
+
image_url: getComparisonDiffImageUrl(comparison)
|
|
261
|
+
};
|
|
262
|
+
if (includeDiffs) {
|
|
263
|
+
compact.regions = diff.regions || diff.diff_regions || [];
|
|
264
|
+
compact.cluster_metadata = diff.cluster_metadata || null;
|
|
265
|
+
compact.ssim_score = diff.ssim_score ?? null;
|
|
266
|
+
compact.gmsd_score = diff.gmsd_score ?? null;
|
|
267
|
+
compact.diff_lines = diff.diff_lines || [];
|
|
268
|
+
}
|
|
269
|
+
return compact;
|
|
270
|
+
}
|
|
271
|
+
function buildCompactComparison(comparison = {}, {
|
|
272
|
+
includeDiffs = false
|
|
273
|
+
} = {}) {
|
|
274
|
+
let screenshot = getComparisonScreenshot(comparison);
|
|
275
|
+
let baseline = getComparisonBaseline(comparison);
|
|
276
|
+
return {
|
|
277
|
+
id: comparison.id || null,
|
|
278
|
+
name: getComparisonName(comparison),
|
|
279
|
+
result: getComparisonDisplayState(comparison),
|
|
280
|
+
approval_status: comparison.approval_status || null,
|
|
281
|
+
needs_review: Boolean(comparison.needs_review),
|
|
282
|
+
is_flaky: Boolean(comparison.is_flaky),
|
|
283
|
+
screenshot: {
|
|
284
|
+
id: screenshot.id || null,
|
|
285
|
+
url: screenshot.url || screenshot.original_url || null
|
|
286
|
+
},
|
|
287
|
+
baseline: {
|
|
288
|
+
id: baseline.id || null,
|
|
289
|
+
build_id: baseline.build_id || null,
|
|
290
|
+
url: baseline.url || baseline.original_url || null
|
|
291
|
+
},
|
|
292
|
+
diff: buildCompactDiff(comparison, includeDiffs)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function summarizeComparisons(comparisons = []) {
|
|
296
|
+
return {
|
|
297
|
+
total: comparisons.length,
|
|
298
|
+
changed: comparisons.filter(isChangedComparison).length,
|
|
299
|
+
new: comparisons.filter(isNewComparison).length,
|
|
300
|
+
needs_review: comparisons.filter(comparison => comparison.needs_review).length
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function buildAgentNextActions(context = {}, comparisonSummary = {}) {
|
|
304
|
+
if (context.status?.needs_review) {
|
|
305
|
+
return ['Inspect the changed and new comparisons before editing related UI.', 'Use approved baselines as the expected visual behavior.', 'Leave approval decisions to human reviewers.'];
|
|
306
|
+
}
|
|
307
|
+
if (comparisonSummary.total > 0) {
|
|
308
|
+
return ['Use the approved baseline and reviewed screenshots as current visual truth.', 'Prefer targeted edits that preserve identical comparisons.'];
|
|
309
|
+
}
|
|
310
|
+
return ['No comparisons were returned for this build context.', 'Open the build URL for more detail if this is unexpected.'];
|
|
311
|
+
}
|
|
312
|
+
function formatAgentStatus(status = {}) {
|
|
313
|
+
return {
|
|
314
|
+
needs_review: Boolean(status.needs_review),
|
|
315
|
+
reasons: status.reasons || [],
|
|
316
|
+
pending_comparisons: status.pending_comparisons || 0,
|
|
317
|
+
unresolved_comments: status.unresolved_comments || 0
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function formatAgentSummary(context = {}, comparisons = []) {
|
|
321
|
+
return {
|
|
322
|
+
comparisons: context.summary?.comparisons || summarizeComparisons(comparisons),
|
|
323
|
+
review: context.summary?.review || {},
|
|
324
|
+
comments: context.summary?.comments || {
|
|
325
|
+
build: getBuildCommentsCount(context),
|
|
326
|
+
screenshot: getScreenshotCommentsCount(context)
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function buildAgentBuildPayload(context, {
|
|
331
|
+
source = null,
|
|
332
|
+
include = [],
|
|
333
|
+
evidenceLimit = 10
|
|
334
|
+
} = {}) {
|
|
335
|
+
let comparisons = context.comparisons || [];
|
|
336
|
+
let includeSet = new Set(include);
|
|
337
|
+
let includeDiffs = includeSet.has('diffs');
|
|
338
|
+
let changed = comparisons.filter(isChangedComparison);
|
|
339
|
+
let fresh = comparisons.filter(isNewComparison);
|
|
340
|
+
let evidence = [...changed, ...fresh].slice(0, evidenceLimit).map(comparison => buildCompactComparison(comparison, {
|
|
341
|
+
includeDiffs
|
|
342
|
+
}));
|
|
343
|
+
let comparisonSummary = summarizeComparisons(comparisons);
|
|
344
|
+
let payload = {
|
|
345
|
+
resource: 'build_agent_context',
|
|
346
|
+
source: context.source || source || 'cloud',
|
|
347
|
+
scope: context.scope || null,
|
|
348
|
+
project: {
|
|
349
|
+
organization: context.scope?.organization?.slug || null,
|
|
350
|
+
slug: context.scope?.project?.slug || null,
|
|
351
|
+
name: context.scope?.project?.name || null,
|
|
352
|
+
visibility: context.scope?.project?.visibility || null
|
|
353
|
+
},
|
|
354
|
+
build: context.build || null,
|
|
355
|
+
baseline: {
|
|
356
|
+
selected: context.baseline?.selected || null,
|
|
357
|
+
selection_reason: context.baseline?.selection_reason || null
|
|
358
|
+
},
|
|
359
|
+
status: formatAgentStatus(context.status),
|
|
360
|
+
summary: formatAgentSummary(context, comparisons),
|
|
361
|
+
evidence,
|
|
362
|
+
links: context.links || {},
|
|
363
|
+
preview: context.preview || null,
|
|
364
|
+
next_actions: buildAgentNextActions(context, comparisonSummary)
|
|
365
|
+
};
|
|
366
|
+
if (includeSet.has('screenshots')) {
|
|
367
|
+
payload.screenshots = context.screenshots || [];
|
|
368
|
+
}
|
|
369
|
+
if (includeSet.has('comments')) {
|
|
370
|
+
payload.comments = context.comments || {};
|
|
371
|
+
}
|
|
372
|
+
return payload;
|
|
373
|
+
}
|
|
374
|
+
function getBuildCommentsCount(context = {}) {
|
|
375
|
+
if (Array.isArray(context.comments?.build)) {
|
|
376
|
+
return context.comments.build.length;
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(context.review?.comments)) {
|
|
379
|
+
return context.review.comments.length;
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
function getScreenshotCommentsCount(context = {}) {
|
|
384
|
+
if (Number.isInteger(context.comments?.screenshot_count)) {
|
|
385
|
+
return context.comments.screenshot_count;
|
|
386
|
+
}
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
function getReviewAssignmentsCount(context = {}) {
|
|
390
|
+
if (Array.isArray(context.review?.assignments)) {
|
|
391
|
+
return context.review.assignments.length;
|
|
392
|
+
}
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
function formatNeedsReview(status = {}) {
|
|
396
|
+
if (!status || status.needs_review == null) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
let pending = status.pending_comparisons || 0;
|
|
400
|
+
let unresolved = status.unresolved_comments || 0;
|
|
401
|
+
if (!status.needs_review) {
|
|
402
|
+
return 'no';
|
|
403
|
+
}
|
|
404
|
+
return `yes · ${pending} comparisons · ${unresolved} unresolved comments`;
|
|
405
|
+
}
|
|
186
406
|
function formatConfirmedRegionLabels(regions = []) {
|
|
187
407
|
return regions.map(region => region.label).filter(Boolean).slice(0, 3).join(' · ');
|
|
188
408
|
}
|
|
@@ -193,13 +413,17 @@ function printComparisonList(output, comparisons = [], {
|
|
|
193
413
|
for (let comparison of comparisons.slice(0, limit)) {
|
|
194
414
|
let displayState = getComparisonDisplayState(comparison);
|
|
195
415
|
let statusTone = getStatusTone(colors, displayState);
|
|
196
|
-
let screenshotName = comparison
|
|
197
|
-
let
|
|
198
|
-
let
|
|
416
|
+
let screenshotName = getComparisonName(comparison);
|
|
417
|
+
let rawDiffPercentage = getComparisonDiffPercentage(comparison);
|
|
418
|
+
let diffPercentage = rawDiffPercentage == null ? null : `${rawDiffPercentage}%`;
|
|
419
|
+
let fingerprint = getComparisonFingerprint(comparison);
|
|
199
420
|
let details = [];
|
|
200
421
|
if (diffPercentage) {
|
|
201
422
|
details.push(diffPercentage);
|
|
202
423
|
}
|
|
424
|
+
if (comparison.needs_review) {
|
|
425
|
+
details.push('needs review');
|
|
426
|
+
}
|
|
203
427
|
if (fingerprint) {
|
|
204
428
|
details.push(`fp:${fingerprint}`);
|
|
205
429
|
}
|
|
@@ -216,23 +440,94 @@ function displayBuildContext(output, context) {
|
|
|
216
440
|
output.header('context', 'build');
|
|
217
441
|
let colors = output.getColors();
|
|
218
442
|
let buildTone = getStatusTone(colors, context.build.status);
|
|
443
|
+
let comparisons = context.comparisons || [];
|
|
444
|
+
let screenshots = context.screenshots || [];
|
|
445
|
+
let reviewSummary = context.summary?.review || {};
|
|
446
|
+
let commentsSummary = context.summary?.comments || {};
|
|
447
|
+
let needsReview = formatNeedsReview(context.status);
|
|
448
|
+
let baseline = context.baseline?.selected || null;
|
|
219
449
|
output.print(` ${colors.bold(context.build.name || context.build.id)} ${buildTone((context.build.status || 'unknown').toUpperCase())}`);
|
|
220
450
|
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
221
451
|
output.blank();
|
|
222
|
-
output.labelValue('Comparisons', String(
|
|
223
|
-
|
|
224
|
-
|
|
452
|
+
output.labelValue('Comparisons', String(comparisons.length));
|
|
453
|
+
if (screenshots.length > 0) {
|
|
454
|
+
output.labelValue('Screenshots', String(screenshots.length));
|
|
455
|
+
}
|
|
456
|
+
if (baseline) {
|
|
457
|
+
output.labelValue('Baseline', `${baseline.name || baseline.id || 'selected'}${context.baseline.selection_reason ? ` · ${context.baseline.selection_reason}` : ''}`);
|
|
458
|
+
}
|
|
459
|
+
if (needsReview) {
|
|
460
|
+
output.labelValue('Needs Review', needsReview);
|
|
461
|
+
}
|
|
462
|
+
output.labelValue('Review', `${reviewSummary.pending || 0} pending · ${reviewSummary.approved || 0} approved · ${reviewSummary.rejected || 0} rejected`);
|
|
463
|
+
output.labelValue('Memory', `${commentsSummary.build ?? getBuildCommentsCount(context)} build comments · ${commentsSummary.screenshot ?? getScreenshotCommentsCount(context)} screenshot comments · ${getReviewAssignmentsCount(context)} assignments`);
|
|
225
464
|
if (context.preview) {
|
|
226
|
-
|
|
465
|
+
let previewUrl = context.preview.preview_url || context.preview.url;
|
|
466
|
+
output.labelValue('Preview', `${context.preview.status || 'unknown'}${previewUrl ? ' · available' : ''}`);
|
|
227
467
|
}
|
|
228
468
|
if (context.links?.build_url) {
|
|
229
469
|
output.labelValue('Build URL', context.links.build_url);
|
|
230
470
|
}
|
|
231
|
-
if (
|
|
471
|
+
if (comparisons.length > 0) {
|
|
232
472
|
output.blank();
|
|
233
473
|
output.print(' Comparisons');
|
|
234
|
-
printComparisonList(output,
|
|
474
|
+
printComparisonList(output, comparisons);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function formatAgentBuildContext(context) {
|
|
478
|
+
let comparisons = context.comparisons || [];
|
|
479
|
+
let changed = comparisons.filter(isChangedComparison);
|
|
480
|
+
let fresh = comparisons.filter(isNewComparison);
|
|
481
|
+
let needsReview = comparisons.filter(comparison => comparison.needs_review);
|
|
482
|
+
let baseline = context.baseline?.selected;
|
|
483
|
+
let lines = [`# Vizzly Visual Context: ${context.build?.name || context.build?.id || 'Build'}`, '', `Project: ${context.scope?.organization?.slug || 'unknown'}/${context.scope?.project?.slug || 'unknown'}`, `Build: ${context.build?.id || 'unknown'} (${context.build?.status || 'unknown'})`];
|
|
484
|
+
if (baseline) {
|
|
485
|
+
lines.push(`Approved baseline: ${baseline.name || baseline.id || 'selected'} (${baseline.approval_status || 'unknown'})`);
|
|
486
|
+
}
|
|
487
|
+
if (context.status) {
|
|
488
|
+
lines.push(`Needs review: ${context.status.needs_review ? 'yes' : 'no'} (${context.status.pending_comparisons || 0} pending comparisons)`);
|
|
489
|
+
}
|
|
490
|
+
if (context.preview?.url || context.preview?.preview_url) {
|
|
491
|
+
lines.push(`Preview: ${context.preview.url || context.preview.preview_url}`);
|
|
492
|
+
}
|
|
493
|
+
if (context.links?.build_url) {
|
|
494
|
+
lines.push(`Build URL: ${context.links.build_url}`);
|
|
495
|
+
}
|
|
496
|
+
if (context.links?.report_url) {
|
|
497
|
+
lines.push(`Report: ${context.links.report_url}`);
|
|
498
|
+
}
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push('## Diff Summary');
|
|
501
|
+
lines.push(`- Total comparisons: ${comparisons.length}`);
|
|
502
|
+
lines.push(`- Changed: ${changed.length}`);
|
|
503
|
+
lines.push(`- New: ${fresh.length}`);
|
|
504
|
+
lines.push(`- Needs review: ${needsReview.length}`);
|
|
505
|
+
if (changed.length > 0 || fresh.length > 0) {
|
|
506
|
+
lines.push('');
|
|
507
|
+
lines.push('## Evidence To Inspect');
|
|
508
|
+
for (let comparison of [...changed, ...fresh].slice(0, 10)) {
|
|
509
|
+
let diffPercentage = getComparisonDiffPercentage(comparison);
|
|
510
|
+
let detail = diffPercentage == null ? '' : ` · ${diffPercentage}% diff`;
|
|
511
|
+
let diffUrl = comparison.diff?.image_url || comparison.analysis?.diff_image_url;
|
|
512
|
+
lines.push(`- ${getComparisonName(comparison)}: ${getComparisonDisplayState(comparison)}${detail}`);
|
|
513
|
+
if (diffUrl) {
|
|
514
|
+
lines.push(` Diff: ${diffUrl}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (comparisons.length > 0 && changed.length === 0 && fresh.length === 0) {
|
|
519
|
+
lines.push('');
|
|
520
|
+
lines.push('## Reviewed Screenshots');
|
|
521
|
+
for (let comparison of comparisons.slice(0, 10)) {
|
|
522
|
+
lines.push(`- ${getComparisonName(comparison)}: ${getComparisonDisplayState(comparison)}`);
|
|
523
|
+
}
|
|
524
|
+
if (comparisons.length > 10) {
|
|
525
|
+
lines.push(`- ...${comparisons.length - 10} more`);
|
|
526
|
+
}
|
|
235
527
|
}
|
|
528
|
+
lines.push('');
|
|
529
|
+
lines.push('Use this as reviewed UI context. Treat approved baselines as visual truth, inspect meaningful diffs, and leave approval decisions to humans.');
|
|
530
|
+
return lines.join('\n');
|
|
236
531
|
}
|
|
237
532
|
function countScreenshotCommentEntries(groups = []) {
|
|
238
533
|
return groups.reduce((total, group) => total + (group.comments?.length || 0), 0);
|
|
@@ -335,14 +630,28 @@ export async function contextBuildCommand(buildId, options = {}, globalOptions =
|
|
|
335
630
|
if (!runtime) {
|
|
336
631
|
return;
|
|
337
632
|
}
|
|
633
|
+
let resolvedBuildId = resolveBuildContextId(buildId, runtime, deps);
|
|
338
634
|
output.startSpinner('Fetching build context...');
|
|
339
|
-
let context = await runtime.provider.getBuildContext(
|
|
635
|
+
let context = await runtime.provider.getBuildContext(resolvedBuildId);
|
|
340
636
|
output.stopSpinner();
|
|
637
|
+
if (globalOptions.json && options.agent && !options.full) {
|
|
638
|
+
output.data(buildAgentBuildPayload(context, {
|
|
639
|
+
source: runtime.source,
|
|
640
|
+
include: parseIncludeOption(options.include)
|
|
641
|
+
}));
|
|
642
|
+
output.cleanup();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
341
645
|
if (globalOptions.json) {
|
|
342
646
|
output.data(context);
|
|
343
647
|
output.cleanup();
|
|
344
648
|
return;
|
|
345
649
|
}
|
|
650
|
+
if (options.agent) {
|
|
651
|
+
output.print(formatAgentBuildContext(context));
|
|
652
|
+
output.cleanup();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
346
655
|
displayBuildContext(output, context);
|
|
347
656
|
output.cleanup();
|
|
348
657
|
} catch (error) {
|
|
@@ -513,7 +822,9 @@ export async function contextReviewQueueCommand(options = {}, globalOptions = {}
|
|
|
513
822
|
}
|
|
514
823
|
}
|
|
515
824
|
export function validateContextBuildOptions(_options = {}) {
|
|
516
|
-
|
|
825
|
+
let errors = validateSourceOption(_options.source);
|
|
826
|
+
errors.push(...validateIncludeOption(_options.include));
|
|
827
|
+
return errors;
|
|
517
828
|
}
|
|
518
829
|
export function validateContextComparisonOptions(options = {}) {
|
|
519
830
|
let errors = [];
|