cleanup
Normalize every deck, collection, and wanted list file in the workspace to the current conventions. Run it after hand-editing files, importing from older tools, or upgrading a workspace whose files predate a convention change.
One pass over all list files applies five normalizations:
-
Every deck gets a
format:. For each deck whose front matter declares no format, you are prompted to pick one. The deck’s shape orders the choices so the likely answer is at the top:- A command zone (a
## Commander,## Oathbreaker, or## Signature Spellsection) lists the command-zone formats first (Commander, Oathbreaker, Brawl, Duel Commander, …), with a note that a commander was detected. - 60+ main-deck cards with no commander lists the 60-card constructed formats first (Standard, Modern, Legacy, Vintage, …).
- 40–59 main-deck cards with no commander lists Limited first.
Cancelling the prompt leaves that deck’s file untouched (it is not rewritten either) and moves on.
- A command zone (a
-
Every file is rewritten in canonical form. Each list is loaded and re-emitted through the standard serializers, so formatting converges on what a fresh save would write (see List Files): a
-bullet on every card line, canonical token order and spacing, uppercase set codes, omitted default finish/condition/language markers, an explicit## Sectionstructure, a# Titleheading on all three types, and&Ncard IDs on every line. A collection or wanted-list line that carries a quantity (- 4 Lightning Bolt (LEA:161)) is expanded to one line per copy; the first keeps the line’s&N, the rest get fresh ids. A deck whose name still lives in legacyname:front matter gets it as the# Titleheading, andname:andcreated:are dropped (tags:and every other key stay). -
Every file is named after its list. The file name is derived from the list’s name (its
# Titleheading, or for a deck without one, its legacyname:front matter), keeping capitalization and punctuation and stripping only filename-illegal characters. A file whose name drifted (for example, an old kebab-case slug) is renamed, and its.sha256,.changes.md,.art.json,.categories.json(with its own.sha256), and.primer.mdcompanion files move with it. -
Every list’s categories file is pruned and canonicalized. A
<name>.categories.jsonentry naming a card the list no longer holds is dropped (reported aspruned 1 category assignment(s) for cards no longer in the list: Rhystic Study). The vocabulary’s display order is resolved and persisted, and the file is re-serialized in canonical form (categories sidecar rewritten in canonical form). Under--dry-runboth are previewed without writing. A file whose parse skipped lines is never pruned, since a card the parser could not read is still in the file; its categories file is only canonicalized, andcategories not pruned: …says so. The.sha256rule below applies to this file as well. -
Every repeated-face card name is folded onto its card. A line spelled the way Scryfall names a reversible printing,
- Steam Vents // Steam Vents (ECL:348), is renamed to the card it prints,- Steam Vents (ECL:348), which is the name the card cache files that printing under. The line keeps its printing, finish, and&N. Its category assignments move to the folded name, merged after any the card already has. The run reportsfolded 1 repeated-face card name(s) onto the card they print: Steam Vents // Steam Vents, and the JSON result lists the original spellings incardNamesFolded. A file whose parse skipped lines is left alone.
Cleanup never touches a .changes.md changelog: a cleaned-up file has the same cards it had before, and a rename carries the changelog along unchanged. It refreshes a file’s .sha256 content hash (and, separately, the categories file’s own) only when that hash already matched the file. A hand-edited list is rewritten but keeps its stale or absent hash, so detect-changes still records the hand edits.
Two cases are reported with a warning instead of fully acted on:
- A rename whose target name is already taken by another list, either the same file name or one that folds onto it. Renaming would leave both lists unaddressable.
- A file holding content the canonical rewrite cannot reproduce: lines the parse skipped (refused card lines, prose, or any other text the list grammar does not model) and fenced code blocks, which parse cleanly but which the serializers do not emit.
//comment lines are read and dropped, so they never block.
In either case the file is still renamed if its name drifted, but its content is left alone. Rewriting it would silently drop that content. Fix, remove, or accept it and rerun.
A file cleanup cannot read at all (broken YAML front matter, bad permissions) is reported by name and skipped. Nothing about it is rewritten or renamed, every other list is still cleaned up, and the run exits 1:
[dry-run] decks/Broken.md: warning: could not be read: unexpected end of the stream within a flow collection at line 3, column 1[dry-run] decks/Broken.md: warning: skipped: fix the file and rerun cleanup1 file could not be read and was skipped (see the warnings above).Its per-file JSON result carries "unreadable": true.
Two further cases are reported without holding anything back:
- A collection or wanted-list line that carries a quantity (
- 4 Sol Ring (C21:240)) reads as that many copies. Cleanup reports it (Read 4 copies: a collection holds one line per copy, so this line becomes 4 lines on the next save.) and the rewrite expands it. See Deck-Style Quantity Prefixes. - A deck’s empty extras section: a
## Maybeboardor## Tokensheader with no cards under it, usually left behind by a sync or aremove-cardthat took its last card. The rewrite drops it and reportsDropped empty section: Maybeboard; this is the one advisory that names something the rewrite removes. Any other empty section header (## Sideboard,## Commander,## Creatures, …) in a deck that has cards elsewhere is kept and written back bare, without a warning.
ritual cleanup [options]Options
Section titled “Options”| Option | Description | Default |
|---|---|---|
-n, --dry-run |
Report what would change without writing files | false |
--skip-formats |
Never prompt for deck formats; leave formatless decks untouched and report them | false |
--check |
Like --dry-run, but exit 1 when any file would change (for hooks and CI) |
false |
--output <format> |
Output format: text, json, or ndjson |
text |
--quiet |
Suppress non-essential output | false |
Under --dry-run nothing is prompted. Decks with no declared format are reported as needs a format and left untouched. --check implies --dry-run.
Headless Runs
Section titled “Headless Runs”The deck-format step is the only interactive part of cleanup. A real run that meets a formatless deck when prompts are unavailable (--no-input / RITUAL_NO_INPUT, stdin is not a terminal, or --output json/ndjson owns stdout) refuses before touching any file with a usage error (exit 2) naming --skip-formats. Pass --skip-formats to run everything else and leave formatless decks as they are (reported as format skipped), or run interactively to answer the prompts.
--check
Section titled “--check”--check is --dry-run with a meaningful exit code, for git hooks and CI:
ritual cleanup --checkIt exits 1 when any file would be rewritten or renamed, would have its categories file pruned or re-serialized, is blocked from its canonical rewrite by parse warnings or a fenced code block, or could not be read at all. It exits 0 when the workspace is already clean. The messages say which case applied, so “needs cleanup” and “could not be parsed” are distinguishable in a hook’s output. A formatless deck alone does not fail --check, since a real run would not change it without an interactive answer.
Scripted Output
Section titled “Scripted Output”With --output json (or ndjson), the text report is replaced by a single payload on stdout containing the per-file results (only files with something to report) and every warning, prefixed with its file:
{ "files": [ { "type": "wanted", "filePath": "/path/to/wanted/binder.md", "renamedTo": "Binder.md", "rewritten": true, "warnings": [] } ], "warnings": []}Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 |
Cleanup ran (or previewed) successfully |
1 |
A file could not be read or parsed (every mode, including --dry-run), a real run could not rewrite a file (skipped lines or a fenced block), or --check found work |
2 |
A real run needed the deck-format prompt but prompts were unavailable (see --skip-formats) |
Examples
Section titled “Examples”Preview what a cleanup would do:
ritual cleanup --dry-run[dry-run] decks/winota-stax.md: renamed to 'Winota Stax.md'[dry-run] decks/Jank.md: needs a format[dry-run] collections/Binder.md: rewritten in canonical form
Would clean up 3 of 12 list files.Run the cleanup:
ritual cleanupRun headless (CI, scripts) without the format prompts:
ritual cleanup --skip-formatsFail a CI job when the workspace needs a cleanup:
ritual cleanup --checkClean up a workspace elsewhere:
ritual --base-dir /path/to/site cleanupRitual is unofficial Fan Content permitted under the Fan Content Policy. Not approved or endorsed by Wizards of the Coast. Portions of the materials used are property of Wizards of the Coast. ©Wizards of the Coast LLC.
Card data, images and symbols come from Scryfall, which is not affiliated with this project. Ritual is free software under the AGPL-3.0. License and attribution.