@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,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 || threshold > 1) {
219
- errors.push('Threshold must be a number between 0 and 1');
217
+ const threshold = parseFloat(options.threshold);
218
+ if (Number.isNaN(threshold) || threshold < 0) {
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
 
package/dist/index.js CHANGED
@@ -8,23 +8,18 @@ import 'dotenv/config';
8
8
  * - Custom integrations: import from '@vizzly-testing/cli/sdk'
9
9
  */
10
10
 
11
+ // Client exports for convenience
12
+ export { configure, setEnabled, vizzlyScreenshot } from './client/index.js';
13
+ // Errors
14
+ export { UploadError } from './errors/vizzly-error.js';
11
15
  // Primary SDK export
12
16
  export { createVizzly } from './sdk/index.js';
13
-
14
- // Client exports for convenience
15
- export { vizzlyScreenshot, configure, setEnabled } from './client/index.js';
16
-
17
+ export { createServices } from './services/index.js';
18
+ export { createTDDService } from './services/tdd-service.js';
17
19
  // Core services (for advanced usage)
18
20
  export { createUploader } from './services/uploader.js';
19
- export { createTDDService } from './services/tdd-service.js';
20
- export { createServices } from './services/index.js';
21
-
22
- // Utilities
23
- export { loadConfig } from './utils/config-loader.js';
24
- export * as output from './utils/output.js';
25
-
26
21
  // Configuration helper
27
22
  export { defineConfig } from './utils/config-helpers.js';
28
-
29
- // Errors
30
- export { UploadError } from './errors/vizzly-error.js';
23
+ // Utilities
24
+ export { loadConfig } from './utils/config-loader.js';
25
+ export * as output from './utils/output.js';
@@ -1,7 +1,7 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
1
4
  import { glob } from 'glob';
2
- import { readFileSync } from 'fs';
3
- import { resolve, dirname } from 'path';
4
- import { pathToFileURL } from 'url';
5
5
  import { z } from 'zod';
6
6
  import * as output from './utils/output.js';
7
7
 
@@ -12,14 +12,14 @@ import * as output from './utils/output.js';
12
12
  * @returns {Promise<Array>} Array of loaded plugins
13
13
  */
14
14
  export async function loadPlugins(configPath, config) {
15
- let plugins = [];
16
- let loadedNames = new Set();
15
+ const plugins = [];
16
+ const loadedNames = new Set();
17
17
 
18
18
  // 1. Auto-discover plugins from @vizzly-testing/* packages
19
- let discoveredPlugins = await discoverInstalledPlugins();
20
- for (let pluginInfo of discoveredPlugins) {
19
+ const discoveredPlugins = await discoverInstalledPlugins();
20
+ for (const pluginInfo of discoveredPlugins) {
21
21
  try {
22
- let plugin = await loadPlugin(pluginInfo.path);
22
+ const plugin = await loadPlugin(pluginInfo.path);
23
23
  if (plugin && !loadedNames.has(plugin.name)) {
24
24
  plugins.push(plugin);
25
25
  loadedNames.add(plugin.name);
@@ -32,15 +32,15 @@ export async function loadPlugins(configPath, config) {
32
32
 
33
33
  // 2. Load explicit plugins from config
34
34
  if (config?.plugins && Array.isArray(config.plugins)) {
35
- for (let pluginSpec of config.plugins) {
35
+ for (const pluginSpec of config.plugins) {
36
36
  try {
37
- let pluginPath = resolvePluginPath(pluginSpec, configPath);
38
- let plugin = await loadPlugin(pluginPath);
37
+ const pluginPath = resolvePluginPath(pluginSpec, configPath);
38
+ const plugin = await loadPlugin(pluginPath);
39
39
  if (plugin && !loadedNames.has(plugin.name)) {
40
40
  plugins.push(plugin);
41
41
  loadedNames.add(plugin.name);
42
42
  } else if (plugin && loadedNames.has(plugin.name)) {
43
- let existingPlugin = plugins.find(p => p.name === plugin.name);
43
+ const existingPlugin = plugins.find(p => p.name === plugin.name);
44
44
  output.warn(`Plugin ${plugin.name} already loaded (v${existingPlugin.version || 'unknown'}), ` + `skipping v${plugin.version || 'unknown'} from config`);
45
45
  }
46
46
  } catch (error) {
@@ -56,20 +56,20 @@ export async function loadPlugins(configPath, config) {
56
56
  * @returns {Promise<Array>} Array of plugin info objects
57
57
  */
58
58
  async function discoverInstalledPlugins() {
59
- let plugins = [];
59
+ const plugins = [];
60
60
  try {
61
61
  // Find all @vizzly-testing packages
62
- let packageJsonPaths = await glob('node_modules/@vizzly-testing/*/package.json', {
62
+ const packageJsonPaths = await glob('node_modules/@vizzly-testing/*/package.json', {
63
63
  cwd: process.cwd(),
64
64
  absolute: true
65
65
  });
66
- for (let pkgPath of packageJsonPaths) {
66
+ for (const pkgPath of packageJsonPaths) {
67
67
  try {
68
- let packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
68
+ const packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
69
69
 
70
70
  // Check if package has a plugin field
71
71
  if (packageJson.vizzly?.plugin) {
72
- let pluginRelativePath = packageJson.vizzly.plugin;
72
+ const pluginRelativePath = packageJson.vizzly.plugin;
73
73
 
74
74
  // Security: Ensure plugin path is relative and doesn't traverse up
75
75
  if (pluginRelativePath.startsWith('/') || pluginRelativePath.includes('..')) {
@@ -78,8 +78,8 @@ async function discoverInstalledPlugins() {
78
78
  }
79
79
 
80
80
  // Resolve plugin path relative to package directory
81
- let packageDir = dirname(pkgPath);
82
- let pluginPath = resolve(packageDir, pluginRelativePath);
81
+ const packageDir = dirname(pkgPath);
82
+ const pluginPath = resolve(packageDir, pluginRelativePath);
83
83
 
84
84
  // Additional security: Ensure resolved path is still within package directory
85
85
  if (!pluginPath.startsWith(packageDir)) {
@@ -109,19 +109,19 @@ async function discoverInstalledPlugins() {
109
109
  async function loadPlugin(pluginPath) {
110
110
  try {
111
111
  // Convert to file URL for ESM import
112
- let pluginUrl = pathToFileURL(pluginPath).href;
112
+ const pluginUrl = pathToFileURL(pluginPath).href;
113
113
 
114
114
  // Dynamic import
115
- let pluginModule = await import(pluginUrl);
115
+ const pluginModule = await import(pluginUrl);
116
116
 
117
117
  // Get the default export
118
- let plugin = pluginModule.default || pluginModule;
118
+ const plugin = pluginModule.default || pluginModule;
119
119
 
120
120
  // Validate plugin structure
121
121
  validatePluginStructure(plugin);
122
122
  return plugin;
123
123
  } catch (error) {
124
- let newError = new Error(`Failed to load plugin from ${pluginPath}: ${error.message}`);
124
+ const newError = new Error(`Failed to load plugin from ${pluginPath}: ${error.message}`);
125
125
  newError.cause = error;
126
126
  throw newError;
127
127
  }
@@ -154,7 +154,7 @@ function validatePluginStructure(plugin) {
154
154
  // configSchema is optional and primarily for documentation
155
155
  } catch (error) {
156
156
  if (error instanceof z.ZodError) {
157
- let messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
157
+ const messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
158
158
  throw new Error(`Invalid plugin structure: ${messages.join(', ')}`);
159
159
  }
160
160
  throw error;
@@ -172,10 +172,10 @@ function resolvePluginPath(pluginSpec, configPath) {
172
172
  if (pluginSpec.startsWith('@') || /^[a-zA-Z0-9-]+$/.test(pluginSpec)) {
173
173
  // Try to resolve as a package
174
174
  try {
175
- let packageJsonPath = resolve(process.cwd(), 'node_modules', pluginSpec, 'package.json');
176
- let packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
175
+ const packageJsonPath = resolve(process.cwd(), 'node_modules', pluginSpec, 'package.json');
176
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
177
177
  if (packageJson.vizzly?.plugin) {
178
- let packageDir = dirname(packageJsonPath);
178
+ const packageDir = dirname(packageJsonPath);
179
179
  return resolve(packageDir, packageJson.vizzly.plugin);
180
180
  }
181
181
  throw new Error('Package does not specify a vizzly.plugin field');
@@ -187,7 +187,7 @@ function resolvePluginPath(pluginSpec, configPath) {
187
187
  // Otherwise treat as a file path
188
188
  if (configPath) {
189
189
  // Resolve relative to config file
190
- let configDir = dirname(configPath);
190
+ const configDir = dirname(configPath);
191
191
  return resolve(configDir, pluginSpec);
192
192
  } else {
193
193
  // Resolve relative to cwd