@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.
@@ -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":"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","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-A2VSDK3C.js";
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 (state && state.versionId === head) {
4628
- r.log("up to date \u2014 HEAD is your last push");
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
- if (state?.versionId) {
4681
+ let walked = null;
4682
+ if (base) {
4645
4683
  const changes = await api(
4646
4684
  ctx,
4647
- `/vos/${vosId}/changes?since=${state.versionId}`
4685
+ `/vos/${vosId}/changes?since=${encodeURIComponent(base)}`
4648
4686
  );
4649
4687
  if (changes.status === 200) {
4650
- r.log("changes since your last push:");
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, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
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
- try {
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 readFileSync5 } from "fs";
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 = readFileSync5(file);
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 readFileSync6 } from "fs";
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 = readFileSync6(file);
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 (1800s): the capture stops there and says so in one line.
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 maxDurationFlag(flags) {
6660
- const v = numFlag(flags, "max-duration", defaultMaxDurationSeconds());
6661
- if (!(v > 0)) throw new UsageError("--max-duration expects seconds above 0");
6662
- return v;
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: maxDurationFlag(flags)
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: maxDurationFlag(flags)
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-DEE2KCDO.js.map
7660
+ //# sourceMappingURL=chunk-YICRU4ER.js.map