cli
Operator CLI for Neon Law Navigator (binary name: navigator; the crate is still cli, so cargo run -p cli -- …
works unchanged). Validates markdown templates against the rule engine, imports clean files into the same SeaORM-managed
Postgres web reads from, seeds canonical reference data, prints rows, renders an ER diagram for the schema, and — over
a browser-loopback login — drives a live site's matter flow against a short-lived bearer token.
Guarded staging lifecycle
navigator dev staging up|reset|status|down manages local KIND staging. Reset and down require
NAVIGATOR_ENVIRONMENT=staging, a managed namespace, and its immutable environment ID.
Getting started
# DB-free subcommand: works on any laptop, no Postgres required.
cargo run -p cli -- validate templates
# DB-touching subcommands take --database-url, falling back to
# the DATABASE_URL environment variable.
export DATABASE_URL=postgres://navigator:navigator@localhost:15432/navigator
cargo run -p cli -- import templates
cargo run -p cli -- list templates
cargo run -p cli -- docs erd | head
# Or install on your PATH
cargo install --path cli
navigator --help
Subcommands split by whether they need a database:
| Subcommand | Needs DB? | Notes |
|---|---|---|
validate | no | Lints every .md, runs typed event checks, parses YAML; N104 seed check. |
validate-no-client-data | no | Flags client emails/phones in store/seeds, templates, web/content. |
render | no | Validation-gated template → PDF; --format letter. |
format | no | Whitespace + bullet cleanup on one .md. |
docs list | no | Lists published docs pages plus glossary term anchors. |
docs glossary | no | Looks up workspace vocabulary by term. |
scaffold | no | Drops template + workflow + feature stubs. |
assets build | no | Transcodes source photos into AVIF/WebP/JPEG. |
assets upload | no | Pushes public image files to the public assets bucket. |
assets fonts upload | no | Pushes licensed GORP Regular and Bold WOFF2 files to the assets bucket. |
assets pull | no | Restores web/public/img/ from the assets bucket for local dev. |
assets verify | no | Checks every content img/ reference is published at the public origin. |
assets stub-referenced | no | Writes tiny local placeholders for content img/ references. |
forms sync | no | Vendors blank government forms to the assets bucket; writes/verifies pins. |
forms fields | no | Prints a pin-verified blank's AcroForm /T names, one per line. |
forms re-author | no | Rewrites a blank's /T names to questionnaire state paths from its map. |
import | yes | Writes into --database-url Postgres. |
list | yes | Auto-runs migrate + seed before printing. |
docs erd | yes | Introspects pg_catalog + information_schema. |
project create | yes | Requires entity, client DRI, and product; seeds DRI participation. |
The live-site commands need no local database — they are an authenticated HTTP client against a deployed web:
| Subcommand | Route hit | Notes |
|---|---|---|
auth login | GET /auth/cli/start | Browser-loopback OAuth → ~/.navigator.json (0600). |
auth logout / auth whoami | (local) | Forget / inspect the stored token; whoami does the expiry math locally. |
projects list | GET /staff/projects.csv | Rendered as a table, or --json. |
project open PROJECT_CODE | GET /staff/projects/:id | Open a visible existing matter workbench. |
notation create | POST /staff/projects/:id/notations/new | Create a Notation; parks at q1. |
intake answer | GET/POST …/step | Walk the questionnaire (interactive, --answer/--person, --transcript). |
retainer clause | …/clauses | add / edit / list the per-matter clauses spliced into the retainer. |
retainer approve | POST …/approve-send | Renders + parks the PDF at generate_pdf__retainer_pdf; no envelope. |
retainer send | POST …/send | One real envelope on prod; deliberate human command. 409 until rendered. |
notation status | GET …/review?format=json | Workflow state, signature request id, document_ready. |
notation approve | POST …/approve-send | Render + park the bound packet (formation form or retainer). |
notation request-changes | POST …/request-changes | Flag wrong answers (--question/--note); → reask. |
notation update | POST …/reask | Re-collect flagged answers (--answer code=value); resubmit for review. |
notation document | GET …/documents/document | Download the rendered (filled) packet to --out <path>. |
Local developer orchestration is nested under dev; these commands are reversible and target the KIND loop:
| Subcommand | Notes |
|---|---|
dev up | Bring up the shared KIND dependency tier and write .devx/env; up remains a top-level alias. |
dev down | Full local teardown; routine handoff leaves the shared dependency tier running. |
dev env / dev status | Print host-side env vars or inspect the cluster and port-forward state. |
dev kind up / dev kind down | Create or delete only the KIND cluster and ingress/operator base. |
dev worktree-env | Prepare a topic worktree with its own host web port on the shared dev topology. |
dev deploy / dev undeploy | Pull the published images into KIND or remove the navigator namespace. |
dev e2e / dev grant-staff / dev logs | Local smoke test, staff-user seed, and navigator-web log tailing. |
dev browser-e2e | Reproduce deploy.yml's browser gate (browser_e2e + accessibility_e2e, pinned Chrome). |
dev kustomize kind / dev kustomize gke | Render local or example production overlays. |
dev i18n | Reconcile the English i18n catalog against the Rust call sites; reports missing/unused keys. |
Production and cloud operations are nested under ops; treat them as operator commands with real blast radius:
| Subcommand | Notes |
|---|---|
ops ship | Roll GKE deployments, trigger images, and Restate registration onto one published YY.M.D tag. |
ops gcp | Provision project infrastructure and manage IAP IAM helper calls. |
ops github setup | Reconcile GitHub's merge gate and DevX labels; --dry-run prints the plan without writes. |
ops restate | Register the public workflows-service URL with Restate Cloud. |
ops doctor | Read-only cluster diagnostic for wedged jobs and unready workloads. |
ops dns / ops rebrand / ops observability | DNS records, brand pack, and production telemetry setup. |
Driving a live site
navigator auth login mints a short-lived (~8h) bearer token like gcloud auth login does — it opens the browser,
reuses the site's existing OIDC session, and lands the token on a 127.0.0.1 loopback listener. The token is the same
HMAC-signed session blob the browser cookie carries, presented as Authorization: Bearer; the server resolves it back
into the caller's session, so every command runs the same handler — and the same staff_review gate, role check, and
authored_by provenance — the browser does. Sending a retainer for signature stays a deliberate authenticated human
command (retainer send); it is never exposed as an LLM-routable tool.
The send is a durable two-step. retainer approve fires approved, the worker durably renders + persists the retainer
PDF, and the workflow parks at generate_pdf__retainer_pdf — no envelope yet. retainer send then confirms the PDF
is present (notation status shows document_ready:true) and dispatches exactly one envelope. Splitting the two is
what makes the pipeline safe against a real worker whose render is a separate durable invocation: send returns 409
with a JSON reason — not an opaque 500 — when the PDF isn't ready yet, so the operator retries rather than racing.
navigator auth login --host www.neonlaw.com # browser → ~8h token, stored 0600 at ~/.navigator.json
navigator auth whoami # "nick@neonlaw.com (admin) — expires in 7h52m"
navigator projects list # table (or --json)
navigator project open shook-estate # prints the matter workbench URL if visible
navigator notation create onboarding__retainer \
--project shook-estate --client-email nick@shook.family
navigator retainer approve <notation-id> # renders + parks the PDF (no envelope)
navigator notation status <notation-id> # state + signature request id + document_ready
navigator retainer send <notation-id> # dispatches one real envelope (409 until document_ready)
navigator auth logout
--host is optional after a single login (the sole stored host is used); pass it to pick between prod, staging, and a
local http://localhost:8080 KIND run, each keyed separately in the credential file.
Creating a matter or notation — project create or notation create — first runs the conflict check before any row is
written. The CLI is non-interactive, so any finding (a hard adverse block or a softer review item) refuses the open;
resolve it in the portal, where authorized staff can review and acknowledge the findings, then retry. The graph itself
is documented in the glossary. project open PROJECT_CODE does not create
anything; it only opens an existing matter the stored token can already see.
The DB-backed project create command also requires --product-code <code> so every new matter is correlated to an
active service in the product catalog before any notation is added.
Forming an LLC from the CLI
A person can form a Nevada LLC end to end without opening a browser. notation create starts a questionnaire-driven
onboarding__* Notation inside an existing matter; intake answer then walks the questionnaire one question at a time
over the same /staff/notations/:id/step route the browser POSTs. The CLI reads each question's prompt, answer_type,
and (for a radio) its choices from that route's ?format=json branch — it never scrapes HTML — and posts a
people_list answer as the widget's p{row}_{part} fields.
notation create --project <project-code> resolves the matter through the projects visible to the stored token. The
template is read from that Project's git repo (templates/<code>.md at HEAD) when authored there, else from the bundled
firm catalog, then auto-saved as an immutable project-scoped version and opened pinned to it — one front door, no
separate import step. The template is validated first and creation refuses loudly if it has any blocking rule
violation.
In interactive mode intake answer shows one prompt per question — a radio lists its choices, and a people_list is
entered row by row (a blank name ends the rows).
Pass --transcript <file> to pre-fill the walk from a recorded sitting. Batch coverage runs server-side first: the
questions the transcript answers are seeded as proposed answers (source = extracted) that the walk then offers as
Enter-to-accept defaults — never silently accepted — and the questions it did not cover still prompt. It is an input
mode of the same walk, so the interactive or scripted loop runs afterward either way.
navigator intake answer <notation-id> --transcript /tmp/sitting.txt
navigator auth login --host http://localhost:8080
navigator notation create nv__llc_formation --project libra --client-email libra@example.com
navigator intake answer <notation-id>
navigator notation status <notation-id>
navigator notation approve <notation-id>
navigator notation document <notation-id> --out /tmp/llc.pdf
To script it (no prompts), answer non-interactively — scalar answers in the order the questionnaire asks, and one
--person per people_list row:
navigator intake answer <notation-id> \
--answer "Libra" --answer "libra@example.com" --answer "Bright Star Ventures" \
--answer "Neon Law Registered Agent" --answer "members" \
--person 'name=Libra,street=1 Main St,city=Las Vegas,state=NV,zip=89101,country=USA' \
--answer "2026-07-01"
A clean staff-entered walk auto-renders the packet on the last answer and drives the matter to the signature wait, so
notation approve is an idempotent confirmation rather than a separate render step; notation document then downloads
the same per-notation PDF the review surface shows. The whole round-trip is proven against an in-process web app in
tests/llc_formation_e2e.rs.
Public image assets
assets build resizes + re-encodes the curated source photos (manifest: views::assets::GALLERY) into responsive AVIF,
WebP, and JPEG width variants under web/public/img/<slug>/. Standalone blog or illustration heroes can be dropped
directly under web/public/img/<slug>/ as PNGs. assets upload then pushes that tree to the public assets bucket
(--bucket, default NAVIGATOR_ASSETS_BUCKET) through the cloud crate's StorageService, stamping a bounded
Cache-Control (~1 week, never immutable). assets pull is the inverse — it downloads the published files from the
bucket back into web/public/img/ so a fresh clone (or any developer without the source JPEGs) can serve the images
locally. assets fetch-referenced downloads only the img/… paths referenced under web/content from a public HTTP
origin (no GCP ADC) into web/public/ for a developer or operator who wants the real local bytes. assets stub-referenced writes tiny valid placeholders at those same paths for CI image packaging checks after the real public
origin has been verified. assets verify closes the loop: because web/public/img/ is gitignored and nothing in git
carries the bytes, it fetches every img/… image reference under web/content from an explicit origin and fails if any
404s. The deploy integration job runs it twice after dev e2e: against the public bucket (production publication gate)
and against http://localhost:8080/public (KIND local serve gate).
# Curate the gallery (needs the source JPEGs):
cargo run -p cli -- assets build # /tmp sources → web/public/img
cargo run -p cli -- assets upload # web/public/img → gs://<project>-assets/img
# Confirm every content image reference is live before shipping:
cargo run -p cli -- assets verify # web/content refs → GET <NAVIGATOR_ASSET_BASE_URL>/img/…
cargo run -p cli -- assets verify --base-url http://localhost:8080/public
# Hydrate referenced heroes into web/public with the real published bytes:
cargo run -p cli -- assets fetch-referenced --base-url "$PUBLIC_ASSET_ORIGIN"
# Write local placeholders after the real public origin has been verified:
cargo run -p cli -- assets stub-referenced --out web/public
# Restore images on a fresh clone (no source JPEGs needed):
cargo run -p cli -- assets pull # gs://<project>-assets/img → web/public/img
First-run note.
web/public/img/is gitignored — the image files ship from the bucket in production, never from git. A fresh clone therefore has empty image slots until you populate them. Runassets pullto download the already-published files (no source JPEGs, no re-encode), orassets buildif you have the responsive photo sources. This is intentional and matches how workshop/marketing assets are handled; everything else underweb/public(Bootstrap, brand SVGs) is tracked and renders immediately. WithNAVIGATOR_ASSET_BASE_URLunset the page markup resolves images against/public, so once the directory is populated the KIND dev loop serves them with zero configuration. Full pipeline:docs/assets.md.
Licensed webfonts
GORP Serif WOFF2 files are operator assets, not repository assets. A licensed operator supplies its own Regular and Bold
files, then publishes them to fonts/gorp-serif/ in the same public assets bucket:
cargo run -p cli -- assets fonts upload --dir '/path/to/GORP Serif/WOFF'
The command uses NAVIGATOR_ASSETS_BUCKET unless --bucket is supplied and refuses a partial pair. Production reads
the public origin from NAVIGATOR_ASSET_BASE_URL; local development can use ignored copies under
web/public/fonts/gorp-serif/.
Navigator source code is Apache-2.0/MIT, while GORP Serif is licensed separately from TrashType.
What's next
cli's shipped binary depends on rules and store — no web dep, so it stays small and starts instantly. (web,
workflows, and pdf are dev-dependencies only, for the in-process end-to-end test in tests/llc_formation_e2e.rs
that drives the binary against a real app on a loopback port; they never link into the shipped binary.) Integration
tests under tests/ drive the compiled binary end-to-end via assert_cmd / CARGO_BIN_EXE_navigator against per-test
Postgres schemas spun up via store::test_support. To add a subcommand, extend the Command enum in src/main.rs and
wire it to a module.