@vizzly-testing/cli 0.39.1 → 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 +8 -0
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +41 -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} +4 -5
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +10 -41
  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/{utils → domains/updates}/cli-update.js +4 -6
  118. package/dist/{utils → domains/updates}/update-notifications.js +7 -7
  119. package/dist/domains/uploads/upload-defaults.js +4 -0
  120. package/dist/domains/uploads/upload-files.js +32 -0
  121. package/dist/domains/uploads/upload-settings.js +21 -0
  122. package/dist/domains/uploads/upload-timing.js +18 -0
  123. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  124. package/dist/domains/uploads/upload.normalizers.js +60 -0
  125. package/dist/domains/uploads/upload.serializers.js +179 -0
  126. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  127. package/dist/domains/uploads/upload.validations.js +77 -0
  128. package/dist/index.js +12 -12
  129. package/dist/reporter/reporter-bundle.iife.js +86 -86
  130. package/dist/reporter-ssr/ssr-entry.js +2 -2
  131. package/dist/shared/cloud-protocol.js +171 -0
  132. package/dist/{utils → shared}/file-changes.js +1 -1
  133. package/dist/shared/files.js +9 -0
  134. package/dist/{utils → shared}/global-config.js +7 -78
  135. package/dist/shared/log-level.js +7 -0
  136. package/dist/{utils → shared}/output.js +1 -1
  137. package/dist/{utils → shared}/package-info.js +5 -4
  138. package/dist/shared/paths.js +57 -0
  139. package/package.json +23 -22
  140. package/dist/api/core.js +0 -382
  141. package/dist/api/endpoints.js +0 -421
  142. package/dist/api/index.js +0 -19
  143. package/dist/auth/core.js +0 -237
  144. package/dist/auth/index.js +0 -31
  145. package/dist/commands/context.js +0 -1066
  146. package/dist/config/core.js +0 -439
  147. package/dist/config/operations.js +0 -327
  148. package/dist/project/core.js +0 -211
  149. package/dist/screenshot-server/core.js +0 -161
  150. package/dist/screenshot-server/index.js +0 -11
  151. package/dist/server/routers/assets.js +0 -95
  152. package/dist/server/routers/cloud-proxy.js +0 -146
  153. package/dist/server/routers/dashboard.js +0 -172
  154. package/dist/services/auth-service.js +0 -117
  155. package/dist/services/config-service.js +0 -313
  156. package/dist/services/project-service.js +0 -88
  157. package/dist/test-runner/core.js +0 -293
  158. package/dist/test-runner/index.js +0 -13
  159. package/dist/types/sdk.d.ts +0 -186
  160. package/dist/uploader/core.js +0 -432
  161. package/dist/uploader/index.js +0 -88
  162. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  163. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  164. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  166. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  167. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  168. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  169. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  170. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  171. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  172. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  173. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  174. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  175. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  176. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  177. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  178. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  179. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  180. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  181. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  182. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  183. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  184. /package/dist/{utils → domains/updates}/update-check.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
@@ -0,0 +1,192 @@
1
+ import { existsSync as defaultExistsSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
2
+ import { fetchWithTimeout as defaultFetchWithTimeout } from '../../shared/fetch-utils.js';
3
+ import { safePath as defaultSafePath } from '../../shared/paths.js';
4
+ import { NetworkError } from '../../shared/vizzly-error.js';
5
+ import { sanitizeScreenshotName as defaultSanitizeScreenshotName } from '../screenshots/screenshot.validations.js';
6
+ import { saveBaselineMetadata as defaultSaveBaselineMetadata } from './baseline-metadata.js';
7
+
8
+ /** Install one selected cloud build without changing how it was selected. */
9
+ export async function installBaselineBuild({
10
+ baselineBuild,
11
+ baselinePath,
12
+ workingDir,
13
+ threshold,
14
+ signatureProperties,
15
+ existingBaseline,
16
+ environment,
17
+ branch,
18
+ output,
19
+ fetchWithTimeout = defaultFetchWithTimeout,
20
+ operations = {}
21
+ }) {
22
+ let {
23
+ existsSync = defaultExistsSync,
24
+ writeFileSync = defaultWriteFileSync,
25
+ safePath = defaultSafePath,
26
+ sanitizeScreenshotName = defaultSanitizeScreenshotName,
27
+ saveBaselineMetadata = defaultSaveBaselineMetadata
28
+ } = operations;
29
+ let existingShaMap = new Map();
30
+ if (existingBaseline) {
31
+ existingBaseline.screenshots.forEach(screenshot => {
32
+ if (screenshot.sha256 && screenshot.filename) {
33
+ existingShaMap.set(screenshot.filename, screenshot.sha256);
34
+ }
35
+ });
36
+ }
37
+
38
+ // Download screenshots
39
+ let downloadedCount = 0;
40
+ let skippedCount = 0;
41
+ let errorCount = 0;
42
+ let batchSize = 5;
43
+ let screenshotsToProcess = [];
44
+ for (let screenshot of baselineBuild.screenshots) {
45
+ let sanitizedName;
46
+ try {
47
+ sanitizedName = sanitizeScreenshotName(screenshot.name);
48
+ } catch (error) {
49
+ output.warn(`Skipping screenshot with invalid name '${screenshot.name}': ${error.message}`);
50
+ errorCount++;
51
+ continue;
52
+ }
53
+ let filename = screenshot.filename;
54
+ if (!filename) {
55
+ output.warn(`Screenshot ${sanitizedName} has no filename from API - skipping`);
56
+ errorCount++;
57
+ continue;
58
+ }
59
+ let imagePath = safePath(baselinePath, filename);
60
+
61
+ // Check SHA
62
+ if (existsSync(imagePath) && screenshot.sha256) {
63
+ let storedSha = existingShaMap.get(filename);
64
+ if (storedSha === screenshot.sha256) {
65
+ downloadedCount++;
66
+ skippedCount++;
67
+ continue;
68
+ }
69
+ }
70
+ let downloadUrl = screenshot.original_url || screenshot.url;
71
+ if (!downloadUrl) {
72
+ output.warn(`Screenshot ${sanitizedName} has no download URL - skipping`);
73
+ errorCount++;
74
+ continue;
75
+ }
76
+ screenshotsToProcess.push({
77
+ sanitizedName,
78
+ imagePath,
79
+ downloadUrl
80
+ });
81
+ }
82
+
83
+ // Process downloads in batches
84
+ if (screenshotsToProcess.length > 0) {
85
+ output.info(`📥 Downloading ${screenshotsToProcess.length} new/updated screenshots...`);
86
+ for (let i = 0; i < screenshotsToProcess.length; i += batchSize) {
87
+ let batch = screenshotsToProcess.slice(i, i + batchSize);
88
+ let batchNum = Math.floor(i / batchSize) + 1;
89
+ let totalBatches = Math.ceil(screenshotsToProcess.length / batchSize);
90
+ output.info(`📦 Processing batch ${batchNum}/${totalBatches}`);
91
+ let downloadPromises = batch.map(async ({
92
+ sanitizedName,
93
+ imagePath,
94
+ downloadUrl
95
+ }) => {
96
+ try {
97
+ let response = await fetchWithTimeout(downloadUrl);
98
+ if (!response.ok) {
99
+ throw new NetworkError(`Failed to download ${sanitizedName}: ${response.statusText}`);
100
+ }
101
+ let arrayBuffer = await response.arrayBuffer();
102
+ let imageBuffer = Buffer.from(arrayBuffer);
103
+ writeFileSync(imagePath, imageBuffer);
104
+ return {
105
+ success: true,
106
+ name: sanitizedName
107
+ };
108
+ } catch (error) {
109
+ output.warn(`Failed to download ${sanitizedName}: ${error.message}`);
110
+ return {
111
+ success: false,
112
+ name: sanitizedName,
113
+ error: error.message
114
+ };
115
+ }
116
+ });
117
+ let batchResults = await Promise.all(downloadPromises);
118
+ let batchSuccesses = batchResults.filter(r => r.success).length;
119
+ let batchFailures = batchResults.filter(r => !r.success).length;
120
+ downloadedCount += batchSuccesses;
121
+ errorCount += batchFailures;
122
+ }
123
+ }
124
+ if (downloadedCount === 0 && skippedCount === 0) {
125
+ output.error('No screenshots were successfully downloaded');
126
+ return null;
127
+ }
128
+
129
+ // Store baseline metadata
130
+ let baselineData = {
131
+ buildId: baselineBuild.id,
132
+ buildName: baselineBuild.name,
133
+ environment,
134
+ branch,
135
+ threshold,
136
+ signatureProperties,
137
+ createdAt: new Date().toISOString(),
138
+ buildInfo: {
139
+ commitSha: baselineBuild.commit_sha,
140
+ commitMessage: baselineBuild.commit_message,
141
+ visual_review: baselineBuild.visual_review || null,
142
+ completedAt: baselineBuild.completed_at
143
+ },
144
+ screenshots: baselineBuild.screenshots.filter(s => s.filename).map(s => ({
145
+ name: sanitizeScreenshotName(s.name),
146
+ originalName: s.name,
147
+ sha256: s.sha256,
148
+ id: s.id,
149
+ filename: s.filename,
150
+ path: safePath(baselinePath, s.filename),
151
+ browser: s.browser,
152
+ viewport_width: s.viewport_width,
153
+ originalUrl: s.original_url,
154
+ fileSize: s.file_size_bytes,
155
+ dimensions: {
156
+ width: s.width,
157
+ height: s.height
158
+ }
159
+ }))
160
+ };
161
+ saveBaselineMetadata(baselinePath, baselineData);
162
+
163
+ // Save baseline build metadata for MCP plugin
164
+ let baselineMetadataPath = safePath(workingDir, '.vizzly', 'baseline-metadata.json');
165
+ writeFileSync(baselineMetadataPath, JSON.stringify({
166
+ buildId: baselineBuild.id,
167
+ buildName: baselineBuild.name,
168
+ branch,
169
+ environment,
170
+ commitSha: baselineBuild.commit_sha,
171
+ commitMessage: baselineBuild.commit_message,
172
+ visual_review: baselineBuild.visual_review || null,
173
+ completedAt: baselineBuild.completed_at,
174
+ downloadedAt: new Date().toISOString()
175
+ }, null, 2));
176
+
177
+ // Summary
178
+ let actualDownloads = downloadedCount - skippedCount;
179
+ if (skippedCount > 0) {
180
+ if (actualDownloads === 0) {
181
+ output.info(`All ${skippedCount} baselines up-to-date`);
182
+ } else {
183
+ output.info(`Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
184
+ }
185
+ } else {
186
+ output.info(`Downloaded ${downloadedCount}/${baselineBuild.screenshots.length} screenshots successfully`);
187
+ }
188
+ if (errorCount > 0) {
189
+ output.warn(`${errorCount} screenshots failed to download`);
190
+ }
191
+ return baselineData;
192
+ }
@@ -0,0 +1,22 @@
1
+ import { writeFileSync as defaultWriteFileSync } from 'node:fs';
2
+ import { createEmptyBaselineMetadata as defaultCreateEmptyBaselineMetadata, upsertScreenshotInMetadata as defaultUpsertScreenshotInMetadata } from './baseline-metadata.js';
3
+
4
+ /**
5
+ * Write one image and update its baseline record. The caller commits metadata
6
+ * after a single approval or after its full bulk update has finished.
7
+ */
8
+ export function writeBaselineScreenshot({
9
+ imageBuffer,
10
+ screenshot,
11
+ metadata,
12
+ baselineOptions
13
+ }, {
14
+ writeFileSync = defaultWriteFileSync,
15
+ createEmptyBaselineMetadata = defaultCreateEmptyBaselineMetadata,
16
+ upsertScreenshotInMetadata = defaultUpsertScreenshotInMetadata
17
+ } = {}) {
18
+ writeFileSync(screenshot.path, imageBuffer);
19
+ let baselineData = metadata || createEmptyBaselineMetadata(baselineOptions);
20
+ upsertScreenshotInMetadata(baselineData, screenshot, screenshot.signature);
21
+ return baselineData;
22
+ }
@@ -3,9 +3,9 @@
3
3
  * Handles baseline management (accept, accept-all, reset)
4
4
  */
5
5
 
6
- import * as output from '../../utils/output.js';
7
- import { parseJsonBody } from '../middleware/json-parser.js';
8
- import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
6
+ import { parseJsonBody } from '../../adapters/http/middleware/json-parser.js';
7
+ import { sendError, sendServiceUnavailable, sendSuccess } from '../../adapters/http/middleware/response.js';
8
+ import * as output from '../../shared/output.js';
9
9
  function sendBaselineUnavailable(res) {
10
10
  sendError(res, 400, 'Baseline management not available');
11
11
  }
@@ -5,7 +5,7 @@
5
5
  * of baseline storage without any network operations.
6
6
  */
7
7
 
8
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
10
 
11
11
  /**
@@ -148,42 +148,4 @@ export function getCurrentPath(currentPath, filename) {
148
148
  */
149
149
  export function getDiffPath(diffPath, filename) {
150
150
  return join(diffPath, filename);
151
- }
152
-
153
- /**
154
- * Promote current screenshot to baseline (accept as new baseline)
155
- *
156
- * @param {string} currentPath - Path to current screenshots directory
157
- * @param {string} baselinePath - Path to baselines directory
158
- * @param {string} filename - Filename
159
- */
160
- export function promoteCurrentToBaseline(currentPath, baselinePath, filename) {
161
- let currentFile = join(currentPath, filename);
162
- let baselineFile = join(baselinePath, filename);
163
- if (!existsSync(currentFile)) {
164
- throw new Error(`Current screenshot not found: ${currentFile}`);
165
- }
166
- copyFileSync(currentFile, baselineFile);
167
- }
168
-
169
- /**
170
- * Read baseline image
171
- *
172
- * @param {string} baselinePath - Path to baselines directory
173
- * @param {string} filename - Filename
174
- * @returns {Buffer}
175
- */
176
- export function readBaseline(baselinePath, filename) {
177
- return readFileSync(join(baselinePath, filename));
178
- }
179
-
180
- /**
181
- * Read current screenshot
182
- *
183
- * @param {string} currentPath - Path to current screenshots directory
184
- * @param {string} filename - Filename
185
- * @returns {Buffer}
186
- */
187
- export function readCurrent(currentPath, filename) {
188
- return readFileSync(join(currentPath, filename));
189
151
  }
@@ -4,9 +4,9 @@
4
4
 
5
5
  import { existsSync as defaultExistsSync, readdirSync as defaultReaddirSync, statSync as defaultStatSync } from 'node:fs';
6
6
  import { basename, join, resolve } from 'node:path';
7
- import { loadBaselineMetadata as defaultLoadBaselineMetadata } from '../tdd/metadata/baseline-metadata.js';
8
- import * as defaultOutput from '../utils/output.js';
9
- import { createWildcardMatcher } from '../utils/patterns.js';
7
+ import * as defaultOutput from '../../shared/output.js';
8
+ import { createWildcardMatcher } from '../../shared/patterns.js';
9
+ import { loadBaselineMetadata as defaultLoadBaselineMetadata } from './baseline-metadata.js';
10
10
 
11
11
  /**
12
12
  * Extract filename from a path
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Get TDD baselines for a build
3
+ * @param {Object} client - API client
4
+ * @param {string} buildId - Build ID
5
+ * @returns {Promise<Object>} { build, screenshots, signatureProperties }
6
+ */
7
+ export async function getTddBaselines(client, buildId) {
8
+ return client.request(`/api/sdk/builds/${buildId}/tdd-baselines`);
9
+ }
@@ -0,0 +1,42 @@
1
+ import { parseJsonBody } from '../../adapters/http/middleware/json-parser.js';
2
+ import { sendError, sendSuccess } from '../../adapters/http/middleware/response.js';
3
+ import * as output from '../../shared/output.js';
4
+ export function createCloudBaselinesRouter({
5
+ authService
6
+ }) {
7
+ return async function handleCloudBaselinesRoute(req, res, pathname) {
8
+ if (req.method !== 'POST' || pathname !== '/api/cloud/baselines/download') {
9
+ return false;
10
+ }
11
+ try {
12
+ let body = await parseJsonBody(req);
13
+ let {
14
+ buildId,
15
+ screenshotNames
16
+ } = body;
17
+ if (!buildId) {
18
+ sendError(res, 400, 'buildId is required');
19
+ return true;
20
+ }
21
+ let response = await authService.authenticatedRequest('/api/cli/baselines/download', {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json'
25
+ },
26
+ body: JSON.stringify({
27
+ buildId,
28
+ screenshotNames
29
+ })
30
+ });
31
+ sendSuccess(res, {
32
+ success: true,
33
+ message: `Baselines downloaded from build ${buildId}`,
34
+ ...response
35
+ });
36
+ } catch (error) {
37
+ output.error('Error downloading baselines from cloud:', error);
38
+ sendError(res, 500, error.message);
39
+ }
40
+ return true;
41
+ };
42
+ }
@@ -0,0 +1,35 @@
1
+ import { getVisualReviewState } from '../context/context.normalizers.js';
2
+ export function normalizeBuildStatus(buildStatus) {
3
+ return buildStatus.build;
4
+ }
5
+
6
+ /**
7
+ * Read the processing counts from the status response.
8
+ *
9
+ * @param {Object} status - Status response.
10
+ * @returns {Object} API-provided processing facts.
11
+ */
12
+ export function getProcessingStatus(status = {}) {
13
+ return status.processing;
14
+ }
15
+
16
+ /**
17
+ * Read comparison totals without deriving them from screenshot counts.
18
+ *
19
+ * @param {Object} status - Status response.
20
+ * @returns {Object} API-provided comparison facts.
21
+ */
22
+ export function getComparisonStatus(status = {}) {
23
+ return status.comparisons;
24
+ }
25
+
26
+ /**
27
+ * Read the build review state without mixing it with processing status.
28
+ *
29
+ * @param {Object} status - Status response.
30
+ * @returns {string|null} API-provided review state when available.
31
+ */
32
+ export function getBuildReviewState(status = {}) {
33
+ let build = normalizeBuildStatus(status);
34
+ return getVisualReviewState(build);
35
+ }
@@ -0,0 +1,164 @@
1
+ import { getComparisonStatus, getProcessingStatus, normalizeBuildStatus } from './build.normalizers.js';
2
+
3
+ /**
4
+ * Build payload for build creation
5
+ * @param {Object} options - Build options
6
+ * @returns {Object} Build creation payload
7
+ */
8
+ export function buildBuildPayload(options) {
9
+ let payload = {
10
+ name: options.name || options.buildName,
11
+ branch: options.branch,
12
+ environment: options.environment
13
+ };
14
+ if (options.commit || options.commit_sha) {
15
+ payload.commit_sha = options.commit || options.commit_sha;
16
+ }
17
+ if (options.message || options.commit_message) {
18
+ payload.commit_message = options.message || options.commit_message;
19
+ }
20
+ if (options.commit_author_name) {
21
+ payload.commit_author_name = options.commit_author_name;
22
+ }
23
+ if (options.commit_author_email) {
24
+ payload.commit_author_email = options.commit_author_email;
25
+ }
26
+ if (options.pullRequestNumber || options.github_pull_request_number) {
27
+ payload.github_pull_request_number = options.pullRequestNumber || options.github_pull_request_number;
28
+ }
29
+ if (options.parallelId || options.parallel_id) {
30
+ payload.parallel_id = options.parallelId || options.parallel_id;
31
+ }
32
+ if (options.threshold != null) {
33
+ payload.threshold = options.threshold;
34
+ }
35
+ if (options.metadata) {
36
+ payload.metadata = options.metadata;
37
+ }
38
+ return payload;
39
+ }
40
+
41
+ /**
42
+ * Format a build for JSON output
43
+ */
44
+ export function formatBuildForJson(build, includeComparisons = false) {
45
+ let result = {
46
+ id: build.id,
47
+ name: build.name,
48
+ status: build.status,
49
+ branch: build.branch,
50
+ commit: build.commit_sha,
51
+ commitMessage: build.commit_message,
52
+ environment: build.environment,
53
+ screenshotCount: build.screenshot_count || 0,
54
+ comparisons: {
55
+ total: build.total_comparisons || 0,
56
+ new: build.new_comparisons || 0,
57
+ changed: build.changed_comparisons || 0,
58
+ identical: build.identical_comparisons || 0
59
+ },
60
+ visual_review: build.visual_review || null,
61
+ createdAt: build.created_at,
62
+ completedAt: build.completed_at
63
+ };
64
+ if (includeComparisons && build.comparisons) {
65
+ result.comparisonDetails = build.comparisons.map(c => {
66
+ let diffUrl = c.diff_image?.url || c.diff_image_url || c.diff_url || null;
67
+ let diffImage = c.diff_image || {};
68
+ let clusterMetadata = c.cluster_metadata || diffImage.cluster_metadata || null;
69
+ let ssimScore = c.ssim_score ?? diffImage.ssim_score ?? null;
70
+ let gmsdScore = c.gmsd_score ?? diffImage.gmsd_score ?? null;
71
+ let fingerprintHash = c.fingerprint_hash || diffImage.fingerprint_hash || null;
72
+ let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash;
73
+ return {
74
+ id: c.id,
75
+ name: c.name || c.current_name,
76
+ status: c.status,
77
+ diffPercentage: c.diff_percentage,
78
+ visual_review: c.visual_review || null,
79
+ urls: {
80
+ baseline: c.baseline_screenshot?.original_url || c.baseline_original_url || c.baseline_screenshot_url || null,
81
+ current: c.current_screenshot?.original_url || c.current_original_url || c.current_screenshot_url || null,
82
+ diff: diffUrl
83
+ },
84
+ honeydiff: hasHoneydiff ? {
85
+ ssimScore,
86
+ gmsdScore,
87
+ clusterClassification: clusterMetadata?.classification || null,
88
+ clusterMetadata,
89
+ fingerprintHash,
90
+ diffRegions: c.diff_regions ?? diffImage.diff_regions ?? null
91
+ } : null
92
+ };
93
+ });
94
+ }
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * Give agents concrete paths from lifecycle status into visual evidence.
100
+ *
101
+ * @param {Object} build - Build record from the status response.
102
+ * @returns {{label: string, command: string}[]} Executable CLI commands.
103
+ */
104
+ export function createStatusSuggestedCommands(build = {}) {
105
+ if (!build.id) {
106
+ return [];
107
+ }
108
+ return [{
109
+ label: 'Discover build context API',
110
+ command: 'vizzly api schema sdk.getBuildContext --json'
111
+ }, {
112
+ label: 'List comparisons',
113
+ command: `vizzly --json comparisons --build ${build.id}`
114
+ }];
115
+ }
116
+
117
+ /**
118
+ * Every lifecycle, processing, comparison, and review value comes directly
119
+ * from the API.
120
+ *
121
+ * @param {Object} status - Status response.
122
+ * @param {Object|null} previewInfo - Optional preview response.
123
+ * @returns {Object} Machine-readable status payload.
124
+ */
125
+ export function createStatusData(status, previewInfo = null) {
126
+ let build = normalizeBuildStatus(status);
127
+ let processing = getProcessingStatus(status);
128
+ let comparisons = getComparisonStatus(status);
129
+ return {
130
+ resource: status.resource,
131
+ schemaVersion: status.schema_version,
132
+ buildId: build.id,
133
+ status: build.status,
134
+ conclusion: status.conclusion,
135
+ name: build.name,
136
+ createdAt: build.created_at,
137
+ updatedAt: build.updated_at,
138
+ completedAt: build.completed_at,
139
+ environment: build.environment,
140
+ branch: build.branch,
141
+ commit: build.commit_sha,
142
+ commitMessage: build.commit_message,
143
+ screenshotsTotal: processing.total,
144
+ processing,
145
+ comparisonsTotal: comparisons.total,
146
+ comparisons,
147
+ newComparisons: comparisons.new,
148
+ changedComparisons: comparisons.changed,
149
+ identicalComparisons: comparisons.identical,
150
+ visual_review: build.visual_review || null,
151
+ executionTime: build.execution_time_ms,
152
+ isBaseline: build.is_baseline,
153
+ userAgent: build.user_agent,
154
+ scope: status.scope,
155
+ links: status.links,
156
+ suggestedCommands: createStatusSuggestedCommands(build),
157
+ preview: previewInfo ? {
158
+ url: previewInfo.preview_url,
159
+ status: previewInfo.status,
160
+ fileCount: previewInfo.file_count,
161
+ expiresAt: previewInfo.expires_at
162
+ } : null
163
+ };
164
+ }
@@ -0,0 +1,119 @@
1
+ import { buildEndpointWithParams, buildQueryParams } from '../../shared/cloud-protocol.js';
2
+ import { buildBuildPayload } from './build.serializers.js';
3
+
4
+ /**
5
+ * Get build information
6
+ * @param {Object} client - API client
7
+ * @param {string} buildId - Build ID
8
+ * @param {string|null} include - Optional include parameter (e.g., 'screenshots')
9
+ * @returns {Promise<Object>} Build data
10
+ */
11
+ export async function getBuild(client, buildId, include = null) {
12
+ let endpoint = `/api/sdk/builds/${buildId}`;
13
+ if (include) {
14
+ endpoint = buildEndpointWithParams(endpoint, {
15
+ include
16
+ });
17
+ }
18
+ return client.request(endpoint);
19
+ }
20
+
21
+ /**
22
+ * Get the server-owned build processing and review status.
23
+ *
24
+ * This endpoint keeps processing, comparison, and review counts separate so
25
+ * callers do not have to reconstruct lifecycle state from legacy build fields.
26
+ *
27
+ * @param {Object} client - API client
28
+ * @param {string} buildId - Build ID
29
+ * @returns {Promise<Object>} Build status response
30
+ */
31
+ export async function getBuildStatus(client, buildId) {
32
+ return client.request(`/api/sdk/builds/${buildId}/status`);
33
+ }
34
+
35
+ /**
36
+ * Get builds for a project
37
+ * @param {Object} client - API client
38
+ * @param {Object} filters - Filter options
39
+ * @returns {Promise<Array>} List of builds
40
+ */
41
+ export async function getBuilds(client, filters = {}) {
42
+ let query = buildQueryParams(filters);
43
+ let endpoint = `/api/sdk/builds${query ? `?${query}` : ''}`;
44
+ return client.request(endpoint);
45
+ }
46
+
47
+ /**
48
+ * Create a new build
49
+ * @param {Object} client - API client
50
+ * @param {Object} metadata - Build metadata
51
+ * @returns {Promise<Object>} Created build data
52
+ */
53
+ export async function createBuild(client, metadata) {
54
+ let payload = buildBuildPayload(metadata);
55
+ return client.request('/api/sdk/builds', {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Content-Type': 'application/json'
59
+ },
60
+ body: JSON.stringify({
61
+ build: payload
62
+ })
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Update build status
68
+ * @param {Object} client - API client
69
+ * @param {string} buildId - Build ID
70
+ * @param {string} status - Build status (pending|running|completed|failed)
71
+ * @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
72
+ * @param {string} [failureReason] - Explanation sent only for a failed build
73
+ * @returns {Promise<Object>} Updated build data
74
+ */
75
+ export async function updateBuildStatus(client, buildId, status, executionTimeMs = null, failureReason) {
76
+ let body = {
77
+ status
78
+ };
79
+ if (status === 'failed' && failureReason) body.failureReason = failureReason;
80
+ if (executionTimeMs != null) {
81
+ body.executionTimeMs = executionTimeMs;
82
+ }
83
+ return client.request(`/api/sdk/builds/${buildId}/status`, {
84
+ method: 'PUT',
85
+ headers: {
86
+ 'Content-Type': 'application/json'
87
+ },
88
+ body: JSON.stringify(body)
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Mark a build completed or failed, with an optional failure explanation.
94
+ * @param {Object} client - API client
95
+ * @param {string} buildId - Build ID
96
+ * @param {boolean} [success=true] - Whether tests and uploads succeeded
97
+ * @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
98
+ * @param {string} [failureReason] - Explanation sent only for a failed build
99
+ * @returns {Promise<Object>} Finalized build data
100
+ */
101
+ export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null, failureReason) {
102
+ let status = success ? 'completed' : 'failed';
103
+ return updateBuildStatus(client, buildId, status, executionTimeMs, failureReason);
104
+ }
105
+
106
+ /**
107
+ * Finalize a parallel build
108
+ * @param {Object} client - API client
109
+ * @param {string} parallelId - Parallel ID to finalize
110
+ * @returns {Promise<Object>} Finalization result
111
+ */
112
+ export async function finalizeParallelBuild(client, parallelId) {
113
+ return client.request(`/api/sdk/parallel/${parallelId}/finalize`, {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json'
117
+ }
118
+ });
119
+ }