@vosjs/cli 0.36.0 → 0.38.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.
@@ -2,11 +2,13 @@
2
2
  import {
3
3
  BrowserUnavailableError,
4
4
  directoryKind,
5
- launchBrowser
6
- } from "./chunk-NHKHTIDR.js";
5
+ launchBrowser,
6
+ noteGivenFlag,
7
+ trackFlags
8
+ } from "./chunk-FACYY7VV.js";
7
9
 
8
10
  // src/plugin/run.ts
9
- import { mkdir as mkdir9, readFile as readFile15, rm as rm4 } from "fs/promises";
11
+ import { mkdir as mkdir10, readFile as readFile15, rm as rm5 } from "fs/promises";
10
12
  import { existsSync as existsSync15 } from "fs";
11
13
  import { join as join18, resolve as resolve9 } from "path";
12
14
  import {
@@ -52,7 +54,9 @@ function parseArgs(argv, booleanFlags, multiFlags = /* @__PURE__ */ new Set()) {
52
54
  }
53
55
  positionals.push(arg);
54
56
  }
55
- return { positionals, flags, multi };
57
+ for (const name of Object.keys(flags)) noteGivenFlag(name);
58
+ for (const name of Object.keys(multi)) noteGivenFlag(name);
59
+ return { positionals, flags: trackFlags(flags), multi: trackFlags(multi) };
56
60
  }
57
61
  function numFlag(flags, name, fallback) {
58
62
  const v = flags[name];
@@ -943,6 +947,17 @@ function lintDoc(docIn) {
943
947
  }
944
948
  const TRANSITIONS = ["none", "fade", "rise"];
945
949
  const PRESETS = ["title", "caption", "label"];
950
+ let footageEnd = null;
951
+ if (recording && Array.isArray(doc.segments)) {
952
+ try {
953
+ footageEnd = ratedSegments(doc).reduce(
954
+ (a, s) => a + (s.out - s.in) / (s.rate ?? 1),
955
+ 0
956
+ );
957
+ } catch {
958
+ footageEnd = null;
959
+ }
960
+ }
946
961
  for (let i = 0; i < (overlays?.length ?? 0); i++) {
947
962
  const o = overlays[i];
948
963
  const name = `overlays[${i}]`;
@@ -960,6 +975,14 @@ function lintDoc(docIn) {
960
975
  if (!isNum(o.duration) || o.duration <= 0) {
961
976
  problems.push(`${name}.duration must be > 0 (seconds)`);
962
977
  }
978
+ if (footageEnd !== null && footageEnd > 0 && isNum(o.start) && isNum(o.duration)) {
979
+ const over = o.start + o.duration - footageEnd;
980
+ if (over > 0.05) {
981
+ warnings.push(
982
+ o.start >= footageEnd ? `${name} starts at ${o.start.toFixed(2)}s, after the footage ends (${footageEnd.toFixed(2)}s): it plays on bare backdrop and lengthens the output by ${over.toFixed(2)}s. A trim moves the footage, never an OUTPUT-anchored layer: re-place it (vos callout with the same --id), or put a freeze under it` : `${name} ends ${over.toFixed(2)}s after the footage (${footageEnd.toFixed(2)}s): the output extends over bare backdrop. Shorten it, or put a freeze under it`
983
+ );
984
+ }
985
+ }
963
986
  checkPin(o, name, docIn, recording, problems, warnings);
964
987
  if (o.kind === "text") {
965
988
  if (typeof o.text !== "string")
@@ -1635,7 +1658,7 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1635
1658
  if (pinned) {
1636
1659
  if (pinned.clamped > 4) {
1637
1660
  warnings.push(
1638
- `overlays[${i}] is pinned ${pinned.side} of its referent but the frame has no room there \u2014 it sits ${Math.round(pinned.clamped)} design px off; a smaller box, another side, or a lower zoom level keeps it beside what it names`
1661
+ `overlays[${i}] is pinned ${pinned.side} of its referent but the frame has no room there \u2014 it sits ${Math.round(pinned.clamped)} design px off; a smaller box, another side, or a lower zoom level keeps it beside what it names. A pin resolves THROUGH the camera, so author the zoom first: a callout composed before it is measured against a frame the viewer never sees`
1639
1662
  );
1640
1663
  }
1641
1664
  return;
@@ -1681,6 +1704,392 @@ function framingWarnings(docIn, doc, zoom, overlays, warnings) {
1681
1704
  }
1682
1705
  }
1683
1706
 
1707
+ // src/plugin/container.ts
1708
+ var OCTET_STREAM = "application/octet-stream";
1709
+ var TYPES_BY_EXTENSION = {
1710
+ svg: "image/svg+xml",
1711
+ png: "image/png",
1712
+ jpg: "image/jpeg",
1713
+ jpeg: "image/jpeg",
1714
+ webp: "image/webp",
1715
+ gif: "image/gif",
1716
+ avif: "image/avif",
1717
+ webm: "video/webm",
1718
+ mp4: "video/mp4",
1719
+ mov: "video/quicktime",
1720
+ mp3: "audio/mpeg",
1721
+ wav: "audio/wav",
1722
+ ogg: "audio/ogg",
1723
+ m4a: "audio/mp4"
1724
+ };
1725
+ var EXTENSION_BY_TYPE = {
1726
+ "image/jpeg": ".jpg",
1727
+ "video/mp4": ".mp4",
1728
+ "audio/mp4": ".m4a"
1729
+ };
1730
+ var TAKE_MEDIA_EXTENSIONS = [
1731
+ ".webm",
1732
+ ".mp4",
1733
+ ".mov",
1734
+ ".m4a",
1735
+ ".mp3",
1736
+ ".ogg",
1737
+ ".wav"
1738
+ ];
1739
+ var MEDIA_HEAD_BYTES = 16;
1740
+ var ISO_BRANDS = {
1741
+ "qt ": "video/quicktime",
1742
+ "M4A ": "audio/mp4",
1743
+ "M4B ": "audio/mp4",
1744
+ avif: "image/avif",
1745
+ avis: "image/avif"
1746
+ };
1747
+ var RIFF_FORMS = {
1748
+ WEBP: "image/webp",
1749
+ WAVE: "audio/wav",
1750
+ "AVI ": "video/x-msvideo"
1751
+ };
1752
+ function sniffMediaType(head) {
1753
+ const ascii = (at2, len) => head.length >= at2 + len ? String.fromCharCode(...head.subarray(at2, at2 + len)) : "";
1754
+ const startsWith = (...bytes) => bytes.every((b, i) => head[i] === b);
1755
+ if (startsWith(26, 69, 223, 163)) return "video/webm";
1756
+ if (ascii(4, 4) === "ftyp") return ISO_BRANDS[ascii(8, 4)] ?? "video/mp4";
1757
+ if (ascii(0, 4) === "RIFF") return RIFF_FORMS[ascii(8, 4)] ?? null;
1758
+ if (startsWith(137, 80, 78, 71, 13, 10, 26, 10))
1759
+ return "image/png";
1760
+ if (startsWith(255, 216, 255)) return "image/jpeg";
1761
+ if (ascii(0, 4) === "GIF8") return "image/gif";
1762
+ if (ascii(0, 4) === "OggS") return "audio/ogg";
1763
+ if (ascii(0, 3) === "ID3") return "audio/mpeg";
1764
+ return null;
1765
+ }
1766
+ function bareType(value) {
1767
+ const type = (value ?? "").split(";")[0].trim().toLowerCase();
1768
+ return !type || type === OCTET_STREAM || type === "binary/octet-stream" ? "" : type;
1769
+ }
1770
+ function mediaContentType(file) {
1771
+ const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
1772
+ return TYPES_BY_EXTENSION[ext] ?? OCTET_STREAM;
1773
+ }
1774
+ function extensionFor(contentType) {
1775
+ const type = (contentType ?? "").split(";")[0].trim().toLowerCase();
1776
+ if (EXTENSION_BY_TYPE[type]) return EXTENSION_BY_TYPE[type];
1777
+ const ext = Object.keys(TYPES_BY_EXTENSION).find(
1778
+ (e) => TYPES_BY_EXTENSION[e] === type
1779
+ );
1780
+ return ext ? `.${ext}` : "";
1781
+ }
1782
+ function resolveMediaType(claims) {
1783
+ const sniffed = claims.head?.length ? sniffMediaType(claims.head) : null;
1784
+ if (sniffed) return { type: sniffed, from: "bytes" };
1785
+ const declared = bareType(claims.declared);
1786
+ if (declared) return { type: declared, from: "declared" };
1787
+ const named = mediaContentType(claims.filename ?? "");
1788
+ if (named !== OCTET_STREAM) return { type: named, from: "filename" };
1789
+ return { type: OCTET_STREAM, from: "unknown" };
1790
+ }
1791
+ function nameForType(filename, type) {
1792
+ const ext = extensionFor(type);
1793
+ if (!ext) return filename;
1794
+ const current = /\.[a-z0-9]+$/i.exec(filename)?.[0] ?? "";
1795
+ if (current && mediaContentType(`x${current}`) === type) return filename;
1796
+ return `${filename.slice(0, filename.length - current.length)}${ext}`;
1797
+ }
1798
+ function containerMismatch(filename, head) {
1799
+ const actual = sniffMediaType(head);
1800
+ if (!actual) return null;
1801
+ const claimed = mediaContentType(filename);
1802
+ if (claimed === actual) return null;
1803
+ return `${filename} is ${actual}, not the ${claimed} its name claims \u2014 rename it to ${nameForType(filename, actual)}`;
1804
+ }
1805
+
1806
+ // src/plugin/media.ts
1807
+ import { isKeyedOverlay } from "@vosjs/studio-core";
1808
+ import { createWriteStream, existsSync as existsSync2 } from "fs";
1809
+ import {
1810
+ mkdir as mkdir2,
1811
+ open,
1812
+ readdir,
1813
+ rename as rename2,
1814
+ rm as rm2,
1815
+ stat,
1816
+ writeFile as writeFile2
1817
+ } from "fs/promises";
1818
+ import { basename as basename2, dirname, join as join2 } from "path";
1819
+ import { Readable } from "stream";
1820
+ import { pipeline } from "stream/promises";
1821
+
1822
+ // src/plugin/take.ts
1823
+ import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
1824
+ import { existsSync, readdirSync, statSync } from "fs";
1825
+ import { basename, join } from "path";
1826
+ var RECORDING_NAME = "recording.webm";
1827
+ function takeMediaFiles(dir, stem) {
1828
+ return TAKE_MEDIA_EXTENSIONS.map((ext) => join(dir, `${stem}${ext}`)).filter(
1829
+ (file) => existsSync(file)
1830
+ );
1831
+ }
1832
+ function takeMediaFile(dir, stem, fallback) {
1833
+ return takeMediaFiles(dir, stem)[0] ?? join(dir, fallback);
1834
+ }
1835
+ var PREV_DOC_NAME = "doc.prev.json";
1836
+ function takePaths(dir) {
1837
+ const recording = takeMediaFile(dir, "recording", RECORDING_NAME);
1838
+ return {
1839
+ dir,
1840
+ recording,
1841
+ recordingName: basename(recording),
1842
+ framesDir: join(dir, "frames"),
1843
+ framesIndex: join(dir, "frames.json"),
1844
+ cursor: join(dir, "cursor.json"),
1845
+ meta: join(dir, "meta.json"),
1846
+ actions: join(dir, "actions.json"),
1847
+ doc: join(dir, "doc.json")
1848
+ };
1849
+ }
1850
+ async function ensureTakeDir(dir) {
1851
+ const p = takePaths(dir);
1852
+ await mkdir(p.framesDir, { recursive: true });
1853
+ return p;
1854
+ }
1855
+ async function dropScreencastFrames(p) {
1856
+ if (!existsSync(p.recording) || statSync(p.recording).size === 0) return 0;
1857
+ if (!existsSync(p.framesDir)) return 0;
1858
+ let freed = 0;
1859
+ for (const name of readdirSync(p.framesDir)) {
1860
+ try {
1861
+ freed += statSync(join(p.framesDir, name)).size;
1862
+ } catch {
1863
+ }
1864
+ }
1865
+ await rm(p.framesDir, { recursive: true, force: true });
1866
+ await rm(p.framesIndex, { force: true });
1867
+ return freed;
1868
+ }
1869
+ async function prepareReRecord(dir) {
1870
+ const p = takePaths(dir);
1871
+ const kept = [];
1872
+ let prevDoc = false;
1873
+ if (existsSync(p.doc)) {
1874
+ await rename(p.doc, join(dir, PREV_DOC_NAME));
1875
+ prevDoc = true;
1876
+ kept.push(PREV_DOC_NAME);
1877
+ }
1878
+ if (existsSync(p.actions)) kept.push("actions.json");
1879
+ if (existsSync(join(dir, "vos.json"))) kept.push("vos.json");
1880
+ for (const stale of [
1881
+ // Every container, not just the one `record` writes: a pulled take's
1882
+ // footage may be mp4, and a survivor would outrank the new recording.
1883
+ ...takeMediaFiles(dir, "recording"),
1884
+ ...takeMediaFiles(dir, "mic"),
1885
+ ...takeMediaFiles(dir, "cam"),
1886
+ p.cursor,
1887
+ p.meta,
1888
+ p.framesIndex,
1889
+ p.framesDir,
1890
+ join(dir, "stills"),
1891
+ join(dir, "digest")
1892
+ ]) {
1893
+ await rm(stale, { recursive: true, force: true });
1894
+ }
1895
+ return { prevDoc, kept };
1896
+ }
1897
+ async function readJson(file) {
1898
+ return JSON.parse(await readFile(file, "utf8"));
1899
+ }
1900
+ async function writeJson(file, value, pretty = false) {
1901
+ await writeFile(
1902
+ file,
1903
+ pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value)
1904
+ );
1905
+ }
1906
+ async function loadTake(dir) {
1907
+ const paths = takePaths(dir);
1908
+ if (!existsSync(paths.meta)) {
1909
+ throw new Error(
1910
+ `${dir} is not a take directory (missing meta.json) \u2014 run record first`
1911
+ );
1912
+ }
1913
+ const meta = await readJson(paths.meta);
1914
+ const cursor = existsSync(paths.cursor) ? await readJson(paths.cursor) : [];
1915
+ const actions = existsSync(paths.actions) ? await readJson(paths.actions) : null;
1916
+ const doc = existsSync(paths.doc) ? await readJson(paths.doc) : null;
1917
+ return { paths, cursor, meta, actions, doc };
1918
+ }
1919
+
1920
+ // src/plugin/media.ts
1921
+ var SOURCE_MEDIA = [
1922
+ { key: "videoKey", stem: "recording", fallbackExt: ".webm" },
1923
+ { key: "micKey", stem: "mic", fallbackExt: ".webm" },
1924
+ { key: "camKey", stem: "cam", fallbackExt: ".webm" }
1925
+ ];
1926
+ function isTakeRelativeKey(key) {
1927
+ return !!key && !/^(https?:|blob:|data:|media:|\/\/)/.test(key) && !key.startsWith("/api/");
1928
+ }
1929
+ var takeRelativeFile = (key) => key.replace(/^\/+/, "");
1930
+ function docMediaRefs(doc, keep = isTakeRelativeKey) {
1931
+ const out = [];
1932
+ for (const m of doc.media ?? []) {
1933
+ if (keep(m.videoKey))
1934
+ out.push({
1935
+ where: `media ${m.id}`,
1936
+ key: m.videoKey,
1937
+ set: (next) => {
1938
+ m.videoKey = next;
1939
+ }
1940
+ });
1941
+ const mic = m.micKey;
1942
+ if (mic && keep(mic))
1943
+ out.push({
1944
+ where: `media ${m.id} mic`,
1945
+ key: mic,
1946
+ set: (next) => {
1947
+ m.micKey = next;
1948
+ }
1949
+ });
1950
+ const cam = m.camKey;
1951
+ if (cam && keep(cam))
1952
+ out.push({
1953
+ where: `media ${m.id} cam`,
1954
+ key: cam,
1955
+ set: (next) => {
1956
+ m.camKey = next;
1957
+ }
1958
+ });
1959
+ }
1960
+ for (const clip of doc.overlays ?? []) {
1961
+ if (!isKeyedOverlay(clip) || !keep(clip.key)) continue;
1962
+ out.push({
1963
+ where: `overlay ${clip.id}`,
1964
+ key: clip.key,
1965
+ set: (next) => {
1966
+ clip.key = next;
1967
+ }
1968
+ });
1969
+ }
1970
+ const mark = doc.endCard?.mark;
1971
+ if (mark && keep(mark.key))
1972
+ out.push({
1973
+ where: "end card mark",
1974
+ key: mark.key,
1975
+ set: (next) => {
1976
+ mark.key = next;
1977
+ }
1978
+ });
1979
+ const bg = doc.frame.backgroundMedia;
1980
+ if (bg && keep(bg.key))
1981
+ out.push({
1982
+ where: "background",
1983
+ key: bg.key,
1984
+ set: (next) => {
1985
+ bg.key = next;
1986
+ }
1987
+ });
1988
+ return out;
1989
+ }
1990
+ function assetIdOf(url) {
1991
+ if (!url) return null;
1992
+ const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
1993
+ return m ? m[1] : null;
1994
+ }
1995
+ async function readMediaHead(file) {
1996
+ const handle = await open(file, "r");
1997
+ try {
1998
+ const buf = Buffer.alloc(MEDIA_HEAD_BYTES);
1999
+ const { bytesRead } = await handle.read(buf, 0, MEDIA_HEAD_BYTES, 0);
2000
+ return new Uint8Array(buf.subarray(0, bytesRead));
2001
+ } finally {
2002
+ await handle.close();
2003
+ }
2004
+ }
2005
+ async function downloadMedia(ctx, url, target) {
2006
+ const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
2007
+ const res = await fetch(abs, {
2008
+ headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
2009
+ });
2010
+ if (!res.ok || !res.body) {
2011
+ throw new Error(
2012
+ `download of ${target.describe} failed (${res.status})${res.status === 404 && target.notFoundHint ? ` \u2014 ${target.notFoundHint}` : ""}`
2013
+ );
2014
+ }
2015
+ const into = target.subdir ? join2(target.dir, target.subdir) : target.dir;
2016
+ const partial = join2(into, `${target.stem}.part`);
2017
+ await mkdir2(dirname(partial), { recursive: true });
2018
+ try {
2019
+ await pipeline(
2020
+ Readable.fromWeb(res.body),
2021
+ createWriteStream(partial)
2022
+ );
2023
+ const { type } = resolveMediaType({
2024
+ head: await readMediaHead(partial),
2025
+ declared: res.headers.get("content-type"),
2026
+ filename: `${target.stem}${target.fallbackExt}`
2027
+ });
2028
+ const name = `${target.stem}${extensionFor(type) || target.fallbackExt}`;
2029
+ await rename2(partial, join2(into, name));
2030
+ const file = target.subdir ? `${target.subdir}/${name}` : name;
2031
+ return { file, bytes: (await stat(join2(target.dir, file))).size };
2032
+ } finally {
2033
+ await rm2(partial, { force: true });
2034
+ }
2035
+ }
2036
+ async function pullMedia(ctx, dir, doc, log) {
2037
+ const result = { downloaded: [], kept: [] };
2038
+ for (const { key, stem, fallbackExt } of SOURCE_MEDIA) {
2039
+ const url = doc.source[key];
2040
+ const assetId = assetIdOf(url);
2041
+ if (!assetId) continue;
2042
+ const present = takeMediaFiles(dir, stem)[0];
2043
+ if (present) {
2044
+ result.kept.push(basename2(present));
2045
+ doc.source[key] = basename2(present);
2046
+ continue;
2047
+ }
2048
+ const got = await downloadMedia(ctx, url, {
2049
+ dir,
2050
+ stem,
2051
+ fallbackExt,
2052
+ describe: stem,
2053
+ notFoundHint: "a private recording needs a content key of its owner"
2054
+ });
2055
+ result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
2056
+ log(` ${got.file} \u2190 asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`);
2057
+ doc.source[key] = got.file;
2058
+ }
2059
+ for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
2060
+ const assetId = assetIdOf(ref.key);
2061
+ const existing = existsSync2(join2(dir, "media")) ? (await readdir(join2(dir, "media"))).find(
2062
+ (f) => f === assetId || f.startsWith(`${assetId}.`)
2063
+ ) : void 0;
2064
+ if (existing) {
2065
+ const file = `media/${existing}`;
2066
+ result.kept.push(file);
2067
+ ref.set(file);
2068
+ continue;
2069
+ }
2070
+ const got = await downloadMedia(ctx, ref.key, {
2071
+ dir,
2072
+ subdir: "media",
2073
+ stem: assetId,
2074
+ // A type with no extension here (an svg served as such) keeps the bare
2075
+ // asset id, exactly as before.
2076
+ fallbackExt: "",
2077
+ describe: `${ref.where} (${ref.key})`
2078
+ });
2079
+ result.downloaded.push({ file: got.file, assetId, bytes: got.bytes });
2080
+ log(
2081
+ ` ${got.file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(got.bytes / 1024)} kB)`
2082
+ );
2083
+ ref.set(got.file);
2084
+ }
2085
+ const metaPath = join2(dir, "meta.json");
2086
+ if (!existsSync2(metaPath)) await writeJson(metaPath, doc.source.meta, true);
2087
+ const cursorPath = join2(dir, "cursor.json");
2088
+ if (!existsSync2(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
2089
+ await writeFile2(join2(dir, "doc.json"), JSON.stringify(doc, null, 2));
2090
+ return result;
2091
+ }
2092
+
1684
2093
  // src/plugin/mediaProbe.ts
1685
2094
  var MEDIA_EXT = /\.(png|jpe?g|webp|gif|webm|mp4|mov|mp3|wav|m4a|ogg|glb|gltf|hdr|exr|woff2?)($|\?)/i;
1686
2095
  var isRemote = (v) => typeof v === "string" && /^https?:\/\//i.test(v);
@@ -1766,9 +2175,9 @@ function mediaProbeLints(results) {
1766
2175
  }
1767
2176
 
1768
2177
  // 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";
2178
+ import { mkdir as mkdir4, readFile as readFile4, rename as rename4, writeFile as writeFile5 } from "fs/promises";
2179
+ import { existsSync as existsSync5 } from "fs";
2180
+ import { join as join5, resolve as resolve2 } from "path";
1772
2181
  import { compileVosConfig } from "@vosjs/core";
1773
2182
  import {
1774
2183
  docStillTime,
@@ -1780,83 +2189,11 @@ import {
1780
2189
  spanOutputExtent as spanOutputExtent2
1781
2190
  } from "@vosjs/studio-core";
1782
2191
 
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
2192
  // src/plugin/server.ts
1856
2193
  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";
2194
+ import { existsSync as existsSync3, readFileSync, statSync as statSync2 } from "fs";
2195
+ import { writeFile as writeFile3 } from "fs/promises";
2196
+ import { extname, join as join3 } from "path";
1860
2197
  var MIME = {
1861
2198
  ".html": "text/html",
1862
2199
  ".js": "text/javascript",
@@ -1890,7 +2227,7 @@ function startTakeServer(rootDir, pages) {
1890
2227
  const chunks = [];
1891
2228
  req.on("data", (c) => chunks.push(c));
1892
2229
  req.on("end", () => {
1893
- void writeFile2(join2(rootDir, name), Buffer.concat(chunks)).then(
2230
+ void writeFile3(join3(rootDir, name), Buffer.concat(chunks)).then(
1894
2231
  () => res.writeHead(200).end("ok"),
1895
2232
  (e) => res.writeHead(500).end(String(e))
1896
2233
  );
@@ -1902,11 +2239,11 @@ function startTakeServer(rootDir, pages) {
1902
2239
  res.end(pages[url.pathname]);
1903
2240
  return;
1904
2241
  }
1905
- const file = join2(rootDir, decodeURIComponent(url.pathname));
1906
- if (existsSync2(file) && statSync(file).isFile()) {
1907
- const type = MIME[extname(file)] ?? "application/octet-stream";
1908
- const size = statSync(file).size;
2242
+ const file = join3(rootDir, decodeURIComponent(url.pathname));
2243
+ if (existsSync3(file) && statSync2(file).isFile()) {
2244
+ const size = statSync2(file).size;
1909
2245
  const buf = readFileSync(file);
2246
+ const type = sniffMediaType(buf.subarray(0, MEDIA_HEAD_BYTES)) ?? MIME[extname(file)] ?? "application/octet-stream";
1910
2247
  const range = req.headers.range;
1911
2248
  const m = range && /^bytes=(\d*)-(\d*)$/.exec(range);
1912
2249
  if (m) {
@@ -1969,7 +2306,7 @@ async function waitForPageDone(page, label, onProgress, timeoutMs) {
1969
2306
  }
1970
2307
 
1971
2308
  // src/plugin/renderAnimation.ts
1972
- import { readFile as readFile2, rm as rm2 } from "fs/promises";
2309
+ import { readFile as readFile2, rm as rm3 } from "fs/promises";
1973
2310
  import { tweenRuntimeCode } from "@vosjs/tween/bundle";
1974
2311
  import { elementsBundleCode } from "@vosjs/elements/bundle";
1975
2312
  import { concatEncodedVideo, planChunks } from "@vosjs/render-core";
@@ -2267,7 +2604,7 @@ async function renderAnimation(browser, opts) {
2267
2604
  outNames.map((name) => readFile2(`${opts.workDir}/${name}`))
2268
2605
  );
2269
2606
  await Promise.all(
2270
- outNames.map((name) => rm2(`${opts.workDir}/${name}`, { force: true }))
2607
+ outNames.map((name) => rm3(`${opts.workDir}/${name}`, { force: true }))
2271
2608
  );
2272
2609
  if (chunks.length === 1) {
2273
2610
  return { bytes: new Uint8Array(files[0]), totalFrames, chunks: 1 };
@@ -2522,9 +2859,9 @@ function applyAndValidate(doc, o) {
2522
2859
  }
2523
2860
 
2524
2861
  // 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";
2862
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename3, writeFile as writeFile4 } from "fs/promises";
2863
+ import { existsSync as existsSync4 } from "fs";
2864
+ import { join as join4, resolve } from "path";
2528
2865
  import {
2529
2866
  DIGEST_CROP_MAX,
2530
2867
  DIGEST_FULL_MAX,
@@ -2746,8 +3083,8 @@ async function digestTake(browser, dir, opts = {}) {
2746
3083
  const meta = doc.source.meta;
2747
3084
  let plan = planForDigest(doc);
2748
3085
  const transcript = opts.transcript ?? await discoverTranscript(dir);
2749
- const outDir = resolve(opts.outDir ?? join3(dir, DIGEST_DIR));
2750
- await mkdir2(outDir, { recursive: true });
3086
+ const outDir = resolve(opts.outDir ?? join4(dir, DIGEST_DIR));
3087
+ await mkdir3(outDir, { recursive: true });
2751
3088
  const fullMax = opts.full ?? DIGEST_FULL_MAX;
2752
3089
  const cropMax = opts.crop ?? DIGEST_CROP_MAX;
2753
3090
  const wantFrames = opts.frames !== false && browser !== null;
@@ -2759,8 +3096,11 @@ async function digestTake(browser, dir, opts = {}) {
2759
3096
  let sheet = null;
2760
3097
  let moments;
2761
3098
  const boxes = /* @__PURE__ */ new Map();
2762
- let source = { kind: "video", url: `/${RECORDING_NAME}` };
2763
- if (existsSync3(take.paths.framesIndex) && existsSync3(take.paths.framesDir)) {
3099
+ let source = {
3100
+ kind: "video",
3101
+ url: `/${take.paths.recordingName}`
3102
+ };
3103
+ if (existsSync4(take.paths.framesIndex) && existsSync4(take.paths.framesDir)) {
2764
3104
  const index = await readJson(
2765
3105
  take.paths.framesIndex
2766
3106
  );
@@ -2775,9 +3115,9 @@ async function digestTake(browser, dir, opts = {}) {
2775
3115
  }
2776
3116
  }
2777
3117
  if (wantFrames) {
2778
- if (source.kind === "video" && !existsSync3(take.paths.recording))
3118
+ if (source.kind === "video" && !existsSync4(take.paths.recording))
2779
3119
  throw new Error(
2780
- `${dir} has no ${RECORDING_NAME} \u2014 pull it with \`vos pull --media\` first`
3120
+ `${dir} has no recording \u2014 pull it with \`vos pull --media\` first`
2781
3121
  );
2782
3122
  const server = await startTakeServer(dir, {
2783
3123
  "/digest.html": digestPageHtml()
@@ -2848,9 +3188,9 @@ async function digestTake(browser, dir, opts = {}) {
2848
3188
  );
2849
3189
  sizes = f.sizes;
2850
3190
  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);
3191
+ const from = join4(dir, name);
3192
+ const to = join4(outDir, name);
3193
+ if (from !== to && existsSync4(from)) await rename3(from, to);
2854
3194
  }
2855
3195
  if (sizes["sheet.png"]) sheet = "sheet.png";
2856
3196
  } finally {
@@ -2876,12 +3216,12 @@ async function digestTake(browser, dir, opts = {}) {
2876
3216
  if (!p) continue;
2877
3217
  for (const f of [p.full, p.crop]) {
2878
3218
  if (!f) continue;
2879
- const size = await pngSize(join3(outDir, f));
3219
+ const size = await pngSize(join4(outDir, f));
2880
3220
  if (size) sizes[f] = size;
2881
3221
  }
2882
3222
  if (p.box) boxes.set(m.id, p.box);
2883
3223
  }
2884
- if (prior.images.sheet && existsSync3(join3(outDir, prior.images.sheet)))
3224
+ if (prior.images.sheet && existsSync4(join4(outDir, prior.images.sheet)))
2885
3225
  sheet = prior.images.sheet;
2886
3226
  }
2887
3227
  }
@@ -2910,14 +3250,14 @@ async function digestTake(browser, dir, opts = {}) {
2910
3250
  style: opts.style ?? null,
2911
3251
  transcript
2912
3252
  });
2913
- const file = join3(outDir, "digest.json");
3253
+ const file = join4(outDir, "digest.json");
2914
3254
  const body = JSON.stringify(digest, null, 2);
2915
- await writeFile3(file, body);
3255
+ await writeFile4(file, body);
2916
3256
  return { file, outDir, digest, bytes: Buffer.byteLength(body) };
2917
3257
  }
2918
3258
  async function discoverTranscript(dir) {
2919
- const file = join3(dir, "transcript.json");
2920
- if (!existsSync3(file)) return null;
3259
+ const file = join4(dir, "transcript.json");
3260
+ if (!existsSync4(file)) return null;
2921
3261
  try {
2922
3262
  return parseTranscript(JSON.parse(await readFile3(file, "utf8")));
2923
3263
  } catch {
@@ -2927,7 +3267,7 @@ async function discoverTranscript(dir) {
2927
3267
  async function readPriorDigest(outDir) {
2928
3268
  try {
2929
3269
  const raw = JSON.parse(
2930
- await readFile3(join3(outDir, "digest.json"), "utf8")
3270
+ await readFile3(join4(outDir, "digest.json"), "utf8")
2931
3271
  );
2932
3272
  return raw.digestVersion === DIGEST_VERSION && Array.isArray(raw.moments) ? raw : null;
2933
3273
  } catch {
@@ -3111,8 +3451,8 @@ async function framesTake(browser, dir, opts) {
3111
3451
  throw new Error(
3112
3452
  "no frame times \u2014 pass --times/--frame, --at-zooms, --at-moments or --at-still"
3113
3453
  );
3114
- const outDir = resolve2(opts.outDir ?? join4(dir, "stills"));
3115
- await mkdir3(outDir, { recursive: true });
3454
+ const outDir = resolve2(opts.outDir ?? join5(dir, "stills"));
3455
+ await mkdir4(outDir, { recursive: true });
3116
3456
  const named = shots.map((s, i) => ({
3117
3457
  ...s,
3118
3458
  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 +3480,7 @@ async function framesTake(browser, dir, opts) {
3140
3480
  void page.evaluate(stillsInPage, {
3141
3481
  animationCode,
3142
3482
  token: VIDEO_TOKEN,
3143
- videoUrl: `/${RECORDING_NAME}`,
3483
+ videoUrl: `/${take.paths.recordingName}`,
3144
3484
  W: width,
3145
3485
  H: height,
3146
3486
  ss,
@@ -3156,9 +3496,9 @@ async function framesTake(browser, dir, opts) {
3156
3496
  );
3157
3497
  const frames = [];
3158
3498
  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);
3499
+ const from = join5(dir, n.name);
3500
+ const to = join5(outDir, n.name);
3501
+ if (from !== to) await rename4(from, to);
3162
3502
  frames.push({
3163
3503
  file: to,
3164
3504
  time: n.time,
@@ -3173,8 +3513,8 @@ async function framesTake(browser, dir, opts) {
3173
3513
  }
3174
3514
  }
3175
3515
  async function momentsFor(dir, doc) {
3176
- const file = join4(dir, DIGEST_DIR, "digest.json");
3177
- if (existsSync4(file)) {
3516
+ const file = join5(dir, DIGEST_DIR, "digest.json");
3517
+ if (existsSync5(file)) {
3178
3518
  try {
3179
3519
  const d = JSON.parse(await readFile4(file, "utf8"));
3180
3520
  if (Array.isArray(d.moments)) return d.moments;
@@ -3196,8 +3536,8 @@ function parseTimes(spec, duration) {
3196
3536
  });
3197
3537
  }
3198
3538
  async function writeIndexJson(result) {
3199
- const index = join4(result.outDir, "stills.json");
3200
- await writeFile4(
3539
+ const index = join5(result.outDir, "stills.json");
3540
+ await writeFile5(
3201
3541
  index,
3202
3542
  JSON.stringify(
3203
3543
  {
@@ -3214,10 +3554,10 @@ async function writeIndexJson(result) {
3214
3554
  }
3215
3555
 
3216
3556
  // src/plugin/deliver.ts
3217
- import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
3557
+ import { mkdir as mkdir5, mkdtemp, rename as rename5, rm as rm4, stat as stat2, writeFile as writeFile7 } from "fs/promises";
3218
3558
  import { tmpdir } from "os";
3219
- import { join as join7, relative, resolve as resolve4 } from "path";
3220
- import { existsSync as existsSync6 } from "fs";
3559
+ import { join as join8, relative, resolve as resolve4 } from "path";
3560
+ import { existsSync as existsSync7 } from "fs";
3221
3561
  import { readFile as readFile6 } from "fs/promises";
3222
3562
  import { parseFrontmatter } from "@vosjs/shared/frontmatter";
3223
3563
  import {
@@ -3232,7 +3572,7 @@ import {
3232
3572
  } from "@vosjs/studio-core";
3233
3573
 
3234
3574
  // src/plugin/renderTake.ts
3235
- import { writeFile as writeFile5 } from "fs/promises";
3575
+ import { writeFile as writeFile6 } from "fs/promises";
3236
3576
  import { compileVosConfig as compileVosConfig2 } from "@vosjs/core";
3237
3577
  import {
3238
3578
  audioProducerCode,
@@ -3304,7 +3644,7 @@ async function renderTake(browser, dir, outFile, opts) {
3304
3644
  const result = await renderAnimation(browser, {
3305
3645
  animationCode,
3306
3646
  workDir: dir,
3307
- videoFile: RECORDING_NAME,
3647
+ videoFile: take.paths.recordingName,
3308
3648
  videoToken: VIDEO_TOKEN2,
3309
3649
  width,
3310
3650
  height,
@@ -3321,7 +3661,7 @@ async function renderTake(browser, dir, outFile, opts) {
3321
3661
  } : void 0,
3322
3662
  onProgress: opts.onProgress
3323
3663
  });
3324
- await writeFile5(outFile, result.bytes);
3664
+ await writeFile6(outFile, result.bytes);
3325
3665
  return {
3326
3666
  out: outFile,
3327
3667
  bytes: result.bytes.length,
@@ -4197,9 +4537,9 @@ function isLightHexGround(hex2) {
4197
4537
  }
4198
4538
 
4199
4539
  // src/plugin/posterDoc.ts
4200
- import { existsSync as existsSync5 } from "fs";
4540
+ import { existsSync as existsSync6 } from "fs";
4201
4541
  import { readFile as readFile5 } from "fs/promises";
4202
- import { isAbsolute, join as join6, resolve as resolve3 } from "path";
4542
+ import { isAbsolute, join as join7, resolve as resolve3 } from "path";
4203
4543
  import {
4204
4544
  computeCardLayout,
4205
4545
  docStillTime as docStillTime2,
@@ -4211,7 +4551,7 @@ import {
4211
4551
  // src/plugin/platform.ts
4212
4552
  import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4213
4553
  import { homedir } from "os";
4214
- import { join as join5 } from "path";
4554
+ import { join as join6 } from "path";
4215
4555
  function platformOrigin(flags = {}) {
4216
4556
  const legacyEnv = process.env.VOS_API_BASE?.trim();
4217
4557
  const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
@@ -4244,7 +4584,7 @@ function parseVosId(input) {
4244
4584
  function deriveSlug(title) {
4245
4585
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
4246
4586
  }
4247
- var CREDENTIALS_PATH = join5(homedir(), ".config", "vos", "credentials");
4587
+ var CREDENTIALS_PATH = join6(homedir(), ".config", "vos", "credentials");
4248
4588
  function resolveCredential(explicit) {
4249
4589
  const flag = explicit?.trim();
4250
4590
  if (flag) return flag;
@@ -4267,7 +4607,7 @@ function requireCredential(explicit) {
4267
4607
  return key;
4268
4608
  }
4269
4609
  function writeCredential(key) {
4270
- mkdirSync(join5(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
4610
+ mkdirSync(join6(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
4271
4611
  writeFileSync(CREDENTIALS_PATH, `${key.trim()}
4272
4612
  `, { mode: 384 });
4273
4613
  return CREDENTIALS_PATH;
@@ -4311,7 +4651,7 @@ function readJsonFile(file) {
4311
4651
  }
4312
4652
  }
4313
4653
  function readSyncState(dir) {
4314
- const own = readJsonFile(join5(dir, SYNC_STATE_NAME));
4654
+ const own = readJsonFile(join6(dir, SYNC_STATE_NAME));
4315
4655
  if (own && typeof own.vosId === "string") {
4316
4656
  return {
4317
4657
  vosId: own.vosId,
@@ -4322,7 +4662,7 @@ function readSyncState(dir) {
4322
4662
  ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
4323
4663
  };
4324
4664
  }
4325
- const push = readJsonFile(join5(dir, "push.json"));
4665
+ const push = readJsonFile(join6(dir, "push.json"));
4326
4666
  if (push && typeof push.vosId === "string") {
4327
4667
  return {
4328
4668
  vosId: push.vosId,
@@ -4330,7 +4670,7 @@ function readSyncState(dir) {
4330
4670
  ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
4331
4671
  };
4332
4672
  }
4333
- const meta = readJsonFile(join5(dir, "meta.json"));
4673
+ const meta = readJsonFile(join6(dir, "meta.json"));
4334
4674
  if (meta && typeof meta.id === "string") {
4335
4675
  return {
4336
4676
  vosId: meta.id,
@@ -4348,7 +4688,7 @@ function writeSyncState(dir, patch) {
4348
4688
  ...patch
4349
4689
  };
4350
4690
  writeFileSync(
4351
- join5(dir, SYNC_STATE_NAME),
4691
+ join6(dir, SYNC_STATE_NAME),
4352
4692
  `${JSON.stringify(merged, null, 2)}
4353
4693
  `
4354
4694
  );
@@ -4387,9 +4727,9 @@ var roleFor = (cls) => `poster-${cls}`;
4387
4727
  async function readPosterAt(cls, path, takeDir, from) {
4388
4728
  let file = path;
4389
4729
  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)) {
4730
+ if (existsSync6(join7(path, "doc.json"))) {
4731
+ file = join7(path, "doc.json");
4732
+ } else if (!/\.json$/i.test(path) || !existsSync6(path)) {
4393
4733
  return null;
4394
4734
  } else {
4395
4735
  docDir = resolve3(path, "..");
@@ -4406,7 +4746,7 @@ async function readPosterAt(cls, path, takeDir, from) {
4406
4746
  const doc = migrateHostedDoc(
4407
4747
  raw
4408
4748
  );
4409
- const ownFootage = existsSync5(join6(docDir, "meta.json")) && existsSync5(join6(docDir, source.videoKey));
4749
+ const ownFootage = existsSync6(join7(docDir, "meta.json")) && existsSync6(join7(docDir, source.videoKey));
4410
4750
  const sync = readSyncState(docDir);
4411
4751
  return {
4412
4752
  cls,
@@ -4429,10 +4769,10 @@ async function findPosterDocs(takeDir, launchRoles) {
4429
4769
  candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
4430
4770
  }
4431
4771
  candidates.push({
4432
- path: join6(takeDir, "poster", cls),
4772
+ path: join7(takeDir, "poster", cls),
4433
4773
  from: `poster/${cls}`
4434
4774
  });
4435
- candidates.push({ path: join6(takeDir, "poster"), from: "poster" });
4775
+ candidates.push({ path: join7(takeDir, "poster"), from: "poster" });
4436
4776
  for (const c of candidates) {
4437
4777
  const ref = await readPosterAt(cls, c.path, takeDir, c.from);
4438
4778
  if (ref) {
@@ -4528,18 +4868,18 @@ function resolveChannels(raw) {
4528
4868
  return out;
4529
4869
  }
4530
4870
  async function readBrandBesideTake(dir, explicit) {
4531
- const candidates = explicit ? [explicit] : [join7(dir, "BRAND.md"), join7(dir, "..", "BRAND.md")];
4871
+ const candidates = explicit ? [explicit] : [join8(dir, "BRAND.md"), join8(dir, "..", "BRAND.md")];
4532
4872
  for (const file of candidates) {
4533
- if (!existsSync6(file)) continue;
4873
+ if (!existsSync7(file)) continue;
4534
4874
  const roles = parseFrontmatter(await readFile6(file, "utf8"));
4535
4875
  return { file, roles };
4536
4876
  }
4537
4877
  return null;
4538
4878
  }
4539
4879
  async function readLaunchBesideTake(dir, explicit) {
4540
- const candidates = explicit ? [explicit] : [join7(dir, "LAUNCH.md"), join7(dir, "..", "LAUNCH.md")];
4880
+ const candidates = explicit ? [explicit] : [join8(dir, "LAUNCH.md"), join8(dir, "..", "LAUNCH.md")];
4541
4881
  for (const file of candidates) {
4542
- if (!existsSync6(file)) continue;
4882
+ if (!existsSync7(file)) continue;
4543
4883
  return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
4544
4884
  }
4545
4885
  return null;
@@ -4604,7 +4944,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4604
4944
  const meta = doc.source.meta;
4605
4945
  const vw = meta.captureWidth ?? meta.width;
4606
4946
  const vh = meta.captureHeight ?? meta.height;
4607
- const probeDir = await mkdtemp(join7(tmpdir(), "vos-moments-"));
4947
+ const probeDir = await mkdtemp(join8(tmpdir(), "vos-moments-"));
4608
4948
  try {
4609
4949
  const probe = await framesTake(browser, dir, {
4610
4950
  times: candidates.map((c) => c.time),
@@ -4643,7 +4983,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4643
4983
  notes: [...notes, ...pick.dropped.map((d) => `moment: ${d}`)]
4644
4984
  };
4645
4985
  } finally {
4646
- await rm3(probeDir, { recursive: true, force: true });
4986
+ await rm4(probeDir, { recursive: true, force: true });
4647
4987
  }
4648
4988
  }
4649
4989
  var overCeiling = (bytes, maxBytes) => `${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(maxBytes / 1024)} KB ceiling`;
@@ -4691,8 +5031,8 @@ async function deliverTake(browser, dir, opts) {
4691
5031
  const doc = take.doc;
4692
5032
  const duration = docOutputDuration2(doc);
4693
5033
  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 });
5034
+ const outDir = resolve4(opts.outDir ?? join8(dir, "kit"));
5035
+ await mkdir5(outDir, { recursive: true });
4696
5036
  const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
4697
5037
  const assets = [];
4698
5038
  const skipped = [];
@@ -4752,7 +5092,7 @@ async function deliverTake(browser, dir, opts) {
4752
5092
  opts.onPhase?.(
4753
5093
  `${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
4754
5094
  );
4755
- const shotDir = await mkdtemp(join7(tmpdir(), "vos-poster-"));
5095
+ const shotDir = await mkdtemp(join8(tmpdir(), "vos-poster-"));
4756
5096
  try {
4757
5097
  const captured = await framesTake(browser, ref.takeDir, {
4758
5098
  times: [time],
@@ -4762,9 +5102,9 @@ async function deliverTake(browser, dir, opts) {
4762
5102
  doc: ref.doc,
4763
5103
  overrides: opts.overrides
4764
5104
  });
4765
- const to = join7(outDir, `${d.id}.png`);
4766
- await rename4(captured.frames[0].file, to);
4767
- const bytes = (await stat(to)).size;
5105
+ const to = join8(outDir, `${d.id}.png`);
5106
+ await rename5(captured.frames[0].file, to);
5107
+ const bytes = (await stat2(to)).size;
4768
5108
  if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4769
5109
  skipped.push(
4770
5110
  `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
@@ -4793,7 +5133,7 @@ async function deliverTake(browser, dir, opts) {
4793
5133
  ...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
4794
5134
  });
4795
5135
  } finally {
4796
- await rm3(shotDir, { recursive: true, force: true });
5136
+ await rm4(shotDir, { recursive: true, force: true });
4797
5137
  }
4798
5138
  }
4799
5139
  if (missing.size) {
@@ -4834,7 +5174,7 @@ async function deliverTake(browser, dir, opts) {
4834
5174
  }
4835
5175
  }
4836
5176
  opts.onPhase?.(`${label} (${specWords(d)})`);
4837
- const outFile = join7(outDir, `${d.id}.${d.format}`);
5177
+ const outFile = join8(outDir, `${d.id}.${d.format}`);
4838
5178
  const bitrate = d.maxBytes !== void 0 ? Math.min(
4839
5179
  1e7,
4840
5180
  Math.floor(d.maxBytes * 8 / seconds * 0.85)
@@ -4881,9 +5221,9 @@ async function deliverTake(browser, dir, opts) {
4881
5221
  for (let i = 0; i < captured.frames.length; i++) {
4882
5222
  const frame = captured.frames[i];
4883
5223
  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;
5224
+ const to = join8(outDir, name);
5225
+ await rename5(frame.file, to);
5226
+ const bytes = (await stat2(to)).size;
4887
5227
  if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4888
5228
  skipped.push(
4889
5229
  `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
@@ -4919,15 +5259,15 @@ async function deliverTake(browser, dir, opts) {
4919
5259
  skipped,
4920
5260
  assets
4921
5261
  };
4922
- const kitFile = join7(outDir, "kit.json");
4923
- await writeFile6(kitFile, JSON.stringify(kit, null, 2));
5262
+ const kitFile = join8(outDir, "kit.json");
5263
+ await writeFile7(kitFile, JSON.stringify(kit, null, 2));
4924
5264
  return { kit, kitFile, outDir };
4925
5265
  }
4926
5266
 
4927
5267
  // 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";
5268
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
5269
+ import { existsSync as existsSync8 } from "fs";
5270
+ import { isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
4931
5271
  function markExtension(url) {
4932
5272
  const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
4933
5273
  if (!m) return null;
@@ -5009,7 +5349,7 @@ async function fetchBrandMarks(takeDir, roles) {
5009
5349
  bytes = new Uint8Array(await res.arrayBuffer());
5010
5350
  } else {
5011
5351
  const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
5012
- if (!existsSync7(file)) throw new Error("no such file");
5352
+ if (!existsSync8(file)) throw new Error("no such file");
5013
5353
  bytes = new Uint8Array(await readFile7(file));
5014
5354
  }
5015
5355
  } catch (e) {
@@ -5025,8 +5365,8 @@ async function fetchBrandMarks(takeDir, roles) {
5025
5365
  );
5026
5366
  return null;
5027
5367
  }
5028
- await mkdir5(join8(takeDir, "brand"), { recursive: true });
5029
- await writeFile7(join8(takeDir, "brand", `${name}.${ext}`), bytes);
5368
+ await mkdir6(join9(takeDir, "brand"), { recursive: true });
5369
+ await writeFile8(join9(takeDir, "brand", `${name}.${ext}`), bytes);
5030
5370
  return { key: `/brand/${name}.${ext}`, aspect };
5031
5371
  };
5032
5372
  out.light = await one("logoUrl", "mark");
@@ -5062,9 +5402,9 @@ async function fetchMusicCatalog(origin) {
5062
5402
  }
5063
5403
 
5064
5404
  // 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";
5405
+ import { existsSync as existsSync9 } from "fs";
5406
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
5407
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join10, resolve as resolve6 } from "path";
5068
5408
  import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
5069
5409
 
5070
5410
  // src/plugin/shotBake.ts
@@ -5147,7 +5487,8 @@ function rolesFor(asset) {
5147
5487
  const portrait = spec.px.w < spec.px.h;
5148
5488
  return portrait ? ["feature-clip", "site-walkthrough"] : ["feature-clip", "site-walkthrough", "feature-clip-dark"];
5149
5489
  }
5150
- if (spec?.genre === "card") return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
5490
+ if (spec?.genre === "card")
5491
+ return ["window-in-scene", "card-on-gradient", "framed-screenshot"];
5151
5492
  if (spec?.genre === "screenshot") return ["framed-screenshot", "app-session"];
5152
5493
  return [];
5153
5494
  }
@@ -5207,7 +5548,8 @@ function composeSheet(left, right, height = 540, gutter = 32) {
5207
5548
  const si = (y * src.w + x) * 4;
5208
5549
  const a = src.data[si + 3] / 255;
5209
5550
  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));
5551
+ for (let c = 0; c < 3; c++)
5552
+ out[o + c] = Math.round(src.data[si + c] * a + out[o + c] * (1 - a));
5211
5553
  }
5212
5554
  }
5213
5555
  };
@@ -5223,51 +5565,70 @@ function composeSheet(left, right, height = 540, gutter = 32) {
5223
5565
  }
5224
5566
  };
5225
5567
  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);
5568
+ fill(
5569
+ margin + lw + gutter,
5570
+ margin + lw + gutter + rw,
5571
+ margin + height + 6,
5572
+ margin + height + 6 + band,
5573
+ 200
5574
+ );
5227
5575
  return { w, h, data: out };
5228
5576
  }
5229
5577
  async function judgeKit(kitPath, manifestPath, outDir) {
5230
5578
  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 });
5579
+ const manifest = JSON.parse(
5580
+ await readFile8(manifestPath, "utf8")
5581
+ );
5582
+ const manifestDir = dirname2(resolve6(manifestPath));
5583
+ const kitDir = dirname2(resolve6(kitPath));
5584
+ const out = resolve6(outDir ?? join10(kitDir, "judge"));
5585
+ await mkdir7(out, { recursive: true });
5236
5586
  const sheets = [];
5237
5587
  const skipped = [];
5238
5588
  const refCache = /* @__PURE__ */ new Map();
5239
5589
  const loadRef = async (file) => {
5240
5590
  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;
5591
+ const p = join10(manifestDir, file);
5592
+ const img = existsSync9(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
5243
5593
  refCache.set(file, img);
5244
5594
  return img;
5245
5595
  };
5246
5596
  for (const a of kit.assets) {
5247
5597
  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;
5598
+ const file = isAbsolute3(a.path) && existsSync9(a.path) ? a.path : join10(kitDir, a.path.split("/").pop() ?? a.path);
5599
+ const img = existsSync9(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
5250
5600
  if (!img) {
5251
5601
  skipped.push(`${a.destination ?? a.path}: unreadable`);
5252
5602
  continue;
5253
5603
  }
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");
5604
+ const roles = rolesFor({
5605
+ destination: a.destination,
5606
+ source: a.source,
5607
+ template: a.template,
5608
+ path: a.path
5609
+ });
5610
+ const ref = manifest.assets.find(
5611
+ (r) => roles.includes(r.role) && r.genre !== "context"
5612
+ );
5256
5613
  if (!ref) {
5257
- skipped.push(`${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`);
5614
+ skipped.push(
5615
+ `${a.destination ?? a.path}: no reference of role ${roles.join("|") || "(none)"}`
5616
+ );
5258
5617
  continue;
5259
5618
  }
5260
5619
  const refImg = await loadRef(ref.file);
5261
5620
  if (!refImg) {
5262
- skipped.push(`${a.destination ?? a.path}: reference ${ref.file} unreadable`);
5621
+ skipped.push(
5622
+ `${a.destination ?? a.path}: reference ${ref.file} unreadable`
5623
+ );
5263
5624
  continue;
5264
5625
  }
5265
5626
  const stem = (a.path.split("/").pop() ?? a.path).replace(/\.png$/i, "");
5266
5627
  const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
5267
5628
  const nameA = `${id}--A.png`;
5268
5629
  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)));
5630
+ await writeFile9(join10(out, nameA), encodePng(composeSheet(img, refImg)));
5631
+ await writeFile9(join10(out, nameB), encodePng(composeSheet(refImg, img)));
5271
5632
  const rubric = [
5272
5633
  `# ${id} against ${ref.id} (${ref.file})`,
5273
5634
  "",
@@ -5287,18 +5648,31 @@ async function judgeKit(kitPath, manifestPath, outDir) {
5287
5648
  "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
5649
  ""
5289
5650
  ].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` });
5651
+ await writeFile9(join10(out, `${id}.md`), rubric);
5652
+ sheets.push({
5653
+ asset: id,
5654
+ reference: ref.id,
5655
+ sheetA: nameA,
5656
+ sheetB: nameB,
5657
+ rubric: `${id}.md`
5658
+ });
5292
5659
  }
5293
- const verdictFile = join9(out, "judge.json");
5294
- if (!existsSync8(verdictFile)) {
5295
- await writeFile8(
5660
+ const verdictFile = join10(out, "judge.json");
5661
+ if (!existsSync9(verdictFile)) {
5662
+ await writeFile9(
5296
5663
  verdictFile,
5297
5664
  JSON.stringify(
5298
5665
  {
5299
5666
  kit: kitPath,
5300
5667
  manifest: manifestPath,
5301
- verdicts: sheets.map((s) => ({ asset: s.asset, reference: s.reference, A: null, B: null, win: null, reasons: [] }))
5668
+ verdicts: sheets.map((s) => ({
5669
+ asset: s.asset,
5670
+ reference: s.reference,
5671
+ A: null,
5672
+ B: null,
5673
+ win: null,
5674
+ reasons: []
5675
+ }))
5302
5676
  },
5303
5677
  null,
5304
5678
  2
@@ -5308,22 +5682,43 @@ async function judgeKit(kitPath, manifestPath, outDir) {
5308
5682
  return { sheets, skipped, outDir: out, verdictFile };
5309
5683
  }
5310
5684
  function winRate(verdicts) {
5311
- const judgedList = verdicts.filter((v) => v.win !== null || (v.reasons?.length ?? 0) > 0);
5685
+ const judgedList = verdicts.filter(
5686
+ (v) => v.win !== null || (v.reasons?.length ?? 0) > 0
5687
+ );
5312
5688
  const wins = judgedList.filter((v) => v.win === true).length;
5313
5689
  const ties = judgedList.filter((v) => v.win === null).length;
5314
5690
  const judged = judgedList.length;
5315
- return { wins, ties, judged, rate: judged ? (wins + ties / 2) / judged : null };
5691
+ return {
5692
+ wins,
5693
+ ties,
5694
+ judged,
5695
+ rate: judged ? (wins + ties / 2) / judged : null
5696
+ };
5697
+ }
5698
+
5699
+ // src/plugin/fontName.ts
5700
+ import { findFontFamily as findFontFamily2 } from "@vosjs/shared";
5701
+ function catalogFamily(family) {
5702
+ const name = family.trim();
5703
+ if (findFontFamily2(name)) return name;
5704
+ const base = name.replace(/\s+Variable$/i, "");
5705
+ if (base !== name) {
5706
+ const hit = findFontFamily2(base);
5707
+ if (hit) return hit.family;
5708
+ }
5709
+ return family;
5316
5710
  }
5317
5711
 
5318
5712
  // src/plugin/callout.ts
5319
- import { existsSync as existsSync9 } from "fs";
5713
+ import { existsSync as existsSync10 } from "fs";
5320
5714
  import { readFile as readFile9 } from "fs/promises";
5321
- import { join as join10 } from "path";
5715
+ import { join as join11 } from "path";
5322
5716
  import { lerpArray, sample } from "@vosjs/timeline";
5323
5717
  import {
5324
5718
  CALLOUT_DEFAULT_BODY_PX,
5325
5719
  CALLOUT_SHAPES,
5326
5720
  calloutClip,
5721
+ calloutLook,
5327
5722
  calloutGroundOf,
5328
5723
  cameraModel as cameraModel2,
5329
5724
  deltaE,
@@ -5342,10 +5737,11 @@ var CALLOUT_DELTA_E_PROBLEM = 8;
5342
5737
  var CALLOUT_DELTA_E_WARNING = 16;
5343
5738
  function registerFrom(roles, flags) {
5344
5739
  const ground = flags.ground ?? roles?.bgA;
5345
- const accent = flags.accent ?? roles?.accent;
5740
+ const accent = flags.accent ?? roles?.callout ?? roles?.accent;
5346
5741
  if (!ground || !accent) return null;
5347
5742
  if (!hexToRgb(ground) || !hexToRgb(accent)) return null;
5348
- const face = flags.face ?? roles?.fontBody;
5743
+ const named = flags.face ?? roles?.fontBody;
5744
+ const face = named ? catalogFamily(named) : named;
5349
5745
  return {
5350
5746
  ground,
5351
5747
  accent,
@@ -5411,16 +5807,28 @@ function composeCallout(doc, register, ask) {
5411
5807
  let n = 2;
5412
5808
  while (taken.has(id) && !ask.id)
5413
5809
  id = `${ask.shape}-${ask.step ?? Math.round(window2.start * 10)}-${n++}`;
5810
+ const seen = appScaleAt(doc, window2.start + 0.35);
5811
+ const scale = ask.bodyPxGiven ? 1 : seen;
5812
+ const look = calloutLook(register, scale);
5414
5813
  const clip = calloutClip(ask.shape, register, ask.words, {
5415
5814
  id,
5416
5815
  start: window2.start,
5417
5816
  duration: window2.duration,
5418
- scale: appScaleAt(doc, window2.start + 0.35),
5817
+ scale,
5419
5818
  pin,
5420
5819
  // The fallback place, in the margin: the lower third.
5421
5820
  at: { x: 0.5, y: 0.82 }
5422
5821
  });
5423
- return { clip, window: window2 };
5822
+ return {
5823
+ clip,
5824
+ window: window2,
5825
+ sizes: {
5826
+ scale: +seen.toFixed(3),
5827
+ kickerPx: look.kickerPx,
5828
+ titlePx: look.titlePx,
5829
+ bodyPx: look.bodyPx
5830
+ }
5831
+ };
5424
5832
  }
5425
5833
  function isCalloutShape(s) {
5426
5834
  return CALLOUT_SHAPES.includes(s);
@@ -5501,10 +5909,10 @@ async function footageFrames(browser, dir, doc, times, framesTake2) {
5501
5909
  const out = /* @__PURE__ */ new Map();
5502
5910
  if (!times.length) return out;
5503
5911
  const bare = { ...doc, overlays: [] };
5504
- const outDir = join10(dir, ".ground-check");
5912
+ const outDir = join11(dir, ".ground-check");
5505
5913
  const res = await framesTake2(browser, dir, { times, outDir, doc: bare });
5506
5914
  for (const f of res.frames) {
5507
- if (!existsSync9(f.file)) continue;
5915
+ if (!existsSync10(f.file)) continue;
5508
5916
  out.set(
5509
5917
  +f.time.toFixed(3),
5510
5918
  decodePng(new Uint8Array(await readFile9(f.file)))
@@ -5885,9 +6293,9 @@ function formatFinding(f) {
5885
6293
  }
5886
6294
 
5887
6295
  // 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";
6296
+ import { existsSync as existsSync11 } from "fs";
6297
+ import { readFile as readFile11, stat as stat3 } from "fs/promises";
6298
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join12 } from "path";
5891
6299
  import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
5892
6300
  var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
5893
6301
  function pngDimensions(bytes) {
@@ -5944,10 +6352,10 @@ async function validateKit(kitPath, opts = {}) {
5944
6352
  measured
5945
6353
  };
5946
6354
  }
5947
- const base = dirname2(kitPath);
6355
+ const base = dirname3(kitPath);
5948
6356
  const resolvePath = (p) => {
5949
- if (isAbsolute4(p) && existsSync10(p)) return p;
5950
- const beside = join11(base, p.split("/").pop() ?? p);
6357
+ if (isAbsolute4(p) && existsSync11(p)) return p;
6358
+ const beside = join12(base, p.split("/").pop() ?? p);
5951
6359
  return beside;
5952
6360
  };
5953
6361
  const perDestination = /* @__PURE__ */ new Map();
@@ -5958,7 +6366,7 @@ async function validateKit(kitPath, opts = {}) {
5958
6366
  const file = resolvePath(a.path);
5959
6367
  let bytes;
5960
6368
  try {
5961
- bytes = (await stat2(file)).size;
6369
+ bytes = (await stat3(file)).size;
5962
6370
  } catch {
5963
6371
  problems.push(`${label}: ${a.path} is missing`);
5964
6372
  continue;
@@ -6083,7 +6491,7 @@ async function validateKit(kitPath, opts = {}) {
6083
6491
 
6084
6492
  // src/plugin/recorder.ts
6085
6493
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
6086
- import { join as join12 } from "path";
6494
+ import { join as join13 } from "path";
6087
6495
 
6088
6496
  // src/plugin/recordingCap.ts
6089
6497
  var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
@@ -6241,7 +6649,8 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6241
6649
  const vh = actions.viewport?.height ?? 720;
6242
6650
  const context = await browser.newContext({
6243
6651
  viewport: { width: vw, height: vh },
6244
- deviceScaleFactor: 1
6652
+ deviceScaleFactor: 1,
6653
+ ...opts.storageState ? { storageState: opts.storageState } : {}
6245
6654
  });
6246
6655
  const page = await context.newPage();
6247
6656
  page.on("console", (m) => {
@@ -6261,7 +6670,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6261
6670
  cdp.on("Page.screencastFrame", (ev) => {
6262
6671
  const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
6263
6672
  const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
6264
- writeFileSync2(join12(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6673
+ writeFileSync2(join13(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6265
6674
  frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
6266
6675
  cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
6267
6676
  });
@@ -6523,7 +6932,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6523
6932
  await sleep(200);
6524
6933
  const pageTitle = await page.title().catch(() => "");
6525
6934
  await context.close();
6526
- const firstFrame = frames[0] ? jpegDims(readFileSync3(join12(paths.framesDir, frames[0].file))) : null;
6935
+ const firstFrame = frames[0] ? jpegDims(readFileSync3(join13(paths.framesDir, frames[0].file))) : null;
6527
6936
  const meta = {
6528
6937
  dpr: 1,
6529
6938
  zoom: 1,
@@ -6634,9 +7043,9 @@ async function encodeRecording(browser, takeDir, onProgress) {
6634
7043
  }
6635
7044
 
6636
7045
  // src/plugin/plan.ts
6637
- import { existsSync as existsSync11 } from "fs";
7046
+ import { existsSync as existsSync12 } from "fs";
6638
7047
  import { readFile as readFile12 } from "fs/promises";
6639
- import { join as join13 } from "path";
7048
+ import { join as join14 } from "path";
6640
7049
  import {
6641
7050
  DEFAULT_FRAME_STYLE,
6642
7051
  STYLE_FIELDS,
@@ -6858,8 +7267,8 @@ function retimeCut(prev, newSteps, newDurationMs) {
6858
7267
  // src/plugin/plan.ts
6859
7268
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6860
7269
  async function readDigestActivity(dir) {
6861
- const file = join13(dir, "digest", "digest.json");
6862
- if (!existsSync11(file)) return null;
7270
+ const file = join14(dir, "digest", "digest.json");
7271
+ if (!existsSync12(file)) return null;
6863
7272
  try {
6864
7273
  const d = JSON.parse(await readFile12(file, "utf8"));
6865
7274
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
@@ -6928,11 +7337,11 @@ async function planTake(dir, opts = {}) {
6928
7337
  if (opts.reuse) {
6929
7338
  const prev = migrateMotion2(opts.reuse.doc);
6930
7339
  const artifact = {
6931
- videoKey: RECORDING_NAME,
7340
+ videoKey: take.paths.recordingName,
6932
7341
  cursor,
6933
7342
  meta
6934
7343
  };
6935
- doc = projectFromArtifact(artifact, RECORDING_NAME, ingest).doc;
7344
+ doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
6936
7345
  doc = copyStyle(prev, doc);
6937
7346
  const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
6938
7347
  doc.segments = rt.segments;
@@ -7020,11 +7429,11 @@ async function planTake(dir, opts = {}) {
7020
7429
  }
7021
7430
  } else {
7022
7431
  const artifact = {
7023
- videoKey: RECORDING_NAME,
7432
+ videoKey: take.paths.recordingName,
7024
7433
  cursor,
7025
7434
  meta
7026
7435
  };
7027
- doc = projectFromArtifact(artifact, RECORDING_NAME, ingest).doc;
7436
+ doc = projectFromArtifact(artifact, take.paths.recordingName, ingest).doc;
7028
7437
  if (opts.style) {
7029
7438
  const applied = applyStyle(opts.style.doc, doc, opts);
7030
7439
  doc = applied.doc;
@@ -7074,207 +7483,11 @@ import { createHash } from "crypto";
7074
7483
  import { existsSync as existsSync13 } from "fs";
7075
7484
  import { readFile as readFile13 } from "fs/promises";
7076
7485
  import { createInterface } from "readline/promises";
7077
- import { basename, join as join16 } from "path";
7486
+ import { basename as basename3, join as join16 } from "path";
7078
7487
  import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
7079
7488
 
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
7489
  // src/plugin/folder.ts
7277
- import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
7490
+ import { mkdir as mkdir8, writeFile as writeFile10 } from "fs/promises";
7278
7491
  import { join as join15 } from "path";
7279
7492
  import { recipeHints } from "@vosjs/shared/frontmatter";
7280
7493
  import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
@@ -7451,7 +7664,7 @@ async function cmdPull(argv) {
7451
7664
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
7452
7665
  const payload = res.body;
7453
7666
  const out = strFlag(flags, "out") ?? folder.slug;
7454
- await mkdir7(join15(out, "recipes"), { recursive: true });
7667
+ await mkdir8(join15(out, "recipes"), { recursive: true });
7455
7668
  const lines = [];
7456
7669
  const recipes = [];
7457
7670
  for (const rec of payload.recipes) {
@@ -7461,7 +7674,7 @@ async function cmdPull(argv) {
7461
7674
  }
7462
7675
  for (const rec of payload.inheritedRecipes) {
7463
7676
  const from = safeName(rec.folderSlug ?? "inherited");
7464
- await mkdir7(join15(out, "recipes", "_inherited", from), { recursive: true });
7677
+ await mkdir8(join15(out, "recipes", "_inherited", from), { recursive: true });
7465
7678
  const path = join15(
7466
7679
  out,
7467
7680
  "recipes",
@@ -7478,7 +7691,7 @@ async function cmdPull(argv) {
7478
7691
  const members = [];
7479
7692
  for (const v of payload.voses) {
7480
7693
  const dir = join15(out, "members", safeName(v.slug || v.title || v.id));
7481
- await mkdir7(dir, { recursive: true });
7694
+ await mkdir8(dir, { recursive: true });
7482
7695
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
7483
7696
  if (cfg.status !== 200) {
7484
7697
  lines.push(`skipped ${v.title}: ${apiError("fetch config", cfg)}`);
@@ -7742,6 +7955,15 @@ function printChanges(r, changes, protectedIds) {
7742
7955
  r.event({ event: "protected", nodes: protectedIds });
7743
7956
  }
7744
7957
  }
7958
+ function takePushRefusal(ask) {
7959
+ if (ask.programOnly.length) {
7960
+ const list = ask.programOnly.map((f) => `--${f}`).join(", ");
7961
+ return `${list} ${ask.programOnly.length > 1 ? "belong" : "belongs"} to a PROGRAM push (a config.json); a take push reads --vos, --title, --label, --note, --folder, --override and --yes`;
7962
+ }
7963
+ if (ask.vos !== void 0 && ask.trackedVosId !== null && ask.trackedVosId !== ask.vos)
7964
+ return `this take tracks vos ${ask.trackedVosId} (its vos.json), not ${ask.vos}. Drop --vos to push there, or remove vos.json to adopt ${ask.vos} instead`;
7965
+ return null;
7966
+ }
7745
7967
  async function pushTake(dir, flags, r) {
7746
7968
  const take = await loadTake(dir);
7747
7969
  if (!take.doc) {
@@ -7753,10 +7975,27 @@ async function pushTake(dir, flags, r) {
7753
7975
  ${lint.problems.join("\n ")}`);
7754
7976
  }
7755
7977
  if (!existsSync13(take.paths.recording)) {
7756
- throw new Error("no recording.webm in this take");
7978
+ throw new Error(
7979
+ "no recording in this take \u2014 record it, or `vos pull --media` to bring it home"
7980
+ );
7757
7981
  }
7758
7982
  const ctx = apiContext(flags);
7759
- const state = readSyncState(dir);
7983
+ const tracked = readSyncState(dir);
7984
+ const firstPush = tracked === null;
7985
+ let state = tracked;
7986
+ if (!state && flags.vos) {
7987
+ const meta = await api(ctx, `/vos/${flags.vos}`);
7988
+ const head = meta.json.vos?.currentVersionId;
7989
+ if (meta.status !== 200 || !head) {
7990
+ throw new Error(
7991
+ `--vos ${flags.vos}: cannot adopt it (${meta.status}${meta.json.error ? `: ${String(meta.json.error)}` : ""}) \u2014 it must be a vos this credential can read, with a version to build on`
7992
+ );
7993
+ }
7994
+ r.log(
7995
+ `adopting vos ${flags.vos}: this take becomes the next version after its head (${head})`
7996
+ );
7997
+ state = { vosId: flags.vos, versionId: head };
7998
+ }
7760
7999
  let folderId;
7761
8000
  if (flags.folder) {
7762
8001
  if (state) {
@@ -7769,7 +8008,7 @@ async function pushTake(dir, flags, r) {
7769
8008
  flags.folder
7770
8009
  ).id;
7771
8010
  }
7772
- if (!state && !flags.yes) {
8011
+ if (firstPush && !flags.yes) {
7773
8012
  if (!process.stdin.isTTY) {
7774
8013
  throw new Error(
7775
8014
  "first push of this take uploads its recording \u2014 pass --yes to consent headlessly"
@@ -7786,12 +8025,22 @@ async function pushTake(dir, flags, r) {
7786
8025
  }
7787
8026
  const bytes = await readFile13(take.paths.recording);
7788
8027
  const hash = createHash("sha256").update(bytes).digest("hex");
8028
+ const { type: contentType } = resolveMediaType({
8029
+ head: bytes.subarray(0, MEDIA_HEAD_BYTES),
8030
+ filename: take.paths.recordingName
8031
+ });
8032
+ const filename = nameForType(take.paths.recordingName, contentType);
8033
+ if (filename !== take.paths.recordingName) {
8034
+ r.log(
8035
+ ` ${take.paths.recordingName} holds ${contentType} \u2014 uploading it as ${filename}`
8036
+ );
8037
+ }
7789
8038
  r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
7790
8039
  let upload;
7791
8040
  try {
7792
8041
  upload = await uploadAsset(ctx, new Uint8Array(bytes), {
7793
- filename: RECORDING_NAME,
7794
- contentType: "video/webm",
8042
+ filename,
8043
+ contentType,
7795
8044
  contentHash: hash,
7796
8045
  // The take's length so the server holds it to the plan's cap.
7797
8046
  ...take.meta.durationMs > 0 ? { durationSeconds: take.meta.durationMs / 1e3 } : {},
@@ -7824,11 +8073,18 @@ async function pushTake(dir, flags, r) {
7824
8073
  }
7825
8074
  const media = await readFile13(file);
7826
8075
  const mediaHash = createHash("sha256").update(media).digest("hex");
7827
- const type = mediaContentType(file);
8076
+ const { type } = resolveMediaType({
8077
+ head: media.subarray(0, MEDIA_HEAD_BYTES),
8078
+ filename: file
8079
+ });
8080
+ const name = nameForType(basename3(file), type);
8081
+ if (name !== basename3(file)) {
8082
+ r.log(` ${ref.where}: ${basename3(file)} holds ${type} \u2014 sent as ${name}`);
8083
+ }
7828
8084
  let put;
7829
8085
  try {
7830
8086
  put = await uploadAsset(ctx, new Uint8Array(media), {
7831
- filename: basename(file),
8087
+ filename: name,
7832
8088
  contentType: type,
7833
8089
  contentHash: mediaHash
7834
8090
  });
@@ -7845,7 +8101,7 @@ async function pushTake(dir, flags, r) {
7845
8101
  const lowered = lowerToComposition3(docForPush);
7846
8102
  const config = { ...lowered.config, data: lowered.data };
7847
8103
  if (!state) {
7848
- const title = flags.title ?? `Take ${basename(dir)} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
8104
+ const title = flags.title ?? `Take ${basename3(dir)} ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
7849
8105
  const slug = `${title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "take"}-${Date.now().toString(36)}`;
7850
8106
  const created = await api(ctx, "/vos", {
7851
8107
  method: "POST",
@@ -8017,8 +8273,9 @@ async function pullTake(dir, flags, r) {
8017
8273
  }
8018
8274
  const hostedDoc = migrateHostedDoc3(docRes.json);
8019
8275
  const doc = hostedDoc;
8020
- if (existsSync13(join16(dir, RECORDING_NAME))) {
8021
- doc.source.videoKey = RECORDING_NAME;
8276
+ const localRecording = takeMediaFile(dir, "recording", RECORDING_NAME);
8277
+ if (existsSync13(localRecording)) {
8278
+ doc.source.videoKey = basename3(localRecording);
8022
8279
  }
8023
8280
  await writeJson(join16(dir, "doc.json"), doc, true);
8024
8281
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
@@ -8040,9 +8297,9 @@ async function pullTake(dir, flags, r) {
8040
8297
 
8041
8298
  // src/plugin/program.ts
8042
8299
  import { existsSync as existsSync14 } from "fs";
8043
- import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8300
+ import { mkdir as mkdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
8044
8301
  import { createInterface as createInterface2 } from "readline/promises";
8045
- import { basename as basename2, dirname as dirname3, join as join17 } from "path";
8302
+ import { basename as basename4, dirname as dirname4, join as join17 } from "path";
8046
8303
  import {
8047
8304
  CURRENT_CONFIG_VERSION,
8048
8305
  migrateConfig,
@@ -8239,7 +8496,7 @@ async function cmdFetch(argv) {
8239
8496
  throw new Error(apiError(`fetch config for ${vosId}`, cfg));
8240
8497
  const slug = typeof vosMeta.slug === "string" && vosMeta.slug ? vosMeta.slug : vosId;
8241
8498
  const out = strFlag(flags, "out") ?? slug;
8242
- await mkdir8(out, { recursive: true });
8499
+ await mkdir9(out, { recursive: true });
8243
8500
  await writeFile11(
8244
8501
  join17(out, "config.json"),
8245
8502
  JSON.stringify(cfg.body.config, null, 2)
@@ -8383,7 +8640,7 @@ async function cmdPushProgram(argv) {
8383
8640
  origin: strFlag(flags, "origin"),
8384
8641
  api: strFlag(flags, "api")
8385
8642
  });
8386
- const dir = dirname3(source);
8643
+ const dir = dirname4(source);
8387
8644
  const parsed = JSON.parse(await readFile14(source, "utf8"));
8388
8645
  const pre = preflightConfig(parsed);
8389
8646
  if (!pre.ok || !pre.config) {
@@ -8411,7 +8668,7 @@ async function cmdPushProgram(argv) {
8411
8668
  "--claimable creates a NEW claimable vos and cannot iterate (--vos). After the human claims it, iterate with their key: vos push --vos <id>"
8412
8669
  );
8413
8670
  }
8414
- const fallback = state?.title ? `${state.title} remix` : basename2(source).replace(/\.json$/i, "") || "vos program";
8671
+ const fallback = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos program";
8415
8672
  const title2 = (strFlag(flags, "title") ?? fallback).slice(0, 100);
8416
8673
  const body = { title: title2, config };
8417
8674
  const slug = strFlag(flags, "slug");
@@ -8509,7 +8766,7 @@ with their key: vos push ${source} --vos ${String(created2.id ?? "<id>")}`
8509
8766
  return EXIT_OK;
8510
8767
  }
8511
8768
  const remixOfId = strFlag(flags, "remix-of") ?? state?.vosId;
8512
- const fallbackTitle = state?.title ? `${state.title} remix` : basename2(source).replace(/\.json$/i, "") || "vos remix";
8769
+ const fallbackTitle = state?.title ? `${state.title} remix` : basename4(source).replace(/\.json$/i, "") || "vos remix";
8513
8770
  const title = (strFlag(flags, "title") ?? fallbackTitle).slice(0, 100);
8514
8771
  const folderId = folderRef ? resolveFolder(await listFolders(origin, key), folderRef).id : void 0;
8515
8772
  const created = await createProgramVos({
@@ -8561,7 +8818,7 @@ Iterate with: vos push ${source} --vos ${created.id}`
8561
8818
  async function cmdPullProgram(argv) {
8562
8819
  const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS2);
8563
8820
  const target = positionals[0] ?? ".";
8564
- const dir = target.endsWith(".json") ? dirname3(target) : target;
8821
+ const dir = target.endsWith(".json") ? dirname4(target) : target;
8565
8822
  const r = createReporter(flags.json === true);
8566
8823
  const origin = platformOrigin({
8567
8824
  origin: strFlag(flags, "origin"),
@@ -8744,7 +9001,7 @@ async function writeProgramDoc(dir, hosted) {
8744
9001
 
8745
9002
  // src/plugin/asset.ts
8746
9003
  import { readFileSync as readFileSync4 } from "fs";
8747
- import { basename as basename3, extname as extname2 } from "path";
9004
+ import { basename as basename5, extname as extname2 } from "path";
8748
9005
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
8749
9006
  var MIME_BY_EXT = {
8750
9007
  ".png": "image/png",
@@ -8795,7 +9052,7 @@ async function cmdPush(argv) {
8795
9052
  const folder = folderRef ? resolveFolder(await listFolders(origin, key), folderRef) : null;
8796
9053
  const uploaded = [];
8797
9054
  for (const file of positionals) {
8798
- const name = basename3(file);
9055
+ const name = basename5(file);
8799
9056
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
8800
9057
  const body = readFileSync4(file);
8801
9058
  const form = new FormData();
@@ -8852,11 +9109,11 @@ async function cmdAsset(argv) {
8852
9109
 
8853
9110
  // src/plugin/recipe.ts
8854
9111
  import { readFileSync as readFileSync5 } from "fs";
8855
- import { basename as basename4 } from "path";
9112
+ import { basename as basename6 } from "path";
8856
9113
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
8857
9114
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
8858
9115
  function recipeFilename(file) {
8859
- const name = basename4(file);
9116
+ const name = basename6(file);
8860
9117
  if (!/\.md$/i.test(name)) {
8861
9118
  throw new UsageError(`a recipe is a markdown file \u2014 ${name} is not .md`);
8862
9119
  }
@@ -9143,10 +9400,12 @@ function composeBrand(input) {
9143
9400
  const bgC = mixHex(bgA, accent, 0.14);
9144
9401
  p.bgC = `bgA tinted 14% toward the accent (a highlight ground)`;
9145
9402
  p.ink = w.h1 ? `the h1's colour` : `the body's colour`;
9146
- const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily(w.h1.fontFamily) : firstFamily(w.body.fontFamily));
9403
+ const fontDisplay = catalogFamily(
9404
+ design?.fonts[0] ?? (w.h1 ? firstFamily(w.h1.fontFamily) : firstFamily(w.body.fontFamily))
9405
+ );
9147
9406
  p.fontDisplay = design?.fonts[0] ? `named in design.md` : w.h1 ? `the h1's computed face` : `the body's computed face (no h1)`;
9148
9407
  const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
9149
- const fontBody = designBody ?? firstFamily(w.body.fontFamily);
9408
+ const fontBody = catalogFamily(designBody ?? firstFamily(w.body.fontFamily));
9150
9409
  p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
9151
9410
  const isIcon = (u) => /favicon|apple-touch|icon/i.test(u);
9152
9411
  const onDark = (u) => /on-dark|white/i.test(u);
@@ -9848,14 +10107,15 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
9848
10107
  "composed",
9849
10108
  "check",
9850
10109
  "motion",
9851
- "leader"
10110
+ "leader",
10111
+ "keep-frames"
9852
10112
  ]);
9853
- var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
10113
+ var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override", "browser-arg"]);
9854
10114
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
9855
10115
 
9856
10116
  Take pipeline
9857
- vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
9858
- vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
10117
+ vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [render flags] [--json]
10118
+ vos record --actions actions.json [--url <url>] [--out take] [--strict] [--keep-frames] [--max-duration <s>] [--storage-state <file>] [--browser-arg=<switch>]... [--background <slug|url|none>] [--json]
9859
10119
  vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--with <doc.json|vosId>[@end|@start|@step:<id>|@<s>]]... [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|fade|slide|none] [--transitions slide|fade|scale|none] [--end-card on|none|<doc.json|vosId>] [--captions none] [--clicks none] [--still <t>] [--release v2.1] [--json]
9860
10120
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
9861
10121
  vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--at-still] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
@@ -9863,7 +10123,7 @@ Take pipeline
9863
10123
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
9864
10124
  vos brand <url> [--out BRAND.md] [--json]
9865
10125
  vos open <take> [--studio <url>] [--print]
9866
- vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side auto|right|left|below|above] [--mark ring|underline|none] [--leader] [--color #hex] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px 14] [--id x] [--print] [--json]
10126
+ vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side auto|right|left|below|above] [--mark ring|underline|none] [--leader] [--color #hex (the mark and leader; the kicker is --accent)] [--brand BRAND.md] [--ground #hex] [--accent #hex] [--font "\u2026"] [--body-px <n> (the body size on the delivered frame)] [--id x] [--print] [--json]
9867
10127
  vos validate <actions.json|take|kit.json> [--picture] [--json]
9868
10128
  vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]
9869
10129
  vos actions from-agent-browser <steps.jsonl> [--out actions.json] [--url <url>] [--viewport WxH] [--json]
@@ -9883,7 +10143,10 @@ Platform (vos.so) \u2014 fetch, edit, push, pull, repeat
9883
10143
  a take DIRECTORY (doc.json) pushes recording + doc; a config.json
9884
10144
  pushes the program. No --vos: create a PRIVATE vos; with --vos:
9885
10145
  add a version against the tracked base \u2014 a stale push 409s WITH
9886
- the platform's typed changelog. --claimable (programs only, NO
10146
+ the platform's typed changelog. A TAKE pushes to the vos in its
10147
+ vos.json; --vos on a take with none ADOPTS that vos (the take
10148
+ becomes its next version, built on its head), and --vos that
10149
+ disagrees with vos.json is refused before anything uploads. --claimable (programs only, NO
9887
10150
  credential): creates a 72h claim link instead \u2014 hand it to the
9888
10151
  user and nowhere else; unclaimed work is deleted after 72h.
9889
10152
  --override consents to touching
@@ -10130,6 +10393,18 @@ function strictReason(rec) {
10130
10393
  if (rec.capped) parts.push("stopped at --max-duration");
10131
10394
  return parts.join(" + ");
10132
10395
  }
10396
+ function takeBrowserArgs(multi) {
10397
+ const given = multi["browser-arg"];
10398
+ return (given ?? []).filter(Boolean);
10399
+ }
10400
+ function takeStorageState(flags) {
10401
+ const raw = strFlag(flags, "storage-state");
10402
+ if (!raw) return void 0;
10403
+ const path = resolve9(raw);
10404
+ if (!existsSync15(path))
10405
+ throw new UsageError(`--storage-state: no such file: ${path}`);
10406
+ return path;
10407
+ }
10133
10408
  async function takeBackdrop(flags, r) {
10134
10409
  const { backdrop, note } = await openingBackdrop(
10135
10410
  strFlag(flags, "background"),
@@ -10141,8 +10416,20 @@ async function takeBackdrop(flags, r) {
10141
10416
  if (note) r.log(`note: ${note}`);
10142
10417
  return backdrop;
10143
10418
  }
10419
+ async function releaseFrames(paths, flags, log) {
10420
+ if (flags["keep-frames"] === true) return;
10421
+ const freed = await dropScreencastFrames(paths);
10422
+ if (freed >= 1e6)
10423
+ log(
10424
+ `released ${Math.round(freed / 1e6)} MB of screencast frames (the recording holds them now; --keep-frames keeps them)`
10425
+ );
10426
+ }
10144
10427
  async function cmdRecord(argv) {
10145
- const { positionals, flags } = parseArgs(argv, BOOLEAN_FLAGS5);
10428
+ const { positionals, flags, multi } = parseArgs(
10429
+ argv,
10430
+ BOOLEAN_FLAGS5,
10431
+ MULTI_FLAGS2
10432
+ );
10146
10433
  const actionsPath = strFlag(flags, "actions") ?? positionals[0];
10147
10434
  if (!actionsPath)
10148
10435
  throw new UsageError(
@@ -10156,20 +10443,23 @@ async function cmdRecord(argv) {
10156
10443
  const outDir = resolve9(strFlag(flags, "out") ?? "take");
10157
10444
  const backdrop = await takeBackdrop(flags, r);
10158
10445
  const maxDurationSeconds = await maxDuration(flags, r);
10446
+ const storageState = takeStorageState(flags);
10447
+ const browserArgs = takeBrowserArgs(multi);
10159
10448
  if (existsSync15(join18(outDir, "meta.json"))) {
10160
10449
  const { prevDoc, kept } = await prepareReRecord(outDir);
10161
10450
  r.log(
10162
10451
  `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" : "")
10163
10452
  );
10164
10453
  }
10165
- await mkdir9(outDir, { recursive: true });
10454
+ await mkdir10(outDir, { recursive: true });
10166
10455
  const paths = await ensureTakeDir(outDir);
10167
- const browser = await launchBrowser();
10456
+ const browser = await launchBrowser(browserArgs);
10168
10457
  try {
10169
10458
  r.log("recording\u2026");
10170
10459
  r.event({ event: "phase", phase: "record" });
10171
10460
  const rec = await recordTake(browser, url, actions, paths, r.log, {
10172
- maxDurationSeconds
10461
+ maxDurationSeconds,
10462
+ storageState
10173
10463
  });
10174
10464
  r.event({ event: "phase", phase: "encode" });
10175
10465
  r.log("encoding\u2026");
@@ -10178,6 +10468,7 @@ async function cmdRecord(argv) {
10178
10468
  outDir,
10179
10469
  (p) => r.event({ event: "progress", phase: "encode", fraction: p })
10180
10470
  );
10471
+ await releaseFrames(paths, flags, r.log);
10181
10472
  r.event({ event: "phase", phase: "plan" });
10182
10473
  const plan = await planTake(outDir, { backdrop });
10183
10474
  const clicks = rec.events.filter((e) => e.type === "down").length;
@@ -10240,20 +10531,23 @@ async function cmdCreate2(argv) {
10240
10531
  }
10241
10532
  const backdrop = await takeBackdrop(flags, r);
10242
10533
  const maxDurationSeconds = await maxDuration(flags, r);
10534
+ const storageState = takeStorageState(flags);
10535
+ const browserArgs = takeBrowserArgs(multi);
10243
10536
  if (existsSync15(join18(outDir, "meta.json"))) {
10244
10537
  const { prevDoc, kept } = await prepareReRecord(outDir);
10245
10538
  r.log(
10246
10539
  `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" : "")
10247
10540
  );
10248
10541
  }
10249
- await mkdir9(outDir, { recursive: true });
10542
+ await mkdir10(outDir, { recursive: true });
10250
10543
  const paths = await ensureTakeDir(outDir);
10251
- const browser = await launchBrowser();
10544
+ const browser = await launchBrowser(browserArgs);
10252
10545
  try {
10253
10546
  r.log("recording\u2026");
10254
10547
  r.event({ event: "phase", phase: "record" });
10255
10548
  const rec = await recordTake(browser, url, actions, paths, r.log, {
10256
- maxDurationSeconds
10549
+ maxDurationSeconds,
10550
+ storageState
10257
10551
  });
10258
10552
  r.event({ event: "phase", phase: "encode" });
10259
10553
  r.log("encoding\u2026");
@@ -10262,6 +10556,7 @@ async function cmdCreate2(argv) {
10262
10556
  outDir,
10263
10557
  (p) => r.event({ event: "progress", phase: "encode", fraction: p })
10264
10558
  );
10559
+ await releaseFrames(paths, flags, r.log);
10265
10560
  r.event({ event: "phase", phase: "plan" });
10266
10561
  await planTake(outDir, { backdrop });
10267
10562
  if (flags.strict === true && (rec.skipped.length > 0 || rec.navTimeout || rec.capped)) {
@@ -10331,7 +10626,7 @@ async function cmdPlan(argv) {
10331
10626
  );
10332
10627
  const r = createReporter(flags.json === true);
10333
10628
  if (flags.fresh === true) {
10334
- await rm4(join18(dir, "doc.json"), { force: true });
10629
+ await rm5(join18(dir, "doc.json"), { force: true });
10335
10630
  r.log("note: --fresh discarded the existing doc.json");
10336
10631
  }
10337
10632
  let reuse;
@@ -10895,7 +11190,7 @@ async function cmdOpen(argv) {
10895
11190
  const r = createReporter(flags.json === true);
10896
11191
  const take = await loadTake(dir);
10897
11192
  if (!existsSync15(take.paths.recording)) {
10898
- throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
11193
+ throw new UsageError(`${dir} has no recording \u2014 re-run record`);
10899
11194
  }
10900
11195
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
10901
11196
  /\/+$/,
@@ -10961,7 +11256,7 @@ async function cmdCallout(argv) {
10961
11256
  const shape = positionals[1];
10962
11257
  if (!dir || !shape || !isCalloutShape(shape))
10963
11258
  throw new UsageError(
10964
- 'vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side \u2026] [--mark ring|underline|none] [--leader] [--color #hex] [--brand BRAND.md] [--ground #hex --accent #hex] [--font "\u2026"] [--body-px 14] [--id x] [--print] [--json]'
11259
+ 'vos callout <take> <note|tag|code> --step <id> [--title "\u2026"] [--body "\u2026"] [--kicker "\u2026"] [--code "\u2026"|--code-file f] [--seconds 3] [--side \u2026] [--mark ring|underline|none] [--leader] [--color #hex (the mark and leader; the kicker is --accent)] [--brand BRAND.md] [--ground #hex --accent #hex] [--font "\u2026"] [--body-px <n> (the body size on the delivered frame)] [--id x] [--print] [--json]'
10965
11260
  );
10966
11261
  const r = createReporter(flags.json === true);
10967
11262
  const take = await loadTake(dir);
@@ -11012,15 +11307,16 @@ async function cmdCallout(argv) {
11012
11307
  side,
11013
11308
  mark,
11014
11309
  leader: flags.leader === true,
11015
- color: strFlag(flags, "color")
11310
+ color: strFlag(flags, "color"),
11311
+ bodyPxGiven: bodyPx !== void 0
11016
11312
  });
11017
11313
  } catch (e) {
11018
11314
  throw new UsageError(e instanceof Error ? e.message : String(e));
11019
11315
  }
11020
- const { clip, window: window2 } = composed;
11316
+ const { clip, window: window2, sizes } = composed;
11021
11317
  if (flags.print === true) {
11022
11318
  r.done(
11023
- { clip, window: window2, register, brand: brand?.file ?? null },
11319
+ { clip, window: window2, sizes, register, brand: brand?.file ?? null },
11024
11320
  JSON.stringify(clip, null, 2)
11025
11321
  );
11026
11322
  return EXIT_OK;
@@ -11042,11 +11338,12 @@ async function cmdCallout(argv) {
11042
11338
  {
11043
11339
  clip,
11044
11340
  window: window2,
11341
+ sizes,
11045
11342
  register,
11046
11343
  brand: brand?.file ?? null,
11047
11344
  warnings: lint.warnings
11048
11345
  },
11049
- `${shape} "${clip.id}" at ${window2.start.toFixed(2)}s for ${window2.duration.toFixed(2)}s${clip.pin ? `, pinned to ${clip.pin.step !== void 0 ? `step ${String(clip.pin.step)}` : `the press at ${String(clip.pin.press)}s`}` : ""}, ground ${register.ground} \u2192 card in the product's hue, written to ${take.paths.doc}${lint.warnings.length ? `
11346
+ `${shape} "${clip.id}" at ${window2.start.toFixed(2)}s for ${window2.duration.toFixed(2)}s${clip.pin ? `, pinned to ${clip.pin.step !== void 0 ? `step ${String(clip.pin.step)}` : `the press at ${String(clip.pin.press)}s`}` : ""}, ground ${register.ground} \u2192 card in the product's hue, type ${sizes.kickerPx} / ${sizes.titlePx} / ${sizes.bodyPx} px (kicker / title / body; ${bodyPx !== void 0 ? `--body-px ${bodyPx} as delivered` : `the app's body as seen, footage scale ${sizes.scale}; --body-px <n> sets the delivered size`}), written to ${take.paths.doc}${lint.warnings.length ? `
11050
11347
  warnings:
11051
11348
  ${lint.warnings.join("\n ")}` : ""}`
11052
11349
  );
@@ -11132,8 +11429,15 @@ async function cmdValidate(argv) {
11132
11429
  const take = await loadTake(target);
11133
11430
  const problems = [];
11134
11431
  const warnings = [];
11135
- if (!existsSync15(take.paths.recording))
11136
- problems.push("missing recording.webm (re-run record)");
11432
+ if (!existsSync15(take.paths.recording)) {
11433
+ problems.push("missing recording (re-run record)");
11434
+ } else {
11435
+ const mismatch = containerMismatch(
11436
+ take.paths.recordingName,
11437
+ await readMediaHead(take.paths.recording)
11438
+ );
11439
+ if (mismatch) warnings.push(mismatch);
11440
+ }
11137
11441
  if (!take.doc) problems.push("missing doc.json (run plan)");
11138
11442
  if (take.doc) {
11139
11443
  const lint = lintDoc(take.doc);
@@ -11202,8 +11506,16 @@ async function cmdPush3(argv) {
11202
11506
  const dir = positionals[0];
11203
11507
  if (!dir)
11204
11508
  throw new UsageError(
11205
- "vos push <take> [--title|--label|--note|--folder|--override|--yes|--key|--api]"
11509
+ "vos push <take> [--vos|--title|--label|--note|--folder|--override|--yes|--key|--api]"
11206
11510
  );
11511
+ const refusal = takePushRefusal({
11512
+ vos: strFlag(flags, "vos"),
11513
+ trackedVosId: readSyncState(resolve9(dir))?.vosId ?? null,
11514
+ programOnly: ["slug", "desc", "tags", "base", "remix-of"].filter(
11515
+ (name) => hasFlag(flags, name)
11516
+ )
11517
+ });
11518
+ if (refusal) throw new UsageError(refusal);
11207
11519
  const r = createReporter(flags.json === true);
11208
11520
  const overrides = multi.override;
11209
11521
  const result = await pushTake(
@@ -11213,6 +11525,7 @@ async function cmdPush3(argv) {
11213
11525
  api: strFlag(flags, "api"),
11214
11526
  origin: strFlag(flags, "origin"),
11215
11527
  yes: flags.yes === true,
11528
+ vos: strFlag(flags, "vos"),
11216
11529
  title: strFlag(flags, "title"),
11217
11530
  label: strFlag(flags, "label"),
11218
11531
  note: strFlag(flags, "note"),
@@ -11353,6 +11666,7 @@ export {
11353
11666
  RECORDING_NAME,
11354
11667
  takePaths,
11355
11668
  loadTake,
11669
+ pullMedia,
11356
11670
  startTakeServer,
11357
11671
  waitForPageDone,
11358
11672
  renderAnimation,
@@ -11369,10 +11683,12 @@ export {
11369
11683
  recordTake,
11370
11684
  encodeRecording,
11371
11685
  planTake,
11372
- pullMedia,
11373
11686
  parseAgentBrowserLog,
11374
11687
  splitCommand,
11375
11688
  convertAgentBrowser,
11689
+ MULTI_FLAGS2 as MULTI_FLAGS,
11690
+ HELP,
11691
+ takeBrowserArgs,
11376
11692
  run
11377
11693
  };
11378
- //# sourceMappingURL=chunk-MIZWK5AN.js.map
11694
+ //# sourceMappingURL=chunk-LGRNHJ2U.js.map