@vizzly-testing/cli 0.33.0 → 0.35.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.
Files changed (37) hide show
  1. package/README.md +53 -3
  2. package/dist/api/core.js +2 -1
  3. package/dist/cli.js +21 -8
  4. package/dist/client/index.js +62 -21
  5. package/dist/commands/api.js +9 -3
  6. package/dist/commands/init.js +214 -2
  7. package/dist/commands/preview.js +13 -11
  8. package/dist/commands/project.js +1 -1
  9. package/dist/commands/run.js +18 -6
  10. package/dist/commands/tdd-daemon.js +8 -3
  11. package/dist/commands/tdd.js +24 -4
  12. package/dist/commands/upload.js +31 -13
  13. package/dist/reporter/reporter-bundle.iife.js +1 -1
  14. package/dist/sdk/index.js +23 -9
  15. package/dist/server/handlers/api-handler.js +3 -2
  16. package/dist/server/handlers/tdd-handler.js +5 -1
  17. package/dist/server/routers/screenshot.js +11 -4
  18. package/dist/services/build-manager.js +6 -1
  19. package/dist/test-runner/core.js +4 -1
  20. package/dist/test-runner/operations.js +2 -1
  21. package/dist/types/client.d.ts +32 -5
  22. package/dist/types/index.d.ts +33 -5
  23. package/dist/uploader/core.js +41 -9
  24. package/dist/uploader/operations.js +19 -17
  25. package/dist/utils/config-loader.js +12 -6
  26. package/dist/utils/context.js +46 -76
  27. package/dist/utils/environment-config.js +20 -0
  28. package/dist/utils/output.js +5 -5
  29. package/dist/utils/screenshot-options.js +60 -10
  30. package/package.json +2 -1
  31. package/skills/vizzly/SKILL.md +99 -0
  32. package/skills/vizzly/agents/openai.yaml +4 -0
  33. package/skills/vizzly/references/cli-context.md +42 -0
  34. package/skills/vizzly/references/dynamic-content.md +44 -0
  35. package/skills/vizzly/references/public-screenshots.md +44 -0
  36. package/skills/vizzly/references/sdks.md +62 -0
  37. package/skills/vizzly/references/setup-ci.md +42 -0
@@ -386,23 +386,25 @@ export async function upload({
386
386
  let buildInfo = buildBuildInfo(uploadOptions, defaultBranch);
387
387
  let build = await createBuild(client, buildInfo);
388
388
  let buildId = build.id;
389
-
390
- // Check existing files
391
- let {
392
- toUpload,
393
- existing,
394
- screenshots
395
- } = await checkExistingFiles({
396
- fileMetadata,
397
- client,
398
- signal,
399
- buildId,
400
- deps: {
401
- checkShas,
402
- createError,
403
- output
404
- }
405
- });
389
+ let toUpload = fileMetadata;
390
+ let existing = [];
391
+ let screenshots = [];
392
+ if (!uploadOptions.uploadAll) {
393
+ let existingResult = await checkExistingFiles({
394
+ fileMetadata,
395
+ client,
396
+ signal,
397
+ buildId,
398
+ deps: {
399
+ checkShas,
400
+ createError,
401
+ output
402
+ }
403
+ });
404
+ toUpload = existingResult.toUpload;
405
+ existing = existingResult.existing;
406
+ screenshots = existingResult.screenshots;
407
+ }
406
408
  onProgress(buildDeduplicationProgress(toUpload.length, existing.length, files.length));
407
409
 
408
410
  // Upload files
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { cosmiconfigSync } from 'cosmiconfig';
3
3
  import { CONFIG_DEFAULTS, deepMerge } from '../config/core.js';
4
4
  import { validateVizzlyConfigWithDefaults } from './config-schema.js';
5
- import { getApiToken, getApiUrl, getBuildName, getParallelId } from './environment-config.js';
5
+ import { getApiToken, getApiUrl, getBuildName, getMinClusterSize, getParallelId, getThreshold } from './environment-config.js';
6
6
  import { getAccessToken } from './global-config.js';
7
7
  import * as output from './output.js';
8
8
  import { getActiveProjectLink } from './project-link-store.js';
@@ -26,10 +26,12 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
26
26
  mergeConfig(config, validatedFileConfig);
27
27
 
28
28
  // 3. Override with environment variables (higher priority than fallbacks)
29
- const envApiKey = getApiToken();
30
- const envApiUrl = getApiUrl();
31
- const envBuildName = getBuildName();
32
- const envParallelId = getParallelId();
29
+ let envApiKey = getApiToken();
30
+ let envApiUrl = getApiUrl();
31
+ let envBuildName = getBuildName();
32
+ let envParallelId = getParallelId();
33
+ let envThreshold = getThreshold();
34
+ let envMinClusterSize = getMinClusterSize();
33
35
  if (envApiKey) {
34
36
  config.apiKey = envApiKey;
35
37
  output.debug('config', 'using token from environment');
@@ -40,6 +42,10 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
40
42
  output.debug('config', 'using build name from environment');
41
43
  }
42
44
  if (envParallelId) config.parallelId = envParallelId;
45
+ if (envThreshold !== undefined) config.comparison.threshold = envThreshold;
46
+ if (envMinClusterSize !== undefined) {
47
+ config.comparison.minClusterSize = envMinClusterSize;
48
+ }
43
49
 
44
50
  // 4. Apply CLI overrides (highest priority)
45
51
  if (cliOverrides.token) {
@@ -109,7 +115,7 @@ function applyCLIOverrides(config, cliOverrides = {}) {
109
115
  }
110
116
 
111
117
  // Comparison overrides
112
- if (cliOverrides.threshold !== undefined) config.comparison.threshold = cliOverrides.threshold;
118
+ if (cliOverrides.threshold !== undefined) config.comparison.threshold = Number(cliOverrides.threshold);
113
119
  if (cliOverrides.minClusterSize !== undefined) {
114
120
  config.comparison.minClusterSize = Number(cliOverrides.minClusterSize);
115
121
  }
@@ -11,6 +11,37 @@
11
11
  import { existsSync, readFileSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
13
13
  import { join } from 'node:path';
14
+ function getGlobalConfigPath() {
15
+ return join(process.env.VIZZLY_HOME || join(homedir(), '.vizzly'), 'config.json');
16
+ }
17
+ function readJsonFile(path) {
18
+ try {
19
+ if (existsSync(path)) {
20
+ return JSON.parse(readFileSync(path, 'utf8'));
21
+ }
22
+ } catch {
23
+ // Optional context should never block the command that is rendering it.
24
+ }
25
+ return null;
26
+ }
27
+ function readGlobalConfig() {
28
+ return readJsonFile(getGlobalConfigPath()) || {};
29
+ }
30
+ function readBaselineInfo(cwd) {
31
+ let path = join(cwd, '.vizzly', 'baselines');
32
+ let metadata = readJsonFile(join(path, 'metadata.json'));
33
+ return {
34
+ count: metadata?.screenshots?.length || 0,
35
+ path: metadata ? path : null
36
+ };
37
+ }
38
+ function readServerInfo(cwd) {
39
+ let serverInfo = readJsonFile(join(cwd, '.vizzly', 'server.json'));
40
+ return {
41
+ running: !!serverInfo,
42
+ port: serverInfo ? serverInfo.port : null
43
+ };
44
+ }
14
45
 
15
46
  /**
16
47
  * Get dynamic context about the current Vizzly state
@@ -22,46 +53,12 @@ export function getContext() {
22
53
  let items = [];
23
54
  try {
24
55
  let cwd = process.cwd();
25
- let globalConfigPath = join(process.env.VIZZLY_HOME || join(homedir(), '.vizzly'), 'config.json');
26
-
27
- // Load global config once
28
- let globalConfig = {};
29
- try {
30
- if (existsSync(globalConfigPath)) {
31
- globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf8'));
32
- }
33
- } catch {
34
- // Ignore
35
- }
56
+ let globalConfig = readGlobalConfig();
36
57
 
37
58
  // Check for vizzly.config.js (project config)
38
59
  let hasProjectConfig = existsSync(join(cwd, 'vizzly.config.js'));
39
-
40
- // Check for .vizzly directory (TDD baselines)
41
- let baselineCount = 0;
42
- try {
43
- let metaPath = join(cwd, '.vizzly', 'baselines', 'metadata.json');
44
- if (existsSync(metaPath)) {
45
- let meta = JSON.parse(readFileSync(metaPath, 'utf8'));
46
- baselineCount = meta.screenshots?.length || 0;
47
- }
48
- } catch {
49
- // Ignore
50
- }
51
-
52
- // Check for TDD server running
53
- let serverRunning = false;
54
- let serverPort = null;
55
- try {
56
- let serverFile = join(cwd, '.vizzly', 'server.json');
57
- if (existsSync(serverFile)) {
58
- let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
59
- serverPort = serverInfo.port;
60
- serverRunning = true;
61
- }
62
- } catch {
63
- // Ignore
64
- }
60
+ let baselineInfo = readBaselineInfo(cwd);
61
+ let serverInfo = readServerInfo(cwd);
65
62
 
66
63
  // Check for OAuth login (from vizzly login)
67
64
  let isLoggedIn = !!globalConfig.auth?.accessToken;
@@ -71,11 +68,11 @@ export function getContext() {
71
68
  let hasEnvToken = !!process.env.VIZZLY_TOKEN;
72
69
 
73
70
  // Build context items - prioritize most useful info
74
- if (serverRunning) {
71
+ if (serverInfo.running) {
75
72
  items.push({
76
73
  type: 'success',
77
74
  label: 'TDD Server',
78
- value: `running on :${serverPort}`
75
+ value: `running on :${serverInfo.port}`
79
76
  });
80
77
  }
81
78
  if (isLoggedIn && userName) {
@@ -97,14 +94,14 @@ export function getContext() {
97
94
  value: 'run vizzly login'
98
95
  });
99
96
  }
100
- if (baselineCount > 0) {
97
+ if (baselineInfo.count > 0) {
101
98
  items.push({
102
99
  type: 'success',
103
100
  label: 'Baselines',
104
- value: `${baselineCount} screenshots`
101
+ value: `${baselineInfo.count} screenshots`
105
102
  });
106
103
  }
107
- if (!hasProjectConfig && !serverRunning && baselineCount === 0) {
104
+ if (!hasProjectConfig && !serverInfo.running && baselineInfo.count === 0) {
108
105
  // Only show "no config" hint if there's nothing else useful
109
106
  items.push({
110
107
  type: 'info',
@@ -126,7 +123,6 @@ export function getContext() {
126
123
  */
127
124
  export function getDetailedContext() {
128
125
  let cwd = process.cwd();
129
- let globalConfigPath = join(process.env.VIZZLY_HOME || join(homedir(), '.vizzly'), 'config.json');
130
126
  let context = {
131
127
  tddServer: {
132
128
  running: false,
@@ -146,42 +142,16 @@ export function getDetailedContext() {
146
142
  }
147
143
  };
148
144
  try {
149
- // Load global config
150
- let globalConfig = {};
151
- try {
152
- if (existsSync(globalConfigPath)) {
153
- globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf8'));
154
- }
155
- } catch {
156
- // Ignore
157
- }
145
+ let globalConfig = readGlobalConfig();
158
146
 
159
147
  // Check for vizzly.config.js
160
148
  context.project.hasConfig = existsSync(join(cwd, 'vizzly.config.js'));
161
-
162
- // Check for baselines
163
- try {
164
- let metaPath = join(cwd, '.vizzly', 'baselines', 'metadata.json');
165
- if (existsSync(metaPath)) {
166
- let meta = JSON.parse(readFileSync(metaPath, 'utf8'));
167
- context.baselines.count = meta.screenshots?.length || 0;
168
- context.baselines.path = join(cwd, '.vizzly', 'baselines');
169
- }
170
- } catch {
171
- // Ignore
172
- }
173
-
174
- // Check for TDD server
175
- try {
176
- let serverFile = join(cwd, '.vizzly', 'server.json');
177
- if (existsSync(serverFile)) {
178
- let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
179
- context.tddServer.running = true;
180
- context.tddServer.port = serverInfo.port;
181
- }
182
- } catch {
183
- // Ignore
184
- }
149
+ let baselineInfo = readBaselineInfo(cwd);
150
+ context.baselines.count = baselineInfo.count;
151
+ context.baselines.path = baselineInfo.path;
152
+ let serverInfo = readServerInfo(cwd);
153
+ context.tddServer.running = serverInfo.running;
154
+ context.tddServer.port = serverInfo.port;
185
155
 
186
156
  // Check auth status
187
157
  context.auth.loggedIn = !!globalConfig.auth?.accessToken;
@@ -85,6 +85,24 @@ export function getBuildName() {
85
85
  return process.env.VIZZLY_BUILD_NAME;
86
86
  }
87
87
 
88
+ /**
89
+ * Get comparison threshold from environment
90
+ * @returns {number|undefined} Threshold value when set
91
+ */
92
+ export function getThreshold() {
93
+ if (process.env.VIZZLY_THRESHOLD === undefined) return undefined;
94
+ return Number(process.env.VIZZLY_THRESHOLD);
95
+ }
96
+
97
+ /**
98
+ * Get minimum changed cluster size from environment
99
+ * @returns {number|undefined} Minimum cluster size when set
100
+ */
101
+ export function getMinClusterSize() {
102
+ if (process.env.VIZZLY_MIN_CLUSTER_SIZE === undefined) return undefined;
103
+ return Number(process.env.VIZZLY_MIN_CLUSTER_SIZE);
104
+ }
105
+
88
106
  /**
89
107
  * Check if TDD mode is enabled
90
108
  * @returns {boolean} Whether TDD mode is enabled
@@ -117,6 +135,8 @@ export function getAllEnvironmentConfig() {
117
135
  buildId: getBuildId(),
118
136
  buildName: getBuildName(),
119
137
  parallelId: getParallelId(),
138
+ threshold: getThreshold(),
139
+ minClusterSize: getMinClusterSize(),
120
140
  tddMode: isTddMode()
121
141
  };
122
142
  }
@@ -282,7 +282,7 @@ export function success(message, data = {}) {
282
282
  });
283
283
  if (config.silent) return;
284
284
  if (config.json) {
285
- console.log(JSON.stringify({
285
+ console.error(JSON.stringify({
286
286
  status: 'success',
287
287
  message,
288
288
  ...data
@@ -301,7 +301,7 @@ export function result(message) {
301
301
  if (config.silent) return;
302
302
  const elapsed = getElapsedTime();
303
303
  if (config.json) {
304
- console.log(JSON.stringify({
304
+ console.error(JSON.stringify({
305
305
  status: 'complete',
306
306
  message,
307
307
  elapsed
@@ -319,7 +319,7 @@ export function info(message, data = {}) {
319
319
  writeLog('info', message, data);
320
320
  if (!shouldLog('info')) return;
321
321
  if (config.json) {
322
- console.log(JSON.stringify({
322
+ console.error(JSON.stringify({
323
323
  status: 'info',
324
324
  message,
325
325
  ...data
@@ -513,7 +513,7 @@ export function stopSpinner() {
513
513
  export function progress(message, current = 0, total = 0) {
514
514
  if (config.silent) return;
515
515
  if (config.json) {
516
- console.log(JSON.stringify({
516
+ console.error(JSON.stringify({
517
517
  status: 'progress',
518
518
  message,
519
519
  progress: {
@@ -941,7 +941,7 @@ export function complete(message, options = {}) {
941
941
  } = options;
942
942
  let detailStr = detail ? ` ${colors.brand.textMuted(detail)}` : '';
943
943
  if (config.json) {
944
- console.log(JSON.stringify({
944
+ console.error(JSON.stringify({
945
945
  status: 'complete',
946
946
  message,
947
947
  detail
@@ -1,21 +1,63 @@
1
+ /**
2
+ * Screenshot option names that are part of the SDK/config contract, not the
3
+ * user's arbitrary metadata bag.
4
+ */
5
+ export let RESERVED_PROPERTY_OPTIONS = Object.freeze({
6
+ threshold: {
7
+ message: 'Move "threshold" out of properties; properties is only for user metadata.'
8
+ },
9
+ minClusterSize: {
10
+ message: 'Move "minClusterSize" out of properties; properties is only for user metadata.'
11
+ },
12
+ fullPage: {
13
+ message: 'Move "fullPage" out of properties; properties is only for user metadata.'
14
+ },
15
+ buildId: {
16
+ message: 'Move "buildId" out of properties; properties is only for user metadata.'
17
+ },
18
+ requestTimeout: {
19
+ message: 'Move "requestTimeout" out of properties; properties is only for user metadata.'
20
+ }
21
+ });
22
+ function createReservedPropertyWarning(option) {
23
+ return {
24
+ code: 'reserved-property-option',
25
+ option,
26
+ message: RESERVED_PROPERTY_OPTIONS[option].message
27
+ };
28
+ }
29
+
1
30
  /**
2
31
  * Normalize screenshot SDK options into the properties payload consumed by the
3
32
  * local TDD server and cloud-compatible comparison path.
4
33
  */
5
- export function createScreenshotProperties(options = {}) {
34
+ export function normalizeScreenshotOptions(options = {}) {
6
35
  let {
7
- buildId: _buildId,
36
+ buildId,
8
37
  properties = {},
9
- requestTimeout: _requestTimeout,
38
+ requestTimeout,
10
39
  threshold,
11
40
  minClusterSize,
12
- fullPage,
13
- ...topLevelProperties
41
+ fullPage
14
42
  } = options;
15
- let normalizedProperties = {
16
- ...topLevelProperties,
17
- ...properties
18
- };
43
+ let warnings = [];
44
+ let normalizedProperties = {};
45
+ for (let [key, value] of Object.entries(properties || {})) {
46
+ if (RESERVED_PROPERTY_OPTIONS[key]) {
47
+ warnings.push(createReservedPropertyWarning(key));
48
+ if (key === 'threshold' && threshold === undefined) threshold = value;
49
+ if (key === 'minClusterSize' && minClusterSize === undefined) {
50
+ minClusterSize = value;
51
+ }
52
+ if (key === 'fullPage' && fullPage === undefined) fullPage = value;
53
+ if (key === 'buildId' && buildId === undefined) buildId = value;
54
+ if (key === 'requestTimeout' && requestTimeout === undefined) {
55
+ requestTimeout = value;
56
+ }
57
+ continue;
58
+ }
59
+ normalizedProperties[key] = value;
60
+ }
19
61
  if (threshold !== undefined) {
20
62
  normalizedProperties.threshold = threshold;
21
63
  }
@@ -25,5 +67,13 @@ export function createScreenshotProperties(options = {}) {
25
67
  if (fullPage !== undefined) {
26
68
  normalizedProperties.fullPage = fullPage;
27
69
  }
28
- return normalizedProperties;
70
+ return {
71
+ buildId,
72
+ requestTimeout,
73
+ properties: normalizedProperties,
74
+ warnings
75
+ };
76
+ }
77
+ export function createScreenshotProperties(options = {}) {
78
+ return normalizeScreenshotOptions(options).properties;
29
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "description": "Visual regression testing from your terminal",
5
5
  "keywords": [
6
6
  "llm-context",
@@ -55,6 +55,7 @@
55
55
  "bin",
56
56
  "dist",
57
57
  "docs/assets",
58
+ "skills",
58
59
  "README.md",
59
60
  "LICENSE"
60
61
  ],
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: vizzly
3
+ description: "Use when a repo has Vizzly configured and you need screenshot memory or visual history: before/after UI changes, visual regression review, screenshot history, approved baselines, diffs, dynamic regions, docs/manual images, public screenshots, or Vizzly builds. Teaches agents to use Vizzly CLI/context for local TDD, cloud builds, review state, comments, hotspots, previews, and SDK capture patterns."
4
+ ---
5
+
6
+ # Vizzly
7
+
8
+ Vizzly is the project's screenshot memory database. It stores approved baselines, current screenshots, diffs, review state, comments, hotspots, previews, and public screenshot URLs.
9
+
10
+ Use Vizzly when you need to understand what the UI looked like before, what changed, what humans already reviewed, or which screenshots already exist. Browser automation is still useful for interacting with a live app; Vizzly is usually the faster first stop for visual history.
11
+
12
+ ## First Instinct
13
+
14
+ Before changing UI, check whether Vizzly already has local screenshot context:
15
+
16
+ ```bash
17
+ vizzly context build current --source local --agent
18
+ ```
19
+
20
+ If a task names a screen, component, or screenshot, inspect that screenshot's history before changing thresholds or re-capturing blindly:
21
+
22
+ ```bash
23
+ vizzly context screenshot "<screenshot-name>" --source local --json
24
+ ```
25
+
26
+ When you make or verify UI changes, run the focused user workflow that owns the surface:
27
+
28
+ ```bash
29
+ vizzly tdd run "<test command>" --no-open
30
+ ```
31
+
32
+ Then inspect what changed:
33
+
34
+ ```bash
35
+ vizzly context build current --source local --agent --json
36
+ ```
37
+
38
+ For specific evidence, drill in:
39
+
40
+ ```bash
41
+ vizzly context comparison <comparison-id> --json
42
+ vizzly context screenshot "<screenshot-name>" --json
43
+ vizzly context review-queue --json
44
+ ```
45
+
46
+ If local context is unavailable and the project uses cloud builds, use the build id from CI or CLI output:
47
+
48
+ ```bash
49
+ vizzly context build <build-id> --agent --json --include diffs,comments
50
+ ```
51
+
52
+ ## What Vizzly Knows
53
+
54
+ - **Approved baselines**: expected UI.
55
+ - **Current screenshots**: what the latest run rendered.
56
+ - **Diffs**: where pixels/layout/content changed.
57
+ - **Review state and comments**: human context attached to builds and screenshots.
58
+ - **Hotspots and confirmed regions**: known dynamic areas.
59
+ - **Preview links**: static or deployed UI context for a build.
60
+ - **Public screenshots**: stable URLs for documentation and manuals.
61
+
62
+ ## Acting On Visual Context
63
+
64
+ - Treat approved baselines as visual truth.
65
+ - Treat diffs as evidence, not as approval instructions.
66
+ - Do not approve or reject visual changes unless the user explicitly asks.
67
+ - Prefer existing E2E or user journeys over narrow screenshot-only specs.
68
+ - For dynamic content, inspect screenshot context before changing thresholds.
69
+ - Prefer deterministic test data, per-screenshot `threshold`, per-screenshot `minClusterSize`, hotspots, or confirmed regions over global tolerance changes.
70
+ - Report visual findings with screenshot names, build/comparison links when available, and the command you ran.
71
+
72
+ ## Capturing Screenshots
73
+
74
+ Use the existing integration when one is present. For direct JavaScript capture:
75
+
76
+ ```javascript
77
+ import { vizzlyScreenshot } from '@vizzly-testing/cli/client';
78
+
79
+ let screenshot = await page.screenshot();
80
+ await vizzlyScreenshot('checkout-form', screenshot, {
81
+ properties: {
82
+ browser: 'chromium',
83
+ viewport: 'desktop',
84
+ state: 'valid-card'
85
+ },
86
+ threshold: 2,
87
+ minClusterSize: 4
88
+ });
89
+ ```
90
+
91
+ Use `properties` to separate variants such as theme, locale, viewport, role, state, component, page, or docs/manual grouping.
92
+
93
+ ## When You Need More Detail
94
+
95
+ - For SDK examples and capture patterns, read `references/sdks.md`.
96
+ - For CLI/context commands and JSON output, read `references/cli-context.md`.
97
+ - For dynamic content, thresholds, and hotspots, read `references/dynamic-content.md`.
98
+ - For public screenshot URLs and docs/manual images, read `references/public-screenshots.md`.
99
+ - For project setup and CI, read `references/setup-ci.md`.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Vizzly"
3
+ short_description: "Use Vizzly visual context while changing UI."
4
+ default_prompt: "Use Vizzly to inspect visual context before and after UI changes."
@@ -0,0 +1,42 @@
1
+ # CLI And Context
2
+
3
+ Use these commands to gather Vizzly evidence before making UI assumptions.
4
+
5
+ ## Local TDD
6
+
7
+ ```bash
8
+ vizzly tdd start
9
+ vizzly tdd run "<test command>" --no-open
10
+ vizzly context build current --source local --agent
11
+ vizzly context build current --source local --agent --json
12
+ ```
13
+
14
+ Local context reads `.vizzly` state and does not require cloud auth.
15
+
16
+ ## Cloud Builds
17
+
18
+ ```bash
19
+ export VIZZLY_TOKEN="project-token"
20
+ vizzly run "<test command>" --wait --json
21
+ vizzly context build <build-id> --agent --json --include diffs,comments
22
+ ```
23
+
24
+ `vizzly run --wait --json` returns a `contextCommand` when a cloud build is created. Prefer that command over constructing URLs by hand.
25
+
26
+ ## Drilldowns
27
+
28
+ ```bash
29
+ vizzly context comparison <comparison-id> --json
30
+ vizzly context screenshot "<screenshot-name>" --json
31
+ vizzly context similar <fingerprint-hash> --json
32
+ vizzly context review-queue --json
33
+ ```
34
+
35
+ Use comparison context for one diff. Use screenshot context for history and recurring dynamic areas. Use review queue context when triaging unresolved visual work.
36
+
37
+ ## Good Agent Behavior
38
+
39
+ - Use `--json` for automation and summaries.
40
+ - Use `--agent` when building prompt context or asking another agent to continue.
41
+ - Keep human-readable command output in final summaries only when it changes the user's next action.
42
+ - Do not approve/reject unless explicitly asked.
@@ -0,0 +1,44 @@
1
+ # Dynamic Content
2
+
3
+ Dynamic content is common: dates, calendars, generated images, avatars, timers, randomized data, responsive text, and API-backed content.
4
+
5
+ ## Order Of Operations
6
+
7
+ 1. Make test data deterministic when the changing content is not what you are testing.
8
+ 2. Inspect screenshot context before changing code:
9
+
10
+ ```bash
11
+ vizzly context screenshot "<screenshot-name>" --json
12
+ ```
13
+
14
+ 3. Use per-screenshot tolerances for local noise:
15
+
16
+ ```javascript
17
+ await vizzlyScreenshot('plant-calendar', screenshot, {
18
+ properties: { surface: 'calendar' },
19
+ threshold: 4,
20
+ minClusterSize: 12
21
+ });
22
+ ```
23
+
24
+ 4. Use hotspots or confirmed regions for recurring valid dynamic regions.
25
+
26
+ ## Avoid
27
+
28
+ - Raising global project thresholds for one dynamic area.
29
+ - Masking a whole page when a small region changes.
30
+ - Treating every daily diff as product breakage.
31
+ - Ignoring structural changes because a region is known to be dynamic.
32
+
33
+ ## How To Explain Findings
34
+
35
+ Say whether the diff looks like:
36
+
37
+ - expected dynamic content
38
+ - deterministic fixture drift
39
+ - real content disappearance
40
+ - layout shift
41
+ - screenshot timing/capture instability
42
+ - baseline mismatch
43
+
44
+ Then name the screenshot and include the relevant context command or link.
@@ -0,0 +1,44 @@
1
+ # Public Screenshots
2
+
3
+ Public screenshots are Vizzly's path for stable image URLs in docs and manuals.
4
+
5
+ Use this when a user asks for hotlinked UI screenshots, generated manual images, docs that stay current with UI, or CDN-like screenshot URLs.
6
+
7
+ ## Mental Model
8
+
9
+ Do not use arbitrary build screenshot URLs as stable docs assets. Use Public Properties.
10
+
11
+ The flow:
12
+
13
+ 1. Capture screenshots with stable metadata.
14
+ 2. Configure project Public Properties in Vizzly.
15
+ 3. Approve/publish the baseline build.
16
+ 4. Use the Public URLs tab to copy the stable URL.
17
+
18
+ Example capture:
19
+
20
+ ```javascript
21
+ await vizzlyScreenshot('watering-widget', screenshot, {
22
+ properties: {
23
+ manual: 'plant-care',
24
+ component: 'watering-widget',
25
+ viewport: 'desktop'
26
+ }
27
+ });
28
+ ```
29
+
30
+ Configure a Public Property like `manual=plant-care`. Vizzly publishes matching approved baseline screenshots and keeps stable URLs for each screenshot/property identity.
31
+
32
+ ## Good Uses
33
+
34
+ - Product manuals
35
+ - User docs
36
+ - Component catalogs
37
+ - In-app UI examples
38
+ - Screenshots that should update when approved UI changes
39
+
40
+ ## Cautions
41
+
42
+ - Public screenshots are public. Do not publish private user data, secrets, or internal-only UI.
43
+ - A new unapproved build does not automatically become the public docs image.
44
+ - Use stable names and properties so URLs do not churn.