@scrthq/runlog
The command line for Runlog rule packs, and the app itself.
Runlog plays rule packs: a game's tables, flow, modes and rules in one YAML file, which the app turns into a run you can play, share, time and document. This package gives a designer the tools around that file:
- check a pack and replay its own fixtures, in CI or by hand;
- write its paper: rulebook, quick start, reference card, run log sheet and marketplace summary, as HTML and Markdown;
- sign releases and seal copies for buyers, so a pack carries its author's name and a sold copy opens with a license key;
- publish to your own library or release to the marketplace, from a terminal or a build server;
- serve the app from your own machine, offline, with nothing else installed.
The same package is a small library for a seller's own backend: seal, open, readHeader, isSealed and generateLicenseKey. The command depends on yaml and zod, declared as such; the library entry point pulls in nothing.
npx @scrthq/runlog help
Node 20 or later. Every command takes a .yaml or .json pack.
Checking a pack
npx @scrthq/runlog validate my-game.yaml
npx @scrthq/runlog validate my-game.yaml --strict
npx @scrthq/runlog test my-game.yaml
npx @scrthq/runlog validate packs/*.yaml --strict
validate reads the pack, checks it against the format, and lints what a schema cannot see: a d100 table whose ranges leave a gap, a state nothing applies, a trigger on a moment the run never reaches. Errors exit 1; with --strict, so do warnings. test runs the fixtures a pack declares for its own tables, so a change that breaks a promised outcome fails before anyone plays it.
Starting one
npx @scrthq/runlog init my-game
The authoring guide in the repository walks through the format; the file carries a $schema line so an editor that reads JSON Schema completes and checks it as you type.
Writing the paper
npx @scrthq/runlog docs my-game.yaml -o dist/docs
npx @scrthq/runlog docs my-game.yaml -o dist/docs --only rulebook,reference
npx @scrthq/runlog docs my-game.yaml -o dist/docs --md
Five documents come out of the pack as it is, so they are never behind the rules: the rulebook (everything, in reading order), the quick start, the reference card (dense columns for the table), the run log sheet (a form to write a run on by hand), and the summary a catalog shows. Each as HTML, which a browser prints to a PDF with the page breaks and columns already right, and as Markdown for editing or pasting. The app's Designer makes PDFs directly.
Bundling
npx @scrthq/runlog bundle my-game.yaml -o dist/my-game.pack.json
One normalized JSON file, which is the convenient thing to attach to a release. YAML stays the better thing to author and keep under version control.
Signing, and sealing copies for buyers
Signing puts your name on a release: an altered copy can no longer claim to be yours, and the app says so when it opens one. Sealing wraps a signed copy for one buyer behind a license key.
npx @scrthq/runlog keygen -o my-key.json
npx @scrthq/runlog login
npx @scrthq/runlog claim my-key.json
npx @scrthq/runlog sign my-game.yaml --key my-key.json --as "Your Name"
sign rewrites the pack file with its signature (comments are dropped; sign a copy if you keep them). keygen prints a fingerprint: publish it wherever you publish, since it is the only thing that ties a signature to you. sign and issue refuse a key nobody has claimed.
For each buyer, from your own shop or by hand:
npx @scrthq/runlog issue my-game.yaml --to "Buyer Name" --ref order-8f3a --key my-key.json --seal
That writes my-game-buyer-name.rlpack and prints a license key, once. Send both. The buyer loads the file with Load a pack from a file and types the key; signed in, the key is kept in their account, so the same file opens on their other devices. --license supplies a key of your own instead; -o names the file. Without --seal the copy is a plain signed pack stamped with the buyer's name.
Signing does not stop copying, and nothing can: the app has to read every word to play. It protects your name on the thing.
Publishing
Both destinations use the same account.
npx @scrthq/runlog publish my-game.yaml
npx @scrthq/runlog release my-game.yaml --price 3.00
npx @scrthq/runlog release my-game.yaml --free
npx @scrthq/runlog release my-game.yaml
npx @scrthq/runlog release my-game.yaml --draft
release refuses an unsigned pack, or one whose signature no longer matches its text, since the marketplace seals a copy for each buyer under what you signed. Becoming a publisher, and setting up payouts, happens on your profile in the app; a priced listing needs payouts finished.
From a build server
Nobody is at a build server to confirm a sign-in code, so two things go in the environment instead:
RUNLOG_API_KEY | a command-line key from your profile page in the app; stands in for login. Make it a key that only releases: it can check, sign, publish and release packs and nothing else, so a leaked build secret cannot reach your runs, sales or people |
RUNLOG_SIGNING_KEY | the whole contents of the my-key.json that keygen wrote; stands in for --key |
RUNLOG_API | optional: another address, such as a copy you run yourself (https://<your domain>/api), with a key made there |
on:
release:
types: [published]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npx @scrthq/runlog validate my-game.yaml --strict
- run: npx @scrthq/runlog test my-game.yaml
- run: npx @scrthq/runlog sign my-game.yaml
env:
RUNLOG_API_KEY: ${{ secrets.RUNLOG_API_KEY }}
RUNLOG_SIGNING_KEY: ${{ secrets.RUNLOG_SIGNING_KEY }}
- run: npx @scrthq/runlog release my-game.yaml --price 3.00
env:
RUNLOG_API_KEY: ${{ secrets.RUNLOG_API_KEY }}
The repository's examples/github-actions has this and two more workflows to copy, with a README on setting the secrets: one that checks every push, and one that signs a release and attaches the pack and its paper to it for selling from your own hands.
Running the app from your machine
npx @scrthq/runlog serve
npx @scrthq/runlog serve --port 8080 --open
npx @scrthq/runlog serve --host 0.0.0.0
The package carries the app, so this puts Runlog on a local port with nothing else installed: for a table with no internet, a designer's own copy, or a machine where the hosted address is not wanted. Packs and runs live in that browser, as they do anywhere. Sign-in and sync belong to the hosted address; the served copy has neither. --dir points at another build of the app; RUNLOG_APP_DIR does the same from the environment.
Your account from the terminal
npx @scrthq/runlog login
npx @scrthq/runlog login --api https://<your domain>/api
npx @scrthq/runlog login --key
npx @scrthq/runlog whoami
npx @scrthq/runlog logout
The sign-in renews itself while it is used and lapses after a month idle. It lives in your user configuration directory, readable by you alone. NO_COLOR in the environment turns the color off.
The library
npm install @scrthq/runlog
import { seal, open, readHeader, isSealed, generateLicenseKey } from "@scrthq/runlog";
const key = generateLicenseKey();
const bytes = await seal(pack, key, { ref: order.id, title: pack.title });
await email(buyer, {
attachment: { name: `${pack.id}.rlpack`, bytes },
text: `Your license key is ${key}. Open the file in Runlog and type it once.`,
});
readHeader(bytes);
const result = await open(bytes, key);
if (result.ok) result.document;
Runs on Node 20 and later and in browsers; this entry point uses none of the package's dependencies. A sealed file made here opens in the app on any address with the key you issued, and nothing is checked online. The container's byte layout is documented in the repository's docs/selling.md.
More
The authoring guide, the format reference, the selling notes and the example workflows live in the repository: github.com/SCRT-HQ/runlog. The app is at runlog.scrthq.com, with its own guide under Docs.