@vosjs/cli 0.35.0 → 0.37.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.
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-NHKHTIDR.js";
7
7
 
8
8
  // src/plugin/run.ts
9
- import { mkdir as mkdir9, readFile as readFile15, rm as rm4 } from "fs/promises";
9
+ import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
10
10
  import { existsSync as existsSync15 } from "fs";
11
11
  import { join as join18, resolve as resolve9 } from "path";
12
12
  import {
@@ -1681,6 +1681,378 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1681
1681
  }
1682
1682
  }
1683
1683
 
1684
+ // src/plugin/container.ts
1685
+ var OCTET_STREAM = "application/octet-stream";
1686
+ var TYPES_BY_EXTENSION = {
1687
+ svg: "image/svg+xml",
1688
+ png: "image/png",
1689
+ jpg: "image/jpeg",
1690
+ jpeg: "image/jpeg",
1691
+ webp: "image/webp",
1692
+ gif: "image/gif",
1693
+ avif: "image/avif",
1694
+ webm: "video/webm",
1695
+ mp4: "video/mp4",
1696
+ mov: "video/quicktime",
1697
+ mp3: "audio/mpeg",
1698
+ wav: "audio/wav",
1699
+ ogg: "audio/ogg",
1700
+ m4a: "audio/mp4"
1701
+ };
1702
+ var EXTENSION_BY_TYPE = {
1703
+ "image/jpeg": ".jpg",
1704
+ "video/mp4": ".mp4",
1705
+ "audio/mp4": ".m4a"
1706
+ };
1707
+ var TAKE_MEDIA_EXTENSIONS = [
1708
+ ".webm",
1709
+ ".mp4",
1710
+ ".mov",
1711
+ ".m4a",
1712
+ ".mp3",
1713
+ ".ogg",
1714
+ ".wav"
1715
+ ];
1716
+ var MEDIA_HEAD_BYTES = 16;
1717
+ var ISO_BRANDS = {
1718
+ "qt ": "video/quicktime",
1719
+ "M4A ": "audio/mp4",
1720
+ "M4B ": "audio/mp4",
1721
+ avif: "image/avif",
1722
+ avis: "image/avif"
1723
+ };
1724
+ var RIFF_FORMS = {
1725
+ WEBP: "image/webp",
1726
+ WAVE: "audio/wav",
1727
+ "AVI ": "video/x-msvideo"
1728
+ };
1729
+ function sniffMediaType(head) {
1730
+ const ascii = (at2, len) => head.length >= at2 + len ? String.fromCharCode(...head.subarray(at2, at2 + len)) : "";
1731
+ const startsWith = (...bytes) => bytes.every((b, i) => head[i] === b);
1732
+ if (startsWith(26, 69, 223, 163)) return "video/webm";
1733
+ if (ascii(4, 4) === "ftyp") return ISO_BRANDS[ascii(8, 4)] ?? "video/mp4";
1734
+ if (ascii(0, 4) === "RIFF") return RIFF_FORMS[ascii(8, 4)] ?? null;
1735
+ if (startsWith(137, 80, 78, 71, 13, 10, 26, 10))
1736
+ return "image/png";
1737
+ if (startsWith(255, 216, 255)) return "image/jpeg";
1738
+ if (ascii(0, 4) === "GIF8") return "image/gif";
1739
+ if (ascii(0, 4) === "OggS") return "audio/ogg";
1740
+ if (ascii(0, 3) === "ID3") return "audio/mpeg";
1741
+ return null;
1742
+ }
1743
+ function bareType(value) {
1744
+ const type = (value ?? "").split(";")[0].trim().toLowerCase();
1745
+ return !type || type === OCTET_STREAM || type === "binary/octet-stream" ? "" : type;
1746
+ }
1747
+ function mediaContentType(file) {
1748
+ const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
1749
+ return TYPES_BY_EXTENSION[ext] ?? OCTET_STREAM;
1750
+ }
1751
+ function extensionFor(contentType) {
1752
+ const type = (contentType ?? "").split(";")[0].trim().toLowerCase();
1753
+ if (EXTENSION_BY_TYPE[type]) return EXTENSION_BY_TYPE[type];
1754
+ const ext = Object.keys(TYPES_BY_EXTENSION).find(
1755
+ (e) => TYPES_BY_EXTENSION[e] === type
1756
+ );
1757
+ return ext ? `.${ext}` : "";
1758
+ }
1759
+ function resolveMediaType(claims) {
1760
+ const sniffed = claims.head?.length ? sniffMediaType(claims.head) : null;
1761
+ if (sniffed) return { type: sniffed, from: "bytes" };
1762
+ const declared = bareType(claims.declared);
1763
+ if (declared) return { type: declared, from: "declared" };
1764
+ const named = mediaContentType(claims.filename ?? "");
1765
+ if (named !== OCTET_STREAM) return { type: named, from: "filename" };
1766
+ return { type: OCTET_STREAM, from: "unknown" };
1767
+ }
1768
+ function nameForType(filename, type) {
1769
+ const ext = extensionFor(type);
1770
+ if (!ext) return filename;
1771
+ const current = /\.[a-z0-9]+$/i.exec(filename)?.[0] ?? "";
1772
+ if (current && mediaContentType(`x${current}`) === type) return filename;
1773
+ return `${filename.slice(0, filename.length - current.length)}${ext}`;
1774
+ }
1775
+ function containerMismatch(filename, head) {
1776
+ const actual = sniffMediaType(head);
1777
+ if (!actual) return null;
1778
+ const claimed = mediaContentType(filename);
1779
+ if (claimed === actual) return null;
1780
+ return `${filename} is ${actual}, not the ${claimed} its name claims \u2014 rename it to ${nameForType(filename, actual)}`;
1781
+ }
1782
+
1783
+ // src/plugin/media.ts
1784
+ import { isKeyedOverlay } from "@vosjs/studio-core";
1785
+ import { createWriteStream, existsSync as existsSync2 } from "fs";
1786
+ import {
1787
+ mkdir as mkdir2,
1788
+ open,
1789
+ readdir,
1790
+ rename as rename2,
1791
+ rm as rm2,
1792
+ stat,
1793
+ writeFile as writeFile2
1794
+ } from "fs/promises";
1795
+ import { basename as basename2, dirname, join as join2 } from "path";
1796
+ import { Readable } from "stream";
1797
+ import { pipeline } from "stream/promises";
1798
+
1799
+ // src/plugin/take.ts
1800
+ import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
1801
+ import { existsSync } from "fs";
1802
+ import { basename, join } from "path";
1803
+ var RECORDING_NAME = "recording.webm";
1804
+ function takeMediaFiles(dir, stem) {
1805
+ return TAKE_MEDIA_EXTENSIONS.map((ext) => join(dir, `${stem}${ext}`)).filter(
1806
+ (file) => existsSync(file)
1807
+ );
1808
+ }
1809
+ function takeMediaFile(dir, stem, fallback) {
1810
+ return takeMediaFiles(dir, stem)[0] ?? join(dir, fallback);
1811
+ }
1812
+ var PREV_DOC_NAME = "doc.prev.json";
1813
+ function takePaths(dir) {
1814
+ const recording = takeMediaFile(dir, "recording", RECORDING_NAME);
1815
+ return {
1816
+ dir,
1817
+ recording,
1818
+ recordingName: basename(recording),
1819
+ framesDir: join(dir, "frames"),
1820
+ framesIndex: join(dir, "frames.json"),
1821
+ cursor: join(dir, "cursor.json"),
1822
+ meta: join(dir, "meta.json"),
1823
+ actions: join(dir, "actions.json"),
1824
+ doc: join(dir, "doc.json")
1825
+ };
1826
+ }
1827
+ async function ensureTakeDir(dir) {
1828
+ const p = takePaths(dir);
1829
+ await mkdir(p.framesDir, { recursive: true });
1830
+ return p;
1831
+ }
1832
+ async function prepareReRecord(dir) {
1833
+ const p = takePaths(dir);
1834
+ const kept = [];
1835
+ let prevDoc = false;
1836
+ if (existsSync(p.doc)) {
1837
+ await rename(p.doc, join(dir, PREV_DOC_NAME));
1838
+ prevDoc = true;
1839
+ kept.push(PREV_DOC_NAME);
1840
+ }
1841
+ if (existsSync(p.actions)) kept.push("actions.json");
1842
+ if (existsSync(join(dir, "vos.json"))) kept.push("vos.json");
1843
+ for (const stale of [
1844
+ // Every container, not just the one `record` writes: a pulled take's
1845
+ // footage may be mp4, and a survivor would outrank the new recording.
1846
+ ...takeMediaFiles(dir, "recording"),
1847
+ ...takeMediaFiles(dir, "mic"),
1848
+ ...takeMediaFiles(dir, "cam"),
1849
+ p.cursor,
1850
+ p.meta,
1851
+ p.framesIndex,
1852
+ p.framesDir,
1853
+ join(dir, "stills"),
1854
+ join(dir, "digest")
1855
+ ]) {
1856
+ await rm(stale, { recursive: true, force: true });
1857
+ }
1858
+ return { prevDoc, kept };
1859
+ }
1860
+ async function readJson(file) {
1861
+ return JSON.parse(await readFile(file, "utf8"));
1862
+ }
1863
+ async function writeJson(file, value, pretty = false) {
1864
+ await writeFile(
1865
+ file,
1866
+ pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)
1867
+ );
1868
+ }
1869
+ async function loadTake(dir) {
1870
+ const paths = takePaths(dir);
1871
+ if (!existsSync(paths.meta)) {
1872
+ throw new Error(
1873
+ `${dir} is not a take directory (missing meta.json) \u2014 run record first`
1874
+ );
1875
+ }
1876
+ const meta = await readJson(paths.meta);
1877
+ const cursor = existsSync(paths.cursor) ? await readJson(paths.cursor) : [];
1878
+ const actions = existsSync(paths.actions) ? await readJson(paths.actions) : null;
1879
+ const doc = existsSync(paths.doc) ? await readJson(paths.doc) : null;
1880
+ return { paths, cursor, meta, actions, doc };
1881
+ }
1882
+
1883
+ // src/plugin/media.ts
1884
+ var SOURCE_MEDIA = [
1885
+ { key: "videoKey", stem: "recording", fallbackExt: ".webm" },
1886
+ { key: "micKey", stem: "mic", fallbackExt: ".webm" },
1887
+ { key: "camKey", stem: "cam", fallbackExt: ".webm" }
1888
+ ];
1889
+ function isTakeRelativeKey(key) {
1890
+ return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
1891
+ }
1892
+ var takeRelativeFile = (key) => key.replace(/^\/+/, "");
1893
+ function docMediaRefs(doc, keep = isTakeRelativeKey) {
1894
+ const out = [];
1895
+ for (const m of doc.media ?? []) {
1896
+ if (keep(m.videoKey))
1897
+ out.push({
1898
+ where: `media ${m.id}`,
1899
+ key: m.videoKey,
1900
+ set: (next) => {
1901
+ m.videoKey = next;
1902
+ }
1903
+ });
1904
+ const mic = m.micKey;
1905
+ if (mic && keep(mic))
1906
+ out.push({
1907
+ where: `media ${m.id} mic`,
1908
+ key: mic,
1909
+ set: (next) => {
1910
+ m.micKey = next;
1911
+ }
1912
+ });
1913
+ const cam = m.camKey;
1914
+ if (cam && keep(cam))
1915
+ out.push({
1916
+ where: `media ${m.id} cam`,
1917
+ key: cam,
1918
+ set: (next) => {
1919
+ m.camKey = next;
1920
+ }
1921
+ });
1922
+ }
1923
+ for (const clip of doc.overlays ?? []) {
1924
+ if (!isKeyedOverlay(clip) || !keep(clip.key)) continue;
1925
+ out.push({
1926
+ where: `overlay ${clip.id}`,
1927
+ key: clip.key,
1928
+ set: (next) => {
1929
+ clip.key = next;
1930
+ }
1931
+ });
1932
+ }
1933
+ const mark = doc.endCard?.mark;
1934
+ if (mark && keep(mark.key))
1935
+ out.push({
1936
+ where: "end card mark",
1937
+ key: mark.key,
1938
+ set: (next) => {
1939
+ mark.key = next;
1940
+ }
1941
+ });
1942
+ const bg = doc.frame.backgroundMedia;
1943
+ if (bg && keep(bg.key))
1944
+ out.push({
1945
+ where: "background",
1946
+ key: bg.key,
1947
+ set: (next) => {
1948
+ bg.key = next;
1949
+ }
1950
+ });
1951
+ return out;
1952
+ }
1953
+ function assetIdOf(url) {
1954
+ if (!url) return null;
1955
+ const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
1956
+ return m ? m[1] : null;
1957
+ }
1958
+ async function readMediaHead(file) {
1959
+ const handle = await open(file, "r");
1960
+ try {
1961
+ const buf = Buffer.alloc(MEDIA_HEAD_BYTES);
1962
+ const { bytesRead } = await handle.read(buf, 0, MEDIA_HEAD_BYTES, 0);
1963
+ return new Uint8Array(buf.subarray(0, bytesRead));
1964
+ } finally {
1965
+ await handle.close();
1966
+ }
1967
+ }
1968
+ async function downloadMedia(ctx, url, target) {
1969
+ const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
1970
+ const res = await fetch(abs, {
1971
+ headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
1972
+ });
1973
+ if (!res.ok || !res.body) {
1974
+ throw new Error(
1975
+ `download of ${target.describe} failed (${res.status})${res.status === 404 && target.notFoundHint ? ` \u2014 ${target.notFoundHint}` : ""}`
1976
+ );
1977
+ }
1978
+ const into = target.subdir ? join2(target.dir, target.subdir) : target.dir;
1979
+ const partial = join2(into, `${target.stem}.part`);
1980
+ await mkdir2(dirname(partial), { recursive: true });
1981
+ try {
1982
+ await pipeline(
1983
+ Readable.fromWeb(res.body),
1984
+ createWriteStream(partial)
1985
+ );
1986
+ const { type } = resolveMediaType({
1987
+ head: await readMediaHead(partial),
1988
+ declared: res.headers.get("content-type"),
1989
+ filename: `${target.stem}${target.fallbackExt}`
1990
+ });
1991
+ const name = `${target.stem}${extensionFor(type) || target.fallbackExt}`;
1992
+ await rename2(partial, join2(into, name));
1993
+ const file = target.subdir ? `${target.subdir}/${name}` : name;
1994
+ return { file, bytes: (await stat(join2(target.dir, file))).size };
1995
+ } finally {
1996
+ await rm2(partial, { force: true });
1997
+ }
1998
+ }
1999
+ async function pullMedia(ctx, dir, doc, log) {
2000
+ const result = { downloaded: [], kept: [] };
2001
+ for (const { key, stem, fallbackExt } of SOURCE_MEDIA) {
2002
+ const url = doc.source[key];
2003
+ const assetId = assetIdOf(url);
2004
+ if (!assetId) continue;
2005
+ const present = takeMediaFiles(dir, stem)[0];
2006
+ if (present) {
2007
+ result.kept.push(basename2(present));
2008
+ doc.source[key] = basename2(present);
2009
+ continue;
2010
+ }
2011
+ const got = await downloadMedia(ctx, url, {
2012
+ dir,
2013
+ stem,
2014
+ fallbackExt,
2015
+ describe: stem,
2016
+ notFoundHint: "a private recording needs a content key of its owner"
2017
+ });
2018
+ result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
2019
+ log(` ${got.file} \u2190 asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`);
2020
+ doc.source[key] = got.file;
2021
+ }
2022
+ for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
2023
+ const assetId = assetIdOf(ref.key);
2024
+ const existing = existsSync2(join2(dir, "media")) ? (await readdir(join2(dir, "media"))).find(
2025
+ (f) => f === assetId || f.startsWith(`${assetId}.`)
2026
+ ) : void 0;
2027
+ if (existing) {
2028
+ const file = `media/${existing}`;
2029
+ result.kept.push(file);
2030
+ ref.set(file);
2031
+ continue;
2032
+ }
2033
+ const got = await downloadMedia(ctx, ref.key, {
2034
+ dir,
2035
+ subdir: "media",
2036
+ stem: assetId,
2037
+ // A type with no extension here (an svg served as such) keeps the bare
2038
+ // asset id, exactly as before.
2039
+ fallbackExt: "",
2040
+ describe: `${ref.where} (${ref.key})`
2041
+ });
2042
+ result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
2043
+ log(
2044
+ ` ${got.file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`
2045
+ );
2046
+ ref.set(got.file);
2047
+ }
2048
+ const metaPath = join2(dir, "meta.json");
2049
+ if (!existsSync2(metaPath)) await writeJson(metaPath, doc.source.meta, true);
2050
+ const cursorPath = join2(dir, "cursor.json");
2051
+ if (!existsSync2(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
2052
+ await writeFile2(join2(dir, "doc.json"), JSON.stringify(doc, null, 2));
2053
+ return result;
2054
+ }
2055
+
1684
2056
  // src/plugin/mediaProbe.ts
1685
2057
  var MEDIA_EXT = /\.(png|jpe?g|webp|gif|webm|mp4|mov|mp3|wav|m4a|ogg|glb|gltf|hdr|exr|woff2?)($|\?)/i;
1686
2058
  var isRemote = (v) => typeof v === "string" && /^https?:\/\//i.test(v);
@@ -1766,9 +2138,9 @@ function mediaProbeLints(results) {
1766
2138
  }
1767
2139
 
1768
2140
  // src/plugin/framesTake.ts
1769
- import { mkdir as mkdir3, readFile as readFile4, rename as rename3, writeFile as writeFile4 } from "fs/promises";
1770
- import { existsSync as existsSync4 } from "fs";
1771
- import { join as join4, resolve as resolve2 } from "path";
2141
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile5 } from "fs/promises";
2142
+ import { existsSync as existsSync5 } from "fs";
2143
+ import { join as join5, resolve as resolve2 } from "path";
1772
2144
  import { compileVosConfig } from "@vosjs/core";
1773
2145
  import {
1774
2146
  docStillTime,
@@ -1780,83 +2152,11 @@ import {
1780
2152
  spanOutputExtent as spanOutputExtent2
1781
2153
  } from "@vosjs/studio-core";
1782
2154
 
1783
- // src/plugin/take.ts
1784
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
1785
- import { existsSync } from "fs";
1786
- import { join } from "path";
1787
- var RECORDING_NAME = "recording.webm";
1788
- var PREV_DOC_NAME = "doc.prev.json";
1789
- function takePaths(dir) {
1790
- return {
1791
- dir,
1792
- recording: join(dir, RECORDING_NAME),
1793
- framesDir: join(dir, "frames"),
1794
- framesIndex: join(dir, "frames.json"),
1795
- cursor: join(dir, "cursor.json"),
1796
- meta: join(dir, "meta.json"),
1797
- actions: join(dir, "actions.json"),
1798
- doc: join(dir, "doc.json")
1799
- };
1800
- }
1801
- async function ensureTakeDir(dir) {
1802
- const p = takePaths(dir);
1803
- await mkdir(p.framesDir, { recursive: true });
1804
- return p;
1805
- }
1806
- async function prepareReRecord(dir) {
1807
- const p = takePaths(dir);
1808
- const kept = [];
1809
- let prevDoc = false;
1810
- if (existsSync(p.doc)) {
1811
- await rename(p.doc, join(dir, PREV_DOC_NAME));
1812
- prevDoc = true;
1813
- kept.push(PREV_DOC_NAME);
1814
- }
1815
- if (existsSync(p.actions)) kept.push("actions.json");
1816
- if (existsSync(join(dir, "vos.json"))) kept.push("vos.json");
1817
- for (const stale of [
1818
- p.recording,
1819
- p.cursor,
1820
- p.meta,
1821
- p.framesIndex,
1822
- p.framesDir,
1823
- join(dir, "stills"),
1824
- join(dir, "digest"),
1825
- join(dir, "mic.webm"),
1826
- join(dir, "cam.webm")
1827
- ]) {
1828
- await rm(stale, { recursive: true, force: true });
1829
- }
1830
- return { prevDoc, kept };
1831
- }
1832
- async function readJson(file) {
1833
- return JSON.parse(await readFile(file, "utf8"));
1834
- }
1835
- async function writeJson(file, value, pretty = false) {
1836
- await writeFile(
1837
- file,
1838
- pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)
1839
- );
1840
- }
1841
- async function loadTake(dir) {
1842
- const paths = takePaths(dir);
1843
- if (!existsSync(paths.meta)) {
1844
- throw new Error(
1845
- `${dir} is not a take directory (missing meta.json) \u2014 run record first`
1846
- );
1847
- }
1848
- const meta = await readJson(paths.meta);
1849
- const cursor = existsSync(paths.cursor) ? await readJson(paths.cursor) : [];
1850
- const actions = existsSync(paths.actions) ? await readJson(paths.actions) : null;
1851
- const doc = existsSync(paths.doc) ? await readJson(paths.doc) : null;
1852
- return { paths, cursor, meta, actions, doc };
1853
- }
1854
-
1855
2155
  // src/plugin/server.ts
1856
2156
  import { createServer } from "http";
1857
- import { existsSync as existsSync2, readFileSync, statSync } from "fs";
1858
- import { writeFile as writeFile2 } from "fs/promises";
1859
- import { extname, join as join2 } from "path";
2157
+ import { existsSync as existsSync3, readFileSync, statSync } from "fs";
2158
+ import { writeFile as writeFile3 } from "fs/promises";
2159
+ import { extname, join as join3 } from "path";
1860
2160
  var MIME = {
1861
2161
  ".html": "text/html",
1862
2162
  ".js": "text/javascript",
@@ -1890,7 +2190,7 @@ function startTakeServer(rootDir, pages) {
1890
2190
  const chunks = [];
1891
2191
  req.on("data", (c) => chunks.push(c));
1892
2192
  req.on("end", () => {
1893
- void writeFile2(join2(rootDir, name), Buffer.concat(chunks)).then(
2193
+ void writeFile3(join3(rootDir, name), Buffer.concat(chunks)).then(
1894
2194
  () => res.writeHead(200).end("ok"),
1895
2195
  (e) => res.writeHead(500).end(String(e))
1896
2196
  );
@@ -1902,11 +2202,11 @@ function startTakeServer(rootDir, pages) {
1902
2202
  res.end(pages[url.pathname]);
1903
2203
  return;
1904
2204
  }
1905
- const file = join2(rootDir, decodeURIComponent(url.pathname));
1906
- if (existsSync2(file) && statSync(file).isFile()) {
1907
- const type = MIME[extname(file)] ?? "application/octet-stream";
2205
+ const file = join3(rootDir, decodeURIComponent(url.pathname));
2206
+ if (existsSync3(file) && statSync(file).isFile()) {
1908
2207
  const size = statSync(file).size;
1909
2208
  const buf = readFileSync(file);
2209
+ const type = sniffMediaType(buf.subarray(0, MEDIA_HEAD_BYTES)) ?? MIME[extname(file)] ?? "application/octet-stream";
1910
2210
  const range = req.headers.range;
1911
2211
  const m = range && /^bytes=(\d*)-(\d*)$/.exec(range);
1912
2212
  if (m) {
@@ -1969,7 +2269,7 @@ async function waitForPageDone(page, label, onProgress, timeoutMs) {
1969
2269
  }
1970
2270
 
1971
2271
  // src/plugin/renderAnimation.ts
1972
- import { readFile as readFile2, rm as rm2 } from "fs/promises";
2272
+ import { readFile as readFile2, rm as rm3 } from "fs/promises";
1973
2273
  import { tweenRuntimeCode } from "@vosjs/tween/bundle";
1974
2274
  import { elementsBundleCode } from "@vosjs/elements/bundle";
1975
2275
  import { concatEncodedVideo, planChunks } from "@vosjs/render-core";
@@ -2267,7 +2567,7 @@ async function renderAnimation(browser, opts) {
2267
2567
  outNames.map((name) => readFile2(`${opts.workDir}/${name}`))
2268
2568
  );
2269
2569
  await Promise.all(
2270
- outNames.map((name) => rm2(`${opts.workDir}/${name}`, { force: true }))
2570
+ outNames.map((name) => rm3(`${opts.workDir}/${name}`, { force: true }))
2271
2571
  );
2272
2572
  if (chunks.length === 1) {
2273
2573
  return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
@@ -2522,9 +2822,9 @@ function applyAndValidate(doc, o) {
2522
2822
  }
2523
2823
 
2524
2824
  // src/plugin/digestTake.ts
2525
- import { mkdir as mkdir2, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
2526
- import { existsSync as existsSync3 } from "fs";
2527
- import { join as join3, resolve } from "path";
2825
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile4 } from "fs/promises";
2826
+ import { existsSync as existsSync4 } from "fs";
2827
+ import { join as join4, resolve } from "path";
2528
2828
  import {
2529
2829
  DIGEST_CROP_MAX,
2530
2830
  DIGEST_FULL_MAX,
@@ -2746,8 +3046,8 @@ async function digestTake(browser, dir, opts = {}) {
2746
3046
  const meta = doc.source.meta;
2747
3047
  let plan = planForDigest(doc);
2748
3048
  const transcript = opts.transcript ?? await discoverTranscript(dir);
2749
- const outDir = resolve(opts.outDir ?? join3(dir, DIGEST_DIR));
2750
- await mkdir2(outDir, { recursive: true });
3049
+ const outDir = resolve(opts.outDir ?? join4(dir, DIGEST_DIR));
3050
+ await mkdir3(outDir, { recursive: true });
2751
3051
  const fullMax = opts.full ?? DIGEST_FULL_MAX;
2752
3052
  const cropMax = opts.crop ?? DIGEST_CROP_MAX;
2753
3053
  const wantFrames = opts.frames !== false && browser !== null;
@@ -2759,8 +3059,11 @@ async function digestTake(browser, dir, opts = {}) {
2759
3059
  let sheet = null;
2760
3060
  let moments;
2761
3061
  const boxes = /* @__PURE__ */ new Map();
2762
- let source = { kind: "video", url: `/${RECORDING_NAME}` };
2763
- if (existsSync3(take.paths.framesIndex) && existsSync3(take.paths.framesDir)) {
3062
+ let source = {
3063
+ kind: "video",
3064
+ url: `/${take.paths.recordingName}`
3065
+ };
3066
+ if (existsSync4(take.paths.framesIndex) && existsSync4(take.paths.framesDir)) {
2764
3067
  const index = await readJson(
2765
3068
  take.paths.framesIndex
2766
3069
  );
@@ -2775,9 +3078,9 @@ async function digestTake(browser, dir, opts = {}) {
2775
3078
  }
2776
3079
  }
2777
3080
  if (wantFrames) {
2778
- if (source.kind === "video" && !existsSync3(take.paths.recording))
3081
+ if (source.kind === "video" && !existsSync4(take.paths.recording))
2779
3082
  throw new Error(
2780
- `${dir} has no ${RECORDING_NAME} \u2014 pull it with \`vos pull --media\` first`
3083
+ `${dir} has no recording \u2014 pull it with \`vos pull --media\` first`
2781
3084
  );
2782
3085
  const server = await startTakeServer(dir, {
2783
3086
  "/digest.html": digestPageHtml()
@@ -2848,9 +3151,9 @@ async function digestTake(browser, dir, opts = {}) {
2848
3151
  );
2849
3152
  sizes = f.sizes;
2850
3153
  for (const name of Object.keys(sizes)) {
2851
- const from = join3(dir, name);
2852
- const to = join3(outDir, name);
2853
- if (from !== to && existsSync3(from)) await rename2(from, to);
3154
+ const from = join4(dir, name);
3155
+ const to = join4(outDir, name);
3156
+ if (from !== to && existsSync4(from)) await rename3(from, to);
2854
3157
  }
2855
3158
  if (sizes["sheet.png"]) sheet = "sheet.png";
2856
3159
  } finally {
@@ -2876,12 +3179,12 @@ async function digestTake(browser, dir, opts = {}) {
2876
3179
  if (!p) continue;
2877
3180
  for (const f of [p.full, p.crop]) {
2878
3181
  if (!f) continue;
2879
- const size = await pngSize(join3(outDir, f));
3182
+ const size = await pngSize(join4(outDir, f));
2880
3183
  if (size) sizes[f] = size;
2881
3184
  }
2882
3185
  if (p.box) boxes.set(m.id, p.box);
2883
3186
  }
2884
- if (prior.images.sheet && existsSync3(join3(outDir, prior.images.sheet)))
3187
+ if (prior.images.sheet && existsSync4(join4(outDir, prior.images.sheet)))
2885
3188
  sheet = prior.images.sheet;
2886
3189
  }
2887
3190
  }
@@ -2910,14 +3213,14 @@ async function digestTake(browser, dir, opts = {}) {
2910
3213
  style: opts.style ?? null,
2911
3214
  transcript
2912
3215
  });
2913
- const file = join3(outDir, "digest.json");
3216
+ const file = join4(outDir, "digest.json");
2914
3217
  const body = JSON.stringify(digest, null, 2);
2915
- await writeFile3(file, body);
3218
+ await writeFile4(file, body);
2916
3219
  return { file, outDir, digest, bytes: Buffer.byteLength(body) };
2917
3220
  }
2918
3221
  async function discoverTranscript(dir) {
2919
- const file = join3(dir, "transcript.json");
2920
- if (!existsSync3(file)) return null;
3222
+ const file = join4(dir, "transcript.json");
3223
+ if (!existsSync4(file)) return null;
2921
3224
  try {
2922
3225
  return parseTranscript(JSON.parse(await readFile3(file, "utf8")));
2923
3226
  } catch {
@@ -2927,7 +3230,7 @@ async function discoverTranscript(dir) {
2927
3230
  async function readPriorDigest(outDir) {
2928
3231
  try {
2929
3232
  const raw = JSON.parse(
2930
- await readFile3(join3(outDir, "digest.json"), "utf8")
3233
+ await readFile3(join4(outDir, "digest.json"), "utf8")
2931
3234
  );
2932
3235
  return raw.digestVersion === DIGEST_VERSION && Array.isArray(raw.moments) ? raw : null;
2933
3236
  } catch {
@@ -3111,8 +3414,8 @@ async function framesTake(browser, dir, opts) {
3111
3414
  throw new Error(
3112
3415
  "no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
3113
3416
  );
3114
- const outDir = resolve2(opts.outDir ?? join4(dir, "stills"));
3115
- await mkdir3(outDir, { recursive: true });
3417
+ const outDir = resolve2(opts.outDir ?? join5(dir, "stills"));
3418
+ await mkdir4(outDir, { recursive: true });
3116
3419
  const named = shots.map((s, i) => ({
3117
3420
  ...s,
3118
3421
  name: s.kind === "moment" ? `moment-${s.momentId}-${s.time.toFixed(2)}s.png` : `${s.kind === "zoom" ? "zoom" : "frame"}-${String(i).padStart(2, "0")}-${s.time.toFixed(2)}s.png`
@@ -3140,7 +3443,7 @@ async function framesTake(browser, dir, opts) {
3140
3443
  void page.evaluate(stillsInPage, {
3141
3444
  animationCode,
3142
3445
  token: VIDEO_TOKEN,
3143
- videoUrl: `/${RECORDING_NAME}`,
3446
+ videoUrl: `/${take.paths.recordingName}`,
3144
3447
  W: width,
3145
3448
  H: height,
3146
3449
  ss,
@@ -3156,9 +3459,9 @@ async function framesTake(browser, dir, opts) {
3156
3459
  );
3157
3460
  const frames = [];
3158
3461
  for (const n of named) {
3159
- const from = join4(dir, n.name);
3160
- const to = join4(outDir, n.name);
3161
- if (from !== to) await rename3(from, to);
3462
+ const from = join5(dir, n.name);
3463
+ const to = join5(outDir, n.name);
3464
+ if (from !== to) await rename4(from, to);
3162
3465
  frames.push({
3163
3466
  file: to,
3164
3467
  time: n.time,
@@ -3173,8 +3476,8 @@ async function framesTake(browser, dir, opts) {
3173
3476
  }
3174
3477
  }
3175
3478
  async function momentsFor(dir, doc) {
3176
- const file = join4(dir, DIGEST_DIR, "digest.json");
3177
- if (existsSync4(file)) {
3479
+ const file = join5(dir, DIGEST_DIR, "digest.json");
3480
+ if (existsSync5(file)) {
3178
3481
  try {
3179
3482
  const d = JSON.parse(await readFile4(file, "utf8"));
3180
3483
  if (Array.isArray(d.moments)) return d.moments;
@@ -3196,8 +3499,8 @@ function parseTimes(spec, duration) {
3196
3499
  });
3197
3500
  }
3198
3501
  async function writeIndexJson(result) {
3199
- const index = join4(result.outDir, "stills.json");
3200
- await writeFile4(
3502
+ const index = join5(result.outDir, "stills.json");
3503
+ await writeFile5(
3201
3504
  index,
3202
3505
  JSON.stringify(
3203
3506
  {
@@ -3214,10 +3517,10 @@ async function writeIndexJson(result) {
3214
3517
  }
3215
3518
 
3216
3519
  // src/plugin/deliver.ts
3217
- import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
3520
+ import { mkdir as mkdir5, mkdtemp, rename as rename5, rm as rm4, stat as stat2, writeFile as writeFile7 } from "fs/promises";
3218
3521
  import { tmpdir } from "os";
3219
- import { join as join7, relative, resolve as resolve4 } from "path";
3220
- import { existsSync as existsSync6 } from "fs";
3522
+ import { join as join8, relative, resolve as resolve4 } from "path";
3523
+ import { existsSync as existsSync7 } from "fs";
3221
3524
  import { readFile as readFile6 } from "fs/promises";
3222
3525
  import { parseFrontmatter } from "@vosjs/shared/frontmatter";
3223
3526
  import {
@@ -3232,7 +3535,7 @@ import {
3232
3535
  } from "@vosjs/studio-core";
3233
3536
 
3234
3537
  // src/plugin/renderTake.ts
3235
- import { writeFile as writeFile5 } from "fs/promises";
3538
+ import { writeFile as writeFile6 } from "fs/promises";
3236
3539
  import { compileVosConfig as compileVosConfig2 } from "@vosjs/core";
3237
3540
  import {
3238
3541
  audioProducerCode,
@@ -3304,7 +3607,7 @@ async function renderTake(browser, dir, outFile, opts) {
3304
3607
  const result = await renderAnimation(browser, {
3305
3608
  animationCode,
3306
3609
  workDir: dir,
3307
- videoFile: RECORDING_NAME,
3610
+ videoFile: take.paths.recordingName,
3308
3611
  videoToken: VIDEO_TOKEN2,
3309
3612
  width,
3310
3613
  height,
@@ -3321,7 +3624,7 @@ async function renderTake(browser, dir, outFile, opts) {
3321
3624
  } : void 0,
3322
3625
  onProgress: opts.onProgress
3323
3626
  });
3324
- await writeFile5(outFile, result.bytes);
3627
+ await writeFile6(outFile, result.bytes);
3325
3628
  return {
3326
3629
  out: outFile,
3327
3630
  bytes: result.bytes.length,
@@ -3876,9 +4179,9 @@ function proposeTransitions(input, kind) {
3876
4179
  for (const step of steps) {
3877
4180
  if (!step.navigated || step.skipped) continue;
3878
4181
  const name = step.id ?? String(step.step);
3879
- const wait = step.do === "wait";
3880
- const outEnd = round3(wait ? step.tStart : step.tEnd);
3881
- const inStart = round3(wait ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
4182
+ const wait2 = step.do === "wait";
4183
+ const outEnd = round3(wait2 ? step.tStart : step.tEnd);
4184
+ const inStart = round3(wait2 ? step.tEnd : step.tEnd + STEP_SETTLE_SECONDS);
3882
4185
  const segs = doc.segments;
3883
4186
  if (segs.some(
3884
4187
  (s) => !s.media && Math.abs(s.out - outEnd) < 0.05 && s !== segs.at(-1)
@@ -4197,9 +4500,9 @@ function isLightHexGround(hex2) {
4197
4500
  }
4198
4501
 
4199
4502
  // src/plugin/posterDoc.ts
4200
- import { existsSync as existsSync5 } from "fs";
4503
+ import { existsSync as existsSync6 } from "fs";
4201
4504
  import { readFile as readFile5 } from "fs/promises";
4202
- import { isAbsolute, join as join6, resolve as resolve3 } from "path";
4505
+ import { isAbsolute, join as join7, resolve as resolve3 } from "path";
4203
4506
  import {
4204
4507
  computeCardLayout,
4205
4508
  docStillTime as docStillTime2,
@@ -4211,7 +4514,7 @@ import {
4211
4514
  // src/plugin/platform.ts
4212
4515
  import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4213
4516
  import { homedir } from "os";
4214
- import { join as join5 } from "path";
4517
+ import { join as join6 } from "path";
4215
4518
  function platformOrigin(flags = {}) {
4216
4519
  const legacyEnv = process.env.VOS_API_BASE?.trim();
4217
4520
  const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
@@ -4244,7 +4547,7 @@ function parseVosId(input) {
4244
4547
  function deriveSlug(title) {
4245
4548
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
4246
4549
  }
4247
- var CREDENTIALS_PATH = join5(homedir(), ".config", "vos", "credentials");
4550
+ var CREDENTIALS_PATH = join6(homedir(), ".config", "vos", "credentials");
4248
4551
  function resolveCredential(explicit) {
4249
4552
  const flag = explicit?.trim();
4250
4553
  if (flag) return flag;
@@ -4267,7 +4570,7 @@ function requireCredential(explicit) {
4267
4570
  return key;
4268
4571
  }
4269
4572
  function writeCredential(key) {
4270
- mkdirSync(join5(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
4573
+ mkdirSync(join6(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
4271
4574
  writeFileSync(CREDENTIALS_PATH, `${key.trim()}
4272
4575
  `, { mode: 384 });
4273
4576
  return CREDENTIALS_PATH;
@@ -4311,7 +4614,7 @@ function readJsonFile(file) {
4311
4614
  }
4312
4615
  }
4313
4616
  function readSyncState(dir) {
4314
- const own = readJsonFile(join5(dir, SYNC_STATE_NAME));
4617
+ const own = readJsonFile(join6(dir, SYNC_STATE_NAME));
4315
4618
  if (own && typeof own.vosId === "string") {
4316
4619
  return {
4317
4620
  vosId: own.vosId,
@@ -4322,7 +4625,7 @@ function readSyncState(dir) {
4322
4625
  ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
4323
4626
  };
4324
4627
  }
4325
- const push = readJsonFile(join5(dir, "push.json"));
4628
+ const push = readJsonFile(join6(dir, "push.json"));
4326
4629
  if (push && typeof push.vosId === "string") {
4327
4630
  return {
4328
4631
  vosId: push.vosId,
@@ -4330,7 +4633,7 @@ function readSyncState(dir) {
4330
4633
  ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
4331
4634
  };
4332
4635
  }
4333
- const meta = readJsonFile(join5(dir, "meta.json"));
4636
+ const meta = readJsonFile(join6(dir, "meta.json"));
4334
4637
  if (meta && typeof meta.id === "string") {
4335
4638
  return {
4336
4639
  vosId: meta.id,
@@ -4348,7 +4651,7 @@ function writeSyncState(dir, patch) {
4348
4651
  ...patch
4349
4652
  };
4350
4653
  writeFileSync(
4351
- join5(dir, SYNC_STATE_NAME),
4654
+ join6(dir, SYNC_STATE_NAME),
4352
4655
  `${JSON.stringify(merged, null, 2)}
4353
4656
  `
4354
4657
  );
@@ -4387,9 +4690,9 @@ var roleFor = (cls) => `poster-${cls}`;
4387
4690
  async function readPosterAt(cls, path, takeDir, from) {
4388
4691
  let file = path;
4389
4692
  let docDir = path;
4390
- if (existsSync5(join6(path, "doc.json"))) {
4391
- file = join6(path, "doc.json");
4392
- } else if (!/\.json$/i.test(path) || !existsSync5(path)) {
4693
+ if (existsSync6(join7(path, "doc.json"))) {
4694
+ file = join7(path, "doc.json");
4695
+ } else if (!/\.json$/i.test(path) || !existsSync6(path)) {
4393
4696
  return null;
4394
4697
  } else {
4395
4698
  docDir = resolve3(path, "..");
@@ -4406,7 +4709,7 @@ async function readPosterAt(cls, path, takeDir, from) {
4406
4709
  const doc = migrateHostedDoc(
4407
4710
  raw
4408
4711
  );
4409
- const ownFootage = existsSync5(join6(docDir, "meta.json")) && existsSync5(join6(docDir, source.videoKey));
4712
+ const ownFootage = existsSync6(join7(docDir, "meta.json")) && existsSync6(join7(docDir, source.videoKey));
4410
4713
  const sync = readSyncState(docDir);
4411
4714
  return {
4412
4715
  cls,
@@ -4429,10 +4732,10 @@ async function findPosterDocs(takeDir, launchRoles) {
4429
4732
  candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
4430
4733
  }
4431
4734
  candidates.push({
4432
- path: join6(takeDir, "poster", cls),
4735
+ path: join7(takeDir, "poster", cls),
4433
4736
  from: `poster/${cls}`
4434
4737
  });
4435
- candidates.push({ path: join6(takeDir, "poster"), from: "poster" });
4738
+ candidates.push({ path: join7(takeDir, "poster"), from: "poster" });
4436
4739
  for (const c of candidates) {
4437
4740
  const ref = await readPosterAt(cls, c.path, takeDir, c.from);
4438
4741
  if (ref) {
@@ -4528,18 +4831,18 @@ function resolveChannels(raw) {
4528
4831
  return out;
4529
4832
  }
4530
4833
  async function readBrandBesideTake(dir, explicit) {
4531
- const candidates = explicit ? [explicit] : [join7(dir, "BRAND.md"), join7(dir, "..", "BRAND.md")];
4834
+ const candidates = explicit ? [explicit] : [join8(dir, "BRAND.md"), join8(dir, "..", "BRAND.md")];
4532
4835
  for (const file of candidates) {
4533
- if (!existsSync6(file)) continue;
4836
+ if (!existsSync7(file)) continue;
4534
4837
  const roles = parseFrontmatter(await readFile6(file, "utf8"));
4535
4838
  return { file, roles };
4536
4839
  }
4537
4840
  return null;
4538
4841
  }
4539
4842
  async function readLaunchBesideTake(dir, explicit) {
4540
- const candidates = explicit ? [explicit] : [join7(dir, "LAUNCH.md"), join7(dir, "..", "LAUNCH.md")];
4843
+ const candidates = explicit ? [explicit] : [join8(dir, "LAUNCH.md"), join8(dir, "..", "LAUNCH.md")];
4541
4844
  for (const file of candidates) {
4542
- if (!existsSync6(file)) continue;
4845
+ if (!existsSync7(file)) continue;
4543
4846
  return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
4544
4847
  }
4545
4848
  return null;
@@ -4604,7 +4907,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4604
4907
  const meta = doc.source.meta;
4605
4908
  const vw = meta.captureWidth ?? meta.width;
4606
4909
  const vh = meta.captureHeight ?? meta.height;
4607
- const probeDir = await mkdtemp(join7(tmpdir(), "vos-moments-"));
4910
+ const probeDir = await mkdtemp(join8(tmpdir(), "vos-moments-"));
4608
4911
  try {
4609
4912
  const probe = await framesTake(browser, dir, {
4610
4913
  times: candidates.map((c) => c.time),
@@ -4643,7 +4946,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4643
4946
  notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
4644
4947
  };
4645
4948
  } finally {
4646
- await rm3(probeDir, { recursive: true, force: true });
4949
+ await rm4(probeDir, { recursive: true, force: true });
4647
4950
  }
4648
4951
  }
4649
4952
  var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
@@ -4691,8 +4994,8 @@ async function deliverTake(browser, dir, opts) {
4691
4994
  const doc = take.doc;
4692
4995
  const duration = docOutputDuration2(doc);
4693
4996
  const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
4694
- const outDir = resolve4(opts.outDir ?? join7(dir, "kit"));
4695
- await mkdir4(outDir, { recursive: true });
4997
+ const outDir = resolve4(opts.outDir ?? join8(dir, "kit"));
4998
+ await mkdir5(outDir, { recursive: true });
4696
4999
  const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
4697
5000
  const assets = [];
4698
5001
  const skipped = [];
@@ -4752,7 +5055,7 @@ async function deliverTake(browser, dir, opts) {
4752
5055
  opts.onPhase?.(
4753
5056
  `${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
4754
5057
  );
4755
- const shotDir = await mkdtemp(join7(tmpdir(), "vos-poster-"));
5058
+ const shotDir = await mkdtemp(join8(tmpdir(), "vos-poster-"));
4756
5059
  try {
4757
5060
  const captured = await framesTake(browser, ref.takeDir, {
4758
5061
  times: [time],
@@ -4762,9 +5065,9 @@ async function deliverTake(browser, dir, opts) {
4762
5065
  doc: ref.doc,
4763
5066
  overrides: opts.overrides
4764
5067
  });
4765
- const to = join7(outDir, `${d.id}.png`);
4766
- await rename4(captured.frames[0].file, to);
4767
- const bytes = (await stat(to)).size;
5068
+ const to = join8(outDir, `${d.id}.png`);
5069
+ await rename5(captured.frames[0].file, to);
5070
+ const bytes = (await stat2(to)).size;
4768
5071
  if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4769
5072
  skipped.push(
4770
5073
  `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
@@ -4793,7 +5096,7 @@ async function deliverTake(browser, dir, opts) {
4793
5096
  ...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
4794
5097
  });
4795
5098
  } finally {
4796
- await rm3(shotDir, { recursive: true, force: true });
5099
+ await rm4(shotDir, { recursive: true, force: true });
4797
5100
  }
4798
5101
  }
4799
5102
  if (missing.size) {
@@ -4834,7 +5137,7 @@ async function deliverTake(browser, dir, opts) {
4834
5137
  }
4835
5138
  }
4836
5139
  opts.onPhase?.(`${label} (${specWords(d)})`);
4837
- const outFile = join7(outDir, `${d.id}.${d.format}`);
5140
+ const outFile = join8(outDir, `${d.id}.${d.format}`);
4838
5141
  const bitrate = d.maxBytes !== void 0 ? Math.min(
4839
5142
  1e7,
4840
5143
  Math.floor(d.maxBytes * 8 / seconds * 0.85)
@@ -4881,9 +5184,9 @@ async function deliverTake(browser, dir, opts) {
4881
5184
  for (let i = 0; i < captured.frames.length; i++) {
4882
5185
  const frame = captured.frames[i];
4883
5186
  const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
4884
- const to = join7(outDir, name);
4885
- await rename4(frame.file, to);
4886
- const bytes = (await stat(to)).size;
5187
+ const to = join8(outDir, name);
5188
+ await rename5(frame.file, to);
5189
+ const bytes = (await stat2(to)).size;
4887
5190
  if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4888
5191
  skipped.push(
4889
5192
  `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
@@ -4919,15 +5222,15 @@ async function deliverTake(browser, dir, opts) {
4919
5222
  skipped,
4920
5223
  assets
4921
5224
  };
4922
- const kitFile = join7(outDir, "kit.json");
4923
- await writeFile6(kitFile, JSON.stringify(kit, null, 2));
5225
+ const kitFile = join8(outDir, "kit.json");
5226
+ await writeFile7(kitFile, JSON.stringify(kit, null, 2));
4924
5227
  return { kit, kitFile, outDir };
4925
5228
  }
4926
5229
 
4927
5230
  // src/plugin/markAsset.ts
4928
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
4929
- import { existsSync as existsSync7 } from "fs";
4930
- import { isAbsolute as isAbsolute2, join as join8, resolve as resolve5 } from "path";
5231
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
5232
+ import { existsSync as existsSync8 } from "fs";
5233
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
4931
5234
  function markExtension(url) {
4932
5235
  const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
4933
5236
  if (!m) return null;
@@ -5009,7 +5312,7 @@ async function fetchBrandMarks(takeDir, roles) {
5009
5312
  bytes = new Uint8Array(await res.arrayBuffer());
5010
5313
  } else {
5011
5314
  const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
5012
- if (!existsSync7(file)) throw new Error("no such file");
5315
+ if (!existsSync8(file)) throw new Error("no such file");
5013
5316
  bytes = new Uint8Array(await readFile7(file));
5014
5317
  }
5015
5318
  } catch (e) {
@@ -5025,8 +5328,8 @@ async function fetchBrandMarks(takeDir, roles) {
5025
5328
  );
5026
5329
  return null;
5027
5330
  }
5028
- await mkdir5(join8(takeDir, "brand"), { recursive: true });
5029
- await writeFile7(join8(takeDir, "brand", `${name}.${ext}`), bytes);
5331
+ await mkdir6(join9(takeDir, "brand"), { recursive: true });
5332
+ await writeFile8(join9(takeDir, "brand", `${name}.${ext}`), bytes);
5030
5333
  return { key: `/brand/${name}.${ext}`, aspect };
5031
5334
  };
5032
5335
  out.light = await one("logoUrl", "mark");
@@ -5062,9 +5365,9 @@ async function fetchMusicCatalog(origin) {
5062
5365
  }
5063
5366
 
5064
5367
  // src/plugin/judge.ts
5065
- import { existsSync as existsSync8 } from "fs";
5066
- import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
5067
- import { dirname, isAbsolute as isAbsolute3, join as join9, resolve as resolve6 } from "path";
5368
+ import { existsSync as existsSync9 } from "fs";
5369
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
5370
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join10, resolve as resolve6 } from "path";
5068
5371
  import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
5069
5372
 
5070
5373
  // src/plugin/shotBake.ts
@@ -5147,7 +5450,8 @@ function rolesFor(asset) {
5147
5450
  const portrait = spec.px.w < spec.px.h;
5148
5451
  return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
5149
5452
  }
5150
- if (spec?.genre === "card") return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
5453
+ if (spec?.genre === "card")
5454
+ return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
5151
5455
  if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
5152
5456
  return [];
5153
5457
  }
@@ -5207,7 +5511,8 @@ function composeSheet(left, right, height = 540, gutter = 32) {
5207
5511
  const si = (y * src.w + x) * 4;
5208
5512
  const a = src.data[si + 3] / 255;
5209
5513
  const o = ((y0 + y) * w + x0 + x) * 4;
5210
- for (let c = 0; c < 3; c++) out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
5514
+ for (let c = 0; c < 3; c++)
5515
+ out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
5211
5516
  }
5212
5517
  }
5213
5518
  };
@@ -5223,51 +5528,70 @@ function composeSheet(left, right, height = 540, gutter = 32) {
5223
5528
  }
5224
5529
  };
5225
5530
  fill(margin, margin + lw, margin + height + 6, margin + height + 6 + band, 40);
5226
- fill(margin + lw + gutter, margin + lw + gutter + rw, margin + height + 6, margin + height + 6 + band, 200);
5531
+ fill(
5532
+ margin + lw + gutter,
5533
+ margin + lw + gutter + rw,
5534
+ margin + height + 6,
5535
+ margin + height + 6 + band,
5536
+ 200
5537
+ );
5227
5538
  return { w, h, data: out };
5228
5539
  }
5229
5540
  async function judgeKit(kitPath, manifestPath, outDir) {
5230
5541
  const kit = JSON.parse(await readFile8(kitPath, "utf8"));
5231
- const manifest = JSON.parse(await readFile8(manifestPath, "utf8"));
5232
- const manifestDir = dirname(resolve6(manifestPath));
5233
- const kitDir = dirname(resolve6(kitPath));
5234
- const out = resolve6(outDir ?? join9(kitDir, "judge"));
5235
- await mkdir6(out, { recursive: true });
5542
+ const manifest = JSON.parse(
5543
+ await readFile8(manifestPath, "utf8")
5544
+ );
5545
+ const manifestDir = dirname2(resolve6(manifestPath));
5546
+ const kitDir = dirname2(resolve6(kitPath));
5547
+ const out = resolve6(outDir ?? join10(kitDir, "judge"));
5548
+ await mkdir7(out, { recursive: true });
5236
5549
  const sheets = [];
5237
5550
  const skipped = [];
5238
5551
  const refCache = /* @__PURE__ */ new Map();
5239
5552
  const loadRef = async (file) => {
5240
5553
  if (refCache.has(file)) return refCache.get(file) ?? null;
5241
- const p = join9(manifestDir, file);
5242
- const img = existsSync8(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
5554
+ const p = join10(manifestDir, file);
5555
+ const img = existsSync9(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
5243
5556
  refCache.set(file, img);
5244
5557
  return img;
5245
5558
  };
5246
5559
  for (const a of kit.assets) {
5247
5560
  if (!/\.png$/i.test(a.path)) continue;
5248
- const file = isAbsolute3(a.path) && existsSync8(a.path) ? a.path : join9(kitDir, a.path.split("/").pop() ?? a.path);
5249
- const img = existsSync8(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
5561
+ const file = isAbsolute3(a.path) && existsSync9(a.path) ? a.path : join10(kitDir, a.path.split("/").pop() ?? a.path);
5562
+ const img = existsSync9(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
5250
5563
  if (!img) {
5251
5564
  skipped.push(`${a.destination ?? a.path}: unreadable`);
5252
5565
  continue;
5253
5566
  }
5254
- const roles = rolesFor({ destination: a.destination, source: a.source, template: a.template, path: a.path });
5255
- const ref = manifest.assets.find((r) => roles.includes(r.role) && r.genre !== "context");
5567
+ const roles = rolesFor({
5568
+ destination: a.destination,
5569
+ source: a.source,
5570
+ template: a.template,
5571
+ path: a.path
5572
+ });
5573
+ const ref = manifest.assets.find(
5574
+ (r) => roles.includes(r.role) && r.genre !== "context"
5575
+ );
5256
5576
  if (!ref) {
5257
- skipped.push(`${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`);
5577
+ skipped.push(
5578
+ `${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`
5579
+ );
5258
5580
  continue;
5259
5581
  }
5260
5582
  const refImg = await loadRef(ref.file);
5261
5583
  if (!refImg) {
5262
- skipped.push(`${a.destination ?? a.path}: reference ${ref.file} unreadable`);
5584
+ skipped.push(
5585
+ `${a.destination ?? a.path}: reference ${ref.file} unreadable`
5586
+ );
5263
5587
  continue;
5264
5588
  }
5265
5589
  const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
5266
5590
  const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
5267
5591
  const nameA = `${id}--A.png`;
5268
5592
  const nameB = `${id}--B.png`;
5269
- await writeFile8(join9(out, nameA), encodePng(composeSheet(img, refImg)));
5270
- await writeFile8(join9(out, nameB), encodePng(composeSheet(refImg, img)));
5593
+ await writeFile9(join10(out, nameA), encodePng(composeSheet(img, refImg)));
5594
+ await writeFile9(join10(out, nameB), encodePng(composeSheet(refImg, img)));
5271
5595
  const rubric = [
5272
5596
  `# ${id} against ${ref.id} (${ref.file})`,
5273
5597
  "",
@@ -5287,18 +5611,31 @@ async function judgeKit(kitPath, manifestPath, outDir) {
5287
5611
  "In judge.json: `win` true when the kit asset is at least as good as the reference on the rules that apply to its genre, false when it is worse, null for a tie; `reasons` names the rules by number.",
5288
5612
  ""
5289
5613
  ].join("\n");
5290
- await writeFile8(join9(out, `${id}.md`), rubric);
5291
- sheets.push({ asset: id, reference: ref.id, sheetA: nameA, sheetB: nameB, rubric: `${id}.md` });
5614
+ await writeFile9(join10(out, `${id}.md`), rubric);
5615
+ sheets.push({
5616
+ asset: id,
5617
+ reference: ref.id,
5618
+ sheetA: nameA,
5619
+ sheetB: nameB,
5620
+ rubric: `${id}.md`
5621
+ });
5292
5622
  }
5293
- const verdictFile = join9(out, "judge.json");
5294
- if (!existsSync8(verdictFile)) {
5295
- await writeFile8(
5623
+ const verdictFile = join10(out, "judge.json");
5624
+ if (!existsSync9(verdictFile)) {
5625
+ await writeFile9(
5296
5626
  verdictFile,
5297
5627
  JSON.stringify(
5298
5628
  {
5299
5629
  kit: kitPath,
5300
5630
  manifest: manifestPath,
5301
- verdicts: sheets.map((s) => ({ asset: s.asset, reference: s.reference, A: null, B: null, win: null, reasons: [] }))
5631
+ verdicts: sheets.map((s) => ({
5632
+ asset: s.asset,
5633
+ reference: s.reference,
5634
+ A: null,
5635
+ B: null,
5636
+ win: null,
5637
+ reasons: []
5638
+ }))
5302
5639
  },
5303
5640
  null,
5304
5641
  2
@@ -5308,17 +5645,24 @@ async function judgeKit(kitPath, manifestPath, outDir) {
5308
5645
  return { sheets, skipped, outDir: out, verdictFile };
5309
5646
  }
5310
5647
  function winRate(verdicts) {
5311
- const judgedList = verdicts.filter((v) => v.win !== null || (v.reasons?.length ?? 0) > 0);
5648
+ const judgedList = verdicts.filter(
5649
+ (v) => v.win !== null || (v.reasons?.length ?? 0) > 0
5650
+ );
5312
5651
  const wins = judgedList.filter((v) => v.win === true).length;
5313
5652
  const ties = judgedList.filter((v) => v.win === null).length;
5314
5653
  const judged = judgedList.length;
5315
- return { wins, ties, judged, rate: judged ? (wins + ties / 2) / judged : null };
5654
+ return {
5655
+ wins,
5656
+ ties,
5657
+ judged,
5658
+ rate: judged ? (wins + ties / 2) / judged : null
5659
+ };
5316
5660
  }
5317
5661
 
5318
5662
  // src/plugin/callout.ts
5319
- import { existsSync as existsSync9 } from "fs";
5663
+ import { existsSync as existsSync10 } from "fs";
5320
5664
  import { readFile as readFile9 } from "fs/promises";
5321
- import { join as join10 } from "path";
5665
+ import { join as join11 } from "path";
5322
5666
  import { lerpArray, sample } from "@vosjs/timeline";
5323
5667
  import {
5324
5668
  CALLOUT_DEFAULT_BODY_PX,
@@ -5501,10 +5845,10 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
5501
5845
  const out = /* @__PURE__ */ new Map();
5502
5846
  if (!times.length) return out;
5503
5847
  const bare = { ...doc, overlays: [] };
5504
- const outDir = join10(dir, ".ground-check");
5848
+ const outDir = join11(dir, ".ground-check");
5505
5849
  const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
5506
5850
  for (const f of res.frames) {
5507
- if (!existsSync9(f.file)) continue;
5851
+ if (!existsSync10(f.file)) continue;
5508
5852
  out.set(
5509
5853
  +f.time.toFixed(3),
5510
5854
  decodePng(new Uint8Array(await readFile9(f.file)))
@@ -5885,9 +6229,9 @@ function formatFinding(f) {
5885
6229
  }
5886
6230
 
5887
6231
  // src/plugin/validateKit.ts
5888
- import { existsSync as existsSync10 } from "fs";
5889
- import { readFile as readFile11, stat as stat2 } from "fs/promises";
5890
- import { dirname as dirname2, isAbsolute as isAbsolute4, join as join11 } from "path";
6232
+ import { existsSync as existsSync11 } from "fs";
6233
+ import { readFile as readFile11, stat as stat3 } from "fs/promises";
6234
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join12 } from "path";
5891
6235
  import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
5892
6236
  var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
5893
6237
  function pngDimensions(bytes) {
@@ -5944,10 +6288,10 @@ async function validateKit(kitPath, opts = {}) {
5944
6288
  measured
5945
6289
  };
5946
6290
  }
5947
- const base = dirname2(kitPath);
6291
+ const base = dirname3(kitPath);
5948
6292
  const resolvePath = (p) => {
5949
- if (isAbsolute4(p) && existsSync10(p)) return p;
5950
- const beside = join11(base, p.split("/").pop() ?? p);
6293
+ if (isAbsolute4(p) && existsSync11(p)) return p;
6294
+ const beside = join12(base, p.split("/").pop() ?? p);
5951
6295
  return beside;
5952
6296
  };
5953
6297
  const perDestination = /* @__PURE__ */ new Map();
@@ -5958,7 +6302,7 @@ async function validateKit(kitPath, opts = {}) {
5958
6302
  const file = resolvePath(a.path);
5959
6303
  let bytes;
5960
6304
  try {
5961
- bytes = (await stat2(file)).size;
6305
+ bytes = (await stat3(file)).size;
5962
6306
  } catch {
5963
6307
  problems.push(`${label}: ${a.path} is missing`);
5964
6308
  continue;
@@ -6083,7 +6427,7 @@ async function validateKit(kitPath, opts = {}) {
6083
6427
 
6084
6428
  // src/plugin/recorder.ts
6085
6429
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
6086
- import { join as join12 } from "path";
6430
+ import { join as join13 } from "path";
6087
6431
 
6088
6432
  // src/plugin/recordingCap.ts
6089
6433
  var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
@@ -6165,8 +6509,8 @@ async function clockMotion(dur, at2, clock) {
6165
6509
  await at2(easeInOutCubic(Math.min(1, elapsed / dur)));
6166
6510
  samples++;
6167
6511
  const next = start + Math.ceil((clock.now() - start) / MOTION_TICK_MS) * MOTION_TICK_MS;
6168
- const wait = Math.min(next, start + dur) - clock.now();
6169
- if (wait > 0) await clock.sleep(wait);
6512
+ const wait2 = Math.min(next, start + dur) - clock.now();
6513
+ if (wait2 > 0) await clock.sleep(wait2);
6170
6514
  }
6171
6515
  await at2(1);
6172
6516
  return samples + 1;
@@ -6176,8 +6520,8 @@ async function clockTyping(chars, delay, type, clock) {
6176
6520
  for (let i = 0; i < chars.length; i++) {
6177
6521
  await type(chars[i], i);
6178
6522
  const due = start + (i + 1) * delay;
6179
- const wait = due - clock.now();
6180
- if (wait > 0 && i < chars.length - 1) await clock.sleep(wait);
6523
+ const wait2 = due - clock.now();
6524
+ if (wait2 > 0 && i < chars.length - 1) await clock.sleep(wait2);
6181
6525
  }
6182
6526
  }
6183
6527
  function askedMs(step) {
@@ -6261,7 +6605,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6261
6605
  cdp.on("Page.screencastFrame", (ev) => {
6262
6606
  const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
6263
6607
  const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
6264
- writeFileSync2(join12(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6608
+ writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6265
6609
  frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
6266
6610
  cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
6267
6611
  });
@@ -6523,7 +6867,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6523
6867
  await sleep(200);
6524
6868
  const pageTitle = await page.title().catch(() => "");
6525
6869
  await context.close();
6526
- const firstFrame = frames[0] ? jpegDims(readFileSync3(join12(paths.framesDir, frames[0].file))) : null;
6870
+ const firstFrame = frames[0] ? jpegDims(readFileSync3(join13(paths.framesDir, frames[0].file))) : null;
6527
6871
  const meta = {
6528
6872
  dpr: 1,
6529
6873
  zoom: 1,
@@ -6634,9 +6978,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
6634
6978
  }
6635
6979
 
6636
6980
  // src/plugin/plan.ts
6637
- import { existsSync as existsSync11 } from "fs";
6981
+ import { existsSync as existsSync12 } from "fs";
6638
6982
  import { readFile as readFile12 } from "fs/promises";
6639
- import { join as join13 } from "path";
6983
+ import { join as join14 } from "path";
6640
6984
  import {
6641
6985
  DEFAULT_FRAME_STYLE,
6642
6986
  STYLE_FIELDS,
@@ -6858,8 +7202,8 @@ function retimeCut(prev, newSteps, newDurationMs) {
6858
7202
  // src/plugin/plan.ts
6859
7203
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6860
7204
  async function readDigestActivity(dir) {
6861
- const file = join13(dir, "digest", "digest.json");
6862
- if (!existsSync11(file)) return null;
7205
+ const file = join14(dir, "digest", "digest.json");
7206
+ if (!existsSync12(file)) return null;
6863
7207
  try {
6864
7208
  const d = JSON.parse(await readFile12(file, "utf8"));
6865
7209
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
@@ -6928,11 +7272,11 @@ async function planTake(dir, opts = {}) {
6928
7272
  if (opts.reuse) {
6929
7273
  const prev = migrateMotion2(opts.reuse.doc);
6930
7274
  const artifact = {
6931
- videoKey: RECORDING_NAME,
7275
+ videoKey: take.paths.recordingName,
6932
7276
  cursor,
6933
7277
  meta
6934
7278
  };
6935
- doc = projectFromArtifact(artifact, RECORDING_NAME, ingest).doc;
7279
+ doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
6936
7280
  doc = copyStyle(prev, doc);
6937
7281
  const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
6938
7282
  doc.segments = rt.segments;
@@ -7020,11 +7364,11 @@ async function planTake(dir, opts = {}) {
7020
7364
  }
7021
7365
  } else {
7022
7366
  const artifact = {
7023
- videoKey: RECORDING_NAME,
7367
+ videoKey: take.paths.recordingName,
7024
7368
  cursor,
7025
7369
  meta
7026
7370
  };
7027
- doc = projectFromArtifact(artifact, RECORDING_NAME, ingest).doc;
7371
+ doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
7028
7372
  if (opts.style) {
7029
7373
  const applied = applyStyle(opts.style.doc, doc, opts);
7030
7374
  doc = applied.doc;
@@ -7074,207 +7418,11 @@ import { createHash } from "crypto";
7074
7418
  import { existsSync as existsSync13 } from "fs";
7075
7419
  import { readFile as readFile13 } from "fs/promises";
7076
7420
  import { createInterface } from "readline/promises";
7077
- import { basename, join as join16 } from "path";
7421
+ import { basename as basename3, join as join16 } from "path";
7078
7422
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
7079
7423
 
7080
- // src/plugin/media.ts
7081
- import { isKeyedOverlay } from "@vosjs/studio-core";
7082
- import { createWriteStream, existsSync as existsSync12 } from "fs";
7083
- import { writeFile as writeFile9 } from "fs/promises";
7084
- import { join as join14 } from "path";
7085
- import { Readable } from "stream";
7086
- import { pipeline } from "stream/promises";
7087
- var MIC_NAME = "mic.webm";
7088
- var CAM_NAME = "cam.webm";
7089
- var KEYS = [
7090
- { key: "videoKey", file: RECORDING_NAME },
7091
- { key: "micKey", file: MIC_NAME },
7092
- { key: "camKey", file: CAM_NAME }
7093
- ];
7094
- function isTakeRelativeKey(key) {
7095
- return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
7096
- }
7097
- var takeRelativeFile = (key) => key.replace(/^\/+/, "");
7098
- function docMediaRefs(doc, keep = isTakeRelativeKey) {
7099
- const out = [];
7100
- for (const m of doc.media ?? []) {
7101
- if (keep(m.videoKey))
7102
- out.push({
7103
- where: `media ${m.id}`,
7104
- key: m.videoKey,
7105
- set: (next) => {
7106
- m.videoKey = next;
7107
- }
7108
- });
7109
- const mic = m.micKey;
7110
- if (mic && keep(mic))
7111
- out.push({
7112
- where: `media ${m.id} mic`,
7113
- key: mic,
7114
- set: (next) => {
7115
- m.micKey = next;
7116
- }
7117
- });
7118
- const cam = m.camKey;
7119
- if (cam && keep(cam))
7120
- out.push({
7121
- where: `media ${m.id} cam`,
7122
- key: cam,
7123
- set: (next) => {
7124
- m.camKey = next;
7125
- }
7126
- });
7127
- }
7128
- for (const clip of doc.overlays ?? []) {
7129
- if (!isKeyedOverlay(clip) || !keep(clip.key)) continue;
7130
- out.push({
7131
- where: `overlay ${clip.id}`,
7132
- key: clip.key,
7133
- set: (next) => {
7134
- clip.key = next;
7135
- }
7136
- });
7137
- }
7138
- const mark = doc.endCard?.mark;
7139
- if (mark && keep(mark.key))
7140
- out.push({
7141
- where: "end card mark",
7142
- key: mark.key,
7143
- set: (next) => {
7144
- mark.key = next;
7145
- }
7146
- });
7147
- const bg = doc.frame.backgroundMedia;
7148
- if (bg && keep(bg.key))
7149
- out.push({
7150
- where: "background",
7151
- key: bg.key,
7152
- set: (next) => {
7153
- bg.key = next;
7154
- }
7155
- });
7156
- return out;
7157
- }
7158
- function mediaContentType(file) {
7159
- const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
7160
- const types = {
7161
- svg: "image/svg+xml",
7162
- png: "image/png",
7163
- jpg: "image/jpeg",
7164
- jpeg: "image/jpeg",
7165
- webp: "image/webp",
7166
- gif: "image/gif",
7167
- avif: "image/avif",
7168
- webm: "video/webm",
7169
- mp4: "video/mp4",
7170
- mov: "video/quicktime",
7171
- mp3: "audio/mpeg",
7172
- wav: "audio/wav",
7173
- ogg: "audio/ogg",
7174
- m4a: "audio/mp4"
7175
- };
7176
- return types[ext] ?? "application/octet-stream";
7177
- }
7178
- function extensionFor(contentType) {
7179
- const t = contentType.split(";")[0].trim().toLowerCase();
7180
- const exts = {
7181
- "image/svg+xml": ".svg",
7182
- "image/png": ".png",
7183
- "image/jpeg": ".jpg",
7184
- "image/webp": ".webp",
7185
- "image/gif": ".gif",
7186
- "image/avif": ".avif",
7187
- "video/webm": ".webm",
7188
- "video/mp4": ".mp4",
7189
- "video/quicktime": ".mov",
7190
- "audio/mpeg": ".mp3",
7191
- "audio/wav": ".wav",
7192
- "audio/ogg": ".ogg",
7193
- "audio/mp4": ".m4a"
7194
- };
7195
- return exts[t] ?? "";
7196
- }
7197
- function assetIdOf(url) {
7198
- if (!url) return null;
7199
- const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
7200
- return m ? m[1] : null;
7201
- }
7202
- async function pullMedia(ctx, dir, doc, log) {
7203
- const result = { downloaded: [], kept: [] };
7204
- for (const { key, file } of KEYS) {
7205
- const url = doc.source[key];
7206
- const assetId = assetIdOf(url);
7207
- if (!assetId) continue;
7208
- const target = join14(dir, file);
7209
- if (existsSync12(target)) {
7210
- result.kept.push(file);
7211
- } else {
7212
- const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
7213
- const res = await fetch(abs, {
7214
- headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
7215
- });
7216
- if (!res.ok || !res.body) {
7217
- throw new Error(
7218
- `download of ${file} failed (${res.status})${res.status === 404 ? " \u2014 a private recording needs a content key of its owner" : ""}`
7219
- );
7220
- }
7221
- await pipeline(
7222
- Readable.fromWeb(res.body),
7223
- createWriteStream(target)
7224
- );
7225
- const bytes = (await import("fs/promises").then((fs) => fs.stat(target))).size;
7226
- result.downloaded.push({ file, assetId, bytes });
7227
- log(` ${file} \u2190 asset ${assetId} (${Math.round(bytes / 1024)} kB)`);
7228
- }
7229
- doc.source[key] = file;
7230
- }
7231
- for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
7232
- const assetId = assetIdOf(ref.key);
7233
- const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
7234
- const existing = existsSync12(join14(dir, "media")) ? (await import("fs/promises").then(
7235
- (fs) => fs.readdir(join14(dir, "media"))
7236
- )).find((f) => f.startsWith(`${assetId}.`)) : void 0;
7237
- let file;
7238
- if (existing) {
7239
- file = `media/${existing}`;
7240
- result.kept.push(file);
7241
- } else {
7242
- const res = await fetch(abs, {
7243
- headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
7244
- });
7245
- if (!res.ok || !res.body) {
7246
- throw new Error(
7247
- `download of ${ref.where} (${ref.key}) failed (${res.status})`
7248
- );
7249
- }
7250
- const ext = extensionFor(res.headers.get("content-type") ?? "");
7251
- file = `media/${assetId}${ext}`;
7252
- const target = join14(dir, file);
7253
- await import("fs/promises").then(
7254
- (fs) => fs.mkdir(join14(dir, "media"), { recursive: true })
7255
- );
7256
- await pipeline(
7257
- Readable.fromWeb(res.body),
7258
- createWriteStream(target)
7259
- );
7260
- const bytes = (await import("fs/promises").then((fs) => fs.stat(target))).size;
7261
- result.downloaded.push({ file, assetId, bytes });
7262
- log(
7263
- ` ${file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(bytes / 1024)} kB)`
7264
- );
7265
- }
7266
- ref.set(file);
7267
- }
7268
- const metaPath = join14(dir, "meta.json");
7269
- if (!existsSync12(metaPath)) await writeJson(metaPath, doc.source.meta, true);
7270
- const cursorPath = join14(dir, "cursor.json");
7271
- if (!existsSync12(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
7272
- await writeFile9(join14(dir, "doc.json"), JSON.stringify(doc, null, 2));
7273
- return result;
7274
- }
7275
-
7276
7424
  // src/plugin/folder.ts
7277
- import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
7425
+ import { mkdir as mkdir8, writeFile as writeFile10 } from "fs/promises";
7278
7426
  import { join as join15 } from "path";
7279
7427
  import { recipeHints } from "@vosjs/shared/frontmatter";
7280
7428
  import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
@@ -7385,7 +7533,7 @@ async function cmdMove(argv) {
7385
7533
  });
7386
7534
  const key = requireCredential(strFlag(flags, "key"));
7387
7535
  const folderId = toRef === "none" ? null : resolveFolder(await listFolders(origin, key), toRef).id;
7388
- let failed = 0;
7536
+ let failed2 = 0;
7389
7537
  const moved = [];
7390
7538
  for (const raw of positionals) {
7391
7539
  const id = parseVosId(raw);
@@ -7400,28 +7548,28 @@ async function cmdMove(argv) {
7400
7548
  continue;
7401
7549
  }
7402
7550
  if (asVos.status === 404) {
7403
- const asAsset = await apiJson(origin, `/api/assets/${id}`, {
7551
+ const asAsset2 = await apiJson(origin, `/api/assets/${id}`, {
7404
7552
  method: "PATCH",
7405
7553
  key,
7406
7554
  body: { folderId }
7407
7555
  });
7408
- if (asAsset.status === 200) {
7556
+ if (asAsset2.status === 200) {
7409
7557
  moved.push({ id, kind: "asset" });
7410
7558
  r.log(`moved asset ${id}`);
7411
7559
  continue;
7412
7560
  }
7413
- failed++;
7414
- r.log(apiError(`move ${id}`, asAsset));
7561
+ failed2++;
7562
+ r.log(apiError(`move ${id}`, asAsset2));
7415
7563
  continue;
7416
7564
  }
7417
- failed++;
7565
+ failed2++;
7418
7566
  r.log(apiError(`move ${id}`, asVos));
7419
7567
  }
7420
7568
  r.done(
7421
- { moved, failed, folderId },
7422
- `moved ${moved.length}/${positionals.length} into ${toRef === "none" ? "the top level" : toRef}${failed ? ` \u2014 ${failed} failed` : ""}`
7569
+ { moved, failed: failed2, folderId },
7570
+ `moved ${moved.length}/${positionals.length} into ${toRef === "none" ? "the top level" : toRef}${failed2 ? ` \u2014 ${failed2} failed` : ""}`
7423
7571
  );
7424
- return failed ? EXIT_ERROR : EXIT_OK;
7572
+ return failed2 ? EXIT_ERROR : EXIT_OK;
7425
7573
  }
7426
7574
  function recipeLine(filename, body) {
7427
7575
  const h = recipeHints(body);
@@ -7451,7 +7599,7 @@ async function cmdPull(argv) {
7451
7599
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
7452
7600
  const payload = res.body;
7453
7601
  const out = strFlag(flags, "out") ?? folder.slug;
7454
- await mkdir7(join15(out, "recipes"), { recursive: true });
7602
+ await mkdir8(join15(out, "recipes"), { recursive: true });
7455
7603
  const lines = [];
7456
7604
  const recipes = [];
7457
7605
  for (const rec of payload.recipes) {
@@ -7461,7 +7609,7 @@ async function cmdPull(argv) {
7461
7609
  }
7462
7610
  for (const rec of payload.inheritedRecipes) {
7463
7611
  const from = safeName(rec.folderSlug ?? "inherited");
7464
- await mkdir7(join15(out, "recipes", "_inherited", from), { recursive: true });
7612
+ await mkdir8(join15(out, "recipes", "_inherited", from), { recursive: true });
7465
7613
  const path = join15(
7466
7614
  out,
7467
7615
  "recipes",
@@ -7478,7 +7626,7 @@ async function cmdPull(argv) {
7478
7626
  const members = [];
7479
7627
  for (const v of payload.voses) {
7480
7628
  const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
7481
- await mkdir7(dir, { recursive: true });
7629
+ await mkdir8(dir, { recursive: true });
7482
7630
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
7483
7631
  if (cfg.status !== 200) {
7484
7632
  lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
@@ -7567,6 +7715,144 @@ async function cmdFolder(argv) {
7567
7715
  }
7568
7716
  }
7569
7717
 
7718
+ // src/plugin/uploadAsset.ts
7719
+ var SINGLE_SHOT_MAX_BYTES = 32 * 1024 * 1024;
7720
+ var PART_ATTEMPTS = 3;
7721
+ function planParts(totalBytes, partBytes) {
7722
+ if (totalBytes <= 0 || partBytes <= 0) return [];
7723
+ const parts = [];
7724
+ for (let start = 0; start < totalBytes; start += partBytes) {
7725
+ parts.push({
7726
+ partNumber: parts.length + 1,
7727
+ start,
7728
+ end: Math.min(start + partBytes, totalBytes)
7729
+ });
7730
+ }
7731
+ return parts;
7732
+ }
7733
+ function failed(what, r) {
7734
+ const detail = typeof r.body.error === "string" ? r.body.error : "";
7735
+ return new Error(`${what} (${r.status})${detail ? `: ${detail}` : ""}`);
7736
+ }
7737
+ function asAsset(body) {
7738
+ return {
7739
+ id: String(body.id),
7740
+ url: String(body.url),
7741
+ size: typeof body.size === "number" ? body.size : 0,
7742
+ reused: body.reused === true
7743
+ };
7744
+ }
7745
+ function transient(status) {
7746
+ return status >= 500 || status === 429;
7747
+ }
7748
+ var wait = (ms) => new Promise((r) => setTimeout(r, ms));
7749
+ async function uploadAsset(target, bytes, opts) {
7750
+ return bytes.length > SINGLE_SHOT_MAX_BYTES ? uploadInParts(target, bytes, opts) : uploadWhole(target, bytes, opts);
7751
+ }
7752
+ async function uploadWhole(target, bytes, opts) {
7753
+ const res = await apiJson(target.origin, "/api/assets/recording", {
7754
+ method: "POST",
7755
+ key: target.key,
7756
+ headers: {
7757
+ "Content-Type": opts.contentType,
7758
+ "Content-Length": String(bytes.length),
7759
+ "X-Filename": opts.filename,
7760
+ "X-Content-Hash": opts.contentHash,
7761
+ ...opts.durationSeconds && opts.durationSeconds > 0 ? { "X-Content-Duration": opts.durationSeconds.toFixed(3) } : {}
7762
+ },
7763
+ raw: bytes
7764
+ });
7765
+ if (res.status !== 201 && res.status !== 200)
7766
+ throw failed("upload failed", res);
7767
+ return asAsset(res.body);
7768
+ }
7769
+ async function uploadInParts(target, bytes, opts) {
7770
+ const begun = await apiJson(
7771
+ target.origin,
7772
+ "/api/assets/recording/multipart",
7773
+ {
7774
+ method: "POST",
7775
+ key: target.key,
7776
+ body: {
7777
+ filename: opts.filename,
7778
+ contentType: opts.contentType,
7779
+ size: bytes.length,
7780
+ contentHash: opts.contentHash,
7781
+ ...opts.durationSeconds && opts.durationSeconds > 0 ? { durationSeconds: opts.durationSeconds } : {}
7782
+ }
7783
+ }
7784
+ );
7785
+ if (begun.status !== 201 && begun.status !== 200) {
7786
+ throw failed("upload failed", begun);
7787
+ }
7788
+ if (begun.body.reused === true || typeof begun.body.url === "string") {
7789
+ return asAsset(begun.body);
7790
+ }
7791
+ const uploadId = String(begun.body.id);
7792
+ const partBytes = Number(begun.body.partBytes);
7793
+ if (!Number.isFinite(partBytes) || partBytes <= 0) {
7794
+ throw new Error("upload failed: the platform returned no part size");
7795
+ }
7796
+ const parts = planParts(bytes.length, partBytes);
7797
+ const etags = [];
7798
+ try {
7799
+ for (const part of parts) {
7800
+ const chunk2 = bytes.subarray(part.start, part.end);
7801
+ let sent = null;
7802
+ for (let attempt = 1; attempt <= PART_ATTEMPTS; attempt++) {
7803
+ try {
7804
+ sent = await apiJson(
7805
+ target.origin,
7806
+ `/api/assets/recording/multipart/${uploadId}/parts/${part.partNumber}`,
7807
+ {
7808
+ method: "PUT",
7809
+ key: target.key,
7810
+ headers: {
7811
+ "Content-Type": "application/octet-stream",
7812
+ "Content-Length": String(chunk2.length)
7813
+ },
7814
+ raw: chunk2
7815
+ }
7816
+ );
7817
+ } catch (networkError) {
7818
+ if (attempt === PART_ATTEMPTS) throw networkError;
7819
+ await wait(500 * 2 ** (attempt - 1));
7820
+ continue;
7821
+ }
7822
+ if (sent.status === 200) break;
7823
+ if (!transient(sent.status) || attempt === PART_ATTEMPTS) {
7824
+ throw failed(`part ${part.partNumber} failed`, sent);
7825
+ }
7826
+ await wait(500 * 2 ** (attempt - 1));
7827
+ }
7828
+ if (!sent || sent.status !== 200) {
7829
+ throw new Error(`part ${part.partNumber} failed`);
7830
+ }
7831
+ etags.push({
7832
+ partNumber: Number(sent.body.partNumber),
7833
+ etag: String(sent.body.etag)
7834
+ });
7835
+ opts.onPart?.(etags.length, parts.length);
7836
+ }
7837
+ const sealed = await apiJson(
7838
+ target.origin,
7839
+ `/api/assets/recording/multipart/${uploadId}/complete`,
7840
+ { method: "POST", key: target.key, body: { parts: etags } }
7841
+ );
7842
+ if (sealed.status !== 201 && sealed.status !== 200) {
7843
+ throw failed("upload failed", sealed);
7844
+ }
7845
+ return asAsset(sealed.body);
7846
+ } catch (err) {
7847
+ await apiJson(
7848
+ target.origin,
7849
+ `/api/assets/recording/multipart/${uploadId}`,
7850
+ { method: "DELETE", key: target.key }
7851
+ ).catch(() => void 0);
7852
+ throw err;
7853
+ }
7854
+ }
7855
+
7570
7856
  // src/plugin/sync.ts
7571
7857
  function apiContext(flags) {
7572
7858
  return {
@@ -7615,7 +7901,9 @@ async function pushTake(dir, flags, r) {
7615
7901
  ${lint.problems.join("\n ")}`);
7616
7902
  }
7617
7903
  if (!existsSync13(take.paths.recording)) {
7618
- throw new Error("no recording.webm in this take");
7904
+ throw new Error(
7905
+ "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
7906
+ );
7619
7907
  }
7620
7908
  const ctx = apiContext(flags);
7621
7909
  const state = readSyncState(dir);
@@ -7648,32 +7936,36 @@ async function pushTake(dir, flags, r) {
7648
7936
  }
7649
7937
  const bytes = await readFile13(take.paths.recording);
7650
7938
  const hash = createHash("sha256").update(bytes).digest("hex");
7939
+ const { type: contentType } = resolveMediaType({
7940
+ head: bytes.subarray(0, MEDIA_HEAD_BYTES),
7941
+ filename: take.paths.recordingName
7942
+ });
7943
+ const filename = nameForType(take.paths.recordingName, contentType);
7944
+ if (filename !== take.paths.recordingName) {
7945
+ r.log(
7946
+ ` ${take.paths.recordingName} holds ${contentType} \u2014 uploading it as ${filename}`
7947
+ );
7948
+ }
7651
7949
  r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
7652
- const upload = await api(ctx, "/assets/recording", {
7653
- method: "POST",
7654
- headers: {
7655
- "Content-Type": "video/webm",
7656
- "Content-Length": String(bytes.length),
7657
- "X-Filename": RECORDING_NAME,
7658
- "X-Content-Hash": hash,
7950
+ let upload;
7951
+ try {
7952
+ upload = await uploadAsset(ctx, new Uint8Array(bytes), {
7953
+ filename,
7954
+ contentType,
7955
+ contentHash: hash,
7659
7956
  // The take's length so the server holds it to the plan's cap.
7660
- ...take.meta.durationMs > 0 ? { "X-Content-Duration": (take.meta.durationMs / 1e3).toFixed(3) } : {}
7661
- },
7662
- raw: new Uint8Array(bytes)
7663
- });
7664
- if (upload.status !== 201 && upload.status !== 200) {
7957
+ ...take.meta.durationMs > 0 ? { durationSeconds: take.meta.durationMs / 1e3 } : {},
7958
+ onPart: (done, total) => r.log(` part ${done}/${total}`)
7959
+ });
7960
+ } catch (e) {
7665
7961
  throw new Error(
7666
- `recording upload failed (${upload.status}): ${String(upload.json.error ?? "")}`
7962
+ `recording upload failed: ${e instanceof Error ? e.message : String(e)}`
7667
7963
  );
7668
7964
  }
7669
- const assetId = String(upload.json.id);
7670
- const assetUrl = String(upload.json.url);
7671
- r.event({
7672
- event: "recording",
7673
- assetId,
7674
- reused: upload.json.reused === true
7675
- });
7676
- if (upload.json.reused === true) r.log(" recording already hosted \u2014 reused");
7965
+ const assetId = upload.id;
7966
+ const assetUrl = upload.url;
7967
+ r.event({ event: "recording", assetId, reused: upload.reused });
7968
+ if (upload.reused) r.log(" recording already hosted \u2014 reused");
7677
7969
  const docForPush = structuredClone(take.doc);
7678
7970
  docForPush.source.videoKey = assetUrl;
7679
7971
  if (docForPush.source.camKey && !/^(https?:)?\//.test(docForPush.source.camKey)) {
@@ -7692,31 +7984,35 @@ async function pushTake(dir, flags, r) {
7692
7984
  }
7693
7985
  const media = await readFile13(file);
7694
7986
  const mediaHash = createHash("sha256").update(media).digest("hex");
7695
- const type = mediaContentType(file);
7696
- const put = await api(ctx, "/assets/recording", {
7697
- method: "POST",
7698
- headers: {
7699
- "Content-Type": type,
7700
- "Content-Length": String(media.length),
7701
- "X-Filename": basename(file),
7702
- "X-Content-Hash": mediaHash
7703
- },
7704
- raw: new Uint8Array(media)
7987
+ const { type } = resolveMediaType({
7988
+ head: media.subarray(0, MEDIA_HEAD_BYTES),
7989
+ filename: file
7705
7990
  });
7706
- if (put.status !== 201 && put.status !== 200) {
7991
+ const name = nameForType(basename3(file), type);
7992
+ if (name !== basename3(file)) {
7993
+ r.log(` ${ref.where}: ${basename3(file)} holds ${type} \u2014 sent as ${name}`);
7994
+ }
7995
+ let put;
7996
+ try {
7997
+ put = await uploadAsset(ctx, new Uint8Array(media), {
7998
+ filename: name,
7999
+ contentType: type,
8000
+ contentHash: mediaHash
8001
+ });
8002
+ } catch (e) {
7707
8003
  throw new Error(
7708
- `${ref.where} upload failed (${put.status}): ${String(put.json.error ?? "")}`
8004
+ `${ref.where} upload failed: ${e instanceof Error ? e.message : String(e)}`
7709
8005
  );
7710
8006
  }
7711
- ref.set(String(put.json.url));
8007
+ ref.set(put.url);
7712
8008
  r.log(
7713
- ` ${ref.where}: ${ref.key} \u2192 asset ${String(put.json.id)}${put.json.reused === true ? " (reused)" : ""}`
8009
+ ` ${ref.where}: ${ref.key} \u2192 asset ${put.id}${put.reused ? " (reused)" : ""}`
7714
8010
  );
7715
8011
  }
7716
8012
  const lowered = lowerToComposition3(docForPush);
7717
8013
  const config = { ...lowered.config, data: lowered.data };
7718
8014
  if (!state) {
7719
- const title = flags.title ?? `Take ${basename(dir)} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
8015
+ const title = flags.title ?? `Take ${basename3(dir)} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
7720
8016
  const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "take"}-${Date.now().toString(36)}`;
7721
8017
  const created = await api(ctx, "/vos", {
7722
8018
  method: "POST",
@@ -7888,8 +8184,9 @@ async function pullTake(dir, flags, r) {
7888
8184
  }
7889
8185
  const hostedDoc = migrateHostedDoc3(docRes.json);
7890
8186
  const doc = hostedDoc;
7891
- if (existsSync13(join16(dir, RECORDING_NAME))) {
7892
- doc.source.videoKey = RECORDING_NAME;
8187
+ const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
8188
+ if (existsSync13(localRecording)) {
8189
+ doc.source.videoKey = basename3(localRecording);
7893
8190
  }
7894
8191
  await writeJson(join16(dir, "doc.json"), doc, true);
7895
8192
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
@@ -7911,9 +8208,9 @@ async function pullTake(dir, flags, r) {
7911
8208
 
7912
8209
  // src/plugin/program.ts
7913
8210
  import { existsSync as existsSync14 } from "fs";
7914
- import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8211
+ import { mkdir as mkdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
7915
8212
  import { createInterface as createInterface2 } from "readline/promises";
7916
- import { basename as basename2, dirname as dirname3, join as join17 } from "path";
8213
+ import { basename as basename4, dirname as dirname4, join as join17 } from "path";
7917
8214
  import {
7918
8215
  CURRENT_CONFIG_VERSION,
7919
8216
  migrateConfig,
@@ -8110,7 +8407,7 @@ async function cmdFetch(argv) {
8110
8407
  throw new Error(apiError(`fetch config for ${vosId}`, cfg));
8111
8408
  const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
8112
8409
  const out = strFlag(flags, "out") ?? slug;
8113
- await mkdir8(out, { recursive: true });
8410
+ await mkdir9(out, { recursive: true });
8114
8411
  await writeFile11(
8115
8412
  join17(out, "config.json"),
8116
8413
  JSON.stringify(cfg.body.config, null, 2)
@@ -8254,7 +8551,7 @@ async function cmdPushProgram(argv) {
8254
8551
  origin: strFlag(flags, "origin"),
8255
8552
  api: strFlag(flags, "api")
8256
8553
  });
8257
- const dir = dirname3(source);
8554
+ const dir = dirname4(source);
8258
8555
  const parsed = JSON.parse(await readFile14(source, "utf8"));
8259
8556
  const pre = preflightConfig(parsed);
8260
8557
  if (!pre.ok || !pre.config) {
@@ -8282,7 +8579,7 @@ async function cmdPushProgram(argv) {
8282
8579
  "--claimable creates a NEW claimable vos and cannot iterate (--vos). After the human claims it, iterate with their key: vos push --vos <id>"
8283
8580
  );
8284
8581
  }
8285
- const fallback = state?.title ? `${state.title} remix` : basename2(source).replace(/\.json$/i, "") || "vos program";
8582
+ const fallback = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos program";
8286
8583
  const title2 = (strFlag(flags, "title") ?? fallback).slice(0, 100);
8287
8584
  const body = { title: title2, config };
8288
8585
  const slug = strFlag(flags, "slug");
@@ -8380,7 +8677,7 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
8380
8677
  return EXIT_OK;
8381
8678
  }
8382
8679
  const remixOfId = strFlag(flags, "remix-of") ?? state?.vosId;
8383
- const fallbackTitle = state?.title ? `${state.title} remix` : basename2(source).replace(/\.json$/i, "") || "vos remix";
8680
+ const fallbackTitle = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos remix";
8384
8681
  const title = (strFlag(flags, "title") ?? fallbackTitle).slice(0, 100);
8385
8682
  const folderId = folderRef ? resolveFolder(await listFolders(origin, key), folderRef).id : void 0;
8386
8683
  const created = await createProgramVos({
@@ -8432,7 +8729,7 @@ Iterate with: vos push ${source} --vos ${created.id}`
8432
8729
  async function cmdPullProgram(argv) {
8433
8730
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
8434
8731
  const target = positionals[0] ?? ".";
8435
- const dir = target.endsWith(".json") ? dirname3(target) : target;
8732
+ const dir = target.endsWith(".json") ? dirname4(target) : target;
8436
8733
  const r = createReporter(flags.json === true);
8437
8734
  const origin = platformOrigin({
8438
8735
  origin: strFlag(flags, "origin"),
@@ -8615,7 +8912,7 @@ async function writeProgramDoc(dir, hosted) {
8615
8912
 
8616
8913
  // src/plugin/asset.ts
8617
8914
  import { readFileSync as readFileSync4 } from "fs";
8618
- import { basename as basename3, extname as extname2 } from "path";
8915
+ import { basename as basename5, extname as extname2 } from "path";
8619
8916
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
8620
8917
  var MIME_BY_EXT = {
8621
8918
  ".png": "image/png",
@@ -8666,7 +8963,7 @@ async function cmdPush(argv) {
8666
8963
  const folder = folderRef ? resolveFolder(await listFolders(origin, key), folderRef) : null;
8667
8964
  const uploaded = [];
8668
8965
  for (const file of positionals) {
8669
- const name = basename3(file);
8966
+ const name = basename5(file);
8670
8967
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
8671
8968
  const body = readFileSync4(file);
8672
8969
  const form = new FormData();
@@ -8723,11 +9020,11 @@ async function cmdAsset(argv) {
8723
9020
 
8724
9021
  // src/plugin/recipe.ts
8725
9022
  import { readFileSync as readFileSync5 } from "fs";
8726
- import { basename as basename4 } from "path";
9023
+ import { basename as basename6 } from "path";
8727
9024
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
8728
9025
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
8729
9026
  function recipeFilename(file) {
8730
- const name = basename4(file);
9027
+ const name = basename6(file);
8731
9028
  if (!/\.md$/i.test(name)) {
8732
9029
  throw new UsageError(`a recipe is a markdown file \u2014 ${name} is not .md`);
8733
9030
  }
@@ -10033,7 +10330,7 @@ async function cmdRecord(argv) {
10033
10330
  `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" : "")
10034
10331
  );
10035
10332
  }
10036
- await mkdir9(outDir, { recursive: true });
10333
+ await mkdir10(outDir, { recursive: true });
10037
10334
  const paths = await ensureTakeDir(outDir);
10038
10335
  const browser = await launchBrowser();
10039
10336
  try {
@@ -10117,7 +10414,7 @@ async function cmdCreate2(argv) {
10117
10414
  `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" : "")
10118
10415
  );
10119
10416
  }
10120
- await mkdir9(outDir, { recursive: true });
10417
+ await mkdir10(outDir, { recursive: true });
10121
10418
  const paths = await ensureTakeDir(outDir);
10122
10419
  const browser = await launchBrowser();
10123
10420
  try {
@@ -10202,7 +10499,7 @@ async function cmdPlan(argv) {
10202
10499
  );
10203
10500
  const r = createReporter(flags.json === true);
10204
10501
  if (flags.fresh === true) {
10205
- await rm4(join18(dir, "doc.json"), { force: true });
10502
+ await rm5(join18(dir, "doc.json"), { force: true });
10206
10503
  r.log("note: --fresh discarded the existing doc.json");
10207
10504
  }
10208
10505
  let reuse;
@@ -10766,7 +11063,7 @@ async function cmdOpen(argv) {
10766
11063
  const r = createReporter(flags.json === true);
10767
11064
  const take = await loadTake(dir);
10768
11065
  if (!existsSync15(take.paths.recording)) {
10769
- throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
11066
+ throw new UsageError(`${dir} has no recording \u2014 re-run record`);
10770
11067
  }
10771
11068
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
10772
11069
  /\/+$/,
@@ -11003,8 +11300,15 @@ async function cmdValidate(argv) {
11003
11300
  const take = await loadTake(target);
11004
11301
  const problems = [];
11005
11302
  const warnings = [];
11006
- if (!existsSync15(take.paths.recording))
11007
- problems.push("missing recording.webm (re-run record)");
11303
+ if (!existsSync15(take.paths.recording)) {
11304
+ problems.push("missing recording (re-run record)");
11305
+ } else {
11306
+ const mismatch = containerMismatch(
11307
+ take.paths.recordingName,
11308
+ await readMediaHead(take.paths.recording)
11309
+ );
11310
+ if (mismatch) warnings.push(mismatch);
11311
+ }
11008
11312
  if (!take.doc) problems.push("missing doc.json (run plan)");
11009
11313
  if (take.doc) {
11010
11314
  const lint = lintDoc(take.doc);
@@ -11224,6 +11528,7 @@ export {
11224
11528
  RECORDING_NAME,
11225
11529
  takePaths,
11226
11530
  loadTake,
11531
+ pullMedia,
11227
11532
  startTakeServer,
11228
11533
  waitForPageDone,
11229
11534
  renderAnimation,
@@ -11240,10 +11545,9 @@ export {
11240
11545
  recordTake,
11241
11546
  encodeRecording,
11242
11547
  planTake,
11243
- pullMedia,
11244
11548
  parseAgentBrowserLog,
11245
11549
  splitCommand,
11246
11550
  convertAgentBrowser,
11247
11551
  run
11248
11552
  };
11249
- //# sourceMappingURL=chunk-VQXOM2NU.js.map
11553
+ //# sourceMappingURL=chunk-DD6OXVFJ.js.map