Was macht demo-video?
bun run demo <feature>/<clip> zeichnet auf einem gemieteten iOS-Simulator auf und rendert dann ein 1920×1080-Video: Titel links, ein Terminal, das jeden echten warden-Befehl eintippt und seine Ausgabe am Ende des Schritts anzeigt, das Telefon rechts (abgerundet, 3px Ring). Kit: scripts/demo/ (demo.ts CLI, record.ts, render.py, timeline.ts, demo.types.ts). Definitionen: apps/example/demos/<feature>/<clip>.demo.ts (name muss gleich <feature>/<clip> sein); Assets: apps/docs/public/demos/<feature>/<clip>.mp4|jpg. Ein Ordner pro Feature (biometrics, network, location, clock), Kleinbuchstaben, Bindestriche. Intern: kein Changeset.
1. Wann erstellt man eines
- Ein Feature, dessen Effekt auf dem Bildschirm sichtbar ist und in Sekunden eintritt (Biometrie, Alarme, Gesten). Nicht für Logs, Performance-Zahlen oder alles, was eine Tabelle besser zeigt.
- Nach einer Änderung der App-UI oder des Verhaltens eines Befehls: Neu aufnehmen, niemals das mp4 bearbeiten.
- Eine Geschichte pro Clip, 10–25 s, 3–6 Schritte mit Bildunterschriften.
Benötigt ffmpeg, ffprobe, uv (brew install ffmpeg uv), Xcode und die Schriftarten in node_modules/@expo-google-fonts (bun install).
2. Definition schreiben
Exportiere demo: DemoDefinition (scripts/demo/demo.types.ts). Kopiere apps/example/demos/biometrics/faceid.demo.ts.
| Feld | Bedeutung |
|---|---|
title / subtitle / terminalTitle / footer | Statischer Text; title ist die Behauptung, subtitle eine Zeile |
phoneCaption | Zeile unter dem Telefon, Standard echter Simulator · keine Animation; "" blendet sie aus |
claim | { biometrics?, profile? } -> warden claim ios --json --label demo-<feature>-<clip> --ttl 30m [--biometrics] |
app | bundleId, kind (Standard simulator), project (repo-relatives cwd), deepLink geöffnet mit --settle, appearance (light/dark, gesetzt über simctl ui auf der gemieteten udid, danach wiederhergestellt) |
setup | warden-argv, die nach dem Öffnen der App und vor dem Deep Link außerhalb des Bildschirms ausgeführt werden; ["ui","network","set","online","--relaunch"] gibt dem iOS-dylib das Launch-Netzwerk / Uhrenschritte, die benötigt werden. Ein Eintrag ist ein argv oder { argv, optional: true }: Ein optionaler Schritt, der mit Nicht-Null-Exitcode beendet wird, wird protokolliert (optionales setup warden … -> <code>) anstatt den Take fehlschlagen zu lassen (eine Berechtigungsabfrage, die möglicherweise nicht erscheint) |
ready | warden ui wait-Selektoren, die nach dem Deep Link und vor dem Start des Videos bestehen müssen (z. B. id=bio-screen); ein Schritt drückt niemals auf einen Bildschirm, der nicht gemountet ist |
leadInMs / tailHoldMs | Leerlauf vor Schritt 1 / letztes Frame halten, vom Renderer aufgefüllt (der Take-Schwanz ist 1 s) |
steps[] | argv (nach warden; der Recorder hängt --udid an), output (ok zeichnet einen Vektor-Haken, plain ist stumm, lines gibt mehrere stumme Zeilen aus), pauseAfterMs, shown?, hidden?, quote? |
outro | # comment-Zeile, die nach der letzten Ausgabe eingetippt wird |
output: { kind: "lines", text: [...] } gibt eine stumme Zeile pro Eintrag aus (maximal etwa 6; jeder weitere kostet 0,4 eines Eintragsabstands im Layout) für Code oder Text, der aus einer Datei angezeigt wird. Kombiniere es mit quote: { file } (Pfad relativ zu app.project): Nach dem Schritt prüft der Recorder, ob jede gedruckte Zeile (getrimmt) in dieser Datei vorkommt, und wirft zitierte Zeile nicht in <file>: <line>, andernfalls wird nie eine Zeile erfunden. Zeilen durchlaufen dennoch maskSecrets.
Zeiten werden niemals von Hand geschrieben: Der Recorder misst Start und Ende jedes Schritts anhand der Video-Clock und die Ausgabezeile erscheint am ENDE des Schritts.
3. Der Kreislauf
bun run demo <feature>/<clip> --record-only # Claim, 9:41 Statusleiste, Start, Aufnahme, Freigabe nach ID
bun run demo <feature>/<clip> --render-only --sheet # mp4 + jpg erstellen, <work>/sheet.jpg schreiben
bun run demo <feature>/<clip> # beides
Arbeitsverzeichnis: $TMPDIR/warden-demo/<feature>/<clip> (--out dir): take.mp4, steps.json, plan.json, sheet.jpg; bei einem fehlgeschlagenen Take: failure-snapshot.txt (Fehler + ui snapshot -i). Nichts davon wird committet.
- Aufnehmen.
sheet.jpglesen (4×4 Kacheln, 1 pro Sekunde, pts eingebrannt). Gegen das Log prüfen: Jede Ausgabezeile erscheint, wenn das Telefon sichtbar reagiert; kein Frame zeigt einen unzusammenhängenden Alarm, eine Tastatur, eine rote Box oder eine falsche Statusleiste; das letzte Frame zeigt den Endzustand.- Für eine genauere Ansicht:
ffmpeg -ss 7 -i take.mp4 -frames:v 1 f.pngund lesen. - In der Definition korrigieren (
pauseAfterMs, Schritt löschen/ausblenden,shown) und--render-only; nur neu aufnehmen, wenn der Take selbst falsch war. Stapelkorrekturen: eine Aufnahme pro Runde. - Das mp4 + jpg in
apps/docs/public/demos/<feature>/committen.
4. Ehrlichkeitsregeln
- Jeder gedruckte Befehl wurde ausgeführt, gegen die echte App. Kein
shown, das sich in der Bedeutung vonargvunterscheidet (es kürzt nur Flags wie--timeout). - Der Ausgabetext ist das, was das Telefon getan hat, nicht das, was der Befehl gedruckt hat. Wenn ein Tastendruck keine sichtbare Wirkung hat (während einer Blatteingabe-Animation fallen gelassen, nichts geändert), beschrifte ihn nicht:
hidden: true, oder korrigiere das Timing mit einem längeren vorherigenpauseAfterMs. - Berechtigungsalarme beim ersten Start erscheinen nur bei einer Neuinstallation; der Claim verwendet einen warmen Build, also beschrifte keinen Alarm, es sei denn, der Take zeigt ihn.
- Keine app-seitige Umgehung oder vorgetäuschte Eingabeaufforderung: Die Behauptung im Titel muss im Take wahr sein.
- Geheimnisse:
maskSecretsverbirgt key=value,ghp_/sk--ähnliche Token und lange Hex/Base64 in gedruckten Zeilen. Überprüfe dennoch das Telefon auf Token, E-Mails, Gerätenamen. - Echter Simulator, echte iOS-Oberfläche. Niemals gefälschte Frames zusammensetzen.
5. Fallstricke (im Kit kodiert)
- simctl video ist VFR und lässt den Leerlauf-Schwanz weg: Der Renderer normalisiert die
fpsund füllt das letzte Frame (tpad) auf die Planlänge auf. - Die Videozeit stimmt mit dem Befehlslog auf ~0,5 s genau überein; der Recorder startet die Uhr direkt nach
video start. - Martian Mono hat kein Häkchen-Glyph:
render.pyzeichnet es als Vektoren. - Der Rahmen ist die dunkle Palette des Device Hub der Docs (Mona Sans + Martian Mono); setze
app.appearance: "dark", damit das Telefon dazu passt. Der Recorder stellt das Erscheinungsbild des Simulators danach wieder her. - Terminalüberlauf:
computeTerminalLayoutverkleinert Zeichenabstand und Schrift auf 17 px und scrollt dann die ältesten Einträge weg. - Geräte-Videolimit 3 min; halte Takes dennoch unter 40 s.
- Statusleiste:
xcrun simctl status_bar <udid> overrideist auf einer gemieteten udid erlaubt; der Recorder löscht es danach.
6. X-Export
1920×1080, H.264 High, yuv420p, 30 fps, +faststart, kein Audio, <= 2:20, kleine Datei (Kit zielt auf <= 2 MB über eine CRF-Leiter 24..36). Prüfen:
ffprobe -v error -show_entries stream=codec_name,profile,width,height,r_frame_rate -show_entries format=duration,size apps/docs/public/demos/<feature>/<clip>.mp4
7. Assets und Docs-Einbettung
Ausgabe: apps/docs/public/demos/<feature>/<clip>.mp4 + <clip>.jpg (Poster = letztes Frame), bereitgestellt unter /demos/<feature>/<clip>.mp4 (Vite public/). Einbettung mit der DemoClip MDX-Komponente (apps/docs/src/components/demo-clip.tsx): Autoplay stumm und Schleife, ruht auf dem Poster bei reduzierter Bewegung. src hat keine Erweiterung; label sagt, was der Clip zeigt (entspricht der Bildunterschrift). Ein rohes <video> besteht apps/docs/src/lib/source.test.ts nicht.
<DemoClip src="/demos/<feature>/<clip>" label="warden ui … führt … auf einem gemieteten Simulator aus" />
Füge einen Bildunterschrifts-Satz hinzu, der die Befehle nennt. Die Batch-Spule apps/docs/public/demos/e2e/batch.* (eingebettet mit <DemoReel>) ist eine andere Pipeline (bun run demo:compose).
8. Die warden dev-Spule (dev/hero)
Eine dritte Pipeline: Wardens ECHTES Dev-Dashboard (links, in einer Terminalkarte) neben dem Simulator, den warden dev selbst claimt (rechts). Nichts wird getippt oder gefälscht: dev-take.ts erzeugt warden dev ios --label demo-dev-<clip> --headless --no-shutdown in einem PTY (Bun.spawn({ terminal })) gespiegelt in @xterm/headless (Frame-Liest für die Warteschritte) und schreibt jeden PTY-Chunk in tui.cast; der Uhrursprung ist der Spawn. Es pollt warden ls --json nach dem markierten Geräte-Lease, setzt dann dunkel + 9:41, deinstalliert die App und startet ui screen video (das Telefon ist bis dahin schwarz). Das Dev-Kind erhält seine eigene WARDEN_SESSION_ID: warden dev übernimmt einen Geräte-Lease, der von der Sitzung des Aufrufers gehalten wird, und jeder Teammitglied-Agent teilt sich einen, also könnte der Take sonst den Simulator eines anderen steuern.
bun run demo:dev dev/hero --record-only # Claim, App deinstallieren, 9:41 + dunkel, aufnehmen, Freigabe nach ID
bun run demo:dev dev/hero --render-only --sheet # agg + ffmpeg -> apps/docs/public/demos/dev/hero.{mp4,webm,jpg}, <work>/sheet.jpg
bun run demo:dev dev/hero # beides
- Kit:
dev-demo.ts(CLI),dev-take.ts(Recorder,runBeats),dev-render.ts(agg + compose,DEV_LAYOUT),dev-frame.py(Karte, Ring, Maske),dev-timeline.ts(Cast-Text, Rampensegmente, Bildunterschriften). Typen:DevDemoDefinition/DevBeatindemo.types.ts. Definition:apps/example/demos/dev/<clip>.demo.ts; Arbeitsverzeichnis$TMPDIR/warden-demo/dev/<clip>(take.mp4,tui.cast,dev.json,sheet.jpg,failure-frame.txt). - Ein Beat =
waitScreen(Regex über den TUI-Frame) +waitPhone(ui wait-Selektoren) -> Aktion (keysin die TUI und/oder eine echteediteiner Repo-Datei) ->afterPhone-Selektoren (das Hot-Reload-Landing) ->pauseAfterMs.dev.jsonhält Start / Bereit / Ende jedes Beats auf der Video-Clock; Bildunterschriften laufen von der Aktion eines Beats bis zur nächsten. - Die Bearbeitung ist eine echte Quellcode-Bearbeitung (
welcome.tsx-Textkörper), die Byte für Byte infinallyrückgängig gemacht wird; überprüfegit diff apps/example/srcnach einem Take: Es muss leer sein. uninstallFirstentfernt die App vom gemieteten Simulator, damitwarden devden Dev-Build aus dem Cache auf der Kamera installiert. Der ERSTE Take auf einem Fingerabdruck ist ein lokaler Build (Minuten,dev build-Reihe kompiliert): Einmal aufnehmen, um den Cache zu wärmen, dann neu aufnehmen.warden app ensure --kind devwärmt nicht den Cache, denwarden devliest.- Geschwindigkeitsrampen:
DevBeat.ramp: Nbeschleunigt das Warten dieses Beats um N (wobei 1,2 s Kopf und 1 s Schwanz in Echtgeschwindigkeit bleiben) und die Spule zeichnet ein× N speed-Tag. Ein warmer Take, der im Dev-Client-Warten langsam ist, rampelt ihn. Tastenanschläge und der Hot Reload sind immer in Echtzeit. - Ehrlichkeit: Fakten in der
<DemoReel>Bildunterschrift stammen aus den TUI-Zeilen des Takes (dev build 3.5s,Metro 3.9s,dev client 161ms) unddev.json(Bearbeitung ->ui waitsieht den neuen Text: 0.76 s). Lies sie nach jeder Neuaufnahme erneut; sie ändern sich. - Grenzen: Das Dashboard hat keine separate Claim-Zeile (es ist innerhalb von
preparing the ios dev build);--headlessdamit kein Device Hub-Fenster geöffnet wird; 100×26 PTY (100 Spalten ist das Minimum des HUD). Ein Beat mitstopVideoFirststoppt das Video vor seiner Aktion (qbeendet den Lease undwarden uiweigert sich dann). Nachqüberprüft der Take, dasswarden lskeinen Lease der Sitzung hält (schlägt andernfalls fehl), und der Cast wird dort abgeschnitten, wo das Dashboard den Alternativbildschirm verlässt, sodass die Spule auf ihrem letzten Frame endet. Der weiße Ladebildschirm des Dev-Launchers und das blaueRefreshing…-Banner sind die echte App; ein Metro-Neuladen kann auch von einem anderen Agenten stammen, derapps/example/srcbearbeitet. - Rampen:
rampgilt nur, wenn das Warten abzüglich 1,2 s Kopf und 1 s Schwanz mindestens 6 s (RAMP_MIN_S) beträgt. Der Hero rampelt sein Dev-Client-Warten 3x mit einem Tag. devEnvfügt Umgebungsvariablen zumwarden dev-Kind hinzu; der Hero setztEXPO_NO_DEPENDENCY_VALIDATION=1(@expo/clicheckDependenciesOnStart), um Expo's "Update verfügbar"-Nervigkeit aus dem Metro-Bereich zu entfernen. Verwende es nur für Rauschen, niemals für Verhalten.- Einbettung mit
<DemoReel src="/demos/dev/hero" command="warden dev ios" facts="…" label="…" />(Landing-Hero,sims/dev.mdx,getting-started/quickstart.mdx). Poster = das Aktionsframe vonposterBeat+ 1 s (HUD eingeschaltet, alle Zeilen erledigt, App läuft).
9. Terminallastige Clips (VID-6)
Alle echten Befehle; jede Spule zeigt nur, was ausgeführt wurde. Ein Clip nach dem anderen, Freigabe nach Label nach jedem.
| Clip | Pipeline | Befehl |
|---|---|---|
e2e/parallel | batch-take.ts: warden e2e <suite> --count N --record --headless in <work>/rec, Styling jedes gemieteten Simulators dunkel + 9:41 beim Booten, dann compose.ts --height 1080 | bun run demo:batch e2e/parallel --suite example --specs devtools,gestures,onboarding,sign-in,todo-detail --count 2 (--record-only / --compose-only; Header zeigt Spec-Anzahl; Zähler liest N/total erledigte Shards, wenn sich Specs von Jobs unterscheiden) |
dev/worktrees | LanesDemoDefinition: ein recordDev pro Lane gleichzeitig, ein Shell-Streifen (warden ls) darunter | bun run demo:lanes dev/worktrees (lanes.ts, lanes-frame.py, lanes-demo.ts; Lanes teilen eine Clock über DevTake.startedAtS) |
golden/clone | DevDemoDefinition.script: Ein Shell-Skript läuft im PTY anstelle von warden dev | bun run demo:dev golden/clone |
debug/failure | script + rawVideo | bun run demo:dev debug/failure --sheet |
script-Modus: Keine Deinstallation, keine Freigabeprüfung; der Take endet mitrelease --mine --label L --shutdownundsims delete <udid> --yes. Ein Beat mitwaitBootpolltsimctl io screenshot, bis das PNG 100 KB überschreitet (HOME_SCREEN_MIN_BYTES): Ein frisch geklonter Headless-Simulator zeigt das Apple-Logo 10+ s nach der Rückkehr vonclaim. Rample dieses Warten (3x, mit Tag).rawVideo: Zeichnet mitsimctl io recordVideoauf (SIGINT zum Stoppen), weil derui screen video-Helfer von warden mitwarden e2eauf einem Gerät in Konflikt gerät.--recordzeichnet nur Geräte auf, die zu Beginn vorhanden sind; elastische späte Beitreter haben kein Video. Das Host-Kapazitäts-Gate bestimmt, wie viele passen, also fordere nicht mehr Sims an, als zu Beginn gebootet sind (hier max 2).- Wegwerf-Spec für
debug/failurewird inapps/example/e2e/debug-demo.e2e.tsgeschrieben und durch eine Shell-Falle entfernt; überprüfegit statusdanach (ein abgebrochener Take kann sie hinterlassen). - Einbettung mit
<DemoReel src="/demos/<feature>/<clip>" command="…" facts="…" label="…" />; Fakten stammen aus dem Take, lies sie nach jeder Neuaufnahme erneut. demo:batchstylt Sims mit einem Poller (styleWhenBooted:warden ls --jsonfür das Label,simctl listfür Booted, dannui appearance dark+status_bar override 9:41), weilwarden e2ekeinen Per-Geräte-Hook hat; gibt--mine --label demo-<feature>-<clip> --shutdowninfinallyfrei. Verwende Specs, die auf einem sauberen Simulator bestehen (perf,settings,todosbestehen nicht zuverlässig); dascompose --height 1080-Flag gibt das 16:9-Layout (Terminal