@vizzly-testing/cli 0.39.0 → 0.39.2

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 (195) hide show
  1. package/README.md +12 -3
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +50 -35
  4. package/dist/adapters/cli/setup.js +34 -0
  5. package/dist/{client → adapters/client}/index.js +6 -7
  6. package/dist/adapters/http/cloud-proxy.js +29 -0
  7. package/dist/{server → adapters/http}/http-server.js +12 -13
  8. package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
  9. package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
  10. package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
  11. package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
  12. package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
  13. package/dist/adapters/sdk/index.d.ts +89 -0
  14. package/dist/{sdk → adapters/sdk}/index.js +22 -19
  15. package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
  16. package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
  17. package/dist/domains/auth/auth-errors.js +50 -0
  18. package/dist/domains/auth/auth-headers.js +41 -0
  19. package/dist/domains/auth/auth.normalizers.js +50 -0
  20. package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
  21. package/dist/domains/auth/auth.rules.js +19 -0
  22. package/dist/domains/auth/auth.serializers.js +32 -0
  23. package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
  24. package/dist/domains/auth/auth.validations.js +21 -0
  25. package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
  26. package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
  27. package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
  28. package/dist/domains/auth/token-store.js +81 -0
  29. package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
  30. package/dist/domains/baselines/baseline-download.js +192 -0
  31. package/dist/domains/baselines/baseline-write.js +22 -0
  32. package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
  33. package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
  34. package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
  35. package/dist/domains/baselines/cloud-baselines.js +9 -0
  36. package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
  37. package/dist/domains/builds/build.normalizers.js +35 -0
  38. package/dist/domains/builds/build.serializers.js +164 -0
  39. package/dist/domains/builds/build.service.js +119 -0
  40. package/dist/domains/builds/build.validations.js +39 -0
  41. package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
  42. package/dist/domains/builds/cloud-builds.routes.js +31 -0
  43. package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
  44. package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
  45. package/dist/domains/comparisons/cloud-comparisons.js +43 -0
  46. package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
  47. package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
  48. package/dist/domains/config/config-defaults.js +27 -0
  49. package/dist/{utils → domains/config}/config-discovery.js +1 -9
  50. package/dist/{utils → domains/config}/config-loader.js +10 -9
  51. package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
  52. package/dist/{types → domains/config}/config.d.ts +3 -3
  53. package/dist/domains/config/config.normalizers.js +126 -0
  54. package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
  55. package/dist/domains/config/config.rules.js +47 -0
  56. package/dist/domains/config/config.serializers.js +202 -0
  57. package/dist/domains/config/config.service.js +625 -0
  58. package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
  59. package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
  60. package/dist/domains/context/compact-context.validations.js +60 -0
  61. package/dist/domains/context/context-presentation.js +368 -0
  62. package/dist/domains/context/context.commands.js +385 -0
  63. package/dist/domains/context/context.serializers.js +157 -0
  64. package/dist/domains/context/context.service.js +76 -0
  65. package/dist/domains/context/context.validations.js +100 -0
  66. package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
  67. package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
  68. package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
  69. package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
  70. package/dist/domains/previews/preview.service.js +37 -0
  71. package/dist/domains/projects/cloud-projects.routes.js +28 -0
  72. package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
  73. package/dist/{utils → domains/projects}/project-binding.js +3 -3
  74. package/dist/domains/projects/project-errors.js +61 -0
  75. package/dist/{utils → domains/projects}/project-link-store.js +2 -2
  76. package/dist/domains/projects/project-requests.js +61 -0
  77. package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
  78. package/dist/domains/projects/project.normalizers.js +69 -0
  79. package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
  80. package/dist/domains/projects/project.validations.js +31 -0
  81. package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
  82. package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
  83. package/dist/domains/reports/dashboard.routes.js +96 -0
  84. package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
  85. package/dist/domains/reports/report-assets.routes.js +81 -0
  86. package/dist/domains/reports/report-state.js +51 -0
  87. package/dist/domains/reports/report-store.js +85 -0
  88. package/dist/domains/reports/report.serializers.js +99 -0
  89. package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
  90. package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
  91. package/dist/domains/runs/run-environment.js +69 -0
  92. package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
  93. package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
  94. package/dist/domains/runs/run.normalizers.js +8 -0
  95. package/dist/domains/runs/run.rules.js +56 -0
  96. package/dist/domains/runs/run.serializers.js +101 -0
  97. package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
  98. package/dist/domains/runs/run.validations.js +17 -0
  99. package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
  100. package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
  101. package/dist/domains/screenshots/cloud-screenshots.js +109 -0
  102. package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
  103. package/dist/domains/screenshots/screenshot-hash.js +53 -0
  104. package/dist/domains/screenshots/screenshot-server-options.js +28 -0
  105. package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
  106. package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
  107. package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
  108. package/dist/domains/screenshots/screenshot.serializers.js +146 -0
  109. package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
  110. package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
  112. package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
  113. package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
  114. package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
  115. package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
  116. package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
  117. package/dist/domains/updates/cli-update.js +103 -0
  118. package/dist/domains/updates/update-check.js +2 -0
  119. package/dist/domains/updates/update-notifications.js +175 -0
  120. package/dist/domains/uploads/upload-defaults.js +4 -0
  121. package/dist/domains/uploads/upload-files.js +32 -0
  122. package/dist/domains/uploads/upload-settings.js +21 -0
  123. package/dist/domains/uploads/upload-timing.js +18 -0
  124. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  125. package/dist/domains/uploads/upload.normalizers.js +60 -0
  126. package/dist/domains/uploads/upload.serializers.js +179 -0
  127. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  128. package/dist/domains/uploads/upload.validations.js +77 -0
  129. package/dist/index.js +12 -12
  130. package/dist/reporter/reporter-bundle.iife.js +86 -86
  131. package/dist/reporter-ssr/ssr-entry.js +3 -6
  132. package/dist/shared/cloud-protocol.js +171 -0
  133. package/dist/{utils → shared}/file-changes.js +1 -1
  134. package/dist/shared/files.js +9 -0
  135. package/dist/{utils → shared}/global-config.js +7 -78
  136. package/dist/shared/log-level.js +7 -0
  137. package/dist/{utils → shared}/output.js +1 -1
  138. package/dist/{utils → shared}/package-info.js +5 -4
  139. package/dist/shared/paths.js +57 -0
  140. package/package.json +37 -36
  141. package/dist/api/core.js +0 -382
  142. package/dist/api/endpoints.js +0 -421
  143. package/dist/api/index.js +0 -19
  144. package/dist/auth/core.js +0 -237
  145. package/dist/auth/index.js +0 -31
  146. package/dist/commands/context.js +0 -1066
  147. package/dist/config/core.js +0 -439
  148. package/dist/config/operations.js +0 -327
  149. package/dist/project/core.js +0 -211
  150. package/dist/screenshot-server/core.js +0 -161
  151. package/dist/screenshot-server/index.js +0 -11
  152. package/dist/server/routers/assets.js +0 -95
  153. package/dist/server/routers/cloud-proxy.js +0 -146
  154. package/dist/server/routers/dashboard.js +0 -172
  155. package/dist/services/auth-service.js +0 -117
  156. package/dist/services/config-service.js +0 -313
  157. package/dist/services/project-service.js +0 -88
  158. package/dist/test-runner/core.js +0 -293
  159. package/dist/test-runner/index.js +0 -13
  160. package/dist/types/sdk.d.ts +0 -186
  161. package/dist/uploader/core.js +0 -432
  162. package/dist/uploader/index.js +0 -88
  163. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  164. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  166. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  167. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  168. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  169. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  170. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  171. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  172. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  173. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  174. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  175. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  176. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  177. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  178. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  179. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  180. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  181. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  182. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  183. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  184. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  185. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  186. /package/dist/{utils → shared}/api-url.js +0 -0
  187. /package/dist/{utils → shared}/async-utils.js +0 -0
  188. /package/dist/{utils → shared}/browser.js +0 -0
  189. /package/dist/{utils → shared}/ci-env.js +0 -0
  190. /package/dist/{utils → shared}/colors.js +0 -0
  191. /package/dist/{utils → shared}/fetch-utils.js +0 -0
  192. /package/dist/{utils → shared}/git.js +0 -0
  193. /package/dist/{utils → shared}/patterns.js +0 -0
  194. /package/dist/{utils → shared}/secret-store.js +0 -0
  195. /package/dist/{errors → shared}/vizzly-error.js +0 -0
@@ -5,25 +5,31 @@
5
5
  * This is a thin orchestration layer - most logic lives in the modules.
6
6
  *
7
7
  * CRITICAL: Signature/filename generation MUST stay in sync with the cloud!
8
- * See src/tdd/core/signature.js for details.
8
+ * See src/domains/screenshots/screenshot-identity.js for details.
9
9
  */
10
10
 
11
11
  import { existsSync as defaultExistsSync, mkdirSync as defaultMkdirSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
12
12
  import { compareSync } from '@vizzly-testing/honeydiff';
13
- import { createApiClient as defaultCreateApiClient, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
14
- import { NetworkError, VizzlyError } from '../errors/vizzly-error.js';
15
- import { colors as defaultColors } from '../utils/colors.js';
16
- import { fetchWithTimeout as defaultFetchWithTimeout } from '../utils/fetch-utils.js';
17
- import { getDefaultBranch as defaultGetDefaultBranch } from '../utils/git.js';
18
- import * as defaultOutput from '../utils/output.js';
19
- import { safePath as defaultSafePath, sanitizeScreenshotName as defaultSanitizeScreenshotName, validatePathSecurity as defaultValidatePathSecurity, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../utils/security.js';
13
+ import { colors as defaultColors } from '../../shared/colors.js';
14
+ import { fetchWithTimeout as defaultFetchWithTimeout } from '../../shared/fetch-utils.js';
15
+ import { getDefaultBranch as defaultGetDefaultBranch } from '../../shared/git.js';
16
+ import * as defaultOutput from '../../shared/output.js';
17
+ import { safePath as defaultSafePath, validatePathSecurity as defaultValidatePathSecurity } from '../../shared/paths.js';
18
+ import { VizzlyError } from '../../shared/vizzly-error.js';
19
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
20
+ import { baselineExists as defaultBaselineExists, clearBaselineData as defaultClearBaselineData, clearRunData as defaultClearRunData, getBaselinePath as defaultGetBaselinePath, getCurrentPath as defaultGetCurrentPath, getDiffPath as defaultGetDiffPath, initializeDirectories as defaultInitializeDirectories, saveBaseline as defaultSaveBaseline, saveCurrent as defaultSaveCurrent } from '../baselines/baseline.service.js';
21
+ import { installBaselineBuild } from '../baselines/baseline-download.js';
22
+ import { createEmptyBaselineMetadata as defaultCreateEmptyBaselineMetadata, loadBaselineMetadata as defaultLoadBaselineMetadata, saveBaselineMetadata as defaultSaveBaselineMetadata, upsertScreenshotInMetadata as defaultUpsertScreenshotInMetadata } from '../baselines/baseline-metadata.js';
23
+ import { writeBaselineScreenshot } from '../baselines/baseline-write.js';
24
+ import { getTddBaselines as defaultGetTddBaselines } from '../baselines/cloud-baselines.js';
25
+ import { getBuilds as defaultGetBuilds } from '../builds/build.service.js';
26
+ import { getComparison as defaultGetComparison } from '../comparisons/cloud-comparisons.js';
27
+ import { buildErrorComparison as defaultBuildErrorComparison, buildFailedComparison as defaultBuildFailedComparison, buildNewComparison as defaultBuildNewComparison, buildPassedComparison as defaultBuildPassedComparison, compareImages as defaultCompareImages, isDimensionMismatchError as defaultIsDimensionMismatchError } from '../comparisons/comparison.service.js';
28
+ import { buildResults as defaultBuildResults, getFailedComparisons as defaultGetFailedComparisons, getNewComparisons as defaultGetNewComparisons } from '../comparisons/comparison-results.js';
29
+ import { sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../screenshots/screenshot.validations.js';
20
30
  // Import from extracted modules
21
- import { generateBaselineFilename as defaultGenerateBaselineFilename, generateComparisonId as defaultGenerateComparisonId, generateScreenshotSignature as defaultGenerateScreenshotSignature } from './core/signature.js';
22
- import { createEmptyBaselineMetadata as defaultCreateEmptyBaselineMetadata, loadBaselineMetadata as defaultLoadBaselineMetadata, saveBaselineMetadata as defaultSaveBaselineMetadata, upsertScreenshotInMetadata as defaultUpsertScreenshotInMetadata } from './metadata/baseline-metadata.js';
31
+ import { generateBaselineFilename as defaultGenerateBaselineFilename, generateComparisonId as defaultGenerateComparisonId, generateScreenshotSignature as defaultGenerateScreenshotSignature } from '../screenshots/screenshot-identity.js';
23
32
  import { buildTddDependencyOps, resolveTddPaths, resolveTddWorkingDirectory } from './runtime-context.js';
24
- import { baselineExists as defaultBaselineExists, clearBaselineData as defaultClearBaselineData, clearRunData as defaultClearRunData, getBaselinePath as defaultGetBaselinePath, getCurrentPath as defaultGetCurrentPath, getDiffPath as defaultGetDiffPath, initializeDirectories as defaultInitializeDirectories, saveBaseline as defaultSaveBaseline, saveCurrent as defaultSaveCurrent } from './services/baseline-manager.js';
25
- import { buildErrorComparison as defaultBuildErrorComparison, buildFailedComparison as defaultBuildFailedComparison, buildNewComparison as defaultBuildNewComparison, buildPassedComparison as defaultBuildPassedComparison, compareImages as defaultCompareImages, isDimensionMismatchError as defaultIsDimensionMismatchError } from './services/comparison-service.js';
26
- import { buildResults as defaultBuildResults, getFailedComparisons as defaultGetFailedComparisons, getNewComparisons as defaultGetNewComparisons } from './services/result-service.js';
27
33
 
28
34
  /**
29
35
  * Create a new TDD service instance
@@ -150,20 +156,13 @@ export class TddService {
150
156
  // Destructure dependencies
151
157
  let {
152
158
  output,
153
- colors,
154
159
  getDefaultBranch,
155
160
  getTddBaselines,
156
161
  getBuilds,
157
162
  getComparison,
158
163
  clearBaselineData,
159
164
  generateScreenshotSignature,
160
- generateBaselineFilename,
161
- sanitizeScreenshotName,
162
- safePath,
163
- existsSync,
164
- fetchWithTimeout,
165
- writeFileSync,
166
- saveBaselineMetadata
165
+ generateBaselineFilename
167
166
  } = this._deps;
168
167
 
169
168
  // If no branch specified, detect default branch
@@ -281,181 +280,7 @@ export class TddService {
281
280
  baselineBuild = apiResponse.build;
282
281
  baselineBuild.screenshots = apiResponse.screenshots;
283
282
  }
284
- let buildDetails = baselineBuild;
285
- if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
286
- output.warn('No screenshots found in baseline build');
287
- return null;
288
- }
289
- output.info(`Using baseline from build: ${colors.info(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
290
- output.info(`Checking ${colors.info(buildDetails.screenshots.length)} baseline screenshots...`);
291
-
292
- // Check existing baseline metadata for SHA comparison
293
- let existingBaseline = await this.loadBaseline();
294
- let existingShaMap = new Map();
295
- if (existingBaseline) {
296
- existingBaseline.screenshots.forEach(s => {
297
- if (s.sha256 && s.filename) {
298
- existingShaMap.set(s.filename, s.sha256);
299
- }
300
- });
301
- }
302
-
303
- // Download screenshots
304
- let downloadedCount = 0;
305
- let skippedCount = 0;
306
- let errorCount = 0;
307
- let batchSize = 5;
308
- let screenshotsToProcess = [];
309
- for (let screenshot of buildDetails.screenshots) {
310
- let sanitizedName;
311
- try {
312
- sanitizedName = sanitizeScreenshotName(screenshot.name);
313
- } catch (error) {
314
- output.warn(`Skipping screenshot with invalid name '${screenshot.name}': ${error.message}`);
315
- errorCount++;
316
- continue;
317
- }
318
- let filename = screenshot.filename;
319
- if (!filename) {
320
- output.warn(`Screenshot ${sanitizedName} has no filename from API - skipping`);
321
- errorCount++;
322
- continue;
323
- }
324
- let imagePath = safePath(this.baselinePath, filename);
325
-
326
- // Check SHA
327
- if (existsSync(imagePath) && screenshot.sha256) {
328
- let storedSha = existingShaMap.get(filename);
329
- if (storedSha === screenshot.sha256) {
330
- downloadedCount++;
331
- skippedCount++;
332
- continue;
333
- }
334
- }
335
- let downloadUrl = screenshot.original_url || screenshot.url;
336
- if (!downloadUrl) {
337
- output.warn(`Screenshot ${sanitizedName} has no download URL - skipping`);
338
- errorCount++;
339
- continue;
340
- }
341
- screenshotsToProcess.push({
342
- screenshot,
343
- sanitizedName,
344
- imagePath,
345
- downloadUrl,
346
- filename
347
- });
348
- }
349
-
350
- // Process downloads in batches
351
- if (screenshotsToProcess.length > 0) {
352
- output.info(`📥 Downloading ${screenshotsToProcess.length} new/updated screenshots...`);
353
- for (let i = 0; i < screenshotsToProcess.length; i += batchSize) {
354
- let batch = screenshotsToProcess.slice(i, i + batchSize);
355
- let batchNum = Math.floor(i / batchSize) + 1;
356
- let totalBatches = Math.ceil(screenshotsToProcess.length / batchSize);
357
- output.info(`📦 Processing batch ${batchNum}/${totalBatches}`);
358
- let downloadPromises = batch.map(async ({
359
- sanitizedName,
360
- imagePath,
361
- downloadUrl
362
- }) => {
363
- try {
364
- let response = await fetchWithTimeout(downloadUrl);
365
- if (!response.ok) {
366
- throw new NetworkError(`Failed to download ${sanitizedName}: ${response.statusText}`);
367
- }
368
- let arrayBuffer = await response.arrayBuffer();
369
- let imageBuffer = Buffer.from(arrayBuffer);
370
- writeFileSync(imagePath, imageBuffer);
371
- return {
372
- success: true,
373
- name: sanitizedName
374
- };
375
- } catch (error) {
376
- output.warn(`Failed to download ${sanitizedName}: ${error.message}`);
377
- return {
378
- success: false,
379
- name: sanitizedName,
380
- error: error.message
381
- };
382
- }
383
- });
384
- let batchResults = await Promise.all(downloadPromises);
385
- let batchSuccesses = batchResults.filter(r => r.success).length;
386
- let batchFailures = batchResults.filter(r => !r.success).length;
387
- downloadedCount += batchSuccesses;
388
- errorCount += batchFailures;
389
- }
390
- }
391
- if (downloadedCount === 0 && skippedCount === 0) {
392
- output.error('No screenshots were successfully downloaded');
393
- return null;
394
- }
395
-
396
- // Store baseline metadata
397
- this.baselineData = {
398
- buildId: baselineBuild.id,
399
- buildName: baselineBuild.name,
400
- environment,
401
- branch,
402
- threshold: this.threshold,
403
- signatureProperties: this.signatureProperties,
404
- createdAt: new Date().toISOString(),
405
- buildInfo: {
406
- commitSha: baselineBuild.commit_sha,
407
- commitMessage: baselineBuild.commit_message,
408
- visual_review: baselineBuild.visual_review || null,
409
- completedAt: baselineBuild.completed_at
410
- },
411
- screenshots: buildDetails.screenshots.filter(s => s.filename).map(s => ({
412
- name: sanitizeScreenshotName(s.name),
413
- originalName: s.name,
414
- sha256: s.sha256,
415
- id: s.id,
416
- filename: s.filename,
417
- path: safePath(this.baselinePath, s.filename),
418
- browser: s.browser,
419
- viewport_width: s.viewport_width,
420
- originalUrl: s.original_url,
421
- fileSize: s.file_size_bytes,
422
- dimensions: {
423
- width: s.width,
424
- height: s.height
425
- }
426
- }))
427
- };
428
- saveBaselineMetadata(this.baselinePath, this.baselineData);
429
-
430
- // Save baseline build metadata for MCP plugin
431
- let baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
432
- writeFileSync(baselineMetadataPath, JSON.stringify({
433
- buildId: baselineBuild.id,
434
- buildName: baselineBuild.name,
435
- branch,
436
- environment,
437
- commitSha: baselineBuild.commit_sha,
438
- commitMessage: baselineBuild.commit_message,
439
- visual_review: baselineBuild.visual_review || null,
440
- completedAt: baselineBuild.completed_at,
441
- downloadedAt: new Date().toISOString()
442
- }, null, 2));
443
-
444
- // Summary
445
- let actualDownloads = downloadedCount - skippedCount;
446
- if (skippedCount > 0) {
447
- if (actualDownloads === 0) {
448
- output.info(`All ${skippedCount} baselines up-to-date`);
449
- } else {
450
- output.info(`Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
451
- }
452
- } else {
453
- output.info(`Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
454
- }
455
- if (errorCount > 0) {
456
- output.warn(`${errorCount} screenshots failed to download`);
457
- }
458
- return this.baselineData;
283
+ return await this._installBaselineBuild(baselineBuild, environment, branch);
459
284
  } catch (error) {
460
285
  output.error(`Failed to download baseline: ${error.message}`);
461
286
  throw error;
@@ -473,14 +298,7 @@ export class TddService {
473
298
  // Destructure dependencies
474
299
  let {
475
300
  output,
476
- colors,
477
- clearBaselineData,
478
- sanitizeScreenshotName,
479
- safePath,
480
- existsSync,
481
- fetchWithTimeout,
482
- writeFileSync,
483
- saveBaselineMetadata
301
+ clearBaselineData
484
302
  } = this._deps;
485
303
 
486
304
  // Clear local state before downloading
@@ -506,181 +324,47 @@ export class TddService {
506
324
  output.warn(`Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
507
325
  }
508
326
  baselineBuild.screenshots = apiResponse.screenshots;
509
- let buildDetails = baselineBuild;
510
- if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
327
+ return this._installBaselineBuild(baselineBuild, 'test', null);
328
+ }
329
+ async _installBaselineBuild(baselineBuild, environment, branch) {
330
+ let {
331
+ output,
332
+ colors,
333
+ fetchWithTimeout,
334
+ existsSync,
335
+ writeFileSync,
336
+ safePath,
337
+ sanitizeScreenshotName,
338
+ saveBaselineMetadata
339
+ } = this._deps;
340
+ if (!baselineBuild.screenshots || baselineBuild.screenshots.length === 0) {
511
341
  output.warn('No screenshots found in baseline build');
512
342
  return null;
513
343
  }
514
344
  output.info(`Using baseline from build: ${colors.info(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
515
- output.info(`Checking ${colors.info(buildDetails.screenshots.length)} baseline screenshots...`);
516
-
517
- // Check existing baseline metadata for SHA comparison
345
+ output.info(`Checking ${colors.info(baselineBuild.screenshots.length)} baseline screenshots...`);
518
346
  let existingBaseline = await this.loadBaseline();
519
- let existingShaMap = new Map();
520
- if (existingBaseline) {
521
- existingBaseline.screenshots.forEach(s => {
522
- if (s.sha256 && s.filename) {
523
- existingShaMap.set(s.filename, s.sha256);
524
- }
525
- });
526
- }
527
-
528
- // Download screenshots
529
- let downloadedCount = 0;
530
- let skippedCount = 0;
531
- let errorCount = 0;
532
- let batchSize = 5;
533
- let screenshotsToProcess = [];
534
- for (let screenshot of buildDetails.screenshots) {
535
- let sanitizedName;
536
- try {
537
- sanitizedName = sanitizeScreenshotName(screenshot.name);
538
- } catch (error) {
539
- output.warn(`Skipping screenshot with invalid name '${screenshot.name}': ${error.message}`);
540
- errorCount++;
541
- continue;
542
- }
543
- let filename = screenshot.filename;
544
- if (!filename) {
545
- output.warn(`Screenshot ${sanitizedName} has no filename from API - skipping`);
546
- errorCount++;
547
- continue;
548
- }
549
- let imagePath = safePath(this.baselinePath, filename);
550
-
551
- // Check SHA
552
- if (existsSync(imagePath) && screenshot.sha256) {
553
- let storedSha = existingShaMap.get(filename);
554
- if (storedSha === screenshot.sha256) {
555
- downloadedCount++;
556
- skippedCount++;
557
- continue;
558
- }
559
- }
560
- let downloadUrl = screenshot.original_url || screenshot.url;
561
- if (!downloadUrl) {
562
- output.warn(`Screenshot ${sanitizedName} has no download URL - skipping`);
563
- errorCount++;
564
- continue;
565
- }
566
- screenshotsToProcess.push({
567
- screenshot,
568
- sanitizedName,
569
- imagePath,
570
- downloadUrl,
571
- filename
572
- });
573
- }
574
-
575
- // Process downloads in batches
576
- if (screenshotsToProcess.length > 0) {
577
- output.info(`📥 Downloading ${screenshotsToProcess.length} new/updated screenshots...`);
578
- for (let i = 0; i < screenshotsToProcess.length; i += batchSize) {
579
- let batch = screenshotsToProcess.slice(i, i + batchSize);
580
- let batchNum = Math.floor(i / batchSize) + 1;
581
- let totalBatches = Math.ceil(screenshotsToProcess.length / batchSize);
582
- output.info(`📦 Processing batch ${batchNum}/${totalBatches}`);
583
- let downloadPromises = batch.map(async ({
584
- sanitizedName,
585
- imagePath,
586
- downloadUrl
587
- }) => {
588
- try {
589
- let response = await fetchWithTimeout(downloadUrl);
590
- if (!response.ok) {
591
- throw new Error(`Failed to download ${sanitizedName}: ${response.statusText}`);
592
- }
593
- let arrayBuffer = await response.arrayBuffer();
594
- let imageBuffer = Buffer.from(arrayBuffer);
595
- writeFileSync(imagePath, imageBuffer);
596
- return {
597
- success: true,
598
- name: sanitizedName
599
- };
600
- } catch (error) {
601
- output.warn(`Failed to download ${sanitizedName}: ${error.message}`);
602
- return {
603
- success: false,
604
- name: sanitizedName,
605
- error: error.message
606
- };
607
- }
608
- });
609
- let batchResults = await Promise.all(downloadPromises);
610
- let batchSuccesses = batchResults.filter(r => r.success).length;
611
- let batchFailures = batchResults.filter(r => !r.success).length;
612
- downloadedCount += batchSuccesses;
613
- errorCount += batchFailures;
614
- }
615
- }
616
- if (downloadedCount === 0 && skippedCount === 0) {
617
- output.error('No screenshots were successfully downloaded');
618
- return null;
619
- }
620
-
621
- // Store baseline metadata
622
- this.baselineData = {
623
- buildId: baselineBuild.id,
624
- buildName: baselineBuild.name,
625
- environment: 'test',
626
- branch: null,
347
+ let installedBaseline = await installBaselineBuild({
348
+ baselineBuild,
349
+ baselinePath: this.baselinePath,
350
+ workingDir: this.workingDir,
627
351
  threshold: this.threshold,
628
352
  signatureProperties: this.signatureProperties,
629
- createdAt: new Date().toISOString(),
630
- buildInfo: {
631
- commitSha: baselineBuild.commit_sha,
632
- commitMessage: baselineBuild.commit_message,
633
- visual_review: baselineBuild.visual_review || null,
634
- completedAt: baselineBuild.completed_at
635
- },
636
- screenshots: buildDetails.screenshots.filter(s => s.filename).map(s => ({
637
- name: sanitizeScreenshotName(s.name),
638
- originalName: s.name,
639
- sha256: s.sha256,
640
- id: s.id,
641
- filename: s.filename,
642
- path: safePath(this.baselinePath, s.filename),
643
- browser: s.browser,
644
- viewport_width: s.viewport_width,
645
- originalUrl: s.original_url,
646
- fileSize: s.file_size_bytes,
647
- dimensions: {
648
- width: s.width,
649
- height: s.height
650
- }
651
- }))
652
- };
653
- saveBaselineMetadata(this.baselinePath, this.baselineData);
654
-
655
- // Save baseline build metadata for MCP plugin
656
- let baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
657
- writeFileSync(baselineMetadataPath, JSON.stringify({
658
- buildId: baselineBuild.id,
659
- buildName: baselineBuild.name,
660
- branch: null,
661
- environment: 'test',
662
- commitSha: baselineBuild.commit_sha,
663
- commitMessage: baselineBuild.commit_message,
664
- visual_review: baselineBuild.visual_review || null,
665
- completedAt: baselineBuild.completed_at,
666
- downloadedAt: new Date().toISOString()
667
- }, null, 2));
668
-
669
- // Summary
670
- let actualDownloads = downloadedCount - skippedCount;
671
- if (skippedCount > 0) {
672
- if (actualDownloads === 0) {
673
- output.info(`All ${skippedCount} baselines up-to-date`);
674
- } else {
675
- output.info(`Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
353
+ existingBaseline,
354
+ environment,
355
+ branch,
356
+ output,
357
+ fetchWithTimeout,
358
+ operations: {
359
+ existsSync,
360
+ writeFileSync,
361
+ safePath,
362
+ sanitizeScreenshotName,
363
+ saveBaselineMetadata
676
364
  }
677
- } else {
678
- output.info(`Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
679
- }
680
- if (errorCount > 0) {
681
- output.warn(`${errorCount} screenshots failed to download`);
682
- }
683
- return this.baselineData;
365
+ });
366
+ if (installedBaseline) this.baselineData = installedBaseline;
367
+ return installedBaseline;
684
368
  }
685
369
 
686
370
  /**
@@ -1079,6 +763,26 @@ export class TddService {
1079
763
  }
1080
764
  return results;
1081
765
  }
766
+ _writeBaselineScreenshot(screenshot, imageBuffer) {
767
+ let {
768
+ writeFileSync,
769
+ createEmptyBaselineMetadata,
770
+ upsertScreenshotInMetadata
771
+ } = this._deps;
772
+ this.baselineData = writeBaselineScreenshot({
773
+ imageBuffer,
774
+ screenshot,
775
+ metadata: this.baselineData,
776
+ baselineOptions: {
777
+ threshold: this.threshold,
778
+ signatureProperties: this.signatureProperties
779
+ }
780
+ }, {
781
+ writeFileSync,
782
+ createEmptyBaselineMetadata,
783
+ upsertScreenshotInMetadata
784
+ });
785
+ }
1082
786
 
1083
787
  /**
1084
788
  * Update all baselines with current screenshots
@@ -1094,9 +798,7 @@ export class TddService {
1094
798
  getBaselinePath,
1095
799
  existsSync,
1096
800
  readFileSync,
1097
- writeFileSync,
1098
801
  createEmptyBaselineMetadata,
1099
- upsertScreenshotInMetadata,
1100
802
  saveBaselineMetadata
1101
803
  } = this._deps;
1102
804
  if (this.comparisons.length === 0) {
@@ -1132,14 +834,12 @@ export class TddService {
1132
834
  let baselineImagePath = getBaselinePath(this.baselinePath, filename);
1133
835
  try {
1134
836
  let currentBuffer = readFileSync(current);
1135
- writeFileSync(baselineImagePath, currentBuffer);
1136
- let screenshotEntry = {
837
+ this._writeBaselineScreenshot({
1137
838
  name: sanitizedName,
1138
839
  properties: validatedProperties,
1139
840
  path: baselineImagePath,
1140
841
  signature
1141
- };
1142
- upsertScreenshotInMetadata(this.baselineData, screenshotEntry, signature);
842
+ }, currentBuffer);
1143
843
  updatedCount++;
1144
844
  output.info(`Updated baseline for ${sanitizedName}`);
1145
845
  } catch (error) {
@@ -1171,9 +871,6 @@ export class TddService {
1171
871
  existsSync,
1172
872
  readFileSync,
1173
873
  mkdirSync,
1174
- writeFileSync,
1175
- createEmptyBaselineMetadata,
1176
- upsertScreenshotInMetadata,
1177
874
  saveBaselineMetadata
1178
875
  } = this._deps;
1179
876
  let comparison;
@@ -1219,22 +916,12 @@ export class TddService {
1219
916
 
1220
917
  // Update the baseline (filename already includes .png extension)
1221
918
  let baselineImagePath = safePath(this.baselinePath, filename);
1222
- writeFileSync(baselineImagePath, imageBuffer);
1223
-
1224
- // Update baseline metadata
1225
- if (!this.baselineData) {
1226
- this.baselineData = createEmptyBaselineMetadata({
1227
- threshold: this.threshold,
1228
- signatureProperties: this.signatureProperties
1229
- });
1230
- }
1231
- let screenshotEntry = {
919
+ this._writeBaselineScreenshot({
1232
920
  name: sanitizedName,
1233
921
  properties,
1234
922
  path: baselineImagePath,
1235
923
  signature
1236
- };
1237
- upsertScreenshotInMetadata(this.baselineData, screenshotEntry, signature);
924
+ }, imageBuffer);
1238
925
  saveBaselineMetadata(this.baselinePath, this.baselineData);
1239
926
  return {
1240
927
  name: sanitizedName,
@@ -1253,27 +940,16 @@ export class TddService {
1253
940
  output,
1254
941
  generateScreenshotSignature,
1255
942
  generateComparisonId,
1256
- writeFileSync,
1257
- createEmptyBaselineMetadata,
1258
- upsertScreenshotInMetadata,
1259
943
  saveBaselineMetadata
1260
944
  } = this._deps;
1261
945
  output.info(`Creating baseline for ${name}`);
1262
- writeFileSync(baselineImagePath, imageBuffer);
1263
- if (!this.baselineData) {
1264
- this.baselineData = createEmptyBaselineMetadata({
1265
- threshold: this.threshold,
1266
- signatureProperties: this.signatureProperties
1267
- });
1268
- }
1269
946
  let signature = generateScreenshotSignature(name, properties || {}, this.signatureProperties);
1270
- let screenshotEntry = {
947
+ this._writeBaselineScreenshot({
1271
948
  name,
1272
949
  properties: properties || {},
1273
950
  path: baselineImagePath,
1274
951
  signature
1275
- };
1276
- upsertScreenshotInMetadata(this.baselineData, screenshotEntry, signature);
952
+ }, imageBuffer);
1277
953
  saveBaselineMetadata(this.baselinePath, this.baselineData);
1278
954
  let result = {
1279
955
  id: generateComparisonId(signature),