@vizzly-testing/cli 0.16.4 → 0.18.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 (68) hide show
  1. package/README.md +4 -4
  2. package/claude-plugin/skills/debug-visual-regression/SKILL.md +2 -2
  3. package/dist/cli.js +84 -58
  4. package/dist/client/index.js +6 -6
  5. package/dist/commands/doctor.js +18 -17
  6. package/dist/commands/finalize.js +7 -7
  7. package/dist/commands/init.js +30 -30
  8. package/dist/commands/login.js +23 -23
  9. package/dist/commands/logout.js +4 -4
  10. package/dist/commands/project.js +36 -36
  11. package/dist/commands/run.js +33 -33
  12. package/dist/commands/status.js +14 -14
  13. package/dist/commands/tdd-daemon.js +43 -43
  14. package/dist/commands/tdd.js +27 -27
  15. package/dist/commands/upload.js +33 -33
  16. package/dist/commands/whoami.js +12 -12
  17. package/dist/index.js +9 -14
  18. package/dist/plugin-loader.js +28 -28
  19. package/dist/reporter/reporter-bundle.css +1 -1
  20. package/dist/reporter/reporter-bundle.iife.js +19 -19
  21. package/dist/sdk/index.js +33 -35
  22. package/dist/server/handlers/api-handler.js +4 -4
  23. package/dist/server/handlers/tdd-handler.js +12 -12
  24. package/dist/server/http-server.js +21 -22
  25. package/dist/server/middleware/json-parser.js +1 -1
  26. package/dist/server/routers/assets.js +14 -14
  27. package/dist/server/routers/auth.js +14 -14
  28. package/dist/server/routers/baseline.js +8 -8
  29. package/dist/server/routers/cloud-proxy.js +15 -15
  30. package/dist/server/routers/config.js +11 -11
  31. package/dist/server/routers/dashboard.js +11 -11
  32. package/dist/server/routers/health.js +4 -4
  33. package/dist/server/routers/projects.js +19 -19
  34. package/dist/server/routers/screenshot.js +9 -9
  35. package/dist/services/api-service.js +16 -16
  36. package/dist/services/auth-service.js +17 -17
  37. package/dist/services/build-manager.js +3 -3
  38. package/dist/services/config-service.js +33 -33
  39. package/dist/services/html-report-generator.js +8 -8
  40. package/dist/services/index.js +11 -11
  41. package/dist/services/project-service.js +19 -19
  42. package/dist/services/report-generator/report.css +3 -3
  43. package/dist/services/report-generator/viewer.js +25 -23
  44. package/dist/services/screenshot-server.js +1 -1
  45. package/dist/services/server-manager.js +5 -5
  46. package/dist/services/static-report-generator.js +14 -14
  47. package/dist/services/tdd-service.js +101 -95
  48. package/dist/services/test-runner.js +14 -4
  49. package/dist/services/uploader.js +10 -8
  50. package/dist/types/config.d.ts +2 -1
  51. package/dist/types/index.d.ts +11 -1
  52. package/dist/types/sdk.d.ts +1 -1
  53. package/dist/utils/browser.js +3 -3
  54. package/dist/utils/build-history.js +12 -12
  55. package/dist/utils/config-loader.js +19 -19
  56. package/dist/utils/config-schema.js +10 -9
  57. package/dist/utils/environment-config.js +11 -0
  58. package/dist/utils/fetch-utils.js +2 -2
  59. package/dist/utils/file-helpers.js +2 -2
  60. package/dist/utils/git.js +3 -6
  61. package/dist/utils/global-config.js +28 -25
  62. package/dist/utils/output.js +136 -28
  63. package/dist/utils/package-info.js +3 -3
  64. package/dist/utils/security.js +12 -12
  65. package/docs/api-reference.md +56 -27
  66. package/docs/doctor-command.md +1 -1
  67. package/docs/tdd-mode.md +3 -3
  68. package/package.json +9 -13
@@ -1,7 +1,7 @@
1
- import { loadConfig } from '../utils/config-loader.js';
2
- import * as output from '../utils/output.js';
3
1
  import { createServices } from '../services/index.js';
2
+ import { loadConfig } from '../utils/config-loader.js';
4
3
  import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
4
+ import * as output from '../utils/output.js';
5
5
 
6
6
  /**
7
7
  * Run command implementation
@@ -21,7 +21,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
21
21
  let isTddMode = false;
22
22
 
23
23
  // Ensure cleanup on exit
24
- let cleanup = async () => {
24
+ const cleanup = async () => {
25
25
  output.cleanup();
26
26
 
27
27
  // Cancel test runner (kills process and stops server)
@@ -36,40 +36,40 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
36
36
  // Finalize build if we have one
37
37
  if (testRunner && buildId) {
38
38
  try {
39
- let executionTime = Date.now() - (startTime || Date.now());
39
+ const executionTime = Date.now() - (startTime || Date.now());
40
40
  await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
41
41
  } catch {
42
42
  // Silent fail on cleanup
43
43
  }
44
44
  }
45
45
  };
46
- let sigintHandler = async () => {
46
+ const sigintHandler = async () => {
47
47
  await cleanup();
48
48
  process.exit(1);
49
49
  };
50
- let exitHandler = () => output.cleanup();
50
+ const exitHandler = () => output.cleanup();
51
51
  process.on('SIGINT', sigintHandler);
52
52
  process.on('exit', exitHandler);
53
53
  try {
54
54
  // Load configuration with CLI overrides
55
- let allOptions = {
55
+ const allOptions = {
56
56
  ...globalOptions,
57
57
  ...options
58
58
  };
59
59
  output.debug('[RUN] Loading config', {
60
60
  hasToken: !!allOptions.token
61
61
  });
62
- let config = await loadConfig(globalOptions.config, allOptions);
62
+ const config = await loadConfig(globalOptions.config, allOptions);
63
63
  output.debug('[RUN] Config loaded', {
64
64
  hasApiKey: !!config.apiKey,
65
- apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 8) + '***' : 'NONE'
65
+ apiKeyPrefix: config.apiKey ? `${config.apiKey.substring(0, 8)}***` : 'NONE'
66
66
  });
67
67
  if (globalOptions.verbose) {
68
68
  output.info('Token check:');
69
69
  output.debug('Token details', {
70
70
  hasApiKey: !!config.apiKey,
71
71
  apiKeyType: typeof config.apiKey,
72
- apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? config.apiKey.substring(0, 10) + '...' : 'none',
72
+ apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? `${config.apiKey.substring(0, 10)}...` : 'none',
73
73
  projectSlug: config.projectSlug || 'none',
74
74
  organizationSlug: config.organizationSlug || 'none'
75
75
  });
@@ -82,11 +82,11 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
82
82
  }
83
83
 
84
84
  // Collect git metadata and build info
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();
85
+ const branch = await detectBranch(options.branch);
86
+ const commit = await detectCommit(options.commit);
87
+ const message = options.message || (await detectCommitMessage());
88
+ const buildName = await generateBuildNameWithGit(options.buildName);
89
+ const pullRequestNumber = detectPullRequestNumber();
90
90
  if (globalOptions.verbose) {
91
91
  output.info('Configuration loaded');
92
92
  output.debug('Config details', {
@@ -104,7 +104,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
104
104
 
105
105
  // Create service container and get test runner service
106
106
  output.startSpinner('Initializing test runner...');
107
- let configWithVerbose = {
107
+ const configWithVerbose = {
108
108
  ...config,
109
109
  verbose: globalOptions.verbose,
110
110
  uploadAll: options.uploadAll || false
@@ -112,7 +112,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
112
112
  output.debug('[RUN] Creating services', {
113
113
  hasApiKey: !!configWithVerbose.apiKey
114
114
  });
115
- let services = createServices(configWithVerbose, 'run');
115
+ const services = createServices(configWithVerbose, 'run');
116
116
  testRunner = services.testRunner;
117
117
  output.stopSpinner();
118
118
 
@@ -121,7 +121,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
121
121
 
122
122
  // Set up event handlers
123
123
  testRunner.on('progress', progressData => {
124
- let {
124
+ const {
125
125
  message: progressMessage
126
126
  } = progressData;
127
127
  output.progress(progressMessage || 'Running tests...');
@@ -164,7 +164,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
164
164
  });
165
165
 
166
166
  // Prepare run options
167
- let runOptions = {
167
+ const runOptions = {
168
168
  testCommand,
169
169
  port: config.server.port,
170
170
  timeout: config.server.timeout,
@@ -210,8 +210,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
210
210
  // Check if it's a test command failure (as opposed to setup failure)
211
211
  if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
212
212
  // Extract exit code from error message if available
213
- let exitCodeMatch = error.message.match(/exited with code (\d+)/);
214
- let exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
213
+ const exitCodeMatch = error.message.match(/exited with code (\d+)/);
214
+ const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
215
215
  output.error('Test run failed');
216
216
  return {
217
217
  success: false,
@@ -233,10 +233,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
233
233
  if (runOptions.wait) {
234
234
  output.info('Waiting for build completion...');
235
235
  output.startSpinner('Processing comparisons...');
236
- let {
236
+ const {
237
237
  uploader
238
238
  } = services;
239
- let buildResult = await uploader.waitForBuild(result.buildId);
239
+ const buildResult = await uploader.waitForBuild(result.buildId);
240
240
  output.success('Build processing completed');
241
241
 
242
242
  // Exit with appropriate code based on comparison results
@@ -255,11 +255,11 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
255
255
 
256
256
  // Provide more context about where the error occurred
257
257
  let errorContext = 'Test run failed';
258
- if (error.message && error.message.includes('build')) {
258
+ if (error.message?.includes('build')) {
259
259
  errorContext = 'Build creation failed';
260
- } else if (error.message && error.message.includes('screenshot')) {
260
+ } else if (error.message?.includes('screenshot')) {
261
261
  errorContext = 'Screenshot processing failed';
262
- } else if (error.message && error.message.includes('server')) {
262
+ } else if (error.message?.includes('server')) {
263
263
  errorContext = 'Server startup failed';
264
264
  }
265
265
  output.error(errorContext, error);
@@ -277,30 +277,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
277
277
  * @param {Object} options - Command options
278
278
  */
279
279
  export function validateRunOptions(testCommand, options) {
280
- let errors = [];
280
+ const errors = [];
281
281
  if (!testCommand || testCommand.trim() === '') {
282
282
  errors.push('Test command is required');
283
283
  }
284
284
  if (options.port) {
285
- let port = parseInt(options.port, 10);
286
- if (isNaN(port) || port < 1 || port > 65535) {
285
+ const port = parseInt(options.port, 10);
286
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
287
287
  errors.push('Port must be a valid number between 1 and 65535');
288
288
  }
289
289
  }
290
290
  if (options.timeout) {
291
- let timeout = parseInt(options.timeout, 10);
292
- if (isNaN(timeout) || timeout < 1000) {
291
+ const timeout = parseInt(options.timeout, 10);
292
+ if (Number.isNaN(timeout) || timeout < 1000) {
293
293
  errors.push('Timeout must be at least 1000 milliseconds');
294
294
  }
295
295
  }
296
296
  if (options.batchSize !== undefined) {
297
- let n = parseInt(options.batchSize, 10);
297
+ const n = parseInt(options.batchSize, 10);
298
298
  if (!Number.isFinite(n) || n <= 0) {
299
299
  errors.push('Batch size must be a positive integer');
300
300
  }
301
301
  }
302
302
  if (options.uploadTimeout !== undefined) {
303
- let n = parseInt(options.uploadTimeout, 10);
303
+ const n = parseInt(options.uploadTimeout, 10);
304
304
  if (!Number.isFinite(n) || n <= 0) {
305
305
  errors.push('Upload timeout must be a positive integer (milliseconds)');
306
306
  }
@@ -1,7 +1,7 @@
1
- import { loadConfig } from '../utils/config-loader.js';
2
- import * as output from '../utils/output.js';
3
1
  import { createServices } from '../services/index.js';
2
+ import { loadConfig } from '../utils/config-loader.js';
4
3
  import { getApiUrl } from '../utils/environment-config.js';
4
+ import * as output from '../utils/output.js';
5
5
 
6
6
  /**
7
7
  * Status command implementation
@@ -19,11 +19,11 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
19
19
  output.info(`Checking status for build: ${buildId}`);
20
20
 
21
21
  // Load configuration with CLI overrides
22
- let allOptions = {
22
+ const allOptions = {
23
23
  ...globalOptions,
24
24
  ...options
25
25
  };
26
- let config = await loadConfig(globalOptions.config, allOptions);
26
+ const config = await loadConfig(globalOptions.config, allOptions);
27
27
 
28
28
  // Validate API token
29
29
  if (!config.apiKey) {
@@ -33,17 +33,17 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
33
33
 
34
34
  // Get API service
35
35
  output.startSpinner('Fetching build status...');
36
- let services = createServices(config, 'status');
37
- let {
36
+ const services = createServices(config, 'status');
37
+ const {
38
38
  apiService
39
39
  } = services;
40
40
 
41
41
  // Get build details via unified ApiService
42
- let buildStatus = await apiService.getBuild(buildId);
42
+ const buildStatus = await apiService.getBuild(buildId);
43
43
  output.stopSpinner();
44
44
 
45
45
  // Extract build data from API response
46
- let build = buildStatus.build || buildStatus;
46
+ const build = buildStatus.build || buildStatus;
47
47
 
48
48
  // Display build summary
49
49
  output.success(`Build: ${build.name || build.id}`);
@@ -77,15 +77,15 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
77
77
  }
78
78
 
79
79
  // Show build URL if we can construct it
80
- let baseUrl = config.baseUrl || getApiUrl();
80
+ const baseUrl = config.baseUrl || getApiUrl();
81
81
  if (baseUrl && build.project_id) {
82
- let buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
82
+ const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
83
83
  output.info(`View Build: ${buildUrl}`);
84
84
  }
85
85
 
86
86
  // Output JSON data for --json mode
87
87
  if (globalOptions.json) {
88
- let statusData = {
88
+ const statusData = {
89
89
  buildId: build.id,
90
90
  status: build.status,
91
91
  name: build.name,
@@ -131,9 +131,9 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
131
131
 
132
132
  // Show progress if build is still processing
133
133
  if (build.status === 'processing' || build.status === 'pending') {
134
- let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
134
+ const totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
135
135
  if (totalJobs > 0) {
136
- let progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
136
+ const progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
137
137
  output.info(`Progress: ${Math.round(progress * 100)}% complete`);
138
138
  }
139
139
  }
@@ -155,7 +155,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
155
155
  * @param {Object} options - Command options
156
156
  */
157
157
  export function validateStatusOptions(buildId) {
158
- let errors = [];
158
+ const errors = [];
159
159
  if (!buildId || buildId.trim() === '') {
160
160
  errors.push('Build ID is required');
161
161
  }
@@ -1,7 +1,7 @@
1
- import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
2
- import { join } from 'path';
3
- import { homedir } from 'os';
4
- import { spawn } from 'child_process';
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
5
  import * as output from '../utils/output.js';
6
6
  import { tddCommand } from './tdd.js';
7
7
 
@@ -19,7 +19,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
19
19
 
20
20
  // Check if server already running
21
21
  if (await isServerRunning(options.port || 47392)) {
22
- let port = options.port || 47392;
22
+ const port = options.port || 47392;
23
23
  output.info(`TDD server already running at http://localhost:${port}`);
24
24
  output.info(`Dashboard: http://localhost:${port}/dashboard`);
25
25
  if (options.open) {
@@ -29,13 +29,13 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
29
29
  }
30
30
  try {
31
31
  // Ensure .vizzly directory exists
32
- let vizzlyDir = join(process.cwd(), '.vizzly');
32
+ const vizzlyDir = join(process.cwd(), '.vizzly');
33
33
  if (!existsSync(vizzlyDir)) {
34
34
  mkdirSync(vizzlyDir, {
35
35
  recursive: true
36
36
  });
37
37
  }
38
- let port = options.port || 47392;
38
+ const port = options.port || 47392;
39
39
 
40
40
  // Show loading indicator if downloading baselines (but not in verbose mode since child shows progress)
41
41
  if (options.baselineBuild && !globalOptions.verbose) {
@@ -43,7 +43,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
43
43
  }
44
44
 
45
45
  // Spawn child process with stdio inherited during init for direct error visibility
46
- let child = spawn(process.execPath, [process.argv[1],
46
+ const child = spawn(process.execPath, [process.argv[1],
47
47
  // CLI entry point
48
48
  'tdd', 'start', '--daemon-child',
49
49
  // Special flag for child process
@@ -72,7 +72,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
72
72
  });
73
73
 
74
74
  // Timeout after 30 seconds to prevent indefinite wait
75
- let timeoutId = setTimeout(() => {
75
+ const timeoutId = setTimeout(() => {
76
76
  if (!initComplete && !initFailed) {
77
77
  initFailed = true;
78
78
  resolve();
@@ -95,8 +95,8 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
95
95
  child.unref();
96
96
 
97
97
  // Verify server started with retries
98
- let maxRetries = 10;
99
- let retryDelay = 200; // Start with 200ms
98
+ const maxRetries = 10;
99
+ const retryDelay = 200; // Start with 200ms
100
100
  let running = false;
101
101
  for (let i = 0; i < maxRetries && !running; i++) {
102
102
  await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
@@ -113,14 +113,14 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
113
113
 
114
114
  // Write server info to global location for SDK discovery (iOS/Swift can read this)
115
115
  try {
116
- let globalVizzlyDir = join(homedir(), '.vizzly');
116
+ const globalVizzlyDir = join(homedir(), '.vizzly');
117
117
  if (!existsSync(globalVizzlyDir)) {
118
118
  mkdirSync(globalVizzlyDir, {
119
119
  recursive: true
120
120
  });
121
121
  }
122
- let globalServerFile = join(globalVizzlyDir, 'server.json');
123
- let serverInfo = {
122
+ const globalServerFile = join(globalVizzlyDir, 'server.json');
123
+ const serverInfo = {
124
124
  pid: child.pid,
125
125
  port: port.toString(),
126
126
  startTime: Date.now()
@@ -160,11 +160,11 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
160
160
  * @private
161
161
  */
162
162
  export async function runDaemonChild(options = {}, globalOptions = {}) {
163
- let vizzlyDir = join(process.cwd(), '.vizzly');
164
- let port = options.port || 47392;
163
+ const vizzlyDir = join(process.cwd(), '.vizzly');
164
+ const port = options.port || 47392;
165
165
  try {
166
166
  // Use existing tddCommand but with daemon mode
167
- let {
167
+ const {
168
168
  cleanup
169
169
  } = await tddCommand(null,
170
170
  // No test command - server only
@@ -179,9 +179,9 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
179
179
  }
180
180
 
181
181
  // Store our PID for the stop command
182
- let pidFile = join(vizzlyDir, 'server.pid');
182
+ const pidFile = join(vizzlyDir, 'server.pid');
183
183
  writeFileSync(pidFile, process.pid.toString());
184
- let serverInfo = {
184
+ const serverInfo = {
185
185
  pid: process.pid,
186
186
  port: port,
187
187
  startTime: Date.now()
@@ -189,16 +189,16 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
189
189
  writeFileSync(join(vizzlyDir, 'server.json'), JSON.stringify(serverInfo, null, 2));
190
190
 
191
191
  // Set up graceful shutdown
192
- let handleShutdown = async () => {
192
+ const handleShutdown = async () => {
193
193
  try {
194
194
  // Clean up PID files
195
195
  if (existsSync(pidFile)) unlinkSync(pidFile);
196
- let serverFile = join(vizzlyDir, 'server.json');
196
+ const serverFile = join(vizzlyDir, 'server.json');
197
197
  if (existsSync(serverFile)) unlinkSync(serverFile);
198
198
 
199
199
  // Clean up global server file
200
200
  try {
201
- let globalServerFile = join(homedir(), '.vizzly', 'server.json');
201
+ const globalServerFile = join(homedir(), '.vizzly', 'server.json');
202
202
  if (existsSync(globalServerFile)) unlinkSync(globalServerFile);
203
203
  } catch {
204
204
  // Non-fatal
@@ -236,9 +236,9 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
236
236
  verbose: globalOptions.verbose,
237
237
  color: !globalOptions.noColor
238
238
  });
239
- let vizzlyDir = join(process.cwd(), '.vizzly');
240
- let pidFile = join(vizzlyDir, 'server.pid');
241
- let serverFile = join(vizzlyDir, 'server.json');
239
+ const vizzlyDir = join(process.cwd(), '.vizzly');
240
+ const pidFile = join(vizzlyDir, 'server.pid');
241
+ const serverFile = join(vizzlyDir, 'server.json');
242
242
 
243
243
  // First try to find process by PID file
244
244
  let pid = null;
@@ -251,10 +251,10 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
251
251
  }
252
252
 
253
253
  // If no PID file or invalid, try to find by port using lsof
254
- let port = options.port || 47392;
254
+ const port = options.port || 47392;
255
255
  if (!pid) {
256
256
  try {
257
- let lsofProcess = spawn('lsof', ['-ti', `:${port}`], {
257
+ const lsofProcess = spawn('lsof', ['-ti', `:${port}`], {
258
258
  stdio: 'pipe'
259
259
  });
260
260
  let lsofOutput = '';
@@ -264,8 +264,8 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
264
264
  await new Promise(resolve => {
265
265
  lsofProcess.on('close', code => {
266
266
  if (code === 0 && lsofOutput.trim()) {
267
- let foundPid = parseInt(lsofOutput.trim().split('\n')[0], 10);
268
- if (foundPid && !isNaN(foundPid)) {
267
+ const foundPid = parseInt(lsofOutput.trim().split('\n')[0], 10);
268
+ if (foundPid && !Number.isNaN(foundPid)) {
269
269
  pid = foundPid;
270
270
  }
271
271
  }
@@ -327,21 +327,21 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
327
327
  * @param {Object} options - Command options
328
328
  * @param {Object} globalOptions - Global CLI options
329
329
  */
330
- export async function tddStatusCommand(options, globalOptions = {}) {
330
+ export async function tddStatusCommand(_options, globalOptions = {}) {
331
331
  output.configure({
332
332
  json: globalOptions.json,
333
333
  verbose: globalOptions.verbose,
334
334
  color: !globalOptions.noColor
335
335
  });
336
- let vizzlyDir = join(process.cwd(), '.vizzly');
337
- let pidFile = join(vizzlyDir, 'server.pid');
338
- let serverFile = join(vizzlyDir, 'server.json');
336
+ const vizzlyDir = join(process.cwd(), '.vizzly');
337
+ const pidFile = join(vizzlyDir, 'server.pid');
338
+ const serverFile = join(vizzlyDir, 'server.json');
339
339
  if (!existsSync(pidFile)) {
340
340
  output.info('TDD server not running');
341
341
  return;
342
342
  }
343
343
  try {
344
- let pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
344
+ const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
345
345
 
346
346
  // Check if process is actually running
347
347
  process.kill(pid, 0); // Signal 0 just checks if process exists
@@ -354,7 +354,7 @@ export async function tddStatusCommand(options, globalOptions = {}) {
354
354
  }
355
355
 
356
356
  // Try to check health endpoint
357
- let health = await checkServerHealth(serverInfo.port);
357
+ const health = await checkServerHealth(serverInfo.port);
358
358
  if (health.running) {
359
359
  output.success(`TDD server running (PID: ${pid})`);
360
360
  output.info(`Dashboard: http://localhost:${serverInfo.port}/`);
@@ -365,10 +365,10 @@ export async function tddStatusCommand(options, globalOptions = {}) {
365
365
  output.info(` Settings: http://localhost:${serverInfo.port}/settings`);
366
366
  output.info(` Projects: http://localhost:${serverInfo.port}/projects`);
367
367
  if (serverInfo.startTime) {
368
- let uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
369
- let hours = Math.floor(uptime / 3600);
370
- let minutes = Math.floor(uptime % 3600 / 60);
371
- let seconds = uptime % 60;
368
+ const uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
369
+ const hours = Math.floor(uptime / 3600);
370
+ const minutes = Math.floor(uptime % 3600 / 60);
371
+ const seconds = uptime % 60;
372
372
  let uptimeStr = '';
373
373
  if (hours > 0) uptimeStr += `${hours}h `;
374
374
  if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
@@ -398,7 +398,7 @@ export async function tddStatusCommand(options, globalOptions = {}) {
398
398
  */
399
399
  async function isServerRunning(port = 47392) {
400
400
  try {
401
- let health = await checkServerHealth(port);
401
+ const health = await checkServerHealth(port);
402
402
  return health.running;
403
403
  } catch {
404
404
  return false;
@@ -411,8 +411,8 @@ async function isServerRunning(port = 47392) {
411
411
  */
412
412
  async function checkServerHealth(port = 47392) {
413
413
  try {
414
- let response = await fetch(`http://localhost:${port}/health`);
415
- let data = await response.json();
414
+ const response = await fetch(`http://localhost:${port}/health`);
415
+ const data = await response.json();
416
416
  return {
417
417
  running: response.ok,
418
418
  port: data.port,
@@ -430,7 +430,7 @@ async function checkServerHealth(port = 47392) {
430
430
  * @private
431
431
  */
432
432
  function openDashboard(port = 47392) {
433
- let url = `http://localhost:${port}/dashboard`;
433
+ const url = `http://localhost:${port}/dashboard`;
434
434
 
435
435
  // Cross-platform open command
436
436
  let openCmd;
@@ -1,7 +1,7 @@
1
- import { loadConfig } from '../utils/config-loader.js';
2
- import * as output from '../utils/output.js';
3
1
  import { createServices } from '../services/index.js';
2
+ import { loadConfig } from '../utils/config-loader.js';
4
3
  import { detectBranch, detectCommit } from '../utils/git.js';
4
+ import * as output from '../utils/output.js';
5
5
 
6
6
  /**
7
7
  * TDD command implementation
@@ -20,7 +20,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
20
20
  let isCleanedUp = false;
21
21
 
22
22
  // Create cleanup function that can be called by the caller
23
- let cleanup = async () => {
23
+ const cleanup = async () => {
24
24
  if (isCleanedUp) return;
25
25
  isCleanedUp = true;
26
26
  output.cleanup();
@@ -30,14 +30,14 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
30
30
  };
31
31
  try {
32
32
  // Load configuration with CLI overrides
33
- let allOptions = {
33
+ const allOptions = {
34
34
  ...globalOptions,
35
35
  ...options
36
36
  };
37
- let config = await loadConfig(globalOptions.config, allOptions);
37
+ const config = await loadConfig(globalOptions.config, allOptions);
38
38
 
39
39
  // Dev mode works locally by default - only needs token for baseline download
40
- let needsToken = options.baselineBuild || options.baselineComparison;
40
+ const needsToken = options.baselineBuild || options.baselineComparison;
41
41
  if (!config.apiKey && needsToken) {
42
42
  throw new Error('API token required when using --baseline-build or --baseline-comparison flags');
43
43
  }
@@ -46,12 +46,12 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
46
46
  config.allowNoToken = true;
47
47
 
48
48
  // Collect git metadata
49
- let branch = await detectBranch(options.branch);
50
- let commit = await detectCommit(options.commit);
49
+ const branch = await detectBranch(options.branch);
50
+ const commit = await detectCommit(options.commit);
51
51
 
52
52
  // Show header (skip in daemon mode)
53
53
  if (!options.daemon) {
54
- let mode = config.apiKey ? 'local' : 'local';
54
+ const mode = config.apiKey ? 'local' : 'local';
55
55
  output.header('tdd', mode);
56
56
 
57
57
  // Show config in verbose mode
@@ -64,17 +64,17 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
64
64
 
65
65
  // Create services
66
66
  output.startSpinner('Initializing TDD server...');
67
- let configWithVerbose = {
67
+ const configWithVerbose = {
68
68
  ...config,
69
69
  verbose: globalOptions.verbose
70
70
  };
71
- let services = createServices(configWithVerbose, 'tdd');
71
+ const services = createServices(configWithVerbose, 'tdd');
72
72
  testRunner = services.testRunner;
73
73
  output.stopSpinner();
74
74
 
75
75
  // Set up event handlers for user feedback
76
76
  testRunner.on('progress', progressData => {
77
- let {
77
+ const {
78
78
  message: progressMessage
79
79
  } = progressData;
80
80
  output.progress(progressMessage || 'Running tests...');
@@ -96,7 +96,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
96
96
  output.debug('capture', screenshotInfo.name);
97
97
  });
98
98
  testRunner.on('comparison-result', comparisonInfo => {
99
- let {
99
+ const {
100
100
  name,
101
101
  status,
102
102
  pixelDifference
@@ -112,7 +112,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
112
112
  testRunner.on('error', error => {
113
113
  output.error('Test runner error', error);
114
114
  });
115
- let runOptions = {
115
+ const runOptions = {
116
116
  testCommand,
117
117
  port: config.server.port,
118
118
  timeout: config.server.timeout,
@@ -149,19 +149,19 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
149
149
 
150
150
  // Normal dev mode - run tests
151
151
  output.debug('run', testCommand);
152
- let runResult = await testRunner.run(runOptions);
152
+ const runResult = await testRunner.run(runOptions);
153
153
 
154
154
  // Show summary
155
- let {
155
+ const {
156
156
  screenshotsCaptured,
157
157
  comparisons
158
158
  } = runResult;
159
159
 
160
160
  // Determine success based on comparison results
161
- let hasFailures = runResult.failed || runResult.comparisons && runResult.comparisons.some(c => c.status === 'failed');
161
+ const hasFailures = runResult.failed || runResult.comparisons?.some(c => c.status === 'failed');
162
162
  if (comparisons && comparisons.length > 0) {
163
- let passed = comparisons.filter(c => c.status === 'passed').length;
164
- let failed = comparisons.filter(c => c.status === 'failed').length;
163
+ const passed = comparisons.filter(c => c.status === 'passed').length;
164
+ const failed = comparisons.filter(c => c.status === 'failed').length;
165
165
  if (hasFailures) {
166
166
  output.error(`${failed} visual difference${failed !== 1 ? 's' : ''} detected`);
167
167
  output.info(`Check .vizzly/diffs/ for diff images`);
@@ -200,26 +200,26 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
200
200
  * @param {Object} options - Command options
201
201
  */
202
202
  export function validateTddOptions(testCommand, options) {
203
- let errors = [];
203
+ const errors = [];
204
204
  if (!testCommand || testCommand.trim() === '') {
205
205
  errors.push('Test command is required');
206
206
  }
207
207
  if (options.port) {
208
- let port = parseInt(options.port, 10);
209
- if (isNaN(port) || port < 1 || port > 65535) {
208
+ const port = parseInt(options.port, 10);
209
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
210
210
  errors.push('Port must be a valid number between 1 and 65535');
211
211
  }
212
212
  }
213
213
  if (options.timeout) {
214
- let timeout = parseInt(options.timeout, 10);
215
- if (isNaN(timeout) || timeout < 1000) {
214
+ const timeout = parseInt(options.timeout, 10);
215
+ if (Number.isNaN(timeout) || timeout < 1000) {
216
216
  errors.push('Timeout must be at least 1000 milliseconds');
217
217
  }
218
218
  }
219
219
  if (options.threshold !== undefined) {
220
- let threshold = parseFloat(options.threshold);
221
- if (isNaN(threshold) || threshold < 0 || threshold > 1) {
222
- errors.push('Threshold must be a number between 0 and 1');
220
+ const threshold = parseFloat(options.threshold);
221
+ if (Number.isNaN(threshold) || threshold < 0) {
222
+ errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
223
223
  }
224
224
  }
225
225
  return errors;