@vizzly-testing/cli 0.13.4 → 0.15.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.
Files changed (143) hide show
  1. package/dist/cli.js +68 -68
  2. package/dist/commands/doctor.js +30 -34
  3. package/dist/commands/finalize.js +24 -23
  4. package/dist/commands/init.js +30 -28
  5. package/dist/commands/login.js +49 -55
  6. package/dist/commands/logout.js +14 -19
  7. package/dist/commands/project.js +83 -103
  8. package/dist/commands/run.js +77 -89
  9. package/dist/commands/status.js +48 -49
  10. package/dist/commands/tdd-daemon.js +90 -86
  11. package/dist/commands/tdd.js +59 -88
  12. package/dist/commands/upload.js +57 -57
  13. package/dist/commands/whoami.js +40 -45
  14. package/dist/index.js +2 -5
  15. package/dist/plugin-loader.js +15 -17
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +78 -32
  18. package/dist/sdk/index.js +36 -45
  19. package/dist/server/handlers/api-handler.js +14 -15
  20. package/dist/server/handlers/tdd-handler.js +34 -37
  21. package/dist/server/http-server.js +75 -869
  22. package/dist/server/middleware/cors.js +22 -0
  23. package/dist/server/middleware/json-parser.js +35 -0
  24. package/dist/server/middleware/response.js +79 -0
  25. package/dist/server/routers/assets.js +91 -0
  26. package/dist/server/routers/auth.js +144 -0
  27. package/dist/server/routers/baseline.js +163 -0
  28. package/dist/server/routers/cloud-proxy.js +146 -0
  29. package/dist/server/routers/config.js +126 -0
  30. package/dist/server/routers/dashboard.js +130 -0
  31. package/dist/server/routers/health.js +61 -0
  32. package/dist/server/routers/projects.js +168 -0
  33. package/dist/server/routers/screenshot.js +86 -0
  34. package/dist/services/auth-service.js +1 -1
  35. package/dist/services/build-manager.js +13 -40
  36. package/dist/services/config-service.js +2 -4
  37. package/dist/services/html-report-generator.js +6 -5
  38. package/dist/services/index.js +64 -0
  39. package/dist/services/project-service.js +121 -40
  40. package/dist/services/screenshot-server.js +9 -9
  41. package/dist/services/server-manager.js +11 -18
  42. package/dist/services/static-report-generator.js +3 -4
  43. package/dist/services/tdd-service.js +246 -103
  44. package/dist/services/test-runner.js +24 -25
  45. package/dist/services/uploader.js +5 -4
  46. package/dist/types/commands/init.d.ts +1 -2
  47. package/dist/types/index.d.ts +2 -3
  48. package/dist/types/plugin-loader.d.ts +1 -2
  49. package/dist/types/reporter/src/api/client.d.ts +178 -0
  50. package/dist/types/reporter/src/components/app-router.d.ts +1 -3
  51. package/dist/types/reporter/src/components/code-block.d.ts +4 -0
  52. package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +10 -0
  53. package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +11 -0
  54. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +14 -0
  55. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +30 -0
  56. package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +8 -0
  57. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  58. package/dist/types/reporter/src/components/comparison/fullscreen-viewer.d.ts +13 -0
  59. package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
  60. package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +9 -0
  61. package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +1 -1
  62. package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
  63. package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
  64. package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
  65. package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
  66. package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
  67. package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
  68. package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
  69. package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
  70. package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
  71. package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
  72. package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
  73. package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
  74. package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
  75. package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
  76. package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
  77. package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
  78. package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +5 -0
  79. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +5 -6
  80. package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
  81. package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
  82. package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
  83. package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
  84. package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
  85. package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
  86. package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
  87. package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
  88. package/dist/types/sdk/index.d.ts +2 -4
  89. package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
  90. package/dist/types/server/http-server.d.ts +1 -1
  91. package/dist/types/server/middleware/cors.d.ts +11 -0
  92. package/dist/types/server/middleware/json-parser.d.ts +10 -0
  93. package/dist/types/server/middleware/response.d.ts +50 -0
  94. package/dist/types/server/routers/assets.d.ts +6 -0
  95. package/dist/types/server/routers/auth.d.ts +9 -0
  96. package/dist/types/server/routers/baseline.d.ts +13 -0
  97. package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
  98. package/dist/types/server/routers/config.d.ts +9 -0
  99. package/dist/types/server/routers/dashboard.d.ts +6 -0
  100. package/dist/types/server/routers/health.d.ts +11 -0
  101. package/dist/types/server/routers/projects.d.ts +9 -0
  102. package/dist/types/server/routers/screenshot.d.ts +11 -0
  103. package/dist/types/services/build-manager.d.ts +4 -3
  104. package/dist/types/services/config-service.d.ts +2 -3
  105. package/dist/types/services/index.d.ts +7 -0
  106. package/dist/types/services/project-service.d.ts +6 -4
  107. package/dist/types/services/screenshot-server.d.ts +5 -5
  108. package/dist/types/services/server-manager.d.ts +5 -3
  109. package/dist/types/services/tdd-service.d.ts +12 -1
  110. package/dist/types/services/test-runner.d.ts +3 -3
  111. package/dist/types/utils/output.d.ts +84 -0
  112. package/dist/utils/config-loader.js +24 -48
  113. package/dist/utils/global-config.js +2 -17
  114. package/dist/utils/output.js +445 -0
  115. package/dist/utils/security.js +3 -4
  116. package/docs/api-reference.md +0 -1
  117. package/docs/plugins.md +22 -22
  118. package/package.json +3 -2
  119. package/dist/container/index.js +0 -215
  120. package/dist/services/base-service.js +0 -154
  121. package/dist/types/container/index.d.ts +0 -59
  122. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
  123. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
  124. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
  125. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
  126. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
  127. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
  128. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
  129. package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
  130. package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
  131. package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
  132. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
  133. package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
  134. package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
  135. package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
  136. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
  137. package/dist/types/services/base-service.d.ts +0 -71
  138. package/dist/types/utils/console-ui.d.ts +0 -61
  139. package/dist/types/utils/logger-factory.d.ts +0 -26
  140. package/dist/types/utils/logger.d.ts +0 -79
  141. package/dist/utils/console-ui.js +0 -241
  142. package/dist/utils/logger-factory.js +0 -76
  143. package/dist/utils/logger.js +0 -231
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from '../utils/config-loader.js';
2
- import { ConsoleUI } from '../utils/console-ui.js';
3
- import { createServiceContainer } from '../container/index.js';
2
+ import * as output from '../utils/output.js';
3
+ import { createServices } from '../services/index.js';
4
4
  import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
5
5
 
6
6
  /**
@@ -10,8 +10,7 @@ import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumbe
10
10
  * @param {Object} globalOptions - Global CLI options
11
11
  */
12
12
  export async function runCommand(testCommand, options = {}, globalOptions = {}) {
13
- // Create UI handler
14
- const ui = new ConsoleUI({
13
+ output.configure({
15
14
  json: globalOptions.json,
16
15
  verbose: globalOptions.verbose,
17
16
  color: !globalOptions.noColor
@@ -22,8 +21,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
22
21
  let isTddMode = false;
23
22
 
24
23
  // Ensure cleanup on exit
25
- const cleanup = async () => {
26
- ui.cleanup();
24
+ let cleanup = async () => {
25
+ output.cleanup();
27
26
 
28
27
  // Cancel test runner (kills process and stops server)
29
28
  if (testRunner) {
@@ -37,42 +36,37 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
37
36
  // Finalize build if we have one
38
37
  if (testRunner && buildId) {
39
38
  try {
40
- const executionTime = Date.now() - (startTime || Date.now());
39
+ let executionTime = Date.now() - (startTime || Date.now());
41
40
  await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
42
41
  } catch {
43
42
  // Silent fail on cleanup
44
43
  }
45
44
  }
46
45
  };
47
- const sigintHandler = async () => {
46
+ let sigintHandler = async () => {
48
47
  await cleanup();
49
48
  process.exit(1);
50
49
  };
51
- const exitHandler = () => ui.cleanup();
50
+ let exitHandler = () => output.cleanup();
52
51
  process.on('SIGINT', sigintHandler);
53
52
  process.on('exit', exitHandler);
54
53
  try {
55
54
  // Load configuration with CLI overrides
56
- const allOptions = {
55
+ let allOptions = {
57
56
  ...globalOptions,
58
57
  ...options
59
58
  };
60
-
61
- // Debug: Check options before loadConfig
62
- if (process.env.DEBUG_CONFIG) {
63
- console.log('[RUN] allOptions.token:', allOptions.token ? allOptions.token.substring(0, 8) + '***' : 'NONE');
64
- }
65
- const config = await loadConfig(globalOptions.config, allOptions);
66
-
67
- // Debug: Check config immediately after loadConfig
68
- if (process.env.DEBUG_CONFIG) {
69
- console.log('[RUN] Config after loadConfig:', {
70
- hasApiKey: !!config.apiKey,
71
- apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE'
72
- });
73
- }
59
+ output.debug('[RUN] Loading config', {
60
+ hasToken: !!allOptions.token
61
+ });
62
+ let config = await loadConfig(globalOptions.config, allOptions);
63
+ output.debug('[RUN] Config loaded', {
64
+ hasApiKey: !!config.apiKey,
65
+ apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE'
66
+ });
74
67
  if (globalOptions.verbose) {
75
- ui.info('Token check:', {
68
+ output.info('Token check:');
69
+ output.debug('Token details', {
76
70
  hasApiKey: !!config.apiKey,
77
71
  apiKeyType: typeof config.apiKey,
78
72
  apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? config.apiKey.substring(0, 10) + '...' : 'none',
@@ -83,18 +77,19 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
83
77
 
84
78
  // Validate API token (unless --allow-no-token is set)
85
79
  if (!config.apiKey && !config.allowNoToken) {
86
- ui.error('API token required. Use --token, set VIZZLY_TOKEN environment variable, or use --allow-no-token to run without uploading');
87
- return;
80
+ 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);
88
82
  }
89
83
 
90
84
  // Collect git metadata and build info
91
- const branch = await detectBranch(options.branch);
92
- const commit = await detectCommit(options.commit);
93
- const message = options.message || (await detectCommitMessage());
94
- const buildName = await generateBuildNameWithGit(options.buildName);
95
- const pullRequestNumber = detectPullRequestNumber();
85
+ let branch = await detectBranch(options.branch);
86
+ let commit = await detectCommit(options.commit);
87
+ let message = options.message || (await detectCommitMessage());
88
+ let buildName = await generateBuildNameWithGit(options.buildName);
89
+ let pullRequestNumber = detectPullRequestNumber();
96
90
  if (globalOptions.verbose) {
97
- ui.info('Configuration loaded', {
91
+ output.info('Configuration loaded');
92
+ output.debug('Config details', {
98
93
  testCommand,
99
94
  port: config.server.port,
100
95
  timeout: config.server.timeout,
@@ -108,77 +103,68 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
108
103
  }
109
104
 
110
105
  // Create service container and get test runner service
111
- ui.startSpinner('Initializing test runner...');
112
- const configWithVerbose = {
106
+ output.startSpinner('Initializing test runner...');
107
+ let configWithVerbose = {
113
108
  ...config,
114
109
  verbose: globalOptions.verbose,
115
110
  uploadAll: options.uploadAll || false
116
111
  };
117
-
118
- // Debug: Check config before creating container
119
- if (process.env.DEBUG_CONFIG) {
120
- console.log('[RUN] Config before container:', {
121
- hasApiKey: !!configWithVerbose.apiKey,
122
- apiKeyPrefix: configWithVerbose.apiKey ? configWithVerbose.apiKey.substring(0, 8) + '***' : 'NONE'
123
- });
124
- }
125
- const command = 'run';
126
- const container = await createServiceContainer(configWithVerbose, command);
127
- testRunner = await container.get('testRunner'); // Assign to outer scope variable
128
- ui.stopSpinner();
112
+ output.debug('[RUN] Creating services', {
113
+ hasApiKey: !!configWithVerbose.apiKey
114
+ });
115
+ let services = createServices(configWithVerbose, 'run');
116
+ testRunner = services.testRunner;
117
+ output.stopSpinner();
129
118
 
130
119
  // Track build URL for display
131
120
  let buildUrl = null;
132
121
 
133
122
  // Set up event handlers
134
123
  testRunner.on('progress', progressData => {
135
- const {
124
+ let {
136
125
  message: progressMessage
137
126
  } = progressData;
138
- ui.progress(progressMessage || 'Running tests...');
127
+ output.progress(progressMessage || 'Running tests...');
139
128
  });
140
- testRunner.on('test-output', output => {
129
+ testRunner.on('test-output', data => {
141
130
  // In non-JSON mode, show test output directly
142
131
  if (!globalOptions.json) {
143
- ui.stopSpinner();
144
- console.log(output.data);
132
+ output.stopSpinner();
133
+ output.print(data.data);
145
134
  }
146
135
  });
147
136
  testRunner.on('server-ready', serverInfo => {
148
137
  if (globalOptions.verbose) {
149
- ui.info(`Screenshot server running on port ${serverInfo.port}`);
150
- ui.info('Server details', serverInfo);
138
+ output.info(`Screenshot server running on port ${serverInfo.port}`);
139
+ output.debug('Server details', serverInfo);
151
140
  }
152
141
  });
153
142
  testRunner.on('screenshot-captured', screenshotInfo => {
154
- // Use UI for consistent formatting
155
- ui.info(`Vizzly: Screenshot captured - ${screenshotInfo.name}`);
143
+ output.info(`Vizzly: Screenshot captured - ${screenshotInfo.name}`);
156
144
  });
157
145
  testRunner.on('build-created', buildInfo => {
158
146
  buildUrl = buildInfo.url;
159
147
  buildId = buildInfo.buildId;
160
- // Debug: Log build creation details
161
148
  if (globalOptions.verbose) {
162
- ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
149
+ output.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
163
150
  }
164
- // Use UI for consistent formatting
165
151
  if (buildUrl) {
166
- ui.info(`Vizzly: ${buildUrl}`);
152
+ output.info(`Vizzly: ${buildUrl}`);
167
153
  }
168
154
  });
169
155
  testRunner.on('build-failed', buildError => {
170
- ui.error('Failed to create build', buildError);
156
+ output.error('Failed to create build', buildError);
171
157
  });
172
158
  testRunner.on('error', error => {
173
- ui.stopSpinner(); // Stop spinner to ensure error is visible
174
- ui.error('Test runner error occurred', error, 0); // Don't exit immediately, let runner handle it
159
+ output.stopSpinner();
160
+ output.error('Test runner error occurred', error);
175
161
  });
176
162
  testRunner.on('build-finalize-failed', errorInfo => {
177
- ui.warning(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
163
+ output.warn(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
178
164
  });
179
165
 
180
166
  // Prepare run options
181
- const runOptions = {
167
+ let runOptions = {
182
168
  testCommand,
183
169
  port: config.server.port,
184
170
  timeout: config.server.timeout,
@@ -197,7 +183,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
197
183
  };
198
184
 
199
185
  // Start test run
200
- ui.info('Starting test execution...');
186
+ output.info('Starting test execution...');
201
187
  startTime = Date.now();
202
188
  isTddMode = runOptions.tdd || false;
203
189
  let result;
@@ -208,32 +194,32 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
208
194
  if (result.buildId) {
209
195
  buildId = result.buildId;
210
196
  }
211
- ui.success('Test run completed successfully');
197
+ output.success('Test run completed successfully');
212
198
 
213
199
  // Show Vizzly summary
214
200
  if (result.buildId) {
215
- console.log(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
201
+ output.print(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
216
202
  if (result.url) {
217
- console.log(`🔗 Vizzly: View results at ${result.url}`);
203
+ output.print(`🔗 Vizzly: View results at ${result.url}`);
218
204
  }
219
205
  }
220
206
  } catch (error) {
221
207
  // Test execution failed - build should already be finalized by test runner
222
- ui.stopSpinner();
208
+ output.stopSpinner();
223
209
 
224
210
  // Check if it's a test command failure (as opposed to setup failure)
225
211
  if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
226
212
  // Extract exit code from error message if available
227
- const exitCodeMatch = error.message.match(/exited with code (\d+)/);
228
- const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
229
- ui.error('Test run failed');
213
+ let exitCodeMatch = error.message.match(/exited with code (\d+)/);
214
+ let exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
215
+ output.error('Test run failed');
230
216
  return {
231
217
  success: false,
232
218
  exitCode
233
219
  };
234
220
  } else {
235
221
  // Setup or other error - VizzlyError.getUserMessage() provides context
236
- ui.error('Test run failed', error);
222
+ output.error('Test run failed', error);
237
223
  return {
238
224
  success: false,
239
225
  exitCode: 1
@@ -245,16 +231,17 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
245
231
  if (result.buildId) {
246
232
  // Wait for build completion if requested
247
233
  if (runOptions.wait) {
248
- ui.info('Waiting for build completion...');
249
- ui.startSpinner('Processing comparisons...');
250
- const uploader = await container.get('uploader');
251
- const buildResult = await uploader.waitForBuild(result.buildId);
252
- ui.success('Build processing completed');
234
+ output.info('Waiting for build completion...');
235
+ output.startSpinner('Processing comparisons...');
236
+ let {
237
+ uploader
238
+ } = services;
239
+ let buildResult = await uploader.waitForBuild(result.buildId);
240
+ output.success('Build processing completed');
253
241
 
254
242
  // Exit with appropriate code based on comparison results
255
243
  if (buildResult.failedComparisons > 0) {
256
- ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {}, 0);
257
- // Return error status without calling process.exit in tests
244
+ output.error(`${buildResult.failedComparisons} visual comparisons failed`);
258
245
  return {
259
246
  success: false,
260
247
  exitCode: 1
@@ -262,9 +249,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
262
249
  }
263
250
  }
264
251
  }
265
- ui.cleanup();
252
+ output.cleanup();
266
253
  } catch (error) {
267
- ui.stopSpinner(); // Ensure spinner is stopped before showing error
254
+ output.stopSpinner();
268
255
 
269
256
  // Provide more context about where the error occurred
270
257
  let errorContext = 'Test run failed';
@@ -275,7 +262,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
275
262
  } else if (error.message && error.message.includes('server')) {
276
263
  errorContext = 'Server startup failed';
277
264
  }
278
- ui.error(errorContext, error);
265
+ output.error(errorContext, error);
266
+ process.exit(1);
279
267
  } finally {
280
268
  // Remove event listeners to prevent memory leaks
281
269
  process.removeListener('SIGINT', sigintHandler);
@@ -289,30 +277,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
289
277
  * @param {Object} options - Command options
290
278
  */
291
279
  export function validateRunOptions(testCommand, options) {
292
- const errors = [];
280
+ let errors = [];
293
281
  if (!testCommand || testCommand.trim() === '') {
294
282
  errors.push('Test command is required');
295
283
  }
296
284
  if (options.port) {
297
- const port = parseInt(options.port, 10);
285
+ let port = parseInt(options.port, 10);
298
286
  if (isNaN(port) || port < 1 || port > 65535) {
299
287
  errors.push('Port must be a valid number between 1 and 65535');
300
288
  }
301
289
  }
302
290
  if (options.timeout) {
303
- const timeout = parseInt(options.timeout, 10);
291
+ let timeout = parseInt(options.timeout, 10);
304
292
  if (isNaN(timeout) || timeout < 1000) {
305
293
  errors.push('Timeout must be at least 1000 milliseconds');
306
294
  }
307
295
  }
308
296
  if (options.batchSize !== undefined) {
309
- const n = parseInt(options.batchSize, 10);
297
+ let n = parseInt(options.batchSize, 10);
310
298
  if (!Number.isFinite(n) || n <= 0) {
311
299
  errors.push('Batch size must be a positive integer');
312
300
  }
313
301
  }
314
302
  if (options.uploadTimeout !== undefined) {
315
- const n = parseInt(options.uploadTimeout, 10);
303
+ let n = parseInt(options.uploadTimeout, 10);
316
304
  if (!Number.isFinite(n) || n <= 0) {
317
305
  errors.push('Upload timeout must be a positive integer (milliseconds)');
318
306
  }
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from '../utils/config-loader.js';
2
- import { ConsoleUI } from '../utils/console-ui.js';
3
- import { createServiceContainer } from '../container/index.js';
2
+ import * as output from '../utils/output.js';
3
+ import { createServices } from '../services/index.js';
4
4
  import { getApiUrl } from '../utils/environment-config.js';
5
5
 
6
6
  /**
@@ -10,84 +10,82 @@ import { getApiUrl } from '../utils/environment-config.js';
10
10
  * @param {Object} globalOptions - Global CLI options
11
11
  */
12
12
  export async function statusCommand(buildId, options = {}, globalOptions = {}) {
13
- // Create UI handler
14
- const ui = new ConsoleUI({
13
+ output.configure({
15
14
  json: globalOptions.json,
16
15
  verbose: globalOptions.verbose,
17
16
  color: !globalOptions.noColor
18
17
  });
19
-
20
- // Note: ConsoleUI handles cleanup via global process listeners
21
-
22
18
  try {
23
- ui.info(`Checking status for build: ${buildId}`);
19
+ output.info(`Checking status for build: ${buildId}`);
24
20
 
25
21
  // Load configuration with CLI overrides
26
- const allOptions = {
22
+ let allOptions = {
27
23
  ...globalOptions,
28
24
  ...options
29
25
  };
30
- const config = await loadConfig(globalOptions.config, allOptions);
26
+ let config = await loadConfig(globalOptions.config, allOptions);
31
27
 
32
28
  // Validate API token
33
29
  if (!config.apiKey) {
34
- ui.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
35
- return;
30
+ output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
31
+ process.exit(1);
36
32
  }
37
33
 
38
34
  // Get API service
39
- ui.startSpinner('Fetching build status...');
40
- const container = await createServiceContainer(config, 'status');
41
- const apiService = await container.get('apiService');
35
+ output.startSpinner('Fetching build status...');
36
+ let services = createServices(config, 'status');
37
+ let {
38
+ apiService
39
+ } = services;
42
40
 
43
41
  // Get build details via unified ApiService
44
- const buildStatus = await apiService.getBuild(buildId);
45
- ui.stopSpinner();
42
+ let buildStatus = await apiService.getBuild(buildId);
43
+ output.stopSpinner();
46
44
 
47
45
  // Extract build data from API response
48
- const build = buildStatus.build || buildStatus;
46
+ let build = buildStatus.build || buildStatus;
49
47
 
50
48
  // Display build summary
51
- ui.success(`Build: ${build.name || build.id}`);
52
- ui.info(`Status: ${build.status.toUpperCase()}`);
53
- ui.info(`Environment: ${build.environment}`);
49
+ output.success(`Build: ${build.name || build.id}`);
50
+ output.info(`Status: ${build.status.toUpperCase()}`);
51
+ output.info(`Environment: ${build.environment}`);
54
52
  if (build.branch) {
55
- ui.info(`Branch: ${build.branch}`);
53
+ output.info(`Branch: ${build.branch}`);
56
54
  }
57
55
  if (build.commit_sha) {
58
- ui.info(`Commit: ${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`);
56
+ output.info(`Commit: ${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`);
59
57
  }
60
58
 
61
59
  // Show screenshot and comparison stats
62
- ui.info(`Screenshots: ${build.screenshot_count || 0} total`);
63
- ui.info(`Comparisons: ${build.total_comparisons || 0} total (${build.new_comparisons || 0} new, ${build.changed_comparisons || 0} changed, ${build.identical_comparisons || 0} identical)`);
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)`);
64
62
  if (build.approval_status) {
65
- ui.info(`Approval Status: ${build.approval_status}`);
63
+ output.info(`Approval Status: ${build.approval_status}`);
66
64
  }
67
65
 
68
66
  // Show timing information
69
67
  if (build.created_at) {
70
- ui.info(`Created: ${new Date(build.created_at).toLocaleString()}`);
68
+ output.info(`Created: ${new Date(build.created_at).toLocaleString()}`);
71
69
  }
72
70
  if (build.completed_at) {
73
- ui.info(`Completed: ${new Date(build.completed_at).toLocaleString()}`);
71
+ output.info(`Completed: ${new Date(build.completed_at).toLocaleString()}`);
74
72
  } else if (build.status !== 'completed' && build.status !== 'failed') {
75
- ui.info(`Started: ${new Date(build.started_at || build.created_at).toLocaleString()}`);
73
+ output.info(`Started: ${new Date(build.started_at || build.created_at).toLocaleString()}`);
76
74
  }
77
75
  if (build.execution_time_ms) {
78
- ui.info(`Execution Time: ${Math.round(build.execution_time_ms / 1000)}s`);
76
+ output.info(`Execution Time: ${Math.round(build.execution_time_ms / 1000)}s`);
79
77
  }
80
78
 
81
79
  // Show build URL if we can construct it
82
- const baseUrl = config.baseUrl || getApiUrl();
80
+ let baseUrl = config.baseUrl || getApiUrl();
83
81
  if (baseUrl && build.project_id) {
84
- const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
85
- ui.info(`View Build: ${buildUrl}`);
82
+ let buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
83
+ output.info(`View Build: ${buildUrl}`);
86
84
  }
87
85
 
88
86
  // Output JSON data for --json mode
89
87
  if (globalOptions.json) {
90
- const statusData = {
88
+ let statusData = {
91
89
  buildId: build.id,
92
90
  status: build.status,
93
91
  name: build.name,
@@ -108,45 +106,46 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
108
106
  isBaseline: build.is_baseline,
109
107
  userAgent: build.user_agent
110
108
  };
111
- ui.data(statusData);
109
+ output.data(statusData);
112
110
  }
113
111
 
114
112
  // Show additional info in verbose mode
115
113
  if (globalOptions.verbose) {
116
- ui.info('\n--- Additional Details ---');
114
+ output.info('\n--- Additional Details ---');
117
115
  if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) {
118
- ui.info(`Screenshot Approvals: ${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`);
116
+ output.info(`Screenshot Approvals: ${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`);
119
117
  }
120
118
  if (build.avg_diff_percentage !== null) {
121
- ui.info(`Average Diff: ${(build.avg_diff_percentage * 100).toFixed(2)}%`);
119
+ output.info(`Average Diff: ${(build.avg_diff_percentage * 100).toFixed(2)}%`);
122
120
  }
123
121
  if (build.github_pull_request_number) {
124
- ui.info(`GitHub PR: #${build.github_pull_request_number}`);
122
+ output.info(`GitHub PR: #${build.github_pull_request_number}`);
125
123
  }
126
124
  if (build.is_baseline) {
127
- ui.info('This build is marked as a baseline');
125
+ output.info('This build is marked as a baseline');
128
126
  }
129
- ui.info(`User Agent: ${build.user_agent || 'Unknown'}`);
130
- ui.info(`Build ID: ${build.id}`);
131
- ui.info(`Project ID: ${build.project_id}`);
127
+ output.info(`User Agent: ${build.user_agent || 'Unknown'}`);
128
+ output.info(`Build ID: ${build.id}`);
129
+ output.info(`Project ID: ${build.project_id}`);
132
130
  }
133
131
 
134
132
  // Show progress if build is still processing
135
133
  if (build.status === 'processing' || build.status === 'pending') {
136
- const totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
134
+ let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
137
135
  if (totalJobs > 0) {
138
- const progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
139
- ui.info(`Progress: ${Math.round(progress * 100)}% complete`);
136
+ let progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
137
+ output.info(`Progress: ${Math.round(progress * 100)}% complete`);
140
138
  }
141
139
  }
142
- ui.cleanup();
140
+ output.cleanup();
143
141
 
144
142
  // Exit with appropriate code based on build status
145
143
  if (build.status === 'failed' || build.failed_jobs > 0) {
146
144
  process.exit(1);
147
145
  }
148
146
  } catch (error) {
149
- ui.error('Failed to get build status', error);
147
+ output.error('Failed to get build status', error);
148
+ process.exit(1);
150
149
  }
151
150
  }
152
151
 
@@ -156,7 +155,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
156
155
  * @param {Object} options - Command options
157
156
  */
158
157
  export function validateStatusOptions(buildId) {
159
- const errors = [];
158
+ let errors = [];
160
159
  if (!buildId || buildId.trim() === '') {
161
160
  errors.push('Build ID is required');
162
161
  }