@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.
- package/README.md +4 -4
- package/claude-plugin/skills/debug-visual-regression/SKILL.md +2 -2
- package/dist/cli.js +84 -58
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +18 -17
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +30 -30
- package/dist/commands/login.js +23 -23
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +36 -36
- package/dist/commands/run.js +33 -33
- package/dist/commands/status.js +14 -14
- package/dist/commands/tdd-daemon.js +43 -43
- package/dist/commands/tdd.js +27 -27
- package/dist/commands/upload.js +33 -33
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-loader.js +28 -28
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/sdk/index.js +33 -35
- package/dist/server/handlers/api-handler.js +4 -4
- package/dist/server/handlers/tdd-handler.js +12 -12
- package/dist/server/http-server.js +21 -22
- package/dist/server/middleware/json-parser.js +1 -1
- package/dist/server/routers/assets.js +14 -14
- package/dist/server/routers/auth.js +14 -14
- package/dist/server/routers/baseline.js +8 -8
- package/dist/server/routers/cloud-proxy.js +15 -15
- package/dist/server/routers/config.js +11 -11
- package/dist/server/routers/dashboard.js +11 -11
- package/dist/server/routers/health.js +4 -4
- package/dist/server/routers/projects.js +19 -19
- package/dist/server/routers/screenshot.js +9 -9
- package/dist/services/api-service.js +16 -16
- package/dist/services/auth-service.js +17 -17
- package/dist/services/build-manager.js +3 -3
- package/dist/services/config-service.js +33 -33
- package/dist/services/html-report-generator.js +8 -8
- package/dist/services/index.js +11 -11
- package/dist/services/project-service.js +19 -19
- package/dist/services/report-generator/report.css +3 -3
- package/dist/services/report-generator/viewer.js +25 -23
- package/dist/services/screenshot-server.js +1 -1
- package/dist/services/server-manager.js +5 -5
- package/dist/services/static-report-generator.js +14 -14
- package/dist/services/tdd-service.js +101 -95
- package/dist/services/test-runner.js +14 -4
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/utils/browser.js +3 -3
- package/dist/utils/build-history.js +12 -12
- package/dist/utils/config-loader.js +19 -19
- package/dist/utils/config-schema.js +10 -9
- package/dist/utils/environment-config.js +11 -0
- package/dist/utils/fetch-utils.js +2 -2
- package/dist/utils/file-helpers.js +2 -2
- package/dist/utils/git.js +3 -6
- package/dist/utils/global-config.js +28 -25
- package/dist/utils/output.js +136 -28
- package/dist/utils/package-info.js +3 -3
- package/dist/utils/security.js +12 -12
- package/docs/api-reference.md +56 -27
- package/docs/doctor-command.md +1 -1
- package/docs/tdd-mode.md +3 -3
- package/package.json +9 -13
package/dist/commands/upload.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
|
|
16
|
+
const apiService = new ApiService({
|
|
17
17
|
baseUrl: apiUrl,
|
|
18
18
|
token: apiToken,
|
|
19
19
|
command: 'upload'
|
|
20
20
|
});
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
89
|
-
|
|
88
|
+
const services = createServices(config, 'upload');
|
|
89
|
+
const uploader = services.uploader;
|
|
90
90
|
|
|
91
91
|
// Prepare upload options with progress callback
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
+
const apiService = new ApiService({
|
|
139
139
|
baseUrl: config.apiUrl,
|
|
140
140
|
token: config.apiKey,
|
|
141
141
|
command: 'upload'
|
|
142
142
|
});
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
const apiService = new ApiService({
|
|
182
182
|
baseUrl: config.apiUrl,
|
|
183
183
|
token: config.apiKey,
|
|
184
184
|
command: 'upload'
|
|
185
185
|
});
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
if (isNaN(threshold) || threshold < 0
|
|
219
|
-
errors.push('Threshold must be a number
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
+
const authService = new AuthService({
|
|
42
42
|
baseUrl: options.apiUrl || getApiUrl()
|
|
43
43
|
});
|
|
44
|
-
|
|
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 (
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
export
|
|
23
|
+
// Utilities
|
|
24
|
+
export { loadConfig } from './utils/config-loader.js';
|
|
25
|
+
export * as output from './utils/output.js';
|
package/dist/plugin-loader.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
15
|
+
const plugins = [];
|
|
16
|
+
const loadedNames = new Set();
|
|
17
17
|
|
|
18
18
|
// 1. Auto-discover plugins from @vizzly-testing/* packages
|
|
19
|
-
|
|
20
|
-
for (
|
|
19
|
+
const discoveredPlugins = await discoverInstalledPlugins();
|
|
20
|
+
for (const pluginInfo of discoveredPlugins) {
|
|
21
21
|
try {
|
|
22
|
-
|
|
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 (
|
|
35
|
+
for (const pluginSpec of config.plugins) {
|
|
36
36
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
const plugins = [];
|
|
60
60
|
try {
|
|
61
61
|
// Find all @vizzly-testing packages
|
|
62
|
-
|
|
62
|
+
const packageJsonPaths = await glob('node_modules/@vizzly-testing/*/package.json', {
|
|
63
63
|
cwd: process.cwd(),
|
|
64
64
|
absolute: true
|
|
65
65
|
});
|
|
66
|
-
for (
|
|
66
|
+
for (const pkgPath of packageJsonPaths) {
|
|
67
67
|
try {
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
112
|
+
const pluginUrl = pathToFileURL(pluginPath).href;
|
|
113
113
|
|
|
114
114
|
// Dynamic import
|
|
115
|
-
|
|
115
|
+
const pluginModule = await import(pluginUrl);
|
|
116
116
|
|
|
117
117
|
// Get the default export
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
+
const configDir = dirname(configPath);
|
|
191
191
|
return resolve(configDir, pluginSpec);
|
|
192
192
|
} else {
|
|
193
193
|
// Resolve relative to cwd
|