@vizzly-testing/cli 0.17.0 → 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/dist/cli.js +84 -58
- 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-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 +11 -11
- 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 +98 -92
- 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 +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 +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/package.json +9 -13
package/dist/utils/output.js
CHANGED
|
@@ -8,14 +8,28 @@
|
|
|
8
8
|
* Replaces both ConsoleUI and Logger with a single, simple API.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
11
13
|
import { createColors } from './colors.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
+
import { getLogLevel as getEnvLogLevel } from './environment-config.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Log levels in order of severity (lowest to highest)
|
|
18
|
+
* debug < info < warn < error
|
|
19
|
+
*/
|
|
20
|
+
const LOG_LEVELS = {
|
|
21
|
+
debug: 0,
|
|
22
|
+
info: 1,
|
|
23
|
+
warn: 2,
|
|
24
|
+
error: 3
|
|
25
|
+
};
|
|
26
|
+
const VALID_LOG_LEVELS = Object.keys(LOG_LEVELS);
|
|
14
27
|
|
|
15
28
|
// Module state
|
|
16
|
-
|
|
29
|
+
const config = {
|
|
17
30
|
json: false,
|
|
18
|
-
|
|
31
|
+
logLevel: null,
|
|
32
|
+
// null = not yet initialized, will check env var on first configure
|
|
19
33
|
color: true,
|
|
20
34
|
silent: false,
|
|
21
35
|
logFile: null
|
|
@@ -31,23 +45,79 @@ let startTime = Date.now();
|
|
|
31
45
|
// Track if we've shown the header
|
|
32
46
|
let headerShown = false;
|
|
33
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Check if a given log level should be displayed based on current config
|
|
50
|
+
* @param {string} level - The level to check (debug, info, warn, error)
|
|
51
|
+
* @returns {boolean} Whether the level should be displayed
|
|
52
|
+
*/
|
|
53
|
+
function shouldLog(level) {
|
|
54
|
+
if (config.silent) return false;
|
|
55
|
+
// If logLevel not yet initialized, default to 'info'
|
|
56
|
+
let configLevel = config.logLevel || 'info';
|
|
57
|
+
let currentLevel = LOG_LEVELS[configLevel];
|
|
58
|
+
let targetLevel = LOG_LEVELS[level];
|
|
59
|
+
// Default to showing everything if levels are invalid
|
|
60
|
+
if (currentLevel === undefined) currentLevel = LOG_LEVELS.info;
|
|
61
|
+
if (targetLevel === undefined) targetLevel = LOG_LEVELS.debug;
|
|
62
|
+
return targetLevel >= currentLevel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize and validate log level
|
|
67
|
+
* @param {string} level - Log level to validate
|
|
68
|
+
* @returns {string} Valid log level (defaults to 'info' if invalid)
|
|
69
|
+
*/
|
|
70
|
+
function normalizeLogLevel(level) {
|
|
71
|
+
if (!level) return 'info';
|
|
72
|
+
let normalized = level.toLowerCase().trim();
|
|
73
|
+
return VALID_LOG_LEVELS.includes(normalized) ? normalized : 'info';
|
|
74
|
+
}
|
|
75
|
+
|
|
34
76
|
/**
|
|
35
77
|
* Configure output settings
|
|
36
78
|
* Call this once at CLI startup with global options
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} options - Configuration options
|
|
81
|
+
* @param {boolean} [options.json] - Enable JSON output mode
|
|
82
|
+
* @param {string} [options.logLevel] - Log level (debug, info, warn, error)
|
|
83
|
+
* @param {boolean} [options.verbose] - Shorthand for logLevel='debug' (backwards compatible)
|
|
84
|
+
* @param {boolean} [options.color] - Enable colored output
|
|
85
|
+
* @param {boolean} [options.silent] - Suppress all output
|
|
86
|
+
* @param {string} [options.logFile] - Path to log file
|
|
87
|
+
* @param {boolean} [options.resetTimer] - Reset the start timer (default: true)
|
|
37
88
|
*/
|
|
38
89
|
export function configure(options = {}) {
|
|
39
90
|
if (options.json !== undefined) config.json = options.json;
|
|
40
|
-
if (options.verbose !== undefined) config.verbose = options.verbose;
|
|
41
91
|
if (options.color !== undefined) config.color = options.color;
|
|
42
92
|
if (options.silent !== undefined) config.silent = options.silent;
|
|
43
93
|
if (options.logFile !== undefined) config.logFile = options.logFile;
|
|
94
|
+
|
|
95
|
+
// Determine log level with priority:
|
|
96
|
+
// 1. Explicit logLevel option (highest priority)
|
|
97
|
+
// 2. verbose flag (maps to 'debug')
|
|
98
|
+
// 3. Keep existing level if already initialized
|
|
99
|
+
// 4. VIZZLY_LOG_LEVEL env var (checked on first configure when logLevel is null)
|
|
100
|
+
// 5. Default ('info')
|
|
101
|
+
if (options.logLevel !== undefined) {
|
|
102
|
+
config.logLevel = normalizeLogLevel(options.logLevel);
|
|
103
|
+
} else if (options.verbose) {
|
|
104
|
+
config.logLevel = 'debug';
|
|
105
|
+
} else if (config.logLevel === null) {
|
|
106
|
+
// First configure call - check env var
|
|
107
|
+
let envLogLevel = getEnvLogLevel();
|
|
108
|
+
config.logLevel = normalizeLogLevel(envLogLevel);
|
|
109
|
+
}
|
|
110
|
+
// If logLevel is already set (not null) and no new option was provided, keep it
|
|
111
|
+
|
|
44
112
|
colors = createColors({
|
|
45
113
|
useColor: config.color
|
|
46
114
|
});
|
|
47
115
|
|
|
48
|
-
// Reset state
|
|
49
|
-
|
|
50
|
-
|
|
116
|
+
// Reset state (optional - commands may want to preserve timer)
|
|
117
|
+
if (options.resetTimer !== false) {
|
|
118
|
+
startTime = Date.now();
|
|
119
|
+
headerShown = false;
|
|
120
|
+
}
|
|
51
121
|
|
|
52
122
|
// Initialize log file if specified
|
|
53
123
|
if (config.logFile) {
|
|
@@ -55,6 +125,22 @@ export function configure(options = {}) {
|
|
|
55
125
|
}
|
|
56
126
|
}
|
|
57
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Get current log level
|
|
130
|
+
* @returns {string} Current log level (defaults to 'info' if not initialized)
|
|
131
|
+
*/
|
|
132
|
+
export function getLogLevel() {
|
|
133
|
+
return config.logLevel || 'info';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if verbose/debug mode is enabled
|
|
138
|
+
* @returns {boolean} True if debug level is active
|
|
139
|
+
*/
|
|
140
|
+
export function isVerbose() {
|
|
141
|
+
return config.logLevel === 'debug';
|
|
142
|
+
}
|
|
143
|
+
|
|
58
144
|
/**
|
|
59
145
|
* Show command header (e.g., "vizzly · tdd · local")
|
|
60
146
|
* Only shows once per command execution
|
|
@@ -62,7 +148,7 @@ export function configure(options = {}) {
|
|
|
62
148
|
export function header(command, mode = null) {
|
|
63
149
|
if (config.json || config.silent || headerShown) return;
|
|
64
150
|
headerShown = true;
|
|
65
|
-
|
|
151
|
+
const parts = ['vizzly', command];
|
|
66
152
|
if (mode) parts.push(mode);
|
|
67
153
|
console.error('');
|
|
68
154
|
console.error(colors.dim(parts.join(' · ')));
|
|
@@ -104,7 +190,7 @@ export function success(message, data = {}) {
|
|
|
104
190
|
export function result(message) {
|
|
105
191
|
stopSpinner();
|
|
106
192
|
if (config.silent) return;
|
|
107
|
-
|
|
193
|
+
const elapsed = getElapsedTime();
|
|
108
194
|
if (config.json) {
|
|
109
195
|
console.log(JSON.stringify({
|
|
110
196
|
status: 'complete',
|
|
@@ -121,7 +207,7 @@ export function result(message) {
|
|
|
121
207
|
* Show an info message
|
|
122
208
|
*/
|
|
123
209
|
export function info(message, data = {}) {
|
|
124
|
-
if (
|
|
210
|
+
if (!shouldLog('info')) return;
|
|
125
211
|
if (config.json) {
|
|
126
212
|
console.log(JSON.stringify({
|
|
127
213
|
status: 'info',
|
|
@@ -138,7 +224,7 @@ export function info(message, data = {}) {
|
|
|
138
224
|
*/
|
|
139
225
|
export function warn(message, data = {}) {
|
|
140
226
|
stopSpinner();
|
|
141
|
-
if (
|
|
227
|
+
if (!shouldLog('warn')) return;
|
|
142
228
|
if (config.json) {
|
|
143
229
|
console.error(JSON.stringify({
|
|
144
230
|
status: 'warning',
|
|
@@ -153,9 +239,13 @@ export function warn(message, data = {}) {
|
|
|
153
239
|
/**
|
|
154
240
|
* Show an error message (goes to stderr)
|
|
155
241
|
* Does NOT exit - caller decides whether to exit
|
|
242
|
+
* Note: Errors are always shown regardless of log level (unless silent mode)
|
|
156
243
|
*/
|
|
157
244
|
export function error(message, err = null, data = {}) {
|
|
158
245
|
stopSpinner();
|
|
246
|
+
|
|
247
|
+
// Errors always show (unless silent), but we still check shouldLog for consistency
|
|
248
|
+
if (config.silent) return;
|
|
159
249
|
if (config.json) {
|
|
160
250
|
let errorData = {
|
|
161
251
|
status: 'error',
|
|
@@ -168,7 +258,7 @@ export function error(message, err = null, data = {}) {
|
|
|
168
258
|
message: err.getUserMessage ? err.getUserMessage() : err.message,
|
|
169
259
|
code: err.code
|
|
170
260
|
};
|
|
171
|
-
if (
|
|
261
|
+
if (isVerbose()) {
|
|
172
262
|
errorData.error.stack = err.stack;
|
|
173
263
|
}
|
|
174
264
|
}
|
|
@@ -182,7 +272,7 @@ export function error(message, err = null, data = {}) {
|
|
|
182
272
|
if (errMessage && errMessage !== message) {
|
|
183
273
|
console.error(colors.dim(errMessage));
|
|
184
274
|
}
|
|
185
|
-
if (
|
|
275
|
+
if (isVerbose() && err.stack) {
|
|
186
276
|
console.error(colors.dim(err.stack));
|
|
187
277
|
}
|
|
188
278
|
} else if (typeof err === 'string' && err) {
|
|
@@ -249,14 +339,14 @@ export function startSpinner(message) {
|
|
|
249
339
|
if (config.json || config.silent || !process.stderr.isTTY) return;
|
|
250
340
|
stopSpinner();
|
|
251
341
|
spinnerMessage = message;
|
|
252
|
-
|
|
342
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
253
343
|
let i = 0;
|
|
254
344
|
spinnerInterval = setInterval(() => {
|
|
255
|
-
|
|
256
|
-
|
|
345
|
+
const frame = frames[i++ % frames.length];
|
|
346
|
+
const line = `${colors.cyan(frame)} ${spinnerMessage}`;
|
|
257
347
|
|
|
258
348
|
// Clear previous line and write new one
|
|
259
|
-
process.stderr.write(
|
|
349
|
+
process.stderr.write(`\r${' '.repeat(lastSpinnerLine.length)}\r`);
|
|
260
350
|
process.stderr.write(line);
|
|
261
351
|
lastSpinnerLine = line;
|
|
262
352
|
}, 80);
|
|
@@ -267,7 +357,7 @@ export function startSpinner(message) {
|
|
|
267
357
|
*/
|
|
268
358
|
export function updateSpinner(message, current = 0, total = 0) {
|
|
269
359
|
if (config.json || config.silent || !process.stderr.isTTY) return;
|
|
270
|
-
|
|
360
|
+
const progressText = total > 0 ? ` (${current}/${total})` : '';
|
|
271
361
|
spinnerMessage = `${message}${progressText}`;
|
|
272
362
|
if (!spinnerInterval) {
|
|
273
363
|
startSpinner(spinnerMessage);
|
|
@@ -284,7 +374,7 @@ export function stopSpinner() {
|
|
|
284
374
|
|
|
285
375
|
// Clear the spinner line
|
|
286
376
|
if (process.stderr.isTTY) {
|
|
287
|
-
process.stderr.write(
|
|
377
|
+
process.stderr.write(`\r${' '.repeat(lastSpinnerLine.length)}\r`);
|
|
288
378
|
}
|
|
289
379
|
lastSpinnerLine = '';
|
|
290
380
|
spinnerMessage = '';
|
|
@@ -318,7 +408,7 @@ export function progress(message, current = 0, total = 0) {
|
|
|
318
408
|
* Format elapsed time since CLI start
|
|
319
409
|
*/
|
|
320
410
|
function getElapsedTime() {
|
|
321
|
-
|
|
411
|
+
const elapsed = Date.now() - startTime;
|
|
322
412
|
if (elapsed < 1000) {
|
|
323
413
|
return `${elapsed}ms`;
|
|
324
414
|
}
|
|
@@ -331,7 +421,7 @@ function getElapsedTime() {
|
|
|
331
421
|
*/
|
|
332
422
|
function formatData(data) {
|
|
333
423
|
if (!data || typeof data !== 'object') return '';
|
|
334
|
-
|
|
424
|
+
const entries = Object.entries(data).filter(([, v]) => {
|
|
335
425
|
if (v === null || v === undefined) return false;
|
|
336
426
|
if (typeof v === 'string' && v === '') return false;
|
|
337
427
|
if (Array.isArray(v) && v.length === 0) return false;
|
|
@@ -354,14 +444,14 @@ function formatData(data) {
|
|
|
354
444
|
}
|
|
355
445
|
|
|
356
446
|
/**
|
|
357
|
-
* Log debug message with component prefix (only shown
|
|
447
|
+
* Log debug message with component prefix (only shown when log level is 'debug')
|
|
358
448
|
*
|
|
359
449
|
* @param {string} component - Component name (e.g., 'server', 'config', 'build')
|
|
360
450
|
* @param {string} message - Debug message
|
|
361
451
|
* @param {Object} data - Optional data object to display inline
|
|
362
452
|
*/
|
|
363
453
|
export function debug(component, message, data = {}) {
|
|
364
|
-
if (!
|
|
454
|
+
if (!shouldLog('debug')) return;
|
|
365
455
|
|
|
366
456
|
// Handle legacy calls: debug('message') or debug('message', {data})
|
|
367
457
|
if (typeof message === 'object' || message === undefined) {
|
|
@@ -406,14 +496,14 @@ function initLogFile() {
|
|
|
406
496
|
mkdirSync(dirname(config.logFile), {
|
|
407
497
|
recursive: true
|
|
408
498
|
});
|
|
409
|
-
|
|
499
|
+
const header = {
|
|
410
500
|
timestamp: new Date().toISOString(),
|
|
411
501
|
session_start: true,
|
|
412
502
|
pid: process.pid,
|
|
413
503
|
node_version: process.version,
|
|
414
504
|
platform: process.platform
|
|
415
505
|
};
|
|
416
|
-
writeFileSync(config.logFile, JSON.stringify(header)
|
|
506
|
+
writeFileSync(config.logFile, `${JSON.stringify(header)}\n`);
|
|
417
507
|
} catch {
|
|
418
508
|
// Silently fail - don't crash CLI for logging issues
|
|
419
509
|
}
|
|
@@ -421,13 +511,13 @@ function initLogFile() {
|
|
|
421
511
|
function writeLog(level, message, data = {}) {
|
|
422
512
|
if (!config.logFile) return;
|
|
423
513
|
try {
|
|
424
|
-
|
|
514
|
+
const entry = {
|
|
425
515
|
timestamp: new Date().toISOString(),
|
|
426
516
|
level,
|
|
427
517
|
message,
|
|
428
518
|
...data
|
|
429
519
|
};
|
|
430
|
-
appendFileSync(config.logFile, JSON.stringify(entry)
|
|
520
|
+
appendFileSync(config.logFile, `${JSON.stringify(entry)}\n`);
|
|
431
521
|
} catch {
|
|
432
522
|
// Silently fail
|
|
433
523
|
}
|
|
@@ -442,4 +532,22 @@ function writeLog(level, message, data = {}) {
|
|
|
442
532
|
*/
|
|
443
533
|
export function cleanup() {
|
|
444
534
|
stopSpinner();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Reset module state to defaults (useful for testing)
|
|
539
|
+
* This resets all configuration to initial state
|
|
540
|
+
*/
|
|
541
|
+
export function reset() {
|
|
542
|
+
stopSpinner();
|
|
543
|
+
config.json = false;
|
|
544
|
+
config.logLevel = null;
|
|
545
|
+
config.color = true;
|
|
546
|
+
config.silent = false;
|
|
547
|
+
config.logFile = null;
|
|
548
|
+
colors = createColors({
|
|
549
|
+
useColor: config.color
|
|
550
|
+
});
|
|
551
|
+
startTime = Date.now();
|
|
552
|
+
headerShown = false;
|
|
445
553
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Centralized access to package.json data
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync } from 'fs';
|
|
7
|
-
import { dirname, join } from 'path';
|
|
8
|
-
import { fileURLToPath } from 'url';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
let packageJson;
|
package/dist/utils/security.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Protects against path traversal attacks and ensures safe file operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { isAbsolute, join, normalize, resolve } from 'node:path';
|
|
7
7
|
import * as output from './output.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -38,12 +38,12 @@ export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = fal
|
|
|
38
38
|
|
|
39
39
|
// Allow only safe characters: alphanumeric, hyphens, underscores, dots, and optionally slashes
|
|
40
40
|
// Replace other characters with underscores
|
|
41
|
-
|
|
41
|
+
const allowedChars = allowSlashes ? /[^a-zA-Z0-9._/-]/g : /[^a-zA-Z0-9._-]/g;
|
|
42
42
|
let sanitized = name.replace(allowedChars, '_');
|
|
43
43
|
|
|
44
44
|
// Prevent names that start with dots (hidden files)
|
|
45
45
|
if (sanitized.startsWith('.')) {
|
|
46
|
-
sanitized =
|
|
46
|
+
sanitized = `file_${sanitized}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Ensure we have a valid filename
|
|
@@ -69,8 +69,8 @@ export function validatePathSecurity(targetPath, workingDir) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Normalize and resolve both paths
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const resolvedWorkingDir = resolve(normalize(workingDir));
|
|
73
|
+
const resolvedTargetPath = resolve(normalize(targetPath));
|
|
74
74
|
|
|
75
75
|
// Ensure the target path starts with the working directory
|
|
76
76
|
if (!resolvedTargetPath.startsWith(resolvedWorkingDir)) {
|
|
@@ -93,7 +93,7 @@ export function safePath(workingDir, ...pathSegments) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// Sanitize each path segment
|
|
96
|
-
|
|
96
|
+
const sanitizedSegments = pathSegments.map(segment => {
|
|
97
97
|
if (typeof segment !== 'string') {
|
|
98
98
|
throw new Error('Path segment must be a string');
|
|
99
99
|
}
|
|
@@ -104,7 +104,7 @@ export function safePath(workingDir, ...pathSegments) {
|
|
|
104
104
|
}
|
|
105
105
|
return segment;
|
|
106
106
|
});
|
|
107
|
-
|
|
107
|
+
const targetPath = join(workingDir, ...sanitizedSegments);
|
|
108
108
|
return validatePathSecurity(targetPath, workingDir);
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -117,13 +117,13 @@ export function validateScreenshotProperties(properties = {}) {
|
|
|
117
117
|
if (properties === null || typeof properties !== 'object') {
|
|
118
118
|
return {};
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
const validated = {};
|
|
121
121
|
|
|
122
122
|
// Validate common properties with safe constraints
|
|
123
123
|
if (properties.browser && typeof properties.browser === 'string') {
|
|
124
124
|
try {
|
|
125
125
|
// Extract browser name without version (e.g., "Chrome/139.0.7258.138" -> "Chrome")
|
|
126
|
-
|
|
126
|
+
const browserName = properties.browser.split('/')[0];
|
|
127
127
|
validated.browser = sanitizeScreenshotName(browserName, 50);
|
|
128
128
|
} catch (error) {
|
|
129
129
|
// Skip invalid browser names, don't include them
|
|
@@ -131,7 +131,7 @@ export function validateScreenshotProperties(properties = {}) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
if (properties.viewport && typeof properties.viewport === 'object') {
|
|
134
|
-
|
|
134
|
+
const viewport = {};
|
|
135
135
|
if (typeof properties.viewport.width === 'number' && properties.viewport.width > 0 && properties.viewport.width <= 10000) {
|
|
136
136
|
viewport.width = Math.floor(properties.viewport.width);
|
|
137
137
|
}
|
|
@@ -144,14 +144,14 @@ export function validateScreenshotProperties(properties = {}) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// Allow other safe string properties but sanitize them
|
|
147
|
-
for (
|
|
147
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
148
148
|
if (key === 'browser' || key === 'viewport') continue; // Already handled
|
|
149
149
|
|
|
150
150
|
if (typeof key === 'string' && key.length <= 50 && /^[a-zA-Z0-9_-]+$/.test(key)) {
|
|
151
151
|
if (typeof value === 'string' && value.length <= 200) {
|
|
152
152
|
// Store sanitized version of string values
|
|
153
153
|
validated[key] = value.replace(/[<>&"']/g, ''); // Basic HTML entity prevention
|
|
154
|
-
} else if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
|
|
154
|
+
} else if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
|
|
155
155
|
validated[key] = value;
|
|
156
156
|
} else if (typeof value === 'boolean') {
|
|
157
157
|
validated[key] = value;
|
package/docs/api-reference.md
CHANGED
|
@@ -613,7 +613,8 @@ Available on all commands:
|
|
|
613
613
|
|
|
614
614
|
- `-c, --config <path>` - Config file path
|
|
615
615
|
- `--token <token>` - Vizzly API token
|
|
616
|
-
- `-v, --verbose` - Verbose output
|
|
616
|
+
- `-v, --verbose` - Verbose output (shorthand for `--log-level debug`)
|
|
617
|
+
- `--log-level <level>` - Log level: `debug`, `info`, `warn`, `error` (default: `info`, or `VIZZLY_LOG_LEVEL` env var)
|
|
617
618
|
- `--json` - Machine-readable JSON output
|
|
618
619
|
- `--no-color` - Disable colored output
|
|
619
620
|
|
|
@@ -661,43 +662,71 @@ Configuration loaded via cosmiconfig in this order:
|
|
|
661
662
|
|
|
662
663
|
// Comparison Configuration
|
|
663
664
|
comparison: {
|
|
664
|
-
threshold: number
|
|
665
|
+
threshold: number, // CIEDE2000 Delta E (default: 2.0)
|
|
666
|
+
minClusterSize: number // Min cluster size for noise filtering (default: 2)
|
|
665
667
|
}
|
|
666
668
|
}
|
|
667
669
|
```
|
|
668
670
|
|
|
669
671
|
### Environment Variables
|
|
670
672
|
|
|
671
|
-
|
|
672
|
-
- `VIZZLY_TOKEN` - API authentication token (project token or access token)
|
|
673
|
-
- For local development: Use `vizzly login` instead of manually managing tokens
|
|
674
|
-
- For CI/CD: Use project tokens from environment variables
|
|
675
|
-
- Token priority: CLI flag → env var → project mapping → user access token
|
|
673
|
+
All Vizzly CLI behavior can be configured via environment variables. This is the complete reference.
|
|
676
674
|
|
|
677
|
-
|
|
678
|
-
- `VIZZLY_API_URL` - API base URL override (default: `https://app.vizzly.dev`)
|
|
675
|
+
#### Configuration
|
|
679
676
|
|
|
680
|
-
|
|
681
|
-
|
|
677
|
+
| Variable | Description | Default |
|
|
678
|
+
|----------|-------------|---------|
|
|
679
|
+
| `VIZZLY_HOME` | Override config directory (auth, project mappings) | `~/.vizzly` |
|
|
680
|
+
| `VIZZLY_TOKEN` | API authentication token (project token `vzt_...`) | - |
|
|
681
|
+
| `VIZZLY_API_URL` | API base URL | `https://app.vizzly.dev` |
|
|
682
|
+
| `VIZZLY_LOG_LEVEL` | Logging verbosity: `debug`, `info`, `warn`, `error` | `info` |
|
|
682
683
|
|
|
683
|
-
**
|
|
684
|
-
- `VIZZLY_COMMIT_SHA` - Override detected commit SHA
|
|
685
|
-
- `VIZZLY_COMMIT_MESSAGE` - Override detected commit message
|
|
686
|
-
- `VIZZLY_BRANCH` - Override detected branch name
|
|
687
|
-
- `VIZZLY_PR_NUMBER` - Override detected pull request number
|
|
684
|
+
**Token Priority:** CLI flag → env var → project mapping → user access token
|
|
688
685
|
|
|
689
|
-
|
|
690
|
-
- `VIZZLY_SERVER_URL` - Screenshot server URL (set by CLI)
|
|
691
|
-
- `VIZZLY_ENABLED` - Enable/disable client (set by CLI)
|
|
692
|
-
- `VIZZLY_BUILD_ID` - Current build ID (set by CLI)
|
|
693
|
-
- `VIZZLY_TDD_MODE` - TDD mode active (set by CLI)
|
|
686
|
+
#### Build & Execution
|
|
694
687
|
|
|
695
|
-
|
|
688
|
+
| Variable | Description | Default |
|
|
689
|
+
|----------|-------------|---------|
|
|
690
|
+
| `VIZZLY_PARALLEL_ID` | Unique identifier for parallel test shards | - |
|
|
691
|
+
| `VIZZLY_BUILD_ID` | Build identifier for grouping screenshots | Auto-generated |
|
|
692
|
+
|
|
693
|
+
#### Git Information Overrides
|
|
694
|
+
|
|
695
|
+
Use these in CI/CD to override auto-detected git metadata:
|
|
696
|
+
|
|
697
|
+
| Variable | Description |
|
|
698
|
+
|----------|-------------|
|
|
699
|
+
| `VIZZLY_COMMIT_SHA` | Commit SHA |
|
|
700
|
+
| `VIZZLY_COMMIT_MESSAGE` | Commit message |
|
|
701
|
+
| `VIZZLY_BRANCH` | Branch name |
|
|
702
|
+
| `VIZZLY_PR_NUMBER` | Pull request number |
|
|
703
|
+
| `VIZZLY_PR_BASE_REF` | PR base branch name |
|
|
704
|
+
| `VIZZLY_PR_BASE_SHA` | PR base commit SHA |
|
|
705
|
+
| `VIZZLY_PR_HEAD_REF` | PR head branch name |
|
|
706
|
+
| `VIZZLY_PR_HEAD_SHA` | PR head commit SHA |
|
|
707
|
+
|
|
708
|
+
**Priority Order:**
|
|
696
709
|
1. CLI arguments (`--commit`, `--branch`, `--message`)
|
|
697
710
|
2. `VIZZLY_*` environment variables
|
|
698
|
-
3. CI-specific
|
|
711
|
+
3. CI-specific variables (e.g., `GITHUB_SHA`, `CI_COMMIT_SHA`)
|
|
699
712
|
4. Git command detection
|
|
700
713
|
|
|
714
|
+
#### Runtime (Set by CLI)
|
|
715
|
+
|
|
716
|
+
These are set automatically when running `vizzly run` or `vizzly tdd`:
|
|
717
|
+
|
|
718
|
+
| Variable | Description |
|
|
719
|
+
|----------|-------------|
|
|
720
|
+
| `VIZZLY_ENABLED` | `true` when Vizzly capture is active |
|
|
721
|
+
| `VIZZLY_SERVER_URL` | Local screenshot server URL |
|
|
722
|
+
| `VIZZLY_TDD` | `true` when TDD mode is active |
|
|
723
|
+
|
|
724
|
+
#### Advanced
|
|
725
|
+
|
|
726
|
+
| Variable | Description |
|
|
727
|
+
|----------|-------------|
|
|
728
|
+
| `VIZZLY_USER_AGENT` | Custom User-Agent string for API requests |
|
|
729
|
+
|
|
701
730
|
## Error Handling
|
|
702
731
|
|
|
703
732
|
### Client Errors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Visual review platform for UI developers and designers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"visual-testing",
|
|
@@ -71,10 +71,12 @@
|
|
|
71
71
|
"test:watch": "vitest",
|
|
72
72
|
"test:reporter": "playwright test --config=tests/reporter/playwright.config.js",
|
|
73
73
|
"test:reporter:visual": "node bin/vizzly.js run \"npm run test:reporter\"",
|
|
74
|
-
"lint": "
|
|
75
|
-
"lint:fix": "
|
|
76
|
-
"format": "
|
|
77
|
-
"format:check": "
|
|
74
|
+
"lint": "biome lint src tests",
|
|
75
|
+
"lint:fix": "biome lint --write src tests",
|
|
76
|
+
"format": "biome format --write src tests",
|
|
77
|
+
"format:check": "biome format src tests",
|
|
78
|
+
"check": "biome check src tests",
|
|
79
|
+
"check:fix": "biome check --write src tests"
|
|
78
80
|
},
|
|
79
81
|
"engines": {
|
|
80
82
|
"node": ">=22.0.0"
|
|
@@ -84,7 +86,7 @@
|
|
|
84
86
|
"registry": "https://registry.npmjs.org/"
|
|
85
87
|
},
|
|
86
88
|
"dependencies": {
|
|
87
|
-
"@vizzly-testing/honeydiff": "^0.
|
|
89
|
+
"@vizzly-testing/honeydiff": "^0.6.0",
|
|
88
90
|
"commander": "^14.0.0",
|
|
89
91
|
"cosmiconfig": "^9.0.0",
|
|
90
92
|
"dotenv": "^17.2.1",
|
|
@@ -108,7 +110,7 @@
|
|
|
108
110
|
"@babel/preset-env": "^7.23.6",
|
|
109
111
|
"@babel/preset-react": "^7.27.1",
|
|
110
112
|
"@babel/preset-typescript": "^7.23.6",
|
|
111
|
-
"@
|
|
113
|
+
"@biomejs/biome": "^2.3.8",
|
|
112
114
|
"@heroicons/react": "^2.2.0",
|
|
113
115
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
114
116
|
"@playwright/test": "^1.55.1",
|
|
@@ -119,13 +121,7 @@
|
|
|
119
121
|
"@vitest/coverage-v8": "^4.0.3",
|
|
120
122
|
"autoprefixer": "^10.4.21",
|
|
121
123
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
|
122
|
-
"eslint": "^9.31.0",
|
|
123
|
-
"eslint-config-prettier": "^10.1.8",
|
|
124
|
-
"eslint-plugin-prettier": "^5.5.3",
|
|
125
|
-
"eslint-plugin-react": "^7.37.5",
|
|
126
|
-
"eslint-plugin-react-hooks": "^7.0.0",
|
|
127
124
|
"postcss": "^8.5.6",
|
|
128
|
-
"prettier": "^3.6.2",
|
|
129
125
|
"react": "^19.1.1",
|
|
130
126
|
"react-dom": "^19.1.1",
|
|
131
127
|
"rimraf": "^6.0.1",
|