@vosjs/cli 0.42.0 → 0.43.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 CHANGED
@@ -80,7 +80,7 @@ vos plan take --reuse # re-time that cut onto
80
80
 
81
81
  | Verb | Flags |
82
82
  | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
- | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--allow-wall` `--keep-frames` `--storage-state <file>` `--browser-arg=<switch>`... `--max-duration <s>` `--background <slug\|url\|none>` |
83
+ | `record` | `--actions <file>` (or positional) `--url` `--out take` `--strict` `--dry-run` `--allow-wall` `--keep-frames` `--storage-state <file>` `--header name=value`... `--browser-arg=<switch>`... `--max-duration <s>` `--background <slug\|url\|none>` |
84
84
  | `create` | The `record` flags plus the render flags (`--width` `--height` `--fps` `--format` `--parallel` `--draft` `--frame` `--set`), no `--range`. With `--strict` an incomplete recording exits 2 before anything is rendered |
85
85
  | `plan` | `--fresh` (discard the current plan) `--reuse` `--from <doc.json>` (defaults to `<take>/doc.prev.json`) `--style <doc.json\|take\|vosId>` `--with <doc.json\|take\|vosId>[@end\|@start\|@step:<id>\|@<seconds>]` (a template, repeatable) `--background` `--motion` (re-propose the motion) `--headline` `--kicker` `--launch` `--brand` `--music` `--entrance` `--transitions slide\|fade\|scale\|none` `--end-card on\|none\|<ref>` `--captions` `--clicks` `--release` |
86
86
  | `digest` | `--out <take>/digest` `--full 960` `--crop 640` (image long edges, the token budget) `--no-frames` `--transcript <file>` (Whisper-shaped segments merged as `said`) `--style <ref>` (report a reference document's style fields) |
@@ -90,6 +90,19 @@ vos plan take --reuse # re-time that cut onto
90
90
 
91
91
  **The wall check.** Once the first navigation settles, and before a frame is captured, `record`, `create` and `--dry-run` ask whether the recorder landed where it was sent. A take that met a sign-in instead is refused with exit 4 and a sentence (`asked for /dashboard, landed on /login: no session for app.acme.com`): the asked URL answered 401 or 403, the recorder was sent to an identity provider or a sign-in path, or the page is a sign-in form (one password field, or a one-time-code field) rendered in place. A redirect somewhere else with no sign-in in sight, which is what a site that shows strangers a public page looks like, is refused under `--strict` and in a rehearsal, and said as a warning otherwise. The check runs before a re-record clears anything, so a session that expired since the last take never costs the footage it failed to replace. The way past a wall is a session: `--storage-state <file>`, minted from the test auth the project already has wherever that exists. `--allow-wall` records the page anyway (a video OF a sign-in page is a legitimate take), and the take's `meta.wall` and its digest then say so.
92
92
 
93
+ **`setup`: the steps that run before the camera rolls.** A sign-in form, a cookie banner, the "choose your editor" modal, an onboarding tour: things a take must get past and must not show. `actions.json` takes `setup: [...]` beside `steps`, with the verbs `goto`, `click`, `type`, `press`, `wait`. They run after the first navigation and before a frame is captured, as plain actions with no cursor, no frames, no pace and nothing in `meta.steps`; then the recorder opens `url` again and the take begins where the setup left it. A `type` step's `text` may be `{ "env": "DEMO_PASSWORD" }`, read from the shell at run time and never logged or stored (the log names the field, never the value; a literal typed into a password field is refused by `validate`, because `actions.json` is committed and pushed with the take). A selector that never appears fails the take before anything is recorded (exit 2), because a take that begins at a half-finished sign-in is the wall by another name; rehearse the setup with `--dry-run` like everything else. This is rung 2 of the session ladder made scriptable: a local or self-hosted instance with a seeded user, no state file needed.
94
+
95
+ ```json
96
+ "setup": [
97
+ { "do": "goto", "url": "http://localhost:3000/login" },
98
+ { "do": "type", "selector": "#email", "text": "demo@acme.test" },
99
+ { "do": "type", "selector": "#password", "text": { "env": "DEMO_PASSWORD" } },
100
+ { "do": "press", "key": "Enter", "ms": 800 }
101
+ ]
102
+ ```
103
+
104
+ **`--header name=value`**, repeatable: a request header on every request the recording browser makes, the way past a preview deployment protected by a bypass header alone (`--header x-vercel-protection-bypass=$TOKEN`). The rehearsal's `Next:` line carries it.
105
+
93
106
  **What the frame shows.** Getting past a login puts the account's own data in the picture, and asking for a demo account does not hold that line: a person asked to sign in signs in as themselves. So the recorder looks. After the page opens and after every step it reads the text visible in the viewport and reports the KIND of thing it saw and where, never the string: an email address (one on `example.com` or a `.test`, `.example`, `.invalid` or `.localhost` domain is demo data and is not reported), something shaped like an API key or a JWT, a card number that passes the Luhn check, a masked card's visible tail. They land in `meta.exposures[]` (`step`, `kind`, `selector`, `rect`, `seen`), in the `record` and `create` done events, at the end of a rehearsal (before anything is recorded), as warnings in `vos validate <take>`, and in the digest's `take.exposures`. They warn; they do not fail a take, because a product may legitimately show addresses. The `selector` reaches that element and no other, so it can be pasted into a mask.
94
107
 
95
108
  **`mask`** in `actions.json` hides a selector BEFORE the first frame is captured and keeps it hidden across navigations and re-renders, so the real value is never in a frame, never in the recording, never pushed:
@@ -101,8 +101,137 @@ function createReporter(json) {
101
101
  };
102
102
  }
103
103
 
104
+ // src/plugin/setup.ts
105
+ var VERBS = /* @__PURE__ */ new Set(["wait", "click", "type", "press", "goto"]);
106
+ function validateSetup(value) {
107
+ const errors = [];
108
+ if (!Array.isArray(value)) return ["setup must be an array of steps"];
109
+ value.forEach((raw, i) => {
110
+ const at2 = `setup[${i}]`;
111
+ if (typeof raw !== "object" || raw === null) {
112
+ errors.push(`${at2}: must be an object`);
113
+ return;
114
+ }
115
+ const s = raw;
116
+ if (typeof s.do !== "string" || !VERBS.has(s.do)) {
117
+ errors.push(`${at2}: "do" must be one of ${[...VERBS].join(", ")}`);
118
+ return;
119
+ }
120
+ if ((s.do === "click" || s.do === "type") && typeof s.selector !== "string")
121
+ errors.push(`${at2}: ${s.do} needs a selector`);
122
+ if (s.do === "wait" && typeof s.ms !== "number")
123
+ errors.push(`${at2}: wait needs ms`);
124
+ if (s.do === "press" && typeof s.key !== "string")
125
+ errors.push(`${at2}: press needs a key`);
126
+ if (s.do === "goto" && typeof s.url !== "string")
127
+ errors.push(`${at2}: goto needs a url`);
128
+ if (s.do === "type") {
129
+ const t = s.text;
130
+ const isEnv = typeof t === "object" && t !== null && typeof t.env === "string" && Object.keys(t).length === 1;
131
+ if (typeof t !== "string" && !isEnv)
132
+ errors.push(
133
+ `${at2}: type needs text, a string or { "env": "NAME" } read at run time`
134
+ );
135
+ if (typeof t === "string" && /passw|secret|token/i.test(String(s.selector)) && t.length > 0)
136
+ errors.push(
137
+ `${at2}: a literal value typed into ${String(s.selector)} would live in actions.json, which is committed and pushed. Use { "env": "NAME" }`
138
+ );
139
+ }
140
+ });
141
+ return errors;
142
+ }
143
+ function resolveSetupText(t, env = process.env) {
144
+ if (typeof t === "string") return { value: t, secret: false };
145
+ const v = env[t.env];
146
+ if (v === void 0 || v === "")
147
+ throw new SetupEnvError(
148
+ `setup: the environment variable ${t.env} is not set, and a type step reads it. Export it in the shell that runs vos record; it is never written to a file`
149
+ );
150
+ return { value: v, secret: true };
151
+ }
152
+ async function runSetup(page, steps, opts) {
153
+ const lines = [];
154
+ for (const [i, step] of steps.entries()) {
155
+ const settle = "ms" in step && typeof step.ms === "number" ? step.ms : 150;
156
+ try {
157
+ switch (step.do) {
158
+ case "wait":
159
+ await opts.sleep(step.ms);
160
+ lines.push(`setup #${i} wait ${step.ms}ms`);
161
+ break;
162
+ case "goto":
163
+ await page.goto(step.url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
164
+ });
165
+ lines.push(`setup #${i} goto ${step.url}`);
166
+ break;
167
+ case "press":
168
+ await page.keyboard.press(step.key);
169
+ await opts.sleep(settle);
170
+ lines.push(`setup #${i} press ${step.key}`);
171
+ break;
172
+ case "click": {
173
+ const loc = page.locator(step.selector).first();
174
+ await loc.waitFor({ state: "visible", timeout: 8e3 });
175
+ await loc.click();
176
+ await opts.sleep(settle);
177
+ lines.push(`setup #${i} click ${step.selector}`);
178
+ break;
179
+ }
180
+ case "type": {
181
+ const loc = page.locator(step.selector).first();
182
+ await loc.waitFor({ state: "visible", timeout: 8e3 });
183
+ const { value, secret } = resolveSetupText(step.text, opts.env);
184
+ await loc.fill(value);
185
+ await opts.sleep(settle);
186
+ lines.push(
187
+ `setup #${i} type ${secret ? `\${${step.text.env}}` : `${value.length} chars`} into ${step.selector}`
188
+ );
189
+ break;
190
+ }
191
+ }
192
+ } catch (e) {
193
+ if (e instanceof SetupEnvError) throw e;
194
+ return {
195
+ ran: i,
196
+ lines,
197
+ failed: {
198
+ step: i,
199
+ do: step.do,
200
+ ..."selector" in step ? { selector: step.selector } : {}
201
+ }
202
+ };
203
+ }
204
+ }
205
+ return { ran: steps.length, lines };
206
+ }
207
+ var SetupEnvError = class extends Error {
208
+ constructor(message) {
209
+ super(message);
210
+ this.name = "SetupEnvError";
211
+ }
212
+ };
213
+ var SetupError = class extends Error {
214
+ constructor(failed2) {
215
+ super(
216
+ `setup #${failed2.step} ${failed2.do}${failed2.selector ? ` ${failed2.selector}` : ""}: the selector never appeared, so the take would begin at a half-finished setup. Nothing was recorded. Fix the setup step (rehearse with --dry-run), then record.`
217
+ );
218
+ this.failed = failed2;
219
+ this.name = "SetupError";
220
+ }
221
+ failed;
222
+ };
223
+ function parseHeaders(given) {
224
+ const out = {};
225
+ for (const h of given ?? []) {
226
+ const i = h.indexOf("=");
227
+ if (i <= 0) throw new Error(`--header wants name=value, got "${h}"`);
228
+ out[h.slice(0, i).trim()] = h.slice(i + 1);
229
+ }
230
+ return out;
231
+ }
232
+
104
233
  // src/plugin/actions.ts
105
- var VERBS = /* @__PURE__ */ new Set([
234
+ var VERBS2 = /* @__PURE__ */ new Set([
106
235
  "wait",
107
236
  "hover",
108
237
  "click",
@@ -129,6 +258,7 @@ function validateActions(value) {
129
258
  }
130
259
  }
131
260
  }
261
+ if (obj.setup !== void 0) errors.push(...validateSetup(obj.setup));
132
262
  if (obj.mask !== void 0) {
133
263
  if (!Array.isArray(obj.mask)) {
134
264
  errors.push("mask must be an array of { selector, as?, text? }");
@@ -159,8 +289,8 @@ function validateActions(value) {
159
289
  return;
160
290
  }
161
291
  const s = raw;
162
- if (typeof s.do !== "string" || !VERBS.has(s.do)) {
163
- errors.push(`${at2}: "do" must be one of ${[...VERBS].join(", ")}`);
292
+ if (typeof s.do !== "string" || !VERBS2.has(s.do)) {
293
+ errors.push(`${at2}: "do" must be one of ${[...VERBS2].join(", ")}`);
164
294
  return;
165
295
  }
166
296
  if (s.id !== void 0) {
@@ -6994,7 +7124,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6994
7124
  const context = await browser.newContext({
6995
7125
  viewport: { width: vw, height: vh },
6996
7126
  deviceScaleFactor: 1,
6997
- ...opts.storageState ? { storageState: opts.storageState } : {}
7127
+ ...opts.storageState ? { storageState: opts.storageState } : {},
7128
+ ...opts.headers && Object.keys(opts.headers).length ? { extraHTTPHeaders: opts.headers } : {}
6998
7129
  });
6999
7130
  const masks = actions.mask ?? [];
7000
7131
  if (masks.length) await context.addInitScript(maskInitScript(masks));
@@ -7009,12 +7140,26 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
7009
7140
  });
7010
7141
  log(`goto ${url}`);
7011
7142
  let navTimeout = false;
7012
- const response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
7143
+ let response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
7013
7144
  navTimeout = true;
7014
7145
  log(" (networkidle timeout \u2014 continuing)");
7015
7146
  return null;
7016
7147
  });
7017
7148
  await sleep(800);
7149
+ const setup = actions.setup ?? [];
7150
+ if (setup.length) {
7151
+ log(`setup: ${setup.length} step(s), off camera`);
7152
+ const result = await runSetup(page, setup, { sleep: realSleep });
7153
+ for (const line of result.lines) log(` ${line}`);
7154
+ if (result.failed) {
7155
+ await context.close().catch(() => {
7156
+ });
7157
+ throw new SetupError(result.failed);
7158
+ }
7159
+ log(`goto ${url} (after setup)`);
7160
+ response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => null);
7161
+ await sleep(800);
7162
+ }
7018
7163
  let wall = null;
7019
7164
  if (opts.onArrival) {
7020
7165
  const seen = await page.evaluate(WALL_PROBE).catch(() => null);
@@ -10518,12 +10663,12 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
10518
10663
  "dry-run",
10519
10664
  "allow-wall"
10520
10665
  ]);
10521
- var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg"]);
10666
+ var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg", "header"]);
10522
10667
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
10523
10668
 
10524
10669
  Take pipeline
10525
- vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10526
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10670
+ vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10671
+ vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--allow-wall] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--header name=value]... [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
10527
10672
  vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
10528
10673
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
10529
10674
  vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
@@ -10816,6 +10961,13 @@ function exposureNote(rec) {
10816
10961
  ${rec.exposures.map((e) => ` - ${exposureLine(e)}`).join("\n")}
10817
10962
  ${EXPOSURE_ADVICE}`;
10818
10963
  }
10964
+ function takeHeaders(multi) {
10965
+ try {
10966
+ return parseHeaders(multi.header);
10967
+ } catch (e) {
10968
+ throw new UsageError(e instanceof Error ? e.message : String(e));
10969
+ }
10970
+ }
10819
10971
  function takeBrowserArgs(multi) {
10820
10972
  const given = multi["browser-arg"];
10821
10973
  return (given ?? []).filter(Boolean);
@@ -10886,6 +11038,7 @@ async function cmdRecord(argv) {
10886
11038
  const maxDurationSeconds = await maxDuration(flags, r);
10887
11039
  const storageState = takeStorageState(flags);
10888
11040
  const browserArgs = takeBrowserArgs(multi);
11041
+ const headers = takeHeaders(multi);
10889
11042
  if (flags["dry-run"] === true) {
10890
11043
  void flags.strict;
10891
11044
  void flags["keep-frames"];
@@ -10902,6 +11055,7 @@ async function cmdRecord(argv) {
10902
11055
  r.log,
10903
11056
  {
10904
11057
  storageState,
11058
+ headers,
10905
11059
  dryRun: true,
10906
11060
  // A rehearsal is always strict, and it touches no directory.
10907
11061
  onArrival: wallGate(flags, r, { strict: true })
@@ -10917,6 +11071,7 @@ async function cmdRecord(argv) {
10917
11071
  const carried = [
10918
11072
  ...strFlag(flags, "storage-state") ? [`--storage-state ${strFlag(flags, "storage-state")}`] : [],
10919
11073
  ...browserArgs.map((a) => `--browser-arg=${a}`),
11074
+ ...Object.entries(headers).map(([k, v]) => `--header ${k}=${v}`),
10920
11075
  ...flags["allow-wall"] === true ? ["--allow-wall"] : []
10921
11076
  ].join(" ");
10922
11077
  const nextOut = strFlag(flags, "out") ?? "take";
@@ -10958,6 +11113,7 @@ ${lines.join("\n")}
10958
11113
  const rec = await recordTake(browser, url, actions, paths, r.log, {
10959
11114
  maxDurationSeconds,
10960
11115
  storageState,
11116
+ headers,
10961
11117
  onArrival: wallGate(flags, r, {
10962
11118
  strict: flags.strict === true,
10963
11119
  prepare
@@ -11038,6 +11194,7 @@ async function cmdCreate2(argv) {
11038
11194
  const maxDurationSeconds = await maxDuration(flags, r);
11039
11195
  const storageState = takeStorageState(flags);
11040
11196
  const browserArgs = takeBrowserArgs(multi);
11197
+ const headers = takeHeaders(multi);
11041
11198
  const paths = takePaths(outDir);
11042
11199
  const prepare = async () => {
11043
11200
  if (existsSync15(join18(outDir, "meta.json"))) {
@@ -11056,6 +11213,7 @@ async function cmdCreate2(argv) {
11056
11213
  const rec = await recordTake(browser, url, actions, paths, r.log, {
11057
11214
  maxDurationSeconds,
11058
11215
  storageState,
11216
+ headers,
11059
11217
  onArrival: wallGate(flags, r, {
11060
11218
  strict: flags.strict === true,
11061
11219
  prepare
@@ -12196,6 +12354,11 @@ async function run(argv) {
12196
12354
  } catch (e) {
12197
12355
  if (e instanceof UsageError) {
12198
12356
  process.stderr.write(`usage error: ${e.message}
12357
+ `);
12358
+ return EXIT_USAGE;
12359
+ }
12360
+ if (e instanceof SetupError || e instanceof SetupEnvError) {
12361
+ process.stderr.write(`${e.message}
12199
12362
  `);
12200
12363
  return EXIT_USAGE;
12201
12364
  }
@@ -12248,4 +12411,4 @@ export {
12248
12411
  verbHelp,
12249
12412
  run
12250
12413
  };
12251
- //# sourceMappingURL=chunk-7RMULTDM.js.map
12414
+ //# sourceMappingURL=chunk-6PJPGBKH.js.map