
Cli Demo Generator
- 707 installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
cli-demo-generator is an agent skill that turns terminal command sequences into polished animated GIF demos using VHS recordings for developers who need visual CLI documentation in READMEs or product pages.
About
cli-demo-generator is an agent skill from daymade/claude-code-skills that records professional animated CLI demos as GIFs with VHS terminal tapes. The workflow handles tape file creation, self-bootstrapping demos with hidden setup steps, output noise filtering, post-processing speed-up, and frame-level verification. Four approaches span fully automated to manual tape editing. Developers reach for cli-demo-generator when README files need animated proof of a CLI workflow, product pages must showcase shell commands, or requests mention "record terminal", "VHS tape", "demo GIF", or "animate my CLI". The skill targets documentation and marketing visuals for command-line tools rather than changing tool behavior.
- Generates professional animated CLI demos as GIFs using VHS terminal recordings
- Four approaches from fully automated to pixel-precise manual control
- Supports self-bootstrapping demos that clean their own state before recording
- Handles tape file creation, output noise filtering, post-processing speed-up, and frame-level verification
- Triggers on requests to record terminal, create VHS tape, demo GIF, or animate any CLI workflow
Cli Demo Generator by the numbers
- 707 all-time installs (skills.sh)
- Ranked #331 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill cli-demo-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 707 |
|---|---|
| repo stars | ★ 1.3k |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
How do you record terminal demos as GIFs for READMEs?
Turn any sequence of terminal commands into a polished, animated GIF demo for READMEs, documentation, or product showcases.
Who is it for?
CLI maintainers and developer advocates who need repeatable VHS-recorded GIF demos for README files or product documentation.
Skip if: GUI application screen recordings or teams documenting workflows without any terminal or shell component.
When should I use this skill?
A user asks to record terminal output, create a VHS tape, generate a CLI demo GIF, or animate shell commands for a README.
What you get
VHS .tape files, polished animated GIF demos, filtered terminal output, and frame-verified recordings ready for README or docs embed.
- VHS .tape file
- Animated GIF demo
- Noise-filtered terminal recording
By the numbers
- Documents four approaches for creating CLI demo GIFs
- Uses VHS terminal recording with frame-level verification
Files
CLI Demo Generator
Create professional animated CLI demos. Four approaches, from fully automated to pixel-precise manual control.
Quick Start
Simplest path — give commands, get GIF:
python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "npm install my-package" \
-c "npm run build" \
-o demo.gifSelf-bootstrapping demo — for repeatable recordings that clean their own state:
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 2Critical: VHS Parser Limitations
VHS Type strings cannot contain $, \", or backticks. These cause parse errors:
# FAILS — VHS parser rejects special chars
Type "echo \"hello $USER\""
Type "claude() { command claude \"$@\"; }"Workaround: base64 encode the command, decode at runtime:
# 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)
python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "command1" -c "command2" \
-o output.gif \
--title "My Demo" \
--theme "Catppuccin Latte" \
--font-size 24 \
--width 1400 --height 600| 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 timing: install/build/test/deploy → 3s, ls/pwd/echo → 1s, others → 2s.
2. Batch Generation
Create multiple demos from one config:
# demos.yaml
demos:
- name: "Install"
output: "install.gif"
commands: ["npm install my-package"]
- name: "Usage"
output: "usage.gif"
commands: ["my-package --help", "my-package run"]python3 ${CLAUDE_SKILL_DIR}/scripts/batch_generate.py demos.yaml --output-dir ./gifs3. Interactive Recording
Record a live terminal session:
bash ${CLAUDE_SKILL_DIR}/scripts/record_interactive.sh output.gif --theme "Catppuccin Latte"
# Type commands naturally, Ctrl+D when doneRequires asciinema (brew install asciinema).
4. Manual Tape File
For maximum control, write a tape directly. Templates in assets/templates/:
basic.tape— simple command sequenceinteractive.tape— typing simulationself-bootstrap.tape— self-cleaning demo with hidden setup (recommended for repeatable demos)
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:
Hide
Type "cleanup-previous-state 2>/dev/null"
Enter
Sleep 2s
Type "clear"
Enter
Sleep 500ms
Show
Type "the-command-users-see"
Enter
Sleep 3sThe 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:
# 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 3sFrame Verification
After recording, verify GIF content by extracting key frames:
# 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 contentPost-Processing Speed-Up
Use gifsicle to speed up recordings without re-recording:
# 2x speed (halve frame delay)
gifsicle -d2 original.gif "#0-" > fast.gif
# 1.5x speed
gifsicle -d4 original.gif "#0-" > faster.gifTemplate Placeholder Pattern
Keep tape files generic with placeholders, replace at build time:
# In tape file
Type "claude plugin marketplace add MARKETPLACE_REPO"
# In build script
sed "s|MARKETPLACE_REPO|$DETECTED_REPO|g" template.tape > rendered.tape
vhs rendered.tapeTiming & 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
| 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 (brew install charmbracelet/tap/vhs)
Optional: gifsicle (speed-up), asciinema (interactive recording), ffmpeg (frame verification), PyYAML (batch YAML configs)
Security scan passed
Scanned at: 2026-06-13T19:44:41.146630
Tool: gitleaks + pattern-based validation
Content hash: 898073faf893100f21f8ab923994f8d3ab8a852d326c25613b5cddd97bbfd296
# Example batch configuration for generating multiple demos
# Usage: batch_generate.py batch-config.yaml --output-dir ./output
demos:
- name: "Installation Demo"
output: "install.gif"
title: "Package Installation"
theme: "Dracula"
width: 1400
height: 700
commands:
- "npm install my-package"
- "npm run build"
- name: "Usage Demo"
output: "usage.gif"
title: "Basic Usage"
theme: "Nord"
commands:
- "my-package --help"
- "my-package init"
- "my-package run"
- name: "Quick Start"
output: "quickstart.gif"
theme: "Tokyo Night"
commands:
- "git clone https://github.com/user/repo.git"
- "cd repo"
- "npm install"
- "npm start"
Output demo.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Dracula"
Set Padding 20
Type "# Demo Title" Sleep 500ms Enter
Sleep 1s
Type "command1" Sleep 500ms
Enter
Sleep 2s
Type "command2" Sleep 500ms
Enter
Sleep 2s
Output demo.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Dracula"
Set Padding 20
Set TypingSpeed 100ms
Type "# Interactive Demo" Sleep 500ms Enter
Sleep 1.5s
Type "# Type commands naturally..." Sleep 500ms Enter
Sleep 1s
Type "echo 'Hello World'" Sleep 500ms
Enter
Sleep 2s
Type "# Typing simulation makes it feel real" Sleep 500ms Enter
Sleep 2s
# 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
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 3sWhy `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: bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0KStep 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 5sWhen 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
doneCheck total frame count and duration
ffmpeg -i demo.gif 2>&1 | grep -E "Duration|fps"
# Duration: 00:00:10.50, ... 25 fps → 262 frames totalWhat 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.gifOptimize file size
# Lossless optimization
gifsicle -O3 input.gif > optimized.gif
# Reduce colors (lossy but smaller)
gifsicle --colors 128 input.gif > smaller.gifTypical 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.gifAuto-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.tapeAuto-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)"CLI Demo Best Practices
Guidelines for creating effective, professional CLI demos.
General Principles
1. Keep It Short
- Target: 15-30 seconds per demo
- Maximum: 60 seconds (unless documenting complex workflows)
- Reason: Short demos maintain viewer attention and are easier to consume
2. Show, Don't Tell
- Focus on visual demonstration over textual explanation
- Let the commands and output speak for themselves
- Add brief comment-style titles when needed
3. One Concept Per Demo
- Each demo should illustrate a single feature or workflow
- For complex topics, create a series of short demos
- Better to have multiple focused demos than one lengthy tutorial
Technical Guidelines
Timing and Pacing
Command Entry Timing:
Type "command" Sleep 500ms # Fast enough to feel natural
Enter # ImmediatePost-Command Sleep (based on operation):
- Quick commands (ls, pwd, echo): 1s
- Standard commands (grep, cat, git status): 1.5-2s
- Heavy operations (install, build, test): 3s+
- Final command: 2-3s for viewers to see result
Between Sections:
- Add 1-1.5s between related commands
- Add 2s+ between different concepts
Terminal Dimensions
Standard Sizes:
# Compact (for narrow contexts)
Set Width 1200
Set Height 600
# Standard (recommended default)
Set Width 1400
Set Height 700
# Wide (for complex output)
Set Width 1600
Set Height 800
# Presentation (for slides)
Set Width 1800
Set Height 900Choosing Dimensions:
- Consider the output width (avoid wrapping)
- Test with longest expected line
- Standard 1400x700 works for most cases
Font Sizing
Recommended Sizes:
# Documentation (small, information-dense)
Set FontSize 14
# Standard demos (readable)
Set FontSize 16
# Presentations (clear from distance)
Set FontSize 20-24Choosing Font Size:
- Smaller fonts allow more output visibility
- Larger fonts improve readability on mobile
- Test on target display devices
Theme Selection
By Context:
Documentation/Tutorials:
- Nord - Clean, professional
- GitHub Dark - Familiar to developers
- Catppuccin - Easy on eyes for long reading
Code Demos:
- Dracula - Popular, high contrast
- Monokai - Classic, widely recognized
- Tokyo Night - Modern, vibrant
Presentations:
- High-contrast themes for visibility
- Avoid very dark themes in bright rooms
- Test on actual projection equipment
Brand Alignment:
- Match company/project color schemes
- Custom themes can be defined
Content Guidelines
Command Structure
Clear Sequencing:
# Good - Shows logical flow
Type "# Step 1: Setup"
Sleep 500ms Enter
Type "mkdir project && cd project"
Sleep 500ms Enter
Sleep 2s
Type "# Step 2: Install"
Sleep 500ms Enter
Type "npm install"
Sleep 500ms Enter
Sleep 3sAvoid:
# Bad - No context, rushed
Type "mkdir project"
Enter
Type "cd project"
Enter
Type "npm install"
EnterAdding Context
Title Slides:
Type "# Demo: Package Installation"
Sleep 500ms Enter
Sleep 1.5sSection Headers:
Type "## Installing dependencies..."
Sleep 500ms Enter
Sleep 1sComments:
Type "npm install # This may take a moment"
Sleep 500ms EnterOutput Visibility
Ensure Key Output is Visible:
- Let important output display fully before next command
- For long output, consider showing excerpts
- Use
Sleepto give viewers time to read
Managing Long Output:
# Option 1: Show beginning only
Type "npm install"
Enter
Sleep 2s # Shows first part of output
Ctrl+C # Stop before it scrolls too much
# Option 2: Use commands that limit output
Type "git log --oneline -5" # Show last 5 commits only
EnterFile Size Optimization
Target Sizes
- Small demos (<500KB): Ideal for documentation
- Medium demos (500KB-1MB): Acceptable for most uses
- Large demos (>1MB): Consider compression or shorter duration
Reducing File Size
1. Shorter Duration:
# Reduce unnecessary sleep time
Sleep 1s # Instead of Sleep 3s2. Smaller Dimensions:
# Use compact size for simple demos
Set Width 1200
Set Height 6003. Appropriate Format:
Output demo.mp4 # Better compression for longer demos
Output demo.webm # Smaller file size than GIF
Output demo.gif # Good for short demosAccessibility
Consider All Viewers
Color Choices:
- High contrast improves readability
- Avoid color-only distinctions
- Test with color-blind simulators
Font Size:
- 16pt minimum for web documentation
- 20pt minimum for presentations
- Test on mobile devices
Pacing:
- Allow time to read output
- Avoid rapid command sequences
- Provide clear visual breaks
Testing and Quality Assurance
Before Publishing
1. Watch the Entire Demo:
- Verify all commands execute as expected
- Check for timing issues
- Ensure output is visible
2. Test on Different Displays:
- Desktop monitors
- Mobile devices
- Projection screens (if for presentations)
3. Check File Size:
- Optimize if necessary
- Consider alternative formats
4. Verify Accessibility:
- Readable fonts
- Clear contrast
- Appropriate pacing
5. Get Feedback:
- Show to someone unfamiliar with the content
- Ask if the flow is clear
- Adjust based on feedback
Common Mistakes to Avoid
❌ Too Fast
Type "command1" Enter Sleep 0.5s
Type "command2" Enter Sleep 0.5s
Type "command3" Enter Sleep 0.5s
# Viewers can't process this✅ Appropriate Pacing
Type "command1" Sleep 500ms Enter
Sleep 2s
Type "command2" Sleep 500ms Enter
Sleep 2s❌ No Context
Type "npm install"
Enter
# What are we installing? Why?✅ With Context
Type "# Installing dependencies"
Sleep 500ms Enter
Sleep 1s
Type "npm install"
Sleep 500ms Enter❌ Output Scrolls Too Fast
Type "long-running-command"
Enter
Sleep 1s # Output still scrolling!
Type "next-command"✅ Allow Output to Complete
Type "long-running-command"
Enter
Sleep 3s # Let it finish
Type "next-command"Examples of Good Demos
Simple Feature Demo (15 seconds)
Output feature-demo.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Dracula"
Type "# Demo: Quick Start" Sleep 500ms Enter
Sleep 1.5s
Type "npm install my-tool" Sleep 500ms Enter
Sleep 2.5s
Type "my-tool --help" Sleep 500ms Enter
Sleep 2sMulti-Step Workflow (30 seconds)
Output workflow-demo.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Nord"
Type "# Demo: Complete Workflow" Sleep 500ms Enter
Sleep 1.5s
Type "# 1. Create project" Sleep 500ms Enter
Type "mkdir my-project && cd my-project" Sleep 500ms Enter
Sleep 2s
Type "# 2. Initialize" Sleep 500ms Enter
Type "npm init -y" Sleep 500ms Enter
Sleep 2s
Type "# 3. Install package" Sleep 500ms Enter
Type "npm install express" Sleep 500ms Enter
Sleep 3s
Type "# 4. Ready to code!" Sleep 500ms Enter
Sleep 2sSummary Checklist
Before publishing a demo, verify:
- [ ] Duration is appropriate (15-30s ideal)
- [ ] Timing allows reading output
- [ ] Commands are clear and purposeful
- [ ] Context is provided where needed
- [ ] Output is fully visible
- [ ] File size is reasonable
- [ ] Theme and fonts are readable
- [ ] Tested on target devices
- [ ] Accessible to all viewers
- [ ] Demonstrates one clear concept
VHS Tape File Syntax Reference
VHS (Video Home System) is a tool for creating terminal recordings as code. This reference covers the complete tape file syntax.
Basic Structure
Output demo.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Dracula"
Type "echo Hello"
Enter
Sleep 1sConfiguration Commands
Output
Specify the output file path and format:
Output demo.gif # GIF format (default)
Output demo.mp4 # MP4 video
Output demo.webm # WebM videoSet Commands
Configure the terminal appearance:
Set FontSize 16 # Font size (10-72)
Set Width 1400 # Terminal width in pixels
Set Height 700 # Terminal height in pixels
Set Theme "Dracula" # Color theme
Set Padding 20 # Padding around terminal (pixels)
Set TypingSpeed 50ms # Speed of typing animation
Set Shell bash # Shell to use (bash, zsh, fish)
Set FontFamily "MonoLisa" # Font family nameInteraction Commands
Type
Simulate typing text:
Type "ls -la" # Type the command
Type "Hello World" # Type any textEnter
Press the Enter key:
EnterBackspace
Delete characters:
Backspace # Delete one character
Backspace 5 # Delete 5 charactersSleep
Pause execution:
Sleep 1s # Sleep for 1 second
Sleep 500ms # Sleep for 500 milliseconds
Sleep 2.5s # Sleep for 2.5 secondsCtrl+C
Send interrupt signal:
Ctrl+CKey combinations
Ctrl+D # End of transmission
Ctrl+L # Clear screen
Tab # Tab completionAdvanced Features
Play (asciinema integration)
Play back an asciinema recording:
Play recording.castHide/Show
Control terminal visibility:
Hide
Type "secret command"
ShowScreenshot
Take a screenshot at specific point:
Screenshot demo-screenshot.pngAvailable Themes
Popular built-in themes:
- Dracula - Dark purple theme
- Monokai - Classic dark theme
- Nord - Arctic-inspired cool theme
- Catppuccin - Soothing pastel theme
- GitHub Dark - GitHub's dark theme
- Tokyo Night - Vibrant dark theme
- Gruvbox - Retro groove colors
Example Templates
Basic Command Demo
Output demo.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Dracula"
Type "# Demo Title" Sleep 500ms Enter
Sleep 1s
Type "command1" Sleep 500ms Enter
Sleep 2s
Type "command2" Sleep 500ms Enter
Sleep 2sInteractive Typing Demo
Output demo.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Dracula"
Set TypingSpeed 100ms
Type "npm install my-package"
Enter
Sleep 3s
Type "npm start"
Enter
Sleep 2sMulti-Step Tutorial
Output tutorial.gif
Set FontSize 16
Set Width 1400
Set Height 700
Set Theme "Tokyo Night"
Type "# Step 1: Clone the repository" Enter
Sleep 1s
Type "git clone https://github.com/user/repo.git" Enter
Sleep 3s
Type "# Step 2: Install dependencies" Enter
Sleep 1s
Type "cd repo && npm install" Enter
Sleep 3s
Type "# Step 3: Run the app" Enter
Sleep 1s
Type "npm start" Enter
Sleep 2sBest Practices
1. Timing: Use appropriate sleep durations
- Quick commands: 1s
- Medium commands: 2s
- Long commands (install, build): 3s+
2. Width/Height: Standard sizes
- Compact: 1200x600
- Standard: 1400x700
- Wide: 1600x800
3. Font Size: Readability
- Small terminals: 14-16
- Standard: 16-18
- Presentations: 20-24
4. Theme Selection: Consider context
- Code demos: Dracula, Monokai
- Documentation: Nord, GitHub Dark
- Presentations: High-contrast themes
5. Title Slides: Add context
Type "# Demo: Project Setup" Enter
Sleep 1s6. Cleanup: Show clear ending
Sleep 2s
Type "# Demo complete!" Enter#!/usr/bin/env python3
"""
Auto-generate CLI demos from command descriptions.
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
from typing import List, Optional
def create_tape_file(
commands: List[str],
output_gif: str,
title: Optional[str] = None,
theme: str = "Dracula",
font_size: int = 16,
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 Padding {padding}',
'Set TypingSpeed 10ms',
'Set Shell zsh',
'',
]
# 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',
'Sleep 1s',
'',
])
# Commands with smart timing
for i, cmd in enumerate(commands, 1):
# 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(kw in cmd.lower() for kw in ['install', 'build', 'test', 'deploy', 'marketplace']):
sleep_time = '3s'
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}')
# 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:
# Simple demo
%(prog)s -c "npm install" -o demo.gif
# With hidden bootstrap (self-cleaning)
%(prog)s -c "my-tool run" -o demo.gif \\
--bootstrap "my-tool reset" --speed 2
# 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 (repeatable)')
parser.add_argument('-o', '--output', required=True,
help='Output GIF file path')
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')
args = parser.parse_args()
tape_content = create_tape_file(
commands=args.command,
output_gif=args.output,
title=args.title,
theme=args.theme,
font_size=args.font_size,
width=args.width,
height=args.height,
bootstrap=args.bootstrap,
filter_pattern=args.filter,
)
output_path = Path(args.output)
tape_file = output_path.with_suffix('.tape')
tape_file.write_text(tape_content)
print(f"✓ Generated tape file: {tape_file}")
if not args.no_execute:
try:
subprocess.run(['vhs', '--version'], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
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
print(f"Recording: {args.output}")
try:
subprocess.run(['vhs', str(tape_file)], check=True)
except subprocess.CalledProcessError as e:
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
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
Batch generate multiple CLI demos from a configuration file.
Supports YAML and JSON formats for defining multiple demos.
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, List
try:
import yaml
YAML_AVAILABLE = True
except ImportError:
YAML_AVAILABLE = False
def load_config(config_file: Path) -> Dict:
"""Load demo configuration from YAML or JSON file."""
suffix = config_file.suffix.lower()
with open(config_file) as f:
if suffix in ['.yaml', '.yml']:
if not YAML_AVAILABLE:
print("Error: PyYAML not installed. Install with: pip install pyyaml", file=sys.stderr)
sys.exit(1)
return yaml.safe_load(f)
elif suffix == '.json':
return json.load(f)
else:
print(f"Error: Unsupported config format: {suffix}", file=sys.stderr)
print("Supported formats: .yaml, .yml, .json", file=sys.stderr)
sys.exit(1)
def generate_demo(demo_config: Dict, base_path: Path, script_path: Path) -> bool:
"""Generate a single demo from configuration."""
name = demo_config.get('name', 'unnamed')
output = demo_config.get('output')
commands = demo_config.get('commands', [])
if not output or not commands:
print(f"✗ Skipping '{name}': missing output or commands", file=sys.stderr)
return False
# Build command
cmd = [sys.executable, str(script_path)]
for command in commands:
cmd.extend(['-c', command])
cmd.extend(['-o', str(base_path / output)])
# Optional parameters
if 'title' in demo_config:
cmd.extend(['--title', demo_config['title']])
if 'theme' in demo_config:
cmd.extend(['--theme', demo_config['theme']])
if 'width' in demo_config:
cmd.extend(['--width', str(demo_config['width'])])
if 'height' in demo_config:
cmd.extend(['--height', str(demo_config['height'])])
print(f"\n{'='*60}")
print(f"Generating: {name}")
print(f"Output: {output}")
print(f"Commands: {len(commands)}")
print(f"{'='*60}")
try:
subprocess.run(cmd, check=True)
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to generate '{name}': {e}", file=sys.stderr)
return False
def main():
parser = argparse.ArgumentParser(
description='Batch generate CLI demos from configuration file',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Configuration file format (YAML):
demos:
- name: "Install Demo"
output: "install.gif"
title: "Installation"
theme: "Dracula"
commands:
- "npm install my-package"
- "npm run build"
- name: "Usage Demo"
output: "usage.gif"
commands:
- "my-package --help"
- "my-package run"
Configuration file format (JSON):
{
"demos": [
{
"name": "Install Demo",
"output": "install.gif",
"commands": ["npm install"]
}
]
}
'''
)
parser.add_argument('config', type=Path,
help='Configuration file (.yaml, .yml, or .json)')
parser.add_argument('--output-dir', type=Path, default=Path.cwd(),
help='Output directory for generated demos')
args = parser.parse_args()
if not args.config.exists():
print(f"Error: Config file not found: {args.config}", file=sys.stderr)
return 1
# Load configuration
config = load_config(args.config)
demos = config.get('demos', [])
if not demos:
print("Error: No demos defined in configuration", file=sys.stderr)
return 1
# Create output directory
args.output_dir.mkdir(parents=True, exist_ok=True)
# Find auto_generate_demo.py script
script_path = Path(__file__).parent / 'auto_generate_demo.py'
if not script_path.exists():
print(f"Error: auto_generate_demo.py not found at {script_path}", file=sys.stderr)
return 1
# Generate demos
total = len(demos)
successful = 0
failed = 0
print(f"\n{'='*60}")
print(f"Starting batch generation: {total} demos")
print(f"Output directory: {args.output_dir}")
print(f"{'='*60}\n")
for i, demo in enumerate(demos, 1):
print(f"\n[{i}/{total}] Processing: {demo.get('name', 'unnamed')}")
if generate_demo(demo, args.output_dir, script_path):
successful += 1
else:
failed += 1
# Summary
print(f"\n{'='*60}")
print(f"Batch generation complete!")
print(f"{'='*60}")
print(f"✓ Successful: {successful}")
if failed > 0:
print(f"✗ Failed: {failed}")
print(f"Total: {total}")
print(f"{'='*60}\n")
return 0 if failed == 0 else 1
if __name__ == '__main__':
sys.exit(main())
#!/bin/bash
#
# Record interactive CLI demos using asciinema and convert to GIF
#
# Usage:
# record_interactive.sh output.gif
# record_interactive.sh output.gif --theme Dracula --width 1200
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
OUTPUT=""
THEME="Dracula"
WIDTH=1400
HEIGHT=700
FONT_SIZE=16
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--theme)
THEME="$2"
shift 2
;;
--width)
WIDTH="$2"
shift 2
;;
--height)
HEIGHT="$2"
shift 2
;;
--font-size)
FONT_SIZE="$2"
shift 2
;;
*)
OUTPUT="$1"
shift
;;
esac
done
if [ -z "$OUTPUT" ]; then
echo -e "${RED}Error: Output file required${NC}" >&2
echo "Usage: $0 output.gif [--theme Theme] [--width 1200] [--height 700]"
exit 1
fi
# Check dependencies
if ! command -v asciinema &> /dev/null; then
echo -e "${RED}Error: asciinema not installed${NC}" >&2
echo "Install it with:"
echo " macOS: brew install asciinema"
echo " Linux: sudo apt install asciinema"
exit 1
fi
if ! command -v vhs &> /dev/null; then
echo -e "${RED}Error: VHS not installed${NC}" >&2
echo "Install it with: brew install vhs"
exit 1
fi
# Generate temp files
CAST_FILE="${OUTPUT%.gif}.cast"
TAPE_FILE="${OUTPUT%.gif}.tape"
echo -e "${GREEN}===========================================================${NC}"
echo -e "${GREEN}Interactive Demo Recording${NC}"
echo -e "${GREEN}===========================================================${NC}"
echo ""
echo -e "${YELLOW}Instructions:${NC}"
echo "1. Type your commands naturally"
echo "2. Press ENTER after each command"
echo "3. Press Ctrl+D when finished"
echo ""
echo -e "${YELLOW}Output:${NC} $OUTPUT"
echo -e "${YELLOW}Theme:${NC} $THEME"
echo -e "${YELLOW}Size:${NC} ${WIDTH}x${HEIGHT}"
echo ""
echo -e "${GREEN}Starting recording in 3 seconds...${NC}"
sleep 3
echo ""
# Record with asciinema
asciinema rec "$CAST_FILE"
echo ""
echo -e "${GREEN}✓ Recording saved to: $CAST_FILE${NC}"
echo ""
echo -e "${YELLOW}Converting to GIF...${NC}"
# Convert asciinema cast to VHS tape format
cat > "$TAPE_FILE" << EOF
Output $OUTPUT
Set FontSize $FONT_SIZE
Set Width $WIDTH
Set Height $HEIGHT
Set Theme "$THEME"
Set Padding 20
Play $CAST_FILE
EOF
echo -e "${GREEN}✓ Generated tape file: $TAPE_FILE${NC}"
# Generate GIF with VHS
vhs < "$TAPE_FILE"
if [ -f "$OUTPUT" ]; then
FILE_SIZE=$(du -h "$OUTPUT" | cut -f1)
echo ""
echo -e "${GREEN}===========================================================${NC}"
echo -e "${GREEN}✓ Demo generated successfully!${NC}"
echo -e "${GREEN}===========================================================${NC}"
echo -e "${YELLOW}Output:${NC} $OUTPUT"
echo -e "${YELLOW}Size:${NC} $FILE_SIZE"
echo ""
echo "Generated files:"
echo " - $CAST_FILE (asciinema recording)"
echo " - $TAPE_FILE (VHS tape file)"
echo " - $OUTPUT (GIF demo)"
echo ""
else
echo -e "${RED}✗ Failed to generate GIF${NC}" >&2
exit 1
fi
Related skills
How it compares
Choose cli-demo-generator for scripted terminal GIFs; use screen-capture tools when the demo is GUI-only without shell commands.
FAQ
What tool does cli-demo-generator use to record terminal demos?
cli-demo-generator uses VHS terminal recordings to produce animated GIF demos. The skill handles tape file creation, noise filtering, speed post-processing, and frame-level verification.
Can cli-demo-generator hide setup commands in a demo?
cli-demo-generator supports self-bootstrapping demos with hidden setup steps so published GIFs show only the user-facing CLI flow. Four approaches cover automated and manual tape workflows.
When should developers invoke cli-demo-generator?
cli-demo-generator fits README or docs that need visual proof of shell workflows. Trigger phrases include "record terminal", "VHS tape", "demo GIF", or "animate my CLI".