From 62c408762f9cffc8128d716b043892ab14e0ffcd Mon Sep 17 00:00:00 2001 From: Tim Beyer Date: Fri, 20 Mar 2026 20:18:35 +0100 Subject: [PATCH 01/10] chore: add task for demo recording expansion Co-Authored-By: Claude Opus 4.6 (1M context) --- .../TASK.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tasks/2026-03-20_2018_demo-recording-expansion/TASK.md diff --git a/tasks/2026-03-20_2018_demo-recording-expansion/TASK.md b/tasks/2026-03-20_2018_demo-recording-expansion/TASK.md new file mode 100644 index 0000000..257b063 --- /dev/null +++ b/tasks/2026-03-20_2018_demo-recording-expansion/TASK.md @@ -0,0 +1,81 @@ +# Expand Demo Recording: CI, Website, E2E + +## Status: In Progress + +## Scope + +Expand the single-demo recording pipeline into a full framework supporting: +- Multiple per-feature demo scripts with a shared library +- CI pipeline for automated GIF generation with PR review +- Live asciinema player integration on the docs-site +- E2E testing layered onto the recording infrastructure + +Does NOT cover: changing the wizard UI itself, adding new CLI commands, +or modifying the Lima VM configuration. + +## Context + +We have a working `scripts/record-demo.sh` that drives `clawctl create` via +tmux + asciinema → agg → GIF in README. The user wants to expand this in +three directions: (1) CI automation so GIFs are never stale, (2) real +asciinema recordings on the docs-site replacing static Terminal mockups, +(3) E2E testing that reuses the recording scripts. + +The recording drives the **real CLI** (not mocked), which constrains CI to +macOS runners with Lima. This is intentional — recordings must be authentic. + +## Plan + +See `/Users/tim/.claude/plans/mutable-crunching-squid.md` for the full plan. + +**Approach**: Five phases, each building on the previous: +1. Recording framework refactor (extract lib.sh, per-demo scripts) +2. Additional demo scripts (list, management, headless) +3. CI pipeline (workflow_dispatch + PR comment) +4. Website asciinema player integration +5. E2E testing (dual-mode scripts) + +**Key decision — React sequencer over `asciinema cat`**: The website needs +to show multiple recordings in sequence. `asciinema cat` concatenates .cast +files but transitions are abrupt and segments can't be updated independently. +A React `DemoSequence` component provides labels, crossfades, and clickable +navigation. + +**Key decision — Manual CI trigger**: Recording creates real VMs on expensive +macOS runners. Auto-triggering on every PR would be wasteful. Manual +dispatch + commit-back-to-branch gives visual review in PR diffs. + +## Steps + +### Phase 1: Recording Framework Refactor +- [ ] Create `scripts/demos/lib.sh` with extracted helpers +- [ ] Create `scripts/demos/record-create.sh` (migrated storyboard) +- [ ] Create `scripts/demos/record-all.sh` orchestrator +- [ ] Update `scripts/record-demo.sh` to delegate +- [ ] Update `docs/demo-recording.md` + +### Phase 2: Additional Demo Scripts +- [ ] Create `scripts/demos/record-list.sh` +- [ ] Create `scripts/demos/record-management.sh` +- [ ] Create `scripts/demos/record-headless.sh` + +### Phase 3: CI Pipeline +- [ ] Create `.github/workflows/demo-recording.yml` + +### Phase 4: Website Player Integration +- [ ] Install `asciinema-player` in docs-site +- [ ] Create `AsciinemaTerminal.tsx` component +- [ ] Create `DemoSequence.tsx` component +- [ ] Add asciinema theme CSS +- [ ] Replace static Terminal sections in App.tsx +- [ ] Set up `docs-site/public/casts/` with gitignore exception +- [ ] Update Pages workflow path triggers + +### Phase 5: E2E Testing +- [ ] Add dual-mode support to `lib.sh` +- [ ] Create `scripts/demos/test-all.sh` +- [ ] Create `.github/workflows/e2e.yml` + +## Notes + +## Outcome From 03447b3763563cb3598f9480ae103c86e4b90bf1 Mon Sep 17 00:00:00 2001 From: Tim Beyer Date: Fri, 20 Mar 2026 20:20:45 +0100 Subject: [PATCH 02/10] feat: refactor demo recording into multi-demo framework Extract shared helpers (wait_for, type_slow, assert_screen, etc.) into scripts/demos/lib.sh. Migrate the create wizard storyboard to record-create.sh and add new demo scripts for list, management, and headless flows. The framework supports dual-mode operation: "record" for asciinema recordings and "test" for fast E2E assertions with TAP output. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + docs/assets/casts/.gitkeep | 0 docs/demo-recording.md | 101 +++++++++++----- scripts/demos/lib.sh | 178 +++++++++++++++++++++++++++++ scripts/demos/record-all.sh | 91 +++++++++++++++ scripts/demos/record-create.sh | 120 +++++++++++++++++++ scripts/demos/record-headless.sh | 45 ++++++++ scripts/demos/record-list.sh | 32 ++++++ scripts/demos/record-management.sh | 63 ++++++++++ scripts/demos/test-all.sh | 18 +++ scripts/record-demo.sh | 170 ++------------------------- 11 files changed, 632 insertions(+), 187 deletions(-) create mode 100644 docs/assets/casts/.gitkeep create mode 100755 scripts/demos/lib.sh create mode 100755 scripts/demos/record-all.sh create mode 100755 scripts/demos/record-create.sh create mode 100755 scripts/demos/record-headless.sh create mode 100755 scripts/demos/record-list.sh create mode 100755 scripts/demos/record-management.sh create mode 100755 scripts/demos/test-all.sh diff --git a/.gitignore b/.gitignore index 2a3dc12..cdea07d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ .env .env.secrets *.cast +!docs-site/public/casts/*.cast diff --git a/docs/assets/casts/.gitkeep b/docs/assets/casts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/demo-recording.md b/docs/demo-recording.md index f6edb8e..70a60e3 100644 --- a/docs/demo-recording.md +++ b/docs/demo-recording.md @@ -1,7 +1,8 @@ # Demo Recording -The README GIF (`docs/assets/demo.gif`) is generated from an automated -terminal recording. Re-record it when the wizard UI changes. +Automated terminal recordings for the README GIF, docs site, and E2E testing. +All recordings drive the **real CLI** — not mocked — so they always reflect +actual behavior. ## Requirements @@ -10,45 +11,89 @@ brew install tmux asciinema agg brew install --cask font-fira-code # for Unicode spinner glyphs ``` -## Record and convert +## Quick start (README GIF) ```bash ./scripts/record-demo.sh agg --font-dir ~/Library/Fonts docs/assets/demo.cast docs/assets/demo.gif ``` -The script launches `clawctl create` inside a tmux session, drives the -wizard with scripted keystrokes, and records with asciinema. It runs the -real wizard — after recording, the VM cleanup runs normally. +This delegates to `scripts/demos/record-create.sh` with 160-column dimensions +and outputs to the legacy `docs/assets/demo.cast` path. -## What it records +## Recording all demos -The storyboard walks through: +```bash +./scripts/demos/record-all.sh # Record all demos +./scripts/demos/record-all.sh create list # Record specific demos +``` + +Output goes to `docs/assets/casts/.cast`. + +## Available demos + +| Script | Shows | Requires | +| ------------------------------- | -------------------------------- | --------------------- | +| `scripts/demos/record-create.sh` | Interactive create wizard | Nothing (creates VM) | +| `scripts/demos/record-list.sh` | `clawctl list` output | Running instance | +| `scripts/demos/record-management.sh` | `use`, `status`, `oc doctor` | Running instance | +| `scripts/demos/record-headless.sh` | `clawctl create --config` | Nothing (creates VM) | + +`record-all.sh` runs them in dependency order: create first (produces the VM +that list and management use), then headless last. + +## E2E testing + +The same scripts double as E2E tests. In test mode, recording is skipped and +sleeps are minimized — only the `assert_screen` checks run: + +```bash +./scripts/demos/test-all.sh # Test all demos +./scripts/demos/test-all.sh create # Test specific demo +DEMO_MODE=test ./scripts/demos/record-create.sh # Direct invocation +``` + +Output is TAP-formatted (`ok 1 - Config builder loaded`). + +## Shared library + +All demo scripts source `scripts/demos/lib.sh`, which provides: + +- `setup_session "command"` — create tmux session, start asciinema (or bare command in test mode) +- `teardown_session` — kill session, trim cleanup output, print test summary +- `wait_for "pattern" [timeout]` — wait for text to appear on screen +- `assert_screen "pattern" "description"` — `wait_for` + PASS/FAIL reporting +- `type_slow "text"` — type with natural speed (50ms/char in record, instant in test) +- `down` / `up` / `enter` / `esc` — send keys with appropriate delay +- `send_key ` — send arbitrary tmux key +- `demo_sleep N` — full delay in record mode, 0.1s in test mode -1. Wizard loads with default config -2. Name set to "hal" -3. Provider → Anthropic selected, API key entered -4. Agent Identity → name "Hal", vibe filled in -5. Review screen with validation passing -6. Provisioning starts (runs for ~8 seconds, then recording ends) +### Configuration -## Editing the storyboard +Override these before calling `setup_session`: -The script is at `scripts/record-demo.sh`. Key helpers: +- `SESSION` — tmux session name (default: `clawctl-demo`) +- `COLS` / `ROWS` — terminal dimensions (default: 100×35) +- `CAST` — output .cast file path +- `DEMO_MODE` — `record` (default) or `test` -- `wait_for "text"` — wait for text to appear on screen before proceeding -- `type_slow "text"` — type with natural speed (50ms per char) -- `down` / `enter` / `esc` — send keys with appropriate delay -- `sleep N` — visual pacing for the viewer +## Editing storyboards -If navigation changes (fields reordered, sections added), adjust the -`down` counts. The `wait_for` calls are the safety net — if navigation -is wrong, the script hangs instead of silently desyncing. +Each demo script is its own storyboard. If navigation changes (fields +reordered, sections added), adjust the `down` counts. The `assert_screen` +calls are the safety net — if navigation is wrong, the script fails instead +of silently desyncing. ## Files -| File | Purpose | -| ------------------------ | --------------------------------------------------------------- | -| `scripts/record-demo.sh` | Automated recording script | -| `docs/assets/demo.cast` | Raw asciicast recording (gitignored, regenerated by the script) | -| `docs/assets/demo.gif` | Final GIF embedded in README | +| Path | Purpose | +| ------------------------------- | --------------------------------------------------------- | +| `scripts/demos/lib.sh` | Shared recording/testing helpers | +| `scripts/demos/record-all.sh` | Orchestrator — runs all demos in dependency order | +| `scripts/demos/record-*.sh` | Individual demo storyboards | +| `scripts/demos/test-all.sh` | E2E test runner (runs demos in test mode) | +| `scripts/record-demo.sh` | Legacy wrapper — delegates to `record-create.sh` at 160 cols | +| `docs/assets/casts/*.cast` | Raw recordings (gitignored) | +| `docs/assets/demo.cast` | Legacy README recording (gitignored) | +| `docs/assets/demo.gif` | Final GIF embedded in README | +| `docs-site/public/casts/*.cast`| Curated recordings for the website (checked in) | diff --git a/scripts/demos/lib.sh b/scripts/demos/lib.sh new file mode 100755 index 0000000..1b89580 --- /dev/null +++ b/scripts/demos/lib.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +# +# lib.sh — Shared helpers for demo recording and E2E testing. +# +# Source this file at the top of each demo script: +# DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# source "$DEMO_DIR/lib.sh" +# +# Then call setup_session / teardown_session around your storyboard. + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration (override in demo scripts before calling setup_session) +# --------------------------------------------------------------------------- + +SESSION="${SESSION:-clawctl-demo}" +COLS="${COLS:-100}" +ROWS="${ROWS:-35}" +CAST="${CAST:-}" + +# Mode: "record" (default) or "test" +# In test mode, asciinema recording is skipped and sleeps are minimized. +DEMO_MODE="${DEMO_MODE:-record}" + +# TAP test counter (used in test mode) +_TEST_NUM=0 +_TEST_FAILURES=0 + +# --------------------------------------------------------------------------- +# Session lifecycle +# --------------------------------------------------------------------------- + +setup_session() { + local command="$1" + + tmux kill-session -t "$SESSION" 2>/dev/null || true + tmux new-session -d -s "$SESSION" -x "$COLS" -y "$ROWS" + sleep 0.5 + + if [[ "$DEMO_MODE" == "record" ]]; then + if [[ -z "$CAST" ]]; then + echo "ERROR: CAST path must be set in record mode" >&2 + exit 1 + fi + mkdir -p "$(dirname "$CAST")" + tmux send-keys -t "$SESSION" \ + "NO_ALT_SCREEN=1 asciinema rec --cols $COLS --rows $ROWS --overwrite -c '$command' $CAST" Enter + else + tmux send-keys -t "$SESSION" \ + "NO_ALT_SCREEN=1 $command" Enter + fi +} + +teardown_session() { + tmux kill-session -t "$SESSION" 2>/dev/null || true + sleep 1 + + # Trim cleanup output from recording + if [[ "$DEMO_MODE" == "record" && -n "$CAST" && -f "$CAST" ]]; then + local cleanup_line + cleanup_line=$(grep -n "SIGTERM\|SIGHUP\|cleaning up\|Provisioning failed\|Deleting VM" "$CAST" | head -1 | cut -d: -f1) + if [[ -n "$cleanup_line" ]]; then + local total + total=$(wc -l < "$CAST" | tr -d ' ') + echo "Trimming from line $cleanup_line (of $total) — removing cleanup output" + head -n "$((cleanup_line - 1))" "$CAST" > "${CAST}.tmp" + mv "${CAST}.tmp" "$CAST" + fi + fi + + # Print test summary in test mode + if [[ "$DEMO_MODE" == "test" ]]; then + echo "" + if (( _TEST_FAILURES > 0 )); then + echo "FAILED: $_TEST_FAILURES of $_TEST_NUM assertions failed" + return 1 + else + echo "PASSED: $_TEST_NUM assertions" + fi + fi +} + +# --------------------------------------------------------------------------- +# Input helpers +# --------------------------------------------------------------------------- + +wait_for() { + local pattern="$1" + local max="${2:-60}" + local i=0 + while ! tmux capture-pane -t "$SESSION" -p | grep -qF "$pattern"; do + sleep 0.5 + ((i++)) + if (( i >= max )); then + echo "Timeout waiting for: $pattern" >&2 + if [[ "$DEMO_MODE" == "test" ]]; then + echo " Screen contents:" >&2 + tmux capture-pane -t "$SESSION" -p >&2 + fi + return 1 + fi + done +} + +type_slow() { + local text="$1" + local delay="${2:-0.05}" + if [[ "$DEMO_MODE" == "test" ]]; then + # In test mode, send the whole string at once + tmux send-keys -t "$SESSION" -l "$text" + else + for (( i=0; i<${#text}; i++ )); do + tmux send-keys -t "$SESSION" -l "${text:$i:1}" + sleep "$delay" + done + fi +} + +send_key() { + tmux send-keys -t "$SESSION" "$@" +} + +down() { + send_key Down + demo_sleep 0.3 +} + +up() { + send_key Up + demo_sleep 0.3 +} + +enter() { + send_key Enter + demo_sleep 0.3 +} + +esc() { + send_key Escape + demo_sleep 0.3 +} + +# Sleep that respects demo mode — full delay for recording, minimal for testing +demo_sleep() { + if [[ "$DEMO_MODE" == "record" ]]; then + sleep "$1" + else + sleep 0.1 + fi +} + +# --------------------------------------------------------------------------- +# Assertions (used in both modes, but only report in test mode) +# --------------------------------------------------------------------------- + +assert_screen() { + local pattern="$1" + local desc="${2:-$pattern}" + local timeout="${3:-30}" + + (( _TEST_NUM++ )) + + if wait_for "$pattern" "$timeout"; then + if [[ "$DEMO_MODE" == "test" ]]; then + echo "ok $_TEST_NUM - $desc" + fi + else + (( _TEST_FAILURES++ )) + if [[ "$DEMO_MODE" == "test" ]]; then + echo "not ok $_TEST_NUM - $desc" + else + echo "FAIL: Expected to see: $desc (pattern: $pattern)" >&2 + tmux kill-session -t "$SESSION" 2>/dev/null + exit 1 + fi + fi +} diff --git a/scripts/demos/record-all.sh b/scripts/demos/record-all.sh new file mode 100755 index 0000000..0f20ce9 --- /dev/null +++ b/scripts/demos/record-all.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# +# record-all.sh — Orchestrate recording of all demo scripts. +# +# Usage (from repo root): +# ./scripts/demos/record-all.sh # Record all demos +# ./scripts/demos/record-all.sh create list # Record specific demos +# +# In test mode (E2E assertions only, no recording): +# DEMO_MODE=test ./scripts/demos/record-all.sh +# +# Available demos: create, list, management, headless + +set -euo pipefail + +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$DEMO_DIR/../.." && pwd)" + +cd "$REPO_ROOT" + +# Parse which demos to run +DEMOS=("$@") +if [[ ${#DEMOS[@]} -eq 0 ]]; then + DEMOS=(create list management headless) +fi + +FAILED=() + +run_demo() { + local name="$1" + local script="$DEMO_DIR/record-${name}.sh" + + if [[ ! -f "$script" ]]; then + echo "Unknown demo: $name (no script at $script)" >&2 + FAILED+=("$name") + return + fi + + echo "" + echo "=== Recording: $name ===" + echo "" + + if bash "$script"; then + echo "=== Done: $name ===" + else + echo "=== FAILED: $name ===" >&2 + FAILED+=("$name") + fi +} + +# Run demos in dependency order. +# create goes first — it produces a VM that list and management use. +# headless goes last — it creates a separate instance. +ORDERED_DEMOS=() +for demo in create list management headless; do + for requested in "${DEMOS[@]}"; do + if [[ "$requested" == "$demo" ]]; then + ORDERED_DEMOS+=("$demo") + fi + done +done + +# Also add any unrecognized names (they'll get the "Unknown demo" error) +for requested in "${DEMOS[@]}"; do + found=false + for known in create list management headless; do + if [[ "$requested" == "$known" ]]; then + found=true + break + fi + done + if [[ "$found" == false ]]; then + ORDERED_DEMOS+=("$requested") + fi +done + +for demo in "${ORDERED_DEMOS[@]}"; do + run_demo "$demo" +done + +# Summary +echo "" +echo "=== Summary ===" +echo "Recorded ${#ORDERED_DEMOS[@]} demo(s): ${ORDERED_DEMOS[*]}" + +if [[ ${#FAILED[@]} -gt 0 ]]; then + echo "Failed: ${FAILED[*]}" >&2 + exit 1 +fi + +echo "All demos recorded successfully." diff --git a/scripts/demos/record-create.sh b/scripts/demos/record-create.sh new file mode 100755 index 0000000..fdd8807 --- /dev/null +++ b/scripts/demos/record-create.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# +# record-create.sh — Record the clawctl create wizard demo. +# +# Usage (from repo root): +# ./scripts/demos/record-create.sh +# +# Produces: docs/assets/casts/create.cast +# +# NOTE: This runs `clawctl create` for real. After recording you may +# need to clean up: limactl delete hal + +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DEMO_DIR/lib.sh" + +SESSION="clawctl-create" +COLS="${COLS:-160}" +ROWS="${ROWS:-35}" +CAST="${CAST:-docs/assets/casts/create.cast}" + +# --- Setup --- + +setup_session "clawctl create" + +# --- Scene 1: Wait for config builder --- + +assert_screen "Review & Create" "Config builder loaded" +demo_sleep 2 + +# --- Scene 2: Type instance name "hal" --- + +enter +demo_sleep 0.5 +type_slow "hal" +demo_sleep 1 +enter +demo_sleep 1 + +# --- Scene 3: Navigate to Provider and expand --- + +# name → project → resources → provider +down; down; down +enter +demo_sleep 0.5 + +# Move to provider.type +down + +# Open provider type select +enter +assert_screen "anthropic" "Provider type select shows anthropic" +demo_sleep 1 + +# Select anthropic +enter +demo_sleep 0.5 + +# --- Scene 4: Fill API key --- + +down +enter +demo_sleep 0.3 +type_slow "sk-ant-api03-xYzDeMoKeY" +demo_sleep 0.8 +enter +demo_sleep 0.5 + +# Collapse provider +esc +demo_sleep 0.8 + +# --- Scene 5: Agent Identity --- + +# provider → network → cap:tailscale → cap:one-password → bootstrap +down; down; down; down + +# Expand bootstrap (Agent Identity) +enter +demo_sleep 0.5 + +# Agent Name +down +enter +demo_sleep 0.3 +type_slow "Hal" +demo_sleep 0.5 +enter +demo_sleep 0.3 + +# Agent Vibe +down +enter +demo_sleep 0.3 +type_slow "Calm, precise, just a little too helpful." +demo_sleep 0.8 +enter +demo_sleep 0.5 + +# Collapse bootstrap +esc +demo_sleep 1 + +# --- Scene 6: Review --- + +send_key "r" +assert_screen "Review Configuration" "Review screen shown" +demo_sleep 3 + +# --- Scene 7: Start provisioning --- + +enter +assert_screen "Provisioning" "Provisioning started" +demo_sleep 20 + +# --- Done --- + +teardown_session + +echo "" +echo "Recording saved to $CAST" diff --git a/scripts/demos/record-headless.sh b/scripts/demos/record-headless.sh new file mode 100755 index 0000000..a20767b --- /dev/null +++ b/scripts/demos/record-headless.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# +# record-headless.sh — Record headless (config-driven) provisioning. +# +# Shows: clawctl create --config +# +# NOTE: This runs `clawctl create` for real with a JSON config. +# After recording you may need to clean up the created instance. +# +# Usage (from repo root): +# ./scripts/demos/record-headless.sh + +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DEMO_DIR/lib.sh" + +SESSION="clawctl-headless" +COLS="${COLS:-100}" +ROWS="${ROWS:-30}" +CAST="${CAST:-docs/assets/casts/headless.cast}" + +# Path to the headless config used for the demo. +# This file must exist — create it or point to an existing one. +CONFIG_FILE="${CONFIG_FILE:-docs/assets/demo-config.json}" + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Config file not found: $CONFIG_FILE" >&2 + echo "Create it first, or set CONFIG_FILE=/path/to/config.json" >&2 + exit 1 +fi + +# --- Setup --- + +setup_session "clawctl create --config $CONFIG_FILE" + +# --- Wait for provisioning to start --- + +assert_screen "Provisioning" "Headless provisioning started" +demo_sleep 15 + +# --- Done --- + +teardown_session + +echo "" +echo "Recording saved to $CAST" diff --git a/scripts/demos/record-list.sh b/scripts/demos/record-list.sh new file mode 100755 index 0000000..a01715e --- /dev/null +++ b/scripts/demos/record-list.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# record-list.sh — Record the clawctl list output. +# +# Requires: at least one existing clawctl instance. +# +# Usage (from repo root): +# ./scripts/demos/record-list.sh + +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DEMO_DIR/lib.sh" + +SESSION="clawctl-list" +COLS="${COLS:-100}" +ROWS="${ROWS:-20}" +CAST="${CAST:-docs/assets/casts/list.cast}" + +# --- Setup --- + +setup_session "clawctl list" + +# --- Wait for output --- + +assert_screen "NAME" "List header row shown" +demo_sleep 3 + +# --- Done --- + +teardown_session + +echo "" +echo "Recording saved to $CAST" diff --git a/scripts/demos/record-management.sh b/scripts/demos/record-management.sh new file mode 100755 index 0000000..a023245 --- /dev/null +++ b/scripts/demos/record-management.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# record-management.sh — Record day-to-day management commands. +# +# Shows: clawctl use, clawctl status, clawctl oc doctor +# +# Requires: at least one existing clawctl instance. +# +# Usage (from repo root): +# ./scripts/demos/record-management.sh + +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$DEMO_DIR/lib.sh" + +SESSION="clawctl-mgmt" +COLS="${COLS:-100}" +ROWS="${ROWS:-30}" +CAST="${CAST:-docs/assets/casts/management.cast}" + +# --- Setup --- +# We record a plain shell session and type commands into it. + +setup_session "bash --norc --noprofile" + +# Wait for shell prompt +demo_sleep 1 + +# Set a clean prompt for the recording +send_key -l 'export PS1="$ "' +enter +demo_sleep 0.5 + +# --- Scene 1: Set default instance --- + +type_slow "clawctl use hal" +demo_sleep 0.5 +enter +assert_screen "hal" "Instance set to hal" +demo_sleep 2 + +# --- Scene 2: Check status --- + +type_slow "clawctl status" +demo_sleep 0.5 +enter +assert_screen "Running" "Status shows running" +demo_sleep 3 + +# --- Scene 3: Run doctor --- + +type_slow "clawctl oc doctor" +demo_sleep 0.5 +enter +# Doctor output varies — wait for completion marker +assert_screen "checks passed" "Doctor checks completed" +demo_sleep 3 + +# --- Done --- + +teardown_session + +echo "" +echo "Recording saved to $CAST" diff --git a/scripts/demos/test-all.sh b/scripts/demos/test-all.sh new file mode 100755 index 0000000..f14b044 --- /dev/null +++ b/scripts/demos/test-all.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# test-all.sh — Run all demo scripts in test mode (E2E assertions, no recording). +# +# Usage (from repo root): +# ./scripts/demos/test-all.sh +# ./scripts/demos/test-all.sh create # Test specific demo + +set -euo pipefail + +export DEMO_MODE=test + +DEMO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "TAP version 13" +echo "" + +exec "$DEMO_DIR/record-all.sh" "$@" diff --git a/scripts/record-demo.sh b/scripts/record-demo.sh index c2fcb1f..7fc7c6d 100755 --- a/scripts/record-demo.sh +++ b/scripts/record-demo.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash # -# record-demo.sh — Record the clawctl create wizard demo. +# record-demo.sh — Record the clawctl create wizard demo (README GIF). # -# Uses tmux for reliable keystroke delivery and screen content detection, -# with asciinema for clean terminal recording. Convert with agg afterward. +# This is a convenience wrapper that delegates to scripts/demos/record-create.sh +# with the original 160-column dimensions and demo.cast output path. # # Usage (from repo root): # ./scripts/record-demo.sh -# agg docs/assets/demo.cast docs/assets/demo.gif +# agg --font-dir ~/Library/Fonts docs/assets/demo.cast docs/assets/demo.gif # # Requirements: tmux, asciinema, agg # @@ -16,160 +16,12 @@ set -euo pipefail -CAST="docs/assets/demo.cast" -SESSION="clawctl-demo" -COLS=160 -ROWS=35 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# --- Helpers --- +# Override output to the legacy path for backward compatibility. +# The README references docs/assets/demo.cast → demo.gif. +export CAST="docs/assets/demo.cast" +export COLS=160 +export ROWS=35 -wait_for() { - local pattern="$1" - local max=60 - local i=0 - while ! tmux capture-pane -t "$SESSION" -p | grep -qF "$pattern"; do - sleep 0.5 - ((i++)) - if (( i >= max )); then - echo "Timeout waiting for: $pattern" >&2 - tmux kill-session -t "$SESSION" 2>/dev/null - exit 1 - fi - done -} - -type_slow() { - local text="$1" - for (( i=0; i<${#text}; i++ )); do - tmux send-keys -t "$SESSION" -l "${text:$i:1}" - sleep 0.05 - done -} - -down() { - tmux send-keys -t "$SESSION" Down - sleep 0.3 -} - -# --- Setup --- - -tmux kill-session -t "$SESSION" 2>/dev/null || true -tmux new-session -d -s "$SESSION" -x "$COLS" -y "$ROWS" -sleep 0.5 - -# Start asciinema recording wrapping clawctl create -tmux send-keys -t "$SESSION" \ - "NO_ALT_SCREEN=1 asciinema rec --cols $COLS --rows $ROWS --overwrite -c 'clawctl create' $CAST" Enter - -# --- Scene 1: Wait for config builder --- - -wait_for "Review & Create" -sleep 2 - -# --- Scene 2: Type instance name "hal" --- - -tmux send-keys -t "$SESSION" Enter -sleep 0.5 -type_slow "hal" -sleep 1 -tmux send-keys -t "$SESSION" Enter -sleep 1 - -# --- Scene 3: Navigate to Provider and expand --- - -# name → project → resources → provider -down; down; down -tmux send-keys -t "$SESSION" Enter -sleep 0.5 - -# Move to provider.type -down - -# Open provider type select -tmux send-keys -t "$SESSION" Enter -wait_for "anthropic" -sleep 1 - -# Select anthropic -tmux send-keys -t "$SESSION" Enter -sleep 0.5 - -# --- Scene 4: Fill API key --- - -down -tmux send-keys -t "$SESSION" Enter -sleep 0.3 -type_slow "sk-ant-api03-xYzDeMoKeY" -sleep 0.8 -tmux send-keys -t "$SESSION" Enter -sleep 0.5 - -# Collapse provider -tmux send-keys -t "$SESSION" Escape -sleep 0.8 - -# --- Scene 5: Agent Identity --- - -# provider → network → cap:tailscale → cap:one-password → bootstrap -down; down; down; down - -# Expand bootstrap (Agent Identity) -tmux send-keys -t "$SESSION" Enter -sleep 0.5 - -# Agent Name -down -tmux send-keys -t "$SESSION" Enter -sleep 0.3 -type_slow "Hal" -sleep 0.5 -tmux send-keys -t "$SESSION" Enter -sleep 0.3 - -# Agent Vibe -down -tmux send-keys -t "$SESSION" Enter -sleep 0.3 -type_slow "Calm, precise, just a little too helpful." -sleep 0.8 -tmux send-keys -t "$SESSION" Enter -sleep 0.5 - -# Collapse bootstrap -tmux send-keys -t "$SESSION" Escape -sleep 1 - -# --- Scene 6: Review --- - -tmux send-keys -t "$SESSION" "r" -wait_for "Review Configuration" -sleep 3 - -# --- Scene 7: Start provisioning --- - -tmux send-keys -t "$SESSION" Enter -wait_for "Provisioning" -sleep 20 - -# --- Done --- - -# Kill the session. asciinema writes .cast incrementally, so the file -# is complete up to this point. -tmux kill-session -t "$SESSION" 2>/dev/null || true -sleep 1 - -# Trim everything after cleanup signals appear in the recording. -# Find the first line containing cleanup text and keep everything before it. -if [[ -f "$CAST" ]]; then - cleanup_line=$(grep -n "SIGTERM\|SIGHUP\|cleaning up\|Provisioning failed\|Deleting VM" "$CAST" | head -1 | cut -d: -f1) - if [[ -n "$cleanup_line" ]]; then - total=$(wc -l < "$CAST" | tr -d ' ') - echo "Trimming from line $cleanup_line (of $total) — removing cleanup output" - head -n "$((cleanup_line - 1))" "$CAST" > "${CAST}.tmp" - mv "${CAST}.tmp" "$CAST" - fi -fi - -echo "" -echo "Recording saved to $CAST" -echo "Convert to GIF: agg $CAST docs/assets/demo.gif" +exec "$SCRIPT_DIR/demos/record-create.sh" From 93d1b8f3b42d78ea422002f3ab8f8d8becb953b6 Mon Sep 17 00:00:00 2001 From: Tim Beyer Date: Fri, 20 Mar 2026 20:21:25 +0100 Subject: [PATCH 03/10] feat: add CI workflows for demo recording and E2E testing demo-recording.yml: manually triggered workflow that records demos on a macOS runner, generates the README GIF, and commits recordings back to the PR branch with a comment for visual review. e2e.yml: weekly + manual E2E test workflow that runs demo scripts in test mode (fast assertions, no recording). Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/demo-recording.yml | 137 +++++++++++++++++++++++++++ .github/workflows/e2e.yml | 44 +++++++++ 2 files changed, 181 insertions(+) create mode 100644 .github/workflows/demo-recording.yml create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/demo-recording.yml b/.github/workflows/demo-recording.yml new file mode 100644 index 0000000..6762090 --- /dev/null +++ b/.github/workflows/demo-recording.yml @@ -0,0 +1,137 @@ +name: Record Demos + +on: + workflow_dispatch: + inputs: + pr_number: + description: "PR number to update with recordings (optional)" + required: false + type: string + demos: + description: "Which demos to record (space-separated, or leave blank for all)" + required: false + default: "" + type: string + +concurrency: + group: demo-recording + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + +jobs: + record: + runs-on: macos-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + # If a PR number is provided, check out the PR branch so we can push back + ref: ${{ inputs.pr_number && format('refs/pull/{0}/head', inputs.pr_number) || github.ref }} + fetch-depth: 0 + + - name: Resolve PR branch name + if: inputs.pr_number != '' + id: pr-branch + run: | + BRANCH=$(gh pr view "${{ inputs.pr_number }}" --json headRefName -q '.headRefName') + echo "name=$BRANCH" >> "$GITHUB_OUTPUT" + git checkout "$BRANCH" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + # Install recording dependencies + - name: Install recording tools + run: | + brew install tmux asciinema + brew install agg + + # Install Lima for real VM creation + - name: Install Lima + run: brew install lima + + # Build clawctl so the recording scripts can invoke it + - name: Build clawctl + run: bun run build:release + + # Record demos + - name: Record demos + run: | + ARGS="${{ inputs.demos }}" + if [[ -n "$ARGS" ]]; then + ./scripts/demos/record-all.sh $ARGS + else + ./scripts/demos/record-all.sh + fi + + # Convert the create demo to GIF for the README + - name: Generate README GIF + run: | + if [[ -f docs/assets/casts/create.cast ]]; then + agg --font-dir /Library/Fonts \ + docs/assets/casts/create.cast \ + docs/assets/demo.gif + fi + + # Copy curated casts to the website public dir + - name: Update website casts + run: | + mkdir -p docs-site/public/casts + cp docs/assets/casts/*.cast docs-site/public/casts/ 2>/dev/null || true + + # Upload all artifacts for download + - name: Upload recording artifacts + uses: actions/upload-artifact@v4 + with: + name: demo-recordings + path: | + docs/assets/casts/*.cast + docs/assets/demo.gif + retention-days: 30 + + # Commit updated recordings back to the PR branch + - name: Commit updated recordings + if: inputs.pr_number != '' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add docs/assets/demo.gif docs-site/public/casts/ || true + + if git diff --cached --quiet; then + echo "No recording changes to commit" + else + git commit -m "chore: update demo recordings + + Generated by demo-recording workflow run #${{ github.run_number }}" + git push origin "HEAD:${{ steps.pr-branch.outputs.name }}" + fi + + # Post a comment on the PR linking the artifacts + - name: Comment on PR + if: inputs.pr_number != '' + uses: actions/github-script@v7 + with: + script: | + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = [ + '## Demo Recordings Updated', + '', + 'New recordings have been committed to this branch. Review the GIF diff in the commit above.', + '', + `[Download all recordings](${runUrl}) (workflow artifacts)`, + '', + `_Generated by [demo-recording workflow](${runUrl}) run #${context.runNumber}_`, + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt('${{ inputs.pr_number }}'), + body, + }); diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..3a9c215 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,44 @@ +name: E2E Tests + +on: + workflow_dispatch: + inputs: + demos: + description: "Which demos to test (space-separated, or leave blank for all)" + required: false + default: "" + type: string + schedule: + # Weekly Monday 6:00 UTC — catch regressions from dependency updates + - cron: "0 6 * * 1" + +concurrency: + group: e2e-tests + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: macos-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - name: Install test dependencies + run: brew install tmux lima + + - name: Build clawctl + run: bun run build:release + + - name: Run E2E tests + run: | + ARGS="${{ inputs.demos }}" + if [[ -n "$ARGS" ]]; then + ./scripts/demos/test-all.sh $ARGS + else + ./scripts/demos/test-all.sh + fi From 192b2705a8b71d53eb9f05a377f5e0adc0aa604c Mon Sep 17 00:00:00 2001 From: Tim Beyer Date: Fri, 20 Mar 2026 20:38:57 +0100 Subject: [PATCH 04/10] feat: integrate asciinema player into docs-site Add AsciinemaTerminal and DemoSequence React components that wrap the asciinema-player library in the site's terminal chrome. Sections gracefully fall back to static content when .cast files aren't present. - AsciinemaTerminal: single recording player with autoplay-on-scroll - DemoSequence: multi-recording sequencer with segment indicators - Custom theme CSS matching the site's dark palette - Type declarations for asciinema-player (no shipped types) - FleetDemo, ManagementDemo, and ConfigSection use live recordings - New CreateDemo section between Hero and FleetDemo Co-Authored-By: Claude Opus 4.6 (1M context) --- docs-site/bun.lock | 19 ++ docs-site/package.json | 1 + docs-site/public/casts/.gitkeep | 0 docs-site/src/App.tsx | 210 +++++++++++++----- .../src/components/AsciinemaTerminal.tsx | 121 ++++++++++ docs-site/src/components/DemoSequence.tsx | 84 +++++++ docs-site/src/index.css | 35 +++ docs-site/src/types/asciinema-player.d.ts | 48 ++++ docs-site/tsconfig.app.tsbuildinfo | 2 +- 9 files changed, 465 insertions(+), 55 deletions(-) create mode 100644 docs-site/public/casts/.gitkeep create mode 100644 docs-site/src/components/AsciinemaTerminal.tsx create mode 100644 docs-site/src/components/DemoSequence.tsx create mode 100644 docs-site/src/types/asciinema-player.d.ts diff --git a/docs-site/bun.lock b/docs-site/bun.lock index a90ebc5..e470f42 100644 --- a/docs-site/bun.lock +++ b/docs-site/bun.lock @@ -5,6 +5,7 @@ "": { "name": "docs-site", "dependencies": { + "asciinema-player": "^3.15.1", "react": "^18.3.1", "react-dom": "^18.3.1", }, @@ -52,6 +53,8 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], @@ -172,6 +175,12 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="], + + "@solid-primitives/transition-group": ["@solid-primitives/transition-group@1.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA=="], + + "@solid-primitives/utils": ["@solid-primitives/utils@6.4.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], @@ -220,6 +229,8 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "asciinema-player": ["asciinema-player@3.15.1", "", { "dependencies": { "@babel/runtime": "^7.21.0", "solid-js": "^1.3.0", "solid-transition-group": "^0.2.3" } }, "sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], @@ -312,6 +323,14 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="], + + "seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="], + + "solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], + + "solid-transition-group": ["solid-transition-group@0.2.3", "", { "dependencies": { "@solid-primitives/refs": "^1.0.5", "@solid-primitives/transition-group": "^1.0.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], diff --git a/docs-site/package.json b/docs-site/package.json index e4fd841..42d6587 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "asciinema-player": "^3.15.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/docs-site/public/casts/.gitkeep b/docs-site/public/casts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs-site/src/App.tsx b/docs-site/src/App.tsx index 243c75b..ea03bea 100644 --- a/docs-site/src/App.tsx +++ b/docs-site/src/App.tsx @@ -1,5 +1,10 @@ import { useEffect, useRef, useState, type ReactNode } from "react"; import "./App.css"; +import { AsciinemaTerminal } from "./components/AsciinemaTerminal"; +import { DemoSequence } from "./components/DemoSequence"; + +// Base path for .cast files served from public/casts/ +const CAST_BASE = import.meta.env.BASE_URL + "casts"; // --------------------------------------------------------------------------- // Hooks @@ -147,6 +152,44 @@ function CopyButton({ text }: { text: string }) { // Sections // --------------------------------------------------------------------------- +function CreateDemo() { + const castSrc = `${CAST_BASE}/create.cast`; + const [hasCast, setHasCast] = useState(false); + + useEffect(() => { + fetch(castSrc, { method: "HEAD" }) + .then((r) => setHasCast(r.ok)) + .catch(() => setHasCast(false)); + }, [castSrc]); + + if (!hasCast) return null; + + return ( +
+ +
+

+ See it in action +

+

+ The interactive wizard walks you through every setting. One command, fully configured + gateway. +

+
+
+ + + + +
+ ); +} + function Divider() { return (
@@ -239,6 +282,15 @@ function Hero() { } function FleetDemo() { + const castSrc = `${CAST_BASE}/list.cast`; + const [hasCast, setHasCast] = useState(false); + + useEffect(() => { + fetch(castSrc, { method: "HEAD" }) + .then((r) => setHasCast(r.ok)) + .catch(() => setHasCast(false)); + }, [castSrc]); + return (
@@ -253,32 +305,41 @@ function FleetDemo() { - -
- - clawctl list -
-
-
- {"NAME STATUS PROJECT PROVIDER PORT"} -
-
- {"research-ai "} - Running - {" ~/openclaw-vms/research-ai anthropic 18789"} -
-
- {"code-review "} - Running - {" ~/openclaw-vms/code-review openai 18790"} + {hasCast ? ( + + ) : ( + +
+ + clawctl list
-
- {"data-pipeline "} - Stopped - {" ~/openclaw-vms/data-pipeline anthropic 18791"} +
+
+ {"NAME STATUS PROJECT PROVIDER PORT"} +
+
+ {"research-ai "} + Running + {" ~/openclaw-vms/research-ai anthropic 18789"} +
+
+ {"code-review "} + Running + {" ~/openclaw-vms/code-review openai 18790"} +
+
+ {"data-pipeline "} + Stopped + {" ~/openclaw-vms/data-pipeline anthropic 18791"} +
-
-
+ + )}
); @@ -328,6 +389,15 @@ function Features() { } function ConfigSection() { + const headlessCast = `${CAST_BASE}/headless.cast`; + const [hasCast, setHasCast] = useState(false); + + useEffect(() => { + fetch(headlessCast, { method: "HEAD" }) + .then((r) => setHasCast(r.ok)) + .catch(() => setHasCast(false)); + }, [headlessCast]); + const configJson = `{ "name": "hal", "project": "~/openclaw-vms/hal", @@ -378,12 +448,21 @@ function ConfigSection() {
- -
- - clawctl create --config hal.json -
-
+ {hasCast ? ( + + ) : ( + +
+ + clawctl create --config hal.json +
+
+ )}
@@ -512,6 +591,15 @@ function highlightJsonLine(line: string): ReactNode { } function ManagementDemo() { + const managementCast = `${CAST_BASE}/management.cast`; + const [hasCast, setHasCast] = useState(false); + + useEffect(() => { + fetch(managementCast, { method: "HEAD" }) + .then((r) => setHasCast(r.ok)) + .catch(() => setHasCast(false)); + }, [managementCast]); + return (
@@ -526,30 +614,43 @@ function ManagementDemo() { -
- -
- {[ - ["clawctl use research-ai", "set default instance"], - ["clawctl oc doctor", "health check"], - ["clawctl restart", "fix what's stuck"], - ["clawctl create", "spin up another"], - ["clawctl delete staging", "clean up"], - ].map(([cmd, comment]) => ( -
- - - {cmd} - - # {comment} -
- ))} -
-
-
+ {hasCast ? ( +
+ +
+ ) : ( +
+ +
+ {[ + ["clawctl use research-ai", "set default instance"], + ["clawctl oc doctor", "health check"], + ["clawctl restart", "fix what's stuck"], + ["clawctl create", "spin up another"], + ["clawctl delete staging", "clean up"], + ].map(([cmd, comment]) => ( +
+ + + {cmd} + + # {comment} +
+ ))} +
+
+
+ )}
); @@ -633,6 +734,7 @@ export default function App() {