@vizzly-testing/cli 0.10.0 → 0.10.2
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 +4 -2
- package/dist/commands/init.js +120 -8
- package/dist/commands/tdd-daemon.js +51 -15
- package/dist/commands/tdd.js +10 -3
- package/dist/plugin-loader.js +32 -11
- package/dist/services/tdd-service.js +30 -13
- package/dist/types/commands/init.d.ts +20 -1
- package/dist/types/utils/config-schema.d.ts +6 -6
- package/package.json +2 -2
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
|
-
|
|
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) => {
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { ConsoleUI } from '../utils/console-ui.js';
|
|
@@ -36,21 +36,56 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
const port = options.port || 47392;
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
// Show loading indicator if downloading baselines (but not in verbose mode since child shows progress)
|
|
40
|
+
if (options.baselineBuild && !globalOptions.verbose) {
|
|
41
|
+
ui.startSpinner(`Downloading baselines from build ${options.baselineBuild}...`);
|
|
42
|
+
}
|
|
42
43
|
|
|
43
|
-
// Spawn
|
|
44
|
+
// Spawn child process with stdio inherited during init for direct error visibility
|
|
44
45
|
const child = spawn(process.execPath, [process.argv[1],
|
|
45
46
|
// CLI entry point
|
|
46
47
|
'tdd', 'start', '--daemon-child',
|
|
47
48
|
// Special flag for child process
|
|
48
49
|
'--port', port.toString(), ...(options.open ? ['--open'] : []), ...(options.baselineBuild ? ['--baseline-build', options.baselineBuild] : []), ...(options.baselineComparison ? ['--baseline-comparison', options.baselineComparison] : []), ...(options.environment ? ['--environment', options.environment] : []), ...(options.threshold !== undefined ? ['--threshold', options.threshold.toString()] : []), ...(options.timeout ? ['--timeout', options.timeout] : []), ...(options.token ? ['--token', options.token] : []), ...(globalOptions.json ? ['--json'] : []), ...(globalOptions.verbose ? ['--verbose'] : []), ...(globalOptions.noColor ? ['--no-color'] : [])], {
|
|
49
50
|
detached: true,
|
|
50
|
-
stdio: ['ignore',
|
|
51
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
51
52
|
cwd: process.cwd()
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
// Wait for child to signal successful init or exit with error
|
|
56
|
+
let initComplete = false;
|
|
57
|
+
let initFailed = false;
|
|
58
|
+
await new Promise(resolve => {
|
|
59
|
+
// Child disconnects IPC when initialization succeeds
|
|
60
|
+
child.on('disconnect', () => {
|
|
61
|
+
initComplete = true;
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Child exits before disconnecting = initialization failed
|
|
66
|
+
child.on('exit', () => {
|
|
67
|
+
if (!initComplete) {
|
|
68
|
+
initFailed = true;
|
|
69
|
+
resolve();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Timeout after 30 seconds to prevent indefinite wait
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
if (!initComplete && !initFailed) {
|
|
76
|
+
initFailed = true;
|
|
77
|
+
resolve();
|
|
78
|
+
}
|
|
79
|
+
}, 30000);
|
|
80
|
+
});
|
|
81
|
+
if (initFailed) {
|
|
82
|
+
if (options.baselineBuild && !globalOptions.verbose) {
|
|
83
|
+
ui.stopSpinner();
|
|
84
|
+
}
|
|
85
|
+
ui.error('TDD server failed to start');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
54
89
|
// Unref so parent can exit
|
|
55
90
|
child.unref();
|
|
56
91
|
|
|
@@ -62,6 +97,9 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
62
97
|
await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
|
|
63
98
|
running = await isServerRunning(port);
|
|
64
99
|
}
|
|
100
|
+
if (options.baselineBuild && !globalOptions.verbose) {
|
|
101
|
+
ui.stopSpinner();
|
|
102
|
+
}
|
|
65
103
|
if (!running) {
|
|
66
104
|
ui.error('Failed to start TDD server - server not responding to health checks');
|
|
67
105
|
process.exit(1);
|
|
@@ -106,6 +144,11 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
106
144
|
daemon: true
|
|
107
145
|
}, globalOptions);
|
|
108
146
|
|
|
147
|
+
// Disconnect IPC after successful initialization to signal parent
|
|
148
|
+
if (process.send) {
|
|
149
|
+
process.disconnect();
|
|
150
|
+
}
|
|
151
|
+
|
|
109
152
|
// Store our PID for the stop command
|
|
110
153
|
const pidFile = join(vizzlyDir, 'server.pid');
|
|
111
154
|
writeFileSync(pidFile, process.pid.toString());
|
|
@@ -139,15 +182,8 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
139
182
|
// Keep process alive
|
|
140
183
|
process.stdin.resume();
|
|
141
184
|
} catch (error) {
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
writeFileSync(logFile, `[${new Date().toISOString()}] ${error.stack || error}\n`, {
|
|
146
|
-
flag: 'a'
|
|
147
|
-
});
|
|
148
|
-
} catch {
|
|
149
|
-
// Silent failure if we can't write log
|
|
150
|
-
}
|
|
185
|
+
// Most errors shown via inherited stdio, but catch any that weren't
|
|
186
|
+
console.error(`Fatal error: ${error.message}`);
|
|
151
187
|
process.exit(1);
|
|
152
188
|
}
|
|
153
189
|
}
|
package/dist/commands/tdd.js
CHANGED
|
@@ -53,7 +53,9 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
53
53
|
// Collect git metadata
|
|
54
54
|
const branch = await detectBranch(options.branch);
|
|
55
55
|
const commit = await detectCommit(options.commit);
|
|
56
|
-
|
|
56
|
+
|
|
57
|
+
// Only show config in verbose mode for non-daemon (daemon shows baseline info instead)
|
|
58
|
+
if (globalOptions.verbose && !options.daemon) {
|
|
57
59
|
ui.info('TDD Configuration loaded', {
|
|
58
60
|
testCommand,
|
|
59
61
|
port: config.server.port,
|
|
@@ -97,8 +99,13 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
97
99
|
ui.info(`TDD screenshot server running on port ${serverInfo.port}`);
|
|
98
100
|
ui.info(`Dashboard: http://localhost:${serverInfo.port}/dashboard`);
|
|
99
101
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// Verbose server details only in non-daemon mode
|
|
103
|
+
if (globalOptions.verbose && !options.daemon) {
|
|
104
|
+
ui.info('Server started', {
|
|
105
|
+
port: serverInfo.port,
|
|
106
|
+
pid: serverInfo.pid,
|
|
107
|
+
uptime: serverInfo.uptime
|
|
108
|
+
});
|
|
102
109
|
}
|
|
103
110
|
});
|
|
104
111
|
testRunner.on('screenshot-captured', screenshotInfo => {
|
package/dist/plugin-loader.js
CHANGED
|
@@ -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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
|
@@ -90,8 +90,6 @@ export class TddService {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
async downloadBaselines(environment = 'test', branch = null, buildId = null, comparisonId = null) {
|
|
93
|
-
logger.info('🔍 Looking for baseline build...');
|
|
94
|
-
|
|
95
93
|
// If no branch specified, try to detect the default branch
|
|
96
94
|
if (!branch) {
|
|
97
95
|
branch = await getDefaultBranch();
|
|
@@ -107,7 +105,6 @@ export class TddService {
|
|
|
107
105
|
let baselineBuild;
|
|
108
106
|
if (buildId) {
|
|
109
107
|
// Use specific build ID - get it with screenshots in one call
|
|
110
|
-
logger.info(`📌 Using specified build: ${buildId}`);
|
|
111
108
|
const apiResponse = await this.api.getBuild(buildId, 'screenshots');
|
|
112
109
|
|
|
113
110
|
// Debug the full API response (only in debug mode)
|
|
@@ -154,7 +151,6 @@ export class TddService {
|
|
|
154
151
|
}
|
|
155
152
|
baselineBuild = builds.data[0];
|
|
156
153
|
}
|
|
157
|
-
logger.info(`📥 Found baseline build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
|
|
158
154
|
|
|
159
155
|
// For specific buildId, we already have screenshots, otherwise get build details
|
|
160
156
|
let buildDetails = baselineBuild;
|
|
@@ -167,7 +163,8 @@ export class TddService {
|
|
|
167
163
|
logger.warn('⚠️ No screenshots found in baseline build');
|
|
168
164
|
return null;
|
|
169
165
|
}
|
|
170
|
-
logger.info(
|
|
166
|
+
logger.info(`Using baseline from build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
|
|
167
|
+
logger.info(`Checking ${colors.cyan(buildDetails.screenshots.length)} baseline screenshots...`);
|
|
171
168
|
|
|
172
169
|
// Debug screenshots structure (only in debug mode)
|
|
173
170
|
logger.debug(`📊 Screenshots array structure:`, {
|
|
@@ -352,17 +349,37 @@ export class TddService {
|
|
|
352
349
|
const metadataPath = join(this.baselinePath, 'metadata.json');
|
|
353
350
|
writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
|
|
354
351
|
|
|
352
|
+
// Save baseline build metadata for MCP plugin
|
|
353
|
+
const baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
|
|
354
|
+
const buildMetadata = {
|
|
355
|
+
buildId: baselineBuild.id,
|
|
356
|
+
buildName: baselineBuild.name,
|
|
357
|
+
branch: branch,
|
|
358
|
+
environment: environment,
|
|
359
|
+
commitSha: baselineBuild.commit_sha,
|
|
360
|
+
commitMessage: baselineBuild.commit_message,
|
|
361
|
+
approvalStatus: baselineBuild.approval_status,
|
|
362
|
+
completedAt: baselineBuild.completed_at,
|
|
363
|
+
downloadedAt: new Date().toISOString()
|
|
364
|
+
};
|
|
365
|
+
writeFileSync(baselineMetadataPath, JSON.stringify(buildMetadata, null, 2));
|
|
366
|
+
|
|
355
367
|
// Final summary
|
|
356
368
|
const actualDownloads = downloadedCount - skippedCount;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
369
|
+
if (skippedCount > 0) {
|
|
370
|
+
// All skipped (up-to-date)
|
|
371
|
+
if (actualDownloads === 0) {
|
|
372
|
+
logger.info(`✅ All ${skippedCount} baselines up-to-date (matching local SHA)`);
|
|
373
|
+
} else {
|
|
374
|
+
// Mixed: some downloaded, some skipped
|
|
375
|
+
logger.info(`✅ Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
|
|
376
|
+
}
|
|
364
377
|
} else {
|
|
365
|
-
|
|
378
|
+
// Fresh download
|
|
379
|
+
logger.info(`✅ Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
|
|
380
|
+
}
|
|
381
|
+
if (errorCount > 0) {
|
|
382
|
+
logger.warn(`⚠️ ${errorCount} screenshots failed to download`);
|
|
366
383
|
}
|
|
367
384
|
return this.baselineData;
|
|
368
385
|
} catch (error) {
|
|
@@ -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.
|
|
3
|
+
"version": "0.10.2",
|
|
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.
|
|
87
|
+
"zod": "^3.25.76"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@babel/cli": "^7.28.0",
|