Skip to content

feat(cli): add sanity assets upload command#494

Draft
runeb wants to merge 8 commits intomainfrom
feat/assets-upload
Draft

feat(cli): add sanity assets upload command#494
runeb wants to merge 8 commits intomainfrom
feat/assets-upload

Conversation

@runeb
Copy link
Member

@runeb runeb commented Feb 27, 2026

Summary

Adds a new sanity assets upload command that uploads one or more files to the Sanity CDN and prints public URLs to stdout.

$ sanity assets upload screenshot.png diagram.pdf
https://cdn.sanity.io/images/abc123/production/hash-800x600.png
https://cdn.sanity.io/files/abc123/production/hash.pdf

Motivation

There's no CLI command to upload files to Sanity CDN and get back public URLs. This is useful for embedding images in GitHub PRs, Linear tickets, docs, or any context where you need a hosted URL.

What's included

packages/@sanity/cli/src/commands/assets/upload.ts

  • New assets topic (separate from existing media topic which handles media library operations)
  • Extends SanityCommand with strict = false for variadic file args (same pattern as exec.ts)
  • --project and --dataset flags with env var support (SANITY_PROJECT_ID, SANITY_DATASET)
  • Content type detection from file extension (.png, .jpg, .gif, .webp, .svg, .pdf)
  • Routes images to /assets/images/ and other files to /assets/files/
  • Auth via getProjectCliClient({ requireUser: true }) — token extracted with client.config().token
  • Pipe-friendly output: one URL per line to stdout

packages/@sanity/cli/src/commands/assets/__tests__/upload.test.ts

  • 4 tests using vitest + nock + @sanity/cli-test (testCommand, mockApi)
  • Tests: image upload, PDF routing to files endpoint, API failure handling, missing project ID error

Resolution chains

  • Project ID: --project flag → SANITY_PROJECT_ID env → this.getProjectId() (reads sanity.config.ts)
  • Dataset: --dataset flag → SANITY_DATASET env → cliConfig.api.dataset"production" fallback
  • Auth: SANITY_AUTH_TOKEN env → ~/.config/sanity/config.json (via sanity login)

Build & test

cd packages/@sanity/cli
pnpm build   # ✅ SWC compiles, oclif manifest picks up assets:upload
pnpm test    # ✅ 4/4 tests pass

@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @runeb's task in 1m 18s —— View job


Code Review

  • Read changed files
  • Analyze for issues
  • Post review

packages/@sanity/cli/src/commands/assets/upload.ts

1. Non-standard flag names (--project / --dataset)

The codebase has shared flag utilities in src/util/sharedFlags.ts that provide --project-id (with -p shorthand) and --dataset (with -d shorthand). Every other command with a project flag uses getProjectIdFlag() from that utility. This PR introduces a --project flag that's inconsistent with the established convention.

// upload.ts L41-50 — current
static override flags = {
  dataset: Flags.string({...env: 'SANITY_DATASET'}),
  project: Flags.string({...env: 'SANITY_PROJECT_ID'}),
}

Should use the shared helpers instead:

import {getDatasetFlag, getProjectIdFlag} from '../../util/sharedFlags.js'

static override flags = {
  ...getProjectIdFlag({env: 'SANITY_PROJECT_ID'}),
  ...getDatasetFlag({env: 'SANITY_DATASET'}),
}

Then update line 70 to use flags['project-id'] instead of flags.project. This makes the command consistent with backup enable, dataset list, etc., and gives users the -p/-d shorthands for free.

Fix this →

2. args definition is dead code

Lines 22–26 define static override args = { file: Args.string({...}) } but run() ignores args.file and reads from argv directly. The defined arg is never used. Either remove the args block or document why argv is preferred (e.g. to handle unlimited positional args when strict = false).

3. No test for multi-file upload

The multi-file upload path (the for loop at L91) has no test coverage. A test uploading two files and asserting both URLs are printed would cover the primary use case shown in the PR description.

4. Silent error suppression on getCliConfig()

// L64
const cliConfig = await this.getCliConfig().catch(() => {})

The empty catch silently swallows any error, including ones unrelated to "not in a Sanity project directory" (e.g. malformed config, I/O errors). Consider only suppressing known error types or at least logging to debug output so errors aren't completely invisible.

@github-actions
Copy link

github-actions bot commented Feb 27, 2026

Coverage Delta

File Statements
packages/@sanity/cli/src/commands/assets/upload.ts 97.0% (new)

Comparing 1 changed file against main @ 73b0d6e1bf2d9017ed20ef978e0b4776bed46b40

Overall Coverage

Metric Coverage
Statements 80.2% (+ 0.1%)
Branches 67.2% (+ 0.0%)
Functions 77.5% (- 0.0%)
Lines 80.5% (+ 0.1%)

@runeb runeb force-pushed the feat/assets-upload branch from f6cf019 to 6eeb5c4 Compare February 27, 2026 20:17
Miriad and others added 6 commits March 5, 2026 22:36
Adds a new `assets` topic with an `upload` command that uploads files
to the Sanity CDN and prints public URLs to stdout.

- Supports multiple files in a single invocation
- Auto-detects content type from file extension
- Routes images to /assets/images/ and other files to /assets/files/
- Resolves project ID from --project flag, env, or sanity.config.ts
- Resolves dataset from --dataset flag, env, config, or "production"
- Auth handled via getProjectCliClient with requireUser validation

Includes tests using vitest + nock + @sanity/cli-test utilities.

Co-authored-by: freud <freud@miriad.systems>
Use client.getUrl() to construct the assets API URL, which respects
SANITY_INTERNAL_ENV for staging environments (api.sanity.work vs
api.sanity.io). Previously the host was hardcoded to api.sanity.io.

Co-authored-by: freud <freud@miriad.systems>
1. Consolidate getCliConfig — call once, use for both project ID and
   dataset resolution. Fixes bug where running outside a Sanity project
   dir with --project flag would throw from findProjectRoot().
2. Add file-not-found error handling — wrap readFile in try/catch with
   friendly "File not found: <path>" message.
3. Remove unused nock import and cleanup from tests — we mock fetch
   globally with vi.stubGlobal, not nock interceptors.
4. Add {exit: 1} to all this.error() calls — matches codebase convention
   (without it, oclif defaults to exit code 2).

Co-authored-by: freud <freud@miriad.systems>
The project uses ES2023 lib without DOM types, so Buffer and
Uint8Array don't satisfy the BodyInit type for fetch(). Using
Blob([buffer], {type}) which is in the ES2023 spec and passes
typecheck.

Co-authored-by: freud <freud@miriad.systems>
Major simplification addressing all 4 review findings:

1. Switch to client.assets.upload() — handles auth, host resolution,
   and returns SanityAssetDocument with .url. No more raw fetch, no
   manual URL construction, no token extraction. Eliminates the
   Bearer undefined edge case entirely.
2. Remove required:true from arg — keep manual files.length check for
   controlled error message. Fixes dead code (oclif would error before
   run() with required:true + strict:false).
3. Fix test mocking — properly mock getProjectCliClient via
   vi.mock('@sanity/cli-core') pattern (matches backup/disable.test.ts).
   Tests now exercise the actual client.assets.upload() call path.
   Added 5th test for file read errors.
4. Preserve original error in catch — "Cannot read file: ENOENT..."
   instead of misleading "File not found" for permission errors etc.

Co-authored-by: freud <freud@miriad.systems>
1. Add test for missing URL in upload response (!doc.url branch)
2. Guard against SANITY_PROJECT_ID env var leaking into 'no project ID'
   test — save/restore pattern ensures test isolation
3. Add explanatory comment for why we use cliConfig?.api?.projectId
   instead of this.getProjectId() — the latter throws from
   findProjectRoot() when run outside a project directory
@runeb runeb force-pushed the feat/assets-upload branch from 75621d1 to 116df93 Compare March 5, 2026 22:38
Miriad added 2 commits March 6, 2026 00:00
1. Bring back CONTENT_TYPES map and pass contentType explicitly to
   client.assets.upload(). The client does not infer content type from
   the filename option when given a Buffer — only browser File objects
   get auto-detection. Without this, SVGs and other files would be
   uploaded as application/octet-stream.
2. Remove duplicate 'no URL in response' test that was accidentally
   added in both the review fixes commit and the Claude findings commit.
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.

1 participant