@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,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'fs/promises';
3
- import path from 'path';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
4
5
  import { VizzlyError } from '../errors/vizzly-error.js';
5
- import * as output from '../utils/output.js';
6
6
  import { loadPlugins } from '../plugin-loader.js';
7
7
  import { loadConfig } from '../utils/config-loader.js';
8
- import { z } from 'zod';
8
+ import * as output from '../utils/output.js';
9
9
 
10
10
  /**
11
11
  * Simple configuration setup for Vizzly CLI
@@ -19,8 +19,8 @@ export class InitCommand {
19
19
  output.blank();
20
20
  try {
21
21
  // Check for existing config
22
- let configPath = path.join(process.cwd(), 'vizzly.config.js');
23
- let hasConfig = await this.fileExists(configPath);
22
+ const configPath = path.join(process.cwd(), 'vizzly.config.js');
23
+ const hasConfig = await this.fileExists(configPath);
24
24
  if (hasConfig && !options.force) {
25
25
  output.info('❌ A vizzly.config.js file already exists. Use --force to overwrite.');
26
26
  return;
@@ -60,9 +60,9 @@ export class InitCommand {
60
60
  timeout: 30000
61
61
  },
62
62
 
63
- // Comparison configuration
63
+ // Comparison configuration (CIEDE2000 Delta E: 0=exact, 1=JND, 2=recommended)
64
64
  comparison: {
65
- threshold: 0.1
65
+ threshold: 2.0
66
66
  },
67
67
 
68
68
  // TDD configuration
@@ -71,16 +71,16 @@ export class InitCommand {
71
71
  }`;
72
72
 
73
73
  // Add plugin configurations
74
- let pluginConfigs = this.generatePluginConfigs();
74
+ const pluginConfigs = this.generatePluginConfigs();
75
75
  if (pluginConfigs) {
76
- coreConfig += ',\n\n' + pluginConfigs;
76
+ coreConfig += `,\n\n${pluginConfigs}`;
77
77
  }
78
78
  coreConfig += '\n};\n';
79
79
  await fs.writeFile(configPath, coreConfig, 'utf8');
80
80
  output.info(`📄 Created vizzly.config.js`);
81
81
 
82
82
  // Log discovered plugins
83
- let pluginsWithConfig = this.plugins.filter(p => p.configSchema);
83
+ const pluginsWithConfig = this.plugins.filter(p => p.configSchema);
84
84
  if (pluginsWithConfig.length > 0) {
85
85
  output.info(` Added config for ${pluginsWithConfig.length} plugin(s):`);
86
86
  pluginsWithConfig.forEach(p => {
@@ -94,10 +94,10 @@ export class InitCommand {
94
94
  * @returns {string} Plugin config sections as formatted string
95
95
  */
96
96
  generatePluginConfigs() {
97
- let sections = [];
98
- for (let plugin of this.plugins) {
97
+ const sections = [];
98
+ for (const plugin of this.plugins) {
99
99
  if (plugin.configSchema) {
100
- let configStr = this.formatPluginConfig(plugin);
100
+ const configStr = this.formatPluginConfig(plugin);
101
101
  if (configStr) {
102
102
  sections.push(configStr);
103
103
  }
@@ -114,18 +114,18 @@ export class InitCommand {
114
114
  formatPluginConfig(plugin) {
115
115
  try {
116
116
  // Validate config schema structure with Zod (defensive check)
117
- let configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
118
- let configSchemaValidator = z.record(configValueSchema);
117
+ const configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
118
+ const configSchemaValidator = z.record(configValueSchema);
119
119
  configSchemaValidator.parse(plugin.configSchema);
120
- let configEntries = [];
121
- for (let [key, value] of Object.entries(plugin.configSchema)) {
122
- let formattedValue = this.formatValue(value, 1);
120
+ const configEntries = [];
121
+ for (const [key, value] of Object.entries(plugin.configSchema)) {
122
+ const formattedValue = this.formatValue(value, 1);
123
123
  configEntries.push(` // ${plugin.name} plugin configuration\n ${key}: ${formattedValue}`);
124
124
  }
125
125
  return configEntries.join(',\n\n');
126
126
  } catch (error) {
127
127
  if (error instanceof z.ZodError) {
128
- let messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
128
+ const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
129
129
  output.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
130
130
  } else {
131
131
  output.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
@@ -141,25 +141,25 @@ export class InitCommand {
141
141
  * @returns {string} Formatted value
142
142
  */
143
143
  formatValue(value, depth = 0) {
144
- let indent = ' '.repeat(depth);
145
- let nextIndent = ' '.repeat(depth + 1);
144
+ const indent = ' '.repeat(depth);
145
+ const nextIndent = ' '.repeat(depth + 1);
146
146
  if (value === null) return 'null';
147
147
  if (value === undefined) return 'undefined';
148
148
  if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
149
149
  if (typeof value === 'number' || typeof value === 'boolean') return String(value);
150
150
  if (Array.isArray(value)) {
151
151
  if (value.length === 0) return '[]';
152
- let items = value.map(item => {
153
- let formatted = this.formatValue(item, depth + 1);
152
+ const items = value.map(item => {
153
+ const formatted = this.formatValue(item, depth + 1);
154
154
  return `${nextIndent}${formatted}`;
155
155
  });
156
156
  return `[\n${items.join(',\n')}\n${indent}]`;
157
157
  }
158
158
  if (typeof value === 'object') {
159
- let entries = Object.entries(value);
159
+ const entries = Object.entries(value);
160
160
  if (entries.length === 0) return '{}';
161
- let props = entries.map(([k, v]) => {
162
- let formatted = this.formatValue(v, depth + 1);
161
+ const props = entries.map(([k, v]) => {
162
+ const formatted = this.formatValue(v, depth + 1);
163
163
  return `${nextIndent}${k}: ${formatted}`;
164
164
  });
165
165
  return `{\n${props.join(',\n')}\n${indent}}`;
@@ -188,7 +188,7 @@ export class InitCommand {
188
188
 
189
189
  // Export factory function for CLI
190
190
  export function createInitCommand(options) {
191
- let command = new InitCommand(options.plugins);
191
+ const command = new InitCommand(options.plugins);
192
192
  return () => command.run(options);
193
193
  }
194
194
 
@@ -204,7 +204,7 @@ export async function init(options = {}) {
204
204
  // Try to load plugins if not provided
205
205
  if (!options.plugins) {
206
206
  try {
207
- let config = await loadConfig(options.config, {});
207
+ const config = await loadConfig(options.config, {});
208
208
  plugins = await loadPlugins(options.config, config, null);
209
209
  } catch {
210
210
  // Silent fail - plugins are optional for init
@@ -212,6 +212,6 @@ export async function init(options = {}) {
212
212
  } else {
213
213
  plugins = options.plugins;
214
214
  }
215
- let command = new InitCommand(plugins);
215
+ const command = new InitCommand(plugins);
216
216
  return await command.run(options);
217
217
  }
@@ -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
  });