CARLOS rastrillo docs

๐Ÿค– The CLI

One binary, eleven commands. Install it with:

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

Every command except session-key takes an optional directory as its last argument and defaults to .. session-key takes a key id instead. 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>] [--locale=<code>]
              [--auth=<kind>] <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. Then it runs go mod tidy in the app and compiles its templates, so the app starts with its rastrillo_templates_gen.go. If that cannot run, for example offline, new still succeeds and prints a line telling you to run rastrillo generate after go mod tidy.

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
--auth none, magic none
--locale en, ga, zh-Hans, es, hi, pt-PT, bn, ru, ja, yue, vi, ar en

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.

--auth=magic starts the app with sign-in by emailed link: the magic links plugin with its sign-in screen, a Sign in and Sign out control in the header, and an example page only a signed-in person can open. Rows belong to the signed-in address, read and written through the owned seam in auth.go. Until you set <APP>_SMTP_HOST and <APP>_SMTP_FROM, each link is written to the log instead of sent. The app's own tests sign in through the emailed link, so make ci passes from the first commit.

--locale picks the one language the app starts with, written to manifest/app.toml. It must be one the framework's own strings ship in, and pt means pt-PT. Anything else fails before a file is written:

rastrillo new --locale "fr": the framework's own strings ship in en, ga, zh-Hans, es, hi, pt-PT, bn, ru, ja, yue, vi, ar. Start with one of those, then add your language to manifest/app.toml

The app offers that one language, with no /xx/ prefixes and no switcher. Add more later; see Localization.

rastrillo generate

rastrillo generate [--check] [--accept-breaking] [--templates=false] [dir]

Walks actions/ and manifest/ and emits gen/: the router on a Go 1.22 http.ServeMux, every declared resource's store, screens and locale keys, gen/boot.go, and gen/locales/packs.go, which links the formatting packs for the languages in manifest/app.toml. Fails loudly on route collisions.

It removes any file in gen/ that an earlier run wrote and nothing writes now, such as the screens of a resource you deleted, and prints a line for each: removed gen/templates/ticket_types/confirm.html: generated by an earlier run, and nothing generates it now.

--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.

--check compares every catalog with the one for default_locale in manifest/app.toml. The --default-locale flag was removed, and passing it fails with --default-locale was removed: the default comes from default_locale in manifest/app.toml.

generate also writes api/v1.json at the app root: every route /api/v1 serves, with its arguments and result fields. Commit it. Before writing, generate compares the file it would write with the committed one. A change that only adds (a new route, a new optional argument, a new result field) is written. A change that breaks a client (a route or field removed, an argument newly required, a kind changed) fails, names each break, and leaves the file as it was. To rewrite it over a break on purpose, run rastrillo generate --accept-breaking; the diff shows the break in review. --check fails if the file is missing or stale, and never writes it.

Last, generate compiles your templates to Go, manifests or not. It starts each program under cmd/ whose func main calls rastrillo.Run or rastrillo.Serve, records the template sets it builds, and writes rastrillo_templates_gen.go into each package that builds one. Commit those files. Anything your program does before that call still runs during generate. Nothing after it does: capture mode exits the process after rastrillo.Serve writes the capture. generate never starts any other program, so a migration or a mail-out under cmd/ does not run. A template it cannot compile, a program that cannot start this way, and a program it does not start get one line each, and those templates render through html/template as before. See tmpl. Pass --templates=false to skip this step.

--check fails when a rastrillo_templates_gen.go is missing or out of date with your templates. It also fails when a program cannot start this way and imports a package with a rastrillo_templates_gen.go, because it cannot check that file. Fix what stops the program, or run --check where it can start.

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.

dev leaves compiled templates out. It runs generate --templates=false and builds with -tags rastrillo_dev, so every page renders through html/template from the files on disk, and a compiled file that no longer fits your code cannot break the build. Run rastrillo generate before you commit: generate --check fails until you do.

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>] [--probe] [dir]

Compares the files rastrillo new copied into the app's static/ directory (tokens.css, theme.css, rastrillo.js, select.js, datetime.js and light.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 or removals
--theme Which theme static/theme.css should be, for an app with no pin to read it from
--probe Also build the app and run it to check its language setup. This runs your app's own code

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.

An app on the old module path

Until v0.25.0 rastrillo's module path was github.com/carlosframework/rastrillo. An app that still imports from there is stuck at v0.23.0. It gets no fix released since, and its own vendored_test.go still passes, because it checks against the same old module. go get github.com/carlosframework/rastrillo/cmd/rastrillo@upgrade fails with "module declares its path as: amadan.net/rastrillo/rastrillo".

doctor checks for this first. If the app's go.mod requires the old path directly, it compares no files, prints the steps below and exits 5. --fix refuses, even with --force.

The app's own CLI is on the old path too, so run a current one:

go run amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest doctor

To move the app, run these from its root on a clean tree:

git grep -lz github.com/carlosframework/rastrillo -- ':(glob,exclude)**/go.mod' ':(glob,exclude)**/go.sum' |
  xargs -0 perl -pi -e 's#github\.com/carlosframework/rastrillo#amadan.net/rastrillo/rastrillo#g'
go mod edit -droprequire=github.com/carlosframework/rastrillo \
  -droptool=github.com/carlosframework/rastrillo/cmd/rastrillo \
  -tool=amadan.net/rastrillo/rastrillo/cmd/rastrillo
go get amadan.net/rastrillo/rastrillo@latest
go mod tidy
go tool rastrillo doctor --fix

The text rewrite leaves go.mod alone on purpose. Many apps already require the new path, and rewriting would add a second requirement on it at v0.23.0, a version that does not build under the new path. Then read the changelog for each release since the one you were on: some change markup your app keeps a copy of.

If other modules sit inside the app, doctor names the ones on the old path. The text rewrite changes their imports too, so run the go mod steps in each of them.

Templates and locales

After the static files, doctor checks every app for two things this release needs, under the headings Templates: and Locales:. --fix skips both.

Templates: runs rastrillo migrate-templates on the app without writing: every template still in the old syntax, and every Go file that registers its own T. Exit code 6 means there is work left. See Upgrading templates.

Locales: checks that gen/locales/packs.go matches manifest/app.toml, and that each language you offer has the catalog it needs. It does not run your app. Exit code 7 means the app would refuse to start.

To also check the language setup by running the app, add --probe. doctor then builds the app's command, cmd/<app>, and runs it with RASTRILLO_CHECK_CONFIG=1. The app checks its language setup, prints what it found and exits. This runs your app's own code first: anything its main or an init function does before it reaches rastrillo still happens. Use --probe where that does no harm, such as a checkout with no live database. An app on a rastrillo from before the probe is not run, and doctor says so.

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
5 The app imports rastrillo from its old module path; nothing was compared
6 templates still use the old syntax: run rastrillo migrate-templates
7 the app's locale configuration would stop it starting

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".

When more than one applies, doctor exits with the first of 5, 4, 3, 6 and 7. It prints every section either way.

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, static/datetime.js or static/light.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 migrate-templates

rastrillo migrate-templates [-write] PATH...    move templates to the localizer-first syntax; exits 1 while anything is left to do

Adds the page's localizer, $L, to every template under the paths you give it. Without -write it changes nothing and reports what it would do. Upgrading templates has the whole procedure.

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.

rastrillo budget

rastrillo budget size [dir]
rastrillo budget code [--init | --lower] [dir]
rastrillo budget test [-no-time] [-require TestA,TestB] [go test arguments]

budget size counts the lines in each directory of the module at dir (default .) and holds them to 5,000 lines of code and 8,000 of tests, minus the exceptions in .rastrillo/budgets.txt. It leaves out generated files, the ones whose first lines say // Code generated ... DO NOT EDIT.. It stops at a nested module, which has its own budget. It exits 1 when something is over and 2 when budgets.txt cannot be read.

budget code holds the Go code of the module at dir (default .) to four limits. A line is at most 80 columns, counting a tab as up to 4. A file is at most 400 lines, and a function at most 40, or 15 for an action. No name is a single letter, except t, b and f with their testing types, and a one-capital-letter type parameter. Tests are held to the columns and names limits only. Generated files are skipped, and a file marked //rastrillo:data <reason> above its package clause is a table, so it is not held to columns or length. A file marked //rastrillo:vendored <source>, a copy of code from elsewhere, is skipped like a generated one.

.rastrillo/code.txt records the offences the code already had. A new offence fails, and so does a record that is looser than the code: lower it, or delete it when the offence is gone. --init writes the file with a record for every offence, and refuses if the file exists. --lower lowers and deletes records to match the code, and never adds or raises one. To add or raise a record by hand, write a reason after its count. The check compares the file with the copy where your branch left origin/main, and fails a new or higher record with no reason. It exits 1 when something is over a limit and 2 when code.txt cannot be read or --init finds one already there.

budget test runs go test -json with your arguments (./... if you give none), and exits with go test's own status. It holds each package to its time budget using the line budget.Main prints, fails a package that ran tests without printing it, and ends with a receipt. -no-time turns the time budget off, for the perf lane. -require names tests that must pass in the run, so a lane cannot go green having run nothing.

The scaffold's make budget, make test and make perf call budget size and budget test. Testing explains the budgets.

rastrillo routes

rastrillo routes [--package path] [--check] [dir]

Prints the routes of the app at dir (default .), one a line, in the order they are registered: the method, the full path, the action and the guards. The last line is a reminder of what guards do not decide.

GET   /           app.(*App).index
GET   /tags       app.(*App).tagList
POST  /tags/{id}  app.(*App).tagRename  signedIn
GET   /admin      app.(*App).adminHome  admin
*     /api/*      api

Guards are page middleware. An operation's Authorize can still refuse a request that passes them.

It does not start the app. It calls the app's Routes function with an empty App and a table that only records. Write the function in a package under internal/:

func Routes(routes *action.Routes, app *App)

App can be any exported type in that package, or a pointer to one. Routes must only register routes: no queries, no network calls, nothing built. See action.

To call it, routes builds a small program inside your module with the go command's -overlay, and runs it. Nothing is written into the app, and go.mod and go.sum stay as they are. What the app prints while it starts goes to standard error, so the table on standard output stays clean.

If your module has vendor/modules.txt, routes builds with your vendored code, as go build does, and needs no network. Otherwise it uses the module cache and changes neither go.mod nor go.sum.

Flag Purpose
--package The import path of the package whose Routes to call, when more than one package has one
--check Also fail for each action no route mounts

--check finds every action in the module: each function or method that takes one *action.Visit and returns nothing. It names each one that no route mounts. Mount it in Routes, or delete it. An action mounted through an interface value counts as not mounted: mount the method on its type instead.

routes refuses to run, and exits 1, in three cases:

Exits 1 when the routes cannot be listed, or when --check finds an action no route mounts, and 2 when the arguments are wrong. Ctrl-C stops it and removes what it built.

rastrillo session-key

rastrillo session-key <kid>

Makes a new signing key for signed sessions and prints it as two lines:

secret <kid>.<private key>
public <kid>.<public key>

Store the value after secret on the hub, with carlos secrets set. The hub signs session cookies with it. Set the value after public on every tenant, with carlos env set. Tenants check cookies with it.

The kid is 1 to 32 characters from a-z, 0-9, _ and -.

To rotate, make a key with a new kid and follow the rotation order.

Exits 2 when the kid is missing or not valid, and prints nothing on standard output.

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