@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.
Files changed (38) hide show
  1. package/README.md +16 -18
  2. package/dist/cli.js +177 -2
  3. package/dist/client/index.js +144 -77
  4. package/dist/commands/doctor.js +118 -33
  5. package/dist/commands/finalize.js +8 -3
  6. package/dist/commands/init.js +13 -18
  7. package/dist/commands/login.js +42 -49
  8. package/dist/commands/logout.js +13 -5
  9. package/dist/commands/project.js +95 -67
  10. package/dist/commands/run.js +32 -6
  11. package/dist/commands/status.js +81 -50
  12. package/dist/commands/tdd-daemon.js +61 -32
  13. package/dist/commands/tdd.js +14 -26
  14. package/dist/commands/upload.js +18 -9
  15. package/dist/commands/whoami.js +40 -38
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +204 -22
  18. package/dist/server/handlers/tdd-handler.js +113 -7
  19. package/dist/server/http-server.js +9 -3
  20. package/dist/server/routers/baseline.js +58 -0
  21. package/dist/server/routers/dashboard.js +10 -6
  22. package/dist/server/routers/screenshot.js +32 -0
  23. package/dist/server-manager/core.js +5 -2
  24. package/dist/server-manager/operations.js +2 -1
  25. package/dist/services/config-service.js +306 -0
  26. package/dist/tdd/tdd-service.js +190 -126
  27. package/dist/types/client.d.ts +25 -2
  28. package/dist/utils/colors.js +187 -39
  29. package/dist/utils/config-loader.js +3 -6
  30. package/dist/utils/context.js +228 -0
  31. package/dist/utils/output.js +449 -14
  32. package/docs/api-reference.md +173 -8
  33. package/docs/tui-elements.md +560 -0
  34. package/package.json +13 -7
  35. package/dist/report-generator/core.js +0 -315
  36. package/dist/report-generator/index.js +0 -8
  37. package/dist/report-generator/operations.js +0 -196
  38. package/dist/services/static-report-generator.js +0 -65
@@ -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
- const auth = await getAuthTokens();
28
+ let auth = await getAuthTokens();
27
29
  if (!auth || !auth.accessToken) {
28
30
  output.error('Not authenticated');
29
- output.blank();
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.blank();
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.blank();
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
- const orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
58
- const selectedOrg = userInfo.organizations[orgChoice - 1];
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
- const response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
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
- const projects = Array.isArray(response) ? response : response.projects || [];
69
+ let projects = Array.isArray(response) ? response : response.projects || [];
72
70
  if (projects.length === 0) {
73
71
  output.error('No projects found');
74
- output.blank();
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.info('Select a project:');
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
- const projectChoice = await promptNumber('Enter number', 1, projects.length);
88
- const selectedProject = projects[projectChoice - 1];
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
- const tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
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
- const currentDir = resolve(process.cwd());
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.success('Project configured!');
110
+ output.complete('Project configured');
115
111
  output.blank();
116
- output.info(`Project: ${selectedProject.name}`);
117
- output.info(`Organization: ${selectedOrg.name}`);
118
- output.info(`Directory: ${currentDir}`);
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
- const mappings = await getProjectMappings();
140
- const paths = Object.keys(mappings);
137
+ let mappings = await getProjectMappings();
138
+ let paths = Object.keys(mappings);
141
139
  if (paths.length === 0) {
142
- output.info('No projects configured');
143
- output.blank();
144
- output.info('Run "vizzly project:select" to configure a project');
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.info('Configured projects:');
154
- output.blank();
155
- const currentDir = resolve(process.cwd());
156
- for (const path of paths) {
157
- const mapping = mappings[path];
158
- const isCurrent = path === currentDir;
159
- const marker = isCurrent ? '→' : ' ';
160
-
161
- // Extract token string (handle both string and object formats)
162
- const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
163
- output.print(`${marker} ${path}`);
164
- output.print(` Project: ${mapping.projectName} (${mapping.projectSlug})`);
165
- output.print(` Organization: ${mapping.organizationSlug}`);
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
- output.print(` Token: ${tokenStr.substring(0, 20)}...`);
168
- output.print(` Created: ${new Date(mapping.createdAt).toLocaleString()}`);
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
- const currentDir = resolve(process.cwd());
192
- const mapping = await getProjectMapping(currentDir);
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.blank();
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
- const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
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.info('Project token:');
212
- output.blank();
213
- output.print(` ${tokenStr}`);
220
+ output.header('project:token');
221
+ output.printBox(tokenStr, {
222
+ title: 'Token'
223
+ });
214
224
  output.blank();
215
- output.info(`Project: ${mapping.projectName} (${mapping.projectSlug})`);
216
- output.info(`Organization: ${mapping.organizationSlug}`);
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
- const currentDir = resolve(process.cwd());
280
- const mapping = await getProjectMapping(currentDir);
291
+ let currentDir = resolve(process.cwd());
292
+ let mapping = await getProjectMapping(currentDir);
281
293
  if (!mapping) {
282
- output.info('No project configured for this directory');
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
- output.info('Current project configuration:');
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.info('Cancelled');
318
+ output.print(' Cancelled');
297
319
  output.cleanup();
298
320
  return;
299
321
  }
300
322
  await deleteProjectMapping(currentDir);
301
- output.success('Project configuration removed');
302
- output.blank();
303
- output.info('Run "vizzly project:select" to configure a different project');
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);
@@ -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.success('Test run completed successfully');
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.print(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
265
- if (result.url) {
266
- output.print(`🔗 Vizzly: View results at ${result.url}`);
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) {
@@ -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
- // Display build summary
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.info('\n--- Additional Details ---');
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
- output.info(`Screenshot Approvals: ${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`);
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
- output.info(`Average Diff: ${(build.avg_diff_percentage * 100).toFixed(2)}%`);
152
+ verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`;
124
153
  }
125
154
  if (build.github_pull_request_number) {
126
- output.info(`GitHub PR: #${build.github_pull_request_number}`);
155
+ verboseInfo['GitHub PR'] = `#${build.github_pull_request_number}`;
127
156
  }
128
157
  if (build.is_baseline) {
129
- output.info('This build is marked as a baseline');
158
+ verboseInfo.Baseline = 'Yes';
130
159
  }
131
- output.info(`User Agent: ${build.user_agent || 'Unknown'}`);
132
- output.info(`Build ID: ${build.id}`);
133
- output.info(`Project ID: ${build.project_id}`);
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.info(`Progress: ${Math.round(progress * 100)}% complete`);
171
+ output.blank();
172
+ output.print(` ${output.progressBar(progress * 100, 100)} ${Math.round(progress * 100)}%`);
142
173
  }
143
174
  }
144
175
  output.cleanup();