@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
@@ -3,14 +3,13 @@ import { join } from 'path';
3
3
  import { compare } from '@vizzly-testing/honeydiff';
4
4
  import crypto from 'crypto';
5
5
  import { ApiService } from '../services/api-service.js';
6
- import { createServiceLogger } from '../utils/logger-factory.js';
6
+ import * as output from '../utils/output.js';
7
7
  import { colors } from '../utils/colors.js';
8
8
  import { getDefaultBranch } from '../utils/git.js';
9
9
  import { fetchWithTimeout } from '../utils/fetch-utils.js';
10
10
  import { NetworkError } from '../errors/vizzly-error.js';
11
11
  import { HtmlReportGenerator } from './html-report-generator.js';
12
12
  import { sanitizeScreenshotName, validatePathSecurity, safePath, validateScreenshotProperties } from '../utils/security.js';
13
- const logger = createServiceLogger('TDD');
14
13
 
15
14
  /**
16
15
  * Generate a screenshot signature for baseline matching
@@ -64,12 +63,13 @@ function generateComparisonId(signature) {
64
63
  * Create a new TDD service instance
65
64
  */
66
65
  export function createTDDService(config, options = {}) {
67
- return new TddService(config, options.workingDir, options.setBaseline);
66
+ return new TddService(config, options.workingDir, options.setBaseline, options.authService);
68
67
  }
69
68
  export class TddService {
70
- constructor(config, workingDir = process.cwd(), setBaseline = false) {
69
+ constructor(config, workingDir = process.cwd(), setBaseline = false, authService = null) {
71
70
  this.config = config;
72
71
  this.setBaseline = setBaseline;
72
+ this.authService = authService;
73
73
  this.api = new ApiService({
74
74
  baseUrl: config.apiUrl,
75
75
  token: config.apiKey,
@@ -81,7 +81,7 @@ export class TddService {
81
81
  try {
82
82
  this.workingDir = validatePathSecurity(workingDir, workingDir);
83
83
  } catch (error) {
84
- logger.error(`Invalid working directory: ${error.message}`);
84
+ output.error(`Invalid working directory: ${error.message}`);
85
85
  throw new Error(`Working directory validation failed: ${error.message}`);
86
86
  }
87
87
 
@@ -95,7 +95,7 @@ export class TddService {
95
95
 
96
96
  // Check if we're in baseline update mode
97
97
  if (this.setBaseline) {
98
- logger.info('🐻 Baseline update mode - will overwrite existing baselines with new ones');
98
+ output.info('🐻 Baseline update mode - will overwrite existing baselines with new ones');
99
99
  }
100
100
 
101
101
  // Ensure directories exist
@@ -106,7 +106,7 @@ export class TddService {
106
106
  recursive: true
107
107
  });
108
108
  } catch (error) {
109
- logger.error(`Failed to create directory ${dir}: ${error.message}`);
109
+ output.error(`Failed to create directory ${dir}: ${error.message}`);
110
110
  throw new Error(`Directory creation failed: ${error.message}`);
111
111
  }
112
112
  }
@@ -119,9 +119,9 @@ export class TddService {
119
119
  if (!branch) {
120
120
  // If we can't detect a default branch, use 'main' as fallback
121
121
  branch = 'main';
122
- logger.warn(`⚠️ Could not detect default branch, using 'main' as fallback`);
122
+ output.warn(`⚠️ Could not detect default branch, using 'main' as fallback`);
123
123
  } else {
124
- logger.debug(`Using detected default branch: ${branch}`);
124
+ output.debug('tdd', `detected default branch: ${branch}`);
125
125
  }
126
126
  }
127
127
  try {
@@ -130,9 +130,9 @@ export class TddService {
130
130
  // Use specific build ID - get it with screenshots in one call
131
131
  const apiResponse = await this.api.getBuild(buildId, 'screenshots');
132
132
 
133
- // Debug the full API response (only in debug mode)
134
- logger.debug(`📊 Raw API response:`, {
135
- apiResponse
133
+ // API response available in verbose mode
134
+ output.debug('tdd', 'fetched baseline build', {
135
+ id: apiResponse?.build?.id || apiResponse?.id
136
136
  });
137
137
  if (!apiResponse) {
138
138
  throw new Error(`Build ${buildId} not found or API returned null`);
@@ -141,22 +141,22 @@ export class TddService {
141
141
  // Handle wrapped response format
142
142
  baselineBuild = apiResponse.build || apiResponse;
143
143
  if (!baselineBuild.id) {
144
- logger.warn(`⚠️ Build response structure: ${JSON.stringify(Object.keys(apiResponse))}`);
145
- logger.warn(`⚠️ Extracted build keys: ${JSON.stringify(Object.keys(baselineBuild))}`);
144
+ output.warn(`⚠️ Build response structure: ${JSON.stringify(Object.keys(apiResponse))}`);
145
+ output.warn(`⚠️ Extracted build keys: ${JSON.stringify(Object.keys(baselineBuild))}`);
146
146
  }
147
147
 
148
148
  // Check build status and warn if it's not successful
149
149
  if (baselineBuild.status === 'failed') {
150
- logger.warn(`⚠️ Build ${buildId} is marked as FAILED - falling back to local baselines`);
151
- logger.info(`💡 To use remote baselines, specify a successful build ID instead`);
150
+ output.warn(`⚠️ Build ${buildId} is marked as FAILED - falling back to local baselines`);
151
+ output.info(`💡 To use remote baselines, specify a successful build ID instead`);
152
152
  // Fall back to local baseline logic
153
153
  return await this.handleLocalBaselines();
154
154
  } else if (baselineBuild.status !== 'completed') {
155
- logger.warn(`⚠️ Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
155
+ output.warn(`⚠️ Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
156
156
  }
157
157
  } else if (comparisonId) {
158
158
  // Use specific comparison ID - download only this comparison's baseline screenshot
159
- logger.info(`Using comparison: ${comparisonId}`);
159
+ output.info(`Using comparison: ${comparisonId}`);
160
160
  const comparison = await this.api.getComparison(comparisonId);
161
161
 
162
162
  // A comparison doesn't have baselineBuild directly - we need to get it
@@ -201,7 +201,7 @@ export class TddService {
201
201
  screenshotProperties.browser = comparison.baseline_browser;
202
202
  }
203
203
  }
204
- logger.info(`📊 Extracted properties for signature: ${JSON.stringify(screenshotProperties)}`);
204
+ output.info(`📊 Extracted properties for signature: ${JSON.stringify(screenshotProperties)}`);
205
205
 
206
206
  // For a specific comparison, we only download that one baseline screenshot
207
207
  // Create a mock build structure with just this one screenshot
@@ -225,8 +225,8 @@ export class TddService {
225
225
  limit: 1
226
226
  });
227
227
  if (!builds.data || builds.data.length === 0) {
228
- logger.warn(`⚠️ No baseline builds found for ${environment}/${branch}`);
229
- logger.info('💡 Run a build in normal mode first to create baselines');
228
+ output.warn(`⚠️ No baseline builds found for ${environment}/${branch}`);
229
+ output.info('💡 Run a build in normal mode first to create baselines');
230
230
  return null;
231
231
  }
232
232
  baselineBuild = builds.data[0];
@@ -242,11 +242,11 @@ export class TddService {
242
242
  buildDetails = await this.api.getBuild(actualBuildId, 'screenshots');
243
243
  }
244
244
  if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
245
- logger.warn('⚠️ No screenshots found in baseline build');
245
+ output.warn('⚠️ No screenshots found in baseline build');
246
246
  return null;
247
247
  }
248
- logger.info(`Using baseline from build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
249
- logger.info(`Checking ${colors.cyan(buildDetails.screenshots.length)} baseline screenshots...`);
248
+ output.info(`Using baseline from build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
249
+ output.info(`Checking ${colors.cyan(buildDetails.screenshots.length)} baseline screenshots...`);
250
250
 
251
251
  // Check existing baseline metadata for efficient SHA comparison
252
252
  const existingBaseline = await this.loadBaseline();
@@ -274,7 +274,7 @@ export class TddService {
274
274
  try {
275
275
  sanitizedName = sanitizeScreenshotName(screenshot.name);
276
276
  } catch (error) {
277
- logger.warn(`Skipping screenshot with invalid name '${screenshot.name}': ${error.message}`);
277
+ output.warn(`Skipping screenshot with invalid name '${screenshot.name}': ${error.message}`);
278
278
  errorCount++;
279
279
  continue;
280
280
  }
@@ -289,19 +289,16 @@ export class TddService {
289
289
  if (existsSync(imagePath) && screenshot.sha256) {
290
290
  const storedSha = existingShaMap.get(signature);
291
291
  if (storedSha === screenshot.sha256) {
292
- logger.debug(`⚡ Skipping ${sanitizedName} - SHA match from metadata`);
293
292
  downloadedCount++; // Count as "downloaded" since we have it
294
293
  skippedCount++;
295
294
  continue;
296
- } else if (storedSha) {
297
- logger.debug(`🔄 SHA mismatch for ${sanitizedName} - will re-download (stored: ${storedSha?.slice(0, 8)}..., remote: ${screenshot.sha256?.slice(0, 8)}...)`);
298
295
  }
299
296
  }
300
297
 
301
298
  // Use original_url as the download URL
302
299
  const downloadUrl = screenshot.original_url || screenshot.url;
303
300
  if (!downloadUrl) {
304
- logger.warn(`⚠️ Screenshot ${sanitizedName} has no download URL - skipping`);
301
+ output.warn(`⚠️ Screenshot ${sanitizedName} has no download URL - skipping`);
305
302
  errorCount++;
306
303
  continue;
307
304
  }
@@ -319,12 +316,12 @@ export class TddService {
319
316
  // Process downloads in batches
320
317
  const actualDownloadsNeeded = screenshotsToProcess.length;
321
318
  if (actualDownloadsNeeded > 0) {
322
- logger.info(`📥 Downloading ${actualDownloadsNeeded} new/updated screenshots in batches of ${batchSize}...`);
319
+ output.info(`📥 Downloading ${actualDownloadsNeeded} new/updated screenshots in batches of ${batchSize}...`);
323
320
  for (let i = 0; i < screenshotsToProcess.length; i += batchSize) {
324
321
  const batch = screenshotsToProcess.slice(i, i + batchSize);
325
322
  const batchNum = Math.floor(i / batchSize) + 1;
326
323
  const totalBatches = Math.ceil(screenshotsToProcess.length / batchSize);
327
- logger.info(`📦 Processing batch ${batchNum}/${totalBatches} (${batch.length} screenshots)`);
324
+ output.info(`📦 Processing batch ${batchNum}/${totalBatches} (${batch.length} screenshots)`);
328
325
 
329
326
  // Download batch concurrently
330
327
  const downloadPromises = batch.map(async ({
@@ -333,7 +330,6 @@ export class TddService {
333
330
  downloadUrl
334
331
  }) => {
335
332
  try {
336
- logger.debug(`📥 Downloading: ${sanitizedName}`);
337
333
  const response = await fetchWithTimeout(downloadUrl);
338
334
  if (!response.ok) {
339
335
  throw new NetworkError(`Failed to download ${sanitizedName}: ${response.statusText}`);
@@ -341,13 +337,12 @@ export class TddService {
341
337
  const arrayBuffer = await response.arrayBuffer();
342
338
  const imageBuffer = Buffer.from(arrayBuffer);
343
339
  writeFileSync(imagePath, imageBuffer);
344
- logger.debug(`✓ Downloaded ${sanitizedName}.png`);
345
340
  return {
346
341
  success: true,
347
342
  name: sanitizedName
348
343
  };
349
344
  } catch (error) {
350
- logger.warn(`⚠️ Failed to download ${sanitizedName}: ${error.message}`);
345
+ output.warn(`⚠️ Failed to download ${sanitizedName}: ${error.message}`);
351
346
  return {
352
347
  success: false,
353
348
  name: sanitizedName,
@@ -364,18 +359,18 @@ export class TddService {
364
359
  // Show progress
365
360
  const totalProcessed = downloadedCount + skippedCount + errorCount;
366
361
  const progressPercent = Math.round(totalProcessed / totalScreenshots * 100);
367
- logger.info(`📊 Progress: ${totalProcessed}/${totalScreenshots} (${progressPercent}%) - ${batchSuccesses} downloaded, ${batchFailures} failed in this batch`);
362
+ output.info(`📊 Progress: ${totalProcessed}/${totalScreenshots} (${progressPercent}%) - ${batchSuccesses} downloaded, ${batchFailures} failed in this batch`);
368
363
  }
369
364
  }
370
365
 
371
366
  // Check if we actually downloaded any screenshots
372
367
  if (downloadedCount === 0 && skippedCount === 0) {
373
- logger.error('❌ No screenshots were successfully downloaded from the baseline build');
368
+ output.error('❌ No screenshots were successfully downloaded from the baseline build');
374
369
  if (errorCount > 0) {
375
- logger.info(`💡 ${errorCount} screenshots had errors - check download URLs and network connection`);
370
+ output.info(`💡 ${errorCount} screenshots had errors - check download URLs and network connection`);
376
371
  }
377
- logger.info('💡 This usually means the build failed or screenshots have no download URLs');
378
- logger.info('💡 Try using a successful build ID, or run without --baseline-build to create local baselines');
372
+ output.info('💡 This usually means the build failed or screenshots have no download URLs');
373
+ output.info('💡 Try using a successful build ID, or run without --baseline-build to create local baselines');
379
374
  return null;
380
375
  }
381
376
 
@@ -398,7 +393,7 @@ export class TddService {
398
393
  try {
399
394
  sanitizedName = sanitizeScreenshotName(s.name);
400
395
  } catch (error) {
401
- logger.warn(`Screenshot name sanitization failed for '${s.name}': ${error.message}`);
396
+ output.warn(`Screenshot name sanitization failed for '${s.name}': ${error.message}`);
402
397
  return null; // Skip invalid screenshots
403
398
  }
404
399
  let properties = validateScreenshotProperties(s.metadata || s.properties || {});
@@ -445,21 +440,176 @@ export class TddService {
445
440
  if (skippedCount > 0) {
446
441
  // All skipped (up-to-date)
447
442
  if (actualDownloads === 0) {
448
- logger.info(`✅ All ${skippedCount} baselines up-to-date (matching local SHA)`);
443
+ output.info(`✅ All ${skippedCount} baselines up-to-date (matching local SHA)`);
449
444
  } else {
450
445
  // Mixed: some downloaded, some skipped
451
- logger.info(`✅ Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
446
+ output.info(`✅ Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
452
447
  }
453
448
  } else {
454
449
  // Fresh download
455
- logger.info(`✅ Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
450
+ output.info(`✅ Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
456
451
  }
457
452
  if (errorCount > 0) {
458
- logger.warn(`⚠️ ${errorCount} screenshots failed to download`);
453
+ output.warn(`⚠️ ${errorCount} screenshots failed to download`);
459
454
  }
460
455
  return this.baselineData;
461
456
  } catch (error) {
462
- logger.error(`❌ Failed to download baseline: ${error.message}`);
457
+ output.error(`❌ Failed to download baseline: ${error.message}`);
458
+ throw error;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Download baselines using OAuth authentication
464
+ * Used when user is logged in via device flow but no API token is configured
465
+ * @param {string} buildId - Build ID to download from
466
+ * @param {string} organizationSlug - Organization slug
467
+ * @param {string} projectSlug - Project slug
468
+ * @param {Object} authService - Auth service for OAuth requests
469
+ * @returns {Promise<Object>} Download result
470
+ */
471
+ async downloadBaselinesWithAuth(buildId, organizationSlug, projectSlug, authService) {
472
+ output.info(`Downloading baselines using OAuth from build ${buildId}...`);
473
+ try {
474
+ // Fetch build with screenshots via OAuth endpoint
475
+ let endpoint = `/api/build/${projectSlug}/${buildId}/tdd-baselines`;
476
+ let response = await authService.authenticatedRequest(endpoint, {
477
+ method: 'GET',
478
+ headers: {
479
+ 'X-Organization': organizationSlug
480
+ }
481
+ });
482
+ let {
483
+ build,
484
+ screenshots
485
+ } = response;
486
+ if (!screenshots || screenshots.length === 0) {
487
+ output.warn('⚠️ No screenshots found in build');
488
+ return {
489
+ downloadedCount: 0,
490
+ skippedCount: 0,
491
+ errorCount: 0
492
+ };
493
+ }
494
+ output.info(`Using baseline from build: ${colors.cyan(build.name || 'Unknown')} (${build.id})`);
495
+ output.info(`Checking ${colors.cyan(screenshots.length)} baseline screenshots...`);
496
+
497
+ // Load existing baseline metadata for SHA comparison
498
+ let existingBaseline = await this.loadBaseline();
499
+ let existingShaMap = new Map();
500
+ if (existingBaseline) {
501
+ existingBaseline.screenshots.forEach(s => {
502
+ if (s.sha256 && s.signature) {
503
+ existingShaMap.set(s.signature, s.sha256);
504
+ }
505
+ });
506
+ }
507
+
508
+ // Process and download screenshots
509
+ let downloadedCount = 0;
510
+ let skippedCount = 0;
511
+ let errorCount = 0;
512
+ let downloadedScreenshots = [];
513
+ for (let screenshot of screenshots) {
514
+ let sanitizedName;
515
+ try {
516
+ sanitizedName = sanitizeScreenshotName(screenshot.name);
517
+ } catch (error) {
518
+ output.warn(`Screenshot name sanitization failed for '${screenshot.name}': ${error.message}`);
519
+ errorCount++;
520
+ continue;
521
+ }
522
+ let properties = validateScreenshotProperties(screenshot.metadata || {});
523
+ let signature = generateScreenshotSignature(sanitizedName, properties);
524
+ let filename = signatureToFilename(signature);
525
+ let filePath = safePath(this.baselinePath, `${filename}.png`);
526
+
527
+ // Check if we can skip via SHA comparison
528
+ if (screenshot.sha256 && existingShaMap.get(signature) === screenshot.sha256) {
529
+ skippedCount++;
530
+ downloadedScreenshots.push({
531
+ name: sanitizedName,
532
+ sha256: screenshot.sha256,
533
+ signature,
534
+ path: filePath,
535
+ properties
536
+ });
537
+ continue;
538
+ }
539
+
540
+ // Download the screenshot
541
+ let downloadUrl = screenshot.original_url;
542
+ if (!downloadUrl) {
543
+ output.warn(`⚠️ No download URL for screenshot: ${sanitizedName}`);
544
+ errorCount++;
545
+ continue;
546
+ }
547
+ try {
548
+ let imageResponse = await fetchWithTimeout(downloadUrl, {}, 30000);
549
+ if (!imageResponse.ok) {
550
+ throw new Error(`HTTP ${imageResponse.status}`);
551
+ }
552
+ let imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
553
+
554
+ // Calculate SHA256 of downloaded content
555
+ let sha256 = crypto.createHash('sha256').update(imageBuffer).digest('hex');
556
+ writeFileSync(filePath, imageBuffer);
557
+ downloadedCount++;
558
+ downloadedScreenshots.push({
559
+ name: sanitizedName,
560
+ sha256,
561
+ signature,
562
+ path: filePath,
563
+ properties,
564
+ originalUrl: downloadUrl
565
+ });
566
+ } catch (error) {
567
+ output.warn(`⚠️ Failed to download ${sanitizedName}: ${error.message}`);
568
+ errorCount++;
569
+ }
570
+ }
571
+
572
+ // Store baseline metadata
573
+ this.baselineData = {
574
+ buildId: build.id,
575
+ buildName: build.name,
576
+ branch: build.branch,
577
+ threshold: this.threshold,
578
+ screenshots: downloadedScreenshots
579
+ };
580
+ let metadataPath = join(this.baselinePath, 'metadata.json');
581
+ writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
582
+
583
+ // Save baseline build metadata
584
+ let baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
585
+ writeFileSync(baselineMetadataPath, JSON.stringify({
586
+ buildId: build.id,
587
+ buildName: build.name,
588
+ branch: build.branch,
589
+ commitSha: build.commit_sha,
590
+ downloadedAt: new Date().toISOString()
591
+ }, null, 2));
592
+
593
+ // Summary
594
+ if (skippedCount > 0 && downloadedCount === 0) {
595
+ output.info(`✅ All ${skippedCount} baselines up-to-date (matching local SHA)`);
596
+ } else if (skippedCount > 0) {
597
+ output.info(`✅ Downloaded ${downloadedCount} new screenshots, ${skippedCount} already up-to-date`);
598
+ } else {
599
+ output.info(`✅ Downloaded ${downloadedCount}/${screenshots.length} screenshots successfully`);
600
+ }
601
+ if (errorCount > 0) {
602
+ output.warn(`⚠️ ${errorCount} screenshots failed to download`);
603
+ }
604
+ return {
605
+ downloadedCount,
606
+ skippedCount,
607
+ errorCount,
608
+ buildId: build.id,
609
+ buildName: build.name
610
+ };
611
+ } catch (error) {
612
+ output.error(`❌ OAuth download failed: ${error.message} (org=${organizationSlug}, project=${projectSlug}, build=${buildId})`);
463
613
  throw error;
464
614
  }
465
615
  }
@@ -471,7 +621,7 @@ export class TddService {
471
621
  async handleLocalBaselines() {
472
622
  // Check if we're in baseline update mode - skip loading existing baselines
473
623
  if (this.setBaseline) {
474
- logger.info('📁 Ready for new baseline creation - all screenshots will be treated as new baselines');
624
+ output.info('📁 Ready for new baseline creation - all screenshots will be treated as new baselines');
475
625
 
476
626
  // Reset baseline data since we're creating new ones
477
627
  this.baselineData = null;
@@ -480,21 +630,21 @@ export class TddService {
480
630
  const baseline = await this.loadBaseline();
481
631
  if (!baseline) {
482
632
  if (this.config.apiKey) {
483
- logger.info('📥 No local baseline found, but API key available for future remote fetching');
484
- logger.info('🆕 Current run will create new local baselines');
633
+ output.info('📥 No local baseline found, but API key available for future remote fetching');
634
+ output.info('🆕 Current run will create new local baselines');
485
635
  } else {
486
- logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
636
+ output.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
487
637
  }
488
638
  return null;
489
639
  } else {
490
- logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
640
+ output.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
491
641
  return baseline;
492
642
  }
493
643
  }
494
644
  async loadBaseline() {
495
645
  // In baseline update mode, never load existing baselines
496
646
  if (this.setBaseline) {
497
- logger.debug('🐻 Baseline update mode - skipping baseline loading');
647
+ output.debug('tdd', 'baseline update mode - skipping loading');
498
648
  return null;
499
649
  }
500
650
  const metadataPath = join(this.baselinePath, 'metadata.json');
@@ -507,7 +657,7 @@ export class TddService {
507
657
  this.threshold = metadata.threshold || this.threshold;
508
658
  return metadata;
509
659
  } catch (error) {
510
- logger.error(`❌ Failed to load baseline metadata: ${error.message}`);
660
+ output.error(`❌ Failed to load baseline metadata: ${error.message}`);
511
661
  return null;
512
662
  }
513
663
  }
@@ -517,14 +667,14 @@ export class TddService {
517
667
  try {
518
668
  sanitizedName = sanitizeScreenshotName(name);
519
669
  } catch (error) {
520
- logger.error(`Invalid screenshot name '${name}': ${error.message}`);
670
+ output.error(`Invalid screenshot name '${name}': ${error.message}`);
521
671
  throw new Error(`Screenshot name validation failed: ${error.message}`);
522
672
  }
523
673
  let validatedProperties;
524
674
  try {
525
675
  validatedProperties = validateScreenshotProperties(properties);
526
676
  } catch (error) {
527
- logger.warn(`Property validation failed for '${sanitizedName}': ${error.message}`);
677
+ output.warn(`Property validation failed for '${sanitizedName}': ${error.message}`);
528
678
  validatedProperties = {};
529
679
  }
530
680
 
@@ -552,13 +702,8 @@ export class TddService {
552
702
  // Check if baseline exists
553
703
  const baselineExists = existsSync(baselineImagePath);
554
704
  if (!baselineExists) {
555
- logger.debug(`No baseline found for ${sanitizedName} - creating baseline`);
556
- logger.debug(`Path: ${baselineImagePath}`);
557
- logger.debug(`Size: ${imageBuffer.length} bytes`);
558
-
559
705
  // Copy current screenshot to baseline directory for future comparisons
560
706
  writeFileSync(baselineImagePath, imageBuffer);
561
- logger.debug(`Created baseline: ${imageBuffer.length} bytes`);
562
707
 
563
708
  // Update or create baseline metadata
564
709
  if (!this.baselineData) {
@@ -589,7 +734,9 @@ export class TddService {
589
734
  // Save updated metadata
590
735
  const metadataPath = join(this.baselinePath, 'metadata.json');
591
736
  writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
592
- logger.debug(`✅ Created baseline for ${sanitizedName}`);
737
+
738
+ // Baseline creation tracked by event handler
739
+
593
740
  const result = {
594
741
  id: generateComparisonId(signature),
595
742
  name: sanitizedName,
@@ -606,13 +753,6 @@ export class TddService {
606
753
 
607
754
  // Baseline exists - compare with it
608
755
  try {
609
- // Log file sizes for debugging
610
- const baselineSize = readFileSync(baselineImagePath).length;
611
- const currentSize = readFileSync(currentImagePath).length;
612
- logger.debug(`Comparing ${sanitizedName}`);
613
- logger.debug(`Baseline: ${baselineImagePath} (${baselineSize} bytes)`);
614
- logger.debug(`Current: ${currentImagePath} (${currentSize} bytes)`);
615
-
616
756
  // Try to compare - honeydiff will throw if dimensions don't match
617
757
  const result = await compare(baselineImagePath, currentImagePath, {
618
758
  colorThreshold: this.threshold,
@@ -639,7 +779,8 @@ export class TddService {
639
779
  aaPixelsIgnored: result.aaPixelsIgnored,
640
780
  aaPercentage: result.aaPercentage
641
781
  };
642
- logger.debug(`PASSED ${sanitizedName}`);
782
+
783
+ // Result tracked by event handler
643
784
  this.comparisons.push(comparison);
644
785
  return comparison;
645
786
  } else {
@@ -672,8 +813,8 @@ export class TddService {
672
813
  intensityStats: result.intensityStats,
673
814
  diffClusters: result.diffClusters
674
815
  };
675
- logger.warn(`❌ ${colors.red('FAILED')} ${sanitizedName} - differences detected${diffInfo}`);
676
- logger.info(` Diff saved to: ${diffImagePath}`);
816
+ output.warn(`❌ ${colors.red('FAILED')} ${sanitizedName} - differences detected${diffInfo}`);
817
+ output.info(` Diff saved to: ${diffImagePath}`);
677
818
  this.comparisons.push(comparison);
678
819
  return comparison;
679
820
  }
@@ -683,9 +824,11 @@ export class TddService {
683
824
  if (isDimensionMismatch) {
684
825
  // Different dimensions = different screenshot signature
685
826
  // This shouldn't happen if signatures are working correctly, but handle gracefully
686
- logger.warn(`⚠️ Dimension mismatch for ${sanitizedName} - baseline file exists but has different dimensions`);
687
- logger.warn(` This indicates a signature collision. Creating new baseline with correct signature.`);
688
- logger.debug(` Error: ${error.message}`);
827
+ output.warn(`⚠️ Dimension mismatch for ${sanitizedName} - baseline file exists but has different dimensions`);
828
+ output.warn(` This indicates a signature collision. Creating new baseline with correct signature.`);
829
+ output.debug('tdd', 'dimension mismatch', {
830
+ error: error.message
831
+ });
689
832
 
690
833
  // Create a new baseline for this screenshot (overwriting the incorrect one)
691
834
  writeFileSync(baselineImagePath, imageBuffer);
@@ -715,7 +858,7 @@ export class TddService {
715
858
  }
716
859
  const metadataPath = join(this.baselinePath, 'metadata.json');
717
860
  writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
718
- logger.info(`✅ Created new baseline for ${sanitizedName} (different dimensions)`);
861
+ output.info(`✅ Created new baseline for ${sanitizedName} (different dimensions)`);
719
862
  const comparison = {
720
863
  id: generateComparisonId(signature),
721
864
  name: sanitizedName,
@@ -731,7 +874,7 @@ export class TddService {
731
874
  }
732
875
 
733
876
  // Handle other file errors or issues
734
- logger.error(`❌ Error comparing ${sanitizedName}: ${error.message}`);
877
+ output.error(`❌ Error comparing ${sanitizedName}: ${error.message}`);
735
878
  const comparison = {
736
879
  id: generateComparisonId(signature),
737
880
  name: sanitizedName,
@@ -764,34 +907,34 @@ export class TddService {
764
907
  }
765
908
  async printResults() {
766
909
  const results = this.getResults();
767
- logger.info('\n📊 TDD Results:');
768
- logger.info(`Total: ${colors.cyan(results.total)}`);
769
- logger.info(`Passed: ${colors.green(results.passed)}`);
910
+ output.info('\n📊 TDD Results:');
911
+ output.info(`Total: ${colors.cyan(results.total)}`);
912
+ output.info(`Passed: ${colors.green(results.passed)}`);
770
913
  if (results.failed > 0) {
771
- logger.info(`Failed: ${colors.red(results.failed)}`);
914
+ output.info(`Failed: ${colors.red(results.failed)}`);
772
915
  }
773
916
  if (results.new > 0) {
774
- logger.info(`New: ${colors.yellow(results.new)}`);
917
+ output.info(`New: ${colors.yellow(results.new)}`);
775
918
  }
776
919
  if (results.errors > 0) {
777
- logger.info(`Errors: ${colors.red(results.errors)}`);
920
+ output.info(`Errors: ${colors.red(results.errors)}`);
778
921
  }
779
922
 
780
923
  // Show failed comparisons
781
924
  const failedComparisons = results.comparisons.filter(c => c.status === 'failed');
782
925
  if (failedComparisons.length > 0) {
783
- logger.info('\n❌ Failed comparisons:');
926
+ output.info('\n❌ Failed comparisons:');
784
927
  failedComparisons.forEach(comp => {
785
- logger.info(` • ${comp.name}`);
928
+ output.info(` • ${comp.name}`);
786
929
  });
787
930
  }
788
931
 
789
932
  // Show new screenshots
790
933
  const newComparisons = results.comparisons.filter(c => c.status === 'new');
791
934
  if (newComparisons.length > 0) {
792
- logger.info('\n📸 New screenshots:');
935
+ output.info('\n📸 New screenshots:');
793
936
  newComparisons.forEach(comp => {
794
- logger.info(` • ${comp.name}`);
937
+ output.info(` • ${comp.name}`);
795
938
  });
796
939
  }
797
940
 
@@ -813,7 +956,7 @@ export class TddService {
813
956
  });
814
957
 
815
958
  // Show report path (always clickable)
816
- logger.info(`\n🐻 View detailed report: ${colors.cyan('file://' + reportPath)}`);
959
+ output.info(`\n🐻 View detailed report: ${colors.cyan('file://' + reportPath)}`);
817
960
 
818
961
  // Auto-open if configured
819
962
  if (this.config.tdd?.openReport) {
@@ -821,7 +964,7 @@ export class TddService {
821
964
  }
822
965
  return reportPath;
823
966
  } catch (error) {
824
- logger.warn(`Failed to generate HTML report: ${error.message}`);
967
+ output.warn(`Failed to generate HTML report: ${error.message}`);
825
968
  }
826
969
  }
827
970
 
@@ -854,9 +997,9 @@ export class TddService {
854
997
  break;
855
998
  }
856
999
  await execAsync(command);
857
- logger.info('📖 Report opened in browser');
858
- } catch (error) {
859
- logger.debug(`Failed to open report: ${error.message}`);
1000
+ output.info('📖 Report opened in browser');
1001
+ } catch {
1002
+ // Browser open may fail silently
860
1003
  }
861
1004
  }
862
1005
 
@@ -866,7 +1009,7 @@ export class TddService {
866
1009
  */
867
1010
  updateBaselines() {
868
1011
  if (this.comparisons.length === 0) {
869
- logger.warn('No comparisons found - nothing to update');
1012
+ output.warn('No comparisons found - nothing to update');
870
1013
  return 0;
871
1014
  }
872
1015
  let updatedCount = 0;
@@ -888,7 +1031,7 @@ export class TddService {
888
1031
  current
889
1032
  } = comparison;
890
1033
  if (!current || !existsSync(current)) {
891
- logger.warn(`Current screenshot not found for ${name}, skipping`);
1034
+ output.warn(`Current screenshot not found for ${name}, skipping`);
892
1035
  continue;
893
1036
  }
894
1037
 
@@ -897,7 +1040,7 @@ export class TddService {
897
1040
  try {
898
1041
  sanitizedName = sanitizeScreenshotName(name);
899
1042
  } catch (error) {
900
- logger.warn(`Skipping baseline update for invalid name '${name}': ${error.message}`);
1043
+ output.warn(`Skipping baseline update for invalid name '${name}': ${error.message}`);
901
1044
  continue;
902
1045
  }
903
1046
  let validatedProperties = validateScreenshotProperties(comparison.properties || {});
@@ -923,9 +1066,9 @@ export class TddService {
923
1066
  this.baselineData.screenshots.push(screenshotEntry);
924
1067
  }
925
1068
  updatedCount++;
926
- logger.info(`✅ Updated baseline for ${sanitizedName}`);
1069
+ output.info(`✅ Updated baseline for ${sanitizedName}`);
927
1070
  } catch (error) {
928
- logger.error(`❌ Failed to update baseline for ${sanitizedName}: ${error.message}`);
1071
+ output.error(`❌ Failed to update baseline for ${sanitizedName}: ${error.message}`);
929
1072
  }
930
1073
  }
931
1074
 
@@ -934,9 +1077,9 @@ export class TddService {
934
1077
  try {
935
1078
  const metadataPath = join(this.baselinePath, 'metadata.json');
936
1079
  writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
937
- logger.info(`✅ Updated ${updatedCount} baseline(s)`);
1080
+ output.info(`✅ Updated ${updatedCount} baseline(s)`);
938
1081
  } catch (error) {
939
- logger.error(`❌ Failed to save baseline metadata: ${error.message}`);
1082
+ output.error(`❌ Failed to save baseline metadata: ${error.message}`);
940
1083
  }
941
1084
  }
942
1085
  return updatedCount;
@@ -947,7 +1090,7 @@ export class TddService {
947
1090
  * @private
948
1091
  */
949
1092
  createNewBaseline(name, imageBuffer, properties, currentImagePath, baselineImagePath) {
950
- logger.info(`🐻 Creating baseline for ${name}`);
1093
+ output.info(`🐻 Creating baseline for ${name}`);
951
1094
 
952
1095
  // Copy current screenshot to baseline directory
953
1096
  writeFileSync(baselineImagePath, imageBuffer);
@@ -995,7 +1138,7 @@ export class TddService {
995
1138
  signature
996
1139
  };
997
1140
  this.comparisons.push(result);
998
- logger.info(`✅ Baseline created for ${name}`);
1141
+ output.info(`✅ Baseline created for ${name}`);
999
1142
  return result;
1000
1143
  }
1001
1144
 
@@ -1004,7 +1147,7 @@ export class TddService {
1004
1147
  * @private
1005
1148
  */
1006
1149
  updateSingleBaseline(name, imageBuffer, properties, currentImagePath, baselineImagePath) {
1007
- logger.info(`🐻 Setting baseline for ${name}`);
1150
+ output.info(`🐻 Setting baseline for ${name}`);
1008
1151
 
1009
1152
  // Copy current screenshot to baseline directory
1010
1153
  writeFileSync(baselineImagePath, imageBuffer);
@@ -1052,7 +1195,7 @@ export class TddService {
1052
1195
  signature
1053
1196
  };
1054
1197
  this.comparisons.push(result);
1055
- logger.info(`🐻 Baseline set for ${name}`);
1198
+ output.info(`🐻 Baseline set for ${name}`);
1056
1199
  return result;
1057
1200
  }
1058
1201
 
@@ -1083,7 +1226,7 @@ export class TddService {
1083
1226
  // Find the current screenshot file
1084
1227
  const currentImagePath = safePath(this.currentPath, `${filename}.png`);
1085
1228
  if (!existsSync(currentImagePath)) {
1086
- logger.error(`Current screenshot not found at: ${currentImagePath}`);
1229
+ output.error(`Current screenshot not found at: ${currentImagePath}`);
1087
1230
  throw new Error(`Current screenshot not found: ${sanitizedName} (looked at ${currentImagePath})`);
1088
1231
  }
1089
1232
 
@@ -1105,7 +1248,7 @@ export class TddService {
1105
1248
 
1106
1249
  // Verify the write
1107
1250
  if (!existsSync(baselineImagePath)) {
1108
- logger.error(`Baseline file does not exist after write!`);
1251
+ output.error(`Baseline file does not exist after write!`);
1109
1252
  }
1110
1253
 
1111
1254
  // Update baseline metadata