CARLOS rastrillo docs

๐Ÿค– The CLI

One binary, seven commands. Install it with:

go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest

Every command takes an optional directory as its last argument and defaults to .. Flags come before the directory. rastrillo help, -h and --help all print usage.

rastrillo new

rastrillo new [--icons=<set>] [--icon-delivery=<mode>] [--ux=<profile>]
              [--theme=<name>] [--shell=<name>] <name>

Scaffolds a complete app in ./<name> โ€” one that compiles, passes its own tests, and serves, before you have written anything. It runs generate once on the way out so go build works immediately.

Flag Values Default
--icons lucide, font-awesome lucide
--icon-delivery inline, cdn, js inline
--ux considered, standard considered
--theme day, plain, signal day
--shell column, topbar, sidebar, console column

All six set ร— delivery combinations scaffold, compile and pass generate --check. The icon set becomes an ordinary app-owned package under internal/<app>/icons, and --ux seeds a UX convention profile into AGENTS.md, which is the source of truth from then on โ€” nothing re-reads the profile name afterwards. Icons explains what each delivery mode costs, including the one worth repeating: with js, icons do not render at all without JavaScript.

--theme picks the colour, type and shape stylesheet written as static/theme.css, and --shell picks the page frame written as templates/layout.html. Both are copied in verbatim and are yours from that moment โ€” the theme like tokens.css, the layout like every other template. Every value is checked before a single file is created, so a typo fails with your working directory still clean. Templates describes the three of each.

--icons=font-awesome also writes the CC BY 4.0 attribution the licence requires, because that obligation is the app's and has to travel with the code.

rastrillo generate

rastrillo generate [--check] [--default-locale <code>] [dir]

Walks actions/ and manifest/ and emits gen/: the router on a Go 1.22 http.ServeMux, plus every declared resource's store, screens and locale keys. Fails loudly on route collisions.

--check verifies without writing, and is the pre-ship gate. It checks route collisions, action build tags, icon slugs that nothing answers, and i18n catalog completeness โ€” and, when the app has a cmd/genvectors, it runs the parity-vectors gate too, so vectors --check never needs a separate CI step. Only --check fails on an incomplete catalog โ€” plain generate, and so dev and new, never does. That split is deliberate: silent fallback while you iterate, loud failure before you ship.

--default-locale names the catalog every other catalog is compared against. It defaults to en and is not read from Options.DefaultLocale, so an app that sets a different default must pass the matching value here or the check compares against the wrong catalog.

rastrillo dev

rastrillo dev [dir] [-- app args]

The watch loop: polls app/, actions/, manifest/, cmd/, locales/ and templates/, and on any change reruns generate, rebuilds ./cmd/<name> to a temporary binary, and restarts the process with a graceful SIGTERM. Anything after -- is passed to your app.

A failed generate or rebuild keeps the previous build serving, and a failed restart keeps the loop watching โ€” either way the next save retries. It expects the rastrillo new layout: exactly one directory under cmd/.

rastrillo migration

Schema changes. The group is a noun on purpose: this CLI never applies migrations, because migrations run at boot โ€” a hibernating route has no operator moment between a new binary landing and the activator exec'ing it. baseline is the one exception, and it is manual by design.

Migrations is the guide; these are the commands.

migration generate

rastrillo migration generate [--allow-destructive] [dir]

Diffs your models against your migrations and writes the delta as a new numbered migration. Prints nothing to do when they already agree.

A change that drops data is refused unless you pass --allow-destructive, and the refusal prints the SQL it would have written so you can see exactly what it thinks is destructive.

Read the generated SQL before committing it. generate may emit a full table rebuild rather than an ALTER, and a rename is indistinguishable from a drop plus an add to any tool โ€” write those by hand with migration new.

migration check

rastrillo migration check [dir]

The CI gate: exits non-zero when models and migrations disagree, listing the SQL that would close the gap. Touches no database, so it runs anywhere. make ci in a scaffolded app runs it for you.

migration new

rastrillo migration new <name> [dir]

Writes a numbered stub migration for a change you will write by hand โ€” a rename, a data backfill, anything the differ cannot infer. The name must be lowercase letters, digits and underscores.

migration status

rastrillo migration status --db <path> [dir]

What a real database's ledger has applied, plus any pending drift. Requires --db because it reports on a database rather than on source. A database that has never booted this version gets a plain "no ledger yet" line rather than an error.

migration baseline

rastrillo migration baseline --db <path> [--through <id>] [dir]

Stamps a ledger by hand, for the case where boot refuses to adopt an existing database. It runs no DDL โ€” it only records migrations as applied.

Order matters, and getting it wrong loses a migration. Run baseline --through <id> first, stamping only up to the migration the database already matches, and then apply the missing one by hand; the next boot runs the rest. Bare baseline, with no --through, stamps everything: while the ledger is still empty any boot finds a matching schema and adopts it, recording every later migration as applied without running it โ€” which silently skips a pending data migration. That is what it is for, and it is why it is manual.

Migrations walks the whole recovery.

rastrillo doctor

rastrillo doctor [--fix] [--force] [--theme <name>] [dir]

Compares the files rastrillo new copied into the app's static/ directory โ€” tokens.css, theme.css, rastrillo.js, select.js and datetime.js โ€” against the copies this binary carries, and says which ones differ and how.

It is a convenience and an upgrade tool, not the thing standing between your app and silent drift. That is the vendored_test.go the scaffold writes, which runs in CI on every commit without anyone remembering to. What doctor adds is re-copying rather than telling you to, working on an app that never had that test, saying how a file differs rather than that it does, and running from outside the app โ€” which is what asking "is this one safe to upgrade?" about somebody else's repository needs.

Flag Purpose
--fix Re-copy each drifted file from this binary's library copy
--force With --fix: re-copy across a version mismatch, and over files recorded as deliberate edits
--theme Which theme static/theme.css should be, for an app with no pin to read it from

The version it compares against

The CLI carries its own compiled-in copy of the library; your app has its own required version in go.mod. These are frequently different, and that difference is not drift โ€” an app deliberately on v0.19.0 checked by a v0.20.0 binary has files that correctly match v0.19.0.

So doctor reads both, says which one it compared against, and makes a mismatch the first line of the report rather than a footnote:

rastrillo doctor is v0.20.0; this app requires v0.19.0.
Comparing against v0.20.0 โ€” upgrade the module first, or these differences are expected.

--fix refuses in that state. Copying v0.20.0 assets into an app that compiles against v0.19.0 produces exactly the fault this checks for โ€” new CSS against old markup โ€” with the difference that doctor would then call it clean. Upgrade the module first, or pass --force.

An app with a replace directive is not a mismatch: it builds against a checkout, so there is no second version to disagree with. doctor says which checkout and compares against its own copy, which is right only if this binary was built from that tree.

What it will not call drift

A hand-edited theme is a supported thing, not damage. If static/theme.css matches no shipped theme, doctor says custom or drifted and compares nothing โ€” it will not pick the closest theme and report a diff against a guess. Pass --theme <name> when you do want it compared against a shipped one.

A file you edited on purpose is the same: name it in vendoredIsMine in the scaffold's vendored_test.go and both that test and doctor leave it alone. Apps scaffolded before that map existed recorded the same thing by deleting the file's line from the pin, and doctor reads that too โ€” but it reads such a file rather than assuming. The first version of that pin listed three files, and theme.css and datetime.js joined the vendored set afterwards, so a name an old pin never mentions may simply mean "this app predates it". You delete a pin line to protect an edit, so the edit is the evidence: a file that is missing is absent and --fix delivers it, a file identical to the library is checked normally, and only a file that is there and differs is left alone as yours. That last rule is also what stops --fix from installing a file and thereby exempting it from every check afterwards.

A file you deleted is the same again. Dropping select.js from an app with no big selects is a supported choice, so an absent file is reported as absent, not as drift, and does not fail the exit code โ€” you get a line saying what the library ships and how big it is. --fix will still add it, because asking for --fix is asking.

Exit codes

Code Meaning
0 Every compared file matches
1 An error โ€” not an app, unreadable files
2 Usage
3 Drift: files differ from the library copy
4 The app and the CLI are on different rastrillo versions, so the comparison is not authoritative

Drift and version mismatch are separate codes because they call for opposite actions: one means "re-copy these", the other means "do not re-copy anything yet".

rastrillo markup

rastrillo markup [--fix] [dir]

Rewrites an app's markup from the class spelling of the UI vocabulary to the attribute spelling: <div class="rst-box"> becomes <div rst-box>, class="rst-callout__body" becomes rst-callout-body, class="rst-btn rst-btn--primary" becomes rst-btn="primary", and data-tone becomes rst-tone. Seven utility classes stay in class, because that is what class is for: rst-sr-only, rst-mono, rst-m-hide, rst-grow, rst-nm, rst-danger and rst-cell-mut.

It reads templates (.html, .htm, .gohtml, .tmpl), Go source (.go โ€” markup in a string literal or a doc comment), JavaScript (.js) and CSS (.css), and it is the same tool the framework flipped itself with. It skips .git, node_modules, vendor, .design-system and .superpowers.

It does not read Markdown

.md files are not scanned, deliberately. A Markdown file has no markup to migrate; it has discussion of markup โ€” an example in a code span, a migration table, a sentence naming both spellings so a reader can tell them apart. Rewriting that destroys it, and the damage does not look like damage: "class="rst-box" and <div rst-box> are identical" becomes a sentence claiming two identical-looking things are identical, and the diff reads fine.

The costs point one way. An example left in the old spelling is stale, visible and harmless โ€” you fix it when you next read the page. A rewritten explanation is gone, and the pages most likely to contain the string are the ones teaching the difference between the two spellings, so the tool would erase its own rationale first.

So the documentation is yours to update. Grep for class="rst- in your .md files after a migration and decide, page by page, which examples should now teach the attribute spelling and which are describing the history on purpose.

v0.22.0 did scan .md, and this is a change from it. On that release, rastrillo markup over a repository of pure documentation could report files to rewrite and exit 3 with no template anywhere in it. The count is a count of files with markup in them again โ€” which is what makes it the number to size the migration by.

Flag Purpose
--fix Write the rewrite. Without it, the command reports what would change and writes nothing
Exit Meaning
0 Nothing to do, or --fix finished with nothing left over
2 Usage
3 There is work here: a rewrite waiting for --fix, or a class attribute only you can take apart

Do the upgrade in one sitting, in this order:

rastrillo doctor --fix     # 1. re-copy tokens.css
rastrillo markup           # 2. read what would change
rastrillo markup --fix     # 3. write it
go test ./...

Step 1 is not optional. tokens.css is copied into your static/ at scaffold time and frozen there while the partials it styles keep upgrading, so an app on the old, class-only stylesheet whose partials have started emitting attributes renders unstyled. That is the one failure this staged migration exists to avoid.

Rewriting is idempotent, so running it twice, or over a tree half of which is already done, changes nothing the second time. Nothing breaks if you stop between steps 1 and 3, either: tokens.css styles both spellings until stage 3, and rastrillo.js dismisses menus written in either one. Step 3 is what you owe stage 3, not what you owe today.

One name changed meaning

rst-form-foot used to be the sticky save bar you wrote by hand, and rst-form__foot the plain closing row the form-foot partial emits. BEM's __ flattens to a hyphen, so both wanted the same attribute, and one attribute cannot carry two rules. The partial's row took the name the partial is called:

Was Is
class="rst-form__foot" โ€” the partial's closing row rst-form-foot
class="rst-form-foot" โ€” your sticky save bar rst-form-bar
class="rst-form-foot__note" rst-form-bar-note

This is the breaking change in the release. If you hand-wrote a save bar and do not run the codemod, your class="rst-form-foot" now means the plain row: no border, no background, and it no longer sticks. rastrillo markup --fix applies the rename, and prints a reminder on every run that changes anything.

The vendored files

It never touches static/tokens.css, static/theme.css, static/rastrillo.js, static/select.js or static/datetime.js. Those are copies of the library's, and doctor is what refreshes them; rewriting one here would make your copy differ from the library's for good.

The one opt-out

A line carrying markup-spelling: old-spelling begin starts a region the tool will not rewrite; markup-spelling: old-spelling end closes it, and an unclosed one runs to the end of the file. Use it in a file that is scanned but whose subject is the spelling you are migrating away from: the Go doc comment holding a before/after table, the HTML page of your own docs that shows both spellings side by side. Markdown needs no fence, because Markdown is not read at all.

What it reports instead of guessing

It leaves alone โ€” and prints โ€” any class attribute whose shape it cannot read: markup built by concatenating string literals, a class list a template assembles in a way it cannot take apart, or markup written as escaped text in a page of your own documentation. A wrong guess renders unstyled and looks like markup somebody wrote on purpose. Those keep the exit code at 3 until you have dealt with them, so a CI gate cannot mark a half-migrated app done.

Your own stylesheet

The report ends with the .rst- selectors in CSS you own โ€” every stylesheet except a byte-identical copy of the library's, and every <style> block in a template. Utility classes are left out; they are still classes and your rules still match them.

A rule you wrote against .rst-lrow stops matching the moment your markup says rst-lrow, and no test in your app will notice. Change them to attribute selectors โ€” .rst-lrow becomes [rst-lrow] โ€” which weigh exactly the same, so nothing in your cascade moves.

rastrillo vectors

Goโ†”JS parity vectors: the app's cmd/genvectors enumerates golden cases from the Go engine, this verb writes them to test/vectors.json, and the app's JS suite must reproduce every one. The derivation engine an app runs client-side exists twice by necessity, and two engines drifting is the E2EE bug class where a wrong answer looks fine.

rastrillo vectors [--init] [--check] [dir]

Plain vectors runs the app's own generator and writes its stdout to test/vectors.json โ€” a new root-level directory, chosen because the JS suite is neither a Go package nor a static asset.

Flag Purpose
--init Scaffold cmd/genvectors, the test/ parity suite, and the go-test belt into an existing app (once)
--check Pre-ship gate: regenerate, byte-compare, then run the JS parity suite with node --test test/parity.test.mjs

generate --check runs this same gate automatically when cmd/genvectors exists โ€” one gate before ship, not two to remember; CI that already runs generate --check needs no extra step.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.