DEV Community

Bala Paranj
Bala Paranj

Posted on

Generate Terminal Recordings for Your CLI Docs Without Touching asciinema

CLI documentation has a trust problem. Users read your examples, try to reproduce them, and get different output. Screenshots go stale the moment a flag name changes. And asking developers to manually record terminal sessions with asciinema is a workflow that doesn't scale.

I solved this by generating terminal recordings programmatically in Go. The recordings are asciicast v2 files — the same format asciinema uses — but they're produced by a build target, not a human. When the CLI changes, make docs-recordings regenerates everything.

The Problem

Stave has 18 demo scenarios showing different security evaluation workflows. Each scenario needs a terminal recording that shows the exact command being typed, the exact output produced, and the exact exit behavior.

Maintaining these manually would mean:

  1. Install asciinema on every developer's machine
  2. Record each scenario by hand (type the command, wait for output)
  3. Re-record every time a flag name, output format, or control ID changes
  4. Hope the recording environment matches production (same terminal width, same colors)

This doesn't work when your CLI is under active development with weekly flag renames and output format changes.

The Solution: Generate Recordings in Go

The genrecordings tool is a standalone Go program that:

  1. Imports the demo scenario definitions
  2. Runs the real stave binary against real observation data
  3. Captures stdout/stderr
  4. Produces asciicast v2 files with simulated typing

No asciinema installation required. The output is a .cast file that any asciinema-compatible player can render.

Asciicast v2 Format

The format is simple — newline-delimited JSON (NDJSON):

{"version": 2, "width": 120, "height": 40, "title": "stave: public-read"}
[0.000000, "o", "$ "]
[0.356112, "o", "s"]
[0.390732, "o", "t"]
[0.425352, "o", "a"]
[0.459972, "o", "v"]
[0.494592, "o", "e"]
[0.529212, "o", " "]
[0.563832, "o", "a"]
[0.598452, "o", "p"]
[0.633072, "o", "p"]
[0.667692, "o", "l"]
[0.702312, "o", "y"]
[1.200000, "o", " --controls ..."]
[2.500000, "o", "\r\n"]
[2.600000, "o", "{\n  \"schema_version\": \"out.v0.1\",\n  ..."]
Enter fullscreen mode Exit fullscreen mode

Line 1: header with terminal dimensions and title.
Remaining lines: [timestamp_seconds, "o", "text"] — output events.

The generator simulates realistic typing by emitting one character at a time with randomized delays (30-100ms per keystroke), then dumps the full CLI output after a pause.

Determinism

Typing delays use a fixed random seed (rand.NewSource(42)). Combined with the --now flag on stave (which pins the evaluation timestamp), the same binary + same observations = byte-identical .cast files.

This means CI can verify recordings are fresh:

make docs-recordings
git diff --exit-code docs-content/recordings/
# If diff is non-empty, recordings are stale
Enter fullscreen mode Exit fullscreen mode

Checksum-Based Skip

Before generating, the tool computes a SHA-256 hash over:

  • The stave binary itself
  • All observation JSON files for every scenario

If the hash matches the stored checksum in .recordings-checksum, generation is skipped entirely. This avoids pointless 30-second regeneration cycles during development when nothing relevant changed.

Embedding in Docusaurus

The .cast files are served as static assets. A React component wraps the asciinema-player library:

import React, { useEffect, useRef } from 'react';
import 'asciinema-player/dist/bundle/asciinema-player.css';

export default function AsciinemaPlayer({ src, ...opts }) {
  const ref = useRef(null);
  useEffect(() => {
    let player;
    import('asciinema-player').then((mod) => {
      player = mod.create(src, ref.current, {
        cols: 120, rows: 40,
        autoPlay: true, speed: 2,
        theme: 'monokai', ...opts,
      });
    });
    return () => { if (player) player.dispose(); };
  }, [src]);
  return <div ref={ref} />;
}
Enter fullscreen mode Exit fullscreen mode

Usage in MDX:

import AsciinemaPlayer from '@site/src/components/AsciinemaPlayer';

## HIPAA Compliance Check

<AsciinemaPlayer src="/recordings/hipaa-compliance.cast" />
Enter fullscreen mode Exit fullscreen mode

The user sees a terminal replay that shows exactly what happens when they run the command. The recording is always in sync with the current binary because it's generated from the same build.

What This Solves

Manual Recording Generated Recording
Requires asciinema installed Requires only Go
Human types the command Program simulates typing
Output depends on environment Output is deterministic
Re-record on every change make docs-recordings
May not match production Uses the real binary + real data
Varies between developers Byte-identical across machines

When to Use This Pattern

  • CLI documentation sites — show users exactly what your tool produces
  • Demo pages — interactive-looking recordings without video hosting
  • Onboarding guides — step-by-step workflows with real output
  • Release notes — "here's what changed" with before/after recordings

When NOT to Use This

  • Interactive sessions — if the demo requires user input (prompts, confirmations), a real recording is better
  • Long-running processes — asciicast files get large for multi-minute sessions
  • GUI tools — this is strictly for terminal output

Your CLI's output is the recording data. You don't need a screen recorder — you need a program that runs your CLI and formats the output as asciicast events.


This system is built for Stave. The generator lives at cmd/genrecordings/ and produces 18 recordings covering security evaluation scenarios.

Top comments (0)