@vizzly-testing/cli 0.35.3-beta.1 → 0.35.3-beta.3
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 +8 -3
- package/dist/api/endpoints.js +1 -46
- package/dist/api/index.js +1 -1
- package/dist/cli.js +5 -5
- package/dist/commands/api.js +3 -3
- package/dist/commands/builds.js +12 -15
- package/dist/commands/comparisons.js +7 -9
- package/dist/commands/context.js +10 -20
- package/dist/commands/init.js +62 -13
- package/dist/commands/review.js +122 -35
- package/dist/commands/run.js +1 -3
- package/dist/commands/status.js +35 -79
- package/dist/commands/upload.js +1 -1
- package/dist/context/local-workspace-provider.js +36 -126
- package/dist/reporter/reporter-bundle.iife.js +49 -81
- package/dist/server/handlers/tdd-handler.js +2 -7
- package/dist/server/routers/dashboard.js +5 -1
- package/dist/tdd/runtime-context.js +0 -2
- package/dist/tdd/services/comparison-service.js +36 -76
- package/dist/tdd/tdd-service.js +7 -205
- package/dist/types/index.d.ts +1 -1
- package/dist/uploader/core.js +7 -8
- package/dist/utils/visual-context-normalizers.js +86 -139
- package/package.json +4 -4
- package/skills/vizzly/SKILL.md +42 -52
- package/skills/vizzly/references/cli-context.md +49 -52
- package/skills/vizzly/references/dynamic-content.md +18 -29
- package/skills/vizzly/references/sdks.md +20 -28
- package/skills/vizzly/references/setup-ci.md +18 -42
- package/dist/tdd/core/hotspot-coverage.js +0 -112
- package/dist/tdd/core/region-coverage.js +0 -203
- package/dist/tdd/metadata/hotspot-metadata.js +0 -93
- package/dist/tdd/metadata/region-metadata.js +0 -56
- package/skills/vizzly/agents/openai.yaml +0 -4
package/README.md
CHANGED
|
@@ -35,7 +35,12 @@ For agent-friendly repos, install the Vizzly skill and add a short project
|
|
|
35
35
|
vizzly init --agent-guidance
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
This installs the portable Agent Skills bundle in `.agents/skills/vizzly` and
|
|
39
|
+
adds a small managed block to `AGENTS.md`. Run the same command again after a
|
|
40
|
+
CLI upgrade to refresh both without replacing the rest of `AGENTS.md` or an
|
|
41
|
+
existing Vizzly config.
|
|
42
|
+
|
|
43
|
+
Use `vizzly init --agent-skill` to install or refresh only the local skill, or
|
|
39
44
|
`vizzly init --skip-agent-skill` when you want config without the agent prompt.
|
|
40
45
|
|
|
41
46
|
### Start Local TDD
|
|
@@ -202,8 +207,8 @@ Generate a config file:
|
|
|
202
207
|
vizzly init
|
|
203
208
|
```
|
|
204
209
|
|
|
205
|
-
To teach project agents
|
|
206
|
-
|
|
210
|
+
To teach project agents the Vizzly evidence workflow, add the repo-local skill
|
|
211
|
+
and AGENTS.md guidance:
|
|
207
212
|
|
|
208
213
|
```bash
|
|
209
214
|
vizzly init --agent-guidance
|
package/dist/api/endpoints.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function getBuild(client, buildId, include = null) {
|
|
|
38
38
|
*
|
|
39
39
|
* @param {Object} client - API client
|
|
40
40
|
* @param {string} buildId - Build ID
|
|
41
|
-
* @returns {Promise<Object>}
|
|
41
|
+
* @returns {Promise<Object>} Build status response
|
|
42
42
|
*/
|
|
43
43
|
export async function getBuildStatus(client, buildId) {
|
|
44
44
|
return client.request(`/api/sdk/builds/${buildId}/status`);
|
|
@@ -343,51 +343,6 @@ export async function searchComparisons(client, name, filters = {}) {
|
|
|
343
343
|
return client.request(endpoint);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
// ============================================================================
|
|
347
|
-
// Hotspot Endpoints
|
|
348
|
-
// ============================================================================
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Get hotspot analysis for a single screenshot
|
|
352
|
-
* @param {Object} client - API client
|
|
353
|
-
* @param {string} screenshotName - Screenshot name
|
|
354
|
-
* @param {Object} options - Optional settings
|
|
355
|
-
* @returns {Promise<Object>} Hotspot analysis data
|
|
356
|
-
*/
|
|
357
|
-
export async function getScreenshotHotspots(client, screenshotName, options = {}) {
|
|
358
|
-
let {
|
|
359
|
-
windowSize = 20
|
|
360
|
-
} = options;
|
|
361
|
-
let encodedName = encodeURIComponent(screenshotName);
|
|
362
|
-
let endpoint = buildEndpointWithParams(`/api/sdk/screenshots/${encodedName}/hotspots`, {
|
|
363
|
-
windowSize: String(windowSize)
|
|
364
|
-
});
|
|
365
|
-
return client.request(endpoint);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Batch get hotspot analysis for multiple screenshots
|
|
370
|
-
* @param {Object} client - API client
|
|
371
|
-
* @param {string[]} screenshotNames - Array of screenshot names
|
|
372
|
-
* @param {Object} options - Optional settings
|
|
373
|
-
* @returns {Promise<Object>} Hotspots keyed by screenshot name
|
|
374
|
-
*/
|
|
375
|
-
export async function getBatchHotspots(client, screenshotNames, options = {}) {
|
|
376
|
-
let {
|
|
377
|
-
windowSize = 20
|
|
378
|
-
} = options;
|
|
379
|
-
return client.request('/api/sdk/screenshots/hotspots', {
|
|
380
|
-
method: 'POST',
|
|
381
|
-
headers: {
|
|
382
|
-
'Content-Type': 'application/json'
|
|
383
|
-
},
|
|
384
|
-
body: JSON.stringify({
|
|
385
|
-
screenshot_names: screenshotNames,
|
|
386
|
-
windowSize
|
|
387
|
-
})
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
346
|
// ============================================================================
|
|
392
347
|
// Auth/Token Endpoints
|
|
393
348
|
// ============================================================================
|
package/dist/api/index.js
CHANGED
|
@@ -16,4 +16,4 @@ export { createApiClient, DEFAULT_API_URL } from './client.js';
|
|
|
16
16
|
export { buildApiUrl, buildAuthHeader, buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildRequestHeaders, buildScreenshotCheckObject, buildScreenshotPayload, buildScreenshotResolvePayload, buildShaCheckPayload, buildUserAgent, computeSha256, extractErrorBody, findScreenshotBySha, isAuthError, isRateLimited, parseApiError, partitionByShaExistence, shaExists, shouldRetryWithRefresh } from './core.js';
|
|
17
17
|
|
|
18
18
|
// Endpoint functions
|
|
19
|
-
export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild,
|
|
19
|
+
export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBuild, getBuildContext, getBuildStatus, getBuilds, getComparison, getComparisonContext, getPreviewInfo, getReviewQueueContext, getScreenshotContext, getSimilarFingerprintContext, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';
|
package/dist/cli.js
CHANGED
|
@@ -450,7 +450,7 @@ try {
|
|
|
450
450
|
} catch (error) {
|
|
451
451
|
output.debug(`Plugin loading failed: ${error.message}`);
|
|
452
452
|
}
|
|
453
|
-
program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install the repo-local Vizzly agent skill').option('--agent-guidance', '
|
|
453
|
+
program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install or refresh the repo-local Vizzly agent skill').option('--agent-guidance', 'Install or refresh the repo-local Vizzly skill and AGENTS.md guidance').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
|
|
454
454
|
let globalOptions = getGlobalOptions();
|
|
455
455
|
await init({
|
|
456
456
|
...globalOptions,
|
|
@@ -681,7 +681,7 @@ Examples:
|
|
|
681
681
|
}
|
|
682
682
|
await contextComparisonCommand(comparisonId, options, globalOptions);
|
|
683
683
|
});
|
|
684
|
-
contextCmd.command('screenshot').description('Fetch screenshot context and historical memory').argument('<name>', 'Screenshot name').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--recent-limit <n>', 'Maximum recent comparisons to return (1-50)', Number).
|
|
684
|
+
contextCmd.command('screenshot').description('Fetch screenshot context and historical memory').argument('<name>', 'Screenshot name').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--recent-limit <n>', 'Maximum recent comparisons to return (1-50)', Number).addHelpText('after', `
|
|
685
685
|
Examples:
|
|
686
686
|
$ vizzly context screenshot Dashboard --source cloud
|
|
687
687
|
$ vizzly context screenshot Dashboard --source local
|
|
@@ -738,15 +738,15 @@ Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.
|
|
|
738
738
|
let globalOptions = getGlobalOptions();
|
|
739
739
|
await baselinesCommand(options, globalOptions);
|
|
740
740
|
});
|
|
741
|
-
program.command('api').description('Make raw API requests (for power users)').argument('<endpoint>', 'API endpoint (e.g., /api/sdk/builds)').option('-X, --method <method>', 'HTTP method (GET or POST for
|
|
741
|
+
program.command('api').description('Make raw API requests (for power users)').argument('<endpoint>', 'API endpoint (e.g., /api/sdk/builds)').option('-X, --method <method>', 'HTTP method (GET or POST for build comments)', 'GET').option('-d, --data <json>', 'Request body (JSON)').option('-H, --header <header>', 'Add header (key:value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).option('-q, --query <param>', 'Add query param (key=value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).addHelpText('after', `
|
|
742
742
|
Examples:
|
|
743
743
|
$ vizzly api /api/sdk/builds # List builds
|
|
744
744
|
$ vizzly api /api/sdk/builds -q limit=5 # With query params
|
|
745
745
|
$ vizzly api /api/sdk/builds/abc123 # Get specific build
|
|
746
|
-
$ vizzly api /api/sdk/
|
|
746
|
+
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Looks good"}'
|
|
747
747
|
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Nice!"}'
|
|
748
748
|
|
|
749
|
-
Note: POST is restricted to approve
|
|
749
|
+
Note: POST is restricted to build comment endpoints. Use dedicated approve/reject commands for review decisions.
|
|
750
750
|
Most operations have dedicated commands (builds, comparisons, approve, etc.).
|
|
751
751
|
`).action(async (endpoint, options) => {
|
|
752
752
|
let globalOptions = getGlobalOptions();
|
package/dist/commands/api.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { createApiClient as defaultCreateApiClient } from '../api/index.js';
|
|
6
6
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
7
7
|
import * as defaultOutput from '../utils/output.js';
|
|
8
|
-
let ALLOWED_POST_ENDPOINTS = [/^\/api\/sdk\/
|
|
8
|
+
let ALLOWED_POST_ENDPOINTS = [/^\/api\/sdk\/builds\/[^/]+\/comments$/];
|
|
9
9
|
function createApiCommandDeps(deps = {}) {
|
|
10
10
|
return {
|
|
11
11
|
loadConfig: deps.loadConfig || defaultLoadConfig,
|
|
@@ -70,11 +70,11 @@ export function validateApiRequest({
|
|
|
70
70
|
}) {
|
|
71
71
|
let errors = [];
|
|
72
72
|
if (method !== 'GET' && method !== 'POST') {
|
|
73
|
-
errors.push(`Method ${method} not allowed. Use GET for queries or POST for
|
|
73
|
+
errors.push(`Method ${method} not allowed. Use GET for queries or POST for build comments.`);
|
|
74
74
|
return errors;
|
|
75
75
|
}
|
|
76
76
|
if (method === 'POST' && !isAllowedPostEndpoint(endpoint)) {
|
|
77
|
-
errors.push(`POST not allowed for ${endpoint}. Only
|
|
77
|
+
errors.push(`POST not allowed for ${endpoint}. Only build comment endpoints support POST.`);
|
|
78
78
|
}
|
|
79
79
|
if (hasData && method !== 'POST') {
|
|
80
80
|
errors.push('Request data requires --method POST.');
|
package/dist/commands/builds.js
CHANGED
|
@@ -112,13 +112,11 @@ export async function buildsCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
112
112
|
for (let build of builds) {
|
|
113
113
|
let statusColor = getStatusColor(colors, build.status);
|
|
114
114
|
let statusBadge = statusColor(build.status.toUpperCase());
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (build.approval_status && build.status === 'completed') {
|
|
119
|
-
approvalBadge = ` ${getApprovalBadge(colors, build.approval_status)}`;
|
|
115
|
+
let reviewBadge = '';
|
|
116
|
+
if (build.visual_review?.state && build.status === 'completed') {
|
|
117
|
+
reviewBadge = ` ${getReviewBadge(colors, build.visual_review.state)}`;
|
|
120
118
|
}
|
|
121
|
-
output.print(` ${colors.bold(build.name || build.id)} ${statusBadge}${
|
|
119
|
+
output.print(` ${colors.bold(build.name || build.id)} ${statusBadge}${reviewBadge}`);
|
|
122
120
|
let details = [];
|
|
123
121
|
if (build.branch) details.push(build.branch);
|
|
124
122
|
if (build.commit_sha) details.push(build.commit_sha.substring(0, 7));
|
|
@@ -174,7 +172,7 @@ function formatBuildForJson(build, includeComparisons = false) {
|
|
|
174
172
|
changed: build.changed_comparisons || 0,
|
|
175
173
|
identical: build.identical_comparisons || 0
|
|
176
174
|
},
|
|
177
|
-
|
|
175
|
+
visual_review: build.visual_review || null,
|
|
178
176
|
createdAt: build.created_at,
|
|
179
177
|
completedAt: build.completed_at
|
|
180
178
|
};
|
|
@@ -192,7 +190,7 @@ function formatBuildForJson(build, includeComparisons = false) {
|
|
|
192
190
|
name: c.name || c.current_name,
|
|
193
191
|
status: c.status,
|
|
194
192
|
diffPercentage: c.diff_percentage,
|
|
195
|
-
|
|
193
|
+
visual_review: c.visual_review || null,
|
|
196
194
|
urls: {
|
|
197
195
|
baseline: c.baseline_screenshot?.original_url || c.baseline_original_url || c.baseline_screenshot_url || null,
|
|
198
196
|
current: c.current_screenshot?.original_url || c.current_original_url || c.current_screenshot_url || null,
|
|
@@ -238,8 +236,8 @@ function displayBuild(output, build, verbose) {
|
|
|
238
236
|
if (stats.length > 0) {
|
|
239
237
|
output.labelValue('Comparisons', stats.join(colors.dim(' · ')));
|
|
240
238
|
}
|
|
241
|
-
if (build.
|
|
242
|
-
output.labelValue('
|
|
239
|
+
if (build.visual_review?.state) {
|
|
240
|
+
output.labelValue('Review', build.visual_review.state);
|
|
243
241
|
}
|
|
244
242
|
output.blank();
|
|
245
243
|
if (build.created_at) {
|
|
@@ -290,19 +288,18 @@ function getStatusColor(colors, status) {
|
|
|
290
288
|
}
|
|
291
289
|
|
|
292
290
|
/**
|
|
293
|
-
* Get colored
|
|
291
|
+
* Get colored review badge
|
|
294
292
|
*/
|
|
295
|
-
function
|
|
296
|
-
switch (
|
|
293
|
+
function getReviewBadge(colors, reviewState) {
|
|
294
|
+
switch (reviewState) {
|
|
297
295
|
case 'approved':
|
|
298
|
-
case 'auto_approved':
|
|
299
296
|
return colors.brand.success('APPROVED');
|
|
300
297
|
case 'rejected':
|
|
301
298
|
return colors.brand.error('REJECTED');
|
|
302
299
|
case 'pending':
|
|
303
300
|
return colors.brand.warning('PENDING');
|
|
304
301
|
default:
|
|
305
|
-
return colors.dim(
|
|
302
|
+
return colors.dim(reviewState?.toUpperCase() || '');
|
|
306
303
|
}
|
|
307
304
|
}
|
|
308
305
|
|
|
@@ -6,7 +6,7 @@ import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild,
|
|
|
6
6
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
7
7
|
import * as defaultOutput from '../utils/output.js';
|
|
8
8
|
import { createWildcardMatcher } from '../utils/patterns.js';
|
|
9
|
-
import { getComparisonBrowser, getComparisonName, getComparisonResult, getComparisonViewport
|
|
9
|
+
import { getComparisonBrowser, getComparisonName, getComparisonResult, getComparisonViewport } from '../utils/visual-context-normalizers.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Comparisons command - query comparisons with various filters
|
|
@@ -165,19 +165,17 @@ function formatComparisonForJson(comparison) {
|
|
|
165
165
|
let ssimScore = comparison.ssim_score ?? diffImage.ssim_score ?? null;
|
|
166
166
|
let gmsdScore = comparison.gmsd_score ?? diffImage.gmsd_score ?? null;
|
|
167
167
|
let fingerprintHash = comparison.fingerprint_hash || diffImage.fingerprint_hash || null;
|
|
168
|
-
let
|
|
168
|
+
let analysisDetails = comparison.analysis_details || diffImage.analysis_details || null;
|
|
169
169
|
let diffRegions = comparison.diff_regions ?? diffImage.diff_regions ?? null;
|
|
170
|
-
let regionCount = comparison.region_count ?? diffImage.region_count ??
|
|
171
|
-
let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash ||
|
|
170
|
+
let regionCount = comparison.region_count ?? diffImage.region_count ?? analysisDetails?.clusters?.count ?? (Array.isArray(diffRegions) ? diffRegions.length : null);
|
|
171
|
+
let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash || analysisDetails || regionCount != null;
|
|
172
172
|
return {
|
|
173
173
|
id: comparison.id,
|
|
174
174
|
name: getComparisonName(comparison),
|
|
175
175
|
status: comparison.status,
|
|
176
176
|
result: getComparisonResult(comparison),
|
|
177
177
|
diffPercentage: comparison.diff_percentage ?? null,
|
|
178
|
-
|
|
179
|
-
reviewState: getVisualReviewState(comparison),
|
|
180
|
-
visualReview: comparison.visual_review || null,
|
|
178
|
+
visual_review: comparison.visual_review || null,
|
|
181
179
|
viewport: getComparisonViewport(comparison),
|
|
182
180
|
browser: getComparisonBrowser(comparison),
|
|
183
181
|
urls: {
|
|
@@ -192,7 +190,7 @@ function formatComparisonForJson(comparison) {
|
|
|
192
190
|
clusterMetadata,
|
|
193
191
|
fingerprintHash,
|
|
194
192
|
regionCount,
|
|
195
|
-
|
|
193
|
+
details: analysisDetails,
|
|
196
194
|
diffRegions,
|
|
197
195
|
diffLines: comparison.diff_lines ?? diffImage.diff_lines ?? null,
|
|
198
196
|
fingerprintData: comparison.fingerprint_data ?? diffImage.fingerprint_data ?? null
|
|
@@ -214,7 +212,7 @@ function displayComparison(output, comparison, verbose) {
|
|
|
214
212
|
Name: formatted.name,
|
|
215
213
|
Status: formatted.result?.toUpperCase(),
|
|
216
214
|
'Diff %': comparison.diff_percentage != null ? `${(comparison.diff_percentage * 100).toFixed(2)}%` : 'N/A',
|
|
217
|
-
Review: formatted.
|
|
215
|
+
Review: formatted.visual_review?.state || 'unknown'
|
|
218
216
|
});
|
|
219
217
|
output.blank();
|
|
220
218
|
if (formatted.viewport) {
|
package/dist/commands/context.js
CHANGED
|
@@ -393,7 +393,7 @@ function buildCompactBuildCommands(context = {}, include = [], cursor = null) {
|
|
|
393
393
|
command: appendContextSource(`vizzly --json context screenshot ${quoteCommandArgument(screenshotName)}`, context)
|
|
394
394
|
});
|
|
395
395
|
}
|
|
396
|
-
if (buildTarget && evidence.length > 0) {
|
|
396
|
+
if (buildTarget && evidence.length > 0 && !include.includes('diffs')) {
|
|
397
397
|
let cursorFlag = cursor ? ` --cursor ${quoteCommandArgument(cursor)}` : '';
|
|
398
398
|
commands.push({
|
|
399
399
|
label: 'Load raw diff diagnostics',
|
|
@@ -427,6 +427,12 @@ function buildCompactComparisonCommands(context = {}, include = []) {
|
|
|
427
427
|
let comparisonId = context.comparison?.id;
|
|
428
428
|
let streams = [['similar_by_fingerprint', 'similar history'], ['recent_by_name', 'recent history']];
|
|
429
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
|
+
}
|
|
430
436
|
for (let [stream, label] of streams) {
|
|
431
437
|
let page = context.history?.[stream]?.page;
|
|
432
438
|
if (!comparisonId || !page?.has_more || !page.next_cursor) {
|
|
@@ -565,9 +571,6 @@ function formatNeedsReview(status = {}) {
|
|
|
565
571
|
}
|
|
566
572
|
return details.length > 0 ? `yes · ${details.join(' · ')}` : 'yes';
|
|
567
573
|
}
|
|
568
|
-
function formatConfirmedRegionLabels(regions = []) {
|
|
569
|
-
return regions.map(region => region.label).filter(Boolean).slice(0, 3).join(' · ');
|
|
570
|
-
}
|
|
571
574
|
function printComparisonList(output, comparisons = [], {
|
|
572
575
|
limit = 5
|
|
573
576
|
} = {}) {
|
|
@@ -735,19 +738,15 @@ function displayComparisonContext(output, context) {
|
|
|
735
738
|
let statusTone = getStatusTone(colors, displayState);
|
|
736
739
|
let screenshotName = context.comparison.screenshot?.name || context.comparison.id;
|
|
737
740
|
let analysis = context.comparison.analysis || {};
|
|
738
|
-
let confirmedRegionLabels = formatConfirmedRegionLabels(context.history.confirmed_regions);
|
|
739
741
|
output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
|
|
740
742
|
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
741
743
|
output.blank();
|
|
742
744
|
output.labelValue('Eyes', `${analysis.diff_image_url ? 'baseline/current/diff' : 'comparison metadata only'}`);
|
|
743
|
-
output.labelValue('Memory', `${context.history.similar_by_fingerprint.length} similar · ${context.history.recent_by_name.length} recent
|
|
745
|
+
output.labelValue('Memory', `${context.history.similar_by_fingerprint.length} similar · ${context.history.recent_by_name.length} recent`);
|
|
744
746
|
output.labelValue('Review', `${context.review.build_comments.length} build comments · ${countScreenshotCommentEntries(context.review.screenshot_comments)} screenshot comments`);
|
|
745
747
|
if (analysis.fingerprint_hash) {
|
|
746
748
|
output.labelValue('Fingerprint', analysis.fingerprint_hash);
|
|
747
749
|
}
|
|
748
|
-
if (confirmedRegionLabels) {
|
|
749
|
-
output.labelValue('Known Regions', confirmedRegionLabels);
|
|
750
|
-
}
|
|
751
750
|
if (context.links?.comparison_url) {
|
|
752
751
|
output.labelValue('Comparison URL', context.links.comparison_url);
|
|
753
752
|
}
|
|
@@ -760,15 +759,10 @@ function displayComparisonContext(output, context) {
|
|
|
760
759
|
function displayScreenshotContext(output, context) {
|
|
761
760
|
output.header('context', 'screenshot');
|
|
762
761
|
let colors = output.getColors();
|
|
763
|
-
let confirmedRegionLabels = formatConfirmedRegionLabels(context.confirmed_regions);
|
|
764
762
|
output.print(` ${colors.bold(context.screenshot.name)}`);
|
|
765
763
|
output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
|
|
766
764
|
output.blank();
|
|
767
|
-
output.labelValue('Memory', `${context.history.recent_comparisons.length} recent comparisons
|
|
768
|
-
output.labelValue('Hotspots', `${context.hotspot_analysis.total_builds_analyzed} builds analyzed · ${context.hotspot_analysis.confidence}`);
|
|
769
|
-
if (confirmedRegionLabels) {
|
|
770
|
-
output.labelValue('Known Regions', confirmedRegionLabels);
|
|
771
|
-
}
|
|
765
|
+
output.labelValue('Memory', `${context.history.recent_comparisons.length} recent comparisons`);
|
|
772
766
|
if (context.history.recent_comparisons.length > 0) {
|
|
773
767
|
output.blank();
|
|
774
768
|
output.print(' Recent Comparisons');
|
|
@@ -934,8 +928,7 @@ export async function contextScreenshotCommand(screenshotName, options = {}, glo
|
|
|
934
928
|
return;
|
|
935
929
|
}
|
|
936
930
|
let query = buildScopeQuery(options, {
|
|
937
|
-
recentLimit: options.recentLimit
|
|
938
|
-
windowSize: options.windowSize
|
|
931
|
+
recentLimit: options.recentLimit
|
|
939
932
|
});
|
|
940
933
|
output.startSpinner('Fetching screenshot context...');
|
|
941
934
|
let context = await runtime.provider.getScreenshotContext(screenshotName, query);
|
|
@@ -1052,9 +1045,6 @@ export function validateContextScreenshotOptions(options = {}) {
|
|
|
1052
1045
|
errors.push(...validateLimitRange(options.recentLimit, '--recent-limit', {
|
|
1053
1046
|
max: 50
|
|
1054
1047
|
}));
|
|
1055
|
-
errors.push(...validateLimitRange(options.windowSize, '--window-size', {
|
|
1056
|
-
max: 50
|
|
1057
|
-
}));
|
|
1058
1048
|
return errors;
|
|
1059
1049
|
}
|
|
1060
1050
|
export function validateContextSimilarOptions(options = {}) {
|
package/dist/commands/init.js
CHANGED
|
@@ -23,6 +23,7 @@ function createInitDeps(deps = {}) {
|
|
|
23
23
|
output: deps.output || output,
|
|
24
24
|
promptAgentSkill: deps.promptAgentSkill || promptAgentSkill,
|
|
25
25
|
readFile: deps.readFile || fs.readFile,
|
|
26
|
+
remove: deps.remove || fs.rm,
|
|
26
27
|
skillSourcePath: deps.skillSourcePath || getPackagedAgentSkillPath(),
|
|
27
28
|
writeFile: deps.writeFile || fs.writeFile
|
|
28
29
|
};
|
|
@@ -213,16 +214,20 @@ async function shouldInstallAgentSkill(options, deps) {
|
|
|
213
214
|
let agentGuidanceStart = '<!-- vizzly-agent-guidance -->';
|
|
214
215
|
let agentGuidanceEnd = '<!-- /vizzly-agent-guidance -->';
|
|
215
216
|
function createAgentGuidanceContent() {
|
|
217
|
+
let skillPath = '.agents/skills/vizzly/SKILL.md';
|
|
216
218
|
return `${agentGuidanceStart}
|
|
217
219
|
## Visual Testing With Vizzly
|
|
218
220
|
|
|
219
|
-
|
|
221
|
+
For user-facing changes, use the repo-local Vizzly skill at
|
|
222
|
+
\`${skillPath}\`.
|
|
220
223
|
|
|
221
|
-
-
|
|
222
|
-
-
|
|
223
|
-
-
|
|
224
|
+
- Inspect existing visual evidence before and after the change.
|
|
225
|
+
- Use the repository's established Vizzly command and owning user workflow.
|
|
226
|
+
- Read bounded JSON with \`--agent --json\`, then inspect baseline, current, and
|
|
227
|
+
diff images together.
|
|
224
228
|
|
|
225
|
-
|
|
229
|
+
Treat Vizzly diffs as review evidence. Do not approve, reject, or replace
|
|
230
|
+
evidence unless the task explicitly asks for that mutation.
|
|
226
231
|
${agentGuidanceEnd}
|
|
227
232
|
`;
|
|
228
233
|
}
|
|
@@ -248,9 +253,28 @@ async function upsertProjectAgentGuidance({
|
|
|
248
253
|
};
|
|
249
254
|
}
|
|
250
255
|
let existingContent = await readFile(agentsPath, 'utf8');
|
|
251
|
-
|
|
256
|
+
let startIndex = existingContent.indexOf(agentGuidanceStart);
|
|
257
|
+
let endIndex = existingContent.indexOf(agentGuidanceEnd);
|
|
258
|
+
let startCount = existingContent.split(agentGuidanceStart).length - 1;
|
|
259
|
+
let endCount = existingContent.split(agentGuidanceEnd).length - 1;
|
|
260
|
+
if (startCount !== endCount || startCount > 1 || startCount === 1 && endIndex < startIndex) {
|
|
252
261
|
return {
|
|
253
|
-
status: '
|
|
262
|
+
status: 'malformed',
|
|
263
|
+
agentsPath
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (startIndex >= 0 && endIndex >= startIndex) {
|
|
267
|
+
let blockEnd = endIndex + agentGuidanceEnd.length;
|
|
268
|
+
let refreshedContent = `${existingContent.slice(0, startIndex)}${guidance.trimEnd()}${existingContent.slice(blockEnd)}`;
|
|
269
|
+
if (refreshedContent === existingContent) {
|
|
270
|
+
return {
|
|
271
|
+
status: 'exists',
|
|
272
|
+
agentsPath
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
await writeFile(agentsPath, refreshedContent, 'utf8');
|
|
276
|
+
return {
|
|
277
|
+
status: 'refreshed',
|
|
254
278
|
agentsPath
|
|
255
279
|
};
|
|
256
280
|
}
|
|
@@ -263,9 +287,11 @@ async function upsertProjectAgentGuidance({
|
|
|
263
287
|
export async function installProjectAgentSkill({
|
|
264
288
|
cwd,
|
|
265
289
|
sourcePath,
|
|
290
|
+
refresh = false,
|
|
266
291
|
access = fs.access,
|
|
267
292
|
copy = fs.cp,
|
|
268
|
-
mkdir = fs.mkdir
|
|
293
|
+
mkdir = fs.mkdir,
|
|
294
|
+
remove = fs.rm
|
|
269
295
|
}) {
|
|
270
296
|
let targetPath = getProjectAgentSkillPath(cwd);
|
|
271
297
|
if (!(await fileExists(path.join(sourcePath, 'SKILL.md'), access))) {
|
|
@@ -275,7 +301,8 @@ export async function installProjectAgentSkill({
|
|
|
275
301
|
targetPath
|
|
276
302
|
};
|
|
277
303
|
}
|
|
278
|
-
|
|
304
|
+
let targetExists = await fileExists(targetPath, access);
|
|
305
|
+
if (targetExists && !refresh) {
|
|
279
306
|
return {
|
|
280
307
|
status: 'exists',
|
|
281
308
|
sourcePath,
|
|
@@ -287,11 +314,16 @@ export async function installProjectAgentSkill({
|
|
|
287
314
|
});
|
|
288
315
|
await copy(sourcePath, targetPath, {
|
|
289
316
|
recursive: true,
|
|
290
|
-
errorOnExist:
|
|
291
|
-
force:
|
|
317
|
+
errorOnExist: !targetExists,
|
|
318
|
+
force: targetExists
|
|
292
319
|
});
|
|
320
|
+
if (targetExists) {
|
|
321
|
+
await remove(path.join(targetPath, 'agents', 'openai.yaml'), {
|
|
322
|
+
force: true
|
|
323
|
+
});
|
|
324
|
+
}
|
|
293
325
|
return {
|
|
294
|
-
status: 'installed',
|
|
326
|
+
status: targetExists ? 'refreshed' : 'installed',
|
|
295
327
|
sourcePath,
|
|
296
328
|
targetPath
|
|
297
329
|
};
|
|
@@ -305,6 +337,11 @@ function writeAgentSkillOutput(output, result) {
|
|
|
305
337
|
output.hint(`Installed at ${result.targetPath}`);
|
|
306
338
|
return;
|
|
307
339
|
}
|
|
340
|
+
if (result.status === 'refreshed') {
|
|
341
|
+
output.complete('Refreshed Vizzly agent skill');
|
|
342
|
+
output.hint(`Updated ${result.targetPath}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
308
345
|
if (result.status === 'exists') {
|
|
309
346
|
output.hint('Vizzly agent skill already exists in this repo');
|
|
310
347
|
return;
|
|
@@ -327,8 +364,18 @@ function writeAgentGuidanceOutput(output, result) {
|
|
|
327
364
|
output.hint(`Updated ${result.agentsPath}`);
|
|
328
365
|
return;
|
|
329
366
|
}
|
|
367
|
+
if (result.status === 'refreshed') {
|
|
368
|
+
output.complete('Refreshed Vizzly guidance in AGENTS.md');
|
|
369
|
+
output.hint(`Updated ${result.agentsPath}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
330
372
|
if (result.status === 'exists') {
|
|
331
373
|
output.hint('Vizzly guidance already exists in AGENTS.md');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (result.status === 'malformed') {
|
|
377
|
+
output.warn('Vizzly guidance markers in AGENTS.md are incomplete');
|
|
378
|
+
output.hint(`Repair the managed block in ${result.agentsPath}`);
|
|
332
379
|
}
|
|
333
380
|
}
|
|
334
381
|
function withAgentSetupResults(payload, {
|
|
@@ -381,7 +428,9 @@ export async function init(options = {}, deps = {}) {
|
|
|
381
428
|
sourcePath: resolvedDeps.skillSourcePath,
|
|
382
429
|
access: resolvedDeps.access,
|
|
383
430
|
copy: resolvedDeps.copy,
|
|
384
|
-
mkdir: resolvedDeps.mkdir
|
|
431
|
+
mkdir: resolvedDeps.mkdir,
|
|
432
|
+
refresh: options.agentGuidance || options.agentSkill,
|
|
433
|
+
remove: resolvedDeps.remove
|
|
385
434
|
});
|
|
386
435
|
}
|
|
387
436
|
if (options.agentGuidance) {
|