Skip to content

Getting Started

An all-in-one toolkit for Magic: The Gathering decks, collections, and self-hosting.

Terminal window
curl -fsSL https://raw.githubusercontent.com/sloshy/ritual/main/scripts/install.sh | bash

Ritual is pre-release, so there is no stable release yet. Install the newest build — prereleases included — with:

Terminal window
curl -fsSL https://raw.githubusercontent.com/sloshy/ritual/main/scripts/install.sh | bash -s -- --prerelease

Both installers accept these options:

OptionEnvironment variableDescription
--prerelease/-PrereleaseRITUAL_PRERELEASE=1Install the newest release, including prereleases.
--version <tag>/-Version <tag>RITUAL_VERSION=<tag>Install a specific release tag. Use prerelease for the newest prerelease.
RITUAL_INSTALL_DIR=<dir>Where to install the binary.

You can also download a platform-specific binary from the releases page and run it directly:

Terminal window
./ritual --help

This project uses Bun as its runtime. First, install dependencies:

Terminal window
bun install

Then build the binary:

Terminal window
bun run build
Terminal window
./ritual new deck "My Commander Deck" --format commander

Import decks from Archidekt, Moxfield, or MTGGoldfish:

Terminal window
./ritual import https://archidekt.com/decks/12345
Terminal window
./ritual price "My Commander Deck"

Ritual supports building static websites to showcase your decks (and soon, your collection). This is an experimental feature, so expect some rough edges. To build and serve the site locally:

Terminal window
./ritual serve --build

Then open http://localhost:3000 to view your deck collection.

These options are available on every command:

OptionEnvironment variableDescription
--base-dir <path>RITUAL_BASE_DIRUse this directory instead of the current working directory
--cache-server <host:port>RITUAL_CACHE_SERVERUse a remote cache server instead of local cache files
--no-inputRITUAL_NO_INPUTNever prompt; fail or use documented defaults where input would be required
--locale <tag>RITUAL_LOCALELanguage for Ritual’s own interface text (BCP-47, e.g. de-AT)

In every case the flag wins when both are given, and an empty or whitespace-only environment variable counts as not set.

By default, Ritual reads and writes files relative to the directory you run it from (the current working directory). Use --base-dir to point Ritual at a different directory without needing to cd there first:

Terminal window
# Run from anywhere, but operate on files in ~/my-collection
ritual --base-dir ~/my-collection build-site
# Price all decks in a specific project directory
ritual --base-dir /projects/mtg price "My Deck"

This affects all file paths: decks, collections, wanted lists, cache, config files, and the output dist/ directory. The directory locations themselves can be customized via ritual.config.json — paths in that file are interpreted relative to --base-dir.

The RITUAL_BASE_DIR environment variable does the same thing and is used whenever --base-dir itself is not passed, so a shell session or a service unit can pin a workspace once:

Terminal window
export RITUAL_BASE_DIR=~/my-collection
ritual lists # operates on ~/my-collection
ritual --base-dir /projects/mtg lists # the flag wins for this one run

The directory must already exist. Ritual never creates a base directory: a path that is missing, or that is not a directory, is a usage error (exit code 2) and nothing is read or written:

base directory does not exist: /home/you/my-collectoin
base directory is not a directory: /home/you/notes.txt
base directory is not readable: /srv/private — EACCES: permission denied, stat '/srv/private'

The third form covers permission problems and any other filesystem failure while checking the path.

This is deliberate — a typo’d --base-dir used to read as a perfectly empty workspace ((no lists)) and a write would have forked your data into a stray tree.

Disables every interactive prompt across the CLI — the headless switch for scripts, CI, and agents. A command that would normally prompt either fails with a usage error or falls back to a documented default instead (the specific behavior is called out in each command’s own docs, e.g. price, export, cleanup).

The RITUAL_NO_INPUT environment variable does the same thing and is checked whenever --no-input itself is not passed — set it once in a CI environment instead of adding the flag to every invocation:

Terminal window
RITUAL_NO_INPUT=1 ritual cleanup --skip-formats
ritual import ./decklist.txt --type deck --no-input

Setting it to a falsy value (0, false, no, off, any casing) counts as not set, so an inherited RITUAL_NO_INPUT=1 can be turned back off for one invocation:

Terminal window
RITUAL_NO_INPUT=0 ritual edit "My Deck" # prompts again, despite the CI default

A prompt cannot run when --no-input/RITUAL_NO_INPUT disabled prompting or when stdin is not a terminal (piped input, </dev/null, most CI runners). The two causes are treated identically everywhere in the CLI: no command opens a prompt, spawns a pager, or exits 0 having quietly skipped the work it was asked to do. Where an answer is genuinely required you get a usage error (exit code 2) naming the flag that would have supplied it, e.g.:

Input required: pass --finish <nonfoil|foil|etched> (no terminal available for prompts).
Input required: pass --finish <nonfoil|foil|etched> (prompts are disabled by --no-input / RITUAL_NO_INPUT).

The parenthetical names which of the two causes applied, since the remedies differ — supply the missing flags, or reconsider --no-input. Interactive-only surfaces (edit, history’s editor, move’s session, dep-license’s picker) refuse to open at all and point at their headless equivalent; license and dep-license <package> print their text straight to stdout instead of paging it through less.

Where a question has a documented default, that default is used instead of failing — each command’s page says which: --refresh ask is declined rather than answered wherever it is a prompt — but three commands document their own reading, and two of them download without asking: build-site bulk-downloads an empty or stale card cache automatically, since it cannot build without card data; sell redownloads an already-downloaded Card Kingdom buylist once it is a day old (as do admin, serve --api, and a sell-mode build-site), since a day-old feed quotes yesterday’s offers — only the first buylist download prompts; and collection-sync fails the run instead of skipping, and a text import without --type falls back to a deck under an explicit --no-input (a merely piped run still errors, since nothing said which type was meant). A URL import (and import-account) without a --sync-printings/--no-sync-printings answer behaves the same way: an explicit --no-input keeps the source’s exact printings and says so, while a merely piped run errors.

Sets the language Ritual’s own interface text is written in — output, prompts, menus, help, and errors. It takes a BCP-47 tag (en, de, de-AT, pt-BR), which is canonicalized, so --locale de-at and --locale de-AT are the same thing.

Terminal window
ritual --locale de lists # this one run
export RITUAL_LOCALE=de # this shell
ritual config set uiLocale de # this workspace, persistently

The full precedence is --localeRITUAL_LOCALE → the uiLocale config key → your OS locale → en. Only the flag is strict: a tag naming no known language is a usage error (exit code 2), while a bad RITUAL_LOCALE or uiLocale warns and falls through to the next tier, since an unusable interface language is cosmetic and refusing to run over one would be worse than the misconfiguration.

Run ritual locale to see which tier won — or ritual locale --detect to run the deeper OS probes (Windows/macOS) and optionally save the result as uiLocale.

Every card line in a deck, collection, or wanted list file carries a stable internal ID — the &N suffix (e.g. 1 Sol Ring &5). The tools manage these IDs; never hand-author or renumber them. When a file is missing IDs (typically after a hand edit), Ritual backfills them automatically before running any command that rewrites list card lines or needs every line to already carry an ID: the editors (edit, history’s editor mode, admin — the server itself, not its setup/reset-password/disable-totp subcommands — mcp, and serve --build/--api), the one-shot card commands (add-card, remove-card, set-card, note, move), the importers and syncs (import, import-account, import-changes, deck-sync pull/deck-sync push — but not the read-only deck-sync status or the front-matter-only deck-sync link — and collection-sync), and the whole-workspace passes (cleanup, build-site).

No other command triggers the backfill, so none of them ever rewrites the card lines in your list files. That covers the read-only commands (lists, diff, price, sell, export, get-primer, history --show, list-all-cards, scry, card, …), the lifecycle commands (new, rename, delete), the front-matter-only metadata command (it writes list files, but never their card lines), and utilities like detect-changes — deliberately, since a backfill would rewrite the very files it is inspecting (that holds for --hash-only and --verify too). (Some of these still write files of their own, of course: detect-changes --hash-only writes .sha256 sidecars, and new/rename/delete create, move, or remove list files as their whole purpose.) -n/--dry-run suppresses the backfill too: a dry run writes nothing.

The backfill also never disturbs change tracking for hand-edited files: it refreshes a file’s .sha256 sidecar only when the sidecar already matched the file, so detect-changes still sees your hand edits and records their changelog entries.

  • List Management: Create and organize your decks, collections, and wanted lists
  • Multi-source Import: Import from Archidekt, Moxfield, MTGGoldfish, or local files
  • Scryfall Integration: Full card search powered by Scryfall
  • Pricing: Get min, max, and latest prices for your decks
  • Static Site Generation: Create a self-hosted website to showcase your decks or collection
  • Caching: Smart caching for fast card lookups

Use structured output for automation:

Terminal window
./ritual price "My Commander Deck" --output json | jq '.totals'

Stream multiple card lookups as NDJSON:

Terminal window
./ritual card --from-file cards.txt --output ndjson --fields name,set,prices.usd

Avoid interactive prompts in CI:

Terminal window
./ritual import-account johndoe --all --no-input --dry-run
  • Prefer --output json or --output ndjson for script parsing.
  • Use --fields to project stable subsets of data.
  • Add the global --no-input flag (or set RITUAL_NO_INPUT) in headless environments so no command ever prompts; pass --yes where a confirmation is required.

Three flags and one exit-code vocabulary behave the same way on every command that has them.

--output always means the scripting format

Section titled “--output always means the scripting format”

--output text|json|ndjson selects the envelope the command writes to stdout — never a file format, never a destination. json emits exactly one document per run (a batch of card lookups or a multi-page search is still one array); ndjson streams one JSON object per line; text is the human rendering. Three commands widen or drop the value list rather than redefining the flag: scry adds --output csv (Scryfall renders the CSV itself), sell adds --output csv too (a different payload — Card Kingdom’s sell-cart upload rows, not the report re-rendered), and export has no --output at all — its stdout payload is the export, chosen with --format csv|json|text|md and redirected with --out <file>.

Errors always go to stderr, structured to match: {"error": {"code", "messageKey", "messageParams", "message", "details"}} under json/ndjson, a plain line under text. code and messageKey are locale-invariant — match on those; message is prose that follows the UI locale (messageKey/messageParams are omitted when the failure has no message-catalog entry behind it, such as an error quoted verbatim from Scryfall or the filesystem). stdout stays parseable in every case.

--quiet removes progress and status messages — “Reading cards from file…”, “Successfully imported…”, applied counts, confirmations. It never removes:

  • the structured payload: --output json/ndjson stdout is always emitted, so --quiet --output json is a JSON document with no chatter around it. Scripts that want total silence redirect stdout.
  • errors, and warnings that mean content was lost — a card line the parser could not read, a change skipped as a conflict, a result set truncated by the page cap. These always print to stderr, --quiet or not, because nothing else would tell you.

A command with no non-essential output does not register the flag at all rather than advertising an inert one. card, diff, scry, cache status, dep-license, history, login status, deck-sync status, and skills list therefore accept --output but no --quiet: everything they print is either the payload or a warning that has to survive anyway.

CodeMeaning
0Success
1Runtime error — the work was attempted and failed
2Usage error — a bad flag value or an impossible combination, including a malformed --base-dir or --cache-server
3Not found — a named list, file, card, or other resource does not exist

A closed stdout pipe is not a failure: ritual … --output ndjson | head stops quietly and exits 0, like any standard Unix tool.