ssctl

ssctl is the command-line client: a thin, kubectl-shaped door onto the REST surface. Everything in a substrate is an entity of a declared type, addressed as /{group}/{plural}/{id}, and the CLI mirrors that: types, get, apply, patch, delete, edit, link, unlink, watch. It lives in the same repository as the substrate (cmd/ssctl) and builds with Go.

ssctl login --server https://substrate.example --tenant ada
ssctl types                        # every installed entity type
ssctl get tasks                    # list a collection
ssctl get tasks t9 -o yaml         # one entity, apply-able envelope
ssctl apply -f task.yaml           # put: merge, never prune
ssctl patch tasks t9 --state status=done
ssctl watch                        # resumable change stream

Login and configuration

ssctl login exchanges the tenant's rotating 6-digit code for a bearer token (authentication) and stores it in the config file, which holds one context per substrate. Config lives at ~/.config/ssctl/config.yaml (override with SSCTL_CONFIG); SUBSTRATE_SERVER, SUBSTRATE_TOKEN, and SUBSTRATE_TENANT override the file, and flags override both. The older TEILD_ and SS_ variables are still accepted as aliases, and the canonical SUBSTRATE_* variable wins if more than one is set. --actor picks which of the token's actors a write is attributed to.

Reading

ssctl get <plural> [id] reads a collection or one entity. The plural may be fully qualified (tasks.tasks.teild.dev) or bare (tasks), which resolves against the type registry; when two installed groups both declare the same plural, that plural needs qualifying, or -g to name the group (every extension installs a config, so configs always needs one). Lists take --filter (the JSON filter grammar), -l label selectors, --order-by, and --limit; --after resends the opaque keyset cursor a page printed, and -w streams that one collection's changes instead of listing it.

-o yaml writes each entity as an envelope document (group, type, metadata, data, and the server-set status), ----separated, and -o json writes the same shape. REST returns the entity flat; the envelope is the CLI's YAML form. status is ignored on input, so the output applies back unchanged.

Writing

Watching, triggers, and bundles

ssctl watch streams the changelog, one line per committed change, resumable with --from and filterable by --types, --actors, and --ops.

Delivery bookkeeping lives on triggers, not on functions, so it is the trigger subcommands that drive it: status shows each trigger's kind, callable, cursor, lag, last fire, and parked count; parked lists the deliveries it gave up on and retry re-runs one; replay resets an entity-sourced trigger's cursor; run synthesizes a single delivery; and wake scans a trigger immediately. Trigger rows are ordinary entities, so get / apply / delete edit them like anything else.

ssctl function call <name> --input <json> invokes one function directly, applies its effects under the function's actor, and prints the output. There is no build step: a function is inline source on its manifest, so it installs with the ordinary apply.

ssctl bundle list / status report an extension's computed state, and disable / enable / uninstall / purge move it through its lifecycle; install and upgrade are ssctl apply of the closure, and connect starts the host OAuth flow for an account entity, printing the consent URL. token create / list / delete manage bearer tokens: create prints the new secret exactly once and the substrate keeps only its hash, so list and delete are metadata alone.

Next: the web console, the same substrate in a browser.