Skip to content

feat: add sanity api command for direct HTTP API access#1561

Open
runeb wants to merge 14 commits into
mainfrom
claude/aigro-5218-solution-plan-k4pdb6
Open

feat: add sanity api command for direct HTTP API access#1561
runeb wants to merge 14 commits into
mainfrom
claude/aigro-5218-solution-plan-k4pdb6

Conversation

@runeb

@runeb runeb commented Jul 17, 2026

Copy link
Copy Markdown
Member

Description

Adds a new top-level command, sanity api <endpoint>, modeled on the gh api pattern: direct, bearer-authenticated HTTP access to every Sanity API documented in the published OpenAPI specifications. It is the fallback for humans and agents whenever a tailored command group doesn't exist yet — the CLI takes care of auth, host selection and API versioning, the user supplies the path.

Closes AIGRO-5218.

How it works with the OpenAPI specs. The set of reachable APIs is driven by the published specs, not hand-maintained code: a routing manifest (src/generated/apiRoutes.ts) is distilled from https://www.sanity.io/docs/api/openapi (the same source as sanity openapi list|get). Per API family it records the host (api.sanity.io vs <projectId>.api.sanity.io), the default API version, and the path patterns. Endpoint paths copied verbatim from sanity openapi get <slug> just work: {projectId}/{dataset} placeholders are filled from flags or CLI config, and the request is routed to the right host with a sensible version. Route matching is deterministic: a pattern only matches when the request path consumes it completely, literal segment matches outrank placeholder matches, the global host wins host-ambiguous ties (it needs no project ID), and same-host ties resolve to the first manifest entry.

Regeneration runs on every build (prebuild hook → pnpm generate:api-routes), so every build — including release builds — compiles a manifest freshly distilled from whatever is published at that moment. The generator retries transient failures, fetches specs concurrently, and fails the build when the spec index is unreachable, any listed spec fails to fetch, or the distilled manifest would be empty — a release can never ship with a missing or empty endpoint list. --check mode additionally lets CI detect drift of the committed snapshot. Proxy-related environment variables (HTTPS_PROXY, NODE_EXTRA_CA_CERTS, …) pass through turbo to the build so the spec fetch works behind TLS-intercepting proxies. Build-time generation was chosen over runtime fetching deliberately: routing info is tiny and degrades gracefully (flags and full URLs always override), runtime fetching would couple every cold CLI call to the docs site's availability plus ~24 spec fetches per cache miss, and agents/CI want deterministic per-version behavior. This matches how stripe/aws/twilio vendor specs into releases. Possible follow-up: an on-miss runtime lookup with a disk cache for APIs published between releases.

Usage examples

sanity api users/me
sanity api projects/{projectId}
sanity api 'data/query/{dataset}' -f query='*[_type == "movie"][0..2]'
sanity api projects/{projectId} -X PATCH -F displayName="My project"
echo '{"mutations": []}' | sanity api 'data/mutate/{dataset}' --input - -H 'Content-Type: application/json'
sanity api jobs/123 --include --api-version v2025-02-19

Behavior (gh-api parity where it makes sense)

  • -f/--raw-field (string) and -F/--field (typed: true/false/null/numbers, @file, @- for stdin) with gh's nested-key semantics: a[b]=1 builds objects, list[]=x appends to arrays, consecutive key[][sub]=… fields fill the current array element until a sub-key repeats (so arrays of multi-key objects are expressible), and a bare key[] declares an empty array
  • --input <file|-> for raw bodies, read as raw bytes so binary uploads (eg assets) arrive byte-for-byte
  • Method defaults to GET, or POST when fields/--input are present; for GET/HEAD, fields become query parameters
  • Bodies built from fields are sent with Content-Type: application/json; raw --input bodies are sent without a default Content-Type — provide one with -H when the API requires it (gh api parity)
  • Response body passes through to stdout (pretty-printed JSON; --pretty colorizes; non-JSON verbatim); --include prints status + headers
  • HTTP errors print the error body and exit 1 (with a sanity login hint on 401); usage errors — bad fields, unresolved placeholders, empty or version-only paths — exit 2, per the repo exit-code convention
  • --global / --project-hosted force a host; full URLs are accepted only when they are https:// and on a Sanity API host, so the CLI token is never sent to other hosts or over unencrypted connections
  • Auth, API host selection and URL construction reuse the existing CLI client factories; --token (or SANITY_AUTH_TOKEN) substitutes an explicit token and --anonymous sends no token; the request itself uses a bare get-it requester so status, headers and non-JSON bodies pass through unfiltered

What to review

The diff is ~2,700 additions, but only ~800 lines need close review. Where the lines are:

Bucket Lines Review posture
Hand-written core logic ~800 Read closely (see priorities below)
Unit tests (4 files) ~950 Skim for coverage gaps
Generated routing manifest (src/generated/apiRoutes.ts) ~400 Don't review line-by-line — it's build output, rewritten on every build; review its generator instead
commands/api.ts declarative surface (flag definitions, help text, examples) ~220 Skim for naming/UX
Generated README docs (pnpm readme) ~100 Skip
Generation script plumbing (fetch/retry/rendering) ~150 Skim
Changeset, package.json, turbo.json, oxfmt ignore ~20 Glance

Review priorities, highest first:

  1. src/actions/api/resolveEndpoint.ts — the security- and correctness-critical piece: full-URL constraints (https-only, Sanity API host allowlist — the CLI token must never leave that boundary), placeholder substitution, deterministic host matching via segment scoring and tie-breaks, API version precedence (--api-version > version embedded in path > matched spec default > fallback constant)
  2. src/services/api.ts — token handling (--token wins over the stored login token; user-supplied Authorization header wins over both, case-insensitively; --anonymous drops the token), header assembly, body serialization (field objects → JSON with JSON content-type, strings/Buffers verbatim without one)
  3. src/actions/api/parseFields.ts — gh-style field parsing/typing with gh's array-element grouping semantics
  4. src/commands/api.ts run()/performRequest() — method inference, fields→query vs body, exit-code mapping
  5. src/actions/api/distillApiRoutes.ts + scripts/generate-api-routes.ts — what the manifest is built from; runs on every build via prebuild and fails the build on fetch errors or an empty manifest

Note for local development: pnpm build:cli requires network access to www.sanity.io (by design — see above).

Known limitations (intentional for v1, called out for follow-up): streaming endpoints (data/listen, large exports) are buffered; no --jq/--paginate.

Testing

Unit tests. 90 tests cover field parsing (typing, gh array-element grouping and empty arrays, @file/stdin, error cases), endpoint resolution (routing, tie-breaks, full-pattern-consumption matching, placeholders including datasets/{name}, version precedence, https/host allowlisting, empty-path rejection), spec distillation (hosts, base paths, embedded versions, query-marker stripping, determinism), and command behavior via testCommand + nock against both hosts (auth header presence/absence including --token and case-insensitive -H overrides, POST bodies and Content-Type defaults, byte-for-byte binary bodies, query fields, stdin input, JSON-string response printing, error passthrough and exit codes). Both prebuild outcomes are verified: a successful build regenerates the manifest, and a build with the spec endpoint unreachable fails with exit 1 after retries.

Live smoke tests (local builds against a non-production environment): ✅ works as designed. Smoke-test passes drove the command live across all 24 sanity openapi list families on test projects:

  • Host routing correct for all three spec shapes: global-hosted, project-hosted (verified via debug traces of the resolved URLs), and version-in-path families (auto-injects the matched spec's default version)
  • gh parity confirmed: method defaults, fields→query on GET, typed -F conversion, @file/@- expansion (with -f staying literal and @-content not re-typed — both matching gh), --input <file> and --input -
  • Binary fidelity: a 272-byte fixture containing invalid-UTF-8 sequences and all 256 byte values uploads with a matching sha256 via both --input <file> and stdin
  • Write lifecycle end-to-end: documents (mutations + Actions API), dataset create, CORS, webhook, schedule, custom role, asset upload, dataset copy → async job polled to completed, agent-action prompt returning a real AI response — all created, read back, and deleted
  • Error semantics: 4xx bodies pass through verbatim with exit 1 (401 login hint shown, suppressed under --anonymous); usage errors and URL-constraint rejections exit 2; feature-gated endpoints surface the API's own error body — the command does no client-side plan/permission checks, by design

A follow-up smoke pass on the current head verified the review-round changes live: gh array-element grouping (one element per repeated key, bare key[][], scalar appends intact), projects/{projectId}/datasets/{name} resolving from the dataset, prefix paths (data) falling back to the global host while full paths still route project-hosted, no default Content-Type on --input with field-built bodies still marked application/json, case-insensitive -H overrides replacing defaults without duplicates, --token behavior (used verbatim, exclusive with --anonymous), repeated query keys parsed into arrays with __proto__ handled safely, and an --input-driven document write lifecycle end-to-end. Notably, the mutate endpoint accepted a piped JSON body without an explicit Content-Type — so the plain --input - flow keeps working even without -H, which stays recommended only for endpoints that are strict about media types.

Notes for release

Added a new sanity api <endpoint> command: make authenticated HTTP requests to any Sanity API documented in the published OpenAPI specs, with {projectId}/{dataset} placeholder substitution, gh-style field flags, --token support, and automatic host and API-version routing. Discover available APIs with sanity openapi list.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @sanity/cli

Compared against main (b00bd52f)

@sanity/cli

Metric Value vs main (b00bd52)
Internal (raw) 2.2 KB -
Internal (gzip) 838 B -
Bundled (raw) 11.20 MB -
Bundled (gzip) 2.11 MB -
Import time 875ms -19ms, -2.1%

bin:sanity

Metric Value vs main (b00bd52)
Internal (raw) 782 B -
Internal (gzip) 423 B -
Bundled (raw) 9.90 MB -
Bundled (gzip) 1.78 MB -
Import time 2.14s -21ms, -1.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against main (b00bd52f)

Metric Value vs main (b00bd52)
Internal (raw) 114.1 KB -
Internal (gzip) 29.2 KB -
Bundled (raw) 21.76 MB -
Bundled (gzip) 3.46 MB -
Import time 789ms +16ms, +2.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-build

Compared against main (b00bd52f)

@sanity/cli-build/_internal/build

Metric Value vs main (b00bd52)
Internal (raw) 113.8 KB -
Internal (gzip) 28.7 KB -
Bundled (raw) 17.76 MB -
Bundled (gzip) 3.56 MB -
Import time 1.12s +0ms, +0.0%

@sanity/cli-build/_internal/env

Metric Value vs main (b00bd52)
Internal (raw) 1.8 KB -
Internal (gzip) 644 B -
Bundled (raw) 1.31 MB -
Bundled (gzip) 333.8 KB -
Import time 121ms -1ms, -1.0%

@sanity/cli-build/_internal/extract

Metric Value vs main (b00bd52)
Internal (raw) 8.6 KB -
Internal (gzip) 2.7 KB -
Bundled (raw) 155.0 KB -
Bundled (gzip) 39.5 KB -
Import time 242ms -3ms, -1.1%

🗺️ ./_internal/env · ./_internal/extract · @sanity/cli-build:./_internal/build treemap too large to embed · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — create-sanity

Compared against main (b00bd52f)

Metric Value vs main (b00bd52)
Internal (raw) 908 B -
Internal (gzip) 483 B -
Bundled (raw) 931 B -
Bundled (gzip) 491 B -
Import time ❌ ChildProcess denied: node -
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Coverage Delta

File Statements
packages/@sanity/cli/src/actions/api/constants.ts 100.0% (new)
packages/@sanity/cli/src/actions/api/distillApiRoutes.ts 96.4% (new)
packages/@sanity/cli/src/actions/api/errors.ts 100.0% (new)
packages/@sanity/cli/src/actions/api/parseFields.ts 98.0% (new)
packages/@sanity/cli/src/actions/api/resolveEndpoint.ts 98.6% (new)
packages/@sanity/cli/src/commands/api.ts 84.9% (new)
packages/@sanity/cli/src/generated/apiRoutes.ts 100.0% (new)
packages/@sanity/cli/src/services/api.ts 93.8% (new)

Comparing 8 changed files against main @ b00bd52f5068b4f59e52c45945d9dc0a200b9f6e

Overall Coverage

Metric Coverage
Statements 78.6% (+ 0.4%)
Branches 70.4% (+ 0.8%)
Functions 74.4% (+ 0.3%)
Lines 79.1% (+ 0.4%)

@runeb
runeb marked this pull request as ready for review July 17, 2026 20:36
@runeb
runeb requested a review from a team as a code owner July 17, 2026 20:36
@runeb

runeb commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

bugbots comment

Minor doc/help tweaks for cors add (-y) and tokens add role text.

seems related to the merge commit bringing the PR up to date with main , not this PRs changes

Edit: rebased instead

runeb pushed a commit that referenced this pull request Jul 18, 2026
@runeb
runeb force-pushed the claude/aigro-5218-solution-plan-k4pdb6 branch from a4280c8 to bd4ac50 Compare July 18, 2026 01:20
Comment thread packages/@sanity/cli/src/actions/api/distillApiRoutes.ts
Comment thread packages/@sanity/cli/src/commands/api.ts Outdated
@runeb
runeb requested a review from jonahsnider July 20, 2026 15:16
Comment thread packages/@sanity/cli/src/actions/api/resolveEndpoint.ts Outdated
Comment thread packages/@sanity/cli/src/services/api.ts
runeb pushed a commit that referenced this pull request Jul 21, 2026
@runeb
runeb force-pushed the claude/aigro-5218-solution-plan-k4pdb6 branch from 5f0b65b to b2f1889 Compare July 21, 2026 17:03
Comment thread packages/@sanity/cli/src/actions/api/resolveEndpoint.ts
Comment thread packages/@sanity/cli/src/actions/api/resolveEndpoint.ts
Comment thread packages/@sanity/cli/src/actions/api/parseFields.ts Outdated
Comment thread packages/@sanity/cli/src/actions/api/resolveEndpoint.ts Outdated
Comment thread packages/@sanity/cli/src/actions/api/resolveEndpoint.ts
Comment thread packages/@sanity/cli/src/commands/api.ts
Comment thread packages/@sanity/cli/src/actions/api/parseFields.ts Outdated

@jonahsnider jonahsnider left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally looks good, i have a few questions + change requests, as well as a handful of nitpicky things

Comment thread packages/@sanity/cli/README.md Outdated
- [`sanity telemetry enable`](#sanity-telemetry-enable)
- [`sanity telemetry status`](#sanity-telemetry-status)
- [`sanity tokens create [LABEL]`](#sanity-tokens-create-label)
- [`sanity tokens add [LABEL]`](#sanity-tokens-add-label)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a bad rebase, if you rerun pnpm readme from this project it'll regenerate

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regenerated in cba4ac8 (via manifest:generate + pnpm readme — the committed README had drifted because main's tokens addtokens create rename (#1566) landed after this branch's last regen). The fresh regen syncs that along with the updated sanity api help.


Generated by Claude Code

Comment on lines +204 to +206
Authenticate with a specific token instead of the logged-in session

SANITY_AUTH_TOKEN=<token> sanity api users/me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this command also allow --token?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 43551fc: --token/-t (exclusive with --anonymous), threaded through the CLI client factories so it takes precedence over the stored login token — same pattern as datasets import --token. Covered by a test.


Generated by Claude Code

import {type ApiRouteEntry, type OpenApiDocument} from '../src/actions/api/types.ts'

const OUTPUT_PATH = join(
dirname(fileURLToPath(import.meta.url)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: import.meta.dirname
can be used here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 43551fc.


Generated by Claude Code

}

async function main(): Promise<void> {
const checkOnly = process.argv.includes('--check')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you could use node:util parseArgs for this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 43551fc.


Generated by Claude Code

Comment on lines +148 to +153
try {
await main()
} catch (error) {
console.error(error)
process.exitCode = 1
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this script can use top-level await instead of wrapping everything in main(), for simplicity

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 43551fc — straight top-level flow now. A failed fetch rejects at top level, which still exits non-zero and fails the build.


Generated by Claude Code

Comment on lines +72 to +74
if (requestBody !== undefined) {
requestHeaders['Content-Type'] = 'application/json'
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it intentional that all requests with a body are marked as JSON?

seems like current logic is:

  1. call fn with body as a buffer
  2. Buffer.isBuffer(body) returns true
  3. requestBody = body
  4. requestBody !== undefined is now true, so content-type is set to JSON

which is no good

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — and my earlier claim on the Bugbot thread that gh does the same was wrong: gh only sets application/json when it builds the body from fields (bodyIsJSON in cli/cli's http.go); --input bodies get no default Content-Type. Fixed in 43551fc to match: field-built bodies are marked application/json, raw --input bodies send no default Content-Type (pass -H when the API requires one). Help text, README, the stdin example and tests updated accordingly.


Generated by Claude Code

return patternSegments.length <= segments.length ? score + 1 : score
}

function parseQueryString(queryString: string): Record<string, string | string[]> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this can be replaced with querystring.parsefrom node:querystring. it uses a null prototype object and uses arrays for repeated values

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 43551fcparseQueryString is now a thin wrapper over node:querystring's parse, kept as a function only for the narrowing cast from ParsedUrlQuery.


Generated by Claude Code

response.statusCode === 401 && !flags.anonymous
? `. You may need to login again with ${styleText('cyan', 'sanity login')}`
: ''
this.error(`${statusLine}${loginHint}`, {exit: 1})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use the exitCodes enum instead of hardcoding 1

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 43551fc — both here (RUNTIME_ERROR) and the usage-error path (USAGE_ERROR).


Generated by Claude Code

if (flags.input === '-') return stdin
try {
// Read raw bytes - `--input` bodies may be binary (eg asset uploads)
return readFileSync(flags.input)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is async, but we aren't using fs/promises to read the file here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 43551fc--input file reads now use node:fs/promises. The remaining readFileSync is only for -F key=@file, which runs inside the synchronous field parser.


Generated by Claude Code

Comment on lines +71 to +73
// Null-prototype containers keep user-supplied keys like `__proto__` or
// `toString` ordinary own properties: nothing to pollute, nothing inherited.
const result: Record<string, FieldValue> = Object.create(null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: null prototype is safe but honestly for things like this where it's just raw user input, i like using Map and not worrying at all about any weirdness

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept null-prototype objects here deliberately: these containers are the JSON request body itself (nested, and passed straight to JSON.stringify), so Maps would need a recursive conversion back to plain objects before serialization. Null-prototype gives the same isolation from prototype weirdness without the extra pass. For the flat user-keyed maps (fieldsToQuery, parseHeaders) I've switched those to null-prototype too in 43551fc so the treatment is consistent everywhere user keys land.


Generated by Claude Code

claude and others added 11 commits July 22, 2026 06:09
Adds a gh api-style escape hatch to the CLI: `sanity api <endpoint>` makes
authenticated raw HTTP requests against any Sanity HTTP API documented in
the published OpenAPI specifications.

- {projectId}/{dataset} placeholders resolve from flags or CLI config, so
  paths copy verbatim from `sanity openapi get <slug>`
- Host (api.sanity.io vs <projectId>.api.sanity.io) and default API version
  are routed via a manifest generated from the published OpenAPI specs
  (pnpm generate:api-routes), not hand-maintained
- gh-style typed fields (-f/-F), raw bodies (--input), custom headers (-H),
  method inference (GET, or POST when a body is present)
- Response body passes through to stdout; HTTP errors exit 1, usage errors
  exit 2; --include prints status and headers

Closes AIGRO-5218

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
The generate:api-routes script now runs as a prebuild hook, so every build
compiles a manifest freshly distilled from the published OpenAPI specs. The
script retries transient fetch failures, fetches specs concurrently, and
fails the build when the spec index is unreachable, any listed spec fails to
fetch, or the distilled manifest would be empty - a release can never ship
with a missing or empty endpoint list.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
…spec paths

Two fixes from review:

- --input file and stdin bodies are now read as raw bytes and passed
  through untouched, so binary uploads (eg assets) are no longer corrupted
  by UTF-8 decoding. Typed -F @file values intentionally stay UTF-8 text -
  they land inside a JSON body where values are strings by definition.
- OpenAPI path keys are split on "?" before distillation, so a stray query
  marker in a spec path (eg /foo/{id}?) no longer produces an unmatchable
  route pattern.

Also passes proxy-related env vars (HTTPS_PROXY, NODE_EXTRA_CA_CERTS, ...)
through turbo, since the build's spec fetch needs them behind TLS-
intercepting proxies.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
pnpm readme regenerates docs for all commands; this picks up help-text
changes that landed on main without a README regeneration.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
…tent type

The global-host preference on route score ties now only applies when it
actually flips a project-host tie; same-host ties keep the first entry, so
the matched spec (and its default API version) no longer depends on later
entries in the manifest.

Also documents that request bodies are sent as application/json unless a
Content-Type header is provided with -H - the intended default for the
JSON-first --input workflow, with -H as the override for binary bodies.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
Full https:// URLs are the only accepted absolute endpoints - http:// is
refused with a usage error so the CLI token is never sent unencrypted.
The empty-path check now runs after version peeling, so an endpoint that
is only an API version (eg "v2025-02-19") is rejected as a usage error
instead of being sent to the API root.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
Field parsing and query-string parsing store user-supplied keys in
null-prototype containers and check conflicts with Object.hasOwn, so keys
like __proto__ or constructor are ordinary own properties: nothing to
pollute, nothing inherited to false-conflict with, and such keys serialize
into the request body like any other. Same approach as vercel/vercel's CLI
query-string parsing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
Route matching counts literal segment matches explicitly and rejects
patterns that match on placeholders alone, so a placeholder-only pattern
in a future OpenAPI spec (eg {resourceType}/{resourceId}) cannot capture
arbitrary paths and steal host/version routing from unrelated endpoints.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
Forcing a host with --global or --project-hosted restricts route matching
to specs served on that host instead of skipping matching entirely, so the
matched spec's default API version still applies and the documented version
precedence holds under forced hosts.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
- Field arrays now follow gh api semantics: consecutive [][key] fields fill
  the current array element until a key repeats, and a bare key[] declares
  an empty array
- Raw --input bodies are sent without a default Content-Type; only bodies
  built from fields get "application/json" (gh api parity)
- New --token/-t flag authenticates with an explicit API token
- JSON string responses print as JSON instead of raw text
- Query strings parse via node:querystring; fieldsToQuery and parseHeaders
  build null-prototype containers
- Generation script uses import.meta.dirname, node:util parseArgs and
  straight top-level await
- Exit codes use the exitCodes enum; --input reads via node:fs/promises

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
@runeb
runeb force-pushed the claude/aigro-5218-solution-plan-k4pdb6 branch from 0b885fd to cba4ac8 Compare July 22, 2026 06:37
Comment thread packages/@sanity/cli/src/actions/api/resolveEndpoint.ts
A request that is a mere prefix of a longer pattern (eg `data` vs
`data/query/{dataset}`) no longer inherits that route's host and default
version - it falls back to the global host instead of demanding a project
ID for a path that cannot be valid on the project host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6f86b7b. Configure here.

Comment thread packages/@sanity/cli/src/actions/api/resolveEndpoint.ts
Comment thread packages/@sanity/cli/src/services/api.ts
… overrides

- {name} fills from the dataset when it directly follows a datasets
  segment (the projects spec's naming), so those paths can be copied
  verbatim; {name} in other contexts stays unresolved
- Request headers are keyed lowercase so a user-provided header replaces
  the Authorization/Content-Type defaults regardless of casing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvSx8KFcwJBduAUszLYH8c
@runeb
runeb requested a review from jonahsnider July 22, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants