@vizzly-testing/cli 0.36.0 → 0.38.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 +23 -17
- package/dist/api/client.js +14 -2
- package/dist/api/core.js +6 -0
- package/dist/cli.js +54 -15
- package/dist/commands/api.js +72 -25
- package/dist/commands/init.js +4 -2
- package/dist/commands/run.js +10 -6
- package/dist/commands/status.js +2 -2
- package/dist/commands/tdd-screenshots.js +417 -0
- package/dist/commands/tdd.js +6 -2
- package/dist/commands/upload.js +5 -1
- package/dist/reporter/reporter-bundle.iife.js +38 -38
- package/dist/test-runner/core.js +2 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/uploader/core.js +2 -0
- package/dist/utils/ci-env.js +43 -4
- package/dist/utils/git.js +46 -6
- package/package.json +8 -7
- package/skills/vizzly/SKILL.md +17 -9
- package/skills/vizzly/references/cli-context.md +74 -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
|
|
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
|
@@ -126,6 +126,12 @@ export function buildBuildPayload(options) {
|
|
|
126
126
|
if (options.message || options.commit_message) {
|
|
127
127
|
payload.commit_message = options.message || options.commit_message;
|
|
128
128
|
}
|
|
129
|
+
if (options.commit_author_name) {
|
|
130
|
+
payload.commit_author_name = options.commit_author_name;
|
|
131
|
+
}
|
|
132
|
+
if (options.commit_author_email) {
|
|
133
|
+
payload.commit_author_email = options.commit_author_email;
|
|
134
|
+
}
|
|
129
135
|
if (options.pullRequestNumber || options.github_pull_request_number) {
|
|
130
136
|
payload.github_pull_request_number = options.pullRequestNumber || options.github_pull_request_number;
|
|
131
137
|
}
|
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';
|
|
@@ -22,6 +22,7 @@ import { runCommand, validateRunOptions } from './commands/run.js';
|
|
|
22
22
|
import { statusCommand, validateStatusOptions } from './commands/status.js';
|
|
23
23
|
import { tddCommand, validateTddOptions } from './commands/tdd.js';
|
|
24
24
|
import { runDaemonChild, tddListCommand, tddStartCommand, tddStatusCommand, tddStopCommand, validateTddStartOptions } from './commands/tdd-daemon.js';
|
|
25
|
+
import { acceptTddScreenshot, latestTddScreenshot, listTddScreenshots, showTddScreenshot } from './commands/tdd-screenshots.js';
|
|
25
26
|
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
26
27
|
import { whoamiCommand } from './commands/whoami.js';
|
|
27
28
|
import { createPluginServices } from './plugin-api.js';
|
|
@@ -357,6 +358,10 @@ function getGlobalOptions() {
|
|
|
357
358
|
noColor: options.noColor || options.color === false
|
|
358
359
|
};
|
|
359
360
|
}
|
|
361
|
+
function warnDeprecatedContextAgentOption() {
|
|
362
|
+
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(' ');
|
|
363
|
+
output.warn(warning);
|
|
364
|
+
}
|
|
360
365
|
function reportValidationErrors(errors) {
|
|
361
366
|
if (output.isJson()) {
|
|
362
367
|
output.error('Validation errors', null, {
|
|
@@ -524,6 +529,19 @@ tddCmd.command('list').description('List all running TDD servers').action(async
|
|
|
524
529
|
let globalOptions = getGlobalOptions();
|
|
525
530
|
await tddListCommand(options, globalOptions);
|
|
526
531
|
});
|
|
532
|
+
let tddScreenshotsCmd = tddCmd.command('screenshots').description('Browse local TDD screenshots without a server');
|
|
533
|
+
tddScreenshotsCmd.command('list').description('List captured screenshots from the latest local TDD run').option('--page <number>', 'Page number', Number, 1).option('--page-size <number>', 'Screenshots per page (max 100)', Number, 20).option('--status <status>', 'Filter by comparison status').action(options => {
|
|
534
|
+
listTddScreenshots(options);
|
|
535
|
+
});
|
|
536
|
+
tddScreenshotsCmd.command('latest [name]').description('Print the latest captured screenshot path').action(name => {
|
|
537
|
+
latestTddScreenshot(name);
|
|
538
|
+
});
|
|
539
|
+
tddScreenshotsCmd.command('show <id-or-name>').description('Inspect a screenshot; print current image by default').option('--image <kind>', 'Print image path: current, baseline, diff, or all', 'current').action((idOrName, options) => {
|
|
540
|
+
showTddScreenshot(idOrName, options);
|
|
541
|
+
});
|
|
542
|
+
tddScreenshotsCmd.command('accept <id-or-name>').description('Accept a captured screenshot as its new local baseline').action(async idOrName => {
|
|
543
|
+
await acceptTddScreenshot(idOrName);
|
|
544
|
+
});
|
|
527
545
|
|
|
528
546
|
// TDD Run - One-off test run with ephemeral server (generates static report)
|
|
529
547
|
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual review').option('--port <port>', 'Port for TDD server').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').option('--fail-on-diff', 'Fail tests when visual differences are detected').option('--no-open', 'Skip opening report in browser').action(async (command, options) => {
|
|
@@ -651,30 +669,38 @@ Examples:
|
|
|
651
669
|
await comparisonsCommand(options, globalOptions);
|
|
652
670
|
});
|
|
653
671
|
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', '
|
|
672
|
+
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
673
|
Examples:
|
|
656
674
|
$ vizzly context build abc123 --source cloud
|
|
657
675
|
$ vizzly context build current --source local
|
|
658
|
-
$ vizzly
|
|
659
|
-
$ vizzly context build
|
|
660
|
-
|
|
661
|
-
|
|
676
|
+
$ vizzly api schema --json
|
|
677
|
+
$ vizzly context build current --source local --json
|
|
678
|
+
|
|
679
|
+
Deprecation:
|
|
680
|
+
--agent will be removed in v0.38.0. Use the API schema for cloud reviews;
|
|
681
|
+
use --json without --agent for local evidence.
|
|
662
682
|
`).action(async (buildId, options) => {
|
|
663
683
|
let globalOptions = getGlobalOptions();
|
|
684
|
+
if (options.agent) warnDeprecatedContextAgentOption();
|
|
664
685
|
const validationErrors = validateContextBuildOptions(options);
|
|
665
686
|
if (validationErrors.length > 0) {
|
|
666
687
|
reportValidationErrors(validationErrors);
|
|
667
688
|
}
|
|
668
689
|
await contextBuildCommand(buildId, options, globalOptions);
|
|
669
690
|
});
|
|
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', '
|
|
691
|
+
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
692
|
Examples:
|
|
672
693
|
$ vizzly context comparison def456 --source cloud
|
|
673
694
|
$ vizzly context comparison def456 --source local
|
|
674
695
|
$ vizzly context comparison def456 --source cloud --json
|
|
675
|
-
$ vizzly
|
|
696
|
+
$ vizzly api schema getComparisonContext --json
|
|
697
|
+
|
|
698
|
+
Deprecation:
|
|
699
|
+
--agent will be removed in v0.38.0. Use the API schema for cloud reviews;
|
|
700
|
+
use --json without --agent for local evidence.
|
|
676
701
|
`).action(async (comparisonId, options) => {
|
|
677
702
|
let globalOptions = getGlobalOptions();
|
|
703
|
+
if (options.agent) warnDeprecatedContextAgentOption();
|
|
678
704
|
const validationErrors = validateContextComparisonOptions(options);
|
|
679
705
|
if (validationErrors.length > 0) {
|
|
680
706
|
reportValidationErrors(validationErrors);
|
|
@@ -738,7 +764,7 @@ Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.
|
|
|
738
764
|
let globalOptions = getGlobalOptions();
|
|
739
765
|
await baselinesCommand(options, globalOptions);
|
|
740
766
|
});
|
|
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
|
|
767
|
+
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
768
|
Examples:
|
|
743
769
|
$ vizzly api /api/sdk/builds # List builds
|
|
744
770
|
$ vizzly api /api/sdk/builds -q limit=5 # With query params
|
|
@@ -746,8 +772,10 @@ Examples:
|
|
|
746
772
|
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Looks good"}'
|
|
747
773
|
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Nice!"}'
|
|
748
774
|
|
|
749
|
-
|
|
750
|
-
|
|
775
|
+
Discover operations: vizzly api schema, then vizzly api schema <operation-id>.
|
|
776
|
+
Use the method, version header, parameters and body described there.
|
|
777
|
+
JSON output places the API payload under data.response. Image downloads use --output.
|
|
778
|
+
Writes are never automatically retried after authentication failures.
|
|
751
779
|
`).action(async (endpoint, options) => {
|
|
752
780
|
let globalOptions = getGlobalOptions();
|
|
753
781
|
|
|
@@ -758,6 +786,17 @@ Most operations have dedicated commands (builds, comparisons, approve, etc.).
|
|
|
758
786
|
}
|
|
759
787
|
await apiCommand(endpoint, options, globalOptions);
|
|
760
788
|
});
|
|
789
|
+
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) => {
|
|
790
|
+
options = {
|
|
791
|
+
...api.opts(),
|
|
792
|
+
...options
|
|
793
|
+
};
|
|
794
|
+
if (options.full && !options.output) {
|
|
795
|
+
reportValidationErrors(['--full requires --output to avoid dumping the entire schema.']);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
await apiSchemaCommand(operationId, options, getGlobalOptions());
|
|
799
|
+
});
|
|
761
800
|
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
801
|
Examples:
|
|
763
802
|
$ vizzly approve abc123-def456-7890 # Approve a comparison
|
|
@@ -909,11 +948,11 @@ program.command('whoami').description('Show current authentication status and us
|
|
|
909
948
|
await whoamiCommand(options, globalOptions);
|
|
910
949
|
});
|
|
911
950
|
|
|
912
|
-
// Save
|
|
913
|
-
//
|
|
914
|
-
saveUserPath().catch(() => {});
|
|
951
|
+
// Save PATH for the menubar app before commands read or update credentials
|
|
952
|
+
// in the same config file.
|
|
953
|
+
await saveUserPath().catch(() => {});
|
|
915
954
|
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()))]));
|
|
955
|
+
let nestedCommandNames = new Map(program.commands.map(command => [command.name(), new Set(command.registeredArguments.length ? [] : command.commands.map(subcommand => subcommand.name()))]));
|
|
917
956
|
let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
|
|
918
957
|
let normalizedGlobals = extractGlobalOptionsFromArgv(normalizedArgv, commandNames);
|
|
919
958
|
output.configure({
|
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.
|
package/dist/commands/run.js
CHANGED
|
@@ -18,7 +18,7 @@ import { finalizeBuild as defaultFinalizeBuild, runTests as defaultRunTests } fr
|
|
|
18
18
|
import { createUploader as defaultCreateUploader } from '../uploader/index.js';
|
|
19
19
|
import { getAppBaseUrl } from '../utils/api-url.js';
|
|
20
20
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
21
|
-
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
21
|
+
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitAuthor as defaultDetectCommitAuthor, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
22
22
|
import * as defaultOutput from '../utils/output.js';
|
|
23
23
|
import { writeSession as defaultWriteSession } from '../utils/session.js';
|
|
24
24
|
function formatUploadCounts({
|
|
@@ -61,19 +61,19 @@ export async function resolveBuildDisplayUrl({
|
|
|
61
61
|
/**
|
|
62
62
|
* Build the follow-up command for a cloud run.
|
|
63
63
|
*
|
|
64
|
-
* JSON consumers
|
|
65
|
-
*
|
|
64
|
+
* JSON consumers start with the public schema so they can discover the current
|
|
65
|
+
* request shape. Human output keeps the readable context summary.
|
|
66
66
|
*
|
|
67
67
|
* @param {string} buildId - Cloud build ID to inspect.
|
|
68
68
|
* @param {Object} options - Command output options.
|
|
69
69
|
* @param {boolean} [options.structured=false] - Include machine-readable JSON.
|
|
70
|
-
* @returns {string}
|
|
70
|
+
* @returns {string} Cloud context or schema discovery command.
|
|
71
71
|
*/
|
|
72
72
|
function buildContextCommand(buildId, {
|
|
73
73
|
structured = false
|
|
74
74
|
} = {}) {
|
|
75
|
-
|
|
76
|
-
return `vizzly context build ${buildId} --
|
|
75
|
+
if (structured) return 'vizzly api schema sdk.getBuildContext --json';
|
|
76
|
+
return `vizzly context build ${buildId} --source cloud`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
@@ -100,6 +100,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
100
100
|
runTests = defaultRunTests,
|
|
101
101
|
detectBranch = defaultDetectBranch,
|
|
102
102
|
detectCommit = defaultDetectCommit,
|
|
103
|
+
detectCommitAuthor = defaultDetectCommitAuthor,
|
|
103
104
|
detectCommitMessage = defaultDetectCommitMessage,
|
|
104
105
|
detectPullRequestNumber = defaultDetectPullRequestNumber,
|
|
105
106
|
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
|
|
@@ -210,6 +211,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
210
211
|
let branch = await detectBranch(options.branch || config.build.branch);
|
|
211
212
|
let commit = await detectCommit(options.commit || config.build.commit);
|
|
212
213
|
let message = options.message || config.build.message || (await detectCommitMessage());
|
|
214
|
+
let commitAuthor = await detectCommitAuthor();
|
|
213
215
|
let buildName = await generateBuildNameWithGit(options.buildName || configuredBuildName);
|
|
214
216
|
let pullRequestNumber = detectPullRequestNumber();
|
|
215
217
|
if (globalOptions.verbose) {
|
|
@@ -269,6 +271,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
269
271
|
branch,
|
|
270
272
|
commit,
|
|
271
273
|
message,
|
|
274
|
+
commitAuthorName: commitAuthor.name,
|
|
275
|
+
commitAuthorEmail: commitAuthor.email,
|
|
272
276
|
environment: config.build.environment,
|
|
273
277
|
threshold: config.comparison.threshold,
|
|
274
278
|
minClusterSize: config.comparison.minClusterSize,
|
package/dist/commands/status.js
CHANGED
|
@@ -94,8 +94,8 @@ export function createStatusSuggestedCommands(build = {}) {
|
|
|
94
94
|
return [];
|
|
95
95
|
}
|
|
96
96
|
return [{
|
|
97
|
-
label: '
|
|
98
|
-
command:
|
|
97
|
+
label: 'Discover build context API',
|
|
98
|
+
command: 'vizzly api schema sdk.getBuildContext --json'
|
|
99
99
|
}, {
|
|
100
100
|
label: 'List comparisons',
|
|
101
101
|
command: `vizzly --json comparisons --build ${build.id}`
|