@vizzly-testing/cli 0.13.4 → 0.15.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 +68 -68
- package/dist/commands/doctor.js +30 -34
- package/dist/commands/finalize.js +24 -23
- package/dist/commands/init.js +30 -28
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +14 -19
- package/dist/commands/project.js +83 -103
- package/dist/commands/run.js +77 -89
- package/dist/commands/status.js +48 -49
- package/dist/commands/tdd-daemon.js +90 -86
- package/dist/commands/tdd.js +59 -88
- package/dist/commands/upload.js +57 -57
- package/dist/commands/whoami.js +40 -45
- package/dist/index.js +2 -5
- package/dist/plugin-loader.js +15 -17
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +78 -32
- package/dist/sdk/index.js +36 -45
- package/dist/server/handlers/api-handler.js +14 -15
- package/dist/server/handlers/tdd-handler.js +34 -37
- package/dist/server/http-server.js +75 -869
- package/dist/server/middleware/cors.js +22 -0
- package/dist/server/middleware/json-parser.js +35 -0
- package/dist/server/middleware/response.js +79 -0
- package/dist/server/routers/assets.js +91 -0
- package/dist/server/routers/auth.js +144 -0
- package/dist/server/routers/baseline.js +163 -0
- package/dist/server/routers/cloud-proxy.js +146 -0
- package/dist/server/routers/config.js +126 -0
- package/dist/server/routers/dashboard.js +130 -0
- package/dist/server/routers/health.js +61 -0
- package/dist/server/routers/projects.js +168 -0
- package/dist/server/routers/screenshot.js +86 -0
- package/dist/services/auth-service.js +1 -1
- package/dist/services/build-manager.js +13 -40
- package/dist/services/config-service.js +2 -4
- package/dist/services/html-report-generator.js +6 -5
- package/dist/services/index.js +64 -0
- package/dist/services/project-service.js +121 -40
- package/dist/services/screenshot-server.js +9 -9
- package/dist/services/server-manager.js +11 -18
- package/dist/services/static-report-generator.js +3 -4
- package/dist/services/tdd-service.js +246 -103
- package/dist/services/test-runner.js +24 -25
- package/dist/services/uploader.js +5 -4
- package/dist/types/commands/init.d.ts +1 -2
- package/dist/types/index.d.ts +2 -3
- package/dist/types/plugin-loader.d.ts +1 -2
- package/dist/types/reporter/src/api/client.d.ts +178 -0
- package/dist/types/reporter/src/components/app-router.d.ts +1 -3
- package/dist/types/reporter/src/components/code-block.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +10 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +11 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +14 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +30 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +8 -0
- package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/fullscreen-viewer.d.ts +13 -0
- package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
- package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +9 -0
- package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +1 -1
- package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
- package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
- package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
- package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
- package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
- package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
- package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
- package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
- package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
- package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
- package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
- package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
- package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
- package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
- package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
- package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
- package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +5 -0
- package/dist/types/reporter/src/components/views/comparisons-view.d.ts +5 -6
- package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
- package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
- package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
- package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
- package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
- package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
- package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
- package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
- package/dist/types/sdk/index.d.ts +2 -4
- package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
- package/dist/types/server/http-server.d.ts +1 -1
- package/dist/types/server/middleware/cors.d.ts +11 -0
- package/dist/types/server/middleware/json-parser.d.ts +10 -0
- package/dist/types/server/middleware/response.d.ts +50 -0
- package/dist/types/server/routers/assets.d.ts +6 -0
- package/dist/types/server/routers/auth.d.ts +9 -0
- package/dist/types/server/routers/baseline.d.ts +13 -0
- package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
- package/dist/types/server/routers/config.d.ts +9 -0
- package/dist/types/server/routers/dashboard.d.ts +6 -0
- package/dist/types/server/routers/health.d.ts +11 -0
- package/dist/types/server/routers/projects.d.ts +9 -0
- package/dist/types/server/routers/screenshot.d.ts +11 -0
- package/dist/types/services/build-manager.d.ts +4 -3
- package/dist/types/services/config-service.d.ts +2 -3
- package/dist/types/services/index.d.ts +7 -0
- package/dist/types/services/project-service.d.ts +6 -4
- package/dist/types/services/screenshot-server.d.ts +5 -5
- package/dist/types/services/server-manager.d.ts +5 -3
- package/dist/types/services/tdd-service.d.ts +12 -1
- package/dist/types/services/test-runner.d.ts +3 -3
- package/dist/types/utils/output.d.ts +84 -0
- package/dist/utils/config-loader.js +24 -48
- package/dist/utils/global-config.js +2 -17
- package/dist/utils/output.js +445 -0
- package/dist/utils/security.js +3 -4
- package/docs/api-reference.md +0 -1
- package/docs/plugins.md +22 -22
- package/package.json +3 -2
- package/dist/container/index.js +0 -215
- package/dist/services/base-service.js +0 -154
- package/dist/types/container/index.d.ts +0 -59
- package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
- package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
- package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
- package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
- package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
- package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
- package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
- package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
- package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
- package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
- package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
- package/dist/types/services/base-service.d.ts +0 -71
- package/dist/types/utils/console-ui.d.ts +0 -61
- package/dist/types/utils/logger-factory.d.ts +0 -26
- package/dist/types/utils/logger.d.ts +0 -79
- package/dist/utils/console-ui.js +0 -241
- package/dist/utils/logger-factory.js +0 -76
- package/dist/utils/logger.js +0 -231
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified CLI output module
|
|
3
|
+
*
|
|
4
|
+
* Handles all console output with proper stream separation:
|
|
5
|
+
* - stdout: program output only (things you can pipe)
|
|
6
|
+
* - stderr: everything else (spinners, progress, errors, debug)
|
|
7
|
+
*
|
|
8
|
+
* Replaces both ConsoleUI and Logger with a single, simple API.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createColors } from './colors.js';
|
|
12
|
+
import { writeFileSync, appendFileSync, mkdirSync } from 'fs';
|
|
13
|
+
import { dirname } from 'path';
|
|
14
|
+
|
|
15
|
+
// Module state
|
|
16
|
+
let config = {
|
|
17
|
+
json: false,
|
|
18
|
+
verbose: false,
|
|
19
|
+
color: true,
|
|
20
|
+
silent: false,
|
|
21
|
+
logFile: null
|
|
22
|
+
};
|
|
23
|
+
let colors = createColors({
|
|
24
|
+
useColor: config.color
|
|
25
|
+
});
|
|
26
|
+
let spinnerInterval = null;
|
|
27
|
+
let spinnerMessage = '';
|
|
28
|
+
let lastSpinnerLine = '';
|
|
29
|
+
let startTime = Date.now();
|
|
30
|
+
|
|
31
|
+
// Track if we've shown the header
|
|
32
|
+
let headerShown = false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Configure output settings
|
|
36
|
+
* Call this once at CLI startup with global options
|
|
37
|
+
*/
|
|
38
|
+
export function configure(options = {}) {
|
|
39
|
+
if (options.json !== undefined) config.json = options.json;
|
|
40
|
+
if (options.verbose !== undefined) config.verbose = options.verbose;
|
|
41
|
+
if (options.color !== undefined) config.color = options.color;
|
|
42
|
+
if (options.silent !== undefined) config.silent = options.silent;
|
|
43
|
+
if (options.logFile !== undefined) config.logFile = options.logFile;
|
|
44
|
+
colors = createColors({
|
|
45
|
+
useColor: config.color
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Reset state
|
|
49
|
+
startTime = Date.now();
|
|
50
|
+
headerShown = false;
|
|
51
|
+
|
|
52
|
+
// Initialize log file if specified
|
|
53
|
+
if (config.logFile) {
|
|
54
|
+
initLogFile();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Show command header (e.g., "vizzly · tdd · local")
|
|
60
|
+
* Only shows once per command execution
|
|
61
|
+
*/
|
|
62
|
+
export function header(command, mode = null) {
|
|
63
|
+
if (config.json || config.silent || headerShown) return;
|
|
64
|
+
headerShown = true;
|
|
65
|
+
let parts = ['vizzly', command];
|
|
66
|
+
if (mode) parts.push(mode);
|
|
67
|
+
console.error('');
|
|
68
|
+
console.error(colors.dim(parts.join(' · ')));
|
|
69
|
+
console.error('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get current colors instance (for custom formatting)
|
|
74
|
+
*/
|
|
75
|
+
export function getColors() {
|
|
76
|
+
return colors;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// User-facing output (what the user asked for)
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Show a success message
|
|
85
|
+
*/
|
|
86
|
+
export function success(message, data = {}) {
|
|
87
|
+
stopSpinner();
|
|
88
|
+
if (config.silent) return;
|
|
89
|
+
if (config.json) {
|
|
90
|
+
console.log(JSON.stringify({
|
|
91
|
+
status: 'success',
|
|
92
|
+
message,
|
|
93
|
+
...data
|
|
94
|
+
}));
|
|
95
|
+
} else {
|
|
96
|
+
console.error('');
|
|
97
|
+
console.error(colors.green('✓'), message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Show final result summary (e.g., "✓ 5 screenshots · 234ms")
|
|
103
|
+
*/
|
|
104
|
+
export function result(message) {
|
|
105
|
+
stopSpinner();
|
|
106
|
+
if (config.silent) return;
|
|
107
|
+
let elapsed = getElapsedTime();
|
|
108
|
+
if (config.json) {
|
|
109
|
+
console.log(JSON.stringify({
|
|
110
|
+
status: 'complete',
|
|
111
|
+
message,
|
|
112
|
+
elapsed
|
|
113
|
+
}));
|
|
114
|
+
} else {
|
|
115
|
+
console.error('');
|
|
116
|
+
console.error(colors.green('✓'), `${message} ${colors.dim(`· ${elapsed}`)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Show an info message
|
|
122
|
+
*/
|
|
123
|
+
export function info(message, data = {}) {
|
|
124
|
+
if (config.silent) return;
|
|
125
|
+
if (config.json) {
|
|
126
|
+
console.log(JSON.stringify({
|
|
127
|
+
status: 'info',
|
|
128
|
+
message,
|
|
129
|
+
...data
|
|
130
|
+
}));
|
|
131
|
+
} else {
|
|
132
|
+
console.log(colors.cyan('ℹ'), message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Show a warning message (goes to stderr)
|
|
138
|
+
*/
|
|
139
|
+
export function warn(message, data = {}) {
|
|
140
|
+
stopSpinner();
|
|
141
|
+
if (config.silent) return;
|
|
142
|
+
if (config.json) {
|
|
143
|
+
console.error(JSON.stringify({
|
|
144
|
+
status: 'warning',
|
|
145
|
+
message,
|
|
146
|
+
...data
|
|
147
|
+
}));
|
|
148
|
+
} else {
|
|
149
|
+
console.error(colors.yellow('⚠'), message);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Show an error message (goes to stderr)
|
|
155
|
+
* Does NOT exit - caller decides whether to exit
|
|
156
|
+
*/
|
|
157
|
+
export function error(message, err = null, data = {}) {
|
|
158
|
+
stopSpinner();
|
|
159
|
+
if (config.json) {
|
|
160
|
+
let errorData = {
|
|
161
|
+
status: 'error',
|
|
162
|
+
message,
|
|
163
|
+
...data
|
|
164
|
+
};
|
|
165
|
+
if (err instanceof Error) {
|
|
166
|
+
errorData.error = {
|
|
167
|
+
name: err.name,
|
|
168
|
+
message: err.getUserMessage ? err.getUserMessage() : err.message,
|
|
169
|
+
code: err.code
|
|
170
|
+
};
|
|
171
|
+
if (config.verbose) {
|
|
172
|
+
errorData.error.stack = err.stack;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
console.error(JSON.stringify(errorData));
|
|
176
|
+
} else {
|
|
177
|
+
console.error(colors.red('✖'), message);
|
|
178
|
+
|
|
179
|
+
// Show error details
|
|
180
|
+
if (err instanceof Error) {
|
|
181
|
+
let errMessage = err.getUserMessage ? err.getUserMessage() : err.message;
|
|
182
|
+
if (errMessage && errMessage !== message) {
|
|
183
|
+
console.error(colors.dim(errMessage));
|
|
184
|
+
}
|
|
185
|
+
if (config.verbose && err.stack) {
|
|
186
|
+
console.error(colors.dim(err.stack));
|
|
187
|
+
}
|
|
188
|
+
} else if (typeof err === 'string' && err) {
|
|
189
|
+
console.error(colors.dim(err));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Write to log file
|
|
194
|
+
writeLog('error', message, {
|
|
195
|
+
error: err?.message,
|
|
196
|
+
...data
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Print a blank line for spacing
|
|
202
|
+
*/
|
|
203
|
+
export function blank() {
|
|
204
|
+
if (!config.json && !config.silent) {
|
|
205
|
+
console.log('');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Print raw text without any formatting
|
|
211
|
+
*/
|
|
212
|
+
export function print(text) {
|
|
213
|
+
if (!config.silent) {
|
|
214
|
+
console.log(text);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Print raw text to stderr
|
|
220
|
+
*/
|
|
221
|
+
export function printErr(text) {
|
|
222
|
+
if (!config.silent) {
|
|
223
|
+
console.error(text);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Output structured data
|
|
229
|
+
*/
|
|
230
|
+
export function data(obj) {
|
|
231
|
+
if (config.json) {
|
|
232
|
+
console.log(JSON.stringify({
|
|
233
|
+
status: 'data',
|
|
234
|
+
data: obj
|
|
235
|
+
}));
|
|
236
|
+
} else {
|
|
237
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Spinner / Progress (stderr so it doesn't pollute piped output)
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Start a spinner with message
|
|
247
|
+
*/
|
|
248
|
+
export function startSpinner(message) {
|
|
249
|
+
if (config.json || config.silent || !process.stderr.isTTY) return;
|
|
250
|
+
stopSpinner();
|
|
251
|
+
spinnerMessage = message;
|
|
252
|
+
let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
253
|
+
let i = 0;
|
|
254
|
+
spinnerInterval = setInterval(() => {
|
|
255
|
+
let frame = frames[i++ % frames.length];
|
|
256
|
+
let line = `${colors.cyan(frame)} ${spinnerMessage}`;
|
|
257
|
+
|
|
258
|
+
// Clear previous line and write new one
|
|
259
|
+
process.stderr.write('\r' + ' '.repeat(lastSpinnerLine.length) + '\r');
|
|
260
|
+
process.stderr.write(line);
|
|
261
|
+
lastSpinnerLine = line;
|
|
262
|
+
}, 80);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Update spinner message
|
|
267
|
+
*/
|
|
268
|
+
export function updateSpinner(message, current = 0, total = 0) {
|
|
269
|
+
if (config.json || config.silent || !process.stderr.isTTY) return;
|
|
270
|
+
let progressText = total > 0 ? ` (${current}/${total})` : '';
|
|
271
|
+
spinnerMessage = `${message}${progressText}`;
|
|
272
|
+
if (!spinnerInterval) {
|
|
273
|
+
startSpinner(spinnerMessage);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Stop the spinner
|
|
279
|
+
*/
|
|
280
|
+
export function stopSpinner() {
|
|
281
|
+
if (spinnerInterval) {
|
|
282
|
+
clearInterval(spinnerInterval);
|
|
283
|
+
spinnerInterval = null;
|
|
284
|
+
|
|
285
|
+
// Clear the spinner line
|
|
286
|
+
if (process.stderr.isTTY) {
|
|
287
|
+
process.stderr.write('\r' + ' '.repeat(lastSpinnerLine.length) + '\r');
|
|
288
|
+
}
|
|
289
|
+
lastSpinnerLine = '';
|
|
290
|
+
spinnerMessage = '';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Show progress update
|
|
296
|
+
*/
|
|
297
|
+
export function progress(message, current = 0, total = 0) {
|
|
298
|
+
if (config.silent) return;
|
|
299
|
+
if (config.json) {
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
status: 'progress',
|
|
302
|
+
message,
|
|
303
|
+
progress: {
|
|
304
|
+
current,
|
|
305
|
+
total
|
|
306
|
+
}
|
|
307
|
+
}));
|
|
308
|
+
} else {
|
|
309
|
+
updateSpinner(message, current, total);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Debug logging (only when verbose, goes to stderr and/or file)
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Format elapsed time since CLI start
|
|
319
|
+
*/
|
|
320
|
+
function getElapsedTime() {
|
|
321
|
+
let elapsed = Date.now() - startTime;
|
|
322
|
+
if (elapsed < 1000) {
|
|
323
|
+
return `${elapsed}ms`;
|
|
324
|
+
}
|
|
325
|
+
return `${(elapsed / 1000).toFixed(1)}s`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Format a data object for human-readable output
|
|
330
|
+
* Only shows meaningful values, skips nulls/undefined/empty
|
|
331
|
+
*/
|
|
332
|
+
function formatData(data) {
|
|
333
|
+
if (!data || typeof data !== 'object') return '';
|
|
334
|
+
let entries = Object.entries(data).filter(([, v]) => {
|
|
335
|
+
if (v === null || v === undefined) return false;
|
|
336
|
+
if (typeof v === 'string' && v === '') return false;
|
|
337
|
+
if (Array.isArray(v) && v.length === 0) return false;
|
|
338
|
+
return true;
|
|
339
|
+
});
|
|
340
|
+
if (entries.length === 0) return '';
|
|
341
|
+
|
|
342
|
+
// For simple key-value pairs, show inline
|
|
343
|
+
if (entries.length <= 4 && entries.every(([, v]) => typeof v !== 'object')) {
|
|
344
|
+
return entries.map(([k, v]) => `${k}=${v}`).join(' ');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// For complex objects, show on multiple lines
|
|
348
|
+
return entries.map(([k, v]) => {
|
|
349
|
+
if (typeof v === 'object') {
|
|
350
|
+
return `${k}: ${JSON.stringify(v)}`;
|
|
351
|
+
}
|
|
352
|
+
return `${k}: ${v}`;
|
|
353
|
+
}).join('\n');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Log debug message with component prefix (only shown in verbose mode)
|
|
358
|
+
*
|
|
359
|
+
* @param {string} component - Component name (e.g., 'server', 'config', 'build')
|
|
360
|
+
* @param {string} message - Debug message
|
|
361
|
+
* @param {Object} data - Optional data object to display inline
|
|
362
|
+
*/
|
|
363
|
+
export function debug(component, message, data = {}) {
|
|
364
|
+
if (!config.verbose) return;
|
|
365
|
+
|
|
366
|
+
// Handle legacy calls: debug('message') or debug('message', {data})
|
|
367
|
+
if (typeof message === 'object' || message === undefined) {
|
|
368
|
+
data = message || {};
|
|
369
|
+
message = component;
|
|
370
|
+
component = null;
|
|
371
|
+
}
|
|
372
|
+
let elapsed = getElapsedTime();
|
|
373
|
+
if (config.json) {
|
|
374
|
+
console.error(JSON.stringify({
|
|
375
|
+
status: 'debug',
|
|
376
|
+
time: elapsed,
|
|
377
|
+
component,
|
|
378
|
+
message,
|
|
379
|
+
...data
|
|
380
|
+
}));
|
|
381
|
+
} else {
|
|
382
|
+
let formattedData = formatData(data);
|
|
383
|
+
let dataStr = formattedData ? ` ${colors.dim(formattedData)}` : '';
|
|
384
|
+
if (component) {
|
|
385
|
+
// Component-based format: " server listening on :47392"
|
|
386
|
+
let paddedComponent = component.padEnd(8);
|
|
387
|
+
console.error(` ${colors.cyan(paddedComponent)} ${message}${dataStr}`);
|
|
388
|
+
} else {
|
|
389
|
+
// Simple format for legacy calls
|
|
390
|
+
console.error(` ${colors.dim('•')} ${colors.dim(message)}${dataStr}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
writeLog('debug', message, {
|
|
394
|
+
component,
|
|
395
|
+
...data
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================================
|
|
400
|
+
// Log file support
|
|
401
|
+
// ============================================================================
|
|
402
|
+
|
|
403
|
+
function initLogFile() {
|
|
404
|
+
if (!config.logFile) return;
|
|
405
|
+
try {
|
|
406
|
+
mkdirSync(dirname(config.logFile), {
|
|
407
|
+
recursive: true
|
|
408
|
+
});
|
|
409
|
+
let header = {
|
|
410
|
+
timestamp: new Date().toISOString(),
|
|
411
|
+
session_start: true,
|
|
412
|
+
pid: process.pid,
|
|
413
|
+
node_version: process.version,
|
|
414
|
+
platform: process.platform
|
|
415
|
+
};
|
|
416
|
+
writeFileSync(config.logFile, JSON.stringify(header) + '\n');
|
|
417
|
+
} catch {
|
|
418
|
+
// Silently fail - don't crash CLI for logging issues
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function writeLog(level, message, data = {}) {
|
|
422
|
+
if (!config.logFile) return;
|
|
423
|
+
try {
|
|
424
|
+
let entry = {
|
|
425
|
+
timestamp: new Date().toISOString(),
|
|
426
|
+
level,
|
|
427
|
+
message,
|
|
428
|
+
...data
|
|
429
|
+
};
|
|
430
|
+
appendFileSync(config.logFile, JSON.stringify(entry) + '\n');
|
|
431
|
+
} catch {
|
|
432
|
+
// Silently fail
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ============================================================================
|
|
437
|
+
// Cleanup
|
|
438
|
+
// ============================================================================
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Clean up (stop spinner, flush logs)
|
|
442
|
+
*/
|
|
443
|
+
export function cleanup() {
|
|
444
|
+
stopSpinner();
|
|
445
|
+
}
|
package/dist/utils/security.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolve, normalize, isAbsolute, join } from 'path';
|
|
7
|
-
import
|
|
8
|
-
const logger = createServiceLogger('SECURITY');
|
|
7
|
+
import * as output from './output.js';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
|
|
@@ -75,7 +74,7 @@ export function validatePathSecurity(targetPath, workingDir) {
|
|
|
75
74
|
|
|
76
75
|
// Ensure the target path starts with the working directory
|
|
77
76
|
if (!resolvedTargetPath.startsWith(resolvedWorkingDir)) {
|
|
78
|
-
|
|
77
|
+
output.warn(`Path traversal attempt blocked: ${targetPath} (resolved: ${resolvedTargetPath}) is outside working directory: ${resolvedWorkingDir}`);
|
|
79
78
|
throw new Error('Path is outside the allowed working directory');
|
|
80
79
|
}
|
|
81
80
|
return resolvedTargetPath;
|
|
@@ -128,7 +127,7 @@ export function validateScreenshotProperties(properties = {}) {
|
|
|
128
127
|
validated.browser = sanitizeScreenshotName(browserName, 50);
|
|
129
128
|
} catch (error) {
|
|
130
129
|
// Skip invalid browser names, don't include them
|
|
131
|
-
|
|
130
|
+
output.warn(`Invalid browser name '${properties.browser}': ${error.message}`);
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
if (properties.viewport && typeof properties.viewport === 'object') {
|
package/docs/api-reference.md
CHANGED
|
@@ -676,7 +676,6 @@ Configuration loaded via cosmiconfig in this order:
|
|
|
676
676
|
|
|
677
677
|
**Core Configuration:**
|
|
678
678
|
- `VIZZLY_API_URL` - API base URL override (default: `https://app.vizzly.dev`)
|
|
679
|
-
- `VIZZLY_LOG_LEVEL` - Logger level (`debug`, `info`, `warn`, `error`)
|
|
680
679
|
|
|
681
680
|
**Parallel Builds:**
|
|
682
681
|
- `VIZZLY_PARALLEL_ID` - Unique identifier for parallel test execution
|
package/docs/plugins.md
CHANGED
|
@@ -13,7 +13,7 @@ file.
|
|
|
13
13
|
## Benefits
|
|
14
14
|
|
|
15
15
|
- **Zero Configuration** - Just `npm install` and the plugin is available
|
|
16
|
-
- **Shared Infrastructure** - Plugins get access to config,
|
|
16
|
+
- **Shared Infrastructure** - Plugins get access to config, output utilities, and services
|
|
17
17
|
- **Independent Releases** - Plugins can iterate without requiring CLI updates
|
|
18
18
|
- **Smaller Core** - Keep the main CLI lean by moving optional features to plugins
|
|
19
19
|
- **Community Extensible** - Anyone can build and share plugins
|
|
@@ -95,14 +95,14 @@ export default {
|
|
|
95
95
|
name: 'my-plugin',
|
|
96
96
|
version: '1.0.0', // Optional but recommended
|
|
97
97
|
|
|
98
|
-
register(program, { config,
|
|
98
|
+
register(program, { config, output, services }) {
|
|
99
99
|
// Register your command with Commander.js
|
|
100
100
|
program
|
|
101
101
|
.command('my-command <arg>')
|
|
102
102
|
.description('Description of my command')
|
|
103
103
|
.option('--option <value>', 'An option')
|
|
104
104
|
.action(async (arg, options) => {
|
|
105
|
-
|
|
105
|
+
output.info(`Running my-command with ${arg}`);
|
|
106
106
|
|
|
107
107
|
// Access shared services if needed
|
|
108
108
|
let apiService = await services.get('apiService');
|
|
@@ -131,14 +131,13 @@ The `register` function receives two arguments:
|
|
|
131
131
|
1. **`program`** - [Commander.js](https://github.com/tj/commander.js) program instance for registering commands
|
|
132
132
|
2. **`context`** - Object containing:
|
|
133
133
|
- `config` - Merged Vizzly configuration object
|
|
134
|
-
- `
|
|
134
|
+
- `output` - Unified output module with `.debug()`, `.info()`, `.warn()`, `.error()`, `.success()` methods
|
|
135
135
|
- `services` - Service container with access to internal Vizzly services
|
|
136
136
|
|
|
137
137
|
### Available Services
|
|
138
138
|
|
|
139
139
|
Plugins can access these services from the container:
|
|
140
140
|
|
|
141
|
-
- **`logger`** - Component logger for consistent output
|
|
142
141
|
- **`apiService`** - Vizzly API client for interacting with the platform
|
|
143
142
|
- **`uploader`** - Screenshot upload service
|
|
144
143
|
- **`buildManager`** - Build lifecycle management
|
|
@@ -149,7 +148,7 @@ Plugins can access these services from the container:
|
|
|
149
148
|
Example accessing a service:
|
|
150
149
|
|
|
151
150
|
```javascript
|
|
152
|
-
register(program, { config,
|
|
151
|
+
register(program, { config, output, services }) {
|
|
153
152
|
program
|
|
154
153
|
.command('upload-screenshots <dir>')
|
|
155
154
|
.action(async (dir) => {
|
|
@@ -177,12 +176,12 @@ touch plugins/my-plugin.js
|
|
|
177
176
|
export default {
|
|
178
177
|
name: 'my-plugin',
|
|
179
178
|
version: '1.0.0',
|
|
180
|
-
register(program, { config,
|
|
179
|
+
register(program, { config, output }) {
|
|
181
180
|
program
|
|
182
181
|
.command('greet <name>')
|
|
183
182
|
.description('Greet someone')
|
|
184
183
|
.action((name) => {
|
|
185
|
-
|
|
184
|
+
output.info(`Hello, ${name}!`);
|
|
186
185
|
});
|
|
187
186
|
}
|
|
188
187
|
};
|
|
@@ -255,33 +254,34 @@ export default {
|
|
|
255
254
|
Always handle errors gracefully and provide helpful error messages:
|
|
256
255
|
|
|
257
256
|
```javascript
|
|
258
|
-
register(program, {
|
|
257
|
+
register(program, { output }) {
|
|
259
258
|
program
|
|
260
259
|
.command('process <file>')
|
|
261
260
|
.action(async (file) => {
|
|
262
261
|
try {
|
|
263
262
|
if (!existsSync(file)) {
|
|
264
|
-
|
|
263
|
+
output.error(`File not found: ${file}`);
|
|
265
264
|
process.exit(1);
|
|
266
265
|
}
|
|
267
266
|
// Process file...
|
|
268
267
|
} catch (error) {
|
|
269
|
-
|
|
268
|
+
output.error(`Failed to process file: ${error.message}`);
|
|
270
269
|
process.exit(1);
|
|
271
270
|
}
|
|
272
271
|
});
|
|
273
272
|
}
|
|
274
273
|
```
|
|
275
274
|
|
|
276
|
-
###
|
|
275
|
+
### Output
|
|
277
276
|
|
|
278
|
-
Use the provided
|
|
277
|
+
Use the provided output module for consistent CLI output:
|
|
279
278
|
|
|
280
279
|
```javascript
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
output.debug('Detailed debug info'); // Only shown with --verbose (stderr)
|
|
281
|
+
output.info('Normal information'); // Info messages (stdout)
|
|
282
|
+
output.success('Completed!'); // Success messages (stdout)
|
|
283
|
+
output.warn('Warning message'); // Warning messages (stderr)
|
|
284
|
+
output.error('Error message'); // Error messages (stderr)
|
|
285
285
|
```
|
|
286
286
|
|
|
287
287
|
### Async Operations
|
|
@@ -292,7 +292,7 @@ Use async/await for asynchronous operations:
|
|
|
292
292
|
.action(async (options) => {
|
|
293
293
|
let service = await services.get('apiService');
|
|
294
294
|
let result = await service.doSomething();
|
|
295
|
-
|
|
295
|
+
output.info(`Result: ${result}`);
|
|
296
296
|
});
|
|
297
297
|
```
|
|
298
298
|
|
|
@@ -318,13 +318,13 @@ Validate user input and provide helpful error messages:
|
|
|
318
318
|
```javascript
|
|
319
319
|
.action(async (path, options) => {
|
|
320
320
|
if (!path) {
|
|
321
|
-
|
|
321
|
+
output.error('Path is required');
|
|
322
322
|
process.exit(1);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
if (!existsSync(path)) {
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
output.error(`Path not found: ${path}`);
|
|
327
|
+
output.info('Please provide a valid path to your build directory');
|
|
328
328
|
process.exit(1);
|
|
329
329
|
}
|
|
330
330
|
|
|
@@ -337,7 +337,7 @@ Validate user input and provide helpful error messages:
|
|
|
337
337
|
Import heavy dependencies only when needed to keep CLI startup fast:
|
|
338
338
|
|
|
339
339
|
```javascript
|
|
340
|
-
register(program, {
|
|
340
|
+
register(program, { output }) {
|
|
341
341
|
program
|
|
342
342
|
.command('process-images <dir>')
|
|
343
343
|
.action(async (dir) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Visual review platform for UI developers and designers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"visual-testing",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"cosmiconfig": "^9.0.0",
|
|
85
85
|
"dotenv": "^17.2.1",
|
|
86
86
|
"form-data": "^4.0.0",
|
|
87
|
-
"glob": "^
|
|
87
|
+
"glob": "^13.0.0",
|
|
88
88
|
"zod": "^4.1.12"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
99
99
|
"@playwright/test": "^1.55.1",
|
|
100
100
|
"@tailwindcss/postcss": "^4.1.13",
|
|
101
|
+
"@tanstack/react-query": "^5.90.11",
|
|
101
102
|
"@vitejs/plugin-react": "^5.0.3",
|
|
102
103
|
"@vitest/coverage-v8": "^4.0.3",
|
|
103
104
|
"autoprefixer": "^10.4.21",
|