@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,109 @@
1
+ import * as output from '../../shared/output.js';
2
+ import { buildScreenshotPayload, buildScreenshotResolvePayload, buildShaCheckPayload } from './screenshot.serializers.js';
3
+ import { computeSha256 } from './screenshot-hash.js';
4
+
5
+ /**
6
+ * Check if SHAs already exist on the server
7
+ * @param {Object} client - API client
8
+ * @param {Array} screenshots - Screenshots to check (objects with sha256, or string SHAs)
9
+ * @param {string} buildId - Build ID for screenshot record creation
10
+ * @returns {Promise<Object>} { existing, missing, screenshots }
11
+ */
12
+ export async function checkShas(client, screenshots, buildId) {
13
+ try {
14
+ let payload = buildShaCheckPayload(screenshots, buildId);
15
+ return await client.request('/api/sdk/check-shas', {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Content-Type': 'application/json'
19
+ },
20
+ body: JSON.stringify(payload)
21
+ });
22
+ } catch (error) {
23
+ // Continue without deduplication on error
24
+ output.debug('sha-check', 'failed, continuing without deduplication', {
25
+ error: error.message
26
+ });
27
+
28
+ // Extract SHAs for fallback response
29
+ let shaList = Array.isArray(screenshots) && screenshots.length > 0 && typeof screenshots[0] === 'object' ? screenshots.map(s => s.sha256) : screenshots;
30
+ return {
31
+ existing: [],
32
+ missing: shaList,
33
+ screenshots: []
34
+ };
35
+ }
36
+ }
37
+ function canUploadBytesAfterShaResolveError(error) {
38
+ let status = error?.context?.status;
39
+
40
+ // Network failures, older servers, and server errors should degrade to the
41
+ // slower byte upload path. Auth and ownership errors must still surface.
42
+ if (!status) return true;
43
+ if (status >= 500) return true;
44
+ if (status === 400 && error.message?.includes('image_data')) return true;
45
+ return false;
46
+ }
47
+
48
+ /**
49
+ * Upload a screenshot with SHA deduplication
50
+ * @param {Object} client - API client
51
+ * @param {string} buildId - Build ID
52
+ * @param {string} name - Screenshot name
53
+ * @param {Buffer} buffer - Screenshot data
54
+ * @param {Object} metadata - Additional metadata
55
+ * @param {boolean} skipDedup - Skip SHA deduplication (uploadAll mode)
56
+ * @returns {Promise<Object>} Upload result
57
+ */
58
+ export async function uploadScreenshot(client, buildId, name, buffer, metadata = {}, skipDedup = false, screenshotOptions = {}) {
59
+ // Skip SHA deduplication if requested
60
+ if (skipDedup) {
61
+ let payload = buildScreenshotPayload(name, buffer, metadata, null, screenshotOptions);
62
+ return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json'
66
+ },
67
+ body: JSON.stringify(payload)
68
+ });
69
+ }
70
+
71
+ // Normal flow with server-side SHA resolution.
72
+ let sha256 = computeSha256(buffer);
73
+ let resolvePayload = buildScreenshotResolvePayload(name, metadata, sha256, screenshotOptions);
74
+ try {
75
+ let resolveResult = await client.request(`/api/sdk/builds/${buildId}/screenshots`, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json'
79
+ },
80
+ body: JSON.stringify(resolvePayload)
81
+ });
82
+ if (resolveResult?.upload_required === false) {
83
+ return {
84
+ message: 'Screenshot already exists, skipped upload',
85
+ sha256,
86
+ skipped: true,
87
+ screenshot: resolveResult,
88
+ fromExisting: true
89
+ };
90
+ }
91
+ } catch (error) {
92
+ if (!canUploadBytesAfterShaResolveError(error)) {
93
+ throw error;
94
+ }
95
+ output.debug('sha-resolve', 'failed, uploading screenshot bytes', {
96
+ error: error.message
97
+ });
98
+ }
99
+
100
+ // File doesn't exist, or resolve failed; proceed with upload.
101
+ let payload = buildScreenshotPayload(name, buffer, metadata, sha256, screenshotOptions);
102
+ return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Content-Type': 'application/json'
106
+ },
107
+ body: JSON.stringify(payload)
108
+ });
109
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
- import { VizzlyError } from '../errors/vizzly-error.js';
8
+ import { VizzlyError } from '../../shared/vizzly-error.js';
9
9
 
10
10
  /**
11
11
  * Resolve image buffer from file path or return buffer as-is
@@ -0,0 +1,53 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ /**
4
+ * Compute SHA256 hash of buffer
5
+ * @param {Buffer} buffer - Data to hash
6
+ * @returns {string} Hex-encoded SHA256 hash
7
+ */
8
+ export function computeSha256(buffer) {
9
+ return crypto.createHash('sha256').update(buffer).digest('hex');
10
+ }
11
+
12
+ /**
13
+ * Partition screenshots by SHA existence
14
+ * @param {Array<Object>} screenshots - Screenshots with sha256 property
15
+ * @param {Set<string>|Array<string>} existingShas - SHAs that already exist
16
+ * @returns {Object} { toUpload, existing } partitioned arrays
17
+ */
18
+ export function partitionByShaExistence(screenshots, existingShas) {
19
+ let existingSet = existingShas instanceof Set ? existingShas : new Set(existingShas);
20
+ let toUpload = [];
21
+ let existing = [];
22
+ for (let screenshot of screenshots) {
23
+ if (existingSet.has(screenshot.sha256)) {
24
+ existing.push(screenshot);
25
+ } else {
26
+ toUpload.push(screenshot);
27
+ }
28
+ }
29
+ return {
30
+ toUpload,
31
+ existing
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Check if SHA check result indicates file exists
37
+ * @param {Object} checkResult - Result from checkShas endpoint
38
+ * @param {string} sha256 - SHA to check
39
+ * @returns {boolean} True if file exists
40
+ */
41
+ export function shaExists(checkResult, sha256) {
42
+ return checkResult?.existing?.includes(sha256) ?? false;
43
+ }
44
+
45
+ /**
46
+ * Find screenshot record from SHA check result
47
+ * @param {Object} checkResult - Result from checkShas endpoint
48
+ * @param {string} sha256 - SHA to find
49
+ * @returns {Object|null} Screenshot record or null
50
+ */
51
+ export function findScreenshotBySha(checkResult, sha256) {
52
+ return checkResult?.screenshots?.find(s => s.sha256 === sha256) ?? null;
53
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Build server listen options
3
+ * @param {Object} config - Server configuration
4
+ * @returns {{ port: number, host: string }}
5
+ */
6
+ export function buildServerListenOptions(config) {
7
+ return {
8
+ port: config?.server?.port || 3000,
9
+ host: '127.0.0.1'
10
+ };
11
+ }
12
+
13
+ /**
14
+ * Build server started message
15
+ * @param {number} port - Server port
16
+ * @returns {string}
17
+ */
18
+ export function buildServerStartedMessage(port) {
19
+ return `Screenshot server listening on http://127.0.0.1:${port}`;
20
+ }
21
+
22
+ /**
23
+ * Build server stopped message
24
+ * @returns {string}
25
+ */
26
+ export function buildServerStoppedMessage() {
27
+ return 'Screenshot server stopped';
28
+ }
@@ -4,7 +4,10 @@
4
4
  * Each operation takes its dependencies as parameters for testability.
5
5
  */
6
6
 
7
- import { buildBadRequestResponse, buildInternalErrorResponse, buildNotFoundResponse, buildServerListenOptions, buildServerStartedMessage, buildServerStoppedMessage, buildSuccessResponse, extractScreenshotData, getEffectiveBuildId, isScreenshotEndpoint, validateScreenshotRequest } from './core.js';
7
+ import { extractScreenshotData, getEffectiveBuildId } from './screenshot.normalizers.js';
8
+ import { buildBadRequestResponse, buildInternalErrorResponse, buildNotFoundResponse, buildSuccessResponse } from './screenshot.serializers.js';
9
+ import { validateScreenshotRequest } from './screenshot.validations.js';
10
+ import { buildServerListenOptions, buildServerStartedMessage, buildServerStoppedMessage } from './screenshot-server-options.js';
8
11
 
9
12
  // ============================================================================
10
13
  // Request Body Parsing
@@ -180,4 +183,14 @@ export function stopServer({
180
183
  resolve();
181
184
  });
182
185
  });
186
+ }
187
+
188
+ /**
189
+ * Check if request is for the screenshot endpoint
190
+ * @param {string} method - HTTP method
191
+ * @param {string} url - Request URL
192
+ * @returns {boolean}
193
+ */
194
+ export function isScreenshotEndpoint(method, url) {
195
+ return method === 'POST' && url === '/screenshot';
183
196
  }
@@ -0,0 +1,40 @@
1
+ import { getScreenshotOptionsPayload } from './screenshot-options.js';
2
+
3
+ /**
4
+ * Get effective build ID (falls back to 'default')
5
+ * @param {string|null|undefined} buildId - Build ID from request
6
+ * @returns {string}
7
+ */
8
+ export function getEffectiveBuildId(buildId) {
9
+ return buildId || 'default';
10
+ }
11
+
12
+ /**
13
+ * Extract screenshot data from request body
14
+ * @param {Object} body - Request body
15
+ * @returns {{ name: string, image: string, properties?: Object }}
16
+ */
17
+ export function extractScreenshotData(body) {
18
+ let screenshotData = {
19
+ name: body.name,
20
+ image: body.image,
21
+ properties: body.properties
22
+ };
23
+ Object.assign(screenshotData, getScreenshotOptionsPayload(body));
24
+ return screenshotData;
25
+ }
26
+
27
+ /**
28
+ * Build the flat internal comparison shape. Width and height always come from
29
+ * the captured image. The untouched user bag remains available in metadata.
30
+ */
31
+ export let extractProperties = (validatedProperties, imageDimensions = {}) => {
32
+ if (!validatedProperties) return {};
33
+ return {
34
+ ...validatedProperties,
35
+ viewport_width: imageDimensions.width ?? null,
36
+ viewport_height: imageDimensions.height ?? null,
37
+ browser: validatedProperties.browser ?? null,
38
+ metadata: validatedProperties
39
+ };
40
+ };
@@ -3,10 +3,10 @@
3
3
  * Handles screenshot uploads and flush requests.
4
4
  */
5
5
 
6
- import * as output from '../../utils/output.js';
7
- import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
8
- import { parseJsonBody } from '../middleware/json-parser.js';
9
- import { sendError, sendJson } from '../middleware/response.js';
6
+ import { parseJsonBody } from '../../adapters/http/middleware/json-parser.js';
7
+ import { sendError, sendJson } from '../../adapters/http/middleware/response.js';
8
+ import * as output from '../../shared/output.js';
9
+ import { normalizeScreenshotOptions } from './screenshot-options.js';
10
10
 
11
11
  /**
12
12
  * Route local capture and flush requests to the active cloud or TDD handler.
@@ -0,0 +1,146 @@
1
+ import { createScreenshotProperties, getScreenshotOptionsPayload } from './screenshot-options.js';
2
+
3
+ /**
4
+ * Build payload for screenshot upload or SHA resolution
5
+ * @param {string} name - Screenshot name
6
+ * @param {Buffer|null} buffer - Image data, or null when resolving by SHA
7
+ * @param {Object} metadata - User screenshot properties
8
+ * @param {string|null} sha256 - Pre-computed SHA256 hash (optional)
9
+ * @returns {Object} Screenshot upload payload
10
+ */
11
+ export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = null, screenshotOptions = {}) {
12
+ let payload = {
13
+ name,
14
+ properties: createScreenshotProperties({
15
+ properties: metadata
16
+ })
17
+ };
18
+ Object.assign(payload, getScreenshotOptionsPayload(screenshotOptions));
19
+ if (buffer) {
20
+ payload.image_data = buffer.toString('base64');
21
+ }
22
+ if (sha256) {
23
+ payload.sha256 = sha256;
24
+ }
25
+ return payload;
26
+ }
27
+
28
+ /**
29
+ * Build payload for server-side screenshot resolution without sending image bytes
30
+ * @param {string} name - Screenshot name
31
+ * @param {Object} metadata - User screenshot properties
32
+ * @param {string} sha256 - Pre-computed SHA256 hash
33
+ * @returns {Object} Screenshot resolution payload
34
+ */
35
+ export function buildScreenshotResolvePayload(name, metadata = {}, sha256, screenshotOptions = {}) {
36
+ return buildScreenshotPayload(name, null, metadata, sha256, screenshotOptions);
37
+ }
38
+
39
+ /**
40
+ * Build payload for SHA existence check (signature-based format)
41
+ * @param {Array<Object>} screenshots - Screenshots with sha256 and metadata
42
+ * @param {string} buildId - Build ID for screenshot record creation
43
+ * @returns {Object} SHA check request payload
44
+ */
45
+ export function buildShaCheckPayload(screenshots, buildId) {
46
+ // Check if using new signature-based format or legacy SHA-only format
47
+ if (screenshots.length > 0 && typeof screenshots[0] === 'object' && screenshots[0].sha256) {
48
+ return {
49
+ buildId,
50
+ screenshots
51
+ };
52
+ }
53
+
54
+ // Legacy format: array of SHA strings
55
+ return {
56
+ shas: screenshots,
57
+ buildId
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Build screenshot object for SHA checking
63
+ * @param {string} sha256 - SHA256 hash of image
64
+ * @param {string} name - Screenshot name
65
+ * @param {Object} metadata - Screenshot metadata
66
+ * @returns {Object} Screenshot check object
67
+ */
68
+ export function buildScreenshotCheckObject(sha256, name, metadata = {}) {
69
+ let meta = metadata || {};
70
+ let properties = createScreenshotProperties({
71
+ properties: meta
72
+ });
73
+ return {
74
+ sha256,
75
+ name,
76
+ properties
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Build success response object
82
+ * @returns {{ status: number, body: Object }}
83
+ */
84
+ export function buildSuccessResponse() {
85
+ return {
86
+ status: 200,
87
+ body: {
88
+ success: true
89
+ }
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Build error response object
95
+ * @param {number} status - HTTP status code
96
+ * @param {string} message - Error message
97
+ * @returns {{ status: number, body: Object }}
98
+ */
99
+ export function buildErrorResponse(status, message) {
100
+ return {
101
+ status,
102
+ body: {
103
+ error: message
104
+ }
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Build not found response
110
+ * @returns {{ status: number, body: Object }}
111
+ */
112
+ export function buildNotFoundResponse() {
113
+ return buildErrorResponse(404, 'Not found');
114
+ }
115
+
116
+ /**
117
+ * Build bad request response
118
+ * @param {string} message - Error message
119
+ * @returns {{ status: number, body: Object }}
120
+ */
121
+ export function buildBadRequestResponse(message) {
122
+ return buildErrorResponse(400, message);
123
+ }
124
+
125
+ /**
126
+ * Build internal error response
127
+ * @returns {{ status: number, body: Object }}
128
+ */
129
+ export function buildInternalErrorResponse() {
130
+ return buildErrorResponse(500, 'Internal server error');
131
+ }
132
+ export function serializeScreenshotRequest({
133
+ buildId,
134
+ name,
135
+ image,
136
+ type
137
+ }, options) {
138
+ return {
139
+ buildId,
140
+ name,
141
+ image,
142
+ type,
143
+ properties: options.properties,
144
+ ...getScreenshotOptionsPayload(options)
145
+ };
146
+ }
@@ -1,18 +1,5 @@
1
- /**
2
- * Security utilities for path sanitization and validation
3
- * Protects against path traversal attacks and ensures safe file operations
4
- */
1
+ import { isAbsolute } from 'node:path';
5
2
 
6
- import { isAbsolute, join, normalize, resolve } from 'node:path';
7
- import * as output from './output.js';
8
-
9
- /**
10
- * Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
11
- * @param {string} name - Original screenshot name
12
- * @param {number} maxLength - Maximum allowed length (default: 255)
13
- * @param {boolean} allowSlashes - Whether to allow forward slashes (for browser version strings)
14
- * @returns {string} Sanitized screenshot name
15
- */
16
3
  /**
17
4
  * Validate screenshot name for security (no transformations, just validation)
18
5
  * Throws if name contains path traversal or other dangerous patterns
@@ -95,7 +82,7 @@ export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = fal
95
82
  // Allow only safe characters: alphanumeric, hyphens, underscores, dots, spaces, and optionally slashes
96
83
  // Spaces are allowed here and will be converted to hyphens in generateBaselineFilename() to match cloud behavior
97
84
  // Replace other characters with underscores
98
- const allowedChars = allowSlashes ? /[^a-zA-Z0-9._ /-]/g : /[^a-zA-Z0-9._ -]/g;
85
+ let allowedChars = allowSlashes ? /[^a-zA-Z0-9._ /-]/g : /[^a-zA-Z0-9._ -]/g;
99
86
  let sanitized = name.replace(allowedChars, '_');
100
87
 
101
88
  // Prevent names that start with dots (hidden files)
@@ -110,61 +97,6 @@ export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = fal
110
97
  return sanitized;
111
98
  }
112
99
 
113
- /**
114
- * Validates that a path stays within the allowed working directory bounds
115
- * @param {string} targetPath - Path to validate
116
- * @param {string} workingDir - Working directory that serves as the security boundary
117
- * @returns {string} Resolved and normalized path if valid
118
- * @throws {Error} If path is invalid or outside bounds
119
- */
120
- export function validatePathSecurity(targetPath, workingDir) {
121
- if (typeof targetPath !== 'string' || targetPath.length === 0) {
122
- throw new Error('Path must be a non-empty string');
123
- }
124
- if (typeof workingDir !== 'string' || workingDir.length === 0) {
125
- throw new Error('Working directory must be a non-empty string');
126
- }
127
-
128
- // Normalize and resolve both paths
129
- const resolvedWorkingDir = resolve(normalize(workingDir));
130
- const resolvedTargetPath = resolve(normalize(targetPath));
131
-
132
- // Ensure the target path starts with the working directory
133
- if (!resolvedTargetPath.startsWith(resolvedWorkingDir)) {
134
- output.warn(`Path traversal attempt blocked: ${targetPath} (resolved: ${resolvedTargetPath}) is outside working directory: ${resolvedWorkingDir}`);
135
- throw new Error('Path is outside the allowed working directory');
136
- }
137
- return resolvedTargetPath;
138
- }
139
-
140
- /**
141
- * Safely constructs a path within the working directory
142
- * @param {string} workingDir - Base working directory
143
- * @param {...string} pathSegments - Path segments to join
144
- * @returns {string} Safely constructed path
145
- * @throws {Error} If resulting path would be outside working directory
146
- */
147
- export function safePath(workingDir, ...pathSegments) {
148
- if (pathSegments.length === 0) {
149
- return validatePathSecurity(workingDir, workingDir);
150
- }
151
-
152
- // Sanitize each path segment
153
- const sanitizedSegments = pathSegments.map(segment => {
154
- if (typeof segment !== 'string') {
155
- throw new Error('Path segment must be a string');
156
- }
157
-
158
- // Block directory traversal in segments
159
- if (segment.includes('..')) {
160
- throw new Error('Path segment contains directory traversal sequence');
161
- }
162
- return segment;
163
- });
164
- const targetPath = join(workingDir, ...sanitizedSegments);
165
- return validatePathSecurity(targetPath, workingDir);
166
- }
167
-
168
100
  /**
169
101
  * Validate user metadata without interpreting names as Vizzly options or
170
102
  * rewriting values. Rendering code must escape strings for its output context.
@@ -202,4 +134,22 @@ export function validateScreenshotProperties(properties = {}) {
202
134
  return result;
203
135
  }
204
136
  return copy(properties);
137
+ }
138
+
139
+ /**
140
+ * Validate screenshot request body
141
+ * @param {Object} body - Request body
142
+ * @returns {{ valid: boolean, error: string|null }}
143
+ */
144
+ export function validateScreenshotRequest(body) {
145
+ if (!body?.name || !body?.image) {
146
+ return {
147
+ valid: false,
148
+ error: 'name and image are required'
149
+ };
150
+ }
151
+ return {
152
+ valid: true,
153
+ error: null
154
+ };
205
155
  }
@@ -1,11 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { readOptionalFile } from '../utils/config-discovery.js';
6
- import { fileChange } from '../utils/file-changes.js';
7
- import { getPackageVersion } from '../utils/package-info.js';
8
- let sourcePath = fileURLToPath(new URL('../../skills/vizzly/', import.meta.url));
4
+ import { fileChange } from '../../shared/file-changes.js';
5
+ import { readOptionalFile } from '../../shared/files.js';
6
+ import { getPackageRoot, getPackageVersion } from '../../shared/package-info.js';
7
+ let sourcePath = path.join(getPackageRoot(), 'skills', 'vizzly');
9
8
  let start = '<!-- vizzly-agent-guidance -->';
10
9
  let end = '<!-- /vizzly-agent-guidance -->';
11
10
  export function agentGuidancePath(cwd) {
@@ -1,24 +1,11 @@
1
- import { spawn } from 'node:child_process';
2
1
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { planAgentSetup } from '../init/agents.js';
5
- import { planConfig } from '../init/core.js';
6
- import { createPrompts } from '../init/prompts.js';
7
- import { discoverConfig } from '../utils/config-discovery.js';
8
- import { applyChanges } from '../utils/file-changes.js';
9
- import * as output from '../utils/output.js';
10
- import { projectLinkCommand, validateProjectLinkOptions } from './project.js';
11
- function runProcess(command, args) {
12
- return new Promise((resolve, reject) => {
13
- let child = spawn(command, args, {
14
- stdio: 'inherit'
15
- });
16
- child.once('error', reject);
17
- child.once('close', (code, signal) => {
18
- if (code === 0) resolve();else reject(new Error(`${command} did not finish successfully (${signal || code}).`));
19
- });
20
- });
21
- }
2
+ import { applyChanges } from '../../shared/file-changes.js';
3
+ import * as output from '../../shared/output.js';
4
+ import { discoverConfig } from '../config/config-discovery.js';
5
+ import { planConfig } from '../config/config-plan.js';
6
+ import { validateProjectLinkOptions } from '../projects/project.validations.js';
7
+ import { planAgentSetup } from './agent-setup.js';
8
+ import { createPrompts } from './setup-prompts.js';
22
9
  async function guidedPlan(cwd, config, prompts) {
23
10
  output.hint(`Project: ${cwd}`);
24
11
  let guidance = await prompts.confirm('Add the Vizzly skill and AGENTS.md instructions?');
@@ -57,26 +44,6 @@ function agentOptions(options) {
57
44
  skill: guidance || !options.skipAgentSkill && Boolean(options.agentSkill)
58
45
  };
59
46
  }
60
- async function connectProject(project, options) {
61
- let exitCode = 0;
62
- let link = () => projectLinkCommand(project, {}, options, {
63
- exit: code => {
64
- exitCode = code;
65
- }
66
- });
67
- let result = await link();
68
- if (result?.reason === 'auth_required') {
69
- let cli = fileURLToPath(new URL('../cli.js', import.meta.url));
70
- await runProcess(process.execPath, [cli, 'login']);
71
- result = await link();
72
- }
73
- let status = result?.linked ? 'linked' : 'failed';
74
- if (!result?.linked && exitCode === 1) status = 'auth_required';
75
- return {
76
- status,
77
- project
78
- };
79
- }
80
47
  function showResult(result, json) {
81
48
  if (json) {
82
49
  output.data(result);
@@ -94,7 +61,9 @@ function showResult(result, json) {
94
61
  output.hint('Use a Vizzly client to add screenshot capture.');
95
62
  }
96
63
  }
97
- export async function init(options = {}) {
64
+ export async function init(options = {}, {
65
+ connectProject
66
+ } = {}) {
98
67
  output.configure({
99
68
  json: options.json || false,
100
69
  verbose: options.verbose,