ivo.zilkenat
  • Joined on 2025-10-29

plexus-cli (0.0.8)

Published 2026-06-18 13:49:19 +00:00 by ivo.zilkenat

Installation

registry=
npm install plexus-cli@0.0.8
"plexus-cli": "0.0.8"

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/npm instead.

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:

  1. 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.
  2. no-explicit-any everywhere — the call surface is fully typed; the only unavoidable casts are as unknown as T (admin auth, the generic call).
  3. Offline validation against the real publish gateoffer validate runs the same runWorkflowValidators / runFormValidators the 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,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 platform 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 "…" [--password …] — the credentialed, ready-to-log-in path: it creates the Better Auth credential too, with a password you give or a server-generated one (returned once; capture it with --output creds.csv --quiet). Idempotent — re-running rotates the password. Refuses an email that already has tenant access (the _platform.* XOR tenant-access invariant). Needs _platform.users.manage — i.e. your own platform-admin token, no Convex admin key. This is the platform twin of users provision (which is the same idea for a tenant user).

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 apply go through the same backend mutations — one write path, no duplication.

Known gap: a brand-new school with its own pedagogy_units bridge 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
Details
npm
2026-06-18 13:49:19 +00:00
0
999 KiB
Assets (1)
Versions (29) View all
0.0.29 2026-06-24
0.0.28 2026-06-24
0.0.27 2026-06-24
0.0.26 2026-06-24
0.0.25 2026-06-23