@vizzly-testing/cli 0.17.0 → 0.19.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 (66) hide show
  1. package/dist/cli.js +87 -59
  2. package/dist/client/index.js +6 -6
  3. package/dist/commands/doctor.js +15 -15
  4. package/dist/commands/finalize.js +7 -7
  5. package/dist/commands/init.js +28 -28
  6. package/dist/commands/login.js +23 -23
  7. package/dist/commands/logout.js +4 -4
  8. package/dist/commands/project.js +36 -36
  9. package/dist/commands/run.js +33 -33
  10. package/dist/commands/status.js +14 -14
  11. package/dist/commands/tdd-daemon.js +43 -43
  12. package/dist/commands/tdd.js +26 -26
  13. package/dist/commands/upload.js +32 -32
  14. package/dist/commands/whoami.js +12 -12
  15. package/dist/index.js +9 -14
  16. package/dist/plugin-api.js +43 -0
  17. package/dist/plugin-loader.js +28 -28
  18. package/dist/reporter/reporter-bundle.css +1 -1
  19. package/dist/reporter/reporter-bundle.iife.js +19 -19
  20. package/dist/sdk/index.js +33 -35
  21. package/dist/server/handlers/api-handler.js +4 -4
  22. package/dist/server/handlers/tdd-handler.js +22 -21
  23. package/dist/server/http-server.js +21 -22
  24. package/dist/server/middleware/json-parser.js +1 -1
  25. package/dist/server/routers/assets.js +14 -14
  26. package/dist/server/routers/auth.js +14 -14
  27. package/dist/server/routers/baseline.js +8 -8
  28. package/dist/server/routers/cloud-proxy.js +15 -15
  29. package/dist/server/routers/config.js +11 -11
  30. package/dist/server/routers/dashboard.js +11 -11
  31. package/dist/server/routers/health.js +4 -4
  32. package/dist/server/routers/projects.js +19 -19
  33. package/dist/server/routers/screenshot.js +9 -9
  34. package/dist/services/api-service.js +16 -16
  35. package/dist/services/auth-service.js +17 -17
  36. package/dist/services/build-manager.js +3 -3
  37. package/dist/services/config-service.js +32 -32
  38. package/dist/services/html-report-generator.js +8 -8
  39. package/dist/services/index.js +11 -11
  40. package/dist/services/project-service.js +19 -19
  41. package/dist/services/report-generator/report.css +3 -3
  42. package/dist/services/report-generator/viewer.js +25 -23
  43. package/dist/services/screenshot-server.js +1 -1
  44. package/dist/services/server-manager.js +5 -5
  45. package/dist/services/static-report-generator.js +14 -14
  46. package/dist/services/tdd-service.js +152 -110
  47. package/dist/services/test-runner.js +3 -3
  48. package/dist/services/uploader.js +10 -8
  49. package/dist/types/config.d.ts +2 -1
  50. package/dist/types/index.d.ts +95 -1
  51. package/dist/types/sdk.d.ts +1 -1
  52. package/dist/utils/browser.js +3 -3
  53. package/dist/utils/build-history.js +12 -12
  54. package/dist/utils/config-loader.js +17 -17
  55. package/dist/utils/config-schema.js +6 -6
  56. package/dist/utils/environment-config.js +11 -0
  57. package/dist/utils/fetch-utils.js +2 -2
  58. package/dist/utils/file-helpers.js +2 -2
  59. package/dist/utils/git.js +3 -6
  60. package/dist/utils/global-config.js +28 -25
  61. package/dist/utils/output.js +136 -28
  62. package/dist/utils/package-info.js +3 -3
  63. package/dist/utils/security.js +12 -12
  64. package/docs/api-reference.md +52 -23
  65. package/docs/plugins.md +60 -25
  66. package/package.json +9 -13
package/dist/cli.js CHANGED
@@ -1,29 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import 'dotenv/config';
3
3
  import { program } from 'commander';
4
- import { init } from './commands/init.js';
5
- import { uploadCommand, validateUploadOptions } from './commands/upload.js';
6
- import { runCommand, validateRunOptions } from './commands/run.js';
7
- import { tddCommand, validateTddOptions } from './commands/tdd.js';
8
- import { tddStartCommand, tddStopCommand, tddStatusCommand, runDaemonChild } from './commands/tdd-daemon.js';
9
- import { statusCommand, validateStatusOptions } from './commands/status.js';
10
- import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
11
4
  import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
5
+ import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
6
+ import { init } from './commands/init.js';
12
7
  import { loginCommand, validateLoginOptions } from './commands/login.js';
13
8
  import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
14
- import { whoamiCommand, validateWhoamiOptions } from './commands/whoami.js';
15
- import { projectSelectCommand, projectListCommand, projectTokenCommand, projectRemoveCommand, validateProjectOptions } from './commands/project.js';
16
- import { getPackageVersion } from './utils/package-info.js';
9
+ import { projectListCommand, projectRemoveCommand, projectSelectCommand, projectTokenCommand, validateProjectOptions } from './commands/project.js';
10
+ import { runCommand, validateRunOptions } from './commands/run.js';
11
+ import { statusCommand, validateStatusOptions } from './commands/status.js';
12
+ import { tddCommand, validateTddOptions } from './commands/tdd.js';
13
+ import { runDaemonChild, tddStartCommand, tddStatusCommand, tddStopCommand } from './commands/tdd-daemon.js';
14
+ import { uploadCommand, validateUploadOptions } from './commands/upload.js';
15
+ import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
17
16
  import { loadPlugins } from './plugin-loader.js';
18
- import { loadConfig } from './utils/config-loader.js';
17
+ import { createPluginServices } from './plugin-api.js';
19
18
  import { createServices } from './services/index.js';
19
+ import { loadConfig } from './utils/config-loader.js';
20
20
  import * as output from './utils/output.js';
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');
21
+ import { getPackageVersion } from './utils/package-info.js';
22
+ 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 (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json', 'Machine-readable output').option('--no-color', 'Disable colored output');
22
23
 
23
24
  // Load plugins before defining commands
24
25
  // We need to manually parse to get the config option early
25
26
  let configPath = null;
26
27
  let verboseMode = false;
28
+ let logLevelArg = null;
27
29
  for (let i = 0; i < process.argv.length; i++) {
28
30
  if ((process.argv[i] === '-c' || process.argv[i] === '--config') && process.argv[i + 1]) {
29
31
  configPath = process.argv[i + 1];
@@ -31,30 +33,36 @@ for (let i = 0; i < process.argv.length; i++) {
31
33
  if (process.argv[i] === '-v' || process.argv[i] === '--verbose') {
32
34
  verboseMode = true;
33
35
  }
36
+ if (process.argv[i] === '--log-level' && process.argv[i + 1]) {
37
+ logLevelArg = process.argv[i + 1];
38
+ }
34
39
  }
35
40
 
36
41
  // Configure output early
42
+ // Priority: --log-level > --verbose > VIZZLY_LOG_LEVEL env var > default ('info')
37
43
  output.configure({
44
+ logLevel: logLevelArg,
38
45
  verbose: verboseMode,
39
46
  color: !process.argv.includes('--no-color'),
40
47
  json: process.argv.includes('--json')
41
48
  });
42
- let config = await loadConfig(configPath, {});
43
- let services = createServices(config);
49
+ const config = await loadConfig(configPath, {});
50
+ const services = createServices(config);
51
+ const pluginServices = createPluginServices(services);
44
52
  let plugins = [];
45
53
  try {
46
54
  plugins = await loadPlugins(configPath, config);
47
- for (let plugin of plugins) {
55
+ for (const plugin of plugins) {
48
56
  try {
49
57
  // Add timeout protection for plugin registration (5 seconds)
50
- let registerPromise = plugin.register(program, {
58
+ const registerPromise = plugin.register(program, {
51
59
  config,
52
- services,
60
+ services: pluginServices,
53
61
  output,
54
62
  // Backwards compatibility alias for plugins using old API
55
63
  logger: output
56
64
  });
57
- let timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin registration timeout (5s)')), 5000));
65
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin registration timeout (5s)')), 5000));
58
66
  await Promise.race([registerPromise, timeoutPromise]);
59
67
  output.debug(`Registered plugin: ${plugin.name}`);
60
68
  } catch (error) {
@@ -65,7 +73,7 @@ try {
65
73
  output.debug(`Plugin loading failed: ${error.message}`);
66
74
  }
67
75
  program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').action(async options => {
68
- let globalOptions = program.opts();
76
+ const globalOptions = program.opts();
69
77
  await init({
70
78
  ...globalOptions,
71
79
  ...options,
@@ -73,24 +81,26 @@ program.command('init').description('Initialize Vizzly in your project').option(
73
81
  });
74
82
  });
75
83
  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) => {
76
- let globalOptions = program.opts();
84
+ const globalOptions = program.opts();
77
85
 
78
86
  // Validate options
79
- let validationErrors = validateUploadOptions(path, options);
87
+ const validationErrors = validateUploadOptions(path, options);
80
88
  if (validationErrors.length > 0) {
81
89
  output.error('Validation errors:');
82
- validationErrors.forEach(error => output.printErr(` - ${error}`));
90
+ for (let error of validationErrors) {
91
+ output.printErr(` - ${error}`);
92
+ }
83
93
  process.exit(1);
84
94
  }
85
95
  await uploadCommand(path, options, globalOptions);
86
96
  });
87
97
 
88
98
  // TDD command with subcommands - Local visual testing with interactive dashboard
89
- let tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
99
+ const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
90
100
 
91
101
  // TDD Start - Background server
92
102
  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 => {
93
- let globalOptions = program.opts();
103
+ const globalOptions = program.opts();
94
104
 
95
105
  // If this is a daemon child process, run the server directly
96
106
  if (options.daemonChild) {
@@ -102,34 +112,36 @@ tddCmd.command('start').description('Start background TDD server with dashboard'
102
112
 
103
113
  // TDD Stop - Kill background server
104
114
  tddCmd.command('stop').description('Stop background TDD server').action(async options => {
105
- let globalOptions = program.opts();
115
+ const globalOptions = program.opts();
106
116
  await tddStopCommand(options, globalOptions);
107
117
  });
108
118
 
109
119
  // TDD Status - Check server status
110
120
  tddCmd.command('status').description('Check TDD server status').action(async options => {
111
- let globalOptions = program.opts();
121
+ const globalOptions = program.opts();
112
122
  await tddStatusCommand(options, globalOptions);
113
123
  });
114
124
 
115
125
  // TDD Run - One-off test run with ephemeral server (generates static report)
116
126
  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) => {
117
- let globalOptions = program.opts();
127
+ const globalOptions = program.opts();
118
128
 
119
129
  // Validate options
120
- let validationErrors = validateTddOptions(command, options);
130
+ const validationErrors = validateTddOptions(command, options);
121
131
  if (validationErrors.length > 0) {
122
132
  output.error('Validation errors:');
123
- validationErrors.forEach(error => output.printErr(` - ${error}`));
133
+ for (let error of validationErrors) {
134
+ output.printErr(` - ${error}`);
135
+ }
124
136
  process.exit(1);
125
137
  }
126
- let {
138
+ const {
127
139
  result,
128
140
  cleanup
129
141
  } = await tddCommand(command, options, globalOptions);
130
142
 
131
143
  // Set up cleanup on process signals
132
- let handleCleanup = async () => {
144
+ const handleCleanup = async () => {
133
145
  await cleanup();
134
146
  };
135
147
  process.once('SIGINT', () => {
@@ -145,17 +157,19 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
145
157
  await cleanup();
146
158
  });
147
159
  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) => {
148
- let globalOptions = program.opts();
160
+ const globalOptions = program.opts();
149
161
 
150
162
  // Validate options
151
- let validationErrors = validateRunOptions(command, options);
163
+ const validationErrors = validateRunOptions(command, options);
152
164
  if (validationErrors.length > 0) {
153
165
  output.error('Validation errors:');
154
- validationErrors.forEach(error => output.printErr(` - ${error}`));
166
+ for (let error of validationErrors) {
167
+ output.printErr(` - ${error}`);
168
+ }
155
169
  process.exit(1);
156
170
  }
157
171
  try {
158
- let result = await runCommand(command, options, globalOptions);
172
+ const result = await runCommand(command, options, globalOptions);
159
173
  if (result && !result.success && result.exitCode > 0) {
160
174
  process.exit(result.exitCode);
161
175
  }
@@ -165,99 +179,113 @@ program.command('run').description('Run tests with Vizzly integration').argument
165
179
  }
166
180
  });
167
181
  program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
168
- let globalOptions = program.opts();
182
+ const globalOptions = program.opts();
169
183
 
170
184
  // Validate options
171
- let validationErrors = validateStatusOptions(buildId, options);
185
+ const validationErrors = validateStatusOptions(buildId, options);
172
186
  if (validationErrors.length > 0) {
173
187
  output.error('Validation errors:');
174
- validationErrors.forEach(error => output.printErr(` - ${error}`));
188
+ for (let error of validationErrors) {
189
+ output.printErr(` - ${error}`);
190
+ }
175
191
  process.exit(1);
176
192
  }
177
193
  await statusCommand(buildId, options, globalOptions);
178
194
  });
179
195
  program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
180
- let globalOptions = program.opts();
196
+ const globalOptions = program.opts();
181
197
 
182
198
  // Validate options
183
- let validationErrors = validateFinalizeOptions(parallelId, options);
199
+ const validationErrors = validateFinalizeOptions(parallelId, options);
184
200
  if (validationErrors.length > 0) {
185
201
  output.error('Validation errors:');
186
- validationErrors.forEach(error => output.printErr(` - ${error}`));
202
+ for (let error of validationErrors) {
203
+ output.printErr(` - ${error}`);
204
+ }
187
205
  process.exit(1);
188
206
  }
189
207
  await finalizeCommand(parallelId, options, globalOptions);
190
208
  });
191
209
  program.command('doctor').description('Run diagnostics to check your environment and configuration').option('--api', 'Include API connectivity checks').action(async options => {
192
- let globalOptions = program.opts();
210
+ const globalOptions = program.opts();
193
211
 
194
212
  // Validate options
195
- let validationErrors = validateDoctorOptions(options);
213
+ const validationErrors = validateDoctorOptions(options);
196
214
  if (validationErrors.length > 0) {
197
215
  output.error('Validation errors:');
198
- validationErrors.forEach(error => output.printErr(` - ${error}`));
216
+ for (let error of validationErrors) {
217
+ output.printErr(` - ${error}`);
218
+ }
199
219
  process.exit(1);
200
220
  }
201
221
  await doctorCommand(options, globalOptions);
202
222
  });
203
223
  program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
204
- let globalOptions = program.opts();
224
+ const globalOptions = program.opts();
205
225
 
206
226
  // Validate options
207
- let validationErrors = validateLoginOptions(options);
227
+ const validationErrors = validateLoginOptions(options);
208
228
  if (validationErrors.length > 0) {
209
229
  output.error('Validation errors:');
210
- validationErrors.forEach(error => output.printErr(` - ${error}`));
230
+ for (let error of validationErrors) {
231
+ output.printErr(` - ${error}`);
232
+ }
211
233
  process.exit(1);
212
234
  }
213
235
  await loginCommand(options, globalOptions);
214
236
  });
215
237
  program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
216
- let globalOptions = program.opts();
238
+ const globalOptions = program.opts();
217
239
 
218
240
  // Validate options
219
- let validationErrors = validateLogoutOptions(options);
241
+ const validationErrors = validateLogoutOptions(options);
220
242
  if (validationErrors.length > 0) {
221
243
  output.error('Validation errors:');
222
- validationErrors.forEach(error => output.printErr(` - ${error}`));
244
+ for (let error of validationErrors) {
245
+ output.printErr(` - ${error}`);
246
+ }
223
247
  process.exit(1);
224
248
  }
225
249
  await logoutCommand(options, globalOptions);
226
250
  });
227
251
  program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
228
- let globalOptions = program.opts();
252
+ const globalOptions = program.opts();
229
253
 
230
254
  // Validate options
231
- let validationErrors = validateWhoamiOptions(options);
255
+ const validationErrors = validateWhoamiOptions(options);
232
256
  if (validationErrors.length > 0) {
233
257
  output.error('Validation errors:');
234
- validationErrors.forEach(error => output.printErr(` - ${error}`));
258
+ for (let error of validationErrors) {
259
+ output.printErr(` - ${error}`);
260
+ }
235
261
  process.exit(1);
236
262
  }
237
263
  await whoamiCommand(options, globalOptions);
238
264
  });
239
265
  program.command('project:select').description('Configure project for current directory').option('--api-url <url>', 'API URL override').action(async options => {
240
- let globalOptions = program.opts();
266
+ const globalOptions = program.opts();
241
267
 
242
268
  // Validate options
243
- let validationErrors = validateProjectOptions(options);
269
+ const validationErrors = validateProjectOptions(options);
244
270
  if (validationErrors.length > 0) {
245
271
  output.error('Validation errors:');
246
- validationErrors.forEach(error => output.printErr(` - ${error}`));
272
+ for (let error of validationErrors) {
273
+ output.printErr(` - ${error}`);
274
+ }
247
275
  process.exit(1);
248
276
  }
249
277
  await projectSelectCommand(options, globalOptions);
250
278
  });
251
279
  program.command('project:list').description('Show all configured projects').action(async options => {
252
- let globalOptions = program.opts();
280
+ const globalOptions = program.opts();
253
281
  await projectListCommand(options, globalOptions);
254
282
  });
255
283
  program.command('project:token').description('Show project token for current directory').action(async options => {
256
- let globalOptions = program.opts();
284
+ const globalOptions = program.opts();
257
285
  await projectTokenCommand(options, globalOptions);
258
286
  });
259
287
  program.command('project:remove').description('Remove project configuration for current directory').action(async options => {
260
- let globalOptions = program.opts();
288
+ const globalOptions = program.opts();
261
289
  await projectRemoveCommand(options, globalOptions);
262
290
  });
263
291
  program.parse();
@@ -3,9 +3,9 @@
3
3
  * @description Thin client for test runners - minimal API for taking screenshots
4
4
  */
5
5
 
6
- import { getServerUrl, getBuildId, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
7
- import { existsSync, readFileSync } from 'fs';
8
- import { join, parse, dirname } from 'path';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { dirname, join, parse } from 'node:path';
8
+ import { getBuildId, getServerUrl, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
9
9
 
10
10
  // Internal client state
11
11
  let currentClient = null;
@@ -104,12 +104,12 @@ function getClient() {
104
104
  function createSimpleClient(serverUrl) {
105
105
  return {
106
106
  async screenshot(name, imageBuffer, options = {}) {
107
- let controller = new AbortController();
108
- let timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
107
+ const controller = new AbortController();
108
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
109
109
  try {
110
110
  // If it's a string, assume it's a file path and send directly
111
111
  // Otherwise it's a Buffer, so convert to base64
112
- let image = typeof imageBuffer === 'string' ? imageBuffer : imageBuffer.toString('base64');
112
+ const image = typeof imageBuffer === 'string' ? imageBuffer : imageBuffer.toString('base64');
113
113
  const response = await fetch(`${serverUrl}/screenshot`, {
114
114
  method: 'POST',
115
115
  headers: {
@@ -1,9 +1,9 @@
1
- import { URL } from 'url';
2
- import { loadConfig } from '../utils/config-loader.js';
3
- import * as output from '../utils/output.js';
4
- import { ApiService } from '../services/api-service.js';
1
+ import { URL } from 'node:url';
5
2
  import { ConfigError } from '../errors/vizzly-error.js';
3
+ import { ApiService } from '../services/api-service.js';
4
+ import { loadConfig } from '../utils/config-loader.js';
6
5
  import { getApiToken } from '../utils/environment-config.js';
6
+ import * as output from '../utils/output.js';
7
7
 
8
8
  /**
9
9
  * Doctor command implementation - Run diagnostics to check environment
@@ -16,7 +16,7 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
16
16
  verbose: globalOptions.verbose,
17
17
  color: !globalOptions.noColor
18
18
  });
19
- let diagnostics = {
19
+ const diagnostics = {
20
20
  environment: {
21
21
  nodeVersion: null,
22
22
  nodeVersionValid: null
@@ -37,14 +37,14 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
37
37
  let hasErrors = false;
38
38
  try {
39
39
  // Determine if we'll attempt remote checks (API connectivity)
40
- let willCheckConnectivity = Boolean(options.api || getApiToken());
40
+ const willCheckConnectivity = Boolean(options.api || getApiToken());
41
41
 
42
42
  // Announce preflight, indicating local-only when no token/connectivity is planned
43
43
  output.info(`Running Vizzly preflight${willCheckConnectivity ? '' : ' (local checks only)'}...`);
44
44
 
45
45
  // Node.js version check (require >= 20)
46
- let nodeVersion = process.version;
47
- let nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
46
+ const nodeVersion = process.version;
47
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
48
48
  diagnostics.environment.nodeVersion = nodeVersion;
49
49
  diagnostics.environment.nodeVersionValid = nodeMajor >= 20;
50
50
  if (nodeMajor >= 20) {
@@ -55,12 +55,12 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
55
55
  }
56
56
 
57
57
  // Load configuration (apply global CLI overrides like --config only)
58
- let config = await loadConfig(globalOptions.config);
58
+ const config = await loadConfig(globalOptions.config);
59
59
 
60
60
  // Validate apiUrl
61
61
  diagnostics.configuration.apiUrl = config.apiUrl;
62
62
  try {
63
- let url = new URL(config.apiUrl);
63
+ const url = new URL(config.apiUrl);
64
64
  if (!['http:', 'https:'].includes(url.protocol)) {
65
65
  throw new ConfigError('URL must use http or https');
66
66
  }
@@ -73,10 +73,10 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
73
73
  }
74
74
 
75
75
  // Validate threshold (0..1 inclusive)
76
- let threshold = Number(config?.comparison?.threshold);
76
+ const threshold = Number(config?.comparison?.threshold);
77
77
  diagnostics.configuration.threshold = threshold;
78
78
  // CIEDE2000 threshold: 0 = exact, 1 = JND, 2 = recommended, 3+ = permissive
79
- let thresholdValid = Number.isFinite(threshold) && threshold >= 0;
79
+ const thresholdValid = Number.isFinite(threshold) && threshold >= 0;
80
80
  diagnostics.configuration.thresholdValid = thresholdValid;
81
81
  if (thresholdValid) {
82
82
  output.success(`Threshold: ${threshold} (CIEDE2000 Delta E)`);
@@ -86,12 +86,12 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
86
86
  }
87
87
 
88
88
  // Report effective port without binding
89
- let port = config?.server?.port ?? 47392;
89
+ const port = config?.server?.port ?? 47392;
90
90
  diagnostics.configuration.port = port;
91
91
  output.info(`Effective port: ${port}`);
92
92
 
93
93
  // Optional: API connectivity check when --api is provided or VIZZLY_TOKEN is present
94
- let autoApi = Boolean(getApiToken());
94
+ const autoApi = Boolean(getApiToken());
95
95
  if (options.api || autoApi) {
96
96
  diagnostics.connectivity.checked = true;
97
97
  if (!config.apiKey) {
@@ -102,7 +102,7 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
102
102
  } else {
103
103
  output.progress('Checking API connectivity...');
104
104
  try {
105
- let api = new ApiService({
105
+ const api = new ApiService({
106
106
  baseUrl: config.apiUrl,
107
107
  token: config.apiKey,
108
108
  command: 'doctor'
@@ -1,6 +1,6 @@
1
+ import { createServices } from '../services/index.js';
1
2
  import { loadConfig } from '../utils/config-loader.js';
2
3
  import * as output from '../utils/output.js';
3
- import { createServices } from '../services/index.js';
4
4
 
5
5
  /**
6
6
  * Finalize command implementation
@@ -16,11 +16,11 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
16
16
  });
17
17
  try {
18
18
  // Load configuration with CLI overrides
19
- let allOptions = {
19
+ const allOptions = {
20
20
  ...globalOptions,
21
21
  ...options
22
22
  };
23
- let config = await loadConfig(globalOptions.config, allOptions);
23
+ const config = await loadConfig(globalOptions.config, allOptions);
24
24
 
25
25
  // Validate API token
26
26
  if (!config.apiKey) {
@@ -37,12 +37,12 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
37
37
 
38
38
  // Create services and get API service
39
39
  output.startSpinner('Finalizing parallel build...');
40
- let services = createServices(config, 'finalize');
41
- let apiService = services.apiService;
40
+ const services = createServices(config, 'finalize');
41
+ const apiService = services.apiService;
42
42
  output.stopSpinner();
43
43
 
44
44
  // Call finalize endpoint
45
- let result = await apiService.finalizeParallelBuild(parallelId);
45
+ const result = await apiService.finalizeParallelBuild(parallelId);
46
46
  if (globalOptions.json) {
47
47
  output.data(result);
48
48
  } else {
@@ -65,7 +65,7 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
65
65
  * @param {Object} options - Command options
66
66
  */
67
67
  export function validateFinalizeOptions(parallelId, _options) {
68
- let errors = [];
68
+ const errors = [];
69
69
  if (!parallelId || parallelId.trim() === '') {
70
70
  errors.push('Parallel ID is required');
71
71
  }
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'fs/promises';
3
- import path from 'path';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
4
5
  import { VizzlyError } from '../errors/vizzly-error.js';
5
- import * as output from '../utils/output.js';
6
6
  import { loadPlugins } from '../plugin-loader.js';
7
7
  import { loadConfig } from '../utils/config-loader.js';
8
- import { z } from 'zod';
8
+ import * as output from '../utils/output.js';
9
9
 
10
10
  /**
11
11
  * Simple configuration setup for Vizzly CLI
@@ -19,8 +19,8 @@ export class InitCommand {
19
19
  output.blank();
20
20
  try {
21
21
  // Check for existing config
22
- let configPath = path.join(process.cwd(), 'vizzly.config.js');
23
- let hasConfig = await this.fileExists(configPath);
22
+ const configPath = path.join(process.cwd(), 'vizzly.config.js');
23
+ const hasConfig = await this.fileExists(configPath);
24
24
  if (hasConfig && !options.force) {
25
25
  output.info('❌ A vizzly.config.js file already exists. Use --force to overwrite.');
26
26
  return;
@@ -71,16 +71,16 @@ export class InitCommand {
71
71
  }`;
72
72
 
73
73
  // Add plugin configurations
74
- let pluginConfigs = this.generatePluginConfigs();
74
+ const pluginConfigs = this.generatePluginConfigs();
75
75
  if (pluginConfigs) {
76
- coreConfig += ',\n\n' + pluginConfigs;
76
+ coreConfig += `,\n\n${pluginConfigs}`;
77
77
  }
78
78
  coreConfig += '\n};\n';
79
79
  await fs.writeFile(configPath, coreConfig, 'utf8');
80
80
  output.info(`📄 Created vizzly.config.js`);
81
81
 
82
82
  // Log discovered plugins
83
- let pluginsWithConfig = this.plugins.filter(p => p.configSchema);
83
+ const pluginsWithConfig = this.plugins.filter(p => p.configSchema);
84
84
  if (pluginsWithConfig.length > 0) {
85
85
  output.info(` Added config for ${pluginsWithConfig.length} plugin(s):`);
86
86
  pluginsWithConfig.forEach(p => {
@@ -94,10 +94,10 @@ export class InitCommand {
94
94
  * @returns {string} Plugin config sections as formatted string
95
95
  */
96
96
  generatePluginConfigs() {
97
- let sections = [];
98
- for (let plugin of this.plugins) {
97
+ const sections = [];
98
+ for (const plugin of this.plugins) {
99
99
  if (plugin.configSchema) {
100
- let configStr = this.formatPluginConfig(plugin);
100
+ const configStr = this.formatPluginConfig(plugin);
101
101
  if (configStr) {
102
102
  sections.push(configStr);
103
103
  }
@@ -114,18 +114,18 @@ export class InitCommand {
114
114
  formatPluginConfig(plugin) {
115
115
  try {
116
116
  // Validate config schema structure with Zod (defensive check)
117
- let configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
118
- let configSchemaValidator = z.record(configValueSchema);
117
+ const configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
118
+ const configSchemaValidator = z.record(configValueSchema);
119
119
  configSchemaValidator.parse(plugin.configSchema);
120
- let configEntries = [];
121
- for (let [key, value] of Object.entries(plugin.configSchema)) {
122
- let formattedValue = this.formatValue(value, 1);
120
+ const configEntries = [];
121
+ for (const [key, value] of Object.entries(plugin.configSchema)) {
122
+ const formattedValue = this.formatValue(value, 1);
123
123
  configEntries.push(` // ${plugin.name} plugin configuration\n ${key}: ${formattedValue}`);
124
124
  }
125
125
  return configEntries.join(',\n\n');
126
126
  } catch (error) {
127
127
  if (error instanceof z.ZodError) {
128
- let messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
128
+ const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
129
129
  output.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
130
130
  } else {
131
131
  output.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
@@ -141,25 +141,25 @@ export class InitCommand {
141
141
  * @returns {string} Formatted value
142
142
  */
143
143
  formatValue(value, depth = 0) {
144
- let indent = ' '.repeat(depth);
145
- let nextIndent = ' '.repeat(depth + 1);
144
+ const indent = ' '.repeat(depth);
145
+ const nextIndent = ' '.repeat(depth + 1);
146
146
  if (value === null) return 'null';
147
147
  if (value === undefined) return 'undefined';
148
148
  if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
149
149
  if (typeof value === 'number' || typeof value === 'boolean') return String(value);
150
150
  if (Array.isArray(value)) {
151
151
  if (value.length === 0) return '[]';
152
- let items = value.map(item => {
153
- let formatted = this.formatValue(item, depth + 1);
152
+ const items = value.map(item => {
153
+ const formatted = this.formatValue(item, depth + 1);
154
154
  return `${nextIndent}${formatted}`;
155
155
  });
156
156
  return `[\n${items.join(',\n')}\n${indent}]`;
157
157
  }
158
158
  if (typeof value === 'object') {
159
- let entries = Object.entries(value);
159
+ const entries = Object.entries(value);
160
160
  if (entries.length === 0) return '{}';
161
- let props = entries.map(([k, v]) => {
162
- let formatted = this.formatValue(v, depth + 1);
161
+ const props = entries.map(([k, v]) => {
162
+ const formatted = this.formatValue(v, depth + 1);
163
163
  return `${nextIndent}${k}: ${formatted}`;
164
164
  });
165
165
  return `{\n${props.join(',\n')}\n${indent}}`;
@@ -188,7 +188,7 @@ export class InitCommand {
188
188
 
189
189
  // Export factory function for CLI
190
190
  export function createInitCommand(options) {
191
- let command = new InitCommand(options.plugins);
191
+ const command = new InitCommand(options.plugins);
192
192
  return () => command.run(options);
193
193
  }
194
194
 
@@ -204,7 +204,7 @@ export async function init(options = {}) {
204
204
  // Try to load plugins if not provided
205
205
  if (!options.plugins) {
206
206
  try {
207
- let config = await loadConfig(options.config, {});
207
+ const config = await loadConfig(options.config, {});
208
208
  plugins = await loadPlugins(options.config, config, null);
209
209
  } catch {
210
210
  // Silent fail - plugins are optional for init
@@ -212,6 +212,6 @@ export async function init(options = {}) {
212
212
  } else {
213
213
  plugins = options.plugins;
214
214
  }
215
- let command = new InitCommand(plugins);
215
+ const command = new InitCommand(plugins);
216
216
  return await command.run(options);
217
217
  }