Files
daymade efda299a9e feat(cli-demo-generator): deep rewrite with battle-tested VHS patterns
SKILL.md: rewritten following Anthropic best practices
- Concise (233 lines, down from 347)
- Critical VHS parser limitations section (base64 workaround)
- Advanced patterns: self-bootstrap, output filtering, frame verification
- Better description for skill triggering

New files:
- references/advanced_patterns.md: production patterns from dbskill project
- assets/templates/self-bootstrap.tape: self-cleaning demo template

auto_generate_demo.py: new flags
- --bootstrap: hidden setup commands (self-cleaning state)
- --filter: regex pattern to filter noisy output
- --speed: post-processing speed multiplier (gifsicle)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:15:16 +08:00

5.8 KiB

Advanced VHS Demo Patterns

Battle-tested patterns from production demo recording workflows.

Contents

  • Self-bootstrapping tapes
  • Output noise filtering with base64 wrapper
  • Frame-level verification
  • Post-processing with gifsicle
  • Auto-detection and template rendering
  • Recording script structure

Self-Bootstrapping Tapes

A self-bootstrapping tape cleans its own state before recording, so running it twice produces identical output. Three phases:

# Phase 1: HIDDEN CLEANUP — remove previous state
Hide
Type "my-tool uninstall 2>/dev/null; my-tool reset 2>/dev/null"
Enter
Sleep 3s

# Phase 2: HIDDEN SETUP — create helpers (base64 for special chars)
Type "echo <base64-wrapper> | base64 -d > /tmp/helper.sh && source /tmp/helper.sh"
Enter
Sleep 500ms

# Phase 3: CLEAR + SHOW — wipe buffer before revealing
Type "clear"
Enter
Sleep 500ms
Show

# Phase 4: VISIBLE DEMO — what the viewer sees
Type "my-tool install"
Enter
Sleep 3s

Why clear before Show: VHS's Hide stops recording frames, but the terminal buffer still accumulates text. Without clear, the hidden commands' text appears in the first visible frame.

Output Noise Filtering with Base64

Many CLI tools produce verbose progress output that clutters demos. The solution: a hidden shell wrapper that filters noise lines.

Step 1: Create the wrapper function

# The function you want (can't type directly in VHS due to $/" chars)
my_tool() { command my_tool "$@" 2>&1 | grep -v -E "cache|progress|downloading|timeout"; }

Step 2: Base64 encode it

echo 'my_tool() { command my_tool "$@" 2>&1 | grep -v -E "cache|progress|downloading|timeout"; }' | base64
# Output: bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0K

Step 3: Use in tape

Hide
Type "echo bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0K | base64 -d > /tmp/w.sh && source /tmp/w.sh"
Enter
Sleep 500ms
Type "clear"
Enter
Sleep 500ms
Show

# Now `my_tool` calls the wrapper — clean output
Type "my_tool deploy"
Enter
Sleep 5s

When to filter

  • Git operations: filter "Cloning", "Refreshing", cache messages
  • Package managers: filter download progress, cache hits
  • Build tools: filter intermediate compilation steps
  • Any command with SSH not configured, timeout: 120s, etc.

Frame-Level Verification

After recording, extract and inspect key frames to verify the GIF shows what you expect.

Extract specific frames

# Frame at position N (0-indexed)
ffmpeg -i demo.gif -vf "select=eq(n\,100)" -frames:v 1 /tmp/frame_100.png -y 2>/dev/null

# Multiple frames at once
for n in 50 200 400; do
  ffmpeg -i demo.gif -vf "select=eq(n\,$n)" -frames:v 1 "/tmp/frame_$n.png" -y 2>/dev/null
done

Check total frame count and duration

ffmpeg -i demo.gif 2>&1 | grep -E "Duration|fps"
# Duration: 00:00:10.50, ... 25 fps → 262 frames total

What to verify

Frame Check
First (~frame 5) No leaked hidden commands
Mid (~frame N/2) Key output visible, no noise
Final (~frame N-10) All commands completed, result shown

Claude can read frames

Use the Read tool on extracted PNG files — Claude's vision can verify text content in terminal screenshots.

Post-Processing with gifsicle

Speed up or optimize GIFs after recording, avoiding re-recording.

Speed control

# 2x speed — halve frame delay (most common)
gifsicle -d2 input.gif "#0-" > output.gif

# 1.5x speed
gifsicle -d4 input.gif "#0-" > output.gif

# 3x speed
gifsicle -d1 input.gif "#0-" > output.gif

Optimize file size

# Lossless optimization
gifsicle -O3 input.gif > optimized.gif

# Reduce colors (lossy but smaller)
gifsicle --colors 128 input.gif > smaller.gif

Typical recording script pattern

# Record at normal speed
vhs demo.tape

# Speed up 2x for final output
cp demo.gif /tmp/demo_raw.gif
gifsicle -d2 /tmp/demo_raw.gif "#0-" > demo.gif
rm /tmp/demo_raw.gif

Auto-Detection and Template Rendering

For demos that need to adapt to the environment (e.g., different repo URLs, detected tools).

Template placeholders

Use sed to replace placeholders before recording:

# demo.tape (template)
Type "tool marketplace add REPO_PLACEHOLDER"
Enter
# Build script detects the correct repo
REPO=$(detect_repo)
sed "s|REPO_PLACEHOLDER|$REPO|g" demo.tape > /tmp/rendered.tape
vhs /tmp/rendered.tape

Auto-detect pattern (shell function)

detect_repo() {
  local upstream origin
  upstream=$(git remote get-url upstream 2>/dev/null | sed 's|.*github.com[:/]||; s|\.git$||') || true
  origin=$(git remote get-url origin 2>/dev/null | sed 's|.*github.com[:/]||; s|\.git$||') || true

  # Check upstream first (canonical), then origin (fork)
  if [[ -n "$upstream" ]] && gh api "repos/$upstream/contents/.target-file" &>/dev/null; then
    echo "$upstream"
  elif [[ -n "$origin" ]]; then
    echo "$origin"
  else
    echo "fallback/default"
  fi
}

Recording Script Structure

A complete recording script follows this pattern:

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# 1. Check prerequisites
for cmd in vhs gifsicle; do
  command -v "$cmd" &>/dev/null || { echo "Missing: $cmd"; exit 1; }
done

# 2. Auto-detect dynamic values
REPO=$(detect_repo)
echo "Using repo: $REPO"

# 3. Render tape template
sed "s|PLACEHOLDER|$REPO|g" "$SCRIPT_DIR/demo.tape" > /tmp/rendered.tape

# 4. Clean previous state
cleanup_state || true

# 5. Record
(cd "$REPO_DIR" && vhs /tmp/rendered.tape)

# 6. Speed up
cp "$REPO_DIR/demo.gif" /tmp/raw.gif
gifsicle -d2 /tmp/raw.gif "#0-" > "$REPO_DIR/demo.gif"

# 7. Clean up
cleanup_state || true
rm -f /tmp/raw.gif /tmp/rendered.tape

# 8. Report
SIZE=$(ls -lh "$REPO_DIR/demo.gif" | awk '{print $5}')
echo "Done: demo.gif ($SIZE)"