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>
This commit is contained in:
daymade
2026-04-06 08:15:16 +08:00
parent 5c9eda4fbd
commit efda299a9e
4 changed files with 571 additions and 329 deletions

View File

@@ -1,346 +1,233 @@
---
name: cli-demo-generator
description: This skill should be used when users want to create animated CLI demos, terminal recordings, or command-line demonstration GIFs. It supports both manual tape file creation and automated demo generation from command descriptions. Use when users mention creating demos, recording terminal sessions, or generating animated GIFs of CLI workflows.
description: Generates professional animated CLI demos as GIFs using VHS terminal recordings. Handles tape file creation, self-bootstrapping demos with hidden setup, output noise filtering, post-processing speed-up, and frame-level verification. Use when users want to create terminal demos, record CLI workflows as GIFs, generate animated documentation, build demo tapes for README files, or need to showcase any command-line tool visually. Also triggers on "record terminal", "VHS tape", "demo GIF", "animate my CLI", or any request to visually demonstrate shell commands.
---
# CLI Demo Generator
Generate professional animated CLI demos with ease. This skill supports both automated generation from command descriptions and manual control for custom demos.
Create professional animated CLI demos. Four approaches, from fully automated to pixel-precise manual control.
## When to Use This Skill
## Quick Start
Trigger this skill when users request:
- "Create a demo showing how to install my package"
- "Generate a CLI demo of these commands"
- "Make an animated GIF of my terminal workflow"
- "Record a terminal session and convert to GIF"
- "Batch generate demos from this config"
- "Create an interactive typing demo"
**Simplest path** — give commands, get GIF:
## Core Capabilities
### 1. Automated Demo Generation (Recommended)
Use the `auto_generate_demo.py` script for quick, automated demo creation. This is the easiest and most common approach.
**Basic Usage:**
```bash
scripts/auto_generate_demo.py \
python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "npm install my-package" \
-c "npm run build" \
-o demo.gif
```
**With Options:**
**Self-bootstrapping demo** — for repeatable recordings that clean their own state:
```bash
scripts/auto_generate_demo.py \
-c "command1" \
-c "command2" \
python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "npm install my-package" \
-c "npm run build" \
-o demo.gif \
--bootstrap "npm uninstall my-package 2>/dev/null" \
--speed 2
```
## Critical: VHS Parser Limitations
VHS `Type` strings cannot contain `$`, `\"`, or backticks. These cause parse errors:
```tape
# FAILS — VHS parser rejects special chars
Type "echo \"hello $USER\""
Type "claude() { command claude \"$@\"; }"
```
**Workaround: base64 encode the command**, decode at runtime:
```bash
# 1. Encode your complex command
echo 'claude() { command claude "$@" 2>&1 | grep -v "noise"; }' | base64
# Output: Y2xhdWRlKCkgey4uLn0K
# 2. Use in tape
Type "echo Y2xhdWRlKCkgey4uLn0K | base64 -d > /tmp/wrapper.sh && source /tmp/wrapper.sh"
```
This pattern is essential for output filtering, function definitions, and any command with shell special characters.
## Approaches
### 1. Automated Generation (Recommended)
```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "command1" -c "command2" \
-o output.gif \
--title "Installation Demo" \
--theme "Dracula" \
--width 1400 \
--height 700
--title "My Demo" \
--theme "Catppuccin Latte" \
--font-size 24 \
--width 1400 --height 600
```
**Script Parameters:**
- `-c, --command`: Command to include (can be specified multiple times)
- `-o, --output`: Output GIF file path (required)
- `--title`: Demo title (optional, shown at start)
- `--theme`: VHS theme (default: Dracula)
- `--font-size`: Font size (default: 16)
- `--width`: Terminal width (default: 1400)
- `--height`: Terminal height (default: 700)
- `--no-execute`: Generate tape file only, don't execute VHS
| Flag | Default | Description |
|------|---------|-------------|
| `-c` | required | Command to include (repeatable) |
| `-o` | required | Output GIF path |
| `--title` | none | Title shown at start |
| `--theme` | Dracula | VHS theme name |
| `--font-size` | 16 | Font size in pt |
| `--width` | 1400 | Terminal width px |
| `--height` | 700 | Terminal height px |
| `--bootstrap` | none | Hidden setup command (repeatable) |
| `--filter` | none | Regex pattern to filter from output |
| `--speed` | 1 | Playback speed multiplier (uses gifsicle) |
| `--no-execute` | false | Generate .tape only |
**Smart Features:**
- Automatic timing based on command complexity
- Optimized sleep durations (1-3s depending on operation)
- Proper spacing between commands
- Professional defaults
Smart timing: `install`/`build`/`test`/`deploy` → 3s, `ls`/`pwd`/`echo` → 1s, others → 2s.
### 2. Batch Demo Generation
### 2. Batch Generation
Use `batch_generate.py` for creating multiple demos from a configuration file.
Create multiple demos from one config:
**Configuration File (YAML):**
```yaml
# demos.yaml
demos:
- name: "Install Demo"
- name: "Install"
output: "install.gif"
title: "Installation"
theme: "Dracula"
commands:
- "npm install my-package"
- "npm run build"
- name: "Usage Demo"
commands: ["npm install my-package"]
- name: "Usage"
output: "usage.gif"
commands:
- "my-package --help"
- "my-package run"
commands: ["my-package --help", "my-package run"]
```
**Usage:**
```bash
scripts/batch_generate.py config.yaml --output-dir ./demos
python3 ${CLAUDE_SKILL_DIR}/scripts/batch_generate.py demos.yaml --output-dir ./gifs
```
**When to Use Batch Generation:**
- Creating a suite of related demos
- Documenting multiple features
- Generating demos for tutorials or documentation
- Maintaining consistent demo series
### 3. Interactive Recording
Use `record_interactive.sh` for recording live terminal sessions.
**Usage:**
```bash
scripts/record_interactive.sh output.gif \
--theme "Dracula" \
--width 1400
```
**Recording Process:**
1. Script starts asciinema recording
2. Type commands naturally in your terminal
3. Press Ctrl+D when finished
4. Script auto-converts to GIF via VHS
**When to Use Interactive Recording:**
- Demonstrating complex workflows
- Showing real command output
- Capturing live interactions
- Recording debugging sessions
### 4. Manual Tape File Creation
For maximum control, create VHS tape files manually using templates.
**Available Templates:**
- `assets/templates/basic.tape` - Simple command demo
- `assets/templates/interactive.tape` - Typing simulation
**Example Workflow:**
1. Copy template: `cp assets/templates/basic.tape my-demo.tape`
2. Edit commands and timing
3. Generate GIF: `vhs < my-demo.tape`
Consult `references/vhs_syntax.md` for complete VHS syntax reference.
## Workflow Guidance
### For Simple Demos (1-3 commands)
Use automated generation for quick results:
Record a live terminal session:
```bash
scripts/auto_generate_demo.py \
-c "echo 'Hello World'" \
-c "ls -la" \
-o hello-demo.gif \
--title "Hello Demo"
bash ${CLAUDE_SKILL_DIR}/scripts/record_interactive.sh output.gif --theme "Catppuccin Latte"
# Type commands naturally, Ctrl+D when done
```
### For Multiple Related Demos
Requires asciinema (`brew install asciinema`).
Create a batch configuration file and use batch generation:
### 4. Manual Tape File
1. Create `demos-config.yaml` with all demo definitions
2. Run: `scripts/batch_generate.py demos-config.yaml --output-dir ./output`
3. All demos generate automatically with consistent settings
For maximum control, write a tape directly. Templates in `assets/templates/`:
### For Interactive/Complex Workflows
- `basic.tape` — simple command sequence
- `interactive.tape` — typing simulation
- `self-bootstrap.tape`**self-cleaning demo with hidden setup** (recommended for repeatable demos)
Use interactive recording to capture real behavior:
## Advanced Patterns
These patterns come from production use. See `references/advanced_patterns.md` for full details.
### Self-Bootstrapping Demos
Demos that clean previous state, set up environment, and hide all of it from the viewer:
```tape
Hide
Type "cleanup-previous-state 2>/dev/null"
Enter
Sleep 2s
Type "clear"
Enter
Sleep 500ms
Show
Type "the-command-users-see"
Enter
Sleep 3s
```
The `Hide` → commands → `clear``Show` sequence is critical. `clear` wipes the terminal buffer so hidden commands don't leak into the GIF.
### Output Noise Filtering
Filter noisy progress lines from commands that produce verbose output:
```tape
# Hidden: create a wrapper function that filters noise
Hide
Type "echo <base64-encoded-wrapper> | base64 -d > /tmp/w.sh && source /tmp/w.sh"
Enter
Sleep 500ms
Type "clear"
Enter
Sleep 500ms
Show
# Visible: clean command, filtered output
Type "my-noisy-command"
Enter
Sleep 3s
```
### Frame Verification
After recording, verify GIF content by extracting key frames:
```bash
scripts/record_interactive.sh my-workflow.gif
# Type commands naturally
# Ctrl+D when done
# Extract frames at specific positions
ffmpeg -i demo.gif -vf "select=eq(n\,100)" -frames:v 1 /tmp/frame.png -y 2>/dev/null
# View the frame (Claude can read images)
# Use Read tool on /tmp/frame.png to verify content
```
### For Custom Timing/Layout
### Post-Processing Speed-Up
Create manual tape file with precise control:
Use gifsicle to speed up recordings without re-recording:
1. Start with template or generate base tape with `--no-execute`
2. Edit timing, add comments, customize layout
3. Generate: `vhs < custom-demo.tape`
```bash
# 2x speed (halve frame delay)
gifsicle -d2 original.gif "#0-" > fast.gif
## Best Practices
# 1.5x speed
gifsicle -d4 original.gif "#0-" > faster.gif
```
Refer to `references/best_practices.md` for comprehensive guidelines. Key recommendations:
### Template Placeholder Pattern
**Timing:**
- Quick commands (ls, pwd): 1s sleep
- Standard commands (grep, cat): 2s sleep
- Heavy operations (install, build): 3s+ sleep
Keep tape files generic with placeholders, replace at build time:
**Sizing:**
- Standard: 1400x700 (recommended)
- Compact: 1200x600
- Presentations: 1800x900
```tape
# In tape file
Type "claude plugin marketplace add MARKETPLACE_REPO"
**Themes:**
- Documentation: Nord, GitHub Dark
- Code demos: Dracula, Monokai
- Presentations: High-contrast themes
# In build script
sed "s|MARKETPLACE_REPO|$DETECTED_REPO|g" template.tape > rendered.tape
vhs rendered.tape
```
**Duration:**
- Target: 15-30 seconds
- Maximum: 60 seconds
- Create series for complex topics
## Timing & Sizing Reference
| Context | Width | Height | Font | Duration |
|---------|-------|--------|------|----------|
| README/docs | 1400 | 600 | 16-20 | 10-20s |
| Presentation | 1800 | 900 | 24 | 15-30s |
| Compact embed | 1200 | 600 | 14-16 | 10-15s |
| Wide output | 1600 | 800 | 16 | 15-30s |
See `references/best_practices.md` for detailed guidelines.
## Troubleshooting
### VHS Not Installed
```bash
# macOS
brew install vhs
# Linux (via Go)
go install github.com/charmbracelet/vhs@latest
```
### Asciinema Not Installed
```bash
# macOS
brew install asciinema
# Linux
sudo apt install asciinema
```
### Demo File Too Large
**Solutions:**
1. Reduce duration (shorter sleep times)
2. Use smaller dimensions (1200x600)
3. Consider MP4 format: `Output demo.mp4`
4. Split into multiple shorter demos
### Output Not Readable
**Solutions:**
1. Increase font size: `--font-size 18`
2. Use wider terminal: `--width 1600`
3. Choose high-contrast theme: `--theme "Dracula"`
4. Test on target display device
## Examples
### Example 1: Quick Install Demo
User request: "Create a demo showing npm install"
```bash
scripts/auto_generate_demo.py \
-c "npm install my-package" \
-o install-demo.gif \
--title "Package Installation"
```
### Example 2: Multi-Step Tutorial
User request: "Create a demo showing project setup with git clone, install, and run"
```bash
scripts/auto_generate_demo.py \
-c "git clone https://github.com/user/repo.git" \
-c "cd repo" \
-c "npm install" \
-c "npm start" \
-o setup-demo.gif \
--title "Project Setup" \
--theme "Nord"
```
### Example 3: Batch Generation
User request: "Generate demos for all my CLI tool features"
1. Create `features-demos.yaml`:
```yaml
demos:
- name: "Help Command"
output: "help.gif"
commands: ["my-tool --help"]
- name: "Init Command"
output: "init.gif"
commands: ["my-tool init", "ls -la"]
- name: "Run Command"
output: "run.gif"
commands: ["my-tool run --verbose"]
```
2. Generate all:
```bash
scripts/batch_generate.py features-demos.yaml --output-dir ./demos
```
### Example 4: Interactive Session
User request: "Record me using my CLI tool interactively"
```bash
scripts/record_interactive.sh my-session.gif --theme "Tokyo Night"
# User types commands naturally
# Ctrl+D to finish
```
## Bundled Resources
### scripts/
- **`auto_generate_demo.py`** - Automated demo generation from command lists
- **`batch_generate.py`** - Generate multiple demos from YAML/JSON config
- **`record_interactive.sh`** - Record and convert interactive terminal sessions
### references/
- **`vhs_syntax.md`** - Complete VHS tape file syntax reference
- **`best_practices.md`** - Demo creation guidelines and best practices
### assets/
- **`templates/basic.tape`** - Basic command demo template
- **`templates/interactive.tape`** - Interactive typing demo template
- **`examples/batch-config.yaml`** - Example batch configuration file
| Problem | Solution |
|---------|----------|
| VHS not installed | `brew install charmbracelet/tap/vhs` |
| gifsicle not installed | `brew install gifsicle` |
| GIF too large | Reduce dimensions, sleep times, or use `--speed 2` |
| Text wraps/breaks | Increase `--width` or decrease `--font-size` |
| VHS parse error on `$` or `\"` | Use base64 encoding (see Critical section above) |
| Hidden commands leak into GIF | Add `clear` + `Sleep 500ms` before `Show` |
| Commands execute before previous finishes | Increase `Sleep` duration |
## Dependencies
**Required:**
- VHS (https://github.com/charmbracelet/vhs)
**Required:** VHS (`brew install charmbracelet/tap/vhs`)
**Optional:**
- asciinema (for interactive recording)
- PyYAML (for batch YAML configs): `pip install pyyaml`
## Output Formats
VHS supports multiple output formats:
```tape
Output demo.gif # GIF (default, best for documentation)
Output demo.mp4 # MP4 (better compression for long demos)
Output demo.webm # WebM (smaller file size)
```
Choose based on use case:
- **GIF**: Documentation, README files, easy embedding
- **MP4**: Longer demos, better quality, smaller size
- **WebM**: Web-optimized, smallest file size
## Summary
This skill provides three main approaches:
1. **Automated** (`auto_generate_demo.py`) - Quick, easy, smart defaults
2. **Batch** (`batch_generate.py`) - Multiple demos, consistent settings
3. **Interactive** (`record_interactive.sh`) - Live recording, real output
Choose the approach that best fits the user's needs. For most cases, automated generation is the fastest and most convenient option.
**Optional:** gifsicle (speed-up), asciinema (interactive recording), ffmpeg (frame verification), PyYAML (batch YAML configs)

View File

@@ -0,0 +1,51 @@
# Self-bootstrapping demo template
# Cleans previous state, sets up environment, records clean demo
#
# MARKETPLACE_REPO — replaced by recording script via sed
# BASE64_WRAPPER — base64-encoded output filter function
# To create: echo 'my_func() { command my_func "$@" 2>&1 | grep -v "noise"; }' | base64
Output demo.gif
Set Theme "Catppuccin Latte"
Set FontSize 24
Set Width 1400
Set Height 600
Set Padding 20
Set TypingSpeed 10ms
Set Shell zsh
# Hidden bootstrap: cleanup + output filter + clear screen
Hide
Type "cleanup-command-here 2>/dev/null"
Enter
Sleep 3s
# Base64-encoded wrapper filters noisy output lines.
# VHS cannot parse shell special chars ($, \") in Type strings, so base64 is the workaround.
Type "echo BASE64_WRAPPER | base64 -d > /tmp/cw.sh && source /tmp/cw.sh"
Enter
Sleep 500ms
Type "clear"
Enter
Sleep 500ms
Show
# Stage 1: Setup
Type "setup-command MARKETPLACE_REPO"
Enter
Sleep 8s
Enter
Sleep 300ms
# Stage 2: Main action
Type "main-command"
Enter
Sleep 3s
Enter
Sleep 300ms
# Stage 3: Verify
Type "verify-command"
Enter
Sleep 2s
Sleep 1s

View File

@@ -0,0 +1,240 @@
# 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:
```tape
# 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
```bash
# 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
```bash
echo 'my_tool() { command my_tool "$@" 2>&1 | grep -v -E "cache|progress|downloading|timeout"; }' | base64
# Output: bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0K
```
### Step 3: Use in tape
```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
```bash
# 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
```bash
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
```bash
# 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
```bash
# Lossless optimization
gifsicle -O3 input.gif > optimized.gif
# Reduce colors (lossy but smaller)
gifsicle --colors 128 input.gif > smaller.gif
```
### Typical recording script pattern
```bash
# 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:
```tape
# demo.tape (template)
Type "tool marketplace add REPO_PLACEHOLDER"
Enter
```
```bash
# 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)
```bash
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:
```bash
#!/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)"
```

View File

@@ -2,10 +2,14 @@
"""
Auto-generate CLI demos from command descriptions.
This script creates VHS tape files and generates GIF demos automatically.
Creates VHS tape files and generates GIF demos with support for:
- Hidden bootstrap commands (self-cleaning state)
- Output noise filtering via base64-encoded wrapper
- Post-processing speed-up via gifsicle
"""
import argparse
import base64
import subprocess
import sys
from pathlib import Path
@@ -21,21 +25,54 @@ def create_tape_file(
width: int = 1400,
height: int = 700,
padding: int = 20,
bootstrap: Optional[List[str]] = None,
filter_pattern: Optional[str] = None,
) -> str:
"""Generate a VHS tape file from commands."""
tape_lines = [
f'Output {output_gif}',
'',
f'Set Theme "{theme}"',
f'Set FontSize {font_size}',
f'Set Width {width}',
f'Set Height {height}',
f'Set Theme "{theme}"',
f'Set Padding {padding}',
'Set TypingSpeed 10ms',
'Set Shell zsh',
'',
]
# Add title if provided
# Hidden bootstrap: cleanup + optional output filter
has_hidden = bootstrap or filter_pattern
if has_hidden:
tape_lines.append('Hide')
if bootstrap:
# Combine all bootstrap commands with semicolons
combined = "; ".join(
cmd if "2>/dev/null" in cmd else f"{cmd} 2>/dev/null"
for cmd in bootstrap
)
tape_lines.append(f'Type "{combined}"')
tape_lines.append('Enter')
tape_lines.append('Sleep 3s')
if filter_pattern:
# Create a wrapper function that filters noisy output
wrapper = f'_wrap() {{ "$@" 2>&1 | grep -v -E "{filter_pattern}"; }}'
encoded = base64.b64encode(wrapper.encode()).decode()
tape_lines.append(f'Type "echo {encoded} | base64 -d > /tmp/cw.sh && source /tmp/cw.sh"')
tape_lines.append('Enter')
tape_lines.append('Sleep 500ms')
# Clear screen before Show to prevent hidden text from leaking
tape_lines.append('Type "clear"')
tape_lines.append('Enter')
tape_lines.append('Sleep 500ms')
tape_lines.append('Show')
tape_lines.append('')
# Title
if title:
tape_lines.extend([
f'Type "# {title}" Sleep 500ms Enter',
@@ -43,68 +80,94 @@ def create_tape_file(
'',
])
# Add commands with smart timing
# Commands with smart timing
for i, cmd in enumerate(commands, 1):
# Type the command
tape_lines.append(f'Type "{cmd}" Sleep 500ms')
# If filter is active, prefix with _wrap
if filter_pattern:
tape_lines.append(f'Type "_wrap {cmd}"')
else:
tape_lines.append(f'Type "{cmd}"')
tape_lines.append('Enter')
# Smart sleep based on command complexity
if any(keyword in cmd.lower() for keyword in ['install', 'build', 'test', 'deploy']):
if any(kw in cmd.lower() for kw in ['install', 'build', 'test', 'deploy', 'marketplace']):
sleep_time = '3s'
elif any(keyword in cmd.lower() for keyword in ['ls', 'pwd', 'echo', 'cat']):
elif any(kw in cmd.lower() for kw in ['ls', 'pwd', 'echo', 'cat', 'grep']):
sleep_time = '1s'
else:
sleep_time = '2s'
tape_lines.append(f'Sleep {sleep_time}')
# Add spacing between commands
# Empty line between stages for readability
if i < len(commands):
tape_lines.append('Enter')
tape_lines.append('Sleep 300ms')
tape_lines.append('')
tape_lines.append('')
tape_lines.append('Sleep 1s')
return '\n'.join(tape_lines)
def speed_up_gif(gif_path: str, speed: int) -> bool:
"""Speed up GIF using gifsicle. Returns True on success."""
try:
subprocess.run(['gifsicle', '--version'], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("⚠ gifsicle not found, skipping speed-up. Install: brew install gifsicle", file=sys.stderr)
return False
# delay = 10 / speed (10 = normal, 5 = 2x, 3 = ~3x)
delay = max(1, 10 // speed)
tmp = f"/tmp/demo_raw_{Path(gif_path).stem}.gif"
subprocess.run(['cp', gif_path, tmp], check=True)
with open(gif_path, 'wb') as out:
subprocess.run(['gifsicle', f'-d{delay}', tmp, '#0-'], stdout=out, check=True)
Path(tmp).unlink(missing_ok=True)
return True
def main():
parser = argparse.ArgumentParser(
description='Auto-generate CLI demos from commands',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
# Generate demo from single command
# Simple demo
%(prog)s -c "npm install" -o demo.gif
# Generate demo with multiple commands
%(prog)s -c "git clone repo" -c "cd repo" -c "npm install" -o setup.gif
# With hidden bootstrap (self-cleaning)
%(prog)s -c "my-tool run" -o demo.gif \\
--bootstrap "my-tool reset" --speed 2
# Custom theme and size
%(prog)s -c "ls -la" -o demo.gif --theme Monokai --width 1200
# With title
%(prog)s -c "echo Hello" -o demo.gif --title "My Demo"
# With output noise filtering
%(prog)s -c "deploy-tool push" -o demo.gif \\
--filter "cache|progress|downloading"
'''
)
parser.add_argument('-c', '--command', action='append', required=True,
help='Command to include in demo (can be specified multiple times)')
help='Command to include (repeatable)')
parser.add_argument('-o', '--output', required=True,
help='Output GIF file path')
parser.add_argument('--title', help='Demo title (optional)')
parser.add_argument('--theme', default='Dracula',
help='VHS theme (default: Dracula)')
parser.add_argument('--font-size', type=int, default=16,
help='Font size (default: 16)')
parser.add_argument('--width', type=int, default=1400,
help='Terminal width (default: 1400)')
parser.add_argument('--height', type=int, default=700,
help='Terminal height (default: 700)')
parser.add_argument('--title', help='Demo title')
parser.add_argument('--theme', default='Dracula', help='VHS theme (default: Dracula)')
parser.add_argument('--font-size', type=int, default=16, help='Font size (default: 16)')
parser.add_argument('--width', type=int, default=1400, help='Terminal width (default: 1400)')
parser.add_argument('--height', type=int, default=700, help='Terminal height (default: 700)')
parser.add_argument('--bootstrap', action='append',
help='Hidden setup command run before demo (repeatable)')
parser.add_argument('--filter',
help='Regex pattern to filter from command output')
parser.add_argument('--speed', type=int, default=1,
help='Playback speed multiplier (default: 1, uses gifsicle)')
parser.add_argument('--no-execute', action='store_true',
help='Generate tape file only, do not execute VHS')
help='Generate tape file only')
args = parser.parse_args()
# Generate tape file content
tape_content = create_tape_file(
commands=args.command,
output_gif=args.output,
@@ -113,37 +176,38 @@ Examples:
font_size=args.font_size,
width=args.width,
height=args.height,
bootstrap=args.bootstrap,
filter_pattern=args.filter,
)
# Write tape file
output_path = Path(args.output)
tape_file = output_path.with_suffix('.tape')
with open(tape_file, 'w') as f:
f.write(tape_content)
tape_file.write_text(tape_content)
print(f"✓ Generated tape file: {tape_file}")
if not args.no_execute:
# Check if VHS is installed
try:
subprocess.run(['vhs', '--version'], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("✗ VHS is not installed!", file=sys.stderr)
print("Install it with: brew install vhs", file=sys.stderr)
print(f"✓ You can manually run: vhs < {tape_file}", file=sys.stderr)
print("✗ VHS not installed. Install: brew install charmbracelet/tap/vhs", file=sys.stderr)
print(f"✓ Run manually: vhs {tape_file}", file=sys.stderr)
return 1
# Execute VHS
print(f"Generating GIF: {args.output}")
print(f"Recording: {args.output}")
try:
subprocess.run(['vhs', str(tape_file)], check=True)
print(f"✓ Demo generated: {args.output}")
print(f" Size: {output_path.stat().st_size / 1024:.1f} KB")
except subprocess.CalledProcessError as e:
print(f"✗ VHS execution failed: {e}", file=sys.stderr)
print(f"✗ VHS failed: {e}", file=sys.stderr)
return 1
# Post-processing speed-up
if args.speed > 1:
print(f"Speeding up {args.speed}x...")
speed_up_gif(args.output, args.speed)
size_kb = output_path.stat().st_size / 1024
print(f"✓ Done: {args.output} ({size_kb:.0f} KB)")
return 0