@vosjs/cli 0.47.0 → 0.48.0

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.
Files changed (33) hide show
  1. package/README.md +15 -5
  2. package/dist/{chunk-E42NCNWP.js → chunk-ANKECV7F.js} +634 -56
  3. package/dist/chunk-ANKECV7F.js.map +1 -0
  4. package/dist/{chunk-EFI3WUQB.js → chunk-P3X5KVA6.js} +15 -2
  5. package/dist/{chunk-EFI3WUQB.js.map → chunk-P3X5KVA6.js.map} +1 -1
  6. package/dist/cli.js +4 -4
  7. package/dist/index.js +2 -2
  8. package/dist/manifest-A4R367EM.js +8 -0
  9. package/dist/{run-DA5QXZ6F.js → run-CYHH56KA.js} +2 -2
  10. package/package.json +8 -6
  11. package/skills/VERSION +1 -0
  12. package/skills/launch-kit/SKILL.md +356 -0
  13. package/skills/launch-kit/references/channel-specs.md +56 -0
  14. package/skills/product-video/SKILL.md +263 -0
  15. package/skills/product-video/references/destinations.md +71 -0
  16. package/skills/product-video/references/sessions.md +226 -0
  17. package/skills/product-video/references/taste.md +115 -0
  18. package/skills/product-video/references/troubleshooting.md +116 -0
  19. package/skills/vos-authoring/SKILL.md +191 -0
  20. package/skills/vos-authoring/references/examples.md +239 -0
  21. package/skills/vos-authoring/references/schema-reference.md +468 -0
  22. package/skills/vos-create/SKILL.md +258 -0
  23. package/skills/vos-cut/SKILL.md +241 -0
  24. package/skills/vos-footage/SKILL.md +98 -0
  25. package/skills/vos-migrate/SKILL.md +108 -0
  26. package/skills/vos-remix/SKILL.md +136 -0
  27. package/skills/vos-remix/references/3d-recipe.md +37 -0
  28. package/skills/vos-remix/references/params-knobs.md +80 -0
  29. package/skills/vos-remix/references/remix-contract.md +81 -0
  30. package/dist/chunk-E42NCNWP.js.map +0 -1
  31. package/dist/manifest-UCS6OVWZ.js +0 -8
  32. /package/dist/{manifest-UCS6OVWZ.js.map → manifest-A4R367EM.js.map} +0 -0
  33. /package/dist/{run-DA5QXZ6F.js.map → run-CYHH56KA.js.map} +0 -0
@@ -0,0 +1,263 @@
1
+ ---
2
+ name: product-video
3
+ description: Record and produce the demo video of a website, app, or feature with the vos CLI — the agent scripts the click path (actions.json), the recording auto-plans zooms, and every editing decision is data in doc.json, so fixes are edits and re-renders, never re-recordings. Renders are deterministic; export is free up to 4K with no watermark. Use when asked to make a product video, demo video, screen recording of a URL or feature, or a marketing clip, including a product behind a login (the session ladder: mint one from the project's own test auth before asking anyone), or when a feature was verified in agent-browser and that walk should become a take. A whole release's asset set (store listing, Product Hunt gallery, social cuts) is the launch-kit skill, which records through this one.
4
+ license: MIT
5
+ ---
6
+
7
+ # Product video (and the assets around it), end to end
8
+
9
+ You are producing a shippable product asset — a polished video, a set of
10
+ exact-size stills, or both — from a live web page, with the `vos` CLI.
11
+ Everything is data: you write a flow script, the CLI records and plans, you
12
+ tune JSON, you re-render. **Never re-record to fix pacing or zooms — edit
13
+ `doc.json` and render again.** Quality bar: `references/taste.md` — follow
14
+ its quality loop and judge stills multimodally.
15
+
16
+ ## Setup
17
+
18
+ ```bash
19
+ npm i -D @vosjs/cli
20
+ ```
21
+
22
+ One command, one package: `@vosjs/cli` is the open source (MIT) `vos`
23
+ binary with the engine verbs, the take pipeline used here (record / plan /
24
+ frames / render on screen recordings) and the vos.so platform verbs. (Until
25
+ 0.9 the take pipeline shipped separately as `@vosso/vos-plugin`; a project
26
+ that still lists it can drop it.) Requirements:
27
+
28
+ - **A Chromium**: system Chrome is found automatically; otherwise
29
+ `npx playwright install chromium` or point `VOS_BROWSER_PATH` at one.
30
+ Exit code 3 means no browser was found.
31
+ - **Network at render time**: the render page loads three/mediabunny from
32
+ esm.sh. Recording also needs to reach the target URL. Fully offline
33
+ sandboxes cannot render — say so instead of shipping nothing.
34
+
35
+ Conventions: logs → stderr, results → stdout; `--json` streams NDJSON ending
36
+ with `{"event":"done",…}`; exit codes 0 ok / 1 error / 2 usage or strict
37
+ failure / 3 no browser / 4 the recorder met a sign-in instead of the page
38
+ (a missing or expired SESSION, never a script bug: `references/sessions.md`).
39
+ `vos <verb> --help` prints that verb's flags (`@vosjs/cli` 0.41.1 and later).
40
+
41
+ ## Step 0 — pick the destination (it decides everything)
42
+
43
+ | Destination | Viewport | Output | Extras |
44
+ |---|---|---|---|
45
+ | **Landing-page clip** (hero/section embed) | 2560×1440 (footage-native 2K) | webm → VP9 re-encode + poster | silent loop; see `references/destinations.md` |
46
+ | **Launch video** (PH, social, store promo) | 2560×1440 or 1280×720 | mp4 (`--format mp4`, needs system Chrome) | music bed via `doc.audio` |
47
+ | **Launch-kit stills** (store screenshots, tiles, OG) | sized to the asset | `frames --frame <t> --size WxH` PNGs from the same take | one take → every asset |
48
+ | **Quick demo** (issue, PR, chat) | 1280×720 | webm, defaults | speed over polish; drafts acceptable here ONLY |
49
+
50
+ Per-channel dimensions and byte budgets: `references/destinations.md`.
51
+
52
+ ## The core loop (every destination)
53
+
54
+ 1. **Explore the target page** with your own tools (fetch HTML / Playwright).
55
+ Identify the 3–6 moments that tell ONE story. Collect STABLE selectors
56
+ (`a[href='…']`, ids, roles — not nth-child chains).
57
+ **Stage the content like a set**: the script must leave the product in
58
+ the state a proud screenshot would show — labels typed, real-looking
59
+ data, the feature mid-story. An empty canvas records fast and demos
60
+ nothing, and no downstream composition rescues it.
61
+
62
+ **Behind a login? Settle the session before the script.** A recorder
63
+ with no session records the wall (the sign-in page, or the public page
64
+ the site sends a stranger to) and the only symptom is a skipped
65
+ selector. Walk the ladder in `references/sessions.md` top to bottom and
66
+ stop at the first rung that holds: no wall (a demo mode, a local server
67
+ with auth off) → MINT a session from the test auth the project already
68
+ has (`playwright/.auth`, an `auth.setup.ts`, a seed script: look before
69
+ you ask anyone anything) → sign in off camera with `setup` in
70
+ `actions.json` (the password from `{ "env": "NAME" }`, never a literal) →
71
+ the human signs in once (`vos session open <url> --name <app>`, then
72
+ `--session <app>` on record) → the human records with the extension from the shot list
73
+ `vos actions script actions.json` prints, and you cut it. Every
74
+ rung ends in `setup`, a state file for `--storage-state`, or a named
75
+ session for `--session`. Never
76
+ type or accept a production password, keep the state file out of the
77
+ take directory and out of git, and record from a demo account: what the
78
+ account shows ships in the video.
79
+
80
+ **Verified the feature with agent-browser already?** Keep that walk and
81
+ skip the second script. agent-browser's `--json` result does not say
82
+ what ran (`scroll` answers `{scrolled:true}`), so wrap each call so the
83
+ command rides beside its result, then convert:
84
+ ```bash
85
+ ab() { agent-browser "$@" --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);process.stdout.write(JSON.stringify({command:process.argv.slice(1),...r})+"\n")})' -- "$@" >> steps.jsonl; }
86
+ ab open https://target.example; ab snapshot -i -u; ab click @e27
87
+ ab wait 800; ab snapshot -i # the page changed: refs renumbered
88
+ ab fill @e2 query; ab press Enter
89
+ vos actions from-agent-browser steps.jsonl --out actions.json
90
+ ```
91
+ (a whole path run as one `agent-browser batch … --json > steps.jsonl`
92
+ already has that shape). The log is `steps.jsonl` in the directory you
93
+ stand in, so walk from one directory. Refs resolve through the last
94
+ `snapshot -i` before them, and they RENUMBER after a navigation and
95
+ again inside a dialog: re-snapshot after anything that changes the page
96
+ and read the refs before you name one (`-u` gives links their href).
97
+ Click the control, do not press its shortcut: ⌘K opens the dialog for a
98
+ human, but a keystroke is the one step the recorder cannot replay.
99
+ Whatever it cannot follow (a shortcut key, a drag, a second `open`) is
100
+ NAMED in the output, never dropped: read the notes, write those steps by
101
+ hand, then record.
102
+
103
+ 2. **Write `actions.json`**:
104
+ ```json
105
+ {
106
+ "url": "https://target.example",
107
+ "viewport": { "width": 1280, "height": 720 },
108
+ "steps": [
109
+ { "do": "wait", "ms": 800 },
110
+ { "do": "hover", "selector": "a[href='/pricing']", "ms": 700 },
111
+ { "do": "click", "selector": "#cta" },
112
+ { "do": "wait", "ms": 1500 },
113
+ { "do": "scroll", "dy": 400 },
114
+ { "do": "move", "x": 640, "y": 320 },
115
+ { "do": "wait", "ms": 900 }
116
+ ]
117
+ }
118
+ ```
119
+ Verbs: `wait` `hover` `click` `type` `scroll` `move` `drag`
120
+ (`type` = `{do:'type', selector, text, delayMs?, ms?, focus?}`: it clicks
121
+ the field, then types `text`; `focus:false` types into what is already
122
+ focused, for a submitting Enter)
123
+ (drag = real edits: `{do:'drag', selector|x,y, tx, ty, ms}` — slide a range
124
+ input, drag a canvas element, move a timeline clip). Pacing IS the zoom
125
+ plan: open `wait ≥700ms`; hover what matters 700–900ms (dwells become
126
+ zooms); a click's `ms` is READING time, held from the page's last change
127
+ (`@vosjs/cli` 0.47 and later; the recorder pays the settle itself), so
128
+ size it as a beat: about 1000ms after a navigation, 600ms after a
129
+ control, never padded for a slow page; end settled. Route the cursor away
130
+ from hover-triggered menus (taste.md, flow rules). Check with
131
+ `vos validate actions.json`.
132
+
133
+ Then REHEARSE it (`@vosjs/cli` 0.39 and later):
134
+ `vos record --actions actions.json --out take --dry-run`. Every step runs
135
+ against the real page, in order, because a later selector usually exists
136
+ only after an earlier click, but nothing is captured and nothing is
137
+ written: a missed selector is named in seconds (exit 2) instead of after
138
+ a real-time take and its encode, and a take already in `--out` keeps its
139
+ footage and its cut. It prints each step's rect in capture px, which is
140
+ what a pinned layer reads. Rehearse again after every script edit, and
141
+ record only a script that passes. A signed-in product rehearses the same
142
+ way: `--storage-state` and `--browser-arg=` apply to it too, and the
143
+ rehearsal ends by listing what the frame EXPOSES (an address, a key, a
144
+ card). Hide those with `mask` before you record:
145
+ `references/sessions.md`.
146
+
147
+ 3. **Record**: `vos record --actions actions.json --out take --strict --json`
148
+ `--strict` always: skipped selector / networkidle timeout → exit 2 with
149
+ `skipped[]` in the done event. A skip means the flow is broken — fix it,
150
+ never ship around it. The take auto-encodes and auto-plans.
151
+ (`vos create --actions actions.json out.webm --strict` is the one-shot
152
+ record+render verb — fine for a quick first pass, but THIS skill's loop
153
+ reviews frames before rendering, so prefer the separate verbs here.)
154
+
155
+ 4. **Tune `doc.json`** (JSON Schema ships in the `@vosjs/cli` npm package:
156
+ `schema/doc.schema.json`):
157
+ - `zoom`: `[{in, out, level, cx, cy, source}]`, SOURCE seconds; levels
158
+ 1.4–2.8; `cx/cy` NORMALIZED [0..1] (0.5,0.5 = center) — NOT pixels; set
159
+ `"source": "manual"` on spans you touch (survives re-plan).
160
+ - `segments` (trims) · `speed` (`rate` 0.1–16) · `frame.*` · `cursor`.
161
+ - `tilt`: `[{in, out, rx, ry, source}]`, SOURCE seconds — the 3D card
162
+ leans to the pose while active, returns to rest between. DEGREES
163
+ (±5..18 reads premium): +rx = top edge closer, +ry = left edge closer
164
+ (lean toward a right-side focus = negative `ry`). Spans ≥ 0.8s;
165
+ pair with zoom moments (same in/out chains the moves), one pose change
166
+ per ~5s beat. `"source": "manual"` on spans you touch;
167
+ `tiltStyle: "subtle"|"medium"|"strong"` records the auto wand.
168
+ - `frame.backgroundMedia`: a video loop / image behind the card —
169
+ `{"kind":"video","key":"/bg.webm","duration":10,"dim":0.2}`.
170
+ `key` = a file dropped in the take dir (`"/bg.webm"`) or a media URL;
171
+ video needs `duration` (OUTPUT-anchored modulo loop); `dim` 0..1 scrim.
172
+ Ambience, not a subject — dim it behind dense UI.
173
+ - `audio`: OUTPUT-anchored clips; `key` may be a file dropped into the
174
+ take dir (`"/music.mp3"`); gain/fades/loop. Muxed on full renders
175
+ (Opus/AAC); `--range` stays silent; forces single-flight.
176
+ - export: `{"resolution": "720p|1080p|2k|4k", "fps": 30}` — never above
177
+ the footage (validate warns).
178
+ Then `vos validate take --json` — lints must pass.
179
+
180
+ 5. **Look before you render** (the taste.md quality loop):
181
+ - `vos frames take --at-zooms --times 0,25%,50%,75%,100% --json` → judge
182
+ every still against taste.md, zoom apexes hardest.
183
+ - Iterate: edit doc.json → `vos render take check.webm --range a..b --draft`
184
+ (seconds, half res — never ship drafts) → re-frame the changed region.
185
+ - **Trying a presentation? Use a flag, not a scratch script.** `render`/`frames`
186
+ take doc overrides — `--set <path>=<value>` (repeatable; JSON-or-string),
187
+ `--frame <macos|windows|minimal|none>` (render), `--background <url>` — that
188
+ patch the doc in memory (doc.json untouched) and are lint-gated. So
189
+ `vos frames take --frame 2.0 --set frame.browserBar.kind=mac-light --set tilt[0].rx=8`
190
+ previews a framed, tilted card without touching the file.
191
+
192
+ 6. **Final render**: `vos render take out.webm --json` (or `--format mp4`).
193
+ Re-frame the final (`frames --at-zooms`) against taste.md before declaring
194
+ done. Renders are deterministic — only your edits change the output.
195
+
196
+ 6b. **Human review round** (when the ask involves one): `vos open take`
197
+ serves the take into the studio — your doc.json edits arrive intact and
198
+ every zoom span is draggable.
199
+
200
+ 7. **Package for the destination**: `references/destinations.md`.
201
+
202
+ ## Launch kit (one take → every store asset)
203
+
204
+ The `launch-kit` skill owns this destination: it establishes the release,
205
+ loops every channel against `channel-specs.json`, verifies each artifact
206
+ against its spec, writes the `kit.json` manifest and pushes labelled for
207
+ the release. The mechanics it loops are this skill's:
208
+ `vos frames take --frame <t> --size WxH` per still spec, the mp4 render for
209
+ video. Follow it when the ask is a release, not one video.
210
+
211
+ ## Gotchas
212
+
213
+ - A WebGL-heavy page (a shader background, a 3D canvas) paints BLACK under
214
+ headless Chromium's software GL, and the recording has no way to say so:
215
+ the take looks right except for a dead canvas. Record such pages with
216
+ `VOS_BROWSER_PATH` pointing at system Chrome (a real GPU), and check the
217
+ digest's sheet for the canvas before cutting.
218
+ - Render time ≈ 1.5× real-time at 1080p (a 12.5s take ≈ 19s; ~5s fixed
219
+ startup); `--parallel N` pays off on takes ≳30s (ignored when audio rides);
220
+ 2K ≈ 2× per-frame cost. Recording is always real-time.
221
+ - Footage resolution = viewport size — decide 2K at RECORD time, from the
222
+ DESTINATION's specs (a 720p take cannot honestly fill a 1080p video
223
+ spec). Coordinate steps (`x`/`y`/`drag`) are VIEWPORT pixels: a viewport
224
+ change means scaling every coordinate; selectors survive.
225
+ - `vos plan take` regenerates only `source:"auto"` spans; manual spans survive.
226
+ - Take dirs: `frames/` is a deletable encode intermediate (~1GB at 2K);
227
+ `recording.webm` is the re-render source — keep it.
228
+ - A take of a local app prints `localhost/…` in the browser bar. Set
229
+ `frame.browserBar.url` to the real address, or `frame.browserBar.showUrl`
230
+ to `false`, in `doc.json`; it is data, so no re-record.
231
+ - Started the app yourself to record it? Stop THAT process, by the PID you
232
+ saved or by its port (`lsof -ti :3000 -sTCP:LISTEN | xargs kill`). Never
233
+ `pkill -f "node server.js"` or any kill by pattern: it takes down every
234
+ matching process on the machine, the maker's other work included.
235
+ - A take that opens on the wrong page, with its first selector skipped,
236
+ is usually a missing or expired session, not a broken script: re-walk
237
+ `references/sessions.md` before touching `actions.json`.
238
+ - More failure modes: `references/troubleshooting.md`.
239
+
240
+
241
+ ## Avoid (the traps that shipped)
242
+
243
+ - A zoom that opens before the click it frames: `validate` names it ("points
244
+ beside what was clicked"); start the span after the click, or aim at it.
245
+ - A focus point in pixels: `cx`/`cy` are fractions of the frame.
246
+ - A `type` verb's field click as a zoom target: the field's centre is empty;
247
+ frame the text, and open the span after the click.
248
+ - Routing the cursor through a hover-triggered menu between beats.
249
+ - A first frame or a last frame that cannot stand alone as a poster.
250
+ - A frozen opening: a static landing page records as freeze-then-bang;
251
+ trim it or speed it, the story opens near the money shot.
252
+ - A store screenshot cut from the composed frame: real UX is the page, full
253
+ bleed (`deliver` does this; a text-heavy page also wants a store-size take).
254
+ - A sign-in in `steps`: every step there is in the footage and a typed
255
+ value is logged. It goes in `setup`, with the password from the shell
256
+ (`references/sessions.md`).
257
+ - A real customer's account on screen: addresses, names and keys ship in
258
+ the video. Record from a demo or seeded account, read the rehearsal's
259
+ `EXPOSED` list, and `mask` what is left.
260
+ - A `mask` with `as: "text"` over product copy or a number: that is no
261
+ longer a recording of the product.
262
+ - A `.png` name on `vos still`: it writes WebP; convert, and `vos validate
263
+ <kit.json>` reads the bytes.
@@ -0,0 +1,71 @@
1
+ # Destination packaging
2
+
3
+ What to produce, at what size, for each place a product video or still
4
+ lands. All assets come from ONE take — pick apex moments with
5
+ `vos frames take --at-zooms`, then cut stills with
6
+ `vos frames take --frame <t> --size WxH` and videos with render presets +
7
+ `--range`.
8
+
9
+ **Producing a full launch kit?** Use the `launch-kit` skill from this
10
+ repo — it drives the whole per-channel loop against the machine-readable
11
+ spec sheet (`schema/channel-specs.json` in `@vosjs/cli`) and verifies
12
+ every asset in a manifest. The tables below are the quick reference for
13
+ one-off cuts.
14
+
15
+ ## Landing-page embed (your site's hero or feature section)
16
+
17
+ - Record at 2560×1440 so a 2K render is footage-native; render webm.
18
+ - Re-encode for the page: a hero clip should be ≲2MB.
19
+ ```bash
20
+ ffmpeg -i in.webm -c:v libvpx-vp9 -crf 42 -b:v 0 -row-mt 1 -cpu-used 4 -an out.webm
21
+ ffmpeg -ss 0.4 -i out.webm -frames:v 1 -q:v 4 poster.jpg
22
+ ```
23
+ - Embed as a silent autoplay loop: `<video autoplay muted loop playsinline
24
+ poster=…>`. **React gotcha**: React does not serialize the `muted`
25
+ attribute into SSR HTML, so browsers deny autoplay — set `muted` on the
26
+ element imperatively (a ref) or verify the attribute survives to the
27
+ served HTML.
28
+ - Verify with a headless browser: `paused === false` and the expected
29
+ `videoWidth`. Keep media out of git; posters and clips belong on a CDN or
30
+ asset bucket.
31
+
32
+ ## GitHub README
33
+
34
+ - A README loop must be an MP4 **≤10MB** (GitHub's inline-player cap).
35
+ Two-pass target bitrate from duration, H.264 for compatibility:
36
+ ```bash
37
+ # bitrate ≈ (10MB × 8) / duration_s, minus ~128k audio if any
38
+ ffmpeg -i in.webm -c:v libx264 -b:v <target>k -pass 1 -an -f mp4 /dev/null
39
+ ffmpeg -i in.webm -c:v libx264 -b:v <target>k -pass 2 -movflags +faststart out.mp4
40
+ ```
41
+ - Repo social-preview image: 1280×640.
42
+
43
+ ## Store and directory listings
44
+
45
+ | Channel | Asset | Size | Notes |
46
+ |---|---|---|---|
47
+ | Chrome Web Store | screenshots (≤5) | 1280×800 | content only, no device chrome |
48
+ | Chrome Web Store | small promo tile | 440×280 | subject centered |
49
+ | Chrome Web Store | marquee promo | 1400×560 | |
50
+ | Chrome Web Store | icon | 128×128 | |
51
+ | Product Hunt | thumbnail | 240×240 | GIF loops autoplay in the feed |
52
+ | Product Hunt | gallery images | 1270×760 | first image is the header |
53
+
54
+ ## Social cuts
55
+
56
+ | Channel | Asset | Size | Notes |
57
+ |---|---|---|---|
58
+ | YouTube | thumbnail | 1280×720 | |
59
+ | X | feed video | 1200×675 (16:9) | ≤140s, H.264 — upload natively, never a link card |
60
+ | LinkedIn | feed video/image | 1200×627 | native upload; mute-legible |
61
+ | OG card (any link) | image | 1200×630 | |
62
+ | Vertical (Shorts/Reels/TikTok) | video | 1080×1920 (9:16) | keep the subject inside the ~900×1160 center safe zone — platform chrome covers the rest |
63
+
64
+ ## Performance rules (every channel)
65
+
66
+ - **Hook in 3 seconds** — the first frame and first beat carry the click.
67
+ - **Mute-legible** — most feeds autoplay silent; the story must read without
68
+ audio (zooms and text do the narration).
69
+ - **Native uploads** beat link embeds on every platform's algorithm.
70
+ - Platform specs drift — re-verify sizes quarterly against the channel's
71
+ current docs.
@@ -0,0 +1,226 @@
1
+ # Sessions: recording a product behind a login
2
+
3
+ A recorder with no session records the wall: the sign-in page, or wherever
4
+ the site sends a stranger (often a public page, with nothing on it that
5
+ looks like a sign-in), and the only symptom is a skipped selector. Settle
6
+ the session BEFORE you write the script.
7
+
8
+ Walk the ladder top to bottom and stop at the first rung that holds. It is
9
+ ordered by who pays. Rungs 0 to 2 cost the human nothing and survive every
10
+ re-record; rung 3 costs one sign-in; rung 4 costs a recording. Jumping to
11
+ rung 3 because it is the most general turns a loop that re-makes itself
12
+ into a chore someone has to show up for.
13
+
14
+ ## 0. No wall
15
+
16
+ A public page, a demo mode, a local dev server with auth off, a preview
17
+ deployment. If the feature shows the same there, record there. A preview
18
+ behind a bypass header alone (Vercel's `x-vercel-protection-bypass`) is
19
+ `vos record … --header x-vercel-protection-bypass=$TOKEN` (0.43 and later):
20
+ the token comes from the shell, never from the script.
21
+
22
+ ## 1. Mint, from the test auth the project already has
23
+
24
+ You are usually standing in the maker's repo, and its e2e suite very often
25
+ signs in with no human. Look before you ask anyone anything:
26
+
27
+ - `playwright/.auth/*.json`, an `auth.setup.ts`, a `storageState` in
28
+ `playwright.config.*`
29
+ - `@clerk/testing`; a Supabase service key in `.env.test`
30
+ (`auth.admin.generateLink`); a Firebase custom token
31
+ - a seed script, a test-only sign-in route, a session table plus a signing
32
+ secret in the dev env
33
+
34
+ Run what is there, WITH THE APP ALREADY RUNNING: a project's auth setup
35
+ signs in through the real page, so it needs the server up first (usually
36
+ `npx playwright test --project=setup`, or whatever the repo's README names).
37
+ The artifact is a Playwright storage state, which is exactly what
38
+ `--storage-state` takes:
39
+
40
+ ```bash
41
+ vos record --actions actions.json --out take --storage-state "$STATE" --dry-run
42
+ vos record --actions actions.json --out take --storage-state "$STATE" --strict --json
43
+ ```
44
+
45
+ This is the only rung that works in CI, and the only one that survives take
46
+ fifty. A Firebase session lives in IndexedDB, which a plain state file
47
+ drops: save it with `context.storageState({ path, indexedDB: true })`
48
+ (Playwright 1.51 and later).
49
+
50
+ ## 2. Script the form, off camera
51
+
52
+ A local or self-hosted instance where you can create the account, or a
53
+ seeded user whose password is in an env var. Put the sign-in in `setup`
54
+ in `actions.json` (`@vosjs/cli` 0.43 and later): it runs after the first
55
+ navigation and BEFORE a frame is captured, with no cursor, no frames and
56
+ nothing in `meta.steps`, then the recorder opens `url` again and the take
57
+ begins signed in. No state file, nothing to mint, nothing to delete.
58
+
59
+ ```json
60
+ {
61
+ "url": "http://localhost:3000/dashboard",
62
+ "setup": [
63
+ { "do": "goto", "url": "http://localhost:3000/login" },
64
+ { "do": "type", "selector": "#email", "text": "demo@acme.test" },
65
+ { "do": "type", "selector": "#password", "text": { "env": "DEMO_PASSWORD" } },
66
+ { "do": "press", "key": "Enter", "ms": 800 }
67
+ ],
68
+ "steps": [ ... ]
69
+ }
70
+ ```
71
+
72
+ The password comes from the SHELL at run time (`{ "env": "NAME" }`) and is
73
+ never logged or stored: the log names the field, never the value.
74
+ `validate` refuses a literal typed into a password field, because
75
+ `actions.json` is committed and pushed with the take. Export the variable
76
+ in the shell that runs `vos record`; an unset one exits 2 in words. A
77
+ setup selector that never appears fails the take before anything is
78
+ recorded, so rehearse the setup with `--dry-run` like everything else. A
79
+ wrong password runs the setup and then meets the wall (exit 4), which is
80
+ the check working.
81
+
82
+ The same field dismisses a cookie banner, a "choose your editor" modal or
83
+ an onboarding tour off camera: a `click` on the dismiss, before the take.
84
+
85
+ Never put the sign-in in `steps`: every step there is IN the footage, and
86
+ a typed value is logged.
87
+
88
+ On an older CLI, or for an account you are creating: a few lines of
89
+ Playwright, then record with `--storage-state`. `playwright` is already
90
+ installed (it arrives with `@vosjs/cli`), launch the SYSTEM Chrome
91
+ (`channel: 'chrome'`), and run the script from INSIDE the project so the
92
+ import resolves; only the STATE FILE lives outside the repo.
93
+
94
+ ```js
95
+ import { chromium } from 'playwright'
96
+ const browser = await chromium.launch({ channel: 'chrome' })
97
+ const context = await browser.newContext()
98
+ const page = await context.newPage()
99
+ await page.goto('http://localhost:3000/login')
100
+ await page.fill('input[name=email]', 'demo@acme.test')
101
+ await page.fill('input[name=password]', process.env.DEMO_PASSWORD) // or, for an account you are creating, a random throwaway you never print
102
+ await page.click('button[type=submit]')
103
+ await page.waitForURL('**/dashboard')
104
+ await context.storageState({ path: process.env.STATE })
105
+ await browser.close()
106
+ ```
107
+
108
+ ## 3. The human signs in once
109
+
110
+ A production app behind an emailed code, SSO, a passkey or a CAPTCHA:
111
+
112
+ ```bash
113
+ vos session open https://app.example.com --name acme
114
+ ```
115
+
116
+ A plain Chrome window opens on a profile vos owns (`@vosjs/cli` 0.45 and
117
+ later). Tell the human one sentence: a browser window opened, sign in with
118
+ a demo account and quit Chrome (⌘Q on a Mac; closing the window is not
119
+ quitting, Chrome stays running and the command keeps waiting). The
120
+ command returns when Chrome exits, and that is the moment the session is
121
+ saved; "I signed in" and "the session is saved" are different things, and
122
+ a person reports the first.
123
+ It then prints what the session holds as counts and dates, never a value.
124
+ Then:
125
+
126
+ ```bash
127
+ vos session check acme --url https://app.example.com/dashboard # still opens signed in? exit 0, or 4
128
+ vos record --actions actions.json --out take --session acme --dry-run
129
+ vos record --actions actions.json --out take --session acme --strict --json
130
+ ```
131
+
132
+ `--session` and `--storage-state` are two doors to one take; pass one.
133
+ No file to mint, nothing in the take, nothing to delete: the profile lives
134
+ under `~/.config/vos/sessions/` and `vos push` refuses a take that holds a
135
+ state file. A re-record that exits 4 is the session expired: `vos session
136
+ check` says so and prints the `open` command to run again.
137
+
138
+ Google sign-in refuses an automated browser, which is why `open` is a
139
+ plain window: it goes through there. If the person cannot be at the
140
+ keyboard now, go to rung 4; do not wait on a window nobody will close.
141
+
142
+ On an older CLI: `npx playwright open --channel chrome
143
+ --save-storage="$STATE" <url>` writes a state file when the window closes,
144
+ for `--storage-state`. `--channel chrome` uses the system Chrome; without
145
+ it the command wants Playwright's own Chromium, which is usually not
146
+ installed.
147
+
148
+ **The human is not there right now?** Do not open a window nobody will see
149
+ and do not block on it. Get everything else ready (the script written and
150
+ validated, a rehearsal that exits 4 to prove the wall is the only thing
151
+ left), then STOP and leave the ask in the words you would say: the one
152
+ `vos session open` command, "sign in with a demo account and quit
153
+ Chrome", and the record command that follows. Ask, in the same note,
154
+ whether there is a faster way in you cannot see (a seeded account, a test
155
+ sign-in route): that turns the next re-record into rung 1.
156
+
157
+ ## 4. The human records, you cut
158
+
159
+ Hand them the flow you worked out, as a shot list. Write `actions.json`
160
+ as you would for any take, give the steps ids and captions a person could
161
+ follow, then:
162
+
163
+ ```bash
164
+ vos actions script actions.json
165
+ ```
166
+
167
+ It prints the beats in plain words with the holds you asked for, the page
168
+ to start on and about how long (`@vosjs/cli` 0.44 and later). Put that in
169
+ your handoff with one sentence: record it with the vosso extension in your
170
+ own signed-in browser, press the icon again to stop, and it lands on your
171
+ shelf. When it does, `vos pull <vos-id> --out take --media` brings it down
172
+ and you cut it (the `vos-cut` skill): the beats you wrote are the moments
173
+ you will be looking for in the digest. This is a rung, not a failure. "I
174
+ cannot get in; here is the shot list, record it and I will cut it" is the
175
+ honest best thing, and the script survives for the day rung 1 or 2 opens.
176
+
177
+ A recording a person made has no `mask` and no exposure list (the scan
178
+ needs the page): look at the digest's frames yourself and say what they
179
+ show, before anything is pushed further or handed over.
180
+
181
+ ## Rules, at every rung
182
+
183
+ - **Never type, ask for, or accept a production password, code or token.**
184
+ If a human offers one in chat, decline and use rung 3.
185
+ - **The state file holds live credentials.** Keep it outside every take
186
+ directory and outside git: a temp dir, or the gitignored path the project
187
+ already uses. `vos push` uploads the recording and `doc.json`, never a
188
+ state file, and nothing about a session ever goes to vos.so.
189
+ - **Delete the state file when the video is done**, unless the project
190
+ keeps one on purpose (a gitignored `playwright/.auth`). It is cheap to
191
+ mint again and it is a live credential for as long as it sits there.
192
+ - **A session expires.** When a re-record that worked last week skips its
193
+ first selector, re-walk the ladder before touching the script.
194
+ - **A person signing in will use their REAL account**, whatever you asked
195
+ for: it is the one they have. The recorder looks for you (`@vosjs/cli`
196
+ 0.42 and later): the rehearsal ends with `EXPOSED in the frame`, naming
197
+ the KIND and the place of what it saw (an email address, something shaped
198
+ like a key, a card number or its visible tail; addresses on `example.com`
199
+ or a `.test` domain are demo data and are not reported). Read that list
200
+ BEFORE you record. It also lands in the done event's `exposures`, in
201
+ `vos validate <take>` and in the digest.
202
+ - **Hide it before the camera rolls, with `mask` in `actions.json`.** The
203
+ selector in each report reaches that element and no other, so paste it:
204
+ ```json
205
+ "mask": [
206
+ { "selector": "nav > span", "as": "text", "text": "jane@acme.test" },
207
+ { "selector": ".card-number" }
208
+ ]
209
+ ```
210
+ `as: "text"` swaps the words, which reads as a product where a blur reads
211
+ as a redaction; the default blurs. It is applied before the first frame
212
+ and re-applied after every navigation and re-render, so the real value is
213
+ never in the recording. Use `text` for IDENTIFIERS only (an email, a
214
+ name, an account id). NEVER substitute product copy or a number: the
215
+ video stays true to the product, and that judgment is yours, no check
216
+ makes it for you. Rehearse again: the list should be empty, and a mask
217
+ that reached nothing fails the rehearsal by name.
218
+ - A recording a HUMAN made (the last rung) has no mask: the scan needs the
219
+ page. Look at the frames yourself and say what they show.
220
+ - The list is a floor, not a verdict. It reads text: a face, a logo, a
221
+ customer's name in a table, a private chart are yours to notice. Offer
222
+ the re-record from a demo account; do not decide for them.
223
+ - **What the account shows ships in the video.** Use a demo or seeded
224
+ account, never a real customer's. Before you push, look at a frame for
225
+ email addresses, names, keys and card numbers, and re-record from an
226
+ account that does not show them.