@vosjs/cli 0.17.4 → 0.18.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.
@@ -6,11 +6,11 @@ import {
6
6
  } from "./chunk-NHKHTIDR.js";
7
7
 
8
8
  // src/plugin/run.ts
9
- import { mkdir as mkdir9, readFile as readFile13, rm as rm4 } from "fs/promises";
10
- import { existsSync as existsSync13 } from "fs";
11
- import { join as join16, resolve as resolve8 } from "path";
9
+ import { mkdir as mkdir9, readFile as readFile14, rm as rm4 } from "fs/promises";
10
+ import { existsSync as existsSync14 } from "fs";
11
+ import { join as join17, resolve as resolve9 } from "path";
12
12
  import { totalDuration as totalDuration2 } from "@vosjs/timeline";
13
- import { migrateHostedDoc as migrateHostedDoc4, ratedSegments as ratedSegments6 } from "@vosjs/studio-core";
13
+ import { migrateHostedDoc as migrateHostedDoc5, ratedSegments as ratedSegments6 } from "@vosjs/studio-core";
14
14
 
15
15
  // src/plugin/args.ts
16
16
  var UsageError = class extends Error {
@@ -1352,10 +1352,10 @@ function startTakeServer(rootDir, pages) {
1352
1352
  res.writeHead(404).end();
1353
1353
  });
1354
1354
  return new Promise(
1355
- (resolve9) => server.listen(0, () => {
1355
+ (resolve10) => server.listen(0, () => {
1356
1356
  const addr = server.address();
1357
1357
  const port = typeof addr === "object" && addr ? addr.port : 0;
1358
- resolve9({ base: `http://localhost:${port}`, close: () => server.close() });
1358
+ resolve10({ base: `http://localhost:${port}`, close: () => server.close() });
1359
1359
  })
1360
1360
  );
1361
1361
  }
@@ -1806,7 +1806,7 @@ async function resolveBackdropSlug(o) {
1806
1806
  o.backgroundDuration = hit.duration;
1807
1807
  }
1808
1808
  function hasOverrides(o) {
1809
- return !!(o.set?.length || o.frame !== void 0 || o.background !== void 0);
1809
+ return !!(o.set?.length || o.unset?.length || o.frame !== void 0 || o.background !== void 0);
1810
1810
  }
1811
1811
  var FRAME_KINDS = {
1812
1812
  macos: "mac-light",
@@ -1913,6 +1913,16 @@ function applyDocOverrides(doc, o) {
1913
1913
  setPath(d, path, value);
1914
1914
  applied.push(`${path} = ${JSON.stringify(value)}`);
1915
1915
  }
1916
+ for (const path of o.unset ?? []) {
1917
+ const segs = parsePath(path);
1918
+ let node = d;
1919
+ for (let i = 0; i < segs.length - 1 && node && typeof node === "object"; i++)
1920
+ node = node[segs[i].key];
1921
+ if (node && typeof node === "object") {
1922
+ delete node[segs[segs.length - 1].key];
1923
+ applied.push(`${path} removed`);
1924
+ }
1925
+ }
1916
1926
  return applied;
1917
1927
  }
1918
1928
  function applyAndValidate(doc, o) {
@@ -2476,8 +2486,8 @@ async function stillsInPage(opts) {
2476
2486
  }
2477
2487
  async function framesTake(browser, dir, opts) {
2478
2488
  const take = await loadTake(dir);
2479
- if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
2480
- const doc = take.doc;
2489
+ const doc = opts.doc ? structuredClone(opts.doc) : take.doc;
2490
+ if (!doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
2481
2491
  if (opts.overrides && hasOverrides(opts.overrides)) {
2482
2492
  await resolveBackdropSlug(opts.overrides);
2483
2493
  applyAndValidate(doc, opts.overrides);
@@ -2505,7 +2515,7 @@ async function framesTake(browser, dir, opts) {
2505
2515
  }
2506
2516
  }
2507
2517
  if (opts.atMoments) {
2508
- for (const m of await momentsFor(dir, take.doc)) {
2518
+ for (const m of await momentsFor(dir, doc)) {
2509
2519
  if (m.outputAt === null) continue;
2510
2520
  shots.push({ time: clamp(m.outputAt), kind: "moment", momentId: m.id });
2511
2521
  }
@@ -2618,9 +2628,9 @@ async function writeIndexJson(result) {
2618
2628
  }
2619
2629
 
2620
2630
  // src/plugin/deliver.ts
2621
- import { mkdir as mkdir5, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile7 } from "fs/promises";
2631
+ import { mkdir as mkdir4, mkdtemp, rename as rename4, rm as rm3, stat, writeFile as writeFile6 } from "fs/promises";
2622
2632
  import { tmpdir } from "os";
2623
- import { join as join6, relative, resolve as resolve4 } from "path";
2633
+ import { join as join7, relative, resolve as resolve4 } from "path";
2624
2634
  import { totalDuration } from "@vosjs/timeline";
2625
2635
  import { existsSync as existsSync6 } from "fs";
2626
2636
  import { readFile as readFile6 } from "fs/promises";
@@ -2741,114 +2751,6 @@ async function renderTake(browser, dir, outFile, opts) {
2741
2751
  };
2742
2752
  }
2743
2753
 
2744
- // src/plugin/posterStill.ts
2745
- import { compileVosConfig as compileVosConfig3 } from "@vosjs/core";
2746
- async function posterStillInPage(opts) {
2747
- const w = window;
2748
- const RW = opts.W * opts.ss;
2749
- const RH = opts.H * opts.ss;
2750
- try {
2751
- const mod = await import(
2752
- /* @vite-ignore */
2753
- URL.createObjectURL(
2754
- new Blob([opts.animationCode], { type: "text/javascript" })
2755
- )
2756
- );
2757
- w.__vos__ = w.__vos__ || {};
2758
- w.__vos__.isPaused = true;
2759
- const deps = {
2760
- THREE: w.__THREE__,
2761
- gsap: w.__gsap__,
2762
- resolution: {
2763
- width: RW,
2764
- height: RH,
2765
- pixelRatio: 1,
2766
- drawingBufferWidth: RW,
2767
- drawingBufferHeight: RH
2768
- },
2769
- preserveDrawingBuffer: true
2770
- };
2771
- const result = await mod.initVos(document.body, deps);
2772
- if (result.assetsReady) await result.assetsReady;
2773
- const { timeline } = result;
2774
- timeline.pause();
2775
- timeline.seek(opts.time, false);
2776
- const canvas = document.querySelector("canvas");
2777
- canvas.width = RW;
2778
- canvas.height = RH;
2779
- canvas.style.width = RW + "px";
2780
- canvas.style.height = RH + "px";
2781
- const raf = () => new Promise((r) => requestAnimationFrame(r));
2782
- await raf();
2783
- await raf();
2784
- await raf();
2785
- let src = canvas;
2786
- if (opts.ss > 1) {
2787
- const out = document.createElement("canvas");
2788
- out.width = opts.W;
2789
- out.height = opts.H;
2790
- const g = out.getContext("2d");
2791
- g.imageSmoothingEnabled = true;
2792
- g.imageSmoothingQuality = "high";
2793
- g.drawImage(canvas, 0, 0, opts.W, opts.H);
2794
- src = out;
2795
- }
2796
- const png = await new Promise(
2797
- (res) => src.toBlob((b) => res(b), "image/png")
2798
- );
2799
- if (!png) throw new Error("toBlob returned null");
2800
- await fetch("/save?name=" + opts.outName, { method: "POST", body: png });
2801
- w.__done = { ok: true };
2802
- } catch (e) {
2803
- w.__error = String(e instanceof Error && e.stack || e);
2804
- }
2805
- }
2806
- async function renderPosterStills(browser, config, serveDir, shots, time) {
2807
- const animationCode = compileVosConfig3(config, {
2808
- tweenEngine: "vos"
2809
- });
2810
- const server = await startTakeServer(serveDir, {
2811
- "/render.html": renderPageHtml()
2812
- });
2813
- try {
2814
- for (const shot of shots) {
2815
- const ss = stillSupersample(shot.width, shot.height);
2816
- const context = await browser.newContext({
2817
- viewport: { width: shot.width * ss, height: shot.height * ss }
2818
- });
2819
- try {
2820
- const page = await context.newPage();
2821
- page.on("console", (m) => {
2822
- if (m.type() === "error")
2823
- process.stderr.write(` [poster page] ${m.text()}
2824
- `);
2825
- });
2826
- await page.addInitScript(() => {
2827
- ;
2828
- globalThis.__name = (f) => f;
2829
- });
2830
- await page.goto(`${server.base}/render.html`);
2831
- await page.waitForFunction("window.__pageReady__ === true");
2832
- void page.evaluate(posterStillInPage, {
2833
- animationCode,
2834
- W: shot.width,
2835
- H: shot.height,
2836
- ss,
2837
- time,
2838
- outName: shot.name
2839
- }).catch(() => {
2840
- });
2841
- await waitForPageDone(page, `poster ${shot.name}`, () => {
2842
- }, 12e4);
2843
- } finally {
2844
- await context.close();
2845
- }
2846
- }
2847
- } finally {
2848
- server.close();
2849
- }
2850
- }
2851
-
2852
2754
  // src/plugin/moments.ts
2853
2755
  import { ratedSegments as ratedSegments3, spanOutputExtent as spanOutputExtent3 } from "@vosjs/studio-core";
2854
2756
 
@@ -3356,1242 +3258,516 @@ function pickMoments(measured, opts = {}) {
3356
3258
  return { times: kept.map((k) => k.time), dropped };
3357
3259
  }
3358
3260
 
3359
- // src/plugin/shotBake.ts
3360
- import { deflateSync } from "zlib";
3361
- function blurPlane(src, w, h, r, passes = 3) {
3362
- if (r < 1) return;
3363
- const tmp = new Float32Array(src.length);
3364
- for (let p = 0; p < passes; p++) {
3365
- for (let y = 0; y < h; y++) {
3366
- let acc = 0;
3367
- const row = y * w;
3368
- for (let x = -r; x <= r; x++)
3369
- acc += src[row + Math.min(w - 1, Math.max(0, x))];
3370
- for (let x = 0; x < w; x++) {
3371
- tmp[row + x] = acc / (2 * r + 1);
3372
- const add = src[row + Math.min(w - 1, x + r + 1)];
3373
- const sub = src[row + Math.max(0, x - r)];
3374
- acc += add - sub;
3375
- }
3376
- }
3377
- for (let x = 0; x < w; x++) {
3378
- let acc = 0;
3379
- for (let y = -r; y <= r; y++)
3380
- acc += tmp[Math.min(h - 1, Math.max(0, y)) * w + x];
3381
- for (let y = 0; y < h; y++) {
3382
- src[y * w + x] = acc / (2 * r + 1);
3383
- const add = tmp[Math.min(h - 1, y + r + 1) * w + x];
3384
- const sub = tmp[Math.max(0, y - r) * w + x];
3385
- acc += add - sub;
3386
- }
3387
- }
3261
+ // src/plugin/motionPlan.ts
3262
+ import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
3263
+ var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
3264
+ "x-feed-cut",
3265
+ "youtube-main-demo",
3266
+ "shorts-linkedin-vertical-cut"
3267
+ ]);
3268
+ var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
3269
+ var BED_ID = "bed";
3270
+ var CLICK_ID_PREFIX = "click-";
3271
+ var CAPTION_ID_PREFIX = "caption-";
3272
+ var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
3273
+ function pickTrack(catalog, ask) {
3274
+ if (!catalog || !ask || off(ask)) return null;
3275
+ const want = ask.trim().toLowerCase();
3276
+ return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
3277
+ }
3278
+ function clickTimes(doc, range) {
3279
+ const rated = ratedSegments4(doc);
3280
+ const out = [];
3281
+ for (const e of doc.source.cursor) {
3282
+ if (e.type !== "down") continue;
3283
+ const src = e.t / 1e3;
3284
+ const ext = spanOutputExtent4(rated, src, src + 1e-3);
3285
+ if (!ext) continue;
3286
+ const t = ext.start;
3287
+ if (t < range[0] || t > range[1]) continue;
3288
+ if (out.length && t - out[out.length - 1] < 0.12) continue;
3289
+ out.push(+(t - range[0]).toFixed(3));
3388
3290
  }
3291
+ return out;
3389
3292
  }
3390
- function roundedCoverage(x, y, rect, r) {
3391
- let inside = 0;
3392
- for (const dy of [0.25, 0.75]) {
3393
- for (const dx of [0.25, 0.75]) {
3394
- const px = x + dx;
3395
- const py = y + dy;
3396
- if (px < rect.x || py < rect.y || px > rect.x + rect.w || py > rect.y + rect.h)
3397
- continue;
3398
- const cx = Math.min(Math.max(px, rect.x + r), rect.x + rect.w - r);
3399
- const cy = Math.min(Math.max(py, rect.y + r), rect.y + rect.h - r);
3400
- if ((px - cx) ** 2 + (py - cy) ** 2 <= r * r) inside++;
3401
- }
3402
- }
3403
- return inside / 4;
3404
- }
3405
- function bakeShot(shot, opts = {}) {
3406
- const margin = Math.round(shot.w * (opts.margin ?? 0.06));
3407
- const radius = shot.w * (opts.radius ?? 0.014);
3408
- const shadowA = opts.shadow ?? 0.3;
3409
- const blur = Math.round(shot.w * (opts.blur ?? 0.05));
3410
- const offsetY = Math.round(shot.w * (opts.offsetY ?? 0.02));
3411
- const hair = opts.hairline ?? 0;
3412
- const w = shot.w + margin * 2;
3413
- const h = shot.h + margin * 2;
3414
- const out = new Uint8Array(w * h * 4);
3415
- const rect = { x: margin, y: margin, w: shot.w, h: shot.h };
3416
- if (shadowA > 0) {
3417
- const layers = [
3418
- [Math.max(1, Math.round(blur / 4)), Math.round(offsetY / 5), 0.4],
3419
- [blur, offsetY, 0.6]
3420
- ];
3421
- const acc = new Float32Array(w * h);
3422
- for (const [lb, lo, share] of layers) {
3423
- const mask = new Float32Array(w * h);
3424
- for (let y = 0; y < h; y++)
3425
- for (let x = 0; x < w; x++) {
3426
- const c = roundedCoverage(x, y - lo, rect, radius);
3427
- if (c > 0) mask[y * w + x] = c;
3428
- }
3429
- blurPlane(mask, w, h, Math.max(1, Math.round(lb / 2)));
3430
- const a = shadowA * share;
3431
- for (let i = 0; i < w * h; i++)
3432
- acc[i] = 1 - (1 - acc[i]) * (1 - mask[i] * a);
3433
- }
3434
- for (let i = 0; i < w * h; i++) {
3435
- const a = acc[i];
3436
- if (a <= 2e-3) continue;
3437
- out[i * 4] = 0;
3438
- out[i * 4 + 1] = 0;
3439
- out[i * 4 + 2] = 0;
3440
- out[i * 4 + 3] = Math.round(a * 255);
3441
- }
3293
+ var outputLength = (doc) => ratedSegments4(doc).reduce((acc, s) => {
3294
+ const rate = s.rate && s.rate > 0 ? s.rate : 1;
3295
+ return acc + (s.out - s.in) / rate;
3296
+ }, 0);
3297
+ function proposeMotion(input, opts) {
3298
+ const doc = structuredClone(input);
3299
+ const { words, launch, catalog } = opts;
3300
+ const notes = [];
3301
+ const skipped = [];
3302
+ const length = outputLength(doc);
3303
+ const range = [0, length];
3304
+ const entrance = launch.entrance;
3305
+ if (!off(entrance)) {
3306
+ const kind = entrance && /^(tilt-in|pull-out|rise)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
3307
+ doc.frame.entrance = { kind };
3308
+ notes.push(`entrance ${kind}`);
3309
+ } else {
3310
+ delete doc.frame.entrance;
3442
3311
  }
3443
- for (let y = 0; y < h; y++) {
3444
- for (let x = 0; x < w; x++) {
3445
- const cov = roundedCoverage(x, y, rect, radius);
3446
- if (cov <= 0) continue;
3447
- const sx = Math.min(shot.w - 1, Math.max(0, x - margin));
3448
- const sy = Math.min(shot.h - 1, Math.max(0, y - margin));
3449
- const si = (sy * shot.w + sx) * 4;
3450
- const o = (y * w + x) * 4;
3451
- let r = shot.data[si];
3452
- let g = shot.data[si + 1];
3453
- let b = shot.data[si + 2];
3454
- if (hair > 0) {
3455
- const edge = Math.min(
3456
- x - rect.x,
3457
- rect.x + rect.w - x,
3458
- y - rect.y,
3459
- rect.y + rect.h - y
3460
- );
3461
- if (edge < 1.5) {
3462
- r = Math.round(r * (1 - hair));
3463
- g = Math.round(g * (1 - hair));
3464
- b = Math.round(b * (1 - hair));
3465
- }
3466
- }
3467
- const a = cov;
3468
- const ba = out[o + 3] / 255;
3469
- const outA = a + ba * (1 - a);
3470
- const mix = (fg, bg) => outA > 0 ? Math.round((fg * a + bg * ba * (1 - a)) / outA) : 0;
3471
- out[o] = mix(r, out[o]);
3472
- out[o + 1] = mix(g, out[o + 1]);
3473
- out[o + 2] = mix(b, out[o + 2]);
3474
- out[o + 3] = Math.round(outA * 255);
3312
+ if (!off(launch.endCard)) {
3313
+ const headline = (words.headline ?? "").trim();
3314
+ const brand = (words.brand ?? "").trim();
3315
+ const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
3316
+ if (headline || brand) {
3317
+ const card = { seconds: 2.5 };
3318
+ if (opts.ink) card.ink = opts.ink;
3319
+ if (headline) card.headline = headline;
3320
+ if (sub && sub !== headline) card.sub = sub;
3321
+ if (brand) card.wordmark = brand;
3322
+ if (opts.mark) card.mark = opts.mark;
3323
+ doc.endCard = card;
3324
+ notes.push("end card");
3325
+ } else {
3326
+ skipped.push(
3327
+ "no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)"
3328
+ );
3475
3329
  }
3330
+ } else {
3331
+ delete doc.endCard;
3476
3332
  }
3477
- return { w, h, data: out };
3478
- }
3479
- var crcTable = (() => {
3480
- const t = new Uint32Array(256);
3481
- for (let n = 0; n < 256; n++) {
3482
- let c = n;
3483
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
3484
- t[n] = c >>> 0;
3485
- }
3486
- return t;
3487
- })();
3488
- function crc32(buf) {
3489
- let c = 4294967295;
3490
- for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
3491
- return (c ^ 4294967295) >>> 0;
3492
- }
3493
- function chunk(type, body) {
3494
- const out = new Uint8Array(12 + body.length);
3495
- const dv = new DataView(out.buffer);
3496
- dv.setUint32(0, body.length);
3497
- for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
3498
- out.set(body, 8);
3499
- dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
3500
- return out;
3501
- }
3502
- function encodePng(img) {
3503
- const stride = img.w * 4;
3504
- const raw = new Uint8Array((stride + 1) * img.h);
3505
- for (let y = 0; y < img.h; y++) {
3506
- raw[y * (stride + 1)] = 2;
3507
- for (let i = 0; i < stride; i++) {
3508
- const cur = img.data[y * stride + i];
3509
- const up = y ? img.data[(y - 1) * stride + i] : 0;
3510
- raw[y * (stride + 1) + 1 + i] = cur - up & 255;
3333
+ const kept = (doc.overlays ?? []).filter(
3334
+ (o) => !o.id.startsWith(CAPTION_ID_PREFIX)
3335
+ );
3336
+ const captionClips = [];
3337
+ if (opts.captions.length && !off(launch.captions)) {
3338
+ const rated = ratedSegments4(doc);
3339
+ const steps = doc.source.meta.steps ?? [];
3340
+ for (const c of opts.captions) {
3341
+ const step = steps.find(
3342
+ (s) => c.id !== void 0 && s.id === c.id || s.step === c.step
3343
+ );
3344
+ if (!step || step.skipped) continue;
3345
+ const t = stepOutputTime(rated, step, 0.2);
3346
+ if (t === null || t < 0 || t > length - 1) continue;
3347
+ captionClips.push({
3348
+ id: `${CAPTION_ID_PREFIX}${c.step}`,
3349
+ kind: "text",
3350
+ text: c.caption,
3351
+ preset: "caption",
3352
+ start: +t.toFixed(3),
3353
+ duration: Math.min(3.5, Math.max(2.5, length - t - 0.2)),
3354
+ transform: { x: 0.5, y: 0.86, scale: 1, rotation: 0 },
3355
+ enter: "rise",
3356
+ exit: "fade",
3357
+ align: "center",
3358
+ box: { color: "rgba(17,17,17,0.72)" }
3359
+ });
3511
3360
  }
3361
+ if (captionClips.length) notes.push(`${captionClips.length} caption(s)`);
3512
3362
  }
3513
- const ihdr = new Uint8Array(13);
3514
- const dv = new DataView(ihdr.buffer);
3515
- dv.setUint32(0, img.w);
3516
- dv.setUint32(4, img.h);
3517
- ihdr[8] = 8;
3518
- ihdr[9] = 6;
3519
- const parts = [
3520
- new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
3521
- chunk("IHDR", ihdr),
3522
- chunk("IDAT", new Uint8Array(deflateSync(raw))),
3523
- chunk("IEND", new Uint8Array(0))
3524
- ];
3525
- const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
3526
- let o = 0;
3527
- for (const p of parts) {
3528
- out.set(p, o);
3529
- o += p.length;
3363
+ const overlays = [...kept, ...captionClips];
3364
+ if (overlays.length) doc.overlays = overlays;
3365
+ else delete doc.overlays;
3366
+ const clips = (doc.audio ?? []).filter(
3367
+ (a) => a.id !== BED_ID && !a.id.startsWith(CLICK_ID_PREFIX)
3368
+ );
3369
+ const track = pickTrack(catalog, launch.music);
3370
+ if (track) {
3371
+ const hasMic = !!doc.source.micKey;
3372
+ const fadeOut = Math.min(2.5, length * 0.15);
3373
+ clips.push({
3374
+ id: BED_ID,
3375
+ key: track.url,
3376
+ name: track.title,
3377
+ start: 0,
3378
+ in: 0,
3379
+ out: Math.min(track.duration, length),
3380
+ duration: track.duration,
3381
+ gain: hasMic ? 0.35 : 0.5,
3382
+ fadeIn: 0.6,
3383
+ fadeOut,
3384
+ loop: track.duration < length,
3385
+ loopLen: track.duration < length ? length : void 0,
3386
+ duck: hasMic
3387
+ });
3388
+ notes.push(`bed ${track.slug}`);
3389
+ } else if (launch.music && !off(launch.music)) {
3390
+ skipped.push(
3391
+ `music "${launch.music}" is not a catalog track or mood${catalog ? "" : " (the catalog could not be read)"}`
3392
+ );
3530
3393
  }
3531
- return out;
3532
- }
3533
-
3534
- // src/plugin/template.ts
3535
- function templateOf(config) {
3536
- const t = config.template;
3537
- if (!t || typeof t !== "object") return null;
3538
- return t;
3539
- }
3540
- function aspectOf(size) {
3541
- const r = size.w / Math.max(1, size.h);
3542
- if (r > 1.15) return "landscape";
3543
- if (r < 0.87) return "portrait";
3544
- return "square";
3545
- }
3546
- function templateProblems(config) {
3547
- const t = templateOf(config);
3548
- if (!t) return ["no template block: a poster template declares config.template"];
3549
- const out = [];
3550
- const elements = Array.isArray(config.elements) ? config.elements : [];
3551
- const byId = new Map(elements.filter((e) => e.id).map((e) => [e.id, e]));
3552
- const params = Array.isArray(config.params) ? config.params : [];
3553
- const keys = new Set(params.map((p) => p.key).filter(Boolean));
3554
- if (!t.family) out.push("template.family is missing");
3555
- for (const s of t.slots ?? []) {
3556
- const el = byId.get(s.id);
3557
- if (!el) out.push(`template.slots: no element with id "${s.id}"`);
3558
- else if (el.type !== s.kind)
3559
- out.push(`template.slots: "${s.id}" is a ${String(el.type)} element, the slot wants ${s.kind}`);
3560
- }
3561
- for (const x of t.text ?? []) {
3562
- const el = byId.get(x.element);
3563
- if (!el) out.push(`template.text: no element with id "${x.element}"`);
3564
- else if (el.type !== "text") out.push(`template.text: "${x.element}" is not a text element`);
3565
- if (!keys.has(x.param)) out.push(`template.text: param "${x.param}" is not declared in config.params`);
3566
- const bound = el?.content;
3567
- if (el && (!bound || typeof bound !== "object" || bound.$data !== x.param))
3568
- out.push(`template.text: "${x.element}" must bind its content to {$data: "${x.param}"}`);
3569
- }
3570
- for (const k of t.params?.required ?? []) {
3571
- if (!keys.has(k)) out.push(`template.params.required: "${k}" is not declared in config.params`);
3572
- }
3573
- if (!t.layouts?.landscape) out.push("template.layouts.landscape is required");
3574
- for (const [name, layout] of Object.entries(t.layouts ?? {})) {
3575
- for (const s of t.slots ?? []) {
3576
- if (!layout?.slots?.[s.id]) out.push(`template.layouts.${name}: slot "${s.id}" is not placed`);
3394
+ const click = catalog?.sfx.find((s) => s.slug === "sfx-click");
3395
+ if (click && !doc.source.micKey && !off(launch.clicks)) {
3396
+ const times = clickTimes(doc, range);
3397
+ for (const [i, t] of times.entries()) {
3398
+ clips.push({
3399
+ id: `${CLICK_ID_PREFIX}${i}`,
3400
+ key: click.url,
3401
+ name: click.title,
3402
+ start: t,
3403
+ in: 0,
3404
+ out: click.duration,
3405
+ duration: click.duration,
3406
+ gain: 0.4,
3407
+ fadeIn: 0,
3408
+ fadeOut: 0
3409
+ });
3577
3410
  }
3411
+ if (times.length) notes.push(`${times.length} click sound(s)`);
3578
3412
  }
3579
- return out;
3413
+ doc.audio = clips;
3414
+ return { doc, notes, skipped };
3580
3415
  }
3581
- var pctOf = (v) => {
3582
- if (typeof v === "number") return null;
3583
- if (typeof v !== "string") return null;
3584
- const m = /^(-?\d+(?:\.\d+)?)%$/.exec(v.trim());
3585
- return m ? Number(m[1]) / 100 : null;
3586
- };
3587
- function fillTemplate(config, input) {
3588
- const t = templateOf(config);
3589
- if (!t) throw new Error("fillTemplate: the config carries no template block");
3590
- const aspect = aspectOf(input.size);
3591
- const layout = t.layouts[aspect] ?? t.layouts.landscape;
3592
- const out = structuredClone(config);
3593
- const designH = 1080;
3594
- const designW = designH * input.size.w / input.size.h;
3595
- const elements = Array.isArray(out.elements) ? out.elements : [];
3596
- const byId = new Map(elements.filter((e) => e.id).map((e) => [e.id, e]));
3597
- const slotRects = {};
3598
- for (const s of t.slots) {
3599
- const el = byId.get(s.id);
3600
- const place = layout.slots[s.id];
3601
- const src = input.slots[s.id];
3602
- if (!el || !place) continue;
3603
- if (src) el.src = src.src;
3604
- const pad = src?.pad ?? 0;
3605
- slotRects[s.id] = {
3606
- x: place.x,
3607
- y: place.y,
3608
- w: place.w,
3609
- h: place.w * (input.size.w / input.size.h) / (src?.aspect ?? 16 / 9)
3610
- };
3611
- const w = place.w * designW * (1 + 2 * pad);
3612
- const x = place.x - place.w * pad;
3613
- const y = place.y - place.w * pad * (input.size.w / input.size.h) / (src?.aspect ?? 16 / 9);
3614
- el.position = { x: `${(x * 100).toFixed(2)}%`, y: `${(y * 100).toFixed(2)}%` };
3615
- el.anchor = "top-left";
3616
- el.size = { ...el.size ?? {}, width: Math.round(w), height: "auto" };
3617
- if (!src && s.required) el.opacity = 0;
3618
- }
3619
- const data = out.data && typeof out.data === "object" ? { ...out.data } : {};
3620
- const params = Array.isArray(out.params) ? out.params : [];
3621
- const missing = [];
3622
- for (const k of [...t.params.required ?? [], ...t.params.brand ?? []]) {
3623
- if (input.values[k] === void 0 || input.values[k] === null || input.values[k] === "") {
3624
- if ((t.params.required ?? []).includes(k)) missing.push(k);
3625
- continue;
3626
- }
3627
- data[k] = input.values[k];
3628
- const p = params.find((q) => q.key === k);
3629
- if (p) p.default = input.values[k];
3630
- }
3631
- for (const [k, v] of Object.entries(input.values)) {
3632
- if (v !== void 0 && v !== null && v !== "") {
3633
- data[k] = v;
3634
- const p = params.find((q) => q.key === k);
3635
- if (p) p.default = v;
3636
- }
3637
- }
3638
- out.data = data;
3639
- const boxes = [];
3640
- for (const x of t.text) {
3641
- const el = byId.get(x.element);
3642
- if (!el) continue;
3643
- const pos = layout.text?.[x.element];
3644
- if (pos) el.position = { x: pos.x, y: pos.y };
3645
- const size = layout.size?.[x.element];
3646
- if (size) el.font = { ...el.font ?? {}, size };
3647
- if (layout.hide?.includes(x.element)) {
3648
- el.opacity = 0;
3649
- continue;
3416
+ function destinationMechanics(d, doc) {
3417
+ const set = [];
3418
+ const unset = [];
3419
+ const notes = [];
3420
+ if (d.kind !== "video") return { set, unset, notes };
3421
+ const loop = LOOP_DESTINATIONS.has(d.id);
3422
+ const sound = SOUND_DESTINATIONS.has(d.id);
3423
+ const portrait = d.px.w / d.px.h < 0.9;
3424
+ if (loop) {
3425
+ unset.push("frame.entrance", "endCard");
3426
+ notes.push("loop: no entrance, no end card");
3427
+ }
3428
+ if (loop || !sound) {
3429
+ set.push("audio=[]");
3430
+ if (!loop) notes.push("silent channel");
3431
+ }
3432
+ if (loop || d.text === "none") {
3433
+ const kept = (doc.overlays ?? []).filter(
3434
+ (o) => !o.id.startsWith(CAPTION_ID_PREFIX)
3435
+ );
3436
+ if (kept.length !== (doc.overlays ?? []).length) {
3437
+ set.push(`overlays=${JSON.stringify(kept)}`);
3438
+ notes.push("no captions");
3650
3439
  }
3651
- const value = data[x.param];
3652
- if (typeof value !== "string" || !value.trim()) continue;
3653
- const font = el.font ?? {};
3654
- const px = typeof font.size === "number" ? font.size : 40;
3655
- const lines = value.split("\n");
3656
- const longest = Math.max(...lines.map((l) => l.length));
3657
- const lh = typeof font.lineHeight === "number" ? font.lineHeight : 1.15;
3658
- const boxW = longest * px * 0.56;
3659
- const boxH = lines.length * px * lh;
3660
- const ex = pctOf(el.position?.x) ?? 0;
3661
- const ey = pctOf(el.position?.y) ?? 0;
3662
- const anchor = String(el.anchor ?? "center");
3663
- let left = ex * designW;
3664
- let top = ey * designH;
3665
- if (anchor.includes("right")) left -= boxW;
3666
- else if (!anchor.includes("left")) left -= boxW / 2;
3667
- if (anchor.includes("bottom")) top -= boxH;
3668
- else if (!anchor.includes("top")) top -= boxH / 2;
3669
- const rawColor = font.color;
3670
- const color = typeof rawColor === "string" ? rawColor : rawColor && typeof rawColor === "object" && typeof rawColor.$data === "string" ? data[rawColor.$data] : void 0;
3671
- boxes.push({
3672
- x: left / designW,
3673
- y: top / designH,
3674
- w: boxW / designW,
3675
- h: boxH / designH,
3676
- color: color && /^#[0-9a-f]{6}$/i.test(color) ? color : void 0,
3677
- role: x.role,
3678
- label: value.length > 24 ? `${value.slice(0, 24)}\u2026` : value
3679
- });
3680
3440
  }
3681
- for (const id of layout.hide ?? []) {
3682
- const el = byId.get(id);
3683
- if (el && !t.text.some((x) => x.element === id)) el.opacity = 0;
3441
+ if (portrait) {
3442
+ set.push("frame.fit=cover");
3443
+ set.push('frame.inset={"left":0.06,"right":0.06,"top":0.17,"bottom":0.17}');
3444
+ set.push("frame.focusFollow=camera");
3445
+ notes.push("vertical reframe follows the camera");
3684
3446
  }
3685
- return { config: out, text: boxes, slots: slotRects, aspect, missing };
3447
+ return { set, unset, notes };
3686
3448
  }
3687
- function textLimitProblems(t, values) {
3688
- const out = [];
3689
- for (const x of t.text) {
3690
- const v = values[x.param];
3691
- if (typeof v !== "string") continue;
3692
- const words = v.trim().split(/\s+/).filter(Boolean).length;
3693
- const lines = v.split("\n").length;
3694
- if (x.maxWords && words > x.maxWords)
3695
- out.push(`${x.param}: ${words} words, the template holds ${x.maxWords}`);
3696
- if (x.lines && lines > x.lines)
3697
- out.push(`${x.param}: ${lines} lines, the template holds ${x.lines}`);
3698
- }
3699
- return out;
3449
+ function endCardInk(look, brand) {
3450
+ if (!look) return null;
3451
+ const ground = look.ground;
3452
+ const m = /#([0-9a-f]{6})/i.exec(ground);
3453
+ const hex2 = m ? m[0] : null;
3454
+ const light = look.kind === "plate" || (hex2 ? isLightHexGround(hex2) : look.kind === "gradient");
3455
+ if (!light) return "#ffffff";
3456
+ const ink = brand?.ink;
3457
+ return ink && /^#[0-9a-f]{6}$/i.test(ink) ? ink : "#111111";
3700
3458
  }
3701
-
3702
- // src/plugin/templates/cardOnGradient.ts
3703
- var GROUND = `(ctx) => {
3704
- const THREE = ctx.THREE
3705
- const d = ctx.data
3706
- const canvas = document.createElement('canvas')
3707
- canvas.width = 1280
3708
- canvas.height = 720
3709
- const c = canvas.getContext('2d')
3710
- c.fillStyle = d.bgA
3711
- c.fillRect(0, 0, 1280, 720)
3712
- const blob = (x, y, r, color) => {
3713
- const g = c.createRadialGradient(x, y, 0, x, y, r)
3714
- g.addColorStop(0, color)
3715
- g.addColorStop(1, 'rgba(0,0,0,0)')
3716
- c.fillStyle = g
3717
- c.fillRect(0, 0, 1280, 720)
3718
- }
3719
- blob(180, 120, 760, d.blobA)
3720
- blob(1120, 620, 820, d.blobB)
3721
- blob(760, 60, 620, d.blobC)
3722
- let seed = 7
3723
- const rand = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296 }
3724
- const img = c.getImageData(0, 0, 1280, 720)
3725
- const px = img.data
3726
- const grain = d.grain
3727
- for (let i = 0; i < px.length; i += 4) {
3728
- const n = (rand() - 0.5) * grain
3729
- px[i] += n; px[i + 1] += n; px[i + 2] += n
3730
- }
3731
- c.putImageData(img, 0, 0)
3732
- const tex = new THREE.CanvasTexture(canvas)
3733
- tex.colorSpace = THREE.SRGBColorSpace
3734
- const plane = new THREE.Mesh(
3735
- new THREE.PlaneGeometry(2, 2),
3736
- new THREE.MeshBasicMaterial({ map: tex, depthTest: false }),
3737
- )
3738
- plane.renderOrder = -10
3739
- ctx.scene.add(plane)
3740
- return { refs: { plane }, dispose: () => { tex.dispose(); plane.geometry.dispose(); plane.material.dispose() } }
3741
- }`;
3742
- var TIMELINE = `(ctx, content, duration) => {
3743
- const { gsap, elements } = ctx
3744
- const tl = gsap.timeline({ paused: true })
3745
- const shot = elements.get('shot')
3746
- if (shot) tl.fromTo(shot.props, { opacity: 0, translateY: 30, scale: 0.97 }, { opacity: 1, translateY: 0, scale: 1, duration: 1.0, ease: 'power3.out' }, 0.2)
3747
- return tl
3748
- }`;
3749
- function cardOnGradient() {
3750
- return {
3751
- version: 2,
3752
- duration: 4,
3753
- camera: { preset: "fullscreen" },
3754
- template: {
3755
- family: "card-on-gradient",
3756
- slots: [{ id: "shot", kind: "image", required: true }],
3757
- params: { required: [], brand: ["bgA", "blobA", "blobB", "blobC", "grain"] },
3758
- text: [],
3759
- layouts: {
3760
- landscape: { slots: { shot: { x: 0.09, y: 0.12, w: 0.82 } } },
3761
- square: { slots: { shot: { x: 0.08, y: 0.27, w: 0.84 } } },
3762
- portrait: { slots: { shot: { x: 0.06, y: 0.3, w: 0.88 } } }
3763
- }
3764
- },
3765
- params: [
3766
- { key: "bgA", type: "color", label: "Ground", default: "#f3efe8" },
3767
- { key: "blobA", type: "color", label: "Blob A", default: "rgba(255,183,146,0.55)" },
3768
- { key: "blobB", type: "color", label: "Blob B", default: "rgba(170,196,255,0.5)" },
3769
- { key: "blobC", type: "color", label: "Blob C", default: "rgba(255,236,170,0.45)" },
3770
- { key: "grain", type: "number", label: "Grain", default: 14, min: 0, max: 40, step: 1 }
3771
- ],
3772
- data: {
3773
- bgA: "#f3efe8",
3774
- blobA: "rgba(255,183,146,0.55)",
3775
- blobB: "rgba(170,196,255,0.5)",
3776
- blobC: "rgba(255,236,170,0.45)",
3777
- grain: 14
3778
- },
3779
- elements: [
3780
- {
3781
- type: "image",
3782
- id: "shot",
3783
- src: "",
3784
- position: { x: "9%", y: "12%" },
3785
- anchor: "top-left",
3786
- size: { width: 1574, height: "auto", fit: "contain" },
3787
- zIndex: 60,
3788
- opacity: 0
3789
- }
3790
- ],
3791
- createContent: GROUND,
3792
- createTimeline: TIMELINE
3793
- };
3459
+ function isLightHexGround(hex2) {
3460
+ const n = parseInt(hex2.slice(1), 16);
3461
+ const r = n >> 16 & 255;
3462
+ const g = n >> 8 & 255;
3463
+ const b = n & 255;
3464
+ return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
3794
3465
  }
3795
3466
 
3796
- // src/plugin/templates/splitCover.ts
3797
- var GROUND2 = `(ctx) => {
3798
- const THREE = ctx.THREE
3799
- const d = ctx.data
3800
- const canvas = document.createElement('canvas')
3801
- canvas.width = 1280
3802
- canvas.height = 720
3803
- const c = canvas.getContext('2d')
3804
- const g = c.createLinearGradient(0, 0, 1280, 720)
3805
- g.addColorStop(0, d.bgA)
3806
- g.addColorStop(0.55, d.bgB)
3807
- g.addColorStop(1, d.bgC)
3808
- c.fillStyle = g
3809
- c.fillRect(0, 0, 1280, 720)
3810
- const rg = c.createRadialGradient(980, 120, 40, 980, 120, 820)
3811
- rg.addColorStop(0, d.streak)
3812
- rg.addColorStop(1, 'rgba(0,0,0,0)')
3813
- c.fillStyle = rg
3814
- c.fillRect(0, 0, 1280, 720)
3815
- let seed = 41
3816
- const rand = () => { seed = (seed * 1664525 + 1013904223) >>> 0; return seed / 4294967296 }
3817
- const img = c.getImageData(0, 0, 1280, 720)
3818
- const px = img.data
3819
- const grain = d.grain
3820
- for (let i = 0; i < px.length; i += 4) {
3821
- const n = (rand() - 0.5) * grain
3822
- px[i] += n; px[i + 1] += n; px[i + 2] += n
3823
- }
3824
- c.putImageData(img, 0, 0)
3825
- const tex = new THREE.CanvasTexture(canvas)
3826
- tex.colorSpace = THREE.SRGBColorSpace
3827
- const plane = new THREE.Mesh(
3828
- new THREE.PlaneGeometry(2, 2),
3829
- new THREE.MeshBasicMaterial({ map: tex, depthTest: false }),
3830
- )
3831
- plane.renderOrder = -10
3832
- ctx.scene.add(plane)
3833
- return { refs: { plane }, dispose: () => { tex.dispose(); plane.geometry.dispose(); plane.material.dispose() } }
3834
- }`;
3835
- var TIMELINE2 = `(ctx, content, duration) => {
3836
- const { gsap, elements } = ctx
3837
- const tl = gsap.timeline({ paused: true })
3838
- const kicker = elements.get('kicker')
3839
- const title = elements.get('title')
3840
- const brand = elements.get('brand')
3841
- const shot = elements.get('shot')
3842
- if (kicker) tl.fromTo(kicker.props, { opacity: 0, translateY: 14 }, { opacity: 1, translateY: 0, duration: 0.7, ease: 'power2.out' }, 0.25)
3843
- if (title) tl.fromTo(title.props, { opacity: 0, translateY: 26 }, { opacity: 1, translateY: 0, duration: 0.9, ease: 'power3.out' }, 0.45)
3844
- if (brand) tl.fromTo(brand.props, { opacity: 0 }, { opacity: 1, duration: 0.8, ease: 'power2.out' }, 0.9)
3845
- if (shot) tl.fromTo(shot.props, { opacity: 0, translateX: 70 }, { opacity: 1, translateX: 0, duration: 1.1, ease: 'power3.out' }, 0.55)
3846
- return tl
3847
- }`;
3848
- function splitCover() {
3849
- return {
3850
- version: 2,
3851
- duration: 6,
3852
- camera: { preset: "fullscreen" },
3853
- fonts: [
3854
- { family: "Fraunces", url: "https://assets.vos.so/fonts/fraunces/600.woff2", weight: 600 },
3855
- { family: "Lexend", url: "https://assets.vos.so/fonts/lexend/700.woff2", weight: 700 },
3856
- { family: "JetBrains Mono", url: "https://assets.vos.so/fonts/jetbrains-mono/400.woff2", weight: 400 }
3857
- ],
3858
- template: {
3859
- family: "split-cover",
3860
- slots: [{ id: "shot", kind: "image", required: true }],
3861
- params: {
3862
- required: ["headline", "brand"],
3863
- brand: ["bgA", "bgB", "bgC", "ink", "inkSoft", "accent", "streak", "fontDisplay", "fontBody", "grain"]
3864
- },
3865
- text: [
3866
- { element: "title", param: "headline", role: "headline", maxWords: 8, lines: 3 },
3867
- { element: "kicker", param: "kicker", role: "body" },
3868
- { element: "brand", param: "brand", role: "body" }
3869
- ],
3870
- layouts: {
3871
- landscape: {
3872
- slots: { shot: { x: 0.46, y: 0.28, w: 1.02 } },
3873
- text: { kicker: { x: "7%", y: "24%" }, title: { x: "7%", y: "44%" }, brand: { x: "7%", y: "86%" } },
3874
- size: { title: 84, kicker: 19, brand: 25 }
3875
- },
3876
- square: {
3877
- slots: { shot: { x: 0.1, y: 0.5, w: 1 } },
3878
- text: { kicker: { x: "8%", y: "12%" }, title: { x: "8%", y: "27%" }, brand: { x: "8%", y: "93%" } },
3879
- size: { title: 72, kicker: 18, brand: 23 }
3880
- },
3881
- portrait: {
3882
- slots: { shot: { x: 0.08, y: 0.44, w: 1.06 } },
3883
- text: { kicker: { x: "8%", y: "12%" }, title: { x: "8%", y: "26%" }, brand: { x: "8%", y: "94%" } },
3884
- size: { title: 64, kicker: 17, brand: 22 }
3885
- }
3886
- }
3887
- },
3888
- params: [
3889
- { key: "kicker", type: "text", label: "Kicker", default: "RELEASE" },
3890
- { key: "headline", type: "text", label: "Headline", default: "What shipped,\nin three lines" },
3891
- { key: "brand", type: "text", label: "Wordmark", default: "brand" },
3892
- { key: "bgA", type: "color", label: "Ground A", default: "#8a3d2a" },
3893
- { key: "bgB", type: "color", label: "Ground B", default: "#c0632f" },
3894
- { key: "bgC", type: "color", label: "Ground C", default: "#5c5a2e" },
3895
- { key: "ink", type: "color", label: "Ink", default: "#fff6ec" },
3896
- { key: "inkSoft", type: "color", label: "Ink, softened", default: "#e9d9c8" },
3897
- { key: "streak", type: "color", label: "Light streak", default: "rgba(255,220,170,0.18)" },
3898
- { key: "fontDisplay", type: "text", label: "Headline face", default: "Fraunces, serif" },
3899
- { key: "fontBody", type: "text", label: "Body face", default: "Lexend, sans-serif" },
3900
- { key: "grain", type: "number", label: "Grain", default: 22, min: 0, max: 40, step: 1 }
3901
- ],
3902
- data: {
3903
- kicker: "RELEASE",
3904
- headline: "What shipped,\nin three lines",
3905
- brand: "brand",
3906
- bgA: "#8a3d2a",
3907
- bgB: "#c0632f",
3908
- bgC: "#5c5a2e",
3909
- ink: "#fff6ec",
3910
- inkSoft: "#e9d9c8",
3911
- streak: "rgba(255,220,170,0.18)",
3912
- fontDisplay: "Fraunces, serif",
3913
- fontBody: "Lexend, sans-serif",
3914
- grain: 22
3915
- },
3916
- elements: [
3917
- {
3918
- type: "text",
3919
- id: "kicker",
3920
- content: { $data: "kicker" },
3921
- position: { x: "7%", y: "24%" },
3922
- anchor: "center-left",
3923
- font: { family: "JetBrains Mono, monospace", size: 19, weight: 400, color: { $data: "inkSoft" }, letterSpacing: 6, align: "left" },
3924
- opacity: 0
3925
- },
3926
- {
3927
- type: "text",
3928
- id: "title",
3929
- content: { $data: "headline" },
3930
- position: { x: "7%", y: "44%" },
3931
- anchor: "center-left",
3932
- font: { family: { $data: "fontDisplay" }, size: 84, weight: 600, color: { $data: "ink" }, lineHeight: 1.08, align: "left", letterSpacing: 0 },
3933
- opacity: 0
3934
- },
3935
- {
3936
- type: "text",
3937
- id: "brand",
3938
- content: { $data: "brand" },
3939
- position: { x: "7%", y: "86%" },
3940
- anchor: "center-left",
3941
- font: { family: { $data: "fontBody" }, size: 25, weight: 700, color: { $data: "ink" }, letterSpacing: 1, align: "left" },
3942
- opacity: 0
3943
- },
3944
- {
3945
- type: "image",
3946
- id: "shot",
3947
- src: "",
3948
- position: { x: "46%", y: "28%" },
3949
- anchor: "top-left",
3950
- size: { width: 1400, height: "auto", fit: "contain" },
3951
- zIndex: 60,
3952
- opacity: 0
3953
- }
3954
- ],
3955
- createContent: GROUND2,
3956
- createTimeline: TIMELINE2
3957
- };
3958
- }
3959
-
3960
- // src/plugin/templates/index.ts
3961
- var TEMPLATES = {
3962
- "split-cover": splitCover,
3963
- "card-on-gradient": cardOnGradient
3964
- };
3965
- var TEMPLATE_NAMES = Object.keys(TEMPLATES);
3966
- function templateByName(name) {
3967
- const make = TEMPLATES[name];
3968
- return make ? make() : null;
3969
- }
3970
-
3971
- // src/plugin/posterValues.ts
3467
+ // src/plugin/posterDoc.ts
3468
+ import { existsSync as existsSync5 } from "fs";
3469
+ import { readFile as readFile5 } from "fs/promises";
3470
+ import { isAbsolute, join as join6, resolve as resolve3 } from "path";
3972
3471
  import {
3973
- findFontFamily as findFontFamily2,
3974
- fontFaceUrl,
3975
- fontStack,
3976
- nearestFontWeight
3977
- } from "@vosjs/shared";
3978
- var HEX = /^#([0-9a-f]{6})$/i;
3979
- var rgb = (hex2) => {
3980
- const m = HEX.exec(hex2.trim());
3981
- if (!m) return null;
3982
- const n = parseInt(m[1], 16);
3983
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
3984
- };
3985
- var toHex = (c) => "#" + c.map(
3986
- (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")
3987
- ).join("");
3988
- function mixHex(a, b, t) {
3989
- const pa = rgb(a);
3990
- const pb = rgb(b);
3991
- if (!pa || !pb) return a;
3992
- return toHex([
3993
- pa[0] + (pb[0] - pa[0]) * t,
3994
- pa[1] + (pb[1] - pa[1]) * t,
3995
- pa[2] + (pb[2] - pa[2]) * t
3996
- ]);
3997
- }
3998
- function rgba(hex2, alpha) {
3999
- const c = rgb(hex2) ?? [255, 255, 255];
4000
- return `rgba(${c[0]},${c[1]},${c[2]},${alpha})`;
4001
- }
4002
- function isLightHex(hex2) {
4003
- const c = rgb(hex2);
4004
- if (!c) return true;
4005
- return (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255 >= 0.6;
4006
- }
4007
- function resolveFace(family, weights) {
4008
- if (!family) return null;
4009
- const first = family.split(",")[0].replace(/['"]/g, "").replace(/\s+variable$/i, "").trim();
4010
- const entry = findFontFamily2(first);
4011
- if (!entry) return null;
4012
- const fonts = [
4013
- ...new Set(weights.map((w) => nearestFontWeight(entry, w)))
4014
- ].map((w) => ({
4015
- family: entry.family,
4016
- url: fontFaceUrl(entry.slug, w),
4017
- weight: w
4018
- }));
4019
- return { stack: fontStack(entry), category: entry.category, fonts };
4020
- }
4021
- function posterValues(brand, words) {
4022
- const b = brand ?? {};
4023
- const bgA = b.bgA && HEX.test(b.bgA) ? b.bgA : null;
4024
- const bgB = b.bgB && HEX.test(b.bgB) ? b.bgB : bgA;
4025
- const bgC = b.bgC && HEX.test(b.bgC) ? b.bgC : bgB;
4026
- const ink = b.ink && HEX.test(b.ink) ? b.ink : null;
4027
- const accent = b.accent && HEX.test(b.accent) ? b.accent : null;
4028
- const wordmark = (words.brand ?? b.wordmark ?? b.name ?? "").trim();
4029
- const release = (words.release ?? "").trim();
4030
- const lightGround = bgA ? isLightHex(bgA) : false;
4031
- const values = {};
4032
- if (bgA) values.bgA = bgA;
4033
- if (bgB) values.bgB = bgB;
4034
- if (bgC) values.bgC = bgC;
4035
- if (ink) values.ink = ink;
4036
- if (accent) values.accent = accent;
4037
- if (ink && bgA) values.inkSoft = mixHex(ink, bgA, 0.28);
4038
- if (accent) values.streak = rgba(accent, lightGround ? 0.12 : 0.18);
4039
- if (bgA) values.grain = lightGround ? 10 : 22;
4040
- if (accent) values.blobA = rgba(accent, lightGround ? 0.28 : 0.4);
4041
- if (bgC) values.blobB = rgba(bgC, 0.75);
4042
- if (accent)
4043
- values.blobC = rgba(
4044
- mixHex(accent, "#ffffff", 0.55),
4045
- lightGround ? 0.35 : 0.25
3472
+ computeCardLayout,
3473
+ docRestTime,
3474
+ migrateHostedDoc,
3475
+ overlayRect,
3476
+ resolveOverlayStyle
3477
+ } from "@vosjs/studio-core";
3478
+
3479
+ // src/plugin/platform.ts
3480
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
3481
+ import { homedir } from "os";
3482
+ import { join as join5 } from "path";
3483
+ function platformOrigin(flags = {}) {
3484
+ const legacyEnv = process.env.VOS_API_BASE?.trim();
3485
+ const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
3486
+ if (!flags.origin && !flags.api && !process.env.VOS_ORIGIN && legacyEnv) {
3487
+ process.stderr.write(
3488
+ "note: VOS_API_BASE is deprecated \u2014 set VOS_ORIGIN (an origin, no /api suffix)\n"
4046
3489
  );
4047
- if (wordmark) values.brand = wordmark;
4048
- const headline = (words.headline ?? "").trim();
4049
- if (headline) values.headline = headline;
4050
- const kicker = (words.kicker ?? "").trim();
4051
- values.kicker = kicker ? kicker : [wordmark, release].filter(Boolean).join(" ").toUpperCase();
4052
- const fonts = [];
4053
- const display = resolveFace(b.fontDisplay, [600, 700]);
4054
- if (display) {
4055
- values.fontDisplay = display.stack;
4056
- fonts.push(...display.fonts);
4057
3490
  }
4058
- const body = resolveFace(b.fontBody, [700]);
4059
- if (body) {
4060
- values.fontBody = body.stack;
4061
- fonts.push(...body.fonts);
4062
- }
4063
- return { values, fonts, lightGround };
3491
+ return raw.replace(/\/+$/, "").replace(/\/api$/, "");
4064
3492
  }
4065
-
4066
- // src/plugin/stages.ts
4067
- var str = (v, fallback) => typeof v === "string" && v.trim() ? v.trim() : fallback;
4068
- var firstFamily = (stack) => stack.split(",")[0].replace(/['"]/g, "").trim();
4069
- function stageSplitCover(input) {
4070
- const R = input.size.w / Math.max(1, input.size.h);
4071
- const portrait = R < 0.87;
4072
- const square = !portrait && R <= 1.15;
4073
- const v = input.values;
4074
- const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
4075
- const ink = str(v.ink, "#fff6ec");
4076
- const inkSoft = str(v.inkSoft, ink);
4077
- const display = firstFamily(str(v.fontDisplay, "Lexend"));
4078
- const body = firstFamily(str(v.fontBody, "Lexend"));
4079
- const headline = str(v.headline, "");
4080
- const kicker = str(v.kicker, "");
4081
- const brand = str(v.brand, "");
4082
- const inset = portrait ? { left: 0.08, right: -0.08, top: 0.44, bottom: -0.1 } : square ? { left: 0.1, right: -0.1, top: 0.5, bottom: -0.12 } : { left: 0.44, right: -0.06, top: 0.3, bottom: -0.12 };
4083
- const shot = {
4084
- x: inset.left,
4085
- y: inset.top,
4086
- w: 1 - inset.left - inset.right,
4087
- h: 1 - inset.top - inset.bottom
4088
- };
4089
- const set = [
4090
- `frame.background=${JSON.stringify(ground)}`,
4091
- "frame.backgroundMedia=null",
4092
- "frame.fit=cover",
4093
- `frame.inset=${JSON.stringify(inset)}`,
4094
- 'frame.focus={"cx":0,"cy":0}',
4095
- "frame.radius=16",
4096
- "frame.shadow=0.5",
4097
- "frame.shadowContact=0",
4098
- "frame.border=0",
4099
- "zoom=[]",
4100
- `tilt=[{"id":"stage","in":0,"out":${input.sourceSeconds.toFixed(3)},"rx":3,"ry":${portrait || square ? 0 : 10}}]`,
4101
- "cursor.visible=false",
4102
- "cursor.clickFx.style=none"
4103
- ];
4104
- const designW = 1080 * input.size.w / input.size.h;
4105
- const boxes = [];
4106
- const clips = [];
4107
- const column = portrait || square ? 0.84 : 0.36;
4108
- const left = portrait || square ? 0.08 : 0.07;
4109
- const size = portrait ? 60 : square ? 68 : 84;
4110
- const word = (id, text, preset, family, px, y, color, extra, role, x0 = left) => {
4111
- if (!text) return;
4112
- const lines = text.split("\n");
4113
- const longest = Math.max(...lines.map((l) => l.length));
4114
- const widest = Math.min(
4115
- column,
4116
- longest * px * (preset === "title" ? 0.5 : 0.62) / designW
4117
- );
4118
- const lh = preset === "title" ? 1.05 : 1.2;
4119
- const h = lines.length * px * lh / 1080;
4120
- clips.push({
4121
- id,
4122
- kind: "text",
4123
- text,
4124
- preset,
4125
- family,
4126
- size: px,
4127
- color,
4128
- align: "left",
4129
- maxWidth: column,
4130
- lineHeight: lh,
4131
- transform: { x: x0 + widest / 2, y, scale: 1, rotation: 0 },
4132
- // Words on the ground carry no footage shadow: a drop shadow under a
4133
- // headline on a plate is the old-web tell.
4134
- shadow: 0,
4135
- start: 0,
4136
- duration: +input.outputSeconds.toFixed(3),
4137
- enter: "none",
4138
- exit: "none",
4139
- ...extra
4140
- });
4141
- boxes.push({
4142
- x: x0,
4143
- y: y - h / 2,
4144
- w: widest,
4145
- h,
4146
- color,
4147
- role,
4148
- label: text.length > 24 ? `${text.slice(0, 24)}\u2026` : text
4149
- });
4150
- };
4151
- const markKey = str(v.logoKey, "");
4152
- const markAspect = typeof v.logoAspect === "number" && v.logoAspect > 0 ? v.logoAspect : 1;
4153
- const wideMark = !!markKey && markAspect > 2.2;
4154
- const lockup = (px, y) => {
4155
- if (!markKey) return left;
4156
- const hPx = wideMark ? px * 1.6 : px * 1.3;
4157
- const w = Math.min(column, hPx * markAspect / designW);
4158
- clips.push({
4159
- id: "stage-mark",
4160
- kind: "image",
4161
- key: markKey,
4162
- width: +w.toFixed(4),
4163
- radius: 0,
4164
- shadow: "none",
4165
- transform: { x: left + w / 2, y, scale: 1, rotation: 0 },
4166
- start: 0,
4167
- duration: +input.outputSeconds.toFixed(3),
4168
- enter: "none",
4169
- exit: "none"
4170
- });
4171
- return left + w + px * 0.5 / designW;
4172
- };
4173
- const kickerY = portrait ? 0.1 : square ? 0.12 : 0.24;
4174
- const titleY = portrait ? 0.24 : square ? 0.28 : 0.46;
4175
- const brandY = portrait ? 0.94 : square ? 0.94 : 0.86;
4176
- word(
4177
- "stage-kicker",
4178
- kicker,
4179
- "label",
4180
- "JetBrains Mono",
4181
- 19,
4182
- kickerY,
4183
- inkSoft,
4184
- { letterSpacing: 6, weight: 400 },
4185
- "body"
4186
- );
4187
- word(
4188
- "stage-title",
4189
- headline,
4190
- "title",
4191
- display,
4192
- size,
4193
- titleY,
4194
- ink,
4195
- { weight: 600, letterSpacing: -Math.round(size * 0.02) },
4196
- "headline"
3493
+ function parseVosId(input) {
3494
+ if (!/^https?:\/\//.test(input)) {
3495
+ if (/^[A-Za-z0-9_-]+$/.test(input)) return input;
3496
+ throw new UsageError(`"${input}" is not a vos id or URL`);
3497
+ }
3498
+ let url;
3499
+ try {
3500
+ url = new URL(input);
3501
+ } catch {
3502
+ throw new UsageError(`"${input}" is not a valid URL`);
3503
+ }
3504
+ const query = url.searchParams.get("vos");
3505
+ if (query) return query;
3506
+ const path = url.pathname.match(/\/vos\/([A-Za-z0-9_-]+)/);
3507
+ if (path) return path[1];
3508
+ throw new UsageError(
3509
+ `could not find a vos id in ${input} \u2014 expected /vos/{id} or ?vos={id}`
4197
3510
  );
4198
- const brandX = lockup(25, brandY);
4199
- if (!wideMark)
4200
- word(
4201
- "stage-brand",
4202
- brand,
4203
- "label",
4204
- body,
4205
- 25,
4206
- brandY,
4207
- ink,
4208
- { weight: 700, letterSpacing: 1 },
4209
- "body",
4210
- brandX
3511
+ }
3512
+ function deriveSlug(title) {
3513
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
3514
+ }
3515
+ var CREDENTIALS_PATH = join5(homedir(), ".config", "vos", "credentials");
3516
+ function resolveCredential(explicit) {
3517
+ const flag = explicit?.trim();
3518
+ if (flag) return flag;
3519
+ const env = process.env.VOS_API_KEY?.trim();
3520
+ if (env) return env;
3521
+ try {
3522
+ const first = readFileSync2(CREDENTIALS_PATH, "utf8").split("\n")[0].trim();
3523
+ return first || null;
3524
+ } catch {
3525
+ return null;
3526
+ }
3527
+ }
3528
+ function requireCredential(explicit) {
3529
+ const key = resolveCredential(explicit);
3530
+ if (!key) {
3531
+ throw new Error(
3532
+ "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)"
4211
3533
  );
4212
- set.push(`overlays=${JSON.stringify(clips)}`);
4213
- return { set, text: boxes, shot };
3534
+ }
3535
+ return key;
4214
3536
  }
4215
- var TILE_MAX_PX = 700;
4216
- function isTileSize(size) {
4217
- return Math.max(size.w, size.h) < TILE_MAX_PX;
4218
- }
4219
- function stageTile(input) {
4220
- const R = input.size.w / Math.max(1, input.size.h);
4221
- const square = R <= 1.15;
4222
- const v = input.values;
4223
- const ground = `linear-gradient(135deg, ${str(v.bgA, "#8a3d2a")}, ${str(v.bgC, str(v.bgB, "#5c5a2e"))})`;
4224
- const ink = str(v.ink, "#fff6ec");
4225
- const display = firstFamily(str(v.fontDisplay, "Lexend"));
4226
- const headline = str(v.headline, "") || str(v.brand, "Release");
4227
- const wordless = input.text === "none";
4228
- const aspect = input.footageAspect && input.footageAspect > 0 ? input.footageAspect : 16 / 9;
4229
- const left = square ? 0.08 : 0.07;
4230
- const top = wordless ? square ? 0.1 : 0.12 : square ? 0.42 : 0.47;
4231
- const visible = wordless ? square ? 0.66 : 0.8 : square ? 0.56 : 0.62;
4232
- const cardW = (1 - left) / visible;
4233
- const cardH = cardW * R / aspect;
4234
- const inset = {
4235
- left,
4236
- right: +(1 - left - cardW).toFixed(4),
4237
- top,
4238
- bottom: +(1 - top - cardH).toFixed(4)
4239
- };
4240
- const shot = {
4241
- x: inset.left,
4242
- y: inset.top,
4243
- w: 1 - inset.left - inset.right,
4244
- h: 1 - inset.top - inset.bottom
4245
- };
4246
- const set = [
4247
- `frame.background=${JSON.stringify(ground)}`,
4248
- "frame.backgroundMedia=null",
4249
- "frame.fit=cover",
4250
- `frame.inset=${JSON.stringify(inset)}`,
4251
- 'frame.focus={"cx":0,"cy":0}',
4252
- "frame.radius=24",
4253
- "frame.shadow=0.5",
4254
- "frame.shadowContact=0",
4255
- "frame.border=0",
4256
- "zoom=[]",
4257
- "tilt=[]",
4258
- "cursor.visible=false",
4259
- "cursor.clickFx.style=none"
4260
- ];
4261
- const designW = 1080 * input.size.w / input.size.h;
4262
- const column = square ? 0.84 : 0.86;
4263
- const px = square ? 96 : 108;
4264
- const lines = headline.split("\n");
4265
- const longest = Math.max(...lines.map((l) => l.length));
4266
- const widest = Math.min(column, longest * px * 0.5 / designW);
4267
- const lh = 1.05;
4268
- const h = lines.length * px * lh / 1080;
4269
- const y = square ? 0.2 : 0.24;
4270
- const clip = {
4271
- id: "stage-title",
4272
- kind: "text",
4273
- text: headline,
4274
- preset: "title",
4275
- family: display,
4276
- size: px,
4277
- color: ink,
4278
- align: "left",
4279
- maxWidth: column,
4280
- lineHeight: lh,
4281
- weight: 600,
4282
- letterSpacing: -Math.round(px * 0.02),
4283
- transform: { x: left + widest / 2, y, scale: 1, rotation: 0 },
4284
- shadow: 0,
4285
- start: 0,
4286
- duration: +input.outputSeconds.toFixed(3),
4287
- enter: "none",
4288
- exit: "none"
3537
+ function writeCredential(key) {
3538
+ mkdirSync(join5(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
3539
+ writeFileSync(CREDENTIALS_PATH, `${key.trim()}
3540
+ `, { mode: 384 });
3541
+ return CREDENTIALS_PATH;
3542
+ }
3543
+ function clientId() {
3544
+ const env = process.env.VOS_CLIENT?.trim();
3545
+ if (env && env.length <= 60 && !/[\r\n]/.test(env)) return env;
3546
+ return "vos-cli";
3547
+ }
3548
+ async function apiJson(origin, path, init = {}) {
3549
+ const headers = {
3550
+ accept: "application/json",
3551
+ ...init.headers
4289
3552
  };
4290
- if (wordless) {
4291
- const markKey = str(v.logoKey, "");
4292
- const markAspect = typeof v.logoAspect === "number" && v.logoAspect > 0 ? v.logoAspect : 1;
4293
- if (markKey) {
4294
- const hFrac = top * 0.5;
4295
- const w = Math.min(
4296
- 0.8,
4297
- hFrac * input.size.h * markAspect / input.size.w
4298
- );
4299
- set.push(
4300
- `overlays=${JSON.stringify([
4301
- {
4302
- id: "stage-mark",
4303
- kind: "image",
4304
- key: markKey,
4305
- width: +w.toFixed(4),
4306
- radius: 0,
4307
- shadow: "none",
4308
- transform: { x: left + w / 2, y: top / 2, scale: 1, rotation: 0 },
4309
- start: 0,
4310
- duration: +input.outputSeconds.toFixed(3),
4311
- enter: "none",
4312
- exit: "none"
4313
- }
4314
- ])}`
4315
- );
4316
- } else set.push("overlays=[]");
4317
- return { set, text: [], shot };
3553
+ if (init.key) headers.authorization = `Bearer ${init.key}`;
3554
+ if (init.body !== void 0) headers["content-type"] = "application/json";
3555
+ const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
3556
+ const res = await fetch(`${origin}${path}`, {
3557
+ method: init.method ?? "GET",
3558
+ headers,
3559
+ body: reqBody
3560
+ });
3561
+ let body = {};
3562
+ try {
3563
+ body = await res.json();
3564
+ } catch {
4318
3565
  }
4319
- set.push(`overlays=${JSON.stringify([clip])}`);
4320
- const text = [
4321
- {
4322
- x: left,
4323
- y: y - h / 2,
4324
- w: widest,
4325
- h,
4326
- color: ink,
4327
- role: "headline",
4328
- label: headline.length > 24 ? `${headline.slice(0, 24)}\u2026` : headline
4329
- }
4330
- ];
4331
- return { set, text, shot };
3566
+ return { status: res.status, body };
4332
3567
  }
4333
-
4334
- // src/plugin/markAsset.ts
4335
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
4336
- import { existsSync as existsSync5 } from "fs";
4337
- import { isAbsolute, join as join5, resolve as resolve3 } from "path";
4338
- function markExtension(url) {
4339
- const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
4340
- if (!m) return null;
4341
- const e = m[1].toLowerCase();
4342
- return e === "jpeg" ? "jpg" : e;
3568
+ function apiError(what, r) {
3569
+ const detail = typeof r.body.error === "string" ? r.body.error : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
3570
+ const hint = r.status === 401 ? " (run `vos login`, or set VOS_API_KEY \u2014 mint a key at https://vos.so/app/api)" : "";
3571
+ return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
4343
3572
  }
4344
- function imageAspect(bytes, ext) {
4345
- if (ext === "svg") {
4346
- const head = new TextDecoder().decode(bytes.subarray(0, 4096));
4347
- const vb = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(
4348
- head
4349
- );
4350
- if (vb) return ratio(+vb[1], +vb[2]);
4351
- const w = /<svg[^>]*\swidth\s*=\s*["']([\d.]+)/i.exec(head);
4352
- const h = /<svg[^>]*\sheight\s*=\s*["']([\d.]+)/i.exec(head);
4353
- if (w && h) return ratio(+w[1], +h[1]);
3573
+ var SYNC_STATE_NAME = "vos.json";
3574
+ function readJsonFile(file) {
3575
+ try {
3576
+ return JSON.parse(readFileSync2(file, "utf8"));
3577
+ } catch {
4354
3578
  return null;
4355
3579
  }
4356
- const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4357
- if (ext === "png") {
4358
- if (bytes.length < 24 || bytes[0] !== 137 || bytes[1] !== 80) return null;
4359
- return ratio(dv.getUint32(16), dv.getUint32(20));
3580
+ }
3581
+ function readSyncState(dir) {
3582
+ const own = readJsonFile(join5(dir, SYNC_STATE_NAME));
3583
+ if (own && typeof own.vosId === "string") {
3584
+ return {
3585
+ vosId: own.vosId,
3586
+ versionId: typeof own.versionId === "string" ? own.versionId : null,
3587
+ ...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
3588
+ ...typeof own.title === "string" ? { title: own.title } : {},
3589
+ ...typeof own.slug === "string" ? { slug: own.slug } : {},
3590
+ ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
3591
+ };
4360
3592
  }
4361
- if (ext === "webp") {
4362
- if (bytes.length < 30) return null;
4363
- const tag = String.fromCharCode(...bytes.subarray(12, 16));
4364
- if (tag === "VP8X")
4365
- return ratio(
4366
- 1 + (dv.getUint32(24, true) & 16777215),
4367
- 1 + (dv.getUint32(27, true) & 16777215)
4368
- );
4369
- if (tag === "VP8L") {
4370
- const b = dv.getUint32(21, true);
4371
- return ratio(1 + (b & 16383), 1 + (b >> 14 & 16383));
4372
- }
4373
- if (tag === "VP8 ")
4374
- return ratio(
4375
- dv.getUint16(26, true) & 16383,
4376
- dv.getUint16(28, true) & 16383
4377
- );
4378
- return null;
3593
+ const push = readJsonFile(join5(dir, "push.json"));
3594
+ if (push && typeof push.vosId === "string") {
3595
+ return {
3596
+ vosId: push.vosId,
3597
+ versionId: typeof push.versionId === "string" ? push.versionId : null,
3598
+ ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
3599
+ };
4379
3600
  }
4380
- if (ext === "jpg") {
4381
- let i = 2;
4382
- while (i + 9 < bytes.length) {
4383
- if (bytes[i] !== 255) return null;
4384
- const marker = bytes[i + 1];
4385
- const len = dv.getUint16(i + 2);
4386
- if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
4387
- return ratio(dv.getUint16(i + 7), dv.getUint16(i + 5));
4388
- }
4389
- i += 2 + len;
4390
- }
4391
- return null;
3601
+ const meta = readJsonFile(join5(dir, "meta.json"));
3602
+ if (meta && typeof meta.id === "string") {
3603
+ return {
3604
+ vosId: meta.id,
3605
+ versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
3606
+ ...typeof meta.title === "string" ? { title: meta.title } : {},
3607
+ ...typeof meta.slug === "string" ? { slug: meta.slug } : {},
3608
+ ...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
3609
+ };
4392
3610
  }
4393
3611
  return null;
4394
3612
  }
4395
- function ratio(w, h) {
4396
- return w > 0 && h > 0 ? w / h : null;
4397
- }
4398
- async function fetchBrandMarks(takeDir, roles) {
4399
- const out = { light: null, dark: null, notes: [] };
4400
- if (!roles) return out;
4401
- const one = async (role, name) => {
4402
- const src = (roles[role] ?? "").trim();
4403
- if (!src) return null;
4404
- const ext = markExtension(src);
4405
- if (!ext) {
4406
- out.notes.push(
4407
- `${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
4408
- );
4409
- return null;
4410
- }
4411
- let bytes;
4412
- try {
4413
- if (/^https?:/i.test(src)) {
4414
- const res = await fetch(src);
4415
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
4416
- bytes = new Uint8Array(await res.arrayBuffer());
4417
- } else {
4418
- const file = isAbsolute(src) ? src : resolve3(takeDir, src);
4419
- if (!existsSync5(file)) throw new Error("no such file");
4420
- bytes = new Uint8Array(await readFile5(file));
4421
- }
4422
- } catch (e) {
4423
- out.notes.push(
4424
- `${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
4425
- );
4426
- return null;
4427
- }
4428
- const aspect = imageAspect(bytes, ext);
4429
- if (!aspect) {
4430
- out.notes.push(
4431
- `${role}: ${src} has no readable size; the wordmark stands in`
4432
- );
4433
- return null;
4434
- }
4435
- await mkdir4(join5(takeDir, "brand"), { recursive: true });
4436
- await writeFile6(join5(takeDir, "brand", `${name}.${ext}`), bytes);
4437
- return { key: `/brand/${name}.${ext}`, aspect };
3613
+ function writeSyncState(dir, patch) {
3614
+ const merged = {
3615
+ ...readSyncState(dir) ?? { versionId: null },
3616
+ ...patch
4438
3617
  };
4439
- out.light = await one("logoUrl", "mark");
4440
- out.dark = await one("logoOnDarkUrl", "mark-on-dark");
4441
- return out;
4442
- }
4443
-
4444
- // src/plugin/motionPlan.ts
4445
- import { ratedSegments as ratedSegments4, spanOutputExtent as spanOutputExtent4 } from "@vosjs/studio-core";
4446
- var SOUND_DESTINATIONS = /* @__PURE__ */ new Set([
4447
- "x-feed-cut",
4448
- "youtube-main-demo",
4449
- "shorts-linkedin-vertical-cut"
4450
- ]);
4451
- var LOOP_DESTINATIONS = /* @__PURE__ */ new Set(["github-readme-loop"]);
4452
- var off = (v) => v !== void 0 && /^(none|off|false|no)$/i.test(v.trim());
4453
- function pickTrack(catalog, ask) {
4454
- if (!catalog || !ask || off(ask)) return null;
4455
- const want = ask.trim().toLowerCase();
4456
- return catalog.tracks.find((t) => t.slug.toLowerCase() === want) ?? catalog.tracks.find((t) => (t.mood ?? "").toLowerCase() === want) ?? null;
4457
- }
4458
- function clickTimes(doc, range) {
4459
- const rated = ratedSegments4(doc);
4460
- const out = [];
4461
- for (const e of doc.source.cursor) {
4462
- if (e.type !== "down") continue;
4463
- const src = e.t / 1e3;
4464
- const ext = spanOutputExtent4(rated, src, src + 1e-3);
4465
- if (!ext) continue;
4466
- const t = ext.start;
4467
- if (t < range[0] || t > range[1]) continue;
4468
- if (out.length && t - out[out.length - 1] < 0.12) continue;
4469
- out.push(+(t - range[0]).toFixed(3));
4470
- }
4471
- return out;
3618
+ writeFileSync(
3619
+ join5(dir, SYNC_STATE_NAME),
3620
+ `${JSON.stringify(merged, null, 2)}
3621
+ `
3622
+ );
3623
+ return merged;
4472
3624
  }
4473
- function planMotion(input) {
4474
- const { destination: d, doc, range, words, launch, catalog } = input;
4475
- const set = [];
4476
- const notes = [];
4477
- const skipped = [];
4478
- if (d.kind !== "video") return { set, notes, skipped };
4479
- const loop = LOOP_DESTINATIONS.has(d.id);
4480
- const sound = SOUND_DESTINATIONS.has(d.id);
4481
- const length = range[1] - range[0];
4482
- const portrait = d.px.w / d.px.h < 0.9;
4483
- const entrance = launch.entrance;
4484
- if (!loop && !off(entrance)) {
4485
- const kind = entrance && /^(tilt-in|pull-out|rise)$/.test(entrance.trim()) ? entrance.trim() : "tilt-in";
4486
- set.push(`frame.entrance={"kind":"${kind}"}`);
4487
- notes.push(`entrance ${kind}`);
4488
- }
4489
- if (!loop && !off(launch.endCard)) {
4490
- const headline = (words.headline ?? "").trim();
4491
- const brand = (words.brand ?? "").trim();
4492
- const sub = [brand, (words.release ?? "").trim()].filter(Boolean).join(" ");
4493
- if (headline || brand) {
4494
- const card = { seconds: 2.5 };
4495
- if (input.ink) card.ink = input.ink;
4496
- if (headline) card.headline = headline;
4497
- if (sub && sub !== headline) card.sub = sub;
4498
- if (brand) card.wordmark = brand;
4499
- if (input.mark) card.mark = input.mark;
4500
- set.push(`endCard=${JSON.stringify(card)}`);
4501
- notes.push("end card");
4502
- } else {
4503
- skipped.push(
4504
- `${d.id}: no end card (no headline or wordmark in LAUNCH.md, BRAND.md or the flags)`
4505
- );
4506
- }
3625
+ function formatChanges(changes) {
3626
+ const lines = [];
3627
+ for (const ch of changes) {
3628
+ const label = ch.label ? ` \xB7 ${ch.label}` : "";
3629
+ const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
3630
+ lines.push(
3631
+ `v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
3632
+ );
3633
+ if (ch.note) lines.push(` note: ${ch.note}`);
4507
3634
  }
4508
- if (!loop && d.text !== "none" && input.captions.length && !off(launch.captions)) {
4509
- const rated = ratedSegments4(doc);
4510
- const steps = doc.source.meta.steps ?? [];
4511
- const clips = [];
4512
- for (const c of input.captions) {
4513
- const step = steps.find(
4514
- (s) => c.id !== void 0 && s.id === c.id || s.step === c.step
4515
- );
4516
- if (!step || step.skipped) continue;
4517
- const t = stepOutputTime(rated, step, 0.2);
4518
- if (t === null || t < range[0] || t > range[1] - 1) continue;
4519
- const start = +(t - range[0]).toFixed(3);
4520
- clips.push({
4521
- id: `caption-${c.step}`,
4522
- kind: "text",
4523
- text: c.caption,
4524
- preset: "caption",
4525
- start,
4526
- duration: Math.min(3.5, Math.max(2.5, range[1] - t - 0.2)),
4527
- transform: { x: 0.5, y: portrait ? 0.8 : 0.86, scale: 1, rotation: 0 },
4528
- enter: "rise",
4529
- exit: "fade",
4530
- align: "center",
4531
- box: { color: "rgba(17,17,17,0.72)" }
4532
- });
4533
- }
4534
- if (clips.length) {
4535
- const existing = Array.isArray(doc.overlays) ? doc.overlays : [];
4536
- set.push(`overlays=${JSON.stringify([...existing, ...clips])}`);
4537
- notes.push(`${clips.length} caption(s)`);
4538
- }
3635
+ return lines;
3636
+ }
3637
+
3638
+ // src/plugin/posterDoc.ts
3639
+ var POSTER_CLASSES = [
3640
+ "landscape",
3641
+ "square",
3642
+ "portrait",
3643
+ "tile"
3644
+ ];
3645
+ var TILE_MAX_PX = 700;
3646
+ function posterClassFor(px) {
3647
+ if (Math.max(px.w, px.h) < TILE_MAX_PX) return "tile";
3648
+ const r = px.w / Math.max(1, px.h);
3649
+ if (r < 0.87) return "portrait";
3650
+ if (r <= 1.15) return "square";
3651
+ return "landscape";
3652
+ }
3653
+ var ROLE_ALL = "poster";
3654
+ var roleFor = (cls) => `poster-${cls}`;
3655
+ async function readPosterAt(cls, path, takeDir, from) {
3656
+ let file = path;
3657
+ let docDir = path;
3658
+ if (existsSync5(join6(path, "doc.json"))) {
3659
+ file = join6(path, "doc.json");
3660
+ } else if (!/\.json$/i.test(path) || !existsSync5(path)) {
3661
+ return null;
3662
+ } else {
3663
+ docDir = resolve3(path, "..");
4539
3664
  }
4540
- if (sound && !loop) {
4541
- const track = pickTrack(catalog, launch.music);
4542
- const clips = Array.isArray(doc.audio) ? [...doc.audio] : [];
4543
- if (track) {
4544
- const hasMic = !!doc.source.micKey;
4545
- const fadeOut = Math.min(2.5, length * 0.15);
4546
- clips.push({
4547
- id: "bed",
4548
- key: track.url,
4549
- name: track.title,
4550
- start: 0,
4551
- in: 0,
4552
- out: Math.min(track.duration, length),
4553
- duration: track.duration,
4554
- gain: hasMic ? 0.35 : 0.5,
4555
- fadeIn: 0.6,
4556
- fadeOut,
4557
- loop: track.duration < length,
4558
- loopLen: track.duration < length ? length : void 0,
4559
- duck: hasMic
4560
- });
4561
- notes.push(`bed ${track.slug}`);
4562
- } else if (launch.music && !off(launch.music)) {
4563
- skipped.push(
4564
- `${d.id}: music "${launch.music}" is not a catalog track or mood${catalog ? "" : " (the catalog could not be read)"}`
4565
- );
4566
- }
4567
- const click = catalog?.sfx.find((s) => s.slug === "sfx-click");
4568
- if (click && !doc.source.micKey && !off(launch.clicks)) {
4569
- const times = clickTimes(doc, range);
4570
- for (const [i, t] of times.entries()) {
4571
- clips.push({
4572
- id: `click-${i}`,
4573
- key: click.url,
4574
- name: click.title,
4575
- start: t,
4576
- in: 0,
4577
- out: click.duration,
4578
- duration: click.duration,
4579
- gain: 0.4,
4580
- fadeIn: 0,
4581
- fadeOut: 0
4582
- });
3665
+ let raw;
3666
+ try {
3667
+ raw = JSON.parse(await readFile5(file, "utf8"));
3668
+ } catch {
3669
+ return null;
3670
+ }
3671
+ if (!raw || typeof raw !== "object") return null;
3672
+ const source = raw.source;
3673
+ if (!source || typeof source.videoKey !== "string") return null;
3674
+ const doc = migrateHostedDoc(
3675
+ raw
3676
+ );
3677
+ const ownFootage = existsSync5(join6(docDir, "meta.json")) && existsSync5(join6(docDir, source.videoKey));
3678
+ const sync = readSyncState(docDir);
3679
+ return {
3680
+ cls,
3681
+ file,
3682
+ takeDir: ownFootage ? docDir : takeDir,
3683
+ ownFootage,
3684
+ doc,
3685
+ ...sync?.vosId ? { vosId: sync.vosId } : {},
3686
+ from
3687
+ };
3688
+ }
3689
+ async function findPosterDocs(takeDir, launchRoles) {
3690
+ const out = {};
3691
+ const at2 = (p) => isAbsolute(p) ? p : resolve3(takeDir, p);
3692
+ for (const cls of POSTER_CLASSES) {
3693
+ const named = launchRoles?.[roleFor(cls)] ?? launchRoles?.[ROLE_ALL];
3694
+ const candidates = [];
3695
+ if (named && !/^(none|off|no|false)$/i.test(named.trim())) {
3696
+ const role = launchRoles?.[roleFor(cls)] ? roleFor(cls) : ROLE_ALL;
3697
+ candidates.push({ path: at2(named.trim()), from: `LAUNCH.md ${role}` });
3698
+ }
3699
+ candidates.push({
3700
+ path: join6(takeDir, "poster", cls),
3701
+ from: `poster/${cls}`
3702
+ });
3703
+ candidates.push({ path: join6(takeDir, "poster"), from: "poster" });
3704
+ for (const c of candidates) {
3705
+ const ref = await readPosterAt(cls, c.path, takeDir, c.from);
3706
+ if (ref) {
3707
+ out[cls] = ref;
3708
+ break;
4583
3709
  }
4584
- if (times.length) notes.push(`${times.length} click sound(s)`);
4585
3710
  }
4586
- if (clips.length) set.push(`audio=${JSON.stringify(clips)}`);
4587
3711
  }
4588
- if (portrait) {
4589
- set.push("frame.fit=cover");
4590
- set.push('frame.inset={"left":0.06,"right":0.06,"top":0.17,"bottom":0.17}');
4591
- set.push("frame.focusFollow=camera");
4592
- notes.push("vertical reframe follows the camera");
3712
+ return out;
3713
+ }
3714
+ function posterStillTime(doc, duration) {
3715
+ const rest = docRestTime(doc);
3716
+ if (rest != null) return rest;
3717
+ return Math.max(0, duration - 1 / 30);
3718
+ }
3719
+ var DESIGN_H = 1080;
3720
+ function designFrame(px) {
3721
+ return {
3722
+ W: Math.max(2, Math.round(DESIGN_H * px.w / Math.max(1, px.h))),
3723
+ H: DESIGN_H
3724
+ };
3725
+ }
3726
+ function posterShotRect(doc, px) {
3727
+ const { W, H } = designFrame(px);
3728
+ const meta = doc.source.meta;
3729
+ const layout = computeCardLayout(
3730
+ doc.frame,
3731
+ {
3732
+ width: meta.captureWidth ?? meta.width,
3733
+ height: meta.captureHeight ?? meta.height
3734
+ },
3735
+ W,
3736
+ H
3737
+ );
3738
+ const r3 = (v) => Math.round(v * 1e3) / 1e3;
3739
+ return {
3740
+ x: r3(layout.cardX / W),
3741
+ y: r3(layout.cardY / H),
3742
+ w: r3(layout.cardW / W),
3743
+ h: r3(layout.cardH / H)
3744
+ };
3745
+ }
3746
+ function estimateWidth(text, font, letterSpacingPx = 0) {
3747
+ const m = /(\d+(?:\.\d+)?)px/.exec(font);
3748
+ const px = m ? Number(m[1]) : 32;
3749
+ return text.length * (px * 0.55 + letterSpacingPx);
3750
+ }
3751
+ function posterTextBoxes(doc, px, time) {
3752
+ const { W, H } = designFrame(px);
3753
+ const out = [];
3754
+ for (const clip of doc.overlays ?? []) {
3755
+ if (clip.kind !== "text") continue;
3756
+ if (time < clip.start || time > clip.start + clip.duration) continue;
3757
+ const rect = overlayRect(clip, estimateWidth, W, H);
3758
+ const style = resolveOverlayStyle(clip);
3759
+ const r3 = (v) => Math.round(v * 1e3) / 1e3;
3760
+ out.push({
3761
+ x: r3((rect.cx - rect.w / 2) / W),
3762
+ y: r3((rect.cy - rect.h / 2) / H),
3763
+ w: r3(rect.w / W),
3764
+ h: r3(rect.h / H),
3765
+ role: clip.preset === "title" ? "headline" : "body",
3766
+ label: clip.text.split("\n")[0].slice(0, 40),
3767
+ .../^#[0-9a-f]{6}$/i.test(style.color) ? { color: style.color } : {}
3768
+ });
4593
3769
  }
4594
- return { set, notes, skipped };
3770
+ return out;
4595
3771
  }
4596
3772
 
4597
3773
  // src/plugin/deliver.ts
@@ -4622,7 +3798,7 @@ function resolveChannels(raw) {
4622
3798
  return out;
4623
3799
  }
4624
3800
  async function readBrandBesideTake(dir, explicit) {
4625
- const candidates = explicit ? [explicit] : [join6(dir, "BRAND.md"), join6(dir, "..", "BRAND.md")];
3801
+ const candidates = explicit ? [explicit] : [join7(dir, "BRAND.md"), join7(dir, "..", "BRAND.md")];
4626
3802
  for (const file of candidates) {
4627
3803
  if (!existsSync6(file)) continue;
4628
3804
  const roles = parseFrontmatter(await readFile6(file, "utf8"));
@@ -4631,7 +3807,7 @@ async function readBrandBesideTake(dir, explicit) {
4631
3807
  return null;
4632
3808
  }
4633
3809
  async function readLaunchBesideTake(dir, explicit) {
4634
- const candidates = explicit ? [explicit] : [join6(dir, "LAUNCH.md"), join6(dir, "..", "LAUNCH.md")];
3810
+ const candidates = explicit ? [explicit] : [join7(dir, "LAUNCH.md"), join7(dir, "..", "LAUNCH.md")];
4635
3811
  for (const file of candidates) {
4636
3812
  if (!existsSync6(file)) continue;
4637
3813
  return { file, roles: parseFrontmatter(await readFile6(file, "utf8")) };
@@ -4659,28 +3835,6 @@ async function resolveLook(dir, opts) {
4659
3835
  roles
4660
3836
  };
4661
3837
  }
4662
- function templateForCard(d, opts) {
4663
- if (opts.poster === null) return null;
4664
- if (opts.poster) return { config: opts.poster.config, from: opts.poster.from };
4665
- if (!d.template) return null;
4666
- if (isTileSize(d.px)) {
4667
- const config2 = templateByName(d.template) ?? templateByName("card-on-gradient");
4668
- if (config2) return { config: config2, from: "stage tile", stage: "tile" };
4669
- }
4670
- let name = d.template;
4671
- let note;
4672
- const wants = templateOf(templateByName(name) ?? {});
4673
- const needsHeadline = wants?.text.some((t) => t.role === "headline");
4674
- if (needsHeadline && !opts.words?.headline?.trim()) {
4675
- name = "card-on-gradient";
4676
- note = `${d.id}: no headline (LAUNCH.md headline: or --headline), so the ${d.template} template stands down for card-on-gradient`;
4677
- }
4678
- const config = templateByName(name);
4679
- if (!config) return null;
4680
- if (name === "split-cover")
4681
- return { config, from: "stage split-cover", note, stage: "split-cover" };
4682
- return { config, from: `template ${name}`, note };
4683
- }
4684
3838
  function lookOverrides(look, placement, size, video, opts) {
4685
3839
  const inset = cardInset(look, size, video, placement);
4686
3840
  const set = [
@@ -4708,23 +3862,6 @@ function lookOverrides(look, placement, size, video, opts) {
4708
3862
  }
4709
3863
  return set;
4710
3864
  }
4711
- function endCardInk(look, brand) {
4712
- if (!look) return null;
4713
- const ground = look.ground;
4714
- const m = /#([0-9a-f]{6})/i.exec(ground);
4715
- const hex2 = m ? m[0] : null;
4716
- const light = look.kind === "plate" || (hex2 ? isLightHexGround(hex2) : look.kind === "gradient");
4717
- if (!light) return "#ffffff";
4718
- const ink = brand?.ink;
4719
- return ink && /^#[0-9a-f]{6}$/i.test(ink) ? ink : "#111111";
4720
- }
4721
- function isLightHexGround(hex2) {
4722
- const n = parseInt(hex2.slice(1), 16);
4723
- const r = n >> 16 & 255;
4724
- const g = n >> 8 & 255;
4725
- const b = n & 255;
4726
- return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 >= 0.6;
4727
- }
4728
3865
  var PROBE_WIDTH = 640;
4729
3866
  async function pickStillTimes(browser, dir, doc, duration, opts) {
4730
3867
  const { candidates, dropped } = momentCandidates(doc, duration);
@@ -4737,7 +3874,7 @@ async function pickStillTimes(browser, dir, doc, duration, opts) {
4737
3874
  const meta = doc.source.meta;
4738
3875
  const vw = meta.captureWidth ?? meta.width;
4739
3876
  const vh = meta.captureHeight ?? meta.height;
4740
- const probeDir = await mkdtemp(join6(tmpdir(), "vos-moments-"));
3877
+ const probeDir = await mkdtemp(join7(tmpdir(), "vos-moments-"));
4741
3878
  try {
4742
3879
  const probe = await framesTake(browser, dir, {
4743
3880
  times: candidates.map((c) => c.time),
@@ -4822,16 +3959,10 @@ async function deliverTake(browser, dir, opts) {
4822
3959
  const take = await loadTake(dir);
4823
3960
  if (!take.doc) throw new Error(`${dir} has no doc.json \u2014 run plan first`);
4824
3961
  const doc = take.doc;
4825
- const marks = await fetchBrandMarks(dir, opts.brandRoles);
4826
- for (const n of marks.notes) opts.onPhase?.(`note: ${n}`);
4827
- const darkGround = opts.look?.kind === "dark";
4828
- const mark = darkGround ? marks.dark : marks.light;
4829
- if (mark)
4830
- opts.onPhase?.(`brand mark: ${mark.key} (${mark.aspect.toFixed(2)}:1)`);
4831
3962
  const duration = totalDuration(ratedSegments5(doc));
4832
3963
  const videoSeconds = opts.range ? Math.min(opts.range[1], duration) - Math.min(opts.range[0], duration) : duration;
4833
- const outDir = resolve4(opts.outDir ?? join6(dir, "kit"));
4834
- await mkdir5(outDir, { recursive: true });
3964
+ const outDir = resolve4(opts.outDir ?? join7(dir, "kit"));
3965
+ await mkdir4(outDir, { recursive: true });
4835
3966
  const destinations = opts.channels.flatMap((c) => destinationsForChannel(c));
4836
3967
  const assets = [];
4837
3968
  const skipped = [];
@@ -4855,7 +3986,7 @@ async function deliverTake(browser, dir, opts) {
4855
3986
  w: meta0.captureWidth ?? meta0.width,
4856
3987
  h: meta0.captureHeight ?? meta0.height
4857
3988
  };
4858
- const videoOverrides = (d, range) => {
3989
+ const videoOverrides = (d) => {
4859
3990
  const set = [];
4860
3991
  if (opts.look) {
4861
3992
  set.push(
@@ -4865,203 +3996,82 @@ async function deliverTake(browser, dir, opts) {
4865
3996
  })
4866
3997
  );
4867
3998
  }
4868
- const plan = planMotion({
4869
- destination: d,
4870
- doc,
4871
- range: range ?? [0, duration],
4872
- words: opts.words ?? {},
4873
- launch: opts.launchRoles ?? {},
4874
- captions: opts.captions ?? [],
4875
- catalog: opts.catalog ?? null,
4876
- ink: endCardInk(opts.look, opts.brandRoles),
4877
- mark
4878
- });
4879
- set.push(...plan.set);
4880
- if (plan.notes.length) opts.onPhase?.(`${d.id}: ${plan.notes.join(", ")}`);
4881
- for (const s of plan.skipped) skipped.push(`note: ${s}`);
3999
+ const mech = destinationMechanics(d, doc);
4000
+ set.push(...mech.set);
4001
+ if (mech.notes.length) opts.onPhase?.(`${d.id}: ${mech.notes.join(", ")}`);
4882
4002
  set.push(...opts.overrides?.set ?? []);
4883
- if (!set.length) return opts.overrides;
4884
- return { ...opts.overrides, set };
4003
+ if (!set.length && !mech.unset.length) return opts.overrides;
4004
+ return { ...opts.overrides, set, unset: mech.unset };
4885
4005
  };
4886
- const cardPlans = destinations.filter(
4887
- (d) => d.kind !== "video" && d.genre === "card" && !NOT_FROM_FOOTAGE[d.id]
4888
- ).map((d) => ({ d, plan: templateForCard(d, opts) })).filter(
4889
- (p) => p.plan !== null
4890
- );
4891
- const posterCardIds = new Set(cardPlans.map((p) => p.d.id));
4892
- for (const p of cardPlans)
4893
- if (p.plan.note) skipped.push(`note: ${p.plan.note}`);
4894
- if (cardPlans.length) {
4895
- const meta = doc.source.meta;
4896
- const heroTime = opts.shotTime ?? (stillTimes.length ? stillTimes[0] : duration / 2);
4897
- const fill = posterValues(opts.brandRoles, opts.words ?? {});
4898
- if (mark) {
4899
- fill.values.logoKey = mark.key;
4900
- fill.values.logoAspect = mark.aspect;
4006
+ const posters = await findPosterDocs(dir, opts.launchRoles);
4007
+ const posterCardIds = /* @__PURE__ */ new Set();
4008
+ const missing = /* @__PURE__ */ new Set();
4009
+ for (const d of destinations) {
4010
+ if (d.kind === "video" || d.genre !== "card" || NOT_FROM_FOOTAGE[d.id])
4011
+ continue;
4012
+ const cls = posterClassFor(d.px);
4013
+ const ref = posters[cls];
4014
+ if (!ref) {
4015
+ missing.add(cls);
4016
+ continue;
4901
4017
  }
4018
+ posterCardIds.add(d.id);
4019
+ const label = `${d.channel} ${d.asset}`;
4020
+ const posterDuration = totalDuration(ratedSegments5(ref.doc));
4021
+ const time = posterStillTime(ref.doc, posterDuration);
4902
4022
  opts.onPhase?.(
4903
- `poster shot (full bleed at ${heroTime.toFixed(2)}s), baked as an object`
4023
+ `${label} (${specWords(d)}) from ${ref.from}${ref.vosId ? ` ${ref.vosId}` : ""}, the rest at ${time.toFixed(2)}s`
4904
4024
  );
4905
- const serveDir = await mkdtemp(join6(tmpdir(), "vos-poster-"));
4025
+ const shotDir = await mkdtemp(join7(tmpdir(), "vos-poster-"));
4906
4026
  try {
4907
- const shot = await framesTake(browser, dir, {
4908
- times: [heroTime],
4909
- width: meta.captureWidth ?? meta.width,
4910
- height: meta.captureHeight ?? meta.height,
4911
- outDir: serveDir,
4912
- overrides: {
4913
- ...opts.overrides,
4914
- set: [
4915
- "frame.fit=contain",
4916
- "frame.padding=0",
4917
- "frame.radius=0",
4918
- "frame.shadow=0",
4919
- "frame.border=0",
4920
- "frame.browserBar.kind=none",
4921
- "cursor.visible=false",
4922
- "cursor.clickFx.style=none",
4923
- ...opts.overrides?.set ?? []
4924
- ]
4925
- }
4926
- });
4927
- const raw = decodePng(new Uint8Array(await readFile6(shot.frames[0].file)));
4928
- if (!raw) throw new Error("the poster shot could not be decoded");
4929
- const PAD = 0.06;
4930
- const baked = bakeShot(raw, {
4931
- margin: PAD,
4932
- hairline: fill.lightGround ? 0.14 : 0,
4933
- shadow: fill.lightGround ? 0.28 : 0.4
4027
+ const captured = await framesTake(browser, ref.takeDir, {
4028
+ times: [time],
4029
+ width: d.px.w,
4030
+ height: d.px.h,
4031
+ outDir: shotDir,
4032
+ doc: ref.doc,
4033
+ overrides: opts.overrides
4934
4034
  });
4935
- await writeFile7(join6(serveDir, "shot.png"), encodePng(baked));
4936
- const shotAspect = raw.w / raw.h;
4937
- for (const { d, plan } of cardPlans) {
4938
- if (plan.stage) {
4939
- const stageInput = {
4940
- size: d.px,
4941
- values: fill.values,
4942
- sourceSeconds: meta.durationMs / 1e3,
4943
- outputSeconds: duration,
4944
- text: d.text,
4945
- footageAspect: (meta.captureWidth ?? meta.width) / (meta.captureHeight ?? meta.height)
4946
- };
4947
- const staged = plan.stage === "tile" ? stageTile(stageInput) : stageSplitCover(stageInput);
4948
- opts.onPhase?.(
4949
- `${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}`
4950
- );
4951
- const shotDir = await mkdtemp(join6(tmpdir(), "vos-stage-"));
4952
- try {
4953
- const captured = await framesTake(browser, dir, {
4954
- times: [heroTime],
4955
- width: d.px.w,
4956
- height: d.px.h,
4957
- outDir: shotDir,
4958
- overrides: {
4959
- ...opts.overrides,
4960
- set: [...staged.set, ...opts.overrides?.set ?? []]
4961
- }
4962
- });
4963
- const to2 = join6(outDir, `${d.id}.png`);
4964
- await rename4(captured.frames[0].file, to2);
4965
- const bytes2 = (await stat(to2)).size;
4966
- if (d.maxBytes !== void 0 && bytes2 > d.maxBytes) {
4967
- skipped.push(
4968
- `${d.channel} ${d.asset}: ${overCeiling(bytes2, d.maxBytes)} (kept at ${to2})`
4969
- );
4970
- continue;
4971
- }
4972
- assets.push({
4973
- channel: d.channel,
4974
- asset: d.asset,
4975
- destination: d.id,
4976
- path: relative(outDir, to2),
4977
- w: d.px.w,
4978
- h: d.px.h,
4979
- bytes: bytes2,
4980
- seconds: null,
4981
- frameTime: heroTime,
4982
- source: "stage",
4983
- template: `${plan.stage}-stage`,
4984
- text: staged.text,
4985
- shot: staged.shot,
4986
- ...plan.stage === "tile" ? { crop: true } : {}
4987
- });
4988
- } finally {
4989
- await rm3(shotDir, { recursive: true, force: true });
4990
- }
4991
- continue;
4992
- }
4993
- const problems = templateProblems(plan.config);
4994
- if (problems.length) {
4995
- skipped.push(
4996
- `${d.channel} ${d.asset}: ${plan.from} is not a valid template (${problems[0]}) \u2014 kept from the take`
4997
- );
4998
- posterCardIds.delete(d.id);
4999
- continue;
5000
- }
5001
- const filled = fillTemplate(plan.config, {
5002
- size: d.px,
5003
- slots: { shot: { src: "/shot.png", aspect: shotAspect, pad: PAD } },
5004
- values: fill.values
5005
- });
5006
- const limits = textLimitProblems(templateOf(plan.config), fill.values);
5007
- for (const l of limits) skipped.push(`note: ${d.id}: ${l}`);
5008
- if (filled.missing.length) {
5009
- skipped.push(
5010
- `${d.channel} ${d.asset}: ${plan.from} needs ${filled.missing.join(", ")} \u2014 kept from the take`
5011
- );
5012
- posterCardIds.delete(d.id);
5013
- continue;
5014
- }
5015
- const config = filled.config;
5016
- if (fill.fonts.length) {
5017
- const declared = Array.isArray(config.fonts) ? config.fonts : [];
5018
- config.fonts = [...declared, ...fill.fonts];
5019
- }
5020
- const posterDuration = typeof config.duration === "number" ? config.duration : 6;
5021
- const time = Math.min(
5022
- opts.posterTime ?? posterDuration * 0.9,
5023
- Math.max(0, posterDuration - 0.05)
5024
- );
5025
- opts.onPhase?.(
5026
- `${d.channel} ${d.asset} (${specWords(d)}) from ${plan.from}, ${filled.aspect}`
5027
- );
5028
- await renderPosterStills(
5029
- browser,
5030
- config,
5031
- serveDir,
5032
- [{ name: `${d.id}.png`, width: d.px.w, height: d.px.h }],
5033
- time
4035
+ const to = join7(outDir, `${d.id}.png`);
4036
+ await rename4(captured.frames[0].file, to);
4037
+ const bytes = (await stat(to)).size;
4038
+ if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
4039
+ skipped.push(
4040
+ `${label}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
5034
4041
  );
5035
- const from = join6(serveDir, `${d.id}.png`);
5036
- const to = join6(outDir, `${d.id}.png`);
5037
- await rename4(from, to);
5038
- const bytes = (await stat(to)).size;
5039
- if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
5040
- skipped.push(
5041
- `${d.channel} ${d.asset}: ${overCeiling(bytes, d.maxBytes)} (kept at ${to})`
5042
- );
5043
- continue;
5044
- }
5045
- assets.push({
5046
- channel: d.channel,
5047
- asset: d.asset,
5048
- destination: d.id,
5049
- path: relative(outDir, to),
5050
- w: d.px.w,
5051
- h: d.px.h,
5052
- bytes,
5053
- seconds: null,
5054
- frameTime: heroTime,
5055
- source: "poster",
5056
- template: templateOf(plan.config)?.family ?? plan.from,
5057
- text: filled.text,
5058
- shot: filled.slots.shot
5059
- });
4042
+ continue;
5060
4043
  }
4044
+ const shot = posterShotRect(ref.doc, d.px);
4045
+ assets.push({
4046
+ channel: d.channel,
4047
+ asset: d.asset,
4048
+ destination: d.id,
4049
+ path: relative(outDir, to),
4050
+ w: d.px.w,
4051
+ h: d.px.h,
4052
+ bytes,
4053
+ seconds: null,
4054
+ frameTime: time,
4055
+ source: "poster",
4056
+ poster: {
4057
+ class: cls,
4058
+ file: relative(dir, ref.file),
4059
+ ...ref.vosId ? { vosId: ref.vosId } : {}
4060
+ },
4061
+ text: posterTextBoxes(ref.doc, d.px, time),
4062
+ shot,
4063
+ ...shot.x < 0 && shot.x + shot.w > 1 ? { crop: true } : {}
4064
+ });
5061
4065
  } finally {
5062
- await rm3(serveDir, { recursive: true, force: true });
4066
+ await rm3(shotDir, { recursive: true, force: true });
5063
4067
  }
5064
4068
  }
4069
+ if (missing.size) {
4070
+ const classes = [...missing].sort().join(", ");
4071
+ skipped.push(
4072
+ `note: no poster document for the ${classes} class${missing.size > 1 ? "es" : ""} beside the take (poster/<class>/doc.json, or LAUNCH.md poster:), so those cards are the take's own frame`
4073
+ );
4074
+ }
5065
4075
  for (const d of destinations) {
5066
4076
  const label = `${d.channel} ${d.asset}`;
5067
4077
  const excuse = NOT_FROM_FOOTAGE[d.id];
@@ -5094,7 +4104,7 @@ async function deliverTake(browser, dir, opts) {
5094
4104
  }
5095
4105
  }
5096
4106
  opts.onPhase?.(`${label} (${specWords(d)})`);
5097
- const outFile = join6(outDir, `${d.id}.${d.format}`);
4107
+ const outFile = join7(outDir, `${d.id}.${d.format}`);
5098
4108
  const bitrate = d.maxBytes !== void 0 ? Math.min(
5099
4109
  1e7,
5100
4110
  Math.floor(d.maxBytes * 8 / seconds * 0.85)
@@ -5106,7 +4116,7 @@ async function deliverTake(browser, dir, opts) {
5106
4116
  parallel: opts.parallel,
5107
4117
  range,
5108
4118
  bitrate,
5109
- overrides: videoOverrides(d, range),
4119
+ overrides: videoOverrides(d),
5110
4120
  onProgress: opts.onProgress
5111
4121
  });
5112
4122
  if (d.maxBytes !== void 0 && result.bytes > d.maxBytes) {
@@ -5141,7 +4151,7 @@ async function deliverTake(browser, dir, opts) {
5141
4151
  for (let i = 0; i < captured.frames.length; i++) {
5142
4152
  const frame = captured.frames[i];
5143
4153
  const name = captured.frames.length > 1 ? `${d.id}-${i + 1}.png` : `${d.id}.png`;
5144
- const to = join6(outDir, name);
4154
+ const to = join7(outDir, name);
5145
4155
  await rename4(frame.file, to);
5146
4156
  const bytes = (await stat(to)).size;
5147
4157
  if (d.maxBytes !== void 0 && bytes > d.maxBytes) {
@@ -5163,25 +4173,135 @@ async function deliverTake(browser, dir, opts) {
5163
4173
  ...d.genre === "screenshot" && opts.composed ? { composed: true } : {}
5164
4174
  });
5165
4175
  }
5166
- if (d.count && captured.frames.length < d.count.min) {
5167
- skipped.push(
5168
- `${label}: spec wants ${d.count.min}-${d.count.max}, only ${captured.frames.length} still time(s) available \u2014 pass --times`
4176
+ if (d.count && captured.frames.length < d.count.min) {
4177
+ skipped.push(
4178
+ `${label}: spec wants ${d.count.min}-${d.count.max}, only ${captured.frames.length} still time(s) available \u2014 pass --times`
4179
+ );
4180
+ }
4181
+ }
4182
+ const kit = {
4183
+ release: opts.release ?? null,
4184
+ look: opts.look?.kind ?? null,
4185
+ take: dir,
4186
+ produced: (/* @__PURE__ */ new Date()).toISOString(),
4187
+ specsVerified: CHANNEL_SPECS_VERIFIED,
4188
+ moments,
4189
+ skipped,
4190
+ assets
4191
+ };
4192
+ const kitFile = join7(outDir, "kit.json");
4193
+ await writeFile6(kitFile, JSON.stringify(kit, null, 2));
4194
+ return { kit, kitFile, outDir };
4195
+ }
4196
+
4197
+ // src/plugin/markAsset.ts
4198
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
4199
+ import { existsSync as existsSync7 } from "fs";
4200
+ import { isAbsolute as isAbsolute2, join as join8, resolve as resolve5 } from "path";
4201
+ function markExtension(url) {
4202
+ const m = /\.(svg|png|webp|jpe?g)(?:[?#]|$)/i.exec(url);
4203
+ if (!m) return null;
4204
+ const e = m[1].toLowerCase();
4205
+ return e === "jpeg" ? "jpg" : e;
4206
+ }
4207
+ function imageAspect(bytes, ext) {
4208
+ if (ext === "svg") {
4209
+ const head = new TextDecoder().decode(bytes.subarray(0, 4096));
4210
+ const vb = /viewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(
4211
+ head
4212
+ );
4213
+ if (vb) return ratio(+vb[1], +vb[2]);
4214
+ const w = /<svg[^>]*\swidth\s*=\s*["']([\d.]+)/i.exec(head);
4215
+ const h = /<svg[^>]*\sheight\s*=\s*["']([\d.]+)/i.exec(head);
4216
+ if (w && h) return ratio(+w[1], +h[1]);
4217
+ return null;
4218
+ }
4219
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4220
+ if (ext === "png") {
4221
+ if (bytes.length < 24 || bytes[0] !== 137 || bytes[1] !== 80) return null;
4222
+ return ratio(dv.getUint32(16), dv.getUint32(20));
4223
+ }
4224
+ if (ext === "webp") {
4225
+ if (bytes.length < 30) return null;
4226
+ const tag = String.fromCharCode(...bytes.subarray(12, 16));
4227
+ if (tag === "VP8X")
4228
+ return ratio(
4229
+ 1 + (dv.getUint32(24, true) & 16777215),
4230
+ 1 + (dv.getUint32(27, true) & 16777215)
4231
+ );
4232
+ if (tag === "VP8L") {
4233
+ const b = dv.getUint32(21, true);
4234
+ return ratio(1 + (b & 16383), 1 + (b >> 14 & 16383));
4235
+ }
4236
+ if (tag === "VP8 ")
4237
+ return ratio(
4238
+ dv.getUint16(26, true) & 16383,
4239
+ dv.getUint16(28, true) & 16383
4240
+ );
4241
+ return null;
4242
+ }
4243
+ if (ext === "jpg") {
4244
+ let i = 2;
4245
+ while (i + 9 < bytes.length) {
4246
+ if (bytes[i] !== 255) return null;
4247
+ const marker = bytes[i + 1];
4248
+ const len = dv.getUint16(i + 2);
4249
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
4250
+ return ratio(dv.getUint16(i + 7), dv.getUint16(i + 5));
4251
+ }
4252
+ i += 2 + len;
4253
+ }
4254
+ return null;
4255
+ }
4256
+ return null;
4257
+ }
4258
+ function ratio(w, h) {
4259
+ return w > 0 && h > 0 ? w / h : null;
4260
+ }
4261
+ async function fetchBrandMarks(takeDir, roles) {
4262
+ const out = { light: null, dark: null, notes: [] };
4263
+ if (!roles) return out;
4264
+ const one = async (role, name) => {
4265
+ const src = (roles[role] ?? "").trim();
4266
+ if (!src) return null;
4267
+ const ext = markExtension(src);
4268
+ if (!ext) {
4269
+ out.notes.push(
4270
+ `${role}: ${src} is not an svg, png, webp or jpg; the wordmark stands in`
4271
+ );
4272
+ return null;
4273
+ }
4274
+ let bytes;
4275
+ try {
4276
+ if (/^https?:/i.test(src)) {
4277
+ const res = await fetch(src);
4278
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
4279
+ bytes = new Uint8Array(await res.arrayBuffer());
4280
+ } else {
4281
+ const file = isAbsolute2(src) ? src : resolve5(takeDir, src);
4282
+ if (!existsSync7(file)) throw new Error("no such file");
4283
+ bytes = new Uint8Array(await readFile7(file));
4284
+ }
4285
+ } catch (e) {
4286
+ out.notes.push(
4287
+ `${role}: ${src} could not be read (${e instanceof Error ? e.message : String(e)}); the wordmark stands in`
4288
+ );
4289
+ return null;
4290
+ }
4291
+ const aspect = imageAspect(bytes, ext);
4292
+ if (!aspect) {
4293
+ out.notes.push(
4294
+ `${role}: ${src} has no readable size; the wordmark stands in`
5169
4295
  );
4296
+ return null;
5170
4297
  }
5171
- }
5172
- const kit = {
5173
- release: opts.release ?? null,
5174
- look: opts.look?.kind ?? null,
5175
- take: dir,
5176
- produced: (/* @__PURE__ */ new Date()).toISOString(),
5177
- specsVerified: CHANNEL_SPECS_VERIFIED,
5178
- moments,
5179
- skipped,
5180
- assets
4298
+ await mkdir5(join8(takeDir, "brand"), { recursive: true });
4299
+ await writeFile7(join8(takeDir, "brand", `${name}.${ext}`), bytes);
4300
+ return { key: `/brand/${name}.${ext}`, aspect };
5181
4301
  };
5182
- const kitFile = join6(outDir, "kit.json");
5183
- await writeFile7(kitFile, JSON.stringify(kit, null, 2));
5184
- return { kit, kitFile, outDir };
4302
+ out.light = await one("logoUrl", "mark");
4303
+ out.dark = await one("logoOnDarkUrl", "mark-on-dark");
4304
+ return out;
5185
4305
  }
5186
4306
 
5187
4307
  // src/plugin/music.ts
@@ -5197,10 +4317,69 @@ async function fetchMusicCatalog(origin) {
5197
4317
  }
5198
4318
 
5199
4319
  // src/plugin/judge.ts
5200
- import { existsSync as existsSync7 } from "fs";
5201
- import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
5202
- import { dirname, isAbsolute as isAbsolute2, join as join7, resolve as resolve5 } from "path";
4320
+ import { existsSync as existsSync8 } from "fs";
4321
+ import { mkdir as mkdir6, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
4322
+ import { dirname, isAbsolute as isAbsolute3, join as join9, resolve as resolve6 } from "path";
5203
4323
  import { DESTINATIONS as DESTINATIONS2 } from "@vosjs/studio-core";
4324
+
4325
+ // src/plugin/shotBake.ts
4326
+ import { deflateSync } from "zlib";
4327
+ var crcTable = (() => {
4328
+ const t = new Uint32Array(256);
4329
+ for (let n = 0; n < 256; n++) {
4330
+ let c = n;
4331
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
4332
+ t[n] = c >>> 0;
4333
+ }
4334
+ return t;
4335
+ })();
4336
+ function crc32(buf) {
4337
+ let c = 4294967295;
4338
+ for (const b of buf) c = crcTable[(c ^ b) & 255] ^ c >>> 8;
4339
+ return (c ^ 4294967295) >>> 0;
4340
+ }
4341
+ function chunk(type, body) {
4342
+ const out = new Uint8Array(12 + body.length);
4343
+ const dv = new DataView(out.buffer);
4344
+ dv.setUint32(0, body.length);
4345
+ for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
4346
+ out.set(body, 8);
4347
+ dv.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
4348
+ return out;
4349
+ }
4350
+ function encodePng(img) {
4351
+ const stride = img.w * 4;
4352
+ const raw = new Uint8Array((stride + 1) * img.h);
4353
+ for (let y = 0; y < img.h; y++) {
4354
+ raw[y * (stride + 1)] = 2;
4355
+ for (let i = 0; i < stride; i++) {
4356
+ const cur = img.data[y * stride + i];
4357
+ const up = y ? img.data[(y - 1) * stride + i] : 0;
4358
+ raw[y * (stride + 1) + 1 + i] = cur - up & 255;
4359
+ }
4360
+ }
4361
+ const ihdr = new Uint8Array(13);
4362
+ const dv = new DataView(ihdr.buffer);
4363
+ dv.setUint32(0, img.w);
4364
+ dv.setUint32(4, img.h);
4365
+ ihdr[8] = 8;
4366
+ ihdr[9] = 6;
4367
+ const parts = [
4368
+ new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
4369
+ chunk("IHDR", ihdr),
4370
+ chunk("IDAT", new Uint8Array(deflateSync(raw))),
4371
+ chunk("IEND", new Uint8Array(0))
4372
+ ];
4373
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
4374
+ let o = 0;
4375
+ for (const p of parts) {
4376
+ out.set(p, o);
4377
+ o += p.length;
4378
+ }
4379
+ return out;
4380
+ }
4381
+
4382
+ // src/plugin/judge.ts
5204
4383
  var RUBRIC = [
5205
4384
  "A plate, and room on it: the subject sits at 74 to 89% of the width with headroom; nobody bleeds a window on all four sides.",
5206
4385
  "The window is an object: rounded corners and a shadow with weight (a soft wide one on a light plate, a tight glow on a dark one).",
@@ -5303,26 +4482,26 @@ function composeSheet(left, right, height = 540, gutter = 32) {
5303
4482
  return { w, h, data: out };
5304
4483
  }
5305
4484
  async function judgeKit(kitPath, manifestPath, outDir) {
5306
- const kit = JSON.parse(await readFile7(kitPath, "utf8"));
5307
- const manifest = JSON.parse(await readFile7(manifestPath, "utf8"));
5308
- const manifestDir = dirname(resolve5(manifestPath));
5309
- const kitDir = dirname(resolve5(kitPath));
5310
- const out = resolve5(outDir ?? join7(kitDir, "judge"));
4485
+ const kit = JSON.parse(await readFile8(kitPath, "utf8"));
4486
+ const manifest = JSON.parse(await readFile8(manifestPath, "utf8"));
4487
+ const manifestDir = dirname(resolve6(manifestPath));
4488
+ const kitDir = dirname(resolve6(kitPath));
4489
+ const out = resolve6(outDir ?? join9(kitDir, "judge"));
5311
4490
  await mkdir6(out, { recursive: true });
5312
4491
  const sheets = [];
5313
4492
  const skipped = [];
5314
4493
  const refCache = /* @__PURE__ */ new Map();
5315
4494
  const loadRef = async (file) => {
5316
4495
  if (refCache.has(file)) return refCache.get(file) ?? null;
5317
- const p = join7(manifestDir, file);
5318
- const img = existsSync7(p) ? decodePng(new Uint8Array(await readFile7(p))) : null;
4496
+ const p = join9(manifestDir, file);
4497
+ const img = existsSync8(p) ? decodePng(new Uint8Array(await readFile8(p))) : null;
5319
4498
  refCache.set(file, img);
5320
4499
  return img;
5321
4500
  };
5322
4501
  for (const a of kit.assets) {
5323
4502
  if (!/\.png$/i.test(a.path)) continue;
5324
- const file = isAbsolute2(a.path) && existsSync7(a.path) ? a.path : join7(kitDir, a.path.split("/").pop() ?? a.path);
5325
- const img = existsSync7(file) ? decodePng(new Uint8Array(await readFile7(file))) : null;
4503
+ const file = isAbsolute3(a.path) && existsSync8(a.path) ? a.path : join9(kitDir, a.path.split("/").pop() ?? a.path);
4504
+ const img = existsSync8(file) ? decodePng(new Uint8Array(await readFile8(file))) : null;
5326
4505
  if (!img) {
5327
4506
  skipped.push(`${a.destination ?? a.path}: unreadable`);
5328
4507
  continue;
@@ -5342,8 +4521,8 @@ async function judgeKit(kitPath, manifestPath, outDir) {
5342
4521
  const id = stem.replace(/[^A-Za-z0-9_-]+/g, "-") || a.destination || "asset";
5343
4522
  const nameA = `${id}--A.png`;
5344
4523
  const nameB = `${id}--B.png`;
5345
- await writeFile8(join7(out, nameA), encodePng(composeSheet(img, refImg)));
5346
- await writeFile8(join7(out, nameB), encodePng(composeSheet(refImg, img)));
4524
+ await writeFile8(join9(out, nameA), encodePng(composeSheet(img, refImg)));
4525
+ await writeFile8(join9(out, nameB), encodePng(composeSheet(refImg, img)));
5347
4526
  const rubric = [
5348
4527
  `# ${id} against ${ref.id} (${ref.file})`,
5349
4528
  "",
@@ -5363,11 +4542,11 @@ async function judgeKit(kitPath, manifestPath, outDir) {
5363
4542
  "In judge.json: `win` true when the kit asset is at least as good as the reference on the rules that apply to its genre, false when it is worse, null for a tie; `reasons` names the rules by number.",
5364
4543
  ""
5365
4544
  ].join("\n");
5366
- await writeFile8(join7(out, `${id}.md`), rubric);
4545
+ await writeFile8(join9(out, `${id}.md`), rubric);
5367
4546
  sheets.push({ asset: id, reference: ref.id, sheetA: nameA, sheetB: nameB, rubric: `${id}.md` });
5368
4547
  }
5369
- const verdictFile = join7(out, "judge.json");
5370
- if (!existsSync7(verdictFile)) {
4548
+ const verdictFile = join9(out, "judge.json");
4549
+ if (!existsSync8(verdictFile)) {
5371
4550
  await writeFile8(
5372
4551
  verdictFile,
5373
4552
  JSON.stringify(
@@ -5394,7 +4573,7 @@ function winRate(verdicts) {
5394
4573
  // src/plugin/kitPicture.ts
5395
4574
  import { execFile } from "child_process";
5396
4575
  import { promisify } from "util";
5397
- import { readFile as readFile8 } from "fs/promises";
4576
+ import { readFile as readFile9 } from "fs/promises";
5398
4577
  var execFileP = promisify(execFile);
5399
4578
  var SUBJECT_BAND = { min: 0.6, max: 0.92 };
5400
4579
  var BLANK_INK = 0.12;
@@ -5571,10 +4750,10 @@ function textFindings(a, img) {
5571
4750
  });
5572
4751
  }
5573
4752
  }
5574
- const rgb2 = t.color ? hexToRgb(t.color) : null;
5575
- if (rgb2) {
4753
+ const rgb = t.color ? hexToRgb(t.color) : null;
4754
+ if (rgb) {
5576
4755
  const ground = medianColour(img, box);
5577
- const lc = apcaContrast(rgb2, ground);
4756
+ const lc = apcaContrast(rgb, ground);
5578
4757
  const floor = t.role === "body" ? 75 : 60;
5579
4758
  if (lc < floor) {
5580
4759
  out.push({
@@ -5625,7 +4804,7 @@ function duplicateFindings(stills) {
5625
4804
  severity: composed ? "info" : g.length >= 3 ? "error" : "warning",
5626
4805
  asset: g.map((s) => s.destination).join(", "),
5627
4806
  message: composed ? `${g.length} channels share one cover: ${g.map((s) => s.destination).join(", ")}` : `${g.length} assets share one frame${g[0].time !== null ? ` (${g[0].time.toFixed(2)}s)` : ""}: ${g.map((s) => s.destination).join(", ")}`,
5628
- fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster template composes each card differently from one shot"
4807
+ fixHint: "a kit is many moments: let deliver pick from the step timeline, or pass --times with one step per still; a poster document per class (poster/<class>/doc.json) or per destination (LAUNCH.md poster roles) gives each channel its own cover"
5629
4808
  };
5630
4809
  });
5631
4810
  }
@@ -5670,7 +4849,7 @@ async function pictureChecks(assets) {
5670
4849
  let ffmpeg = null;
5671
4850
  for (const a of assets) {
5672
4851
  if (/\.png$/i.test(a.file)) {
5673
- const img = decodePng(new Uint8Array(await readFile8(a.file)));
4852
+ const img = decodePng(new Uint8Array(await readFile9(a.file)));
5674
4853
  if (!img) {
5675
4854
  findings.push({
5676
4855
  code: "unreadable",
@@ -5763,9 +4942,9 @@ function formatFinding(f) {
5763
4942
  }
5764
4943
 
5765
4944
  // src/plugin/validateKit.ts
5766
- import { existsSync as existsSync8 } from "fs";
5767
- import { readFile as readFile9, stat as stat2 } from "fs/promises";
5768
- import { dirname as dirname2, isAbsolute as isAbsolute3, join as join8 } from "path";
4945
+ import { existsSync as existsSync9 } from "fs";
4946
+ import { readFile as readFile10, stat as stat2 } from "fs/promises";
4947
+ import { dirname as dirname2, isAbsolute as isAbsolute4, join as join10 } from "path";
5769
4948
  import { DESTINATIONS as DESTINATIONS3 } from "@vosjs/studio-core";
5770
4949
  var PNG_SIG2 = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
5771
4950
  function pngDimensions(bytes) {
@@ -5784,7 +4963,7 @@ async function probeVideo(path) {
5784
4963
  const MB = await import("mediabunny");
5785
4964
  const input = new MB.Input({
5786
4965
  formats: MB.ALL_FORMATS,
5787
- source: new MB.BufferSource(new Uint8Array(await readFile9(path)))
4966
+ source: new MB.BufferSource(new Uint8Array(await readFile10(path)))
5788
4967
  });
5789
4968
  try {
5790
4969
  const track = await input.getPrimaryVideoTrack();
@@ -5805,7 +4984,7 @@ async function validateKit(kitPath, opts = {}) {
5805
4984
  const measured = [];
5806
4985
  let kit;
5807
4986
  try {
5808
- kit = JSON.parse(await readFile9(kitPath, "utf8"));
4987
+ kit = JSON.parse(await readFile10(kitPath, "utf8"));
5809
4988
  } catch (e) {
5810
4989
  return {
5811
4990
  valid: false,
@@ -5824,8 +5003,8 @@ async function validateKit(kitPath, opts = {}) {
5824
5003
  }
5825
5004
  const base = dirname2(kitPath);
5826
5005
  const resolvePath = (p) => {
5827
- if (isAbsolute3(p) && existsSync8(p)) return p;
5828
- const beside = join8(base, p.split("/").pop() ?? p);
5006
+ if (isAbsolute4(p) && existsSync9(p)) return p;
5007
+ const beside = join10(base, p.split("/").pop() ?? p);
5829
5008
  return beside;
5830
5009
  };
5831
5010
  const perDestination = /* @__PURE__ */ new Map();
@@ -5851,7 +5030,7 @@ async function validateKit(kitPath, opts = {}) {
5851
5030
  let h = null;
5852
5031
  let seconds = null;
5853
5032
  if (/\.(png|jpe?g|webp)$/i.test(file)) {
5854
- const head = Buffer.from(await readFile9(file));
5033
+ const head = Buffer.from(await readFile10(file));
5855
5034
  const kind = sniffImage(head);
5856
5035
  if (file.toLowerCase().endsWith(".png") && kind !== "png")
5857
5036
  problems.push(
@@ -5899,228 +5078,69 @@ async function validateKit(kitPath, opts = {}) {
5899
5078
  pictureAssets.push({
5900
5079
  destination: id,
5901
5080
  path: a.path,
5902
- file,
5903
- spec,
5904
- text: a.text,
5905
- seconds,
5906
- composed: a.composed,
5907
- shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
5908
- crop: a.crop
5909
- });
5910
- if (!spec) {
5911
- if (a.channel !== "demo")
5912
- warnings.push(
5913
- `${label}: no channel spec for "${id}" \u2014 not verified against one`
5914
- );
5915
- continue;
5916
- }
5917
- if (w !== null && h !== null && (w !== spec.px.w || h !== spec.px.h))
5918
- problems.push(
5919
- `${label}: spec wants ${spec.px.w}x${spec.px.h}, the file is ${w}x${h}`
5920
- );
5921
- if (spec.maxBytes !== void 0 && bytes > spec.maxBytes)
5922
- problems.push(
5923
- `${label}: ${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(spec.maxBytes / 1024)} KB ceiling`
5924
- );
5925
- if (seconds !== null) {
5926
- if (spec.minSeconds !== void 0 && seconds < spec.minSeconds - 0.25)
5927
- problems.push(
5928
- `${label}: spec wants at least ${spec.minSeconds} s, the file is ${seconds.toFixed(1)} s`
5929
- );
5930
- if (spec.maxSeconds !== void 0 && seconds > spec.maxSeconds + 0.25)
5931
- problems.push(
5932
- `${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
5933
- );
5934
- }
5935
- if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
5936
- warnings.push(
5937
- `${label}: spec renders png, the file is ${file.split(".").pop()}`
5938
- );
5939
- }
5940
- for (const [id, n] of perDestination) {
5941
- const spec = specById.get(id);
5942
- if (spec?.count && (n < spec.count.min || n > spec.count.max))
5943
- problems.push(
5944
- `${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
5945
- );
5946
- }
5947
- if (!opts.picture) {
5948
- return { valid: problems.length === 0, problems, warnings, measured };
5949
- }
5950
- const picture = await pictureChecks(pictureAssets);
5951
- const pictureErrors = picture.findings.filter((f) => f.severity === "error");
5952
- return {
5953
- valid: problems.length === 0 && pictureErrors.length === 0,
5954
- problems,
5955
- warnings,
5956
- measured,
5957
- picture: picture.findings,
5958
- pictureMeasured: picture.measured
5959
- };
5960
- }
5961
-
5962
- // src/plugin/platform.ts
5963
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5964
- import { homedir } from "os";
5965
- import { join as join9 } from "path";
5966
- function platformOrigin(flags = {}) {
5967
- const legacyEnv = process.env.VOS_API_BASE?.trim();
5968
- const raw = flags.origin ?? flags.api ?? process.env.VOS_ORIGIN?.trim() ?? legacyEnv ?? "https://vos.so";
5969
- if (!flags.origin && !flags.api && !process.env.VOS_ORIGIN && legacyEnv) {
5970
- process.stderr.write(
5971
- "note: VOS_API_BASE is deprecated \u2014 set VOS_ORIGIN (an origin, no /api suffix)\n"
5972
- );
5973
- }
5974
- return raw.replace(/\/+$/, "").replace(/\/api$/, "");
5975
- }
5976
- function parseVosId(input) {
5977
- if (!/^https?:\/\//.test(input)) {
5978
- if (/^[A-Za-z0-9_-]+$/.test(input)) return input;
5979
- throw new UsageError(`"${input}" is not a vos id or URL`);
5980
- }
5981
- let url;
5982
- try {
5983
- url = new URL(input);
5984
- } catch {
5985
- throw new UsageError(`"${input}" is not a valid URL`);
5986
- }
5987
- const query = url.searchParams.get("vos");
5988
- if (query) return query;
5989
- const path = url.pathname.match(/\/vos\/([A-Za-z0-9_-]+)/);
5990
- if (path) return path[1];
5991
- throw new UsageError(
5992
- `could not find a vos id in ${input} \u2014 expected /vos/{id} or ?vos={id}`
5993
- );
5994
- }
5995
- function deriveSlug(title) {
5996
- return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "") || "remix";
5997
- }
5998
- var CREDENTIALS_PATH = join9(homedir(), ".config", "vos", "credentials");
5999
- function resolveCredential(explicit) {
6000
- const flag = explicit?.trim();
6001
- if (flag) return flag;
6002
- const env = process.env.VOS_API_KEY?.trim();
6003
- if (env) return env;
6004
- try {
6005
- const first = readFileSync2(CREDENTIALS_PATH, "utf8").split("\n")[0].trim();
6006
- return first || null;
6007
- } catch {
6008
- return null;
6009
- }
6010
- }
6011
- function requireCredential(explicit) {
6012
- const key = resolveCredential(explicit);
6013
- if (!key) {
6014
- throw new Error(
6015
- "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)"
6016
- );
6017
- }
6018
- return key;
6019
- }
6020
- function writeCredential(key) {
6021
- mkdirSync(join9(homedir(), ".config", "vos"), { recursive: true, mode: 448 });
6022
- writeFileSync(CREDENTIALS_PATH, `${key.trim()}
6023
- `, { mode: 384 });
6024
- return CREDENTIALS_PATH;
6025
- }
6026
- function clientId() {
6027
- const env = process.env.VOS_CLIENT?.trim();
6028
- if (env && env.length <= 60 && !/[\r\n]/.test(env)) return env;
6029
- return "vos-cli";
6030
- }
6031
- async function apiJson(origin, path, init = {}) {
6032
- const headers = {
6033
- accept: "application/json",
6034
- ...init.headers
6035
- };
6036
- if (init.key) headers.authorization = `Bearer ${init.key}`;
6037
- if (init.body !== void 0) headers["content-type"] = "application/json";
6038
- const reqBody = init.raw !== void 0 ? init.raw : init.body === void 0 ? void 0 : JSON.stringify(init.body);
6039
- const res = await fetch(`${origin}${path}`, {
6040
- method: init.method ?? "GET",
6041
- headers,
6042
- body: reqBody
6043
- });
6044
- let body = {};
6045
- try {
6046
- body = await res.json();
6047
- } catch {
6048
- }
6049
- return { status: res.status, body };
6050
- }
6051
- function apiError(what, r) {
6052
- const detail = typeof r.body.error === "string" ? r.body.error : r.body.details !== void 0 ? JSON.stringify(r.body.details) : "";
6053
- const hint = r.status === 401 ? " (run `vos login`, or set VOS_API_KEY \u2014 mint a key at https://vos.so/app/api)" : "";
6054
- return `${what} \u2192 ${r.status}${detail ? `: ${detail}` : ""}${hint}`;
6055
- }
6056
- var SYNC_STATE_NAME = "vos.json";
6057
- function readJsonFile(file) {
6058
- try {
6059
- return JSON.parse(readFileSync2(file, "utf8"));
6060
- } catch {
6061
- return null;
6062
- }
6063
- }
6064
- function readSyncState(dir) {
6065
- const own = readJsonFile(join9(dir, SYNC_STATE_NAME));
6066
- if (own && typeof own.vosId === "string") {
6067
- return {
6068
- vosId: own.vosId,
6069
- versionId: typeof own.versionId === "string" ? own.versionId : null,
6070
- ...typeof own.pushedAt === "string" ? { pushedAt: own.pushedAt } : {},
6071
- ...typeof own.title === "string" ? { title: own.title } : {},
6072
- ...typeof own.slug === "string" ? { slug: own.slug } : {},
6073
- ...typeof own.remixOfId === "string" ? { remixOfId: own.remixOfId } : {}
6074
- };
5081
+ file,
5082
+ spec,
5083
+ text: a.text,
5084
+ seconds,
5085
+ composed: a.composed,
5086
+ shot: a.source === "poster" || a.source === "stage" ? a.shot : void 0,
5087
+ crop: a.crop
5088
+ });
5089
+ if (!spec) {
5090
+ if (a.channel !== "demo")
5091
+ warnings.push(
5092
+ `${label}: no channel spec for "${id}" \u2014 not verified against one`
5093
+ );
5094
+ continue;
5095
+ }
5096
+ if (w !== null && h !== null && (w !== spec.px.w || h !== spec.px.h))
5097
+ problems.push(
5098
+ `${label}: spec wants ${spec.px.w}x${spec.px.h}, the file is ${w}x${h}`
5099
+ );
5100
+ if (spec.maxBytes !== void 0 && bytes > spec.maxBytes)
5101
+ problems.push(
5102
+ `${label}: ${Math.ceil(bytes / 1024)} KB exceeds the ${Math.floor(spec.maxBytes / 1024)} KB ceiling`
5103
+ );
5104
+ if (seconds !== null) {
5105
+ if (spec.minSeconds !== void 0 && seconds < spec.minSeconds - 0.25)
5106
+ problems.push(
5107
+ `${label}: spec wants at least ${spec.minSeconds} s, the file is ${seconds.toFixed(1)} s`
5108
+ );
5109
+ if (spec.maxSeconds !== void 0 && seconds > spec.maxSeconds + 0.25)
5110
+ problems.push(
5111
+ `${label}: spec caps at ${spec.maxSeconds} s, the file is ${seconds.toFixed(1)} s`
5112
+ );
5113
+ }
5114
+ if (spec.format === "png" && !file.toLowerCase().endsWith(".png"))
5115
+ warnings.push(
5116
+ `${label}: spec renders png, the file is ${file.split(".").pop()}`
5117
+ );
6075
5118
  }
6076
- const push = readJsonFile(join9(dir, "push.json"));
6077
- if (push && typeof push.vosId === "string") {
6078
- return {
6079
- vosId: push.vosId,
6080
- versionId: typeof push.versionId === "string" ? push.versionId : null,
6081
- ...typeof push.pushedAt === "string" ? { pushedAt: push.pushedAt } : {}
6082
- };
5119
+ for (const [id, n] of perDestination) {
5120
+ const spec = specById.get(id);
5121
+ if (spec?.count && (n < spec.count.min || n > spec.count.max))
5122
+ problems.push(
5123
+ `${spec.channel} ${spec.asset}: spec wants ${spec.count.min}-${spec.count.max}, the kit has ${n}`
5124
+ );
6083
5125
  }
6084
- const meta = readJsonFile(join9(dir, "meta.json"));
6085
- if (meta && typeof meta.id === "string") {
6086
- return {
6087
- vosId: meta.id,
6088
- versionId: typeof meta.currentVersionId === "string" ? meta.currentVersionId : null,
6089
- ...typeof meta.title === "string" ? { title: meta.title } : {},
6090
- ...typeof meta.slug === "string" ? { slug: meta.slug } : {},
6091
- ...typeof meta.remixOfId === "string" ? { remixOfId: meta.remixOfId } : {}
6092
- };
5126
+ if (!opts.picture) {
5127
+ return { valid: problems.length === 0, problems, warnings, measured };
6093
5128
  }
6094
- return null;
6095
- }
6096
- function writeSyncState(dir, patch) {
6097
- const merged = {
6098
- ...readSyncState(dir) ?? { versionId: null },
6099
- ...patch
5129
+ const picture = await pictureChecks(pictureAssets);
5130
+ const pictureErrors = picture.findings.filter((f) => f.severity === "error");
5131
+ return {
5132
+ valid: problems.length === 0 && pictureErrors.length === 0,
5133
+ problems,
5134
+ warnings,
5135
+ measured,
5136
+ picture: picture.findings,
5137
+ pictureMeasured: picture.measured
6100
5138
  };
6101
- writeFileSync(
6102
- join9(dir, SYNC_STATE_NAME),
6103
- `${JSON.stringify(merged, null, 2)}
6104
- `
6105
- );
6106
- return merged;
6107
- }
6108
- function formatChanges(changes) {
6109
- const lines = [];
6110
- for (const ch of changes) {
6111
- const label = ch.label ? ` \xB7 ${ch.label}` : "";
6112
- const who = ch.origin === "studio" ? "human" : ch.origin ?? "unknown";
6113
- lines.push(
6114
- `v${String(ch.versionNumber ?? "?")} (${who}${label}): ${ch.summary ?? ""}`
6115
- );
6116
- if (ch.note) lines.push(` note: ${ch.note}`);
6117
- }
6118
- return lines;
6119
5139
  }
6120
5140
 
6121
5141
  // src/plugin/recorder.ts
6122
5142
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
6123
- import { join as join10 } from "path";
5143
+ import { join as join11 } from "path";
6124
5144
 
6125
5145
  // src/plugin/recordingCap.ts
6126
5146
  var HOSTED_RECORDING_CAP_SECONDS = 30 * 60;
@@ -6208,7 +5228,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6208
5228
  cdp.on("Page.screencastFrame", (ev) => {
6209
5229
  const tsMs = ev.metadata.timestamp ? ev.metadata.timestamp * 1e3 : Date.now();
6210
5230
  const file = `frame-${String(frameIdx++).padStart(5, "0")}.jpg`;
6211
- writeFileSync2(join10(paths.framesDir, file), Buffer.from(ev.data, "base64"));
5231
+ writeFileSync2(join11(paths.framesDir, file), Buffer.from(ev.data, "base64"));
6212
5232
  frames.push({ file, tMs: Math.max(0, Math.round(tsMs - t0)) });
6213
5233
  cdp.send("Page.screencastFrameAck", { sessionId: ev.sessionId }).catch(() => {
6214
5234
  });
@@ -6431,7 +5451,7 @@ async function recordTake(browser, url, actions, paths, log, opts = {}) {
6431
5451
  await sleep(200);
6432
5452
  const pageTitle = await page.title().catch(() => "");
6433
5453
  await context.close();
6434
- const firstFrame = frames[0] ? jpegDims(readFileSync3(join10(paths.framesDir, frames[0].file))) : null;
5454
+ const firstFrame = frames[0] ? jpegDims(readFileSync3(join11(paths.framesDir, frames[0].file))) : null;
6435
5455
  const meta = {
6436
5456
  dpr: 1,
6437
5457
  zoom: 1,
@@ -6539,14 +5559,17 @@ async function encodeRecording(browser, takeDir, onProgress) {
6539
5559
  }
6540
5560
 
6541
5561
  // src/plugin/plan.ts
6542
- import { existsSync as existsSync9 } from "fs";
6543
- import { readFile as readFile10 } from "fs/promises";
6544
- import { join as join11 } from "path";
5562
+ import { existsSync as existsSync10 } from "fs";
5563
+ import { readFile as readFile11 } from "fs/promises";
5564
+ import { join as join12 } from "path";
6545
5565
  import {
6546
5566
  DEFAULT_FRAME_STYLE,
6547
5567
  STYLE_FIELDS,
5568
+ copyLayout,
6548
5569
  copyStyle,
6549
5570
  isRejected,
5571
+ isStageClip,
5572
+ layoutOf,
6550
5573
  planAutoSpeed,
6551
5574
  planAutoZoom,
6552
5575
  projectFromArtifact,
@@ -6734,15 +5757,36 @@ function retimeCut(prev, newSteps, newDurationMs) {
6734
5757
  // src/plugin/plan.ts
6735
5758
  var overlaps = (a, b) => a.in < b.out && b.in < a.out;
6736
5759
  async function readDigestActivity(dir) {
6737
- const file = join11(dir, "digest", "digest.json");
6738
- if (!existsSync9(file)) return null;
5760
+ const file = join12(dir, "digest", "digest.json");
5761
+ if (!existsSync10(file)) return null;
6739
5762
  try {
6740
- const d = JSON.parse(await readFile10(file, "utf8"));
5763
+ const d = JSON.parse(await readFile11(file, "utf8"));
6741
5764
  return Array.isArray(d.activity) && d.activity.every((v) => typeof v === "number") ? d.activity : null;
6742
5765
  } catch {
6743
5766
  return null;
6744
5767
  }
6745
5768
  }
5769
+ function patchStageWords(doc, words) {
5770
+ const map = {
5771
+ "stage-title": words.headline,
5772
+ "stage-kicker": words.kicker,
5773
+ "stage-brand": words.brand
5774
+ };
5775
+ for (const clip of doc.overlays ?? []) {
5776
+ if (clip.kind !== "text" || !isStageClip(clip)) continue;
5777
+ const v = map[clip.id];
5778
+ if (typeof v === "string" && v.trim()) clip.text = v.trim();
5779
+ }
5780
+ }
5781
+ function applyStyle(seed, doc, opts) {
5782
+ const parts = layoutOf(seed);
5783
+ const carries = parts.clips.length > 0 || parts.lean || parts.hold;
5784
+ if (!carries) return { doc: copyStyle(seed, doc), layout: void 0 };
5785
+ const keys = opts.mark ? { "stage-mark": opts.mark.key } : void 0;
5786
+ const { doc: next, notes } = copyLayout(seed, copyStyle(seed, doc), { keys });
5787
+ if (opts.words) patchStageWords(next, opts.words);
5788
+ return { doc: next, layout: { ...parts, notes } };
5789
+ }
6746
5790
  async function planTake(dir, opts = {}) {
6747
5791
  const take = await loadTake(dir);
6748
5792
  const { meta, cursor } = take;
@@ -6750,6 +5794,7 @@ async function planTake(dir, opts = {}) {
6750
5794
  const ingest = opts.backdrop ? { frame: withBackdrop(DEFAULT_FRAME_STYLE, opts.backdrop) } : {};
6751
5795
  let doc;
6752
5796
  let fresh;
5797
+ let layout;
6753
5798
  if (opts.reuse) {
6754
5799
  const prev = opts.reuse.doc;
6755
5800
  const artifact = {
@@ -6761,6 +5806,10 @@ async function planTake(dir, opts = {}) {
6761
5806
  doc = copyStyle(prev, doc);
6762
5807
  const rt = retimeCut(prev, meta.steps ?? [], meta.durationMs);
6763
5808
  doc.segments = rt.segments;
5809
+ const prevHold = prev.segments.at(-1)?.hold;
5810
+ const last = doc.segments.at(-1);
5811
+ if (typeof prevHold === "number" && prevHold > 0 && last)
5812
+ last.hold = prevHold;
6764
5813
  if (rt.rejected.length) doc.rejected = rt.rejected;
6765
5814
  const manualZoom = rt.zoom;
6766
5815
  const autoZoom = planAutoZoom(doc.source.cursor, {
@@ -6782,6 +5831,7 @@ async function planTake(dir, opts = {}) {
6782
5831
  if (prev.objects?.length) doc.objects = prev.objects;
6783
5832
  if (prev.audio.length) doc.audio = prev.audio;
6784
5833
  if (prev.camMotion?.length) doc.camMotion = prev.camMotion;
5834
+ if (prev.endCard) doc.endCard = structuredClone(prev.endCard);
6785
5835
  await writeJson(take.paths.doc, doc, true);
6786
5836
  return {
6787
5837
  doc,
@@ -6793,7 +5843,13 @@ async function planTake(dir, opts = {}) {
6793
5843
  };
6794
5844
  }
6795
5845
  if (take.doc) {
6796
- doc = opts.style ? copyStyle(opts.style.doc, take.doc) : take.doc;
5846
+ if (opts.style) {
5847
+ const applied = applyStyle(opts.style.doc, take.doc, opts);
5848
+ doc = applied.doc;
5849
+ layout = applied.layout;
5850
+ } else {
5851
+ doc = take.doc;
5852
+ }
6797
5853
  fresh = false;
6798
5854
  const manual = doc.zoom.filter((z) => z.source === "manual");
6799
5855
  const auto = planAutoZoom(doc.source.cursor, {
@@ -6817,7 +5873,11 @@ async function planTake(dir, opts = {}) {
6817
5873
  meta
6818
5874
  };
6819
5875
  doc = projectFromArtifact(artifact, RECORDING_NAME, ingest).doc;
6820
- if (opts.style) doc = copyStyle(opts.style.doc, doc);
5876
+ if (opts.style) {
5877
+ const applied = applyStyle(opts.style.doc, doc, opts);
5878
+ doc = applied.doc;
5879
+ layout = applied.layout;
5880
+ }
6821
5881
  doc.zoom = planAutoZoom(doc.source.cursor, {
6822
5882
  width: doc.source.meta.width,
6823
5883
  height: doc.source.meta.height,
@@ -6831,6 +5891,12 @@ async function planTake(dir, opts = {}) {
6831
5891
  });
6832
5892
  fresh = true;
6833
5893
  }
5894
+ let motion;
5895
+ if (opts.motion && (fresh || opts.motion.again)) {
5896
+ const proposed = proposeMotion(doc, opts.motion);
5897
+ doc = proposed.doc;
5898
+ motion = { notes: proposed.notes, skipped: proposed.skipped };
5899
+ }
6834
5900
  await writeJson(take.paths.doc, doc, true);
6835
5901
  return {
6836
5902
  doc,
@@ -6844,22 +5910,24 @@ async function planTake(dir, opts = {}) {
6844
5910
  styleFields: STYLE_FIELDS.filter(
6845
5911
  (k) => opts.style.doc[k] !== void 0
6846
5912
  )
6847
- } : {}
5913
+ } : {},
5914
+ ...layout ? { layout } : {},
5915
+ ...motion ? { motion } : {}
6848
5916
  };
6849
5917
  }
6850
5918
 
6851
5919
  // src/plugin/sync.ts
6852
5920
  import { createHash } from "crypto";
6853
- import { existsSync as existsSync11 } from "fs";
6854
- import { readFile as readFile11 } from "fs/promises";
5921
+ import { existsSync as existsSync12 } from "fs";
5922
+ import { readFile as readFile12 } from "fs/promises";
6855
5923
  import { createInterface } from "readline/promises";
6856
- import { basename, join as join14 } from "path";
6857
- import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
5924
+ import { basename, join as join15 } from "path";
5925
+ import { lowerToComposition as lowerToComposition3, migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
6858
5926
 
6859
5927
  // src/plugin/media.ts
6860
- import { createWriteStream, existsSync as existsSync10 } from "fs";
5928
+ import { createWriteStream, existsSync as existsSync11 } from "fs";
6861
5929
  import { writeFile as writeFile9 } from "fs/promises";
6862
- import { join as join12 } from "path";
5930
+ import { join as join13 } from "path";
6863
5931
  import { Readable } from "stream";
6864
5932
  import { pipeline } from "stream/promises";
6865
5933
  var MIC_NAME = "mic.webm";
@@ -6869,6 +5937,81 @@ var KEYS = [
6869
5937
  { key: "micKey", file: MIC_NAME },
6870
5938
  { key: "camKey", file: CAM_NAME }
6871
5939
  ];
5940
+ function isTakeRelativeKey(key) {
5941
+ return !!key && !/^(https?:|blob:|data:|\/\/)/.test(key) && !key.startsWith("/api/");
5942
+ }
5943
+ var takeRelativeFile = (key) => key.replace(/^\/+/, "");
5944
+ function docMediaRefs(doc, keep = isTakeRelativeKey) {
5945
+ const out = [];
5946
+ for (const clip of doc.overlays ?? []) {
5947
+ if (clip.kind === "text" || !keep(clip.key)) continue;
5948
+ out.push({
5949
+ where: `overlay ${clip.id}`,
5950
+ key: clip.key,
5951
+ set: (next) => {
5952
+ clip.key = next;
5953
+ }
5954
+ });
5955
+ }
5956
+ const mark = doc.endCard?.mark;
5957
+ if (mark && keep(mark.key))
5958
+ out.push({
5959
+ where: "end card mark",
5960
+ key: mark.key,
5961
+ set: (next) => {
5962
+ mark.key = next;
5963
+ }
5964
+ });
5965
+ const bg = doc.frame.backgroundMedia;
5966
+ if (bg && keep(bg.key))
5967
+ out.push({
5968
+ where: "background",
5969
+ key: bg.key,
5970
+ set: (next) => {
5971
+ bg.key = next;
5972
+ }
5973
+ });
5974
+ return out;
5975
+ }
5976
+ function mediaContentType(file) {
5977
+ const ext = (/\.([a-z0-9]+)$/i.exec(file)?.[1] ?? "").toLowerCase();
5978
+ const types = {
5979
+ svg: "image/svg+xml",
5980
+ png: "image/png",
5981
+ jpg: "image/jpeg",
5982
+ jpeg: "image/jpeg",
5983
+ webp: "image/webp",
5984
+ gif: "image/gif",
5985
+ avif: "image/avif",
5986
+ webm: "video/webm",
5987
+ mp4: "video/mp4",
5988
+ mov: "video/quicktime",
5989
+ mp3: "audio/mpeg",
5990
+ wav: "audio/wav",
5991
+ ogg: "audio/ogg",
5992
+ m4a: "audio/mp4"
5993
+ };
5994
+ return types[ext] ?? "application/octet-stream";
5995
+ }
5996
+ function extensionFor(contentType) {
5997
+ const t = contentType.split(";")[0].trim().toLowerCase();
5998
+ const exts = {
5999
+ "image/svg+xml": ".svg",
6000
+ "image/png": ".png",
6001
+ "image/jpeg": ".jpg",
6002
+ "image/webp": ".webp",
6003
+ "image/gif": ".gif",
6004
+ "image/avif": ".avif",
6005
+ "video/webm": ".webm",
6006
+ "video/mp4": ".mp4",
6007
+ "video/quicktime": ".mov",
6008
+ "audio/mpeg": ".mp3",
6009
+ "audio/wav": ".wav",
6010
+ "audio/ogg": ".ogg",
6011
+ "audio/mp4": ".m4a"
6012
+ };
6013
+ return exts[t] ?? "";
6014
+ }
6872
6015
  function assetIdOf(url) {
6873
6016
  if (!url) return null;
6874
6017
  const m = /\/api\/assets\/([A-Za-z0-9_-]+)\/file/.exec(url);
@@ -6880,8 +6023,8 @@ async function pullMedia(ctx, dir, doc, log) {
6880
6023
  const url = doc.source[key];
6881
6024
  const assetId = assetIdOf(url);
6882
6025
  if (!assetId) continue;
6883
- const target = join12(dir, file);
6884
- if (existsSync10(target)) {
6026
+ const target = join13(dir, file);
6027
+ if (existsSync11(target)) {
6885
6028
  result.kept.push(file);
6886
6029
  } else {
6887
6030
  const abs = /^https?:/.test(url) ? url : `${ctx.origin}${url}`;
@@ -6903,19 +6046,56 @@ async function pullMedia(ctx, dir, doc, log) {
6903
6046
  }
6904
6047
  doc.source[key] = file;
6905
6048
  }
6906
- const metaPath = join12(dir, "meta.json");
6907
- if (!existsSync10(metaPath)) await writeJson(metaPath, doc.source.meta, true);
6908
- const cursorPath = join12(dir, "cursor.json");
6909
- if (!existsSync10(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
6910
- await writeFile9(join12(dir, "doc.json"), JSON.stringify(doc, null, 2));
6049
+ for (const ref of docMediaRefs(doc, (k) => assetIdOf(k) !== null)) {
6050
+ const assetId = assetIdOf(ref.key);
6051
+ const abs = /^https?:/.test(ref.key) ? ref.key : `${ctx.origin}${ref.key}`;
6052
+ const existing = existsSync11(join13(dir, "media")) ? (await import("fs/promises").then(
6053
+ (fs) => fs.readdir(join13(dir, "media"))
6054
+ )).find((f) => f.startsWith(`${assetId}.`)) : void 0;
6055
+ let file;
6056
+ if (existing) {
6057
+ file = `media/${existing}`;
6058
+ result.kept.push(file);
6059
+ } else {
6060
+ const res = await fetch(abs, {
6061
+ headers: ctx.key ? { authorization: `Bearer ${ctx.key}` } : {}
6062
+ });
6063
+ if (!res.ok || !res.body) {
6064
+ throw new Error(
6065
+ `download of ${ref.where} (${ref.key}) failed (${res.status})`
6066
+ );
6067
+ }
6068
+ const ext = extensionFor(res.headers.get("content-type") ?? "");
6069
+ file = `media/${assetId}${ext}`;
6070
+ const target = join13(dir, file);
6071
+ await import("fs/promises").then(
6072
+ (fs) => fs.mkdir(join13(dir, "media"), { recursive: true })
6073
+ );
6074
+ await pipeline(
6075
+ Readable.fromWeb(res.body),
6076
+ createWriteStream(target)
6077
+ );
6078
+ const bytes = (await import("fs/promises").then((fs) => fs.stat(target))).size;
6079
+ result.downloaded.push({ file, assetId, bytes });
6080
+ log(
6081
+ ` ${file} \u2190 ${ref.where}, asset ${assetId} (${Math.round(bytes / 1024)} kB)`
6082
+ );
6083
+ }
6084
+ ref.set(file);
6085
+ }
6086
+ const metaPath = join13(dir, "meta.json");
6087
+ if (!existsSync11(metaPath)) await writeJson(metaPath, doc.source.meta, true);
6088
+ const cursorPath = join13(dir, "cursor.json");
6089
+ if (!existsSync11(cursorPath)) await writeJson(cursorPath, doc.source.cursor);
6090
+ await writeFile9(join13(dir, "doc.json"), JSON.stringify(doc, null, 2));
6911
6091
  return result;
6912
6092
  }
6913
6093
 
6914
6094
  // src/plugin/folder.ts
6915
6095
  import { mkdir as mkdir7, writeFile as writeFile10 } from "fs/promises";
6916
- import { join as join13 } from "path";
6096
+ import { join as join14 } from "path";
6917
6097
  import { recipeHints } from "@vosjs/shared/frontmatter";
6918
- import { migrateHostedDoc } from "@vosjs/studio-core";
6098
+ import { migrateHostedDoc as migrateHostedDoc2 } from "@vosjs/studio-core";
6919
6099
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["json", "help", "media"]);
6920
6100
  async function listFolders(origin, key) {
6921
6101
  const r = await apiJson(origin, "/api/folders", { key });
@@ -7089,18 +6269,18 @@ async function cmdPull(argv) {
7089
6269
  throw new Error(apiError(`pull folder ${folder.slug}`, res));
7090
6270
  const payload = res.body;
7091
6271
  const out = strFlag(flags, "out") ?? folder.slug;
7092
- await mkdir7(join13(out, "recipes"), { recursive: true });
6272
+ await mkdir7(join14(out, "recipes"), { recursive: true });
7093
6273
  const lines = [];
7094
6274
  const recipes = [];
7095
6275
  for (const rec of payload.recipes) {
7096
- const path = join13(out, "recipes", safeName(rec.filename));
6276
+ const path = join14(out, "recipes", safeName(rec.filename));
7097
6277
  await writeFile10(path, rec.body ?? "");
7098
6278
  recipes.push({ path, line: recipeLine(rec.filename, rec.body ?? "") });
7099
6279
  }
7100
6280
  for (const rec of payload.inheritedRecipes) {
7101
6281
  const from = safeName(rec.folderSlug ?? "inherited");
7102
- await mkdir7(join13(out, "recipes", "_inherited", from), { recursive: true });
7103
- const path = join13(
6282
+ await mkdir7(join14(out, "recipes", "_inherited", from), { recursive: true });
6283
+ const path = join14(
7104
6284
  out,
7105
6285
  "recipes",
7106
6286
  "_inherited",
@@ -7115,7 +6295,7 @@ async function cmdPull(argv) {
7115
6295
  }
7116
6296
  const members = [];
7117
6297
  for (const v of payload.voses) {
7118
- const dir = join13(out, "members", safeName(v.slug || v.title || v.id));
6298
+ const dir = join14(out, "members", safeName(v.slug || v.title || v.id));
7119
6299
  await mkdir7(dir, { recursive: true });
7120
6300
  const cfg = await apiJson(origin, `/api/vos/${v.id}/config`, { key });
7121
6301
  if (cfg.status !== 200) {
@@ -7123,7 +6303,7 @@ async function cmdPull(argv) {
7123
6303
  continue;
7124
6304
  }
7125
6305
  await writeFile10(
7126
- join13(dir, "config.json"),
6306
+ join14(dir, "config.json"),
7127
6307
  JSON.stringify(cfg.body.config, null, 2)
7128
6308
  );
7129
6309
  let take = false;
@@ -7135,12 +6315,12 @@ async function cmdPull(argv) {
7135
6315
  );
7136
6316
  if (doc.status === 200) {
7137
6317
  await writeFile10(
7138
- join13(dir, "doc.json"),
6318
+ join14(dir, "doc.json"),
7139
6319
  JSON.stringify(doc.body, null, 2)
7140
6320
  );
7141
6321
  take = true;
7142
6322
  if (flags.media === true) {
7143
- const hosted = migrateHostedDoc(doc.body);
6323
+ const hosted = migrateHostedDoc2(doc.body);
7144
6324
  await pullMedia({ origin, key }, dir, hosted, r.log);
7145
6325
  }
7146
6326
  }
@@ -7252,7 +6432,7 @@ async function pushTake(dir, flags, r) {
7252
6432
  throw new Error(`doc.json fails lint:
7253
6433
  ${lint.problems.join("\n ")}`);
7254
6434
  }
7255
- if (!existsSync11(take.paths.recording)) {
6435
+ if (!existsSync12(take.paths.recording)) {
7256
6436
  throw new Error("no recording.webm in this take");
7257
6437
  }
7258
6438
  const ctx = apiContext(flags);
@@ -7284,7 +6464,7 @@ async function pushTake(dir, flags, r) {
7284
6464
  throw new Error("push cancelled");
7285
6465
  }
7286
6466
  }
7287
- const bytes = await readFile11(take.paths.recording);
6467
+ const bytes = await readFile12(take.paths.recording);
7288
6468
  const hash = createHash("sha256").update(bytes).digest("hex");
7289
6469
  r.log(`uploading recording (${Math.round(bytes.length / 1024)} kB)\u2026`);
7290
6470
  const upload = await api(ctx, "/assets/recording", {
@@ -7322,6 +6502,35 @@ async function pushTake(dir, flags, r) {
7322
6502
  r.log(" mic track is local-only \u2014 dropped from the push");
7323
6503
  delete docForPush.source.micKey;
7324
6504
  }
6505
+ for (const ref of docMediaRefs(docForPush)) {
6506
+ const file = join15(dir, takeRelativeFile(ref.key));
6507
+ if (!existsSync12(file)) {
6508
+ r.log(` ${ref.where}: ${ref.key} is not in the take \u2014 left as is`);
6509
+ continue;
6510
+ }
6511
+ const media = await readFile12(file);
6512
+ const mediaHash = createHash("sha256").update(media).digest("hex");
6513
+ const type = mediaContentType(file);
6514
+ const put = await api(ctx, "/assets/recording", {
6515
+ method: "POST",
6516
+ headers: {
6517
+ "Content-Type": type,
6518
+ "Content-Length": String(media.length),
6519
+ "X-Filename": basename(file),
6520
+ "X-Content-Hash": mediaHash
6521
+ },
6522
+ raw: new Uint8Array(media)
6523
+ });
6524
+ if (put.status !== 201 && put.status !== 200) {
6525
+ throw new Error(
6526
+ `${ref.where} upload failed (${put.status}): ${String(put.json.error ?? "")}`
6527
+ );
6528
+ }
6529
+ ref.set(String(put.json.url));
6530
+ r.log(
6531
+ ` ${ref.where}: ${ref.key} \u2192 asset ${String(put.json.id)}${put.json.reused === true ? " (reused)" : ""}`
6532
+ );
6533
+ }
7325
6534
  const lowered = lowerToComposition3(docForPush);
7326
6535
  const config = { ...lowered.config, data: lowered.data };
7327
6536
  if (!state) {
@@ -7435,9 +6644,9 @@ async function pullTake(dir, flags, r) {
7435
6644
  };
7436
6645
  }
7437
6646
  let media2;
7438
- if (flags.media && existsSync11(join14(dir, "doc.json"))) {
6647
+ if (flags.media && existsSync12(join15(dir, "doc.json"))) {
7439
6648
  const local = JSON.parse(
7440
- await readFile11(join14(dir, "doc.json"), "utf8")
6649
+ await readFile12(join15(dir, "doc.json"), "utf8")
7441
6650
  );
7442
6651
  media2 = await pullMedia(ctx, dir, local, r.log);
7443
6652
  }
@@ -7495,12 +6704,12 @@ async function pullTake(dir, flags, r) {
7495
6704
  docRes.status === 404 ? "the head version carries no doc \u2014 is this a take vos?" : `doc fetch failed (${docRes.status})`
7496
6705
  );
7497
6706
  }
7498
- const hostedDoc = migrateHostedDoc2(docRes.json);
6707
+ const hostedDoc = migrateHostedDoc3(docRes.json);
7499
6708
  const doc = hostedDoc;
7500
- if (existsSync11(join14(dir, RECORDING_NAME))) {
6709
+ if (existsSync12(join15(dir, RECORDING_NAME))) {
7501
6710
  doc.source.videoKey = RECORDING_NAME;
7502
6711
  }
7503
- await writeJson(join14(dir, "doc.json"), doc, true);
6712
+ await writeJson(join15(dir, "doc.json"), doc, true);
7504
6713
  const media = flags.media ? await pullMedia(ctx, dir, doc, r.log) : void 0;
7505
6714
  const versions = await api(ctx, `/vos/${vosId}/versions`);
7506
6715
  const headRow = (versions.json.versions ?? []).find((v) => v.id === head);
@@ -7519,16 +6728,16 @@ async function pullTake(dir, flags, r) {
7519
6728
  }
7520
6729
 
7521
6730
  // src/plugin/program.ts
7522
- import { existsSync as existsSync12 } from "fs";
7523
- import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile11 } from "fs/promises";
6731
+ import { existsSync as existsSync13 } from "fs";
6732
+ import { mkdir as mkdir8, readFile as readFile13, writeFile as writeFile11 } from "fs/promises";
7524
6733
  import { createInterface as createInterface2 } from "readline/promises";
7525
- import { basename as basename2, dirname as dirname3, join as join15 } from "path";
6734
+ import { basename as basename2, dirname as dirname3, join as join16 } from "path";
7526
6735
  import {
7527
6736
  CURRENT_CONFIG_VERSION,
7528
6737
  migrateConfig,
7529
6738
  vosConfigJsonSchema
7530
6739
  } from "@vosjs/core";
7531
- import { migrateHostedDoc as migrateHostedDoc3 } from "@vosjs/studio-core";
6740
+ import { migrateHostedDoc as migrateHostedDoc4 } from "@vosjs/studio-core";
7532
6741
 
7533
6742
  // src/plugin/login.ts
7534
6743
  import { hostname } from "os";
@@ -7692,8 +6901,8 @@ function preflightConfig(parsed) {
7692
6901
  }
7693
6902
  function resolveConfigPath(target) {
7694
6903
  if (target.endsWith(".json")) return target;
7695
- const inDir = join15(target, "config.json");
7696
- if (existsSync12(inDir)) return inDir;
6904
+ const inDir = join16(target, "config.json");
6905
+ if (existsSync13(inDir)) return inDir;
7697
6906
  throw new UsageError(
7698
6907
  `${target} has no config.json (and is not a take \u2014 no doc.json)`
7699
6908
  );
@@ -7721,7 +6930,7 @@ async function cmdFetch(argv) {
7721
6930
  const out = strFlag(flags, "out") ?? slug;
7722
6931
  await mkdir8(out, { recursive: true });
7723
6932
  await writeFile11(
7724
- join15(out, "config.json"),
6933
+ join16(out, "config.json"),
7725
6934
  JSON.stringify(cfg.body.config, null, 2)
7726
6935
  );
7727
6936
  const title = typeof vosMeta.title === "string" ? vosMeta.title : "";
@@ -7743,15 +6952,15 @@ async function cmdFetch(argv) {
7743
6952
  }
7744
6953
  );
7745
6954
  if (doc.status === 200 && !("source" in doc.body)) {
7746
- const hosted = migrateHostedDoc3(doc.body);
6955
+ const hosted = migrateHostedDoc4(doc.body);
7747
6956
  const { config: own } = await writeProgramDoc(out, hosted);
7748
6957
  if (own) {
7749
- await writeFile11(join15(out, "config.json"), JSON.stringify(own, null, 2));
6958
+ await writeFile11(join16(out, "config.json"), JSON.stringify(own, null, 2));
7750
6959
  }
7751
6960
  } else if (doc.status === 200) {
7752
6961
  take = true;
7753
- const hosted = migrateHostedDoc3(doc.body);
7754
- await writeFile11(join15(out, "doc.json"), JSON.stringify(hosted, null, 2));
6962
+ const hosted = migrateHostedDoc4(doc.body);
6963
+ await writeFile11(join16(out, "doc.json"), JSON.stringify(hosted, null, 2));
7755
6964
  if (flags.media === true) {
7756
6965
  const media = await pullMedia({ origin, key }, out, hosted, r.log);
7757
6966
  mediaLine = media.downloaded.length ? ` + ${media.downloaded.map((m) => m.file).join(", ")}` : "";
@@ -7863,7 +7072,7 @@ async function cmdPushProgram(argv) {
7863
7072
  api: strFlag(flags, "api")
7864
7073
  });
7865
7074
  const dir = dirname3(source);
7866
- const parsed = JSON.parse(await readFile12(source, "utf8"));
7075
+ const parsed = JSON.parse(await readFile13(source, "utf8"));
7867
7076
  const pre = preflightConfig(parsed);
7868
7077
  if (!pre.ok || !pre.config) {
7869
7078
  for (const issue of pre.issues) r.log(`error ${issue}`);
@@ -8096,10 +7305,10 @@ async function cmdPullProgram(argv) {
8096
7305
  const cfg = await apiJson(origin, `/api/vos/${vosId}/config`, { key });
8097
7306
  if (cfg.status !== 200)
8098
7307
  throw new Error(apiError(`fetch head config for ${vosId}`, cfg));
8099
- const configPath = join15(dir, "config.json");
7308
+ const configPath = join16(dir, "config.json");
8100
7309
  let backedUp = false;
8101
- if (existsSync12(configPath)) {
8102
- await writeFile11(join15(dir, "config.backup.json"), await readFile12(configPath));
7310
+ if (existsSync13(configPath)) {
7311
+ await writeFile11(join16(dir, "config.backup.json"), await readFile13(configPath));
8103
7312
  backedUp = true;
8104
7313
  }
8105
7314
  let headConfig = cfg.body.config;
@@ -8113,7 +7322,7 @@ async function cmdPullProgram(argv) {
8113
7322
  if (docRes.status === 200 && !("source" in docRes.body)) {
8114
7323
  const { config: own } = await writeProgramDoc(
8115
7324
  dir,
8116
- migrateHostedDoc3(docRes.body)
7325
+ migrateHostedDoc4(docRes.body)
8117
7326
  );
8118
7327
  if (own) headConfig = own;
8119
7328
  }
@@ -8132,7 +7341,7 @@ async function cmdPullProgram(argv) {
8132
7341
  protected: protectedIds,
8133
7342
  changes,
8134
7343
  out: configPath,
8135
- backup: backedUp ? join15(dir, "config.backup.json") : null
7344
+ backup: backedUp ? join16(dir, "config.backup.json") : null
8136
7345
  },
8137
7346
  `Pulled ${changes.length} version${changes.length === 1 ? "" : "s"} \u2192 ${configPath}` + (backedUp ? ` (previous copy: config.backup.json)` : "") + `
8138
7347
  Re-apply your edit on the new head, then: vos push ${configPath} --vos ${vosId}`
@@ -8189,9 +7398,9 @@ function isTakeDir(target) {
8189
7398
  return directoryKind(target) === "take";
8190
7399
  }
8191
7400
  async function readProgramDoc(dir, config) {
8192
- const docPath = join15(dir, "doc.json");
8193
- if (!existsSync12(docPath)) return null;
8194
- const parsed = JSON.parse(await readFile12(docPath, "utf8"));
7401
+ const docPath = join16(dir, "doc.json");
7402
+ if (!existsSync13(docPath)) return null;
7403
+ const parsed = JSON.parse(await readFile13(docPath, "utf8"));
8195
7404
  if (typeof parsed !== "object" || parsed === null || "source" in parsed)
8196
7405
  return null;
8197
7406
  const raw = parsed;
@@ -8202,7 +7411,7 @@ async function writeProgramDoc(dir, hosted) {
8202
7411
  const program = hosted.program && typeof hosted.program === "object" ? hosted.program : {};
8203
7412
  const { config, ...rest } = program;
8204
7413
  const onDisk = { ...hosted, program: rest };
8205
- await writeFile11(join15(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
7414
+ await writeFile11(join16(dir, "doc.json"), JSON.stringify(onDisk, null, 2));
8206
7415
  return {
8207
7416
  config: config && typeof config === "object" ? config : null
8208
7417
  };
@@ -8410,7 +7619,7 @@ async function cmdRecipe(argv) {
8410
7619
 
8411
7620
  // src/plugin/brand.ts
8412
7621
  import { writeFile as writeFile12 } from "fs/promises";
8413
- import { resolve as resolve6 } from "path";
7622
+ import { resolve as resolve7 } from "path";
8414
7623
  import { parseFrontmatter as parseFrontmatter2 } from "@vosjs/shared/frontmatter";
8415
7624
  import { lookKindForGround } from "@vosjs/studio-core";
8416
7625
  var HEX_RE = /#(?:[0-9a-f]{6}|[0-9a-f]{3})\b/gi;
@@ -8459,7 +7668,7 @@ function isSaturated(hex2) {
8459
7668
  return s >= 0.25 && l > 0.12 && l < 0.88;
8460
7669
  }
8461
7670
  var isLight = (hex2) => hsl(hex2).l >= 0.5;
8462
- function mixHex2(a, b, t) {
7671
+ function mixHex(a, b, t) {
8463
7672
  const ch = (i) => Math.round(
8464
7673
  parseInt(a.slice(i, i + 2), 16) * (1 - t) + parseInt(b.slice(i, i + 2), 16) * t
8465
7674
  ).toString(16).padStart(2, "0");
@@ -8477,7 +7686,7 @@ function mostCommon(values) {
8477
7686
  }
8478
7687
  return best;
8479
7688
  }
8480
- function firstFamily2(fontFamily) {
7689
+ function firstFamily(fontFamily) {
8481
7690
  const first = fontFamily.split(",")[0]?.trim() ?? "";
8482
7691
  return first.replace(/^["']|["']$/g, "");
8483
7692
  }
@@ -8596,7 +7805,7 @@ function composeBrand(input) {
8596
7805
  p.bgA = `the body's background`;
8597
7806
  const surfaceHexes = w.surfaces.map(rgbToHex).filter((h) => h !== null && h !== bgA);
8598
7807
  const neutralSurface = mostCommon(surfaceHexes.filter((h) => !isSaturated(h)));
8599
- const bgB = neutralSurface ?? mixHex2(bgA, isLight(bgA) ? "#000000" : "#ffffff", 0.04);
7808
+ const bgB = neutralSurface ?? mixHex(bgA, isLight(bgA) ? "#000000" : "#ffffff", 0.04);
8600
7809
  p.bgB = neutralSurface ? `the most common section / card ground` : `no distinct surface found; bgA stepped 4% toward ${isLight(bgA) ? "black" : "white"}`;
8601
7810
  const accentHexes = w.accents.map(rgbToHex).filter((h) => h !== null && isSaturated(h));
8602
7811
  const themeHex = w.themeColor ? rgbToHex(w.themeColor) : null;
@@ -8606,13 +7815,13 @@ function composeBrand(input) {
8606
7815
  const inkHex = rgbToHex(w.h1?.color ?? w.body.color) ?? (isLight(bgA) ? "#111111" : "#f5f5f5");
8607
7816
  if (!accent) accent = inkHex;
8608
7817
  p.accent = accentHexes.length ? `the saturated colour buttons and links agree on` : themeHex && isSaturated(themeHex) ? `the theme-color meta` : design?.hexes.find(isSaturated) ? `a hex quoted in design.md` : `no saturated colour on the page; the ink stands in`;
8609
- const bgC = mixHex2(bgA, accent, 0.14);
7818
+ const bgC = mixHex(bgA, accent, 0.14);
8610
7819
  p.bgC = `bgA tinted 14% toward the accent (a highlight ground)`;
8611
7820
  p.ink = w.h1 ? `the h1's colour` : `the body's colour`;
8612
- const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily2(w.h1.fontFamily) : firstFamily2(w.body.fontFamily));
7821
+ const fontDisplay = design?.fonts[0] ?? (w.h1 ? firstFamily(w.h1.fontFamily) : firstFamily(w.body.fontFamily));
8613
7822
  p.fontDisplay = design?.fonts[0] ? `named in design.md` : w.h1 ? `the h1's computed face` : `the body's computed face (no h1)`;
8614
7823
  const designBody = design?.fonts.slice(1).find((f) => !/\bmono\b/i.test(f));
8615
- const fontBody = designBody ?? firstFamily2(w.body.fontFamily);
7824
+ const fontBody = designBody ?? firstFamily(w.body.fontFamily);
8616
7825
  p.fontBody = designBody ? `named in design.md` : `the body's computed face`;
8617
7826
  const isIcon = (u) => /favicon|apple-touch|icon/i.test(u);
8618
7827
  const onDark = (u) => /on-dark|white/i.test(u);
@@ -8727,7 +7936,7 @@ async function cmdBrand(argv) {
8727
7936
  throw new UsageError(`not a URL: ${raw}`);
8728
7937
  }
8729
7938
  const r = createReporter(flags.json === true);
8730
- const out = resolve6(strFlag(flags, "out") ?? "BRAND.md");
7939
+ const out = resolve7(strFlag(flags, "out") ?? "BRAND.md");
8731
7940
  r.log(`reading ${origin}/design.md and /llms.txt\u2026`);
8732
7941
  const [designText, llmsText] = await Promise.all([
8733
7942
  fetchText(`${origin}/design.md`),
@@ -8780,7 +7989,7 @@ async function cmdBrand(argv) {
8780
7989
 
8781
7990
  // src/plugin/agentBrowser.ts
8782
7991
  import { writeFile as writeFile13 } from "fs/promises";
8783
- import { resolve as resolve7 } from "path";
7992
+ import { resolve as resolve8 } from "path";
8784
7993
  function parseAgentBrowserLog(text) {
8785
7994
  const records = [];
8786
7995
  const problems = [];
@@ -9250,7 +8459,7 @@ async function cmdActions(argv) {
9250
8459
  );
9251
8460
  }
9252
8461
  const r = createReporter(flags.json === true);
9253
- const out = resolve7(strFlag(flags, "out") ?? "actions.json");
8462
+ const out = resolve8(strFlag(flags, "out") ?? "actions.json");
9254
8463
  const viewportFlag = strFlag(flags, "viewport");
9255
8464
  let viewport;
9256
8465
  if (viewportFlag) {
@@ -9258,8 +8467,8 @@ async function cmdActions(argv) {
9258
8467
  if (!m) throw new UsageError("--viewport expects WxH, e.g. 1280x720");
9259
8468
  viewport = { width: Number(m[1]), height: Number(m[2]) };
9260
8469
  }
9261
- const { readFile: readFile14 } = await import("fs/promises");
9262
- const text = await readFile14(resolve7(input), "utf8");
8470
+ const { readFile: readFile15 } = await import("fs/promises");
8471
+ const text = await readFile15(resolve8(input), "utf8");
9263
8472
  const { records, problems } = parseAgentBrowserLog(text);
9264
8473
  for (const p of problems) r.log(` ${p}`);
9265
8474
  const result = convertAgentBrowser(records, {
@@ -9311,7 +8520,8 @@ var BOOLEAN_FLAGS5 = /* @__PURE__ */ new Set([
9311
8520
  "print",
9312
8521
  "yes",
9313
8522
  "composed",
9314
- "check"
8523
+ "check",
8524
+ "motion"
9315
8525
  ]);
9316
8526
  var MULTI_FLAGS2 = /* @__PURE__ */ new Set(["set", "override"]);
9317
8527
  var HELP = `vos \u2014 record a browser flow, plan effects, render a product video; sync with vos.so
@@ -9319,10 +8529,10 @@ var HELP = `vos \u2014 record a browser flow, plan effects, render a product vid
9319
8529
  Take pipeline
9320
8530
  vos create --actions actions.json [--url <url>] [--out take] [out.webm] [--strict] [--max-duration <s>] [--background <slug|url|none>] [render flags] [--json]
9321
8531
  vos record --actions actions.json [--url <url>] [--out take] [--strict] [--max-duration <s>] [--background <slug|url|none>] [--json]
9322
- vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--background <slug|url|none>] [--json]
8532
+ vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--background <slug|url|none>] [--motion] [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|none] [--end-card none] [--captions none] [--clicks none] [--release v2.1] [--json]
9323
8533
  vos render <take> [out.webm] [--width] [--height] [--fps] [--format webm|mp4] [--parallel N] [--range a..b] [--draft] [--frame <kind>] [--background <url|slug>] [--set <path=value>]... [--json]
9324
8534
  vos frames <take> [--times 0,25%,50%,75%,100%] [--frame <t>] [--at-zooms] [--at-moments] [--size WxH] [--out dir] [--background <url|slug>] [--set <path=value>]... [--json]
9325
- vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--headline "\u2026"] [--kicker "\u2026"] [--launch LAUNCH.md] [--music <slug|mood|none>] [--entrance tilt-in|pull-out|rise|none] [--end-card none] [--captions none] [--clicks none] [--look plate|gradient|dark|none] [--brand BRAND.md] [--poster <split-cover|card-on-gradient|config.json|vosId|none>] [--shot-time <t>] [--poster-time <t>] [--composed] [--set path=value] [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--parallel N] [--json]
8535
+ vos deliver <take> --to cws,producthunt,x,linkedin,og,github,youtube (or all) [--launch LAUNCH.md] [--look plate|gradient|dark|none] [--brand BRAND.md] [--composed] [--set path=value] [--release v2.1] [--out dir] [--times a,b] [--range a..b] [--parallel N] [--json]
9326
8536
  vos digest <take> [--out dir] [--full 960] [--crop 640] [--no-frames] [--transcript <file.json>] [--style <doc.json|vosId>] [--json]
9327
8537
  vos brand <url> [--out BRAND.md] [--json]
9328
8538
  vos open <take> [--studio <url>] [--print]
@@ -9422,44 +8632,47 @@ read once as the real page, blank ones (a wallpaper, an empty canvas) are
9422
8632
  dropped and two of one frame collapse to one, with every drop said in
9423
8633
  skipped[]. --times overrides with seconds, percents or step:<id>[+offset]
9424
8634
  (the id from actions.json); --range cuts every video destination.
9425
- The LOOK presents the card: card-genre stills with no poster and every
9426
- video cut sit on a ground (a cream plate, the house gradient, a dark plate
9427
- with a light streak) at ~84% of the width with headroom, a soft ambient
9428
- shadow plus a tight contact shadow, and a hairline when card and ground
9429
- are both light; a wide frame runs the card off the bottom. --look picks a
9430
- house look (or none for the pre-look crops); with no flag the BRAND.md
9431
- beside the take (or --brand <file>) decides from its look role or its own
9432
- ground (a paper site is a plate, a dark site is dark), and with no brand
9433
- the house gradient. Screenshot-genre stills never take a look.
9434
- Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the CWS
9435
- tile + marquee, GitHub social preview) COMPOSE by default: each renders
9436
- from its destination's poster TEMPLATE (split-cover is a STAGE, the
9437
- take's own card leaning in perspective with its chrome and shadow beside
9438
- a serif headline column, on the brand's ground; card-on-gradient is the
9439
- shot alone on a mesh, the store's tile rule), filled with BRAND.md's colours and faces and the
9440
- release's words, the shot baked as an object (padded, rounded, shadowed,
9441
- a hairline on a light ground), PNG at exact pixels; kit.json records the
9442
- template and the text boxes. The headline is LAUNCH.md's headline role
9443
- beside the take or --headline (with none, the headline templates stand
9444
- down for card-on-gradient, said); --kicker overrides the wordmark plus
9445
- --release line. --poster names a bundled template for every card, your
9446
- own template (a config.json or a hosted vos id carrying a template
9447
- block), or none to keep the take path.
9448
- Every video cut but the README loop opens on an ENTRANCE (tilt-in by
9449
- default: the card swings in from a perspective pose and settles) and
9450
- closes on an END CARD (the last frame holds 2.5 s while the card recedes
9451
- and the headline, the release line and the wordmark rise); LAUNCH.md's
9452
- entrance and endCard roles, or --entrance and --end-card, change or
9453
- switch them off. A step's caption in actions.json lands as a lower-third
9454
- at the step's moment on cuts that take words (--captions none to skip).
9455
- Destinations that play sound (the X cut, the YouTube demo, the vertical
9456
- cut) take a music bed from LAUNCH.md's music role (a catalog slug or a
9457
- mood; --music overrides) and a click sound on every press when the take
9458
- has no mic (--clicks none). The 9:16 cut is a reframe, not a letterbox:
9459
- the crop follows the camera. --shot-time <t> picks the take moment (OUTPUT seconds;
9460
- default the first still time \u2014 pick a zoom apex, the cut's camera makes
9461
- the shot the feature, not the whole page); --poster-time <t> is the instant
9462
- inside the poster's OWN timeline (default 90% through it). Screenshot-genre
8635
+ The LOOK presents the card: card-genre stills with no poster document
8636
+ and every video cut sit on a ground (a cream plate, the house gradient, a
8637
+ dark plate with a light streak) at ~84% of the width with headroom, a
8638
+ soft ambient shadow plus a tight contact shadow, and a hairline when card
8639
+ and ground are both light; a wide frame runs the card off the bottom.
8640
+ --look picks a house look (or none for the pre-look crops); with no flag
8641
+ the BRAND.md beside the take (or --brand <file>) decides from its look
8642
+ role or its own ground, and with no brand the house gradient.
8643
+ Screenshot-genre stills never take a look.
8644
+ A POSTER is a document, never a template: a plain take whose card sits
8645
+ where the poster wants it (frame.inset), leans (the tilt track), carries
8646
+ the words and the mark as clips (ids stage-title, stage-kicker,
8647
+ stage-brand, stage-mark) and ends on a trailing hold whose start is the
8648
+ still. Card-genre destinations (OG, LinkedIn, X, YouTube thumbnail, the
8649
+ CWS tile + marquee, GitHub social preview) render from the poster
8650
+ document of their aspect CLASS (landscape, square, portrait, tile), found
8651
+ beside the take as poster/<class>/doc.json (poster/doc.json serves every
8652
+ class) or named in LAUNCH.md (poster: <path>, poster-landscape: \u2026), at
8653
+ the document's rest; kit.json records source poster, the class, the file,
8654
+ the vos it tracks, the shot rect and the text boxes read FROM the
8655
+ document. A class with no document is the take's own frame, said once.
8656
+ deliver renders and verifies; it composes nothing. The composition is
8657
+ made with plan --style <poster>, which copies a poster's layout onto a
8658
+ take (its card placement, its stage clips with the release's words
8659
+ patched in from LAUNCH.md's headline and kicker roles or --headline and
8660
+ --kicker, its rest lean, its hold), or by hand in doc.json.
8661
+ The cut's MOTION is the document's too. plan proposes it on a fresh plan
8662
+ (--motion re-proposes onto an existing doc.json, replacing only its own
8663
+ proposals): the card's ENTRANCE (tilt-in by default: the card swings in
8664
+ from a perspective pose and settles), the END CARD (the last frame holds
8665
+ 2.5 s while the card recedes and the headline, the release line and the
8666
+ wordmark rise; the brand's mark from BRAND.md logoUrl above them), a
8667
+ CAPTION per actions.json step at the step's moment, a music BED from
8668
+ LAUNCH.md's music role (a catalog slug or a mood) and a click sound on
8669
+ every press when the take has no mic. LAUNCH.md's entrance, endCard,
8670
+ captions, music and clicks roles, or the flags, change or switch each
8671
+ off; a deleted proposal stays deleted on a refresh. deliver applies each
8672
+ destination's MECHANICS and nothing more: the README loop plays no
8673
+ entrance, end card or sound; a channel that autoplays muted drops the
8674
+ bed; the 9:16 cut is a reframe, not a letterbox, and the crop follows
8675
+ the camera. Screenshot-genre
9463
8676
  destinations (CWS screenshots, the PH gallery) are the real page at that
9464
8677
  moment, FULL BLEED: no zoom, no tilt, no browser bar, no padding (store
9465
8678
  policy); --composed keeps the cut's camera and chrome instead. --set
@@ -9547,7 +8760,7 @@ lint-gated, so a bad override fails like a bad doc.json):
9547
8760
  first ready loop (the house backdrop the studio opens on), or a flat ground offline
9548
8761
  `;
9549
8762
  async function loadActions(file) {
9550
- const raw = JSON.parse(await readFile13(file, "utf8"));
8763
+ const raw = JSON.parse(await readFile14(file, "utf8"));
9551
8764
  const errors = validateActions(raw);
9552
8765
  if (errors.length)
9553
8766
  throw new UsageError(`invalid actions file:
@@ -9603,10 +8816,10 @@ async function cmdRecord(argv) {
9603
8816
  const url = strFlag(flags, "url") ?? actions.url;
9604
8817
  if (!url)
9605
8818
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
9606
- const outDir = resolve8(strFlag(flags, "out") ?? "take");
8819
+ const outDir = resolve9(strFlag(flags, "out") ?? "take");
9607
8820
  const backdrop = await takeBackdrop(flags, r);
9608
8821
  const maxDurationSeconds = await maxDuration(flags, r);
9609
- if (existsSync13(join16(outDir, "meta.json"))) {
8822
+ if (existsSync14(join17(outDir, "meta.json"))) {
9610
8823
  const { prevDoc, kept } = await prepareReRecord(outDir);
9611
8824
  r.log(
9612
8825
  `note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
@@ -9653,7 +8866,7 @@ async function cmdRecord(argv) {
9653
8866
  },
9654
8867
  strictFail ? `STRICT: take recorded but incomplete \u2014 ${strictReason(rec)}; fix the flow and re-record.${skippedNote}` : `Take ready: ${outDir}
9655
8868
  ${(rec.meta.durationMs / 1e3).toFixed(1)}s \xB7 ${rec.frames.length} frames \xB7 ${rec.events.length} cursor events \xB7 ${clicks} clicks \xB7 ${plan.doc.zoom.length} zoom spans planned \xB7 ${rec.freezePct}% frozen${rec.freezePct >= 25 ? " \u26A0 keep motion in frame or trim" : ""}${skippedNote}
9656
- Next: edit ${join16(outDir, "doc.json")} (optional), then: vos render ${outDir}`
8869
+ Next: edit ${join17(outDir, "doc.json")} (optional), then: vos render ${outDir}`
9657
8870
  );
9658
8871
  return strictFail ? EXIT_USAGE : EXIT_OK;
9659
8872
  } finally {
@@ -9677,19 +8890,19 @@ async function cmdCreate2(argv) {
9677
8890
  const url = strFlag(flags, "url") ?? actions.url;
9678
8891
  if (!url)
9679
8892
  throw new UsageError('no URL \u2014 set "url" in the actions file or pass --url');
9680
- const outDir = resolve8(strFlag(flags, "out") ?? "take");
8893
+ const outDir = resolve9(strFlag(flags, "out") ?? "take");
9681
8894
  const fmtRaw = strFlag(flags, "format") ?? "webm";
9682
8895
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
9683
8896
  throw new UsageError("--format must be webm or mp4");
9684
8897
  const format = fmtRaw;
9685
- const out = positionals[0] ?? join16(outDir, `out.${format}`);
8898
+ const out = positionals[0] ?? join17(outDir, `out.${format}`);
9686
8899
  const parallel = numFlag(flags, "parallel", 1);
9687
8900
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
9688
8901
  throw new UsageError("--parallel expects an integer between 1 and 16");
9689
8902
  }
9690
8903
  const backdrop = await takeBackdrop(flags, r);
9691
8904
  const maxDurationSeconds = await maxDuration(flags, r);
9692
- if (existsSync13(join16(outDir, "meta.json"))) {
8905
+ if (existsSync14(join17(outDir, "meta.json"))) {
9693
8906
  const { prevDoc, kept } = await prepareReRecord(outDir);
9694
8907
  r.log(
9695
8908
  `note: re-recording ${outDir} \u2014 kept ${kept.length ? kept.join(", ") : "nothing"}` + (prevDoc ? "; apply the previous cut to the new footage with: vos plan " + outDir + " --reuse" : "")
@@ -9772,40 +8985,66 @@ async function cmdPlan(argv) {
9772
8985
  const dir = positionals[0];
9773
8986
  if (!dir)
9774
8987
  throw new UsageError(
9775
- "vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>]"
8988
+ 'vos plan <take> [--fresh] [--reuse [--from <doc.json>]] [--style <doc.json|vosId>] [--motion] [--headline "\u2026"] [--launch LAUNCH.md] [--brand BRAND.md]'
9776
8989
  );
9777
8990
  const r = createReporter(flags.json === true);
9778
8991
  if (flags.fresh === true) {
9779
- await rm4(join16(dir, "doc.json"), { force: true });
8992
+ await rm4(join17(dir, "doc.json"), { force: true });
9780
8993
  r.log("note: --fresh discarded the existing doc.json");
9781
8994
  }
9782
8995
  let reuse;
9783
8996
  if (flags.reuse === true) {
9784
- const from = resolve8(strFlag(flags, "from") ?? join16(dir, PREV_DOC_NAME));
9785
- if (!existsSync13(from)) {
8997
+ const from = resolve9(strFlag(flags, "from") ?? join17(dir, PREV_DOC_NAME));
8998
+ if (!existsSync14(from)) {
9786
8999
  throw new UsageError(
9787
9000
  `--reuse: ${from} does not exist \u2014 a re-record into this take writes doc.prev.json, or pass --from <doc.json>`
9788
9001
  );
9789
9002
  }
9790
9003
  reuse = {
9791
9004
  from,
9792
- doc: JSON.parse(await readFile13(from, "utf8"))
9005
+ doc: JSON.parse(await readFile14(from, "utf8"))
9793
9006
  };
9794
9007
  }
9795
9008
  const style = await resolveStyleRef(flags);
9796
- const backdrop = existsSync13(join16(dir, "doc.json")) ? null : await takeBackdrop(flags, r);
9009
+ const hasDoc = existsSync14(join17(dir, "doc.json"));
9010
+ const backdrop = hasDoc ? null : await takeBackdrop(flags, r);
9011
+ const release = await releaseInputs(dir, flags, r);
9012
+ const motionWanted = !hasDoc || flags.motion === true || flags.fresh === true;
9797
9013
  const s = await planTake(dir, {
9798
9014
  ...style ? { style } : {},
9799
9015
  ...reuse ? { reuse } : {},
9800
- backdrop
9016
+ backdrop,
9017
+ words: release.words,
9018
+ mark: release.mark,
9019
+ ...motionWanted ? {
9020
+ motion: {
9021
+ words: release.words,
9022
+ launch: release.launchRoles,
9023
+ ink: release.ink,
9024
+ mark: release.mark,
9025
+ captions: release.captions,
9026
+ catalog: release.catalog,
9027
+ again: flags.motion === true
9028
+ }
9029
+ } : {}
9801
9030
  });
9031
+ const layoutLines = s.layout ? `
9032
+ layout from ${s.styleFrom}: ${[
9033
+ s.layout.clips.length ? `${s.layout.clips.length} stage clip(s)` : "",
9034
+ s.layout.lean ? "the rest lean" : "",
9035
+ s.layout.hold ? "the hold" : ""
9036
+ ].filter(Boolean).join(", ")}` + (s.layout.notes.length ? `
9037
+ ${s.layout.notes.join("\n ")}` : "") : "";
9038
+ const motionLines = s.motion ? (s.motion.notes.length ? `
9039
+ proposed: ${s.motion.notes.join(", ")}` : "") + (s.motion.skipped.length ? `
9040
+ not proposed: ${s.motion.skipped.join("; ")}` : "") : "";
9802
9041
  const reuseLines = s.reuse ? `
9803
9042
  reused ${s.reuse.from}: ${s.reuse.anchored} anchored + ${s.reuse.mapped} mapped span(s)` + (s.reuse.flagged.length ? `
9804
9043
  flagged:
9805
9044
  ${s.reuse.flagged.join("\n ")}` : "") : "";
9806
9045
  r.done(
9807
9046
  {
9808
- take: resolve8(dir),
9047
+ take: resolve9(dir),
9809
9048
  fresh: s.fresh,
9810
9049
  cursorKept: s.cursorKept,
9811
9050
  zoomAuto: s.zoomAuto,
@@ -9813,12 +9052,78 @@ async function cmdPlan(argv) {
9813
9052
  duration: s.doc.source.meta.durationMs / 1e3,
9814
9053
  ...s.backdrop !== void 0 ? { backdrop: s.backdrop } : {},
9815
9054
  ...s.styleFrom ? { styleFrom: s.styleFrom, styleFields: s.styleFields } : {},
9055
+ ...s.layout ? { layout: s.layout } : {},
9056
+ ...s.motion ? { motion: s.motion } : {},
9816
9057
  ...s.reuse ? { reuse: s.reuse } : {}
9817
9058
  },
9818
- `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join16(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${reuseLines}`
9059
+ `${s.reuse ? "Reused" : s.fresh ? "Planned" : "Refreshed"} ${join17(dir, "doc.json")}: ${s.zoomAuto} auto + ${s.zoomManual} manual zoom spans${s.cursorKept ? "" : " (cursor track dropped)"}${layoutLines}${motionLines}${reuseLines}`
9819
9060
  );
9820
9061
  return EXIT_OK;
9821
9062
  }
9063
+ async function releaseInputs(dir, flags, r) {
9064
+ let lookPick;
9065
+ try {
9066
+ lookPick = await resolveLook(dir, {
9067
+ look: strFlag(flags, "look"),
9068
+ brand: strFlag(flags, "brand")
9069
+ });
9070
+ } catch (e) {
9071
+ throw new UsageError(e instanceof Error ? e.message : String(e));
9072
+ }
9073
+ const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
9074
+ const lines = (v) => v === null || v === void 0 ? null : v.replace(/\\n/g, "\n");
9075
+ const words = {
9076
+ headline: lines(strFlag(flags, "headline") ?? launch?.roles.headline),
9077
+ kicker: lines(strFlag(flags, "kicker") ?? launch?.roles.kicker),
9078
+ brand: strFlag(flags, "brand-name") ?? lookPick.roles?.wordmark ?? null,
9079
+ release: strFlag(flags, "release") ?? null
9080
+ };
9081
+ if (launch) r.log(`words: ${launch.file}`);
9082
+ const launchRoles = { ...launch?.roles ?? {} };
9083
+ for (const [flag, role] of [
9084
+ ["music", "music"],
9085
+ ["entrance", "entrance"],
9086
+ ["end-card", "endCard"],
9087
+ ["captions", "captions"],
9088
+ ["clicks", "clicks"]
9089
+ ]) {
9090
+ const v = strFlag(flags, flag);
9091
+ if (v !== void 0) launchRoles[role] = v;
9092
+ }
9093
+ const marks = await fetchBrandMarks(dir, lookPick.roles);
9094
+ for (const n of marks.notes) r.log(`note: ${n}`);
9095
+ const mark = lookPick.look?.kind === "dark" ? marks.dark : marks.light;
9096
+ if (mark) r.log(`brand mark: ${mark.key} (${mark.aspect.toFixed(2)}:1)`);
9097
+ const off2 = (v) => v !== void 0 && /^(none|off|no|false)$/i.test(v);
9098
+ let catalog = null;
9099
+ if (launchRoles.music && !off2(launchRoles.music)) {
9100
+ try {
9101
+ catalog = await fetchMusicCatalog(
9102
+ platformOrigin({
9103
+ origin: strFlag(flags, "origin"),
9104
+ api: strFlag(flags, "api")
9105
+ })
9106
+ );
9107
+ } catch (e) {
9108
+ r.log(
9109
+ `music catalog: ${e instanceof Error ? e.message : String(e)} \u2014 no bed`
9110
+ );
9111
+ }
9112
+ }
9113
+ const take = await loadTake(dir);
9114
+ const captions = (take.actions?.steps ?? []).flatMap((st, i) => {
9115
+ const step = st;
9116
+ return typeof step.caption === "string" && step.caption.trim() ? [{ step: i, id: step.id, caption: step.caption.trim() }] : [];
9117
+ });
9118
+ return {
9119
+ words,
9120
+ launchRoles,
9121
+ ink: endCardInk(lookPick.look, lookPick.roles),
9122
+ mark,
9123
+ captions,
9124
+ catalog
9125
+ };
9126
+ }
9822
9127
  async function cmdRender(argv) {
9823
9128
  const { positionals, flags, multi } = parseArgs(
9824
9129
  argv,
@@ -9835,7 +9140,7 @@ async function cmdRender(argv) {
9835
9140
  if (fmtRaw !== "webm" && fmtRaw !== "mp4")
9836
9141
  throw new UsageError("--format must be webm or mp4");
9837
9142
  const format = fmtRaw;
9838
- const out = positionals[1] ?? join16(dir, `out.${format}`);
9143
+ const out = positionals[1] ?? join17(dir, `out.${format}`);
9839
9144
  const parallel = numFlag(flags, "parallel", 1);
9840
9145
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
9841
9146
  throw new UsageError("--parallel expects an integer between 1 and 16");
@@ -10013,34 +9318,6 @@ async function cmdDeliver(argv) {
10013
9318
  if (!Number.isInteger(parallel) || parallel < 1 || parallel > 16) {
10014
9319
  throw new UsageError("--parallel expects an integer between 1 and 16");
10015
9320
  }
10016
- let poster;
10017
- const posterRef = strFlag(flags, "poster");
10018
- if (posterRef === "none") {
10019
- poster = null;
10020
- } else if (posterRef !== void 0 && templateByName(posterRef)) {
10021
- poster = { from: `template ${posterRef}`, config: templateByName(posterRef) };
10022
- } else if (posterRef !== void 0) {
10023
- if (existsSync13(posterRef)) {
10024
- poster = {
10025
- from: resolve8(posterRef),
10026
- config: JSON.parse(await readFile13(posterRef, "utf8"))
10027
- };
10028
- } else {
10029
- const origin = platformOrigin({
10030
- origin: strFlag(flags, "origin"),
10031
- api: strFlag(flags, "api")
10032
- });
10033
- const key = await resolveCredential(strFlag(flags, "key"));
10034
- const res = await apiJson(origin, `/api/vos/${posterRef}/config`, {
10035
- key
10036
- });
10037
- if (!res.config)
10038
- throw new UsageError(
10039
- `--poster ${posterRef}: not a file on disk and the platform returned no config`
10040
- );
10041
- poster = { from: posterRef, config: res.config };
10042
- }
10043
- }
10044
9321
  let lookPick;
10045
9322
  try {
10046
9323
  lookPick = await resolveLook(dir, {
@@ -10052,63 +9329,18 @@ async function cmdDeliver(argv) {
10052
9329
  }
10053
9330
  r.log(`look: ${lookPick.from}`);
10054
9331
  const launch = await readLaunchBesideTake(dir, strFlag(flags, "launch"));
10055
- const lines = (v) => v === null || v === void 0 ? null : v.replace(/\\n/g, "\n");
10056
- const words = {
10057
- headline: lines(strFlag(flags, "headline") ?? launch?.roles.headline),
10058
- kicker: lines(strFlag(flags, "kicker") ?? launch?.roles.kicker),
10059
- brand: strFlag(flags, "brand-name") ?? lookPick.roles?.wordmark ?? null,
10060
- release: strFlag(flags, "release") ?? null
10061
- };
10062
- if (launch) r.log(`words: ${launch.file}`);
10063
- const launchRoles = { ...launch?.roles ?? {} };
10064
- for (const [flag, role] of [
10065
- ["music", "music"],
10066
- ["entrance", "entrance"],
10067
- ["end-card", "endCard"],
10068
- ["captions", "captions"],
10069
- ["clicks", "clicks"]
10070
- ]) {
10071
- const v = strFlag(flags, flag);
10072
- if (v !== void 0) launchRoles[role] = v;
10073
- }
10074
- const soundWanted = channels.some(
10075
- (c) => ["x", "youtube", "shorts-linkedin"].includes(c)
10076
- );
10077
- let catalog = null;
10078
- if (soundWanted && !/^(none|off|no|false)$/i.test(launchRoles.music ?? "") && !/^(none|off|no|false)$/i.test(launchRoles.clicks ?? "")) {
10079
- try {
10080
- catalog = await fetchMusicCatalog(
10081
- platformOrigin({
10082
- origin: strFlag(flags, "origin"),
10083
- api: strFlag(flags, "api")
10084
- })
10085
- );
10086
- } catch (e) {
10087
- r.log(`music catalog: ${e instanceof Error ? e.message : String(e)} \u2014 the cuts stay silent`);
10088
- }
10089
- }
10090
- const captions = (take.actions?.steps ?? []).flatMap((s, i) => {
10091
- const step = s;
10092
- return typeof step.caption === "string" && step.caption.trim() ? [{ step: i, id: step.id, caption: step.caption.trim() }] : [];
10093
- });
9332
+ if (launch) r.log(`launch: ${launch.file}`);
10094
9333
  const browser = await launchBrowser();
10095
9334
  try {
10096
9335
  const result = await deliverTake(browser, dir, {
10097
9336
  look: lookPick.look,
10098
- brandRoles: lookPick.roles,
10099
- launchRoles,
10100
- catalog,
10101
- captions,
10102
- words,
9337
+ launchRoles: launch?.roles ?? null,
10103
9338
  channels,
10104
9339
  outDir: strFlag(flags, "out"),
10105
9340
  release: strFlag(flags, "release"),
10106
9341
  times,
10107
9342
  range,
10108
9343
  parallel,
10109
- poster,
10110
- posterTime: hasFlag(flags, "poster-time") ? numFlag(flags, "poster-time", 0) : void 0,
10111
- shotTime: hasFlag(flags, "shot-time") ? numFlag(flags, "shot-time", 0) : void 0,
10112
9344
  composed: hasFlag(flags, "composed"),
10113
9345
  overrides: {
10114
9346
  set: multi.set,
@@ -10126,7 +9358,7 @@ async function cmdDeliver(argv) {
10126
9358
  });
10127
9359
  const { kit } = result;
10128
9360
  const assetLines = kit.assets.map(
10129
- (a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB${a.seconds !== null ? `, ${a.seconds.toFixed(1)}s` : ""}${a.frameTime !== null ? `, frame ${a.frameTime.toFixed(2)}s` : ""}${a.source === "poster" ? ", poster" : ""})`
9361
+ (a) => `${a.destination} \u2192 ${a.path} (${a.w}x${a.h}, ${(a.bytes / 1024).toFixed(0)} KB${a.seconds !== null ? `, ${a.seconds.toFixed(1)}s` : ""}${a.frameTime !== null ? `, frame ${a.frameTime.toFixed(2)}s` : ""}${a.poster ? `, poster ${a.poster.class}` : ""})`
10130
9362
  );
10131
9363
  const skippedLines = kit.skipped.map((s) => `skipped: ${s}`);
10132
9364
  r.done(
@@ -10149,14 +9381,14 @@ Store uploads stay manual: hand the human this directory and the manifest.`
10149
9381
  async function resolveStyleRef(flags) {
10150
9382
  const styleRef = strFlag(flags, "style");
10151
9383
  if (!styleRef) return null;
10152
- const file = existsSync13(styleRef) ? resolve8(
9384
+ const file = existsSync14(styleRef) ? resolve9(
10153
9385
  styleRef,
10154
- existsSync13(join16(styleRef, "doc.json")) ? "doc.json" : ""
9386
+ existsSync14(join17(styleRef, "doc.json")) ? "doc.json" : ""
10155
9387
  ) : null;
10156
- if (file && existsSync13(file)) {
9388
+ if (file && existsSync14(file)) {
10157
9389
  return {
10158
9390
  from: file,
10159
- doc: JSON.parse(await readFile13(file, "utf8"))
9391
+ doc: JSON.parse(await readFile14(file, "utf8"))
10160
9392
  };
10161
9393
  }
10162
9394
  const origin = platformOrigin({
@@ -10179,7 +9411,7 @@ async function resolveStyleRef(flags) {
10179
9411
  throw new UsageError(`--style: ${styleRef} carries no doc (is it a take?)`);
10180
9412
  return {
10181
9413
  from: `${origin}/vos/${styleRef}`,
10182
- doc: migrateHostedDoc4(doc.body)
9414
+ doc: migrateHostedDoc5(doc.body)
10183
9415
  };
10184
9416
  }
10185
9417
  async function cmdDigest(argv) {
@@ -10193,7 +9425,7 @@ async function cmdDigest(argv) {
10193
9425
  const take = await loadTake(dir);
10194
9426
  if (!take.doc) throw new UsageError(`${dir} has no doc.json \u2014 run plan first`);
10195
9427
  const transcriptPath = strFlag(flags, "transcript");
10196
- const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile13(transcriptPath, "utf8"))) : null;
9428
+ const transcript = transcriptPath ? parseTranscript(JSON.parse(await readFile14(transcriptPath, "utf8"))) : null;
10197
9429
  const style = await resolveStyleRef(flags);
10198
9430
  const noFrames = flags["no-frames"] === true;
10199
9431
  let browser = null;
@@ -10224,7 +9456,7 @@ async function cmdDigest(argv) {
10224
9456
  kinds,
10225
9457
  frames: d.images.full,
10226
9458
  crops: d.images.crop,
10227
- sheet: d.images.sheet ? join16(result.outDir, d.images.sheet) : null,
9459
+ sheet: d.images.sheet ? join17(result.outDir, d.images.sheet) : null,
10228
9460
  scenes: kinds.scene ?? 0,
10229
9461
  sourceDuration: d.take.sourceDuration,
10230
9462
  outputDuration: d.take.outputDuration,
@@ -10248,14 +9480,14 @@ async function cmdOpen(argv) {
10248
9480
  if (!dir) throw new UsageError("vos open <take> [--studio <url>]");
10249
9481
  const r = createReporter(flags.json === true);
10250
9482
  const take = await loadTake(dir);
10251
- if (!existsSync13(take.paths.recording)) {
9483
+ if (!existsSync14(take.paths.recording)) {
10252
9484
  throw new UsageError(`${dir} has no recording.webm \u2014 re-run record`);
10253
9485
  }
10254
9486
  const studio = (strFlag(flags, "studio") ?? "http://localhost:6060").replace(
10255
9487
  /\/+$/,
10256
9488
  ""
10257
9489
  );
10258
- const server = await startTakeServer(resolve8(dir), {});
9490
+ const server = await startTakeServer(resolve9(dir), {});
10259
9491
  const url = `${studio}/studio?take=${encodeURIComponent(server.base)}`;
10260
9492
  r.event({ event: "open", server: server.base, url });
10261
9493
  r.log(`take served at ${server.base}`);
@@ -10289,11 +9521,12 @@ async function cmdJudge(argv) {
10289
9521
  "vos judge <kit.json> --against <MANIFEST.json> [--out dir] [--json]\n(the manifest names the reference set: id, file, role, layout, facts, rule per asset)"
10290
9522
  );
10291
9523
  const r = createReporter(flags.json === true);
10292
- const kitFile = target.endsWith("kit.json") ? target : join16(target, "kit.json");
10293
- if (!existsSync13(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
10294
- if (!existsSync13(against)) throw new UsageError(`${against}: no reference manifest`);
9524
+ const kitFile = target.endsWith("kit.json") ? target : join17(target, "kit.json");
9525
+ if (!existsSync14(kitFile)) throw new UsageError(`${kitFile}: no kit manifest`);
9526
+ if (!existsSync14(against))
9527
+ throw new UsageError(`${against}: no reference manifest`);
10295
9528
  const result = await judgeKit(kitFile, against, strFlag(flags, "out"));
10296
- const verdicts = JSON.parse(await readFile13(result.verdictFile, "utf8")).verdicts;
9529
+ const verdicts = JSON.parse(await readFile14(result.verdictFile, "utf8")).verdicts;
10297
9530
  const rate = winRate(verdicts);
10298
9531
  const lines = result.sheets.map(
10299
9532
  (s) => `${s.asset} vs ${s.reference}: ${s.sheetA}, ${s.sheetB}, ${s.rubric}`
@@ -10313,7 +9546,7 @@ async function cmdValidate(argv) {
10313
9546
  const target = positionals[0];
10314
9547
  if (!target) throw new UsageError("vos validate <actions.json|take>");
10315
9548
  const r = createReporter(flags.json === true);
10316
- const kitTarget = target.endsWith("kit.json") ? target : existsSync13(join16(target, "kit.json")) && !isTakeDir(target) ? join16(target, "kit.json") : null;
9549
+ const kitTarget = target.endsWith("kit.json") ? target : existsSync14(join17(target, "kit.json")) && !isTakeDir(target) ? join17(target, "kit.json") : null;
10317
9550
  if (kitTarget) {
10318
9551
  const picture = flags.picture === true;
10319
9552
  const verdict = await validateKit(kitTarget, { picture });
@@ -10345,9 +9578,9 @@ async function cmdValidate(argv) {
10345
9578
  r.done({ valid: true, target }, `${target}: valid actions file`);
10346
9579
  return EXIT_OK;
10347
9580
  }
10348
- if (!isTakeDir(target) && existsSync13(join16(target, "config.json"))) {
9581
+ if (!isTakeDir(target) && existsSync14(join17(target, "config.json"))) {
10349
9582
  const parsed = JSON.parse(
10350
- await readFile13(join16(target, "config.json"), "utf8")
9583
+ await readFile14(join17(target, "config.json"), "utf8")
10351
9584
  );
10352
9585
  const pre = preflightConfig(parsed);
10353
9586
  const problems2 = pre.ok ? [] : pre.issues.map((i) => `config.json: ${i}`);
@@ -10388,7 +9621,7 @@ async function cmdValidate(argv) {
10388
9621
  const take = await loadTake(target);
10389
9622
  const problems = [];
10390
9623
  const warnings = [];
10391
- if (!existsSync13(take.paths.recording))
9624
+ if (!existsSync14(take.paths.recording))
10392
9625
  problems.push("missing recording.webm (re-run record)");
10393
9626
  if (!take.doc) problems.push("missing doc.json (run plan)");
10394
9627
  if (take.doc) {
@@ -10433,7 +9666,7 @@ async function cmdPush3(argv) {
10433
9666
  const r = createReporter(flags.json === true);
10434
9667
  const overrides = multi.override;
10435
9668
  const result = await pushTake(
10436
- resolve8(dir),
9669
+ resolve9(dir),
10437
9670
  {
10438
9671
  key: strFlag(flags, "key"),
10439
9672
  api: strFlag(flags, "api"),
@@ -10447,11 +9680,15 @@ async function cmdPush3(argv) {
10447
9680
  },
10448
9681
  r
10449
9682
  );
9683
+ const pushedTo = platformOrigin({
9684
+ origin: strFlag(flags, "origin"),
9685
+ api: strFlag(flags, "api")
9686
+ }).replace(/\/+$/, "");
10450
9687
  r.done(
10451
9688
  { ...result },
10452
9689
  `pushed v${result.versionNumber} \u2192 vos ${result.vosId}
10453
- review: https://vos.so/vos/${result.vosId}
10454
- studio: https://vos.so/studio?vos=${result.vosId}`
9690
+ review: ${pushedTo}/vos/${result.vosId}
9691
+ studio: ${pushedTo}/studio?vos=${result.vosId}`
10455
9692
  );
10456
9693
  return EXIT_OK;
10457
9694
  }
@@ -10460,7 +9697,7 @@ async function cmdPull2(argv) {
10460
9697
  const dir = positionals[0] ?? ".";
10461
9698
  const r = createReporter(flags.json === true);
10462
9699
  const result = await pullTake(
10463
- resolve8(dir),
9700
+ resolve9(dir),
10464
9701
  {
10465
9702
  key: strFlag(flags, "key"),
10466
9703
  api: strFlag(flags, "api"),
@@ -10595,4 +9832,4 @@ export {
10595
9832
  convertAgentBrowser,
10596
9833
  run
10597
9834
  };
10598
- //# sourceMappingURL=chunk-Y7BXFF2W.js.map
9835
+ //# sourceMappingURL=chunk-HYUVNBEM.js.map