@vizzly-testing/cli 0.29.6 → 0.31.0
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.
- package/README.md +34 -6
- package/dist/api/client.js +6 -3
- package/dist/api/endpoints.js +59 -0
- package/dist/api/index.js +1 -1
- package/dist/auth/core.js +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/cli.js +206 -51
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +870 -0
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +18 -7
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +64 -26
- package/dist/config/core.js +37 -36
- package/dist/context/local-workspace-provider.js +532 -0
- package/dist/context/provider-resolver.js +36 -0
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +2 -1
- package/dist/reporter/reporter-bundle.iife.js +119 -119
- package/dist/reporter-ssr/ssr-entry.js +605 -509
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +226 -148
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/hotspot-coverage.js +1 -1
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/hotspot-service.js +1 -14
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +47 -7
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/colors.js +46 -44
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +22 -3
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/report-data.js +31 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/dist/utils/security.js +3 -2
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
|
@@ -0,0 +1,870 @@
|
|
|
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 { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
9
|
+
import * as defaultOutput from '../utils/output.js';
|
|
10
|
+
import { readSession as defaultReadSession } from '../utils/session.js';
|
|
11
|
+
function buildAuthErrorMessage() {
|
|
12
|
+
return 'Authentication required. Use --token, set VIZZLY_TOKEN, run "vizzly login", or link a project.';
|
|
13
|
+
}
|
|
14
|
+
function buildSourceErrorMessage() {
|
|
15
|
+
return '--source must be one of: auto, cloud, local';
|
|
16
|
+
}
|
|
17
|
+
function buildIncludeErrorMessage() {
|
|
18
|
+
return '--include must contain only: screenshots, diffs, comments';
|
|
19
|
+
}
|
|
20
|
+
function validateLimitRange(value, flagName, {
|
|
21
|
+
min = 1,
|
|
22
|
+
max
|
|
23
|
+
}) {
|
|
24
|
+
if (value == null) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
28
|
+
return [`${flagName} must be an integer between ${min} and ${max}`];
|
|
29
|
+
}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
function validateOffset(value) {
|
|
33
|
+
if (value == null) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
37
|
+
return ['--offset must be a non-negative integer'];
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
function validateSourceOption(value) {
|
|
42
|
+
if (value == null) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
if (!['auto', 'cloud', 'local'].includes(value)) {
|
|
46
|
+
return [buildSourceErrorMessage()];
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
function parseIncludeOption(value) {
|
|
51
|
+
if (!value) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
let rawItems = Array.isArray(value) ? value : String(value).split(',');
|
|
55
|
+
return rawItems.map(item => item.trim()).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
function validateIncludeOption(value) {
|
|
58
|
+
let allowed = new Set(['screenshots', 'diffs', 'comments']);
|
|
59
|
+
let invalid = parseIncludeOption(value).filter(item => !allowed.has(item));
|
|
60
|
+
return invalid.length > 0 ? [buildIncludeErrorMessage()] : [];
|
|
61
|
+
}
|
|
62
|
+
function validateScopedProjectOptions(options = {}) {
|
|
63
|
+
let errors = [];
|
|
64
|
+
if (options.org && !options.project) {
|
|
65
|
+
errors.push('--org requires --project');
|
|
66
|
+
}
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
function hasExplicitCloudScope(options = {}, config = {}) {
|
|
70
|
+
return Boolean(options.org || options.project || config.linkedProject?.organizationSlug || config.linkedProject?.projectSlug);
|
|
71
|
+
}
|
|
72
|
+
function createClient(config, createApiClient) {
|
|
73
|
+
return createApiClient({
|
|
74
|
+
baseUrl: config.apiUrl,
|
|
75
|
+
token: config.apiKey || config.userToken,
|
|
76
|
+
command: 'context'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function loadContextConfig(globalOptions, options, deps) {
|
|
80
|
+
let {
|
|
81
|
+
loadConfig = defaultLoadConfig,
|
|
82
|
+
requireApiKey = true,
|
|
83
|
+
output = defaultOutput,
|
|
84
|
+
exit = code => process.exit(code)
|
|
85
|
+
} = deps;
|
|
86
|
+
let allOptions = {
|
|
87
|
+
...globalOptions,
|
|
88
|
+
...options
|
|
89
|
+
};
|
|
90
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
91
|
+
if (requireApiKey && !config.apiKey && !config.userToken) {
|
|
92
|
+
output.error(buildAuthErrorMessage());
|
|
93
|
+
output.cleanup();
|
|
94
|
+
exit(1);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
function createCloudContextProvider(config, deps = {}) {
|
|
100
|
+
let {
|
|
101
|
+
createApiClient = defaultCreateApiClient,
|
|
102
|
+
getBuildContext = defaultGetBuildContext,
|
|
103
|
+
getComparisonContext = defaultGetComparisonContext,
|
|
104
|
+
getScreenshotContext = defaultGetScreenshotContext,
|
|
105
|
+
getSimilarFingerprintContext = defaultGetSimilarFingerprintContext,
|
|
106
|
+
getReviewQueueContext = defaultGetReviewQueueContext
|
|
107
|
+
} = deps;
|
|
108
|
+
let client = createClient(config, createApiClient);
|
|
109
|
+
return {
|
|
110
|
+
source: 'cloud',
|
|
111
|
+
async getBuildContext(buildId) {
|
|
112
|
+
return await getBuildContext(client, buildId);
|
|
113
|
+
},
|
|
114
|
+
async getComparisonContext(comparisonId, query) {
|
|
115
|
+
return await getComparisonContext(client, comparisonId, query);
|
|
116
|
+
},
|
|
117
|
+
async getScreenshotContext(screenshotName, query) {
|
|
118
|
+
return await getScreenshotContext(client, screenshotName, query);
|
|
119
|
+
},
|
|
120
|
+
async getSimilarFingerprintContext(fingerprintHash, query) {
|
|
121
|
+
return await getSimilarFingerprintContext(client, fingerprintHash, query);
|
|
122
|
+
},
|
|
123
|
+
async getReviewQueueContext(query) {
|
|
124
|
+
return await getReviewQueueContext(client, query);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildLocalFingerprintCapabilityError() {
|
|
129
|
+
let error = new Error('Local workspace context does not support fingerprint similarity yet. Use --source cloud for this query.');
|
|
130
|
+
error.code = 'LOCAL_WORKSPACE_CONTEXT';
|
|
131
|
+
return error;
|
|
132
|
+
}
|
|
133
|
+
function resolveBuildContextId(buildId, runtime, deps = {}) {
|
|
134
|
+
let {
|
|
135
|
+
readSession = defaultReadSession
|
|
136
|
+
} = deps;
|
|
137
|
+
if (buildId !== 'current' || runtime.source !== 'cloud') {
|
|
138
|
+
return buildId;
|
|
139
|
+
}
|
|
140
|
+
let session = readSession({
|
|
141
|
+
cwd: runtime.projectRoot
|
|
142
|
+
});
|
|
143
|
+
if (session?.buildId && !session.expired) {
|
|
144
|
+
return session.buildId;
|
|
145
|
+
}
|
|
146
|
+
let error = new Error('No current cloud build found. Run "vizzly run" first, or pass a build ID.');
|
|
147
|
+
error.code = 'NO_CURRENT_CLOUD_BUILD';
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
function shouldExplainLocalSimilarityGap(requestedSource, command, localProvider) {
|
|
151
|
+
return requestedSource === 'auto' && command === 'similar' && localProvider.isAvailable();
|
|
152
|
+
}
|
|
153
|
+
async function loadContextRuntime(command, target, globalOptions, options, deps = {}) {
|
|
154
|
+
let {
|
|
155
|
+
createLocalWorkspaceContextProvider = defaultCreateLocalWorkspaceContextProvider,
|
|
156
|
+
resolveContextSource = defaultResolveContextSource,
|
|
157
|
+
output = defaultOutput,
|
|
158
|
+
exit = code => process.exit(code)
|
|
159
|
+
} = deps;
|
|
160
|
+
let config = await loadContextConfig(globalOptions, options, {
|
|
161
|
+
...deps,
|
|
162
|
+
output,
|
|
163
|
+
exit,
|
|
164
|
+
requireApiKey: false
|
|
165
|
+
});
|
|
166
|
+
let requestedSource = options.source || 'auto';
|
|
167
|
+
let projectRoot = deps.projectRoot || process.cwd();
|
|
168
|
+
let localProvider = createLocalWorkspaceContextProvider({
|
|
169
|
+
projectRoot
|
|
170
|
+
});
|
|
171
|
+
let source = resolveContextSource({
|
|
172
|
+
requestedSource,
|
|
173
|
+
command,
|
|
174
|
+
target,
|
|
175
|
+
projectRoot,
|
|
176
|
+
hasCloudScope: hasExplicitCloudScope(options, config)
|
|
177
|
+
}, {
|
|
178
|
+
createLocalWorkspaceContextProvider
|
|
179
|
+
});
|
|
180
|
+
if (source === 'cloud' && !config.apiKey && !config.userToken) {
|
|
181
|
+
if (shouldExplainLocalSimilarityGap(requestedSource, command, localProvider)) {
|
|
182
|
+
throw buildLocalFingerprintCapabilityError();
|
|
183
|
+
}
|
|
184
|
+
output.error(buildAuthErrorMessage());
|
|
185
|
+
output.cleanup();
|
|
186
|
+
exit(1);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
let provider = source === 'local' ? localProvider : createCloudContextProvider(config, deps);
|
|
190
|
+
return {
|
|
191
|
+
config,
|
|
192
|
+
source,
|
|
193
|
+
provider
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function buildScopeQuery(options = {}, query = {}) {
|
|
197
|
+
let scopedQuery = {
|
|
198
|
+
...query
|
|
199
|
+
};
|
|
200
|
+
if (options.project) {
|
|
201
|
+
scopedQuery.project = options.project;
|
|
202
|
+
}
|
|
203
|
+
if (options.org) {
|
|
204
|
+
scopedQuery.organization = options.org;
|
|
205
|
+
}
|
|
206
|
+
return scopedQuery;
|
|
207
|
+
}
|
|
208
|
+
function getStatusTone(colors, status) {
|
|
209
|
+
if (status === 'changed' || status === 'pending' || status === 'failed') {
|
|
210
|
+
return colors.brand.warning;
|
|
211
|
+
}
|
|
212
|
+
if (status === 'approved' || status === 'completed' || status === 'identical') {
|
|
213
|
+
return colors.brand.success;
|
|
214
|
+
}
|
|
215
|
+
if (status === 'rejected' || status === 'error') {
|
|
216
|
+
return colors.brand.error;
|
|
217
|
+
}
|
|
218
|
+
return colors.brand.info;
|
|
219
|
+
}
|
|
220
|
+
function getComparisonDisplayState(comparison = {}) {
|
|
221
|
+
return comparison.result || comparison.status || 'unknown';
|
|
222
|
+
}
|
|
223
|
+
function isChangedComparison(comparison = {}) {
|
|
224
|
+
return ['changed', 'failed', 'pending'].includes(getComparisonDisplayState(comparison));
|
|
225
|
+
}
|
|
226
|
+
function isNewComparison(comparison = {}) {
|
|
227
|
+
return getComparisonDisplayState(comparison) === 'new';
|
|
228
|
+
}
|
|
229
|
+
function getComparisonName(comparison = {}) {
|
|
230
|
+
return comparison.screenshot_name || comparison.screenshot?.name || comparison.name || comparison.id || 'unknown screenshot';
|
|
231
|
+
}
|
|
232
|
+
function getComparisonDiffPercentage(comparison = {}) {
|
|
233
|
+
return comparison.diff?.percentage ?? comparison.diff_percentage ?? null;
|
|
234
|
+
}
|
|
235
|
+
function getComparisonFingerprint(comparison = {}) {
|
|
236
|
+
return comparison.diff?.fingerprint_hash || comparison.analysis?.fingerprint_hash || null;
|
|
237
|
+
}
|
|
238
|
+
function getComparisonDiffImageUrl(comparison = {}) {
|
|
239
|
+
return comparison.diff?.image_url || comparison.analysis?.diff_image_url || null;
|
|
240
|
+
}
|
|
241
|
+
function getComparisonRegionCount(comparison = {}) {
|
|
242
|
+
let regions = comparison.diff?.regions || comparison.analysis?.diff_regions;
|
|
243
|
+
return Array.isArray(regions) ? regions.length : 0;
|
|
244
|
+
}
|
|
245
|
+
function getComparisonScreenshot(comparison = {}) {
|
|
246
|
+
return comparison.screenshot || {};
|
|
247
|
+
}
|
|
248
|
+
function getComparisonBaseline(comparison = {}) {
|
|
249
|
+
return comparison.baseline || comparison.screenshot?.baseline || {};
|
|
250
|
+
}
|
|
251
|
+
function buildCompactDiff(comparison = {}, includeDiffs = false) {
|
|
252
|
+
let diff = comparison.diff || comparison.analysis || {};
|
|
253
|
+
let compact = {
|
|
254
|
+
percentage: getComparisonDiffPercentage(comparison),
|
|
255
|
+
changed_pixels: diff.changed_pixels ?? comparison.changed_pixels ?? null,
|
|
256
|
+
total_pixels: diff.total_pixels ?? comparison.total_pixels ?? null,
|
|
257
|
+
threshold: diff.threshold ?? comparison.threshold ?? null,
|
|
258
|
+
fingerprint_hash: getComparisonFingerprint(comparison),
|
|
259
|
+
region_count: getComparisonRegionCount(comparison),
|
|
260
|
+
image_url: getComparisonDiffImageUrl(comparison)
|
|
261
|
+
};
|
|
262
|
+
if (includeDiffs) {
|
|
263
|
+
compact.regions = diff.regions || diff.diff_regions || [];
|
|
264
|
+
compact.cluster_metadata = diff.cluster_metadata || null;
|
|
265
|
+
compact.ssim_score = diff.ssim_score ?? null;
|
|
266
|
+
compact.gmsd_score = diff.gmsd_score ?? null;
|
|
267
|
+
compact.diff_lines = diff.diff_lines || [];
|
|
268
|
+
}
|
|
269
|
+
return compact;
|
|
270
|
+
}
|
|
271
|
+
function buildCompactComparison(comparison = {}, {
|
|
272
|
+
includeDiffs = false
|
|
273
|
+
} = {}) {
|
|
274
|
+
let screenshot = getComparisonScreenshot(comparison);
|
|
275
|
+
let baseline = getComparisonBaseline(comparison);
|
|
276
|
+
return {
|
|
277
|
+
id: comparison.id || null,
|
|
278
|
+
name: getComparisonName(comparison),
|
|
279
|
+
result: getComparisonDisplayState(comparison),
|
|
280
|
+
approval_status: comparison.approval_status || null,
|
|
281
|
+
needs_review: Boolean(comparison.needs_review),
|
|
282
|
+
is_flaky: Boolean(comparison.is_flaky),
|
|
283
|
+
screenshot: {
|
|
284
|
+
id: screenshot.id || null,
|
|
285
|
+
url: screenshot.url || screenshot.original_url || null
|
|
286
|
+
},
|
|
287
|
+
baseline: {
|
|
288
|
+
id: baseline.id || null,
|
|
289
|
+
build_id: baseline.build_id || null,
|
|
290
|
+
url: baseline.url || baseline.original_url || null
|
|
291
|
+
},
|
|
292
|
+
diff: buildCompactDiff(comparison, includeDiffs)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function summarizeComparisons(comparisons = []) {
|
|
296
|
+
return {
|
|
297
|
+
total: comparisons.length,
|
|
298
|
+
changed: comparisons.filter(isChangedComparison).length,
|
|
299
|
+
new: comparisons.filter(isNewComparison).length,
|
|
300
|
+
needs_review: comparisons.filter(comparison => comparison.needs_review).length
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function buildAgentNextActions(context = {}, comparisonSummary = {}) {
|
|
304
|
+
if (context.status?.needs_review) {
|
|
305
|
+
return ['Inspect the changed and new comparisons before editing related UI.', 'Use approved baselines as the expected visual behavior.', 'Leave approval decisions to human reviewers.'];
|
|
306
|
+
}
|
|
307
|
+
if (comparisonSummary.total > 0) {
|
|
308
|
+
return ['Use the approved baseline and reviewed screenshots as current visual truth.', 'Prefer targeted edits that preserve identical comparisons.'];
|
|
309
|
+
}
|
|
310
|
+
return ['No comparisons were returned for this build context.', 'Open the build URL for more detail if this is unexpected.'];
|
|
311
|
+
}
|
|
312
|
+
function formatAgentStatus(status = {}) {
|
|
313
|
+
return {
|
|
314
|
+
needs_review: Boolean(status.needs_review),
|
|
315
|
+
reasons: status.reasons || [],
|
|
316
|
+
pending_comparisons: status.pending_comparisons || 0,
|
|
317
|
+
unresolved_comments: status.unresolved_comments || 0
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function formatAgentSummary(context = {}, comparisons = []) {
|
|
321
|
+
return {
|
|
322
|
+
comparisons: context.summary?.comparisons || summarizeComparisons(comparisons),
|
|
323
|
+
review: context.summary?.review || {},
|
|
324
|
+
comments: context.summary?.comments || {
|
|
325
|
+
build: getBuildCommentsCount(context),
|
|
326
|
+
screenshot: getScreenshotCommentsCount(context)
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function buildAgentBuildPayload(context, {
|
|
331
|
+
source = null,
|
|
332
|
+
include = [],
|
|
333
|
+
evidenceLimit = 10
|
|
334
|
+
} = {}) {
|
|
335
|
+
let comparisons = context.comparisons || [];
|
|
336
|
+
let includeSet = new Set(include);
|
|
337
|
+
let includeDiffs = includeSet.has('diffs');
|
|
338
|
+
let changed = comparisons.filter(isChangedComparison);
|
|
339
|
+
let fresh = comparisons.filter(isNewComparison);
|
|
340
|
+
let evidence = [...changed, ...fresh].slice(0, evidenceLimit).map(comparison => buildCompactComparison(comparison, {
|
|
341
|
+
includeDiffs
|
|
342
|
+
}));
|
|
343
|
+
let comparisonSummary = summarizeComparisons(comparisons);
|
|
344
|
+
let payload = {
|
|
345
|
+
resource: 'build_agent_context',
|
|
346
|
+
source: context.source || source || 'cloud',
|
|
347
|
+
scope: context.scope || null,
|
|
348
|
+
project: {
|
|
349
|
+
organization: context.scope?.organization?.slug || null,
|
|
350
|
+
slug: context.scope?.project?.slug || null,
|
|
351
|
+
name: context.scope?.project?.name || null,
|
|
352
|
+
visibility: context.scope?.project?.visibility || null
|
|
353
|
+
},
|
|
354
|
+
build: context.build || null,
|
|
355
|
+
baseline: {
|
|
356
|
+
selected: context.baseline?.selected || null,
|
|
357
|
+
selection_reason: context.baseline?.selection_reason || null
|
|
358
|
+
},
|
|
359
|
+
status: formatAgentStatus(context.status),
|
|
360
|
+
summary: formatAgentSummary(context, comparisons),
|
|
361
|
+
evidence,
|
|
362
|
+
links: context.links || {},
|
|
363
|
+
preview: context.preview || null,
|
|
364
|
+
next_actions: buildAgentNextActions(context, comparisonSummary)
|
|
365
|
+
};
|
|
366
|
+
if (includeSet.has('screenshots')) {
|
|
367
|
+
payload.screenshots = context.screenshots || [];
|
|
368
|
+
}
|
|
369
|
+
if (includeSet.has('comments')) {
|
|
370
|
+
payload.comments = context.comments || {};
|
|
371
|
+
}
|
|
372
|
+
return payload;
|
|
373
|
+
}
|
|
374
|
+
function getBuildCommentsCount(context = {}) {
|
|
375
|
+
if (Array.isArray(context.comments?.build)) {
|
|
376
|
+
return context.comments.build.length;
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(context.review?.comments)) {
|
|
379
|
+
return context.review.comments.length;
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
function getScreenshotCommentsCount(context = {}) {
|
|
384
|
+
if (Number.isInteger(context.comments?.screenshot_count)) {
|
|
385
|
+
return context.comments.screenshot_count;
|
|
386
|
+
}
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
function getReviewAssignmentsCount(context = {}) {
|
|
390
|
+
if (Array.isArray(context.review?.assignments)) {
|
|
391
|
+
return context.review.assignments.length;
|
|
392
|
+
}
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
function formatNeedsReview(status = {}) {
|
|
396
|
+
if (!status || status.needs_review == null) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
let pending = status.pending_comparisons || 0;
|
|
400
|
+
let unresolved = status.unresolved_comments || 0;
|
|
401
|
+
if (!status.needs_review) {
|
|
402
|
+
return 'no';
|
|
403
|
+
}
|
|
404
|
+
return `yes · ${pending} comparisons · ${unresolved} unresolved comments`;
|
|
405
|
+
}
|
|
406
|
+
function formatConfirmedRegionLabels(regions = []) {
|
|
407
|
+
return regions.map(region => region.label).filter(Boolean).slice(0, 3).join(' · ');
|
|
408
|
+
}
|
|
409
|
+
function printComparisonList(output, comparisons = [], {
|
|
410
|
+
limit = 5
|
|
411
|
+
} = {}) {
|
|
412
|
+
let colors = output.getColors();
|
|
413
|
+
for (let comparison of comparisons.slice(0, limit)) {
|
|
414
|
+
let displayState = getComparisonDisplayState(comparison);
|
|
415
|
+
let statusTone = getStatusTone(colors, displayState);
|
|
416
|
+
let screenshotName = getComparisonName(comparison);
|
|
417
|
+
let rawDiffPercentage = getComparisonDiffPercentage(comparison);
|
|
418
|
+
let diffPercentage = rawDiffPercentage == null ? null : `${rawDiffPercentage}%`;
|
|
419
|
+
let fingerprint = getComparisonFingerprint(comparison);
|
|
420
|
+
let details = [];
|
|
421
|
+
if (diffPercentage) {
|
|
422
|
+
details.push(diffPercentage);
|
|
423
|
+
}
|
|
424
|
+
if (comparison.needs_review) {
|
|
425
|
+
details.push('needs review');
|
|
426
|
+
}
|
|
427
|
+
if (fingerprint) {
|
|
428
|
+
details.push(`fp:${fingerprint}`);
|
|
429
|
+
}
|
|
430
|
+
if (comparison.build_branch) {
|
|
431
|
+
details.push(comparison.build_branch);
|
|
432
|
+
}
|
|
433
|
+
output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
|
|
434
|
+
if (details.length > 0) {
|
|
435
|
+
output.print(` ${colors.dim(details.join(' · '))}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function displayBuildContext(output, context) {
|
|
440
|
+
output.header('context', 'build');
|
|
441
|
+
let colors = output.getColors();
|
|
442
|
+
let buildTone = getStatusTone(colors, context.build.status);
|
|
443
|
+
let comparisons = context.comparisons || [];
|
|
444
|
+
let screenshots = context.screenshots || [];
|
|
445
|
+
let reviewSummary = context.summary?.review || {};
|
|
446
|
+
let commentsSummary = context.summary?.comments || {};
|
|
447
|
+
let needsReview = formatNeedsReview(context.status);
|
|
448
|
+
let baseline = context.baseline?.selected || null;
|
|
449
|
+
output.print(` ${colors.bold(context.build.name || context.build.id)} ${buildTone((context.build.status || 'unknown').toUpperCase())}`);
|
|
450
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
451
|
+
output.blank();
|
|
452
|
+
output.labelValue('Comparisons', String(comparisons.length));
|
|
453
|
+
if (screenshots.length > 0) {
|
|
454
|
+
output.labelValue('Screenshots', String(screenshots.length));
|
|
455
|
+
}
|
|
456
|
+
if (baseline) {
|
|
457
|
+
output.labelValue('Baseline', `${baseline.name || baseline.id || 'selected'}${context.baseline.selection_reason ? ` · ${context.baseline.selection_reason}` : ''}`);
|
|
458
|
+
}
|
|
459
|
+
if (needsReview) {
|
|
460
|
+
output.labelValue('Needs Review', needsReview);
|
|
461
|
+
}
|
|
462
|
+
output.labelValue('Review', `${reviewSummary.pending || 0} pending · ${reviewSummary.approved || 0} approved · ${reviewSummary.rejected || 0} rejected`);
|
|
463
|
+
output.labelValue('Memory', `${commentsSummary.build ?? getBuildCommentsCount(context)} build comments · ${commentsSummary.screenshot ?? getScreenshotCommentsCount(context)} screenshot comments · ${getReviewAssignmentsCount(context)} assignments`);
|
|
464
|
+
if (context.preview) {
|
|
465
|
+
let previewUrl = context.preview.preview_url || context.preview.url;
|
|
466
|
+
output.labelValue('Preview', `${context.preview.status || 'unknown'}${previewUrl ? ' · available' : ''}`);
|
|
467
|
+
}
|
|
468
|
+
if (context.links?.build_url) {
|
|
469
|
+
output.labelValue('Build URL', context.links.build_url);
|
|
470
|
+
}
|
|
471
|
+
if (comparisons.length > 0) {
|
|
472
|
+
output.blank();
|
|
473
|
+
output.print(' Comparisons');
|
|
474
|
+
printComparisonList(output, comparisons);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function formatAgentBuildContext(context) {
|
|
478
|
+
let comparisons = context.comparisons || [];
|
|
479
|
+
let changed = comparisons.filter(isChangedComparison);
|
|
480
|
+
let fresh = comparisons.filter(isNewComparison);
|
|
481
|
+
let needsReview = comparisons.filter(comparison => comparison.needs_review);
|
|
482
|
+
let baseline = context.baseline?.selected;
|
|
483
|
+
let lines = [`# Vizzly Visual Context: ${context.build?.name || context.build?.id || 'Build'}`, '', `Project: ${context.scope?.organization?.slug || 'unknown'}/${context.scope?.project?.slug || 'unknown'}`, `Build: ${context.build?.id || 'unknown'} (${context.build?.status || 'unknown'})`];
|
|
484
|
+
if (baseline) {
|
|
485
|
+
lines.push(`Approved baseline: ${baseline.name || baseline.id || 'selected'} (${baseline.approval_status || 'unknown'})`);
|
|
486
|
+
}
|
|
487
|
+
if (context.status) {
|
|
488
|
+
lines.push(`Needs review: ${context.status.needs_review ? 'yes' : 'no'} (${context.status.pending_comparisons || 0} pending comparisons)`);
|
|
489
|
+
}
|
|
490
|
+
if (context.preview?.url || context.preview?.preview_url) {
|
|
491
|
+
lines.push(`Preview: ${context.preview.url || context.preview.preview_url}`);
|
|
492
|
+
}
|
|
493
|
+
if (context.links?.build_url) {
|
|
494
|
+
lines.push(`Build URL: ${context.links.build_url}`);
|
|
495
|
+
}
|
|
496
|
+
if (context.links?.report_url) {
|
|
497
|
+
lines.push(`Report: ${context.links.report_url}`);
|
|
498
|
+
}
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push('## Diff Summary');
|
|
501
|
+
lines.push(`- Total comparisons: ${comparisons.length}`);
|
|
502
|
+
lines.push(`- Changed: ${changed.length}`);
|
|
503
|
+
lines.push(`- New: ${fresh.length}`);
|
|
504
|
+
lines.push(`- Needs review: ${needsReview.length}`);
|
|
505
|
+
if (changed.length > 0 || fresh.length > 0) {
|
|
506
|
+
lines.push('');
|
|
507
|
+
lines.push('## Evidence To Inspect');
|
|
508
|
+
for (let comparison of [...changed, ...fresh].slice(0, 10)) {
|
|
509
|
+
let diffPercentage = getComparisonDiffPercentage(comparison);
|
|
510
|
+
let detail = diffPercentage == null ? '' : ` · ${diffPercentage}% diff`;
|
|
511
|
+
let diffUrl = comparison.diff?.image_url || comparison.analysis?.diff_image_url;
|
|
512
|
+
lines.push(`- ${getComparisonName(comparison)}: ${getComparisonDisplayState(comparison)}${detail}`);
|
|
513
|
+
if (diffUrl) {
|
|
514
|
+
lines.push(` Diff: ${diffUrl}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (comparisons.length > 0 && changed.length === 0 && fresh.length === 0) {
|
|
519
|
+
lines.push('');
|
|
520
|
+
lines.push('## Reviewed Screenshots');
|
|
521
|
+
for (let comparison of comparisons.slice(0, 10)) {
|
|
522
|
+
lines.push(`- ${getComparisonName(comparison)}: ${getComparisonDisplayState(comparison)}`);
|
|
523
|
+
}
|
|
524
|
+
if (comparisons.length > 10) {
|
|
525
|
+
lines.push(`- ...${comparisons.length - 10} more`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
lines.push('');
|
|
529
|
+
lines.push('Use this as reviewed UI context. Treat approved baselines as visual truth, inspect meaningful diffs, and leave approval decisions to humans.');
|
|
530
|
+
return lines.join('\n');
|
|
531
|
+
}
|
|
532
|
+
function countScreenshotCommentEntries(groups = []) {
|
|
533
|
+
return groups.reduce((total, group) => total + (group.comments?.length || 0), 0);
|
|
534
|
+
}
|
|
535
|
+
function displayComparisonContext(output, context) {
|
|
536
|
+
output.header('context', 'comparison');
|
|
537
|
+
let colors = output.getColors();
|
|
538
|
+
let displayState = getComparisonDisplayState(context.comparison);
|
|
539
|
+
let statusTone = getStatusTone(colors, displayState);
|
|
540
|
+
let screenshotName = context.comparison.screenshot?.name || context.comparison.id;
|
|
541
|
+
let analysis = context.comparison.analysis || {};
|
|
542
|
+
let confirmedRegionLabels = formatConfirmedRegionLabels(context.history.confirmed_regions);
|
|
543
|
+
output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
|
|
544
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
545
|
+
output.blank();
|
|
546
|
+
output.labelValue('Eyes', `${analysis.diff_image_url ? 'baseline/current/diff' : 'comparison metadata only'}`);
|
|
547
|
+
output.labelValue('Memory', `${context.history.similar_by_fingerprint.length} similar · ${context.history.recent_by_name.length} recent · ${context.history.confirmed_regions.length} confirmed regions`);
|
|
548
|
+
output.labelValue('Review', `${context.review.build_comments.length} build comments · ${countScreenshotCommentEntries(context.review.screenshot_comments)} screenshot comments`);
|
|
549
|
+
if (analysis.fingerprint_hash) {
|
|
550
|
+
output.labelValue('Fingerprint', analysis.fingerprint_hash);
|
|
551
|
+
}
|
|
552
|
+
if (confirmedRegionLabels) {
|
|
553
|
+
output.labelValue('Known Regions', confirmedRegionLabels);
|
|
554
|
+
}
|
|
555
|
+
if (context.links?.comparison_url) {
|
|
556
|
+
output.labelValue('Comparison URL', context.links.comparison_url);
|
|
557
|
+
}
|
|
558
|
+
if (context.history.similar_by_fingerprint.length > 0) {
|
|
559
|
+
output.blank();
|
|
560
|
+
output.print(' Similar Diffs');
|
|
561
|
+
printComparisonList(output, context.history.similar_by_fingerprint);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function displayScreenshotContext(output, context) {
|
|
565
|
+
output.header('context', 'screenshot');
|
|
566
|
+
let colors = output.getColors();
|
|
567
|
+
let confirmedRegionLabels = formatConfirmedRegionLabels(context.confirmed_regions);
|
|
568
|
+
output.print(` ${colors.bold(context.screenshot.name)}`);
|
|
569
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
570
|
+
output.blank();
|
|
571
|
+
output.labelValue('Memory', `${context.history.recent_comparisons.length} recent comparisons · ${context.confirmed_regions.length} confirmed regions`);
|
|
572
|
+
output.labelValue('Hotspots', `${context.hotspot_analysis.total_builds_analyzed} builds analyzed · ${context.hotspot_analysis.confidence}`);
|
|
573
|
+
if (confirmedRegionLabels) {
|
|
574
|
+
output.labelValue('Known Regions', confirmedRegionLabels);
|
|
575
|
+
}
|
|
576
|
+
if (context.history.recent_comparisons.length > 0) {
|
|
577
|
+
output.blank();
|
|
578
|
+
output.print(' Recent Comparisons');
|
|
579
|
+
printComparisonList(output, context.history.recent_comparisons);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function displayFingerprintContext(output, context) {
|
|
583
|
+
output.header('context', 'similar');
|
|
584
|
+
let colors = output.getColors();
|
|
585
|
+
let fingerprintHash = context.fingerprint?.hash || context.fingerprint_hash || 'unknown';
|
|
586
|
+
let comparisons = context.comparisons || context.matches || [];
|
|
587
|
+
output.print(` ${colors.bold(fingerprintHash)}`);
|
|
588
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
589
|
+
output.blank();
|
|
590
|
+
output.labelValue('Matches', String(comparisons.length));
|
|
591
|
+
if (comparisons.length > 0) {
|
|
592
|
+
output.blank();
|
|
593
|
+
output.print(' Similar Diffs');
|
|
594
|
+
printComparisonList(output, comparisons, {
|
|
595
|
+
limit: 10
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function displayReviewQueueContext(output, context) {
|
|
600
|
+
output.header('context', 'review');
|
|
601
|
+
let colors = output.getColors();
|
|
602
|
+
output.print(` ${colors.bold(`${context.summary.total} pending comparisons`)}`);
|
|
603
|
+
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
604
|
+
output.blank();
|
|
605
|
+
output.labelValue('Queue', `${context.summary.changed} changed · ${context.summary.new} new · ${context.summary.builds} builds`);
|
|
606
|
+
if (context.comparisons.length > 0) {
|
|
607
|
+
output.blank();
|
|
608
|
+
output.print(' Needs Review');
|
|
609
|
+
printComparisonList(output, context.comparisons, {
|
|
610
|
+
limit: 10
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
export async function contextBuildCommand(buildId, options = {}, globalOptions = {}, deps = {}) {
|
|
615
|
+
let {
|
|
616
|
+
output = defaultOutput,
|
|
617
|
+
exit = code => process.exit(code)
|
|
618
|
+
} = deps;
|
|
619
|
+
output.configure({
|
|
620
|
+
json: globalOptions.json,
|
|
621
|
+
verbose: globalOptions.verbose,
|
|
622
|
+
color: !globalOptions.noColor
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
let runtime = await loadContextRuntime('build', buildId, globalOptions, options, {
|
|
626
|
+
...deps,
|
|
627
|
+
output,
|
|
628
|
+
exit
|
|
629
|
+
});
|
|
630
|
+
if (!runtime) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
let resolvedBuildId = resolveBuildContextId(buildId, runtime, deps);
|
|
634
|
+
output.startSpinner('Fetching build context...');
|
|
635
|
+
let context = await runtime.provider.getBuildContext(resolvedBuildId);
|
|
636
|
+
output.stopSpinner();
|
|
637
|
+
if (globalOptions.json && options.agent && !options.full) {
|
|
638
|
+
output.data(buildAgentBuildPayload(context, {
|
|
639
|
+
source: runtime.source,
|
|
640
|
+
include: parseIncludeOption(options.include)
|
|
641
|
+
}));
|
|
642
|
+
output.cleanup();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (globalOptions.json) {
|
|
646
|
+
output.data(context);
|
|
647
|
+
output.cleanup();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (options.agent) {
|
|
651
|
+
output.print(formatAgentBuildContext(context));
|
|
652
|
+
output.cleanup();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
displayBuildContext(output, context);
|
|
656
|
+
output.cleanup();
|
|
657
|
+
} catch (error) {
|
|
658
|
+
output.stopSpinner();
|
|
659
|
+
output.error('Failed to fetch build context', error);
|
|
660
|
+
output.cleanup();
|
|
661
|
+
exit(1);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
export async function contextComparisonCommand(comparisonId, options = {}, globalOptions = {}, deps = {}) {
|
|
665
|
+
let {
|
|
666
|
+
output = defaultOutput,
|
|
667
|
+
exit = code => process.exit(code)
|
|
668
|
+
} = deps;
|
|
669
|
+
output.configure({
|
|
670
|
+
json: globalOptions.json,
|
|
671
|
+
verbose: globalOptions.verbose,
|
|
672
|
+
color: !globalOptions.noColor
|
|
673
|
+
});
|
|
674
|
+
try {
|
|
675
|
+
let runtime = await loadContextRuntime('comparison', comparisonId, globalOptions, options, {
|
|
676
|
+
...deps,
|
|
677
|
+
output,
|
|
678
|
+
exit
|
|
679
|
+
});
|
|
680
|
+
if (!runtime) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
let query = {
|
|
684
|
+
similarLimit: options.similarLimit,
|
|
685
|
+
recentLimit: options.recentLimit,
|
|
686
|
+
windowSize: options.windowSize
|
|
687
|
+
};
|
|
688
|
+
output.startSpinner('Fetching comparison context...');
|
|
689
|
+
let context = await runtime.provider.getComparisonContext(comparisonId, query);
|
|
690
|
+
output.stopSpinner();
|
|
691
|
+
if (globalOptions.json) {
|
|
692
|
+
output.data(context);
|
|
693
|
+
output.cleanup();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
displayComparisonContext(output, context);
|
|
697
|
+
output.cleanup();
|
|
698
|
+
} catch (error) {
|
|
699
|
+
output.stopSpinner();
|
|
700
|
+
output.error('Failed to fetch comparison context', error);
|
|
701
|
+
output.cleanup();
|
|
702
|
+
exit(1);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
export async function contextScreenshotCommand(screenshotName, options = {}, globalOptions = {}, deps = {}) {
|
|
706
|
+
let {
|
|
707
|
+
output = defaultOutput,
|
|
708
|
+
exit = code => process.exit(code)
|
|
709
|
+
} = deps;
|
|
710
|
+
output.configure({
|
|
711
|
+
json: globalOptions.json,
|
|
712
|
+
verbose: globalOptions.verbose,
|
|
713
|
+
color: !globalOptions.noColor
|
|
714
|
+
});
|
|
715
|
+
try {
|
|
716
|
+
let runtime = await loadContextRuntime('screenshot', screenshotName, globalOptions, options, {
|
|
717
|
+
...deps,
|
|
718
|
+
output,
|
|
719
|
+
exit
|
|
720
|
+
});
|
|
721
|
+
if (!runtime) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
let query = buildScopeQuery(options, {
|
|
725
|
+
recentLimit: options.recentLimit,
|
|
726
|
+
windowSize: options.windowSize
|
|
727
|
+
});
|
|
728
|
+
output.startSpinner('Fetching screenshot context...');
|
|
729
|
+
let context = await runtime.provider.getScreenshotContext(screenshotName, query);
|
|
730
|
+
output.stopSpinner();
|
|
731
|
+
if (globalOptions.json) {
|
|
732
|
+
output.data(context);
|
|
733
|
+
output.cleanup();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
displayScreenshotContext(output, context);
|
|
737
|
+
output.cleanup();
|
|
738
|
+
} catch (error) {
|
|
739
|
+
output.stopSpinner();
|
|
740
|
+
output.error('Failed to fetch screenshot context', error);
|
|
741
|
+
output.cleanup();
|
|
742
|
+
exit(1);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
export async function contextSimilarCommand(fingerprintHash, options = {}, globalOptions = {}, deps = {}) {
|
|
746
|
+
let {
|
|
747
|
+
output = defaultOutput,
|
|
748
|
+
exit = code => process.exit(code)
|
|
749
|
+
} = deps;
|
|
750
|
+
output.configure({
|
|
751
|
+
json: globalOptions.json,
|
|
752
|
+
verbose: globalOptions.verbose,
|
|
753
|
+
color: !globalOptions.noColor
|
|
754
|
+
});
|
|
755
|
+
try {
|
|
756
|
+
let runtime = await loadContextRuntime('similar', fingerprintHash, globalOptions, options, {
|
|
757
|
+
...deps,
|
|
758
|
+
output,
|
|
759
|
+
exit
|
|
760
|
+
});
|
|
761
|
+
if (!runtime) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
let query = buildScopeQuery(options, {
|
|
765
|
+
limit: options.limit
|
|
766
|
+
});
|
|
767
|
+
output.startSpinner('Fetching similar visual context...');
|
|
768
|
+
let context = await runtime.provider.getSimilarFingerprintContext(fingerprintHash, query);
|
|
769
|
+
output.stopSpinner();
|
|
770
|
+
if (globalOptions.json) {
|
|
771
|
+
output.data(context);
|
|
772
|
+
output.cleanup();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
displayFingerprintContext(output, context);
|
|
776
|
+
output.cleanup();
|
|
777
|
+
} catch (error) {
|
|
778
|
+
output.stopSpinner();
|
|
779
|
+
output.error('Failed to fetch similar visual context', error);
|
|
780
|
+
output.cleanup();
|
|
781
|
+
exit(1);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
export async function contextReviewQueueCommand(options = {}, globalOptions = {}, deps = {}) {
|
|
785
|
+
let {
|
|
786
|
+
output = defaultOutput,
|
|
787
|
+
exit = code => process.exit(code)
|
|
788
|
+
} = deps;
|
|
789
|
+
output.configure({
|
|
790
|
+
json: globalOptions.json,
|
|
791
|
+
verbose: globalOptions.verbose,
|
|
792
|
+
color: !globalOptions.noColor
|
|
793
|
+
});
|
|
794
|
+
try {
|
|
795
|
+
let runtime = await loadContextRuntime('review-queue', null, globalOptions, options, {
|
|
796
|
+
...deps,
|
|
797
|
+
output,
|
|
798
|
+
exit
|
|
799
|
+
});
|
|
800
|
+
if (!runtime) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
let query = buildScopeQuery(options, {
|
|
804
|
+
limit: options.limit,
|
|
805
|
+
offset: options.offset
|
|
806
|
+
});
|
|
807
|
+
output.startSpinner('Fetching review queue context...');
|
|
808
|
+
let context = await runtime.provider.getReviewQueueContext(query);
|
|
809
|
+
output.stopSpinner();
|
|
810
|
+
if (globalOptions.json) {
|
|
811
|
+
output.data(context);
|
|
812
|
+
output.cleanup();
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
displayReviewQueueContext(output, context);
|
|
816
|
+
output.cleanup();
|
|
817
|
+
} catch (error) {
|
|
818
|
+
output.stopSpinner();
|
|
819
|
+
output.error('Failed to fetch review queue context', error);
|
|
820
|
+
output.cleanup();
|
|
821
|
+
exit(1);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
export function validateContextBuildOptions(_options = {}) {
|
|
825
|
+
let errors = validateSourceOption(_options.source);
|
|
826
|
+
errors.push(...validateIncludeOption(_options.include));
|
|
827
|
+
return errors;
|
|
828
|
+
}
|
|
829
|
+
export function validateContextComparisonOptions(options = {}) {
|
|
830
|
+
let errors = [];
|
|
831
|
+
errors.push(...validateSourceOption(options.source));
|
|
832
|
+
errors.push(...validateLimitRange(options.similarLimit, '--similar-limit', {
|
|
833
|
+
max: 50
|
|
834
|
+
}));
|
|
835
|
+
errors.push(...validateLimitRange(options.recentLimit, '--recent-limit', {
|
|
836
|
+
max: 50
|
|
837
|
+
}));
|
|
838
|
+
errors.push(...validateLimitRange(options.windowSize, '--window-size', {
|
|
839
|
+
max: 50
|
|
840
|
+
}));
|
|
841
|
+
return errors;
|
|
842
|
+
}
|
|
843
|
+
export function validateContextScreenshotOptions(options = {}) {
|
|
844
|
+
let errors = validateScopedProjectOptions(options);
|
|
845
|
+
errors.push(...validateSourceOption(options.source));
|
|
846
|
+
errors.push(...validateLimitRange(options.recentLimit, '--recent-limit', {
|
|
847
|
+
max: 50
|
|
848
|
+
}));
|
|
849
|
+
errors.push(...validateLimitRange(options.windowSize, '--window-size', {
|
|
850
|
+
max: 50
|
|
851
|
+
}));
|
|
852
|
+
return errors;
|
|
853
|
+
}
|
|
854
|
+
export function validateContextSimilarOptions(options = {}) {
|
|
855
|
+
let errors = validateScopedProjectOptions(options);
|
|
856
|
+
errors.push(...validateSourceOption(options.source));
|
|
857
|
+
errors.push(...validateLimitRange(options.limit, '--limit', {
|
|
858
|
+
max: 50
|
|
859
|
+
}));
|
|
860
|
+
return errors;
|
|
861
|
+
}
|
|
862
|
+
export function validateContextReviewQueueOptions(options = {}) {
|
|
863
|
+
let errors = validateScopedProjectOptions(options);
|
|
864
|
+
errors.push(...validateSourceOption(options.source));
|
|
865
|
+
errors.push(...validateLimitRange(options.limit, '--limit', {
|
|
866
|
+
max: 100
|
|
867
|
+
}));
|
|
868
|
+
errors.push(...validateOffset(options.offset));
|
|
869
|
+
return errors;
|
|
870
|
+
}
|