@vosjs/cli 0.13.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";
@@ -4635,6 +4636,7 @@ async function pullTake(dir, flags, r) {
4635
4636
  if (!vosId) {
4636
4637
  throw new Error("this take was never pushed \u2014 pass --vos <id> to link it");
4637
4638
  }
4639
+ const base = flags.since ?? state?.versionId ?? null;
4638
4640
  const meta = await api(ctx, `/vos/${vosId}`);
4639
4641
  if (meta.status !== 200) {
4640
4642
  throw new Error(
@@ -4645,8 +4647,22 @@ async function pullTake(dir, flags, r) {
4645
4647
  meta.json.vos?.currentVersionId ?? ""
4646
4648
  );
4647
4649
  if (!head) throw new Error("this vos has no versions");
4648
- if (state && state.versionId === head) {
4649
- 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
+ }
4650
4666
  let media2;
4651
4667
  if (flags.media && existsSync8(join12(dir, "doc.json"))) {
4652
4668
  const local = JSON.parse(
@@ -4662,15 +4678,45 @@ async function pullTake(dir, flags, r) {
4662
4678
  media: media2
4663
4679
  };
4664
4680
  }
4665
- if (state?.versionId) {
4681
+ let walked = null;
4682
+ if (base) {
4666
4683
  const changes = await api(
4667
4684
  ctx,
4668
- `/vos/${vosId}/changes?since=${state.versionId}`
4685
+ `/vos/${vosId}/changes?since=${encodeURIComponent(base)}`
4669
4686
  );
4670
4687
  if (changes.status === 200) {
4671
- 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
+ );
4672
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
+ );
4673
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
+ };
4674
4720
  }
4675
4721
  const docRes = await api(ctx, `/vos/${vosId}/versions/${head}/doc`);
4676
4722
  if (docRes.status !== 200) {
@@ -4702,7 +4748,7 @@ async function pullTake(dir, flags, r) {
4702
4748
  }
4703
4749
 
4704
4750
  // src/plugin/program.ts
4705
- import { existsSync as existsSync9, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
4751
+ import { existsSync as existsSync9 } from "fs";
4706
4752
  import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
4707
4753
  import { createInterface as createInterface2 } from "readline/promises";
4708
4754
  import { basename as basename2, dirname as dirname2, join as join13 } from "path";
@@ -5369,19 +5415,7 @@ async function storeKey(key, origin, r) {
5369
5415
  return EXIT_OK;
5370
5416
  }
5371
5417
  function isTakeDir(target) {
5372
- try {
5373
- if (!statSync2(target).isDirectory()) return false;
5374
- const docPath = join13(target, "doc.json");
5375
- if (!existsSync9(docPath)) return false;
5376
- try {
5377
- const doc = JSON.parse(readFileSync4(docPath, "utf8"));
5378
- return typeof doc === "object" && doc !== null && "source" in doc;
5379
- } catch {
5380
- return true;
5381
- }
5382
- } catch {
5383
- return false;
5384
- }
5418
+ return directoryKind(target) === "take";
5385
5419
  }
5386
5420
  async function readProgramDoc(dir, config) {
5387
5421
  const docPath = join13(dir, "doc.json");
@@ -5404,7 +5438,7 @@ async function writeProgramDoc(dir, hosted) {
5404
5438
  }
5405
5439
 
5406
5440
  // src/plugin/asset.ts
5407
- import { readFileSync as readFileSync5 } from "fs";
5441
+ import { readFileSync as readFileSync4 } from "fs";
5408
5442
  import { basename as basename3, extname as extname2 } from "path";
5409
5443
  var BOOLEAN_FLAGS3 = /* @__PURE__ */ new Set(["json", "help"]);
5410
5444
  var MIME_BY_EXT = {
@@ -5458,7 +5492,7 @@ async function cmdPush(argv) {
5458
5492
  for (const file of positionals) {
5459
5493
  const name = basename3(file);
5460
5494
  const mime = MIME_BY_EXT[extname2(name).toLowerCase()] ?? "application/octet-stream";
5461
- const body = readFileSync5(file);
5495
+ const body = readFileSync4(file);
5462
5496
  const form = new FormData();
5463
5497
  form.set("file", new Blob([body], { type: mime }), name);
5464
5498
  if (folder) form.set("folderId", folder.id);
@@ -5512,7 +5546,7 @@ async function cmdAsset(argv) {
5512
5546
  }
5513
5547
 
5514
5548
  // src/plugin/recipe.ts
5515
- import { readFileSync as readFileSync6 } from "fs";
5549
+ import { readFileSync as readFileSync5 } from "fs";
5516
5550
  import { basename as basename4 } from "path";
5517
5551
  var BOOLEAN_FLAGS4 = /* @__PURE__ */ new Set(["json", "help"]);
5518
5552
  var USAGE = "vos recipe push <file.md> (--folder <folderId|slug> | --asset <assetId>)";
@@ -5532,7 +5566,7 @@ async function cmdPush2(argv) {
5532
5566
  throw new UsageError(USAGE);
5533
5567
  }
5534
5568
  const filename = recipeFilename(file);
5535
- const body = readFileSync6(file);
5569
+ const body = readFileSync5(file);
5536
5570
  const r = createReporter(flags.json === true);
5537
5571
  const origin = platformOrigin({
5538
5572
  origin: strFlag(flags, "origin"),
@@ -6490,7 +6524,9 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
6490
6524
  "no-frames",
6491
6525
  "media",
6492
6526
  "print",
6493
- "yes"
6527
+ "yes",
6528
+ "composed",
6529
+ "check"
6494
6530
  ]);
6495
6531
  var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
6496
6532
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
@@ -6581,7 +6617,8 @@ actions.json so anchors survive script edits.
6581
6617
  record --strict exits 2 when any selector was skipped, the page never
6582
6618
  reached networkidle, or the take hit --max-duration (the done event lists
6583
6619
  skipped[] and capped either way). --max-duration defaults to the hosted
6584
- 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.
6585
6622
  frames captures PNG stills at OUTPUT times (seconds or % of duration);
6586
6623
  --at-zooms adds every zoom span's apex, --at-moments every digest moment \u2014
6587
6624
  the agent's eyes AND exact-size posters/screenshots (--size WxH). render
@@ -7493,10 +7530,19 @@ async function cmdPull2(argv) {
7493
7530
  api: strFlag(flags, "api"),
7494
7531
  origin: strFlag(flags, "origin"),
7495
7532
  vos: strFlag(flags, "vos"),
7496
- media: flags.media === true
7533
+ media: flags.media === true,
7534
+ since: strFlag(flags, "since"),
7535
+ check: flags.check === true
7497
7536
  },
7498
7537
  r
7499
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
+ }
7500
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)" : "";
7501
7547
  r.done(
7502
7548
  { ...result },
@@ -7611,4 +7657,4 @@ export {
7611
7657
  convertAgentBrowser,
7612
7658
  run
7613
7659
  };
7614
- //# sourceMappingURL=chunk-6TFKMNSG.js.map
7660
+ //# sourceMappingURL=chunk-YICRU4ER.js.map