@vosjs/cli 0.80.0 → 0.81.1
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 +22 -8
- package/dist/{assetManifest-ETT3C7ZA.js → assetManifest-27NKSS7N.js} +2 -2
- package/dist/{chunk-PJ6MEJZV.js → chunk-352AC3V6.js} +37 -9
- package/dist/chunk-352AC3V6.js.map +1 -0
- package/dist/{chunk-SP5OHTVB.js → chunk-S3RI2SHH.js} +563 -250
- package/dist/chunk-S3RI2SHH.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +27 -27
- package/dist/index.js +2 -2
- package/dist/{run-2ELHMPTB.js → run-MEDD2Q74.js} +7 -3
- package/dist/{shotList-MSNWLVDN.js → shotList-PL7XP6AC.js} +6 -1
- package/dist/shotList-PL7XP6AC.js.map +1 -0
- package/package.json +4 -4
- package/schema/actions.schema.json +56 -45
- package/schema/doc.schema.json +27 -1
- package/skills/VERSION +1 -1
- package/skills/launch-kit/SKILL.md +7 -0
- package/skills/product-video/SKILL.md +91 -3
- package/skills/product-video/references/how-to.md +78 -0
- package/skills/product-video/references/taste.md +4 -0
- package/skills/product-video/references/troubleshooting.md +8 -4
- package/skills/vos-cut/SKILL.md +34 -0
- package/dist/chunk-PJ6MEJZV.js.map +0 -1
- package/dist/chunk-SP5OHTVB.js.map +0 -1
- package/dist/shotList-MSNWLVDN.js.map +0 -1
- /package/dist/{assetManifest-ETT3C7ZA.js.map → assetManifest-27NKSS7N.js.map} +0 -0
- /package/dist/{run-2ELHMPTB.js.map → run-MEDD2Q74.js.map} +0 -0
package/README.md
CHANGED
|
@@ -88,8 +88,7 @@ vos open take # hand the take to the s
|
|
|
88
88
|
When the product ships again, re-record the same script and keep the cut:
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
vos record --actions actions.json --out take --strict # footage replaced
|
|
92
|
-
vos plan take --reuse # re-time that cut onto the new recording; what could not follow is named
|
|
91
|
+
vos record --actions actions.json --out take --strict --reuse # footage replaced, the cut re-timed onto it; what could not follow is named
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
Every manual span follows the step it was made on (the gesture whose window held it, or the one just before it), keeping its offset exactly; a span whose step is missing or skipped in the new recording is dropped and named with what it framed, never re-timed to a place with nothing under it. A layer pinned to a step (`pin: { step }`) plays when that step plays in the new cut; a layer that is not pinned keeps its output second, and when the step under it moved the report names it with the step's new time and the pin to write. A take fetched from vos.so can be recorded again: the script rides the document and `vos fetch --media` writes it back as `actions.json`.
|
|
@@ -107,7 +106,7 @@ vos ingest loom.mp4 --as take --out take # a screen demo with no
|
|
|
107
106
|
|
|
108
107
|
**A file with no trace is finished footage by default.** Nothing but a cursor trace says a file is a screen recording, so without one the take opens bare: no card, no browser bar, no drawn cursor or webcam, exported at the footage's own frame rate. `--as take` gives a screen demo the card; `--as footage` says the opposite outright.
|
|
109
108
|
|
|
110
|
-
What a trace cannot give is named, never guessed: agent-browser's own log carries neither timestamps nor coordinates (measured), so stamp `ts` and a `point` per command when you keep one, or replay the walk with `vos actions from-agent-browser` and `vos record` instead. `--offset <ms>` shifts a trace whose zero is not the video's; `--viewport WxH` names the coordinate space when the video was scaled. A headed capture with the real pointer baked in shows two cursors under a trace's drawn one; turn the drawn cursor off in `doc.json` for those.
|
|
109
|
+
What a trace cannot give is named, never guessed: agent-browser's own log carries neither timestamps nor coordinates (measured), so stamp `ts` and a `point` per command when you keep one, or replay the walk with `vos actions from-agent-browser` and `vos record` instead. `--offset <ms>` shifts a trace whose zero is not the video's; `--viewport WxH` names the coordinate space when the video was scaled. A headed capture with the real pointer baked in shows two cursors under a trace's drawn one; turn the drawn cursor off in `doc.json` for those (`cursor.visible: false`; for part of the film, `cursor.off: [{ in, out? }]` in output seconds, an absent `out` running to the end, which `vos plan` also writes under its end card).
|
|
111
110
|
|
|
112
111
|
**Several takes, one film.** `vos join` cuts takes (and plain video files) into one take, in argument order, and the result is one document to edit, render and push like any other:
|
|
113
112
|
|
|
@@ -144,7 +143,7 @@ The track is decoded (mediabunny in Node for PCM; a compressed track in the head
|
|
|
144
143
|
vos record --hosted --actions actions.json --out take # needs a credential (vos login, or VOS_API_KEY); a public URL, or a preview behind --header
|
|
145
144
|
```
|
|
146
145
|
|
|
147
|
-
What travels: the script, the viewport, every `--header` value (a preview's bypass token), and the value of every `{ "env": "NAME" }` a `setup` step reads, from this shell. What never travels: a session (`--hosted` refuses `--session` and `--storage-state`, because none of a person's ever reaches vos.so). A page behind a sign-in fails on the fleet in words naming the local ladder, exit 4; a private-network URL is refused before a job exists. The fleet records up to ten minutes. `--dry-run` stays a local act. A script with a guarded step, a `download`, a `paste`, a `js` check or `peers` is refused on `--hosted` in words: nobody can confirm a step that spends on the fleet, a hosted take has no `outputs/` to keep a file in and no clipboard to paste from, a fleet browser runs no code it was handed, and it runs one browser.
|
|
146
|
+
What travels: the script, the viewport, every `--header` value (a preview's bypass token), and the value of every `{ "env": "NAME" }` a `setup` or `teardown` step reads, from this shell. What never travels: a session (`--hosted` refuses `--session` and `--storage-state`, because none of a person's ever reaches vos.so). A page behind a sign-in fails on the fleet in words naming the local ladder, exit 4; a private-network URL is refused before a job exists. The fleet records up to ten minutes. `--dry-run` stays a local act. A script with a guarded step, a `download`, a `paste`, a `js` check or `peers` is refused on `--hosted` in words: nobody can confirm a step that spends on the fleet, a hosted take has no `outputs/` to keep a file in and no clipboard to paste from, a fleet browser runs no code it was handed, and it runs one browser.
|
|
148
147
|
|
|
149
148
|
| Verb | Flags |
|
|
150
149
|
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -170,6 +169,7 @@ What travels: the script, the viewport, every `--header` value (a preview's bypa
|
|
|
170
169
|
vos session open https://app.acme.com --name acme # a plain Chrome window opens; sign in, set the account up, close the window (or press Enter)
|
|
171
170
|
vos session check acme --url https://app.acme.com/dashboard # does it still open the page? exit 0, or 4
|
|
172
171
|
vos record --actions actions.json --out take --session acme --strict
|
|
172
|
+
vos session exec acme list-keys.mjs --url https://app.acme.com/settings # your own Playwright code, inside the session
|
|
173
173
|
vos session list · vos session rm acme
|
|
174
174
|
```
|
|
175
175
|
|
|
@@ -177,6 +177,8 @@ vos session list · vos session rm acme
|
|
|
177
177
|
|
|
178
178
|
Measured before this was written: a profile plain Chrome signed in reopens signed in ONLY under the system Chrome with the real keychain. Playwright's bundled Chromium keeps its own keychain entry and can never decrypt Chrome's cookies, so every reader of a session here is the system Chrome (`VOS_BROWSER_PATH` overrides it) with Playwright's `--use-mock-keychain` removed. Two things `check` says because they cost real runs: an app whose session cookie has no expiry leaves nothing to reuse once the window closes, and a Chrome that was killed rather than quit never wrote its store.
|
|
179
179
|
|
|
180
|
+
**Never open a session's profile yourself.** A browser that cannot decrypt the profile's cookies does not merely fail to read them: it starts the cookie store over, and the session is signed out for good. Measured: a cookie the system Chrome kept through every reopen was gone after ONE launch of Playwright's bundled Chromium on the profile, for the system Chrome too. So a script that needs the signed-in browser (a state a step cannot reach yet, a list of what the account holds, cleaning up after a flow) runs inside the session with `vos session exec <name> <script.mjs>`: an ES module whose default export receives `{ context, page, url, name }` (the page already on `--url` when one is given), run on the profile exactly as `record` opens it, headless unless `--headed`, `--viewport WxH` when it matters. What the function returns is printed (`result` in the `--json` done event), the profile is closed cleanly whatever the script does, and a script that throws exits 1 with its error. `check` names a profile opened another way as a cause of a session that no longer opens signed in.
|
|
181
|
+
|
|
180
182
|
A session never enters a take directory and never reaches vos.so: `vos push` refuses a take holding a storage-state-shaped JSON, naming the file.
|
|
181
183
|
|
|
182
184
|
**`vos look`: see a page before scripting it.** An agent writing `actions.json` needs the page and a selector for each control it will touch, and behind a login the only other way to see the page is to record it. `vos look <url>` opens the page on the same browser and the same door `record` would use (`--session <name>` or `--storage-state <file>`, `--header`, `--browser-arg=`; headless unless `--headed`, `--viewport WxH` 1280x720 by default), waits until the DOM has held still for 0.8 s (never for network idle, which many apps never reach), runs any `--click <selector>` and `--type <selector>=<text>` in the order given, each followed by the same settle, and looks:
|
|
@@ -199,7 +201,17 @@ It writes `<out>/look.png` (the viewport; `--out look` by default) and lists eve
|
|
|
199
201
|
]
|
|
200
202
|
```
|
|
201
203
|
|
|
202
|
-
**`
|
|
204
|
+
**`teardown`: the steps that run after it.** A flow that CREATES something in the account (an API key, a project, an invite) creates it on every rehearsal and every take, since `--dry-run` clicks for real. `teardown: [...]` beside `setup` removes it again: the same verbs and the same rules (a `{ "env": "NAME" }` text, no literal in a password field, http and https only), run off camera once the screencast has stopped, on the take's own page and session, so nothing of it is captured or lands in `meta.steps`. It runs after a rehearsal too, and when the take stops early (a failed `await`, `--strict` on a page that stopped answering, the cap) or throws once its first step ran; not when the take never began (a setup that failed, a wall). The steps together get 60 s. A teardown never fails the take or changes the exit code: it stops at the first step that fails, the done event carries `teardown: { ran, of, failed?: { step, do, selector?, error } }`, and the summary says `teardown: 3 step(s) ran` or `TEARDOWN FAILED at teardown[1] (click #confirm): <reason>. What the take created may still be in the product; remove it by hand.` A `{ env }` the shell does not hold refuses the take before it begins, since the take would create what its teardown cannot remove.
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
"teardown": [
|
|
208
|
+
{ "do": "goto", "url": "http://localhost:3000/settings/keys" },
|
|
209
|
+
{ "do": "click", "selector": "[data-testid=key-row]:first-child button[aria-label=Revoke]" },
|
|
210
|
+
{ "do": "click", "selector": "role=button[name=\"Revoke key\"]" }
|
|
211
|
+
]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**`vos actions script <actions.json>`: the flow as a shot list.** When no rung of the session ladder holds and a person has to record the take themselves (in their own signed-in browser, with the vosso extension), the agent hands over the flow it worked out rather than an apology: numbered beats in plain words with the holds the script asked for, the page to start on and about how long. A step's `caption` leads its beat, a `text=` or `:has-text()` selector is said as its words, and a step's `id` is said as a name (`new-order` reads as "new order"), so give steps ids and captions that a person could follow. A script's `setup` is said as what to do before pressing record, and its `teardown` as what to undo after stopping. `--json` carries the beats beside the text. The take comes back through the ordinary handoff, and the agent cuts it (`vos pull --media`, then the `vos-cut` skill).
|
|
203
215
|
|
|
204
216
|
**`--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.
|
|
205
217
|
|
|
@@ -226,7 +238,7 @@ It writes `<out>/look.png` (the viewport; `--out look` by default) and lists eve
|
|
|
226
238
|
|
|
227
239
|
**Many media in one take (concat).** `doc.json` may carry `media: [{ id, videoKey, cursor, meta, … }]`, the take's OTHER recordings or uploads, each the `source` shape with an `id`; a segment on one (`{ in, out, media: "m1" }`) plays it, and a zoom, tilt, speed, freeze or cam-move span names the media its source seconds belong to (absent = the primary, `source`). The Video row shows the clips in order, a span is drawn where its media plays, `vos plan` proposes zoom and speed spans on every media from its own cursor track, `vos validate` measures each span against its own media's length, and `vos push` and `vos pull --media` carry every media through the recording door. A media wears its OWN card: `media[].frame` carries the card-owned fields (its placement and size as `inset`, the browser bar, the corner, the shadow, the border, the cover fit and its focus) over the take's frame while it plays; the bar names that media's recorded page, an upload with no page wears none, and the frame-wide fields (the aspect, the padding, the ground, the backdrop, the card's animation) stay the take's. Sound: the primary's tracks play at the primary's moments; another media's own audio is not spliced into the cut yet. **Many cards**: an image or video overlay may show a document media by reference (`key: "media:<id>"`, `media:` alone the primary; its cursor track and recorded page come with it) and wear a card (`frame`: `browserBar`, a `lean` `{rx, ry}` in degrees, `shadow`, `shadowContact`, `shadowColor`, `cursor`), drawn by the card painter on its own plane above the primary card with the media's cursor dot and click rings inside it; a layer without `frame` stays the flat picture. The primary card stays primary: the sequence, the camera and the cut are its.
|
|
228
240
|
|
|
229
|
-
**A series shares its look by data.** `vos plan <take> --style <seed doc.json | vosId>` copies the seed's `zoomStyle`, `zoomParams`, `speedParams`, `tiltStyle`, `frame`, `cursor`, `cam` and `export` onto a new take (never its spans, overlays or
|
|
241
|
+
**A series shares its look by data.** `vos plan <take> --style <seed doc.json | vosId>` copies the seed's `zoomStyle`, `zoomParams`, `speedParams`, `tiltStyle`, `frame`, `cursor`, `cam` and `export` onto a new take (never its spans, overlays, audio or cursor-off ranges) and re-plans the automatic spans under them. A seed that is a POSTER carries its layout too: its card placement, its `stage-*` clips by id (with the release's words from LAUNCH.md's `headline` and `kicker` roles, BRAND.md's `wordmark`, or `--headline` and `--kicker` patched in; the brand's mark from BRAND.md `logoUrl` fetched into `<take>/brand/` for `stage-mark`), its rest lean, its trailing freeze. What could not follow is said in words.
|
|
230
242
|
|
|
231
243
|
**The cut's motion is the document's, in one vocabulary.** Every visual thing carries `anim` (`enter`, `exit`, `idle`; a kind, or a step with its seconds and, for words, `unit`, `direction`, `stagger`): the card (`frame.anim`, entering by `tilt-in`, `pull-out`, `rise`, `fade` or `slide` and leaving by `recede` or `fade`), a text, image or video clip, a prop, and a FOOTAGE clip at its boundaries (`segments[].anim`: `exit` is how it leaves at the boundary after it, `enter` how it arrives at the one before, by `slide`, `fade` or `scale`; a slide names its `side`, a step its `seconds`, never longer than half the shorter clip). An exit and the next clip's enter at one boundary run together as a push: the incoming clip plays live while the outgoing, frozen on its last frame, moves away, the camera rests through the window, and one cursor dot crosses from the outgoing card's last point to the incoming's. A page change inside one recording is the same primitive: two clips of one media, the load between them cut. The output lasts until the last clip ends; the card is on screen exactly while its clip runs (the footage, freezes included) and leaves at its end, so its `exit` plays over the clip's last seconds like every clip's; past the footage the clips play over the ground alone. A FREEZE (`freeze: [{id, at, seconds}]`, a source moment held for output seconds, beside the speed spans on the retime lane) freezes the footage anywhere, the middle of the take included; a segment's older `hold` is read as a freeze at its end. **The frame that STANDS for a take** is `still` (output seconds; `vos plan --still <t>`, LAUNCH.md `still:`), else the take's own last freeze (a poster's rest; a template's freezes, stamped `from`, never count), else the hero moment after the card and the opening clips have entered: the shelf's cover and `vos frames --at-still` both read that one derivation, and a plan never moves a still the author set. There is no end-card field and no entrance field. A COMPONENT is a TEMPLATE: a plain take on a shelf whose clips carry stable ids, laid onto a take at an anchor and stamped `from` with where its clips came from. A fresh `vos plan` proposes the card's enter, the templates the recipe names (`LAUNCH.md` `with: <ref>[@end|@start|@step:<id>|@<seconds>], …` or `--with`, repeatable; `endCard: <ref>` names one at the end), the END CARD when `endCard` is on or absent: the official `End card` template on vos.so (the platform's `Templates` project, promoted; any official template resolves by its title, `--with "End card"`, `--style "Split cover, landscape"`), laid at the end with the release's words and BRAND.md's mark, stamped `from: endcard`; offline, the house clips stand in (a freeze of the last frame for the card's seconds, the card receding over it, the headline, the release line, the wordmark and the mark as clips over the freeze), a caption per `actions.json` step, a music bed and click sounds, a transition at every page change the recorder marked (`meta.steps[].navigated`: the outgoing clip ends on the frame before the change, the incoming begins once the page has settled; `transitions: slide|fade|scale|none`, slide by default), from LAUNCH.md's `entrance`, `transitions`, `endCard`, `with`, `captions`, `music` and `clicks` roles (or the flags), as data in doc.json with stable ids (`bed`, `click-<n>`, `caption-<step>`), so the studio shows what the kit will render; a refresh never re-proposes (a deleted clip stays deleted) and `--motion` re-proposes on purpose, replacing only its own work. A document written in the older spellings (`frame.entrance`, `endCard`, a clip's `enter`/`exit`/`fx`, a prop's `animation`) is read into the vocabulary and `vos validate` says so.
|
|
232
244
|
|
|
@@ -261,7 +273,9 @@ take/
|
|
|
261
273
|
meta.json RecordingMeta (producer: "cli", per-step source extents)
|
|
262
274
|
actions.json the script that produced it, with the resolved url; pushed inside the document and written back by a fetch or pull with --media
|
|
263
275
|
doc.json ProjectDoc, the editable cut
|
|
264
|
-
doc.prev.json the previous cut, kept by a re-record for
|
|
276
|
+
doc.prev.json the previous cut, kept by a re-record for `--reuse`
|
|
277
|
+
.doc-planned the hash of the plan the last recording wrote, so a re-record
|
|
278
|
+
knows an unedited doc.json from a cut
|
|
265
279
|
join.json a joined take's inputs: each one's path, footage sha256 and media id, read by `vos join --keep-edits`
|
|
266
280
|
outputs/ the files `download` steps kept, listed in meta.json's outputs
|
|
267
281
|
page-sound/ the sound the page played during the take, listed in meta.json's media
|
|
@@ -279,7 +293,7 @@ claimed. Every verb resolves the name, and a push declares the container it
|
|
|
279
293
|
finds in the bytes, so a file never reaches vos.so under the wrong type.
|
|
280
294
|
`vos validate <take>` warns when an existing take holds one that does.
|
|
281
295
|
|
|
282
|
-
A re-record replaces the footage, the cursor track, the frames, `outputs/` and everything derived from them, moves `doc.json` to `doc.prev.json`, and keeps `actions.json` and `vos.json`.
|
|
296
|
+
A re-record replaces the footage, the cursor track, the frames, `outputs/` and everything derived from them, moves `doc.json` to `doc.prev.json`, and keeps `actions.json` and `vos.json`. With `--reuse` the cut goes onto the new footage in the same command (what `vos plan <take> --reuse` does afterwards); without it the take is planned fresh, and its last line and its done event (`previousCut`, `next_step`) say where the cut is and how to apply it. A plan nobody edited never takes the cut's place: recording again over a take whose `doc.json` is still the plan its recording wrote keeps the cut already in `doc.prev.json`.
|
|
283
297
|
|
|
284
298
|
## actions.json
|
|
285
299
|
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
localManifestRefs,
|
|
5
5
|
pullManifest,
|
|
6
6
|
uploadManifest
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-352AC3V6.js";
|
|
8
8
|
import "./chunk-J3KHIZCH.js";
|
|
9
9
|
export {
|
|
10
10
|
hostedLiteralWarnings,
|
|
@@ -12,4 +12,4 @@ export {
|
|
|
12
12
|
pullManifest,
|
|
13
13
|
uploadManifest
|
|
14
14
|
};
|
|
15
|
-
//# sourceMappingURL=assetManifest-
|
|
15
|
+
//# sourceMappingURL=assetManifest-27NKSS7N.js.map
|
|
@@ -30,7 +30,7 @@ function safePathName(name, fallback) {
|
|
|
30
30
|
|
|
31
31
|
// src/plugin/media.ts
|
|
32
32
|
import { isKeyedOverlay } from "@vosjs/studio-core";
|
|
33
|
-
import { createHash as
|
|
33
|
+
import { createHash as createHash3 } from "crypto";
|
|
34
34
|
import { createWriteStream, existsSync as existsSync3 } from "fs";
|
|
35
35
|
import {
|
|
36
36
|
mkdir as mkdir2,
|
|
@@ -46,6 +46,7 @@ import { Readable } from "stream";
|
|
|
46
46
|
import { pipeline } from "stream/promises";
|
|
47
47
|
|
|
48
48
|
// src/plugin/take.ts
|
|
49
|
+
import { createHash } from "crypto";
|
|
49
50
|
import { mkdir, readFile, rename, rm } from "fs/promises";
|
|
50
51
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
51
52
|
import { basename, join } from "path";
|
|
@@ -59,6 +60,26 @@ function takeMediaFile(dir, stem, fallback) {
|
|
|
59
60
|
return takeMediaFiles(dir, stem)[0] ?? join(dir, fallback);
|
|
60
61
|
}
|
|
61
62
|
var PREV_DOC_NAME = "doc.prev.json";
|
|
63
|
+
var PLANNED_HASH_NAME = ".doc-planned";
|
|
64
|
+
var sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
65
|
+
async function notePlannedDoc(dir) {
|
|
66
|
+
const doc = takePaths(dir).doc;
|
|
67
|
+
if (!existsSync(doc)) return;
|
|
68
|
+
await writeFileSafe(
|
|
69
|
+
join(dir, PLANNED_HASH_NAME),
|
|
70
|
+
sha256(await readFile(doc, "utf8"))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
async function docIsUntouchedPlan(dir) {
|
|
74
|
+
const mark = join(dir, PLANNED_HASH_NAME);
|
|
75
|
+
const doc = takePaths(dir).doc;
|
|
76
|
+
if (!existsSync(mark) || !existsSync(doc)) return false;
|
|
77
|
+
const [want, have] = await Promise.all([
|
|
78
|
+
readFile(mark, "utf8"),
|
|
79
|
+
readFile(doc, "utf8")
|
|
80
|
+
]);
|
|
81
|
+
return want.trim() === sha256(have);
|
|
82
|
+
}
|
|
62
83
|
function takePaths(dir) {
|
|
63
84
|
const recording = takeMediaFile(dir, "recording", RECORDING_NAME);
|
|
64
85
|
return {
|
|
@@ -96,11 +117,17 @@ async function prepareReRecord(dir) {
|
|
|
96
117
|
const p = takePaths(dir);
|
|
97
118
|
const kept = [];
|
|
98
119
|
let prevDoc = false;
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
const prevPath = join(dir, PREV_DOC_NAME);
|
|
121
|
+
if (existsSync(prevPath) && await docIsUntouchedPlan(dir)) {
|
|
122
|
+
await rm(p.doc, { force: true });
|
|
123
|
+
prevDoc = true;
|
|
124
|
+
kept.push(PREV_DOC_NAME);
|
|
125
|
+
} else if (existsSync(p.doc)) {
|
|
126
|
+
await rename(p.doc, prevPath);
|
|
101
127
|
prevDoc = true;
|
|
102
128
|
kept.push(PREV_DOC_NAME);
|
|
103
129
|
}
|
|
130
|
+
await rm(join(dir, PLANNED_HASH_NAME), { force: true });
|
|
104
131
|
if (existsSync(p.actions)) kept.push("actions.json");
|
|
105
132
|
if (existsSync(join(dir, "vos.json"))) kept.push("vos.json");
|
|
106
133
|
for (const stale of [
|
|
@@ -277,7 +304,7 @@ async function uploadAsset(target, bytes, opts) {
|
|
|
277
304
|
}
|
|
278
305
|
|
|
279
306
|
// src/plugin/pasteFiles.ts
|
|
280
|
-
import { createHash } from "crypto";
|
|
307
|
+
import { createHash as createHash2 } from "crypto";
|
|
281
308
|
import { existsSync as existsSync2 } from "fs";
|
|
282
309
|
import { readFile as readFile2 } from "fs/promises";
|
|
283
310
|
import { join as join2 } from "path";
|
|
@@ -351,7 +378,7 @@ async function sendPasteFiles(doc, found, target, log) {
|
|
|
351
378
|
put = await uploadAsset(target, f.bytes, {
|
|
352
379
|
filename: f.name,
|
|
353
380
|
contentType: f.type,
|
|
354
|
-
contentHash:
|
|
381
|
+
contentHash: createHash2("sha256").update(f.bytes).digest("hex")
|
|
355
382
|
});
|
|
356
383
|
} catch (e) {
|
|
357
384
|
throw new Error(
|
|
@@ -432,7 +459,7 @@ async function claimablePasteFiles(doc, found, hosted, log) {
|
|
|
432
459
|
continue;
|
|
433
460
|
}
|
|
434
461
|
const origin = hosted[f.name];
|
|
435
|
-
const sha =
|
|
462
|
+
const sha = createHash2("sha256").update(f.bytes).digest("hex");
|
|
436
463
|
if (origin && origin.sha256 === sha) listed[f.name] = origin.ref;
|
|
437
464
|
else
|
|
438
465
|
log(
|
|
@@ -599,7 +626,7 @@ async function uploadDocRefs(refs, dir, target, log) {
|
|
|
599
626
|
}
|
|
600
627
|
const file = local.file;
|
|
601
628
|
const media = await readFile3(file);
|
|
602
|
-
const mediaHash =
|
|
629
|
+
const mediaHash = createHash3("sha256").update(media).digest("hex");
|
|
603
630
|
const { type } = resolveMediaType({
|
|
604
631
|
head: media.subarray(0, MEDIA_HEAD_BYTES),
|
|
605
632
|
filename: file
|
|
@@ -758,7 +785,7 @@ async function pullMedia(ctx, dir, doc, log) {
|
|
|
758
785
|
return result;
|
|
759
786
|
}
|
|
760
787
|
async function fileSha256(file) {
|
|
761
|
-
return
|
|
788
|
+
return createHash3("sha256").update(await readFile3(file)).digest("hex");
|
|
762
789
|
}
|
|
763
790
|
async function recordHostedMedia(dir, vosId, pulled) {
|
|
764
791
|
if (pulled.hosted.length === 0) return;
|
|
@@ -900,6 +927,7 @@ export {
|
|
|
900
927
|
RECORDING_NAME,
|
|
901
928
|
takeMediaFile,
|
|
902
929
|
PREV_DOC_NAME,
|
|
930
|
+
notePlannedDoc,
|
|
903
931
|
takePaths,
|
|
904
932
|
ensureTakeDir,
|
|
905
933
|
dropScreencastFrames,
|
|
@@ -930,4 +958,4 @@ export {
|
|
|
930
958
|
pullManifest,
|
|
931
959
|
hostedLiteralWarnings
|
|
932
960
|
};
|
|
933
|
-
//# sourceMappingURL=chunk-
|
|
961
|
+
//# sourceMappingURL=chunk-352AC3V6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin/assetManifest.ts","../src/plugin/pathName.ts","../src/plugin/media.ts","../src/plugin/take.ts","../src/plugin/uploadAsset.ts","../src/plugin/pasteFiles.ts"],"sourcesContent":["/**\n * A program's declared files (`config.assets`) crossing to and from vos.so.\n *\n * On disk a manifest names files beside the config (`./logo.png`), so a\n * program renders offline and reads like the repository it lives in. On the\n * platform a manifest names hosted assets (`asset:<id>`), the one spelling\n * the platform resolves for every surface: a private file reaches a render\n * page, a remix brings its files along, and a file a program still plays is\n * never collected. A push is the crossing one way, `vos fetch --media` the\n * other; neither rewrites the other side's spelling in place.\n */\nimport { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs'\nimport { join, relative } from 'node:path'\nimport { hostedAssetId, localFile, manifestRefs } from '../programAssets'\nimport { safePathName } from './pathName'\nimport { writeFileSafeSync } from './safeWrite'\nimport { downloadMedia, uploadDocRefs } from './media'\nimport type { DocMediaRef } from './media'\nimport type { UploadTarget } from './uploadAsset'\n\ntype Manifest = Record<string, { ref: string | string[] }>\n\nconst isUrl = (ref: string) => /^(https?:|data:|blob:)/.test(ref)\n\n/** The origins whose `/api/assets/<id>/file` is a hosted file of ours. */\nconst homeOrigins = (origin: string) => [\n origin,\n 'https://vos.so',\n 'https://www.vos.so',\n]\n\n/** `assets/.hosted.json`: which hosted file each file brought home is. */\nconst HOME_INDEX = '.hosted.json'\n\n/** A deep copy of the manifest, so a caller's rewrite never reaches disk. */\nfunction cloneManifest(config: Record<string, unknown>): Manifest {\n return JSON.parse(JSON.stringify(config.assets ?? {})) as Manifest\n}\n\nfunction setRef(\n manifest: Manifest,\n name: string,\n index: number | null,\n next: string,\n): void {\n const decl = manifest[name]\n if (index === null) decl.ref = next\n else (decl.ref as string[])[index] = next\n}\n\nconst where = (name: string, index: number | null) =>\n index === null ? `assets.${name}` : `assets.${name}[${index}]`\n\n/**\n * Upload the manifest's local files and point every entry at its hosted\n * asset as `asset:<id>`; a hosted file spelled as a URL takes the same\n * spelling. `config.assets` is REPLACED on the object (the file on disk is\n * untouched), so everything composed from it afterwards carries the hosted\n * refs. Uploads are content-addressed: a re-push sends nothing.\n */\nexport async function uploadManifest(\n config: Record<string, unknown>,\n dir: string,\n target: UploadTarget,\n log: (line: string) => void,\n): Promise<{ uploaded: number }> {\n const refs = manifestRefs(config)\n if (refs.length === 0) return { uploaded: 0 }\n const manifest = cloneManifest(config)\n const local: DocMediaRef[] = []\n for (const { name, index, ref } of refs) {\n const hosted = hostedAssetId(ref, homeOrigins(target.origin))\n if (hosted) {\n setRef(manifest, name, index, `asset:${hosted}`)\n continue\n }\n if (isUrl(ref)) continue\n // A push uploads what the manifest names, so it holds the same line a\n // render does: only a file inside the program's own directory.\n const found = localFile(ref, dir)\n if ('refused' in found) {\n throw new Error(`${where(name, index)} names ${ref}, ${found.refused}`)\n }\n local.push({\n where: where(name, index),\n // The upload helper reads keys against the directory.\n key: relative(realpathSync(dir), found.file),\n set: (url) => {\n const id = hostedAssetId(url, homeOrigins(target.origin))\n setRef(manifest, name, index, id ? `asset:${id}` : url)\n },\n })\n }\n await uploadDocRefs(local, dir, target, log)\n config.assets = manifest\n return { uploaded: local.length }\n}\n\n/** The manifest entries that are still files on this machine. */\nexport function localManifestRefs(config: Record<string, unknown>): string[] {\n return manifestRefs(config)\n .filter(({ ref }) => !isUrl(ref) && !hostedAssetId(ref, homeOrigins('')))\n .map(({ name, index }) => where(name, index))\n}\n\n/**\n * Bring a fetched program's hosted files home: each `asset:<id>` lands as\n * `assets/<name>.<ext>` beside the config (the bytes choose the extension)\n * and its entry is re-pointed at that file.\n *\n * A file already there is kept only when it IS that hosted file\n * (`assets/.hosted.json` remembers which id each one came from). Kept by\n * name alone, a second fetch after someone changed `logo` left the old\n * picture under the new manifest, and the next push stored the old one\n * again without a word.\n *\n * Returns the rewritten manifest, or null when nothing in it is hosted.\n */\nexport async function pullManifest(\n ctx: { origin: string; key: string | null },\n dir: string,\n config: Record<string, unknown>,\n log: (line: string) => void,\n): Promise<{ assets: Manifest; files: string[] } | null> {\n const home = homeOrigins(ctx.origin)\n const hosted = manifestRefs(config).filter(({ ref }) =>\n hostedAssetId(ref, home),\n )\n if (hosted.length === 0) return null\n const manifest = cloneManifest(config)\n const indexPath = join(dir, 'assets', HOME_INDEX)\n let index: Record<string, string> = {}\n try {\n const parsed: unknown = JSON.parse(readFileSync(indexPath, 'utf8'))\n if (parsed && typeof parsed === 'object') {\n index = parsed as Record<string, string>\n }\n } catch {\n // no index yet: nothing here is known to be a hosted file\n }\n const files: string[] = []\n for (const { name, index: at, ref } of hosted) {\n const id = hostedAssetId(ref, home)!\n // The manifest name is the config's word and becomes a file name here,\n // so it is held to a plain path segment (the asset id stands in).\n const stem = safePathName(at === null ? name : `${name}-${at}`, id)\n const known = Object.keys(index).find(\n (f) => f === stem || f.startsWith(`${stem}.`),\n )\n let file: string\n if (\n known &&\n index[known] === id &&\n existsSync(join(dir, 'assets', known))\n ) {\n file = `assets/${known}`\n } else {\n // A different hosted file under this name: the old copy goes first,\n // or a changed extension would leave both lying side by side.\n if (known) {\n rmSync(join(dir, 'assets', known), { force: true })\n delete index[known]\n }\n const got = await downloadMedia(ctx, `/api/assets/${id}/file`, {\n dir,\n subdir: 'assets',\n stem,\n fallbackExt: '',\n describe: `${where(name, at)} (asset ${id})`,\n notFoundHint: 'a private file needs a content key of its owner',\n })\n file = got.file\n index[file.slice('assets/'.length)] = id\n log(\n ` ${file} ← ${where(name, at)}, asset ${id} (${Math.round(got.bytes / 1024)} kB)`,\n )\n }\n files.push(file)\n setRef(manifest, name, at, `./${file}`)\n }\n writeFileSafeSync(indexPath, JSON.stringify(index, null, 2) + '\\n')\n return { assets: manifest, files }\n}\n\nconst HOSTED_FILE_LITERAL =\n /(?:asset:|\\/api\\/assets\\/)([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})/g\nconst FUNCTION_FIELDS = ['setup', 'createContent', 'createTimeline', 'onFrame']\n\n/**\n * A hosted file named inside a function instead of in the manifest. The\n * platform cannot give a string in code a URL per surface, so the file does\n * not reach a render of a private program and does not follow a remix.\n * One line per function and file, in words that say the fix.\n */\nexport function hostedLiteralWarnings(\n config: Record<string, unknown>,\n): string[] {\n const out: string[] = []\n const scan = (label: string, src: unknown) => {\n if (typeof src !== 'string') return\n const seen = new Set<string>()\n for (const m of src.matchAll(HOSTED_FILE_LITERAL)) {\n if (seen.has(m[1])) continue\n seen.add(m[1])\n out.push(\n `${label} names hosted file ${m[1]} in code. Declare it instead: \"assets\": { \"name\": { \"ref\": \"asset:${m[1]}\" } }, and read ctx.assets.name, so it loads on every render and follows a remix`,\n )\n }\n }\n for (const field of FUNCTION_FIELDS) scan(field, config[field])\n const stack = Array.isArray(config.stack) ? config.stack : []\n for (const entry of stack as Record<string, unknown>[]) {\n for (const field of ['setup', 'createContent', 'onFrame']) {\n scan(`stack.${String(entry.id)}.${field}`, entry[field])\n }\n }\n return out\n}\n","/**\n * A name the server chose (a slug, a title, a file name) as a path segment\n * on this disk. The server's word is never trusted as a path: a separator,\n * a `..`, a control character or a leading dot is dropped, everything else\n * collapses to `[A-Za-z0-9._-]`, and a name with nothing left falls back to\n * the id, which is ours to shape.\n */\nexport function safePathName(\n name: string | undefined,\n fallback: string,\n): string {\n const cleaned = (name ?? '')\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f\\u007f]/g, '')\n .split(/[\\\\/]/)\n .filter((part) => part !== '' && part !== '.' && part !== '..')\n .join('-')\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/^[.-]+/, '')\n .replace(/-+$/, '')\n .slice(0, 80)\n if (cleaned) return cleaned\n const id = fallback.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+/, '')\n return id || 'file'\n}\n","import { isKeyedOverlay } from '@vosjs/studio-core'\n/**\n * A hosted take comes home: download the recording (and the\n * mic/cam sidecars when the doc carries them) beside a pulled doc.json, and\n * re-anchor the doc's keys to the local files, so `vos digest`, `frames` and\n * `render` run on a pulled take exactly as on a local one. Also writes the\n * take-dir markers (meta.json, cursor.json) from the doc when absent, so the\n * directory IS a take directory to every other verb.\n *\n * Same consent shape as the upload direction: nothing moves without the verb\n * the user ran (`--media`). A file already on disk is kept, never re-fetched.\n */\nimport { createHash } from 'node:crypto'\nimport { createWriteStream, existsSync } from 'node:fs'\nimport {\n mkdir,\n open,\n readFile,\n readdir,\n rename,\n rm,\n stat,\n} from 'node:fs/promises'\nimport { basename, dirname, join } from 'node:path'\nimport { Readable } from 'node:stream'\nimport { pipeline } from 'node:stream/promises'\nimport { localFile } from '../programAssets'\nimport { refuseSymlink, writeFileSafe } from './safeWrite'\nimport { takeMediaFiles, writeJson } from './take'\nimport {\n MEDIA_HEAD_BYTES,\n extensionFor,\n nameForType,\n resolveMediaType,\n} from './container'\nimport { uploadAsset } from './uploadAsset'\nimport { pullPasteFiles } from './pasteFiles'\nimport { bearerFor, writeSyncState } from './platform'\nimport type { UploadTarget, UploadedAsset } from './uploadAsset'\nimport type {\n AudioClip,\n ObjectClip,\n OverlayClip,\n ProjectDoc,\n} from '@vosjs/studio-core'\n\n/** The names the recorders write their sidecars under. */\nexport const MIC_NAME = 'mic.webm'\nexport const CAM_NAME = 'cam.webm'\n\n/**\n * A take's own media: the stem each one lives under, and the container to\n * assume when the bytes that come back say nothing. The EXTENSION is not part\n * of the identity — a pulled recording wears whatever its hosted asset holds.\n */\nconst SOURCE_MEDIA = [\n { key: 'videoKey', stem: 'recording', fallbackExt: '.webm' },\n { key: 'micKey', stem: 'mic', fallbackExt: '.webm' },\n { key: 'camKey', stem: 'cam', fallbackExt: '.webm' },\n] as const\n\n/** A take-relative media key: neither a URL, a blob, nor a hosted asset path. */\nexport function isTakeRelativeKey(key: string | undefined): key is string {\n // `media:<id>` names a document media (a layer that shows one); the media\n // itself rides the upload door, so the reference is never a file.\n return (\n !!key &&\n !/^(https?:|blob:|data:|media:|\\/\\/)/.test(key) &&\n !key.startsWith('/api/')\n )\n}\n\n/** The file a take-relative key names, under the take directory. */\nexport const takeRelativeFile = (key: string): string => key.replace(/^\\/+/, '')\n\nexport interface DocMediaRef {\n /** Where the key lives, in words (for a log line). */\n where: string\n key: string\n /** Replace the key in the document. */\n set: (next: string) => void\n}\n\n/**\n * A take's own sidecars, when they are local files: the voice (`micKey`)\n * and the webcam (`camKey`). A push used to DROP them (\"local-only\"), while\n * `vos fetch --media` writes them beside the take as `mic.webm` and\n * `cam.webm`: fetch, then push, and the voice and the webcam were gone from\n * the hosted take, in one log line nobody reads.\n */\nexport function sourceSidecarRefs(\n doc: ProjectDoc,\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const [field, where] of [\n ['micKey', 'mic track'],\n ['camKey', 'cam track'],\n ] as const) {\n const key = doc.source[field]\n if (!key || !keep(key)) continue\n out.push({\n where,\n key,\n set: (next) => {\n doc.source[field] = next\n },\n })\n }\n return out\n}\n\n/** The 3D props a document places: each names a model file (a GLB). */\nexport function docObjectRefs(\n doc: { objects?: ObjectClip[] },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const clip of doc.objects ?? []) {\n const asset = clip.asset as { kind?: string; key?: string }\n if (asset.kind !== 'gltf' || !keep(asset.key)) continue\n out.push({\n where: `object ${clip.id}`,\n key: asset.key as string,\n set: (next) => {\n asset.key = next\n },\n })\n }\n return out\n}\n\n/** The picture and video layers a document places, by key. */\nexport function docOverlayRefs(\n doc: { overlays?: OverlayClip[] },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const clip of doc.overlays ?? []) {\n // An HTML layer has no key: its content IS its source. Its only files are\n // the faces it brings (`fonts`), each a url the page fetches, so a face\n // still beside the document crosses like any other layer file.\n if (clip.kind === 'html') {\n for (const [i, font] of (clip.fonts ?? []).entries()) {\n if (!keep(font.url)) continue\n out.push({\n where: `overlay ${clip.id} font ${i}`,\n key: font.url,\n set: (next) => {\n font.url = next\n },\n })\n }\n continue\n }\n if (!isKeyedOverlay(clip) || !keep(clip.key)) continue\n out.push({\n where: `overlay ${clip.id}`,\n key: clip.key,\n set: (next) => {\n clip.key = next\n },\n })\n }\n return out\n}\n\n/**\n * The shared layers either kind of document carries, as files: picture and\n * video overlays, 3D props, and added sound. A program document has no\n * recording, frame or end card, so this is all it can name; a take reads\n * the same three through `docMediaRefs`.\n */\nexport function docLayerRefs(\n doc: {\n overlays?: OverlayClip[]\n objects?: ObjectClip[]\n audio?: AudioClip[]\n },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n return [\n ...docOverlayRefs(doc, keep),\n ...docObjectRefs(doc, keep),\n ...docAudioRefs(doc, keep),\n ]\n}\n\n/**\n * Every media key a document carries beside its recording, mic and cam:\n * image and video overlay clips (a poster's mark), 3D props, the end card's\n * mark, a background image or loop, added sound. The recording's own keys\n * are `KEYS` and handled by their own path. A push rehosts these; a pull\n * brings them home.\n */\nexport function docMediaRefs(\n doc: ProjectDoc,\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n // The take's OTHER media (concat): each recording, and its sidecars,\n // rides the upload door like the primary's; a pull brings them home.\n for (const m of doc.media ?? []) {\n if (keep(m.videoKey))\n out.push({\n where: `media ${m.id}`,\n key: m.videoKey,\n set: (next) => {\n m.videoKey = next\n },\n })\n const mic = m.micKey\n if (mic && keep(mic))\n out.push({\n where: `media ${m.id} mic`,\n key: mic,\n set: (next) => {\n m.micKey = next\n },\n })\n const cam = m.camKey\n if (cam && keep(cam))\n out.push({\n where: `media ${m.id} cam`,\n key: cam,\n set: (next) => {\n m.camKey = next\n },\n })\n }\n out.push(...docOverlayRefs(doc, keep))\n // A prop's model was never walked: a take with a GLB pushed a document\n // whose object keyed a file only the pusher's disk held.\n out.push(...docObjectRefs(doc, keep))\n const mark = doc.endCard?.mark\n if (mark && keep(mark.key))\n out.push({\n where: 'end card mark',\n key: mark.key,\n set: (next) => {\n mark.key = next\n },\n })\n const bg = doc.frame.backgroundMedia\n if (bg && keep(bg.key))\n out.push({\n where: 'background',\n key: bg.key,\n set: (next) => {\n bg.key = next\n },\n })\n out.push(...docAudioRefs(doc, keep))\n return out\n}\n\n/**\n * The sound a document adds (`doc.audio`: a score, a voice-over, an effect),\n * for either kind of document. A take reads it through `docMediaRefs`; a\n * program document, which has no recording or frame, reads it here. A\n * catalog track is an absolute URL and is never walked.\n */\nexport function docAudioRefs(\n doc: { audio?: AudioClip[] },\n keep: (key: string | undefined) => boolean = isTakeRelativeKey,\n): DocMediaRef[] {\n const out: DocMediaRef[] = []\n for (const clip of doc.audio ?? []) {\n if (!keep(clip.key)) continue\n out.push({\n where: `audio ${clip.id}`,\n key: clip.key,\n set: (next) => {\n // The same bytes under a new name (uploaded, or brought home): a\n // measurement taken for the old name holds for the new one.\n if (clip.loudness?.src === clip.key) clip.loudness.src = next\n clip.key = next\n },\n })\n }\n return out\n}\n\n/**\n * Upload the files a document names and point its keys at the hosted\n * assets. Each file rides the content-addressed upload door, so a re-push\n * reuses it; the bytes name the type, and the uploaded filename is corrected\n * to match so the asset is never self-contradictory. A key whose file is\n * missing is said and left as it is (the hosted render will 404 on it,\n * honestly). Take pushes and program pushes share it.\n */\nexport async function uploadDocRefs(\n refs: DocMediaRef[],\n dir: string,\n target: UploadTarget,\n log: (line: string) => void,\n): Promise<void> {\n for (const ref of refs) {\n // The key names a file in the take directory or below, never outside\n // it: a fetched or pasted document is not trusted with the disk.\n const local = localFile(takeRelativeFile(ref.key), dir)\n if ('refused' in local) {\n log(\n `warning ${ref.where}: ${ref.key} is not beside the document, so it was not uploaded`,\n )\n continue\n }\n const file = local.file\n const media = await readFile(file)\n const mediaHash = createHash('sha256').update(media).digest('hex')\n const { type } = resolveMediaType({\n head: media.subarray(0, MEDIA_HEAD_BYTES),\n filename: file,\n })\n const name = nameForType(basename(file), type)\n if (name !== basename(file)) {\n log(` ${ref.where}: ${basename(file)} holds ${type} — sent as ${name}`)\n }\n let put: UploadedAsset\n try {\n put = await uploadAsset(target, new Uint8Array(media), {\n filename: name,\n contentType: type,\n contentHash: mediaHash,\n })\n } catch (e) {\n throw new Error(\n `${ref.where} upload failed: ${e instanceof Error ? e.message : String(e)}`,\n )\n }\n ref.set(put.url)\n log(\n ` ${ref.where}: ${ref.key} → asset ${put.id}${put.reused ? ' (reused)' : ''}`,\n )\n for (const note of put.notes) log(` ${ref.where}: ${note}`)\n }\n}\n\n// The extension↔type table and the byte sniffer live in `container.ts`; these\n// stay the door every media caller already knows.\nexport { extensionFor, mediaContentType } from './container'\n\n/** An asset file URL (relative `/api/assets/{id}/file` or absolute). */\nexport function assetIdOf(url: string | undefined): string | null {\n if (!url) return null\n const m = /\\/api\\/assets\\/([A-Za-z0-9_-]+)\\/file/.exec(url)\n return m ? m[1] : null\n}\n\nexport interface MediaPullResult {\n downloaded: { file: string; assetId: string; bytes: number }[]\n kept: string[]\n /** Every hosted file now beside the take, downloaded or kept: its path\n * here and the address it came from. */\n hosted: { file: string; url: string }[]\n}\n\n/** The first bytes of a file, enough for every container signature. */\nexport async function readMediaHead(file: string): Promise<Uint8Array> {\n const handle = await open(file, 'r')\n try {\n const buf = Buffer.alloc(MEDIA_HEAD_BYTES)\n const { bytesRead } = await handle.read(buf, 0, MEDIA_HEAD_BYTES, 0)\n return new Uint8Array(buf.subarray(0, bytesRead))\n } finally {\n await handle.close()\n }\n}\n\nexport interface DownloadTarget {\n dir: string\n /** Directory under the take dir, when the file is filed away. */\n subdir?: string\n /** The name without its extension — the extension is the BYTES' to choose. */\n stem: string\n /** The container to assume when the bytes say nothing. */\n fallbackExt: string\n /** What is being fetched, in words, for an error or a log line. */\n describe: string\n /** Added to a 404, where the reason is usually the credential. */\n notFoundHint?: string\n}\n\n/**\n * One media file, brought home under the name its BYTES earn.\n *\n * Streamed to a `.part` sibling first, then sniffed and renamed, so a take\n * never holds a file whose extension lies — whatever the server declared, and\n * whatever the hosted asset happened to be called. A push of a pulled take\n * re-uploads that file, so a wrong name here becomes a wrong Content-Type on\n * the asset, and a render that dies inside the video element.\n */\n/**\n * Where a document key is fetched from, and whether the content key rides\n * along. The key goes ONLY to the configured origin: a hosted file named\n * as a file-door address on some other host is read from the home door by\n * its id, and any other URL is fetched bare, so a stranger's document\n * can never send the reader's credential elsewhere.\n */\nexport function downloadPlan(\n ctx: { origin: string },\n url: string,\n): { url: string; bearer: boolean } {\n const home = new URL(ctx.origin).origin\n if (!/^https?:/.test(url)) return { url: `${home}${url}`, bearer: true }\n let abs: URL\n try {\n abs = new URL(url)\n } catch {\n return { url, bearer: false }\n }\n if (abs.origin === home) return { url: abs.href, bearer: true }\n const id = assetIdOf(url)\n if (id) return { url: `${home}/api/assets/${id}/file`, bearer: true }\n return { url: abs.href, bearer: false }\n}\n\nexport async function downloadMedia(\n ctx: { origin: string; key: string | null },\n url: string,\n target: DownloadTarget,\n): Promise<{ file: string; bytes: number }> {\n const plan = downloadPlan(ctx, url)\n const res = await fetch(plan.url, {\n headers: plan.bearer ? bearerFor(ctx.key, plan.url) : {},\n })\n if (!res.ok || !res.body) {\n throw new Error(\n `download of ${target.describe} failed (${res.status})${\n res.status === 404 && target.notFoundHint\n ? ` — ${target.notFoundHint}`\n : ''\n }`,\n )\n }\n const into = target.subdir ? join(target.dir, target.subdir) : target.dir\n const partial = join(into, `${target.stem}.part`)\n await mkdir(dirname(partial), { recursive: true })\n // A link already at the path would carry the bytes wherever it points.\n await refuseSymlink(partial)\n try {\n await pipeline(\n Readable.fromWeb(res.body as never),\n createWriteStream(partial),\n )\n const { type } = resolveMediaType({\n head: await readMediaHead(partial),\n declared: res.headers.get('content-type'),\n filename: `${target.stem}${target.fallbackExt}`,\n })\n const name = `${target.stem}${extensionFor(type) || target.fallbackExt}`\n await refuseSymlink(join(into, name))\n await rename(partial, join(into, name))\n const file = target.subdir ? `${target.subdir}/${name}` : name\n return { file, bytes: (await stat(join(target.dir, file))).size }\n } finally {\n // A rename leaves nothing behind; a failed transfer must not.\n await rm(partial, { force: true })\n }\n}\n\nexport async function pullMedia(\n ctx: { origin: string; key: string | null },\n dir: string,\n doc: ProjectDoc,\n log: (line: string) => void,\n): Promise<MediaPullResult> {\n const result: MediaPullResult = { downloaded: [], kept: [], hosted: [] }\n for (const { key, stem, fallbackExt } of SOURCE_MEDIA) {\n const url = doc.source[key]\n const assetId = assetIdOf(url)\n if (!assetId) continue\n // A file already here is kept whatever container it wears, so a second\n // pull never re-fetches and never leaves two of the same footage.\n const present = takeMediaFiles(dir, stem)[0]\n if (present) {\n result.kept.push(basename(present))\n result.hosted.push({ file: basename(present), url: url! })\n doc.source[key] = basename(present)\n continue\n }\n const got = await downloadMedia(ctx, url!, {\n dir,\n stem,\n fallbackExt,\n describe: stem,\n notFoundHint: 'a private recording needs a content key of its owner',\n })\n result.downloaded.push({ file: got.file, assetId, bytes: got.bytes })\n log(` ${got.file} ← asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`)\n result.hosted.push({ file: got.file, url: url! })\n doc.source[key] = got.file\n }\n // The document's other media (a poster's mark, a pasted ground): a hosted\n // asset comes home under media/<assetId>.<ext> and the key re-anchors to\n // it, so the pulled poster renders and re-pushes as a whole.\n for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {\n const assetId = assetIdOf(ref.key)!\n const existing = existsSync(join(dir, 'media'))\n ? (await readdir(join(dir, 'media'))).find(\n (f) => f === assetId || f.startsWith(`${assetId}.`),\n )\n : undefined\n if (existing) {\n const file = `media/${existing}`\n result.kept.push(file)\n result.hosted.push({ file, url: ref.key })\n ref.set(file)\n continue\n }\n const got = await downloadMedia(ctx, ref.key, {\n dir,\n subdir: 'media',\n stem: assetId,\n // A type with no extension here (an svg served as such) keeps the bare\n // asset id, exactly as before.\n fallbackExt: '',\n describe: `${ref.where} (${ref.key})`,\n })\n result.downloaded.push({ file: got.file, assetId, bytes: got.bytes })\n log(\n ` ${got.file} ← ${ref.where}, asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`,\n )\n result.hosted.push({ file: got.file, url: ref.key })\n ref.set(got.file)\n }\n // The files the take's script pastes come home beside it, under the\n // names it pastes them by, so the take records again from here.\n const pasted = await pullPasteFiles(ctx, dir, doc, log)\n for (const w of pasted.written) {\n const assetId = assetIdOf(w.url)\n if (assetId)\n result.downloaded.push({ file: w.file, assetId, bytes: w.bytes })\n }\n for (const h of pasted.hosted) result.hosted.push(h)\n result.kept.push(...pasted.kept)\n // The take-dir markers: meta.json and cursor.json live on the doc for a\n // hosted take; write them out so loadTake() recognizes the directory.\n const metaPath = join(dir, 'meta.json')\n if (!existsSync(metaPath)) await writeJson(metaPath, doc.source.meta, true)\n const cursorPath = join(dir, 'cursor.json')\n if (!existsSync(cursorPath)) await writeJson(cursorPath, doc.source.cursor)\n await writeFileSafe(join(dir, 'doc.json'), JSON.stringify(doc, null, 2))\n return result\n}\n\n/** The sha256 of a file beside the take, hex. */\nexport async function fileSha256(file: string): Promise<string> {\n return createHash('sha256')\n .update(await readFile(file))\n .digest('hex')\n}\n\n/**\n * Write what a pull brought home into vos.json (`hostedMedia`): each file's\n * path here, the address it came from, and its sha256 now. A claimable push\n * of the take names those originals instead of uploading, and only while\n * the bytes are the ones that were fetched.\n */\nexport async function recordHostedMedia(\n dir: string,\n vosId: string,\n pulled: MediaPullResult,\n): Promise<void> {\n if (pulled.hosted.length === 0) return\n const hostedMedia: Record<string, { ref: string; sha256: string }> = {}\n for (const { file, url } of pulled.hosted) {\n hostedMedia[file] = { ref: url, sha256: await fileSha256(join(dir, file)) }\n }\n writeSyncState(dir, { vosId, hostedMedia })\n}\n","/**\n * The take directory — the CLI's unit of work, human-inspectable on purpose:\n * take/\n * recording.webm encoded footage (CFR; .mp4 when a pull brought it home\n * in that container, so readers RESOLVE the name)\n * frames/ raw screencast JPEGs + frames.json (kept for re-encode)\n * cursor.json synthesized CursorTrack\n * meta.json RecordingMeta (producer: 'cli')\n * actions.json the script that produced it (replay recipe)\n * doc.json ProjectDoc — the agent-editable surface\n */\nimport { createHash } from 'node:crypto'\nimport { mkdir, readFile, rename, rm } from 'node:fs/promises'\nimport { existsSync, readdirSync, statSync } from 'node:fs'\nimport { writeFileSafe } from './safeWrite'\nimport { basename, join } from 'node:path'\nimport { TAKE_MEDIA_EXTENSIONS } from './container'\nimport type { CursorTrack, ProjectDoc, RecordingMeta } from '@vosjs/studio-core'\nimport type { ActionsFile } from './actions'\n\n/** The name `record` writes its footage under — its encoder emits WebM. */\nexport const RECORDING_NAME = 'recording.webm'\n\n/**\n * A take's own media under `dir`, whatever container it came home in.\n *\n * `record` writes WebM, but a PULLED take wears the container its hosted asset\n * actually holds, so nothing may spell `recording.webm` at a read site: a take\n * whose footage is mp4 must still render, digest and push. Ordered by\n * `TAKE_MEDIA_EXTENSIONS`, so the answer is deterministic if a stale sibling\n * survives.\n */\nexport function takeMediaFiles(dir: string, stem: string): string[] {\n return TAKE_MEDIA_EXTENSIONS.map((ext) => join(dir, `${stem}${ext}`)).filter(\n (file) => existsSync(file),\n )\n}\n\n/**\n * The one file a take's media stem names, falling back to what `record` would\n * write — so a caller can `existsSync` the answer and get an honest\n * \"no footage here\" rather than a path that never existed.\n */\nexport function takeMediaFile(\n dir: string,\n stem: string,\n fallback: string,\n): string {\n return takeMediaFiles(dir, stem)[0] ?? join(dir, fallback)\n}\n\n/** The previous cut, preserved by a re-record. */\nexport const PREV_DOC_NAME = 'doc.prev.json'\n/**\n * The hash of the doc.json a recording planned, beside the take. A doc.json\n * that still matches it was never edited, so a re-record keeps the cut\n * already in doc.prev.json instead of putting that plan over it.\n */\nexport const PLANNED_HASH_NAME = '.doc-planned'\n\nconst sha256 = (text: string): string =>\n createHash('sha256').update(text).digest('hex')\n\n/** Note the doc.json a recording just planned (see PLANNED_HASH_NAME). */\nexport async function notePlannedDoc(dir: string): Promise<void> {\n const doc = takePaths(dir).doc\n if (!existsSync(doc)) return\n await writeFileSafe(\n join(dir, PLANNED_HASH_NAME),\n sha256(await readFile(doc, 'utf8')),\n )\n}\n\n/** Is the take's doc.json exactly the plan its recording wrote? */\nasync function docIsUntouchedPlan(dir: string): Promise<boolean> {\n const mark = join(dir, PLANNED_HASH_NAME)\n const doc = takePaths(dir).doc\n if (!existsSync(mark) || !existsSync(doc)) return false\n const [want, have] = await Promise.all([\n readFile(mark, 'utf8'),\n readFile(doc, 'utf8'),\n ])\n return want.trim() === sha256(have)\n}\n\nexport interface TakePaths {\n dir: string\n recording: string\n /** The recording's bare filename — what a doc key or a served URL spells. */\n recordingName: string\n framesDir: string\n framesIndex: string\n cursor: string\n meta: string\n actions: string\n doc: string\n}\n\nexport function takePaths(dir: string): TakePaths {\n const recording = takeMediaFile(dir, 'recording', RECORDING_NAME)\n return {\n dir,\n recording,\n recordingName: basename(recording),\n framesDir: join(dir, 'frames'),\n framesIndex: join(dir, 'frames.json'),\n cursor: join(dir, 'cursor.json'),\n meta: join(dir, 'meta.json'),\n actions: join(dir, 'actions.json'),\n doc: join(dir, 'doc.json'),\n }\n}\n\nexport async function ensureTakeDir(dir: string): Promise<TakePaths> {\n const p = takePaths(dir)\n await mkdir(p.framesDir, { recursive: true })\n return p\n}\n\n/**\n * Drop the screencast JPEGs once the recording they were encoded into is on\n * disk. They are the bulk of a take (one hero take: 437 MB of a 452 MB\n * directory) and nothing needs them afterwards: the digest reads frames\n * from them WHEN PRESENT and from the video otherwise, which is the path\n * every pulled take already runs on. Refuses unless the recording exists and\n * has bytes, so a failed encode never costs the only copy of the footage.\n * Returns the bytes freed, 0 when it left everything alone.\n */\nexport async function dropScreencastFrames(p: TakePaths): Promise<number> {\n if (!existsSync(p.recording) || statSync(p.recording).size === 0) return 0\n if (!existsSync(p.framesDir)) return 0\n let freed = 0\n for (const name of readdirSync(p.framesDir)) {\n try {\n freed += statSync(join(p.framesDir, name)).size\n } catch {\n // a file gone between the listing and the stat frees nothing\n }\n }\n await rm(p.framesDir, { recursive: true, force: true })\n await rm(p.framesIndex, { force: true })\n return freed\n}\n\n/**\n * Re-record into an existing take WITHOUT destroying the cut (the\n * re-render loop's first rung; `rm -rf` here is what made \"re-renders on the\n * next version\" a false claim). What the old footage produced is cleared\n * (recording, cursor, meta, raw frames, stills/, digest/ — all derived and\n * regenerable); what the human's work lives in survives: `doc.json` moves to\n * `doc.prev.json` — the explicit reuse base `vos plan --reuse` re-anchors\n * onto the new recording — and `actions.json` + `vos.json` stay put. The doc\n * is MOVED, never left in place: a doc.json embeds its footage's cursor and\n * meta (`doc.source`), so leaving it beside new footage would be a document\n * lying about its source, and the auto-plan after recording would replan\n * from the OLD cursor. Returns what was kept, for the terminal to say.\n */\nexport async function prepareReRecord(\n dir: string,\n): Promise<{ prevDoc: boolean; kept: string[] }> {\n const p = takePaths(dir)\n const kept: string[] = []\n let prevDoc = false\n const prevPath = join(dir, PREV_DOC_NAME)\n if (existsSync(prevPath) && (await docIsUntouchedPlan(dir))) {\n // The last recording's plan was never edited: the cut a person or an\n // agent made is still the one in doc.prev.json, and it stays there.\n await rm(p.doc, { force: true })\n prevDoc = true\n kept.push(PREV_DOC_NAME)\n } else if (existsSync(p.doc)) {\n await rename(p.doc, prevPath)\n prevDoc = true\n kept.push(PREV_DOC_NAME)\n }\n await rm(join(dir, PLANNED_HASH_NAME), { force: true })\n if (existsSync(p.actions)) kept.push('actions.json')\n if (existsSync(join(dir, 'vos.json'))) kept.push('vos.json')\n for (const stale of [\n // Every container, not just the one `record` writes: a pulled take's\n // footage may be mp4, and a survivor would outrank the new recording.\n ...takeMediaFiles(dir, 'recording'),\n ...takeMediaFiles(dir, 'mic'),\n ...takeMediaFiles(dir, 'cam'),\n p.cursor,\n p.meta,\n p.framesIndex,\n p.framesDir,\n join(dir, 'stills'),\n join(dir, 'digest'),\n // What the last run's download steps kept: this run downloads again.\n join(dir, 'outputs'),\n // The sound the last run's page played: this run hears it again.\n join(dir, 'page-sound'),\n ]) {\n await rm(stale, { recursive: true, force: true })\n }\n return { prevDoc, kept }\n}\n\nexport async function readJson<T>(file: string): Promise<T> {\n return JSON.parse(await readFile(file, 'utf8')) as T\n}\n\nexport async function writeJson(\n file: string,\n value: unknown,\n pretty = false,\n): Promise<void> {\n await writeFileSafe(\n file,\n pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value),\n )\n}\n\nexport interface TakeData {\n paths: TakePaths\n cursor: CursorTrack\n meta: RecordingMeta\n actions: ActionsFile | null\n doc: ProjectDoc | null\n}\n\n/** Load a take directory; cursor+meta are required, doc/actions optional. */\nexport async function loadTake(dir: string): Promise<TakeData> {\n const paths = takePaths(dir)\n if (!existsSync(paths.meta)) {\n throw new Error(\n `${dir} is not a take directory (missing meta.json) — run record first`,\n )\n }\n const meta = await readJson<RecordingMeta>(paths.meta)\n const cursor = existsSync(paths.cursor)\n ? await readJson<CursorTrack>(paths.cursor)\n : []\n const actions = existsSync(paths.actions)\n ? await readJson<ActionsFile>(paths.actions)\n : null\n const doc = existsSync(paths.doc)\n ? await readJson<ProjectDoc>(paths.doc)\n : null\n return { paths, cursor, meta, actions, doc }\n}\n","/**\n * Uploading a file to vos.so: one door, for every kind and every size.\n *\n * The file is declared first, sent as parts, and sealed. Three reasons it\n * is never one request:\n *\n * - A request body has a ceiling the platform does not set and cannot\n * raise from inside a handler: the edge refuses anything past it before\n * the request arrives, with its own error page rather than a worded\n * refusal. A screen recording of any real length clears that ceiling.\n * - Everything the platform may refuse a file for (its kind, its size, a\n * daily rate, the account's storage) is weighed at the declaring call,\n * before a byte moves, so a refusal costs nothing and arrives in words.\n * - The declaring call is content-addressed: the same bytes pushed twice\n * are recognised and never re-sent, which is what keeps an iterating\n * push from re-uploading a take on every round.\n *\n * The part size is NOT decided here. The platform returns it from the\n * declaring call, so the split can change server-side without a CLI\n * release.\n *\n * What the file IS comes back from the sealing call: the platform reads\n * the stored bytes and answers its kind, the name it is stored under and\n * anything worth saying about it. Nothing here decides what is accepted;\n * `GET /api/limits` lists the kinds and their caps.\n */\nimport { apiJson } from './platform'\nimport type { ApiResult } from './platform'\n\n/** Attempts per part. A part commits nothing until the upload is sealed,\n * so it is always safe to send again. */\nconst PART_ATTEMPTS = 3\n\nexport interface PartPlan {\n /** Part numbers are 1-based. */\n partNumber: number\n start: number\n /** One past the last byte. */\n end: number\n}\n\n/**\n * The slices to send, in order. Every part but the last is exactly\n * `partBytes`; the last is the remainder. The platform holds each part to\n * exactly this length, so the split has to agree with the size that was\n * declared — which is why `partBytes` comes from the platform rather than\n * from a constant here.\n */\nexport function planParts(totalBytes: number, partBytes: number): PartPlan[] {\n if (totalBytes <= 0 || partBytes <= 0) return []\n const parts: PartPlan[] = []\n for (let start = 0; start < totalBytes; start += partBytes) {\n parts.push({\n partNumber: parts.length + 1,\n start,\n end: Math.min(start + partBytes, totalBytes),\n })\n }\n return parts\n}\n\nexport interface UploadedAsset {\n id: string\n /** The path a document names the file by. */\n url: string\n size: number\n /** The platform already had these exact bytes; nothing was sent. */\n reused: boolean\n /** What the bytes are, as the platform read them (video, audio, image,\n * vector, model, font, hdr, recipe, captions). */\n kind: string\n /** The name it is stored under: the extension follows the bytes. */\n filename: string\n /** The frontmatter summary of a recipe, a picture's size, a clip's codec. */\n metadata: Record<string, unknown> | null\n /** What the platform did to the file or wants said about it. */\n notes: string[]\n /** The reference a program's manifest takes. */\n ref: string\n /** Seconds, for what plays. */\n duration: number | null\n}\n\nexport interface UploadTarget {\n origin: string\n /** The bearer: a content key, or a claimable push's session token. */\n key: string\n /**\n * The door's path. The account's by default; a claimable push sends its\n * files through `/api/claim/uploads/files`, which speaks the same\n * protocol under its session token.\n */\n door?: string\n}\n\nexport interface UploadOptions {\n filename: string\n /** Only a hint: the platform types a file by its bytes. It settles the\n * one thing bytes cannot, a WebM that holds sound alone. */\n contentType?: string\n /** sha256 hex — the platform dedupes on it, per owner. */\n contentHash: string\n /** The take's length, so a refusal for one past the hosted cap arrives\n * before anything is sent. The platform measures the file either way. */\n durationSeconds?: number\n /**\n * The bytes are about to be sent: the platform does not hold them yet.\n * Never called for bytes it already holds, so a caller says \"uploading\"\n * here and never before.\n */\n onSending?: (parts: number) => void\n /** Parts landed, for a caller that reports a long upload's progress. */\n onPart?: (done: number, total: number) => void\n /** File the asset into one of the caller's folders. */\n folderId?: string\n /**\n * `library`: a file added to keep (`vos asset push`, a recipe).\n * `attached` (the default): a file a document is about to name, which\n * the platform collects if nothing ever does.\n */\n intent?: 'library' | 'attached'\n /**\n * Where the file came from, as the uploader states it: kept beside the\n * file and shown with it. The platform does not check it.\n */\n provenance?: { sourceUrl?: string; license?: string; attribution?: string }\n}\n\nfunction failed(what: string, r: ApiResult): Error {\n const detail = typeof r.body.error === 'string' ? r.body.error : ''\n const hint = typeof r.body.hint === 'string' ? ` ${r.body.hint}` : ''\n return new Error(`${what} (${r.status})${detail ? `: ${detail}${hint}` : ''}`)\n}\n\nfunction asAsset(\n body: Record<string, unknown>,\n reused: boolean,\n): UploadedAsset {\n const asset = (body.asset ?? {}) as Record<string, unknown>\n return {\n id: String(asset.id),\n url: String(asset.fileUrl),\n size: typeof asset.size === 'number' ? asset.size : 0,\n reused,\n kind: typeof asset.kind === 'string' ? asset.kind : '',\n filename: typeof asset.filename === 'string' ? asset.filename : '',\n metadata:\n asset.metadata && typeof asset.metadata === 'object'\n ? (asset.metadata as Record<string, unknown>)\n : null,\n notes: Array.isArray(body.notes)\n ? body.notes.filter((n): n is string => typeof n === 'string')\n : [],\n ref:\n typeof asset.ref === 'string' ? asset.ref : `asset:${String(asset.id)}`,\n duration: typeof asset.duration === 'number' ? asset.duration : null,\n }\n}\n\n/** Worth sending again: the platform was busy or briefly unreachable. A\n * refusal is a decision and will read the same on a second try. */\nfunction transient(status: number): boolean {\n return status >= 500 || status === 429\n}\n\nconst wait = (ms: number) => new Promise((r) => setTimeout(r, ms))\n\n/** Upload one file as an owned asset: declare it, send its parts, seal it. */\nexport async function uploadAsset(\n target: UploadTarget,\n bytes: Uint8Array,\n opts: UploadOptions,\n): Promise<UploadedAsset> {\n // Declare it first. Every quota is weighed here, before a byte moves, so\n // a refusal costs nothing and arrives in words — and the dedupe answers\n // here too, which is what makes re-pushing a hosted take free.\n const door = target.door ?? '/api/assets/uploads'\n const begun = await apiJson(target.origin, door, {\n method: 'POST',\n key: target.key,\n body: {\n filename: opts.filename,\n ...(opts.contentType ? { contentType: opts.contentType } : {}),\n size: bytes.length,\n sha256: opts.contentHash,\n ...(opts.durationSeconds && opts.durationSeconds > 0\n ? { durationSeconds: opts.durationSeconds }\n : {}),\n ...(opts.folderId ? { folderId: opts.folderId } : {}),\n ...(opts.intent ? { intent: opts.intent } : {}),\n },\n })\n if (begun.status !== 201 && begun.status !== 200) {\n throw failed('upload failed', begun)\n }\n // The platform already holds these bytes: nothing to send.\n if (begun.body.asset) return asAsset(begun.body, true)\n\n const uploadId = String(begun.body.uploadId)\n const partBytes = Number(begun.body.partBytes)\n if (!Number.isFinite(partBytes) || partBytes <= 0) {\n throw new Error('upload failed: the platform returned no part size')\n }\n const parts = planParts(bytes.length, partBytes)\n const etags: { partNumber: number; etag: string }[] = []\n opts.onSending?.(parts.length)\n\n try {\n for (const part of parts) {\n const chunk = bytes.subarray(part.start, part.end)\n let sent: ApiResult | null = null\n for (let attempt = 1; attempt <= PART_ATTEMPTS; attempt++) {\n try {\n sent = await apiJson(\n target.origin,\n `${door}/${uploadId}/parts/${part.partNumber}`,\n {\n method: 'PUT',\n key: target.key,\n headers: {\n 'Content-Type': 'application/octet-stream',\n 'Content-Length': String(chunk.length),\n },\n raw: chunk,\n },\n )\n } catch (networkError) {\n // Dropped connection, a sleeping machine: worth another try.\n if (attempt === PART_ATTEMPTS) throw networkError\n await wait(500 * 2 ** (attempt - 1))\n continue\n }\n if (sent.status === 200) break\n if (!transient(sent.status) || attempt === PART_ATTEMPTS) {\n throw failed(`part ${part.partNumber} failed`, sent)\n }\n await wait(500 * 2 ** (attempt - 1))\n }\n if (!sent || sent.status !== 200) {\n throw new Error(`part ${part.partNumber} failed`)\n }\n etags.push({\n partNumber: Number(sent.body.partNumber),\n etag: String(sent.body.etag),\n })\n opts.onPart?.(etags.length, parts.length)\n }\n\n // Sealing is where the platform reads the file. A refusal here means\n // the bytes are not what the name said, and it has already taken them\n // back out.\n const sealed = await apiJson(\n target.origin,\n `${door}/${uploadId}/complete`,\n {\n method: 'POST',\n key: target.key,\n body: {\n parts: etags,\n ...(opts.provenance ? { provenance: opts.provenance } : {}),\n },\n },\n )\n if (sealed.status !== 201 && sealed.status !== 200) {\n throw failed('upload failed', sealed)\n }\n // The platform hashes what it stored: bytes this account already held\n // come back as the file it had, whatever was declared.\n return asAsset(sealed.body, sealed.body.reused === true)\n } catch (err) {\n // Hand the parts back rather than leaving them held server-side.\n await apiJson(target.origin, `${door}/${uploadId}`, {\n method: 'DELETE',\n key: target.key,\n }).catch(() => undefined)\n throw err\n }\n}\n","/**\n * A pasted file travels with its take. A `paste` step names a file beside\n * the script (the take keeps it as `paste-<name>`), and the script rides\n * the pushed document (`source.script`), so a take fetched from vos.so came\n * home with a script naming a file that was not there: a re-record from\n * anywhere but the recording machine refused at its first paste.\n *\n * So `vos push` sends each paste file with the take and the document lists\n * them (`source.scriptFiles`: the name the script uses → where the bytes\n * are), and `vos fetch --media` / `vos pull --media` write each one back\n * beside `actions.json` under that name. A picture (PNG, JPEG, WebP, GIF,\n * SVG) rides the same upload door as every other file the document names;\n * a text file (words, HTML, JSON, CSV) is not a kind the platform stores,\n * so it rides inside the document as a `data:` address, at most\n * `SCRIPT_TEXT_MAX_BYTES`. A drag's path needs nothing of this: the\n * recorder writes its points inline into the take's script.\n *\n * Every name is a plain file name the script itself pastes, read from the\n * take directory or below (never through a link out of it), and written\n * back only where nothing is: a file on disk is the person's.\n */\nimport { createHash } from 'node:crypto'\nimport { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport {\n fileNameProblem,\n pasteTypeOf,\n scriptFiles,\n} from '@vosjs/render-core/record'\nimport { localFile } from '../programAssets'\nimport { assetIdOf, downloadPlan } from './media'\nimport { bearerFor } from './platform'\nimport { writeFileSafe } from './safeWrite'\nimport { uploadAsset } from './uploadAsset'\nimport type { UploadTarget } from './uploadAsset'\nimport type { ProjectDoc } from '@vosjs/studio-core'\n\n/** The most bytes a pasted TEXT file carries inside a document. */\nexport const SCRIPT_TEXT_MAX_BYTES = 256 * 1024\n\n/** The files a stored script pastes, by the plain name it uses, once each. */\nexport function pasteNames(script: unknown): string[] {\n if (!script || typeof script !== 'object') return []\n const steps = (script as { steps?: unknown }).steps\n if (!Array.isArray(steps)) return []\n const names = scriptFiles({ steps: steps as never })\n .filter((f) => f.as === 'paste')\n .map((f) => f.name)\n return [...new Set(names)]\n}\n\n/** A refusal the person can fix in the take: exit 2, said before any upload. */\nexport class PasteFileRefusal extends Error {}\n\n/** A paste file found beside the take: its bytes, or why it is not there. */\ntype Found =\n | { name: string; bytes: Uint8Array; type: string }\n | { name: string; missing: true }\n\n/**\n * Read every file the take's script pastes, before anything is sent. A\n * name that is not plain, or a file that resolves outside the take\n * directory (a link out of it), is REFUSED in words: the push would carry\n * bytes from somewhere the take does not hold. A file that is not there is\n * only missing (a take fetched before paste files travelled): said, and\n * the push goes on without it.\n */\nexport async function readPasteFiles(\n script: unknown,\n dir: string,\n): Promise<Found[]> {\n const out: Found[] = []\n for (const name of pasteNames(script)) {\n const bad = fileNameProblem(name)\n if (bad)\n throw new PasteFileRefusal(\n `actions.json pastes \"${name}\", which ${bad.replace(/^\"[^\"]*\" /, '')}: a pasted file sits beside the script, in the take directory`,\n )\n if (!existsSync(join(dir, name))) {\n out.push({ name, missing: true })\n continue\n }\n const local = localFile(name, dir)\n if ('refused' in local)\n throw new PasteFileRefusal(\n `actions.json pastes ${name}, which resolves outside the take directory (${dir}), so the push would send a file the take does not hold. Copy the file into the take as ${name} (not a link) and push again`,\n )\n out.push({\n name,\n bytes: new Uint8Array(await readFile(local.file)),\n type: pasteTypeOf(name) ?? 'application/octet-stream',\n })\n }\n return out\n}\n\n/** Bytes as a `data:` address of the given type. */\nconst dataUrl = (type: string, bytes: Uint8Array): string =>\n `data:${type};base64,${Buffer.from(bytes).toString('base64')}`\n\n/**\n * Send the paste files with the take and list them on the document about\n * to be pushed (`source.scriptFiles`). A picture rides the upload door; a\n * text file rides the document. A missing file is said and left out, so\n * a re-record of the fetched take names what to put back.\n */\nexport async function sendPasteFiles(\n doc: ProjectDoc,\n found: Found[],\n target: UploadTarget,\n log: (line: string) => void,\n): Promise<void> {\n const listed: Record<string, string> = {}\n // A file the take no longer holds but the document already lists (it\n // came with a fetch, and was moved aside since) keeps its listing.\n const prior = Object.fromEntries(listedFiles(doc))\n for (const f of found) {\n if ('missing' in f && prior[f.name]) {\n listed[f.name] = prior[f.name]\n log(` pasted file ${f.name}: not in the take, kept as listed`)\n continue\n }\n if ('missing' in f) {\n log(\n ` warning: actions.json pastes ${f.name}, which is not in the take, so it does not travel with the push: a re-record of the pushed take needs it put beside actions.json again`,\n )\n continue\n }\n if (!f.type.startsWith('image/')) {\n if (f.bytes.length > SCRIPT_TEXT_MAX_BYTES)\n throw new PasteFileRefusal(\n `actions.json pastes ${f.name} (${Math.round(f.bytes.length / 1024)} kB), and a pasted text file rides inside the document, at most ${SCRIPT_TEXT_MAX_BYTES / 1024} kB: paste less, or paste the words as text in the step`,\n )\n listed[f.name] = dataUrl(f.type, f.bytes)\n log(` pasted file ${f.name}: carried in the document`)\n continue\n }\n let put\n try {\n put = await uploadAsset(target, f.bytes, {\n filename: f.name,\n contentType: f.type,\n contentHash: createHash('sha256').update(f.bytes).digest('hex'),\n })\n } catch (e) {\n throw new Error(\n `pasted file ${f.name} upload failed: ${e instanceof Error ? e.message : String(e)}`,\n )\n }\n listed[f.name] = put.url\n log(\n ` pasted file ${f.name} → asset ${put.id}${put.reused ? ' (reused)' : ''}`,\n )\n for (const note of put.notes) log(` pasted file ${f.name}: ${note}`)\n }\n if (Object.keys(listed).length) doc.source.scriptFiles = listed\n else delete doc.source.scriptFiles\n}\n\n/** The listed paste files a fetched document carries, read defensively. */\nfunction listedFiles(doc: Pick<ProjectDoc, 'source'>): [string, string][] {\n const listed = doc.source.scriptFiles\n if (!listed || typeof listed !== 'object' || Array.isArray(listed)) return []\n return Object.entries(listed).filter(\n (e): e is [string, string] => typeof e[1] === 'string',\n )\n}\n\n/** A `data:` address's bytes, or null when it is not one this reads. */\nfunction fromDataUrl(url: string): Uint8Array | null {\n const m = /^data:[^,;]*(;base64)?,(.*)$/s.exec(url)\n if (!m) return null\n return m[1]\n ? new Uint8Array(Buffer.from(m[2], 'base64'))\n : new TextEncoder().encode(decodeURIComponent(m[2]))\n}\n\nexport interface PasteFilesPulled {\n /** Written beside actions.json, with where each came from. */\n written: { file: string; url: string; bytes: number }[]\n /** Already on disk, kept. */\n kept: string[]\n /** Every hosted one now beside the take, written or kept, and its address. */\n hosted: { file: string; url: string }[]\n}\n\n/**\n * Bring a fetched take's paste files home beside its script, under the\n * names the script pastes. Only a name the document's own script pastes\n * is written, only where nothing is, only from the platform's file door\n * (with the reader's key, sent home only) or a `data:` address. Anything\n * else the list names is said and skipped: a stranger's document does not\n * choose what this machine fetches or where it lands.\n */\nexport async function pullPasteFiles(\n ctx: { origin: string; key: string | null },\n dir: string,\n doc: Pick<ProjectDoc, 'source'>,\n log: (line: string) => void,\n): Promise<PasteFilesPulled> {\n const result: PasteFilesPulled = { written: [], kept: [], hosted: [] }\n const wanted = new Set(pasteNames(doc.source.script))\n for (const [name, url] of listedFiles(doc)) {\n if (!wanted.has(name) || fileNameProblem(name)) {\n log(\n ` skipped ${JSON.stringify(name).slice(0, 80)}: the document lists it as a pasted file, and its script pastes no file by that name`,\n )\n continue\n }\n const path = join(dir, name)\n const door = assetIdOf(url) !== null\n if (existsSync(path)) {\n result.kept.push(name)\n if (door) result.hosted.push({ file: name, url })\n continue\n }\n let bytes: Uint8Array | null = null\n if (url.startsWith('data:')) bytes = fromDataUrl(url)\n else if (door) {\n const plan = downloadPlan(ctx, url)\n const res = await fetch(plan.url, {\n headers: plan.bearer ? bearerFor(ctx.key, plan.url) : {},\n })\n if (!res.ok)\n throw new Error(\n `download of the pasted file ${name} failed (${res.status})`,\n )\n bytes = new Uint8Array(await res.arrayBuffer())\n }\n if (!bytes) {\n log(\n ` skipped ${name}: the document names it at an address that is neither a hosted file nor carried in the document`,\n )\n continue\n }\n await writeFileSafe(path, bytes)\n result.written.push({ file: name, url, bytes: bytes.length })\n if (door) result.hosted.push({ file: name, url })\n log(` ${name} ← the pasted file (${Math.round(bytes.length / 1024)} kB)`)\n }\n return result\n}\n\n/**\n * A claimable push carries the paste files it may name: a text file inside\n * the document, a picture this take fetched and left unchanged by its\n * hosted address. Anything else is said and left out, because a claimable\n * push uploads nothing.\n */\nexport async function claimablePasteFiles(\n doc: ProjectDoc,\n found: Found[],\n hosted: Record<string, { ref: string; sha256: string }>,\n log: (line: string) => void,\n): Promise<void> {\n const listed: Record<string, string> = {}\n for (const f of found) {\n if ('missing' in f) continue\n if (!f.type.startsWith('image/')) {\n if (f.bytes.length <= SCRIPT_TEXT_MAX_BYTES)\n listed[f.name] = dataUrl(f.type, f.bytes)\n continue\n }\n const origin = hosted[f.name]\n const sha = createHash('sha256').update(f.bytes).digest('hex')\n if (origin && origin.sha256 === sha) listed[f.name] = origin.ref\n else\n log(\n ` note: the pasted file ${f.name} is not one this take fetched, and a claimable push uploads nothing, so it does not travel: a re-record of the claimed take needs it put back`,\n )\n }\n if (Object.keys(listed).length) doc.source.scriptFiles = listed\n else delete doc.source.scriptFiles\n}\n"],"mappings":"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","names":["existsSync","join","createHash","existsSync","mkdir","readFile","rename","rm","basename","join","createHash","existsSync","readFile","join","existsSync","join","readFile","createHash","where","readFile","createHash","basename","join","mkdir","rename","rm","existsSync","join","existsSync"]}
|