Vaporware demos as code. Describe an imaginary CLI in a YAML script, and
mockcast renders it into a real asciicast v3
.cast file — a scripted, fake terminal session you can play with the
asciinema player and share. No real product required,
no terminal recorded.
It ships with a headline demo for an imagined product, acme ai (a tool
that manages AI coding harnesses — Claude Code, Codex, Gemini CLI — plus
profiles and skills).
(Above: media/hero.gif, generated by mockcast from examples/acme-ai-day.yaml. See also media/styled.gif — the ANSI styling showcase.)
maya@laptop ~/acme $ acme ai profile install acme-team
Resolving acme-team…
✔ claude-code installed
✔ codex already up to date
✔ gemini-cli installed
✔ synced 7 skills · logged in as maya
A recording embeds wall-clock time and human timing, so it can't be tested or
reproduced. mockcast emits the v3 format directly from a definition, which
makes the output a pure function of its script:
- Deterministic — a seeded RNG drives typing "jitter," and the optional
timestampheader is omitted, so the same YAML always renders byte-identical bytes. - Diffable & reviewable — the demo lives in version control as readable YAML.
- Reshootable — tweak one line of timing or output and re-render; you never have to perform the demo again.
- Python ≥ 3.12 and
uv asciinema≥ 3.0 for playback (v3 is an asciinema 3.x format; the legacy 2.x CLI can't play it)agg— only for thegifcommand (brew install agg)
Check your toolchain:
make checkuv syncThis installs the package and the mockcast console script into the project
environment (run it via uv run mockcast …).
# Schema-check a demo script (friendly errors, no output written)
uv run mockcast validate examples/acme-ai-day.yaml
# Render to a .cast file
uv run mockcast render examples/acme-ai-day.yaml -o hero.cast
# Render to a temp file and play it in your terminal
uv run mockcast play examples/acme-ai-day.yaml
# Render to an animated GIF (via agg)
uv run mockcast gif examples/acme-ai-day.yaml -o hero.gif
uv run mockcast gif examples/styled-output.yaml -o styled.gif --speed 1.5 --theme draculagif accepts --speed <factor> and --theme <name> (passed through to agg;
themes include asciinema, dracula, monokai, nord, solarized-dark, …).
Then watch the real thing — timed playback with navigable chapter markers:
asciinema play hero.cast # press the marker keys to jump between chaptersNon-interactive sanity check (replays the timed stream to plain text — proves asciinema accepts the file):
asciinema convert -f txt hero.cast -In asciinema 3.x,
catis a concatenation command (needs ≥2 files); useconvertto dump a single cast to text.
A demo is global settings (meta/vars) plus an ordered list of scenes,
each holding steps. Each scene name becomes a navigable chapter marker (when
auto_markers is on).
meta:
title: "acme ai — a day in the life"
term: { cols: 100, rows: 30, type: xterm-256color }
typing: { cps: 22, jitter: 0.35 } # chars/sec + randomness
prompt: "{user}@{host} {cwd} $ "
pauses: { after_command: 0.5, between_scenes: 1.2 }
idle_time_limit: 2.0
seed: 42 # makes jitter reproducible
auto_markers: true # one chapter marker per scene
vars: { user: maya, host: laptop, cwd: "~/acme" }
scenes:
- name: onboard
steps:
- type: "acme ai profile install acme-team"
- output: |
Resolving acme-team…
✔ claude-code installed
stream: lines # reveal line-by-line
- pause: 1.0
- name: teammate # the two-machine trick
vars: { user: sam, host: workstation } # swaps the prompt identity
prompt: "{user}@{host} $ "
steps:
- banner: "─── Sam's machine ───"
- type: "acme ai search recipe"| Step | Behavior |
|---|---|
type: "<cmd>" |
renders the prompt, then "types" the command char-by-char (cps + seeded jitter), then Enter + an after_command pause |
output: "<text>" |
prints text; modifier stream: instant|lines|chars (default instant), optional delay: seconds before |
pause: <sec> |
idle gap (think time) |
banner: "<text>" |
prints a styled scene-divider line (set dressing) |
clear: true |
clears the screen |
marker: "<label>" |
drops a v3 m event → a navigable chapter marker |
Scene-level vars: / prompt: override the globals — that's how the
"Sam's machine" identity swap works. You can embed raw ANSI escapes in
output/banner strings for color.
- Header (line 1):
{"version": 3, "term": {"cols", "rows", "type"}, …}.timestampis omitted for reproducibility. - Events:
[interval, code, data], whereintervalis relative seconds since the previous event (the first event's interval is0.0), rounded to 3 decimals. Only two codes are emitted:o(output — every keystroke and printout) andm(marker).
Three I/O-free units behind a thin CLI:
demo.yaml ──▶ [models.py] ──▶ [engine.py] ──▶ [writer.py] ──▶ out.cast ──▶ asciinema
(author) validate render serialize (play)
models.py— pydantic v2 schema + validation (what a valid demo is).engine.py— purerender(Demo) -> Cast: all timing, typing, prompt, and marker logic. No filesystem access (so it's golden-testable).writer.py— serializes aCastto newline-delimited v3 JSON.cli.py—render/validate/play; owns all I/O.
src/mockcast/{__init__,models,engine,writer,cli}.py
tests/{test_models,test_engine,test_writer,test_cli}.py
examples/acme-ai-day.yaml # the 10-beat hero cut
docs/superpowers/specs/ # design spec
docs/superpowers/plans/ # implementation plan
examples/acme-ai-day.yaml is one continuous "day in the life" of acme ai:
onboard → verify → launch → usage → search → author a skill → ship → promote →
Sam's machine (pull) → status. It renders to ~374 events with 10 chapter
markers, and a teammate scene swaps the prompt from maya@laptop to
sam@workstation to prove the producer→consumer loop in a single cast.
examples/styled-output.yaml is a second demo showing bold, color, symbols,
and an ★ Insight box — the rich-terminal look. Styling is just raw ANSI in
output strings; the engine passes it straight through. Two rules:
- Use double-quoted YAML so
\ebecomes a real ESC byte (block scalars|keep text literal and won't interpret\e). - Instant
outputadds no trailing newline — end standalone lines with\n, or usestream: lines(which adds\r\nper line and reveals them one at a time). Keep each line self-contained (open and\e[0m-close its color on the same line).
uv run mockcast play examples/styled-output.yaml # see it in colorjust all # format, lint, typecheck, test
just test # pytest onlyTooling: uv (deps/packaging), ruff (format + lint), ty (typecheck),
pytest (tests), just (command runner). The test suite covers schema
validation, the engine's timing/marker/identity logic, deterministic output
(jitter: 0 exact intervals + render-twice byte equality), v3 serialization,
and the CLI.
MIT © Steve Morin
