plexus-cli (0.0.12)
Installation
registry=npm install plexus-cli@0.0.12"plexus-cli": "0.0.12"About this package
plexus-cli — the unified Plexus admin + config CLI
One pure-TypeScript CLI for operating and configuring Plexus against any
environment (local / staging / prod) — runtime ops, introspection, and
admission-offer config-as-code from a single binary. The system command is
plexus-cli.
Install (system tool)
Published to the Gitea npm registry; install it globally and run from anywhere:
# one-time: point the registry at Gitea (in ~/.npmrc), then install
npm config set registry https://gitea.i.ivo-zilkenat.de/api/packages/ivo.zilkenat/npm
npm i -g plexus-cli
plexus-cli --help # all commands, grouped by category
plexus-cli <command> --help # per-command help (clipanion auto-generates it)
Prefer to keep your default registry on public npm? Install with an explicit
--registry https://gitea.i.ivo-zilkenat.de/api/packages/ivo.zilkenat/npminstead.
Develop in-repo
The CLI lives in app/tools/plexus-cli/. To run the working tree (not the published
build) against a backend:
cd app/tools/plexus-cli
pnpm install # one-time: tool deps (clipanion, esbuild, tsx…)
pnpm dev -- <command> [...flags] # = node --import tsx cli.ts
pnpm build # → dist/cli.mjs (the published bundle)
pnpm typecheck # tsc against the live backend (build-from-truth gate)
pnpm spec # regenerate spec.snapshot.json (functions/describe)
How it's built — in-repo source, bundled to ship
The CLI imports the backend's real types, the generated api, and the publish-gate
validators directly from app/convex/**. The publish workflow type-checks against
that live tree and bundles it (esbuild) into a standalone artifact — so the
benefits are baked in at build time:
- Typed
api, zero drift — every call's args + returns are inferred end-to-end from the live function definitions; the published bundle is the real code, type-checked and versioned, never a hand-maintained re-derivation. no-explicit-anyeverywhere — the call surface is fully typed; the only unavoidable casts areas unknown as T(admin auth, the genericcall).- Offline validation against the real publish gate —
offer validateruns the samerunWorkflowValidators/runFormValidatorsthe server runs, inlined into the bundle (works on any machine, no repo, no deployment).
The only caveats: the installed CLI is a point-in-time release (rebuild/republish to
pick up backend changes), and the --env local rdu-admin-key shortcut works only when
run from inside the repo — the token path works everywhere.
Auth & environments
Connection config resolves in this order: --env/--tenant flags → env-vars →
~/.config/agent-tools/plexus.yml.
# ~/.config/agent-tools/plexus.yml
plexus:
default_env: local
default_tenant: test
environments:
local: { convex_url: "http://127.0.0.1:3264", site_url: "http://127.0.0.1:3265", token: "plx_…" }
staging: { convex_url: "https://staging.db.plexus.swop.schule", site_url: "https://staging.actions.db.plexus.swop.schule", token: "plx_…" }
prod: { convex_url: "https://db.plexus.swop.schule", site_url: "https://actions.db.plexus.swop.schule", token: "plx_…" }
Env-var fallback (no YAML): PLEXUS_ENV, PLEXUS_CONVEX_URL, PLEXUS_SITE_URL,
PLEXUS_TOKEN, PLEXUS_TENANT.
Token path (faithful): a plx_… API token (mint one in the app under
Settings → API-Token) is exchanged at {site_url}/cli/token for a short-lived
RS256 JWT, cached at ~/.config/agent-tools/plexus-jwt-<env>.json. Every call
lands as the real authenticated user through the public API + its gates.
Local fallback: with no token for --env local, the CLI uses the rdu admin key
from the worktree's Convex config.json (resolved via scripts/lib/convex-local.ts).
Run plexus-cli config to see exactly what resolved (authMode: token | admin).
Commands
| Group | Commands |
|---|---|
| local | config, functions, describe, offer validate |
| introspection | functions [--kind] [--grep], describe <fn>, call <fn> [--kind] key=value… |
| identity | whoami, tenants, units |
| tokens | tokens list, tokens create <name>, tokens revoke <id> |
| users | users {list,show,add,remove,set-group,unset-group,groups,set-platform-admin,set-platform-permission,create,provision,provision-platform-admin,set-password,delete,doctor,relink-auth} |
| platform | platform {tenants,users,workers} |
| stammdaten | stammdaten persons [-q] [--limit] [--all] |
| admission | admission laeufe |
| imports | imports {status,trigger,trigger-all} |
| feedback | feedback {list,to-issue,sync,set-status} |
| offer | offer {create,show,set,delete,restore,list,status} · offer run {add,set,archive,restore} · offer level {add,rename,remove,restore} · offer capacity set · offer {export,validate,plan,diff,apply} |
Every command supports --json (raw output; default is a table). Network commands
take --env / --tenant. functions / describe / offer validate / config
work offline (no deployment).
Provisioning admins
Two distinct shapes for a tenant-less platform operator:
users create <email> --platform-admin— mints the bare identity + the_platform.*row but no credential (the user can't log in until they self-sign-up / reset).users provision-platform-admin <email> --name "…"— the credentialed, ready-to-log-in path (creates the Better Auth credential too). Needs_platform.users.manage(your own platform-admin token, no Convex admin key) and refuses an email that already has tenant access (the_platform.* XOR tenant-accessinvariant). It is the platform twin ofusers provision, which does the same for a tenant user (--tenant <slug> --group tenant_admin).
Step-by-step recipes (exact commands + verification + gotchas): playbooks/add-platform-admin.md · playbooks/add-tenant-admin.md. For any common CLI task, see playbooks/.
Generic escape hatch
plexus-cli call parties:listForMainPage client=test q="Müller" --kind query
functions / describe read a committed snapshot (spec.snapshot.json).
Regenerate it with the tool's in-repo spec script (run from
app/tools/plexus-cli/) after adding/removing public functions:
pnpm spec # = node --import tsx scripts/regen-spec.ts (convex function-spec → public filter → spec.snapshot.json)
The snapshot drives display only (call builds its reference directly), so
staleness is cosmetic, not a correctness hazard.
Offer / admission CRUD
Model. An offer's identity = name + unit. The title is the bare program name
("Kosmetik"); the school/location is the pedagogic-unit link (the namespace).
Runs (Schuljahre) and levels (Klassenstufen) are settings of the offer. A
smartform and a workflow are optional later stages that make the offer
applyable — a draft offer needs neither.
Lifecycle: create (name + unit) → add runs / levels / capacity → [smartform → workflow → applyable].
Imperative CRUD
plexus-cli offer create --slug kosmetik-bs --title "Kosmetik" --unit "Morgenstern Schulen Braunschweig"
plexus-cli offer show --slug kosmetik-bs # title, unit, levels, runs, capacity, state
plexus-cli offer set --slug kosmetik-bs --title "Kosmetik" --unit "<unit>"
plexus-cli offer list [--all] # --all includes archived (soft-deleted)
plexus-cli offer level add --slug kosmetik-bs --name "Jahr 1" --grade 1
plexus-cli offer level rename --slug kosmetik-bs --level "Jahr 1" --name "Jahrgang 1"
plexus-cli offer level remove|restore --slug kosmetik-bs --level "Jahrgang 1"
plexus-cli offer run add --slug kosmetik-bs --name "2026/27" --start 2026-08-01 --capacity 20
plexus-cli offer run set|archive|restore --slug kosmetik-bs --run "2026/27"
plexus-cli offer capacity set --slug kosmetik-bs --run "2026/27" --level "Jahrgang 1" --capacity 15 [--open|--close]
plexus-cli offer delete|restore --slug kosmetik-bs # soft-delete (archive) / restore
Deletes are soft (archive) — restorable, never orphaning. offer delete,
offer run archive, and offer level remove set an archive marker: the row stays and
keeps resolving for anything that references it (offering cells, applications), but the
item is hidden from active lists and is not applyable in the portal. restore
clears the marker. There is no hard delete.
Declarative config-as-code (bundles, for promotion)
Offers are also versioned bundles on disk — use this for cross-env promotion or a reviewable whole-offer change:
offers/<tenant>/<offerSlug>/
bundle.yaml # index — module, tenant, slugs, label, links, program, pinned
manifest.json # == saveManifestDraft.data (incl. progressLayout)
smartform.json # OPTIONAL — == saveSmartFormDraft.definition (absent for a draft offer)
workflow.json # OPTIONAL — { graph, progressWiring } (absent for a draft offer)
plexus-cli offer export --tenant test --slug grundschule-havelland --out offers/test/gymnasium
# edit bundle.yaml (slugs/label/unit/stages/runs) + the JSON blobs
plexus-cli offer validate --dir offers/test/gymnasium # offline, real publish gate
plexus-cli offer plan --dir offers/test/gymnasium # remote↔local diff (smartform/workflow = ∅ when absent)
plexus-cli offer apply [--publish] --dir offers/test/gymnasium
plexus-cli offer status --tenant test --slug gymnasium-havelland # applyable?
A workflow-less draft exports/applies a manifest-only bundle (no
smartform.json / workflow.json). Apply order mirrors the seed
(init/seed/admission/seedAdmissionPortal.ts): manifest → scaffold (pedagogy-unit link
- program spine) → smartform (if present) → workflow (if present). Publish happens
only on a canonical-JSON diff (byte-equal blob = no-op). The imperative commands and
offer applygo through the same backend mutations — one write path, no duplication.
Known gap: a brand-new school with its own
pedagogy_unitsbridge has no public writer yet — new offers must reuse an existing pedagogy unit (links.unitName).
Architecture
core/ # GENERIC, brand-neutral, zero backend-type imports
transport.ts auth.ts jwt-cache.ts config.ts output.ts spec.ts command.ts
plexus/ # BRANDED — imports backend types + validators
branding.ts client.ts command.ts commands/* modules/admission.ts offer/*
cli.ts # clipanion bootstrap — registers every command group
See CLAUDE.md in this directory for the design rules (the thin-client /
"where does a change belong" principle).
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @types/js-yaml | ^4.0.9 |
| @types/node | ^22.10.0 |
| clipanion | 4.0.0-rc.4 |
| convex | ^1.38.0 |
| esbuild | 0.27.0 |
| js-yaml | ^4.2.0 |
| tsx | ^4.21.0 |
| typescript | ^6.0.3 |