@vizzly-testing/cli 0.35.1 → 0.35.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/api/endpoints.js +24 -3
- package/dist/api/index.js +1 -1
- package/dist/cli.js +16 -16
- package/dist/commands/comparisons.js +52 -45
- package/dist/commands/context.js +399 -135
- package/dist/commands/run.js +84 -29
- package/dist/commands/status.js +276 -59
- package/dist/commands/tdd.js +19 -3
- package/dist/context/local-workspace-provider.js +17 -7
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +5 -5
- 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 +446 -0
- package/package.json +4 -5
- package/skills/vizzly/SKILL.md +68 -101
- package/skills/vizzly/agents/openai.yaml +2 -2
- package/skills/vizzly/references/cli-context.md +75 -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/skills/vizzly/references/public-screenshots.md +0 -44
package/dist/commands/run.js
CHANGED
|
@@ -44,8 +44,23 @@ export async function resolveBuildDisplayUrl({
|
|
|
44
44
|
}
|
|
45
45
|
return undefined;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the follow-up command for a cloud run.
|
|
50
|
+
*
|
|
51
|
+
* JSON consumers need a self-contained command that returns structured
|
|
52
|
+
* evidence when executed. Human output keeps the existing readable summary.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} buildId - Cloud build ID to inspect.
|
|
55
|
+
* @param {Object} options - Command output options.
|
|
56
|
+
* @param {boolean} [options.structured=false] - Include machine-readable JSON.
|
|
57
|
+
* @returns {string} Executable build context command.
|
|
58
|
+
*/
|
|
59
|
+
function buildContextCommand(buildId, {
|
|
60
|
+
structured = false
|
|
61
|
+
} = {}) {
|
|
62
|
+
let jsonFlag = structured ? ' --json' : '';
|
|
63
|
+
return `vizzly context build ${buildId} --agent${jsonFlag} --source cloud`;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
/**
|
|
@@ -91,6 +106,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
91
106
|
let startTime = null;
|
|
92
107
|
let isTddMode = false;
|
|
93
108
|
let config = null;
|
|
109
|
+
let result = null;
|
|
94
110
|
|
|
95
111
|
// Ensure cleanup on exit
|
|
96
112
|
let cleanup = async () => {
|
|
@@ -254,7 +270,6 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
254
270
|
output.info('Starting test execution...');
|
|
255
271
|
startTime = Date.now();
|
|
256
272
|
isTddMode = runOptions.tdd || false;
|
|
257
|
-
let result;
|
|
258
273
|
try {
|
|
259
274
|
result = await runTests({
|
|
260
275
|
runOptions,
|
|
@@ -327,7 +342,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
327
342
|
commit,
|
|
328
343
|
message
|
|
329
344
|
},
|
|
330
|
-
contextCommand: result.buildId ? buildContextCommand(result.buildId
|
|
345
|
+
contextCommand: result.buildId ? buildContextCommand(result.buildId, {
|
|
346
|
+
structured: true
|
|
347
|
+
}) : null,
|
|
331
348
|
exitCode: 0
|
|
332
349
|
};
|
|
333
350
|
output.data(jsonResult);
|
|
@@ -396,7 +413,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
396
413
|
exitCode
|
|
397
414
|
};
|
|
398
415
|
} else {
|
|
399
|
-
//
|
|
416
|
+
// This should only be reached for failures outside the SDK paths that
|
|
417
|
+
// runTests handles by disabling Vizzly and running the child command.
|
|
400
418
|
if (globalOptions.json) {
|
|
401
419
|
let executionTimeMs = Date.now() - (startTime || Date.now());
|
|
402
420
|
output.data({
|
|
@@ -443,9 +461,22 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
443
461
|
getTokenContext
|
|
444
462
|
});
|
|
445
463
|
let exitCode = buildResult.failedComparisons > 0 ? 1 : 0;
|
|
464
|
+
let comparisons = {};
|
|
465
|
+
if (buildResult.totalComparisons !== undefined) {
|
|
466
|
+
comparisons.total = buildResult.totalComparisons;
|
|
467
|
+
}
|
|
468
|
+
if (buildResult.newComparisons !== undefined) {
|
|
469
|
+
comparisons.new = buildResult.newComparisons;
|
|
470
|
+
}
|
|
471
|
+
if (buildResult.failedComparisons !== undefined) {
|
|
472
|
+
comparisons.changed = buildResult.failedComparisons;
|
|
473
|
+
}
|
|
474
|
+
if (buildResult.identicalComparisons !== undefined) {
|
|
475
|
+
comparisons.identical = buildResult.identicalComparisons;
|
|
476
|
+
}
|
|
446
477
|
let jsonResult = {
|
|
447
478
|
buildId: result.buildId,
|
|
448
|
-
status: buildResult.
|
|
479
|
+
status: buildResult.status,
|
|
449
480
|
url: displayUrl,
|
|
450
481
|
screenshotsCaptured: result.screenshotsCaptured || 0,
|
|
451
482
|
executionTimeMs,
|
|
@@ -454,16 +485,20 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
454
485
|
commit,
|
|
455
486
|
message
|
|
456
487
|
},
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
changed: buildResult.failedComparisons || 0,
|
|
461
|
-
identical: buildResult.identicalComparisons || 0
|
|
462
|
-
},
|
|
463
|
-
approvalStatus: buildResult.approvalStatus || 'pending',
|
|
464
|
-
contextCommand: buildContextCommand(result.buildId),
|
|
488
|
+
contextCommand: buildContextCommand(result.buildId, {
|
|
489
|
+
structured: true
|
|
490
|
+
}),
|
|
465
491
|
exitCode
|
|
466
492
|
};
|
|
493
|
+
if (Object.keys(comparisons).length > 0) {
|
|
494
|
+
jsonResult.comparisons = comparisons;
|
|
495
|
+
}
|
|
496
|
+
if (buildResult.approvalStatus !== undefined) {
|
|
497
|
+
jsonResult.approvalStatus = buildResult.approvalStatus;
|
|
498
|
+
}
|
|
499
|
+
if (buildResult.build?.conclusion !== undefined) {
|
|
500
|
+
jsonResult.conclusion = buildResult.build.conclusion;
|
|
501
|
+
}
|
|
467
502
|
output.data(jsonResult);
|
|
468
503
|
output.cleanup();
|
|
469
504
|
return {
|
|
@@ -492,28 +527,48 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
492
527
|
} catch (error) {
|
|
493
528
|
output.stopSpinner();
|
|
494
529
|
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
530
|
+
// Once the user's tests have passed, no Vizzly-side error can change the
|
|
531
|
+
// test result. Keep that outcome while reporting that the API-backed build
|
|
532
|
+
// status is unavailable instead of inventing a terminal build state.
|
|
533
|
+
if (result) {
|
|
534
|
+
let apiBuild = error.context?.build;
|
|
535
|
+
let buildStatus = apiBuild?.status ?? null;
|
|
536
|
+
let exitCode = buildStatus === 'failed' ? 1 : 0;
|
|
537
|
+
let errorDetails = {
|
|
538
|
+
code: error.code || 'VIZZLY_STATUS_UNAVAILABLE',
|
|
539
|
+
message: error.getUserMessage ? error.getUserMessage() : error.message
|
|
540
|
+
};
|
|
541
|
+
let unavailableResult = {
|
|
542
|
+
buildId: result.buildId || null,
|
|
543
|
+
status: buildStatus,
|
|
544
|
+
testsPassed: true,
|
|
545
|
+
error: errorDetails,
|
|
546
|
+
executionTimeMs: Date.now() - (startTime || Date.now()),
|
|
547
|
+
exitCode
|
|
548
|
+
};
|
|
549
|
+
if (apiBuild?.conclusion !== undefined) {
|
|
550
|
+
unavailableResult.conclusion = apiBuild.conclusion;
|
|
551
|
+
}
|
|
552
|
+
if (result.buildId) {
|
|
553
|
+
unavailableResult.contextCommand = buildContextCommand(result.buildId, {
|
|
554
|
+
structured: true
|
|
504
555
|
});
|
|
556
|
+
}
|
|
557
|
+
if (globalOptions.json) {
|
|
558
|
+
output.data(unavailableResult);
|
|
559
|
+
} else if (buildStatus === 'failed') {
|
|
560
|
+
output.error(errorDetails.message);
|
|
505
561
|
} else {
|
|
506
|
-
output.warn(
|
|
562
|
+
output.warn(`Tests passed, but Vizzly could not confirm the build status: ${errorDetails.message}`);
|
|
507
563
|
}
|
|
508
564
|
output.cleanup();
|
|
509
|
-
output.debug('
|
|
565
|
+
output.debug('run', 'Vizzly SDK error details', {
|
|
510
566
|
error: error.message
|
|
511
567
|
});
|
|
512
568
|
return {
|
|
513
|
-
success:
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
569
|
+
success: exitCode === 0,
|
|
570
|
+
exitCode,
|
|
571
|
+
result: unavailableResult
|
|
517
572
|
};
|
|
518
573
|
}
|
|
519
574
|
|
package/dist/commands/status.js
CHANGED
|
@@ -3,18 +3,17 @@
|
|
|
3
3
|
* Uses functional API operations directly
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createApiClient as defaultCreateApiClient,
|
|
6
|
+
import { createApiClient as defaultCreateApiClient, getBuildStatus as defaultGetBuildStatus, getPreviewInfo as defaultGetPreviewInfo } from '../api/index.js';
|
|
7
7
|
import { getAppBaseUrl } from '../utils/api-url.js';
|
|
8
8
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
9
|
-
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
|
|
10
9
|
import * as defaultOutput from '../utils/output.js';
|
|
10
|
+
import { getVisualReviewState } from '../utils/visual-context-normalizers.js';
|
|
11
11
|
function createStatusDeps(deps = {}) {
|
|
12
12
|
return {
|
|
13
13
|
loadConfig: deps.loadConfig || defaultLoadConfig,
|
|
14
14
|
createApiClient: deps.createApiClient || defaultCreateApiClient,
|
|
15
|
-
|
|
15
|
+
getBuildStatus: deps.getBuildStatus || defaultGetBuildStatus,
|
|
16
16
|
getPreviewInfo: deps.getPreviewInfo || defaultGetPreviewInfo,
|
|
17
|
-
getApiUrl: deps.getApiUrl || defaultGetApiUrl,
|
|
18
17
|
output: deps.output || defaultOutput,
|
|
19
18
|
exit: deps.exit || (code => process.exit(code))
|
|
20
19
|
};
|
|
@@ -32,7 +31,7 @@ function createStatusClient({
|
|
|
32
31
|
}) {
|
|
33
32
|
return createApiClient({
|
|
34
33
|
baseUrl: config.apiUrl,
|
|
35
|
-
token: config.apiKey,
|
|
34
|
+
token: config.apiKey || config.userToken,
|
|
36
35
|
command: 'status'
|
|
37
36
|
});
|
|
38
37
|
}
|
|
@@ -52,10 +51,108 @@ async function fetchOptionalPreviewInfo(getPreviewInfo, client, buildId) {
|
|
|
52
51
|
export function normalizeBuildStatus(buildStatus) {
|
|
53
52
|
return buildStatus.build || buildStatus;
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read exact processing counts from canonical or legacy status responses.
|
|
57
|
+
*
|
|
58
|
+
* Legacy `pending_screenshots` is review state, not queue state, so it is
|
|
59
|
+
* deliberately excluded. Missing processing fields remain missing instead of
|
|
60
|
+
* becoming client-authored zeroes.
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} status - Canonical status bundle or legacy build record.
|
|
63
|
+
* @returns {Object|undefined} API-provided processing facts when available.
|
|
64
|
+
*/
|
|
65
|
+
export function getProcessingStatus(status = {}) {
|
|
66
|
+
if (status.processing) {
|
|
67
|
+
return status.processing;
|
|
68
|
+
}
|
|
69
|
+
let build = normalizeBuildStatus(status);
|
|
70
|
+
let processing = {};
|
|
71
|
+
let fields = [['total', build.screenshot_count], ['completed', build.completed_jobs], ['failed', build.failed_jobs], ['active', build.processing_screenshots]];
|
|
72
|
+
for (let [name, value] of fields) {
|
|
73
|
+
if (value != null) {
|
|
74
|
+
processing[name] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Object.keys(processing).length > 0 ? processing : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read comparison totals without deriving them from screenshot counts.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} status - Canonical status bundle or legacy build record.
|
|
84
|
+
* @returns {Object|undefined} API-provided comparison facts when available.
|
|
85
|
+
*/
|
|
86
|
+
export function getComparisonStatus(status = {}) {
|
|
87
|
+
if (status.comparisons) {
|
|
88
|
+
return status.comparisons;
|
|
89
|
+
}
|
|
90
|
+
let build = normalizeBuildStatus(status);
|
|
91
|
+
let comparisons = {};
|
|
92
|
+
let fields = [['total', build.total_comparisons], ['new', build.new_comparisons], ['changed', build.changed_comparisons], ['identical', build.identical_comparisons]];
|
|
93
|
+
for (let [name, value] of fields) {
|
|
94
|
+
if (value != null) {
|
|
95
|
+
comparisons[name] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return Object.keys(comparisons).length > 0 ? comparisons : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Prefer canonical Cricket review state while retaining the legacy fallback.
|
|
103
|
+
*
|
|
104
|
+
* Processing status stays separate: a pending review never means a screenshot
|
|
105
|
+
* is still being processed.
|
|
106
|
+
*
|
|
107
|
+
* @param {Object} status - Canonical status bundle or legacy build record.
|
|
108
|
+
* @returns {string|null} API-provided review state when available.
|
|
109
|
+
*/
|
|
110
|
+
export function getBuildReviewState(status = {}) {
|
|
111
|
+
let build = normalizeBuildStatus(status);
|
|
112
|
+
return getVisualReviewState(build) || getVisualReviewState(status.visualReview?.build || status.visual_review) || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Give agents concrete paths from lifecycle status into visual evidence.
|
|
117
|
+
*
|
|
118
|
+
* @param {Object} build - Build record from the status response.
|
|
119
|
+
* @returns {{label: string, command: string}[]} Executable CLI commands.
|
|
120
|
+
*/
|
|
121
|
+
export function createStatusSuggestedCommands(build = {}) {
|
|
122
|
+
if (!build.id) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
return [{
|
|
126
|
+
label: 'Inspect build context',
|
|
127
|
+
command: `vizzly --json context build ${build.id} --agent --source cloud`
|
|
128
|
+
}, {
|
|
129
|
+
label: 'List comparisons',
|
|
130
|
+
command: `vizzly --json comparisons --build ${build.id}`
|
|
131
|
+
}];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Preserve the established JSON fields while exposing canonical status facts.
|
|
136
|
+
*
|
|
137
|
+
* Every lifecycle, processing, comparison, and review value comes directly
|
|
138
|
+
* from the API. Undefined values intentionally disappear during JSON encoding
|
|
139
|
+
* rather than being presented as false certainty.
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} status - Canonical status bundle or legacy build record.
|
|
142
|
+
* @param {Object|null} previewInfo - Optional preview response.
|
|
143
|
+
* @returns {Object} Machine-readable status payload.
|
|
144
|
+
*/
|
|
145
|
+
export function createStatusData(status, previewInfo = null) {
|
|
146
|
+
let build = normalizeBuildStatus(status);
|
|
147
|
+
let processing = getProcessingStatus(status);
|
|
148
|
+
let comparisons = getComparisonStatus(status);
|
|
149
|
+
let visualReview = build.visual_review || status.visualReview?.build || status.visual_review;
|
|
56
150
|
return {
|
|
151
|
+
resource: status.resource,
|
|
152
|
+
schemaVersion: status.schema_version,
|
|
57
153
|
buildId: build.id,
|
|
58
154
|
status: build.status,
|
|
155
|
+
conclusion: status.conclusion,
|
|
59
156
|
name: build.name,
|
|
60
157
|
createdAt: build.created_at,
|
|
61
158
|
updatedAt: build.updated_at,
|
|
@@ -64,15 +161,24 @@ export function createStatusData(build, previewInfo = null) {
|
|
|
64
161
|
branch: build.branch,
|
|
65
162
|
commit: build.commit_sha,
|
|
66
163
|
commitMessage: build.commit_message,
|
|
67
|
-
screenshotsTotal: build.screenshot_count
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
164
|
+
screenshotsTotal: processing?.total ?? build.screenshot_count,
|
|
165
|
+
processing,
|
|
166
|
+
comparisonsTotal: comparisons?.total ?? build.total_comparisons,
|
|
167
|
+
comparisons,
|
|
168
|
+
newComparisons: comparisons?.new ?? build.new_comparisons,
|
|
169
|
+
changedComparisons: comparisons?.changed ?? build.changed_comparisons,
|
|
170
|
+
identicalComparisons: comparisons?.identical ?? build.identical_comparisons,
|
|
171
|
+
reviewState: getBuildReviewState(status),
|
|
172
|
+
review: status.review,
|
|
173
|
+
reviewFlow: status.reviewFlow,
|
|
174
|
+
visualReview,
|
|
72
175
|
approvalStatus: build.approval_status,
|
|
73
176
|
executionTime: build.execution_time_ms,
|
|
74
177
|
isBaseline: build.is_baseline,
|
|
75
178
|
userAgent: build.user_agent,
|
|
179
|
+
scope: status.scope,
|
|
180
|
+
links: status.links,
|
|
181
|
+
suggestedCommands: createStatusSuggestedCommands(build),
|
|
76
182
|
preview: previewInfo ? {
|
|
77
183
|
url: previewInfo.preview_url,
|
|
78
184
|
status: previewInfo.status,
|
|
@@ -95,11 +201,21 @@ export function createBuildInfo(build) {
|
|
|
95
201
|
}
|
|
96
202
|
return buildInfo;
|
|
97
203
|
}
|
|
98
|
-
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Format API comparison outcomes without filling missing buckets with zeroes.
|
|
207
|
+
*
|
|
208
|
+
* @param {Object} status - Canonical status bundle or legacy build record.
|
|
209
|
+
* @param {Object} colors - Output color helpers.
|
|
210
|
+
* @returns {string} Human-readable comparison summary.
|
|
211
|
+
*/
|
|
212
|
+
export function createComparisonStats(status, colors) {
|
|
213
|
+
let build = normalizeBuildStatus(status);
|
|
214
|
+
let comparisons = getComparisonStatus(status) || {};
|
|
99
215
|
let stats = [];
|
|
100
|
-
let newCount = build.new_comparisons
|
|
101
|
-
let changedCount = build.changed_comparisons
|
|
102
|
-
let identicalCount = build.identical_comparisons
|
|
216
|
+
let newCount = comparisons.new ?? build.new_comparisons;
|
|
217
|
+
let changedCount = comparisons.changed ?? build.changed_comparisons;
|
|
218
|
+
let identicalCount = comparisons.identical ?? build.identical_comparisons;
|
|
103
219
|
if (newCount > 0) {
|
|
104
220
|
stats.push(`${colors.brand.info(newCount)} new`);
|
|
105
221
|
}
|
|
@@ -111,27 +227,47 @@ export function createComparisonStats(build, colors) {
|
|
|
111
227
|
}
|
|
112
228
|
return stats.join(colors.brand.textMuted(' · '));
|
|
113
229
|
}
|
|
114
|
-
|
|
115
|
-
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Build the best available legacy link when the API did not return one.
|
|
233
|
+
*
|
|
234
|
+
* Slug routes match the current app, while the project-ID route remains as a
|
|
235
|
+
* compatibility fallback for older build responses.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} baseUrl - API or app base URL.
|
|
238
|
+
* @param {Object} build - Build record.
|
|
239
|
+
* @param {Object|null} scope - Canonical organization and project scope.
|
|
240
|
+
* @returns {string|null} Build URL when the response has enough identity.
|
|
241
|
+
*/
|
|
242
|
+
export function createBuildUrl(baseUrl, build, scope = null) {
|
|
243
|
+
if (!baseUrl) {
|
|
116
244
|
return null;
|
|
117
245
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
export function getProcessingProgress(build) {
|
|
124
|
-
if (build.status !== 'processing' && build.status !== 'pending') {
|
|
125
|
-
return null;
|
|
246
|
+
let organizationSlug = scope?.organization?.slug || build.organization_slug || build.org_slug;
|
|
247
|
+
let projectSlug = scope?.project?.slug || build.project_slug;
|
|
248
|
+
let appBaseUrl = getAppBaseUrl(baseUrl);
|
|
249
|
+
if (organizationSlug && projectSlug) {
|
|
250
|
+
return `${appBaseUrl}/${organizationSlug}/${projectSlug}/builds/${build.id}`;
|
|
126
251
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let processingScreenshots = build.processing_screenshots || 0;
|
|
130
|
-
let totalJobs = completedJobs + failedJobs + processingScreenshots;
|
|
131
|
-
if (totalJobs <= 0) {
|
|
132
|
-
return null;
|
|
252
|
+
if (build.project_id) {
|
|
253
|
+
return `${appBaseUrl}/projects/${build.project_id}/builds/${build.id}`;
|
|
133
254
|
}
|
|
134
|
-
return
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Preserve status failure behavior while accepting the canonical conclusion.
|
|
260
|
+
*
|
|
261
|
+
* Review-required and rejected builds keep their existing successful command
|
|
262
|
+
* exit behavior. Only build or processing failures produce a failing status.
|
|
263
|
+
*
|
|
264
|
+
* @param {Object} status - Canonical status bundle or legacy build record.
|
|
265
|
+
* @returns {boolean} Whether human status should exit non-zero.
|
|
266
|
+
*/
|
|
267
|
+
export function shouldFailStatus(status) {
|
|
268
|
+
let build = normalizeBuildStatus(status);
|
|
269
|
+
let processing = getProcessingStatus(status);
|
|
270
|
+
return build.status === 'failed' || ['build_failed', 'processing_failed'].includes(status.conclusion) || (processing?.failed ?? 0) > 0;
|
|
135
271
|
}
|
|
136
272
|
function writeStatusTiming({
|
|
137
273
|
build,
|
|
@@ -149,10 +285,46 @@ function writeStatusTiming({
|
|
|
149
285
|
output.hint(`Took ${Math.round(build.execution_time_ms / 1000)}s`);
|
|
150
286
|
}
|
|
151
287
|
}
|
|
152
|
-
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Render follow-up commands after the factual status summary.
|
|
291
|
+
*
|
|
292
|
+
* Status answers lifecycle questions; these commands are the explicit bridge
|
|
293
|
+
* to screenshot evidence without bloating the status response itself.
|
|
294
|
+
*
|
|
295
|
+
* @param {Object} build - Build record from the status response.
|
|
296
|
+
* @param {Object} output - Configured output boundary.
|
|
297
|
+
*/
|
|
298
|
+
function writeSuggestedCommands({
|
|
299
|
+
build,
|
|
300
|
+
output
|
|
301
|
+
}) {
|
|
302
|
+
let commands = createStatusSuggestedCommands(build);
|
|
303
|
+
if (commands.length === 0) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let colors = output.getColors();
|
|
307
|
+
output.blank();
|
|
308
|
+
output.print(' Suggested commands');
|
|
309
|
+
for (let item of commands) {
|
|
310
|
+
output.print(` ${colors.brand.textMuted(item.command)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Add optional API facts without inventing placeholders for missing metadata.
|
|
316
|
+
*
|
|
317
|
+
* @param {Object} status - Canonical status bundle or legacy build record.
|
|
318
|
+
* @returns {Object} Available verbose fields.
|
|
319
|
+
*/
|
|
320
|
+
function createVerboseInfo(status) {
|
|
321
|
+
let build = normalizeBuildStatus(status);
|
|
153
322
|
let verboseInfo = {};
|
|
154
|
-
if (
|
|
155
|
-
|
|
323
|
+
if (status.review) {
|
|
324
|
+
let reviewParts = [['approved', status.review.approved], ['rejected', status.review.rejected], ['pending', status.review.pending]].filter(([, value]) => value != null).map(([label, value]) => `${value} ${label}`);
|
|
325
|
+
if (reviewParts.length > 0) {
|
|
326
|
+
verboseInfo.Review = reviewParts.join(', ');
|
|
327
|
+
}
|
|
156
328
|
}
|
|
157
329
|
if (build.avg_diff_percentage != null) {
|
|
158
330
|
verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`;
|
|
@@ -163,29 +335,72 @@ function createVerboseInfo(build) {
|
|
|
163
335
|
if (build.is_baseline) {
|
|
164
336
|
verboseInfo.Baseline = 'Yes';
|
|
165
337
|
}
|
|
166
|
-
|
|
338
|
+
if (build.user_agent) {
|
|
339
|
+
verboseInfo['User Agent'] = build.user_agent;
|
|
340
|
+
}
|
|
167
341
|
verboseInfo['Build ID'] = build.id;
|
|
168
|
-
|
|
342
|
+
if (status.scope?.project?.id || build.project_id) {
|
|
343
|
+
verboseInfo['Project ID'] = status.scope?.project?.id || build.project_id;
|
|
344
|
+
}
|
|
169
345
|
return verboseInfo;
|
|
170
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Format only the processing facts supplied by the API.
|
|
350
|
+
*
|
|
351
|
+
* @param {Object|undefined} processing - Canonical processing counts.
|
|
352
|
+
* @returns {string} Human-readable processing summary.
|
|
353
|
+
*/
|
|
354
|
+
function formatProcessingStatus(processing) {
|
|
355
|
+
if (!processing) {
|
|
356
|
+
return '';
|
|
357
|
+
}
|
|
358
|
+
let labels = {
|
|
359
|
+
total: 'total',
|
|
360
|
+
completed: 'completed',
|
|
361
|
+
failed: 'failed',
|
|
362
|
+
active: 'active',
|
|
363
|
+
pending: 'pending'
|
|
364
|
+
};
|
|
365
|
+
return Object.entries(labels).filter(([field]) => processing[field] != null).map(([field, label]) => `${processing[field]} ${label}`).join(' · ');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Present lifecycle, processing, comparisons, and review as separate facts.
|
|
370
|
+
*
|
|
371
|
+
* Keeping these lanes separate prevents review-pending counts from looking
|
|
372
|
+
* like unfinished processing and keeps the human view aligned with JSON.
|
|
373
|
+
*
|
|
374
|
+
* @param {Object} options - Human status presentation inputs.
|
|
375
|
+
*/
|
|
171
376
|
function writeHumanStatus({
|
|
172
377
|
build,
|
|
173
378
|
buildUrl,
|
|
174
379
|
globalOptions,
|
|
175
380
|
output,
|
|
176
|
-
previewInfo
|
|
381
|
+
previewInfo,
|
|
382
|
+
status
|
|
177
383
|
}) {
|
|
178
384
|
output.header('status', build.status);
|
|
179
385
|
output.keyValue(createBuildInfo(build));
|
|
180
386
|
output.blank();
|
|
181
387
|
let colors = output.getColors();
|
|
182
|
-
let comparisonStats = createComparisonStats(
|
|
183
|
-
|
|
388
|
+
let comparisonStats = createComparisonStats(status, colors);
|
|
389
|
+
let processing = getProcessingStatus(status);
|
|
390
|
+
let screenshotsTotal = processing?.total ?? build.screenshot_count;
|
|
391
|
+
let processingSummary = formatProcessingStatus(processing);
|
|
392
|
+
if (screenshotsTotal != null) {
|
|
393
|
+
output.labelValue('Screenshots', String(screenshotsTotal));
|
|
394
|
+
}
|
|
395
|
+
if (processingSummary) {
|
|
396
|
+
output.labelValue('Processing', processingSummary);
|
|
397
|
+
}
|
|
184
398
|
if (comparisonStats) {
|
|
185
399
|
output.labelValue('Comparisons', comparisonStats);
|
|
186
400
|
}
|
|
187
|
-
|
|
188
|
-
|
|
401
|
+
let reviewState = getBuildReviewState(status);
|
|
402
|
+
if (reviewState) {
|
|
403
|
+
output.labelValue('Review', reviewState);
|
|
189
404
|
}
|
|
190
405
|
output.blank();
|
|
191
406
|
writeStatusTiming({
|
|
@@ -207,17 +422,20 @@ function writeHumanStatus({
|
|
|
207
422
|
output.blank();
|
|
208
423
|
output.divider();
|
|
209
424
|
output.blank();
|
|
210
|
-
output.keyValue(createVerboseInfo(
|
|
211
|
-
}
|
|
212
|
-
let progress = getProcessingProgress(build);
|
|
213
|
-
if (progress !== null) {
|
|
214
|
-
output.blank();
|
|
215
|
-
output.print(` ${output.progressBar(progress, 100)} ${Math.round(progress)}%`);
|
|
425
|
+
output.keyValue(createVerboseInfo(status));
|
|
216
426
|
}
|
|
427
|
+
writeSuggestedCommands({
|
|
428
|
+
build,
|
|
429
|
+
output
|
|
430
|
+
});
|
|
217
431
|
}
|
|
218
432
|
|
|
219
433
|
/**
|
|
220
|
-
*
|
|
434
|
+
* Fetch and present the server-owned build status contract.
|
|
435
|
+
*
|
|
436
|
+
* Preview remains an optional adjacent resource, and established 5xx and
|
|
437
|
+
* build-failure exit semantics remain unchanged.
|
|
438
|
+
*
|
|
221
439
|
* @param {string} buildId - Build ID to check status for
|
|
222
440
|
* @param {Object} options - Command options
|
|
223
441
|
* @param {Object} globalOptions - Global CLI options
|
|
@@ -227,9 +445,8 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
227
445
|
let {
|
|
228
446
|
loadConfig,
|
|
229
447
|
createApiClient,
|
|
230
|
-
|
|
448
|
+
getBuildStatus,
|
|
231
449
|
getPreviewInfo,
|
|
232
|
-
getApiUrl,
|
|
233
450
|
output,
|
|
234
451
|
exit
|
|
235
452
|
} = createStatusDeps(deps);
|
|
@@ -243,8 +460,8 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
243
460
|
let config = await loadConfig(globalOptions.config, allOptions);
|
|
244
461
|
|
|
245
462
|
// Validate API token
|
|
246
|
-
if (!config.apiKey) {
|
|
247
|
-
output.error('
|
|
463
|
+
if (!config.apiKey && !config.userToken) {
|
|
464
|
+
output.error('Authentication required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"');
|
|
248
465
|
output.cleanup();
|
|
249
466
|
exit(1);
|
|
250
467
|
return {
|
|
@@ -261,7 +478,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
261
478
|
createApiClient,
|
|
262
479
|
config
|
|
263
480
|
});
|
|
264
|
-
let buildStatus = await
|
|
481
|
+
let buildStatus = await getBuildStatus(client, buildId);
|
|
265
482
|
|
|
266
483
|
// Also fetch preview info (if exists)
|
|
267
484
|
let previewInfo = await fetchOptionalPreviewInfo(getPreviewInfo, client, buildId);
|
|
@@ -272,26 +489,26 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
272
489
|
|
|
273
490
|
// Output in JSON mode
|
|
274
491
|
if (globalOptions.json) {
|
|
275
|
-
output.data(createStatusData(
|
|
492
|
+
output.data(createStatusData(buildStatus, previewInfo));
|
|
276
493
|
output.cleanup();
|
|
277
494
|
return;
|
|
278
495
|
}
|
|
279
496
|
|
|
280
497
|
// Human-readable output
|
|
281
498
|
// Show build URL if we can construct it
|
|
282
|
-
let
|
|
283
|
-
let buildUrl = createBuildUrl(baseUrl, build);
|
|
499
|
+
let buildUrl = buildStatus.links?.web || createBuildUrl(config.apiUrl, build, buildStatus.scope);
|
|
284
500
|
writeHumanStatus({
|
|
285
501
|
build,
|
|
286
502
|
buildUrl,
|
|
287
503
|
globalOptions,
|
|
288
504
|
output,
|
|
289
|
-
previewInfo
|
|
505
|
+
previewInfo,
|
|
506
|
+
status: buildStatus
|
|
290
507
|
});
|
|
291
508
|
output.cleanup();
|
|
292
509
|
|
|
293
510
|
// Exit with appropriate code based on build status
|
|
294
|
-
if (shouldFailStatus(
|
|
511
|
+
if (shouldFailStatus(buildStatus)) {
|
|
295
512
|
exit(1);
|
|
296
513
|
}
|
|
297
514
|
} catch (error) {
|