Back to Spree

`spree api` — Admin API command group in `@spree/cli`

docs/plans/5.5-admin-api-cli.md

5.5.017.8 KB
Original Source

spree api — Admin API command group in @spree/cli

Status: In Progress Target: Spree 5.5.x (CLI minor + one core patch) Depends on: 5.5-admin-api-key-scopes.md (shipped), 6.0-admin-api.md (conventions), 5.5-agent-skills.md (distribution of the companion skill) Author: Damian Legawiec ([email protected]) Last updated: 2026-06-12

Summary

Add a gh api-style command group to @spree/cli: generic HTTP verbs against the Admin API v3 (spree api get|post|patch|delete <path>), schema introspection from the bundled OpenAPI spec (spree api endpoints, spree api schema), and layered credential resolution (flags > env > project auto-mint > profiles). This turns the CLI into the first executable agent surface for Spree's back office — usable by Claude Code, Cursor, CI scripts, and humans alike — before any MCP server exists.

This is a deliberate sequencing decision ("CLI-first"): developer agents already have shell access and need zero client configuration; the consumer MCP clients that would justify a hosted Admin MCP (claude.ai, ChatGPT) reject static API keys and are blocked on the OAuth 2.1 work in 6.0-platform-auth regardless; and a CLI is immune to MCP protocol churn (a breaking, stateless MCP spec revision lands 2026-07-28). Shopify made the same call — there is no official Shopify Admin MCP; agent store management goes through their AI Toolkit CLI. Everything built here (credential bootstrap, generic verbs, schema tools, the SPREE_BASE_URL/SPREE_API_KEY env contract) becomes the substrate a later spree mcp wrapper exposes as MCP tools, so nothing is throwaway.

Key Decisions (do not deviate without discussion)

  • CLI-first; MCP servers come later and get their own plans. This plan deliberately contains zero MCP code. The future lanes are: a shopper-facing Storefront MCP (/api/mcp, separate plan, highest strategic priority since it has no CLI substitute), a hosted Admin MCP riding 6.0-platform-auth OAuth, and an optional spree mcp stdio wrapper over this command group. None of them start before spree api ships.
  • Generic verbs, not per-resource subcommands. spree api get /products — never spree products list. A per-resource command family (~38 resources) would duplicate @spree/admin-sdk, sprawl the CLI, and add nothing for agents, which compose generic verbs with the schema tools. Precedents: gh api, and the Wix/Datadog/Harness redesigns away from API-parity toward few generic verbs.
  • Layered credential resolution, in this order:
    1. flags (--base-url, --api-key — the key flag is supported but discouraged: it leaks into shell history and process lists)
    2. env vars SPREE_BASE_URL + SPREE_API_KEY (the agent/CI/sandbox path)
    3. project context: inside a spree-starter directory (detectProject()), resolve the base URL from .env and auto-mint a key through Docker on first use — zero configuration
    4. named profiles in ~/.config/spree/config.json, selected via --profile or a default
  • sk_ secret keys are the only credential. They are store-bound (tenant resolution is free), scoped, HMAC-digest stored, and revocable from the admin dashboard — revocation is the kill switch. No JWT support: ~1h expiry with refresh rotation is the admin SPA's model, not a CLI's. A browser/device-code login becomes a fifth resolution source once 6.0-platform-auth ships an OAuth stack; the resolution chain is designed so it slots in without breaking anything.
  • Auto-minted keys get read_all only. Write scopes always require an explicit spree api-key create --scopes write_products,.... An agent pointed at a long-lived dev database must not be able to mutate by accident.
  • spree api and spree auth work without a Docker project. Today every CLI command hard-requires docker-compose.yml via detectProject() (packages/cli/src/context.ts). The new commands consult project context only as resolution layer 3 and otherwise run standalone against any Spree 5.5+ host — local or remote.
  • The CLI gains a dependency on @spree/admin-sdk (it currently depends on neither SDK). Retry with backoff, Retry-After honoring, automatic Idempotency-Key on writes, SpreeError with code/status/details, and X-Spree-Store-Id handling all come free. No hand-rolled fetch layer.
  • JSON to stdout by default; humans opt into tables. Agents and jq are the primary consumers. --format table renders collections for humans. Errors go to stderr with non-zero exit codes; a 403 scope denial prints the required_scope from the error payload plus the exact spree api-key create --scopes ... remediation.
  • Schema tools read a bundled, versioned admin.yaml snapshot. docs/api-reference/admin.yaml is rswag-generated (never hand-edited) and already drives @spree/admin-sdk codegen — the CLI bundles a snapshot per release and parses it at startup. No live spec fetch in v1; version skew is surfaced as a warning when the server's Spree version (from spree api status) doesn't match the bundled snapshot's.
  • Two core fixes ship first as a 5.5.x patch (the CLI is unusable against unpatched servers otherwise):
    1. spree:cli:create_api_key cannot mint secret keys at all today — Spree::ApiKey presence-validates scopes on secret keys (spree/core/app/models/spree/api_key.rb) and the task passes none, so KEY_TYPE=secret raises RecordInvalid. Add a SCOPES env var (comma-separated, validated against Spree::ApiKey::SCOPES).
    2. Spree::ApiKey::SCOPES has no read_promotions/write_promotions even though the promotions controllers declare scoped_resource :promotions — promotion endpoints are reachable only by *_all keys. Add both scopes.
  • Pair with an agent skill, distributed per 5.5-agent-skills.md. A spree-cli skill in spree/agent-skills teaches agents the credential flow, the verb + schema-tool loop, and the read-before-write conventions. Skills are the guidance channel; the CLI is the execution channel — don't duplicate conventions into CLI help text beyond --help basics.

Design Details

Command surface

bash
# Reads — Ransack filters as repeatable -q, JSON:API-style sort
spree api get /products -q status_eq=active -q name_cont=shirt --sort -created_at --limit 10
spree api get /orders/ord_x8k2J9aQ --expand items,payments,fulfillments

# Writes — JSON body inline, from file, or from stdin
spree api post /products -d '{"name":"Classic Tee","prices":[{"currency":"USD","amount":"29.99"}]}'
spree api post /orders/ord_x8k2J9aQ/refunds -d @refund.json
cat batch.json | spree api post /prices/bulk_upsert -d -

# State-machine and custom actions are just paths
spree api patch /orders/ord_x8k2J9aQ/cancel
spree api delete /products/prod_86Rf07xd

# Schema introspection (bundled admin.yaml)
spree api endpoints --resource orders          # method, path, summary, required scope
spree api schema "POST /orders/{id}/refunds"   # request/response JSON Schema, Ransack fields
spree api status                               # server reachability, Spree version, credential + scopes

# Credentials
spree auth login --profile prod --base-url https://store.example.com   # prompts for sk_ on stdin
spree auth status                              # profile, base URL, key prefix (sk_a1b2c3d4e5f6…), scopes
spree auth logout --profile prod
spree api-key create --scopes read_all         # extended with --scopes (maps to rake SCOPES env)

Flag conventions:

FlagMeaning
-q key=value (repeatable)Ransack predicate; wrapped into q[...] via the SDK's transformListParams
--sort, --page, --limit, --expand, --fieldspassed through to the Admin API conventions from 6.0-admin-api.md
-d <json> / -d @file / -d -request body (inline, file, stdin)
--store-idX-Spree-Store-Id header, only needed when one host serves multiple stores
--profile, --base-url, --api-keycredential resolution overrides
--format json|tableoutput rendering (default json)

Credential resolution and storage

Resolution order (first hit wins): flags → SPREE_BASE_URL/SPREE_API_KEY env → project credentials → profile.

  • Project credentials live in .spree/credentials.json in the project directory, 0600, gitignored (create-spree-app and spree-starter add .spree/ to the scaffolded .gitignore). Written by the auto-mint flow: detectProject() resolves the port → run spree:cli:create_api_key NAME="@spree/cli (auto)" KEY_TYPE=secret SCOPES=read_all through Docker (same path as the existing api-key create command) → persist { baseUrl, token, scopes, mintedAt }. First spree api get /products in a fresh project requires zero setup.
  • Profiles live in ~/.config/spree/config.json (0600): { defaultProfile, profiles: { prod: { baseUrl, token } } }. spree auth login reads the key from an interactive prompt or stdin — never from a flag — and validates it with a test call before saving. The display prefix (first 12 chars) shown by auth status is computed from the stored token, not persisted separately; the plaintext is never echoed after creation.
  • OS-keychain storage is an open question (below); v1 is file-based with 0600, matching gh's fallback behavior.

Core rake task changes (5.5.x patch)

ruby
desc 'Create an API key'
task create_api_key: :environment do
  name = ENV.fetch('NAME')
  key_type = ENV.fetch('KEY_TYPE')
  scopes = ENV.fetch('SCOPES', '').split(',').map(&:strip).reject(&:blank?)
  store = Spree::Store.default
  key = store.api_keys.create!(name: name, key_type: key_type, scopes: scopes)
  print key.plaintext_token
end

Unknown scopes are already rejected by the model's validate_known_scopes; the task stays dumb. read_promotions/write_promotions are added to Spree::ApiKey::SCOPES in the same patch (additive, safe — existing keys are unaffected).

Output and errors

  • Collections pass through the API's {data, meta} envelope untouched; single resources print the bare object. Everything is jq-composable.
  • Exit codes: 0 success, 1 API error (4xx/5xx — the Stripe-style {error: {code, message, details}} payload is printed to stderr), 2 usage/configuration error (no credential resolvable, bad flags, unreachable host).
  • A 403 with code: access_denied prints details.required_scope and the remediation command. This is load-bearing for agents: the error text is what the model reads to self-correct.

Relationship to future MCP work

LaneSurfaceStatus after this plan
Dev/admin agents (Claude Code, Cursor, CI)spree api + spree-cli skillShipped here
Shopper agents (claude.ai, ChatGPT consumers)Storefront MCP at /api/mcpSeparate plan, next minor — no CLI substitute exists
Merchant copilot on consumer clientsHosted Admin MCP + OAuth 2.16.0, rides 6.0-platform-auth; tool catalog informed by observed CLI usage
MCP clients that can't shell outspree mcp stdio wrapper exposing this command group as toolsOptional, later — the SPREE_BASE_URL/SPREE_API_KEY env contract defined here is already the stdio-MCP credential convention

agent-skills pairing

One new spree-cli skill in github.com/spree/agent-skills (per that plan's constraints: targets users, not monorepo contributors). It covers: when to reach for spree api vs the TypeScript SDKs, the credential layers, the endpoints → schema → get/post loop, read-before-write habits, and the scope-denial remediation flow. The existing spree-api-v3 skill gets a cross-reference.

Migration Path

All steps are additive; no breaking changes anywhere.

  1. Core 5.5.x patch (spree/core) — ✅ shipped (PR #14166): SCOPES env on spree:cli:create_api_key (fixes secret-key minting), read_promotions/write_promotions in Spree::ApiKey::SCOPES (plus the broader scope cleanup: dedicated api_keys/webhooks scopes, stock covering inventory, exports gated by the exported resource). Specs for all.
  2. @spree/cli minor — ✅ done: @spree/admin-sdk dependency; src/config.ts (paired-layer resolution + credential files); src/commands/auth.ts + src/commands/api.ts; helper modules under src/api/ (params, body, output, ping, spec); --scopes flag on api-key create; scripts/bundle-spec.mjs slims admin.yamlsrc/generated/admin-spec.json at build time (via the yaml package); Changesets entries.
  3. Scaffolding — ✅ partial: create-spree-app adds .spree/ to its .gitignore; the auto-mint flow also writes a self-ignoring .spree/.gitignore so any project (regardless of scaffold vintage) is covered. Pending: the same .gitignore line in the spree/spree-starter template repo (separate repo PR).
  4. agent-skills — ⏳ open: spree-cli skill PR to spree/agent-skills.
  5. Docs — ✅ done: docs/developer/cli/admin-api.mdx (command group + credential layers + agent usage), docs/api-reference/admin-api/endpoints.mdx (generated endpoint index), README + quickstart sections.

As-built surface (beyond the original sketch)

  • Commands: spree api get|post|patch|delete <path>, spree api endpoints (--resource, --search, --format), spree api schema <operation>, spree api status; spree auth login|status|logout|list (logout --project removes .spree/credentials.json).
  • Credential resolution is paired-per-source, not per-field. The API key anchors each layer; host and key resolve together so a saved/minted key is never silently sent to a host from SPREE_BASE_URL alone. An explicit --base-url/--api-key flag is the only sanctioned cross-source override. Auto-mint is suppressed when an explicit --base-url targets a non-project host.
  • Profile schema persisted at ~/.config/spree/config.json (0600): { defaultProfile, profiles: { <name>: { baseUrl, token } } }. A corrupt file is backed up to config.json.bak rather than silently emptied.
  • Schema snapshot: a slimmed (examples-stripped) JSON copy of admin.yaml bundled in the package; spree api schema resolves $refs inline and prefers exact literal paths over {id} templates.
  • Exit codes: 0 success, 1 API error (envelope to stderr), 2 usage/configuration error (bad flag, invalid -q/--page/--limit, no credentials, unreachable host). 403 scope denials print the full mint-and-wire remediation.
  • Deferred: a true server-vs-snapshot version-skew warning needs an Admin API endpoint exposing the Spree version (none exists yet); spree api status notes the snapshot may lag the live server instead.

Constraints on Current Work

  • Do not add per-resource data subcommands to @spree/cli (spree products list, spree orders show, …). The generic-verb decision is policy; convenience wrappers route through spree api or don't exist.
  • Do not start MCP server implementation (storefront, admin, or stdio wrapper) before this ships and has its own plan doc. Future MCP servers wrap @spree/admin-sdk / these CLI internals — never fork a parallel tool surface.
  • Keep regenerating admin.yaml via rswag after any Admin API change (bundle exec rake rswag:specs:swaggerize). It was already the rule; the CLI's schema tools now make it load-bearing — a stale spec means spree api schema lies to agents.
  • New admin controllers must declare an accurate scoped_resource and, if they introduce a new resource family, add the matching read_*/write_* pair to Spree::ApiKey::SCOPES in the same PR. The CLI surfaces required scopes from the spec; gaps become user-visible immediately.

Open Questions

  • OS keychain storage for profile keys (vs 0600 files only)? Cross-platform keychain libs for Node add native-dependency weight to a so-far pure-JS CLI; gh ships file-fallback and most users live there. Lean: file-only in v1, revisit on demand.
  • Built-in --jq filter (like gh api --jq)? Convenient, but adds a jq-engine dependency; agents and humans can pipe to real jq. Lean: skip in v1.
  • --paginate auto-pagination for collections? Useful for exports, dangerous for context-limited agents. Lean: skip in v1; meta.next is in the envelope.
  • Should spree api also cover the Store API (--surface store, bundling store.yaml) for storefront/checkout testing? Plausible later; out of scope now.
  • Re-scoping existing controllers — resolved during 5.5 RC (2026-06-12): inventory controllers (stock_items, stock_locations, stock_transfers) moved to stock, webhook controllers to webhooks, and api_keys got a dedicated read/write_api_keys pair. See the vocabulary table in 5.5-admin-api-key-scopes.md.
  • Version-skew handling beyond a warning: should spree api status compare bundled-spec version vs server version and offer --spec <path> overrides for edge/pre-release servers?

References