@vizzly-testing/cli 0.20.0 → 0.20.1-beta.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 (84) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +178 -3
  10. package/dist/client/index.js +144 -77
  11. package/dist/commands/doctor.js +121 -36
  12. package/dist/commands/finalize.js +49 -18
  13. package/dist/commands/init.js +13 -18
  14. package/dist/commands/login.js +49 -55
  15. package/dist/commands/logout.js +17 -9
  16. package/dist/commands/project.js +100 -71
  17. package/dist/commands/run.js +189 -95
  18. package/dist/commands/status.js +101 -66
  19. package/dist/commands/tdd-daemon.js +61 -32
  20. package/dist/commands/tdd.js +104 -98
  21. package/dist/commands/upload.js +78 -34
  22. package/dist/commands/whoami.js +44 -42
  23. package/dist/config/core.js +438 -0
  24. package/dist/config/index.js +13 -0
  25. package/dist/config/operations.js +327 -0
  26. package/dist/index.js +1 -1
  27. package/dist/project/core.js +295 -0
  28. package/dist/project/index.js +13 -0
  29. package/dist/project/operations.js +393 -0
  30. package/dist/reporter/reporter-bundle.css +1 -1
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +191 -53
  38. package/dist/server/http-server.js +9 -3
  39. package/dist/server/routers/baseline.js +58 -0
  40. package/dist/server/routers/dashboard.js +10 -6
  41. package/dist/server/routers/screenshot.js +32 -0
  42. package/dist/server-manager/core.js +186 -0
  43. package/dist/server-manager/index.js +81 -0
  44. package/dist/server-manager/operations.js +209 -0
  45. package/dist/services/build-manager.js +2 -69
  46. package/dist/services/index.js +21 -48
  47. package/dist/services/screenshot-server.js +40 -74
  48. package/dist/services/server-manager.js +45 -80
  49. package/dist/services/test-runner.js +90 -250
  50. package/dist/services/uploader.js +56 -358
  51. package/dist/tdd/core/hotspot-coverage.js +112 -0
  52. package/dist/tdd/core/signature.js +101 -0
  53. package/dist/tdd/index.js +19 -0
  54. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  55. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  56. package/dist/tdd/services/baseline-downloader.js +151 -0
  57. package/dist/tdd/services/baseline-manager.js +166 -0
  58. package/dist/tdd/services/comparison-service.js +230 -0
  59. package/dist/tdd/services/hotspot-service.js +71 -0
  60. package/dist/tdd/services/result-service.js +123 -0
  61. package/dist/tdd/tdd-service.js +1145 -0
  62. package/dist/test-runner/core.js +255 -0
  63. package/dist/test-runner/index.js +13 -0
  64. package/dist/test-runner/operations.js +483 -0
  65. package/dist/types/client.d.ts +25 -2
  66. package/dist/uploader/core.js +396 -0
  67. package/dist/uploader/index.js +11 -0
  68. package/dist/uploader/operations.js +412 -0
  69. package/dist/utils/colors.js +187 -39
  70. package/dist/utils/config-loader.js +3 -6
  71. package/dist/utils/context.js +228 -0
  72. package/dist/utils/output.js +449 -14
  73. package/docs/api-reference.md +173 -8
  74. package/docs/tui-elements.md +560 -0
  75. package/package.json +13 -13
  76. package/dist/services/api-service.js +0 -412
  77. package/dist/services/auth-service.js +0 -226
  78. package/dist/services/config-service.js +0 -369
  79. package/dist/services/html-report-generator.js +0 -455
  80. package/dist/services/project-service.js +0 -326
  81. package/dist/services/report-generator/report.css +0 -411
  82. package/dist/services/report-generator/viewer.js +0 -102
  83. package/dist/services/static-report-generator.js +0 -207
  84. package/dist/services/tdd-service.js +0 -1437
@@ -1,65 +1,119 @@
1
- import { createServices } from '../services/index.js';
2
- import { loadConfig } from '../utils/config-loader.js';
3
- import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
4
- import * as output from '../utils/output.js';
1
+ /**
2
+ * Run command implementation
3
+ * Uses functional operations directly - no class wrappers needed
4
+ */
5
+
6
+ import { spawn as defaultSpawn } from 'node:child_process';
7
+ import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
8
+ import { VizzlyError } from '../errors/vizzly-error.js';
9
+ import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
10
+ import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
11
+ import { createUploader as defaultCreateUploader } from '../services/uploader.js';
12
+ import { finalizeBuild as defaultFinalizeBuild, runTests as defaultRunTests } from '../test-runner/index.js';
13
+ import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
14
+ import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
15
+ import * as defaultOutput from '../utils/output.js';
5
16
 
6
17
  /**
7
18
  * Run command implementation
8
19
  * @param {string} testCommand - Test command to execute
9
20
  * @param {Object} options - Command options
10
21
  * @param {Object} globalOptions - Global CLI options
22
+ * @param {Object} deps - Dependencies for testing
11
23
  */
12
- export async function runCommand(testCommand, options = {}, globalOptions = {}) {
24
+ export async function runCommand(testCommand, options = {}, globalOptions = {}, deps = {}) {
25
+ let {
26
+ loadConfig = defaultLoadConfig,
27
+ createApiClient = defaultCreateApiClient,
28
+ createApiBuild = defaultCreateApiBuild,
29
+ finalizeApiBuild = defaultFinalizeApiBuild,
30
+ getBuild = defaultGetBuild,
31
+ getTokenContext = defaultGetTokenContext,
32
+ createServerManager = defaultCreateServerManager,
33
+ createBuildObject = defaultCreateBuildObject,
34
+ createUploader = defaultCreateUploader,
35
+ finalizeBuild = defaultFinalizeBuild,
36
+ runTests = defaultRunTests,
37
+ detectBranch = defaultDetectBranch,
38
+ detectCommit = defaultDetectCommit,
39
+ detectCommitMessage = defaultDetectCommitMessage,
40
+ detectPullRequestNumber = defaultDetectPullRequestNumber,
41
+ generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
42
+ spawn = defaultSpawn,
43
+ output = defaultOutput,
44
+ exit = code => process.exit(code),
45
+ processOn = (event, handler) => process.on(event, handler),
46
+ processRemoveListener = (event, handler) => process.removeListener(event, handler)
47
+ } = deps;
13
48
  output.configure({
14
49
  json: globalOptions.json,
15
50
  verbose: globalOptions.verbose,
16
51
  color: !globalOptions.noColor
17
52
  });
18
- let testRunner = null;
53
+ let serverManager = null;
54
+ let testProcess = null;
19
55
  let buildId = null;
20
56
  let startTime = null;
21
57
  let isTddMode = false;
58
+ let config = null;
22
59
 
23
60
  // Ensure cleanup on exit
24
- const cleanup = async () => {
61
+ let cleanup = async () => {
25
62
  output.cleanup();
26
63
 
27
- // Cancel test runner (kills process and stops server)
28
- if (testRunner) {
64
+ // Kill test process if running
65
+ if (testProcess && !testProcess.killed) {
66
+ testProcess.kill('SIGKILL');
67
+ }
68
+
69
+ // Stop server
70
+ if (serverManager) {
29
71
  try {
30
- await testRunner.cancel();
72
+ await serverManager.stop();
31
73
  } catch {
32
74
  // Silent fail
33
75
  }
34
76
  }
35
77
 
36
78
  // Finalize build if we have one
37
- if (testRunner && buildId) {
79
+ if (buildId && config) {
38
80
  try {
39
- const executionTime = Date.now() - (startTime || Date.now());
40
- await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
81
+ let executionTime = Date.now() - (startTime || Date.now());
82
+ await finalizeBuild({
83
+ buildId,
84
+ tdd: isTddMode,
85
+ success: false,
86
+ executionTime,
87
+ config,
88
+ deps: {
89
+ serverManager,
90
+ createApiClient,
91
+ finalizeApiBuild,
92
+ output
93
+ }
94
+ });
41
95
  } catch {
42
96
  // Silent fail on cleanup
43
97
  }
44
98
  }
45
99
  };
46
- const sigintHandler = async () => {
100
+ let sigintHandler = async () => {
47
101
  await cleanup();
48
- process.exit(1);
102
+ exit(1);
49
103
  };
50
- const exitHandler = () => output.cleanup();
51
- process.on('SIGINT', sigintHandler);
52
- process.on('exit', exitHandler);
104
+ let exitHandler = () => output.cleanup();
105
+ processOn('SIGINT', sigintHandler);
106
+ processOn('exit', exitHandler);
53
107
  try {
54
108
  // Load configuration with CLI overrides
55
- const allOptions = {
109
+ let allOptions = {
56
110
  ...globalOptions,
57
111
  ...options
58
112
  };
59
113
  output.debug('[RUN] Loading config', {
60
114
  hasToken: !!allOptions.token
61
115
  });
62
- const config = await loadConfig(globalOptions.config, allOptions);
116
+ config = await loadConfig(globalOptions.config, allOptions);
63
117
  output.debug('[RUN] Config loaded', {
64
118
  hasApiKey: !!config.apiKey,
65
119
  apiKeyPrefix: config.apiKey ? `${config.apiKey.substring(0, 8)}***` : 'NONE'
@@ -78,15 +132,19 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
78
132
  // Validate API token (unless --allow-no-token is set)
79
133
  if (!config.apiKey && !config.allowNoToken) {
80
134
  output.error('API token required. Use --token, set VIZZLY_TOKEN environment variable, or use --allow-no-token to run without uploading');
81
- process.exit(1);
135
+ exit(1);
136
+ return {
137
+ success: false,
138
+ reason: 'no-api-key'
139
+ };
82
140
  }
83
141
 
84
142
  // Collect git metadata and build info
85
- const branch = await detectBranch(options.branch);
86
- const commit = await detectCommit(options.commit);
87
- const message = options.message || (await detectCommitMessage());
88
- const buildName = await generateBuildNameWithGit(options.buildName);
89
- const pullRequestNumber = detectPullRequestNumber();
143
+ let branch = await detectBranch(options.branch);
144
+ let commit = await detectCommit(options.commit);
145
+ let message = options.message || (await detectCommitMessage());
146
+ let buildName = await generateBuildNameWithGit(options.buildName);
147
+ let pullRequestNumber = detectPullRequestNumber();
90
148
  if (globalOptions.verbose) {
91
149
  output.info('Configuration loaded');
92
150
  output.debug('Config details', {
@@ -102,9 +160,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
102
160
  });
103
161
  }
104
162
 
105
- // Create service container and get test runner service
163
+ // Create functional dependencies
106
164
  output.startSpinner('Initializing test runner...');
107
- const configWithVerbose = {
165
+ let configWithVerbose = {
108
166
  ...config,
109
167
  verbose: globalOptions.verbose,
110
168
  uploadAll: options.uploadAll || false
@@ -112,59 +170,29 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
112
170
  output.debug('[RUN] Creating services', {
113
171
  hasApiKey: !!configWithVerbose.apiKey
114
172
  });
115
- const services = createServices(configWithVerbose, 'run');
116
- testRunner = services.testRunner;
117
- output.stopSpinner();
118
173
 
119
- // Track build URL for display
120
- let buildUrl = null;
174
+ // Create server manager (functional object)
175
+ serverManager = createServerManager(configWithVerbose, {});
121
176
 
122
- // Set up event handlers
123
- testRunner.on('progress', progressData => {
124
- const {
125
- message: progressMessage
126
- } = progressData;
127
- output.progress(progressMessage || 'Running tests...');
128
- });
129
- testRunner.on('test-output', data => {
130
- // In non-JSON mode, show test output directly
131
- if (!globalOptions.json) {
132
- output.stopSpinner();
133
- output.print(data.data);
134
- }
135
- });
136
- testRunner.on('server-ready', serverInfo => {
137
- if (globalOptions.verbose) {
138
- output.info(`Screenshot server running on port ${serverInfo.port}`);
139
- output.debug('Server details', serverInfo);
140
- }
141
- });
142
- testRunner.on('screenshot-captured', screenshotInfo => {
143
- output.info(`Vizzly: Screenshot captured - ${screenshotInfo.name}`);
144
- });
145
- testRunner.on('build-created', buildInfo => {
146
- buildUrl = buildInfo.url;
147
- buildId = buildInfo.buildId;
148
- if (globalOptions.verbose) {
149
- output.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
150
- }
151
- if (buildUrl) {
152
- output.info(`Vizzly: ${buildUrl}`);
177
+ // Create build manager (functional object)
178
+ let buildManager = {
179
+ async createBuild(buildOptions) {
180
+ return createBuildObject(buildOptions);
153
181
  }
182
+ };
183
+
184
+ // Create uploader for --wait functionality
185
+ let uploader = createUploader({
186
+ ...configWithVerbose,
187
+ command: 'run'
154
188
  });
155
- testRunner.on('build-failed', buildError => {
156
- output.error('Failed to create build', buildError);
157
- });
158
- testRunner.on('error', error => {
159
- output.stopSpinner();
160
- output.error('Test runner error occurred', error);
161
- });
162
- testRunner.on('build-finalize-failed', errorInfo => {
163
- output.warn(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
164
- });
189
+ output.stopSpinner();
190
+
191
+ // Track build URL for display
192
+ let buildUrl = null;
165
193
 
166
194
  // Prepare run options
167
- const runOptions = {
195
+ let runOptions = {
168
196
  testCommand,
169
197
  port: config.server.port,
170
198
  timeout: config.server.timeout,
@@ -188,19 +216,80 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
188
216
  isTddMode = runOptions.tdd || false;
189
217
  let result;
190
218
  try {
191
- result = await testRunner.run(runOptions);
219
+ result = await runTests({
220
+ runOptions,
221
+ config: configWithVerbose,
222
+ deps: {
223
+ serverManager,
224
+ buildManager,
225
+ spawn: (command, spawnOptions) => {
226
+ let proc = spawn(command, spawnOptions);
227
+ testProcess = proc;
228
+ return proc;
229
+ },
230
+ createApiClient,
231
+ createApiBuild,
232
+ getBuild,
233
+ finalizeApiBuild,
234
+ createError: (msg, code) => new VizzlyError(msg, code),
235
+ output,
236
+ onBuildCreated: data => {
237
+ buildUrl = data.url;
238
+ buildId = data.buildId;
239
+ if (globalOptions.verbose) {
240
+ output.info(`Build created: ${data.buildId}`);
241
+ }
242
+ if (buildUrl) {
243
+ output.info(`Vizzly: ${buildUrl}`);
244
+ }
245
+ },
246
+ onServerReady: data => {
247
+ if (globalOptions.verbose) {
248
+ output.info(`Screenshot server running on port ${data.port}`);
249
+ }
250
+ },
251
+ onFinalizeFailed: data => {
252
+ output.warn(`Failed to finalize build ${data.buildId}: ${data.error}`);
253
+ }
254
+ }
255
+ });
192
256
 
193
257
  // Store buildId for cleanup purposes
194
258
  if (result.buildId) {
195
259
  buildId = result.buildId;
196
260
  }
197
- output.success('Test run completed successfully');
261
+ output.complete('Test run completed');
198
262
 
199
- // Show Vizzly summary
263
+ // Show Vizzly summary with link to results
200
264
  if (result.buildId) {
201
- output.print(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
202
- if (result.url) {
203
- 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)}`);
204
293
  }
205
294
  }
206
295
  } catch (error) {
@@ -210,8 +299,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
210
299
  // Check if it's a test command failure (as opposed to setup failure)
211
300
  if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
212
301
  // Extract exit code from error message if available
213
- const exitCodeMatch = error.message.match(/exited with code (\d+)/);
214
- const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
302
+ let exitCodeMatch = error.message.match(/exited with code (\d+)/);
303
+ let exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
215
304
  output.error('Test run failed');
216
305
  return {
217
306
  success: false,
@@ -233,10 +322,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
233
322
  if (runOptions.wait) {
234
323
  output.info('Waiting for build completion...');
235
324
  output.startSpinner('Processing comparisons...');
236
- const {
237
- uploader
238
- } = services;
239
- const buildResult = await uploader.waitForBuild(result.buildId);
325
+ let buildResult = await uploader.waitForBuild(result.buildId);
240
326
  output.success('Build processing completed');
241
327
 
242
328
  // Exit with appropriate code based on comparison results
@@ -250,6 +336,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
250
336
  }
251
337
  }
252
338
  output.cleanup();
339
+ return {
340
+ success: true,
341
+ result
342
+ };
253
343
  } catch (error) {
254
344
  output.stopSpinner();
255
345
 
@@ -263,11 +353,15 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
263
353
  errorContext = 'Server startup failed';
264
354
  }
265
355
  output.error(errorContext, error);
266
- process.exit(1);
356
+ exit(1);
357
+ return {
358
+ success: false,
359
+ error
360
+ };
267
361
  } finally {
268
362
  // Remove event listeners to prevent memory leaks
269
- process.removeListener('SIGINT', sigintHandler);
270
- process.removeListener('exit', exitHandler);
363
+ processRemoveListener('SIGINT', sigintHandler);
364
+ processRemoveListener('exit', exitHandler);
271
365
  }
272
366
  }
273
367
 
@@ -277,30 +371,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
277
371
  * @param {Object} options - Command options
278
372
  */
279
373
  export function validateRunOptions(testCommand, options) {
280
- const errors = [];
374
+ let errors = [];
281
375
  if (!testCommand || testCommand.trim() === '') {
282
376
  errors.push('Test command is required');
283
377
  }
284
378
  if (options.port) {
285
- const port = parseInt(options.port, 10);
379
+ let port = parseInt(options.port, 10);
286
380
  if (Number.isNaN(port) || port < 1 || port > 65535) {
287
381
  errors.push('Port must be a valid number between 1 and 65535');
288
382
  }
289
383
  }
290
384
  if (options.timeout) {
291
- const timeout = parseInt(options.timeout, 10);
385
+ let timeout = parseInt(options.timeout, 10);
292
386
  if (Number.isNaN(timeout) || timeout < 1000) {
293
387
  errors.push('Timeout must be at least 1000 milliseconds');
294
388
  }
295
389
  }
296
390
  if (options.batchSize !== undefined) {
297
- const n = parseInt(options.batchSize, 10);
391
+ let n = parseInt(options.batchSize, 10);
298
392
  if (!Number.isFinite(n) || n <= 0) {
299
393
  errors.push('Batch size must be a positive integer');
300
394
  }
301
395
  }
302
396
  if (options.uploadTimeout !== undefined) {
303
- const n = parseInt(options.uploadTimeout, 10);
397
+ let n = parseInt(options.uploadTimeout, 10);
304
398
  if (!Number.isFinite(n) || n <= 0) {
305
399
  errors.push('Upload timeout must be a positive integer (milliseconds)');
306
400
  }
@@ -1,4 +1,9 @@
1
- import { createServices } from '../services/index.js';
1
+ /**
2
+ * Status command implementation
3
+ * Uses functional API operations directly
4
+ */
5
+
6
+ import { createApiClient, getBuild } from '../api/index.js';
2
7
  import { loadConfig } from '../utils/config-loader.js';
3
8
  import { getApiUrl } from '../utils/environment-config.js';
4
9
  import * as output from '../utils/output.js';
@@ -16,14 +21,12 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
16
21
  color: !globalOptions.noColor
17
22
  });
18
23
  try {
19
- output.info(`Checking status for build: ${buildId}`);
20
-
21
24
  // Load configuration with CLI overrides
22
- const allOptions = {
25
+ let allOptions = {
23
26
  ...globalOptions,
24
27
  ...options
25
28
  };
26
- const config = await loadConfig(globalOptions.config, allOptions);
29
+ let config = await loadConfig(globalOptions.config, allOptions);
27
30
 
28
31
  // Validate API token
29
32
  if (!config.apiKey) {
@@ -31,61 +34,22 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
31
34
  process.exit(1);
32
35
  }
33
36
 
34
- // Get API service
37
+ // Get build details via functional API
35
38
  output.startSpinner('Fetching build status...');
36
- const services = createServices(config, 'status');
37
- const {
38
- apiService
39
- } = services;
40
-
41
- // Get build details via unified ApiService
42
- const buildStatus = await apiService.getBuild(buildId);
39
+ let client = createApiClient({
40
+ baseUrl: config.apiUrl,
41
+ token: config.apiKey,
42
+ command: 'status'
43
+ });
44
+ let buildStatus = await getBuild(client, buildId);
43
45
  output.stopSpinner();
44
46
 
45
47
  // Extract build data from API response
46
- const build = buildStatus.build || buildStatus;
47
-
48
- // Display build summary
49
- output.success(`Build: ${build.name || build.id}`);
50
- output.info(`Status: ${build.status.toUpperCase()}`);
51
- output.info(`Environment: ${build.environment}`);
52
- if (build.branch) {
53
- output.info(`Branch: ${build.branch}`);
54
- }
55
- if (build.commit_sha) {
56
- output.info(`Commit: ${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`);
57
- }
58
-
59
- // Show screenshot and comparison stats
60
- output.info(`Screenshots: ${build.screenshot_count || 0} total`);
61
- output.info(`Comparisons: ${build.total_comparisons || 0} total (${build.new_comparisons || 0} new, ${build.changed_comparisons || 0} changed, ${build.identical_comparisons || 0} identical)`);
62
- if (build.approval_status) {
63
- output.info(`Approval Status: ${build.approval_status}`);
64
- }
65
-
66
- // Show timing information
67
- if (build.created_at) {
68
- output.info(`Created: ${new Date(build.created_at).toLocaleString()}`);
69
- }
70
- if (build.completed_at) {
71
- output.info(`Completed: ${new Date(build.completed_at).toLocaleString()}`);
72
- } else if (build.status !== 'completed' && build.status !== 'failed') {
73
- output.info(`Started: ${new Date(build.started_at || build.created_at).toLocaleString()}`);
74
- }
75
- if (build.execution_time_ms) {
76
- output.info(`Execution Time: ${Math.round(build.execution_time_ms / 1000)}s`);
77
- }
78
-
79
- // Show build URL if we can construct it
80
- const baseUrl = config.baseUrl || getApiUrl();
81
- if (baseUrl && build.project_id) {
82
- const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
83
- output.info(`View Build: ${buildUrl}`);
84
- }
48
+ let build = buildStatus.build || buildStatus;
85
49
 
86
- // Output JSON data for --json mode
50
+ // Output in JSON mode
87
51
  if (globalOptions.json) {
88
- const statusData = {
52
+ let statusData = {
89
53
  buildId: build.id,
90
54
  status: build.status,
91
55
  name: build.name,
@@ -107,34 +71,105 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
107
71
  userAgent: build.user_agent
108
72
  };
109
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));
110
140
  }
111
141
 
112
142
  // Show additional info in verbose mode
113
143
  if (globalOptions.verbose) {
114
- output.info('\n--- Additional Details ---');
144
+ output.blank();
145
+ output.divider();
146
+ output.blank();
147
+ let verboseInfo = {};
115
148
  if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) {
116
- 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`;
117
150
  }
118
151
  if (build.avg_diff_percentage !== null) {
119
- output.info(`Average Diff: ${(build.avg_diff_percentage * 100).toFixed(2)}%`);
152
+ verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`;
120
153
  }
121
154
  if (build.github_pull_request_number) {
122
- output.info(`GitHub PR: #${build.github_pull_request_number}`);
155
+ verboseInfo['GitHub PR'] = `#${build.github_pull_request_number}`;
123
156
  }
124
157
  if (build.is_baseline) {
125
- output.info('This build is marked as a baseline');
158
+ verboseInfo.Baseline = 'Yes';
126
159
  }
127
- output.info(`User Agent: ${build.user_agent || 'Unknown'}`);
128
- output.info(`Build ID: ${build.id}`);
129
- 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);
130
164
  }
131
165
 
132
166
  // Show progress if build is still processing
133
167
  if (build.status === 'processing' || build.status === 'pending') {
134
- const totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
168
+ let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
135
169
  if (totalJobs > 0) {
136
- const progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
137
- output.info(`Progress: ${Math.round(progress * 100)}% complete`);
170
+ let progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
171
+ output.blank();
172
+ output.print(` ${output.progressBar(progress * 100, 100)} ${Math.round(progress * 100)}%`);
138
173
  }
139
174
  }
140
175
  output.cleanup();
@@ -155,7 +190,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
155
190
  * @param {Object} options - Command options
156
191
  */
157
192
  export function validateStatusOptions(buildId) {
158
- const errors = [];
193
+ let errors = [];
159
194
  if (!buildId || buildId.trim() === '') {
160
195
  errors.push('Build ID is required');
161
196
  }