Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion skills/vizzly/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ test workflow in charge of how the UI is exercised.

## Inspect And Verify

For cloud API queries, selectable fields, or an authorized review decision,
start with `vizzly api schema --json` and follow
[schema discovery](references/cli-context.md#query-the-cloud-api).
Discover request details as needed instead of loading the entire OpenAPI document.
The `context` commands below remain useful for local evidence and guided inspection.

1. Choose the supplied cloud build or comparison when one is named. Otherwise,
use current local evidence or find the relevant cloud build.
2. Request bounded JSON:
Expand Down Expand Up @@ -57,7 +63,7 @@ test workflow in charge of how the UI is exercised.
## Load A Reference When Needed

- [CLI context](references/cli-context.md): local and cloud evidence, build
discovery, drill-downs, images, and TDD lifecycle.
discovery, schema queries, review decisions, images, and TDD lifecycle.
- [SDK capture](references/sdks.md): add or change screenshot capture code.
- [Dynamic content](references/dynamic-content.md): investigate unstable
content and screenshot-specific tolerances.
Expand Down
35 changes: 35 additions & 0 deletions skills/vizzly/references/cli-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,41 @@ Use the repository's established CLI invocation and existing authentication.
If cloud authentication is unavailable, report the blocker. Do not start an
interactive login unless setup is in scope.

## Query The Cloud API

Discover the live review API instead of guessing endpoints or adding command flags:

```bash
vizzly api schema --json
vizzly api schema <operation-id> --json
vizzly api schema <operation-id> -q view=fields --json
vizzly api schema <operation-id> -q view=response --json
```

The index lists available operations. The default operation view describes the
method, path, parameters, authentication, and example arguments. Request field
choices or response types only when needed. API JSON payloads are under
`data.response`.

Call the discovered path using `vizzly api <path>`, `-X` for its method, `-H`
for headers, and `-q` for query parameters. Send the discovered API version
header on data requests. Use `fields` to select just the evidence needed.
Follow the response's pagination values explicitly; the CLI fetches one page
per request. Keep cursors opaque and preserve the query they belong to.

For a review, discover projects/builds, then inspect the build's screenshots,
comparisons, and image endpoints. Download images with `--output <new-file>`
and view baseline, current, and diff together. Use file output for large analysis
responses too. Existing files are not overwritten. Export the full schema only
when needed: `vizzly api schema --full --output <new-file> --json`.

Review decisions require an authorized task, user credentials, and the documented
organization header. Discover the decision operation's body before sending it
with `-d @<file>` or `-d @-` for stdin. Generate a fresh `commandId` for each
intended decision, then read back the review state. Generic writes are not
automatically replayed; after an uncertain result, inspect state before retrying.
Treat schema examples as argument arrays, not shell scripts.

## Choose The Evidence

Use an ID supplied by the task. If no cloud build is supplied, list recent
Expand Down
21 changes: 20 additions & 1 deletion src/api/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export function createApiClient(options = {}) {
* @returns {Promise<Object>} Parsed JSON response
*/
async function request(endpoint, fetchOptions = {}, isRetry = false) {
let {
responseType = 'json',
retryAuthentication = true,
...httpOptions
} = fetchOptions;
let url = buildApiUrl(baseUrl, endpoint);

let headers = buildRequestHeaders({
Expand All @@ -88,7 +93,7 @@ export function createApiClient(options = {}) {
let response;
try {
response = await fetch(url, {
...fetchOptions,
...httpOptions,
headers,
});
} catch (error) {
Expand All @@ -106,6 +111,7 @@ export function createApiClient(options = {}) {

// Handle 401 with token refresh
if (
retryAuthentication &&
shouldRetryWithRefresh(
response.status,
isRetry,
Expand Down Expand Up @@ -134,6 +140,18 @@ export function createApiClient(options = {}) {
});
}

if (responseType === 'response') return response;
if (response.status === 204 || httpOptions.method === 'HEAD') return null;
let contentType = response.headers?.get?.('content-type');
if (
contentType &&
!/application\/(?:[\w.+-]+\+)?json\b/i.test(contentType)
) {
throw new VizzlyError(
'This response contains file data. Use api --output <file> to download it.',
'BINARY_RESPONSE'
);
}
return response.json();
}

Expand All @@ -157,6 +175,7 @@ export function createApiClient(options = {}) {
let refreshUrl = buildApiUrl(baseUrl, '/api/auth/cli/refresh');
let response = await fetch(refreshUrl, {
method: 'POST',
redirect: 'error',
headers: {
'Content-Type': 'application/json',
'User-Agent': userAgent,
Expand Down
54 changes: 44 additions & 10 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
import 'dotenv/config';
import { existsSync, statSync } from 'node:fs';
import { Option, program } from 'commander';
import { apiCommand, validateApiOptions } from './commands/api.js';
import {
apiCommand,
apiSchemaCommand,
validateApiOptions,
} from './commands/api.js';
import { baselinesCommand } from './commands/baselines.js';
import { buildsCommand, validateBuildsOptions } from './commands/builds.js';
import {
Expand Down Expand Up @@ -1170,16 +1174,17 @@ Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.
await baselinesCommand(options, globalOptions);
});

program
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 (GET or POST for build comments)',
'HTTP method; discover supported requests with api schema',
'GET'
)
.option('-d, --data <json>', 'Request body (JSON)')
.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',
Expand All @@ -1200,8 +1205,10 @@ Examples:
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Looks good"}'
$ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Nice!"}'

Note: POST is restricted to build comment endpoints. Use dedicated approve/reject commands for review decisions.
Most operations have dedicated commands (builds, comparisons, approve, etc.).
Discover operations: vizzly api schema, then vizzly api schema <operation-id>.
Use the method, version header, parameters and body described there.
JSON output places the API payload under data.response. Image downloads use --output.
Writes are never automatically retried after authentication failures.
`
)
.action(async (endpoint, options) => {
Expand All @@ -1216,6 +1223,29 @@ Most operations have dedicated commands (builds, comparisons, approve, etc.).
await apiCommand(endpoint, options, globalOptions);
});

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) => {
options = { ...api.opts(), ...options };
if (options.full && !options.output) {
reportValidationErrors([
'--full requires --output to avoid dumping the entire schema.',
]);
return;
}
await apiSchemaCommand(operationId, options, getGlobalOptions());
});

program
.command('approve')
.description('Approve a comparison')
Expand Down Expand Up @@ -1502,15 +1532,19 @@ program
await whoamiCommand(options, globalOptions);
});

// Save user's PATH for menubar app (non-blocking, runs in background)
// This auto-configures the menubar app so it can find package runners/node
saveUserPath().catch(() => {});
// Save PATH for the menubar app before commands read or update credentials
// in the same config file.
await saveUserPath().catch(() => {});

let commandNames = new Set(program.commands.map(command => command.name()));
let nestedCommandNames = new Map(
program.commands.map(command => [
command.name(),
new Set(command.commands.map(subcommand => subcommand.name())),
new Set(
command.registeredArguments.length
? []
: command.commands.map(subcommand => subcommand.name())
),
])
);
let normalizedArgv = normalizeJsonArgv(process.argv, commandNames);
Expand Down
Loading
Loading