@vosjs/cli 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -90
- package/dist/{chunk-5BFGODBI.js → chunk-EFI3WUQB.js} +5 -1
- package/dist/chunk-EFI3WUQB.js.map +1 -0
- package/dist/{chunk-X2BKHZ56.js → chunk-GCWE75OA.js} +1 -105
- package/dist/chunk-GCWE75OA.js.map +1 -0
- package/dist/chunk-NHKHTIDR.js +216 -0
- package/dist/chunk-NHKHTIDR.js.map +1 -0
- package/dist/{chunk-DEE2KCDO.js → chunk-YICRU4ER.js} +118 -34
- package/dist/chunk-YICRU4ER.js.map +1 -0
- package/dist/cli.js +32 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +7 -7
- package/dist/manifest-UCS6OVWZ.js +8 -0
- package/dist/run-PCWIQ4GI.js +9 -0
- package/package.json +9 -9
- package/schema/doc.schema.json +1 -1
- package/dist/chunk-5BFGODBI.js.map +0 -1
- package/dist/chunk-A2VSDK3C.js +0 -42
- package/dist/chunk-A2VSDK3C.js.map +0 -1
- package/dist/chunk-DEE2KCDO.js.map +0 -1
- package/dist/chunk-X2BKHZ56.js.map +0 -1
- package/dist/manifest-3LIL5M3F.js +0 -8
- package/dist/run-GY5QMKJ4.js +0 -9
- /package/dist/{manifest-3LIL5M3F.js.map → manifest-UCS6OVWZ.js.map} +0 -0
- /package/dist/{run-GY5QMKJ4.js.map → run-PCWIQ4GI.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/render.ts"],"sourcesContent":["import { compileVosConfig } from '@vosjs/core'\nimport { generateRenderTemplate } from '@vosjs/core/runtime'\nimport { elementsBundleCode } from '@vosjs/elements/bundle'\nimport { tweenRuntimeCode } from '@vosjs/tween/bundle'\nimport type { Browser, Page } from 'playwright'\n\n/** Fake secure origin the render page is served from (WebCodecs needs one). */\nconst RENDER_ORIGIN = 'https://vos-cli.render'\n\nexport interface RenderCommonOptions {\n config: Record<string, unknown>\n width: number\n height: number\n /** Phase callback for progress reporting. */\n onPhase?: (phase: string) => void\n}\n\nexport interface RenderVideoOptions extends RenderCommonOptions {\n fps: number\n /** Output duration in seconds. */\n duration: number\n format: 'webm' | 'mp4'\n}\n\nexport interface RenderStillOptions extends RenderCommonOptions {\n /** Seek time for the captured frame, seconds. */\n time: number\n}\n\nexport interface RenderResult {\n bytes: Uint8Array\n mimeType: string\n}\n\ninterface RenderComplete {\n success: boolean\n data?: string\n error?: string\n}\n\nfunction compile(config: Record<string, unknown>): string {\n return compileVosConfig(config as never, { tweenEngine: 'vos' })\n}\n\nasync function runCapturePage(\n browser: Browser,\n html: string,\n opts: { width: number; height: number; timeoutMs: number },\n): Promise<RenderComplete> {\n const context = await browser.newContext({\n viewport: { width: opts.width, height: opts.height },\n })\n const page: Page = await context.newPage()\n const errors: string[] = []\n page.on('console', (m) => {\n if (m.type() === 'error') errors.push(m.text())\n })\n try {\n await page.route(`${RENDER_ORIGIN}/**`, (route) =>\n route.fulfill({ status: 200, contentType: 'text/html', body: html }),\n )\n await page.goto(`${RENDER_ORIGIN}/render`, {\n waitUntil: 'domcontentloaded',\n })\n const start = Date.now()\n for (;;) {\n const done = (await page.evaluate(\n 'window.__renderComplete ?? null',\n )) as RenderComplete | null\n if (done) return done\n if (Date.now() - start > opts.timeoutMs) {\n return {\n success: false,\n error: `render timed out after ${Math.round(opts.timeoutMs / 1000)}s${\n errors.length\n ? ` (page errors: ${errors.slice(0, 3).join(' | ')})`\n : ''\n }`,\n }\n }\n await new Promise((r) => setTimeout(r, 400))\n }\n } finally {\n await context.close()\n }\n}\n\nfunction decodeDataUrl(dataUrl: string): RenderResult {\n const m = /^data:([^;,]+);base64,(.*)$/s.exec(dataUrl)\n if (!m) throw new Error('capture page returned an unexpected payload')\n return { bytes: Uint8Array.from(Buffer.from(m[2], 'base64')), mimeType: m[1] }\n}\n\n/** Render a vos config to a video (WebM/MP4) in a headless browser. */\nexport async function renderVideo(\n browser: Browser,\n opts: RenderVideoOptions,\n): Promise<RenderResult> {\n opts.onPhase?.('compile')\n const code = compile(opts.config)\n const html = generateRenderTemplate(code, {\n mode: 'capture-video',\n capture: {\n width: opts.width,\n height: opts.height,\n duration: opts.duration,\n fps: opts.fps,\n format: opts.format,\n },\n elementsBundleCode,\n tweenEngine: 'vos',\n tweenBundleCode: tweenRuntimeCode,\n })\n opts.onPhase?.('render')\n // Generous ceiling: startup + CDN module fetch + ~4× realtime per frame batch.\n const timeoutMs = 90_000 + opts.duration * opts.fps * 400\n const done = await runCapturePage(browser, html, {\n width: opts.width,\n height: opts.height,\n timeoutMs,\n })\n if (!done.success || !done.data)\n throw new Error(done.error ?? 'render failed')\n return decodeDataUrl(done.data)\n}\n\n/** Render a single frame of a vos config to an image in a headless browser. */\nexport async function renderStill(\n browser: Browser,\n opts: RenderStillOptions,\n): Promise<RenderResult> {\n opts.onPhase?.('compile')\n const code = compile(opts.config)\n const html = generateRenderTemplate(code, {\n mode: 'capture-thumbnail',\n capture: {\n width: opts.width,\n height: opts.height,\n duration: Math.max(1, opts.time + 1),\n fps: 30,\n thumbnailTime: opts.time,\n },\n elementsBundleCode,\n tweenEngine: 'vos',\n tweenBundleCode: tweenRuntimeCode,\n })\n opts.onPhase?.('render')\n const done = await runCapturePage(browser, html, {\n width: opts.width,\n height: opts.height,\n timeoutMs: 120_000,\n })\n if (!done.success || !done.data)\n throw new Error(done.error ?? 'still render failed')\n return decodeDataUrl(done.data)\n}\n\nexport interface PreviewPages {\n /** Host page: iframes the player and drives it over the bridge. */\n hostHtml: string\n /** The engine's playback-mode player (waits for a LOAD postMessage). */\n playerHtml: string\n}\n\n/**\n * Pages for `vos preview`. The playback template is the iframe HALF of the\n * player bridge — it renders nothing until a host posts `LOAD`, so the CLI\n * serves a minimal host page that sends `LOAD { code, autoplay }` and shows\n * a transport line (time / duration, click to play-pause).\n */\nexport function previewPages(config: Record<string, unknown>): PreviewPages {\n const code = compile(config)\n const playerHtml = generateRenderTemplate(code, {\n mode: 'playback',\n elementsBundleCode,\n tweenEngine: 'vos',\n tweenBundleCode: tweenRuntimeCode,\n })\n const codeJson = JSON.stringify(code).replace(/<\\//g, '<\\\\/')\n const hostHtml = `<!doctype html><html><head><meta charset=\"utf-8\"><title>vos preview</title>\n<style>\n html,body{margin:0;height:100%;background:#000;overflow:hidden}\n iframe{position:absolute;inset:0;width:100%;height:100%;border:0}\n #cover{position:fixed;inset:0;cursor:pointer}\n #hud{position:fixed;left:12px;bottom:10px;color:#fff;opacity:.75;font:12px/1.4 ui-monospace,monospace;\n background:rgba(0,0,0,.45);padding:4px 8px;border-radius:6px;pointer-events:none}\n</style></head><body>\n<iframe id=\"p\" src=\"/player\" allow=\"autoplay\"></iframe>\n<div id=\"cover\"></div>\n<div id=\"hud\">loading…</div>\n<script>\n const code = ${codeJson}\n const frame = document.getElementById('p')\n const hud = document.getElementById('hud')\n let ready = false\n let playing = true\n const post = (msg) => frame.contentWindow && frame.contentWindow.postMessage(msg, '*')\n const load = () => post({ type: 'LOAD', code, autoplay: true })\n window.addEventListener('message', (e) => {\n const msg = e.data || {}\n if (msg.type === 'BRIDGE_READY') load()\n if (msg.type === 'READY') { ready = true; hud.textContent = '0.0 / ' + msg.duration.toFixed(1) + 's — click to pause' }\n if (msg.type === 'UPDATE') hud.textContent = msg.time.toFixed(1) + ' / ' + msg.duration.toFixed(1) + 's — click to ' + (playing ? 'pause' : 'play')\n if (msg.type === 'ERROR') hud.textContent = 'error: ' + msg.error\n })\n // Belt and braces: if BRIDGE_READY raced the host, retry LOAD until READY.\n let tries = 0\n const iv = setInterval(() => {\n if (ready || tries++ > 20) return clearInterval(iv)\n load()\n }, 400)\n // The iframe swallows clicks — capture them on an overlay above it.\n document.getElementById('cover').addEventListener('click', () => {\n if (!ready) return\n playing = !playing\n post({ type: playing ? 'PLAY' : 'PAUSE' })\n hud.textContent = hud.textContent.replace(/click to (pause|play)/, 'click to ' + (playing ? 'pause' : 'play'))\n })\n</script></body></html>`\n return { hostHtml, playerHtml }\n}\n"],"mappings":";;;AAAA,SAAS,wBAAwB;AACjC,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AAIjC,IAAM,gBAAgB;AAiCtB,SAAS,QAAQ,QAAyC;AACxD,SAAO,iBAAiB,QAAiB,EAAE,aAAa,MAAM,CAAC;AACjE;AAEA,eAAe,eACb,SACA,MACA,MACyB;AACzB,QAAM,UAAU,MAAM,QAAQ,WAAW;AAAA,IACvC,UAAU,EAAE,OAAO,KAAK,OAAO,QAAQ,KAAK,OAAO;AAAA,EACrD,CAAC;AACD,QAAM,OAAa,MAAM,QAAQ,QAAQ;AACzC,QAAM,SAAmB,CAAC;AAC1B,OAAK,GAAG,WAAW,CAAC,MAAM;AACxB,QAAI,EAAE,KAAK,MAAM,QAAS,QAAO,KAAK,EAAE,KAAK,CAAC;AAAA,EAChD,CAAC;AACD,MAAI;AACF,UAAM,KAAK;AAAA,MAAM,GAAG,aAAa;AAAA,MAAO,CAAC,UACvC,MAAM,QAAQ,EAAE,QAAQ,KAAK,aAAa,aAAa,MAAM,KAAK,CAAC;AAAA,IACrE;AACA,UAAM,KAAK,KAAK,GAAG,aAAa,WAAW;AAAA,MACzC,WAAW;AAAA,IACb,CAAC;AACD,UAAM,QAAQ,KAAK,IAAI;AACvB,eAAS;AACP,YAAM,OAAQ,MAAM,KAAK;AAAA,QACvB;AAAA,MACF;AACA,UAAI,KAAM,QAAO;AACjB,UAAI,KAAK,IAAI,IAAI,QAAQ,KAAK,WAAW;AACvC,eAAO;AAAA,UACL,SAAS;AAAA,UACT,OAAO,0BAA0B,KAAK,MAAM,KAAK,YAAY,GAAI,CAAC,IAChE,OAAO,SACH,kBAAkB,OAAO,MAAM,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,MAChD,EACN;AAAA,QACF;AAAA,MACF;AACA,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAAA,IAC7C;AAAA,EACF,UAAE;AACA,UAAM,QAAQ,MAAM;AAAA,EACtB;AACF;AAEA,SAAS,cAAc,SAA+B;AACpD,QAAM,IAAI,+BAA+B,KAAK,OAAO;AACrD,MAAI,CAAC,EAAG,OAAM,IAAI,MAAM,6CAA6C;AACrE,SAAO,EAAE,OAAO,WAAW,KAAK,OAAO,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,GAAG,UAAU,EAAE,CAAC,EAAE;AAC/E;AAGA,eAAsB,YACpB,SACA,MACuB;AACvB,OAAK,UAAU,SAAS;AACxB,QAAM,OAAO,QAAQ,KAAK,MAAM;AAChC,QAAM,OAAO,uBAAuB,MAAM;AAAA,IACxC,MAAM;AAAA,IACN,SAAS;AAAA,MACP,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK;AAAA,MACf,KAAK,KAAK;AAAA,MACV,QAAQ,KAAK;AAAA,IACf;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB,CAAC;AACD,OAAK,UAAU,QAAQ;AAEvB,QAAM,YAAY,MAAS,KAAK,WAAW,KAAK,MAAM;AACtD,QAAM,OAAO,MAAM,eAAe,SAAS,MAAM;AAAA,IAC/C,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb;AAAA,EACF,CAAC;AACD,MAAI,CAAC,KAAK,WAAW,CAAC,KAAK;AACzB,UAAM,IAAI,MAAM,KAAK,SAAS,eAAe;AAC/C,SAAO,cAAc,KAAK,IAAI;AAChC;AAGA,eAAsB,YACpB,SACA,MACuB;AACvB,OAAK,UAAU,SAAS;AACxB,QAAM,OAAO,QAAQ,KAAK,MAAM;AAChC,QAAM,OAAO,uBAAuB,MAAM;AAAA,IACxC,MAAM;AAAA,IACN,SAAS;AAAA,MACP,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK,IAAI,GAAG,KAAK,OAAO,CAAC;AAAA,MACnC,KAAK;AAAA,MACL,eAAe,KAAK;AAAA,IACtB;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB,CAAC;AACD,OAAK,UAAU,QAAQ;AACvB,QAAM,OAAO,MAAM,eAAe,SAAS,MAAM;AAAA,IAC/C,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,KAAK,WAAW,CAAC,KAAK;AACzB,UAAM,IAAI,MAAM,KAAK,SAAS,qBAAqB;AACrD,SAAO,cAAc,KAAK,IAAI;AAChC;AAeO,SAAS,aAAa,QAA+C;AAC1E,QAAM,OAAO,QAAQ,MAAM;AAC3B,QAAM,aAAa,uBAAuB,MAAM;AAAA,IAC9C,MAAM;AAAA,IACN;AAAA,IACA,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB,CAAC;AACD,QAAM,WAAW,KAAK,UAAU,IAAI,EAAE,QAAQ,QAAQ,MAAM;AAC5D,QAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAYF,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BvB,SAAO,EAAE,UAAU,WAAW;AAChC;","names":[]}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/args.ts
|
|
4
|
+
var UsageError = class extends Error {
|
|
5
|
+
};
|
|
6
|
+
function parseArgs(argv, booleanFlags) {
|
|
7
|
+
const positionals = [];
|
|
8
|
+
const flags = {};
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
if (arg === "--") {
|
|
12
|
+
positionals.push(...argv.slice(i + 1));
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
if (arg.startsWith("--")) {
|
|
16
|
+
const eq = arg.indexOf("=");
|
|
17
|
+
if (eq !== -1) {
|
|
18
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const name = arg.slice(2);
|
|
22
|
+
if (booleanFlags.has(name)) {
|
|
23
|
+
flags[name] = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
if (next === void 0 || next.startsWith("--")) {
|
|
28
|
+
throw new UsageError(`--${name} expects a value`);
|
|
29
|
+
}
|
|
30
|
+
flags[name] = next;
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
positionals.push(arg);
|
|
35
|
+
}
|
|
36
|
+
return { positionals, flags };
|
|
37
|
+
}
|
|
38
|
+
function numFlag(flags, name, fallback) {
|
|
39
|
+
const v = flags[name];
|
|
40
|
+
if (v === void 0) return fallback;
|
|
41
|
+
const n = Number(v);
|
|
42
|
+
if (!Number.isFinite(n))
|
|
43
|
+
throw new UsageError(`--${name} expects a number, got "${String(v)}"`);
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/loadConfig.ts
|
|
48
|
+
import { readFile } from "fs/promises";
|
|
49
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
50
|
+
import { join } from "path";
|
|
51
|
+
import {
|
|
52
|
+
CURRENT_CONFIG_VERSION,
|
|
53
|
+
migrateConfig,
|
|
54
|
+
vosConfigJsonSchema
|
|
55
|
+
} from "@vosjs/core";
|
|
56
|
+
function directoryKind(target) {
|
|
57
|
+
try {
|
|
58
|
+
if (!statSync(target).isDirectory()) return "none";
|
|
59
|
+
} catch {
|
|
60
|
+
return "none";
|
|
61
|
+
}
|
|
62
|
+
const docPath = join(target, "doc.json");
|
|
63
|
+
if (existsSync(docPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const doc = JSON.parse(readFileSync(docPath, "utf8"));
|
|
66
|
+
if (typeof doc === "object" && doc !== null && "source" in doc)
|
|
67
|
+
return "take";
|
|
68
|
+
} catch {
|
|
69
|
+
return "take";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return existsSync(join(target, "config.json")) ? "program" : "none";
|
|
73
|
+
}
|
|
74
|
+
async function loadProgramDirectory(dir, warnings) {
|
|
75
|
+
const config = JSON.parse(
|
|
76
|
+
await readFile(join(dir, "config.json"), "utf8")
|
|
77
|
+
);
|
|
78
|
+
const docPath = join(dir, "doc.json");
|
|
79
|
+
if (!existsSync(docPath)) return config;
|
|
80
|
+
const doc = JSON.parse(await readFile(docPath, "utf8"));
|
|
81
|
+
if (typeof doc !== "object" || doc === null || "source" in doc) return config;
|
|
82
|
+
if (typeof config !== "object" || config === null) {
|
|
83
|
+
throw new UsageError(`${dir}/config.json does not contain a JSON object`);
|
|
84
|
+
}
|
|
85
|
+
const raw = doc;
|
|
86
|
+
const program = raw.program && typeof raw.program === "object" ? raw.program : {};
|
|
87
|
+
const { lowerProgramDoc } = await import("@vosjs/studio-core");
|
|
88
|
+
const composed = lowerProgramDoc(
|
|
89
|
+
{ ...raw, program: { ...program, config } },
|
|
90
|
+
{ bake: true }
|
|
91
|
+
).config;
|
|
92
|
+
warnings.push(
|
|
93
|
+
"composed config.json with doc.json (a program document: its layers and tween edits ride the render)"
|
|
94
|
+
);
|
|
95
|
+
return composed;
|
|
96
|
+
}
|
|
97
|
+
async function readSourceText(source) {
|
|
98
|
+
if (/^https?:\/\//.test(source)) {
|
|
99
|
+
const res = await fetch(source);
|
|
100
|
+
if (!res.ok) throw new Error(`fetch ${source} \u2192 ${res.status}`);
|
|
101
|
+
return await res.text();
|
|
102
|
+
}
|
|
103
|
+
let isDir = false;
|
|
104
|
+
try {
|
|
105
|
+
isDir = statSync(source).isDirectory();
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
if (isDir) {
|
|
109
|
+
throw new UsageError(
|
|
110
|
+
`${source} is a directory with no config.json and no doc.json \u2014 pass a config file, a URL, a program directory or a take`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return await readFile(source, "utf8");
|
|
114
|
+
}
|
|
115
|
+
async function loadVosConfig(source) {
|
|
116
|
+
const warnings = [];
|
|
117
|
+
let parsed;
|
|
118
|
+
const kind = /^https?:\/\//.test(source) ? "none" : directoryKind(source);
|
|
119
|
+
if (kind === "take") {
|
|
120
|
+
throw new UsageError(
|
|
121
|
+
`${source} is a take (its doc.json is a recording document). Render it through the take pipeline: vos render ${source} [out]; vos frames ${source} for stills.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (kind === "program") {
|
|
125
|
+
parsed = await loadProgramDirectory(source, warnings);
|
|
126
|
+
return finish(source, parsed, warnings);
|
|
127
|
+
}
|
|
128
|
+
const raw = await readSourceText(source);
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(raw);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
throw new UsageError(`${source} is not valid JSON: ${e.message}`);
|
|
133
|
+
}
|
|
134
|
+
return finish(source, parsed, warnings);
|
|
135
|
+
}
|
|
136
|
+
function finish(source, parsed, warnings) {
|
|
137
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
138
|
+
throw new UsageError(`${source} does not contain a JSON object`);
|
|
139
|
+
}
|
|
140
|
+
let obj = parsed;
|
|
141
|
+
if (typeof obj.config === "object" && obj.config !== null && !("createTimeline" in obj)) {
|
|
142
|
+
obj = obj.config;
|
|
143
|
+
warnings.push("unwrapped { config } envelope");
|
|
144
|
+
}
|
|
145
|
+
const version = obj.version;
|
|
146
|
+
const migrated = migrateConfig(obj);
|
|
147
|
+
if (version === void 0) {
|
|
148
|
+
warnings.push(
|
|
149
|
+
`config declares no "version". Add "version": ${CURRENT_CONFIG_VERSION} before pushing it.`
|
|
150
|
+
);
|
|
151
|
+
} else if (version !== CURRENT_CONFIG_VERSION) {
|
|
152
|
+
warnings.push(
|
|
153
|
+
`migrated config v${String(version)} to v${CURRENT_CONFIG_VERSION}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const check = vosConfigJsonSchema.safeParse(migrated);
|
|
157
|
+
if (!check.success) {
|
|
158
|
+
const issues = check.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
159
|
+
throw new UsageError(`invalid vos config:
|
|
160
|
+
${issues}`);
|
|
161
|
+
}
|
|
162
|
+
return { config: migrated, warnings };
|
|
163
|
+
}
|
|
164
|
+
function configDuration(config) {
|
|
165
|
+
const d = config.duration;
|
|
166
|
+
return typeof d === "number" && Number.isFinite(d) && d > 0 ? d : void 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/browser.ts
|
|
170
|
+
import { chromium } from "playwright";
|
|
171
|
+
var BrowserUnavailableError = class extends Error {
|
|
172
|
+
constructor(cause) {
|
|
173
|
+
super(
|
|
174
|
+
`Could not launch a Chromium-family browser (${cause}).
|
|
175
|
+
Fixes, in order of preference:
|
|
176
|
+
1. Install Google Chrome (used automatically), or
|
|
177
|
+
2. npx playwright install chromium, or
|
|
178
|
+
3. Set VOS_BROWSER_PATH to a Chrome/Chromium executable.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
async function launchBrowser() {
|
|
183
|
+
const explicit = process.env.VOS_BROWSER_PATH;
|
|
184
|
+
if (explicit) {
|
|
185
|
+
return chromium.launch({ executablePath: explicit }).catch((e) => {
|
|
186
|
+
throw new BrowserUnavailableError(
|
|
187
|
+
`VOS_BROWSER_PATH failed: ${e.message}`
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
return await chromium.launch({ channel: "chrome" });
|
|
193
|
+
} catch {
|
|
194
|
+
try {
|
|
195
|
+
return await chromium.launch();
|
|
196
|
+
} catch (e) {
|
|
197
|
+
throw new BrowserUnavailableError(
|
|
198
|
+
e.message.split("\n")[0] ?? "unknown"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export {
|
|
205
|
+
UsageError,
|
|
206
|
+
parseArgs,
|
|
207
|
+
numFlag,
|
|
208
|
+
directoryKind,
|
|
209
|
+
loadProgramDirectory,
|
|
210
|
+
readSourceText,
|
|
211
|
+
loadVosConfig,
|
|
212
|
+
configDuration,
|
|
213
|
+
BrowserUnavailableError,
|
|
214
|
+
launchBrowser
|
|
215
|
+
};
|
|
216
|
+
//# sourceMappingURL=chunk-NHKHTIDR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/args.ts","../src/loadConfig.ts","../src/browser.ts"],"sourcesContent":["/**\n * Minimal argv parser — positionals + `--flag value` / `--flag=value` /\n * boolean flags. Deliberately tiny: the CLI has a small, stable surface and\n * agents benefit from predictable, dependency-free parsing.\n */\nexport interface ParsedArgs {\n positionals: string[]\n flags: Record<string, string | true>\n}\n\nexport class UsageError extends Error {}\n\nexport function parseArgs(\n argv: string[],\n booleanFlags: ReadonlySet<string>,\n): ParsedArgs {\n const positionals: string[] = []\n const flags: Record<string, string | true> = {}\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n if (arg === '--') {\n positionals.push(...argv.slice(i + 1))\n break\n }\n if (arg.startsWith('--')) {\n const eq = arg.indexOf('=')\n if (eq !== -1) {\n flags[arg.slice(2, eq)] = arg.slice(eq + 1)\n continue\n }\n const name = arg.slice(2)\n if (booleanFlags.has(name)) {\n flags[name] = true\n continue\n }\n const next = argv[i + 1]\n if (next === undefined || next.startsWith('--')) {\n throw new UsageError(`--${name} expects a value`)\n }\n flags[name] = next\n i++\n continue\n }\n positionals.push(arg)\n }\n return { positionals, flags }\n}\n\nexport function numFlag(\n flags: ParsedArgs['flags'],\n name: string,\n fallback: number,\n): number {\n const v = flags[name]\n if (v === undefined) return fallback\n const n = Number(v)\n if (!Number.isFinite(n))\n throw new UsageError(`--${name} expects a number, got \"${String(v)}\"`)\n return n\n}\n","import { readFile } from 'node:fs/promises'\nimport { existsSync, readFileSync, statSync } from 'node:fs'\nimport { join } from 'node:path'\nimport {\n CURRENT_CONFIG_VERSION,\n migrateConfig,\n vosConfigJsonSchema,\n} from '@vosjs/core'\nimport { UsageError } from './args'\n\nexport interface LoadedConfig {\n config: Record<string, unknown>\n warnings: string[]\n}\n\n/**\n * What a directory holds, by one deterministic sniff and never a flag:\n * a TAKE carries a recording document (`doc.json` with `source`); a PROGRAM\n * directory carries `config.json`, optionally beside a program document\n * (`doc.json` without `source`: the shared layers, the tween overlay, the\n * program's own length, with `program.config` omitted on disk). An unparsable\n * doc.json counts as a take so the take path reports the real error.\n */\nexport type DirectoryKind = 'take' | 'program' | 'none'\n\nexport function directoryKind(target: string): DirectoryKind {\n try {\n if (!statSync(target).isDirectory()) return 'none'\n } catch {\n return 'none'\n }\n const docPath = join(target, 'doc.json')\n if (existsSync(docPath)) {\n try {\n const doc: unknown = JSON.parse(readFileSync(docPath, 'utf8'))\n if (typeof doc === 'object' && doc !== null && 'source' in doc)\n return 'take'\n } catch {\n return 'take'\n }\n }\n return existsSync(join(target, 'config.json')) ? 'program' : 'none'\n}\n\n/**\n * A program directory's config: `config.json`, composed with the program\n * document beside it when there is one (the layers ride as the studio stack\n * entry, the tween overlay is baked into `createTimeline`), so a render of\n * the directory is a render of what the studio and vos.so play.\n */\nexport async function loadProgramDirectory(\n dir: string,\n warnings: string[],\n): Promise<unknown> {\n const config: unknown = JSON.parse(\n await readFile(join(dir, 'config.json'), 'utf8'),\n )\n const docPath = join(dir, 'doc.json')\n if (!existsSync(docPath)) return config\n const doc: unknown = JSON.parse(await readFile(docPath, 'utf8'))\n if (typeof doc !== 'object' || doc === null || 'source' in doc) return config\n if (typeof config !== 'object' || config === null) {\n throw new UsageError(`${dir}/config.json does not contain a JSON object`)\n }\n const raw = doc as Record<string, unknown>\n const program =\n raw.program && typeof raw.program === 'object'\n ? (raw.program as Record<string, unknown>)\n : {}\n const { lowerProgramDoc } = await import('@vosjs/studio-core')\n const composed = lowerProgramDoc(\n { ...raw, program: { ...program, config } } as never,\n { bake: true },\n ).config\n warnings.push(\n 'composed config.json with doc.json (a program document: its layers and tween edits ride the render)',\n )\n return composed\n}\n\n/**\n * The text of a config source: an http(s) URL or a file. A directory that is\n * neither a take nor a program directory is refused in words, since the raw\n * read would report a bare EISDIR.\n */\nexport async function readSourceText(source: string): Promise<string> {\n if (/^https?:\\/\\//.test(source)) {\n const res = await fetch(source)\n if (!res.ok) throw new Error(`fetch ${source} → ${res.status}`)\n return await res.text()\n }\n let isDir = false\n try {\n isDir = statSync(source).isDirectory()\n } catch {\n /* a missing path reads below and reports itself */\n }\n if (isDir) {\n throw new UsageError(\n `${source} is a directory with no config.json and no doc.json — pass a config file, a URL, a program directory or a take`,\n )\n }\n return await readFile(source, 'utf8')\n}\n\n/**\n * Load a VosConfigJson from a file path, an http(s) URL, or a program\n * directory (config.json, composed with a program document when one sits\n * beside it); unwrap API `{ config }` envelopes, migrate old versions, and\n * validate against the schema. A take directory is refused here in words:\n * takes render through the take pipeline (`vos render <take>`).\n */\nexport async function loadVosConfig(source: string): Promise<LoadedConfig> {\n const warnings: string[] = []\n let parsed: unknown\n const kind = /^https?:\\/\\//.test(source) ? 'none' : directoryKind(source)\n if (kind === 'take') {\n throw new UsageError(\n `${source} is a take (its doc.json is a recording document). Render it through the take pipeline: vos render ${source} [out]; vos frames ${source} for stills.`,\n )\n }\n if (kind === 'program') {\n parsed = await loadProgramDirectory(source, warnings)\n return finish(source, parsed, warnings)\n }\n const raw = await readSourceText(source)\n\n try {\n parsed = JSON.parse(raw)\n } catch (e) {\n throw new UsageError(`${source} is not valid JSON: ${(e as Error).message}`)\n }\n return finish(source, parsed, warnings)\n}\n\nfunction finish(\n source: string,\n parsed: unknown,\n warnings: string[],\n): LoadedConfig {\n if (typeof parsed !== 'object' || parsed === null) {\n throw new UsageError(`${source} does not contain a JSON object`)\n }\n\n // API endpoints wrap the config: { config: {...} }.\n let obj = parsed as Record<string, unknown>\n if (\n typeof obj.config === 'object' &&\n obj.config !== null &&\n !('createTimeline' in obj)\n ) {\n obj = obj.config as Record<string, unknown>\n warnings.push('unwrapped { config } envelope')\n }\n\n const version = obj.version\n const migrated = migrateConfig(obj)\n if (version === undefined) {\n // Plays fine (the stamp happens above), but a storer refuses it.\n warnings.push(\n `config declares no \"version\". Add \"version\": ${CURRENT_CONFIG_VERSION} before pushing it.`,\n )\n } else if (version !== CURRENT_CONFIG_VERSION) {\n warnings.push(\n `migrated config v${String(version)} to v${CURRENT_CONFIG_VERSION}`,\n )\n }\n\n const check = vosConfigJsonSchema.safeParse(migrated)\n if (!check.success) {\n const issues = check.error.issues\n .slice(0, 5)\n .map((i) => ` ${i.path.join('.') || '(root)'}: ${i.message}`)\n .join('\\n')\n throw new UsageError(`invalid vos config:\\n${issues}`)\n }\n\n return { config: migrated, warnings }\n}\n\n/** Best-effort duration from the config (seconds). */\nexport function configDuration(\n config: Record<string, unknown>,\n): number | undefined {\n const d = config.duration\n return typeof d === 'number' && Number.isFinite(d) && d > 0 ? d : undefined\n}\n","import { chromium } from 'playwright'\nimport type { Browser } from 'playwright'\n\nexport class BrowserUnavailableError extends Error {\n constructor(cause: string) {\n super(\n `Could not launch a Chromium-family browser (${cause}).\\n` +\n `Fixes, in order of preference:\\n` +\n ` 1. Install Google Chrome (used automatically), or\\n` +\n ` 2. npx playwright install chromium, or\\n` +\n ` 3. Set VOS_BROWSER_PATH to a Chrome/Chromium executable.`,\n )\n }\n}\n\n/**\n * Launch headless Chromium: explicit VOS_BROWSER_PATH → system Chrome\n * (no download needed) → Playwright's bundled Chromium.\n */\nexport async function launchBrowser(): Promise<Browser> {\n const explicit = process.env.VOS_BROWSER_PATH\n if (explicit) {\n return chromium.launch({ executablePath: explicit }).catch((e) => {\n throw new BrowserUnavailableError(\n `VOS_BROWSER_PATH failed: ${(e as Error).message}`,\n )\n })\n }\n try {\n return await chromium.launch({ channel: 'chrome' })\n } catch {\n try {\n return await chromium.launch()\n } catch (e) {\n throw new BrowserUnavailableError(\n (e as Error).message.split('\\n')[0] ?? 'unknown',\n )\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
BrowserUnavailableError,
|
|
4
|
+
directoryKind,
|
|
4
5
|
launchBrowser
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-NHKHTIDR.js";
|
|
6
7
|
|
|
7
8
|
// src/plugin/run.ts
|
|
8
9
|
import { mkdir as mkdir7, readFile as readFile9, rm as rm4 } from "fs/promises";
|
|
@@ -3352,6 +3353,27 @@ import { join as join8 } from "path";
|
|
|
3352
3353
|
|
|
3353
3354
|
// src/plugin/recordingCap.ts
|
|
3354
3355
|
var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
|
|
3356
|
+
function parseHostedCap(body) {
|
|
3357
|
+
const limits = body && typeof body === "object" ? body.limits : void 0;
|
|
3358
|
+
const cap = limits?.recordingMaxSeconds;
|
|
3359
|
+
return typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null;
|
|
3360
|
+
}
|
|
3361
|
+
async function hostedRecordingCap(origin, key, fetchImpl = fetch) {
|
|
3362
|
+
const ctl = new AbortController();
|
|
3363
|
+
const timer = setTimeout(() => ctl.abort(), 2e3);
|
|
3364
|
+
try {
|
|
3365
|
+
const res = await fetchImpl(`${origin.replace(/\/+$/, "")}/api/limits`, {
|
|
3366
|
+
signal: ctl.signal,
|
|
3367
|
+
...key ? { headers: { authorization: `Bearer ${key}` } } : {}
|
|
3368
|
+
});
|
|
3369
|
+
if (!res.ok) return null;
|
|
3370
|
+
return parseHostedCap(await res.json());
|
|
3371
|
+
} catch {
|
|
3372
|
+
return null;
|
|
3373
|
+
} finally {
|
|
3374
|
+
clearTimeout(timer);
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3355
3377
|
function defaultMaxDurationSeconds() {
|
|
3356
3378
|
return HOSTED_RECORDING_CAP_SECONDS;
|
|
3357
3379
|
}
|
|
@@ -4614,6 +4636,7 @@ async function pullTake(dir, flags, r) {
|
|
|
4614
4636
|
if (!vosId) {
|
|
4615
4637
|
throw new Error("this take was never pushed \u2014 pass --vos <id> to link it");
|
|
4616
4638
|
}
|
|
4639
|
+
const base = flags.since ?? state?.versionId ?? null;
|
|
4617
4640
|
const meta = await api(ctx, `/vos/${vosId}`);
|
|
4618
4641
|
if (meta.status !== 200) {
|
|
4619
4642
|
throw new Error(
|
|
@@ -4624,8 +4647,22 @@ async function pullTake(dir, flags, r) {
|
|
|
4624
4647
|
meta.json.vos?.currentVersionId ?? ""
|
|
4625
4648
|
);
|
|
4626
4649
|
if (!head) throw new Error("this vos has no versions");
|
|
4627
|
-
if (
|
|
4628
|
-
r.log(
|
|
4650
|
+
if (base === head) {
|
|
4651
|
+
r.log(
|
|
4652
|
+
flags.since ? "up to date \u2014 --since names the head" : "up to date \u2014 HEAD is your last push"
|
|
4653
|
+
);
|
|
4654
|
+
if (flags.check) {
|
|
4655
|
+
return {
|
|
4656
|
+
vosId,
|
|
4657
|
+
versionId: head,
|
|
4658
|
+
versionNumber: null,
|
|
4659
|
+
changed: false,
|
|
4660
|
+
checked: true,
|
|
4661
|
+
behind: 0,
|
|
4662
|
+
changes: [],
|
|
4663
|
+
protected: []
|
|
4664
|
+
};
|
|
4665
|
+
}
|
|
4629
4666
|
let media2;
|
|
4630
4667
|
if (flags.media && existsSync8(join12(dir, "doc.json"))) {
|
|
4631
4668
|
const local = JSON.parse(
|
|
@@ -4641,15 +4678,45 @@ async function pullTake(dir, flags, r) {
|
|
|
4641
4678
|
media: media2
|
|
4642
4679
|
};
|
|
4643
4680
|
}
|
|
4644
|
-
|
|
4681
|
+
let walked = null;
|
|
4682
|
+
if (base) {
|
|
4645
4683
|
const changes = await api(
|
|
4646
4684
|
ctx,
|
|
4647
|
-
`/vos/${vosId}/changes?since=${
|
|
4685
|
+
`/vos/${vosId}/changes?since=${encodeURIComponent(base)}`
|
|
4648
4686
|
);
|
|
4649
4687
|
if (changes.status === 200) {
|
|
4650
|
-
r.log(
|
|
4688
|
+
r.log(
|
|
4689
|
+
flags.since ? `changes since ${base.slice(0, 8)}\u2026:` : "changes since your last push:"
|
|
4690
|
+
);
|
|
4651
4691
|
printChanges(r, changes.json.changes, changes.json.protected);
|
|
4692
|
+
walked = {
|
|
4693
|
+
changes: Array.isArray(changes.json.changes) ? changes.json.changes : [],
|
|
4694
|
+
protected: Array.isArray(changes.json.protected) ? changes.json.protected : []
|
|
4695
|
+
};
|
|
4696
|
+
if (changes.json.truncated === true) {
|
|
4697
|
+
r.log("walk truncated \u2014 more versions exist; pull again after syncing");
|
|
4698
|
+
}
|
|
4699
|
+
} else {
|
|
4700
|
+
r.log(
|
|
4701
|
+
`changelog unavailable (${changes.status}) \u2014 the head is v?; pass --since <versionId> you can read, or pull to sync`
|
|
4702
|
+
);
|
|
4652
4703
|
}
|
|
4704
|
+
} else {
|
|
4705
|
+
r.log(
|
|
4706
|
+
"no base to compare from (this take was linked with --vos) \u2014 pass --since <versionId> for the changelog"
|
|
4707
|
+
);
|
|
4708
|
+
}
|
|
4709
|
+
if (flags.check) {
|
|
4710
|
+
return {
|
|
4711
|
+
vosId,
|
|
4712
|
+
versionId: head,
|
|
4713
|
+
versionNumber: null,
|
|
4714
|
+
changed: false,
|
|
4715
|
+
checked: true,
|
|
4716
|
+
behind: walked ? walked.changes.length : null,
|
|
4717
|
+
changes: walked?.changes ?? [],
|
|
4718
|
+
protected: walked?.protected ?? []
|
|
4719
|
+
};
|
|
4653
4720
|
}
|
|
4654
4721
|
const docRes = await api(ctx, `/vos/${vosId}/versions/${head}/doc`);
|
|
4655
4722
|
if (docRes.status !== 200) {
|
|
@@ -4681,7 +4748,7 @@ async function pullTake(dir, flags, r) {
|
|
|
4681
4748
|
}
|
|
4682
4749
|
|
|
4683
4750
|
// src/plugin/program.ts
|
|
4684
|
-
import { existsSync as existsSync9
|
|
4751
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4685
4752
|
import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
|
|
4686
4753
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
4687
4754
|
import { basename as basename2, dirname as dirname2, join as join13 } from "path";
|
|
@@ -5348,19 +5415,7 @@ async function storeKey(key, origin, r) {
|
|
|
5348
5415
|
return EXIT_OK;
|
|
5349
5416
|
}
|
|
5350
5417
|
function isTakeDir(target) {
|
|
5351
|
-
|
|
5352
|
-
if (!statSync2(target).isDirectory()) return false;
|
|
5353
|
-
const docPath = join13(target, "doc.json");
|
|
5354
|
-
if (!existsSync9(docPath)) return false;
|
|
5355
|
-
try {
|
|
5356
|
-
const doc = JSON.parse(readFileSync4(docPath, "utf8"));
|
|
5357
|
-
return typeof doc === "object" && doc !== null && "source" in doc;
|
|
5358
|
-
} catch {
|
|
5359
|
-
return true;
|
|
5360
|
-
}
|
|
5361
|
-
} catch {
|
|
5362
|
-
return false;
|
|
5363
|
-
}
|
|
5418
|
+
return directoryKind(target) === "take";
|
|
5364
5419
|
}
|
|
5365
5420
|
async function readProgramDoc(dir, config) {
|
|
5366
5421
|
const docPath = join13(dir, "doc.json");
|
|
@@ -5383,7 +5438,7 @@ async function writeProgramDoc(dir, hosted) {
|
|
|
5383
5438
|
}
|
|
5384
5439
|
|
|
5385
5440
|
// src/plugin/asset.ts
|
|
5386
|
-
import { readFileSync as
|
|
5441
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5387
5442
|
import { basename as basename3, extname as extname2 } from "path";
|
|
5388
5443
|
var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
5389
5444
|
var MIME_BY_EXT = {
|
|
@@ -5437,7 +5492,7 @@ async function cmdPush(argv) {
|
|
|
5437
5492
|
for (const file of positionals) {
|
|
5438
5493
|
const name = basename3(file);
|
|
5439
5494
|
const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
|
|
5440
|
-
const body =
|
|
5495
|
+
const body = readFileSync4(file);
|
|
5441
5496
|
const form = new FormData();
|
|
5442
5497
|
form.set("file", new Blob([body], { type: mime }), name);
|
|
5443
5498
|
if (folder) form.set("folderId", folder.id);
|
|
@@ -5491,7 +5546,7 @@ async function cmdAsset(argv) {
|
|
|
5491
5546
|
}
|
|
5492
5547
|
|
|
5493
5548
|
// src/plugin/recipe.ts
|
|
5494
|
-
import { readFileSync as
|
|
5549
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5495
5550
|
import { basename as basename4 } from "path";
|
|
5496
5551
|
var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
|
|
5497
5552
|
var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
|
|
@@ -5511,7 +5566,7 @@ async function cmdPush2(argv) {
|
|
|
5511
5566
|
throw new UsageError(USAGE);
|
|
5512
5567
|
}
|
|
5513
5568
|
const filename = recipeFilename(file);
|
|
5514
|
-
const body =
|
|
5569
|
+
const body = readFileSync5(file);
|
|
5515
5570
|
const r = createReporter(flags.json === true);
|
|
5516
5571
|
const origin = platformOrigin({
|
|
5517
5572
|
origin: strFlag(flags, "origin"),
|
|
@@ -6469,7 +6524,9 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
|
|
|
6469
6524
|
"no-frames",
|
|
6470
6525
|
"media",
|
|
6471
6526
|
"print",
|
|
6472
|
-
"yes"
|
|
6527
|
+
"yes",
|
|
6528
|
+
"composed",
|
|
6529
|
+
"check"
|
|
6473
6530
|
]);
|
|
6474
6531
|
var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
|
|
6475
6532
|
var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
|
|
@@ -6560,7 +6617,8 @@ actions.json so anchors survive script edits.
|
|
|
6560
6617
|
record --strict exits 2 when any selector was skipped, the page never
|
|
6561
6618
|
reached networkidle, or the take hit --max-duration (the done event lists
|
|
6562
6619
|
skipped[] and capped either way). --max-duration defaults to the hosted
|
|
6563
|
-
recording cap
|
|
6620
|
+
recording cap, read live from GET /api/limits (30 min when the origin is
|
|
6621
|
+
unreachable): the capture stops there and says so in one line.
|
|
6564
6622
|
frames captures PNG stills at OUTPUT times (seconds or % of duration);
|
|
6565
6623
|
--at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
|
|
6566
6624
|
the agent's eyes AND exact-size posters/screenshots (--size WxH). render
|
|
@@ -6656,10 +6714,25 @@ async function loadActions(file) {
|
|
|
6656
6714
|
${errors.join("\n ")}`);
|
|
6657
6715
|
return raw;
|
|
6658
6716
|
}
|
|
6659
|
-
function
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6717
|
+
async function maxDuration(flags, r) {
|
|
6718
|
+
if (hasFlag(flags, "max-duration")) {
|
|
6719
|
+
const v = numFlag(flags, "max-duration", defaultMaxDurationSeconds());
|
|
6720
|
+
if (!(v > 0)) throw new UsageError("--max-duration expects seconds above 0");
|
|
6721
|
+
return v;
|
|
6722
|
+
}
|
|
6723
|
+
const origin = platformOrigin({
|
|
6724
|
+
origin: strFlag(flags, "origin"),
|
|
6725
|
+
api: strFlag(flags, "api")
|
|
6726
|
+
});
|
|
6727
|
+
const live = await hostedRecordingCap(
|
|
6728
|
+
origin,
|
|
6729
|
+
resolveCredential(strFlag(flags, "key"))
|
|
6730
|
+
);
|
|
6731
|
+
if (live) return live;
|
|
6732
|
+
r.log(
|
|
6733
|
+
`note: could not read the hosted cap from ${origin}/api/limits; recording up to the built-in ${formatDurationCap(defaultMaxDurationSeconds())}`
|
|
6734
|
+
);
|
|
6735
|
+
return defaultMaxDurationSeconds();
|
|
6663
6736
|
}
|
|
6664
6737
|
function strictReason(rec) {
|
|
6665
6738
|
const parts = [`${rec.skipped.length} skipped step(s)`];
|
|
@@ -6692,6 +6765,7 @@ async function cmdRecord(argv) {
|
|
|
6692
6765
|
throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
|
|
6693
6766
|
const outDir = resolve6(strFlag(flags, "out") ?? "take");
|
|
6694
6767
|
const backdrop = await takeBackdrop(flags, r);
|
|
6768
|
+
const maxDurationSeconds = await maxDuration(flags, r);
|
|
6695
6769
|
if (existsSync10(join14(outDir, "meta.json"))) {
|
|
6696
6770
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
6697
6771
|
r.log(
|
|
@@ -6705,7 +6779,7 @@ async function cmdRecord(argv) {
|
|
|
6705
6779
|
r.log("recording\u2026");
|
|
6706
6780
|
r.event({ event: "phase", phase: "record" });
|
|
6707
6781
|
const rec = await recordTake(browser, url, actions, paths, r.log, {
|
|
6708
|
-
maxDurationSeconds
|
|
6782
|
+
maxDurationSeconds
|
|
6709
6783
|
});
|
|
6710
6784
|
r.event({ event: "phase", phase: "encode" });
|
|
6711
6785
|
r.log("encoding\u2026");
|
|
@@ -6774,6 +6848,7 @@ async function cmdCreate2(argv) {
|
|
|
6774
6848
|
throw new UsageError("--parallel expects an integer between 1 and 16");
|
|
6775
6849
|
}
|
|
6776
6850
|
const backdrop = await takeBackdrop(flags, r);
|
|
6851
|
+
const maxDurationSeconds = await maxDuration(flags, r);
|
|
6777
6852
|
if (existsSync10(join14(outDir, "meta.json"))) {
|
|
6778
6853
|
const { prevDoc, kept } = await prepareReRecord(outDir);
|
|
6779
6854
|
r.log(
|
|
@@ -6787,7 +6862,7 @@ async function cmdCreate2(argv) {
|
|
|
6787
6862
|
r.log("recording\u2026");
|
|
6788
6863
|
r.event({ event: "phase", phase: "record" });
|
|
6789
6864
|
const rec = await recordTake(browser, url, actions, paths, r.log, {
|
|
6790
|
-
maxDurationSeconds
|
|
6865
|
+
maxDurationSeconds
|
|
6791
6866
|
});
|
|
6792
6867
|
r.event({ event: "phase", phase: "encode" });
|
|
6793
6868
|
r.log("encoding\u2026");
|
|
@@ -7455,10 +7530,19 @@ async function cmdPull2(argv) {
|
|
|
7455
7530
|
api: strFlag(flags, "api"),
|
|
7456
7531
|
origin: strFlag(flags, "origin"),
|
|
7457
7532
|
vos: strFlag(flags, "vos"),
|
|
7458
|
-
media: flags.media === true
|
|
7533
|
+
media: flags.media === true,
|
|
7534
|
+
since: strFlag(flags, "since"),
|
|
7535
|
+
check: flags.check === true
|
|
7459
7536
|
},
|
|
7460
7537
|
r
|
|
7461
7538
|
);
|
|
7539
|
+
if (result.checked) {
|
|
7540
|
+
r.done(
|
|
7541
|
+
{ ...result },
|
|
7542
|
+
result.behind === null ? `${result.vosId}: head is ${result.versionId.slice(0, 8)}\u2026 \u2014 no base to count from; run without --check to sync` : result.behind === 0 ? `${result.vosId}: up to date` : `${result.behind} version${result.behind === 1 ? "" : "s"} behind \u2014 run without --check to sync`
|
|
7543
|
+
);
|
|
7544
|
+
return EXIT_OK;
|
|
7545
|
+
}
|
|
7462
7546
|
const mediaLine = result.media ? result.media.downloaded.length ? ` + ${result.media.downloaded.map((m) => m.file).join(", ")} (footage home \u2014 digest/frames/render run here now)` : result.media.kept.length ? ` (footage already here: ${result.media.kept.join(", ")})` : " (no hosted media on this doc)" : "";
|
|
7463
7547
|
r.done(
|
|
7464
7548
|
{ ...result },
|
|
@@ -7573,4 +7657,4 @@ export {
|
|
|
7573
7657
|
convertAgentBrowser,
|
|
7574
7658
|
run
|
|
7575
7659
|
};
|
|
7576
|
-
//# sourceMappingURL=chunk-
|
|
7660
|
+
//# sourceMappingURL=chunk-YICRU4ER.js.map
|