¿Qué hace demo-video?
bun run demo <feature>/<clip> graba una toma en un simulador iOS arrendado, luego renderiza un video 1920x1080: título a la izquierda, una terminal que escribe cada comando real de warden e imprime su salida cuando el paso termina, el teléfono a la derecha (redondeado, anillo de 3px). Kit: scripts/demo/ (CLI demo.ts, record.ts, render.py, timeline.ts, demo.types.ts). Definiciones: apps/example/demos/<feature>/<clip>.demo.ts (name debe ser igual a <feature>/<clip>); assets: apps/docs/public/demos/<feature>/<clip>.mp4|jpg. Una carpeta por característica (biometrics, network, location, clock), minúsculas, guiones. Interno: sin changeset.
1. Cuándo hacer uno
- Una característica cuyo efecto es visible en pantalla y ocurre en segundos (biometría, alertas, gestos). No para logs, números de rendimiento o cualquier cosa que una tabla muestre mejor.
- Después de que la UI de la app o el comportamiento de un comando cambió: regrabar, nunca editar el mp4.
- Una historia por clip, 10–25 s, 3–6 pasos con subtítulos.
Requiere ffmpeg, ffprobe, uv (brew install ffmpeg uv), Xcode, y las fuentes en node_modules/@expo-google-fonts (bun install).
2. Escribir una definición
Exporta demo: DemoDefinition (scripts/demo/demo.types.ts). Copia apps/example/demos/biometrics/faceid.demo.ts.
| Campo | Significado |
|---|---|
title / subtitle / terminalTitle / footer | texto estático; title es la afirmación, subtitle una línea |
phoneCaption | línea debajo del teléfono, por defecto simulador real · no es una animación; "" lo oculta |
claim | { biometrics?, profile? } -> warden claim ios --json --label demo-<feature>-<clip> --ttl 30m [--biometrics] |
app | bundleId, kind (simulator por defecto), project (cwd relativo al repositorio), deepLink abierto con --settle, appearance (light/dark, establecido via simctl ui en el udid arrendado, restaurado después) |
setup | warden argv ejecutados fuera de cámara después de abrir la app, antes del deep link; ["ui","network","set","online","--relaunch"] da el lanzamiento del dylib de iOS que los pasos de red/reloj necesitan. Una entrada es un argv o { argv, optional: true }: un paso opcional que sale con código distinto de cero se registra (setup opcional warden … -> <code>) en lugar de fallar la toma (un permiso que puede no aparecer) |
ready | selectores warden ui wait que deben pasar después del deep link y antes de que el video comience (ej. id=bio-screen); un paso nunca presiona una pantalla que no se ha montado |
leadInMs / tailHoldMs | tiempo de inactividad antes del paso 1 / última retención de fotograma, rellenado por el renderizador (la cola de la toma es 1 s) |
steps[] | argv (después de warden; el grabador añade --udid), output (ok dibuja un check vectorial, plain es silenciado, lines imprime varias líneas silenciadas), pauseAfterMs, shown?, hidden?, quote? |
outro | línea # comment escrita después de la última salida |
output: { kind: "lines", text: [...] } imprime una línea silenciada por entrada (mantén alrededor de 6; cada una después de la primera cuesta 0.4 de un paso de entrada en el diseño) para código o texto mostrado desde un archivo. Combínalo con quote: { file } (ruta relativa a app.project): después del paso, el grabador verifica que cada línea impresa, recortada, aparece en ese archivo y lanza línea citada no está en <file>: <line> en caso contrario, así una línea citada nunca se inventa. Las líneas aún pasan por maskSecrets.
Los tiempos nunca se escriben a mano: el grabador mide el inicio y fin de cada paso contra el reloj del video y la línea de salida aparece al FINAL del paso.
3. El bucle
bun run demo <feature>/<clip> --record-only # claim, barra de estado 9:41, lanzar, grabar, liberar por id
bun run demo <feature>/<clip> --render-only --sheet # construir mp4 + jpg, escribir <work>/sheet.jpg
bun run demo <feature>/<clip> # ambos
Directorio de trabajo: $TMPDIR/warden-demo/<feature>/<clip> (--out dir): take.mp4, steps.json, plan.json, sheet.jpg; en una toma fallida failure-snapshot.txt (error + ui snapshot -i). Nada de eso se commitea.
- Grabar.
- Leer
sheet.jpg(mosaico 4x4, 1 por segundo, pts quemados). Verificar contra el log: cada línea de salida aparece cuando el teléfono reacciona visiblemente; ningún fotograma muestra una alerta no relacionada, teclado, caja roja o barra de estado incorrecta; el último fotograma muestra el estado final. - Para un análisis más detallado:
ffmpeg -ss 7 -i take.mp4 -frames:v 1 f.pngyRead. - Arreglar en la definición (
pauseAfterMs, eliminar/ocultar un paso,shown) y--render-only; regrabar solo cuando la toma en sí fue incorrecta. Correcciones por lotes: una grabación por ronda. - Committear el mp4 + jpg en
apps/docs/public/demos/<feature>/.
4. Reglas de honestidad
- Cada comando impreso se ejecutó, contra la app real. Sin
shownque difiera en significado deargv(solo recorta banderas como--timeout). - El texto de salida es lo que el teléfono hizo, no lo que el comando imprimió. Si una pulsación no tiene efecto visible (se perdió durante la animación de entrada de una hoja, nada cambió), no le pongas subtítulo:
hidden: true, o arregla el timing con unpauseAfterMsanterior más largo. - Las alertas de permisos de primera ejecución solo aparecen en una instalación fresca; el claim reutiliza una compilación caliente, así que no subtitules una alerta a menos que la toma la muestre.
- Sin bypass del lado de la app ni prompt simulado: la afirmación en el título debe ser verdadera en la toma.
- Secretos:
maskSecretsoculta clave=valor, tokens estiloghp_/sk-y hex/base64 largos en líneas impresas. Aún así verifica el teléfono por tokens, correos, nombres de dispositivo. - Simulador real, UI iOS real. Nunca compongas fotogramas falsos.
5. Trampas (codificadas en el kit)
- simctl video es VFR y descarta la cola inactiva: el renderizador normaliza
fpsy rellena el último fotograma (tpad) a la duración del plan. - El tiempo del video coincide con el log de comandos dentro de ~0.5 s; el grabador inicia el reloj justo después de
video start. - Martian Mono no tiene glifo de check:
render.pylo dibuja como vectores. - El fotograma es la paleta oscura del Device Hub de docs (Mona Sans + Martian Mono); establece
app.appearance: "dark"para que el teléfono coincida. El grabador restaura la apariencia del simulador después. - Desbordamiento de terminal:
computeTerminalLayoutreduce paso y fuente a 17 px, luego desplaza las entradas más antiguas. - Límite de video del dispositivo 3 min; mantén las tomas bajo 40 s de todas formas.
- Barra de estado:
xcrun simctl status_bar <udid> overrideestá permitido en un udid arrendado; el grabador lo limpia después.
6. Exportación a X
1920x1080, H.264 High, yuv420p, 30 fps, +faststart, sin audio, <= 2:20, archivo pequeño (el kit apunta a <= 2 MB via una escalera CRF 24..36). Verificar:
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 y embebido en docs
Salida: apps/docs/public/demos/<feature>/<clip>.mp4 + <clip>.jpg (poster = último fotograma), servido en /demos/<feature>/<clip>.mp4 (Vite public/). Embebe con el componente MDX DemoClip (apps/docs/src/components/demo-clip.tsx): reproducción automática silenciada y en bucle, reposa en el poster bajo movimiento reducido. src no tiene extensión; label dice lo que muestra el clip (coincide con el subtítulo). Un <video> sin formato falla apps/docs/src/lib/source.test.ts.
<DemoClip src="/demos/<feature>/<clip>" label="warden ui … haciendo … en un simulador arrendado" />
Añade una oración de subtítulo nombrando los comandos. El reel por lotes apps/docs/public/demos/e2e/batch.* (embebido con <DemoReel>) es un pipeline diferente (bun run demo:compose).
8. El reel warden dev (dev/hero)
Un tercer pipeline: el panel de desarrollo REAL de warden (izquierda, en una tarjeta de terminal) junto al simulador que warden dev reclama por sí mismo (derecha). Nada se escribe o falsifica: dev-take.ts lanza warden dev ios --label demo-dev-<clip> --headless --no-shutdown en un PTY (Bun.spawn({ terminal })) reflejado en @xterm/headless (lecturas de fotogramas para las esperas) y escribe cada chunk del PTY en tui.cast; el origen del reloj es el lanzamiento. Sondea warden ls --json en busca de la concesión de dispositivo etiquetado, luego establece oscuro + 9:41, desinstala la app e inicia ui screen video (el teléfono está negro hasta entonces). El proceso hijo de dev obtiene su propio WARDEN_SESSION_ID: warden dev adopta una concesión de dispositivo retenida por la sesión del llamante, y cada agente compañero comparte una, así que sin él la toma podría manejar el simulador de otra persona.
bun run demo:dev dev/hero --record-only # claim, desinstalar app, 9:41 + oscuro, grabar, liberar por 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 # ambos
- Kit:
dev-demo.ts(CLI),dev-take.ts(grabador,runBeats),dev-render.ts(agg + compose,DEV_LAYOUT),dev-frame.py(tarjeta, anillo, máscara),dev-timeline.ts(texto cast, segmentos de rampa, subtítulos). Tipos:DevDemoDefinition/DevBeatendemo.types.ts. Definición:apps/example/demos/dev/<clip>.demo.ts; directorio de trabajo$TMPDIR/warden-demo/dev/<clip>(take.mp4,tui.cast,dev.json,sheet.jpg,failure-frame.txt). - Un beat =
waitScreen(regex sobre el fotograma TUI) +waitPhone(selectoresui wait) -> acción (keysen el TUI, y/o unaeditreal de un archivo del repositorio) -> selectoresafterPhone(el aterrizaje de hot reload) ->pauseAfterMs.dev.jsoncontiene el inicio / listo / fin de cada beat en el reloj del video; los subtítulos van desde la acción de un beat hasta la del siguiente. - La edit es una edición real de fuente (
welcome.tsxstring del cuerpo), revertida byte por byte enfinally; verifica quegit diff apps/example/srcesté vacío después de una toma. uninstallFirstelimina la app del simulador arrendado para quewarden devinstale la compilación de desarrollo del caché en cámara. La PRIMERA toma en una huella es una compilación local (minutos, la filadev buildcompila): graba una vez para calentar el caché, luego regraba.warden app ensure --kind devno calienta el caché quewarden devlee.- Rampas de velocidad:
DevBeat.ramp: Nacelera la espera de ese beat en N (manteniendo 1.2 s de cabeza y 1 s de cola a velocidad real) y el reel dibuja una etiqueta× N speed. Una toma caliente que es lenta en la espera del cliente dev la acelera. Las teclas y el hot reload son siempre en tiempo real. - Honestidad: los hechos en el subtítulo de
<DemoReel>provienen de las filas TUI de la toma (dev build 3.5s,Metro 3.9s,dev client 161ms) ydev.json(edit ->ui waitve el nuevo texto: 0.76 s). Revisarlos después de cada regrabación; cambian. - Límites: el panel no tiene fila de claim separada (está dentro de
preparando la compilación dev de ios);--headlesspara que no se abra ninguna ventana de Device Hub; PTY 100x26 (100 columnas es el mínimo del HUD). Un beat constopVideoFirstdetiene el video antes de su acción (qfinaliza la concesión ywarden uiluego se niega). Después deq, la toma verifica quewarden lsno tenga ninguna concesión de la sesión (falla en caso contrario), y el cast se corta donde el panel sale de la pantalla alternativa para que el reel termine en su último fotograma. La pantalla de carga blanca del launcher dev y el banner azulRefrescando…son la app real; una recarga de Metro también puede venir de otro agente editandoapps/example/src. - Rampas:
rampsolo aplica cuando la espera, menos su cabeza de 1.2 s y cola de 1 s, es al menos 6 s (RAMP_MIN_S). El hero acelera su espera de cliente dev 3x con una etiqueta. devEnvañade entorno al hijowarden dev; el hero estableceEXPO_NO_DEPENDENCY_VALIDATION=1(@expo/clicheckDependenciesOnStart) para eliminar la molestia "actualización disponible" de Expo del panel Metro. Úsalo solo para ruido, nunca comportamiento.- Embebe con
<DemoReel src="/demos/dev/hero" command="warden dev ios" facts="…" label="…" />(hero de aterrizaje,sims/dev.mdx,getting-started/quickstart.mdx). Poster = el fotograma de acción delposterBeat+ 1 s (HUD encendido, todas las filas hechas, app ejecutándose).
9. Clips de terminal pesada (VID-6)
Todos comandos reales; cada reel muestra solo lo que se ejecutó. Un clip a la vez, liberar por etiqueta después de cada uno.
| Clip | Pipeline | Comando |
|---|---|---|
e2e/parallel | batch-take.ts: warden e2e <suite> --count N --record --headless en <work>/rec, estilizando cada simulador arrendado oscuro + 9:41 mientras arranca, luego 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; el encabezado muestra el recuento de specs; el contador lee N/total shards done cuando las specs difieren de los trabajos) |
dev/worktrees | LanesDemoDefinition: un recordDev por carril concurrentemente, una tira de shell (warden ls) debajo | bun run demo:lanes dev/worktrees (lanes.ts, lanes-frame.py, lanes-demo.ts; los carriles comparten un reloj via DevTake.startedAtS) |
golden/clone | DevDemoDefinition.script: un script de shell se ejecuta en el PTY en lugar de warden dev | bun run demo:dev golden/clone |
debug/failure | script + rawVideo | bun run demo:dev debug/failure --sheet |
- Modo
script: sin desinstalación, sin verificación de liberación; la toma termina conrelease --mine --label L --shutdownysims delete <udid> --yes. Un beat conwaitBootsondeasimctl io screenshothasta que el PNG supera los 100 KB (HOME_SCREEN_MIN_BYTES): un simulador headless recién clonado muestra el logo de Apple 10+ s después de queclaimretorna. Acelera esa espera (3x, con etiqueta). rawVideo: graba consimctl io recordVideo(SIGINT para detener) porque el helperui screen videode warden entra en conflicto conwarden e2een un dispositivo.--recordsolo graba dispositivos presentes al inicio; los que se unen tarde de forma elástica no tienen video. La puerta de capacidad del host establece cuántos caben, así que pide no más simuladores de los que se iniciaron al inicio (máx 2 aquí).- Spec desechable para
debug/failurese escribe enapps/example/e2e/debug-demo.e2e.tsy se elimina mediante un trap de shell; verificagit statusdespués (una toma matada puede dejarlo). - Embebe con
<DemoReel src="/demos/<feature>/<clip>" command="…" facts="…" label="…" />; los hechos provienen de la toma, reléelos después de cada regrabación. demo:batchestiliza simuladores con un sondeo (styleWhenBooted:warden ls --jsonpara la etiqueta,simctl listpara Booted, luegoui appearance dark+status_bar override 9:41) porquewarden e2eno tiene hook por dispositivo; libera--mine --label demo-<feature>-<clip> --shutdownenfinally. Usa specs que pasen en un simulador limpio (perf,settings,todosno son fiables); la bandera--height 1080de compose da el diseño 16:9 (terminal