@vizzly-testing/cli 0.10.0 → 0.10.1

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/dist/cli.js CHANGED
@@ -34,8 +34,9 @@ let logger = createComponentLogger('CLI', {
34
34
  verbose: verboseMode || false
35
35
  });
36
36
  let container = await createServiceContainer(config);
37
+ let plugins = [];
37
38
  try {
38
- let plugins = await loadPlugins(configPath, config, logger);
39
+ plugins = await loadPlugins(configPath, config, logger);
39
40
  for (let plugin of plugins) {
40
41
  try {
41
42
  // Add timeout protection for plugin registration (5 seconds)
@@ -58,7 +59,8 @@ program.command('init').description('Initialize Vizzly in your project').option(
58
59
  const globalOptions = program.opts();
59
60
  await init({
60
61
  ...globalOptions,
61
- ...options
62
+ ...options,
63
+ plugins
62
64
  });
63
65
  });
64
66
  program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (path, options) => {
@@ -3,15 +3,19 @@ import fs from 'fs/promises';
3
3
  import path from 'path';
4
4
  import { VizzlyError } from '../errors/vizzly-error.js';
5
5
  import { createComponentLogger } from '../utils/logger-factory.js';
6
+ import { loadPlugins } from '../plugin-loader.js';
7
+ import { loadConfig } from '../utils/config-loader.js';
8
+ import { z } from 'zod';
6
9
 
7
10
  /**
8
11
  * Simple configuration setup for Vizzly CLI
9
12
  */
10
13
  export class InitCommand {
11
- constructor(logger) {
14
+ constructor(logger, plugins = []) {
12
15
  this.logger = logger || createComponentLogger('INIT', {
13
16
  level: 'info'
14
17
  });
18
+ this.plugins = plugins;
15
19
  }
16
20
  async run(options = {}) {
17
21
  this.logger.info('🎯 Initializing Vizzly configuration...\n');
@@ -37,7 +41,7 @@ export class InitCommand {
37
41
  }
38
42
  }
39
43
  async generateConfigFile(configPath) {
40
- const configContent = `export default {
44
+ let coreConfig = `export default {
41
45
  // Server configuration (for run command)
42
46
  server: {
43
47
  port: 47392,
@@ -65,11 +69,103 @@ export class InitCommand {
65
69
  // TDD configuration
66
70
  tdd: {
67
71
  openReport: false // Whether to auto-open HTML report in browser
68
- }
69
- };
70
- `;
71
- await fs.writeFile(configPath, configContent, 'utf8');
72
+ }`;
73
+
74
+ // Add plugin configurations
75
+ let pluginConfigs = this.generatePluginConfigs();
76
+ if (pluginConfigs) {
77
+ coreConfig += ',\n\n' + pluginConfigs;
78
+ }
79
+ coreConfig += '\n};\n';
80
+ await fs.writeFile(configPath, coreConfig, 'utf8');
72
81
  this.logger.info(`📄 Created vizzly.config.js`);
82
+
83
+ // Log discovered plugins
84
+ let pluginsWithConfig = this.plugins.filter(p => p.configSchema);
85
+ if (pluginsWithConfig.length > 0) {
86
+ this.logger.info(` Added config for ${pluginsWithConfig.length} plugin(s):`);
87
+ pluginsWithConfig.forEach(p => {
88
+ this.logger.info(` - ${p.name}`);
89
+ });
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Generate configuration sections for plugins
95
+ * @returns {string} Plugin config sections as formatted string
96
+ */
97
+ generatePluginConfigs() {
98
+ let sections = [];
99
+ for (let plugin of this.plugins) {
100
+ if (plugin.configSchema) {
101
+ let configStr = this.formatPluginConfig(plugin);
102
+ if (configStr) {
103
+ sections.push(configStr);
104
+ }
105
+ }
106
+ }
107
+ return sections.length > 0 ? sections.join(',\n\n') : '';
108
+ }
109
+
110
+ /**
111
+ * Format a plugin's config schema as JavaScript code
112
+ * @param {Object} plugin - Plugin with configSchema
113
+ * @returns {string} Formatted config string
114
+ */
115
+ formatPluginConfig(plugin) {
116
+ try {
117
+ // Validate config schema structure with Zod (defensive check)
118
+ let configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
119
+ let configSchemaValidator = z.record(configValueSchema);
120
+ configSchemaValidator.parse(plugin.configSchema);
121
+ let configEntries = [];
122
+ for (let [key, value] of Object.entries(plugin.configSchema)) {
123
+ let formattedValue = this.formatValue(value, 1);
124
+ configEntries.push(` // ${plugin.name} plugin configuration\n ${key}: ${formattedValue}`);
125
+ }
126
+ return configEntries.join(',\n\n');
127
+ } catch (error) {
128
+ if (error instanceof z.ZodError) {
129
+ let messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
130
+ this.logger.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
131
+ } else {
132
+ this.logger.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
133
+ }
134
+ return '';
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Format a JavaScript value with proper indentation
140
+ * @param {*} value - Value to format
141
+ * @param {number} depth - Current indentation depth
142
+ * @returns {string} Formatted value
143
+ */
144
+ formatValue(value, depth = 0) {
145
+ let indent = ' '.repeat(depth);
146
+ let nextIndent = ' '.repeat(depth + 1);
147
+ if (value === null) return 'null';
148
+ if (value === undefined) return 'undefined';
149
+ if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
150
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
151
+ if (Array.isArray(value)) {
152
+ if (value.length === 0) return '[]';
153
+ let items = value.map(item => {
154
+ let formatted = this.formatValue(item, depth + 1);
155
+ return `${nextIndent}${formatted}`;
156
+ });
157
+ return `[\n${items.join(',\n')}\n${indent}]`;
158
+ }
159
+ if (typeof value === 'object') {
160
+ let entries = Object.entries(value);
161
+ if (entries.length === 0) return '{}';
162
+ let props = entries.map(([k, v]) => {
163
+ let formatted = this.formatValue(v, depth + 1);
164
+ return `${nextIndent}${k}: ${formatted}`;
165
+ });
166
+ return `{\n${props.join(',\n')}\n${indent}}`;
167
+ }
168
+ return String(value);
73
169
  }
74
170
  showNextSteps() {
75
171
  this.logger.info('\n📚 Next steps:');
@@ -92,12 +188,28 @@ export class InitCommand {
92
188
 
93
189
  // Export factory function for CLI
94
190
  export function createInitCommand(options) {
95
- const command = new InitCommand(options.logger);
191
+ const command = new InitCommand(options.logger, options.plugins);
96
192
  return () => command.run(options);
97
193
  }
98
194
 
99
195
  // Simple export for direct CLI usage
100
196
  export async function init(options = {}) {
101
- const command = new InitCommand();
197
+ let plugins = [];
198
+
199
+ // Try to load plugins if not provided
200
+ if (!options.plugins) {
201
+ try {
202
+ let config = await loadConfig(options.config, {});
203
+ let logger = createComponentLogger('INIT', {
204
+ level: 'debug'
205
+ });
206
+ plugins = await loadPlugins(options.config, config, logger);
207
+ } catch {
208
+ // Silent fail - plugins are optional for init
209
+ }
210
+ } else {
211
+ plugins = options.plugins;
212
+ }
213
+ const command = new InitCommand(null, plugins);
102
214
  return await command.run(options);
103
215
  }
@@ -2,6 +2,7 @@ import { glob } from 'glob';
2
2
  import { readFileSync } from 'fs';
3
3
  import { resolve, dirname } from 'path';
4
4
  import { pathToFileURL } from 'url';
5
+ import { z } from 'zod';
5
6
 
6
7
  /**
7
8
  * Load and register plugins from node_modules and config
@@ -128,23 +129,43 @@ async function loadPlugin(pluginPath) {
128
129
  }
129
130
  }
130
131
 
132
+ /**
133
+ * Zod schema for validating plugin structure
134
+ */
135
+ const pluginSchema = z.object({
136
+ name: z.string().min(1, 'Plugin name is required'),
137
+ version: z.string().optional(),
138
+ register: z.function(z.tuple([z.any(), z.any()]), z.void()),
139
+ configSchema: z.record(z.any()).optional()
140
+ });
141
+
142
+ /**
143
+ * Zod schema for validating plugin config values
144
+ * Supports: string, number, boolean, null, arrays, and nested objects
145
+ */
146
+ const configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
147
+
131
148
  /**
132
149
  * Validate plugin has required structure
133
150
  * @param {Object} plugin - Plugin object
134
151
  * @throws {Error} If plugin structure is invalid
135
152
  */
136
153
  function validatePluginStructure(plugin) {
137
- if (!plugin || typeof plugin !== 'object') {
138
- throw new Error('Plugin must export an object');
139
- }
140
- if (!plugin.name || typeof plugin.name !== 'string') {
141
- throw new Error('Plugin must have a name (string)');
142
- }
143
- if (!plugin.register || typeof plugin.register !== 'function') {
144
- throw new Error('Plugin must have a register function');
145
- }
146
- if (plugin.version && typeof plugin.version !== 'string') {
147
- throw new Error('Plugin version must be a string');
154
+ try {
155
+ // Validate basic plugin structure
156
+ pluginSchema.parse(plugin);
157
+
158
+ // If configSchema exists, validate it contains valid config values
159
+ if (plugin.configSchema) {
160
+ let configSchemaValidator = z.record(configValueSchema);
161
+ configSchemaValidator.parse(plugin.configSchema);
162
+ }
163
+ } catch (error) {
164
+ if (error instanceof z.ZodError) {
165
+ let messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
166
+ throw new Error(`Invalid plugin structure: ${messages.join(', ')}`);
167
+ }
168
+ throw error;
148
169
  }
149
170
  }
150
171
 
@@ -5,10 +5,29 @@ export function init(options?: {}): Promise<void>;
5
5
  * Simple configuration setup for Vizzly CLI
6
6
  */
7
7
  export class InitCommand {
8
- constructor(logger: any);
8
+ constructor(logger: any, plugins?: any[]);
9
9
  logger: any;
10
+ plugins: any[];
10
11
  run(options?: {}): Promise<void>;
11
12
  generateConfigFile(configPath: any): Promise<void>;
13
+ /**
14
+ * Generate configuration sections for plugins
15
+ * @returns {string} Plugin config sections as formatted string
16
+ */
17
+ generatePluginConfigs(): string;
18
+ /**
19
+ * Format a plugin's config schema as JavaScript code
20
+ * @param {Object} plugin - Plugin with configSchema
21
+ * @returns {string} Formatted config string
22
+ */
23
+ formatPluginConfig(plugin: any): string;
24
+ /**
25
+ * Format a JavaScript value with proper indentation
26
+ * @param {*} value - Value to format
27
+ * @param {number} depth - Current indentation depth
28
+ * @returns {string} Formatted value
29
+ */
30
+ formatValue(value: any, depth?: number): string;
12
31
  showNextSteps(): void;
13
32
  fileExists(filePath: any): Promise<boolean>;
14
33
  }
@@ -35,14 +35,14 @@ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
35
35
  commit: z.ZodOptional<z.ZodString>;
36
36
  message: z.ZodOptional<z.ZodString>;
37
37
  }, "strip", z.ZodTypeAny, {
38
- message?: string;
39
38
  name?: string;
39
+ message?: string;
40
40
  environment?: string;
41
41
  branch?: string;
42
42
  commit?: string;
43
43
  }, {
44
- message?: string;
45
44
  name?: string;
45
+ message?: string;
46
46
  environment?: string;
47
47
  branch?: string;
48
48
  commit?: string;
@@ -101,14 +101,14 @@ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
101
101
  commit: z.ZodOptional<z.ZodString>;
102
102
  message: z.ZodOptional<z.ZodString>;
103
103
  }, "strip", z.ZodTypeAny, {
104
- message?: string;
105
104
  name?: string;
105
+ message?: string;
106
106
  environment?: string;
107
107
  branch?: string;
108
108
  commit?: string;
109
109
  }, {
110
- message?: string;
111
110
  name?: string;
111
+ message?: string;
112
112
  environment?: string;
113
113
  branch?: string;
114
114
  commit?: string;
@@ -167,14 +167,14 @@ export let vizzlyConfigSchema: z.ZodDefault<z.ZodObject<{
167
167
  commit: z.ZodOptional<z.ZodString>;
168
168
  message: z.ZodOptional<z.ZodString>;
169
169
  }, "strip", z.ZodTypeAny, {
170
- message?: string;
171
170
  name?: string;
171
+ message?: string;
172
172
  environment?: string;
173
173
  branch?: string;
174
174
  commit?: string;
175
175
  }, {
176
- message?: string;
177
176
  name?: string;
177
+ message?: string;
178
178
  environment?: string;
179
179
  branch?: string;
180
180
  commit?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -84,7 +84,7 @@
84
84
  "form-data": "^4.0.0",
85
85
  "glob": "^11.0.3",
86
86
  "odiff-bin": "^3.2.1",
87
- "zod": "^3.24.1"
87
+ "zod": "^3.25.76"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@babel/cli": "^7.28.0",