@vizzly-testing/cli 0.17.0 → 0.19.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/dist/cli.js +87 -59
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +15 -15
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +28 -28
- 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 +26 -26
- package/dist/commands/upload.js +32 -32
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-api.js +43 -0
- 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 +22 -21
- 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 +32 -32
- 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 +152 -110
- package/dist/services/test-runner.js +3 -3
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +95 -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 +17 -17
- package/dist/utils/config-schema.js +6 -6
- 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 +52 -23
- package/docs/plugins.md +60 -25
- package/package.json +9 -13
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { program } from 'commander';
|
|
4
|
-
import { init } from './commands/init.js';
|
|
5
|
-
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
6
|
-
import { runCommand, validateRunOptions } from './commands/run.js';
|
|
7
|
-
import { tddCommand, validateTddOptions } from './commands/tdd.js';
|
|
8
|
-
import { tddStartCommand, tddStopCommand, tddStatusCommand, runDaemonChild } from './commands/tdd-daemon.js';
|
|
9
|
-
import { statusCommand, validateStatusOptions } from './commands/status.js';
|
|
10
|
-
import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
|
|
11
4
|
import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
|
|
5
|
+
import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
|
|
6
|
+
import { init } from './commands/init.js';
|
|
12
7
|
import { loginCommand, validateLoginOptions } from './commands/login.js';
|
|
13
8
|
import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
9
|
+
import { projectListCommand, projectRemoveCommand, projectSelectCommand, projectTokenCommand, validateProjectOptions } from './commands/project.js';
|
|
10
|
+
import { runCommand, validateRunOptions } from './commands/run.js';
|
|
11
|
+
import { statusCommand, validateStatusOptions } from './commands/status.js';
|
|
12
|
+
import { tddCommand, validateTddOptions } from './commands/tdd.js';
|
|
13
|
+
import { runDaemonChild, tddStartCommand, tddStatusCommand, tddStopCommand } from './commands/tdd-daemon.js';
|
|
14
|
+
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
15
|
+
import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
|
|
17
16
|
import { loadPlugins } from './plugin-loader.js';
|
|
18
|
-
import {
|
|
17
|
+
import { createPluginServices } from './plugin-api.js';
|
|
19
18
|
import { createServices } from './services/index.js';
|
|
19
|
+
import { loadConfig } from './utils/config-loader.js';
|
|
20
20
|
import * as output from './utils/output.js';
|
|
21
|
-
|
|
21
|
+
import { getPackageVersion } from './utils/package-info.js';
|
|
22
|
+
program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json', 'Machine-readable output').option('--no-color', 'Disable colored output');
|
|
22
23
|
|
|
23
24
|
// Load plugins before defining commands
|
|
24
25
|
// We need to manually parse to get the config option early
|
|
25
26
|
let configPath = null;
|
|
26
27
|
let verboseMode = false;
|
|
28
|
+
let logLevelArg = null;
|
|
27
29
|
for (let i = 0; i < process.argv.length; i++) {
|
|
28
30
|
if ((process.argv[i] === '-c' || process.argv[i] === '--config') && process.argv[i + 1]) {
|
|
29
31
|
configPath = process.argv[i + 1];
|
|
@@ -31,30 +33,36 @@ for (let i = 0; i < process.argv.length; i++) {
|
|
|
31
33
|
if (process.argv[i] === '-v' || process.argv[i] === '--verbose') {
|
|
32
34
|
verboseMode = true;
|
|
33
35
|
}
|
|
36
|
+
if (process.argv[i] === '--log-level' && process.argv[i + 1]) {
|
|
37
|
+
logLevelArg = process.argv[i + 1];
|
|
38
|
+
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
// Configure output early
|
|
42
|
+
// Priority: --log-level > --verbose > VIZZLY_LOG_LEVEL env var > default ('info')
|
|
37
43
|
output.configure({
|
|
44
|
+
logLevel: logLevelArg,
|
|
38
45
|
verbose: verboseMode,
|
|
39
46
|
color: !process.argv.includes('--no-color'),
|
|
40
47
|
json: process.argv.includes('--json')
|
|
41
48
|
});
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
const config = await loadConfig(configPath, {});
|
|
50
|
+
const services = createServices(config);
|
|
51
|
+
const pluginServices = createPluginServices(services);
|
|
44
52
|
let plugins = [];
|
|
45
53
|
try {
|
|
46
54
|
plugins = await loadPlugins(configPath, config);
|
|
47
|
-
for (
|
|
55
|
+
for (const plugin of plugins) {
|
|
48
56
|
try {
|
|
49
57
|
// Add timeout protection for plugin registration (5 seconds)
|
|
50
|
-
|
|
58
|
+
const registerPromise = plugin.register(program, {
|
|
51
59
|
config,
|
|
52
|
-
services,
|
|
60
|
+
services: pluginServices,
|
|
53
61
|
output,
|
|
54
62
|
// Backwards compatibility alias for plugins using old API
|
|
55
63
|
logger: output
|
|
56
64
|
});
|
|
57
|
-
|
|
65
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Plugin registration timeout (5s)')), 5000));
|
|
58
66
|
await Promise.race([registerPromise, timeoutPromise]);
|
|
59
67
|
output.debug(`Registered plugin: ${plugin.name}`);
|
|
60
68
|
} catch (error) {
|
|
@@ -65,7 +73,7 @@ try {
|
|
|
65
73
|
output.debug(`Plugin loading failed: ${error.message}`);
|
|
66
74
|
}
|
|
67
75
|
program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').action(async options => {
|
|
68
|
-
|
|
76
|
+
const globalOptions = program.opts();
|
|
69
77
|
await init({
|
|
70
78
|
...globalOptions,
|
|
71
79
|
...options,
|
|
@@ -73,24 +81,26 @@ program.command('init').description('Initialize Vizzly in your project').option(
|
|
|
73
81
|
});
|
|
74
82
|
});
|
|
75
83
|
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) => {
|
|
76
|
-
|
|
84
|
+
const globalOptions = program.opts();
|
|
77
85
|
|
|
78
86
|
// Validate options
|
|
79
|
-
|
|
87
|
+
const validationErrors = validateUploadOptions(path, options);
|
|
80
88
|
if (validationErrors.length > 0) {
|
|
81
89
|
output.error('Validation errors:');
|
|
82
|
-
|
|
90
|
+
for (let error of validationErrors) {
|
|
91
|
+
output.printErr(` - ${error}`);
|
|
92
|
+
}
|
|
83
93
|
process.exit(1);
|
|
84
94
|
}
|
|
85
95
|
await uploadCommand(path, options, globalOptions);
|
|
86
96
|
});
|
|
87
97
|
|
|
88
98
|
// TDD command with subcommands - Local visual testing with interactive dashboard
|
|
89
|
-
|
|
99
|
+
const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
|
|
90
100
|
|
|
91
101
|
// TDD Start - Background server
|
|
92
102
|
tddCmd.command('start').description('Start background TDD server with dashboard').option('--port <port>', 'Port for TDD server', '47392').option('--open', 'Open dashboard in browser').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--token <token>', 'API token override').option('--daemon-child', 'Internal: run as daemon child process').action(async options => {
|
|
93
|
-
|
|
103
|
+
const globalOptions = program.opts();
|
|
94
104
|
|
|
95
105
|
// If this is a daemon child process, run the server directly
|
|
96
106
|
if (options.daemonChild) {
|
|
@@ -102,34 +112,36 @@ tddCmd.command('start').description('Start background TDD server with dashboard'
|
|
|
102
112
|
|
|
103
113
|
// TDD Stop - Kill background server
|
|
104
114
|
tddCmd.command('stop').description('Stop background TDD server').action(async options => {
|
|
105
|
-
|
|
115
|
+
const globalOptions = program.opts();
|
|
106
116
|
await tddStopCommand(options, globalOptions);
|
|
107
117
|
});
|
|
108
118
|
|
|
109
119
|
// TDD Status - Check server status
|
|
110
120
|
tddCmd.command('status').description('Check TDD server status').action(async options => {
|
|
111
|
-
|
|
121
|
+
const globalOptions = program.opts();
|
|
112
122
|
await tddStatusCommand(options, globalOptions);
|
|
113
123
|
});
|
|
114
124
|
|
|
115
125
|
// TDD Run - One-off test run with ephemeral server (generates static report)
|
|
116
126
|
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual comparisons').option('--port <port>', 'Port for TDD server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').action(async (command, options) => {
|
|
117
|
-
|
|
127
|
+
const globalOptions = program.opts();
|
|
118
128
|
|
|
119
129
|
// Validate options
|
|
120
|
-
|
|
130
|
+
const validationErrors = validateTddOptions(command, options);
|
|
121
131
|
if (validationErrors.length > 0) {
|
|
122
132
|
output.error('Validation errors:');
|
|
123
|
-
|
|
133
|
+
for (let error of validationErrors) {
|
|
134
|
+
output.printErr(` - ${error}`);
|
|
135
|
+
}
|
|
124
136
|
process.exit(1);
|
|
125
137
|
}
|
|
126
|
-
|
|
138
|
+
const {
|
|
127
139
|
result,
|
|
128
140
|
cleanup
|
|
129
141
|
} = await tddCommand(command, options, globalOptions);
|
|
130
142
|
|
|
131
143
|
// Set up cleanup on process signals
|
|
132
|
-
|
|
144
|
+
const handleCleanup = async () => {
|
|
133
145
|
await cleanup();
|
|
134
146
|
};
|
|
135
147
|
process.once('SIGINT', () => {
|
|
@@ -145,17 +157,19 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
|
|
|
145
157
|
await cleanup();
|
|
146
158
|
});
|
|
147
159
|
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (command, options) => {
|
|
148
|
-
|
|
160
|
+
const globalOptions = program.opts();
|
|
149
161
|
|
|
150
162
|
// Validate options
|
|
151
|
-
|
|
163
|
+
const validationErrors = validateRunOptions(command, options);
|
|
152
164
|
if (validationErrors.length > 0) {
|
|
153
165
|
output.error('Validation errors:');
|
|
154
|
-
|
|
166
|
+
for (let error of validationErrors) {
|
|
167
|
+
output.printErr(` - ${error}`);
|
|
168
|
+
}
|
|
155
169
|
process.exit(1);
|
|
156
170
|
}
|
|
157
171
|
try {
|
|
158
|
-
|
|
172
|
+
const result = await runCommand(command, options, globalOptions);
|
|
159
173
|
if (result && !result.success && result.exitCode > 0) {
|
|
160
174
|
process.exit(result.exitCode);
|
|
161
175
|
}
|
|
@@ -165,99 +179,113 @@ program.command('run').description('Run tests with Vizzly integration').argument
|
|
|
165
179
|
}
|
|
166
180
|
});
|
|
167
181
|
program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
|
|
168
|
-
|
|
182
|
+
const globalOptions = program.opts();
|
|
169
183
|
|
|
170
184
|
// Validate options
|
|
171
|
-
|
|
185
|
+
const validationErrors = validateStatusOptions(buildId, options);
|
|
172
186
|
if (validationErrors.length > 0) {
|
|
173
187
|
output.error('Validation errors:');
|
|
174
|
-
|
|
188
|
+
for (let error of validationErrors) {
|
|
189
|
+
output.printErr(` - ${error}`);
|
|
190
|
+
}
|
|
175
191
|
process.exit(1);
|
|
176
192
|
}
|
|
177
193
|
await statusCommand(buildId, options, globalOptions);
|
|
178
194
|
});
|
|
179
195
|
program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
|
|
180
|
-
|
|
196
|
+
const globalOptions = program.opts();
|
|
181
197
|
|
|
182
198
|
// Validate options
|
|
183
|
-
|
|
199
|
+
const validationErrors = validateFinalizeOptions(parallelId, options);
|
|
184
200
|
if (validationErrors.length > 0) {
|
|
185
201
|
output.error('Validation errors:');
|
|
186
|
-
|
|
202
|
+
for (let error of validationErrors) {
|
|
203
|
+
output.printErr(` - ${error}`);
|
|
204
|
+
}
|
|
187
205
|
process.exit(1);
|
|
188
206
|
}
|
|
189
207
|
await finalizeCommand(parallelId, options, globalOptions);
|
|
190
208
|
});
|
|
191
209
|
program.command('doctor').description('Run diagnostics to check your environment and configuration').option('--api', 'Include API connectivity checks').action(async options => {
|
|
192
|
-
|
|
210
|
+
const globalOptions = program.opts();
|
|
193
211
|
|
|
194
212
|
// Validate options
|
|
195
|
-
|
|
213
|
+
const validationErrors = validateDoctorOptions(options);
|
|
196
214
|
if (validationErrors.length > 0) {
|
|
197
215
|
output.error('Validation errors:');
|
|
198
|
-
|
|
216
|
+
for (let error of validationErrors) {
|
|
217
|
+
output.printErr(` - ${error}`);
|
|
218
|
+
}
|
|
199
219
|
process.exit(1);
|
|
200
220
|
}
|
|
201
221
|
await doctorCommand(options, globalOptions);
|
|
202
222
|
});
|
|
203
223
|
program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
|
|
204
|
-
|
|
224
|
+
const globalOptions = program.opts();
|
|
205
225
|
|
|
206
226
|
// Validate options
|
|
207
|
-
|
|
227
|
+
const validationErrors = validateLoginOptions(options);
|
|
208
228
|
if (validationErrors.length > 0) {
|
|
209
229
|
output.error('Validation errors:');
|
|
210
|
-
|
|
230
|
+
for (let error of validationErrors) {
|
|
231
|
+
output.printErr(` - ${error}`);
|
|
232
|
+
}
|
|
211
233
|
process.exit(1);
|
|
212
234
|
}
|
|
213
235
|
await loginCommand(options, globalOptions);
|
|
214
236
|
});
|
|
215
237
|
program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
|
|
216
|
-
|
|
238
|
+
const globalOptions = program.opts();
|
|
217
239
|
|
|
218
240
|
// Validate options
|
|
219
|
-
|
|
241
|
+
const validationErrors = validateLogoutOptions(options);
|
|
220
242
|
if (validationErrors.length > 0) {
|
|
221
243
|
output.error('Validation errors:');
|
|
222
|
-
|
|
244
|
+
for (let error of validationErrors) {
|
|
245
|
+
output.printErr(` - ${error}`);
|
|
246
|
+
}
|
|
223
247
|
process.exit(1);
|
|
224
248
|
}
|
|
225
249
|
await logoutCommand(options, globalOptions);
|
|
226
250
|
});
|
|
227
251
|
program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
|
|
228
|
-
|
|
252
|
+
const globalOptions = program.opts();
|
|
229
253
|
|
|
230
254
|
// Validate options
|
|
231
|
-
|
|
255
|
+
const validationErrors = validateWhoamiOptions(options);
|
|
232
256
|
if (validationErrors.length > 0) {
|
|
233
257
|
output.error('Validation errors:');
|
|
234
|
-
|
|
258
|
+
for (let error of validationErrors) {
|
|
259
|
+
output.printErr(` - ${error}`);
|
|
260
|
+
}
|
|
235
261
|
process.exit(1);
|
|
236
262
|
}
|
|
237
263
|
await whoamiCommand(options, globalOptions);
|
|
238
264
|
});
|
|
239
265
|
program.command('project:select').description('Configure project for current directory').option('--api-url <url>', 'API URL override').action(async options => {
|
|
240
|
-
|
|
266
|
+
const globalOptions = program.opts();
|
|
241
267
|
|
|
242
268
|
// Validate options
|
|
243
|
-
|
|
269
|
+
const validationErrors = validateProjectOptions(options);
|
|
244
270
|
if (validationErrors.length > 0) {
|
|
245
271
|
output.error('Validation errors:');
|
|
246
|
-
|
|
272
|
+
for (let error of validationErrors) {
|
|
273
|
+
output.printErr(` - ${error}`);
|
|
274
|
+
}
|
|
247
275
|
process.exit(1);
|
|
248
276
|
}
|
|
249
277
|
await projectSelectCommand(options, globalOptions);
|
|
250
278
|
});
|
|
251
279
|
program.command('project:list').description('Show all configured projects').action(async options => {
|
|
252
|
-
|
|
280
|
+
const globalOptions = program.opts();
|
|
253
281
|
await projectListCommand(options, globalOptions);
|
|
254
282
|
});
|
|
255
283
|
program.command('project:token').description('Show project token for current directory').action(async options => {
|
|
256
|
-
|
|
284
|
+
const globalOptions = program.opts();
|
|
257
285
|
await projectTokenCommand(options, globalOptions);
|
|
258
286
|
});
|
|
259
287
|
program.command('project:remove').description('Remove project configuration for current directory').action(async options => {
|
|
260
|
-
|
|
288
|
+
const globalOptions = program.opts();
|
|
261
289
|
await projectRemoveCommand(options, globalOptions);
|
|
262
290
|
});
|
|
263
291
|
program.parse();
|
package/dist/client/index.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* @description Thin client for test runners - minimal API for taking screenshots
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join, parse } from 'node:path';
|
|
8
|
+
import { getBuildId, getServerUrl, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
|
|
9
9
|
|
|
10
10
|
// Internal client state
|
|
11
11
|
let currentClient = null;
|
|
@@ -104,12 +104,12 @@ function getClient() {
|
|
|
104
104
|
function createSimpleClient(serverUrl) {
|
|
105
105
|
return {
|
|
106
106
|
async screenshot(name, imageBuffer, options = {}) {
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
109
109
|
try {
|
|
110
110
|
// If it's a string, assume it's a file path and send directly
|
|
111
111
|
// Otherwise it's a Buffer, so convert to base64
|
|
112
|
-
|
|
112
|
+
const image = typeof imageBuffer === 'string' ? imageBuffer : imageBuffer.toString('base64');
|
|
113
113
|
const response = await fetch(`${serverUrl}/screenshot`, {
|
|
114
114
|
method: 'POST',
|
|
115
115
|
headers: {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { URL } from 'url';
|
|
2
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
3
|
-
import * as output from '../utils/output.js';
|
|
4
|
-
import { ApiService } from '../services/api-service.js';
|
|
1
|
+
import { URL } from 'node:url';
|
|
5
2
|
import { ConfigError } from '../errors/vizzly-error.js';
|
|
3
|
+
import { ApiService } from '../services/api-service.js';
|
|
4
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
6
5
|
import { getApiToken } from '../utils/environment-config.js';
|
|
6
|
+
import * as output from '../utils/output.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Doctor command implementation - Run diagnostics to check environment
|
|
@@ -16,7 +16,7 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
16
16
|
verbose: globalOptions.verbose,
|
|
17
17
|
color: !globalOptions.noColor
|
|
18
18
|
});
|
|
19
|
-
|
|
19
|
+
const diagnostics = {
|
|
20
20
|
environment: {
|
|
21
21
|
nodeVersion: null,
|
|
22
22
|
nodeVersionValid: null
|
|
@@ -37,14 +37,14 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
37
37
|
let hasErrors = false;
|
|
38
38
|
try {
|
|
39
39
|
// Determine if we'll attempt remote checks (API connectivity)
|
|
40
|
-
|
|
40
|
+
const willCheckConnectivity = Boolean(options.api || getApiToken());
|
|
41
41
|
|
|
42
42
|
// Announce preflight, indicating local-only when no token/connectivity is planned
|
|
43
43
|
output.info(`Running Vizzly preflight${willCheckConnectivity ? '' : ' (local checks only)'}...`);
|
|
44
44
|
|
|
45
45
|
// Node.js version check (require >= 20)
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const nodeVersion = process.version;
|
|
47
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
48
48
|
diagnostics.environment.nodeVersion = nodeVersion;
|
|
49
49
|
diagnostics.environment.nodeVersionValid = nodeMajor >= 20;
|
|
50
50
|
if (nodeMajor >= 20) {
|
|
@@ -55,12 +55,12 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Load configuration (apply global CLI overrides like --config only)
|
|
58
|
-
|
|
58
|
+
const config = await loadConfig(globalOptions.config);
|
|
59
59
|
|
|
60
60
|
// Validate apiUrl
|
|
61
61
|
diagnostics.configuration.apiUrl = config.apiUrl;
|
|
62
62
|
try {
|
|
63
|
-
|
|
63
|
+
const url = new URL(config.apiUrl);
|
|
64
64
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
65
65
|
throw new ConfigError('URL must use http or https');
|
|
66
66
|
}
|
|
@@ -73,10 +73,10 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Validate threshold (0..1 inclusive)
|
|
76
|
-
|
|
76
|
+
const threshold = Number(config?.comparison?.threshold);
|
|
77
77
|
diagnostics.configuration.threshold = threshold;
|
|
78
78
|
// CIEDE2000 threshold: 0 = exact, 1 = JND, 2 = recommended, 3+ = permissive
|
|
79
|
-
|
|
79
|
+
const thresholdValid = Number.isFinite(threshold) && threshold >= 0;
|
|
80
80
|
diagnostics.configuration.thresholdValid = thresholdValid;
|
|
81
81
|
if (thresholdValid) {
|
|
82
82
|
output.success(`Threshold: ${threshold} (CIEDE2000 Delta E)`);
|
|
@@ -86,12 +86,12 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// Report effective port without binding
|
|
89
|
-
|
|
89
|
+
const port = config?.server?.port ?? 47392;
|
|
90
90
|
diagnostics.configuration.port = port;
|
|
91
91
|
output.info(`Effective port: ${port}`);
|
|
92
92
|
|
|
93
93
|
// Optional: API connectivity check when --api is provided or VIZZLY_TOKEN is present
|
|
94
|
-
|
|
94
|
+
const autoApi = Boolean(getApiToken());
|
|
95
95
|
if (options.api || autoApi) {
|
|
96
96
|
diagnostics.connectivity.checked = true;
|
|
97
97
|
if (!config.apiKey) {
|
|
@@ -102,7 +102,7 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
102
102
|
} else {
|
|
103
103
|
output.progress('Checking API connectivity...');
|
|
104
104
|
try {
|
|
105
|
-
|
|
105
|
+
const api = new ApiService({
|
|
106
106
|
baseUrl: config.apiUrl,
|
|
107
107
|
token: config.apiKey,
|
|
108
108
|
command: 'doctor'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { createServices } from '../services/index.js';
|
|
1
2
|
import { loadConfig } from '../utils/config-loader.js';
|
|
2
3
|
import * as output from '../utils/output.js';
|
|
3
|
-
import { createServices } from '../services/index.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Finalize command implementation
|
|
@@ -16,11 +16,11 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
16
16
|
});
|
|
17
17
|
try {
|
|
18
18
|
// Load configuration with CLI overrides
|
|
19
|
-
|
|
19
|
+
const allOptions = {
|
|
20
20
|
...globalOptions,
|
|
21
21
|
...options
|
|
22
22
|
};
|
|
23
|
-
|
|
23
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
24
24
|
|
|
25
25
|
// Validate API token
|
|
26
26
|
if (!config.apiKey) {
|
|
@@ -37,12 +37,12 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
37
37
|
|
|
38
38
|
// Create services and get API service
|
|
39
39
|
output.startSpinner('Finalizing parallel build...');
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const services = createServices(config, 'finalize');
|
|
41
|
+
const apiService = services.apiService;
|
|
42
42
|
output.stopSpinner();
|
|
43
43
|
|
|
44
44
|
// Call finalize endpoint
|
|
45
|
-
|
|
45
|
+
const result = await apiService.finalizeParallelBuild(parallelId);
|
|
46
46
|
if (globalOptions.json) {
|
|
47
47
|
output.data(result);
|
|
48
48
|
} else {
|
|
@@ -65,7 +65,7 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
65
65
|
* @param {Object} options - Command options
|
|
66
66
|
*/
|
|
67
67
|
export function validateFinalizeOptions(parallelId, _options) {
|
|
68
|
-
|
|
68
|
+
const errors = [];
|
|
69
69
|
if (!parallelId || parallelId.trim() === '') {
|
|
70
70
|
errors.push('Parallel ID is required');
|
|
71
71
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
4
5
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
5
|
-
import * as output from '../utils/output.js';
|
|
6
6
|
import { loadPlugins } from '../plugin-loader.js';
|
|
7
7
|
import { loadConfig } from '../utils/config-loader.js';
|
|
8
|
-
import
|
|
8
|
+
import * as output from '../utils/output.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Simple configuration setup for Vizzly CLI
|
|
@@ -19,8 +19,8 @@ export class InitCommand {
|
|
|
19
19
|
output.blank();
|
|
20
20
|
try {
|
|
21
21
|
// Check for existing config
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const configPath = path.join(process.cwd(), 'vizzly.config.js');
|
|
23
|
+
const hasConfig = await this.fileExists(configPath);
|
|
24
24
|
if (hasConfig && !options.force) {
|
|
25
25
|
output.info('❌ A vizzly.config.js file already exists. Use --force to overwrite.');
|
|
26
26
|
return;
|
|
@@ -71,16 +71,16 @@ export class InitCommand {
|
|
|
71
71
|
}`;
|
|
72
72
|
|
|
73
73
|
// Add plugin configurations
|
|
74
|
-
|
|
74
|
+
const pluginConfigs = this.generatePluginConfigs();
|
|
75
75
|
if (pluginConfigs) {
|
|
76
|
-
coreConfig +=
|
|
76
|
+
coreConfig += `,\n\n${pluginConfigs}`;
|
|
77
77
|
}
|
|
78
78
|
coreConfig += '\n};\n';
|
|
79
79
|
await fs.writeFile(configPath, coreConfig, 'utf8');
|
|
80
80
|
output.info(`📄 Created vizzly.config.js`);
|
|
81
81
|
|
|
82
82
|
// Log discovered plugins
|
|
83
|
-
|
|
83
|
+
const pluginsWithConfig = this.plugins.filter(p => p.configSchema);
|
|
84
84
|
if (pluginsWithConfig.length > 0) {
|
|
85
85
|
output.info(` Added config for ${pluginsWithConfig.length} plugin(s):`);
|
|
86
86
|
pluginsWithConfig.forEach(p => {
|
|
@@ -94,10 +94,10 @@ export class InitCommand {
|
|
|
94
94
|
* @returns {string} Plugin config sections as formatted string
|
|
95
95
|
*/
|
|
96
96
|
generatePluginConfigs() {
|
|
97
|
-
|
|
98
|
-
for (
|
|
97
|
+
const sections = [];
|
|
98
|
+
for (const plugin of this.plugins) {
|
|
99
99
|
if (plugin.configSchema) {
|
|
100
|
-
|
|
100
|
+
const configStr = this.formatPluginConfig(plugin);
|
|
101
101
|
if (configStr) {
|
|
102
102
|
sections.push(configStr);
|
|
103
103
|
}
|
|
@@ -114,18 +114,18 @@ export class InitCommand {
|
|
|
114
114
|
formatPluginConfig(plugin) {
|
|
115
115
|
try {
|
|
116
116
|
// Validate config schema structure with Zod (defensive check)
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
|
|
118
|
+
const configSchemaValidator = z.record(configValueSchema);
|
|
119
119
|
configSchemaValidator.parse(plugin.configSchema);
|
|
120
|
-
|
|
121
|
-
for (
|
|
122
|
-
|
|
120
|
+
const configEntries = [];
|
|
121
|
+
for (const [key, value] of Object.entries(plugin.configSchema)) {
|
|
122
|
+
const formattedValue = this.formatValue(value, 1);
|
|
123
123
|
configEntries.push(` // ${plugin.name} plugin configuration\n ${key}: ${formattedValue}`);
|
|
124
124
|
}
|
|
125
125
|
return configEntries.join(',\n\n');
|
|
126
126
|
} catch (error) {
|
|
127
127
|
if (error instanceof z.ZodError) {
|
|
128
|
-
|
|
128
|
+
const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
|
|
129
129
|
output.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
|
|
130
130
|
} else {
|
|
131
131
|
output.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
|
|
@@ -141,25 +141,25 @@ export class InitCommand {
|
|
|
141
141
|
* @returns {string} Formatted value
|
|
142
142
|
*/
|
|
143
143
|
formatValue(value, depth = 0) {
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
const indent = ' '.repeat(depth);
|
|
145
|
+
const nextIndent = ' '.repeat(depth + 1);
|
|
146
146
|
if (value === null) return 'null';
|
|
147
147
|
if (value === undefined) return 'undefined';
|
|
148
148
|
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`;
|
|
149
149
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
150
150
|
if (Array.isArray(value)) {
|
|
151
151
|
if (value.length === 0) return '[]';
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
const items = value.map(item => {
|
|
153
|
+
const formatted = this.formatValue(item, depth + 1);
|
|
154
154
|
return `${nextIndent}${formatted}`;
|
|
155
155
|
});
|
|
156
156
|
return `[\n${items.join(',\n')}\n${indent}]`;
|
|
157
157
|
}
|
|
158
158
|
if (typeof value === 'object') {
|
|
159
|
-
|
|
159
|
+
const entries = Object.entries(value);
|
|
160
160
|
if (entries.length === 0) return '{}';
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
const props = entries.map(([k, v]) => {
|
|
162
|
+
const formatted = this.formatValue(v, depth + 1);
|
|
163
163
|
return `${nextIndent}${k}: ${formatted}`;
|
|
164
164
|
});
|
|
165
165
|
return `{\n${props.join(',\n')}\n${indent}}`;
|
|
@@ -188,7 +188,7 @@ export class InitCommand {
|
|
|
188
188
|
|
|
189
189
|
// Export factory function for CLI
|
|
190
190
|
export function createInitCommand(options) {
|
|
191
|
-
|
|
191
|
+
const command = new InitCommand(options.plugins);
|
|
192
192
|
return () => command.run(options);
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -204,7 +204,7 @@ export async function init(options = {}) {
|
|
|
204
204
|
// Try to load plugins if not provided
|
|
205
205
|
if (!options.plugins) {
|
|
206
206
|
try {
|
|
207
|
-
|
|
207
|
+
const config = await loadConfig(options.config, {});
|
|
208
208
|
plugins = await loadPlugins(options.config, config, null);
|
|
209
209
|
} catch {
|
|
210
210
|
// Silent fail - plugins are optional for init
|
|
@@ -212,6 +212,6 @@ export async function init(options = {}) {
|
|
|
212
212
|
} else {
|
|
213
213
|
plugins = options.plugins;
|
|
214
214
|
}
|
|
215
|
-
|
|
215
|
+
const command = new InitCommand(plugins);
|
|
216
216
|
return await command.run(options);
|
|
217
217
|
}
|