Keep routes in sync

pithy ui sync --worker api

Re-derives the asset-routing allowlist from that Worker’s composed route table, and rewrites that one key.

It touches nothing else — no file is created, no dependency moves, no scaffolded screen is regenerated. Idempotent, and a run with nothing to change says so.

When the list goes stale

The list exists because of one origin, one deploy; pithy ui sync rewrites it; and a CI gate is what stops a stale one shipping.

The allowlist is derived from the route table, and the route table changes:

WhenRuns a command
pithy add <capability>Yes
pithy remove <capability>Yes
You mount a route in your own app capabilityNo

That last row is how the list goes stale without anybody touching it.

What a stale list actually does

A shadowed route answers a 200 of the SPA shell, with your handler never invoked.

Not a 404. Not an error. A 200, with HTML, to a caller expecting JSON.

And no test suite of yours sees it, because tests call handlers directly and the asset router is never in the picture. So the first place it shows up is a client that cannot parse a response, in production.

The CI gate

pithy ui sync --check --worker api

Writes nothing, reports what the list no longer covers, and exits non-zero.

$ pithy ui sync --check --worker api
api: the SPA shell is answering these, not the worker.
  /api/cli/device/start
  /api/organizations
Run pithy ui sync --worker api.

Put it beside pithy doctor. Both catch failures a unit test structurally cannot.

What it never reports

A route the allowlist cannot express — a catch-all, or one mounted at the root.

Those are paths the derivation deliberately leaves to the shell, and a check that flagged them would mark every project drifted forever.

The derivation, in three rules

Two forms per entry — the bare path and its glob — because a glob does not match the bare path.

Never a bare-prefix glob. A prefix glob also captures a longer word beginning the same way; the pair captures the table exactly.

Once per environment, unioned. A Worker composes differently per environment — one capability mounts a route only in dev — so a single composition would produce one environment’s table and call it the Worker’s.

The CI variable is ignored while deriving, so a check in CI and a sync on a laptop derive the same list from the same repository.

The edit is a one-line diff

Every file this touches is checked into your repository, so the writer emits what your formatter would — short arrays on one line, objects in whatever shape they already had, comments where you put them.

So a sync that adds one path is a one-line diff, and its output passes your formatter with no step of your own.

It did neither once: the writer expanded every array in the file, so a two-line change arrived as seventy-eight insertions and then failed the pre-commit hook the CLI itself installs.

What it reports

KeyMeaning
before, afterThe allowlist either side, so CI can log the delta without recomputing it
changedWhether anything moved — the list, or a fallback setting it had to write
uncoveredAlways empty after a write. Under --check it is the finding, and the one thing that fails the exit
notFoundHandlingReported because SPA routing depends on it, and sync does not overwrite a value you chose
ESC