showrun-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
showrun/shell.py ADDED
@@ -0,0 +1,238 @@
1
+ """A persistent shell session driven through a pty.
2
+
3
+ All steps run in the same shell process, so `cd`, `export`, functions and
4
+ aliases carry over between steps. Completion of each command is detected by
5
+ a random sentinel printed together with the command's exit status.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import re
12
+ import secrets
13
+ import shlex
14
+ import shutil
15
+ import tempfile
16
+ import time
17
+ from dataclasses import dataclass
18
+ from typing import Callable, Dict, Optional
19
+
20
+ import pexpect
21
+
22
+
23
+ @dataclass
24
+ class Result:
25
+ exit_code: int
26
+ output: str
27
+ duration: float
28
+ timed_out: bool = False
29
+ interrupted: bool = False
30
+
31
+
32
+ class ShellError(Exception):
33
+ pass
34
+
35
+
36
+ def _kill_tree(pid: int) -> None:
37
+ """Terminate *pid* and its descendants (background groups run in a subshell)."""
38
+ import subprocess
39
+
40
+ try:
41
+ kids = subprocess.run(["pgrep", "-P", str(pid)], capture_output=True, text=True).stdout.split()
42
+ except FileNotFoundError:
43
+ kids = []
44
+ for kid in kids:
45
+ _kill_tree(int(kid))
46
+ try:
47
+ os.kill(pid, 15)
48
+ except (ProcessLookupError, PermissionError):
49
+ pass
50
+
51
+
52
+ class Shell:
53
+ def __init__(
54
+ self,
55
+ shell: str = "bash",
56
+ cwd: Optional[str] = None,
57
+ env: Optional[Dict[str, str]] = None,
58
+ size: Optional[tuple] = None,
59
+ ):
60
+ self.token = secrets.token_hex(6)
61
+ self._marker_prefix = "__SR_" + self.token
62
+ full_env = dict(os.environ)
63
+ full_env.update(env or {})
64
+ full_env.setdefault("TERM", "xterm-256color")
65
+ # A pager (git log, man, systemctl) would wait for keys the audience never sends.
66
+ # The demo's own settings.env can still choose a pager.
67
+ for var in ("PAGER", "GIT_PAGER", "SYSTEMD_PAGER"):
68
+ if var not in (env or {}):
69
+ full_env[var] = "cat"
70
+ # Keep users' rc files out of the way; demos should be reproducible.
71
+ full_env["PS1"] = ""
72
+ full_env["PS2"] = ""
73
+ full_env.pop("PROMPT_COMMAND", None)
74
+
75
+ args = ["--norc", "--noprofile"] if os.path.basename(shell) == "bash" else []
76
+ if os.path.basename(shell) == "zsh":
77
+ args = ["-f"]
78
+ cols, rows = size or shutil.get_terminal_size((100, 30))
79
+ try:
80
+ self.child = pexpect.spawn(
81
+ shell, args, cwd=cwd, env=full_env, encoding="utf-8",
82
+ codec_errors="replace", dimensions=(rows, cols), echo=False,
83
+ )
84
+ except pexpect.ExceptionPexpect as e:
85
+ raise ShellError(f"could not start shell '{shell}': {e}") from e
86
+
87
+ self._raw("stty -echo 2>/dev/null; PS1=''; PS2=''; unset PROMPT_COMMAND; set +m 2>/dev/null; bind 'set enable-bracketed-paste off' 2>/dev/null")
88
+ self.run("true") # sync: swallow any startup noise
89
+ self.background: list = [] # (pid, logfile, command)
90
+
91
+ # -- low level -----------------------------------------------------
92
+ def _raw(self, line: str) -> None:
93
+ self.child.sendline(line)
94
+
95
+ def resize(self, cols: int, rows: int) -> None:
96
+ self.child.setwinsize(rows, cols)
97
+
98
+ @property
99
+ def alive(self) -> bool:
100
+ return self.child.isalive()
101
+
102
+ # -- running -------------------------------------------------------
103
+ def run(
104
+ self,
105
+ command: str,
106
+ on_output: Optional[Callable[[str], None]] = None,
107
+ timeout: Optional[float] = None,
108
+ ) -> Result:
109
+ """Run *command* to completion, streaming output to *on_output*."""
110
+ if not self.alive:
111
+ raise ShellError("shell has exited")
112
+ # The status line is sent on its own line so multi-line commands work.
113
+ # Each run gets its own marker id, so a stale marker can never end a later run.
114
+ self._seq = getattr(self, "_seq", 0) + 1
115
+ seq = self._seq
116
+ marker_re = re.compile(r"\r?\n?__SR_" + self.token + "_" + str(seq) + r"_(\d+)__\r?\n")
117
+ marker_cmd = f"printf '\\n__SR_{self.token}_{seq}_%d__\\n' \"$?\"\n"
118
+ self.child.send(command.rstrip("\n") + "\n")
119
+ self.child.send(marker_cmd)
120
+
121
+ def interrupt() -> None:
122
+ # Ctrl-C makes the tty discard typed-ahead input, including our
123
+ # marker line, so send it again afterwards.
124
+ self.child.sendintr()
125
+ time.sleep(0.1)
126
+ self.child.send(marker_cmd)
127
+
128
+ start = time.monotonic()
129
+ buf = ""
130
+ emitted = []
131
+ hold = len(self._marker_prefix) + 16 # >= full marker length
132
+ timed_out = interrupted = False
133
+
134
+ def emit(text: str) -> None:
135
+ if text:
136
+ emitted.append(text)
137
+ if on_output:
138
+ on_output(text)
139
+
140
+ while True:
141
+ try:
142
+ chunk = self.child.read_nonblocking(4096, timeout=0.05)
143
+ buf += chunk
144
+ except pexpect.TIMEOUT:
145
+ pass
146
+ except pexpect.EOF:
147
+ emit(buf)
148
+ raise ShellError("shell exited while running a command (did the step run `exit`?)")
149
+ except KeyboardInterrupt:
150
+ interrupt()
151
+ interrupted = True
152
+ continue
153
+
154
+ m = marker_re.search(buf)
155
+ if m:
156
+ emit(buf[: m.start()])
157
+ rest = buf[m.end():]
158
+ if rest.strip():
159
+ emit(rest)
160
+ code = int(m.group(1))
161
+ break
162
+ # Emit everything except a tail that might hold a partial marker.
163
+ safe = len(buf) - hold
164
+ if safe > 0:
165
+ emit(buf[:safe])
166
+ buf = buf[safe:]
167
+
168
+ if timeout and not timed_out and time.monotonic() - start > timeout:
169
+ timed_out = True
170
+ interrupt()
171
+
172
+ return Result(code, "".join(emitted), time.monotonic() - start, timed_out, interrupted)
173
+
174
+ def start_background(self, command: str) -> tuple:
175
+ """Start *command* in the background inside the session; returns (pid, logfile)."""
176
+ log = os.path.join(tempfile.gettempdir(), f"showrun-{self.token}-bg{len(self.background)}.log")
177
+ # A subshell keeps bash from printing job-control noise like "[1] 1234".
178
+ out = self.capture(f"( {{ {command.rstrip().rstrip(';')}\n}} > {shlex.quote(log)} 2>&1 & echo $! )")
179
+ try:
180
+ pid = int(out.strip().splitlines()[-1])
181
+ except (ValueError, IndexError):
182
+ raise ShellError(f"could not start background command: {out}")
183
+ self.background.append((pid, log, command))
184
+ return pid, log
185
+
186
+ def stop_background(self) -> None:
187
+ for pid, _, _ in self.background:
188
+ _kill_tree(pid)
189
+ self.background.clear()
190
+
191
+ def wait_for(self, command: str, timeout: float = 30, interval: float = 0.5) -> bool:
192
+ """Poll *command* (silently) until it exits 0, or give up after *timeout* seconds."""
193
+ deadline = time.monotonic() + timeout
194
+ while True:
195
+ if self.run(f"{{ {command}\n}} >/dev/null 2>&1", timeout=max(1.0, min(10, timeout))).exit_code == 0:
196
+ return True
197
+ if time.monotonic() > deadline:
198
+ return False
199
+ time.sleep(interval)
200
+
201
+ def capture(self, command: str, timeout: float = 10) -> str:
202
+ out = self.run(command, timeout=timeout).output
203
+ return re.sub(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\r", "", out).strip()
204
+
205
+ def cwd(self) -> str:
206
+ path = self.capture("pwd")
207
+ home = os.path.expanduser("~")
208
+ if home != "/" and (path == home or path.startswith(home + "/")):
209
+ path = "~" + path[len(home):]
210
+ return path
211
+
212
+ # -- live interaction -----------------------------------------------
213
+ def interact(self, prompt: str = "(live) \\w $ ", escape: str = "\x1d") -> None:
214
+ """Hand the session to the user until they press the escape key (Ctrl-])."""
215
+ self.run(f"stty echo 2>/dev/null; PS1='{prompt}'")
216
+ try:
217
+ self.child.interact(escape_character=escape)
218
+ finally:
219
+ self.child.sendcontrol("c") # discard any half-typed line
220
+ time.sleep(0.05)
221
+ self._raw("stty -echo 2>/dev/null; PS1=''")
222
+ self.run("true")
223
+
224
+ def close(self) -> None:
225
+ self.stop_background()
226
+ if self.alive:
227
+ try:
228
+ self._raw("exit")
229
+ self.child.expect(pexpect.EOF, timeout=2)
230
+ except Exception:
231
+ pass
232
+ self.child.close(force=True)
233
+
234
+ def __enter__(self) -> "Shell":
235
+ return self
236
+
237
+ def __exit__(self, *exc) -> None:
238
+ self.close()
showrun/starter.yaml ADDED
@@ -0,0 +1,65 @@
1
+ # showrun demo — press Enter to advance, ? for all keys.
2
+ title: showrun tour
3
+
4
+ settings:
5
+ prompt: "[bold green]anand@demo[/] [bold blue]{cwd}[/] [bold]$[/] "
6
+ typing_speed: 0.035 # seconds per character
7
+ jitter: 0.5 # human-ish timing
8
+ typo_rate: 0.015 # the occasional fat-fingered key, corrected live
9
+
10
+ vars:
11
+ port: 8765 # override: showrun play demo.yaml -v port=9000
12
+ workdir: /tmp/showrun-tour
13
+
14
+ setup: # runs silently before the first step
15
+ - rm -rf {{workdir}} && mkdir -p {{workdir}}
16
+
17
+ cleanup: # always runs, even if you quit with q
18
+ - rm -rf {{workdir}}
19
+
20
+ steps:
21
+ - title: Getting set up
22
+ say: Every step shares one shell, so cd and export stick.
23
+ run: cd {{workdir}}
24
+ notes: Mention that this is a real shell, not a recording.
25
+
26
+ - run: export GREETING="hello from showrun" # shorthand also works: - echo hi
27
+ hidden: true
28
+
29
+ - run: echo "$GREETING"
30
+ expect_output: hello
31
+
32
+ - run: |
33
+ cat > app.txt <<'EOF'
34
+ status: ok
35
+ version: 1.4.2
36
+ warning: cache is cold
37
+ EOF
38
+ type: false # appears instantly, no typing
39
+
40
+ - run: cat app.txt
41
+ highlight: ["warning.*"] # draw the audience's eye
42
+ notes: Point at the highlighted warning line.
43
+
44
+ - title: Services
45
+ run: python3 -m http.server {{port}} --bind 127.0.0.1
46
+ background: true
47
+ wait_for: curl -sf http://127.0.0.1:{{port}}/ -o /dev/null
48
+
49
+ - run: curl -s http://127.0.0.1:{{port}}/ | grep -m1 -o 'app.txt'
50
+ expect_output: app.txt
51
+
52
+ - title: Safety nets
53
+ say: Slow or flaky? Show canned output instead of running it.
54
+ run: kubectl rollout status deploy/web
55
+ execute: false
56
+ output: |
57
+ Waiting for deployment "web" rollout to finish: 2 of 3 updated replicas are available...
58
+ deployment "web" successfully rolled out
59
+ highlight: successfully
60
+
61
+ - run: ls /does-not-exist
62
+ expect_exit: any # failures you meant to show
63
+
64
+ - say: That's the tour. Try `showrun record my-demo.yaml` to capture your own.
65
+ pause: true
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.5
2
+ Name: showrun-cli
3
+ Version: 0.1.0
4
+ Summary: Scripted, keypress-driven terminal demos: record once, present with Enter, replay offline.
5
+ Project-URL: Homepage, https://showrun.clues.apps.clue2.app
6
+ Project-URL: Repository, https://github.com/honnuanand/showrun
7
+ Project-URL: Issues, https://github.com/honnuanand/showrun/issues
8
+ Project-URL: Documentation, https://github.com/honnuanand/showrun/tree/main/docs
9
+ Author-email: Anand Rao <anand@clue2solve.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: asciinema,cli,demo,demo-magic,live-coding,mcp,presentation,terminal
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Software Development :: Documentation
21
+ Classifier: Topic :: Terminals
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: pexpect>=4.8
24
+ Requires-Dist: pydantic>=2
25
+ Requires-Dist: pyyaml>=6
26
+ Requires-Dist: rich>=13
27
+ Requires-Dist: typer>=0.9
28
+ Provides-Extra: dev
29
+ Requires-Dist: build; extra == 'dev'
30
+ Requires-Dist: pytest>=7; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Provides-Extra: mcp
33
+ Requires-Dist: mcp>=1.2; (python_version >= '3.10') and extra == 'mcp'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # showrun
37
+
38
+ [![CI](https://github.com/honnuanand/showrun/actions/workflows/ci.yml/badge.svg)](https://github.com/honnuanand/showrun/actions/workflows/ci.yml)
39
+ [![Publish](https://github.com/honnuanand/showrun/actions/workflows/publish.yml/badge.svg)](https://github.com/honnuanand/showrun/actions/workflows/publish.yml)
40
+ [![PyPI](https://img.shields.io/pypi/v/showrun-cli)](https://pypi.org/p/showrun-cli)
41
+ [![Python](https://img.shields.io/pypi/pyversions/showrun-cli)](https://pypi.org/p/showrun-cli)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/honnuanand/showrun/blob/main/LICENSE)
43
+
44
+ Scripted, keypress-driven terminal demos. You write (or **record**) the demo once, then
45
+ present it by just hitting **Enter**: commands are typed out like a human would, run in a
46
+ real shell, and stream live output. Hidden setup, canned output, presenter notes, rehearsal,
47
+ offline playback and recordings are built in.
48
+
49
+ It's made for anyone who shows a command line to other people: conference talks, team demos,
50
+ and especially **how-tos for CLI tools**. Write the steps once, rehearse them in CI, present
51
+ them live, and publish the same file as a replay and a Markdown runbook.
52
+
53
+ **Website and live replays: [showrun.clues.apps.clue2.app](https://showrun.clues.apps.clue2.app)**
54
+
55
+ ![showrun presenting a short git how-to](https://raw.githubusercontent.com/honnuanand/showrun/main/docs/media/git-undo.gif)
56
+
57
+ ```bash
58
+ pip install showrun-cli # the command is `showrun`
59
+ showrun init demo.yaml # starter demo that shows off every feature
60
+ showrun check demo.yaml # rehearse unattended
61
+ showrun play demo.yaml # present it
62
+ ```
63
+
64
+ ## Sample replays
65
+
66
+ Each of these is a YAML file in this repo, rendered with `showrun render`. The `.cast` files
67
+ play in [asciinema-player](https://github.com/asciinema/asciinema-player) or with
68
+ `showrun replay docs/casts/<name>.cast`.
69
+
70
+ | Demo | Replay |
71
+ |---|---|
72
+ | **How-to: undo your last git commit** ([`docs/demos/git-undo.yaml`](https://github.com/honnuanand/showrun/blob/main/docs/demos/git-undo.yaml)): narration, highlights, a real git repo set up silently | [GIF](https://raw.githubusercontent.com/honnuanand/showrun/main/docs/media/git-undo.gif) · [cast](https://github.com/honnuanand/showrun/blob/main/docs/casts/git-undo.cast) |
73
+ | **The feature tour** ([`examples/tour.yaml`](https://github.com/honnuanand/showrun/blob/main/examples/tour.yaml)): shared shell state, hidden steps, a background server with `wait_for`, canned output | ![tour](https://raw.githubusercontent.com/honnuanand/showrun/main/docs/media/tour.gif) |
74
+ | **showrun rehearsing itself** ([`docs/demos/showrun-cli.yaml`](https://github.com/honnuanand/showrun/blob/main/docs/demos/showrun-cli.yaml)): `validate`, `check` and `export` | ![showrun cli](https://raw.githubusercontent.com/honnuanand/showrun/main/docs/media/showrun-cli.gif) |
75
+ | **Offline playback** ([`examples/recorded.yaml`](https://github.com/honnuanand/showrun/blob/main/examples/recorded.yaml) with `--offline`): recorded output, nothing executed | ![offline](https://raw.githubusercontent.com/honnuanand/showrun/main/docs/media/offline.gif) |
76
+
77
+ The full guide, with every option and workflow, is in [docs/](https://github.com/honnuanand/showrun/blob/main/docs/README.md).
78
+
79
+ ## Commands
80
+
81
+ | Command | What it does |
82
+ |---|---|
83
+ | `showrun play demo.yaml` | Present. `--offline` replays recorded output, `--fallback` runs live but swaps in the recording if a step fails, `--auto 3` advances by itself, `--start 7`, `--speed 2`, `--cast talk.cast`, `-v port=9000` |
84
+ | `showrun record demo.yaml` | Do the demo once in a shell; every command and its output becomes a step |
85
+ | `showrun check demo.yaml` | Rehearsal: runs every step, checks exit codes and expected output, flags slow steps. Exit code 1 on failure, so it works in CI |
86
+ | `showrun notes demo.yaml` | Presenter view for a second terminal: current step, talking points, what's next, timer |
87
+ | `showrun render demo.yaml -o demo.cast` | Present unattended and save an asciinema cast, no terminal needed (`--offline`, `--delay 1.5`, `--width`, `--height`). Good for docs and CI |
88
+ | `showrun replay talk.cast` | Play back a recording (`--speed 2 --max-idle 1`). Casts also work with asciinema / asciinema-player |
89
+ | `showrun export demo.yaml -f md` | Markdown runbook (`-f sh` for a plain bash script) |
90
+ | `showrun validate demo.yaml` | Parse and list steps |
91
+ | `showrun mcp` | MCP server for AI agents (see below) |
92
+
93
+ ## Keys while presenting
94
+
95
+ | Key | Action |
96
+ |---|---|
97
+ | Enter / Space / → | run the typed command, or continue |
98
+ | any key while typing | finish typing instantly |
99
+ | `s` | skip this step |
100
+ | `b` / ← | back one step |
101
+ | `r` | retype the command |
102
+ | `g` + number + Enter | jump to step N |
103
+ | `l` | list all steps |
104
+ | `i` | live shell in the same session (Ctrl-] returns to the demo) |
105
+ | `?` | help |
106
+ | `q` / Ctrl-C | quit (cleanup still runs) |
107
+
108
+ ## Recording
109
+
110
+ ```
111
+ showrun record kafka-demo.yaml
112
+ ● rec ~/work $ #title Producing events
113
+ ● rec ~/work $ #say Let's start the broker first
114
+ ● rec ~/work $ #bg docker compose up kafka
115
+ ● rec ~/work $ #wait nc -z localhost 9092
116
+ ● rec ~/work $ kcat -b localhost:9092 -L
117
+ ● rec ~/work $ #note mention partitions here
118
+ ● rec ~/work $ ./produce.sh 100
119
+ ● rec ~/work $ #done
120
+ ```
121
+
122
+ Directives: `#say`, `#title`, `#note` (attach to the next command), `#hide cmd`, `#bg cmd`,
123
+ `#wait cmd`, `#fake` (turn the last step into canned output), `#pause`, `#undo`, `#list`,
124
+ `#done`. The file is saved after every step, so nothing is lost if something crashes.
125
+
126
+ Each recorded step keeps its output in `recorded_output`, which gives you three ways to
127
+ present:
128
+
129
+ - `showrun play demo.yaml` runs everything live.
130
+ - `showrun play demo.yaml --offline` shows the recordings and runs nothing that has one
131
+ (background services and `wait_for` are skipped). It's for bad wifi or no cluster.
132
+ - `showrun play demo.yaml --fallback` runs live, but if a step fails, times out or misses
133
+ `expect_output`, it says so and shows the recorded run instead.
134
+
135
+ ## Demo file reference
136
+
137
+ ```yaml
138
+ title: My demo
139
+ settings:
140
+ prompt: "[bold green]me@demo[/] [blue]{cwd}[/] $ " # Rich markup; {cwd} is live
141
+ typing_speed: 0.04 # s per char
142
+ jitter: 0.5 # timing randomness
143
+ typo_rate: 0.01 # occasional fake typo, corrected on screen
144
+ wait_before_typing: false
145
+ theme: monokai # syntax highlighting theme
146
+ timeout: 60 # default per-step timeout
147
+ shell: bash # or zsh
148
+ cwd: ./project # relative to the YAML file
149
+ env: {KUBECONFIG: ./kc}
150
+ vars: # {{name}}; override with -v name=x or SHOWRUN_VAR_NAME
151
+ ns: demo
152
+ setup: [kubectl create ns {{ns}}] # silent, before step 1
153
+ cleanup: [kubectl delete ns {{ns}}] # always runs
154
+ steps:
155
+ - kubectl get pods -n {{ns}} # plain string = run it
156
+ - run: export TOKEN=$(./get-token)
157
+ hidden: true # runs silently
158
+ - title: Deploying # section rule
159
+ say: Now we roll out v2 # narration shown as a comment
160
+ notes: Stress zero downtime # presenter view only
161
+ run: kubectl apply -f v2.yaml
162
+ highlight: ["configured", "created"] # regexes highlighted in output
163
+ expect_output: configured # checked by `check`, warned live
164
+ expect_exit: 0 # or "any"
165
+ timeout: 30
166
+ - run: ./server --port 8080
167
+ background: true
168
+ wait_for: curl -sf localhost:8080/health
169
+ wait_timeout: 30
170
+ - run: terraform apply # too slow to run on stage?
171
+ execute: false
172
+ output: "Apply complete! Resources: 3 added"
173
+ - run: cat big.json
174
+ type: false # appears instantly
175
+ speed: 0.01 # per-step typing speed
176
+ typos: false
177
+ clear: true # clear screen first
178
+ - pause: true # wait for Enter
179
+ ```
180
+
181
+ ## How it works
182
+
183
+ - **One persistent shell** (`pexpect` on a pty) runs every step, so `cd`, `export` and
184
+ functions carry over. A unique marker after each command gives the exit code and tells
185
+ showrun when the command has finished, without guessing at prompts.
186
+ - **Rich** draws the prompt, the syntax-highlighted typing, highlights and the notes view.
187
+ - **Recordings** are plain asciinema v2 files with a marker at each step, written by
188
+ showrun itself, so asciinema doesn't need to be installed.
189
+ - **Background jobs** run inside the session, and their process trees are killed at the end.
190
+
191
+ ## Limits
192
+
193
+ Full-screen apps (vim, top, k9s) don't stream well inside a step. Use `i` to drop into the
194
+ live shell for those. Commands that read from stdin need their input piped in.
195
+
196
+ ## Use it from AI agents (MCP)
197
+
198
+ `showrun mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server over
199
+ stdio, so an agent such as Claude Code, Claude Desktop or Cursor can write, rehearse and
200
+ render demos for you.
201
+
202
+ ```bash
203
+ pip install 'showrun-cli[mcp]' # needs Python 3.10+
204
+ claude mcp add showrun -- showrun mcp # Claude Code
205
+ ```
206
+
207
+ There's also a hosted, read-only server at `https://showrun.clues.apps.clue2.app/mcp`. It can explain the format, validate
208
+ and export demos, and never runs commands. Use it when you don't want to install anything:
209
+
210
+ ```bash
211
+ claude mcp add --transport http showrun https://showrun.clues.apps.clue2.app/mcp
212
+ ```
213
+
214
+ For other clients, add the local server to the client's MCP config:
215
+
216
+ ```json
217
+ { "mcpServers": { "showrun": { "command": "showrun", "args": ["mcp"] } } }
218
+ ```
219
+
220
+ | Tool | What it does |
221
+ |---|---|
222
+ | `demo_format`, `starter_demo` | The YAML format reference and a starter file to edit |
223
+ | `validate_demo` | Parse a demo (file or inline YAML) and list its steps |
224
+ | `write_demo` | Validate and save a demo; won't overwrite unless asked |
225
+ | `check_demo` | Rehearse: **runs** every command and reports failures |
226
+ | `export_demo` | Markdown runbook or bash script |
227
+ | `render_demo` | Present unattended into a `.cast` (**runs** commands unless `offline`) |
228
+ | `read_cast` | A `.cast` as plain text with step markers, so the agent can see the result |
229
+
230
+ The server also publishes the one-page guide as the resource `showrun://guide`. Ask your
231
+ agent something like *"write a showrun demo that shows how to use `jq` to filter JSON,
232
+ check it, and render it"*. `check_demo` and `render_demo` run shell commands on your
233
+ machine, so leave your client's tool approval on.
234
+
235
+ ## Credits
236
+
237
+ showrun is inspired by [demo-magic](https://github.com/paxtonhare/demo-magic) by Paxton
238
+ Hare, the little bash script that types your commands for you while you talk. I've used
239
+ demo-magic for my own talks and demos for more than a decade, and showrun is my attempt at
240
+ the same idea with rehearsal, recording, offline playback and presenter notes built in.
241
+ Thank you, Paxton.
242
+
243
+ ## Development
244
+
245
+ ```bash
246
+ pip install -e '.[dev]' && pytest
247
+ ```
@@ -0,0 +1,19 @@
1
+ showrun/__init__.py,sha256=LvoOF17ZQSvNnFVJX4f2V_KEpmmNwuGvyEI_1Y0J9b8,184
2
+ showrun/cast.py,sha256=3ZGKfCq1jtCMvjshnhtJvxYck6pCcC8nOdJMfNzlS8g,2830
3
+ showrun/check.py,sha256=47d1_Schqn_Hx3dA1EUHSA7AHo6vMpxsRh5h_J0WeoU,3814
4
+ showrun/cli.py,sha256=21oc1aNHKMXtTRSsDk-rAnEHPgamKNZIe8E8ceZyQh0,8931
5
+ showrun/export.py,sha256=zBQkbJqROpp1XYAJglqbrHguWvOFBh4rUdGYNxeuYXQ,2006
6
+ showrun/keys.py,sha256=z8SHKg_--BwabGbGaLXGYQgDhK4B6SPmgut1hhZu5NU,4020
7
+ showrun/mcp_server.py,sha256=jof91ajo6XfEfVWgADm57JrdjFu3Ci-7sgh2MYoaX8w,8828
8
+ showrun/model.py,sha256=9IzLAsToGCNtkPi9iUCkXwaTTBXi45vmb5HdOU09dAU,7741
9
+ showrun/notes.py,sha256=cyPQvFKkIJHLtL4V1wWIcpCBx52xWpRCf7tS_V8mRQ0,3607
10
+ showrun/player.py,sha256=rU67IMO9vd0OY7WrELv5rzYcBhpuQGaIGqZNpfUGHS4,17557
11
+ showrun/record.py,sha256=aNNc2ZJaGWY-AMUKuvF9mHroa5v1sMBDhbxlsOROUhM,6770
12
+ showrun/render.py,sha256=ghWNZE0Klge-wxJA5kC6f5_aw0mHCiDbGeQI4WvwQuk,1626
13
+ showrun/shell.py,sha256=2zYK25204Il1r3ar-OrTxn7vroYMJyiRq7Kscf5ZgNw,8613
14
+ showrun/starter.yaml,sha256=JrmTwDyOJ6mh8ChA2Bu0IHclg7sWnoaH9ASlosMuszs,2062
15
+ showrun_cli-0.1.0.dist-info/METADATA,sha256=rpIaz6s3FpPYYHPZa-CVrVnQ-cwdmc_Xa-77olI7DjM,12320
16
+ showrun_cli-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
17
+ showrun_cli-0.1.0.dist-info/entry_points.txt,sha256=mwf9MCaDGAqNnDJmF8hTp5olapHQzoYDEISrKdL0NPc,44
18
+ showrun_cli-0.1.0.dist-info/licenses/LICENSE,sha256=_sMEFpXIlvRW0vJhpbJ5Xpi0LQOp7YTKz_4_LCUpOm0,1066
19
+ showrun_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ showrun = showrun.cli:app
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anand Rao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.