@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
@@ -3,10 +3,10 @@
3
3
  * Authenticates user via OAuth device flow
4
4
  */
5
5
 
6
- import * as output from '../utils/output.js';
7
6
  import { AuthService } from '../services/auth-service.js';
8
- import { getApiUrl } from '../utils/environment-config.js';
9
7
  import { openBrowser } from '../utils/browser.js';
8
+ import { getApiUrl } from '../utils/environment-config.js';
9
+ import * as output from '../utils/output.js';
10
10
 
11
11
  /**
12
12
  * Login command implementation using OAuth device flow
@@ -19,31 +19,31 @@ export async function loginCommand(options = {}, globalOptions = {}) {
19
19
  verbose: globalOptions.verbose,
20
20
  color: !globalOptions.noColor
21
21
  });
22
- let colors = output.getColors();
22
+ const colors = output.getColors();
23
23
  try {
24
24
  output.info('Starting Vizzly authentication...');
25
25
  output.blank();
26
26
 
27
27
  // Create auth service
28
- let authService = new AuthService({
28
+ const authService = new AuthService({
29
29
  baseUrl: options.apiUrl || getApiUrl()
30
30
  });
31
31
 
32
32
  // Initiate device flow
33
33
  output.startSpinner('Connecting to Vizzly...');
34
- let deviceFlow = await authService.initiateDeviceFlow();
34
+ const deviceFlow = await authService.initiateDeviceFlow();
35
35
  output.stopSpinner();
36
36
 
37
37
  // Handle both snake_case and camelCase field names
38
- let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
39
- let userCode = deviceFlow.user_code || deviceFlow.userCode;
40
- let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
38
+ const verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
39
+ const userCode = deviceFlow.user_code || deviceFlow.userCode;
40
+ const deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
41
41
  if (!verificationUri || !userCode || !deviceCode) {
42
42
  throw new Error('Invalid device flow response from server');
43
43
  }
44
44
 
45
45
  // Build URL with pre-filled code
46
- let urlWithCode = `${verificationUri}?code=${userCode}`;
46
+ const urlWithCode = `${verificationUri}?code=${userCode}`;
47
47
 
48
48
  // Display user code prominently
49
49
  output.blank();
@@ -61,7 +61,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
61
61
  output.blank();
62
62
 
63
63
  // Try to open browser with pre-filled code
64
- let browserOpened = await openBrowser(urlWithCode);
64
+ const browserOpened = await openBrowser(urlWithCode);
65
65
  if (browserOpened) {
66
66
  output.info('Opening browser...');
67
67
  } else {
@@ -83,12 +83,12 @@ export async function loginCommand(options = {}, globalOptions = {}) {
83
83
 
84
84
  // Check authorization status
85
85
  output.startSpinner('Checking authorization status...');
86
- let pollResponse = await authService.pollDeviceAuthorization(deviceCode);
86
+ const pollResponse = await authService.pollDeviceAuthorization(deviceCode);
87
87
  output.stopSpinner();
88
88
  let tokenData = null;
89
89
 
90
90
  // Check if authorization was successful by looking for tokens
91
- if (pollResponse.tokens && pollResponse.tokens.accessToken) {
91
+ if (pollResponse.tokens?.accessToken) {
92
92
  // Success! We got tokens
93
93
  tokenData = pollResponse;
94
94
  } else if (pollResponse.status === 'pending') {
@@ -103,10 +103,10 @@ export async function loginCommand(options = {}, globalOptions = {}) {
103
103
 
104
104
  // Complete device flow and save tokens
105
105
  // Handle both snake_case and camelCase for token data, and nested tokens object
106
- let tokensData = tokenData.tokens || tokenData;
107
- let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
108
- let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
109
- let tokens = {
106
+ const tokensData = tokenData.tokens || tokenData;
107
+ const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
108
+ const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
109
+ const tokens = {
110
110
  accessToken: tokensData.accessToken || tokensData.access_token,
111
111
  refreshToken: tokensData.refreshToken || tokensData.refresh_token,
112
112
  expiresAt: tokenExpiresAt,
@@ -129,7 +129,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
129
129
  if (tokens.organizations && tokens.organizations.length > 0) {
130
130
  output.blank();
131
131
  output.info('Organizations:');
132
- for (let org of tokens.organizations) {
132
+ for (const org of tokens.organizations) {
133
133
  output.print(` - ${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
134
134
  }
135
135
  }
@@ -137,11 +137,11 @@ export async function loginCommand(options = {}, globalOptions = {}) {
137
137
  // Show token expiry info
138
138
  if (tokens.expiresAt) {
139
139
  output.blank();
140
- let expiresAt = new Date(tokens.expiresAt);
141
- let msUntilExpiry = expiresAt.getTime() - Date.now();
142
- let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
143
- let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
144
- let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
140
+ const expiresAt = new Date(tokens.expiresAt);
141
+ const msUntilExpiry = expiresAt.getTime() - Date.now();
142
+ const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
143
+ const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
144
+ const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
145
145
  if (daysUntilExpiry > 0) {
146
146
  output.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
147
147
  } else if (hoursUntilExpiry > 0) {
@@ -180,7 +180,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
180
180
  * @param {Object} options - Command options
181
181
  */
182
182
  export function validateLoginOptions() {
183
- let errors = [];
183
+ const errors = [];
184
184
 
185
185
  // No specific validation needed for login command
186
186
  // OAuth device flow handles everything via browser
@@ -3,10 +3,10 @@
3
3
  * Clears stored authentication tokens
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
  * Logout command implementation
@@ -21,7 +21,7 @@ export async function logoutCommand(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
  output.info('You are not logged in');
27
27
  output.cleanup();
@@ -30,7 +30,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
30
30
 
31
31
  // Logout
32
32
  output.startSpinner('Logging out...');
33
- let authService = new AuthService({
33
+ const authService = new AuthService({
34
34
  baseUrl: options.apiUrl || getApiUrl()
35
35
  });
36
36
  await authService.logout();
@@ -58,7 +58,7 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
58
58
  * @param {Object} options - Command options
59
59
  */
60
60
  export function validateLogoutOptions() {
61
- let errors = [];
61
+ const errors = [];
62
62
 
63
63
  // No specific validation needed for logout command
64
64
 
@@ -3,12 +3,12 @@
3
3
  * Select, list, and manage project tokens
4
4
  */
5
5
 
6
- import * as output from '../utils/output.js';
6
+ import { resolve } from 'node:path';
7
+ import readline from 'node:readline';
7
8
  import { AuthService } from '../services/auth-service.js';
8
9
  import { getApiUrl } from '../utils/environment-config.js';
9
- import { getAuthTokens, saveProjectMapping, getProjectMapping, getProjectMappings, deleteProjectMapping } from '../utils/global-config.js';
10
- import { resolve } from 'path';
11
- import readline from 'readline';
10
+ import { deleteProjectMapping, getAuthTokens, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
11
+ import * as output from '../utils/output.js';
12
12
 
13
13
  /**
14
14
  * Project select command - configure project for current directory
@@ -23,20 +23,20 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
23
23
  });
24
24
  try {
25
25
  // Check authentication
26
- let auth = await getAuthTokens();
26
+ const auth = await getAuthTokens();
27
27
  if (!auth || !auth.accessToken) {
28
28
  output.error('Not authenticated');
29
29
  output.blank();
30
30
  output.info('Run "vizzly login" to authenticate first');
31
31
  process.exit(1);
32
32
  }
33
- let authService = new AuthService({
33
+ const authService = new AuthService({
34
34
  baseUrl: options.apiUrl || getApiUrl()
35
35
  });
36
36
 
37
37
  // Get user info to show organizations
38
38
  output.startSpinner('Fetching organizations...');
39
- let userInfo = await authService.whoami();
39
+ const userInfo = await authService.whoami();
40
40
  output.stopSpinner();
41
41
  if (!userInfo.organizations || userInfo.organizations.length === 0) {
42
42
  output.error('No organizations found');
@@ -53,12 +53,12 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
53
53
  output.print(` ${index + 1}. ${org.name} (@${org.slug})`);
54
54
  });
55
55
  output.blank();
56
- let orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
57
- let selectedOrg = userInfo.organizations[orgChoice - 1];
56
+ const orgChoice = await promptNumber('Enter number', 1, userInfo.organizations.length);
57
+ const selectedOrg = userInfo.organizations[orgChoice - 1];
58
58
 
59
59
  // List projects for organization
60
60
  output.startSpinner(`Fetching projects for ${selectedOrg.name}...`);
61
- let response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
61
+ const response = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project`, {
62
62
  headers: {
63
63
  Authorization: `Bearer ${auth.accessToken}`,
64
64
  'X-Organization': selectedOrg.slug
@@ -67,7 +67,7 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
67
67
  output.stopSpinner();
68
68
 
69
69
  // Handle both array response and object with projects property
70
- let projects = Array.isArray(response) ? response : response.projects || [];
70
+ const projects = Array.isArray(response) ? response : response.projects || [];
71
71
  if (projects.length === 0) {
72
72
  output.error('No projects found');
73
73
  output.blank();
@@ -83,12 +83,12 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
83
83
  output.print(` ${index + 1}. ${project.name} (${project.slug})`);
84
84
  });
85
85
  output.blank();
86
- let projectChoice = await promptNumber('Enter number', 1, projects.length);
87
- let selectedProject = projects[projectChoice - 1];
86
+ const projectChoice = await promptNumber('Enter number', 1, projects.length);
87
+ const selectedProject = projects[projectChoice - 1];
88
88
 
89
89
  // Create API token for project
90
90
  output.startSpinner(`Creating API token for ${selectedProject.name}...`);
91
- let tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
91
+ const tokenResponse = await makeAuthenticatedRequest(`${options.apiUrl || getApiUrl()}/api/project/${selectedProject.slug}/tokens`, {
92
92
  method: 'POST',
93
93
  headers: {
94
94
  Authorization: `Bearer ${auth.accessToken}`,
@@ -103,7 +103,7 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
103
103
  output.stopSpinner();
104
104
 
105
105
  // Save project mapping
106
- let currentDir = resolve(process.cwd());
106
+ const currentDir = resolve(process.cwd());
107
107
  await saveProjectMapping(currentDir, {
108
108
  token: tokenResponse.token,
109
109
  projectSlug: selectedProject.slug,
@@ -135,8 +135,8 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
135
135
  color: !globalOptions.noColor
136
136
  });
137
137
  try {
138
- let mappings = await getProjectMappings();
139
- let paths = Object.keys(mappings);
138
+ const mappings = await getProjectMappings();
139
+ const paths = Object.keys(mappings);
140
140
  if (paths.length === 0) {
141
141
  output.info('No projects configured');
142
142
  output.blank();
@@ -151,14 +151,14 @@ export async function projectListCommand(_options = {}, globalOptions = {}) {
151
151
  }
152
152
  output.info('Configured projects:');
153
153
  output.blank();
154
- let currentDir = resolve(process.cwd());
155
- for (let path of paths) {
156
- let mapping = mappings[path];
157
- let isCurrent = path === currentDir;
158
- let marker = isCurrent ? '→' : ' ';
154
+ const currentDir = resolve(process.cwd());
155
+ for (const path of paths) {
156
+ const mapping = mappings[path];
157
+ const isCurrent = path === currentDir;
158
+ const marker = isCurrent ? '→' : ' ';
159
159
 
160
160
  // Extract token string (handle both string and object formats)
161
- let tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
161
+ const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
162
162
  output.print(`${marker} ${path}`);
163
163
  output.print(` Project: ${mapping.projectName} (${mapping.projectSlug})`);
164
164
  output.print(` Organization: ${mapping.organizationSlug}`);
@@ -187,8 +187,8 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
187
187
  color: !globalOptions.noColor
188
188
  });
189
189
  try {
190
- let currentDir = resolve(process.cwd());
191
- let mapping = await getProjectMapping(currentDir);
190
+ const currentDir = resolve(process.cwd());
191
+ const mapping = await getProjectMapping(currentDir);
192
192
  if (!mapping) {
193
193
  output.error('No project configured for this directory');
194
194
  output.blank();
@@ -197,7 +197,7 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
197
197
  }
198
198
 
199
199
  // Extract token string (handle both string and object formats)
200
- let tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
200
+ const tokenStr = typeof mapping.token === 'string' ? mapping.token : mapping.token?.token || '[invalid token]';
201
201
  if (globalOptions.json) {
202
202
  output.data({
203
203
  token: tokenStr,
@@ -224,11 +224,11 @@ export async function projectTokenCommand(_options = {}, globalOptions = {}) {
224
224
  * Helper to make authenticated API request
225
225
  */
226
226
  async function makeAuthenticatedRequest(url, options = {}) {
227
- let response = await fetch(url, options);
227
+ const response = await fetch(url, options);
228
228
  if (!response.ok) {
229
229
  let errorText = '';
230
230
  try {
231
- let errorData = await response.json();
231
+ const errorData = await response.json();
232
232
  errorText = errorData.error || errorData.message || '';
233
233
  } catch {
234
234
  errorText = await response.text();
@@ -243,14 +243,14 @@ async function makeAuthenticatedRequest(url, options = {}) {
243
243
  */
244
244
  function promptNumber(message, min, max) {
245
245
  return new Promise(resolve => {
246
- let rl = readline.createInterface({
246
+ const rl = readline.createInterface({
247
247
  input: process.stdin,
248
248
  output: process.stdout
249
249
  });
250
- let ask = () => {
250
+ const ask = () => {
251
251
  rl.question(`${message} (${min}-${max}): `, answer => {
252
- let num = parseInt(answer, 10);
253
- if (isNaN(num) || num < min || num > max) {
252
+ const num = parseInt(answer, 10);
253
+ if (Number.isNaN(num) || num < min || num > max) {
254
254
  output.print(`Please enter a number between ${min} and ${max}`);
255
255
  ask();
256
256
  } else {
@@ -275,8 +275,8 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
275
275
  color: !globalOptions.noColor
276
276
  });
277
277
  try {
278
- let currentDir = resolve(process.cwd());
279
- let mapping = await getProjectMapping(currentDir);
278
+ const currentDir = resolve(process.cwd());
279
+ const mapping = await getProjectMapping(currentDir);
280
280
  if (!mapping) {
281
281
  output.info('No project configured for this directory');
282
282
  output.cleanup();
@@ -290,7 +290,7 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
290
290
  output.print(` Organization: ${mapping.organizationSlug}`);
291
291
  output.print(` Directory: ${currentDir}`);
292
292
  output.blank();
293
- let confirmed = await promptConfirm('Remove this project configuration?');
293
+ const confirmed = await promptConfirm('Remove this project configuration?');
294
294
  if (!confirmed) {
295
295
  output.info('Cancelled');
296
296
  output.cleanup();
@@ -312,7 +312,7 @@ export async function projectRemoveCommand(_options = {}, globalOptions = {}) {
312
312
  */
313
313
  function promptConfirm(message) {
314
314
  return new Promise(resolve => {
315
- let rl = readline.createInterface({
315
+ const rl = readline.createInterface({
316
316
  input: process.stdin,
317
317
  output: process.stdout
318
318
  });
@@ -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
  }