@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
@@ -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,25 +200,25 @@ 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) {
220
+ const threshold = parseFloat(options.threshold);
221
+ if (Number.isNaN(threshold) || threshold < 0) {
222
222
  errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
223
223
  }
224
224
  }
@@ -1,8 +1,8 @@
1
- import { loadConfig } from '../utils/config-loader.js';
2
- import * as output from '../utils/output.js';
1
+ import { ApiService } from '../services/api-service.js';
3
2
  import { createServices } from '../services/index.js';
3
+ import { loadConfig } from '../utils/config-loader.js';
4
4
  import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
5
- import { ApiService } from '../services/api-service.js';
5
+ import * as output from '../utils/output.js';
6
6
 
7
7
  /**
8
8
  * Construct proper build URL with org/project context
@@ -13,13 +13,13 @@ import { ApiService } from '../services/api-service.js';
13
13
  */
14
14
  async function constructBuildUrl(buildId, apiUrl, apiToken) {
15
15
  try {
16
- let apiService = new ApiService({
16
+ const apiService = new ApiService({
17
17
  baseUrl: apiUrl,
18
18
  token: apiToken,
19
19
  command: 'upload'
20
20
  });
21
- let tokenContext = await apiService.getTokenContext();
22
- let baseUrl = apiUrl.replace(/\/api.*$/, '');
21
+ const tokenContext = await apiService.getTokenContext();
22
+ const baseUrl = apiUrl.replace(/\/api.*$/, '');
23
23
  if (tokenContext.organization?.slug && tokenContext.project?.slug) {
24
24
  return `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${buildId}`;
25
25
  }
@@ -31,7 +31,7 @@ async function constructBuildUrl(buildId, apiUrl, apiToken) {
31
31
  }
32
32
 
33
33
  // Fallback URL construction
34
- let baseUrl = apiUrl.replace(/\/api.*$/, '');
34
+ const baseUrl = apiUrl.replace(/\/api.*$/, '');
35
35
  return `${baseUrl}/builds/${buildId}`;
36
36
  }
37
37
 
@@ -49,12 +49,12 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
49
49
  });
50
50
  let buildId = null;
51
51
  let config = null;
52
- let uploadStartTime = Date.now();
52
+ const uploadStartTime = Date.now();
53
53
  try {
54
54
  output.info('Starting upload process...');
55
55
 
56
56
  // Load configuration with CLI overrides
57
- let allOptions = {
57
+ const allOptions = {
58
58
  ...globalOptions,
59
59
  ...options
60
60
  };
@@ -67,11 +67,11 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
67
67
  }
68
68
 
69
69
  // Collect git metadata if not provided
70
- let branch = await detectBranch(options.branch);
71
- let commit = await detectCommit(options.commit);
72
- let message = options.message || (await detectCommitMessage());
73
- let buildName = await generateBuildNameWithGit(options.buildName);
74
- let pullRequestNumber = detectPullRequestNumber();
70
+ const branch = await detectBranch(options.branch);
71
+ const commit = await detectCommit(options.commit);
72
+ const message = options.message || (await detectCommitMessage());
73
+ const buildName = await generateBuildNameWithGit(options.buildName);
74
+ const pullRequestNumber = detectPullRequestNumber();
75
75
  output.info(`Uploading screenshots from: ${screenshotsPath}`);
76
76
  if (globalOptions.verbose) {
77
77
  output.info('Configuration loaded');
@@ -85,11 +85,11 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
85
85
 
86
86
  // Get uploader service
87
87
  output.startSpinner('Initializing uploader...');
88
- let services = createServices(config, 'upload');
89
- let uploader = services.uploader;
88
+ const services = createServices(config, 'upload');
89
+ const uploader = services.uploader;
90
90
 
91
91
  // Prepare upload options with progress callback
92
- let uploadOptions = {
92
+ const uploadOptions = {
93
93
  screenshotsDir: screenshotsPath,
94
94
  buildName,
95
95
  branch,
@@ -102,7 +102,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
102
102
  pullRequestNumber,
103
103
  parallelId: config.parallelId,
104
104
  onProgress: progressData => {
105
- let {
105
+ const {
106
106
  message: progressMessage,
107
107
  current,
108
108
  total,
@@ -128,19 +128,19 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
128
128
 
129
129
  // Start upload
130
130
  output.progress('Starting upload...');
131
- let result = await uploader.upload(uploadOptions);
131
+ const result = await uploader.upload(uploadOptions);
132
132
  buildId = result.buildId; // Ensure we have the buildId
133
133
 
134
134
  // Mark build as completed
135
135
  if (result.buildId) {
136
136
  output.progress('Finalizing build...');
137
137
  try {
138
- let apiService = new ApiService({
138
+ const apiService = new ApiService({
139
139
  baseUrl: config.apiUrl,
140
140
  token: config.apiKey,
141
141
  command: 'upload'
142
142
  });
143
- let executionTime = Date.now() - uploadStartTime;
143
+ const executionTime = Date.now() - uploadStartTime;
144
144
  await apiService.finalizeBuild(result.buildId, true, executionTime);
145
145
  } catch (error) {
146
146
  output.warn(`Failed to finalize build: ${error.message}`);
@@ -152,7 +152,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
152
152
  if (result.buildId) {
153
153
  output.info(`🐻 Vizzly: Uploaded ${result.stats.uploaded} of ${result.stats.total} screenshots to build ${result.buildId}`);
154
154
  // Use API-provided URL or construct proper URL with org/project context
155
- let buildUrl = result.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
155
+ const buildUrl = result.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
156
156
  output.info(`🔗 Vizzly: View results at ${buildUrl}`);
157
157
  }
158
158
 
@@ -160,7 +160,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
160
160
  if (options.wait && result.buildId) {
161
161
  output.info('Waiting for build completion...');
162
162
  output.startSpinner('Processing comparisons...');
163
- let buildResult = await uploader.waitForBuild(result.buildId);
163
+ const buildResult = await uploader.waitForBuild(result.buildId);
164
164
  output.success('Build processing completed');
165
165
 
166
166
  // Show build processing results
@@ -170,7 +170,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
170
170
  output.success(`All ${buildResult.passedComparisons} visual comparisons passed`);
171
171
  }
172
172
  // Use API-provided URL or construct proper URL with org/project context
173
- let buildUrl = buildResult.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
173
+ const buildUrl = buildResult.url || (await constructBuildUrl(result.buildId, config.apiUrl, config.apiKey));
174
174
  output.info(`🔗 Vizzly: View results at ${buildUrl}`);
175
175
  }
176
176
  output.cleanup();
@@ -178,19 +178,19 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
178
178
  // Mark build as failed if we have a buildId and config
179
179
  if (buildId && config) {
180
180
  try {
181
- let apiService = new ApiService({
181
+ const apiService = new ApiService({
182
182
  baseUrl: config.apiUrl,
183
183
  token: config.apiKey,
184
184
  command: 'upload'
185
185
  });
186
- let executionTime = Date.now() - uploadStartTime;
186
+ const executionTime = Date.now() - uploadStartTime;
187
187
  await apiService.finalizeBuild(buildId, false, executionTime);
188
188
  } catch {
189
189
  // Silent fail on cleanup
190
190
  }
191
191
  }
192
192
  // Use user-friendly error message if available
193
- let errorMessage = error?.getUserMessage ? error.getUserMessage() : error.message;
193
+ const errorMessage = error?.getUserMessage ? error.getUserMessage() : error.message;
194
194
  output.error(errorMessage || 'Upload failed', error);
195
195
  process.exit(1);
196
196
  }
@@ -202,7 +202,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
202
202
  * @param {Object} options - Command options
203
203
  */
204
204
  export function validateUploadOptions(screenshotsPath, options) {
205
- let errors = [];
205
+ const errors = [];
206
206
  if (!screenshotsPath) {
207
207
  errors.push('Screenshots path is required');
208
208
  }
@@ -214,19 +214,19 @@ export function validateUploadOptions(screenshotsPath, options) {
214
214
  }
215
215
  }
216
216
  if (options.threshold !== undefined) {
217
- let threshold = parseFloat(options.threshold);
218
- if (isNaN(threshold) || threshold < 0) {
217
+ const threshold = parseFloat(options.threshold);
218
+ if (Number.isNaN(threshold) || threshold < 0) {
219
219
  errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
220
220
  }
221
221
  }
222
222
  if (options.batchSize !== undefined) {
223
- let n = parseInt(options.batchSize, 10);
223
+ const n = parseInt(options.batchSize, 10);
224
224
  if (!Number.isFinite(n) || n <= 0) {
225
225
  errors.push('Batch size must be a positive integer');
226
226
  }
227
227
  }
228
228
  if (options.uploadTimeout !== undefined) {
229
- let n = parseInt(options.uploadTimeout, 10);
229
+ const n = parseInt(options.uploadTimeout, 10);
230
230
  if (!Number.isFinite(n) || n <= 0) {
231
231
  errors.push('Upload timeout must be a positive integer (milliseconds)');
232
232
  }
@@ -3,10 +3,10 @@
3
3
  * Shows current user and authentication status
4
4
  */
5
5
 
6
- import * as output from '../utils/output.js';
7
6
  import { AuthService } from '../services/auth-service.js';
8
7
  import { getApiUrl } from '../utils/environment-config.js';
9
8
  import { getAuthTokens } from '../utils/global-config.js';
9
+ import * as output from '../utils/output.js';
10
10
 
11
11
  /**
12
12
  * Whoami command implementation
@@ -21,7 +21,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
21
21
  });
22
22
  try {
23
23
  // Check if user is logged in
24
- let auth = await getAuthTokens();
24
+ const auth = await getAuthTokens();
25
25
  if (!auth || !auth.accessToken) {
26
26
  if (globalOptions.json) {
27
27
  output.data({
@@ -38,10 +38,10 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
38
38
 
39
39
  // Get current user info
40
40
  output.startSpinner('Fetching user information...');
41
- let authService = new AuthService({
41
+ const authService = new AuthService({
42
42
  baseUrl: options.apiUrl || getApiUrl()
43
43
  });
44
- let response = await authService.whoami();
44
+ const response = await authService.whoami();
45
45
  output.stopSpinner();
46
46
 
47
47
  // Output in JSON mode
@@ -76,7 +76,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
76
76
  if (response.organizations && response.organizations.length > 0) {
77
77
  output.blank();
78
78
  output.info('Organizations:');
79
- for (let org of response.organizations) {
79
+ for (const org of response.organizations) {
80
80
  let orgInfo = ` - ${org.name}`;
81
81
  if (org.slug) {
82
82
  orgInfo += ` (@${org.slug})`;
@@ -94,12 +94,12 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
94
94
  // Show token expiry info
95
95
  if (auth.expiresAt) {
96
96
  output.blank();
97
- let expiresAt = new Date(auth.expiresAt);
98
- let now = new Date();
99
- let msUntilExpiry = expiresAt.getTime() - now.getTime();
100
- let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
101
- let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
102
- let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
97
+ const expiresAt = new Date(auth.expiresAt);
98
+ const now = new Date();
99
+ const msUntilExpiry = expiresAt.getTime() - now.getTime();
100
+ const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
101
+ const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
102
+ const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
103
103
  if (msUntilExpiry <= 0) {
104
104
  output.warn('Token has expired');
105
105
  output.blank();
@@ -149,7 +149,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
149
149
  * @param {Object} options - Command options
150
150
  */
151
151
  export function validateWhoamiOptions() {
152
- let errors = [];
152
+ const errors = [];
153
153
 
154
154
  // No specific validation needed for whoami command
155
155