@vizzly-testing/cli 0.29.6 → 0.31.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 (87) hide show
  1. package/README.md +34 -6
  2. package/dist/api/client.js +6 -3
  3. package/dist/api/endpoints.js +59 -0
  4. package/dist/api/index.js +1 -1
  5. package/dist/auth/core.js +1 -1
  6. package/dist/auth/index.js +2 -1
  7. package/dist/cli.js +206 -51
  8. package/dist/client/index.js +21 -29
  9. package/dist/commands/api.js +145 -90
  10. package/dist/commands/baselines.js +78 -47
  11. package/dist/commands/builds.js +9 -8
  12. package/dist/commands/comparisons.js +7 -8
  13. package/dist/commands/config-cmd.js +15 -7
  14. package/dist/commands/context.js +870 -0
  15. package/dist/commands/doctor.js +116 -81
  16. package/dist/commands/init.js +202 -201
  17. package/dist/commands/login.js +134 -65
  18. package/dist/commands/logout.js +18 -7
  19. package/dist/commands/preview.js +29 -57
  20. package/dist/commands/project.js +120 -0
  21. package/dist/commands/projects.js +9 -2
  22. package/dist/commands/review.js +236 -202
  23. package/dist/commands/run.js +75 -72
  24. package/dist/commands/status.js +228 -150
  25. package/dist/commands/tdd-daemon-process.js +98 -0
  26. package/dist/commands/tdd-daemon.js +318 -212
  27. package/dist/commands/tdd.js +23 -8
  28. package/dist/commands/upload.js +10 -9
  29. package/dist/commands/whoami.js +64 -26
  30. package/dist/config/core.js +37 -36
  31. package/dist/context/local-workspace-provider.js +532 -0
  32. package/dist/context/provider-resolver.js +36 -0
  33. package/dist/index.js +5 -4
  34. package/dist/plugin-loader.js +55 -49
  35. package/dist/project/core.js +2 -2
  36. package/dist/project/operations.js +6 -3
  37. package/dist/reporter/reporter-bundle.css +2 -1
  38. package/dist/reporter/reporter-bundle.iife.js +119 -119
  39. package/dist/reporter-ssr/ssr-entry.js +605 -509
  40. package/dist/sdk/index.js +162 -107
  41. package/dist/server/handlers/tdd-handler.js +30 -54
  42. package/dist/server/routers/assets.js +4 -0
  43. package/dist/server/routers/baseline.js +195 -178
  44. package/dist/server/routers/events.js +226 -148
  45. package/dist/server/routers/projects.js +14 -5
  46. package/dist/server/routers/screenshot.js +1 -31
  47. package/dist/services/build-manager.js +19 -97
  48. package/dist/services/config-service.js +26 -34
  49. package/dist/services/index.js +2 -4
  50. package/dist/services/static-report-generator.js +11 -12
  51. package/dist/services/test-runner.js +21 -27
  52. package/dist/tdd/core/hotspot-coverage.js +1 -1
  53. package/dist/tdd/index.js +1 -1
  54. package/dist/tdd/metadata/region-metadata.js +0 -37
  55. package/dist/tdd/runtime-context.js +78 -0
  56. package/dist/tdd/server-registry.js +38 -17
  57. package/dist/tdd/services/baseline-manager.js +23 -0
  58. package/dist/tdd/services/hotspot-service.js +1 -14
  59. package/dist/tdd/tdd-service.js +82 -113
  60. package/dist/types/client.d.ts +36 -1
  61. package/dist/types/index.d.ts +114 -22
  62. package/dist/types/sdk.d.ts +47 -7
  63. package/dist/uploader/core.js +5 -4
  64. package/dist/uploader/index.js +79 -2
  65. package/dist/uploader/operations.js +40 -5
  66. package/dist/utils/api-url.js +14 -0
  67. package/dist/utils/async-utils.js +26 -0
  68. package/dist/utils/ci-env.js +19 -12
  69. package/dist/utils/colors.js +46 -44
  70. package/dist/utils/config-loader.js +39 -65
  71. package/dist/utils/config-schema.js +19 -55
  72. package/dist/utils/fetch-utils.js +12 -5
  73. package/dist/utils/git.js +35 -72
  74. package/dist/utils/global-config.js +22 -3
  75. package/dist/utils/output.js +19 -7
  76. package/dist/utils/patterns.js +14 -0
  77. package/dist/utils/project-link-store.js +105 -0
  78. package/dist/utils/report-data.js +31 -0
  79. package/dist/utils/screenshot-options.js +28 -0
  80. package/dist/utils/secret-store.js +65 -0
  81. package/dist/utils/security.js +3 -2
  82. package/package.json +14 -11
  83. package/dist/services/screenshot-server.js +0 -65
  84. package/dist/services/server-manager.js +0 -81
  85. package/dist/services/uploader.js +0 -100
  86. package/dist/tdd/services/region-service.js +0 -75
  87. package/dist/utils/build-history.js +0 -103
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
2
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { glob } from 'glob';
5
5
  import { z } from 'zod';
@@ -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
- const plugins = [];
16
- const loadedNames = new Set();
15
+ let plugins = [];
16
+ let loadedNames = new Set();
17
17
 
18
18
  // 1. Auto-discover plugins from @vizzly-testing/* packages
19
- const discoveredPlugins = await discoverInstalledPlugins();
20
- for (const pluginInfo of discoveredPlugins) {
19
+ let discoveredPlugins = await discoverInstalledPlugins();
20
+ for (let pluginInfo of discoveredPlugins) {
21
21
  try {
22
- const plugin = await loadPlugin(pluginInfo.path);
22
+ let 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 (const pluginSpec of config.plugins) {
35
+ for (let pluginSpec of config.plugins) {
36
36
  try {
37
- const pluginPath = resolvePluginPath(pluginSpec, configPath);
38
- const plugin = await loadPlugin(pluginPath);
37
+ let pluginPath = resolvePluginPath(pluginSpec, configPath);
38
+ let 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
- const existingPlugin = plugins.find(p => p.name === plugin.name);
43
+ let 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) {
@@ -55,39 +55,24 @@ export async function loadPlugins(configPath, config) {
55
55
  * Discover installed plugins from node_modules/@vizzly-testing/*
56
56
  * @returns {Promise<Array>} Array of plugin info objects
57
57
  */
58
- async function discoverInstalledPlugins() {
59
- const plugins = [];
58
+ export async function discoverInstalledPlugins() {
59
+ let plugins = [];
60
60
  try {
61
61
  // Find all @vizzly-testing packages
62
- const packageJsonPaths = await glob('node_modules/@vizzly-testing/*/package.json', {
62
+ let packageJsonPaths = await glob('node_modules/@vizzly-testing/*/package.json', {
63
63
  cwd: process.cwd(),
64
64
  absolute: true
65
65
  });
66
- for (const pkgPath of packageJsonPaths) {
66
+ for (let pkgPath of packageJsonPaths) {
67
67
  try {
68
- const packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
68
+ let packageJson = JSON.parse(readFileSync(pkgPath, 'utf-8'));
69
69
 
70
70
  // Check if package has a plugin field
71
71
  // Support both new `vizzlyPlugin` and legacy `vizzly.plugin` for backwards compatibility
72
- const pluginField = packageJson.vizzlyPlugin || packageJson.vizzly?.plugin;
72
+ let pluginField = getPluginField(packageJson);
73
73
  if (pluginField) {
74
- const pluginRelativePath = pluginField;
75
-
76
- // Security: Ensure plugin path is relative and doesn't traverse up
77
- if (pluginRelativePath.startsWith('/') || pluginRelativePath.includes('..')) {
78
- output.warn(`Invalid plugin path in ${packageJson.name}: path must be relative and cannot traverse directories`);
79
- continue;
80
- }
81
-
82
- // Resolve plugin path relative to package directory
83
- const packageDir = dirname(pkgPath);
84
- const pluginPath = resolve(packageDir, pluginRelativePath);
85
-
86
- // Additional security: Ensure resolved path is still within package directory
87
- if (!pluginPath.startsWith(packageDir)) {
88
- output.warn(`Plugin path escapes package directory: ${packageJson.name}`);
89
- continue;
90
- }
74
+ let packageDir = dirname(pkgPath);
75
+ let pluginPath = resolvePackagePluginPath(packageJson.name, packageDir, pluginField);
91
76
  plugins.push({
92
77
  packageName: packageJson.name,
93
78
  path: pluginPath
@@ -102,28 +87,45 @@ async function discoverInstalledPlugins() {
102
87
  }
103
88
  return plugins;
104
89
  }
90
+ function getPluginField(packageJson) {
91
+ return packageJson.vizzlyPlugin || packageJson.vizzly?.plugin;
92
+ }
93
+ export function resolvePackagePluginPath(packageName, packageDir, pluginRelativePath) {
94
+ if (typeof pluginRelativePath !== 'string' || pluginRelativePath === '') {
95
+ throw new Error(`Invalid plugin path in ${packageName}: path must be a non-empty string`);
96
+ }
97
+ if (isAbsolute(pluginRelativePath)) {
98
+ throw new Error(`Invalid plugin path in ${packageName}: path must be relative`);
99
+ }
100
+ let pluginPath = resolve(packageDir, pluginRelativePath);
101
+ let relativePath = relative(packageDir, pluginPath);
102
+ if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
103
+ throw new Error(`Invalid plugin path in ${packageName}: path cannot escape package directory`);
104
+ }
105
+ return pluginPath;
106
+ }
105
107
 
106
108
  /**
107
109
  * Load a plugin from a file path
108
110
  * @param {string} pluginPath - Path to plugin file
109
111
  * @returns {Promise<Object|null>} Loaded plugin or null
110
112
  */
111
- async function loadPlugin(pluginPath) {
113
+ export async function loadPlugin(pluginPath) {
112
114
  try {
113
115
  // Convert to file URL for ESM import
114
- const pluginUrl = pathToFileURL(pluginPath).href;
116
+ let pluginUrl = pathToFileURL(pluginPath).href;
115
117
 
116
118
  // Dynamic import
117
- const pluginModule = await import(pluginUrl);
119
+ let pluginModule = await import(pluginUrl);
118
120
 
119
121
  // Get the default export
120
- const plugin = pluginModule.default || pluginModule;
122
+ let plugin = pluginModule.default || pluginModule;
121
123
 
122
124
  // Validate plugin structure
123
125
  validatePluginStructure(plugin);
124
126
  return plugin;
125
127
  } catch (error) {
126
- const newError = new Error(`Failed to load plugin from ${pluginPath}: ${error.message}`);
128
+ let newError = new Error(`Failed to load plugin from ${pluginPath}: ${error.message}`);
127
129
  newError.cause = error;
128
130
  throw newError;
129
131
  }
@@ -156,7 +158,7 @@ function validatePluginStructure(plugin) {
156
158
  // configSchema is optional and primarily for documentation
157
159
  } catch (error) {
158
160
  if (error instanceof z.ZodError) {
159
- const messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
161
+ let messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
160
162
  throw new Error(`Invalid plugin structure: ${messages.join(', ')}`);
161
163
  }
162
164
  throw error;
@@ -169,19 +171,23 @@ function validatePluginStructure(plugin) {
169
171
  * @param {string|null} configPath - Path to config file
170
172
  * @returns {string} Resolved plugin path
171
173
  */
172
- function resolvePluginPath(pluginSpec, configPath) {
174
+ export function resolvePluginPath(pluginSpec, configPath) {
175
+ if (typeof pluginSpec !== 'string' || pluginSpec === '') {
176
+ throw new Error('Plugin spec must be a non-empty string');
177
+ }
178
+
173
179
  // If it's a package name (starts with @ or is alphanumeric), try to resolve from node_modules
174
180
  if (pluginSpec.startsWith('@') || /^[a-zA-Z0-9-]+$/.test(pluginSpec)) {
175
181
  // Try to resolve as a package
176
182
  try {
177
- const packageJsonPath = resolve(process.cwd(), 'node_modules', pluginSpec, 'package.json');
178
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
183
+ let packageJsonPath = resolve(process.cwd(), 'node_modules', pluginSpec, 'package.json');
184
+ let packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
179
185
 
180
186
  // Support both new `vizzlyPlugin` and legacy `vizzly.plugin`
181
- const pluginField = packageJson.vizzlyPlugin || packageJson.vizzly?.plugin;
187
+ let pluginField = getPluginField(packageJson);
182
188
  if (pluginField) {
183
- const packageDir = dirname(packageJsonPath);
184
- return resolve(packageDir, pluginField);
189
+ let packageDir = dirname(packageJsonPath);
190
+ return resolvePackagePluginPath(packageJson.name || pluginSpec, packageDir, pluginField);
185
191
  }
186
192
  throw new Error('Package does not specify a vizzlyPlugin or vizzly.plugin field');
187
193
  } catch (error) {
@@ -192,10 +198,10 @@ function resolvePluginPath(pluginSpec, configPath) {
192
198
  // Otherwise treat as a file path
193
199
  if (configPath) {
194
200
  // Resolve relative to config file
195
- const configDir = dirname(configPath);
201
+ let configDir = dirname(configPath);
196
202
  return resolve(configDir, pluginSpec);
197
- } else {
198
- // Resolve relative to cwd
199
- return resolve(process.cwd(), pluginSpec);
200
203
  }
204
+
205
+ // Resolve relative to cwd
206
+ return resolve(process.cwd(), pluginSpec);
201
207
  }
@@ -67,8 +67,8 @@ export function buildBuildsUrl(projectSlug, options = {}) {
67
67
  * @param {string} [tokenId] - Optional token ID for specific token operations
68
68
  * @returns {string} API URL path
69
69
  */
70
- export function buildTokensUrl(organizationSlug, projectSlug, tokenId) {
71
- let base = `/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`;
70
+ export function buildTokensUrl(_organizationSlug, projectSlug, tokenId) {
71
+ let base = `/api/project/${projectSlug}/tokens`;
72
72
  return tokenId ? `${base}/${tokenId}` : base;
73
73
  }
74
74
 
@@ -264,7 +264,8 @@ export async function createProjectToken(apiClient, projectSlug, organizationSlu
264
264
  let response = await apiClient.request(buildTokensUrl(organizationSlug, projectSlug), {
265
265
  method: 'POST',
266
266
  headers: {
267
- 'Content-Type': 'application/json'
267
+ 'Content-Type': 'application/json',
268
+ ...buildOrgHeader(organizationSlug)
268
269
  },
269
270
  body: JSON.stringify(tokenData)
270
271
  });
@@ -287,7 +288,8 @@ export async function listProjectTokens(apiClient, projectSlug, organizationSlug
287
288
  }
288
289
  try {
289
290
  let response = await apiClient.request(buildTokensUrl(organizationSlug, projectSlug), {
290
- method: 'GET'
291
+ method: 'GET',
292
+ headers: buildOrgHeader(organizationSlug)
291
293
  });
292
294
  return extractTokens(response);
293
295
  } catch (error) {
@@ -309,7 +311,8 @@ export async function revokeProjectToken(apiClient, projectSlug, organizationSlu
309
311
  }
310
312
  try {
311
313
  await apiClient.request(buildTokensUrl(organizationSlug, projectSlug, tokenId), {
312
- method: 'DELETE'
314
+ method: 'DELETE',
315
+ headers: buildOrgHeader(organizationSlug)
313
316
  });
314
317
  } catch (error) {
315
318
  throw buildTokenRevokeError(error);