@vosjs/cli 0.39.0 → 0.41.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 +8 -5
- package/dist/{chunk-RECQEBXN.js → chunk-BD4FXBUO.js} +250 -29
- package/dist/chunk-BD4FXBUO.js.map +1 -0
- package/dist/cli.js +15 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +57 -0
- package/dist/index.js +1 -1
- package/dist/{run-BTBEJLWL.js → run-5PCYT25T.js} +2 -2
- package/package.json +6 -6
- package/schema/digest.schema.json +13 -1
- package/dist/chunk-RECQEBXN.js.map +0 -1
- /package/dist/{run-BTBEJLWL.js.map → run-5PCYT25T.js.map} +0 -0
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` `--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>` `--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) |
|
|
@@ -88,6 +88,8 @@ vos plan take --reuse # re-time that cut onto
|
|
|
88
88
|
| `render` | `--width` `--height` `--fps` `--format webm\|mp4` `--parallel N` (1..16 sessions) `--range a..b` (output seconds; keeps its audio) `--draft` `--frame <kind>` `--background` `--set …`; `out` defaults to `<take>/out.<format>` |
|
|
89
89
|
| `open` | `--studio http://localhost:6060` `--print` (print the URL, do not launch a browser) |
|
|
90
90
|
|
|
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
|
+
|
|
91
93
|
**Rehearse before you record.** `vos record … --dry-run` runs every step against the real page, in order, because a later selector usually exists only after an earlier click. Nothing is captured and nothing is written: the pointer lands instead of travelling, every pause is cut to a beat, and the take directory beside it keeps its footage, its cut and its script exactly as they were (a real re-record moves `doc.json` aside; a rehearsal does not). Selector lookups keep their whole timeout, so a miss here is a miss in the take. It prints each step with the rect it resolved, in capture px, which are the rects a pin or `vos callout --step` reads, and exits 2 on any miss or a first load that never settled. Add `--dry-run` to the exact command you were about to run; `--storage-state` and `--browser-arg=` apply to it too.
|
|
92
94
|
|
|
93
95
|
**Digest first.** `vos digest <take>` is how an agent sees a recording without reading the video. It writes `digest/digest.json`: one moment per thing the cursor track says mattered (click clusters, typing sessions, scroll runs, dwells, idle gaps, head, tail, and frame-diff scene changes), each with source and output extents, a normalized `focus` and `rect` you can copy into a zoom span, per-second `activity`, and the planners' `proposed` span ids; plus one footage frame and a crop around the target per moment, and `sheet.png`, the contact sheet. Read the JSON, then the sheet, then a crop only where you must decide. `vos validate` then warns when a zoom does not contain what was clicked under it, and `vos frames --at-moments` renders the composed output at every moment so a still and its footage crop share an id.
|
|
@@ -217,8 +219,9 @@ Everything that runs on your machine is open source; [vos.so](https://vos.so) is
|
|
|
217
219
|
vos login [--key <k>] [--label <name>] [--no-browser] # browser sign-in: a code and a vos.so/cli/auth URL, a human approves, the key stores itself
|
|
218
220
|
vos fetch <vosId|url> [--out <slug>] [--media] # a program writes config.json + vos.json; a take writes doc.json (+ the recording with --media)
|
|
219
221
|
vos check bright-loop/config.json # full local validation
|
|
220
|
-
vos push bright-loop/config.json # create a PRIVATE vos (
|
|
221
|
-
vos push bright-loop/config.json --vos <id> # add a version
|
|
222
|
+
vos push bright-loop/config.json # untracked: create a PRIVATE vos. Tracked (vos.json): add a version to it
|
|
223
|
+
vos push bright-loop/config.json --vos <id> # add a version to a vos this directory does not track
|
|
224
|
+
vos push bright-loop/config.json --remix-of <id> # create a SEPARATE vos from this config, with that lineage
|
|
222
225
|
vos push bright-loop/config.json --claimable # no credential: a 72 h claim link instead (programs only)
|
|
223
226
|
vos push take --yes --label "first pass" --note "…" # host a take: private vos + version history (the recording uploads once)
|
|
224
227
|
vos pull bright-loop [--since <versionId>] [--check] # what changed on vos.so since your base; syncs config.json (backup kept), or doc.json for a take
|
|
@@ -229,7 +232,7 @@ vos asset push <file…> [--folder <slug>] | rename <id> <name.ext>
|
|
|
229
232
|
vos recipe push <FILE.md> --folder <slug> | --asset <id> # the one recipe write: create, or replace in place
|
|
230
233
|
```
|
|
231
234
|
|
|
232
|
-
`push` is polymorphic by a deterministic sniff, never a flag: a take directory (a `doc.json` carrying `source`) pushes recording and document through the take pipeline; a `config.json` (or a directory holding one) pushes the program, and a `doc.json` beside it that carries `program` (a program document: overlays, objects, audio, speed, tween edits, its own length; `program.config` omitted on disk) rides along, lint-gated. `fetch` and `pull` write a program document back the same way. `pull` takes `--since <versionId>` (walk the changelog from that base instead of the tracked one) and `--check` (print what changed and stop: nothing on disk moves) on both paths. Program pushes take `--vos`, `--title`, `--slug`, `--desc`, `--tags`, `--folder`, `--remix-of`, `--base`, `--label`, `--note`, `--override <id>` (repeatable) and `--claimable`; take pushes take `--title`, `--label`, `--note`, `--folder`, `--override` and `--yes`.
|
|
235
|
+
`push` is polymorphic by a deterministic sniff, never a flag: a take directory (a `doc.json` carrying `source`) pushes recording and document through the take pipeline; a `config.json` (or a directory holding one) pushes the program, and a `doc.json` beside it that carries `program` (a program document: overlays, objects, audio, speed, tween edits, its own length; `program.config` omitted on disk) rides along, lint-gated. `fetch` and `pull` write a program document back the same way. `pull` takes `--since <versionId>` (walk the changelog from that base instead of the tracked one) and `--check` (print what changed and stop: nothing on disk moves) on both paths. A program push LANDS where `vos.json` says: a directory that tracks a vos iterates it, so the fetch, edit, check, push, pull loop needs no flags, and making something separate from the same config is the explicit `--remix-of` door. Program pushes take `--vos`, `--title`, `--slug`, `--desc`, `--tags`, `--folder`, `--remix-of`, `--base`, `--label`, `--note`, `--override <id>` (repeatable) and `--claimable`; take pushes take `--title`, `--label`, `--note`, `--folder`, `--override` and `--yes`.
|
|
233
236
|
|
|
234
237
|
Both paths share the same base tracking and the same two 409 shapes. `stale_base` replays the platform's typed changelog: run `vos pull`, re-apply, push again. `protected_conflict` lists nodes a human edited in the studio: keep their values, or re-push with `--override <id>` only when the user asked for that exact change. The first push of a take asks before uploading (`--yes` for headless); agents never upload unprompted. The take's duration rides the upload, and the platform refuses a take over the hosted recording cap. Every push should carry `--label` (what changed, one line) and `--note` (why: the user's ask); the version history reads as a conversation, and an unlabelled push is a turn the human cannot read.
|
|
235
238
|
|
|
@@ -253,7 +256,7 @@ vos push /tmp/take --label "$TAG launch" --yes --json # VOS_API_KEY as a r
|
|
|
253
256
|
## For scripts and agents
|
|
254
257
|
|
|
255
258
|
- **Output.** Logs on stderr, results on stdout. `--json` turns every verb into NDJSON events ending with `{"event":"done",…}`.
|
|
256
|
-
- **Exit codes.** 0 ok, 1 error, 2 usage (including `--strict` failures), 3 no browser found.
|
|
259
|
+
- **Exit codes.** 0 ok, 1 error, 2 usage (including `--strict` failures), 3 no browser found, 4 the recorder met a wall (a sign-in, or under `--strict` a redirect away from the asked page) and nothing was recorded or cleared. `--json` carries the verdict as a `wall` event (`kind`, `level`, `asked`, `landed`, `refused`).
|
|
257
260
|
- **`--strict`** on `record` and `create` (agents: always): a skipped selector, a page that never reaches network idle, or a take that hit `--max-duration` exits 2 and lists `skipped[]` (and `capped`) in the done event. The default is lenient (exit 0, `skipped[]` still reported) for exploratory runs.
|
|
258
261
|
- **`--max-duration <s>`** on `record` and `create` defaults to the hosted recording cap, read live from `GET /api/limits` (2 s, fail-open to 30 min when the origin is unreachable): the capture stops there and the done event says so. Cut the flow rather than raising the cap; the platform refuses a longer take.
|
|
259
262
|
- **`create`** is the one-shot verb: record, auto-plan and render in one command and one browser session. The take directory still lands on disk, so the full loop (frames, edit `doc.json`, re-render) stays open afterwards.
|
|
@@ -79,6 +79,7 @@ var EXIT_OK = 0;
|
|
|
79
79
|
var EXIT_ERROR = 1;
|
|
80
80
|
var EXIT_USAGE = 2;
|
|
81
81
|
var EXIT_NO_BROWSER = 3;
|
|
82
|
+
var EXIT_WALL = 4;
|
|
82
83
|
function createReporter(json) {
|
|
83
84
|
return {
|
|
84
85
|
json,
|
|
@@ -6628,6 +6629,135 @@ function paceLine(r) {
|
|
|
6628
6629
|
return `pace: the script asked ${s(r.askedMs)}, the gestures added ${s(r.gestureMs)}, the take ran ${s(r.wallMs)} (${r.overheadPct} % overhead${slow})`;
|
|
6629
6630
|
}
|
|
6630
6631
|
|
|
6632
|
+
// src/plugin/wall.ts
|
|
6633
|
+
var IDP_HOSTS = [
|
|
6634
|
+
"accounts.google.com",
|
|
6635
|
+
"login.microsoftonline.com",
|
|
6636
|
+
"login.live.com",
|
|
6637
|
+
"appleid.apple.com",
|
|
6638
|
+
"auth0.com",
|
|
6639
|
+
"okta.com",
|
|
6640
|
+
"onelogin.com",
|
|
6641
|
+
"clerk.accounts.dev",
|
|
6642
|
+
"accounts.dev",
|
|
6643
|
+
"b2clogin.com",
|
|
6644
|
+
"amazoncognito.com",
|
|
6645
|
+
"workos.com",
|
|
6646
|
+
"stytch.com"
|
|
6647
|
+
];
|
|
6648
|
+
var SIGNIN_SEGMENTS = /* @__PURE__ */ new Set([
|
|
6649
|
+
"login",
|
|
6650
|
+
"log-in",
|
|
6651
|
+
"signin",
|
|
6652
|
+
"sign-in",
|
|
6653
|
+
"sign_in",
|
|
6654
|
+
"sso",
|
|
6655
|
+
"auth",
|
|
6656
|
+
"authenticate",
|
|
6657
|
+
"authorize",
|
|
6658
|
+
"oauth",
|
|
6659
|
+
"oauth2",
|
|
6660
|
+
"saml"
|
|
6661
|
+
]);
|
|
6662
|
+
function parse(url) {
|
|
6663
|
+
try {
|
|
6664
|
+
return new URL(url);
|
|
6665
|
+
} catch {
|
|
6666
|
+
return null;
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
var bareHost = (h) => h.replace(/^www\./, "").toLowerCase();
|
|
6670
|
+
var trimSlash = (p) => p.length > 1 ? p.replace(/\/+$/, "") : p;
|
|
6671
|
+
var place = (u) => `${u.origin}${trimSlash(u.pathname)}`;
|
|
6672
|
+
function isIdpHost(host) {
|
|
6673
|
+
const h = host.toLowerCase();
|
|
6674
|
+
if (h.startsWith("clerk.")) return true;
|
|
6675
|
+
return IDP_HOSTS.some((idp) => h === idp || h.endsWith(`.${idp}`));
|
|
6676
|
+
}
|
|
6677
|
+
function isSignInPath(pathname) {
|
|
6678
|
+
const segs = pathname.toLowerCase().split("/").filter(Boolean);
|
|
6679
|
+
return segs.some((s) => SIGNIN_SEGMENTS.has(s));
|
|
6680
|
+
}
|
|
6681
|
+
function redirectedAway(asked, landed) {
|
|
6682
|
+
if (bareHost(asked.hostname) !== bareHost(landed.hostname)) return true;
|
|
6683
|
+
const a = trimSlash(asked.pathname).toLowerCase();
|
|
6684
|
+
const l = trimSlash(landed.pathname).toLowerCase();
|
|
6685
|
+
if (a === l || a === "/") return false;
|
|
6686
|
+
if (l.startsWith(`${a}/`)) return false;
|
|
6687
|
+
if (l.endsWith(a)) return false;
|
|
6688
|
+
return true;
|
|
6689
|
+
}
|
|
6690
|
+
var LADDER = 'Walk the session ladder (mint a session from the test auth the project already has, or pass --storage-state): https://vos.so/llms-full.txt, "Sessions". A take OF this page is --allow-wall.';
|
|
6691
|
+
function wallVerdict(a) {
|
|
6692
|
+
const asked = parse(a.askedUrl);
|
|
6693
|
+
const landed = parse(a.landedUrl);
|
|
6694
|
+
if (!asked || !landed) return null;
|
|
6695
|
+
if (!/^https?:$/.test(asked.protocol)) return null;
|
|
6696
|
+
const away = redirectedAway(asked, landed);
|
|
6697
|
+
const where = { asked: place(asked), landed: place(landed) };
|
|
6698
|
+
const host = bareHost(asked.hostname);
|
|
6699
|
+
if (a.status === 401 || a.status === 403) {
|
|
6700
|
+
return {
|
|
6701
|
+
kind: "status",
|
|
6702
|
+
level: "hard",
|
|
6703
|
+
...where,
|
|
6704
|
+
message: `asked for ${asked.pathname}, answered ${a.status}: no session for ${host}. ${LADDER}`
|
|
6705
|
+
};
|
|
6706
|
+
}
|
|
6707
|
+
const askedForSignIn = isSignInPath(asked.pathname) || isIdpHost(asked.hostname);
|
|
6708
|
+
if (askedForSignIn && !away) return null;
|
|
6709
|
+
if (away && isIdpHost(landed.hostname) && !isIdpHost(asked.hostname)) {
|
|
6710
|
+
return {
|
|
6711
|
+
kind: "idp",
|
|
6712
|
+
level: "hard",
|
|
6713
|
+
...where,
|
|
6714
|
+
message: `asked for ${asked.pathname}, landed on ${landed.hostname}: no session for ${host}. ${LADDER}`
|
|
6715
|
+
};
|
|
6716
|
+
}
|
|
6717
|
+
const signInForm = a.passwordFields === 1 && !a.newPasswordField || a.oneTimeCodeField;
|
|
6718
|
+
if (away && isSignInPath(landed.pathname) || signInForm && !askedForSignIn) {
|
|
6719
|
+
const seen = away ? `landed on ${landed.pathname}` : "the page is a sign-in form";
|
|
6720
|
+
return {
|
|
6721
|
+
kind: "signin",
|
|
6722
|
+
level: "hard",
|
|
6723
|
+
...where,
|
|
6724
|
+
message: `asked for ${asked.pathname}, ${seen}: no session for ${host}. ${LADDER}`
|
|
6725
|
+
};
|
|
6726
|
+
}
|
|
6727
|
+
if (away) {
|
|
6728
|
+
return {
|
|
6729
|
+
kind: "redirect",
|
|
6730
|
+
level: "soft",
|
|
6731
|
+
...where,
|
|
6732
|
+
message: `asked for ${asked.pathname}, landed on ${landed.pathname}: the site sent the recorder elsewhere, which is what a page behind a login does to a stranger. If that is the page you meant, record it by its own URL. ${LADDER}`
|
|
6733
|
+
};
|
|
6734
|
+
}
|
|
6735
|
+
return null;
|
|
6736
|
+
}
|
|
6737
|
+
var WallError = class extends Error {
|
|
6738
|
+
constructor(verdict) {
|
|
6739
|
+
super(verdict.message);
|
|
6740
|
+
this.verdict = verdict;
|
|
6741
|
+
this.name = "WallError";
|
|
6742
|
+
}
|
|
6743
|
+
verdict;
|
|
6744
|
+
};
|
|
6745
|
+
var WALL_PROBE = `(() => {
|
|
6746
|
+
const shown = (el) => {
|
|
6747
|
+
const r = el.getBoundingClientRect()
|
|
6748
|
+
if (r.width < 2 || r.height < 2) return false
|
|
6749
|
+
const s = getComputedStyle(el)
|
|
6750
|
+
return s.visibility !== 'hidden' && s.display !== 'none'
|
|
6751
|
+
}
|
|
6752
|
+
const pw = [...document.querySelectorAll('input[type="password"]')].filter(shown)
|
|
6753
|
+
const otp = [...document.querySelectorAll('input[autocomplete="one-time-code"]')].filter(shown)
|
|
6754
|
+
return {
|
|
6755
|
+
passwordFields: pw.length,
|
|
6756
|
+
newPasswordField: pw.some((el) => (el.getAttribute('autocomplete') || '').includes('new-password')),
|
|
6757
|
+
oneTimeCodeField: otp.length > 0,
|
|
6758
|
+
}
|
|
6759
|
+
})()`;
|
|
6760
|
+
|
|
6631
6761
|
// src/plugin/recorder.ts
|
|
6632
6762
|
var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6633
6763
|
var DRY_PAUSE_MS = 120;
|
|
@@ -6661,11 +6791,30 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6661
6791
|
});
|
|
6662
6792
|
log(`goto ${url}`);
|
|
6663
6793
|
let navTimeout = false;
|
|
6664
|
-
await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
|
|
6794
|
+
const response = await page.goto(url, { waitUntil: "networkidle", timeout: 45e3 }).catch(() => {
|
|
6665
6795
|
navTimeout = true;
|
|
6666
6796
|
log(" (networkidle timeout \u2014 continuing)");
|
|
6797
|
+
return null;
|
|
6667
6798
|
});
|
|
6668
6799
|
await sleep(800);
|
|
6800
|
+
let wall = null;
|
|
6801
|
+
if (opts.onArrival) {
|
|
6802
|
+
const seen = await page.evaluate(WALL_PROBE).catch(() => null);
|
|
6803
|
+
try {
|
|
6804
|
+
wall = await opts.onArrival({
|
|
6805
|
+
askedUrl: url,
|
|
6806
|
+
landedUrl: page.url(),
|
|
6807
|
+
...response ? { status: response.status() } : {},
|
|
6808
|
+
passwordFields: seen?.passwordFields ?? 0,
|
|
6809
|
+
newPasswordField: seen?.newPasswordField ?? false,
|
|
6810
|
+
oneTimeCodeField: seen?.oneTimeCodeField ?? false
|
|
6811
|
+
}) ?? null;
|
|
6812
|
+
} catch (e) {
|
|
6813
|
+
await context.close().catch(() => {
|
|
6814
|
+
});
|
|
6815
|
+
throw e;
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6669
6818
|
const cdp = await context.newCDPSession(page);
|
|
6670
6819
|
const frames = [];
|
|
6671
6820
|
let frameIdx = 0;
|
|
@@ -6954,7 +7103,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6954
7103
|
pageTitle,
|
|
6955
7104
|
platform: process.platform === "darwin" ? "mac" : process.platform === "win32" ? "windows" : "linux",
|
|
6956
7105
|
producer: "cli",
|
|
6957
|
-
steps: stepSpans
|
|
7106
|
+
steps: stepSpans,
|
|
7107
|
+
...wall ? { wall: { kind: wall.kind, asked: wall.asked, landed: wall.landed } } : {}
|
|
6958
7108
|
};
|
|
6959
7109
|
const freezes = [];
|
|
6960
7110
|
for (let i = 1; i < frames.length; i++) {
|
|
@@ -6989,6 +7139,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
|
|
|
6989
7139
|
pace,
|
|
6990
7140
|
skipped,
|
|
6991
7141
|
navTimeout,
|
|
7142
|
+
wall,
|
|
6992
7143
|
freezes,
|
|
6993
7144
|
freezePct,
|
|
6994
7145
|
capped
|
|
@@ -7971,6 +8122,21 @@ function takePushRefusal(ask) {
|
|
|
7971
8122
|
return `this take tracks vos ${ask.trackedVosId} (its vos.json), not ${ask.vos}. Drop --vos to push there, or remove vos.json to adopt ${ask.vos} instead`;
|
|
7972
8123
|
return null;
|
|
7973
8124
|
}
|
|
8125
|
+
function programPushTarget(ask) {
|
|
8126
|
+
if (ask.vos) return { kind: "version", vosId: ask.vos, tracked: false };
|
|
8127
|
+
if (ask.remixOf) return { kind: "create", remixOfId: ask.remixOf };
|
|
8128
|
+
const tracked = ask.trackedVosId;
|
|
8129
|
+
if (!tracked) return { kind: "create" };
|
|
8130
|
+
const extra = ask.createOnly ?? [];
|
|
8131
|
+
if (extra.length) {
|
|
8132
|
+
const list = extra.map((f) => `--${f}`).join(", ");
|
|
8133
|
+
return {
|
|
8134
|
+
kind: "refuse",
|
|
8135
|
+
why: `this directory tracks vos ${tracked} (vos.json), so a bare push adds a VERSION to it, and ${list} ${extra.length > 1 ? "apply" : "applies"} only when creating. To make a separate vos from this config: vos push <config.json> --remix-of ${tracked} ${list}. To change the tracked vos's title or slug, edit it on vos.so`
|
|
8136
|
+
};
|
|
8137
|
+
}
|
|
8138
|
+
return { kind: "version", vosId: tracked, tracked: true };
|
|
8139
|
+
}
|
|
7974
8140
|
async function pushTake(dir, flags, r) {
|
|
7975
8141
|
const take = await loadTake(dir);
|
|
7976
8142
|
if (!take.doc) {
|
|
@@ -8659,7 +8825,7 @@ async function cmdPushProgram(argv) {
|
|
|
8659
8825
|
const config = pre.config;
|
|
8660
8826
|
const state = readSyncState(dir);
|
|
8661
8827
|
const vosFlag = strFlag(flags, "vos");
|
|
8662
|
-
const
|
|
8828
|
+
const explicitVosId = vosFlag ? parseVosId(vosFlag) : null;
|
|
8663
8829
|
const programDoc = await readProgramDoc(dir, config);
|
|
8664
8830
|
if (programDoc) {
|
|
8665
8831
|
const lint = lintDoc(programDoc);
|
|
@@ -8670,7 +8836,7 @@ async function cmdPushProgram(argv) {
|
|
|
8670
8836
|
for (const w of lint.warnings) r.log(`warning doc.json: ${w}`);
|
|
8671
8837
|
}
|
|
8672
8838
|
if (flags.claimable === true) {
|
|
8673
|
-
if (
|
|
8839
|
+
if (explicitVosId) {
|
|
8674
8840
|
throw new UsageError(
|
|
8675
8841
|
"--claimable creates a NEW claimable vos and cannot iterate (--vos). After the human claims it, iterate with their key: vos push --vos <id>"
|
|
8676
8842
|
);
|
|
@@ -8704,6 +8870,15 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
|
|
|
8704
8870
|
const desc = strFlag(flags, "desc");
|
|
8705
8871
|
const tagsFlag = strFlag(flags, "tags");
|
|
8706
8872
|
const folderRef = strFlag(flags, "folder");
|
|
8873
|
+
const remixOfFlag = strFlag(flags, "remix-of");
|
|
8874
|
+
const pushTarget = programPushTarget({
|
|
8875
|
+
vos: explicitVosId,
|
|
8876
|
+
remixOf: remixOfFlag,
|
|
8877
|
+
trackedVosId: state?.vosId ?? null,
|
|
8878
|
+
createOnly: ["title", "slug"].filter((f) => strFlag(flags, f))
|
|
8879
|
+
});
|
|
8880
|
+
if (pushTarget.kind === "refuse") throw new UsageError(pushTarget.why);
|
|
8881
|
+
const vosId = pushTarget.kind === "version" ? pushTarget.vosId : null;
|
|
8707
8882
|
if (vosId && (desc || tagsFlag || folderRef)) {
|
|
8708
8883
|
throw new UsageError(
|
|
8709
8884
|
"--desc/--tags/--folder apply when CREATING a vos. On an existing one: vos folder move <id> --to <folder> for filing; edit title/description/tags on vos.so"
|
|
@@ -8772,7 +8947,7 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
|
|
|
8772
8947
|
);
|
|
8773
8948
|
return EXIT_OK;
|
|
8774
8949
|
}
|
|
8775
|
-
const remixOfId =
|
|
8950
|
+
const remixOfId = pushTarget.kind === "create" ? pushTarget.remixOfId : void 0;
|
|
8776
8951
|
const fallbackTitle = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos remix";
|
|
8777
8952
|
const title = (strFlag(flags, "title") ?? fallbackTitle).slice(0, 100);
|
|
8778
8953
|
const folderId = folderRef ? resolveFolder(await listFolders(origin, key), folderRef).id : void 0;
|
|
@@ -10116,14 +10291,15 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
10116
10291
|
"motion",
|
|
10117
10292
|
"leader",
|
|
10118
10293
|
"keep-frames",
|
|
10119
|
-
"dry-run"
|
|
10294
|
+
"dry-run",
|
|
10295
|
+
"allow-wall"
|
|
10120
10296
|
]);
|
|
10121
10297
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg"]);
|
|
10122
10298
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
10123
10299
|
|
|
10124
10300
|
Take pipeline
|
|
10125
|
-
vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
|
|
10126
|
-
vos record --actions actions.json [--url <url>] [--out take] [--strict] [--dry-run] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
|
|
10301
|
+
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]
|
|
10302
|
+
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]
|
|
10127
10303
|
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]
|
|
10128
10304
|
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]
|
|
10129
10305
|
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]
|
|
@@ -10432,6 +10608,24 @@ async function releaseFrames(paths, flags, log) {
|
|
|
10432
10608
|
`released ${Math.round(freed / 1e6)} MB of screencast frames (the recording holds them now; --keep-frames keeps them)`
|
|
10433
10609
|
);
|
|
10434
10610
|
}
|
|
10611
|
+
function wallGate(flags, r, opts) {
|
|
10612
|
+
const allow = flags["allow-wall"] === true;
|
|
10613
|
+
return async (arrival) => {
|
|
10614
|
+
const verdict = wallVerdict(arrival);
|
|
10615
|
+
if (verdict && !allow && (verdict.level === "hard" || opts.strict)) {
|
|
10616
|
+
r.event({ event: "wall", ...verdict, refused: true });
|
|
10617
|
+
throw new WallError(verdict);
|
|
10618
|
+
}
|
|
10619
|
+
if (verdict) {
|
|
10620
|
+
r.event({ event: "wall", ...verdict, refused: false });
|
|
10621
|
+
r.log(
|
|
10622
|
+
allow ? ` note: recording past the wall (--allow-wall): landed on ${verdict.landed}` : ` WARNING: ${verdict.message}`
|
|
10623
|
+
);
|
|
10624
|
+
}
|
|
10625
|
+
await opts.prepare?.();
|
|
10626
|
+
return verdict;
|
|
10627
|
+
};
|
|
10628
|
+
}
|
|
10435
10629
|
async function cmdRecord(argv) {
|
|
10436
10630
|
const { positionals, flags, multi } = parseArgs(
|
|
10437
10631
|
argv,
|
|
@@ -10467,7 +10661,12 @@ async function cmdRecord(argv) {
|
|
|
10467
10661
|
actions,
|
|
10468
10662
|
takePaths(outDir),
|
|
10469
10663
|
r.log,
|
|
10470
|
-
{
|
|
10664
|
+
{
|
|
10665
|
+
storageState,
|
|
10666
|
+
dryRun: true,
|
|
10667
|
+
// A rehearsal is always strict, and it touches no directory.
|
|
10668
|
+
onArrival: wallGate(flags, r, { strict: true })
|
|
10669
|
+
}
|
|
10471
10670
|
);
|
|
10472
10671
|
const steps = rec.meta.steps ?? [];
|
|
10473
10672
|
const lines = steps.map((s) => {
|
|
@@ -10494,21 +10693,28 @@ ${lines.join("\n")}
|
|
|
10494
10693
|
await browser2.close();
|
|
10495
10694
|
}
|
|
10496
10695
|
}
|
|
10497
|
-
|
|
10498
|
-
|
|
10499
|
-
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10696
|
+
const paths = takePaths(outDir);
|
|
10697
|
+
const prepare = async () => {
|
|
10698
|
+
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
10699
|
+
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
10700
|
+
r.log(
|
|
10701
|
+
`note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
|
|
10702
|
+
);
|
|
10703
|
+
}
|
|
10704
|
+
await mkdir10(outDir, { recursive: true });
|
|
10705
|
+
await ensureTakeDir(outDir);
|
|
10706
|
+
};
|
|
10505
10707
|
const browser = await launchBrowser(browserArgs);
|
|
10506
10708
|
try {
|
|
10507
10709
|
r.log("recording\u2026");
|
|
10508
10710
|
r.event({ event: "phase", phase: "record" });
|
|
10509
10711
|
const rec = await recordTake(browser, url, actions, paths, r.log, {
|
|
10510
10712
|
maxDurationSeconds,
|
|
10511
|
-
storageState
|
|
10713
|
+
storageState,
|
|
10714
|
+
onArrival: wallGate(flags, r, {
|
|
10715
|
+
strict: flags.strict === true,
|
|
10716
|
+
prepare
|
|
10717
|
+
})
|
|
10512
10718
|
});
|
|
10513
10719
|
r.event({ event: "phase", phase: "encode" });
|
|
10514
10720
|
r.log("encoding\u2026");
|
|
@@ -10540,6 +10746,7 @@ ${lines.join("\n")}
|
|
|
10540
10746
|
freezePct: rec.freezePct,
|
|
10541
10747
|
capped: rec.capped,
|
|
10542
10748
|
pace: rec.pace,
|
|
10749
|
+
...rec.wall ? { wall: rec.wall } : {},
|
|
10543
10750
|
...strictFail ? { strictFailed: true } : {}
|
|
10544
10751
|
},
|
|
10545
10752
|
strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
|
|
@@ -10582,21 +10789,28 @@ async function cmdCreate2(argv) {
|
|
|
10582
10789
|
const maxDurationSeconds = await maxDuration(flags, r);
|
|
10583
10790
|
const storageState = takeStorageState(flags);
|
|
10584
10791
|
const browserArgs = takeBrowserArgs(multi);
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10792
|
+
const paths = takePaths(outDir);
|
|
10793
|
+
const prepare = async () => {
|
|
10794
|
+
if (existsSync15(join18(outDir, "meta.json"))) {
|
|
10795
|
+
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
10796
|
+
r.log(
|
|
10797
|
+
`note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
|
|
10798
|
+
);
|
|
10799
|
+
}
|
|
10800
|
+
await mkdir10(outDir, { recursive: true });
|
|
10801
|
+
await ensureTakeDir(outDir);
|
|
10802
|
+
};
|
|
10593
10803
|
const browser = await launchBrowser(browserArgs);
|
|
10594
10804
|
try {
|
|
10595
10805
|
r.log("recording\u2026");
|
|
10596
10806
|
r.event({ event: "phase", phase: "record" });
|
|
10597
10807
|
const rec = await recordTake(browser, url, actions, paths, r.log, {
|
|
10598
10808
|
maxDurationSeconds,
|
|
10599
|
-
storageState
|
|
10809
|
+
storageState,
|
|
10810
|
+
onArrival: wallGate(flags, r, {
|
|
10811
|
+
strict: flags.strict === true,
|
|
10812
|
+
prepare
|
|
10813
|
+
})
|
|
10600
10814
|
});
|
|
10601
10815
|
r.event({ event: "phase", phase: "encode" });
|
|
10602
10816
|
r.log("encoding\u2026");
|
|
@@ -11219,12 +11433,14 @@ async function cmdDigest(argv) {
|
|
|
11219
11433
|
sourceDuration: d.take.sourceDuration,
|
|
11220
11434
|
outputDuration: d.take.outputDuration,
|
|
11221
11435
|
hasCursor: d.take.hasCursor,
|
|
11436
|
+
...d.take.wall ? { wall: d.take.wall } : {},
|
|
11222
11437
|
tokensEstimate: d.images.tokensEstimate,
|
|
11223
11438
|
bytes: result.bytes
|
|
11224
11439
|
},
|
|
11225
11440
|
`Wrote ${result.file} \u2014 ${d.moments.length} moments (${Object.entries(
|
|
11226
11441
|
kinds
|
|
11227
|
-
).map(([k, n]) => `${n} ${k}`).join(", ")}), ${d.images.full} frames + ${d.images.crop} crops` + (d.images.sheet ? `, sheet.png` : "") + ` \u2014 ~${d.images.tokensEstimate} image tokens to read them all` + (d.take.hasCursor ? "" : "\n no cursor track: moments are head/tail/scenes only \u2014 pace by activity, zoom only where the ask names a place") + `
|
|
11442
|
+
).map(([k, n]) => `${n} ${k}`).join(", ")}), ${d.images.full} frames + ${d.images.crop} crops` + (d.images.sheet ? `, sheet.png` : "") + ` \u2014 ~${d.images.tokensEstimate} image tokens to read them all` + (d.take.hasCursor ? "" : "\n no cursor track: moments are head/tail/scenes only \u2014 pace by activity, zoom only where the ask names a place") + (d.take.wall ? `
|
|
11443
|
+
WALL: asked for ${d.take.wall.asked}, recorded ${d.take.wall.landed} \u2014 this footage may be the wrong page; re-record with a session before cutting it` : "") + `
|
|
11228
11444
|
Read digest.json, then the sheet, then a crop only where you must decide.`
|
|
11229
11445
|
);
|
|
11230
11446
|
return EXIT_OK;
|
|
@@ -11697,6 +11913,11 @@ async function run(argv) {
|
|
|
11697
11913
|
`);
|
|
11698
11914
|
return EXIT_USAGE;
|
|
11699
11915
|
}
|
|
11916
|
+
if (e instanceof WallError) {
|
|
11917
|
+
process.stderr.write(`wall: ${e.message}
|
|
11918
|
+
`);
|
|
11919
|
+
return EXIT_WALL;
|
|
11920
|
+
}
|
|
11700
11921
|
if (e instanceof BrowserUnavailableError) {
|
|
11701
11922
|
process.stderr.write(`${e.message}
|
|
11702
11923
|
`);
|
|
@@ -11740,4 +11961,4 @@ export {
|
|
|
11740
11961
|
takeBrowserArgs,
|
|
11741
11962
|
run
|
|
11742
11963
|
};
|
|
11743
|
-
//# sourceMappingURL=chunk-
|
|
11964
|
+
//# sourceMappingURL=chunk-BD4FXBUO.js.map
|