@vizzly-testing/cli 0.20.1-beta.0 → 0.20.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 +16 -18
- package/dist/cli.js +177 -2
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +118 -33
- package/dist/commands/finalize.js +8 -3
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +42 -49
- package/dist/commands/logout.js +13 -5
- package/dist/commands/project.js +95 -67
- package/dist/commands/run.js +32 -6
- package/dist/commands/status.js +81 -50
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +14 -26
- package/dist/commands/upload.js +18 -9
- package/dist/commands/whoami.js +40 -38
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +204 -22
- package/dist/server/handlers/tdd-handler.js +113 -7
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +5 -2
- package/dist/server-manager/operations.js +2 -1
- package/dist/services/config-service.js +306 -0
- package/dist/tdd/tdd-service.js +190 -126
- package/dist/types/client.d.ts +25 -2
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -7
- package/dist/report-generator/core.js +0 -315
- package/dist/report-generator/index.js +0 -8
- package/dist/report-generator/operations.js +0 -196
- package/dist/services/static-report-generator.js +0 -65
package/dist/commands/project.js
CHANGED
|
@@ -22,12 +22,13 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
22
22
|
color: !globalOptions.noColor
|
|
23
23
|
});
|
|
24
24
|
try {
|
|
25
|
+
output.header('project:select');
|
|
26
|
+
|
|
25
27
|
// Check authentication
|
|
26
|
-
|
|
28
|
+
let auth = await getAuthTokens();
|
|
27
29
|
if (!auth || !auth.accessToken) {
|
|
28
30
|
output.error('Not authenticated');
|
|
29
|
-
output.
|
|
30
|
-
output.info('Run "vizzly login" to authenticate first');
|
|
31
|
+
output.hint('Run "vizzly login" to authenticate first');
|
|
31
32
|
process.exit(1);
|
|
32
33
|
}
|
|
33
34
|
let client = createAuthClient({
|
|
@@ -41,25 +42,22 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
41
42
|
output.stopSpinner();
|
|
42
43
|
if (!userInfo.organizations || userInfo.organizations.length === 0) {
|
|
43
44
|
output.error('No organizations found');
|
|
44
|
-
output.
|
|
45
|
-
output.info('Create an organization at https://vizzly.dev');
|
|
45
|
+
output.hint('Create an organization at https://vizzly.dev');
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Select organization
|
|
50
|
-
output.
|
|
51
|
-
output.info('Select an organization:');
|
|
52
|
-
output.blank();
|
|
50
|
+
output.labelValue('Organizations', '');
|
|
53
51
|
userInfo.organizations.forEach((org, index) => {
|
|
54
52
|
output.print(` ${index + 1}. ${org.name} (@${org.slug})`);
|
|
55
53
|
});
|
|
56
54
|
output.blank();
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
let orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
|
|
56
|
+
let selectedOrg = userInfo.organizations[orgChoice - 1];
|
|
59
57
|
|
|
60
58
|
// List projects for organization
|
|
61
59
|
output.startSpinner(`Fetching projects for ${selectedOrg.name}...`);
|
|
62
|
-
|
|
60
|
+
let response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
|
|
63
61
|
headers: {
|
|
64
62
|
Authorization: `Bearer ${auth.accessToken}`,
|
|
65
63
|
'X-Organization': selectedOrg.slug
|
|
@@ -68,28 +66,26 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
68
66
|
output.stopSpinner();
|
|
69
67
|
|
|
70
68
|
// Handle both array response and object with projects property
|
|
71
|
-
|
|
69
|
+
let projects = Array.isArray(response) ? response : response.projects || [];
|
|
72
70
|
if (projects.length === 0) {
|
|
73
71
|
output.error('No projects found');
|
|
74
|
-
output.
|
|
75
|
-
output.info(`Create a project in ${selectedOrg.name} at https://vizzly.dev`);
|
|
72
|
+
output.hint(`Create a project in ${selectedOrg.name} at https://vizzly.dev`);
|
|
76
73
|
process.exit(1);
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
// Select project
|
|
80
77
|
output.blank();
|
|
81
|
-
output.
|
|
82
|
-
output.blank();
|
|
78
|
+
output.labelValue('Projects', '');
|
|
83
79
|
projects.forEach((project, index) => {
|
|
84
80
|
output.print(` ${index + 1}. ${project.name} (${project.slug})`);
|
|
85
81
|
});
|
|
86
82
|
output.blank();
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
let projectChoice = await promptNumber('Enter number', 1, projects.length);
|
|
84
|
+
let selectedProject = projects[projectChoice - 1];
|
|
89
85
|
|
|
90
86
|
// Create API token for project
|
|
91
87
|
output.startSpinner(`Creating API token for ${selectedProject.name}...`);
|
|
92
|
-
|
|
88
|
+
let tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
|
|
93
89
|
method: 'POST',
|
|
94
90
|
headers: {
|
|
95
91
|
Authorization: `Bearer ${auth.accessToken}`,
|
|
@@ -104,18 +100,20 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
|
|
|
104
100
|
output.stopSpinner();
|
|
105
101
|
|
|
106
102
|
// Save project mapping
|
|
107
|
-
|
|
103
|
+
let currentDir = resolve(process.cwd());
|
|
108
104
|
await saveProjectMapping(currentDir, {
|
|
109
105
|
token: tokenResponse.token,
|
|
110
106
|
projectSlug: selectedProject.slug,
|
|
111
107
|
projectName: selectedProject.name,
|
|
112
108
|
organizationSlug: selectedOrg.slug
|
|
113
109
|
});
|
|
114
|
-
output.
|
|
110
|
+
output.complete('Project configured');
|
|
115
111
|
output.blank();
|
|
116
|
-
output.
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
output.keyValue({
|
|
113
|
+
Project: selectedProject.name,
|
|
114
|
+
Organization: selectedOrg.name,
|
|
115
|
+
Directory: currentDir
|
|
116
|
+
});
|
|
119
117
|
output.cleanup();
|
|
120
118
|
} catch (error) {
|
|
121
119
|
output.stopSpinner();
|
|
@@ -136,12 +134,17 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
136
134
|
color: !globalOptions.noColor
|
|
137
135
|
});
|
|
138
136
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
let mappings = await getProjectMappings();
|
|
138
|
+
let paths = Object.keys(mappings);
|
|
141
139
|
if (paths.length === 0) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
if (globalOptions.json) {
|
|
141
|
+
output.data({});
|
|
142
|
+
} else {
|
|
143
|
+
output.header('project:list');
|
|
144
|
+
output.print(' No projects configured');
|
|
145
|
+
output.blank();
|
|
146
|
+
output.hint('Run "vizzly project:select" to configure a project');
|
|
147
|
+
}
|
|
145
148
|
output.cleanup();
|
|
146
149
|
return;
|
|
147
150
|
}
|
|
@@ -150,22 +153,29 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
|
|
|
150
153
|
output.cleanup();
|
|
151
154
|
return;
|
|
152
155
|
}
|
|
153
|
-
output.
|
|
154
|
-
output.
|
|
155
|
-
|
|
156
|
-
for (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
output.header('project:list');
|
|
157
|
+
let colors = output.getColors();
|
|
158
|
+
let currentDir = resolve(process.cwd());
|
|
159
|
+
for (let path of paths) {
|
|
160
|
+
let mapping = mappings[path];
|
|
161
|
+
let isCurrent = path === currentDir;
|
|
162
|
+
let marker = isCurrent ? colors.brand.amber('→') : ' ';
|
|
163
|
+
output.print(`${marker} ${isCurrent ? colors.bold(path) : path}`);
|
|
164
|
+
output.keyValue({
|
|
165
|
+
Project: `${mapping.projectName} (${mapping.projectSlug})`,
|
|
166
|
+
Org: mapping.organizationSlug
|
|
167
|
+
}, {
|
|
168
|
+
indent: 4
|
|
169
|
+
});
|
|
166
170
|
if (globalOptions.verbose) {
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
// Extract token string (handle both string and object formats)
|
|
172
|
+
let tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
|
|
173
|
+
output.hint(`Token: ${tokenStr.substring(0, 20)}...`, {
|
|
174
|
+
indent: 4
|
|
175
|
+
});
|
|
176
|
+
output.hint(`Created: ${new Date(mapping.createdAt).toLocaleString()}`, {
|
|
177
|
+
indent: 4
|
|
178
|
+
});
|
|
169
179
|
}
|
|
170
180
|
output.blank();
|
|
171
181
|
}
|
|
@@ -188,17 +198,16 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
188
198
|
color: !globalOptions.noColor
|
|
189
199
|
});
|
|
190
200
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
let currentDir = resolve(process.cwd());
|
|
202
|
+
let mapping = await getProjectMapping(currentDir);
|
|
193
203
|
if (!mapping) {
|
|
194
204
|
output.error('No project configured for this directory');
|
|
195
|
-
output.
|
|
196
|
-
output.info('Run "vizzly project:select" to configure a project');
|
|
205
|
+
output.hint('Run "vizzly project:select" to configure a project');
|
|
197
206
|
process.exit(1);
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
// Extract token string (handle both string and object formats)
|
|
201
|
-
|
|
210
|
+
let tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
|
|
202
211
|
if (globalOptions.json) {
|
|
203
212
|
output.data({
|
|
204
213
|
token: tokenStr,
|
|
@@ -208,12 +217,15 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
|
|
|
208
217
|
output.cleanup();
|
|
209
218
|
return;
|
|
210
219
|
}
|
|
211
|
-
output.
|
|
212
|
-
output.
|
|
213
|
-
|
|
220
|
+
output.header('project:token');
|
|
221
|
+
output.printBox(tokenStr, {
|
|
222
|
+
title: 'Token'
|
|
223
|
+
});
|
|
214
224
|
output.blank();
|
|
215
|
-
output.
|
|
216
|
-
|
|
225
|
+
output.keyValue({
|
|
226
|
+
Project: `${mapping.projectName} (${mapping.projectSlug})`,
|
|
227
|
+
Org: mapping.organizationSlug
|
|
228
|
+
});
|
|
217
229
|
output.cleanup();
|
|
218
230
|
} catch (error) {
|
|
219
231
|
output.error('Failed to get project token', error);
|
|
@@ -276,31 +288,47 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
|
|
|
276
288
|
color: !globalOptions.noColor
|
|
277
289
|
});
|
|
278
290
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
291
|
+
let currentDir = resolve(process.cwd());
|
|
292
|
+
let mapping = await getProjectMapping(currentDir);
|
|
281
293
|
if (!mapping) {
|
|
282
|
-
|
|
294
|
+
if (globalOptions.json) {
|
|
295
|
+
output.data({
|
|
296
|
+
removed: false,
|
|
297
|
+
reason: 'not_configured'
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
output.header('project:remove');
|
|
301
|
+
output.print(' No project configured for this directory');
|
|
302
|
+
}
|
|
283
303
|
output.cleanup();
|
|
284
304
|
return;
|
|
285
305
|
}
|
|
286
306
|
|
|
287
307
|
// Confirm removal
|
|
308
|
+
output.header('project:remove');
|
|
309
|
+
output.labelValue('Current configuration', '');
|
|
310
|
+
output.keyValue({
|
|
311
|
+
Project: `${mapping.projectName} (${mapping.projectSlug})`,
|
|
312
|
+
Org: mapping.organizationSlug,
|
|
313
|
+
Directory: currentDir
|
|
314
|
+
});
|
|
288
315
|
output.blank();
|
|
289
|
-
|
|
290
|
-
output.print(` Project: ${mapping.projectName} (${mapping.projectSlug})`);
|
|
291
|
-
output.print(` Organization: ${mapping.organizationSlug}`);
|
|
292
|
-
output.print(` Directory: ${currentDir}`);
|
|
293
|
-
output.blank();
|
|
294
|
-
const confirmed = await promptConfirm('Remove this project configuration?');
|
|
316
|
+
let confirmed = await promptConfirm('Remove this project configuration?');
|
|
295
317
|
if (!confirmed) {
|
|
296
|
-
output.
|
|
318
|
+
output.print(' Cancelled');
|
|
297
319
|
output.cleanup();
|
|
298
320
|
return;
|
|
299
321
|
}
|
|
300
322
|
await deleteProjectMapping(currentDir);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
323
|
+
if (globalOptions.json) {
|
|
324
|
+
output.data({
|
|
325
|
+
removed: true
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
output.complete('Project configuration removed');
|
|
329
|
+
output.blank();
|
|
330
|
+
output.hint('Run "vizzly project:select" to configure a different project');
|
|
331
|
+
}
|
|
304
332
|
output.cleanup();
|
|
305
333
|
} catch (error) {
|
|
306
334
|
output.error('Failed to remove project configuration', error);
|
package/dist/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
7
|
-
import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild } from '../api/index.js';
|
|
7
|
+
import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
|
|
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';
|
|
@@ -28,6 +28,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
28
28
|
createApiBuild = defaultCreateApiBuild,
|
|
29
29
|
finalizeApiBuild = defaultFinalizeApiBuild,
|
|
30
30
|
getBuild = defaultGetBuild,
|
|
31
|
+
getTokenContext = defaultGetTokenContext,
|
|
31
32
|
createServerManager = defaultCreateServerManager,
|
|
32
33
|
createBuildObject = defaultCreateBuildObject,
|
|
33
34
|
createUploader = defaultCreateUploader,
|
|
@@ -257,13 +258,38 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
257
258
|
if (result.buildId) {
|
|
258
259
|
buildId = result.buildId;
|
|
259
260
|
}
|
|
260
|
-
output.
|
|
261
|
+
output.complete('Test run completed');
|
|
261
262
|
|
|
262
|
-
// Show Vizzly summary
|
|
263
|
+
// Show Vizzly summary with link to results
|
|
263
264
|
if (result.buildId) {
|
|
264
|
-
output.
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
output.blank();
|
|
266
|
+
let colors = output.getColors();
|
|
267
|
+
output.print(` ${colors.brand.textTertiary('Screenshots')} ${colors.white(result.screenshotsCaptured)}`);
|
|
268
|
+
|
|
269
|
+
// Get URL from result, or construct one as fallback
|
|
270
|
+
let displayUrl = result.url;
|
|
271
|
+
if (!displayUrl && config.apiKey) {
|
|
272
|
+
try {
|
|
273
|
+
let client = createApiClient({
|
|
274
|
+
baseUrl: config.apiUrl,
|
|
275
|
+
token: config.apiKey,
|
|
276
|
+
command: 'run'
|
|
277
|
+
});
|
|
278
|
+
let tokenContext = await getTokenContext(client);
|
|
279
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
280
|
+
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
281
|
+
displayUrl = `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// Fallback to simple URL if context fetch fails
|
|
285
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
286
|
+
displayUrl = `${baseUrl}/builds/${result.buildId}`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (displayUrl) {
|
|
290
|
+
output.print(` ${colors.brand.textTertiary('Results')} ${colors.cyan(colors.underline(displayUrl))}`);
|
|
291
|
+
} else {
|
|
292
|
+
output.print(` ${colors.brand.textTertiary('Build')} ${colors.dim(result.buildId)}`);
|
|
267
293
|
}
|
|
268
294
|
}
|
|
269
295
|
} catch (error) {
|
package/dist/commands/status.js
CHANGED
|
@@ -21,8 +21,6 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
21
21
|
color: !globalOptions.noColor
|
|
22
22
|
});
|
|
23
23
|
try {
|
|
24
|
-
output.info(`Checking status for build: ${buildId}`);
|
|
25
|
-
|
|
26
24
|
// Load configuration with CLI overrides
|
|
27
25
|
let allOptions = {
|
|
28
26
|
...globalOptions,
|
|
@@ -49,45 +47,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
49
47
|
// Extract build data from API response
|
|
50
48
|
let build = buildStatus.build || buildStatus;
|
|
51
49
|
|
|
52
|
-
//
|
|
53
|
-
output.success(`Build: ${build.name || build.id}`);
|
|
54
|
-
output.info(`Status: ${build.status.toUpperCase()}`);
|
|
55
|
-
output.info(`Environment: ${build.environment}`);
|
|
56
|
-
if (build.branch) {
|
|
57
|
-
output.info(`Branch: ${build.branch}`);
|
|
58
|
-
}
|
|
59
|
-
if (build.commit_sha) {
|
|
60
|
-
output.info(`Commit: ${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Show screenshot and comparison stats
|
|
64
|
-
output.info(`Screenshots: ${build.screenshot_count || 0} total`);
|
|
65
|
-
output.info(`Comparisons: ${build.total_comparisons || 0} total (${build.new_comparisons || 0} new, ${build.changed_comparisons || 0} changed, ${build.identical_comparisons || 0} identical)`);
|
|
66
|
-
if (build.approval_status) {
|
|
67
|
-
output.info(`Approval Status: ${build.approval_status}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Show timing information
|
|
71
|
-
if (build.created_at) {
|
|
72
|
-
output.info(`Created: ${new Date(build.created_at).toLocaleString()}`);
|
|
73
|
-
}
|
|
74
|
-
if (build.completed_at) {
|
|
75
|
-
output.info(`Completed: ${new Date(build.completed_at).toLocaleString()}`);
|
|
76
|
-
} else if (build.status !== 'completed' && build.status !== 'failed') {
|
|
77
|
-
output.info(`Started: ${new Date(build.started_at || build.created_at).toLocaleString()}`);
|
|
78
|
-
}
|
|
79
|
-
if (build.execution_time_ms) {
|
|
80
|
-
output.info(`Execution Time: ${Math.round(build.execution_time_ms / 1000)}s`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Show build URL if we can construct it
|
|
84
|
-
let baseUrl = config.baseUrl || getApiUrl();
|
|
85
|
-
if (baseUrl && build.project_id) {
|
|
86
|
-
let buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
|
|
87
|
-
output.info(`View Build: ${buildUrl}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Output JSON data for --json mode
|
|
50
|
+
// Output in JSON mode
|
|
91
51
|
if (globalOptions.json) {
|
|
92
52
|
let statusData = {
|
|
93
53
|
buildId: build.id,
|
|
@@ -111,26 +71,96 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
111
71
|
userAgent: build.user_agent
|
|
112
72
|
};
|
|
113
73
|
output.data(statusData);
|
|
74
|
+
output.cleanup();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Human-readable output
|
|
79
|
+
output.header('status', build.status);
|
|
80
|
+
|
|
81
|
+
// Build info section
|
|
82
|
+
let buildInfo = {
|
|
83
|
+
Name: build.name || build.id,
|
|
84
|
+
Status: build.status.toUpperCase(),
|
|
85
|
+
Environment: build.environment
|
|
86
|
+
};
|
|
87
|
+
if (build.branch) {
|
|
88
|
+
buildInfo.Branch = build.branch;
|
|
89
|
+
}
|
|
90
|
+
if (build.commit_sha) {
|
|
91
|
+
buildInfo.Commit = `${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`;
|
|
92
|
+
}
|
|
93
|
+
output.keyValue(buildInfo);
|
|
94
|
+
output.blank();
|
|
95
|
+
|
|
96
|
+
// Comparison stats with visual indicators
|
|
97
|
+
let colors = output.getColors();
|
|
98
|
+
let stats = [];
|
|
99
|
+
let newCount = build.new_comparisons || 0;
|
|
100
|
+
let changedCount = build.changed_comparisons || 0;
|
|
101
|
+
let identicalCount = build.identical_comparisons || 0;
|
|
102
|
+
let screenshotCount = build.screenshot_count || 0;
|
|
103
|
+
output.labelValue('Screenshots', String(screenshotCount));
|
|
104
|
+
if (newCount > 0) {
|
|
105
|
+
stats.push(`${colors.brand.info(newCount)} new`);
|
|
106
|
+
}
|
|
107
|
+
if (changedCount > 0) {
|
|
108
|
+
stats.push(`${colors.brand.warning(changedCount)} changed`);
|
|
109
|
+
}
|
|
110
|
+
if (identicalCount > 0) {
|
|
111
|
+
stats.push(`${colors.brand.success(identicalCount)} identical`);
|
|
112
|
+
}
|
|
113
|
+
if (stats.length > 0) {
|
|
114
|
+
output.labelValue('Comparisons', stats.join(colors.brand.textMuted(' · ')));
|
|
115
|
+
}
|
|
116
|
+
if (build.approval_status) {
|
|
117
|
+
output.labelValue('Approval', build.approval_status);
|
|
118
|
+
}
|
|
119
|
+
output.blank();
|
|
120
|
+
|
|
121
|
+
// Timing info
|
|
122
|
+
if (build.created_at) {
|
|
123
|
+
output.hint(`Created ${new Date(build.created_at).toLocaleString()}`);
|
|
124
|
+
}
|
|
125
|
+
if (build.completed_at) {
|
|
126
|
+
output.hint(`Completed ${new Date(build.completed_at).toLocaleString()}`);
|
|
127
|
+
} else if (build.status !== 'completed' && build.status !== 'failed') {
|
|
128
|
+
output.hint(`Started ${new Date(build.started_at || build.created_at).toLocaleString()}`);
|
|
129
|
+
}
|
|
130
|
+
if (build.execution_time_ms) {
|
|
131
|
+
output.hint(`Took ${Math.round(build.execution_time_ms / 1000)}s`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Show build URL if we can construct it
|
|
135
|
+
let baseUrl = config.baseUrl || getApiUrl();
|
|
136
|
+
if (baseUrl && build.project_id) {
|
|
137
|
+
let buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
|
|
138
|
+
output.blank();
|
|
139
|
+
output.labelValue('View', output.link('Build', buildUrl));
|
|
114
140
|
}
|
|
115
141
|
|
|
116
142
|
// Show additional info in verbose mode
|
|
117
143
|
if (globalOptions.verbose) {
|
|
118
|
-
output.
|
|
144
|
+
output.blank();
|
|
145
|
+
output.divider();
|
|
146
|
+
output.blank();
|
|
147
|
+
let verboseInfo = {};
|
|
119
148
|
if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) {
|
|
120
|
-
|
|
149
|
+
verboseInfo.Approvals = `${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`;
|
|
121
150
|
}
|
|
122
151
|
if (build.avg_diff_percentage !== null) {
|
|
123
|
-
|
|
152
|
+
verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`;
|
|
124
153
|
}
|
|
125
154
|
if (build.github_pull_request_number) {
|
|
126
|
-
|
|
155
|
+
verboseInfo['GitHub PR'] = `#${build.github_pull_request_number}`;
|
|
127
156
|
}
|
|
128
157
|
if (build.is_baseline) {
|
|
129
|
-
|
|
158
|
+
verboseInfo.Baseline = 'Yes';
|
|
130
159
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
verboseInfo['User Agent'] = build.user_agent || 'Unknown';
|
|
161
|
+
verboseInfo['Build ID'] = build.id;
|
|
162
|
+
verboseInfo['Project ID'] = build.project_id;
|
|
163
|
+
output.keyValue(verboseInfo);
|
|
134
164
|
}
|
|
135
165
|
|
|
136
166
|
// Show progress if build is still processing
|
|
@@ -138,7 +168,8 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
138
168
|
let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
|
|
139
169
|
if (totalJobs > 0) {
|
|
140
170
|
let progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
|
|
141
|
-
output.
|
|
171
|
+
output.blank();
|
|
172
|
+
output.print(` ${output.progressBar(progress * 100, 100)} ${Math.round(progress * 100)}%`);
|
|
142
173
|
}
|
|
143
174
|
}
|
|
144
175
|
output.cleanup();
|