@vizzly-testing/cli 0.35.3-beta.4 → 0.37.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 +31 -24
- package/dist/api/client.js +14 -2
- package/dist/api/core.js +8 -8
- package/dist/api/endpoints.js +10 -7
- package/dist/cli.js +40 -15
- package/dist/client/index.js +0 -6
- package/dist/commands/api.js +72 -25
- package/dist/commands/init.js +4 -2
- package/dist/commands/run.js +60 -8
- package/dist/commands/status.js +2 -2
- package/dist/commands/tdd.js +1 -1
- package/dist/reporter/reporter-bundle.iife.js +51 -51
- package/dist/sdk/index.js +0 -10
- package/dist/server/handlers/api-handler.js +105 -195
- package/dist/server/handlers/tdd-handler.js +13 -54
- package/dist/server/routers/screenshot.js +6 -3
- package/dist/test-runner/core.js +7 -1
- package/dist/test-runner/operations.js +22 -7
- package/dist/types/client.d.ts +12 -5
- package/dist/uploader/core.js +4 -3
- package/dist/utils/screenshot-options.js +4 -84
- package/dist/utils/security.js +27 -42
- package/package.json +8 -7
- package/skills/vizzly/SKILL.md +17 -9
- package/skills/vizzly/references/cli-context.md +49 -15
- package/skills/vizzly/references/setup-ci.md +2 -2
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ For a one-off local check, wrap the test command with `tdd run`:
|
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
64
|
vizzly tdd run "pnpm test" --no-open
|
|
65
|
-
vizzly context build current --source local --
|
|
65
|
+
vizzly context build current --source local --json
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
That run writes review data under `.vizzly/` and prints a context command you
|
|
@@ -96,38 +96,44 @@ vizzly run "pnpm test" --wait
|
|
|
96
96
|
|
|
97
97
|
### Inspect Builds And Diffs
|
|
98
98
|
|
|
99
|
-
Use `vizzly context`
|
|
100
|
-
|
|
99
|
+
Use `vizzly context` for human summaries and local `.vizzly` evidence. For cloud
|
|
100
|
+
reviews, agents should discover and call the public API through its schema:
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
```bash
|
|
103
|
+
vizzly api schema --json
|
|
104
|
+
vizzly api schema sdk.listBuilds --json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The `context build --agent` and `context comparison --agent` formats are
|
|
108
|
+
deprecated and scheduled for removal in v0.38.0. Use the schema-driven API for
|
|
109
|
+
cloud evidence; use `vizzly context ... --json` for local evidence.
|
|
105
110
|
|
|
106
111
|
```bash
|
|
107
|
-
#
|
|
112
|
+
# Human-readable cloud build context
|
|
108
113
|
vizzly context build abc123 --source cloud
|
|
109
|
-
|
|
114
|
+
|
|
115
|
+
# Discover cloud evidence operations for an agent workflow
|
|
116
|
+
vizzly api schema sdk.getBuildContext --json
|
|
117
|
+
vizzly api schema getComparisonContext --json
|
|
110
118
|
|
|
111
119
|
# Local workspace context from .vizzly/
|
|
112
120
|
vizzly context build current --source local
|
|
113
|
-
vizzly context build current --source local --
|
|
121
|
+
vizzly context build current --source local --json
|
|
114
122
|
vizzly context screenshot build-detail-screenshots --source local --json
|
|
115
123
|
vizzly context review-queue --source local --json
|
|
116
124
|
```
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`--include diffs` for raw diff diagnostics, or `--full` when you need the whole
|
|
123
|
-
payload.
|
|
126
|
+
Use `vizzly api schema` to inspect cloud operation inputs, output fields, and
|
|
127
|
+
pagination before making requests. The old compact `--agent` handoff remains
|
|
128
|
+
available during deprecation, but new cloud workflows should use the schema.
|
|
129
|
+
Use `--json` for machine-readable local context.
|
|
124
130
|
|
|
125
131
|
Local context is read-only and file-backed. It reads your existing `.vizzly`
|
|
126
132
|
workspace state from TDD runs, including screenshots, diffs, and saved hotspot
|
|
127
133
|
or region metadata.
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
The `context` commands are read-only. The schema-discovered review API also
|
|
136
|
+
supports explicit decisions when a task asks for them.
|
|
131
137
|
|
|
132
138
|
## Capture Screenshots
|
|
133
139
|
|
|
@@ -144,18 +150,19 @@ test('homepage looks correct', async ({ page }) => {
|
|
|
144
150
|
fullPage: true,
|
|
145
151
|
requestTimeout: 5000,
|
|
146
152
|
properties: {
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
theme: 'dark',
|
|
154
|
+
locale: 'en-US',
|
|
149
155
|
},
|
|
150
156
|
});
|
|
151
157
|
});
|
|
152
158
|
```
|
|
153
159
|
|
|
154
|
-
`properties` is your metadata bag for baseline grouping, filtering,
|
|
155
|
-
debugging.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
`properties` is your key/value metadata bag for baseline grouping, filtering,
|
|
161
|
+
and debugging. Vizzly passes every property through as user metadata; property
|
|
162
|
+
names are never interpreted as options. The supported top-level options are
|
|
163
|
+
`threshold`, `minClusterSize`, `fullPage`, `requestTimeout`, and `buildId`.
|
|
164
|
+
Vizzly reads width and height from the captured image, so viewport dimensions do
|
|
165
|
+
not need to be included in `properties`.
|
|
159
166
|
|
|
160
167
|
The client SDK is lightweight. It posts screenshots to the local Vizzly server
|
|
161
168
|
or the cloud build wrapper. It works with any test runner.
|
package/dist/api/client.js
CHANGED
|
@@ -56,6 +56,11 @@ export function createApiClient(options = {}) {
|
|
|
56
56
|
* @returns {Promise<Object>} Parsed JSON response
|
|
57
57
|
*/
|
|
58
58
|
async function request(endpoint, fetchOptions = {}, isRetry = false) {
|
|
59
|
+
let {
|
|
60
|
+
responseType = 'json',
|
|
61
|
+
retryAuthentication = true,
|
|
62
|
+
...httpOptions
|
|
63
|
+
} = fetchOptions;
|
|
59
64
|
let url = buildApiUrl(baseUrl, endpoint);
|
|
60
65
|
let headers = buildRequestHeaders({
|
|
61
66
|
token,
|
|
@@ -66,7 +71,7 @@ export function createApiClient(options = {}) {
|
|
|
66
71
|
let response;
|
|
67
72
|
try {
|
|
68
73
|
response = await fetch(url, {
|
|
69
|
-
...
|
|
74
|
+
...httpOptions,
|
|
70
75
|
headers
|
|
71
76
|
});
|
|
72
77
|
} catch (error) {
|
|
@@ -80,7 +85,7 @@ export function createApiClient(options = {}) {
|
|
|
80
85
|
let errorBody = await extractErrorBody(response);
|
|
81
86
|
|
|
82
87
|
// Handle 401 with token refresh
|
|
83
|
-
if (shouldRetryWithRefresh(response.status, isRetry, !isProjectToken(token) && (await hasRefreshToken()))) {
|
|
88
|
+
if (retryAuthentication && shouldRetryWithRefresh(response.status, isRetry, !isProjectToken(token) && (await hasRefreshToken()))) {
|
|
84
89
|
let refreshed = await attemptTokenRefresh();
|
|
85
90
|
if (refreshed) {
|
|
86
91
|
token = refreshed;
|
|
@@ -100,6 +105,12 @@ export function createApiClient(options = {}) {
|
|
|
100
105
|
details: error.details
|
|
101
106
|
});
|
|
102
107
|
}
|
|
108
|
+
if (responseType === 'response') return response;
|
|
109
|
+
if (response.status === 204 || httpOptions.method === 'HEAD') return null;
|
|
110
|
+
let contentType = response.headers?.get?.('content-type');
|
|
111
|
+
if (contentType && !/application\/(?:[\w.+-]+\+)?json\b/i.test(contentType)) {
|
|
112
|
+
throw new VizzlyError('This response contains file data. Use api --output <file> to download it.', 'BINARY_RESPONSE');
|
|
113
|
+
}
|
|
103
114
|
return response.json();
|
|
104
115
|
}
|
|
105
116
|
|
|
@@ -122,6 +133,7 @@ export function createApiClient(options = {}) {
|
|
|
122
133
|
let refreshUrl = buildApiUrl(baseUrl, '/api/auth/cli/refresh');
|
|
123
134
|
let response = await fetch(refreshUrl, {
|
|
124
135
|
method: 'POST',
|
|
136
|
+
redirect: 'error',
|
|
125
137
|
headers: {
|
|
126
138
|
'Content-Type': 'application/json',
|
|
127
139
|
'User-Agent': userAgent
|
package/dist/api/core.js
CHANGED
|
@@ -30,14 +30,17 @@ export function buildAuthHeader(token) {
|
|
|
30
30
|
* @param {string} version - CLI version
|
|
31
31
|
* @param {string} command - Command being executed (run, upload, tdd, etc.)
|
|
32
32
|
* @param {string|null} sdkUserAgent - Optional SDK user agent to append
|
|
33
|
+
* @param {string} nodeVersion - Node.js version to append
|
|
33
34
|
* @returns {string} Complete User-Agent string
|
|
34
35
|
*/
|
|
35
|
-
export function buildUserAgent(version, command, sdkUserAgent = null) {
|
|
36
|
+
export function buildUserAgent(version, command, sdkUserAgent = null, nodeVersion = process.versions.node) {
|
|
36
37
|
let baseUserAgent = `vizzly-cli/${version} (${command})`;
|
|
38
|
+
let userAgentParts = [baseUserAgent];
|
|
37
39
|
if (sdkUserAgent) {
|
|
38
|
-
|
|
40
|
+
userAgentParts.push(sdkUserAgent);
|
|
39
41
|
}
|
|
40
|
-
|
|
42
|
+
if (nodeVersion) userAgentParts.push(`node/${nodeVersion}`);
|
|
43
|
+
return userAgentParts.join(' ');
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -74,7 +77,7 @@ export function buildRequestHeaders({
|
|
|
74
77
|
* Build payload for screenshot upload or SHA resolution
|
|
75
78
|
* @param {string} name - Screenshot name
|
|
76
79
|
* @param {Buffer|null} buffer - Image data, or null when resolving by SHA
|
|
77
|
-
* @param {Object} metadata -
|
|
80
|
+
* @param {Object} metadata - User screenshot properties
|
|
78
81
|
* @param {string|null} sha256 - Pre-computed SHA256 hash (optional)
|
|
79
82
|
* @returns {Object} Screenshot upload payload
|
|
80
83
|
*/
|
|
@@ -98,7 +101,7 @@ export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = nul
|
|
|
98
101
|
/**
|
|
99
102
|
* Build payload for server-side screenshot resolution without sending image bytes
|
|
100
103
|
* @param {string} name - Screenshot name
|
|
101
|
-
* @param {Object} metadata -
|
|
104
|
+
* @param {Object} metadata - User screenshot properties
|
|
102
105
|
* @param {string} sha256 - Pre-computed SHA256 hash
|
|
103
106
|
* @returns {Object} Screenshot resolution payload
|
|
104
107
|
*/
|
|
@@ -190,9 +193,6 @@ export function buildScreenshotCheckObject(sha256, name, metadata = {}) {
|
|
|
190
193
|
return {
|
|
191
194
|
sha256,
|
|
192
195
|
name,
|
|
193
|
-
browser: meta.browser || 'chrome',
|
|
194
|
-
viewport_width: meta.viewport?.width || meta.viewport_width || 1920,
|
|
195
|
-
viewport_height: meta.viewport?.height || meta.viewport_height || 1080,
|
|
196
196
|
properties
|
|
197
197
|
};
|
|
198
198
|
}
|
package/dist/api/endpoints.js
CHANGED
|
@@ -80,13 +80,15 @@ export async function createBuild(client, metadata) {
|
|
|
80
80
|
* @param {Object} client - API client
|
|
81
81
|
* @param {string} buildId - Build ID
|
|
82
82
|
* @param {string} status - Build status (pending|running|completed|failed)
|
|
83
|
-
* @param {number|null} executionTimeMs - Execution time in milliseconds
|
|
83
|
+
* @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
|
|
84
|
+
* @param {string} [failureReason] - Explanation sent only for a failed build
|
|
84
85
|
* @returns {Promise<Object>} Updated build data
|
|
85
86
|
*/
|
|
86
|
-
export async function updateBuildStatus(client, buildId, status, executionTimeMs = null) {
|
|
87
|
+
export async function updateBuildStatus(client, buildId, status, executionTimeMs = null, failureReason) {
|
|
87
88
|
let body = {
|
|
88
89
|
status
|
|
89
90
|
};
|
|
91
|
+
if (status === 'failed' && failureReason) body.failureReason = failureReason;
|
|
90
92
|
if (executionTimeMs != null) {
|
|
91
93
|
body.executionTimeMs = executionTimeMs;
|
|
92
94
|
}
|
|
@@ -100,16 +102,17 @@ export async function updateBuildStatus(client, buildId, status, executionTimeMs
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
/**
|
|
103
|
-
*
|
|
105
|
+
* Mark a build completed or failed, with an optional failure explanation.
|
|
104
106
|
* @param {Object} client - API client
|
|
105
107
|
* @param {string} buildId - Build ID
|
|
106
|
-
* @param {boolean} success - Whether
|
|
107
|
-
* @param {number|null} executionTimeMs - Execution time in milliseconds
|
|
108
|
+
* @param {boolean} [success=true] - Whether tests and uploads succeeded
|
|
109
|
+
* @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
|
|
110
|
+
* @param {string} [failureReason] - Explanation sent only for a failed build
|
|
108
111
|
* @returns {Promise<Object>} Finalized build data
|
|
109
112
|
*/
|
|
110
|
-
export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null) {
|
|
113
|
+
export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null, failureReason) {
|
|
111
114
|
let status = success ? 'completed' : 'failed';
|
|
112
|
-
return updateBuildStatus(client, buildId, status, executionTimeMs);
|
|
115
|
+
return updateBuildStatus(client, buildId, status, executionTimeMs, failureReason);
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { existsSync, statSync } from 'node:fs';
|
|
4
4
|
import { Option, program } from 'commander';
|
|
5
|
-
import { apiCommand, validateApiOptions } from './commands/api.js';
|
|
5
|
+
import { apiCommand, apiSchemaCommand, validateApiOptions } from './commands/api.js';
|
|
6
6
|
import { baselinesCommand } from './commands/baselines.js';
|
|
7
7
|
import { buildsCommand, validateBuildsOptions } from './commands/builds.js';
|
|
8
8
|
import { comparisonsCommand, validateComparisonsOptions } from './commands/comparisons.js';
|
|
@@ -357,6 +357,10 @@ function getGlobalOptions() {
|
|
|
357
357
|
noColor: options.noColor || options.color === false
|
|
358
358
|
};
|
|
359
359
|
}
|
|
360
|
+
function warnDeprecatedContextAgentOption() {
|
|
361
|
+
let warning = ['`vizzly context --agent` is deprecated and will be removed in v0.38.0.', 'For cloud reviews, use `vizzly api schema --json` and follow the', 'discovered operations. For local evidence, use', '`vizzly context ... --json`.'].join(' ');
|
|
362
|
+
output.warn(warning);
|
|
363
|
+
}
|
|
360
364
|
function reportValidationErrors(errors) {
|
|
361
365
|
if (output.isJson()) {
|
|
362
366
|
output.error('Validation errors', null, {
|
|
@@ -651,30 +655,38 @@ Examples:
|
|
|
651
655
|
await comparisonsCommand(options, globalOptions);
|
|
652
656
|
});
|
|
653
657
|
let contextCmd = program.command('context').description('Fetch build and diff context').showHelpAfterError('(Run vizzly context --help for available commands)').showSuggestionAfterError();
|
|
654
|
-
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', '
|
|
658
|
+
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', 'Deprecated; use API schema for cloud review.').option('--full', 'Return the full build context instead of compact context').option('--cursor <cursor>', 'Continue compact evidence from an opaque API cursor').option('--include <items>', 'Add raw diff diagnostics to compact context: diffs').addHelpText('after', `
|
|
655
659
|
Examples:
|
|
656
660
|
$ vizzly context build abc123 --source cloud
|
|
657
661
|
$ vizzly context build current --source local
|
|
658
|
-
$ vizzly
|
|
659
|
-
$ vizzly context build
|
|
660
|
-
|
|
661
|
-
|
|
662
|
+
$ vizzly api schema --json
|
|
663
|
+
$ vizzly context build current --source local --json
|
|
664
|
+
|
|
665
|
+
Deprecation:
|
|
666
|
+
--agent will be removed in v0.38.0. Use the API schema for cloud reviews;
|
|
667
|
+
use --json without --agent for local evidence.
|
|
662
668
|
`).action(async (buildId, options) => {
|
|
663
669
|
let globalOptions = getGlobalOptions();
|
|
670
|
+
if (options.agent) warnDeprecatedContextAgentOption();
|
|
664
671
|
const validationErrors = validateContextBuildOptions(options);
|
|
665
672
|
if (validationErrors.length > 0) {
|
|
666
673
|
reportValidationErrors(validationErrors);
|
|
667
674
|
}
|
|
668
675
|
await contextBuildCommand(buildId, options, globalOptions);
|
|
669
676
|
});
|
|
670
|
-
contextCmd.command('comparison').description('Fetch a comparison context bundle').argument('<comparison-id>', 'Comparison ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('--agent', '
|
|
677
|
+
contextCmd.command('comparison').description('Fetch a comparison context bundle').argument('<comparison-id>', 'Comparison ID to fetch context for').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('--agent', 'Deprecated; use API schema for cloud review.').option('--full', 'Return the full comparison context instead of compact context').option('--cursor <cursor>', 'Continue compact history from an opaque API cursor').option('--include <items>', 'Add detail to compact context: diffs').addHelpText('after', `
|
|
671
678
|
Examples:
|
|
672
679
|
$ vizzly context comparison def456 --source cloud
|
|
673
680
|
$ vizzly context comparison def456 --source local
|
|
674
681
|
$ vizzly context comparison def456 --source cloud --json
|
|
675
|
-
$ vizzly
|
|
682
|
+
$ vizzly api schema getComparisonContext --json
|
|
683
|
+
|
|
684
|
+
Deprecation:
|
|
685
|
+
--agent will be removed in v0.38.0. Use the API schema for cloud reviews;
|
|
686
|
+
use --json without --agent for local evidence.
|
|
676
687
|
`).action(async (comparisonId, options) => {
|
|
677
688
|
let globalOptions = getGlobalOptions();
|
|
689
|
+
if (options.agent) warnDeprecatedContextAgentOption();
|
|
678
690
|
const validationErrors = validateContextComparisonOptions(options);
|
|
679
691
|
if (validationErrors.length > 0) {
|
|
680
692
|
reportValidationErrors(validationErrors);
|
|
@@ -738,7 +750,7 @@ Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.
|
|
|
738
750
|
let globalOptions = getGlobalOptions();
|
|
739
751
|
await baselinesCommand(options, globalOptions);
|
|
740
752
|
});
|
|
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
|
|
753
|
+
let api = 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; discover supported requests with api schema', 'GET').option('-d, --data <json>', 'JSON body, @file, or @- for stdin').option('-o, --output <file>', 'Write response bytes to a new file').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
754
|
Examples:
|
|
743
755
|
$ vizzly api /api/sdk/builds # List builds
|
|
744
756
|
$ vizzly api /api/sdk/builds -q limit=5 # With query params
|
|
@@ -746,8 +758,10 @@ Examples:
|
|
|
746
758
|
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Looks good"}'
|
|
747
759
|
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Nice!"}'
|
|
748
760
|
|
|
749
|
-
|
|
750
|
-
|
|
761
|
+
Discover operations: vizzly api schema, then vizzly api schema <operation-id>.
|
|
762
|
+
Use the method, version header, parameters and body described there.
|
|
763
|
+
JSON output places the API payload under data.response. Image downloads use --output.
|
|
764
|
+
Writes are never automatically retried after authentication failures.
|
|
751
765
|
`).action(async (endpoint, options) => {
|
|
752
766
|
let globalOptions = getGlobalOptions();
|
|
753
767
|
|
|
@@ -758,6 +772,17 @@ Most operations have dedicated commands (builds, comparisons, approve, etc.).
|
|
|
758
772
|
}
|
|
759
773
|
await apiCommand(endpoint, options, globalOptions);
|
|
760
774
|
});
|
|
775
|
+
api.command('schema [operation-id]').description('Discover supported API operations and their request/response schemas').option('--full', 'Download full OpenAPI (requires --output)').option('-q, --query <param>', 'Schema view=request, fields, response, or full', (value, previous) => [...(previous || []), value]).option('-o, --output <file>', 'Write the schema to a new file').action(async (operationId, options) => {
|
|
776
|
+
options = {
|
|
777
|
+
...api.opts(),
|
|
778
|
+
...options
|
|
779
|
+
};
|
|
780
|
+
if (options.full && !options.output) {
|
|
781
|
+
reportValidationErrors(['--full requires --output to avoid dumping the entire schema.']);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
await apiSchemaCommand(operationId, options, getGlobalOptions());
|
|
785
|
+
});
|
|
761
786
|
program.command('approve').description('Approve a comparison').argument('<comparison-id>', 'Comparison ID to approve').option('-m, --comment <message>', 'Optional comment explaining the approval').addHelpText('after', `
|
|
762
787
|
Examples:
|
|
763
788
|
$ vizzly approve abc123-def456-7890 # Approve a comparison
|
|
@@ -909,11 +934,11 @@ program.command('whoami').description('Show current authentication status and us
|
|
|
909
934
|
await whoamiCommand(options, globalOptions);
|
|
910
935
|
});
|
|
911
936
|
|
|
912
|
-
// Save
|
|
913
|
-
//
|
|
914
|
-
saveUserPath().catch(() => {});
|
|
937
|
+
// Save PATH for the menubar app before commands read or update credentials
|
|
938
|
+
// in the same config file.
|
|
939
|
+
await saveUserPath().catch(() => {});
|
|
915
940
|
let commandNames = new Set(program.commands.map(command => command.name()));
|
|
916
|
-
let nestedCommandNames = new Map(program.commands.map(command => [command.name(), new Set(command.commands.map(subcommand => subcommand.name()))]));
|
|
941
|
+
let nestedCommandNames = new Map(program.commands.map(command => [command.name(), new Set(command.registeredArguments.length ? [] : command.commands.map(subcommand => subcommand.name()))]));
|
|
917
942
|
let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
|
|
918
943
|
let normalizedGlobals = extractGlobalOptionsFromArgv(normalizedArgv, commandNames);
|
|
919
944
|
output.configure({
|
package/dist/client/index.js
CHANGED
|
@@ -203,9 +203,6 @@ function createSimpleClient(serverUrl, clientOptions = {}) {
|
|
|
203
203
|
async screenshot(name, imageBuffer, options = {}) {
|
|
204
204
|
let normalizedOptions = normalizeScreenshotOptions(options);
|
|
205
205
|
let requestTimeout = normalizedOptions.requestTimeout || DEFAULT_TIMEOUT_MS;
|
|
206
|
-
for (let warning of normalizedOptions.warnings) {
|
|
207
|
-
console.warn(`[vizzly] ${warning.message}`);
|
|
208
|
-
}
|
|
209
206
|
try {
|
|
210
207
|
// If it's a string, assume it's a file path and send directly
|
|
211
208
|
// Otherwise it's a Buffer, so convert to base64
|
|
@@ -220,9 +217,6 @@ function createSimpleClient(serverUrl, clientOptions = {}) {
|
|
|
220
217
|
properties: normalizedOptions.properties
|
|
221
218
|
};
|
|
222
219
|
Object.assign(screenshotData, getScreenshotOptionsPayload(normalizedOptions));
|
|
223
|
-
if (normalizedOptions.warnings.length > 0) {
|
|
224
|
-
screenshotData.warnings = normalizedOptions.warnings;
|
|
225
|
-
}
|
|
226
220
|
let httpStart = Date.now();
|
|
227
221
|
let {
|
|
228
222
|
status,
|
package/dist/commands/api.js
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* API command - raw API access for power users
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { open, readFile, unlink } from 'node:fs/promises';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { Readable } from 'node:stream';
|
|
8
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
9
|
import { createApiClient as defaultCreateApiClient } from '../api/index.js';
|
|
6
10
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
7
11
|
import * as defaultOutput from '../utils/output.js';
|
|
8
|
-
let
|
|
12
|
+
let API_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
9
13
|
function createApiCommandDeps(deps = {}) {
|
|
10
14
|
return {
|
|
11
15
|
loadConfig: deps.loadConfig || defaultLoadConfig,
|
|
@@ -31,9 +35,6 @@ export function normalizeApiEndpoint(endpoint) {
|
|
|
31
35
|
export function normalizeApiMethod(method = 'GET') {
|
|
32
36
|
return method.toUpperCase();
|
|
33
37
|
}
|
|
34
|
-
export function isAllowedPostEndpoint(endpoint) {
|
|
35
|
-
return ALLOWED_POST_ENDPOINTS.some(pattern => pattern.test(endpoint));
|
|
36
|
-
}
|
|
37
38
|
export function parseApiHeaders(headerOption) {
|
|
38
39
|
let headers = {};
|
|
39
40
|
let headerList = Array.isArray(headerOption) ? headerOption : [headerOption];
|
|
@@ -69,15 +70,14 @@ export function validateApiRequest({
|
|
|
69
70
|
hasData = false
|
|
70
71
|
}) {
|
|
71
72
|
let errors = [];
|
|
72
|
-
if (method
|
|
73
|
-
errors.push(`
|
|
74
|
-
return errors;
|
|
73
|
+
if (!API_METHODS.includes(method)) {
|
|
74
|
+
errors.push(`Unsupported HTTP method: ${method}`);
|
|
75
75
|
}
|
|
76
|
-
if (
|
|
77
|
-
errors.push(
|
|
76
|
+
if (hasData && ['GET', 'HEAD'].includes(method)) {
|
|
77
|
+
errors.push('Request data requires a method other than GET or HEAD.');
|
|
78
78
|
}
|
|
79
|
-
if (
|
|
80
|
-
errors.push('
|
|
79
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(endpoint) || endpoint.startsWith('//')) {
|
|
80
|
+
errors.push('Use an API path on the configured Vizzly server.');
|
|
81
81
|
}
|
|
82
82
|
return errors;
|
|
83
83
|
}
|
|
@@ -88,7 +88,7 @@ export function buildApiRequest({
|
|
|
88
88
|
let normalizedEndpoint = normalizeApiEndpoint(endpoint);
|
|
89
89
|
let method = normalizeApiMethod(options.method || 'GET');
|
|
90
90
|
let errors = validateApiRequest({
|
|
91
|
-
endpoint
|
|
91
|
+
endpoint,
|
|
92
92
|
method,
|
|
93
93
|
hasData: options.data !== undefined
|
|
94
94
|
});
|
|
@@ -104,7 +104,7 @@ export function buildApiRequest({
|
|
|
104
104
|
let requestOptions = {
|
|
105
105
|
method
|
|
106
106
|
};
|
|
107
|
-
if (options.data
|
|
107
|
+
if (options.data !== undefined && !['GET', 'HEAD'].includes(method)) {
|
|
108
108
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
|
109
109
|
requestOptions.body = options.data;
|
|
110
110
|
}
|
|
@@ -144,13 +144,24 @@ export async function apiCommand(endpoint, options = {}, globalOptions = {}, dep
|
|
|
144
144
|
};
|
|
145
145
|
let config = await loadConfig(globalOptions.config, allOptions);
|
|
146
146
|
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
// A linked project's upload credential must not hide an existing user login.
|
|
148
|
+
// Explicit file/env/--token credentials still take precedence.
|
|
149
|
+
let token = config.linkedProject ? config.userToken || config.apiKey : config.apiKey || config.userToken;
|
|
150
|
+
if (!token && !options.schemaDiscovery) {
|
|
151
|
+
output.error('Authentication required. Run vizzly login or set VIZZLY_TOKEN.');
|
|
150
152
|
output.cleanup();
|
|
151
153
|
exit(1);
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
156
|
+
if (options.data?.startsWith('@')) {
|
|
157
|
+
let source = options.data.slice(1);
|
|
158
|
+
let data = source === '-' ? await readStdin() : await readFile(source, 'utf8');
|
|
159
|
+
options = {
|
|
160
|
+
...options,
|
|
161
|
+
data
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (options.data !== undefined) JSON.parse(options.data);
|
|
154
165
|
let {
|
|
155
166
|
errors,
|
|
156
167
|
method,
|
|
@@ -164,10 +175,7 @@ export async function apiCommand(endpoint, options = {}, globalOptions = {}, dep
|
|
|
164
175
|
displayMethod = method;
|
|
165
176
|
if (errors.length > 0) {
|
|
166
177
|
output.error(errors[0]);
|
|
167
|
-
|
|
168
|
-
output.hint('Use GET for queries, or use dedicated commands (vizzly approve, vizzly reject, vizzly comment)');
|
|
169
|
-
}
|
|
170
|
-
output.hint('Most raw API use should stay read-only; prefer dedicated commands for mutations.');
|
|
178
|
+
output.hint('Use vizzly api schema to discover supported operations.');
|
|
171
179
|
output.cleanup();
|
|
172
180
|
exit(1);
|
|
173
181
|
return;
|
|
@@ -177,10 +185,31 @@ export async function apiCommand(endpoint, options = {}, globalOptions = {}, dep
|
|
|
177
185
|
output.startSpinner(`${method} ${normalizedEndpoint}`);
|
|
178
186
|
let client = createApiClient({
|
|
179
187
|
baseUrl: config.apiUrl,
|
|
180
|
-
token
|
|
181
|
-
command: 'api'
|
|
188
|
+
token,
|
|
189
|
+
command: 'api',
|
|
190
|
+
allowNoToken: Boolean(options.schemaDiscovery)
|
|
191
|
+
});
|
|
192
|
+
let response = await client.request(normalizedEndpoint, {
|
|
193
|
+
...requestOptions,
|
|
194
|
+
redirect: 'error',
|
|
195
|
+
retryAuthentication: ['GET', 'HEAD'].includes(method),
|
|
196
|
+
responseType: options.output ? 'response' : 'json'
|
|
182
197
|
});
|
|
183
|
-
|
|
198
|
+
if (options.output) {
|
|
199
|
+
let path = resolve(options.output);
|
|
200
|
+
let file = await open(path, 'wx');
|
|
201
|
+
try {
|
|
202
|
+
await pipeline(response.body || Readable.from([]), file.createWriteStream());
|
|
203
|
+
} catch (error) {
|
|
204
|
+
await file.close();
|
|
205
|
+
await unlink(path);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
response = {
|
|
209
|
+
file: path,
|
|
210
|
+
contentType: response.headers.get('content-type')
|
|
211
|
+
};
|
|
212
|
+
}
|
|
184
213
|
output.stopSpinner();
|
|
185
214
|
|
|
186
215
|
// Output response
|
|
@@ -240,12 +269,30 @@ export function validateApiOptions(endpoint, options = {}) {
|
|
|
240
269
|
if (!endpoint || endpoint.trim() === '') {
|
|
241
270
|
return errors;
|
|
242
271
|
}
|
|
243
|
-
let normalizedEndpoint = normalizeApiEndpoint(endpoint);
|
|
244
272
|
let method = normalizeApiMethod(options.method || 'GET');
|
|
245
273
|
errors.push(...validateApiRequest({
|
|
246
|
-
endpoint
|
|
274
|
+
endpoint,
|
|
247
275
|
method,
|
|
248
276
|
hasData: options.data !== undefined
|
|
249
277
|
}));
|
|
250
278
|
return errors;
|
|
279
|
+
}
|
|
280
|
+
async function readStdin() {
|
|
281
|
+
let chunks = [];
|
|
282
|
+
for await (let chunk of process.stdin) chunks.push(chunk);
|
|
283
|
+
return Buffer.concat(chunks.map(chunk => Buffer.from(chunk))).toString('utf8');
|
|
284
|
+
}
|
|
285
|
+
export async function apiSchemaCommand(operationId, options, globalOptions) {
|
|
286
|
+
let endpoint = '/api/sdk/schema';
|
|
287
|
+
if (operationId) {
|
|
288
|
+
endpoint += `/${encodeURIComponent(operationId)}`;
|
|
289
|
+
if (options.full) options = {
|
|
290
|
+
...options,
|
|
291
|
+
query: [...(options.query || []), 'view=full']
|
|
292
|
+
};
|
|
293
|
+
} else if (options.full) endpoint += '/openapi';
|
|
294
|
+
return apiCommand(endpoint, {
|
|
295
|
+
...options,
|
|
296
|
+
schemaDiscovery: true
|
|
297
|
+
}, globalOptions);
|
|
251
298
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -223,8 +223,10 @@ For user-facing changes, use the repo-local Vizzly skill at
|
|
|
223
223
|
|
|
224
224
|
- Inspect existing visual evidence before and after the change.
|
|
225
225
|
- Use the repository's established Vizzly command and owning user workflow.
|
|
226
|
-
-
|
|
227
|
-
diff images
|
|
226
|
+
- For cloud reviews, discover supported requests with
|
|
227
|
+
\`vizzly api schema --json\` and inspect baseline, current, and diff images
|
|
228
|
+
together.
|
|
229
|
+
- For local evidence, use \`vizzly context ... --json\`.
|
|
228
230
|
|
|
229
231
|
Treat Vizzly diffs as review evidence. Do not approve, reject, or replace
|
|
230
232
|
evidence unless the task explicitly asks for that mutation.
|