@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.
Files changed (143) hide show
  1. package/dist/cli.js +68 -68
  2. package/dist/commands/doctor.js +30 -34
  3. package/dist/commands/finalize.js +24 -23
  4. package/dist/commands/init.js +30 -28
  5. package/dist/commands/login.js +49 -55
  6. package/dist/commands/logout.js +14 -19
  7. package/dist/commands/project.js +83 -103
  8. package/dist/commands/run.js +77 -89
  9. package/dist/commands/status.js +48 -49
  10. package/dist/commands/tdd-daemon.js +90 -86
  11. package/dist/commands/tdd.js +59 -88
  12. package/dist/commands/upload.js +57 -57
  13. package/dist/commands/whoami.js +40 -45
  14. package/dist/index.js +2 -5
  15. package/dist/plugin-loader.js +15 -17
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +78 -32
  18. package/dist/sdk/index.js +36 -45
  19. package/dist/server/handlers/api-handler.js +14 -15
  20. package/dist/server/handlers/tdd-handler.js +34 -37
  21. package/dist/server/http-server.js +75 -869
  22. package/dist/server/middleware/cors.js +22 -0
  23. package/dist/server/middleware/json-parser.js +35 -0
  24. package/dist/server/middleware/response.js +79 -0
  25. package/dist/server/routers/assets.js +91 -0
  26. package/dist/server/routers/auth.js +144 -0
  27. package/dist/server/routers/baseline.js +163 -0
  28. package/dist/server/routers/cloud-proxy.js +146 -0
  29. package/dist/server/routers/config.js +126 -0
  30. package/dist/server/routers/dashboard.js +130 -0
  31. package/dist/server/routers/health.js +61 -0
  32. package/dist/server/routers/projects.js +168 -0
  33. package/dist/server/routers/screenshot.js +86 -0
  34. package/dist/services/auth-service.js +1 -1
  35. package/dist/services/build-manager.js +13 -40
  36. package/dist/services/config-service.js +2 -4
  37. package/dist/services/html-report-generator.js +6 -5
  38. package/dist/services/index.js +64 -0
  39. package/dist/services/project-service.js +121 -40
  40. package/dist/services/screenshot-server.js +9 -9
  41. package/dist/services/server-manager.js +11 -18
  42. package/dist/services/static-report-generator.js +3 -4
  43. package/dist/services/tdd-service.js +246 -103
  44. package/dist/services/test-runner.js +24 -25
  45. package/dist/services/uploader.js +5 -4
  46. package/dist/types/commands/init.d.ts +1 -2
  47. package/dist/types/index.d.ts +2 -3
  48. package/dist/types/plugin-loader.d.ts +1 -2
  49. package/dist/types/reporter/src/api/client.d.ts +178 -0
  50. package/dist/types/reporter/src/components/app-router.d.ts +1 -3
  51. package/dist/types/reporter/src/components/code-block.d.ts +4 -0
  52. package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +10 -0
  53. package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +11 -0
  54. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +14 -0
  55. package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +30 -0
  56. package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +8 -0
  57. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  58. package/dist/types/reporter/src/components/comparison/fullscreen-viewer.d.ts +13 -0
  59. package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
  60. package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +9 -0
  61. package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +1 -1
  62. package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
  63. package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
  64. package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
  65. package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
  66. package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
  67. package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
  68. package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
  69. package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
  70. package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
  71. package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
  72. package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
  73. package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
  74. package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
  75. package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
  76. package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
  77. package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
  78. package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +5 -0
  79. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +5 -6
  80. package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
  81. package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
  82. package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
  83. package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
  84. package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
  85. package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
  86. package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
  87. package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
  88. package/dist/types/sdk/index.d.ts +2 -4
  89. package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
  90. package/dist/types/server/http-server.d.ts +1 -1
  91. package/dist/types/server/middleware/cors.d.ts +11 -0
  92. package/dist/types/server/middleware/json-parser.d.ts +10 -0
  93. package/dist/types/server/middleware/response.d.ts +50 -0
  94. package/dist/types/server/routers/assets.d.ts +6 -0
  95. package/dist/types/server/routers/auth.d.ts +9 -0
  96. package/dist/types/server/routers/baseline.d.ts +13 -0
  97. package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
  98. package/dist/types/server/routers/config.d.ts +9 -0
  99. package/dist/types/server/routers/dashboard.d.ts +6 -0
  100. package/dist/types/server/routers/health.d.ts +11 -0
  101. package/dist/types/server/routers/projects.d.ts +9 -0
  102. package/dist/types/server/routers/screenshot.d.ts +11 -0
  103. package/dist/types/services/build-manager.d.ts +4 -3
  104. package/dist/types/services/config-service.d.ts +2 -3
  105. package/dist/types/services/index.d.ts +7 -0
  106. package/dist/types/services/project-service.d.ts +6 -4
  107. package/dist/types/services/screenshot-server.d.ts +5 -5
  108. package/dist/types/services/server-manager.d.ts +5 -3
  109. package/dist/types/services/tdd-service.d.ts +12 -1
  110. package/dist/types/services/test-runner.d.ts +3 -3
  111. package/dist/types/utils/output.d.ts +84 -0
  112. package/dist/utils/config-loader.js +24 -48
  113. package/dist/utils/global-config.js +2 -17
  114. package/dist/utils/output.js +445 -0
  115. package/dist/utils/security.js +3 -4
  116. package/docs/api-reference.md +0 -1
  117. package/docs/plugins.md +22 -22
  118. package/package.json +3 -2
  119. package/dist/container/index.js +0 -215
  120. package/dist/services/base-service.js +0 -154
  121. package/dist/types/container/index.d.ts +0 -59
  122. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
  123. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
  124. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
  125. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
  126. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
  127. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
  128. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
  129. package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
  130. package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
  131. package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
  132. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
  133. package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
  134. package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
  135. package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
  136. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
  137. package/dist/types/services/base-service.d.ts +0 -71
  138. package/dist/types/utils/console-ui.d.ts +0 -61
  139. package/dist/types/utils/logger-factory.d.ts +0 -26
  140. package/dist/types/utils/logger.d.ts +0 -79
  141. package/dist/utils/console-ui.js +0 -241
  142. package/dist/utils/logger-factory.js +0 -76
  143. 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
+ }
@@ -4,8 +4,7 @@
4
4
  */
5
5
 
6
6
  import { resolve, normalize, isAbsolute, join } from 'path';
7
- import { createServiceLogger } from './logger-factory.js';
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
- logger.warn(`Path traversal attempt blocked: ${targetPath} (resolved: ${resolvedTargetPath}) is outside working directory: ${resolvedWorkingDir}`);
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
- logger.warn(`Invalid browser name '${properties.browser}': ${error.message}`);
130
+ output.warn(`Invalid browser name '${properties.browser}': ${error.message}`);
132
131
  }
133
132
  }
134
133
  if (properties.viewport && typeof properties.viewport === 'object') {
@@ -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, logger, and services
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, logger, services }) {
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
- logger.info(`Running my-command with ${arg}`);
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
- - `logger` - Shared logger instance with `.debug()`, `.info()`, `.warn()`, `.error()` methods
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, logger, services }) {
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, logger }) {
179
+ register(program, { config, output }) {
181
180
  program
182
181
  .command('greet <name>')
183
182
  .description('Greet someone')
184
183
  .action((name) => {
185
- logger.info(`Hello, ${name}!`);
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, { logger }) {
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
- logger.error(`File not found: ${file}`);
263
+ output.error(`File not found: ${file}`);
265
264
  process.exit(1);
266
265
  }
267
266
  // Process file...
268
267
  } catch (error) {
269
- logger.error(`Failed to process file: ${error.message}`);
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
- ### Logging
275
+ ### Output
277
276
 
278
- Use the provided logger for consistent output across all CLI commands:
277
+ Use the provided output module for consistent CLI output:
279
278
 
280
279
  ```javascript
281
- logger.debug('Detailed debug info'); // Only shown with --verbose
282
- logger.info('Normal information'); // Standard output
283
- logger.warn('Warning message'); // Warning output
284
- logger.error('Error message'); // Error output
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
- logger.info(`Result: ${result}`);
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
- logger.error('Path is required');
321
+ output.error('Path is required');
322
322
  process.exit(1);
323
323
  }
324
324
 
325
325
  if (!existsSync(path)) {
326
- logger.error(`Path not found: ${path}`);
327
- logger.info('Please provide a valid path to your build directory');
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, { logger }) {
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.13.4",
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": "^11.0.3",
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",