@vizzly-testing/cli 0.30.0 → 0.31.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/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/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +82 -113
- 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 +11 -8
- 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/run.js
CHANGED
|
@@ -8,12 +8,44 @@ import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateA
|
|
|
8
8
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
9
9
|
import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
|
|
10
10
|
import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
|
|
11
|
-
import { createUploader as defaultCreateUploader } from '../services/uploader.js';
|
|
12
11
|
import { finalizeBuild as defaultFinalizeBuild, runTests as defaultRunTests } from '../test-runner/index.js';
|
|
12
|
+
import { createUploader as defaultCreateUploader } from '../uploader/index.js';
|
|
13
|
+
import { getAppBaseUrl } from '../utils/api-url.js';
|
|
13
14
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
14
15
|
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
15
16
|
import * as defaultOutput from '../utils/output.js';
|
|
16
17
|
import { writeSession as defaultWriteSession } from '../utils/session.js';
|
|
18
|
+
export async function resolveBuildDisplayUrl({
|
|
19
|
+
result,
|
|
20
|
+
config,
|
|
21
|
+
createApiClient = defaultCreateApiClient,
|
|
22
|
+
getTokenContext = defaultGetTokenContext
|
|
23
|
+
}) {
|
|
24
|
+
if (result.url) {
|
|
25
|
+
return result.url;
|
|
26
|
+
}
|
|
27
|
+
if (!config.apiKey) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
let baseUrl = getAppBaseUrl(config.apiUrl);
|
|
31
|
+
try {
|
|
32
|
+
let client = createApiClient({
|
|
33
|
+
baseUrl: config.apiUrl,
|
|
34
|
+
token: config.apiKey,
|
|
35
|
+
command: 'run'
|
|
36
|
+
});
|
|
37
|
+
let tokenContext = await getTokenContext(client);
|
|
38
|
+
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
39
|
+
return `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return `${baseUrl}/builds/${result.buildId}`;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function buildContextCommand(buildId) {
|
|
47
|
+
return `vizzly context build ${buildId} --agent`;
|
|
48
|
+
}
|
|
17
49
|
|
|
18
50
|
/**
|
|
19
51
|
* Run command implementation
|
|
@@ -275,27 +307,12 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
275
307
|
// JSON output mode - output structured data and exit
|
|
276
308
|
if (globalOptions.json) {
|
|
277
309
|
let executionTimeMs = Date.now() - startTime;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
baseUrl: config.apiUrl,
|
|
285
|
-
token: config.apiKey,
|
|
286
|
-
command: 'run'
|
|
287
|
-
});
|
|
288
|
-
let tokenContext = await getTokenContext(client);
|
|
289
|
-
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
290
|
-
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
291
|
-
displayUrl = `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
292
|
-
}
|
|
293
|
-
} catch {
|
|
294
|
-
// Fallback to simple URL if context fetch fails
|
|
295
|
-
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
296
|
-
displayUrl = `${baseUrl}/builds/${result.buildId}`;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
310
|
+
let displayUrl = await resolveBuildDisplayUrl({
|
|
311
|
+
result,
|
|
312
|
+
config,
|
|
313
|
+
createApiClient,
|
|
314
|
+
getTokenContext
|
|
315
|
+
});
|
|
299
316
|
let jsonResult = {
|
|
300
317
|
buildId: result.buildId,
|
|
301
318
|
status: 'completed',
|
|
@@ -307,6 +324,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
307
324
|
commit,
|
|
308
325
|
message
|
|
309
326
|
},
|
|
327
|
+
contextCommand: result.buildId ? buildContextCommand(result.buildId) : null,
|
|
310
328
|
exitCode: 0
|
|
311
329
|
};
|
|
312
330
|
output.data(jsonResult);
|
|
@@ -323,32 +341,18 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
323
341
|
output.blank();
|
|
324
342
|
let colors = output.getColors();
|
|
325
343
|
output.print(` ${colors.brand.textTertiary('Screenshots')} ${colors.white(result.screenshotsCaptured)}`);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
baseUrl: config.apiUrl,
|
|
333
|
-
token: config.apiKey,
|
|
334
|
-
command: 'run'
|
|
335
|
-
});
|
|
336
|
-
let tokenContext = await getTokenContext(client);
|
|
337
|
-
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
338
|
-
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
339
|
-
displayUrl = `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
340
|
-
}
|
|
341
|
-
} catch {
|
|
342
|
-
// Fallback to simple URL if context fetch fails
|
|
343
|
-
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
344
|
-
displayUrl = `${baseUrl}/builds/${result.buildId}`;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
344
|
+
let displayUrl = await resolveBuildDisplayUrl({
|
|
345
|
+
result,
|
|
346
|
+
config,
|
|
347
|
+
createApiClient,
|
|
348
|
+
getTokenContext
|
|
349
|
+
});
|
|
347
350
|
if (displayUrl) {
|
|
348
351
|
output.print(` ${colors.brand.textTertiary('Results')} ${colors.cyan(colors.underline(displayUrl))}`);
|
|
349
352
|
} else {
|
|
350
353
|
output.print(` ${colors.brand.textTertiary('Build')} ${colors.dim(result.buildId)}`);
|
|
351
354
|
}
|
|
355
|
+
output.print(` ${colors.brand.textTertiary('Context')} ${colors.dim(buildContextCommand(result.buildId))}`);
|
|
352
356
|
}
|
|
353
357
|
} catch (error) {
|
|
354
358
|
// Test execution failed - build should already be finalized by test runner
|
|
@@ -358,7 +362,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
358
362
|
if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
|
|
359
363
|
// Extract exit code from error message if available
|
|
360
364
|
let exitCodeMatch = error.message.match(/exited with code (\d+)/);
|
|
361
|
-
let exitCode = exitCodeMatch ?
|
|
365
|
+
let exitCode = exitCodeMatch ? Number(exitCodeMatch[1]) : 1;
|
|
362
366
|
|
|
363
367
|
// JSON output for test command failure
|
|
364
368
|
if (globalOptions.json) {
|
|
@@ -427,26 +431,12 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
427
431
|
// JSON output for --wait mode
|
|
428
432
|
if (globalOptions.json) {
|
|
429
433
|
let executionTimeMs = Date.now() - startTime;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
baseUrl: config.apiUrl,
|
|
437
|
-
token: config.apiKey,
|
|
438
|
-
command: 'run'
|
|
439
|
-
});
|
|
440
|
-
let tokenContext = await getTokenContext(client);
|
|
441
|
-
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
442
|
-
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
443
|
-
displayUrl = `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
444
|
-
}
|
|
445
|
-
} catch {
|
|
446
|
-
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
447
|
-
displayUrl = `${baseUrl}/builds/${result.buildId}`;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
434
|
+
let displayUrl = await resolveBuildDisplayUrl({
|
|
435
|
+
result,
|
|
436
|
+
config,
|
|
437
|
+
createApiClient,
|
|
438
|
+
getTokenContext
|
|
439
|
+
});
|
|
450
440
|
let exitCode = buildResult.failedComparisons > 0 ? 1 : 0;
|
|
451
441
|
let jsonResult = {
|
|
452
442
|
buildId: result.buildId,
|
|
@@ -466,6 +456,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
466
456
|
identical: buildResult.identicalComparisons || 0
|
|
467
457
|
},
|
|
468
458
|
approvalStatus: buildResult.approvalStatus || 'pending',
|
|
459
|
+
contextCommand: buildContextCommand(result.buildId),
|
|
469
460
|
exitCode
|
|
470
461
|
};
|
|
471
462
|
output.data(jsonResult);
|
|
@@ -544,28 +535,40 @@ export function validateRunOptions(testCommand, options) {
|
|
|
544
535
|
errors.push('Test command is required');
|
|
545
536
|
}
|
|
546
537
|
if (options.port) {
|
|
547
|
-
let port =
|
|
548
|
-
if (Number.
|
|
538
|
+
let port = Number(options.port);
|
|
539
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
549
540
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
550
541
|
}
|
|
551
542
|
}
|
|
552
543
|
if (options.timeout) {
|
|
553
|
-
let timeout =
|
|
554
|
-
if (Number.
|
|
544
|
+
let timeout = Number(options.timeout);
|
|
545
|
+
if (!Number.isInteger(timeout) || timeout < 1000) {
|
|
555
546
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
556
547
|
}
|
|
557
548
|
}
|
|
558
549
|
if (options.batchSize !== undefined) {
|
|
559
|
-
let n =
|
|
560
|
-
if (!Number.
|
|
550
|
+
let n = Number(options.batchSize);
|
|
551
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
561
552
|
errors.push('Batch size must be a positive integer');
|
|
562
553
|
}
|
|
563
554
|
}
|
|
564
555
|
if (options.uploadTimeout !== undefined) {
|
|
565
|
-
let n =
|
|
566
|
-
if (!Number.
|
|
556
|
+
let n = Number(options.uploadTimeout);
|
|
557
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
567
558
|
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
568
559
|
}
|
|
569
560
|
}
|
|
561
|
+
if (options.threshold !== undefined) {
|
|
562
|
+
let threshold = Number(options.threshold);
|
|
563
|
+
if (!Number.isFinite(threshold) || threshold < 0) {
|
|
564
|
+
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (options.minClusterSize !== undefined) {
|
|
568
|
+
let minClusterSize = Number(options.minClusterSize);
|
|
569
|
+
if (!Number.isInteger(minClusterSize) || minClusterSize < 1) {
|
|
570
|
+
errors.push('Min cluster size must be a positive integer');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
570
573
|
return errors;
|
|
571
574
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -4,9 +4,204 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild, getPreviewInfo as defaultGetPreviewInfo } from '../api/index.js';
|
|
7
|
+
import { getAppBaseUrl } from '../utils/api-url.js';
|
|
7
8
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
8
9
|
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
|
|
9
10
|
import * as defaultOutput from '../utils/output.js';
|
|
11
|
+
function createStatusDeps(deps = {}) {
|
|
12
|
+
return {
|
|
13
|
+
loadConfig: deps.loadConfig || defaultLoadConfig,
|
|
14
|
+
createApiClient: deps.createApiClient || defaultCreateApiClient,
|
|
15
|
+
getBuild: deps.getBuild || defaultGetBuild,
|
|
16
|
+
getPreviewInfo: deps.getPreviewInfo || defaultGetPreviewInfo,
|
|
17
|
+
getApiUrl: deps.getApiUrl || defaultGetApiUrl,
|
|
18
|
+
output: deps.output || defaultOutput,
|
|
19
|
+
exit: deps.exit || (code => process.exit(code))
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function configureOutput(output, globalOptions) {
|
|
23
|
+
output.configure({
|
|
24
|
+
json: globalOptions.json,
|
|
25
|
+
verbose: globalOptions.verbose,
|
|
26
|
+
color: !globalOptions.noColor
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function createStatusClient({
|
|
30
|
+
createApiClient,
|
|
31
|
+
config
|
|
32
|
+
}) {
|
|
33
|
+
return createApiClient({
|
|
34
|
+
baseUrl: config.apiUrl,
|
|
35
|
+
token: config.apiKey,
|
|
36
|
+
command: 'status'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function normalizeBuildStatus(buildStatus) {
|
|
40
|
+
return buildStatus.build || buildStatus;
|
|
41
|
+
}
|
|
42
|
+
export function createStatusData(build, previewInfo = null) {
|
|
43
|
+
return {
|
|
44
|
+
buildId: build.id,
|
|
45
|
+
status: build.status,
|
|
46
|
+
name: build.name,
|
|
47
|
+
createdAt: build.created_at,
|
|
48
|
+
updatedAt: build.updated_at,
|
|
49
|
+
completedAt: build.completed_at,
|
|
50
|
+
environment: build.environment,
|
|
51
|
+
branch: build.branch,
|
|
52
|
+
commit: build.commit_sha,
|
|
53
|
+
commitMessage: build.commit_message,
|
|
54
|
+
screenshotsTotal: build.screenshot_count || 0,
|
|
55
|
+
comparisonsTotal: build.total_comparisons || 0,
|
|
56
|
+
newComparisons: build.new_comparisons || 0,
|
|
57
|
+
changedComparisons: build.changed_comparisons || 0,
|
|
58
|
+
identicalComparisons: build.identical_comparisons || 0,
|
|
59
|
+
approvalStatus: build.approval_status,
|
|
60
|
+
executionTime: build.execution_time_ms,
|
|
61
|
+
isBaseline: build.is_baseline,
|
|
62
|
+
userAgent: build.user_agent,
|
|
63
|
+
preview: previewInfo ? {
|
|
64
|
+
url: previewInfo.preview_url,
|
|
65
|
+
status: previewInfo.status,
|
|
66
|
+
fileCount: previewInfo.file_count,
|
|
67
|
+
expiresAt: previewInfo.expires_at
|
|
68
|
+
} : null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function createBuildInfo(build) {
|
|
72
|
+
let buildInfo = {
|
|
73
|
+
Name: build.name || build.id,
|
|
74
|
+
Status: build.status.toUpperCase(),
|
|
75
|
+
Environment: build.environment
|
|
76
|
+
};
|
|
77
|
+
if (build.branch) {
|
|
78
|
+
buildInfo.Branch = build.branch;
|
|
79
|
+
}
|
|
80
|
+
if (build.commit_sha) {
|
|
81
|
+
buildInfo.Commit = `${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`;
|
|
82
|
+
}
|
|
83
|
+
return buildInfo;
|
|
84
|
+
}
|
|
85
|
+
export function createComparisonStats(build, colors) {
|
|
86
|
+
let stats = [];
|
|
87
|
+
let newCount = build.new_comparisons || 0;
|
|
88
|
+
let changedCount = build.changed_comparisons || 0;
|
|
89
|
+
let identicalCount = build.identical_comparisons || 0;
|
|
90
|
+
if (newCount > 0) {
|
|
91
|
+
stats.push(`${colors.brand.info(newCount)} new`);
|
|
92
|
+
}
|
|
93
|
+
if (changedCount > 0) {
|
|
94
|
+
stats.push(`${colors.brand.warning(changedCount)} changed`);
|
|
95
|
+
}
|
|
96
|
+
if (identicalCount > 0) {
|
|
97
|
+
stats.push(`${colors.brand.success(identicalCount)} identical`);
|
|
98
|
+
}
|
|
99
|
+
return stats.join(colors.brand.textMuted(' · '));
|
|
100
|
+
}
|
|
101
|
+
export function createBuildUrl(baseUrl, build) {
|
|
102
|
+
if (!baseUrl || !build.project_id) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return `${getAppBaseUrl(baseUrl)}/projects/${build.project_id}/builds/${build.id}`;
|
|
106
|
+
}
|
|
107
|
+
export function shouldFailStatus(build) {
|
|
108
|
+
return build.status === 'failed' || build.failed_jobs > 0;
|
|
109
|
+
}
|
|
110
|
+
export function getProcessingProgress(build) {
|
|
111
|
+
if (build.status !== 'processing' && build.status !== 'pending') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
let completedJobs = build.completed_jobs || 0;
|
|
115
|
+
let failedJobs = build.failed_jobs || 0;
|
|
116
|
+
let processingScreenshots = build.processing_screenshots || 0;
|
|
117
|
+
let totalJobs = completedJobs + failedJobs + processingScreenshots;
|
|
118
|
+
if (totalJobs <= 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return (completedJobs + failedJobs) / totalJobs * 100;
|
|
122
|
+
}
|
|
123
|
+
function writeStatusTiming({
|
|
124
|
+
build,
|
|
125
|
+
output
|
|
126
|
+
}) {
|
|
127
|
+
if (build.created_at) {
|
|
128
|
+
output.hint(`Created ${new Date(build.created_at).toLocaleString()}`);
|
|
129
|
+
}
|
|
130
|
+
if (build.completed_at) {
|
|
131
|
+
output.hint(`Completed ${new Date(build.completed_at).toLocaleString()}`);
|
|
132
|
+
} else if (build.status !== 'completed' && build.status !== 'failed') {
|
|
133
|
+
output.hint(`Started ${new Date(build.started_at || build.created_at).toLocaleString()}`);
|
|
134
|
+
}
|
|
135
|
+
if (build.execution_time_ms) {
|
|
136
|
+
output.hint(`Took ${Math.round(build.execution_time_ms / 1000)}s`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function createVerboseInfo(build) {
|
|
140
|
+
let verboseInfo = {};
|
|
141
|
+
if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) {
|
|
142
|
+
verboseInfo.Approvals = `${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`;
|
|
143
|
+
}
|
|
144
|
+
if (build.avg_diff_percentage != null) {
|
|
145
|
+
verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`;
|
|
146
|
+
}
|
|
147
|
+
if (build.github_pull_request_number) {
|
|
148
|
+
verboseInfo['GitHub PR'] = `#${build.github_pull_request_number}`;
|
|
149
|
+
}
|
|
150
|
+
if (build.is_baseline) {
|
|
151
|
+
verboseInfo.Baseline = 'Yes';
|
|
152
|
+
}
|
|
153
|
+
verboseInfo['User Agent'] = build.user_agent || 'Unknown';
|
|
154
|
+
verboseInfo['Build ID'] = build.id;
|
|
155
|
+
verboseInfo['Project ID'] = build.project_id;
|
|
156
|
+
return verboseInfo;
|
|
157
|
+
}
|
|
158
|
+
function writeHumanStatus({
|
|
159
|
+
build,
|
|
160
|
+
buildUrl,
|
|
161
|
+
globalOptions,
|
|
162
|
+
output,
|
|
163
|
+
previewInfo
|
|
164
|
+
}) {
|
|
165
|
+
output.header('status', build.status);
|
|
166
|
+
output.keyValue(createBuildInfo(build));
|
|
167
|
+
output.blank();
|
|
168
|
+
let colors = output.getColors();
|
|
169
|
+
let comparisonStats = createComparisonStats(build, colors);
|
|
170
|
+
output.labelValue('Screenshots', String(build.screenshot_count || 0));
|
|
171
|
+
if (comparisonStats) {
|
|
172
|
+
output.labelValue('Comparisons', comparisonStats);
|
|
173
|
+
}
|
|
174
|
+
if (build.approval_status) {
|
|
175
|
+
output.labelValue('Approval', build.approval_status);
|
|
176
|
+
}
|
|
177
|
+
output.blank();
|
|
178
|
+
writeStatusTiming({
|
|
179
|
+
build,
|
|
180
|
+
output
|
|
181
|
+
});
|
|
182
|
+
if (buildUrl) {
|
|
183
|
+
output.blank();
|
|
184
|
+
output.labelValue('View', output.link('Build', buildUrl));
|
|
185
|
+
}
|
|
186
|
+
if (previewInfo?.preview_url) {
|
|
187
|
+
output.labelValue('Preview', output.link('Preview', previewInfo.preview_url));
|
|
188
|
+
if (previewInfo.expires_at) {
|
|
189
|
+
let expiresDate = new Date(previewInfo.expires_at);
|
|
190
|
+
output.hint(`Preview expires ${expiresDate.toLocaleDateString()}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (globalOptions.verbose) {
|
|
194
|
+
output.blank();
|
|
195
|
+
output.divider();
|
|
196
|
+
output.blank();
|
|
197
|
+
output.keyValue(createVerboseInfo(build));
|
|
198
|
+
}
|
|
199
|
+
let progress = getProcessingProgress(build);
|
|
200
|
+
if (progress !== null) {
|
|
201
|
+
output.blank();
|
|
202
|
+
output.print(` ${output.progressBar(progress, 100)} ${Math.round(progress)}%`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
10
205
|
|
|
11
206
|
/**
|
|
12
207
|
* Status command implementation
|
|
@@ -17,19 +212,15 @@ import * as defaultOutput from '../utils/output.js';
|
|
|
17
212
|
*/
|
|
18
213
|
export async function statusCommand(buildId, options = {}, globalOptions = {}, deps = {}) {
|
|
19
214
|
let {
|
|
20
|
-
loadConfig
|
|
21
|
-
createApiClient
|
|
22
|
-
getBuild
|
|
23
|
-
getPreviewInfo
|
|
24
|
-
getApiUrl
|
|
25
|
-
output
|
|
26
|
-
exit
|
|
27
|
-
} = deps;
|
|
28
|
-
output
|
|
29
|
-
json: globalOptions.json,
|
|
30
|
-
verbose: globalOptions.verbose,
|
|
31
|
-
color: !globalOptions.noColor
|
|
32
|
-
});
|
|
215
|
+
loadConfig,
|
|
216
|
+
createApiClient,
|
|
217
|
+
getBuild,
|
|
218
|
+
getPreviewInfo,
|
|
219
|
+
getApiUrl,
|
|
220
|
+
output,
|
|
221
|
+
exit
|
|
222
|
+
} = createStatusDeps(deps);
|
|
223
|
+
configureOutput(output, globalOptions);
|
|
33
224
|
try {
|
|
34
225
|
// Load configuration with CLI overrides
|
|
35
226
|
let allOptions = {
|
|
@@ -41,15 +232,21 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
41
232
|
// Validate API token
|
|
42
233
|
if (!config.apiKey) {
|
|
43
234
|
output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
235
|
+
output.cleanup();
|
|
44
236
|
exit(1);
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
result: {
|
|
240
|
+
reason: 'missing_token'
|
|
241
|
+
}
|
|
242
|
+
};
|
|
45
243
|
}
|
|
46
244
|
|
|
47
245
|
// Get build details via functional API
|
|
48
246
|
output.startSpinner('Fetching build status...');
|
|
49
|
-
let client =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
command: 'status'
|
|
247
|
+
let client = createStatusClient({
|
|
248
|
+
createApiClient,
|
|
249
|
+
config
|
|
53
250
|
});
|
|
54
251
|
let buildStatus = await getBuild(client, buildId);
|
|
55
252
|
|
|
@@ -58,155 +255,35 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
58
255
|
output.stopSpinner();
|
|
59
256
|
|
|
60
257
|
// Extract build data from API response
|
|
61
|
-
let build = buildStatus
|
|
258
|
+
let build = normalizeBuildStatus(buildStatus);
|
|
62
259
|
|
|
63
260
|
// Output in JSON mode
|
|
64
261
|
if (globalOptions.json) {
|
|
65
|
-
|
|
66
|
-
buildId: build.id,
|
|
67
|
-
status: build.status,
|
|
68
|
-
name: build.name,
|
|
69
|
-
createdAt: build.created_at,
|
|
70
|
-
updatedAt: build.updated_at,
|
|
71
|
-
completedAt: build.completed_at,
|
|
72
|
-
environment: build.environment,
|
|
73
|
-
branch: build.branch,
|
|
74
|
-
commit: build.commit_sha,
|
|
75
|
-
commitMessage: build.commit_message,
|
|
76
|
-
screenshotsTotal: build.screenshot_count || 0,
|
|
77
|
-
comparisonsTotal: build.total_comparisons || 0,
|
|
78
|
-
newComparisons: build.new_comparisons || 0,
|
|
79
|
-
changedComparisons: build.changed_comparisons || 0,
|
|
80
|
-
identicalComparisons: build.identical_comparisons || 0,
|
|
81
|
-
approvalStatus: build.approval_status,
|
|
82
|
-
executionTime: build.execution_time_ms,
|
|
83
|
-
isBaseline: build.is_baseline,
|
|
84
|
-
userAgent: build.user_agent,
|
|
85
|
-
preview: previewInfo ? {
|
|
86
|
-
url: previewInfo.preview_url,
|
|
87
|
-
status: previewInfo.status,
|
|
88
|
-
fileCount: previewInfo.file_count,
|
|
89
|
-
expiresAt: previewInfo.expires_at
|
|
90
|
-
} : null
|
|
91
|
-
};
|
|
92
|
-
output.data(statusData);
|
|
262
|
+
output.data(createStatusData(build, previewInfo));
|
|
93
263
|
output.cleanup();
|
|
94
264
|
return;
|
|
95
265
|
}
|
|
96
266
|
|
|
97
267
|
// Human-readable output
|
|
98
|
-
output.header('status', build.status);
|
|
99
|
-
|
|
100
|
-
// Build info section
|
|
101
|
-
let buildInfo = {
|
|
102
|
-
Name: build.name || build.id,
|
|
103
|
-
Status: build.status.toUpperCase(),
|
|
104
|
-
Environment: build.environment
|
|
105
|
-
};
|
|
106
|
-
if (build.branch) {
|
|
107
|
-
buildInfo.Branch = build.branch;
|
|
108
|
-
}
|
|
109
|
-
if (build.commit_sha) {
|
|
110
|
-
buildInfo.Commit = `${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`;
|
|
111
|
-
}
|
|
112
|
-
output.keyValue(buildInfo);
|
|
113
|
-
output.blank();
|
|
114
|
-
|
|
115
|
-
// Comparison stats with visual indicators
|
|
116
|
-
let colors = output.getColors();
|
|
117
|
-
let stats = [];
|
|
118
|
-
let newCount = build.new_comparisons || 0;
|
|
119
|
-
let changedCount = build.changed_comparisons || 0;
|
|
120
|
-
let identicalCount = build.identical_comparisons || 0;
|
|
121
|
-
let screenshotCount = build.screenshot_count || 0;
|
|
122
|
-
output.labelValue('Screenshots', String(screenshotCount));
|
|
123
|
-
if (newCount > 0) {
|
|
124
|
-
stats.push(`${colors.brand.info(newCount)} new`);
|
|
125
|
-
}
|
|
126
|
-
if (changedCount > 0) {
|
|
127
|
-
stats.push(`${colors.brand.warning(changedCount)} changed`);
|
|
128
|
-
}
|
|
129
|
-
if (identicalCount > 0) {
|
|
130
|
-
stats.push(`${colors.brand.success(identicalCount)} identical`);
|
|
131
|
-
}
|
|
132
|
-
if (stats.length > 0) {
|
|
133
|
-
output.labelValue('Comparisons', stats.join(colors.brand.textMuted(' · ')));
|
|
134
|
-
}
|
|
135
|
-
if (build.approval_status) {
|
|
136
|
-
output.labelValue('Approval', build.approval_status);
|
|
137
|
-
}
|
|
138
|
-
output.blank();
|
|
139
|
-
|
|
140
|
-
// Timing info
|
|
141
|
-
if (build.created_at) {
|
|
142
|
-
output.hint(`Created ${new Date(build.created_at).toLocaleString()}`);
|
|
143
|
-
}
|
|
144
|
-
if (build.completed_at) {
|
|
145
|
-
output.hint(`Completed ${new Date(build.completed_at).toLocaleString()}`);
|
|
146
|
-
} else if (build.status !== 'completed' && build.status !== 'failed') {
|
|
147
|
-
output.hint(`Started ${new Date(build.started_at || build.created_at).toLocaleString()}`);
|
|
148
|
-
}
|
|
149
|
-
if (build.execution_time_ms) {
|
|
150
|
-
output.hint(`Took ${Math.round(build.execution_time_ms / 1000)}s`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
268
|
// Show build URL if we can construct it
|
|
154
269
|
let baseUrl = config.baseUrl || getApiUrl();
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
output.labelValue('Preview', output.link('Preview', previewInfo.preview_url));
|
|
164
|
-
if (previewInfo.expires_at) {
|
|
165
|
-
let expiresDate = new Date(previewInfo.expires_at);
|
|
166
|
-
output.hint(`Preview expires ${expiresDate.toLocaleDateString()}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Show additional info in verbose mode
|
|
171
|
-
if (globalOptions.verbose) {
|
|
172
|
-
output.blank();
|
|
173
|
-
output.divider();
|
|
174
|
-
output.blank();
|
|
175
|
-
let verboseInfo = {};
|
|
176
|
-
if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) {
|
|
177
|
-
verboseInfo.Approvals = `${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`;
|
|
178
|
-
}
|
|
179
|
-
if (build.avg_diff_percentage !== null) {
|
|
180
|
-
verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`;
|
|
181
|
-
}
|
|
182
|
-
if (build.github_pull_request_number) {
|
|
183
|
-
verboseInfo['GitHub PR'] = `#${build.github_pull_request_number}`;
|
|
184
|
-
}
|
|
185
|
-
if (build.is_baseline) {
|
|
186
|
-
verboseInfo.Baseline = 'Yes';
|
|
187
|
-
}
|
|
188
|
-
verboseInfo['User Agent'] = build.user_agent || 'Unknown';
|
|
189
|
-
verboseInfo['Build ID'] = build.id;
|
|
190
|
-
verboseInfo['Project ID'] = build.project_id;
|
|
191
|
-
output.keyValue(verboseInfo);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Show progress if build is still processing
|
|
195
|
-
if (build.status === 'processing' || build.status === 'pending') {
|
|
196
|
-
let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
|
|
197
|
-
if (totalJobs > 0) {
|
|
198
|
-
let progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
|
|
199
|
-
output.blank();
|
|
200
|
-
output.print(` ${output.progressBar(progress * 100, 100)} ${Math.round(progress * 100)}%`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
270
|
+
let buildUrl = createBuildUrl(baseUrl, build);
|
|
271
|
+
writeHumanStatus({
|
|
272
|
+
build,
|
|
273
|
+
buildUrl,
|
|
274
|
+
globalOptions,
|
|
275
|
+
output,
|
|
276
|
+
previewInfo
|
|
277
|
+
});
|
|
203
278
|
output.cleanup();
|
|
204
279
|
|
|
205
280
|
// Exit with appropriate code based on build status
|
|
206
|
-
if (build
|
|
281
|
+
if (shouldFailStatus(build)) {
|
|
207
282
|
exit(1);
|
|
208
283
|
}
|
|
209
284
|
} catch (error) {
|
|
285
|
+
output.stopSpinner();
|
|
286
|
+
|
|
210
287
|
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
211
288
|
let status = error.context?.status;
|
|
212
289
|
if (status >= 500) {
|
|
@@ -220,6 +297,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
220
297
|
};
|
|
221
298
|
}
|
|
222
299
|
output.error('Failed to get build status', error);
|
|
300
|
+
output.cleanup();
|
|
223
301
|
exit(1);
|
|
224
302
|
return {
|
|
225
303
|
success: false,
|