plexus-cli (0.0.2)
Installation
registry=npm install plexus-cli@0.0.2"plexus-cli": "0.0.2"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,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 {export,validate,plan,diff,apply,list,status} |
Every command supports --json (raw output; default is a table). Network commands
take --env / --tenant. functions / describe / offer validate / config
work offline (no deployment).
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 config-as-code
Configure admission offers (Angebote) as versioned bundles on disk:
offers/<tenant>/<offerSlug>/
bundle.yaml # index — module, tenant, slugs, label, links, program, pinned
manifest.json # == saveManifestDraft.data (incl. progressLayout)
smartform.json # == saveSmartFormDraft.definition (incl. testScenarios[])
workflow.json # { graph, progressWiring } == saveWorkflowDraft args
Typical loop:
plexus-cli offer export --tenant test --slug grundschule-havelland --out offers/test/gymnasium
# edit bundle.yaml (new 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
plexus-cli offer apply --publish --dir offers/test/gymnasium
plexus-cli offer status --tenant test --slug gymnasium-havelland # applyable?
Apply order mirrors the seed (init/seed/admission/seedAdmissionPortal.ts):
manifest → scaffold (pedagogy-unit link + program spine) → smartform → workflow.
Publish happens only on a canonical-JSON diff (byte-equal blob = no-op).
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 |