ivo.zilkenat
  • Joined on 2025-10-29

plexus-cli (0.0.1)

Published 2026-06-13 11:52:37 +00:00 by ivo.zilkenat

Installation

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

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,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_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-13 11:52:37 +00:00
0
924 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