diff --git a/cli-demo-generator/SKILL.md b/cli-demo-generator/SKILL.md index 929085c..21ba1af 100644 --- a/cli-demo-generator/SKILL.md +++ b/cli-demo-generator/SKILL.md @@ -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 -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) diff --git a/cli-demo-generator/assets/templates/self-bootstrap.tape b/cli-demo-generator/assets/templates/self-bootstrap.tape new file mode 100644 index 0000000..993f324 --- /dev/null +++ b/cli-demo-generator/assets/templates/self-bootstrap.tape @@ -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 diff --git a/cli-demo-generator/references/advanced_patterns.md b/cli-demo-generator/references/advanced_patterns.md new file mode 100644 index 0000000..c4d7383 --- /dev/null +++ b/cli-demo-generator/references/advanced_patterns.md @@ -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 -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)" +``` diff --git a/cli-demo-generator/scripts/auto_generate_demo.py b/cli-demo-generator/scripts/auto_generate_demo.py index 455fff9..7e0da3d 100755 --- a/cli-demo-generator/scripts/auto_generate_demo.py +++ b/cli-demo-generator/scripts/auto_generate_demo.py @@ -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