@vosjs/cli 0.51.0 → 0.52.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.
@@ -1,308 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/flagUse.ts
4
- var given = /* @__PURE__ */ new Set();
5
- var used = /* @__PURE__ */ new Set();
6
- function trackFlags(bag) {
7
- return new Proxy(bag, {
8
- get(target, key, receiver) {
9
- if (typeof key === "string") used.add(key);
10
- return Reflect.get(target, key, receiver);
11
- },
12
- has(target, key) {
13
- if (typeof key === "string") used.add(key);
14
- return Reflect.has(target, key);
15
- },
16
- // hasOwnProperty.call(flags, name) lands here.
17
- getOwnPropertyDescriptor(target, key) {
18
- if (typeof key === "string") used.add(key);
19
- return Reflect.getOwnPropertyDescriptor(target, key);
20
- },
21
- // A spread, Object.keys or JSON.stringify hands every flag onward; what
22
- // happens to them after that cannot be seen, so they all count as read.
23
- ownKeys(target) {
24
- for (const key of Reflect.ownKeys(target))
25
- if (typeof key === "string") used.add(key);
26
- return Reflect.ownKeys(target);
27
- }
28
- });
29
- }
30
- function noteGivenFlag(name) {
31
- given.add(name);
32
- }
33
- function unusedFlags() {
34
- return [...given].filter((name) => !used.has(name) && name !== "help");
35
- }
36
- function helpFlagsFor(help, verb) {
37
- const out = /* @__PURE__ */ new Set();
38
- let inVerb = false;
39
- for (const line of help.split("\n")) {
40
- const head = /^\s{2}vos\s+(\S+)/.exec(line);
41
- if (head) inVerb = head[1] === verb;
42
- else if (!/^\s{3,}\S/.test(line)) inVerb = false;
43
- if (!inVerb) continue;
44
- for (const m of line.matchAll(/--([a-z][a-z0-9-]*)/g)) out.add(m[1]);
45
- }
46
- return [...out];
47
- }
48
- function editDistance(a, b) {
49
- const row = Array.from({ length: b.length + 1 }, (_, i) => i);
50
- for (let i = 1; i <= a.length; i++) {
51
- let prev = row[0];
52
- row[0] = i;
53
- for (let j = 1; j <= b.length; j++) {
54
- const keep = row[j];
55
- row[j] = Math.min(
56
- row[j] + 1,
57
- row[j - 1] + 1,
58
- prev + (a[i - 1] === b[j - 1] ? 0 : 1)
59
- );
60
- prev = keep;
61
- }
62
- }
63
- return row[b.length];
64
- }
65
- function nearestFlag(name, candidates) {
66
- let best = null;
67
- let bestScore = Infinity;
68
- for (const c of candidates) {
69
- if (c === name) continue;
70
- const d = c.includes(name) || name.includes(c) ? 1 : editDistance(name, c);
71
- if (d < bestScore) {
72
- bestScore = d;
73
- best = c;
74
- }
75
- }
76
- return best !== null && bestScore <= Math.max(2, Math.floor(name.length / 3)) ? best : null;
77
- }
78
- function unusedFlagsMessage(verb, documented) {
79
- const unused = unusedFlags();
80
- if (!unused.length) return null;
81
- const parts = unused.map((name) => {
82
- const near = nearestFlag(name, documented);
83
- return `--${name}${near ? ` (did you mean --${near}?)` : ""}`;
84
- });
85
- const list = documented.length ? ` It reads: ${documented.map((f) => `--${f}`).join(" ")}.` : "";
86
- return `vos ${verb} RAN, and ignored ${parts.join(", ")}: ${unused.length > 1 ? "they are" : "it is"} not something this verb reads, so what it printed above was made without ${unused.length > 1 ? "them" : "it"}.${list}`;
87
- }
88
-
89
- // src/args.ts
90
- var UsageError = class extends Error {
91
- };
92
- function parseArgs(argv, booleanFlags) {
93
- const positionals = [];
94
- const flags = {};
95
- for (let i = 0; i < argv.length; i++) {
96
- const arg = argv[i];
97
- if (arg === "--") {
98
- positionals.push(...argv.slice(i + 1));
99
- break;
100
- }
101
- if (arg.startsWith("--")) {
102
- const eq = arg.indexOf("=");
103
- if (eq !== -1) {
104
- flags[arg.slice(2, eq)] = arg.slice(eq + 1);
105
- continue;
106
- }
107
- const name = arg.slice(2);
108
- if (booleanFlags.has(name)) {
109
- flags[name] = true;
110
- continue;
111
- }
112
- const next = argv[i + 1];
113
- if (next === void 0 || next.startsWith("--")) {
114
- throw new UsageError(`--${name} expects a value`);
115
- }
116
- flags[name] = next;
117
- i++;
118
- continue;
119
- }
120
- positionals.push(arg);
121
- }
122
- for (const name of Object.keys(flags)) noteGivenFlag(name);
123
- return { positionals, flags: trackFlags(flags) };
124
- }
125
- function numFlag(flags, name, fallback) {
126
- const v = flags[name];
127
- if (v === void 0) return fallback;
128
- const n = Number(v);
129
- if (!Number.isFinite(n))
130
- throw new UsageError(`--${name} expects a number, got "${String(v)}"`);
131
- return n;
132
- }
133
-
134
- // src/loadConfig.ts
135
- import { readFile } from "fs/promises";
136
- import { existsSync, readFileSync, statSync } from "fs";
137
- import { join } from "path";
138
- import {
139
- CURRENT_CONFIG_VERSION,
140
- migrateConfig,
141
- vosConfigJsonSchema
142
- } from "@vosjs/core";
143
- function directoryKind(target) {
144
- try {
145
- if (!statSync(target).isDirectory()) return "none";
146
- } catch {
147
- return "none";
148
- }
149
- const docPath = join(target, "doc.json");
150
- if (existsSync(docPath)) {
151
- try {
152
- const doc = JSON.parse(readFileSync(docPath, "utf8"));
153
- if (typeof doc === "object" && doc !== null && "source" in doc)
154
- return "take";
155
- } catch {
156
- return "take";
157
- }
158
- }
159
- return existsSync(join(target, "config.json")) ? "program" : "none";
160
- }
161
- async function loadProgramDirectory(dir, warnings) {
162
- const config = JSON.parse(
163
- await readFile(join(dir, "config.json"), "utf8")
164
- );
165
- const docPath = join(dir, "doc.json");
166
- if (!existsSync(docPath)) return config;
167
- const doc = JSON.parse(await readFile(docPath, "utf8"));
168
- if (typeof doc !== "object" || doc === null || "source" in doc) return config;
169
- if (typeof config !== "object" || config === null) {
170
- throw new UsageError(`${dir}/config.json does not contain a JSON object`);
171
- }
172
- const raw = doc;
173
- const program = raw.program && typeof raw.program === "object" ? raw.program : {};
174
- const { lowerProgramDoc } = await import("@vosjs/studio-core");
175
- const composed = lowerProgramDoc(
176
- { ...raw, program: { ...program, config } },
177
- { bake: true }
178
- ).config;
179
- warnings.push(
180
- "composed config.json with doc.json (a program document: its layers and tween edits ride the render)"
181
- );
182
- return composed;
183
- }
184
- async function readSourceText(source) {
185
- if (/^https?:\/\//.test(source)) {
186
- const res = await fetch(source);
187
- if (!res.ok) throw new Error(`fetch ${source} \u2192 ${res.status}`);
188
- return await res.text();
189
- }
190
- let isDir = false;
191
- try {
192
- isDir = statSync(source).isDirectory();
193
- } catch {
194
- }
195
- if (isDir) {
196
- throw new UsageError(
197
- `${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`
198
- );
199
- }
200
- return await readFile(source, "utf8");
201
- }
202
- async function loadVosConfig(source) {
203
- const warnings = [];
204
- let parsed;
205
- const kind = /^https?:\/\//.test(source) ? "none" : directoryKind(source);
206
- if (kind === "take") {
207
- throw new UsageError(
208
- `${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.`
209
- );
210
- }
211
- if (kind === "program") {
212
- parsed = await loadProgramDirectory(source, warnings);
213
- return finish(source, parsed, warnings);
214
- }
215
- const raw = await readSourceText(source);
216
- try {
217
- parsed = JSON.parse(raw);
218
- } catch (e) {
219
- throw new UsageError(`${source} is not valid JSON: ${e.message}`);
220
- }
221
- return finish(source, parsed, warnings);
222
- }
223
- function finish(source, parsed, warnings) {
224
- if (typeof parsed !== "object" || parsed === null) {
225
- throw new UsageError(`${source} does not contain a JSON object`);
226
- }
227
- let obj = parsed;
228
- if (typeof obj.config === "object" && obj.config !== null && !("createTimeline" in obj)) {
229
- obj = obj.config;
230
- warnings.push("unwrapped { config } envelope");
231
- }
232
- const version = obj.version;
233
- const migrated = migrateConfig(obj);
234
- if (version === void 0) {
235
- warnings.push(
236
- `config declares no "version". Add "version": ${CURRENT_CONFIG_VERSION} before pushing it.`
237
- );
238
- } else if (version !== CURRENT_CONFIG_VERSION) {
239
- warnings.push(
240
- `migrated config v${String(version)} to v${CURRENT_CONFIG_VERSION}`
241
- );
242
- }
243
- const check = vosConfigJsonSchema.safeParse(migrated);
244
- if (!check.success) {
245
- const issues = check.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
246
- throw new UsageError(`invalid vos config:
247
- ${issues}`);
248
- }
249
- return { config: migrated, warnings };
250
- }
251
- function configDuration(config) {
252
- const d = config.duration;
253
- return typeof d === "number" && Number.isFinite(d) && d > 0 ? d : void 0;
254
- }
255
-
256
- // src/browser.ts
257
- import { chromium } from "playwright";
258
- var BrowserUnavailableError = class extends Error {
259
- constructor(cause) {
260
- super(
261
- `Could not launch a Chromium-family browser (${cause}).
262
- Fixes, in order of preference:
263
- 1. Install Google Chrome (used automatically), or
264
- 2. npx playwright install chromium, or
265
- 3. Set VOS_BROWSER_PATH to a Chrome/Chromium executable.`
266
- );
267
- }
268
- };
269
- async function launchBrowser(args = []) {
270
- const opts = args.length ? { args } : {};
271
- const explicit = process.env.VOS_BROWSER_PATH;
272
- if (explicit) {
273
- return chromium.launch({ ...opts, executablePath: explicit }).catch((e) => {
274
- throw new BrowserUnavailableError(
275
- `VOS_BROWSER_PATH failed: ${e.message}`
276
- );
277
- });
278
- }
279
- try {
280
- return await chromium.launch({ ...opts, channel: "chrome" });
281
- } catch {
282
- try {
283
- return await chromium.launch(opts);
284
- } catch (e) {
285
- throw new BrowserUnavailableError(
286
- e.message.split("\n")[0] ?? "unknown"
287
- );
288
- }
289
- }
290
- }
291
-
292
- export {
293
- trackFlags,
294
- noteGivenFlag,
295
- helpFlagsFor,
296
- unusedFlagsMessage,
297
- UsageError,
298
- parseArgs,
299
- numFlag,
300
- directoryKind,
301
- loadProgramDirectory,
302
- readSourceText,
303
- loadVosConfig,
304
- configDuration,
305
- BrowserUnavailableError,
306
- launchBrowser
307
- };
308
- //# sourceMappingURL=chunk-FACYY7VV.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/flagUse.ts","../src/args.ts","../src/loadConfig.ts","../src/browser.ts"],"sourcesContent":["/**\n * Which of the flags a person gave did the verb actually READ?\n *\n * The parsers accept any `--name`, so a mistyped or misplaced flag used to be\n * swallowed: `vos frames <take> --at 3,5,8` wrote five evenly spread stills\n * and exited 0, because the flag is `--times` and nothing ever looked at\n * `at`. It looked like it worked.\n *\n * A declared list per verb would be the obvious guard and the wrong one here:\n * several verbs read their options through data (a name table, a helper three\n * calls down), so a list drifts, and the day it drifts it REFUSES a real flag\n * for every agent at once. Reading is the truth, so reading is what is\n * recorded: both parsers hand back their flags behind a proxy that notes each\n * name a verb asks for, and the CLI's one exit point compares that with what\n * was given. It cannot reject a flag a verb uses, by construction.\n *\n * The cost of being certain is that the verdict lands AFTER the verb ran, so\n * the message says so plainly: the command ran, without that flag.\n */\n\nconst given = new Set<string>()\nconst used = new Set<string>()\n\n/** A flag bag that remembers which names were asked for. */\nexport function trackFlags<T extends Record<string, unknown>>(bag: T): T {\n return new Proxy(bag, {\n get(target, key, receiver) {\n if (typeof key === 'string') used.add(key)\n return Reflect.get(target, key, receiver)\n },\n has(target, key) {\n if (typeof key === 'string') used.add(key)\n return Reflect.has(target, key)\n },\n // hasOwnProperty.call(flags, name) lands here.\n getOwnPropertyDescriptor(target, key) {\n if (typeof key === 'string') used.add(key)\n return Reflect.getOwnPropertyDescriptor(target, key)\n },\n // A spread, Object.keys or JSON.stringify hands every flag onward; what\n // happens to them after that cannot be seen, so they all count as read.\n ownKeys(target) {\n for (const key of Reflect.ownKeys(target))\n if (typeof key === 'string') used.add(key)\n return Reflect.ownKeys(target)\n },\n })\n}\n\nexport function noteGivenFlag(name: string): void {\n given.add(name)\n}\n\n/** Flags that were given and never read, in the order they were given. */\nexport function unusedFlags(): string[] {\n return [...given].filter((name) => !used.has(name) && name !== 'help')\n}\n\nexport function resetFlagUse(): void {\n given.clear()\n used.clear()\n}\n\n/**\n * The `--flags` a verb documents: its `vos <verb>` line in a help text plus\n * the indented lines that continue it.\n */\nexport function helpFlagsFor(help: string, verb: string): string[] {\n const out = new Set<string>()\n let inVerb = false\n for (const line of help.split('\\n')) {\n const head = /^\\s{2}vos\\s+(\\S+)/.exec(line)\n if (head) inVerb = head[1] === verb\n else if (!/^\\s{3,}\\S/.test(line)) inVerb = false\n if (!inVerb) continue\n for (const m of line.matchAll(/--([a-z][a-z0-9-]*)/g)) out.add(m[1])\n }\n return [...out]\n}\n\nfunction editDistance(a: string, b: string): number {\n const row = Array.from({ length: b.length + 1 }, (_, i) => i)\n for (let i = 1; i <= a.length; i++) {\n let prev = row[0]\n row[0] = i\n for (let j = 1; j <= b.length; j++) {\n const keep = row[j]\n row[j] = Math.min(\n row[j] + 1,\n row[j - 1] + 1,\n prev + (a[i - 1] === b[j - 1] ? 0 : 1),\n )\n prev = keep\n }\n }\n return row[b.length]\n}\n\n/** The documented flag a mistyped one most likely meant, or null. */\nexport function nearestFlag(name: string, candidates: string[]): string | null {\n let best: string | null = null\n let bestScore = Infinity\n for (const c of candidates) {\n if (c === name) continue\n // A candidate that contains the name (or the reverse) is as close as a\n // one-letter slip: `--zooms` for `--at-zooms`, `--body` for `--body-px`.\n const d = c.includes(name) || name.includes(c) ? 1 : editDistance(name, c)\n if (d < bestScore) {\n bestScore = d\n best = c\n }\n }\n return best !== null && bestScore <= Math.max(2, Math.floor(name.length / 3))\n ? best\n : null\n}\n\n/** The sentence for a run that ignored what it was given, or null when nothing was. */\nexport function unusedFlagsMessage(\n verb: string,\n documented: string[],\n): string | null {\n const unused = unusedFlags()\n if (!unused.length) return null\n const parts = unused.map((name) => {\n const near = nearestFlag(name, documented)\n return `--${name}${near ? ` (did you mean --${near}?)` : ''}`\n })\n const list = documented.length\n ? ` It reads: ${documented.map((f) => `--${f}`).join(' ')}.`\n : ''\n return `vos ${verb} RAN, and ignored ${parts.join(', ')}: ${unused.length > 1 ? 'they are' : 'it is'} not something this verb reads, so what it printed above was made without ${unused.length > 1 ? 'them' : 'it'}.${list}`\n}\n","/**\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 */\nimport { noteGivenFlag, trackFlags } from './flagUse'\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 for (const name of Object.keys(flags)) noteGivenFlag(name)\n return { positionals, flags: trackFlags(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 *\n * `args` are extra Chromium switches (`--browser-arg` on the verbs that\n * record). Some product surfaces cannot be reached without one: a recorder\n * needs a fake capture device to get past a permission prompt, an extension\n * page needs the extension loaded. Passed through verbatim.\n */\nexport async function launchBrowser(args: string[] = []): Promise<Browser> {\n const opts = args.length ? { args } : {}\n const explicit = process.env.VOS_BROWSER_PATH\n if (explicit) {\n return chromium.launch({ ...opts, 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({ ...opts, channel: 'chrome' })\n } catch {\n try {\n return await chromium.launch(opts)\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 +0,0 @@
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":[]}