@vizzly-testing/cli 0.32.0 → 0.34.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 +81 -50
- package/dist/cli.js +10 -10
- package/dist/client/index.js +2 -1
- package/dist/commands/doctor.js +4 -4
- package/dist/commands/init.js +215 -3
- package/dist/commands/preview.js +3 -3
- package/dist/commands/run.js +1 -1
- package/dist/commands/tdd-daemon.js +1 -1
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +118 -105
- package/dist/reporter-ssr/ssr-entry.js +88 -88
- package/dist/services/static-report-generator.js +2 -2
- package/dist/tdd/tdd-service.js +14 -14
- package/dist/utils/colors.js +28 -45
- package/dist/utils/global-config.js +1 -1
- package/dist/utils/output.js +16 -16
- package/dist/utils/screenshot-options.js +1 -0
- package/docs/assets/vizzly-mascot-inspector.png +0 -0
- package/package.json +11 -8
- package/skills/vizzly/SKILL.md +99 -0
- package/skills/vizzly/agents/openai.yaml +4 -0
- package/skills/vizzly/references/cli-context.md +42 -0
- package/skills/vizzly/references/dynamic-content.md +44 -0
- package/skills/vizzly/references/public-screenshots.md +44 -0
- package/skills/vizzly/references/sdks.md +62 -0
- package/skills/vizzly/references/setup-ci.md +42 -0
package/README.md
CHANGED
|
@@ -1,70 +1,80 @@
|
|
|
1
1
|
# Vizzly CLI
|
|
2
2
|
|
|
3
|
-
> Visual
|
|
3
|
+
> Visual regression testing from your terminal
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@vizzly-testing/cli)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="./docs/assets/vizzly-mascot-inspector.png" alt="Vizzly bear inspecting a screenshot" width="220" />
|
|
10
|
+
</p>
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
Vizzly is a visual testing and regression platform for teams that ship UI. It
|
|
13
|
+
captures screenshots from your real tests, compares them to approved baselines,
|
|
14
|
+
and gives you useful review data: meaningful diffs, comments, approvals, build
|
|
15
|
+
status, and preview links.
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
**Meaningful diff metadata.** Vizzly stores rich diff evidence: changed regions, cluster metadata, fingerprints, hotspots, confirmed regions, and image URLs. Agents can inspect what changed instead of guessing from a pass/fail label.
|
|
17
|
-
|
|
18
|
-
**Any screenshot source.** Playwright, Cypress, Puppeteer, Selenium, native mobile apps, or even design mockups. If you can capture it, Vizzly can compare it.
|
|
19
|
-
|
|
20
|
-
**Approved baselines as truth.** Cloud context carries human review state. Local context carries the downloaded or generated baseline metadata. That is the bridge between TDD locally and collaborative review in Vizzly.
|
|
17
|
+
This package is the CLI and local SDK surface. Use it to run local visual TDD,
|
|
18
|
+
upload screenshots from CI, generate static reports, and fetch machine-readable
|
|
19
|
+
build or diff data for scripts, automation, and coding agents.
|
|
21
20
|
|
|
22
21
|
## Quick Start
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
You need Node.js 22+.
|
|
25
24
|
|
|
26
25
|
```bash
|
|
27
|
-
|
|
26
|
+
pnpm install -g @vizzly-testing/cli
|
|
28
27
|
|
|
29
28
|
vizzly init
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
For agent-friendly repos, install the Vizzly skill and add a short project
|
|
32
|
+
`AGENTS.md` note:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
vizzly init --agent-guidance
|
|
36
|
+
```
|
|
33
37
|
|
|
34
|
-
Start
|
|
38
|
+
### Start Local TDD
|
|
39
|
+
|
|
40
|
+
Start the TDD server, run your tests, and open the dashboard at
|
|
41
|
+
`http://localhost:47392`.
|
|
35
42
|
|
|
36
43
|
```bash
|
|
37
44
|
vizzly tdd start
|
|
38
|
-
|
|
45
|
+
pnpm test -- --watch
|
|
39
46
|
```
|
|
40
47
|
|
|
41
|
-
The dashboard shows diffs
|
|
48
|
+
The dashboard shows screenshots, baselines, and diffs as they arrive. Accept or
|
|
49
|
+
reject changes right from the UI.
|
|
42
50
|
|
|
43
|
-
### Cloud
|
|
51
|
+
### Run With Cloud Review
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
Use cloud builds when you want shared baselines, team review, and CI status:
|
|
46
54
|
|
|
47
55
|
```bash
|
|
48
56
|
vizzly login
|
|
49
|
-
vizzly run "
|
|
57
|
+
vizzly run "pnpm test" --wait
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
`--wait` blocks until Vizzly finishes processing the build. It exits with code
|
|
61
|
+
`1` when visual differences need review.
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
In CI, use a project token:
|
|
55
64
|
|
|
56
65
|
```bash
|
|
57
66
|
export VIZZLY_TOKEN=your-project-token
|
|
58
|
-
vizzly run "
|
|
67
|
+
vizzly run "pnpm test" --wait
|
|
59
68
|
```
|
|
60
69
|
|
|
61
|
-
###
|
|
70
|
+
### Inspect Builds And Diffs
|
|
62
71
|
|
|
63
|
-
Use `vizzly context` when you
|
|
72
|
+
Use `vizzly context` when you need build, comparison, screenshot, or review
|
|
73
|
+
queue data from the terminal.
|
|
64
74
|
|
|
65
|
-
This is
|
|
66
|
-
making a
|
|
67
|
-
|
|
75
|
+
This is useful for scripts, coding agents, and debugging loops. Instead of
|
|
76
|
+
making a pile of narrow API calls, ask for one focused bundle and get the
|
|
77
|
+
evidence in one place.
|
|
68
78
|
|
|
69
79
|
```bash
|
|
70
80
|
# Cloud context for a build or comparison
|
|
@@ -75,15 +85,19 @@ vizzly context comparison def456 --json
|
|
|
75
85
|
vizzly context build current --source local
|
|
76
86
|
vizzly context build current --source local --agent
|
|
77
87
|
vizzly context screenshot build-detail-screenshots --source local --json
|
|
88
|
+
vizzly context review-queue --source local --json
|
|
78
89
|
```
|
|
79
90
|
|
|
80
|
-
`--json` is the durable automation path. `--agent` gives a compact handoff for
|
|
91
|
+
`--json` is the durable automation path. `--agent` gives a compact handoff for
|
|
92
|
+
prompt assembly. Add `--full` when you need the whole payload, or
|
|
93
|
+
`--include screenshots,diffs,comments` when compact JSON needs selected detail.
|
|
81
94
|
|
|
82
|
-
Local context is read-only and file-backed. It reads your existing `.vizzly`
|
|
83
|
-
TDD runs, including screenshots, diffs, and
|
|
95
|
+
Local context is read-only and file-backed. It reads your existing `.vizzly`
|
|
96
|
+
workspace state from TDD runs, including screenshots, diffs, and saved hotspot
|
|
97
|
+
or region metadata.
|
|
84
98
|
|
|
85
|
-
Cloud context is also read-only right now. That is intentional.
|
|
86
|
-
|
|
99
|
+
Cloud context is also read-only right now. That is intentional. Vizzly helps you
|
|
100
|
+
see and inspect visual changes, while people still decide what gets approved.
|
|
87
101
|
|
|
88
102
|
## Capture Screenshots
|
|
89
103
|
|
|
@@ -103,7 +117,14 @@ test('homepage looks correct', async ({ page }) => {
|
|
|
103
117
|
});
|
|
104
118
|
```
|
|
105
119
|
|
|
106
|
-
The client SDK is lightweight
|
|
120
|
+
The client SDK is lightweight. It posts screenshots to the local Vizzly server
|
|
121
|
+
or the cloud build wrapper. It works with any test runner.
|
|
122
|
+
|
|
123
|
+
SDKs are available for
|
|
124
|
+
[JavaScript](https://docs.vizzly.dev/integration/sdk/javascript),
|
|
125
|
+
[Ruby](https://docs.vizzly.dev/integration/sdk/ruby),
|
|
126
|
+
[Swift](https://docs.vizzly.dev/integration/sdk/swift), and
|
|
127
|
+
[more](https://docs.vizzly.dev/integration/sdk/overview).
|
|
107
128
|
|
|
108
129
|
Already saving screenshots to disk? Pass the file path instead:
|
|
109
130
|
|
|
@@ -126,36 +147,46 @@ Generate a config file:
|
|
|
126
147
|
vizzly init
|
|
127
148
|
```
|
|
128
149
|
|
|
150
|
+
To teach project agents about Vizzly screenshot memory and the local visual TDD
|
|
151
|
+
loop, add the repo-local skill and AGENTS.md guidance:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
vizzly init --agent-guidance
|
|
155
|
+
```
|
|
156
|
+
|
|
129
157
|
Or create `vizzly.config.js` manually:
|
|
130
158
|
|
|
131
159
|
```javascript
|
|
132
160
|
export default {
|
|
133
161
|
comparison: {
|
|
134
|
-
|
|
135
|
-
|
|
162
|
+
// CIEDE2000 Delta E. 0 is exact. 2 is a good default.
|
|
163
|
+
threshold: 2.0,
|
|
164
|
+
},
|
|
136
165
|
};
|
|
137
166
|
```
|
|
138
167
|
|
|
139
168
|
## Commands
|
|
140
169
|
|
|
141
|
-
| Command |
|
|
142
|
-
|
|
143
|
-
| `vizzly tdd start` | Start local TDD server
|
|
144
|
-
| `vizzly tdd run "cmd"` | Run tests once
|
|
145
|
-
| `vizzly run "cmd"` | Run tests with cloud integration |
|
|
146
|
-
| `vizzly context ...` | Fetch visual context
|
|
147
|
-
| `vizzly upload <dir>` | Upload existing screenshots |
|
|
148
|
-
| `vizzly login` | Authenticate
|
|
149
|
-
| `vizzly doctor` | Validate local setup |
|
|
170
|
+
| Command | What it does |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `vizzly tdd start` | Start the local TDD server and dashboard. |
|
|
173
|
+
| `vizzly tdd run "cmd"` | Run tests once and generate a static visual report. |
|
|
174
|
+
| `vizzly run "cmd"` | Run tests with cloud build and review integration. |
|
|
175
|
+
| `vizzly context ...` | Fetch visual context for builds, comparisons, screenshots, and review queues. |
|
|
176
|
+
| `vizzly upload <dir>` | Upload an existing folder of screenshots. |
|
|
177
|
+
| `vizzly login` | Authenticate through the browser. |
|
|
178
|
+
| `vizzly doctor` | Validate your local setup. |
|
|
150
179
|
|
|
151
180
|
## Documentation
|
|
152
181
|
|
|
153
|
-
Full documentation at **[docs.vizzly.dev](https://docs.vizzly.dev)
|
|
154
|
-
CI
|
|
182
|
+
Full documentation lives at **[docs.vizzly.dev](https://docs.vizzly.dev)**.
|
|
183
|
+
Start there for framework guides, CI setup, SDK examples, and the configuration
|
|
184
|
+
reference.
|
|
155
185
|
|
|
156
186
|
## Contributing
|
|
157
187
|
|
|
158
|
-
Found a bug
|
|
188
|
+
Found a bug or have an idea?
|
|
189
|
+
[Open an issue](https://github.com/vizzly-testing/cli/issues) or send a PR.
|
|
159
190
|
|
|
160
191
|
## License
|
|
161
192
|
|
package/dist/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ import { saveUserPath } from './utils/global-config.js';
|
|
|
36
36
|
import * as output from './utils/output.js';
|
|
37
37
|
import { getPackageVersion } from './utils/package-info.js';
|
|
38
38
|
|
|
39
|
-
// Custom help formatting with
|
|
39
|
+
// Custom help formatting with BearDen design system
|
|
40
40
|
const formatHelp = (cmd, helper) => {
|
|
41
41
|
let c = colors;
|
|
42
42
|
let lines = [];
|
|
@@ -49,7 +49,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
49
49
|
// Cute grizzly bear mascot with square eyes (like the Vizzly logo!)
|
|
50
50
|
lines.push(c.brand.amber(' ʕ□ᴥ□ʔ'));
|
|
51
51
|
lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.dim(`v${version}`)}`);
|
|
52
|
-
lines.push(` ${c.gray('Visual
|
|
52
|
+
lines.push(` ${c.gray('Visual regression testing from your terminal')}`);
|
|
53
53
|
} else {
|
|
54
54
|
// Compact header for subcommands
|
|
55
55
|
lines.push(` ${c.brand.amber(c.bold('vizzly'))} ${c.white(cmd.name())}`);
|
|
@@ -177,7 +177,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
177
177
|
lines.push(` ${c.gray('$')} ${c.white('vizzly tdd start')}`);
|
|
178
178
|
lines.push('');
|
|
179
179
|
lines.push(` ${c.dim('# CI pipeline')}`);
|
|
180
|
-
lines.push(` ${c.gray('$')} ${c.white('vizzly run "
|
|
180
|
+
lines.push(` ${c.gray('$')} ${c.white('vizzly run "pnpm test" --wait')}`);
|
|
181
181
|
lines.push('');
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -252,7 +252,7 @@ function normalizeJsonArgv(argv, commandNames) {
|
|
|
252
252
|
}
|
|
253
253
|
return normalizedArgv;
|
|
254
254
|
}
|
|
255
|
-
program.name('vizzly').description('Vizzly CLI for visual
|
|
255
|
+
program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json [fields]', 'JSON output, optionally specify fields (e.g., --json id,status,branch)').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)').configureHelp({
|
|
256
256
|
formatHelp
|
|
257
257
|
});
|
|
258
258
|
|
|
@@ -303,8 +303,8 @@ try {
|
|
|
303
303
|
} catch (error) {
|
|
304
304
|
output.debug(`Plugin loading failed: ${error.message}`);
|
|
305
305
|
}
|
|
306
|
-
program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').action(async options => {
|
|
307
|
-
|
|
306
|
+
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', 'Add Vizzly guidance to this project AGENTS.md and install the agent skill').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
|
|
307
|
+
let globalOptions = program.opts();
|
|
308
308
|
await init({
|
|
309
309
|
...globalOptions,
|
|
310
310
|
...options,
|
|
@@ -411,7 +411,7 @@ tddCmd.command('run <command>').description('Run tests once in TDD mode with loc
|
|
|
411
411
|
const reportResult = await generateStaticReport(process.cwd());
|
|
412
412
|
if (reportResult.success) {
|
|
413
413
|
const reportUrl = getReportFileUrl(reportResult.reportPath);
|
|
414
|
-
output.print(` ${colors.brand.textTertiary('→')} Report: ${colors.
|
|
414
|
+
output.print(` ${colors.brand.textTertiary('→')} Report: ${colors.info(reportUrl)}`);
|
|
415
415
|
output.blank();
|
|
416
416
|
|
|
417
417
|
// Open report in browser unless --no-open
|
|
@@ -512,8 +512,8 @@ Examples:
|
|
|
512
512
|
}
|
|
513
513
|
await comparisonsCommand(options, globalOptions);
|
|
514
514
|
});
|
|
515
|
-
let contextCmd = program.command('context').description('Fetch build
|
|
516
|
-
contextCmd.command('build').description('Fetch build context
|
|
515
|
+
let contextCmd = program.command('context').description('Fetch build and diff context');
|
|
516
|
+
contextCmd.command('build').description('Fetch build context').argument('<build-id>', 'Build ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('--agent', 'Output compact context for LLM agents').option('--full', 'Return the full build context payload with --agent --json').option('--include <items>', 'Add detail to compact agent JSON: screenshots,diffs,comments').addHelpText('after', `
|
|
517
517
|
Examples:
|
|
518
518
|
$ vizzly context build abc123
|
|
519
519
|
$ vizzly context build current --source local
|
|
@@ -890,7 +890,7 @@ program.command('whoami').description('Show current authentication status and us
|
|
|
890
890
|
});
|
|
891
891
|
|
|
892
892
|
// Save user's PATH for menubar app (non-blocking, runs in background)
|
|
893
|
-
// This auto-configures the menubar app so it can find
|
|
893
|
+
// This auto-configures the menubar app so it can find package runners/node
|
|
894
894
|
saveUserPath().catch(() => {});
|
|
895
895
|
let commandNames = new Set(program.commands.map(command => command.name()));
|
|
896
896
|
let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
|
package/dist/client/index.js
CHANGED
|
@@ -192,6 +192,7 @@ function createSimpleClient(serverUrl) {
|
|
|
192
192
|
let isFilePath = typeof imageBuffer === 'string';
|
|
193
193
|
let image = isFilePath ? imageBuffer : imageBuffer.toString('base64');
|
|
194
194
|
let type = isFilePath ? 'file-path' : 'base64';
|
|
195
|
+
let requestTimeout = options.requestTimeout || DEFAULT_TIMEOUT_MS;
|
|
195
196
|
let properties = createScreenshotProperties(options);
|
|
196
197
|
let httpStart = Date.now();
|
|
197
198
|
let {
|
|
@@ -203,7 +204,7 @@ function createSimpleClient(serverUrl) {
|
|
|
203
204
|
image,
|
|
204
205
|
type,
|
|
205
206
|
properties
|
|
206
|
-
},
|
|
207
|
+
}, requestTimeout);
|
|
207
208
|
let httpMs = Date.now() - httpStart;
|
|
208
209
|
if (shouldLogClient('debug')) {
|
|
209
210
|
console.debug(`[vizzly-client] ${name} HTTP completed in ${httpMs}ms`);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -234,11 +234,11 @@ export async function doctorCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
234
234
|
output.printErr(` ${colors.dim('─'.repeat(52))}`);
|
|
235
235
|
for (let item of contextItems) {
|
|
236
236
|
if (item.type === 'success') {
|
|
237
|
-
output.printErr(` ${colors.
|
|
237
|
+
output.printErr(` ${colors.success('✓')} ${colors.brand.textMuted(item.label)} ${colors.white(item.value)}`);
|
|
238
238
|
} else if (item.type === 'warning') {
|
|
239
|
-
output.printErr(` ${colors.yellow('!')} ${colors.
|
|
239
|
+
output.printErr(` ${colors.yellow('!')} ${colors.brand.textMuted(item.label)} ${colors.yellow(item.value)}`);
|
|
240
240
|
} else {
|
|
241
|
-
output.printErr(` ${colors.dim('○')} ${colors.
|
|
241
|
+
output.printErr(` ${colors.dim('○')} ${colors.brand.textMuted(item.label)} ${colors.dim(item.value)}`);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
}
|
|
@@ -246,7 +246,7 @@ export async function doctorCommand(options = {}, globalOptions = {}, deps = {})
|
|
|
246
246
|
// Footer with links
|
|
247
247
|
output.printErr('');
|
|
248
248
|
output.printErr(` ${colors.dim('─'.repeat(52))}`);
|
|
249
|
-
output.printErr(` ${colors.dim('Docs')} ${colors.
|
|
249
|
+
output.printErr(` ${colors.dim('Docs')} ${colors.info(colors.underline('docs.vizzly.dev'))} ${colors.dim('GitHub')} ${colors.info(colors.underline('github.com/vizzly-testing/cli'))}`);
|
|
250
250
|
|
|
251
251
|
// Emit structured data in verbose mode (in addition to visual output)
|
|
252
252
|
if (globalOptions.verbose) {
|
package/dist/commands/init.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { z } from 'zod';
|
|
5
7
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
6
8
|
import { loadPlugins } from '../plugin-loader.js';
|
|
7
9
|
import { loadConfig } from '../utils/config-loader.js';
|
|
8
10
|
import * as output from '../utils/output.js';
|
|
11
|
+
let commandDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
12
|
let configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(z.string(), configValueSchema)]));
|
|
10
13
|
let configSchemaValidator = z.record(z.string(), configValueSchema);
|
|
11
14
|
function createInitDeps(deps = {}) {
|
|
12
15
|
return {
|
|
13
16
|
access: deps.access || fs.access,
|
|
17
|
+
copy: deps.copy || fs.cp,
|
|
14
18
|
cwd: deps.cwd || (() => process.cwd()),
|
|
19
|
+
isInteractive: deps.isInteractive || (() => Boolean(process.stdin.isTTY && process.stdout.isTTY)),
|
|
15
20
|
loadConfig: deps.loadConfig || loadConfig,
|
|
16
21
|
loadPlugins: deps.loadPlugins || loadPlugins,
|
|
22
|
+
mkdir: deps.mkdir || fs.mkdir,
|
|
17
23
|
output: deps.output || output,
|
|
24
|
+
promptAgentSkill: deps.promptAgentSkill || promptAgentSkill,
|
|
25
|
+
readFile: deps.readFile || fs.readFile,
|
|
26
|
+
skillSourcePath: deps.skillSourcePath || getPackagedAgentSkillPath(),
|
|
18
27
|
writeFile: deps.writeFile || fs.writeFile
|
|
19
28
|
};
|
|
20
29
|
}
|
|
@@ -28,6 +37,15 @@ function configureOutput(output, options) {
|
|
|
28
37
|
export function getInitConfigPath(cwd = process.cwd()) {
|
|
29
38
|
return path.join(cwd, 'vizzly.config.js');
|
|
30
39
|
}
|
|
40
|
+
export function getPackagedAgentSkillPath(baseDir = commandDir) {
|
|
41
|
+
return path.resolve(baseDir, '..', '..', 'skills', 'vizzly');
|
|
42
|
+
}
|
|
43
|
+
export function getProjectAgentSkillPath(cwd = process.cwd()) {
|
|
44
|
+
return path.join(cwd, '.agents', 'skills', 'vizzly');
|
|
45
|
+
}
|
|
46
|
+
export function getProjectAgentsPath(cwd = process.cwd()) {
|
|
47
|
+
return path.join(cwd, 'AGENTS.md');
|
|
48
|
+
}
|
|
31
49
|
export async function fileExists(filePath, access = fs.access) {
|
|
32
50
|
try {
|
|
33
51
|
await access(filePath);
|
|
@@ -160,7 +178,170 @@ function writeHumanCreatedOutput(output, plugins) {
|
|
|
160
178
|
function showNextSteps(output) {
|
|
161
179
|
output.blank();
|
|
162
180
|
output.labelValue('Next steps', '');
|
|
163
|
-
output.list(['Set your API token: export VIZZLY_TOKEN="your-api-key"', 'Run your tests with Vizzly:
|
|
181
|
+
output.list(['Set your API token: export VIZZLY_TOKEN="your-api-key"', 'Run your tests with Vizzly: pnpm exec vizzly run "pnpm test"', 'Upload screenshots: pnpm exec vizzly upload ./screenshots']);
|
|
182
|
+
}
|
|
183
|
+
async function promptAgentSkill({
|
|
184
|
+
stdin = process.stdin,
|
|
185
|
+
stdout = process.stdout
|
|
186
|
+
} = {}) {
|
|
187
|
+
let rl = createInterface({
|
|
188
|
+
input: stdin,
|
|
189
|
+
output: stdout
|
|
190
|
+
});
|
|
191
|
+
try {
|
|
192
|
+
let answer = await rl.question('Add the Vizzly agent skill to this repo? [Y/n] ');
|
|
193
|
+
return !['n', 'no'].includes(answer.trim().toLowerCase());
|
|
194
|
+
} finally {
|
|
195
|
+
rl.close();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function shouldInstallAgentSkill(options, deps) {
|
|
199
|
+
if (options.agentGuidance) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (options.skipAgentSkill) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (options.agentSkill) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (options.json || !deps.isInteractive()) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return await deps.promptAgentSkill();
|
|
212
|
+
}
|
|
213
|
+
let agentGuidanceStart = '<!-- vizzly-agent-guidance -->';
|
|
214
|
+
let agentGuidanceEnd = '<!-- /vizzly-agent-guidance -->';
|
|
215
|
+
export function createAgentGuidanceContent() {
|
|
216
|
+
return `${agentGuidanceStart}
|
|
217
|
+
## Visual Testing With Vizzly
|
|
218
|
+
|
|
219
|
+
Use Vizzly for user-facing UI changes when this repo has Vizzly configured.
|
|
220
|
+
|
|
221
|
+
- Read visual history before changing UI: \`npx vizzly context build current --source local --agent\`
|
|
222
|
+
- Inspect named screenshot history when useful: \`npx vizzly context screenshot "<screenshot-name>" --source local --json\`
|
|
223
|
+
- Verify visual changes through the existing user workflow that owns the surface: \`npx vizzly tdd run "<test command>" --no-open\`
|
|
224
|
+
|
|
225
|
+
Prefer existing E2E/user journeys over narrow screenshot-only specs. Treat Vizzly diffs as review evidence; do not approve or reject changes unless asked.
|
|
226
|
+
${agentGuidanceEnd}
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
function appendSection(content, section) {
|
|
230
|
+
if (!content.trim()) {
|
|
231
|
+
return section;
|
|
232
|
+
}
|
|
233
|
+
return `${content.replace(/\s*$/, '')}\n\n${section}`;
|
|
234
|
+
}
|
|
235
|
+
export async function upsertProjectAgentGuidance({
|
|
236
|
+
cwd,
|
|
237
|
+
access = fs.access,
|
|
238
|
+
readFile = fs.readFile,
|
|
239
|
+
writeFile = fs.writeFile
|
|
240
|
+
}) {
|
|
241
|
+
let agentsPath = getProjectAgentsPath(cwd);
|
|
242
|
+
let guidance = createAgentGuidanceContent();
|
|
243
|
+
if (!(await fileExists(agentsPath, access))) {
|
|
244
|
+
await writeFile(agentsPath, guidance, 'utf8');
|
|
245
|
+
return {
|
|
246
|
+
status: 'created',
|
|
247
|
+
agentsPath
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
let existingContent = await readFile(agentsPath, 'utf8');
|
|
251
|
+
if (existingContent.includes(agentGuidanceStart)) {
|
|
252
|
+
return {
|
|
253
|
+
status: 'exists',
|
|
254
|
+
agentsPath
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
await writeFile(agentsPath, appendSection(existingContent, guidance), 'utf8');
|
|
258
|
+
return {
|
|
259
|
+
status: 'updated',
|
|
260
|
+
agentsPath
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export async function installProjectAgentSkill({
|
|
264
|
+
cwd,
|
|
265
|
+
sourcePath,
|
|
266
|
+
access = fs.access,
|
|
267
|
+
copy = fs.cp,
|
|
268
|
+
mkdir = fs.mkdir
|
|
269
|
+
}) {
|
|
270
|
+
let targetPath = getProjectAgentSkillPath(cwd);
|
|
271
|
+
if (!(await fileExists(path.join(sourcePath, 'SKILL.md'), access))) {
|
|
272
|
+
return {
|
|
273
|
+
status: 'missing-source',
|
|
274
|
+
sourcePath,
|
|
275
|
+
targetPath
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (await fileExists(targetPath, access)) {
|
|
279
|
+
return {
|
|
280
|
+
status: 'exists',
|
|
281
|
+
sourcePath,
|
|
282
|
+
targetPath
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
await mkdir(path.dirname(targetPath), {
|
|
286
|
+
recursive: true
|
|
287
|
+
});
|
|
288
|
+
await copy(sourcePath, targetPath, {
|
|
289
|
+
recursive: true,
|
|
290
|
+
errorOnExist: true,
|
|
291
|
+
force: false
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
status: 'installed',
|
|
295
|
+
sourcePath,
|
|
296
|
+
targetPath
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function writeAgentSkillOutput(output, result) {
|
|
300
|
+
if (!result) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (result.status === 'installed') {
|
|
304
|
+
output.complete('Added Vizzly agent skill');
|
|
305
|
+
output.hint(`Installed at ${result.targetPath}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (result.status === 'exists') {
|
|
309
|
+
output.hint('Vizzly agent skill already exists in this repo');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (result.status === 'missing-source') {
|
|
313
|
+
output.warn('Vizzly agent skill was not found in this CLI package');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function writeAgentGuidanceOutput(output, result) {
|
|
317
|
+
if (!result) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (result.status === 'created') {
|
|
321
|
+
output.complete('Created AGENTS.md with Vizzly guidance');
|
|
322
|
+
output.hint(`Wrote ${result.agentsPath}`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (result.status === 'updated') {
|
|
326
|
+
output.complete('Added Vizzly guidance to AGENTS.md');
|
|
327
|
+
output.hint(`Updated ${result.agentsPath}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (result.status === 'exists') {
|
|
331
|
+
output.hint('Vizzly guidance already exists in AGENTS.md');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function withAgentSetupResults(payload, {
|
|
335
|
+
agentSkillResult,
|
|
336
|
+
agentGuidanceResult
|
|
337
|
+
}) {
|
|
338
|
+
if (agentSkillResult) {
|
|
339
|
+
payload.agentSkill = agentSkillResult;
|
|
340
|
+
}
|
|
341
|
+
if (agentGuidanceResult) {
|
|
342
|
+
payload.agentGuidance = agentGuidanceResult;
|
|
343
|
+
}
|
|
344
|
+
return payload;
|
|
164
345
|
}
|
|
165
346
|
async function writeConfigFile({
|
|
166
347
|
configPath,
|
|
@@ -197,14 +378,37 @@ export async function init(options = {}, deps = {}) {
|
|
|
197
378
|
let plugins = await loadInitPlugins(options, resolvedDeps);
|
|
198
379
|
let configPath = getInitConfigPath(resolvedDeps.cwd());
|
|
199
380
|
let hasConfig = await fileExists(configPath, resolvedDeps.access);
|
|
381
|
+
let agentSkillResult = null;
|
|
382
|
+
let agentGuidanceResult = null;
|
|
383
|
+
if (await shouldInstallAgentSkill(options, resolvedDeps)) {
|
|
384
|
+
agentSkillResult = await installProjectAgentSkill({
|
|
385
|
+
cwd: resolvedDeps.cwd(),
|
|
386
|
+
sourcePath: resolvedDeps.skillSourcePath,
|
|
387
|
+
access: resolvedDeps.access,
|
|
388
|
+
copy: resolvedDeps.copy,
|
|
389
|
+
mkdir: resolvedDeps.mkdir
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (options.agentGuidance) {
|
|
393
|
+
agentGuidanceResult = await upsertProjectAgentGuidance({
|
|
394
|
+
cwd: resolvedDeps.cwd(),
|
|
395
|
+
access: resolvedDeps.access,
|
|
396
|
+
readFile: resolvedDeps.readFile,
|
|
397
|
+
writeFile: resolvedDeps.writeFile
|
|
398
|
+
});
|
|
399
|
+
}
|
|
200
400
|
if (hasConfig && !options.force) {
|
|
201
401
|
if (options.json) {
|
|
202
|
-
|
|
402
|
+
let payload = withAgentSetupResults({
|
|
203
403
|
status: 'skipped',
|
|
204
404
|
reason: 'config_exists',
|
|
205
405
|
configPath,
|
|
206
406
|
message: 'A vizzly.config.js file already exists. Use --force to overwrite.'
|
|
407
|
+
}, {
|
|
408
|
+
agentSkillResult,
|
|
409
|
+
agentGuidanceResult
|
|
207
410
|
});
|
|
411
|
+
resolvedDeps.output.data(payload);
|
|
208
412
|
return {
|
|
209
413
|
status: 'skipped',
|
|
210
414
|
configPath
|
|
@@ -213,6 +417,8 @@ export async function init(options = {}, deps = {}) {
|
|
|
213
417
|
resolvedDeps.output.header('init');
|
|
214
418
|
resolvedDeps.output.warn('A vizzly.config.js file already exists');
|
|
215
419
|
resolvedDeps.output.hint('Use --force to overwrite');
|
|
420
|
+
writeAgentSkillOutput(resolvedDeps.output, agentSkillResult);
|
|
421
|
+
writeAgentGuidanceOutput(resolvedDeps.output, agentGuidanceResult);
|
|
216
422
|
return {
|
|
217
423
|
status: 'skipped',
|
|
218
424
|
configPath
|
|
@@ -228,11 +434,15 @@ export async function init(options = {}, deps = {}) {
|
|
|
228
434
|
});
|
|
229
435
|
let pluginNames = getPluginConfigNames(plugins);
|
|
230
436
|
if (options.json) {
|
|
231
|
-
|
|
437
|
+
let payload = withAgentSetupResults({
|
|
232
438
|
status: 'created',
|
|
233
439
|
configPath,
|
|
234
440
|
plugins: pluginNames
|
|
441
|
+
}, {
|
|
442
|
+
agentSkillResult,
|
|
443
|
+
agentGuidanceResult
|
|
235
444
|
});
|
|
445
|
+
resolvedDeps.output.data(payload);
|
|
236
446
|
return {
|
|
237
447
|
status: 'created',
|
|
238
448
|
configPath,
|
|
@@ -240,6 +450,8 @@ export async function init(options = {}, deps = {}) {
|
|
|
240
450
|
};
|
|
241
451
|
}
|
|
242
452
|
showNextSteps(resolvedDeps.output);
|
|
453
|
+
writeAgentSkillOutput(resolvedDeps.output, agentSkillResult);
|
|
454
|
+
writeAgentGuidanceOutput(resolvedDeps.output, agentGuidanceResult);
|
|
243
455
|
resolvedDeps.output.blank();
|
|
244
456
|
resolvedDeps.output.complete('Vizzly CLI setup complete');
|
|
245
457
|
return {
|
package/dist/commands/preview.js
CHANGED
|
@@ -335,7 +335,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
335
335
|
output.blank();
|
|
336
336
|
output.print(' Run visual tests first, then upload your preview:');
|
|
337
337
|
output.blank();
|
|
338
|
-
output.print(' vizzly run "
|
|
338
|
+
output.print(' vizzly run "pnpm test"');
|
|
339
339
|
output.print(' vizzly preview ./dist');
|
|
340
340
|
output.blank();
|
|
341
341
|
output.print(' Or specify a build explicitly:');
|
|
@@ -591,13 +591,13 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
591
591
|
output.print(` ${colors.brand.textTertiary('Files')} ${colors.white(result.uploaded)} (${formatBytes(result.totalBytes)} compressed)`);
|
|
592
592
|
if (result.reusedBlobs > 0) {
|
|
593
593
|
let savedBytes = result.totalBytes - result.newBytes;
|
|
594
|
-
output.print(` ${colors.brand.textTertiary('Deduped')} ${colors.
|
|
594
|
+
output.print(` ${colors.brand.textTertiary('Deduped')} ${colors.success(result.reusedBlobs)} files (saved ${formatBytes(savedBytes)})`);
|
|
595
595
|
}
|
|
596
596
|
if (result.basePath) {
|
|
597
597
|
output.print(` ${colors.brand.textTertiary('Base path')} ${colors.dim(result.basePath)}`);
|
|
598
598
|
}
|
|
599
599
|
output.blank();
|
|
600
|
-
output.print(` ${colors.brand.textTertiary('Preview')} ${colors.
|
|
600
|
+
output.print(` ${colors.brand.textTertiary('Preview')} ${colors.info(colors.underline(result.previewUrl))}`);
|
|
601
601
|
|
|
602
602
|
// Open in browser if requested
|
|
603
603
|
if (options.open) {
|
package/dist/commands/run.js
CHANGED
|
@@ -348,7 +348,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
348
348
|
getTokenContext
|
|
349
349
|
});
|
|
350
350
|
if (displayUrl) {
|
|
351
|
-
output.print(` ${colors.brand.textTertiary('Results')} ${colors.
|
|
351
|
+
output.print(` ${colors.brand.textTertiary('Results')} ${colors.info(colors.underline(displayUrl))}`);
|
|
352
352
|
} else {
|
|
353
353
|
output.print(` ${colors.brand.textTertiary('Build')} ${colors.dim(result.buildId)}`);
|
|
354
354
|
}
|