@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.
- package/README.md +15 -5
- package/dist/{chunk-E42NCNWP.js → chunk-ANKECV7F.js} +634 -56
- package/dist/chunk-ANKECV7F.js.map +1 -0
- package/dist/{chunk-EFI3WUQB.js → chunk-P3X5KVA6.js} +15 -2
- package/dist/{chunk-EFI3WUQB.js.map → chunk-P3X5KVA6.js.map} +1 -1
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/manifest-A4R367EM.js +8 -0
- package/dist/{run-DA5QXZ6F.js → run-CYHH56KA.js} +2 -2
- package/package.json +8 -6
- package/skills/VERSION +1 -0
- package/skills/launch-kit/SKILL.md +356 -0
- package/skills/launch-kit/references/channel-specs.md +56 -0
- package/skills/product-video/SKILL.md +263 -0
- package/skills/product-video/references/destinations.md +71 -0
- package/skills/product-video/references/sessions.md +226 -0
- package/skills/product-video/references/taste.md +115 -0
- package/skills/product-video/references/troubleshooting.md +116 -0
- package/skills/vos-authoring/SKILL.md +191 -0
- package/skills/vos-authoring/references/examples.md +239 -0
- package/skills/vos-authoring/references/schema-reference.md +468 -0
- package/skills/vos-create/SKILL.md +258 -0
- package/skills/vos-cut/SKILL.md +241 -0
- package/skills/vos-footage/SKILL.md +98 -0
- package/skills/vos-migrate/SKILL.md +108 -0
- package/skills/vos-remix/SKILL.md +136 -0
- package/skills/vos-remix/references/3d-recipe.md +37 -0
- package/skills/vos-remix/references/params-knobs.md +80 -0
- package/skills/vos-remix/references/remix-contract.md +81 -0
- package/dist/chunk-E42NCNWP.js.map +0 -1
- package/dist/manifest-UCS6OVWZ.js +0 -8
- /package/dist/{manifest-UCS6OVWZ.js.map → manifest-A4R367EM.js.map} +0 -0
- /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.
|