@vizzly-testing/cli 0.14.0 → 0.15.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 (140) hide show
  1. package/dist/cli.js +70 -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 +74 -41
  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/screenshot-display.d.ts +16 -0
  59. package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
  60. package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
  61. package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
  62. package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
  63. package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
  64. package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
  65. package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
  66. package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
  67. package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
  68. package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
  69. package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
  70. package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
  71. package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
  72. package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
  73. package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
  74. package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
  75. package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +1 -4
  76. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +1 -6
  77. package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
  78. package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
  79. package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
  80. package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
  81. package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
  82. package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
  83. package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
  84. package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
  85. package/dist/types/sdk/index.d.ts +2 -4
  86. package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
  87. package/dist/types/server/http-server.d.ts +1 -1
  88. package/dist/types/server/middleware/cors.d.ts +11 -0
  89. package/dist/types/server/middleware/json-parser.d.ts +10 -0
  90. package/dist/types/server/middleware/response.d.ts +50 -0
  91. package/dist/types/server/routers/assets.d.ts +6 -0
  92. package/dist/types/server/routers/auth.d.ts +9 -0
  93. package/dist/types/server/routers/baseline.d.ts +13 -0
  94. package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
  95. package/dist/types/server/routers/config.d.ts +9 -0
  96. package/dist/types/server/routers/dashboard.d.ts +6 -0
  97. package/dist/types/server/routers/health.d.ts +11 -0
  98. package/dist/types/server/routers/projects.d.ts +9 -0
  99. package/dist/types/server/routers/screenshot.d.ts +11 -0
  100. package/dist/types/services/build-manager.d.ts +4 -3
  101. package/dist/types/services/config-service.d.ts +2 -3
  102. package/dist/types/services/index.d.ts +7 -0
  103. package/dist/types/services/project-service.d.ts +6 -4
  104. package/dist/types/services/screenshot-server.d.ts +5 -5
  105. package/dist/types/services/server-manager.d.ts +5 -3
  106. package/dist/types/services/tdd-service.d.ts +12 -1
  107. package/dist/types/services/test-runner.d.ts +3 -3
  108. package/dist/types/utils/output.d.ts +84 -0
  109. package/dist/utils/config-loader.js +24 -48
  110. package/dist/utils/global-config.js +2 -17
  111. package/dist/utils/output.js +445 -0
  112. package/dist/utils/security.js +3 -4
  113. package/docs/api-reference.md +0 -1
  114. package/docs/plugins.md +33 -34
  115. package/package.json +3 -2
  116. package/dist/container/index.js +0 -215
  117. package/dist/services/base-service.js +0 -154
  118. package/dist/types/container/index.d.ts +0 -59
  119. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
  120. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
  121. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
  122. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
  123. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
  124. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
  125. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
  126. package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
  127. package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
  128. package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
  129. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
  130. package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
  131. package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
  132. package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
  133. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
  134. package/dist/types/services/base-service.d.ts +0 -71
  135. package/dist/types/utils/console-ui.d.ts +0 -61
  136. package/dist/types/utils/logger-factory.d.ts +0 -26
  137. package/dist/types/utils/logger.d.ts +0 -79
  138. package/dist/utils/console-ui.js +0 -241
  139. package/dist/utils/logger-factory.js +0 -76
  140. package/dist/utils/logger.js +0 -231
package/dist/cli.js CHANGED
@@ -16,8 +16,8 @@ import { projectSelectCommand, projectListCommand, projectTokenCommand, projectR
16
16
  import { getPackageVersion } from './utils/package-info.js';
17
17
  import { loadPlugins } from './plugin-loader.js';
18
18
  import { loadConfig } from './utils/config-loader.js';
19
- import { createComponentLogger } from './utils/logger-factory.js';
20
- import { createServiceContainer } from './container/index.js';
19
+ import { createServices } from './services/index.js';
20
+ import * as output from './utils/output.js';
21
21
  program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output').option('--json', 'Machine-readable output').option('--no-color', 'Disable colored output');
22
22
 
23
23
  // Load plugins before defining commands
@@ -32,35 +32,40 @@ for (let i = 0; i < process.argv.length; i++) {
32
32
  verboseMode = true;
33
33
  }
34
34
  }
35
- let config = await loadConfig(configPath, {});
36
- let logger = createComponentLogger('CLI', {
37
- level: config.logLevel || (verboseMode ? 'debug' : 'warn'),
38
- verbose: verboseMode || false
35
+
36
+ // Configure output early
37
+ output.configure({
38
+ verbose: verboseMode,
39
+ color: !process.argv.includes('--no-color'),
40
+ json: process.argv.includes('--json')
39
41
  });
40
- let container = await createServiceContainer(config);
42
+ let config = await loadConfig(configPath, {});
43
+ let services = createServices(config);
41
44
  let plugins = [];
42
45
  try {
43
- plugins = await loadPlugins(configPath, config, logger);
46
+ plugins = await loadPlugins(configPath, config);
44
47
  for (let plugin of plugins) {
45
48
  try {
46
49
  // Add timeout protection for plugin registration (5 seconds)
47
50
  let registerPromise = plugin.register(program, {
48
51
  config,
49
- logger,
50
- services: container
52
+ services,
53
+ output,
54
+ // Backwards compatibility alias for plugins using old API
55
+ logger: output
51
56
  });
52
57
  let timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin registration timeout (5s)')), 5000));
53
58
  await Promise.race([registerPromise, timeoutPromise]);
54
- logger.debug(`Registered plugin: ${plugin.name}`);
59
+ output.debug(`Registered plugin: ${plugin.name}`);
55
60
  } catch (error) {
56
- logger.warn(`Failed to register plugin ${plugin.name}: ${error.message}`);
61
+ output.warn(`Failed to register plugin ${plugin.name}: ${error.message}`);
57
62
  }
58
63
  }
59
64
  } catch (error) {
60
- logger.debug(`Plugin loading failed: ${error.message}`);
65
+ output.debug(`Plugin loading failed: ${error.message}`);
61
66
  }
62
67
  program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').action(async options => {
63
- const globalOptions = program.opts();
68
+ let globalOptions = program.opts();
64
69
  await init({
65
70
  ...globalOptions,
66
71
  ...options,
@@ -68,24 +73,24 @@ program.command('init').description('Initialize Vizzly in your project').option(
68
73
  });
69
74
  });
70
75
  program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (path, options) => {
71
- const globalOptions = program.opts();
76
+ let globalOptions = program.opts();
72
77
 
73
78
  // Validate options
74
- const validationErrors = validateUploadOptions(path, options);
79
+ let validationErrors = validateUploadOptions(path, options);
75
80
  if (validationErrors.length > 0) {
76
- console.error('Validation errors:');
77
- validationErrors.forEach(error => console.error(` - ${error}`));
81
+ output.error('Validation errors:');
82
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
78
83
  process.exit(1);
79
84
  }
80
85
  await uploadCommand(path, options, globalOptions);
81
86
  });
82
87
 
83
88
  // TDD command with subcommands - Local visual testing with interactive dashboard
84
- const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
89
+ let tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
85
90
 
86
91
  // TDD Start - Background server
87
92
  tddCmd.command('start').description('Start background TDD server with dashboard').option('--port <port>', 'Port for TDD server', '47392').option('--open', 'Open dashboard in browser').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--token <token>', 'API token override').option('--daemon-child', 'Internal: run as daemon child process').action(async options => {
88
- const globalOptions = program.opts();
93
+ let globalOptions = program.opts();
89
94
 
90
95
  // If this is a daemon child process, run the server directly
91
96
  if (options.daemonChild) {
@@ -97,34 +102,34 @@ tddCmd.command('start').description('Start background TDD server with dashboard'
97
102
 
98
103
  // TDD Stop - Kill background server
99
104
  tddCmd.command('stop').description('Stop background TDD server').action(async options => {
100
- const globalOptions = program.opts();
105
+ let globalOptions = program.opts();
101
106
  await tddStopCommand(options, globalOptions);
102
107
  });
103
108
 
104
109
  // TDD Status - Check server status
105
110
  tddCmd.command('status').description('Check TDD server status').action(async options => {
106
- const globalOptions = program.opts();
111
+ let globalOptions = program.opts();
107
112
  await tddStatusCommand(options, globalOptions);
108
113
  });
109
114
 
110
115
  // TDD Run - One-off test run with ephemeral server (generates static report)
111
116
  tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual comparisons').option('--port <port>', 'Port for TDD server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').action(async (command, options) => {
112
- const globalOptions = program.opts();
117
+ let globalOptions = program.opts();
113
118
 
114
119
  // Validate options
115
- const validationErrors = validateTddOptions(command, options);
120
+ let validationErrors = validateTddOptions(command, options);
116
121
  if (validationErrors.length > 0) {
117
- console.error('Validation errors:');
118
- validationErrors.forEach(error => console.error(` - ${error}`));
122
+ output.error('Validation errors:');
123
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
119
124
  process.exit(1);
120
125
  }
121
- const {
126
+ let {
122
127
  result,
123
128
  cleanup
124
129
  } = await tddCommand(command, options, globalOptions);
125
130
 
126
131
  // Set up cleanup on process signals
127
- const handleCleanup = async () => {
132
+ let handleCleanup = async () => {
128
133
  await cleanup();
129
134
  };
130
135
  process.once('SIGINT', () => {
@@ -140,122 +145,119 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
140
145
  await cleanup();
141
146
  });
142
147
  program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (command, options) => {
143
- const globalOptions = program.opts();
148
+ let globalOptions = program.opts();
144
149
 
145
150
  // Validate options
146
- const validationErrors = validateRunOptions(command, options);
151
+ let validationErrors = validateRunOptions(command, options);
147
152
  if (validationErrors.length > 0) {
148
- console.error('Validation errors:');
149
- validationErrors.forEach(error => console.error(` - ${error}`));
153
+ output.error('Validation errors:');
154
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
150
155
  process.exit(1);
151
156
  }
152
157
  try {
153
- const result = await runCommand(command, options, globalOptions);
158
+ let result = await runCommand(command, options, globalOptions);
154
159
  if (result && !result.success && result.exitCode > 0) {
155
160
  process.exit(result.exitCode);
156
161
  }
157
162
  } catch (error) {
158
- console.error('Command failed:', error.message);
159
- if (globalOptions.verbose) {
160
- console.error('Stack trace:', error.stack);
161
- }
163
+ output.error('Command failed', error);
162
164
  process.exit(1);
163
165
  }
164
166
  });
165
167
  program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
166
- const globalOptions = program.opts();
168
+ let globalOptions = program.opts();
167
169
 
168
170
  // Validate options
169
- const validationErrors = validateStatusOptions(buildId, options);
171
+ let validationErrors = validateStatusOptions(buildId, options);
170
172
  if (validationErrors.length > 0) {
171
- console.error('Validation errors:');
172
- validationErrors.forEach(error => console.error(` - ${error}`));
173
+ output.error('Validation errors:');
174
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
173
175
  process.exit(1);
174
176
  }
175
177
  await statusCommand(buildId, options, globalOptions);
176
178
  });
177
179
  program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
178
- const globalOptions = program.opts();
180
+ let globalOptions = program.opts();
179
181
 
180
182
  // Validate options
181
- const validationErrors = validateFinalizeOptions(parallelId, options);
183
+ let validationErrors = validateFinalizeOptions(parallelId, options);
182
184
  if (validationErrors.length > 0) {
183
- console.error('Validation errors:');
184
- validationErrors.forEach(error => console.error(` - ${error}`));
185
+ output.error('Validation errors:');
186
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
185
187
  process.exit(1);
186
188
  }
187
189
  await finalizeCommand(parallelId, options, globalOptions);
188
190
  });
189
191
  program.command('doctor').description('Run diagnostics to check your environment and configuration').option('--api', 'Include API connectivity checks').action(async options => {
190
- const globalOptions = program.opts();
192
+ let globalOptions = program.opts();
191
193
 
192
194
  // Validate options
193
- const validationErrors = validateDoctorOptions(options);
195
+ let validationErrors = validateDoctorOptions(options);
194
196
  if (validationErrors.length > 0) {
195
- console.error('Validation errors:');
196
- validationErrors.forEach(error => console.error(` - ${error}`));
197
+ output.error('Validation errors:');
198
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
197
199
  process.exit(1);
198
200
  }
199
201
  await doctorCommand(options, globalOptions);
200
202
  });
201
203
  program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
202
- const globalOptions = program.opts();
204
+ let globalOptions = program.opts();
203
205
 
204
206
  // Validate options
205
- const validationErrors = validateLoginOptions(options);
207
+ let validationErrors = validateLoginOptions(options);
206
208
  if (validationErrors.length > 0) {
207
- console.error('Validation errors:');
208
- validationErrors.forEach(error => console.error(` - ${error}`));
209
+ output.error('Validation errors:');
210
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
209
211
  process.exit(1);
210
212
  }
211
213
  await loginCommand(options, globalOptions);
212
214
  });
213
215
  program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
214
- const globalOptions = program.opts();
216
+ let globalOptions = program.opts();
215
217
 
216
218
  // Validate options
217
- const validationErrors = validateLogoutOptions(options);
219
+ let validationErrors = validateLogoutOptions(options);
218
220
  if (validationErrors.length > 0) {
219
- console.error('Validation errors:');
220
- validationErrors.forEach(error => console.error(` - ${error}`));
221
+ output.error('Validation errors:');
222
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
221
223
  process.exit(1);
222
224
  }
223
225
  await logoutCommand(options, globalOptions);
224
226
  });
225
227
  program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
226
- const globalOptions = program.opts();
228
+ let globalOptions = program.opts();
227
229
 
228
230
  // Validate options
229
- const validationErrors = validateWhoamiOptions(options);
231
+ let validationErrors = validateWhoamiOptions(options);
230
232
  if (validationErrors.length > 0) {
231
- console.error('Validation errors:');
232
- validationErrors.forEach(error => console.error(` - ${error}`));
233
+ output.error('Validation errors:');
234
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
233
235
  process.exit(1);
234
236
  }
235
237
  await whoamiCommand(options, globalOptions);
236
238
  });
237
239
  program.command('project:select').description('Configure project for current directory').option('--api-url <url>', 'API URL override').action(async options => {
238
- const globalOptions = program.opts();
240
+ let globalOptions = program.opts();
239
241
 
240
242
  // Validate options
241
- const validationErrors = validateProjectOptions(options);
243
+ let validationErrors = validateProjectOptions(options);
242
244
  if (validationErrors.length > 0) {
243
- console.error('Validation errors:');
244
- validationErrors.forEach(error => console.error(` - ${error}`));
245
+ output.error('Validation errors:');
246
+ validationErrors.forEach(error => output.printErr(` - ${error}`));
245
247
  process.exit(1);
246
248
  }
247
249
  await projectSelectCommand(options, globalOptions);
248
250
  });
249
251
  program.command('project:list').description('Show all configured projects').action(async options => {
250
- const globalOptions = program.opts();
252
+ let globalOptions = program.opts();
251
253
  await projectListCommand(options, globalOptions);
252
254
  });
253
255
  program.command('project:token').description('Show project token for current directory').action(async options => {
254
- const globalOptions = program.opts();
256
+ let globalOptions = program.opts();
255
257
  await projectTokenCommand(options, globalOptions);
256
258
  });
257
259
  program.command('project:remove').description('Remove project configuration for current directory').action(async options => {
258
- const globalOptions = program.opts();
260
+ let globalOptions = program.opts();
259
261
  await projectRemoveCommand(options, globalOptions);
260
262
  });
261
263
  program.parse();
@@ -1,6 +1,6 @@
1
1
  import { URL } from 'url';
2
2
  import { loadConfig } from '../utils/config-loader.js';
3
- import { ConsoleUI } from '../utils/console-ui.js';
3
+ import * as output from '../utils/output.js';
4
4
  import { ApiService } from '../services/api-service.js';
5
5
  import { ConfigError } from '../errors/vizzly-error.js';
6
6
  import { getApiToken } from '../utils/environment-config.js';
@@ -11,16 +11,12 @@ import { getApiToken } from '../utils/environment-config.js';
11
11
  * @param {Object} globalOptions - Global CLI options
12
12
  */
13
13
  export async function doctorCommand(options = {}, globalOptions = {}) {
14
- // Create UI handler
15
- const ui = new ConsoleUI({
14
+ output.configure({
16
15
  json: globalOptions.json,
17
16
  verbose: globalOptions.verbose,
18
17
  color: !globalOptions.noColor
19
18
  });
20
-
21
- // Note: ConsoleUI handles cleanup via global process listeners
22
-
23
- const diagnostics = {
19
+ let diagnostics = {
24
20
  environment: {
25
21
  nodeVersion: null,
26
22
  nodeVersionValid: null
@@ -41,71 +37,71 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
41
37
  let hasErrors = false;
42
38
  try {
43
39
  // Determine if we'll attempt remote checks (API connectivity)
44
- const willCheckConnectivity = Boolean(options.api || getApiToken());
40
+ let willCheckConnectivity = Boolean(options.api || getApiToken());
45
41
 
46
42
  // Announce preflight, indicating local-only when no token/connectivity is planned
47
- ui.info(`Running Vizzly preflight${willCheckConnectivity ? '' : ' (local checks only)'}...`);
43
+ output.info(`Running Vizzly preflight${willCheckConnectivity ? '' : ' (local checks only)'}...`);
48
44
 
49
45
  // Node.js version check (require >= 20)
50
- const nodeVersion = process.version;
51
- const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
46
+ let nodeVersion = process.version;
47
+ let nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
52
48
  diagnostics.environment.nodeVersion = nodeVersion;
53
49
  diagnostics.environment.nodeVersionValid = nodeMajor >= 20;
54
50
  if (nodeMajor >= 20) {
55
- ui.success(`Node.js version: ${nodeVersion} (supported)`);
51
+ output.success(`Node.js version: ${nodeVersion} (supported)`);
56
52
  } else {
57
53
  hasErrors = true;
58
- ui.error('Node.js version must be >= 20', {}, 0);
54
+ output.error('Node.js version must be >= 20');
59
55
  }
60
56
 
61
57
  // Load configuration (apply global CLI overrides like --config only)
62
- const config = await loadConfig(globalOptions.config);
58
+ let config = await loadConfig(globalOptions.config);
63
59
 
64
60
  // Validate apiUrl
65
61
  diagnostics.configuration.apiUrl = config.apiUrl;
66
62
  try {
67
- const url = new URL(config.apiUrl);
63
+ let url = new URL(config.apiUrl);
68
64
  if (!['http:', 'https:'].includes(url.protocol)) {
69
65
  throw new ConfigError('URL must use http or https');
70
66
  }
71
67
  diagnostics.configuration.apiUrlValid = true;
72
- ui.success(`API URL: ${config.apiUrl}`);
68
+ output.success(`API URL: ${config.apiUrl}`);
73
69
  } catch (e) {
74
70
  diagnostics.configuration.apiUrlValid = false;
75
71
  hasErrors = true;
76
- ui.error('Invalid apiUrl in configuration (set VIZZLY_API_URL or config file)', e, 0);
72
+ output.error('Invalid apiUrl in configuration (set VIZZLY_API_URL or config file)', e);
77
73
  }
78
74
 
79
75
  // Validate threshold (0..1 inclusive)
80
- const threshold = Number(config?.comparison?.threshold);
76
+ let threshold = Number(config?.comparison?.threshold);
81
77
  diagnostics.configuration.threshold = threshold;
82
- const thresholdValid = Number.isFinite(threshold) && threshold >= 0 && threshold <= 1;
78
+ let thresholdValid = Number.isFinite(threshold) && threshold >= 0 && threshold <= 1;
83
79
  diagnostics.configuration.thresholdValid = thresholdValid;
84
80
  if (thresholdValid) {
85
- ui.success(`Threshold: ${threshold}`);
81
+ output.success(`Threshold: ${threshold}`);
86
82
  } else {
87
83
  hasErrors = true;
88
- ui.error('Invalid threshold (expected number between 0 and 1)', {}, 0);
84
+ output.error('Invalid threshold (expected number between 0 and 1)');
89
85
  }
90
86
 
91
87
  // Report effective port without binding
92
- const port = config?.server?.port ?? 47392;
88
+ let port = config?.server?.port ?? 47392;
93
89
  diagnostics.configuration.port = port;
94
- ui.info(`Effective port: ${port}`);
90
+ output.info(`Effective port: ${port}`);
95
91
 
96
92
  // Optional: API connectivity check when --api is provided or VIZZLY_TOKEN is present
97
- const autoApi = Boolean(getApiToken());
93
+ let autoApi = Boolean(getApiToken());
98
94
  if (options.api || autoApi) {
99
95
  diagnostics.connectivity.checked = true;
100
96
  if (!config.apiKey) {
101
97
  diagnostics.connectivity.ok = false;
102
98
  diagnostics.connectivity.error = 'Missing API token (VIZZLY_TOKEN)';
103
99
  hasErrors = true;
104
- ui.error('Missing API token for connectivity check', {}, 0);
100
+ output.error('Missing API token for connectivity check');
105
101
  } else {
106
- ui.progress('Checking API connectivity...');
102
+ output.progress('Checking API connectivity...');
107
103
  try {
108
- const api = new ApiService({
104
+ let api = new ApiService({
109
105
  baseUrl: config.apiUrl,
110
106
  token: config.apiKey,
111
107
  command: 'doctor'
@@ -115,26 +111,26 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
115
111
  limit: 1
116
112
  });
117
113
  diagnostics.connectivity.ok = true;
118
- ui.success('API connectivity OK');
114
+ output.success('API connectivity OK');
119
115
  } catch (err) {
120
116
  diagnostics.connectivity.ok = false;
121
117
  diagnostics.connectivity.error = err?.message || String(err);
122
118
  hasErrors = true;
123
- ui.error('API connectivity failed', err, 0);
119
+ output.error('API connectivity failed', err);
124
120
  }
125
121
  }
126
122
  }
127
123
 
128
124
  // Summary
129
125
  if (hasErrors) {
130
- ui.warning('Preflight completed with issues.');
126
+ output.warn('Preflight completed with issues.');
131
127
  } else {
132
- ui.success('Preflight passed.');
128
+ output.success('Preflight passed.');
133
129
  }
134
130
 
135
131
  // Emit structured data in json/verbose modes
136
132
  if (globalOptions.json || globalOptions.verbose) {
137
- ui.data({
133
+ output.data({
138
134
  passed: !hasErrors,
139
135
  diagnostics,
140
136
  timestamp: new Date().toISOString()
@@ -142,9 +138,9 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
142
138
  }
143
139
  } catch (error) {
144
140
  hasErrors = true;
145
- ui.error('Failed to run preflight', error, 0);
141
+ output.error('Failed to run preflight', error);
146
142
  } finally {
147
- ui.cleanup();
143
+ output.cleanup();
148
144
  if (hasErrors) process.exit(1);
149
145
  }
150
146
  }
@@ -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
 
5
5
  /**
6
6
  * Finalize command implementation
@@ -9,52 +9,53 @@ import { createServiceContainer } from '../container/index.js';
9
9
  * @param {Object} globalOptions - Global CLI options
10
10
  */
11
11
  export async function finalizeCommand(parallelId, options = {}, globalOptions = {}) {
12
- // Create UI handler
13
- const ui = new ConsoleUI({
12
+ output.configure({
14
13
  json: globalOptions.json,
15
14
  verbose: globalOptions.verbose,
16
15
  color: !globalOptions.noColor
17
16
  });
18
17
  try {
19
18
  // Load configuration with CLI overrides
20
- const allOptions = {
19
+ let allOptions = {
21
20
  ...globalOptions,
22
21
  ...options
23
22
  };
24
- const config = await loadConfig(globalOptions.config, allOptions);
23
+ let config = await loadConfig(globalOptions.config, allOptions);
25
24
 
26
25
  // Validate API token
27
26
  if (!config.apiKey) {
28
- ui.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
29
- return;
27
+ output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
28
+ process.exit(1);
30
29
  }
31
30
  if (globalOptions.verbose) {
32
- ui.info('Configuration loaded', {
31
+ output.info('Configuration loaded');
32
+ output.debug('Config details', {
33
33
  parallelId,
34
34
  apiUrl: config.apiUrl
35
35
  });
36
36
  }
37
37
 
38
- // Create service container and get API service
39
- ui.startSpinner('Finalizing parallel build...');
40
- const container = await createServiceContainer(config, 'finalize');
41
- const apiService = await container.get('apiService');
42
- ui.stopSpinner();
38
+ // Create services and get API service
39
+ output.startSpinner('Finalizing parallel build...');
40
+ let services = createServices(config, 'finalize');
41
+ let apiService = services.apiService;
42
+ output.stopSpinner();
43
43
 
44
44
  // Call finalize endpoint
45
- const result = await apiService.finalizeParallelBuild(parallelId);
45
+ let result = await apiService.finalizeParallelBuild(parallelId);
46
46
  if (globalOptions.json) {
47
- console.log(JSON.stringify(result, null, 2));
47
+ output.data(result);
48
48
  } else {
49
- ui.success(`Parallel build ${result.build.id} finalized successfully`);
50
- ui.info(`Status: ${result.build.status}`);
51
- ui.info(`Parallel ID: ${result.build.parallel_id}`);
49
+ output.success(`Parallel build ${result.build.id} finalized successfully`);
50
+ output.info(`Status: ${result.build.status}`);
51
+ output.info(`Parallel ID: ${result.build.parallel_id}`);
52
52
  }
53
53
  } catch (error) {
54
- ui.stopSpinner();
55
- ui.error('Failed to finalize parallel build', error);
54
+ output.stopSpinner();
55
+ output.error('Failed to finalize parallel build', error);
56
+ process.exit(1);
56
57
  } finally {
57
- ui.cleanup();
58
+ output.cleanup();
58
59
  }
59
60
  }
60
61
 
@@ -64,7 +65,7 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
64
65
  * @param {Object} options - Command options
65
66
  */
66
67
  export function validateFinalizeOptions(parallelId, _options) {
67
- const errors = [];
68
+ let errors = [];
68
69
  if (!parallelId || parallelId.trim() === '') {
69
70
  errors.push('Parallel ID is required');
70
71
  }