Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:18:13 +08:00
commit b8a1bfd4a1
12 changed files with 1582 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Auto-generate CLI demos from command descriptions.
This script creates VHS tape files and generates GIF demos automatically.
"""
import argparse
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,
) -> str:
"""Generate a VHS tape file from commands."""
tape_lines = [
f'Output {output_gif}',
'',
f'Set FontSize {font_size}',
f'Set Width {width}',
f'Set Height {height}',
f'Set Theme "{theme}"',
f'Set Padding {padding}',
'',
]
# Add title if provided
if title:
tape_lines.extend([
f'Type "# {title}" Sleep 500ms Enter',
'Sleep 1s',
'',
])
# Add commands with smart timing
for i, cmd in enumerate(commands, 1):
# Type the command
tape_lines.append(f'Type "{cmd}" Sleep 500ms')
tape_lines.append('Enter')
# Smart sleep based on command complexity
if any(keyword in cmd.lower() for keyword in ['install', 'build', 'test', 'deploy']):
sleep_time = '3s'
elif any(keyword in cmd.lower() for keyword in ['ls', 'pwd', 'echo', 'cat']):
sleep_time = '1s'
else:
sleep_time = '2s'
tape_lines.append(f'Sleep {sleep_time}')
# Add spacing between commands
if i < len(commands):
tape_lines.append('')
return '\n'.join(tape_lines)
def main():
parser = argparse.ArgumentParser(
description='Auto-generate CLI demos from commands',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
# Generate demo from single command
%(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
# 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"
'''
)
parser.add_argument('-c', '--command', action='append', required=True,
help='Command to include in demo (can be specified multiple times)')
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('--no-execute', action='store_true',
help='Generate tape file only, do not execute VHS')
args = parser.parse_args()
# Generate tape file content
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,
)
# 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)
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)
return 1
# Execute VHS
print(f"Generating GIF: {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)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,175 @@
#!/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())

View File

@@ -0,0 +1,136 @@
#!/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