@vizzly-testing/cli 0.39.0 → 0.39.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +12 -3
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +50 -35
  4. package/dist/adapters/cli/setup.js +34 -0
  5. package/dist/{client → adapters/client}/index.js +6 -7
  6. package/dist/adapters/http/cloud-proxy.js +29 -0
  7. package/dist/{server → adapters/http}/http-server.js +12 -13
  8. package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
  9. package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
  10. package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
  11. package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
  12. package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
  13. package/dist/adapters/sdk/index.d.ts +89 -0
  14. package/dist/{sdk → adapters/sdk}/index.js +22 -19
  15. package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
  16. package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
  17. package/dist/domains/auth/auth-errors.js +50 -0
  18. package/dist/domains/auth/auth-headers.js +41 -0
  19. package/dist/domains/auth/auth.normalizers.js +50 -0
  20. package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
  21. package/dist/domains/auth/auth.rules.js +19 -0
  22. package/dist/domains/auth/auth.serializers.js +32 -0
  23. package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
  24. package/dist/domains/auth/auth.validations.js +21 -0
  25. package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
  26. package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
  27. package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
  28. package/dist/domains/auth/token-store.js +81 -0
  29. package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
  30. package/dist/domains/baselines/baseline-download.js +192 -0
  31. package/dist/domains/baselines/baseline-write.js +22 -0
  32. package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
  33. package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
  34. package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
  35. package/dist/domains/baselines/cloud-baselines.js +9 -0
  36. package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
  37. package/dist/domains/builds/build.normalizers.js +35 -0
  38. package/dist/domains/builds/build.serializers.js +164 -0
  39. package/dist/domains/builds/build.service.js +119 -0
  40. package/dist/domains/builds/build.validations.js +39 -0
  41. package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
  42. package/dist/domains/builds/cloud-builds.routes.js +31 -0
  43. package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
  44. package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
  45. package/dist/domains/comparisons/cloud-comparisons.js +43 -0
  46. package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
  47. package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
  48. package/dist/domains/config/config-defaults.js +27 -0
  49. package/dist/{utils → domains/config}/config-discovery.js +1 -9
  50. package/dist/{utils → domains/config}/config-loader.js +10 -9
  51. package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
  52. package/dist/{types → domains/config}/config.d.ts +3 -3
  53. package/dist/domains/config/config.normalizers.js +126 -0
  54. package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
  55. package/dist/domains/config/config.rules.js +47 -0
  56. package/dist/domains/config/config.serializers.js +202 -0
  57. package/dist/domains/config/config.service.js +625 -0
  58. package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
  59. package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
  60. package/dist/domains/context/compact-context.validations.js +60 -0
  61. package/dist/domains/context/context-presentation.js +368 -0
  62. package/dist/domains/context/context.commands.js +385 -0
  63. package/dist/domains/context/context.serializers.js +157 -0
  64. package/dist/domains/context/context.service.js +76 -0
  65. package/dist/domains/context/context.validations.js +100 -0
  66. package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
  67. package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
  68. package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
  69. package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
  70. package/dist/domains/previews/preview.service.js +37 -0
  71. package/dist/domains/projects/cloud-projects.routes.js +28 -0
  72. package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
  73. package/dist/{utils → domains/projects}/project-binding.js +3 -3
  74. package/dist/domains/projects/project-errors.js +61 -0
  75. package/dist/{utils → domains/projects}/project-link-store.js +2 -2
  76. package/dist/domains/projects/project-requests.js +61 -0
  77. package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
  78. package/dist/domains/projects/project.normalizers.js +69 -0
  79. package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
  80. package/dist/domains/projects/project.validations.js +31 -0
  81. package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
  82. package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
  83. package/dist/domains/reports/dashboard.routes.js +96 -0
  84. package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
  85. package/dist/domains/reports/report-assets.routes.js +81 -0
  86. package/dist/domains/reports/report-state.js +51 -0
  87. package/dist/domains/reports/report-store.js +85 -0
  88. package/dist/domains/reports/report.serializers.js +99 -0
  89. package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
  90. package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
  91. package/dist/domains/runs/run-environment.js +69 -0
  92. package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
  93. package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
  94. package/dist/domains/runs/run.normalizers.js +8 -0
  95. package/dist/domains/runs/run.rules.js +56 -0
  96. package/dist/domains/runs/run.serializers.js +101 -0
  97. package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
  98. package/dist/domains/runs/run.validations.js +17 -0
  99. package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
  100. package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
  101. package/dist/domains/screenshots/cloud-screenshots.js +109 -0
  102. package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
  103. package/dist/domains/screenshots/screenshot-hash.js +53 -0
  104. package/dist/domains/screenshots/screenshot-server-options.js +28 -0
  105. package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
  106. package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
  107. package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
  108. package/dist/domains/screenshots/screenshot.serializers.js +146 -0
  109. package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
  110. package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
  112. package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
  113. package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
  114. package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
  115. package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
  116. package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
  117. package/dist/domains/updates/cli-update.js +103 -0
  118. package/dist/domains/updates/update-check.js +2 -0
  119. package/dist/domains/updates/update-notifications.js +175 -0
  120. package/dist/domains/uploads/upload-defaults.js +4 -0
  121. package/dist/domains/uploads/upload-files.js +32 -0
  122. package/dist/domains/uploads/upload-settings.js +21 -0
  123. package/dist/domains/uploads/upload-timing.js +18 -0
  124. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  125. package/dist/domains/uploads/upload.normalizers.js +60 -0
  126. package/dist/domains/uploads/upload.serializers.js +179 -0
  127. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  128. package/dist/domains/uploads/upload.validations.js +77 -0
  129. package/dist/index.js +12 -12
  130. package/dist/reporter/reporter-bundle.iife.js +86 -86
  131. package/dist/reporter-ssr/ssr-entry.js +3 -6
  132. package/dist/shared/cloud-protocol.js +171 -0
  133. package/dist/{utils → shared}/file-changes.js +1 -1
  134. package/dist/shared/files.js +9 -0
  135. package/dist/{utils → shared}/global-config.js +7 -78
  136. package/dist/shared/log-level.js +7 -0
  137. package/dist/{utils → shared}/output.js +1 -1
  138. package/dist/{utils → shared}/package-info.js +5 -4
  139. package/dist/shared/paths.js +57 -0
  140. package/package.json +37 -36
  141. package/dist/api/core.js +0 -382
  142. package/dist/api/endpoints.js +0 -421
  143. package/dist/api/index.js +0 -19
  144. package/dist/auth/core.js +0 -237
  145. package/dist/auth/index.js +0 -31
  146. package/dist/commands/context.js +0 -1066
  147. package/dist/config/core.js +0 -439
  148. package/dist/config/operations.js +0 -327
  149. package/dist/project/core.js +0 -211
  150. package/dist/screenshot-server/core.js +0 -161
  151. package/dist/screenshot-server/index.js +0 -11
  152. package/dist/server/routers/assets.js +0 -95
  153. package/dist/server/routers/cloud-proxy.js +0 -146
  154. package/dist/server/routers/dashboard.js +0 -172
  155. package/dist/services/auth-service.js +0 -117
  156. package/dist/services/config-service.js +0 -313
  157. package/dist/services/project-service.js +0 -88
  158. package/dist/test-runner/core.js +0 -293
  159. package/dist/test-runner/index.js +0 -13
  160. package/dist/types/sdk.d.ts +0 -186
  161. package/dist/uploader/core.js +0 -432
  162. package/dist/uploader/index.js +0 -88
  163. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  164. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  166. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  167. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  168. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  169. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  170. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  171. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  172. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  173. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  174. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  175. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  176. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  177. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  178. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  179. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  180. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  181. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  182. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  183. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  184. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  185. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  186. /package/dist/{utils → shared}/api-url.js +0 -0
  187. /package/dist/{utils → shared}/async-utils.js +0 -0
  188. /package/dist/{utils → shared}/browser.js +0 -0
  189. /package/dist/{utils → shared}/ci-env.js +0 -0
  190. /package/dist/{utils → shared}/colors.js +0 -0
  191. /package/dist/{utils → shared}/fetch-utils.js +0 -0
  192. /package/dist/{utils → shared}/git.js +0 -0
  193. /package/dist/{utils → shared}/patterns.js +0 -0
  194. /package/dist/{utils → shared}/secret-store.js +0 -0
  195. /package/dist/{errors → shared}/vizzly-error.js +0 -0
@@ -1,1066 +0,0 @@
1
- /**
2
- * Context commands - fetch visual context bundles for agents and reviewers
3
- */
4
-
5
- import { createApiClient as defaultCreateApiClient, getBuildContext as defaultGetBuildContext, getComparisonContext as defaultGetComparisonContext, getReviewQueueContext as defaultGetReviewQueueContext, getScreenshotContext as defaultGetScreenshotContext, getSimilarFingerprintContext as defaultGetSimilarFingerprintContext } from '../api/index.js';
6
- import { createLocalWorkspaceContextProvider as defaultCreateLocalWorkspaceContextProvider } from '../context/local-workspace-provider.js';
7
- import { resolveContextSource as defaultResolveContextSource } from '../context/provider-resolver.js';
8
- import { VizzlyError } from '../errors/vizzly-error.js';
9
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
10
- import * as defaultOutput from '../utils/output.js';
11
- import { readSession as defaultReadSession } from '../utils/session.js';
12
- import { normalizeBuildContext } from '../utils/visual-context-normalizers.js';
13
- function buildAuthErrorMessage() {
14
- return 'Authentication required. Use --token, set VIZZLY_TOKEN, run "vizzly login", or link a project.';
15
- }
16
- function buildSourceErrorMessage() {
17
- return '--source must be one of: auto, cloud, local';
18
- }
19
- function buildIncludeErrorMessage() {
20
- return '--include must contain only: diffs';
21
- }
22
- function validateLimitRange(value, flagName, {
23
- min = 1,
24
- max
25
- }) {
26
- if (value == null) {
27
- return [];
28
- }
29
- if (!Number.isInteger(value) || value < min || value > max) {
30
- return [`${flagName} must be an integer between ${min} and ${max}`];
31
- }
32
- return [];
33
- }
34
- function validateCursor(value) {
35
- if (value == null) {
36
- return [];
37
- }
38
- if (typeof value !== 'string' || value.trim().length === 0) {
39
- return ['--cursor must be a non-empty opaque cursor'];
40
- }
41
- return [];
42
- }
43
- function validateOffset(value) {
44
- if (value == null) return [];
45
- if (!Number.isInteger(value) || value < 0) {
46
- return ['--offset must be a non-negative integer'];
47
- }
48
- return [];
49
- }
50
- let COMPACT_CONTEXT_LIMIT = 10;
51
- let COMPACT_SUMMARY_MAX_BYTES = 64 * 1024;
52
- let COMPACT_DIFFS_MAX_BYTES = 512 * 1024;
53
- function getCompactPage(collection, label) {
54
- if (!collection || !Array.isArray(collection.items) || !collection.page) {
55
- throw new VizzlyError(`Vizzly returned an invalid compact ${label} collection`, 'COMPACT_CONTEXT_INVALID');
56
- }
57
- if (collection.items.length > COMPACT_CONTEXT_LIMIT) {
58
- throw new VizzlyError(`Vizzly returned more than ${COMPACT_CONTEXT_LIMIT} compact ${label} items`, 'COMPACT_CONTEXT_INVALID');
59
- }
60
- if (collection.items.some(item => !item || typeof item !== 'object')) {
61
- throw new VizzlyError(`Vizzly returned an invalid item in compact ${label}`, 'COMPACT_CONTEXT_INVALID');
62
- }
63
- let {
64
- page
65
- } = collection;
66
- let validLimit = Number.isInteger(page.limit) && page.limit >= 1 && page.limit <= COMPACT_CONTEXT_LIMIT;
67
- let validReturned = Number.isInteger(page.returned) && page.returned === collection.items.length;
68
- let validTotal = page.total == null || Number.isInteger(page.total) && page.total >= 0 && page.total >= page.returned;
69
- let validHasMore = typeof page.has_more === 'boolean';
70
- let validCursor = page.next_cursor == null || typeof page.next_cursor === 'string' && page.next_cursor.length > 0;
71
- if (!validLimit || !validReturned || !validTotal || !validHasMore || !validCursor) {
72
- throw new VizzlyError(`Vizzly returned invalid pagination facts for compact ${label}`, 'COMPACT_CONTEXT_INVALID');
73
- }
74
- if (page.included === false && collection.items.length > 0) {
75
- throw new VizzlyError(`Vizzly returned items for an omitted compact ${label} collection`, 'COMPACT_CONTEXT_INVALID');
76
- }
77
- if (page.has_more !== Boolean(page.next_cursor)) {
78
- throw new VizzlyError(`Vizzly returned contradictory pagination facts for compact ${label}`, 'COMPACT_CONTEXT_INVALID');
79
- }
80
- return collection;
81
- }
82
- function validateCompactContext(context, resource) {
83
- if (!context || typeof context !== 'object') {
84
- throw new VizzlyError(`Vizzly returned an invalid compact ${resource} context`, 'COMPACT_CONTEXT_INVALID');
85
- }
86
- if (context.resource !== `${resource}_context` || !context[resource]?.id) {
87
- throw new VizzlyError(`Vizzly returned an invalid compact ${resource} context`, 'COMPACT_CONTEXT_INVALID');
88
- }
89
- if (resource === 'build') {
90
- getCompactPage(context.evidence, 'build evidence');
91
- } else {
92
- let history = context.history || {};
93
- getCompactPage(history.similar_by_fingerprint, 'similar history');
94
- getCompactPage(history.recent_by_name, 'recent history');
95
- }
96
- return context;
97
- }
98
- function validateCompactOutputSize(payload, resource, details) {
99
- let bytes = Buffer.byteLength(JSON.stringify(payload));
100
- let maxBytes = details === 'diffs' ? COMPACT_DIFFS_MAX_BYTES : COMPACT_SUMMARY_MAX_BYTES;
101
- if (bytes > maxBytes) {
102
- throw new VizzlyError(`Vizzly produced an oversized compact ${resource} context`, 'COMPACT_CONTEXT_OVERSIZED', {
103
- bytes,
104
- max_bytes: maxBytes
105
- });
106
- }
107
- return payload;
108
- }
109
- function validateSourceOption(value) {
110
- if (value == null) {
111
- return [];
112
- }
113
- if (!['auto', 'cloud', 'local'].includes(value)) {
114
- return [buildSourceErrorMessage()];
115
- }
116
- return [];
117
- }
118
- function parseIncludeOption(value) {
119
- if (!value) {
120
- return [];
121
- }
122
- let rawItems = Array.isArray(value) ? value : String(value).split(',');
123
- return rawItems.map(item => item.trim()).filter(Boolean);
124
- }
125
- function buildCompactContextRequest(options = {}, globalOptions = {}) {
126
- let include = parseIncludeOption(options.include);
127
- let compact = !options.full && (!globalOptions.json || options.agent);
128
- let details = include.includes('diffs') ? 'diffs' : 'summary';
129
- return {
130
- compact,
131
- details,
132
- include,
133
- query: compact ? {
134
- details,
135
- limit: COMPACT_CONTEXT_LIMIT,
136
- cursor: options.cursor
137
- } : undefined
138
- };
139
- }
140
- function validateIncludeOption(value) {
141
- let allowed = new Set(['diffs']);
142
- let invalid = parseIncludeOption(value).filter(item => !allowed.has(item));
143
- return invalid.length > 0 ? [buildIncludeErrorMessage()] : [];
144
- }
145
- function validateScopedProjectOptions(options = {}) {
146
- let errors = [];
147
- if (options.org && !options.project) {
148
- errors.push('--org requires --project');
149
- }
150
- return errors;
151
- }
152
- function hasExplicitCloudScope(options = {}, config = {}) {
153
- return Boolean(options.org || options.project || config.linkedProject?.organizationSlug || config.linkedProject?.projectSlug);
154
- }
155
- function createClient(config, createApiClient) {
156
- return createApiClient({
157
- baseUrl: config.apiUrl,
158
- token: config.apiKey || config.userToken,
159
- command: 'context'
160
- });
161
- }
162
-
163
- /**
164
- * Attach the provider that produced a context payload.
165
- *
166
- * Context endpoints return API-owned evidence, while source selection belongs
167
- * to the CLI. Keeping both in one payload prevents follow-up agent commands
168
- * from silently crossing between cloud and local evidence.
169
- *
170
- * @param {Object} context - Context payload returned by a provider.
171
- * @param {string} source - Provider source selected by the CLI.
172
- * @returns {Object} Context payload with explicit source provenance.
173
- */
174
- function attachContextSource(context, source) {
175
- return {
176
- ...context,
177
- source
178
- };
179
- }
180
- async function loadContextConfig(globalOptions, options, deps) {
181
- let {
182
- loadConfig = defaultLoadConfig,
183
- requireApiKey = true,
184
- output = defaultOutput,
185
- exit = code => process.exit(code)
186
- } = deps;
187
- let allOptions = {
188
- ...globalOptions,
189
- ...options
190
- };
191
- let config = await loadConfig(globalOptions.config, allOptions);
192
- if (requireApiKey && !config.apiKey && !config.userToken) {
193
- output.error(buildAuthErrorMessage());
194
- output.cleanup();
195
- exit(1);
196
- return null;
197
- }
198
- return config;
199
- }
200
- function createCloudContextProvider(config, deps = {}) {
201
- let {
202
- createApiClient = defaultCreateApiClient,
203
- getBuildContext = defaultGetBuildContext,
204
- getComparisonContext = defaultGetComparisonContext,
205
- getScreenshotContext = defaultGetScreenshotContext,
206
- getSimilarFingerprintContext = defaultGetSimilarFingerprintContext,
207
- getReviewQueueContext = defaultGetReviewQueueContext
208
- } = deps;
209
- let client = createClient(config, createApiClient);
210
- return {
211
- source: 'cloud',
212
- async getBuildContext(buildId, query) {
213
- return attachContextSource(await getBuildContext(client, buildId, query), 'cloud');
214
- },
215
- async getComparisonContext(comparisonId, query) {
216
- return attachContextSource(await getComparisonContext(client, comparisonId, query), 'cloud');
217
- },
218
- async getScreenshotContext(screenshotName, query) {
219
- return attachContextSource(await getScreenshotContext(client, screenshotName, query), 'cloud');
220
- },
221
- async getSimilarFingerprintContext(fingerprintHash, query) {
222
- return attachContextSource(await getSimilarFingerprintContext(client, fingerprintHash, query), 'cloud');
223
- },
224
- async getReviewQueueContext(query) {
225
- return attachContextSource(await getReviewQueueContext(client, query), 'cloud');
226
- }
227
- };
228
- }
229
- function buildLocalFingerprintCapabilityError() {
230
- let error = new Error('Local workspace context does not support fingerprint similarity yet. Use --source cloud for this query.');
231
- error.code = 'LOCAL_WORKSPACE_CONTEXT';
232
- return error;
233
- }
234
- function resolveBuildContextId(buildId, runtime, deps = {}) {
235
- let {
236
- readSession = defaultReadSession
237
- } = deps;
238
- if (buildId !== 'current' || runtime.source !== 'cloud') {
239
- return buildId;
240
- }
241
- let session = readSession({
242
- cwd: runtime.projectRoot
243
- });
244
- if (session?.buildId && !session.expired) {
245
- return session.buildId;
246
- }
247
- let error = new Error('No current cloud build found. Run "vizzly run" first, or pass a build ID.');
248
- error.code = 'NO_CURRENT_CLOUD_BUILD';
249
- throw error;
250
- }
251
- function shouldExplainLocalSimilarityGap(requestedSource, command, localProvider) {
252
- return requestedSource === 'auto' && command === 'similar' && localProvider.isAvailable();
253
- }
254
- async function loadContextRuntime(command, target, globalOptions, options, deps = {}) {
255
- let {
256
- createLocalWorkspaceContextProvider = defaultCreateLocalWorkspaceContextProvider,
257
- resolveContextSource = defaultResolveContextSource,
258
- output = defaultOutput,
259
- exit = code => process.exit(code)
260
- } = deps;
261
- let config = await loadContextConfig(globalOptions, options, {
262
- ...deps,
263
- output,
264
- exit,
265
- requireApiKey: false
266
- });
267
- let requestedSource = options.source || 'auto';
268
- let projectRoot = deps.projectRoot || process.cwd();
269
- let localProvider = createLocalWorkspaceContextProvider({
270
- projectRoot
271
- });
272
- let source = resolveContextSource({
273
- requestedSource,
274
- command,
275
- target,
276
- projectRoot,
277
- hasCloudScope: hasExplicitCloudScope(options, config)
278
- }, {
279
- createLocalWorkspaceContextProvider
280
- });
281
- if (source === 'cloud' && !config.apiKey && !config.userToken) {
282
- if (shouldExplainLocalSimilarityGap(requestedSource, command, localProvider)) {
283
- throw buildLocalFingerprintCapabilityError();
284
- }
285
- output.error(buildAuthErrorMessage());
286
- output.cleanup();
287
- exit(1);
288
- return null;
289
- }
290
- let provider = source === 'local' ? localProvider : createCloudContextProvider(config, deps);
291
- return {
292
- config,
293
- source,
294
- provider
295
- };
296
- }
297
- function buildScopeQuery(options = {}, query = {}) {
298
- let scopedQuery = {
299
- ...query
300
- };
301
- if (options.project) {
302
- scopedQuery.project = options.project;
303
- }
304
- if (options.org) {
305
- scopedQuery.organization = options.org;
306
- }
307
- return scopedQuery;
308
- }
309
- function getStatusTone(colors, status) {
310
- if (status === 'changed' || status === 'pending' || status === 'failed') {
311
- return colors.brand.warning;
312
- }
313
- if (status === 'approved' || status === 'completed' || status === 'identical') {
314
- return colors.brand.success;
315
- }
316
- if (status === 'rejected' || status === 'error') {
317
- return colors.brand.error;
318
- }
319
- return colors.brand.info;
320
- }
321
- function getComparisonDisplayState(comparison = {}) {
322
- return comparison.result || comparison.status || 'unknown';
323
- }
324
- function getComparisonName(comparison = {}) {
325
- return comparison.screenshot_name || comparison.screenshot?.name || comparison.name || comparison.id || 'unknown screenshot';
326
- }
327
- function getComparisonDiffPercentage(comparison = {}) {
328
- return comparison.diff?.percentage ?? comparison.diff_percentage ?? null;
329
- }
330
- function getComparisonFingerprint(comparison = {}) {
331
- return comparison.diff?.fingerprint_hash || comparison.analysis?.fingerprint_hash || null;
332
- }
333
-
334
- /**
335
- * Quote a value only when a suggested command needs shell protection.
336
- *
337
- * Suggested commands are meant to be executable, so names with whitespace or
338
- * apostrophes must survive copy and paste without changing their value.
339
- *
340
- * @param {unknown} value - CLI argument value.
341
- * @returns {string} A shell-safe argument for the displayed command.
342
- */
343
- function quoteCommandArgument(value) {
344
- let stringValue = String(value);
345
- if (/^[A-Za-z0-9._:/-]+$/.test(stringValue)) {
346
- return stringValue;
347
- }
348
- return `'${stringValue.replaceAll("'", `'\\''`)}'`;
349
- }
350
-
351
- /**
352
- * Recognize both local source labels emitted across supported context shapes.
353
- *
354
- * @param {Object} context - Normalized context response.
355
- * @returns {boolean} Whether follow-up commands must stay in local mode.
356
- */
357
- function isLocalContext(context = {}) {
358
- return ['local', 'local_workspace'].includes(context.source);
359
- }
360
-
361
- /**
362
- * Keep executable suggestions on the source that produced their evidence.
363
- *
364
- * Mixed workspaces can contain both a cloud run session and persisted local
365
- * TDD results. Pinning either source prevents a follow-up command from
366
- * silently crossing that boundary and describing a different build.
367
- *
368
- * @param {string} command - Base CLI command.
369
- * @param {Object} context - Context that produced the command.
370
- * @returns {string} Command pinned to its originating source.
371
- */
372
- function appendContextSource(command, context = {}) {
373
- let source = isLocalContext(context) ? 'local' : 'cloud';
374
- return `${command} --source ${source}`;
375
- }
376
- function buildCompactBuildCommands(context = {}, include = [], cursor = null) {
377
- let evidence = context.evidence?.items || [];
378
- let page = context.evidence?.page || {};
379
- let commands = [];
380
- let buildTarget = isLocalContext(context) ? 'current' : context.build?.id || null;
381
- let firstComparison = evidence.find(item => item.type === 'comparison' && item.id);
382
- let firstNamedEvidence = evidence.find(item => item.screenshot_name);
383
- if (firstComparison) {
384
- commands.push({
385
- label: 'Inspect comparison context',
386
- command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(firstComparison.id)} --agent`, context)
387
- });
388
- }
389
- if (firstNamedEvidence) {
390
- let screenshotName = firstNamedEvidence.screenshot_name;
391
- commands.push({
392
- label: 'Inspect screenshot history',
393
- command: appendContextSource(`vizzly --json context screenshot ${quoteCommandArgument(screenshotName)}`, context)
394
- });
395
- }
396
- if (buildTarget && evidence.length > 0 && !include.includes('diffs')) {
397
- let cursorFlag = cursor ? ` --cursor ${quoteCommandArgument(cursor)}` : '';
398
- commands.push({
399
- label: 'Load raw diff diagnostics',
400
- command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --include diffs${cursorFlag}`, context)
401
- });
402
- }
403
- if (buildTarget && page.has_more && page.next_cursor) {
404
- let includeFlag = include.length > 0 ? ` --include ${include.join(',')}` : '';
405
- commands.push({
406
- label: 'Load next evidence page',
407
- command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --cursor ${quoteCommandArgument(page.next_cursor)}${includeFlag}`, context)
408
- });
409
- }
410
- if (buildTarget) {
411
- commands.push({
412
- label: 'Load full build context',
413
- command: appendContextSource(`vizzly --json context build ${quoteCommandArgument(buildTarget)} --agent --full`, context)
414
- });
415
- }
416
- return commands;
417
- }
418
- function buildCompactBuildPayload(context, include = [], cursor = null) {
419
- return {
420
- ...context,
421
- resource: 'build_agent_context',
422
- suggested_commands: buildCompactBuildCommands(context, include, cursor)
423
- };
424
- }
425
- function buildCompactComparisonCommands(context = {}, include = []) {
426
- let commands = [];
427
- let comparisonId = context.comparison?.id;
428
- let streams = [['similar_by_fingerprint', 'similar history'], ['recent_by_name', 'recent history']];
429
- let includeFlag = include.length > 0 ? ` --include ${include.join(',')}` : '';
430
- if (comparisonId && !include.includes('diffs')) {
431
- commands.push({
432
- label: 'Load raw diff diagnostics',
433
- command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(comparisonId)} --agent --include diffs`, context)
434
- });
435
- }
436
- for (let [stream, label] of streams) {
437
- let page = context.history?.[stream]?.page;
438
- if (!comparisonId || !page?.has_more || !page.next_cursor) {
439
- continue;
440
- }
441
- commands.push({
442
- label: `Load next ${label} page`,
443
- command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(comparisonId)} --agent --cursor ${quoteCommandArgument(page.next_cursor)}${includeFlag}`, context)
444
- });
445
- }
446
- if (comparisonId) {
447
- commands.push({
448
- label: 'Load full comparison context',
449
- command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(comparisonId)} --agent --full`, context)
450
- });
451
- }
452
- return commands;
453
- }
454
- function buildCompactComparisonPayload(context, include = []) {
455
- return {
456
- ...context,
457
- resource: 'comparison_agent_context',
458
- suggested_commands: buildCompactComparisonCommands(context, include)
459
- };
460
- }
461
- function formatKnownBoolean(value) {
462
- if (value === true) return 'yes';
463
- if (value === false) return 'no';
464
- return 'unknown';
465
- }
466
- function formatCompactPage(collection) {
467
- let page = collection?.page || {};
468
- let returned = page.returned ?? collection?.items?.length;
469
- let total = page.total;
470
- if (returned == null) return 'unknown';
471
- if (total == null) return String(returned);
472
- return `${returned} of ${total}${page.has_more ? ' · more available' : ''}`;
473
- }
474
- function displayCompactHeading(output, context, title, status) {
475
- let colors = output.getColors();
476
- let tone = getStatusTone(colors, status);
477
- let organization = context.scope?.organization?.slug || 'unknown';
478
- let project = context.scope?.project?.slug || 'unknown';
479
- output.print(` ${colors.bold(title)} ${tone(status.toUpperCase())}`);
480
- output.print(` ${colors.dim(`@${organization}/${project}`)}`);
481
- output.blank();
482
- }
483
- function displayCompactBuildContext(output, context) {
484
- output.header('context', 'build');
485
- let colors = output.getColors();
486
- let build = context.build || {};
487
- let displayStatus = build.status || context.status?.state || 'unknown';
488
- displayCompactHeading(output, context, build.name || build.id || 'unknown build', displayStatus);
489
- output.labelValue('Attention', formatKnownBoolean(context.status?.needs_review));
490
- output.labelValue('Evidence', formatCompactPage(context.evidence));
491
- let items = context.evidence?.items || [];
492
- if (items.length > 0) {
493
- output.blank();
494
- output.print(' Evidence');
495
- for (let item of items) {
496
- let result = item.result || item.status || 'unknown';
497
- let percentage = getComparisonDiffPercentage(item);
498
- let detail = percentage == null ? '' : ` · ${percentage}% diff`;
499
- output.print(` ${colors.dim('•')} ${getComparisonName(item)}: ${result}${detail}`);
500
- }
501
- }
502
- if (context.evidence?.page?.has_more) {
503
- let next = context.suggested_commands?.find(command => command.label === 'Load next evidence page');
504
- if (next) {
505
- output.blank();
506
- output.labelValue('Next', next.command);
507
- }
508
- }
509
- if (context.links?.build_url) {
510
- output.labelValue('Build URL', context.links.build_url);
511
- }
512
- }
513
- function displayCompactComparisonContext(output, context) {
514
- output.header('context', 'comparison');
515
- let comparison = context.comparison || {};
516
- let displayState = getComparisonDisplayState(comparison);
517
- displayCompactHeading(output, context, getComparisonName(comparison), displayState);
518
- output.labelValue('Images', comparison.diff?.image_url || comparison.analysis?.diff_image_url ? 'baseline/current/diff available' : 'unavailable');
519
- output.labelValue('Similar history', formatCompactPage(context.history?.similar_by_fingerprint));
520
- output.labelValue('Recent history', formatCompactPage(context.history?.recent_by_name));
521
- let similar = context.history?.similar_by_fingerprint?.items || [];
522
- if (similar.length > 0) {
523
- output.blank();
524
- output.print(' Similar Diffs');
525
- printComparisonList(output, similar);
526
- }
527
- let recent = context.history?.recent_by_name?.items || [];
528
- if (recent.length > 0) {
529
- output.blank();
530
- output.print(' Recent Diffs');
531
- printComparisonList(output, recent);
532
- }
533
- if (context.links?.comparison_url) {
534
- output.labelValue('Comparison URL', context.links.comparison_url);
535
- }
536
- }
537
- function getBuildCommentsCount(context = {}) {
538
- if (Array.isArray(context.comments?.build)) {
539
- return context.comments.build.length;
540
- }
541
- if (Array.isArray(context.review?.comments)) {
542
- return context.review.comments.length;
543
- }
544
- return 0;
545
- }
546
- function getScreenshotCommentsCount(context = {}) {
547
- if (Number.isInteger(context.comments?.screenshot_count)) {
548
- return context.comments.screenshot_count;
549
- }
550
- return 0;
551
- }
552
- function getReviewAssignmentsCount(context = {}) {
553
- if (Array.isArray(context.review?.assignments)) {
554
- return context.review.assignments.length;
555
- }
556
- return 0;
557
- }
558
- function formatNeedsReview(status = {}) {
559
- if (!status || status.needs_review == null) {
560
- return null;
561
- }
562
- if (!status.needs_review) {
563
- return 'no';
564
- }
565
- let details = [];
566
- if (status.pending_comparisons != null) {
567
- details.push(`${status.pending_comparisons} comparisons`);
568
- }
569
- if (status.unresolved_comments != null) {
570
- details.push(`${status.unresolved_comments} unresolved comments`);
571
- }
572
- return details.length > 0 ? `yes · ${details.join(' · ')}` : 'yes';
573
- }
574
- function printComparisonList(output, comparisons = [], {
575
- limit = 5
576
- } = {}) {
577
- let colors = output.getColors();
578
- for (let comparison of comparisons.slice(0, limit)) {
579
- let displayState = getComparisonDisplayState(comparison);
580
- let statusTone = getStatusTone(colors, displayState);
581
- let screenshotName = getComparisonName(comparison);
582
- let rawDiffPercentage = getComparisonDiffPercentage(comparison);
583
- let diffPercentage = rawDiffPercentage == null ? null : `${rawDiffPercentage}%`;
584
- let fingerprint = getComparisonFingerprint(comparison);
585
- let details = [];
586
- if (diffPercentage) {
587
- details.push(diffPercentage);
588
- }
589
- if (comparison.needs_review) {
590
- details.push('needs review');
591
- }
592
- if (fingerprint) {
593
- details.push(`fp:${fingerprint}`);
594
- }
595
- if (comparison.build_branch) {
596
- details.push(comparison.build_branch);
597
- }
598
- output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
599
- if (details.length > 0) {
600
- output.print(` ${colors.dim(details.join(' · '))}`);
601
- }
602
- }
603
- }
604
- function getGroupDisplayState(group = {}) {
605
- let aggregate = group.aggregate_status || {};
606
- if ((aggregate.failed_count ?? 0) > 0) return 'failed';
607
- if (aggregate.has_rejected === true) return 'rejected';
608
- if (aggregate.has_changes === true) return 'changed';
609
- if (aggregate.has_new === true) return 'new';
610
- if (aggregate.needs_review === true) return 'needs review';
611
- if (aggregate.all_approved === true) return 'approved';
612
- return null;
613
- }
614
- function formatViewport(viewport) {
615
- if (!viewport) {
616
- return null;
617
- }
618
- return `${viewport.width ?? '?'}×${viewport.height ?? '?'}`;
619
- }
620
- function printScreenshotGroups(output, groups = [], {
621
- limit = 8
622
- } = {}) {
623
- let colors = output.getColors();
624
- let visibleGroups = groups.slice(0, limit);
625
- output.print(' Screenshot groups');
626
- for (let group of visibleGroups) {
627
- let displayState = getGroupDisplayState(group);
628
- let tone = getStatusTone(colors, displayState);
629
- let aggregate = group.aggregate_status || {};
630
- let details = [];
631
- if (group.variant_count != null) {
632
- details.push(`${group.variant_count} variants`);
633
- }
634
- if (aggregate.needs_review_count != null) {
635
- details.push(`${aggregate.needs_review_count} needs review`);
636
- }
637
- if (aggregate.failed_count != null && aggregate.failed_count > 0) {
638
- details.push(`${aggregate.failed_count} failed`);
639
- }
640
- if (aggregate.max_diff_percentage != null) {
641
- details.push(`${aggregate.max_diff_percentage}% max diff`);
642
- }
643
- output.print(` ${colors.bold(group.name)}${displayState ? ` ${tone(displayState.toUpperCase())}` : ''}`);
644
- if (details.length > 0) {
645
- output.print(` ${colors.dim(details.join(' · '))}`);
646
- }
647
- for (let variant of group.variants.slice(0, 3)) {
648
- let variantDetails = [variant.review_state, variant.browser, formatViewport(variant.viewport)].filter(Boolean);
649
- output.print(` ${getStatusTone(colors, variant.result)((variant.result || 'unknown').toUpperCase())}${variantDetails.length > 0 ? ` · ${colors.dim(variantDetails.join(' · '))}` : ''}`);
650
- }
651
- }
652
- if (groups.length > visibleGroups.length) {
653
- output.print(` ${colors.dim(`...${groups.length - visibleGroups.length} more groups`)}`);
654
- }
655
- }
656
- function printFailedCaptures(output, captures = []) {
657
- let colors = output.getColors();
658
- output.print(' Failed captures');
659
- for (let capture of captures.slice(0, 8)) {
660
- let details = [capture.error_message, capture.browser, formatViewport(capture.viewport), capture.screenshot.url].filter(Boolean);
661
- output.print(` ${colors.bold(capture.name)} ${colors.brand.error('FAILED')}`);
662
- if (details.length > 0) {
663
- output.print(` ${colors.dim(details.join(' · '))}`);
664
- }
665
- }
666
- }
667
- function formatReviewSummary(review = {}) {
668
- let details = [review.pending != null ? `${review.pending} pending` : null, review.approved != null ? `${review.approved} approved` : null, review.rejected != null ? `${review.rejected} rejected` : null].filter(Boolean);
669
- return details.length > 0 ? details.join(' · ') : null;
670
- }
671
- function displayBuildContext(output, context) {
672
- output.header('context', 'build');
673
- let normalizedContext = normalizeBuildContext(context);
674
- let colors = output.getColors();
675
- let build = normalizedContext.build || {};
676
- let buildTone = getStatusTone(colors, build.status);
677
- let comparisons = normalizedContext.comparisons;
678
- let groups = normalizedContext.groups;
679
- let screenshots = normalizedContext.screenshots || [];
680
- let failedCaptures = normalizedContext.failed_captures;
681
- let reviewSummary = formatReviewSummary(normalizedContext.summary?.review);
682
- let commentsSummary = normalizedContext.summary?.comments || {};
683
- let needsReview = formatNeedsReview(normalizedContext.status);
684
- let baseline = normalizedContext.baseline?.selected || null;
685
- let comparisonCount = normalizedContext.summary?.comparisons?.total ?? normalizedContext.total_comparisons ?? (Array.isArray(context.comparisons) ? comparisons.length : null);
686
- let screenshotCount = normalizedContext.summary?.screenshots?.total ?? normalizedContext.screenshot_count ?? (Array.isArray(context.screenshots) ? screenshots.length : null);
687
- output.print(` ${colors.bold(build.name || build.id || 'unknown build')} ${buildTone((build.status || 'unknown').toUpperCase())}`);
688
- output.print(` ${colors.dim(`@${normalizedContext.scope?.organization?.slug || 'unknown'}/${normalizedContext.scope?.project?.slug || 'unknown'}`)}`);
689
- output.blank();
690
- if (comparisonCount != null) {
691
- output.labelValue('Comparisons', String(comparisonCount));
692
- }
693
- if (groups.length > 0) {
694
- output.labelValue('Screenshot Groups', String(groups.length));
695
- }
696
- if (screenshotCount != null) {
697
- output.labelValue('Screenshots', String(screenshotCount));
698
- }
699
- if (baseline) {
700
- output.labelValue('Baseline', `${baseline.name || baseline.id || 'selected'}${normalizedContext.baseline.selection_reason ? ` · ${normalizedContext.baseline.selection_reason}` : ''}`);
701
- }
702
- if (needsReview) {
703
- output.labelValue('Needs Review', needsReview);
704
- }
705
- if (reviewSummary) {
706
- output.labelValue('Review', reviewSummary);
707
- }
708
- let buildComments = commentsSummary.build ?? (Array.isArray(normalizedContext.comments?.build) ? getBuildCommentsCount(normalizedContext) : null);
709
- let screenshotComments = commentsSummary.screenshot ?? (Number.isInteger(normalizedContext.comments?.screenshot_count) ? getScreenshotCommentsCount(normalizedContext) : null);
710
- let assignments = Array.isArray(normalizedContext.review?.assignments) ? getReviewAssignmentsCount(normalizedContext) : null;
711
- let memory = [buildComments != null ? `${buildComments} build comments` : null, screenshotComments != null ? `${screenshotComments} screenshot comments` : null, assignments != null ? `${assignments} assignments` : null].filter(Boolean);
712
- if (memory.length > 0) {
713
- output.labelValue('Memory', memory.join(' · '));
714
- }
715
- if (normalizedContext.preview) {
716
- let previewUrl = normalizedContext.preview.preview_url || normalizedContext.preview.url;
717
- output.labelValue('Preview', `${normalizedContext.preview.status || 'unknown'}${previewUrl ? ' · available' : ''}`);
718
- }
719
- if (normalizedContext.links?.build_url) {
720
- output.labelValue('Build URL', normalizedContext.links.build_url);
721
- }
722
- if (groups.length > 0) {
723
- output.blank();
724
- printScreenshotGroups(output, groups);
725
- }
726
- if (failedCaptures.length > 0) {
727
- output.blank();
728
- printFailedCaptures(output, failedCaptures);
729
- }
730
- }
731
- function countScreenshotCommentEntries(groups = []) {
732
- return groups.reduce((total, group) => total + (group.comments?.length || 0), 0);
733
- }
734
- function displayComparisonContext(output, context) {
735
- output.header('context', 'comparison');
736
- let colors = output.getColors();
737
- let displayState = getComparisonDisplayState(context.comparison);
738
- let statusTone = getStatusTone(colors, displayState);
739
- let screenshotName = context.comparison.screenshot?.name || context.comparison.id;
740
- let analysis = context.comparison.analysis || {};
741
- output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
742
- output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
743
- output.blank();
744
- output.labelValue('Eyes', `${analysis.diff_image_url ? 'baseline/current/diff' : 'comparison metadata only'}`);
745
- output.labelValue('Memory', `${context.history.similar_by_fingerprint.length} similar · ${context.history.recent_by_name.length} recent`);
746
- output.labelValue('Review', `${context.review.build_comments.length} build comments · ${countScreenshotCommentEntries(context.review.screenshot_comments)} screenshot comments`);
747
- if (analysis.fingerprint_hash) {
748
- output.labelValue('Fingerprint', analysis.fingerprint_hash);
749
- }
750
- if (context.links?.comparison_url) {
751
- output.labelValue('Comparison URL', context.links.comparison_url);
752
- }
753
- if (context.history.similar_by_fingerprint.length > 0) {
754
- output.blank();
755
- output.print(' Similar Diffs');
756
- printComparisonList(output, context.history.similar_by_fingerprint);
757
- }
758
- }
759
- function displayScreenshotContext(output, context) {
760
- output.header('context', 'screenshot');
761
- let colors = output.getColors();
762
- output.print(` ${colors.bold(context.screenshot.name)}`);
763
- output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
764
- output.blank();
765
- output.labelValue('Memory', `${context.history.recent_comparisons.length} recent comparisons`);
766
- if (context.history.recent_comparisons.length > 0) {
767
- output.blank();
768
- output.print(' Recent Comparisons');
769
- printComparisonList(output, context.history.recent_comparisons);
770
- }
771
- }
772
- function displayFingerprintContext(output, context) {
773
- output.header('context', 'similar');
774
- let colors = output.getColors();
775
- let fingerprintHash = context.fingerprint?.hash || context.fingerprint_hash || 'unknown';
776
- let comparisons = context.comparisons || context.matches || [];
777
- output.print(` ${colors.bold(fingerprintHash)}`);
778
- output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
779
- output.blank();
780
- output.labelValue('Matches', String(comparisons.length));
781
- if (comparisons.length > 0) {
782
- output.blank();
783
- output.print(' Similar Diffs');
784
- printComparisonList(output, comparisons, {
785
- limit: 10
786
- });
787
- }
788
- }
789
- function displayReviewQueueContext(output, context) {
790
- output.header('context', 'review');
791
- let colors = output.getColors();
792
- output.print(` ${colors.bold(`${context.summary.total} pending comparisons`)}`);
793
- output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
794
- output.blank();
795
- output.labelValue('Queue', `${context.summary.changed} changed · ${context.summary.new} new · ${context.summary.builds} builds`);
796
- if (context.comparisons.length > 0) {
797
- output.blank();
798
- output.print(' Needs Review');
799
- printComparisonList(output, context.comparisons, {
800
- limit: 10
801
- });
802
- }
803
- }
804
- export async function contextBuildCommand(buildId, options = {}, globalOptions = {}, deps = {}) {
805
- let {
806
- output = defaultOutput,
807
- exit = code => process.exit(code)
808
- } = deps;
809
- output.configure({
810
- json: globalOptions.json,
811
- verbose: globalOptions.verbose,
812
- color: !globalOptions.noColor
813
- });
814
- try {
815
- let runtime = await loadContextRuntime('build', buildId, globalOptions, options, {
816
- ...deps,
817
- output,
818
- exit
819
- });
820
- if (!runtime) {
821
- return;
822
- }
823
- let resolvedBuildId = resolveBuildContextId(buildId, runtime, deps);
824
- let {
825
- compact,
826
- details,
827
- include,
828
- query
829
- } = buildCompactContextRequest(options, globalOptions);
830
- output.startSpinner('Fetching build context...');
831
- let context = await runtime.provider.getBuildContext(resolvedBuildId, query);
832
- output.stopSpinner();
833
- if (compact) {
834
- let compactContext = validateCompactContext(context, 'build');
835
- let payload = validateCompactOutputSize(buildCompactBuildPayload(compactContext, include, options.cursor), 'build', details);
836
- if (globalOptions.json) {
837
- output.data(payload);
838
- } else {
839
- displayCompactBuildContext(output, payload);
840
- }
841
- output.cleanup();
842
- return;
843
- }
844
- if (globalOptions.json) {
845
- output.data(context);
846
- output.cleanup();
847
- return;
848
- }
849
- displayBuildContext(output, context);
850
- output.cleanup();
851
- } catch (error) {
852
- output.stopSpinner();
853
- output.error('Failed to fetch build context', error);
854
- output.cleanup();
855
- exit(1);
856
- }
857
- }
858
- export async function contextComparisonCommand(comparisonId, options = {}, globalOptions = {}, deps = {}) {
859
- let {
860
- output = defaultOutput,
861
- exit = code => process.exit(code)
862
- } = deps;
863
- output.configure({
864
- json: globalOptions.json,
865
- verbose: globalOptions.verbose,
866
- color: !globalOptions.noColor
867
- });
868
- try {
869
- let runtime = await loadContextRuntime('comparison', comparisonId, globalOptions, options, {
870
- ...deps,
871
- output,
872
- exit
873
- });
874
- if (!runtime) {
875
- return;
876
- }
877
- let {
878
- compact,
879
- details,
880
- include,
881
- query
882
- } = buildCompactContextRequest(options, globalOptions);
883
- output.startSpinner('Fetching comparison context...');
884
- let context = await runtime.provider.getComparisonContext(comparisonId, query);
885
- output.stopSpinner();
886
- if (compact) {
887
- let compactContext = validateCompactContext(context, 'comparison');
888
- let payload = validateCompactOutputSize(buildCompactComparisonPayload(compactContext, include), 'comparison', details);
889
- if (globalOptions.json) {
890
- output.data(payload);
891
- } else {
892
- displayCompactComparisonContext(output, payload);
893
- }
894
- output.cleanup();
895
- return;
896
- }
897
- if (globalOptions.json) {
898
- output.data(context);
899
- output.cleanup();
900
- return;
901
- }
902
- displayComparisonContext(output, context);
903
- output.cleanup();
904
- } catch (error) {
905
- output.stopSpinner();
906
- output.error('Failed to fetch comparison context', error);
907
- output.cleanup();
908
- exit(1);
909
- }
910
- }
911
- export async function contextScreenshotCommand(screenshotName, options = {}, globalOptions = {}, deps = {}) {
912
- let {
913
- output = defaultOutput,
914
- exit = code => process.exit(code)
915
- } = deps;
916
- output.configure({
917
- json: globalOptions.json,
918
- verbose: globalOptions.verbose,
919
- color: !globalOptions.noColor
920
- });
921
- try {
922
- let runtime = await loadContextRuntime('screenshot', screenshotName, globalOptions, options, {
923
- ...deps,
924
- output,
925
- exit
926
- });
927
- if (!runtime) {
928
- return;
929
- }
930
- let query = buildScopeQuery(options, {
931
- recentLimit: options.recentLimit
932
- });
933
- output.startSpinner('Fetching screenshot context...');
934
- let context = await runtime.provider.getScreenshotContext(screenshotName, query);
935
- output.stopSpinner();
936
- if (globalOptions.json) {
937
- output.data(context);
938
- output.cleanup();
939
- return;
940
- }
941
- displayScreenshotContext(output, context);
942
- output.cleanup();
943
- } catch (error) {
944
- output.stopSpinner();
945
- output.error('Failed to fetch screenshot context', error);
946
- output.cleanup();
947
- exit(1);
948
- }
949
- }
950
- export async function contextSimilarCommand(fingerprintHash, options = {}, globalOptions = {}, deps = {}) {
951
- let {
952
- output = defaultOutput,
953
- exit = code => process.exit(code)
954
- } = deps;
955
- output.configure({
956
- json: globalOptions.json,
957
- verbose: globalOptions.verbose,
958
- color: !globalOptions.noColor
959
- });
960
- try {
961
- let runtime = await loadContextRuntime('similar', fingerprintHash, globalOptions, options, {
962
- ...deps,
963
- output,
964
- exit
965
- });
966
- if (!runtime) {
967
- return;
968
- }
969
- let query = buildScopeQuery(options, {
970
- limit: options.limit
971
- });
972
- output.startSpinner('Fetching similar visual context...');
973
- let context = await runtime.provider.getSimilarFingerprintContext(fingerprintHash, query);
974
- output.stopSpinner();
975
- if (globalOptions.json) {
976
- output.data(context);
977
- output.cleanup();
978
- return;
979
- }
980
- displayFingerprintContext(output, context);
981
- output.cleanup();
982
- } catch (error) {
983
- output.stopSpinner();
984
- output.error('Failed to fetch similar visual context', error);
985
- output.cleanup();
986
- exit(1);
987
- }
988
- }
989
- export async function contextReviewQueueCommand(options = {}, globalOptions = {}, deps = {}) {
990
- let {
991
- output = defaultOutput,
992
- exit = code => process.exit(code)
993
- } = deps;
994
- output.configure({
995
- json: globalOptions.json,
996
- verbose: globalOptions.verbose,
997
- color: !globalOptions.noColor
998
- });
999
- try {
1000
- let runtime = await loadContextRuntime('review-queue', null, globalOptions, options, {
1001
- ...deps,
1002
- output,
1003
- exit
1004
- });
1005
- if (!runtime) {
1006
- return;
1007
- }
1008
- let query = buildScopeQuery(options, {
1009
- limit: options.limit,
1010
- offset: options.offset
1011
- });
1012
- output.startSpinner('Fetching review queue context...');
1013
- let context = await runtime.provider.getReviewQueueContext(query);
1014
- output.stopSpinner();
1015
- if (globalOptions.json) {
1016
- output.data(context);
1017
- output.cleanup();
1018
- return;
1019
- }
1020
- displayReviewQueueContext(output, context);
1021
- output.cleanup();
1022
- } catch (error) {
1023
- output.stopSpinner();
1024
- output.error('Failed to fetch review queue context', error);
1025
- output.cleanup();
1026
- exit(1);
1027
- }
1028
- }
1029
- export function validateContextBuildOptions(_options = {}) {
1030
- let errors = validateSourceOption(_options.source);
1031
- errors.push(...validateIncludeOption(_options.include));
1032
- errors.push(...validateCursor(_options.cursor));
1033
- return errors;
1034
- }
1035
- export function validateContextComparisonOptions(options = {}) {
1036
- let errors = [];
1037
- errors.push(...validateSourceOption(options.source));
1038
- errors.push(...validateIncludeOption(options.include));
1039
- errors.push(...validateCursor(options.cursor));
1040
- return errors;
1041
- }
1042
- export function validateContextScreenshotOptions(options = {}) {
1043
- let errors = validateScopedProjectOptions(options);
1044
- errors.push(...validateSourceOption(options.source));
1045
- errors.push(...validateLimitRange(options.recentLimit, '--recent-limit', {
1046
- max: 50
1047
- }));
1048
- return errors;
1049
- }
1050
- export function validateContextSimilarOptions(options = {}) {
1051
- let errors = validateScopedProjectOptions(options);
1052
- errors.push(...validateSourceOption(options.source));
1053
- errors.push(...validateLimitRange(options.limit, '--limit', {
1054
- max: 50
1055
- }));
1056
- return errors;
1057
- }
1058
- export function validateContextReviewQueueOptions(options = {}) {
1059
- let errors = validateScopedProjectOptions(options);
1060
- errors.push(...validateSourceOption(options.source));
1061
- errors.push(...validateLimitRange(options.limit, '--limit', {
1062
- max: 100
1063
- }));
1064
- errors.push(...validateOffset(options.offset));
1065
- return errors;
1066
- }