@vizzly-testing/cli 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/cli.js +87 -59
  2. package/dist/client/index.js +6 -6
  3. package/dist/commands/doctor.js +15 -15
  4. package/dist/commands/finalize.js +7 -7
  5. package/dist/commands/init.js +28 -28
  6. package/dist/commands/login.js +23 -23
  7. package/dist/commands/logout.js +4 -4
  8. package/dist/commands/project.js +36 -36
  9. package/dist/commands/run.js +33 -33
  10. package/dist/commands/status.js +14 -14
  11. package/dist/commands/tdd-daemon.js +43 -43
  12. package/dist/commands/tdd.js +26 -26
  13. package/dist/commands/upload.js +32 -32
  14. package/dist/commands/whoami.js +12 -12
  15. package/dist/index.js +9 -14
  16. package/dist/plugin-api.js +43 -0
  17. package/dist/plugin-loader.js +28 -28
  18. package/dist/reporter/reporter-bundle.css +1 -1
  19. package/dist/reporter/reporter-bundle.iife.js +19 -19
  20. package/dist/sdk/index.js +33 -35
  21. package/dist/server/handlers/api-handler.js +4 -4
  22. package/dist/server/handlers/tdd-handler.js +22 -21
  23. package/dist/server/http-server.js +21 -22
  24. package/dist/server/middleware/json-parser.js +1 -1
  25. package/dist/server/routers/assets.js +14 -14
  26. package/dist/server/routers/auth.js +14 -14
  27. package/dist/server/routers/baseline.js +8 -8
  28. package/dist/server/routers/cloud-proxy.js +15 -15
  29. package/dist/server/routers/config.js +11 -11
  30. package/dist/server/routers/dashboard.js +11 -11
  31. package/dist/server/routers/health.js +4 -4
  32. package/dist/server/routers/projects.js +19 -19
  33. package/dist/server/routers/screenshot.js +9 -9
  34. package/dist/services/api-service.js +16 -16
  35. package/dist/services/auth-service.js +17 -17
  36. package/dist/services/build-manager.js +3 -3
  37. package/dist/services/config-service.js +32 -32
  38. package/dist/services/html-report-generator.js +8 -8
  39. package/dist/services/index.js +11 -11
  40. package/dist/services/project-service.js +19 -19
  41. package/dist/services/report-generator/report.css +3 -3
  42. package/dist/services/report-generator/viewer.js +25 -23
  43. package/dist/services/screenshot-server.js +1 -1
  44. package/dist/services/server-manager.js +5 -5
  45. package/dist/services/static-report-generator.js +14 -14
  46. package/dist/services/tdd-service.js +152 -110
  47. package/dist/services/test-runner.js +3 -3
  48. package/dist/services/uploader.js +10 -8
  49. package/dist/types/config.d.ts +2 -1
  50. package/dist/types/index.d.ts +95 -1
  51. package/dist/types/sdk.d.ts +1 -1
  52. package/dist/utils/browser.js +3 -3
  53. package/dist/utils/build-history.js +12 -12
  54. package/dist/utils/config-loader.js +17 -17
  55. package/dist/utils/config-schema.js +6 -6
  56. package/dist/utils/environment-config.js +11 -0
  57. package/dist/utils/fetch-utils.js +2 -2
  58. package/dist/utils/file-helpers.js +2 -2
  59. package/dist/utils/git.js +3 -6
  60. package/dist/utils/global-config.js +28 -25
  61. package/dist/utils/output.js +136 -28
  62. package/dist/utils/package-info.js +3 -3
  63. package/dist/utils/security.js +12 -12
  64. package/docs/api-reference.md +52 -23
  65. package/docs/plugins.md +60 -25
  66. package/package.json +9 -13
@@ -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 { writeFileSync, appendFileSync, mkdirSync } from 'fs';
13
- import { dirname } from 'path';
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
- let config = {
29
+ const config = {
17
30
  json: false,
18
- verbose: false,
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
- startTime = Date.now();
50
- headerShown = false;
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
- let parts = ['vizzly', command];
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
- let elapsed = getElapsedTime();
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 (config.silent) return;
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 (config.silent) return;
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 (config.verbose) {
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 (config.verbose && err.stack) {
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
- let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
342
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
253
343
  let i = 0;
254
344
  spinnerInterval = setInterval(() => {
255
- let frame = frames[i++ % frames.length];
256
- let line = `${colors.cyan(frame)} ${spinnerMessage}`;
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('\r' + ' '.repeat(lastSpinnerLine.length) + '\r');
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
- let progressText = total > 0 ? ` (${current}/${total})` : '';
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('\r' + ' '.repeat(lastSpinnerLine.length) + '\r');
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
- let elapsed = Date.now() - startTime;
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
- let entries = Object.entries(data).filter(([, v]) => {
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 in verbose mode)
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 (!config.verbose) return;
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
- let header = {
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) + '\n');
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
- let entry = {
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) + '\n');
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;
@@ -3,7 +3,7 @@
3
3
  * Protects against path traversal attacks and ensures safe file operations
4
4
  */
5
5
 
6
- import { resolve, normalize, isAbsolute, join } from 'path';
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
- let allowedChars = allowSlashes ? /[^a-zA-Z0-9._/-]/g : /[^a-zA-Z0-9._-]/g;
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 = 'file_' + 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
- let resolvedWorkingDir = resolve(normalize(workingDir));
73
- let resolvedTargetPath = resolve(normalize(targetPath));
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
- let sanitizedSegments = pathSegments.map(segment => {
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
- let targetPath = join(workingDir, ...sanitizedSegments);
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
- let validated = {};
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
- let browserName = properties.browser.split('/')[0];
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
- let viewport = {};
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 (let [key, value] of Object.entries(properties)) {
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;
@@ -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 // CIEDE2000 Delta E (default: 2.0)
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
- **Authentication:**
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
- **Core Configuration:**
678
- - `VIZZLY_API_URL` - API base URL override (default: `https://app.vizzly.dev`)
675
+ #### Configuration
679
676
 
680
- **Parallel Builds:**
681
- - `VIZZLY_PARALLEL_ID` - Unique identifier for parallel test execution
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
- **Git Information Override (CI/CD Enhancement):**
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
- **Runtime (Set by CLI):**
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
- **Priority Order for Git Information:**
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 environment variables (e.g., `GITHUB_SHA`, `CI_COMMIT_SHA`)
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/docs/plugins.md CHANGED
@@ -104,8 +104,8 @@ export default {
104
104
  .action(async (arg, options) => {
105
105
  output.info(`Running my-command with ${arg}`);
106
106
 
107
- // Access shared services if needed
108
- let apiService = await services.get('apiService');
107
+ // Access shared services directly
108
+ let apiService = services.apiService;
109
109
 
110
110
  // Your command logic here
111
111
  });
@@ -134,26 +134,55 @@ The `register` function receives two arguments:
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
- ### Available Services
137
+ ### Available Services (Stable API)
138
138
 
139
- Plugins can access these services from the container:
139
+ The `services` object provides a stable API for plugins. Only these services and methods are
140
+ guaranteed to remain stable across minor versions:
140
141
 
141
- - **`apiService`** - Vizzly API client for interacting with the platform
142
- - **`uploader`** - Screenshot upload service
143
- - **`buildManager`** - Build lifecycle management
144
- - **`serverManager`** - Screenshot server management
145
- - **`tddService`** - TDD mode services
146
- - **`testRunner`** - Test execution service
142
+ #### `services.testRunner`
147
143
 
148
- Example accessing a service:
144
+ Manages build lifecycle and emits events:
145
+
146
+ - **`once(event, callback)`** - Listen for a single event emission
147
+ - **`on(event, callback)`** - Subscribe to events
148
+ - **`off(event, callback)`** - Unsubscribe from events
149
+ - **`createBuild(options, isTddMode)`** - Create a new build, returns `Promise<buildId>`
150
+ - **`finalizeBuild(buildId, isTddMode, success, executionTime)`** - Finalize a build
151
+
152
+ Events emitted:
153
+ - `build-created` - Emitted with `{ url }` when a build is created
154
+
155
+ #### `services.serverManager`
156
+
157
+ Controls the screenshot capture server:
158
+
159
+ - **`start(buildId, tddMode, setBaseline)`** - Start the screenshot server
160
+ - **`stop()`** - Stop the screenshot server
161
+
162
+ Example accessing services:
149
163
 
150
164
  ```javascript
151
165
  register(program, { config, output, services }) {
152
166
  program
153
- .command('upload-screenshots <dir>')
154
- .action(async (dir) => {
155
- let uploader = await services.get('uploader');
156
- await uploader.uploadScreenshots(screenshots);
167
+ .command('capture')
168
+ .description('Capture screenshots with custom workflow')
169
+ .action(async () => {
170
+ let { testRunner, serverManager } = services;
171
+
172
+ // Listen for build creation
173
+ testRunner.once('build-created', ({ url }) => {
174
+ output.info(`Build created: ${url}`);
175
+ });
176
+
177
+ // Create build and start server
178
+ let buildId = await testRunner.createBuild({ buildName: 'Custom' }, false);
179
+ await serverManager.start(buildId, false, false);
180
+
181
+ // ... capture screenshots ...
182
+
183
+ // Finalize and cleanup
184
+ await testRunner.finalizeBuild(buildId, false, true, Date.now());
185
+ await serverManager.stop();
157
186
  });
158
187
  }
159
188
  ```
@@ -290,9 +319,9 @@ Use async/await for asynchronous operations:
290
319
 
291
320
  ```javascript
292
321
  .action(async (options) => {
293
- let service = await services.get('apiService');
294
- let result = await service.doSomething();
295
- output.info(`Result: ${result}`);
322
+ let { testRunner } = services;
323
+ let buildId = await testRunner.createBuild({ buildName: 'Test' }, false);
324
+ output.info(`Created build: ${buildId}`);
296
325
  });
297
326
  ```
298
327
 
@@ -384,21 +413,27 @@ export default {
384
413
  .description('Capture screenshots from Storybook build')
385
414
  .option('--viewports <list>', 'Comma-separated viewports', '1280x720')
386
415
  .action(async (path, options) => {
416
+ let { testRunner, serverManager } = services;
417
+ let startTime = Date.now();
418
+
419
+ // Create build and start server
420
+ let buildId = await testRunner.createBuild({ buildName: 'Storybook' }, false);
421
+ await serverManager.start(buildId, false, false);
422
+
387
423
  output.info(`Crawling Storybook at ${path}`);
388
424
 
389
425
  // Import dependencies lazily
390
426
  let { crawlStorybook } = await import('./crawler.js');
391
427
 
392
- // Capture screenshots
393
- let screenshots = await crawlStorybook(path, {
428
+ // Capture screenshots (uses vizzlyScreenshot internally)
429
+ await crawlStorybook(path, {
394
430
  viewports: options.viewports.split(','),
395
431
  });
396
432
 
397
- output.info(`Captured ${screenshots.length} screenshots`);
398
-
399
- // Upload using Vizzly's uploader service
400
- let uploader = await services.get('uploader');
401
- await uploader.uploadScreenshots(screenshots);
433
+ // Finalize build
434
+ let executionTime = Date.now() - startTime;
435
+ await testRunner.finalizeBuild(buildId, false, true, executionTime);
436
+ await serverManager.stop();
402
437
 
403
438
  output.success('Upload complete!');
404
439
  });