@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,39 @@
1
+ /**
2
+ * Validate builds command options
3
+ */
4
+ export function validateBuildsOptions(options = {}) {
5
+ let errors = [];
6
+ if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 250)) {
7
+ errors.push('--limit must be an integer between 1 and 250');
8
+ }
9
+ if (options.offset !== undefined && (!Number.isInteger(options.offset) || options.offset < 0)) {
10
+ errors.push('--offset must be a non-negative integer');
11
+ }
12
+ return errors;
13
+ }
14
+
15
+ /**
16
+ * Validate status options
17
+ * @param {string} buildId - Build ID to check
18
+ * @param {Object} options - Command options
19
+ */
20
+ export function validateStatusOptions(buildId) {
21
+ let errors = [];
22
+ if (!buildId || buildId.trim() === '') {
23
+ errors.push('Build ID is required');
24
+ }
25
+ return errors;
26
+ }
27
+
28
+ /**
29
+ * Validate finalize options
30
+ * @param {string} parallelId - Parallel ID to finalize
31
+ * @param {Object} options - Command options
32
+ */
33
+ export function validateFinalizeOptions(parallelId, _options) {
34
+ let errors = [];
35
+ if (!parallelId || parallelId.trim() === '') {
36
+ errors.push('Parallel ID is required');
37
+ }
38
+ return errors;
39
+ }
@@ -1,10 +1,13 @@
1
+ import { formatBuildForJson } from './build.serializers.js';
2
+
1
3
  /**
2
4
  * Builds command - list and query builds
3
5
  */
4
6
 
5
- import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild, getBuilds as defaultGetBuilds } from '../api/index.js';
6
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
- import * as defaultOutput from '../utils/output.js';
7
+ import * as defaultOutput from '../../shared/output.js';
8
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
9
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
10
+ import { getBuild as defaultGetBuild, getBuilds as defaultGetBuilds } from './build.service.js';
8
11
 
9
12
  /**
10
13
  * Builds list command - list builds with optional filters
@@ -153,63 +156,6 @@ export async function buildsCommand(options = {}, globalOptions = {}, deps = {})
153
156
  }
154
157
  }
155
158
 
156
- /**
157
- * Format a build for JSON output
158
- */
159
- function formatBuildForJson(build, includeComparisons = false) {
160
- let result = {
161
- id: build.id,
162
- name: build.name,
163
- status: build.status,
164
- branch: build.branch,
165
- commit: build.commit_sha,
166
- commitMessage: build.commit_message,
167
- environment: build.environment,
168
- screenshotCount: build.screenshot_count || 0,
169
- comparisons: {
170
- total: build.total_comparisons || 0,
171
- new: build.new_comparisons || 0,
172
- changed: build.changed_comparisons || 0,
173
- identical: build.identical_comparisons || 0
174
- },
175
- visual_review: build.visual_review || null,
176
- createdAt: build.created_at,
177
- completedAt: build.completed_at
178
- };
179
- if (includeComparisons && build.comparisons) {
180
- result.comparisonDetails = build.comparisons.map(c => {
181
- let diffUrl = c.diff_image?.url || c.diff_image_url || c.diff_url || null;
182
- let diffImage = c.diff_image || {};
183
- let clusterMetadata = c.cluster_metadata || diffImage.cluster_metadata || null;
184
- let ssimScore = c.ssim_score ?? diffImage.ssim_score ?? null;
185
- let gmsdScore = c.gmsd_score ?? diffImage.gmsd_score ?? null;
186
- let fingerprintHash = c.fingerprint_hash || diffImage.fingerprint_hash || null;
187
- let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash;
188
- return {
189
- id: c.id,
190
- name: c.name || c.current_name,
191
- status: c.status,
192
- diffPercentage: c.diff_percentage,
193
- visual_review: c.visual_review || null,
194
- urls: {
195
- baseline: c.baseline_screenshot?.original_url || c.baseline_original_url || c.baseline_screenshot_url || null,
196
- current: c.current_screenshot?.original_url || c.current_original_url || c.current_screenshot_url || null,
197
- diff: diffUrl
198
- },
199
- honeydiff: hasHoneydiff ? {
200
- ssimScore,
201
- gmsdScore,
202
- clusterClassification: clusterMetadata?.classification || null,
203
- clusterMetadata,
204
- fingerprintHash,
205
- diffRegions: c.diff_regions ?? diffImage.diff_regions ?? null
206
- } : null
207
- };
208
- });
209
- }
210
- return result;
211
- }
212
-
213
159
  /**
214
160
  * Display a single build in human-readable format
215
161
  */
@@ -319,18 +265,4 @@ function getComparisonStatusIcon(colors, status) {
319
265
  default:
320
266
  return colors.dim('○');
321
267
  }
322
- }
323
-
324
- /**
325
- * Validate builds command options
326
- */
327
- export function validateBuildsOptions(options = {}) {
328
- let errors = [];
329
- if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 250)) {
330
- errors.push('--limit must be an integer between 1 and 250');
331
- }
332
- if (options.offset !== undefined && (!Number.isInteger(options.offset) || options.offset < 0)) {
333
- errors.push('--offset must be a non-negative integer');
334
- }
335
- return errors;
336
268
  }
@@ -0,0 +1,31 @@
1
+ import { sendError, sendSuccess } from '../../adapters/http/middleware/response.js';
2
+ import * as output from '../../shared/output.js';
3
+ export function createCloudBuildsRouter({
4
+ authService
5
+ }) {
6
+ return async function handleCloudBuildsRoute(req, res, pathname, parsedUrl) {
7
+ let buildsMatch = pathname.match(/^\/api\/cloud\/organizations\/([^/]+)\/projects\/([^/]+)\/builds$/);
8
+ if (req.method !== 'GET' || !buildsMatch) return false;
9
+ try {
10
+ let organizationSlug = decodeURIComponent(buildsMatch[1]);
11
+ let projectSlug = decodeURIComponent(buildsMatch[2]);
12
+ let limit = parsedUrl.searchParams.get('limit') || '20';
13
+ let branch = parsedUrl.searchParams.get('branch');
14
+ let queryParams = new URLSearchParams();
15
+ if (limit) queryParams.append('limit', limit);
16
+ if (branch) queryParams.append('branch', branch);
17
+ let query = queryParams.toString();
18
+ let endpoint = `/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/builds${query ? `?${query}` : ''}`;
19
+ let response = await authService.authenticatedRequest(endpoint, {
20
+ method: 'GET'
21
+ });
22
+ sendSuccess(res, {
23
+ builds: response.builds || []
24
+ });
25
+ } catch (error) {
26
+ output.error('Error fetching builds from cloud:', error);
27
+ sendError(res, 500, error.message);
28
+ }
29
+ return true;
30
+ };
31
+ }
@@ -3,10 +3,11 @@
3
3
  * Uses functional API operations directly
4
4
  */
5
5
 
6
- import { createApiClient as defaultCreateApiClient, finalizeParallelBuild as defaultFinalizeParallelBuild } from '../api/index.js';
7
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
8
- import * as defaultOutput from '../utils/output.js';
9
- import { writeSession as defaultWriteSession } from '../utils/session.js';
6
+ import * as defaultOutput from '../../shared/output.js';
7
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
8
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
9
+ import { writeSession as defaultWriteSession } from '../runs/session-store.js';
10
+ import { finalizeParallelBuild as defaultFinalizeParallelBuild } from './build.service.js';
10
11
  let MISSING_BUILD_HINTS = [' • No screenshots were uploaded with this parallel-id', ' • Tests were skipped or failed before capturing screenshots', ' • The parallel-id does not match what was used during test runs'];
11
12
 
12
13
  /**
@@ -150,17 +151,4 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
150
151
  } finally {
151
152
  output.cleanup();
152
153
  }
153
- }
154
-
155
- /**
156
- * Validate finalize options
157
- * @param {string} parallelId - Parallel ID to finalize
158
- * @param {Object} options - Command options
159
- */
160
- export function validateFinalizeOptions(parallelId, _options) {
161
- let errors = [];
162
- if (!parallelId || parallelId.trim() === '') {
163
- errors.push('Parallel ID is required');
164
- }
165
- return errors;
166
154
  }
@@ -1,13 +1,17 @@
1
+ import { getBuildReviewState, getComparisonStatus, getProcessingStatus, normalizeBuildStatus } from './build.normalizers.js';
2
+ import { createStatusData, createStatusSuggestedCommands } from './build.serializers.js';
3
+
1
4
  /**
2
5
  * Status command implementation
3
6
  * Uses functional API operations directly
4
7
  */
5
8
 
6
- import { createApiClient as defaultCreateApiClient, getBuildStatus as defaultGetBuildStatus, getPreviewInfo as defaultGetPreviewInfo } from '../api/index.js';
7
- import { getAppBaseUrl } from '../utils/api-url.js';
8
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
9
- import * as defaultOutput from '../utils/output.js';
10
- import { getVisualReviewState } from '../utils/visual-context-normalizers.js';
9
+ import { getAppBaseUrl } from '../../shared/api-url.js';
10
+ import * as defaultOutput from '../../shared/output.js';
11
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
12
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
13
+ import { getPreviewInfo as defaultGetPreviewInfo } from '../previews/preview.service.js';
14
+ import { getBuildStatus as defaultGetBuildStatus } from './build.service.js';
11
15
  function createStatusDeps(deps = {}) {
12
16
  return {
13
17
  loadConfig: deps.loadConfig || defaultLoadConfig,
@@ -48,108 +52,6 @@ async function fetchOptionalPreviewInfo(getPreviewInfo, client, buildId) {
48
52
  throw error;
49
53
  }
50
54
  }
51
- export function normalizeBuildStatus(buildStatus) {
52
- return buildStatus.build;
53
- }
54
-
55
- /**
56
- * Read the processing counts from the status response.
57
- *
58
- * @param {Object} status - Status response.
59
- * @returns {Object} API-provided processing facts.
60
- */
61
- export function getProcessingStatus(status = {}) {
62
- return status.processing;
63
- }
64
-
65
- /**
66
- * Read comparison totals without deriving them from screenshot counts.
67
- *
68
- * @param {Object} status - Status response.
69
- * @returns {Object} API-provided comparison facts.
70
- */
71
- export function getComparisonStatus(status = {}) {
72
- return status.comparisons;
73
- }
74
-
75
- /**
76
- * Read the build review state without mixing it with processing status.
77
- *
78
- * @param {Object} status - Status response.
79
- * @returns {string|null} API-provided review state when available.
80
- */
81
- export function getBuildReviewState(status = {}) {
82
- let build = normalizeBuildStatus(status);
83
- return getVisualReviewState(build);
84
- }
85
-
86
- /**
87
- * Give agents concrete paths from lifecycle status into visual evidence.
88
- *
89
- * @param {Object} build - Build record from the status response.
90
- * @returns {{label: string, command: string}[]} Executable CLI commands.
91
- */
92
- export function createStatusSuggestedCommands(build = {}) {
93
- if (!build.id) {
94
- return [];
95
- }
96
- return [{
97
- label: 'Discover build context API',
98
- command: 'vizzly api schema sdk.getBuildContext --json'
99
- }, {
100
- label: 'List comparisons',
101
- command: `vizzly --json comparisons --build ${build.id}`
102
- }];
103
- }
104
-
105
- /**
106
- * Every lifecycle, processing, comparison, and review value comes directly
107
- * from the API.
108
- *
109
- * @param {Object} status - Status response.
110
- * @param {Object|null} previewInfo - Optional preview response.
111
- * @returns {Object} Machine-readable status payload.
112
- */
113
- export function createStatusData(status, previewInfo = null) {
114
- let build = normalizeBuildStatus(status);
115
- let processing = getProcessingStatus(status);
116
- let comparisons = getComparisonStatus(status);
117
- return {
118
- resource: status.resource,
119
- schemaVersion: status.schema_version,
120
- buildId: build.id,
121
- status: build.status,
122
- conclusion: status.conclusion,
123
- name: build.name,
124
- createdAt: build.created_at,
125
- updatedAt: build.updated_at,
126
- completedAt: build.completed_at,
127
- environment: build.environment,
128
- branch: build.branch,
129
- commit: build.commit_sha,
130
- commitMessage: build.commit_message,
131
- screenshotsTotal: processing.total,
132
- processing,
133
- comparisonsTotal: comparisons.total,
134
- comparisons,
135
- newComparisons: comparisons.new,
136
- changedComparisons: comparisons.changed,
137
- identicalComparisons: comparisons.identical,
138
- visual_review: build.visual_review || null,
139
- executionTime: build.execution_time_ms,
140
- isBaseline: build.is_baseline,
141
- userAgent: build.user_agent,
142
- scope: status.scope,
143
- links: status.links,
144
- suggestedCommands: createStatusSuggestedCommands(build),
145
- preview: previewInfo ? {
146
- url: previewInfo.preview_url,
147
- status: previewInfo.status,
148
- fileCount: previewInfo.file_count,
149
- expiresAt: previewInfo.expires_at
150
- } : null
151
- };
152
- }
153
55
  export function createBuildInfo(build) {
154
56
  let buildInfo = {
155
57
  Name: build.name || build.id,
@@ -490,17 +392,4 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
490
392
  error
491
393
  };
492
394
  }
493
- }
494
-
495
- /**
496
- * Validate status options
497
- * @param {string} buildId - Build ID to check
498
- * @param {Object} options - Command options
499
- */
500
- export function validateStatusOptions(buildId) {
501
- let errors = [];
502
- if (!buildId || buildId.trim() === '') {
503
- errors.push('Build ID is required');
504
- }
505
- return errors;
506
395
  }
@@ -0,0 +1,43 @@
1
+ import { buildEndpointWithParams } from '../../shared/cloud-protocol.js';
2
+ import { VizzlyError } from '../../shared/vizzly-error.js';
3
+
4
+ /**
5
+ * Get comparison information
6
+ * @param {Object} client - API client
7
+ * @param {string} comparisonId - Comparison ID
8
+ * @returns {Promise<Object>} Comparison data
9
+ */
10
+ export async function getComparison(client, comparisonId) {
11
+ let response = await client.request(`/api/sdk/comparisons/${comparisonId}`);
12
+ return response.comparison;
13
+ }
14
+
15
+ /**
16
+ * Search for comparisons by name
17
+ * @param {Object} client - API client
18
+ * @param {string} name - Screenshot name to search for
19
+ * @param {Object} filters - Optional filters (branch, project, organization, limit, offset)
20
+ * @returns {Promise<Object>} Search results with comparisons and pagination
21
+ */
22
+ export async function searchComparisons(client, name, filters = {}) {
23
+ if (!name || typeof name !== 'string') {
24
+ throw new VizzlyError('name is required and must be a non-empty string');
25
+ }
26
+ let {
27
+ branch,
28
+ project,
29
+ organization,
30
+ limit = 50,
31
+ offset = 0
32
+ } = filters;
33
+ let params = {
34
+ name,
35
+ limit: String(limit),
36
+ offset: String(offset)
37
+ };
38
+ if (branch) params.branch = branch;
39
+ if (project) params.project = project;
40
+ if (organization) params.organization = organization;
41
+ let endpoint = buildEndpointWithParams('/api/sdk/comparisons/search', params);
42
+ return client.request(endpoint);
43
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { analyze } from '@vizzly-testing/honeydiff';
8
- import { generateComparisonId } from '../core/signature.js';
8
+ import { generateComparisonId } from '../screenshots/screenshot-identity.js';
9
9
 
10
10
  /**
11
11
  * Compare two images using honeydiff
@@ -2,11 +2,13 @@
2
2
  * Comparisons command - query and search comparisons
3
3
  */
4
4
 
5
- import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild, getComparison as defaultGetComparison, searchComparisons as defaultSearchComparisons } from '../api/index.js';
6
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
- import * as defaultOutput from '../utils/output.js';
8
- import { createWildcardMatcher } from '../utils/patterns.js';
9
- import { getComparisonBrowser, getComparisonName, getComparisonResult, getComparisonViewport } from '../utils/visual-context-normalizers.js';
5
+ import * as defaultOutput from '../../shared/output.js';
6
+ import { createWildcardMatcher } from '../../shared/patterns.js';
7
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
8
+ import { getBuild as defaultGetBuild } from '../builds/build.service.js';
9
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
10
+ import { getComparisonBrowser, getComparisonName, getComparisonResult, getComparisonViewport } from '../context/context.normalizers.js';
11
+ import { getComparison as defaultGetComparison, searchComparisons as defaultSearchComparisons } from './cloud-comparisons.js';
10
12
 
11
13
  /**
12
14
  * Comparisons command - query comparisons with various filters
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Default configuration values
3
+ */
4
+ export let CONFIG_DEFAULTS = {
5
+ apiUrl: 'https://app.vizzly.dev',
6
+ server: {
7
+ port: 47392,
8
+ timeout: 30000
9
+ },
10
+ build: {
11
+ name: 'Build {timestamp}',
12
+ environment: 'test'
13
+ },
14
+ upload: {
15
+ screenshotsDir: './screenshots',
16
+ batchSize: 10,
17
+ timeout: 30000
18
+ },
19
+ comparison: {
20
+ threshold: 2.0,
21
+ minClusterSize: 2
22
+ },
23
+ tdd: {
24
+ openReport: false
25
+ },
26
+ plugins: []
27
+ };
@@ -1,6 +1,6 @@
1
- import { readFile } from 'node:fs/promises';
2
1
  import path from 'node:path';
3
2
  import { cosmiconfigSync, defaultLoadersSync, getDefaultSearchPlacesSync } from 'cosmiconfig';
3
+ import { readOptionalFile } from '../../shared/files.js';
4
4
  let searchPlaces = getDefaultSearchPlacesSync('vizzly');
5
5
  export function createConfigExplorer() {
6
6
  return cosmiconfigSync('vizzly', {
@@ -8,14 +8,6 @@ export function createConfigExplorer() {
8
8
  searchStrategy: 'none'
9
9
  });
10
10
  }
11
- export async function readOptionalFile(filename) {
12
- try {
13
- return await readFile(filename, 'utf8');
14
- } catch (error) {
15
- if (error.code === 'ENOENT') return null;
16
- throw error;
17
- }
18
- }
19
11
  export function configFormat(filename) {
20
12
  if (path.basename(filename) === 'package.json') return 'package';
21
13
  let extension = path.extname(filename);
@@ -1,11 +1,12 @@
1
1
  import { resolve } from 'node:path';
2
- import { CONFIG_DEFAULTS, deepMerge } from '../config/core.js';
2
+ import * as output from '../../shared/output.js';
3
+ import { getAccessToken } from '../auth/token-store.js';
4
+ import { getProjectLink } from '../projects/project-link-store.js';
5
+ import { deepMerge } from './config.normalizers.js';
6
+ import { validateVizzlyConfigWithDefaults } from './config.validations.js';
7
+ import { CONFIG_DEFAULTS } from './config-defaults.js';
3
8
  import { createConfigExplorer } from './config-discovery.js';
4
- import { validateVizzlyConfigWithDefaults } from './config-schema.js';
5
- import { getApiToken, getApiUrl, getBuildName, getMinClusterSize, getParallelId, getThreshold } from './environment-config.js';
6
- import { getAccessToken } from './global-config.js';
7
- import * as output from './output.js';
8
- import { getProjectLink } from './project-link-store.js';
9
+ import { getApiToken, getApiUrl, getBuildName, getMinClusterSize, getParallelId, getThreshold } from './environment.js';
9
10
  export async function loadConfig(configPath = null, cliOverrides = {}) {
10
11
  // 1. Load from config file using cosmiconfig
11
12
  let explorer = createConfigExplorer();
@@ -152,7 +153,7 @@ function applyCLIOverrides(config, cliOverrides = {}) {
152
153
  if (cliOverrides.allowNoToken !== undefined) config.allowNoToken = cliOverrides.allowNoToken;
153
154
  }
154
155
  function mergeConfig(target, source) {
155
- for (const key in source) {
156
+ for (let key in source) {
156
157
  if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
157
158
  if (!target[key]) target[key] = {};
158
159
  mergeConfig(target[key], source[key]);
@@ -162,7 +163,7 @@ function mergeConfig(target, source) {
162
163
  }
163
164
  }
164
165
  export function getScreenshotPaths(config) {
165
- const screenshotsDir = config.upload?.screenshotsDir || './screenshots';
166
- const paths = Array.isArray(screenshotsDir) ? screenshotsDir : [screenshotsDir];
166
+ let screenshotsDir = config.upload?.screenshotsDir || './screenshots';
167
+ let paths = Array.isArray(screenshotsDir) ? screenshotsDir : [screenshotsDir];
167
168
  return paths.map(p => resolve(process.cwd(), p));
168
169
  }
@@ -1,11 +1,11 @@
1
+ import * as defaultOutput from '../../shared/output.js';
2
+ import { CONFIG_DEFAULTS } from './config-defaults.js';
3
+ import { loadConfig as defaultLoadConfig } from './config-loader.js';
4
+
1
5
  /**
2
6
  * Config command - query and display configuration
3
7
  */
4
8
 
5
- import { CONFIG_DEFAULTS } from '../config/core.js';
6
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
- import * as defaultOutput from '../utils/output.js';
8
-
9
9
  /**
10
10
  * Config command - display current configuration
11
11
  * @param {string|null} key - Optional specific key to get (dot notation)
@@ -4,7 +4,7 @@
4
4
  * @module @vizzly-testing/cli/config
5
5
  */
6
6
 
7
- export { VizzlyConfig } from './index';
7
+ export { VizzlyConfig } from '../../index';
8
8
 
9
9
  /**
10
10
  * Define Vizzly configuration with type hints
@@ -26,5 +26,5 @@ export { VizzlyConfig } from './index';
26
26
  * });
27
27
  */
28
28
  export function defineConfig(
29
- config: import('./index').VizzlyConfig
30
- ): import('./index').VizzlyConfig;
29
+ config: import('../../index').VizzlyConfig
30
+ ): import('../../index').VizzlyConfig;
@@ -0,0 +1,126 @@
1
+ import { CONFIG_DEFAULTS } from './config-defaults.js';
2
+ function isPlainConfigObject(value) {
3
+ return value && typeof value === 'object' && !Array.isArray(value);
4
+ }
5
+ function cloneConfigValue(value) {
6
+ if (Array.isArray(value)) {
7
+ return value.map(cloneConfigValue);
8
+ }
9
+ if (isPlainConfigObject(value)) {
10
+ let cloned = {};
11
+ for (let key of Object.keys(value)) {
12
+ cloned[key] = cloneConfigValue(value[key]);
13
+ }
14
+ return cloned;
15
+ }
16
+ return value;
17
+ }
18
+
19
+ /**
20
+ * Deep merge two objects
21
+ * @param {Object} target - Target object
22
+ * @param {Object} source - Source object
23
+ * @returns {Object} Merged object (new object, inputs not mutated)
24
+ */
25
+ export function deepMerge(target, source) {
26
+ let output = cloneConfigValue(target);
27
+ for (let key of Object.keys(source)) {
28
+ if (isPlainConfigObject(source[key])) {
29
+ if (isPlainConfigObject(target[key])) {
30
+ output[key] = deepMerge(target[key], source[key]);
31
+ } else {
32
+ output[key] = cloneConfigValue(source[key]);
33
+ }
34
+ } else {
35
+ output[key] = cloneConfigValue(source[key]);
36
+ }
37
+ }
38
+ return output;
39
+ }
40
+
41
+ /**
42
+ * Ensure value is a plain object, return empty object otherwise
43
+ * @param {*} value - Value to check
44
+ * @returns {Object} The value if it's an object, empty object otherwise
45
+ */
46
+ function ensureObject(value) {
47
+ if (isPlainConfigObject(value)) {
48
+ return value;
49
+ }
50
+ return {};
51
+ }
52
+ function applyConfigLayer(config, sources, layer, sourceName) {
53
+ let merged = deepMerge(config, layer);
54
+ for (let key of Object.keys(layer)) {
55
+ sources[key] = sourceName;
56
+ }
57
+ return merged;
58
+ }
59
+
60
+ /**
61
+ * Build merged config from layers with source tracking
62
+ * @param {Object} options - Config layers
63
+ * @param {Object} options.projectConfig - Project config (from vizzly.config.js)
64
+ * @param {Object} options.globalConfig - Global config (from ~/.vizzly/config.json)
65
+ * @param {Object} [options.envOverrides] - Environment variable overrides
66
+ * @returns {{ config: Object, sources: Object }}
67
+ */
68
+ export function buildMergedConfig({
69
+ projectConfig = {},
70
+ globalConfig = {},
71
+ envOverrides = {}
72
+ } = {}) {
73
+ // Ensure all inputs are plain objects
74
+ let safeProjectConfig = ensureObject(projectConfig);
75
+ let safeGlobalConfig = ensureObject(globalConfig);
76
+ let safeEnvOverrides = ensureObject(envOverrides);
77
+ let mergedConfig = cloneConfigValue(CONFIG_DEFAULTS);
78
+ let sources = {};
79
+ for (let key of Object.keys(CONFIG_DEFAULTS)) {
80
+ sources[key] = 'default';
81
+ }
82
+ mergedConfig = applyConfigLayer(mergedConfig, sources, safeGlobalConfig, 'global');
83
+ mergedConfig = applyConfigLayer(mergedConfig, sources, safeProjectConfig, 'project');
84
+ mergedConfig = applyConfigLayer(mergedConfig, sources, safeEnvOverrides, 'env');
85
+ return {
86
+ config: mergedConfig,
87
+ sources
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Extract environment variable overrides
93
+ * @param {Object} env - Environment variables object (defaults to process.env)
94
+ * @returns {Object} Overrides from environment
95
+ */
96
+ export function extractEnvOverrides(env = process.env) {
97
+ let overrides = {};
98
+ if (env.VIZZLY_TOKEN) {
99
+ overrides.apiKey = env.VIZZLY_TOKEN;
100
+ }
101
+ if (env.VIZZLY_API_URL) {
102
+ overrides.apiUrl = env.VIZZLY_API_URL;
103
+ }
104
+ return overrides;
105
+ }
106
+
107
+ /**
108
+ * Extract config from cosmiconfig result (handles .default exports)
109
+ * @param {Object|null} result - Cosmiconfig result
110
+ * @returns {{ config: Object|null, filepath: string|null }}
111
+ */
112
+ export function extractCosmiconfigResult(result) {
113
+ if (!result?.config) {
114
+ return {
115
+ config: null,
116
+ filepath: null
117
+ };
118
+ }
119
+
120
+ // Handle both `export default` and `module.exports`
121
+ let config = result.config.default || result.config;
122
+ return {
123
+ config,
124
+ filepath: result.filepath
125
+ };
126
+ }