Que fait demo-video ?
bun run demo <feature>/<clip> enregistre une prise sur un simulateur iOS loué, puis génère une vidéo 1920×1080 : titre à gauche, un terminal qui tape chaque commande warden réelle et affiche sa sortie à la fin de l'étape, le téléphone à droite (arrondi, anneau de 3 px). Kit : scripts/demo/ (demo.ts CLI, record.ts, render.py, timeline.ts, demo.types.ts). Définitions : apps/example/demos/<feature>/<clip>.demo.ts (name doit être égal à <feature>/<clip>) ; assets : apps/docs/public/demos/<feature>/<clip>.mp4|jpg. Un dossier par fonctionnalité (biometrics, network, location, clock), en minuscules, avec des tirets. Interne : pas de changeset.
1. Quand en faire une
- Une fonctionnalité dont l'effet est visible à l'écran et se produit en quelques secondes (biométrie, alertes, gestes). Pas pour les logs, les mesures de performance ou tout ce qu'un tableau montre mieux.
- Après un changement de l'interface utilisateur de l'application ou du comportement d'une commande : réenregistrer, ne jamais éditer le mp4.
- Une histoire par clip, 10-25 s, 3-6 étapes avec légendes.
Nécessite ffmpeg, ffprobe, uv (brew install ffmpeg uv), Xcode, et les polices dans node_modules/@expo-google-fonts (bun install).
2. Rédiger une définition
Exportez demo: DemoDefinition (scripts/demo/demo.types.ts). Copiez apps/example/demos/biometrics/faceid.demo.ts.
| Champ | Signification |
|---|---|
title / subtitle / terminalTitle / footer | texte statique ; title est l'affirmation, subtitle une ligne |
phoneCaption | ligne sous le téléphone, par défaut real simulator · not an animation ; "" la masque |
claim | { biometrics?, profile? } -> warden claim ios --json --label demo-<feature>-<clip> --ttl 30m [--biometrics] |
app | bundleId, kind (par défaut simulator), project (cwd relatif au dépôt), deepLink ouvert avec --settle, appearance (light/dark, défini via simctl ui sur l'udid loué, restauré après) |
setup | argv warden exécuté hors caméra après l'ouverture de l'application, avant le deep link ; ["ui","network","set","online","--relaunch"] donne le lancement du dylib iOS dont les étapes réseau/horloge ont besoin. Une entrée est un argv ou { argv, optional: true } : une étape optionnelle qui se termine avec un code non nul est enregistrée (optional setup warden … -> <code>) au lieu d'échouer la prise (une invite de permission qui peut ne pas apparaître) |
ready | sélecteurs warden ui wait qui doivent réussir après le deep link et avant le début de la vidéo (par exemple id=bio-screen) ; une étape n'appuie jamais sur un écran qui n'a pas été monté |
leadInMs / tailHoldMs | temps mort avant l'étape 1 / maintien de la dernière image, complété par le rendu (la queue de la prise est de 1 s) |
steps[] | argv (après warden ; l'enregistreur ajoute --udid), output (ok dessine une coche vectorielle, plain est muet, lines affiche plusieurs lignes muettes), pauseAfterMs, shown?, hidden?, quote? |
outro | ligne # comment tapée après la dernière sortie |
output: { kind: "lines", text: [...] } affiche une ligne muette par entrée (gardez environ 6 ; chaque ligne supplémentaire coûte 0,4 pas d'entrée dans la disposition) pour le code ou le texte affiché à partir d'un fichier. Associez-le à quote: { file } (chemin relatif à app.project) : après l'étape, l'enregistreur vérifie que chaque ligne imprimée, une fois nettoyée, apparaît dans ce fichier et lève une erreur quoted line not in <file>: <line> dans le cas contraire, de sorte qu'une ligne citée n'est jamais inventée. Les lignes passent toujours par maskSecrets.
Les temps ne sont jamais écrits à la main : l'enregistreur mesure le début et la fin de chaque étape par rapport à l'horloge vidéo et la ligne de sortie apparaît à la FIN de l'étape.
3. La boucle
bun run demo <feature>/<clip> --record-only # claim, barre de statut 9:41, lancement, enregistrement, libération par id
bun run demo <feature>/<clip> --render-only --sheet # construit mp4 + jpg, écrit <work>/sheet.jpg
bun run demo <feature>/<clip> # les deux
Répertoire de travail : $TMPDIR/warden-demo/<feature>/<clip> (--out dir) : take.mp4, steps.json, plan.json, sheet.jpg ; en cas d'échec de prise failure-snapshot.txt (erreur + ui snapshot -i). Rien n'est commité.
- Enregistrer.
- Lire
sheet.jpg(tuiles 4x4, 1 par seconde, pts incrustés). Vérifier par rapport au log : chaque ligne de sortie atterrit lorsque le téléphone réagit visiblement ; aucune image ne montre une alerte, un clavier, une boîte rouge ou une barre de statut incorrecte non liés ; la dernière image montre l'état final. - Pour un examen plus approfondi :
ffmpeg -ss 7 -i take.mp4 -frames:v 1 f.pngetReadle fichier. - Corriger dans la définition (
pauseAfterMs, supprimer/masquer une étape,shown) et--render-only; ne réenregistrer que lorsque la prise elle-même était erronée. Corrections par lots : un enregistrement par tour. - Commiter le mp4 + jpg dans
apps/docs/public/demos/<feature>/.
4. Règles d'honnêteté
- Chaque commande imprimée a été exécutée, contre l'application réelle. Pas de
shownqui diffère en signification deargv(il supprime uniquement les indicateurs tels que--timeout). - Le texte de sortie est ce que le téléphone a fait, pas ce que la commande a imprimé. Si une pression n'a aucun effet visible (abandonnée pendant l'animation d'entrée d'une feuille, rien n'a changé), ne la légendez pas :
hidden: true, ou corrigez le timing avec unpauseAfterMsprécédent plus long. - Les alertes de permission de première exécution n'apparaissent que sur une nouvelle installation ; la revendication réutilise une construction chaude, donc ne légendez pas une alerte à moins que la prise ne la montre.
- Pas de contournement côté application ou d'invite simulée : l'affirmation dans le titre doit être vraie sur la prise.
- Secrets :
maskSecretsmasque key=value, les jetons de styleghp_/sk-et les longues chaînes hexadécimales/base64 dans les lignes imprimées. Vérifiez toujours le téléphone pour les jetons, les e-mails, les noms d'appareil. - Simulateur réel, interface iOS réelle. Ne composez jamais de fausses images.
5. Pièges (codés dans le kit)
- simctl video est VFR et supprime la queue inactive : le rendu normalise
fpset complète la dernière image (tpad) jusqu'à la durée prévue. - Le temps vidéo correspond au log de commande à ~0,5 s près ; l'enregistreur démarre l'horloge juste après
video start. - Martian Mono n'a pas de glyphe de coche :
render.pyle dessine sous forme de vecteurs. - L'image est la palette sombre du Device Hub de la documentation (Mona Sans + Martian Mono) ; définissez
app.appearance: "dark"pour que le téléphone corresponde. L'enregistreur restaure l'apparence du simulateur après. - Débordement du terminal :
computeTerminalLayoutréduit le pas et la police à 17 px, puis fait défiler les entrées les plus anciennes. - Limite vidéo de l'appareil 3 min ; gardez les prises en dessous de 40 s de toute façon.
- Barre de statut :
xcrun simctl status_bar <udid> overrideest autorisé sur un udid loué ; l'enregistreur l'efface après.
6. Export X
1920×1080, H.264 High, yuv420p, 30 ips, +faststart, pas d'audio, <= 2:20, petit fichier (le kit vise <= 2 Mo via une échelle CRF 24..36). Vérifiez :
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. Actifs et intégration dans la documentation
Sortie : apps/docs/public/demos/<feature>/<clip>.mp4 + <clip>.jpg (affiche = dernière image), servie à /demos/<feature>/<clip>.mp4 (Vite public/). Intégrez avec le composant MDX DemoClip (apps/docs/src/components/demo-clip.tsx) : lecture automatique en muet et en boucle, s'arrête sur l'affiche en cas de mouvement réduit. src n'a pas d'extension ; label indique ce que montre le clip (correspond à la légende). Une balise <video> brute échoue apps/docs/src/lib/source.test.ts.
<DemoClip src="/demos/<feature>/<clip>" label="warden ui … faisant … sur un simulateur loué" />
Ajoutez une phrase de légende nommant les commandes. Le montage batch apps/docs/public/demos/e2e/batch.* (intégré avec <DemoReel>) est un pipeline différent (bun run demo:compose).
8. Le montage warden dev (dev/hero)
Un troisième pipeline : le VRAI tableau de bord de développement de warden (à gauche, dans une carte terminal) à côté du simulateur que warden dev revendique lui-même (à droite). Rien n'est tapé ou falsifié : dev-take.ts lance warden dev ios --label demo-dev-<clip> --headless --no-shutdown dans un PTY (Bun.spawn({ terminal })) reflété dans @xterm/headless (lectures d'image pour les attentes) et écrit chaque morceau PTY dans tui.cast ; l'origine de l'horloge est le lancement. Il interroge warden ls --json pour le bail de l'appareil étiqueté, puis définit le thème sombre + 9:41, désinstalle l'application et démarre ui screen video (le téléphone est noir jusque-là). Le processus enfant dev obtient son propre WARDEN_SESSION_ID : warden dev adopte un bail d'appareil détenu par la session de l'appelant, et chaque agent coéquipier en partage un, donc sans cela, la prise pourrait conduire le simulateur de quelqu'un d'autre.
bun run demo:dev dev/hero --record-only # claim, désinstaller l'app, 9:41 + sombre, enregistrer, libérer par 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 # les deux
- Kit :
dev-demo.ts(CLI),dev-take.ts(enregistreur,runBeats),dev-render.ts(agg + compose,DEV_LAYOUT),dev-frame.py(carte, anneau, masque),dev-timeline.ts(texte de la distribution, segments de rampe, légendes). Types :DevDemoDefinition/DevBeatdansdemo.types.ts. Définition :apps/example/demos/dev/<clip>.demo.ts; répertoire de travail$TMPDIR/warden-demo/dev/<clip>(take.mp4,tui.cast,dev.json,sheet.jpg,failure-frame.txt). - Un beat =
waitScreen(regex sur l'image TUI) +waitPhone(sélecteursui wait) -> action (keysdans le TUI, et/ou une vraieeditd'un fichier du dépôt) -> sélecteursafterPhone(l'atterrissage du rechargement à chaud) ->pauseAfterMs.dev.jsoncontient le début / prêt / fin de chaque beat sur l'horloge vidéo ; les légendes vont du début d'une action de beat à celui du suivant. - L'édition est une vraie édition de source (corps de chaîne
welcome.tsx), annulée octet par octet dansfinally; vérifiez quegit diff apps/example/srcest vide après une prise. uninstallFirstsupprime l'application du simulateur loué afin quewarden devinstalle la version de développement depuis le cache sous la caméra. La PREMIÈRE prise sur une empreinte digitale est une construction locale (minutes, la lignedev buildcompile) : enregistrez une fois pour réchauffer le cache, puis réenregistrez.warden app ensure --kind devne réchauffe pas le cache quewarden devlit.- Accélérations :
DevBeat.ramp: Naccélère l'attente de ce beat de N (en gardant 1,2 s de tête et 1 s de queue à vitesse réelle) et le montage dessine une étiquette× N speed. Une prise chaude qui est lente dans l'attente du client dev l'accélère. Les touches et le rechargement à chaud sont toujours en temps réel. - Honnêteté : les faits dans la légende
<DemoReel>proviennent des lignes TUI de la prise (dev build 3.5s,Metro 3.9s,dev client 161ms) etdev.json(édition ->ui waitvoit le nouveau texte : 0,76 s). Relisez-les après chaque réenregistrement ; ils changent. - Limites : le tableau de bord n'a pas de ligne de revendication séparée (elle est à l'intérieur de
preparing the ios dev build) ;--headlesspour qu'aucune fenêtre Device Hub ne s'ouvre ; 100x26 PTY (100 colonnes est le minimum du HUD). Un beat avecstopVideoFirstarrête la vidéo avant son action (qtermine le bail etwarden uirefuse alors). Aprèsq, la prise vérifie quewarden lsne détient aucun bail de la session (échoue sinon), et la distribution est coupée là où le tableau de bord quitte l'écran alternatif pour que le montage se termine sur sa dernière image. L'écran de chargement blanc du lanceur dev et la bannière bleueRefreshing…sont la vraie application ; un rechargement Metro peut également provenir d'un autre agent modifiantapps/example/src. - Accélérations :
rampne s'applique que lorsque l'attente, moins ses 1,2 s de tête et 1 s de queue, est d'au moins 6 s (RAMP_MIN_S). Le héros accélère son attente client dev 3x avec une étiquette. devEnvajoute env à l'enfantwarden dev; le héros définitEXPO_NO_DEPENDENCY_VALIDATION=1(@expo/clicheckDependenciesOnStart) pour supprimer le message "update available" d'Expo du volet Metro. Utilisez-le uniquement pour le bruit, jamais pour le comportement.- Intégrez avec
<DemoReel src="/demos/dev/hero" command="warden dev ios" facts="…" label="…" />(héros de la page d'accueil,sims/dev.mdx,getting-started/quickstart.mdx). Affiche = l'image de l'actionposterBeat+ 1 s (HUD allumé, toutes les lignes terminées, application en cours d'exécution).
9. Clips lourds en terminal (VID-6)
Toutes les commandes réelles ; chaque montage ne montre que ce qui a été exécuté. Un clip à la fois, libération par étiquette après chacun.
| Clip | Pipeline | Commande |
|---|---|---|
e2e/parallel | batch-take.ts : warden e2e <suite> --count N --record --headless dans <work>/rec, en stylant chaque simulateur loué en sombre + 9:41 au démarrage, puis 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 ; l'en-tête montre le nombre de specs ; le compteur lit N/total shards done lorsque les specs diffèrent des jobs) |
dev/worktrees | LanesDemoDefinition : un recordDev par voie simultanément, une bande shell (warden ls) en dessous | bun run demo:lanes dev/worktrees (lanes.ts, lanes-frame.py, lanes-demo.ts ; les voies partagent une horloge via DevTake.startedAtS) |
golden/clone | DevDemoDefinition.script : un script shell s'exécute dans le PTY au lieu de warden dev | bun run demo:dev golden/clone |
debug/failure | script + rawVideo | bun run demo:dev debug/failure --sheet |
- Mode
script: pas de désinstallation, pas de vérification de libération ; la prise se termine parrelease --mine --label L --shutdownetsims delete <udid> --yes. Un beat avecwaitBootinterrogesimctl io screenshotjusqu'à ce que le PNG dépasse 100 Ko (HOME_SCREEN_MIN_BYTES) : un simulateur sans tête fraîchement cloné montre le logo Apple 10+ s après le retour declaim. Accélérez cette attente (3x, avec étiquette). rawVideo: enregistre avecsimctl io recordVideo(SIGINT pour arrêter) car l'assistantui screen videode warden entre en conflit avecwarden e2esur un seul appareil.--recordn'enregistre que les appareils présents au démarrage ; les arrivants tardifs élastiques n'ont pas de vidéo. La porte de capacité de l'hôte détermine combien peuvent tenir, donc ne demandez pas plus de simulateurs que ceux démarrés au départ (max 2 ici).- La spec jetable pour
debug/failureest écrite dansapps/example/e2e/debug-demo.e2e.tset supprimée par un piège shell ; vérifiezgit statusaprès (une prise tuée peut la laisser). - Intégrez avec
<DemoReel src="/demos/<feature>/<clip>" command="…" facts="…" label="…" />; les faits proviennent de la prise, relisez-les après chaque réenregistrement. demo:batchstyle les simulateurs avec un sondeur (styleWhenBooted:warden ls --jsonpour l'étiquette,simctl listpour Booted, puisui appearance dark+status_bar override 9:41) carwarden e2en'a pas de crochet par appareil ; il libère--mine --label demo-<feature>-<clip> --shutdowndansfinally. Utilisez des specs qui réussissent sur un simulateur propre (perf,settings,todosne réussissent pas de manière fiable) ; le drapeau de composition--height 1080donne la disposition 16:9 (terminal …