Metadata-Version: 2.4
Name: mockcast
Version: 0.2.0
Summary: Vaporware demos as code: render scripted terminal sessions to asciicast v3 (and GIF via agg)
Author: Steve Morin
Author-email: Steve Morin <steve.morin@gmail.com>
License-Expression: MIT
Requires-Dist: pydantic>=2.13.4
Requires-Dist: pyyaml>=6.0.3
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# mockcast

[![CI](https://github.com/smorinlabs/mockcast/actions/workflows/ci.yml/badge.svg)](https://github.com/smorinlabs/mockcast/actions/workflows/ci.yml)

**Vaporware demos as code.** Describe an imaginary CLI in a YAML script, and
`mockcast` renders it into a real [asciicast v3](https://docs.asciinema.org/manual/asciicast/v3/)
`.cast` file — a scripted, fake terminal session you can play with the
[asciinema](https://asciinema.org/) 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).

![mockcast rendering the acme ai demo](media/hero.gif)

*(Above: `media/hero.gif`, generated by mockcast from [`examples/acme-ai-day.yaml`](examples/acme-ai-day.yaml). See also [`media/styled.gif`](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
```

## Why generate instead of record?

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
  `timestamp` header 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.

## Requirements

- Python ≥ 3.12 and [`uv`](https://docs.astral.sh/uv/)
- [`asciinema`](https://asciinema.org/) **≥ 3.0** for playback (v3 is an
  asciinema 3.x format; the legacy 2.x CLI can't play it)
- [`agg`](https://github.com/asciinema/agg) — only for the `gif` command
  (`brew install agg`)

Check your toolchain:

```bash
make check
```

## Install

```bash
uv sync
```

This installs the package and the `mockcast` console script into the project
environment (run it via `uv run mockcast …`).

## Usage

```bash
# 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 dracula
```

`gif` 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**:

```bash
asciinema play hero.cast        # press the marker keys to jump between chapters
```

Non-interactive sanity check (replays the timed stream to plain text — proves
asciinema accepts the file):

```bash
asciinema convert -f txt hero.cast -
```

> In asciinema 3.x, `cat` is a *concatenation* command (needs ≥2 files); use
> `convert` to dump a single cast to text.

## Writing a demo script

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

```yaml
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 vocabulary

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

## How it maps to asciicast v3

- **Header** (line 1): `{"version": 3, "term": {"cols", "rows", "type"}, …}`.
  `timestamp` is omitted for reproducibility.
- **Events**: `[interval, code, data]`, where `interval` is **relative** seconds
  since the previous event (the first event's interval is `0.0`), rounded to
  3 decimals. Only two codes are emitted: `o` (output — every keystroke and
  printout) and `m` (marker).

## Architecture

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`** — pure `render(Demo) -> Cast`: all timing, typing, prompt, and
  marker logic. No filesystem access (so it's golden-testable).
- **`writer.py`** — serializes a `Cast` to 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
```

## The hero cut

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

## Styling with ANSI

`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:

1. Use **double-quoted** YAML so `\e` becomes a real ESC byte (block scalars `|`
   keep text literal and won't interpret `\e`).
2. Instant `output` adds **no** trailing newline — end standalone lines with
   `\n`, or use `stream: lines` (which adds `\r\n` per line and reveals them one
   at a time). Keep each line self-contained (open *and* `\e[0m`-close its color
   on the same line).

```bash
uv run mockcast play examples/styled-output.yaml     # see it in color
```

## Development

```bash
just all        # format, lint, typecheck, test
just test       # pytest only
```

Tooling: `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.

## License

[MIT](LICENSE) © Steve Morin
