npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 --help

The 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:3102
Wrote 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 required entry naming a property the same level does not declare; this command accepts it.
  • The registry compiles every pattern as 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 the u flag 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 — minLength on an integer, required on a string. Declare each on a type it applies to: minLength/maxLength/pattern on a string, minimum/maximum on a number or integer, minItems/maxItems on an array, required on 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-sync

It 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 10
2026-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.684888c0ebb17f37

Ctrl-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 --after it 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 --types with it, because the filter is part of the cursor.
  • --once prints one page and exits. --types a,b takes exact types, family wildcards (order.*) or *; the filter is part of the cursor, so keep it the same when resuming.
  • --json prints one envelope per line. Envelope fields are camelCased (occurredAt, schemaVersion) and the event's data is 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 now starts at the newest event: only what happens from now on is printed. It cannot be combined with --after.
  • --stream follows GET /events/stream instead of polling, printing the same lines (or --json envelopes). 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 with EVENT_STREAM_SUPERSEDED. Events arrive up to about a minute after the change (the platform's relay cadence). --once and --interval are poll-only.
  • There is no tunnel mode: to see events on your machine, --stream needs 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 fresh id, actor: system:send-test and 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 tail does 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