@vizzly-testing/cli 0.36.0 → 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 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 --agent
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` when you need build, comparison, screenshot, or review
100
- queue data from the terminal.
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
- This is useful for scripts, coding agents, and debugging loops. Instead of
103
- making a pile of narrow API calls, ask for one focused bundle and get the
104
- evidence in one place.
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
- # Cloud context for a build or comparison
112
+ # Human-readable cloud build context
108
113
  vizzly context build abc123 --source cloud
109
- vizzly context comparison def456 --source cloud --agent --json
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 --agent
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
- `--json` is the durable automation path. `--agent` gives you the compact API
119
- handoff for prompt assembly. Build handoffs contain up to 10 records; use the
120
- exact next-page command returned in `suggested_commands` to continue safely.
121
- That command carries the API's opaque `--cursor`. Add
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
- Cloud context is also read-only right now. That is intentional. Vizzly helps you
130
- see and inspect visual changes, while people still decide what gets approved.
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
 
@@ -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
- ...fetchOptions,
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/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', 'Output compact context for LLM agents').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', `
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 context build current --source local --agent
659
- $ vizzly context build abc123 --source cloud --agent --json
660
- $ vizzly context build abc123 --source cloud --agent --json --include diffs
661
- $ vizzly context build abc123 --source cloud --agent --json --full
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', 'Output compact context for LLM agents').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', `
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 context comparison def456 --source cloud --agent --json
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 (GET or POST for build comments)', 'GET').option('-d, --data <json>', 'Request body (JSON)').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', `
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
- Note: POST is restricted to build comment endpoints. Use dedicated approve/reject commands for review decisions.
750
- Most operations have dedicated commands (builds, comparisons, approve, etc.).
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 user's PATH for menubar app (non-blocking, runs in background)
913
- // This auto-configures the menubar app so it can find package runners/node
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({
@@ -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 ALLOWED_POST_ENDPOINTS = [/^\/api\/sdk\/builds\/[^/]+\/comments$/];
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 !== 'GET' && method !== 'POST') {
73
- errors.push(`Method ${method} not allowed. Use GET for queries or POST for build comments.`);
74
- return errors;
73
+ if (!API_METHODS.includes(method)) {
74
+ errors.push(`Unsupported HTTP method: ${method}`);
75
75
  }
76
- if (method === 'POST' && !isAllowedPostEndpoint(endpoint)) {
77
- errors.push(`POST not allowed for ${endpoint}. Only build comment endpoints support POST.`);
76
+ if (hasData && ['GET', 'HEAD'].includes(method)) {
77
+ errors.push('Request data requires a method other than GET or HEAD.');
78
78
  }
79
- if (hasData && method !== 'POST') {
80
- errors.push('Request data requires --method POST.');
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: normalizedEndpoint,
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 && method === 'POST') {
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
- // Validate API token
148
- if (!config.apiKey) {
149
- output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
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
- if (method === 'POST') {
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: config.apiKey,
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
- let response = await client.request(normalizedEndpoint, requestOptions);
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: normalizedEndpoint,
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
  }
@@ -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
- - Read bounded JSON with \`--agent --json\`, then inspect baseline, current, and
227
- diff images together.
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.
@@ -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 need a self-contained command that returns structured
65
- * evidence when executed. Human output keeps the existing readable summary.
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} Executable build context command.
70
+ * @returns {string} Cloud context or schema discovery command.
71
71
  */
72
72
  function buildContextCommand(buildId, {
73
73
  structured = false
74
74
  } = {}) {
75
- let jsonFlag = structured ? ' --json' : '';
76
- return `vizzly context build ${buildId} --agent${jsonFlag} --source cloud`;
75
+ if (structured) return 'vizzly api schema sdk.getBuildContext --json';
76
+ return `vizzly context build ${buildId} --source cloud`;
77
77
  }
78
78
 
79
79
  /**
@@ -94,8 +94,8 @@ export function createStatusSuggestedCommands(build = {}) {
94
94
  return [];
95
95
  }
96
96
  return [{
97
- label: 'Inspect build context',
98
- command: `vizzly --json context build ${build.id} --agent --source cloud`
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}`
@@ -31,7 +31,7 @@ function buildLocalContextCommand({
31
31
  structured = false
32
32
  } = {}) {
33
33
  let jsonFlag = structured ? ' --json' : '';
34
- return `vizzly context build current --source local --agent${jsonFlag}`;
34
+ return `vizzly context build current --source local${jsonFlag}`;
35
35
  }
36
36
 
37
37
  /**