@vosjs/cli 0.77.1 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +163 -21
  2. package/dist/{assetManifest-MQ7HO7RZ.js → assetManifest-U25FJER6.js} +3 -3
  3. package/dist/{chunk-UY2N42P2.js → chunk-4ZUHEXUX.js} +13 -4
  4. package/dist/chunk-4ZUHEXUX.js.map +1 -0
  5. package/dist/{chunk-B4U4URE3.js → chunk-J3KHIZCH.js} +466 -412
  6. package/dist/chunk-J3KHIZCH.js.map +1 -0
  7. package/dist/{chunk-2KEQ3LKH.js → chunk-OWYOJB7K.js} +22 -2
  8. package/dist/chunk-OWYOJB7K.js.map +1 -0
  9. package/dist/{chunk-IJZ3C435.js → chunk-TWAP4BKR.js} +4372 -587
  10. package/dist/chunk-TWAP4BKR.js.map +1 -0
  11. package/dist/{chunk-KDIRW72G.js → chunk-Z6TLHRFQ.js} +7 -4
  12. package/dist/chunk-Z6TLHRFQ.js.map +1 -0
  13. package/dist/cli.js +9 -8
  14. package/dist/cli.js.map +1 -1
  15. package/dist/index.d.ts +41 -29
  16. package/dist/index.js +5 -5
  17. package/dist/manifest-VTOIEUHJ.js +8 -0
  18. package/dist/{run-UNOXIGZ3.js → run-UKWKAGDK.js} +7 -5
  19. package/dist/{shotList-MAQVLZBY.js → shotList-EE3S5LVF.js} +52 -7
  20. package/dist/shotList-EE3S5LVF.js.map +1 -0
  21. package/package.json +4 -4
  22. package/schema/actions.schema.json +489 -12
  23. package/schema/doc.schema.json +63 -3
  24. package/skills/VERSION +1 -1
  25. package/skills/launch-kit/SKILL.md +6 -1
  26. package/skills/product-video/SKILL.md +7 -0
  27. package/skills/vos-authoring/SKILL.md +8 -1
  28. package/skills/vos-create/SKILL.md +5 -0
  29. package/skills/vos-cut/SKILL.md +8 -0
  30. package/skills/vos-footage/SKILL.md +4 -0
  31. package/skills/vos-migrate/SKILL.md +4 -0
  32. package/dist/chunk-2KEQ3LKH.js.map +0 -1
  33. package/dist/chunk-B4U4URE3.js.map +0 -1
  34. package/dist/chunk-IJZ3C435.js.map +0 -1
  35. package/dist/chunk-KDIRW72G.js.map +0 -1
  36. package/dist/chunk-UY2N42P2.js.map +0 -1
  37. package/dist/manifest-QLQKVSML.js +0 -8
  38. package/dist/shotList-MAQVLZBY.js.map +0 -1
  39. /package/dist/{assetManifest-MQ7HO7RZ.js.map → assetManifest-U25FJER6.js.map} +0 -0
  40. /package/dist/{manifest-QLQKVSML.js.map → manifest-VTOIEUHJ.js.map} +0 -0
  41. /package/dist/{run-UNOXIGZ3.js.map → run-UKWKAGDK.js.map} +0 -0
@@ -232,279 +232,9 @@ function containerMismatch(filename, head) {
232
232
  return `${filename} is ${actual}, not the ${claimed} its name claims \u2014 rename it to ${nameForType(filename, actual)}`;
233
233
  }
234
234
 
235
- // src/programAssets.ts
236
- import { createHash } from "crypto";
237
- import {
238
- closeSync,
239
- existsSync,
240
- mkdirSync,
241
- openSync,
242
- readSync,
243
- readdirSync,
244
- realpathSync,
245
- renameSync,
246
- rmSync,
247
- statSync,
248
- writeFileSync
249
- } from "fs";
250
- import { homedir } from "os";
251
- import { extname, join, resolve, sep } from "path";
252
- var ASSET_ROUTE = "/__assets/";
253
- var UUID = "[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}";
254
- var HOSTED_URI = new RegExp(`^asset:(${UUID})$`);
255
- var HOSTED_FILE = new RegExp(
256
- `^(https?://[^/?#]+)?/api/assets/(${UUID})/file(?:[?#].*)?$`
257
- );
258
- function hostedAssetId(ref, homeOrigins = []) {
259
- const uri = HOSTED_URI.exec(ref);
260
- if (uri) return uri[1];
261
- const file = HOSTED_FILE.exec(ref);
262
- if (!file) return null;
263
- return file[1] === void 0 || homeOrigins.includes(file[1]) ? file[2] : null;
264
- }
265
- var ASSET_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
266
- var SERVED_TYPES = {
267
- ".png": "image/png",
268
- ".jpg": "image/jpeg",
269
- ".jpeg": "image/jpeg",
270
- ".webp": "image/webp",
271
- ".gif": "image/gif",
272
- ".svg": "image/svg+xml",
273
- ".avif": "image/avif",
274
- ".mp4": "video/mp4",
275
- ".webm": "video/webm",
276
- ".mov": "video/quicktime",
277
- ".mp3": "audio/mpeg",
278
- ".ogg": "audio/ogg",
279
- ".wav": "audio/wav",
280
- ".m4a": "audio/mp4",
281
- ".glb": "model/gltf-binary",
282
- ".gltf": "model/gltf+json",
283
- ".woff2": "font/woff2",
284
- ".woff": "font/woff",
285
- ".ttf": "font/ttf",
286
- ".otf": "font/otf",
287
- ".json": "application/json"
288
- };
289
- function serveFile(file, range) {
290
- const size = statSync(file).size;
291
- const type = SERVED_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
292
- const base = { "content-type": type, "accept-ranges": "bytes" };
293
- const read = (start2, end2) => {
294
- const body = Buffer.alloc(Math.max(0, end2 - start2 + 1));
295
- const fd = openSync(file, "r");
296
- try {
297
- readSync(fd, body, 0, body.length, start2);
298
- } finally {
299
- closeSync(fd);
300
- }
301
- return body;
302
- };
303
- const m = range ? /^bytes=(\d*)-(\d*)$/.exec(range.trim()) : null;
304
- if (!m || m[1] === "" && m[2] === "") {
305
- return {
306
- status: 200,
307
- headers: { ...base, "content-length": String(size) },
308
- body: read(0, size - 1)
309
- };
310
- }
311
- const start = m[1] === "" ? Math.max(0, size - Number(m[2])) : Number(m[1]);
312
- const end = m[1] === "" || m[2] === "" ? size - 1 : Math.min(Number(m[2]), size - 1);
313
- if (start >= size || start > end) {
314
- return {
315
- status: 416,
316
- headers: { ...base, "content-range": `bytes */${size}` },
317
- body: Buffer.alloc(0)
318
- };
319
- }
320
- return {
321
- status: 206,
322
- headers: {
323
- ...base,
324
- "content-range": `bytes ${start}-${end}/${size}`,
325
- "content-length": String(end - start + 1)
326
- },
327
- body: read(start, end)
328
- };
329
- }
330
- function localFile(ref, baseDir) {
331
- if (baseDir === null) {
332
- return {
333
- refused: "a config loaded from a URL has no directory, so it reads no local file"
334
- };
335
- }
336
- const file = resolve(baseDir, ref);
337
- if (!existsSync(file) || !statSync(file).isFile()) {
338
- return { refused: "which is not a file beside the config" };
339
- }
340
- const root = realpathSync(baseDir);
341
- const real = realpathSync(file);
342
- if (!real.startsWith(root + sep)) {
343
- return {
344
- refused: `which is outside the program's directory (${root}). A program reads only files in its own directory or below: copy the file there`
345
- };
346
- }
347
- return { file: real };
348
- }
349
- function isUrl(ref) {
350
- return /^(https?:|data:|blob:)/.test(ref);
351
- }
352
- function manifestOf(config) {
353
- const assets = config.assets;
354
- if (!assets || typeof assets !== "object" || Array.isArray(assets))
355
- return null;
356
- return Object.keys(assets).length > 0 ? assets : null;
357
- }
358
- function manifestRefs(config) {
359
- const out = [];
360
- for (const [name, decl] of Object.entries(manifestOf(config) ?? {})) {
361
- if (!ASSET_NAME.test(name) || !decl || typeof decl !== "object") continue;
362
- if (Array.isArray(decl.ref)) {
363
- decl.ref.forEach((ref, index) => {
364
- if (typeof ref === "string") out.push({ name, index, ref });
365
- });
366
- } else if (typeof decl.ref === "string") {
367
- out.push({ name, index: null, ref: decl.ref });
368
- }
369
- }
370
- return out;
371
- }
372
- async function fetchHosted(id, opts) {
373
- const dir = opts.cacheDir ?? join(homedir(), ".cache", "vos", "assets");
374
- const cached = existsSync(dir) ? readdirSync(dir).find(
375
- (f) => (f === id || f.startsWith(`${id}.`)) && !f.endsWith(".part")
376
- ) : void 0;
377
- if (cached) return join(dir, cached);
378
- if (!opts.origin || !opts.key) {
379
- opts.log(
380
- `note: asset ${id} is hosted and no credential is set, so the render cannot load it (vos login, or keep the file beside the config and name its path)`
381
- );
382
- return null;
383
- }
384
- const res = await fetch(`${opts.origin}/api/assets/${id}/file`, {
385
- headers: { authorization: `Bearer ${opts.key}` }
386
- });
387
- if (!res.ok) {
388
- opts.log(
389
- `note: asset ${id} could not be read (${res.status}), so the render cannot load it`
390
- );
391
- return null;
392
- }
393
- mkdirSync(dir, { recursive: true, mode: 448 });
394
- const type = (res.headers.get("content-type") ?? "").split(";")[0].trim();
395
- const file = join(dir, `${id}${extensionFor(type)}`);
396
- const partial = `${file}.part`;
397
- try {
398
- writeFileSync(partial, new Uint8Array(await res.arrayBuffer()), {
399
- mode: 384
400
- });
401
- renameSync(partial, file);
402
- } finally {
403
- rmSync(partial, { force: true });
404
- }
405
- return file;
406
- }
407
- function htmlFaceRefs(config) {
408
- const out = [];
409
- const stack = Array.isArray(config.stack) ? config.stack : [];
410
- for (const entry of stack) {
411
- const data = entry?.data;
412
- const overlays = Array.isArray(data?.overlays) ? data.overlays : [];
413
- for (const clip of overlays) {
414
- const html = clip?.html;
415
- const faces = Array.isArray(html?.faces) ? html.faces : [];
416
- for (const face of faces) {
417
- if (typeof face?.url !== "string" || !face.url) continue;
418
- out.push({
419
- where: `overlay ${String(clip.id)} font ${String(face.family)}`,
420
- url: face.url,
421
- set: (next) => {
422
- face.url = next;
423
- },
424
- drop: () => {
425
- const at = faces.indexOf(face);
426
- if (at >= 0) faces.splice(at, 1);
427
- }
428
- });
429
- }
430
- }
431
- }
432
- return out;
433
- }
434
- var FACE_HOST = /^https:\/\/assets\.vos\.so\//;
435
- async function programAssets(config, opts) {
436
- const refs = manifestRefs(config);
437
- const faces = [];
438
- for (const f of htmlFaceRefs(config)) {
439
- const hosted = hostedAssetId(f.url, opts.origin ? [opts.origin] : []);
440
- if (!isUrl(f.url) || hosted) {
441
- faces.push(f);
442
- } else if (!FACE_HOST.test(f.url)) {
443
- opts.log(
444
- `note: ${f.where} names ${f.url}, which is not on https://assets.vos.so, so the render does not fetch it (a face comes from the hosted catalog, a hosted file, or a .woff2 beside the document)`
445
- );
446
- f.drop();
447
- }
448
- }
449
- if (refs.length === 0 && faces.length === 0) return null;
450
- const files = {};
451
- const served = /* @__PURE__ */ new Map();
452
- const serve = (file) => {
453
- const known = served.get(file);
454
- if (known) return known;
455
- const tag = createHash("sha256").update(file).digest("hex").slice(0, 16);
456
- const ext = extname(file);
457
- const path = `${ASSET_ROUTE}${tag}${/^\.[A-Za-z0-9]+$/.test(ext) ? ext.toLowerCase() : ""}`;
458
- files[path] = file;
459
- served.set(file, path);
460
- return path;
461
- };
462
- const reach = async (ref, where) => {
463
- const hosted = hostedAssetId(ref, opts.origin ? [opts.origin] : []);
464
- if (hosted) {
465
- const file = await fetchHosted(hosted, opts);
466
- return file ? serve(file) : ref;
467
- }
468
- if (isUrl(ref)) return ref;
469
- const local = localFile(ref, opts.baseDir);
470
- if ("refused" in local) {
471
- opts.log(
472
- `note: ${where} names ${ref}, ${local.refused}, so the render cannot load it`
473
- );
474
- return ref;
475
- }
476
- return serve(local.file);
477
- };
478
- for (const face of faces) face.set(await reach(face.url, face.where));
479
- const assets = {};
480
- for (const { name, index, ref } of refs) {
481
- const url = await reach(
482
- ref,
483
- index === null ? `assets.${name}` : `assets.${name}[${index}]`
484
- );
485
- if (index === null) assets[name] = url;
486
- else {
487
- const list = assets[name] ??= [];
488
- list[index] = url;
489
- }
490
- }
491
- return { assets, files };
492
- }
493
- function missingManifestFiles(config, baseDir) {
494
- const out = [];
495
- for (const { name, index, ref } of manifestRefs(config)) {
496
- if (isUrl(ref) || hostedAssetId(ref)) continue;
497
- const local = localFile(ref, baseDir);
498
- if ("file" in local) continue;
499
- const where = index === null ? `assets.${name}` : `assets.${name}[${index}]`;
500
- out.push(`${where} names ${ref}, ${local.refused}`);
501
- }
502
- return out;
503
- }
504
-
505
235
  // src/plugin/safeWrite.ts
506
236
  import { lstat, writeFile } from "fs/promises";
507
- import { lstatSync, writeFileSync as writeFileSync2 } from "fs";
237
+ import { lstatSync, writeFileSync } from "fs";
508
238
  async function writeFileSafe(path, data) {
509
239
  await refuseSymlink(path);
510
240
  await writeFile(path, data);
@@ -524,7 +254,7 @@ function writeFileSafeSync(path, data) {
524
254
  } catch {
525
255
  }
526
256
  if (link) throw new Error(symlinkRefusal(path));
527
- writeFileSync2(path, data);
257
+ writeFileSync(path, data);
528
258
  }
529
259
  function symlinkRefusal(path) {
530
260
  return `${path} is a symbolic link, and the CLI writes only a regular file there: remove the link and run again`;
@@ -586,12 +316,12 @@ function hasFlag(flags, name) {
586
316
  }
587
317
 
588
318
  // src/plugin/platform.ts
589
- import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync3 } from "fs";
590
- import { homedir as homedir2 } from "os";
591
- import { join as join2 } from "path";
319
+ import { mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
320
+ import { homedir } from "os";
321
+ import { dirname, join } from "path";
592
322
 
593
323
  // src/plugin/hostAgent.ts
594
- import { existsSync as existsSync2 } from "fs";
324
+ import { existsSync } from "fs";
595
325
  var SLUG = /^[a-z0-9][a-z0-9.-]{0,39}$/;
596
326
  var MARKERS = [
597
327
  ["cursor", (e) => !!e.CURSOR_TRACE_ID],
@@ -621,7 +351,7 @@ function slugFromAiAgent(value) {
621
351
  if (head === "github-copilot-cli") return "github-copilot";
622
352
  return SLUG.test(head) ? head : null;
623
353
  }
624
- function detectHostAgent(env = process.env, exists = existsSync2) {
354
+ function detectHostAgent(env = process.env, exists = existsSync) {
625
355
  if (env.AI_AGENT) {
626
356
  const slug = slugFromAiAgent(env.AI_AGENT);
627
357
  if (slug) return slug;
@@ -667,157 +397,479 @@ function parseVosId(input) {
667
397
  function deriveSlug(title) {
668
398
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
669
399
  }
670
- var CREDENTIALS_PATH = join2(homedir2(), ".config", "vos", "credentials");
671
- function resolveCredential(explicit) {
400
+ var CREDENTIALS_PATH = join(homedir(), ".config", "vos", "credentials");
401
+ var DEFAULT_ORIGIN = "https://vos.so";
402
+ function originOf(url) {
403
+ try {
404
+ return new URL(url).origin;
405
+ } catch {
406
+ return url.replace(/\/+$/, "");
407
+ }
408
+ }
409
+ function readStoredCredential(path = CREDENTIALS_PATH) {
410
+ let text;
411
+ try {
412
+ text = readFileSync(path, "utf8");
413
+ } catch {
414
+ return null;
415
+ }
416
+ const [first, ...rest] = text.split("\n");
417
+ const key = first.trim();
418
+ if (!key) return null;
419
+ const line = rest.map((l) => l.trim()).find((l) => l.startsWith("origin="));
420
+ const origin = line ? originOf(line.slice("origin=".length).trim()) : "";
421
+ return { key, origin: origin || DEFAULT_ORIGIN };
422
+ }
423
+ var storedFor = /* @__PURE__ */ new Map();
424
+ var warned = /* @__PURE__ */ new Set();
425
+ function resolveCredential(explicit, path = CREDENTIALS_PATH) {
672
426
  const flag = explicit?.trim();
673
427
  if (flag) return flag;
674
428
  const env = process.env.VOS_API_KEY?.trim();
675
429
  if (env) return env;
430
+ const stored = readStoredCredential(path);
431
+ if (!stored) return null;
432
+ storedFor.set(stored.key, stored.origin);
433
+ return stored.key;
434
+ }
435
+ function bearerFor(key, url) {
436
+ if (!key) return {};
437
+ const bound = storedFor.get(key);
438
+ const to = originOf(url);
439
+ if (bound && bound !== to) {
440
+ if (!warned.has(to)) {
441
+ warned.add(to);
442
+ process.stderr.write(
443
+ `note: the credential vos login stored is for ${bound}, so it is not sent to ${to}; for ${to}, run vos login --origin ${to}, or pass --key or VOS_API_KEY
444
+ `
445
+ );
446
+ }
447
+ return {};
448
+ }
449
+ return { authorization: `Bearer ${key}` };
450
+ }
451
+ var mintAt = (origin = DEFAULT_ORIGIN) => `${originOf(origin)}/app/api`;
452
+ function requireCredential(explicit, origin) {
453
+ const key = resolveCredential(explicit);
454
+ if (!key) {
455
+ throw new Error(
456
+ `no credential found \u2014 pass --key, set VOS_API_KEY, or run \`vos login\` (mint a content key at ${mintAt(origin ?? platformOrigin())}; a vos_rg_ remix grant works too)`
457
+ );
458
+ }
459
+ return key;
460
+ }
461
+ function writeCredential(key, origin = DEFAULT_ORIGIN, path = CREDENTIALS_PATH) {
462
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
463
+ writeFileSync2(path, `${key.trim()}
464
+ origin=${originOf(origin)}
465
+ `, {
466
+ mode: 384
467
+ });
468
+ return path;
469
+ }
470
+ function clientId(env = process.env, exists) {
471
+ const explicit = env.VOS_CLIENT?.trim();
472
+ if (explicit && explicit.length <= 60 && !/[\r\n]/.test(explicit))
473
+ return explicit;
474
+ const host = detectHostAgent(env, exists);
475
+ return host ? `${host} vos-cli` : "vos-cli";
476
+ }
477
+ function unreachable(origin, e) {
478
+ const err = e;
479
+ const why = [err?.message, err?.cause?.code].filter(Boolean).join(": ");
480
+ return `could not reach ${origin}${why ? ` (${why})` : ""}; is it the origin you meant? (--origin, or VOS_ORIGIN)`;
481
+ }
482
+ async function apiJson(origin, path, init = {}) {
483
+ const headers = {
484
+ accept: "application/json",
485
+ ...init.headers
486
+ };
487
+ Object.assign(headers, bearerFor(init.key, origin));
488
+ if (init.body !== void 0) headers["content-type"] = "application/json";
489
+ const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
490
+ let res;
491
+ try {
492
+ res = await fetch(`${origin}${path}`, {
493
+ method: init.method ?? "GET",
494
+ headers,
495
+ body: reqBody
496
+ });
497
+ } catch (e) {
498
+ throw new Error(unreachable(origin, e));
499
+ }
500
+ let body = {};
501
+ try {
502
+ body = await res.json();
503
+ } catch {
504
+ }
505
+ return { status: res.status, body, origin };
506
+ }
507
+ function detailLine(body) {
508
+ const details = body.details;
509
+ if (!Array.isArray(details) || !details.length) return "";
510
+ const lines = details.map((d) => {
511
+ const path = Array.isArray(d.path) ? d.path.join(".") : "";
512
+ return `${path ? `${path}: ` : ""}${String(d.message ?? JSON.stringify(d))}`;
513
+ });
514
+ return `
515
+ ${lines.join("\n ")}`;
516
+ }
517
+ function apiError(what, r) {
518
+ const detail = typeof r.body.error === "string" ? r.body.error + detailLine(r.body) : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
519
+ const hint = r.status === 401 ? ` (run \`vos login\`, or set VOS_API_KEY \u2014 mint a key at ${mintAt(r.origin)})` : "";
520
+ return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
521
+ }
522
+ function promptHint(target, vosId) {
523
+ return `vos push ${target} --vos ${vosId} --prompt "<the person's prompt, as they wrote it>"`;
524
+ }
525
+ var SYNC_STATE_NAME = "vos.json";
526
+ function hostedMediaOf(value) {
527
+ if (!value || typeof value !== "object") return void 0;
528
+ const out = {};
529
+ for (const [file, entry] of Object.entries(value)) {
530
+ const e = entry;
531
+ if (e && typeof e.ref === "string" && typeof e.sha256 === "string")
532
+ out[file] = { ref: e.ref, sha256: e.sha256 };
533
+ }
534
+ return Object.keys(out).length ? out : void 0;
535
+ }
536
+ function readJsonFile(file) {
676
537
  try {
677
- const first = readFileSync(CREDENTIALS_PATH, "utf8").split("\n")[0].trim();
678
- return first || null;
538
+ return JSON.parse(readFileSync(file, "utf8"));
679
539
  } catch {
680
540
  return null;
681
541
  }
682
- }
683
- function requireCredential(explicit) {
684
- const key = resolveCredential(explicit);
685
- if (!key) {
686
- throw new Error(
687
- "no credential found \u2014 pass --key, set VOS_API_KEY, or run `vos login` (mint a content key at https://vos.so/app/api; a vos_rg_ remix grant works too)"
688
- );
542
+ }
543
+ function readSyncState(dir) {
544
+ const own = readJsonFile(join(dir, SYNC_STATE_NAME));
545
+ if (own && typeof own.vosId === "string") {
546
+ return {
547
+ vosId: own.vosId,
548
+ versionId: typeof own.versionId === "string" ? own.versionId : null,
549
+ ...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
550
+ ...typeof own.title === "string" ? { title: own.title } : {},
551
+ ...typeof own.slug === "string" ? { slug: own.slug } : {},
552
+ ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {},
553
+ ...hostedMediaOf(own.hostedMedia) ? { hostedMedia: hostedMediaOf(own.hostedMedia) } : {}
554
+ };
555
+ }
556
+ const push = readJsonFile(join(dir, "push.json"));
557
+ if (push && typeof push.vosId === "string") {
558
+ return {
559
+ vosId: push.vosId,
560
+ versionId: typeof push.versionId === "string" ? push.versionId : null,
561
+ ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
562
+ };
563
+ }
564
+ const meta = readJsonFile(join(dir, "meta.json"));
565
+ if (meta && typeof meta.id === "string") {
566
+ return {
567
+ vosId: meta.id,
568
+ versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
569
+ ...typeof meta.title === "string" ? { title: meta.title } : {},
570
+ ...typeof meta.slug === "string" ? { slug: meta.slug } : {},
571
+ ...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
572
+ };
573
+ }
574
+ return null;
575
+ }
576
+ function writeSyncState(dir, patch) {
577
+ const merged = {
578
+ ...readSyncState(dir) ?? { versionId: null },
579
+ ...patch
580
+ };
581
+ writeFileSafeSync(
582
+ join(dir, SYNC_STATE_NAME),
583
+ `${JSON.stringify(merged, null, 2)}
584
+ `
585
+ );
586
+ return merged;
587
+ }
588
+ function formatChanges(changes) {
589
+ const lines = [];
590
+ for (const ch of changes) {
591
+ const label = ch.label ? ` \xB7 ${ch.label}` : "";
592
+ const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
593
+ lines.push(
594
+ `v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
595
+ );
596
+ if (ch.note) lines.push(` note: ${ch.note}`);
597
+ }
598
+ return lines;
599
+ }
600
+ function serverWarnings(body, log) {
601
+ const warnings = Array.isArray(body.warnings) ? body.warnings : [];
602
+ for (const w of warnings) if (typeof w === "string") log(`warning ${w}`);
603
+ }
604
+
605
+ // src/programAssets.ts
606
+ import { createHash } from "crypto";
607
+ import {
608
+ closeSync,
609
+ existsSync as existsSync2,
610
+ mkdirSync as mkdirSync2,
611
+ openSync,
612
+ readSync,
613
+ readdirSync,
614
+ realpathSync,
615
+ renameSync,
616
+ rmSync,
617
+ statSync,
618
+ writeFileSync as writeFileSync3
619
+ } from "fs";
620
+ import { homedir as homedir2 } from "os";
621
+ import { extname, join as join2, resolve, sep } from "path";
622
+ var ASSET_ROUTE = "/__assets/";
623
+ var UUID = "[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}";
624
+ var HOSTED_URI = new RegExp(`^asset:(${UUID})$`);
625
+ var HOSTED_FILE = new RegExp(
626
+ `^(https?://[^/?#]+)?/api/assets/(${UUID})/file(?:[?#].*)?$`
627
+ );
628
+ function hostedAssetId(ref, homeOrigins = []) {
629
+ const uri = HOSTED_URI.exec(ref);
630
+ if (uri) return uri[1];
631
+ const file = HOSTED_FILE.exec(ref);
632
+ if (!file) return null;
633
+ return file[1] === void 0 || homeOrigins.includes(file[1]) ? file[2] : null;
634
+ }
635
+ var ASSET_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
636
+ var SERVED_TYPES = {
637
+ ".png": "image/png",
638
+ ".jpg": "image/jpeg",
639
+ ".jpeg": "image/jpeg",
640
+ ".webp": "image/webp",
641
+ ".gif": "image/gif",
642
+ ".svg": "image/svg+xml",
643
+ ".avif": "image/avif",
644
+ ".mp4": "video/mp4",
645
+ ".webm": "video/webm",
646
+ ".mov": "video/quicktime",
647
+ ".mp3": "audio/mpeg",
648
+ ".ogg": "audio/ogg",
649
+ ".wav": "audio/wav",
650
+ ".m4a": "audio/mp4",
651
+ ".glb": "model/gltf-binary",
652
+ ".gltf": "model/gltf+json",
653
+ ".woff2": "font/woff2",
654
+ ".woff": "font/woff",
655
+ ".ttf": "font/ttf",
656
+ ".otf": "font/otf",
657
+ ".json": "application/json"
658
+ };
659
+ function serveFile(file, range) {
660
+ const size = statSync(file).size;
661
+ const type = SERVED_TYPES[extname(file).toLowerCase()] ?? "application/octet-stream";
662
+ const base = { "content-type": type, "accept-ranges": "bytes" };
663
+ const read = (start2, end2) => {
664
+ const body = Buffer.alloc(Math.max(0, end2 - start2 + 1));
665
+ const fd = openSync(file, "r");
666
+ try {
667
+ readSync(fd, body, 0, body.length, start2);
668
+ } finally {
669
+ closeSync(fd);
670
+ }
671
+ return body;
672
+ };
673
+ const m = range ? /^bytes=(\d*)-(\d*)$/.exec(range.trim()) : null;
674
+ if (!m || m[1] === "" && m[2] === "") {
675
+ return {
676
+ status: 200,
677
+ headers: { ...base, "content-length": String(size) },
678
+ body: read(0, size - 1)
679
+ };
680
+ }
681
+ const start = m[1] === "" ? Math.max(0, size - Number(m[2])) : Number(m[1]);
682
+ const end = m[1] === "" || m[2] === "" ? size - 1 : Math.min(Number(m[2]), size - 1);
683
+ if (start >= size || start > end) {
684
+ return {
685
+ status: 416,
686
+ headers: { ...base, "content-range": `bytes */${size}` },
687
+ body: Buffer.alloc(0)
688
+ };
689
+ }
690
+ return {
691
+ status: 206,
692
+ headers: {
693
+ ...base,
694
+ "content-range": `bytes ${start}-${end}/${size}`,
695
+ "content-length": String(end - start + 1)
696
+ },
697
+ body: read(start, end)
698
+ };
699
+ }
700
+ function localFile(ref, baseDir) {
701
+ if (baseDir === null) {
702
+ return {
703
+ refused: "a config loaded from a URL has no directory, so it reads no local file"
704
+ };
705
+ }
706
+ const file = resolve(baseDir, ref);
707
+ if (!existsSync2(file) || !statSync(file).isFile()) {
708
+ return { refused: "which is not a file beside the config" };
709
+ }
710
+ const root = realpathSync(baseDir);
711
+ const real = realpathSync(file);
712
+ if (!real.startsWith(root + sep)) {
713
+ return {
714
+ refused: `which is outside the program's directory (${root}). A program reads only files in its own directory or below: copy the file there`
715
+ };
689
716
  }
690
- return key;
717
+ return { file: real };
691
718
  }
692
- function writeCredential(key) {
693
- mkdirSync2(join2(homedir2(), ".config", "vos"), { recursive: true, mode: 448 });
694
- writeFileSync3(CREDENTIALS_PATH, `${key.trim()}
695
- `, { mode: 384 });
696
- return CREDENTIALS_PATH;
719
+ function isUrl(ref) {
720
+ return /^(https?:|data:|blob:)/.test(ref);
697
721
  }
698
- function clientId(env = process.env, exists) {
699
- const explicit = env.VOS_CLIENT?.trim();
700
- if (explicit && explicit.length <= 60 && !/[\r\n]/.test(explicit))
701
- return explicit;
702
- const host = detectHostAgent(env, exists);
703
- return host ? `${host} vos-cli` : "vos-cli";
722
+ function manifestOf(config) {
723
+ const assets = config.assets;
724
+ if (!assets || typeof assets !== "object" || Array.isArray(assets))
725
+ return null;
726
+ return Object.keys(assets).length > 0 ? assets : null;
704
727
  }
705
- async function apiJson(origin, path, init = {}) {
706
- const headers = {
707
- accept: "application/json",
708
- ...init.headers
709
- };
710
- if (init.key) headers.authorization = `Bearer ${init.key}`;
711
- if (init.body !== void 0) headers["content-type"] = "application/json";
712
- const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
713
- const res = await fetch(`${origin}${path}`, {
714
- method: init.method ?? "GET",
715
- headers,
716
- body: reqBody
717
- });
718
- let body = {};
719
- try {
720
- body = await res.json();
721
- } catch {
728
+ function manifestRefs(config) {
729
+ const out = [];
730
+ for (const [name, decl] of Object.entries(manifestOf(config) ?? {})) {
731
+ if (!ASSET_NAME.test(name) || !decl || typeof decl !== "object") continue;
732
+ if (Array.isArray(decl.ref)) {
733
+ decl.ref.forEach((ref, index) => {
734
+ if (typeof ref === "string") out.push({ name, index, ref });
735
+ });
736
+ } else if (typeof decl.ref === "string") {
737
+ out.push({ name, index: null, ref: decl.ref });
738
+ }
722
739
  }
723
- return { status: res.status, body };
740
+ return out;
724
741
  }
725
- function detailLine(body) {
726
- const details = body.details;
727
- if (!Array.isArray(details) || !details.length) return "";
728
- const lines = details.map((d) => {
729
- const path = Array.isArray(d.path) ? d.path.join(".") : "";
730
- return `${path ? `${path}: ` : ""}${String(d.message ?? JSON.stringify(d))}`;
742
+ async function fetchHosted(id, opts) {
743
+ const dir = opts.cacheDir ?? join2(homedir2(), ".cache", "vos", "assets");
744
+ const cached = existsSync2(dir) ? readdirSync(dir).find(
745
+ (f) => (f === id || f.startsWith(`${id}.`)) && !f.endsWith(".part")
746
+ ) : void 0;
747
+ if (cached) return join2(dir, cached);
748
+ if (!opts.origin || !opts.key) {
749
+ opts.log(
750
+ `note: asset ${id} is hosted and no credential is set, so the render cannot load it (vos login, or keep the file beside the config and name its path)`
751
+ );
752
+ return null;
753
+ }
754
+ const res = await fetch(`${opts.origin}/api/assets/${id}/file`, {
755
+ headers: bearerFor(opts.key, opts.origin)
731
756
  });
732
- return `
733
- ${lines.join("\n ")}`;
734
- }
735
- function apiError(what, r) {
736
- const detail = typeof r.body.error === "string" ? r.body.error + detailLine(r.body) : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
737
- const hint = r.status === 401 ? " (run `vos login`, or set VOS_API_KEY \u2014 mint a key at https://vos.so/app/api)" : "";
738
- return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
739
- }
740
- function promptHint(target, vosId) {
741
- return `vos push ${target} --vos ${vosId} --prompt "<the person's prompt, as they wrote it>"`;
742
- }
743
- var SYNC_STATE_NAME = "vos.json";
744
- function hostedMediaOf(value) {
745
- if (!value || typeof value !== "object") return void 0;
746
- const out = {};
747
- for (const [file, entry] of Object.entries(value)) {
748
- const e = entry;
749
- if (e && typeof e.ref === "string" && typeof e.sha256 === "string")
750
- out[file] = { ref: e.ref, sha256: e.sha256 };
757
+ if (!res.ok) {
758
+ opts.log(
759
+ `note: asset ${id} could not be read (${res.status}), so the render cannot load it`
760
+ );
761
+ return null;
751
762
  }
752
- return Object.keys(out).length ? out : void 0;
753
- }
754
- function readJsonFile(file) {
763
+ mkdirSync2(dir, { recursive: true, mode: 448 });
764
+ const type = (res.headers.get("content-type") ?? "").split(";")[0].trim();
765
+ const file = join2(dir, `${id}${extensionFor(type)}`);
766
+ const partial = `${file}.part`;
755
767
  try {
756
- return JSON.parse(readFileSync(file, "utf8"));
757
- } catch {
758
- return null;
768
+ writeFileSync3(partial, new Uint8Array(await res.arrayBuffer()), {
769
+ mode: 384
770
+ });
771
+ renameSync(partial, file);
772
+ } finally {
773
+ rmSync(partial, { force: true });
759
774
  }
775
+ return file;
760
776
  }
761
- function readSyncState(dir) {
762
- const own = readJsonFile(join2(dir, SYNC_STATE_NAME));
763
- if (own && typeof own.vosId === "string") {
764
- return {
765
- vosId: own.vosId,
766
- versionId: typeof own.versionId === "string" ? own.versionId : null,
767
- ...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
768
- ...typeof own.title === "string" ? { title: own.title } : {},
769
- ...typeof own.slug === "string" ? { slug: own.slug } : {},
770
- ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {},
771
- ...hostedMediaOf(own.hostedMedia) ? { hostedMedia: hostedMediaOf(own.hostedMedia) } : {}
772
- };
773
- }
774
- const push = readJsonFile(join2(dir, "push.json"));
775
- if (push && typeof push.vosId === "string") {
776
- return {
777
- vosId: push.vosId,
778
- versionId: typeof push.versionId === "string" ? push.versionId : null,
779
- ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
780
- };
781
- }
782
- const meta = readJsonFile(join2(dir, "meta.json"));
783
- if (meta && typeof meta.id === "string") {
784
- return {
785
- vosId: meta.id,
786
- versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
787
- ...typeof meta.title === "string" ? { title: meta.title } : {},
788
- ...typeof meta.slug === "string" ? { slug: meta.slug } : {},
789
- ...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
790
- };
777
+ function htmlFaceRefs(config) {
778
+ const out = [];
779
+ const stack = Array.isArray(config.stack) ? config.stack : [];
780
+ for (const entry of stack) {
781
+ const data = entry?.data;
782
+ const overlays = Array.isArray(data?.overlays) ? data.overlays : [];
783
+ for (const clip of overlays) {
784
+ const html = clip?.html;
785
+ const faces = Array.isArray(html?.faces) ? html.faces : [];
786
+ for (const face of faces) {
787
+ if (typeof face?.url !== "string" || !face.url) continue;
788
+ out.push({
789
+ where: `overlay ${String(clip.id)} font ${String(face.family)}`,
790
+ url: face.url,
791
+ set: (next) => {
792
+ face.url = next;
793
+ },
794
+ drop: () => {
795
+ const at = faces.indexOf(face);
796
+ if (at >= 0) faces.splice(at, 1);
797
+ }
798
+ });
799
+ }
800
+ }
791
801
  }
792
- return null;
802
+ return out;
793
803
  }
794
- function writeSyncState(dir, patch) {
795
- const merged = {
796
- ...readSyncState(dir) ?? { versionId: null },
797
- ...patch
804
+ var FACE_HOST = /^https:\/\/assets\.vos\.so\//;
805
+ async function programAssets(config, opts) {
806
+ const refs = manifestRefs(config);
807
+ const faces = [];
808
+ for (const f of htmlFaceRefs(config)) {
809
+ const hosted = hostedAssetId(f.url, opts.origin ? [opts.origin] : []);
810
+ if (!isUrl(f.url) || hosted) {
811
+ faces.push(f);
812
+ } else if (!FACE_HOST.test(f.url)) {
813
+ opts.log(
814
+ `note: ${f.where} names ${f.url}, which is not on https://assets.vos.so, so the render does not fetch it (a face comes from the hosted catalog, a hosted file, or a .woff2 beside the document)`
815
+ );
816
+ f.drop();
817
+ }
818
+ }
819
+ if (refs.length === 0 && faces.length === 0) return null;
820
+ const files = {};
821
+ const served = /* @__PURE__ */ new Map();
822
+ const serve = (file) => {
823
+ const known = served.get(file);
824
+ if (known) return known;
825
+ const tag = createHash("sha256").update(file).digest("hex").slice(0, 16);
826
+ const ext = extname(file);
827
+ const path = `${ASSET_ROUTE}${tag}${/^\.[A-Za-z0-9]+$/.test(ext) ? ext.toLowerCase() : ""}`;
828
+ files[path] = file;
829
+ served.set(file, path);
830
+ return path;
798
831
  };
799
- writeFileSafeSync(
800
- join2(dir, SYNC_STATE_NAME),
801
- `${JSON.stringify(merged, null, 2)}
802
- `
803
- );
804
- return merged;
805
- }
806
- function formatChanges(changes) {
807
- const lines = [];
808
- for (const ch of changes) {
809
- const label = ch.label ? ` \xB7 ${ch.label}` : "";
810
- const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
811
- lines.push(
812
- `v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
832
+ const reach = async (ref, where) => {
833
+ const hosted = hostedAssetId(ref, opts.origin ? [opts.origin] : []);
834
+ if (hosted) {
835
+ const file = await fetchHosted(hosted, opts);
836
+ return file ? serve(file) : ref;
837
+ }
838
+ if (isUrl(ref)) return ref;
839
+ const local = localFile(ref, opts.baseDir);
840
+ if ("refused" in local) {
841
+ opts.log(
842
+ `note: ${where} names ${ref}, ${local.refused}, so the render cannot load it`
843
+ );
844
+ return ref;
845
+ }
846
+ return serve(local.file);
847
+ };
848
+ for (const face of faces) face.set(await reach(face.url, face.where));
849
+ const assets = {};
850
+ for (const { name, index, ref } of refs) {
851
+ const url = await reach(
852
+ ref,
853
+ index === null ? `assets.${name}` : `assets.${name}[${index}]`
813
854
  );
814
- if (ch.note) lines.push(` note: ${ch.note}`);
855
+ if (index === null) assets[name] = url;
856
+ else {
857
+ const list = assets[name] ??= [];
858
+ list[index] = url;
859
+ }
815
860
  }
816
- return lines;
861
+ return { assets, files };
817
862
  }
818
- function serverWarnings(body, log) {
819
- const warnings = Array.isArray(body.warnings) ? body.warnings : [];
820
- for (const w of warnings) if (typeof w === "string") log(`warning ${w}`);
863
+ function missingManifestFiles(config, baseDir) {
864
+ const out = [];
865
+ for (const { name, index, ref } of manifestRefs(config)) {
866
+ if (isUrl(ref) || hostedAssetId(ref)) continue;
867
+ const local = localFile(ref, baseDir);
868
+ if ("file" in local) continue;
869
+ const where = index === null ? `assets.${name}` : `assets.${name}[${index}]`;
870
+ out.push(`${where} names ${ref}, ${local.refused}`);
871
+ }
872
+ return out;
821
873
  }
822
874
 
823
875
  export {
@@ -833,13 +885,6 @@ export {
833
885
  resolveMediaType,
834
886
  nameForType,
835
887
  containerMismatch,
836
- ASSET_ROUTE,
837
- hostedAssetId,
838
- serveFile,
839
- localFile,
840
- manifestRefs,
841
- programAssets,
842
- missingManifestFiles,
843
888
  writeFileSafe,
844
889
  refuseSymlink,
845
890
  writeFileSafeSync,
@@ -852,6 +897,8 @@ export {
852
897
  deriveSlug,
853
898
  CREDENTIALS_PATH,
854
899
  resolveCredential,
900
+ bearerFor,
901
+ mintAt,
855
902
  requireCredential,
856
903
  writeCredential,
857
904
  clientId,
@@ -863,6 +910,13 @@ export {
863
910
  readSyncState,
864
911
  writeSyncState,
865
912
  formatChanges,
866
- serverWarnings
913
+ serverWarnings,
914
+ ASSET_ROUTE,
915
+ hostedAssetId,
916
+ serveFile,
917
+ localFile,
918
+ manifestRefs,
919
+ programAssets,
920
+ missingManifestFiles
867
921
  };
868
- //# sourceMappingURL=chunk-B4U4URE3.js.map
922
+ //# sourceMappingURL=chunk-J3KHIZCH.js.map