@mevdragon/vidfarm-devcli 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.agents/skills/vidfarm-media/SKILL.md +43 -6
  2. package/SKILL.director.md +37 -23
  3. package/SKILL.platform.md +6 -6
  4. package/demo/dist/app.js +69 -69
  5. package/demo/dist/favicon.ico +0 -0
  6. package/dist/src/app.js +1832 -213
  7. package/dist/src/cli.js +238 -17
  8. package/dist/src/devcli/clip-store.js +29 -2
  9. package/dist/src/devcli/clips.js +116 -73
  10. package/dist/src/devcli/composition-edit.js +262 -0
  11. package/dist/src/devcli/timeline-edit.js +283 -0
  12. package/dist/src/editor-chat.js +29 -6
  13. package/dist/src/frontend/discover-client.js +130 -0
  14. package/dist/src/frontend/discover-store.js +23 -0
  15. package/dist/src/frontend/file-directory.js +744 -0
  16. package/dist/src/frontend/template-editor-chat.js +22 -19
  17. package/dist/src/landing-page.js +24 -7
  18. package/dist/src/page-shell.js +25 -1
  19. package/dist/src/reskin/agency-page.js +214 -170
  20. package/dist/src/reskin/calendar-page.js +503 -187
  21. package/dist/src/reskin/chat-page.js +739 -299
  22. package/dist/src/reskin/discover-page.js +1450 -279
  23. package/dist/src/reskin/document.js +1392 -16
  24. package/dist/src/reskin/help-page.js +139 -100
  25. package/dist/src/reskin/index-page.js +1 -1
  26. package/dist/src/reskin/inpaint-page.js +547 -0
  27. package/dist/src/reskin/job-runs-page.js +355 -127
  28. package/dist/src/reskin/library-page.js +1188 -317
  29. package/dist/src/reskin/login-page.js +124 -87
  30. package/dist/src/reskin/pricing-page.js +197 -166
  31. package/dist/src/reskin/settings-page.js +566 -187
  32. package/dist/src/reskin/theme.js +434 -17
  33. package/dist/src/services/clip-curation/gemini.js +5 -0
  34. package/dist/src/services/clip-curation/hunt.js +79 -1
  35. package/dist/src/services/clip-curation/index.js +2 -1
  36. package/dist/src/services/clip-curation/local-agent.js +4 -3
  37. package/dist/src/services/clip-curation/media-select.js +85 -0
  38. package/dist/src/services/clip-curation/query.js +5 -1
  39. package/dist/src/services/clip-curation/refine.js +50 -20
  40. package/dist/src/services/clip-curation/scan.js +10 -3
  41. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  42. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  43. package/dist/src/services/clip-records.js +14 -1
  44. package/dist/src/services/clip-search.js +43 -13
  45. package/dist/src/services/file-directory.js +114 -0
  46. package/dist/src/services/storage.js +24 -1
  47. package/dist/src/services/upstream.js +5 -5
  48. package/dist/src/template-editor-shell.js +16 -2
  49. package/package.json +1 -1
  50. package/public/assets/discover-client-app.js +1 -0
  51. package/public/assets/file-directory-app.js +2 -0
  52. package/public/assets/homepage-client-app.js +12 -12
  53. package/public/assets/page-runtime-client-app.js +24 -24
  54. package/src/assets/favicon.ico +0 -0
  55. package/src/assets/logo-vidfarm.png +0 -0
@@ -606,4 +606,266 @@ export function readTransitions(html) {
606
606
  };
607
607
  }).sort((a, b) => (a.start - b.start) || (a.track - b.track));
608
608
  }
609
+ const KEYFRAME_EASINGS = ["linear", "ease", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end"];
610
+ // Whitelist the CSS timing-function so an easing can never smuggle arbitrary
611
+ // declarations into the injected <style>/inline style. Mirrors the editor's
612
+ // normalizeKeyframeEasing exactly.
613
+ function normalizeKeyframeEasing(raw) {
614
+ const value = (raw ?? "").trim().toLowerCase();
615
+ if (KEYFRAME_EASINGS.includes(value))
616
+ return value;
617
+ const cb = /^cubic-bezier\(\s*(-?\d*\.?\d+\s*,\s*){3}-?\d*\.?\d+\s*\)$/;
618
+ if (cb.test(value))
619
+ return value;
620
+ const steps = /^steps\(\s*\d+\s*(,\s*(start|end|jump-(start|end|none|both))\s*)?\)$/;
621
+ if (steps.test(value))
622
+ return value;
623
+ return "linear";
624
+ }
625
+ function nodeTimelineId(node) {
626
+ return node?.getAttribute?.("data-hf-id") || node?.id || "";
627
+ }
628
+ // Couple the timing fields the way the editor's patchElementTiming does: a start
629
+ // or duration change re-derives data-end; a track change re-derives z-index (the
630
+ // runtime/publish normalizer keeps z-index == track); a media in-point (video/
631
+ // audio) rides on data-media-start + data-playback-start together.
632
+ function applyTimingToNode(node, updates) {
633
+ let start = numAttr(node, "data-start");
634
+ let duration = numAttr(node, "data-duration");
635
+ if (updates.start !== undefined) {
636
+ start = updates.start;
637
+ node.setAttribute("data-start", String(round3(start)));
638
+ }
639
+ if (updates.duration !== undefined) {
640
+ duration = updates.duration;
641
+ node.setAttribute("data-duration", String(round3(duration)));
642
+ }
643
+ if (updates.start !== undefined || updates.duration !== undefined) {
644
+ const s = Number.isFinite(start) ? start : 0;
645
+ const d = Number.isFinite(duration) ? duration : 0;
646
+ node.setAttribute("data-end", String(round3(s + d)));
647
+ }
648
+ if (updates.track !== undefined) {
649
+ const t = Math.max(0, Math.trunc(updates.track));
650
+ node.setAttribute("data-track-index", String(t));
651
+ upsertStyleDecl(node, "z-index", String(t));
652
+ }
653
+ if (updates.playbackStart !== undefined) {
654
+ const ps = String(round3(Math.max(0, updates.playbackStart)));
655
+ node.setAttribute("data-media-start", ps);
656
+ node.setAttribute("data-playback-start", ps);
657
+ }
658
+ }
659
+ // set_layer_keyframes twin: author a script-free CSS @keyframes animation on ONE
660
+ // layer. The rule lives in a <style data-vf-kf="hfk-…"> in <head> (any prior
661
+ // block for the same layer is removed first); the layer gets an inline
662
+ // `animation: hfk-… <dur>s <easing> both paused`. The keyframe name is NEVER
663
+ // vf-* (those are reserved for kenburns/transition/caption machinery); paused +
664
+ // fill:both keeps it seek-safe in preview AND render.
665
+ export function setLayerKeyframes(html, layerKey, opts) {
666
+ const frames = (opts.keyframes ?? []).filter((f) => Number.isFinite(f.offset)).sort((a, b) => a.offset - b.offset);
667
+ if (frames.length < 2) {
668
+ throw new Error("keyframes requires at least 2 stops, each with a numeric offset in 0..1.");
669
+ }
670
+ if (frames.every((f) => f.opacity === undefined && f.translate_x === undefined && f.translate_y === undefined && f.scale === undefined && f.rotate === undefined)) {
671
+ throw new Error("each keyframe needs at least one animated property (opacity, translate_x, translate_y, scale, rotate).");
672
+ }
673
+ const { document, root } = readCompositionDoc(html);
674
+ const node = resolveLayerNode(root, layerKey);
675
+ if (!node)
676
+ throw new Error(`Layer not found: ${layerKey}`);
677
+ const baseId = nodeTimelineId(node) || "layer";
678
+ const animName = `hfk-${baseId.replace(/[^A-Za-z0-9_-]/g, "-")}`;
679
+ const clipDuration = numAttr(node, "data-duration");
680
+ const durationSec = opts.durationSec !== undefined && opts.durationSec > 0
681
+ ? opts.durationSec
682
+ : (Number.isFinite(clipDuration) && clipDuration > 0 ? clipDuration : 1);
683
+ const easing = normalizeKeyframeEasing(opts.easing);
684
+ const cssFrames = frames.map((f) => {
685
+ const pct = Math.max(0, Math.min(100, f.offset * 100));
686
+ const pctLabel = Number(pct.toFixed(3)).toString();
687
+ const transforms = [];
688
+ if (f.translate_x !== undefined || f.translate_y !== undefined) {
689
+ transforms.push(`translate(${f.translate_x ?? 0}%, ${f.translate_y ?? 0}%)`);
690
+ }
691
+ if (f.scale !== undefined)
692
+ transforms.push(`scale(${f.scale})`);
693
+ if (f.rotate !== undefined)
694
+ transforms.push(`rotate(${f.rotate}deg)`);
695
+ const decls = [];
696
+ if (transforms.length)
697
+ decls.push(`transform:${transforms.join(" ")}`);
698
+ if (f.opacity !== undefined)
699
+ decls.push(`opacity:${Math.max(0, Math.min(1, f.opacity))}`);
700
+ return `${pctLabel}%{${decls.join(";")}}`;
701
+ }).join("");
702
+ const keyframeCss = `@keyframes ${animName}{${cssFrames}}`;
703
+ // Replace any prior keyframe block authored for THIS layer, then inject fresh.
704
+ // animName is already restricted to [A-Za-z0-9_-] so no selector escaping needed.
705
+ for (const old of Array.from(document.querySelectorAll(`style[data-vf-kf="${animName}"]`))) {
706
+ old.parentNode?.removeChild(old);
707
+ }
708
+ const styleEl = document.createElement("style");
709
+ styleEl.setAttribute("data-vf-kf", animName);
710
+ styleEl.textContent = keyframeCss;
711
+ const head = document.querySelector("head") ?? document.body ?? document.documentElement;
712
+ head.appendChild(styleEl);
713
+ upsertStyleDecl(node, "animation", `${animName} ${round3(durationSec)}s ${easing} both paused`);
714
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey, stops: frames.length, animName, durationSec: round3(durationSec), easing };
715
+ }
716
+ // nudge_layers twin: relative timeline move of one or more layers, group-aware
717
+ // (a grouped clip carries its whole data-vf-group with it). Start/track clamp to
718
+ // >= 0. Applied in one pass; collisions between the moving clips are NOT
719
+ // auto-resolved (unlike the single-layer restack) — the composition-edit helper
720
+ // only resolves one excluded node at a time.
721
+ export function nudgeLayers(html, keys, opts) {
722
+ const deltaStart = opts.deltaStart ?? 0;
723
+ const deltaTrack = opts.deltaTrack ?? 0;
724
+ if (!deltaStart && !deltaTrack) {
725
+ throw new Error("nudge needs a non-zero --delta-start (seconds) and/or --delta-track (lanes).");
726
+ }
727
+ const cleanKeys = keys.filter((k) => typeof k === "string" && k.trim().length > 0);
728
+ if (!cleanKeys.length)
729
+ throw new Error("nudge requires --layer <key> (repeatable).");
730
+ const { document, root } = readCompositionDoc(html);
731
+ const moveSet = new Map();
732
+ for (const key of cleanKeys) {
733
+ const seed = resolveLayerNode(root, key);
734
+ if (!seed)
735
+ throw new Error(`Layer not found: ${key}`);
736
+ const groupId = seed.getAttribute("data-vf-group");
737
+ if (groupId) {
738
+ const escaped = groupId.replace(/["\\]/g, "\\$&");
739
+ for (const gn of Array.from(root.querySelectorAll(`[data-vf-group="${escaped}"]`))) {
740
+ moveSet.set(nodeTimelineId(gn) || `n${moveSet.size}`, gn);
741
+ }
742
+ }
743
+ else {
744
+ moveSet.set(nodeTimelineId(seed) || `n${moveSet.size}`, seed);
745
+ }
746
+ }
747
+ for (const node of moveSet.values()) {
748
+ const start = numAttr(node, "data-start");
749
+ const track = numAttr(node, "data-track-index");
750
+ applyTimingToNode(node, {
751
+ start: Math.max(0, (Number.isFinite(start) ? start : 0) + deltaStart),
752
+ track: Math.max(0, (Number.isFinite(track) ? track : 0) + Math.trunc(deltaTrack))
753
+ });
754
+ }
755
+ return { html: serialize(document), moved: Array.from(moveSet.values()).map((n) => nodeTimelineId(n)) };
756
+ }
757
+ // ripple_edit twin: insert (delta > 0) or close (delta < 0) time at `at`,
758
+ // shifting every clip whose data-start >= at across ALL tracks so downstream
759
+ // timing stays in sync. Starts clamp to >= 0.
760
+ export function rippleEdit(html, opts) {
761
+ if (!Number.isFinite(opts.at))
762
+ throw new Error("ripple requires --at (seconds on the composition timeline).");
763
+ if (!opts.delta || !Number.isFinite(opts.delta)) {
764
+ throw new Error("ripple requires a non-zero --delta (seconds; positive inserts a gap, negative closes one).");
765
+ }
766
+ const { document, root } = readCompositionDoc(html);
767
+ const affected = collectClips(root).filter((node) => {
768
+ const s = numAttr(node, "data-start");
769
+ return Number.isFinite(s) && s >= opts.at - 0.0001;
770
+ });
771
+ if (!affected.length)
772
+ throw new Error(`No clips start at or after ${opts.at}s to ripple.`);
773
+ for (const node of affected) {
774
+ const s = numAttr(node, "data-start");
775
+ applyTimingToNode(node, { start: Math.max(0, s + opts.delta) });
776
+ }
777
+ return { html: serialize(document), affected: affected.length };
778
+ }
779
+ // trim_layer twin: move ONE edge of a clip to a composition time. edge='start'
780
+ // advances data-start, shrinks data-duration, and (video/audio only) pushes the
781
+ // media in-point forward by the same delta so the visible frame is unchanged;
782
+ // edge='end' just changes data-duration. Guards against non-positive duration.
783
+ export function trimLayer(html, layerKey, opts) {
784
+ if (opts.edge !== "start" && opts.edge !== "end")
785
+ throw new Error("trim requires --edge start|end.");
786
+ if (!Number.isFinite(opts.to))
787
+ throw new Error("trim requires --to (seconds on the composition timeline).");
788
+ const { document, root } = readCompositionDoc(html);
789
+ const node = resolveLayerNode(root, layerKey);
790
+ if (!node)
791
+ throw new Error(`Layer not found: ${layerKey}`);
792
+ const start = numAttr(node, "data-start");
793
+ const duration = numAttr(node, "data-duration");
794
+ const s = Number.isFinite(start) ? start : 0;
795
+ const d = Number.isFinite(duration) ? duration : 0;
796
+ const end = s + d;
797
+ const tag = String(node.tagName || "").toLowerCase();
798
+ const isMedia = tag === "video" || tag === "audio";
799
+ const mediaStart = numAttr(node, "data-media-start");
800
+ const ms = Number.isFinite(mediaStart) ? mediaStart : 0;
801
+ const updates = {};
802
+ if (opts.edge === "start") {
803
+ if (opts.to <= s + 0.0001) {
804
+ throw new Error(`Left trim --to (${opts.to}s) must be after the clip start (${s}s). Use \`move\` to shift the clip earlier.`);
805
+ }
806
+ if (opts.to >= end - 0.05) {
807
+ throw new Error("Left trim would leave no duration — pick a --to before the clip's end.");
808
+ }
809
+ const delta = opts.to - s;
810
+ updates.start = opts.to;
811
+ updates.duration = d - delta;
812
+ if (isMedia)
813
+ updates.playbackStart = ms + delta;
814
+ }
815
+ else {
816
+ if (opts.to <= s + 0.05) {
817
+ throw new Error(`Right trim --to (${opts.to}s) must be after the clip start (${s}s).`);
818
+ }
819
+ updates.duration = opts.to - s;
820
+ }
821
+ applyTimingToNode(node, updates);
822
+ const report = {};
823
+ if (updates.start !== undefined)
824
+ report.start = round3(updates.start);
825
+ if (updates.duration !== undefined)
826
+ report.duration = round3(updates.duration);
827
+ if (updates.playbackStart !== undefined)
828
+ report.playback_start = round3(updates.playbackStart);
829
+ return { html: serialize(document), layerKey: nodeTimelineId(node) || layerKey, updates: report };
830
+ }
831
+ // set_layer_zindex twin: restack a layer. Stacking == data-track-index (higher
832
+ // draws on top). front = maxTrack+1, back = max(0,minTrack-1), forward = track+1,
833
+ // backward = max(0,track-1); an explicit --track wins. The single-node collision
834
+ // resolver (findNonCollidingTrack) re-lanes upward when the target collides, so
835
+ // 'back'/'backward' are best-effort (two time-overlapping clips can't share a lane).
836
+ export function restackLayer(html, layerKey, opts) {
837
+ if (!opts.order && opts.track === undefined) {
838
+ throw new Error("restack needs --order front|back|forward|backward or an explicit --track.");
839
+ }
840
+ const { document, root } = readCompositionDoc(html);
841
+ const node = resolveLayerNode(root, layerKey);
842
+ if (!node)
843
+ throw new Error(`Layer not found: ${layerKey}`);
844
+ const clips = collectClips(root);
845
+ const tracks = clips.map((n) => numAttr(n, "data-track-index")).filter((t) => Number.isFinite(t));
846
+ const currentTrack = numAttr(node, "data-track-index");
847
+ const ct = Number.isFinite(currentTrack) ? currentTrack : 0;
848
+ const maxTrack = tracks.length ? Math.max(...tracks) : ct;
849
+ const minTrack = tracks.length ? Math.min(...tracks) : ct;
850
+ let targetTrack;
851
+ if (opts.track !== undefined)
852
+ targetTrack = Math.max(0, Math.trunc(opts.track));
853
+ else if (opts.order === "front")
854
+ targetTrack = maxTrack + 1;
855
+ else if (opts.order === "back")
856
+ targetTrack = Math.max(0, minTrack - 1);
857
+ else if (opts.order === "forward")
858
+ targetTrack = ct + 1;
859
+ else
860
+ targetTrack = Math.max(0, ct - 1);
861
+ const excludeId = nodeTimelineId(node);
862
+ const start = numAttr(node, "data-start");
863
+ const dur = numAttr(node, "data-duration");
864
+ const resolvedTrack = findNonCollidingTrack(clips, { start: Number.isFinite(start) ? start : 0, duration: Number.isFinite(dur) ? dur : 0, track: targetTrack }, excludeId);
865
+ if (resolvedTrack === ct) {
866
+ throw new Error(`${excludeId || layerKey} is already at track ${ct} — no stacking change.`);
867
+ }
868
+ applyTimingToNode(node, { track: resolvedTrack });
869
+ return { html: serialize(document), layerKey: excludeId || layerKey, track: resolvedTrack, order: opts.order ?? "explicit" };
870
+ }
609
871
  //# sourceMappingURL=composition-edit.js.map
@@ -0,0 +1,283 @@
1
+ // devcli fine timeline control — the local-file twins of the browser editor's
2
+ // trackpad-level verbs (set_layer_keyframes / nudge_layers / ripple_edit /
3
+ // trim_layer / set_layer_zindex). Pure local DOM edits (linkedom) on a
4
+ // composition.html: no cloud, no wallet — a running `vidfarm serve` live-morphs
5
+ // the change into open editor tabs and `publish` pushes it like any browser
6
+ // edit. Same read/write/serialize idioms as `vidfarm place` / `captions` /
7
+ // `transitions`.
8
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
+ import path from "node:path";
10
+ import { parseArgs } from "node:util";
11
+ import { nudgeLayers, restackLayer, rippleEdit, setLayerKeyframes, trimLayer } from "./composition-edit.js";
12
+ // Convenience motion presets so `keyframes` is usable without hand-writing JSON.
13
+ // Each is a plain KeyframeStop[] — identical to what --keyframes '<json>' takes.
14
+ const KEYFRAME_PRESETS = {
15
+ "fade-in": [{ offset: 0, opacity: 0 }, { offset: 1, opacity: 1 }],
16
+ "fade-out": [{ offset: 0, opacity: 1 }, { offset: 1, opacity: 0 }],
17
+ "fly-up": [{ offset: 0, opacity: 0, translate_y: 40 }, { offset: 1, opacity: 1, translate_y: 0 }],
18
+ "fly-down": [{ offset: 0, opacity: 0, translate_y: -40 }, { offset: 1, opacity: 1, translate_y: 0 }],
19
+ "fly-left": [{ offset: 0, opacity: 0, translate_x: 40 }, { offset: 1, opacity: 1, translate_x: 0 }],
20
+ "fly-right": [{ offset: 0, opacity: 0, translate_x: -40 }, { offset: 1, opacity: 1, translate_x: 0 }],
21
+ "pop-in": [{ offset: 0, opacity: 0, scale: 0.6 }, { offset: 0.7, opacity: 1, scale: 1.05 }, { offset: 1, opacity: 1, scale: 1 }],
22
+ "zoom-in": [{ offset: 0, scale: 1 }, { offset: 1, scale: 1.15 }],
23
+ "spin-in": [{ offset: 0, opacity: 0, rotate: -90, scale: 0.8 }, { offset: 1, opacity: 1, rotate: 0, scale: 1 }],
24
+ "pulse": [{ offset: 0, scale: 1 }, { offset: 0.5, scale: 1.08 }, { offset: 1, scale: 1 }]
25
+ };
26
+ const KEYFRAMES_HELP = `vidfarm keyframes — author a script-free CSS @keyframes animation on ONE layer (local file write)
27
+
28
+ keyframes <dir|composition.html> --layer <key> --preset <name>
29
+ keyframes <dir|composition.html> --layer <key> --keyframes '<json>'
30
+ --layer <key> Target layer (data-hf-id / slug / element id) — required
31
+ --preset <name> Built-in motion: ${Object.keys(KEYFRAME_PRESETS).join(" | ")}
32
+ --keyframes '<json>' Custom stops: an array of >=2 {offset(0..1), opacity?,
33
+ translate_x?, translate_y?, scale?, rotate?}. translate_*
34
+ are percentages of the layer's OWN box, rotate is degrees.
35
+ e.g. --keyframes '[{"offset":0,"opacity":0,"translate_y":40},{"offset":1,"opacity":1,"translate_y":0}]'
36
+ --easing <fn> linear (default) | ease | ease-in | ease-out | ease-in-out
37
+ | cubic-bezier(...) | steps(...) (anything else → linear)
38
+ --duration <sec> Seconds the animation spans (default: the clip's duration)
39
+ --json
40
+
41
+ Writes a <style data-vf-kf="hfk-<layer>"> keyframe block + an inline
42
+ "animation: hfk-<layer> <dur>s <easing> both paused" on the layer. paused +
43
+ fill:both is seek-safe: it previews, serve-live-morphs, and bakes into the MP4
44
+ identically. The keyframe name is never vf-* (reserved for kenburns/transition/caption).`;
45
+ const MOVE_HELP = `vidfarm move — shift layers on the timeline by a relative delta (local file write)
46
+
47
+ move <dir|composition.html> --layer <key> [--layer <key> …]
48
+ --layer <key> Layer to move (repeatable); a grouped clip carries its
49
+ whole data-vf-group with it
50
+ --delta-start <sec> Shift start by N seconds (clamped to >= 0)
51
+ --delta-track <lanes> Shift track/lane by N (clamped to >= 0; higher = on top)
52
+ --json
53
+
54
+ Alias: move | nudge. Collisions between the moving clips are not auto-resolved
55
+ (use \`restack\` for single-layer lane resolution).`;
56
+ const RIPPLE_HELP = `vidfarm ripple — insert or close time across the whole timeline (local file write)
57
+
58
+ ripple <dir|composition.html> --at <sec> --delta <sec>
59
+ --at <sec> Ripple point on the composition timeline
60
+ --delta <sec> Shift every clip that starts at/after --at by N seconds
61
+ (positive inserts a gap, negative closes one; clamps >= 0)
62
+ --json`;
63
+ const TRIM_HELP = `vidfarm trim — move ONE edge of a clip to a time (local file write)
64
+
65
+ trim <dir|composition.html> --layer <key> --edge start|end --to <sec>
66
+ --layer <key> Clip to trim — required
67
+ --edge start|end 'start' advances the in-point (and, for video/audio,
68
+ pushes the media start so the visible frame is unchanged);
69
+ 'end' changes the duration
70
+ --to <sec> New edge position on the composition timeline
71
+ --json`;
72
+ const RESTACK_HELP = `vidfarm restack — change a layer's stacking order (local file write)
73
+
74
+ restack <dir|composition.html> --layer <key> --order front|back|forward|backward
75
+ restack <dir|composition.html> --layer <key> --track <n>
76
+ --layer <key> Layer to restack — required
77
+ --order <o> front (top of all) | back | forward (+1) | backward (-1);
78
+ stacking == track index (higher draws on top)
79
+ --track <n> Explicit target lane (wins over --order)
80
+ --json
81
+
82
+ Alias: restack | zindex. 'back'/'backward' are best-effort — two time-overlapping
83
+ clips can't share a lane, so the resolver re-lanes upward on collision.`;
84
+ // Same dir-or-file resolution as `vidfarm place` / `captions` / `transitions`.
85
+ function resolveCompositionHtml(target) {
86
+ if (!target)
87
+ throw new Error("Provide the composition dir or composition.html path.");
88
+ const abs = path.resolve(process.cwd(), target);
89
+ if (existsSync(abs) && statSync(abs).isDirectory()) {
90
+ const inside = path.join(abs, "composition.html");
91
+ if (!existsSync(inside))
92
+ throw new Error(`No composition.html inside ${abs}. Run \`vidfarm pull <forkId> --dir ${target}\` first.`);
93
+ return inside;
94
+ }
95
+ if (!existsSync(abs))
96
+ throw new Error(`No such composition file or dir: ${abs}.`);
97
+ return abs;
98
+ }
99
+ function num(values, key) {
100
+ const raw = values[key];
101
+ if (typeof raw !== "string" || !raw.trim())
102
+ return undefined;
103
+ const parsed = Number(raw);
104
+ if (!Number.isFinite(parsed))
105
+ throw new Error(`--${key} must be a number.`);
106
+ return parsed;
107
+ }
108
+ // ── keyframes ────────────────────────────────────────────────────────────────
109
+ export async function runKeyframesCommand(argv) {
110
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
111
+ console.log(KEYFRAMES_HELP);
112
+ return;
113
+ }
114
+ const { values, positionals } = parseArgs({
115
+ args: argv,
116
+ allowPositionals: true,
117
+ options: {
118
+ layer: { type: "string" },
119
+ preset: { type: "string" },
120
+ keyframes: { type: "string" },
121
+ easing: { type: "string" },
122
+ duration: { type: "string" },
123
+ json: { type: "boolean" }
124
+ }
125
+ });
126
+ const file = resolveCompositionHtml(positionals[0]);
127
+ if (typeof values.layer !== "string" || !values.layer.trim()) {
128
+ throw new Error("keyframes requires --layer <layerKey|slug>.");
129
+ }
130
+ let frames;
131
+ if (typeof values.keyframes === "string" && values.keyframes.trim()) {
132
+ let parsed;
133
+ try {
134
+ parsed = JSON.parse(values.keyframes);
135
+ }
136
+ catch {
137
+ throw new Error("--keyframes must be a JSON array of stops, e.g. '[{\"offset\":0,\"opacity\":0},{\"offset\":1,\"opacity\":1}]'.");
138
+ }
139
+ if (!Array.isArray(parsed))
140
+ throw new Error("--keyframes must be a JSON ARRAY of stops.");
141
+ frames = parsed;
142
+ }
143
+ else if (typeof values.preset === "string" && values.preset.trim()) {
144
+ const preset = KEYFRAME_PRESETS[values.preset.trim()];
145
+ if (!preset)
146
+ throw new Error(`Unknown --preset "${values.preset}". Use one of: ${Object.keys(KEYFRAME_PRESETS).join(", ")} (or pass --keyframes '<json>').`);
147
+ frames = preset.map((f) => ({ ...f }));
148
+ }
149
+ else {
150
+ throw new Error(`keyframes needs --preset <name> or --keyframes '<json>'. Presets: ${Object.keys(KEYFRAME_PRESETS).join(", ")}.`);
151
+ }
152
+ const result = setLayerKeyframes(readFileSync(file, "utf8"), values.layer, {
153
+ keyframes: frames,
154
+ easing: typeof values.easing === "string" ? values.easing : undefined,
155
+ durationSec: num(values, "duration")
156
+ });
157
+ writeFileSync(file, result.html, "utf8");
158
+ if (values.json) {
159
+ console.log(JSON.stringify({ file, layerKey: result.layerKey, animation: result.animName, stops: result.stops, duration: result.durationSec, easing: result.easing }, null, 2));
160
+ return;
161
+ }
162
+ console.log(`Set a ${result.stops}-stop CSS keyframe animation on ${result.layerKey} (${result.animName} ${result.durationSec}s ${result.easing}).`);
163
+ console.log(`Wrote ${file}`);
164
+ }
165
+ // ── move / nudge ─────────────────────────────────────────────────────────────
166
+ export async function runMoveCommand(argv) {
167
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
168
+ console.log(MOVE_HELP);
169
+ return;
170
+ }
171
+ const { values, positionals } = parseArgs({
172
+ args: argv,
173
+ allowPositionals: true,
174
+ options: {
175
+ layer: { type: "string", multiple: true },
176
+ "delta-start": { type: "string" },
177
+ "delta-track": { type: "string" },
178
+ json: { type: "boolean" }
179
+ }
180
+ });
181
+ const file = resolveCompositionHtml(positionals[0]);
182
+ const layers = values.layer ?? [];
183
+ if (!layers.length)
184
+ throw new Error("move requires --layer <layerKey|slug> (repeatable).");
185
+ const result = nudgeLayers(readFileSync(file, "utf8"), layers, {
186
+ deltaStart: num(values, "delta-start"),
187
+ deltaTrack: num(values, "delta-track")
188
+ });
189
+ writeFileSync(file, result.html, "utf8");
190
+ if (values.json) {
191
+ console.log(JSON.stringify({ file, moved: result.moved, delta_start: num(values, "delta-start") ?? 0, delta_track: num(values, "delta-track") ?? 0 }, null, 2));
192
+ return;
193
+ }
194
+ console.log(`Moved ${result.moved.length} layer(s) by Δstart=${num(values, "delta-start") ?? 0}s Δtrack=${Math.trunc(num(values, "delta-track") ?? 0)}: ${result.moved.join(", ")}.`);
195
+ console.log(`Wrote ${file}`);
196
+ }
197
+ // ── ripple ───────────────────────────────────────────────────────────────────
198
+ export async function runRippleCommand(argv) {
199
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
200
+ console.log(RIPPLE_HELP);
201
+ return;
202
+ }
203
+ const { values, positionals } = parseArgs({
204
+ args: argv,
205
+ allowPositionals: true,
206
+ options: { at: { type: "string" }, delta: { type: "string" }, json: { type: "boolean" } }
207
+ });
208
+ const file = resolveCompositionHtml(positionals[0]);
209
+ const at = num(values, "at");
210
+ const delta = num(values, "delta");
211
+ if (at === undefined)
212
+ throw new Error("ripple requires --at <sec>.");
213
+ if (delta === undefined)
214
+ throw new Error("ripple requires --delta <sec>.");
215
+ const result = rippleEdit(readFileSync(file, "utf8"), { at, delta });
216
+ writeFileSync(file, result.html, "utf8");
217
+ if (values.json) {
218
+ console.log(JSON.stringify({ file, at, delta, affected: result.affected }, null, 2));
219
+ return;
220
+ }
221
+ console.log(`Rippled ${result.affected} clip(s) at ${at}s by ${delta > 0 ? "+" : ""}${delta}s.`);
222
+ console.log(`Wrote ${file}`);
223
+ }
224
+ // ── trim ─────────────────────────────────────────────────────────────────────
225
+ export async function runTrimCommand(argv) {
226
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
227
+ console.log(TRIM_HELP);
228
+ return;
229
+ }
230
+ const { values, positionals } = parseArgs({
231
+ args: argv,
232
+ allowPositionals: true,
233
+ options: { layer: { type: "string" }, edge: { type: "string" }, to: { type: "string" }, json: { type: "boolean" } }
234
+ });
235
+ const file = resolveCompositionHtml(positionals[0]);
236
+ if (typeof values.layer !== "string" || !values.layer.trim())
237
+ throw new Error("trim requires --layer <layerKey|slug>.");
238
+ if (values.edge !== "start" && values.edge !== "end")
239
+ throw new Error("trim requires --edge start|end.");
240
+ const to = num(values, "to");
241
+ if (to === undefined)
242
+ throw new Error("trim requires --to <sec>.");
243
+ const result = trimLayer(readFileSync(file, "utf8"), values.layer, { edge: values.edge, to });
244
+ writeFileSync(file, result.html, "utf8");
245
+ if (values.json) {
246
+ console.log(JSON.stringify({ file, layerKey: result.layerKey, edge: values.edge, to, updates: result.updates }, null, 2));
247
+ return;
248
+ }
249
+ const parts = Object.entries(result.updates).map(([k, v]) => `${k}=${v}`).join(" ");
250
+ console.log(`Trimmed ${result.layerKey} ${values.edge} to ${to}s (${parts}).`);
251
+ console.log(`Wrote ${file}`);
252
+ }
253
+ // ── restack / zindex ─────────────────────────────────────────────────────────
254
+ export async function runRestackCommand(argv) {
255
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
256
+ console.log(RESTACK_HELP);
257
+ return;
258
+ }
259
+ const { values, positionals } = parseArgs({
260
+ args: argv,
261
+ allowPositionals: true,
262
+ options: { layer: { type: "string" }, order: { type: "string" }, track: { type: "string" }, json: { type: "boolean" } }
263
+ });
264
+ const file = resolveCompositionHtml(positionals[0]);
265
+ if (typeof values.layer !== "string" || !values.layer.trim())
266
+ throw new Error("restack requires --layer <layerKey|slug>.");
267
+ const order = values.order;
268
+ if (order && !["front", "back", "forward", "backward"].includes(order)) {
269
+ throw new Error(`--order must be front | back | forward | backward (got "${order}").`);
270
+ }
271
+ const result = restackLayer(readFileSync(file, "utf8"), values.layer, {
272
+ order: order,
273
+ track: num(values, "track")
274
+ });
275
+ writeFileSync(file, result.html, "utf8");
276
+ if (values.json) {
277
+ console.log(JSON.stringify({ file, layerKey: result.layerKey, track: result.track, order: result.order }, null, 2));
278
+ return;
279
+ }
280
+ console.log(`Restacked ${result.layerKey} to track ${result.track} (${result.order}; higher = on top).`);
281
+ console.log(`Wrote ${file}`);
282
+ }
283
+ //# sourceMappingURL=timeline-edit.js.map