@sixtyninecommerce/cli
v0.5.3
Published
Command line tool for sixtynine-commerce: pull API schemas, validate and scaffold apps, tail platform events
Readme
@sixtyninecommerce/cli
The command line tool for sixtynine-commerce: pull the API schemas, validate and scaffold an app, follow a tenant's platform events and send a test event to a webhook receiver.
npx @sixtyninecommerce/cli --help
# or
npm install -g @sixtyninecommerce/cli
sixtynine --helpThe binary is sixtynine (with sixtyninecommerce as an alias, for a machine that already has
something called sixtynine).
Configuration
Every command takes its URLs and credentials from flags, then the environment, then the hosted defaults:
| Environment | Flag | Default |
|---|---|---|
| SIXTYNINE_VENDURE_URL | --vendure-url | https://engine.sixtyninecommerce.com |
| SIXTYNINE_CMS_URL | --cms-url | https://cms.sixtyninecommerce.com |
| SIXTYNINE_PLUGINS_URL | --plugins-url | https://plugins.sixtyninecommerce.com |
| SIXTYNINE_API_TOKEN | --token | — |
| SIXTYNINE_CHANNEL | --channel | — |
An empty environment variable counts as unset.
Exit codes. 0 success, 1 the command ran and failed (an invalid manifest, a schema URL that
404s, a refused token), 2 the invocation was wrong or is not available yet, 130 interrupted with
Ctrl-C. A 2 will not succeed on a retry; a 1 might.
Every command takes --json for machine-readable output on stdout. Progress and warnings always go
to stderr, so a pipe carries only the answer.
sixtynine schema pull
Downloads the five machine-readable API schemas — Shop and Admin GraphQL SDL, CMS and plugins
OpenAPI, and the platform-event catalogue as AsyncAPI 3.0.0 — into ./schemas (or --out <dir>).
The URLs are public in every environment.
sixtynine schema pull --vendure-url http://localhost:3100 \
--cms-url http://localhost:3101 --plugins-url http://localhost:3102Wrote 5 schemas to ./schemas
schema-shop.graphql 159685 bytes (155.9 KB)
schema-admin.graphql 598662 bytes (584.6 KB)
cms-openapi.json 68701 bytes (67.1 KB)
plugins-openapi.json 114784 bytes (112.1 KB)
asyncapi.json 361014 bytes (352.6 KB)Nothing is written unless every download succeeds: a directory holding four fresh schemas and
one stale one silently produces a wrong client. The one exception is asyncapi.json answering
404, which means a plugins service that predates the route. It is skipped with a note on
stderr and listed under skipped in --json, and the other four are written. An
asyncapi.json left in the directory by an earlier pull is removed, because it came from
another environment. Any other failure of it still writes nothing.
sixtynine app validate [file]
Validates a sixtynine.app.json against the app manifest JSON Schema the CLI ships (draft
2020-12). Offline, and exits 1 with a JSON pointer per problem — drop it into CI.
sixtynine app validate✗ /path/sixtynine.app.json is not a valid app manifest:
/ must have required property "slug"
/customerData must have required property "purpose"
/metafieldDefinitions/0/visibility must have required property "storefront"The manifest declares everything the platform will enforce about an app: slug, name,
ownerLabel, customerData (none | identity | full, with a purpose whenever it is not
none), declaredPermissions, declaredEvents, declaredFieldOwnership, metafieldDefinitions,
configSchema, hooks, headTemplates, redirectRules and sandboxOnly. Point your editor at
the $schema URL in the scaffolded manifest for completion as you type.
declaredFieldOwnership names field groups, never fields: any of price, stock,
product_details and customer_details, each a fixed list of fields the platform defines (the
schema's description lists them). The earlier { "entity": …, "fields": [...] } form is refused.
events.endpoint is required once declaredEvents is not empty. It is an https URL template;
{installation_id} and {channel_code} are filled in per installation and are the only
placeholders. Installing the app creates that installation's event subscription; the app fetches
the signing secret ONCE with its own installation token (GET /event-subscriptions/installation,
admin.apps.installation.eventSubscription()), and a lost secret is replaced by rotating it. At
customerData: none the customer's name and email are withheld from every delivery; identity
and full receive them. Below full that is all the personal data an event may carry, so the
registry refuses event types whose personal data it cannot withhold (return.*, *) unless the
level is full.
configSchema is a bounded dialect, not full JSON Schema: fifteen keywords over six types, with
additionalProperties defaulting to false. A green app validate is "worth sending", never
"certain to be accepted" — three differences to know about:
- The registry refuses a
requiredentry naming a property the same level does not declare; this command accepts it. - The registry compiles every
patternas a unicode-mode regular expression and refuses one that does not; this command checks only that it is a string of at most 200 characters.^a\-b$is valid without theuflag and invalid with it. - The registry refuses a schema nested more than 32 levels deep; this command has no such limit,
because JSON Schema cannot express a recursion bound. Nothing a settings form needs comes close.
Both readers now also refuse a keyword on a type it cannot constrain —
minLengthon aninteger,requiredon astring. Declare each on a type it applies to:minLength/maxLength/patternon a string,minimum/maximumon a number or integer,minItems/maxItemson an array,requiredon an object.
sixtynine app init <dir>
Scaffolds an app project: the manifest, a webhook receiver that verifies the delivery signature,
a package.json depending on @sixtyninecommerce/admin, a tsconfig and a README. The directory
must be empty.
sixtynine app init ./acme-syncIt fetches the published starter repository when that exists and falls back to a built-in minimal
template when it does not (today it does not). --template-ref <branch|tag|sha> picks a ref of the
starter; an explicit ref that does not resolve is an error rather than a silent fallback.
sixtynine events tail
Follows a tenant's platform events, by polling the feed or (--stream) over the real-time SSE
stream. Needs an API token carrying the ReadPlatformEvents permission, scoped to exactly one
channel.
sixtynine events tail --channel acme-nl --token s69t_… --types "order.*" --interval 102026-09-16T05:41:22.113Z order.created 9f1c…-8a2e actor=api_token:order-import
2026-09-16T05:41:30.004Z product_variant.price_changed 27df…-6b23 actor=user
Resume with --after 50.684888c0ebb17f37Ctrl-C stops the tail, prints the resume line and exits 130.
- The feed is a log reader: a page is read forward from a position. Without
--afterit starts at the oldest event retention still holds;--after <cursor>resumes. The cursor to resume from is printed when the command exits — on Ctrl-C, and on a failure mid-tail — and it reprints--typeswith it, because the filter is part of the cursor. --onceprints one page and exits.--types a,btakes exact types, family wildcards (order.*) or*; the filter is part of the cursor, so keep it the same when resuming.--jsonprints one envelope per line. Envelope fields are camelCased (occurredAt,schemaVersion) and the event'sdatais left exactly as the platform sent it — that payload is the wire contract your integration consumes.- Deliveries you receive over a webhook subscription are at-least-once; the feed is ordered and
gapless from a cursor.
complete: false— reported as a warning — means retention destroyed events above your position. --from nowstarts at the newest event: only what happens from now on is printed. It cannot be combined with--after.--streamfollowsGET /events/streaminstead of polling, printing the same lines (or--jsonenvelopes). A dropped connection is reopened by itself from the last event printed, with no gap and no duplicate; the resume line works for a poll too, since stream and feed cursors are the same thing. One live stream per token: opening a second one (another terminal, another process) closes the first, which exits 1 withEVENT_STREAM_SUPERSEDED. Events arrive up to about a minute after the change (the platform's relay cadence).--onceand--intervalare poll-only.- There is no tunnel mode: to see events on your machine,
--streamneeds nothing but the token.
sixtynine events send-test <type>
Has the platform sign and POST ONE synthetic event to a webhook subscription's endpoint, exactly as
a real delivery is signed (both webhook-signature and x-webhook-signature), so you can check a
receiver without waiting for the store to produce the event. Needs a token with
ManageEventSubscriptions (the events template), or UpdateSettings for a token minted before
that permission existed.
sixtynine events send-test order.created --subscription 0199… --channel acme-nl --token s69t_…
✓ order.created delivered: HTTP 204 in 31 ms (webhook-id 5b0e…)- The envelope carries
"test": true, a freshid,actor: system:send-testand the platform's example payload for the type — or your own with--data payload.json(a JSON object, sent verbatim). Your receiver should acknowledge it and not act on it. - It is never written to the event log:
events taildoes not show it, and it does not count toward the subscription's auto-disable. It is listed among the subscription's deliveries. - Exit 0 when the endpoint answered 2xx, 1 when it did not (the line says what came back).
- Works on a disabled subscription, so you can check a fixed endpoint before enabling it. Refused: a type outside the subscription's filters, an app's subscription, a closed store.
--idempotency-key <key>replays the first answer on a retry instead of sending again.
Not here yet
app register|update, app keys *, sandbox *, install --sandbox and verify are specified
but need server work that has not shipped (the apps registry, store provisioning, the conformance
harness). They are absent
rather than stubbed: a command that exists and cannot work is worse than one that does not exist.
License
MIT
