@mevdragon/vidfarm-devcli 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +166 -0
  2. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  3. package/.agents/skills/vidfarm-media/SKILL.md +43 -4
  4. package/SKILL.director.md +190 -18
  5. package/SKILL.platform.md +8 -4
  6. package/demo/dist/app.css +1 -1
  7. package/demo/dist/app.js +77 -75
  8. package/demo/dist/favicon.ico +0 -0
  9. package/dist/src/app.js +3031 -300
  10. package/dist/src/cli.js +1549 -69
  11. package/dist/src/config.js +7 -0
  12. package/dist/src/devcli/clip-store.js +29 -2
  13. package/dist/src/devcli/clips.js +55 -9
  14. package/dist/src/devcli/composition-edit.js +658 -8
  15. package/dist/src/devcli/local-backend.js +123 -0
  16. package/dist/src/devcli/migrate-local.js +140 -0
  17. package/dist/src/devcli/sync.js +311 -0
  18. package/dist/src/devcli/timeline-edit.js +490 -0
  19. package/dist/src/editor-chat.js +56 -17
  20. package/dist/src/frontend/discover-client.js +130 -0
  21. package/dist/src/frontend/discover-store.js +23 -0
  22. package/dist/src/frontend/file-directory.js +995 -0
  23. package/dist/src/frontend/homepage-view.js +3 -3
  24. package/dist/src/frontend/template-editor-chat.js +28 -22
  25. package/dist/src/landing-page.js +24 -7
  26. package/dist/src/page-shell.js +26 -2
  27. package/dist/src/primitive-registry.js +409 -4
  28. package/dist/src/reskin/agency-page.js +1 -1
  29. package/dist/src/reskin/calendar-page.js +2 -1
  30. package/dist/src/reskin/chat-page.js +420 -85
  31. package/dist/src/reskin/discover-page.js +731 -39
  32. package/dist/src/reskin/document.js +1311 -387
  33. package/dist/src/reskin/help-page.js +1 -0
  34. package/dist/src/reskin/inpaint-clipper-page.js +649 -0
  35. package/dist/src/reskin/inpaint-page.js +2459 -446
  36. package/dist/src/reskin/inpaint-video-page.js +1339 -0
  37. package/dist/src/reskin/library-page.js +1168 -228
  38. package/dist/src/reskin/login-page.js +6 -6
  39. package/dist/src/reskin/pricing-page.js +2 -0
  40. package/dist/src/reskin/settings-page.js +55 -10
  41. package/dist/src/reskin/theme.js +365 -20
  42. package/dist/src/services/billing.js +4 -0
  43. package/dist/src/services/clip-curation/gemini.js +5 -0
  44. package/dist/src/services/clip-curation/hunt.js +81 -1
  45. package/dist/src/services/clip-curation/index.js +2 -1
  46. package/dist/src/services/clip-curation/local-agent.js +4 -3
  47. package/dist/src/services/clip-curation/media-select.js +85 -0
  48. package/dist/src/services/clip-curation/query.js +5 -1
  49. package/dist/src/services/clip-curation/refine.js +50 -20
  50. package/dist/src/services/clip-curation/scan.js +10 -3
  51. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  52. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  53. package/dist/src/services/clip-records.js +42 -1
  54. package/dist/src/services/clip-search.js +43 -13
  55. package/dist/src/services/file-directory.js +117 -0
  56. package/dist/src/services/hyperframes.js +283 -3
  57. package/dist/src/services/serverless-records.js +43 -0
  58. package/dist/src/services/storage.js +47 -2
  59. package/dist/src/services/upstream.js +5 -5
  60. package/dist/src/template-editor-shell.js +16 -2
  61. package/package.json +1 -1
  62. package/public/assets/discover-client-app.js +1 -0
  63. package/public/assets/file-directory-app.js +2 -0
  64. package/public/assets/homepage-client-app.js +12 -12
  65. package/public/assets/page-runtime-client-app.js +24 -24
  66. package/public/assets/placeholders/scene-placeholder.png +0 -0
  67. package/src/assets/favicon.ico +0 -0
  68. package/src/assets/logo-vidfarm.png +0 -0
@@ -0,0 +1,490 @@
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 { duplicateLayer, nudgeLayers, restackLayer, rippleEdit, setComposition, setLayerIdentity, setLayerKeyframes, setLayerStyle, setLayerText, setLayerTiming, setLayerVisual, splitLayer, 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
+ // ── Named layer-edit verbs — devcli twins of the web editor_action set ─────────
284
+ // set-style / set-visual / set-text / set-identity / duplicate / split / retime
285
+ // / set-composition. Same shape as the timeline verbs above: parseArgs →
286
+ // resolveCompositionHtml → primitive → writeFileSync.
287
+ const boolFlag = (values, key) => typeof values[key] === "boolean" ? values[key] : undefined;
288
+ const strVal = (values, key) => typeof values[key] === "string" && values[key].length > 0 ? values[key] : undefined;
289
+ // ── set-style (color/background/typography/opacity) ───────────────────────────
290
+ const SET_STYLE_HELP = `vidfarm set-style — restyle a text/media layer's look (local file write)
291
+ vidfarm set-style [dir|composition.html] --layer <key|slug> [flags]
292
+ --color <css> text foreground color
293
+ --background <css> text background color --background-style plain|outline|highlight-solid|highlight-translucent|fullwidth
294
+ --font-family <name> --font-weight <100-900> --italic / --no-italic --underline / --no-underline
295
+ --text-align <left|center|right|justify> --font-size <px> --border-radius <px>
296
+ --line-height <n> unitless leading --letter-spacing <px> negative tightens
297
+ --opacity <0..1> constant layer opacity --json`;
298
+ export async function runSetStyleCommand(argv) {
299
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
300
+ console.log(SET_STYLE_HELP);
301
+ return;
302
+ }
303
+ const { values, positionals } = parseArgs({
304
+ args: argv, allowPositionals: true,
305
+ options: {
306
+ layer: { type: "string" }, color: { type: "string" }, background: { type: "string" }, "background-style": { type: "string" },
307
+ "font-family": { type: "string" }, "font-weight": { type: "string" }, italic: { type: "boolean" }, "no-italic": { type: "boolean" },
308
+ underline: { type: "boolean" }, "no-underline": { type: "boolean" }, "text-align": { type: "string" }, "font-size": { type: "string" },
309
+ "border-radius": { type: "string" }, "line-height": { type: "string" }, "letter-spacing": { type: "string" }, opacity: { type: "string" }, json: { type: "boolean" }
310
+ }
311
+ });
312
+ const file = resolveCompositionHtml(positionals[0]);
313
+ if (typeof values.layer !== "string" || !values.layer.trim())
314
+ throw new Error("set-style requires --layer <layerKey|slug>.");
315
+ const italic = boolFlag(values, "italic") ?? (boolFlag(values, "no-italic") ? false : undefined);
316
+ const underline = boolFlag(values, "underline") ?? (boolFlag(values, "no-underline") ? false : undefined);
317
+ const result = setLayerStyle(readFileSync(file, "utf8"), values.layer, {
318
+ color: strVal(values, "color"), background: strVal(values, "background"), backgroundStyle: strVal(values, "background-style"),
319
+ fontFamily: strVal(values, "font-family"), fontWeight: num(values, "font-weight"), italic, underline,
320
+ textAlign: strVal(values, "text-align"), fontSize: num(values, "font-size"), borderRadius: num(values, "border-radius"),
321
+ lineHeight: num(values, "line-height"), letterSpacing: num(values, "letter-spacing"), opacity: num(values, "opacity")
322
+ });
323
+ writeFileSync(file, result.html, "utf8");
324
+ if (values.json) {
325
+ console.log(JSON.stringify({ file, layerKey: result.layerKey, changed: result.changed }, null, 2));
326
+ return;
327
+ }
328
+ console.log(`Restyled ${result.layerKey} (${result.changed.join(" ")}).`);
329
+ console.log(`Wrote ${file}`);
330
+ }
331
+ // ── set-visual (geometry + opacity) ───────────────────────────────────────────
332
+ const SET_VISUAL_HELP = `vidfarm set-visual — geometry + opacity of a layer (local file write)
333
+ vidfarm set-visual [dir|composition.html] --layer <key|slug> [--x <%>] [--y <%>] [--width <%>] [--height <%>] [--font-size <px>] [--border-radius <px>] [--opacity <0..1>] [--json]`;
334
+ export async function runSetVisualCommand(argv) {
335
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
336
+ console.log(SET_VISUAL_HELP);
337
+ return;
338
+ }
339
+ const { values, positionals } = parseArgs({
340
+ args: argv, allowPositionals: true,
341
+ options: { layer: { type: "string" }, x: { type: "string" }, y: { type: "string" }, width: { type: "string" }, height: { type: "string" }, "font-size": { type: "string" }, "border-radius": { type: "string" }, opacity: { type: "string" }, json: { type: "boolean" } }
342
+ });
343
+ const file = resolveCompositionHtml(positionals[0]);
344
+ if (typeof values.layer !== "string" || !values.layer.trim())
345
+ throw new Error("set-visual requires --layer <layerKey|slug>.");
346
+ const result = setLayerVisual(readFileSync(file, "utf8"), values.layer, {
347
+ x: num(values, "x"), y: num(values, "y"), width: num(values, "width"), height: num(values, "height"),
348
+ fontSize: num(values, "font-size"), borderRadius: num(values, "border-radius"), opacity: num(values, "opacity")
349
+ });
350
+ writeFileSync(file, result.html, "utf8");
351
+ if (values.json) {
352
+ console.log(JSON.stringify({ file, layerKey: result.layerKey, changed: result.changed }, null, 2));
353
+ return;
354
+ }
355
+ console.log(`Updated ${result.layerKey} visual (${result.changed.join(" ")}).`);
356
+ console.log(`Wrote ${file}`);
357
+ }
358
+ // ── set-text (rewrite words) ──────────────────────────────────────────────────
359
+ const SET_TEXT_HELP = `vidfarm set-text — rewrite a text/caption layer's words (local file write)
360
+ vidfarm set-text [dir|composition.html] --layer <key|slug> --text "new words" [--json]`;
361
+ export async function runSetTextCommand(argv) {
362
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
363
+ console.log(SET_TEXT_HELP);
364
+ return;
365
+ }
366
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { layer: { type: "string" }, text: { type: "string" }, json: { type: "boolean" } } });
367
+ const file = resolveCompositionHtml(positionals[0]);
368
+ if (typeof values.layer !== "string" || !values.layer.trim())
369
+ throw new Error("set-text requires --layer <layerKey|slug>.");
370
+ if (typeof values.text !== "string")
371
+ throw new Error("set-text requires --text \"...\".");
372
+ const result = setLayerText(readFileSync(file, "utf8"), values.layer, values.text);
373
+ writeFileSync(file, result.html, "utf8");
374
+ if (values.json) {
375
+ console.log(JSON.stringify({ file, layerKey: result.layerKey }, null, 2));
376
+ return;
377
+ }
378
+ console.log(`Rewrote text on ${result.layerKey}.`);
379
+ console.log(`Wrote ${file}`);
380
+ }
381
+ // ── set-identity (slug + note) ────────────────────────────────────────────────
382
+ const SET_IDENTITY_HELP = `vidfarm set-identity — set a layer's slug + note (the AI/viral-DNA handle) (local file write)
383
+ vidfarm set-identity [dir|composition.html] --layer <key|slug> [--slug <snake_case>] [--note "role"] [--json] (empty --note clears)`;
384
+ export async function runSetIdentityCommand(argv) {
385
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
386
+ console.log(SET_IDENTITY_HELP);
387
+ return;
388
+ }
389
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { layer: { type: "string" }, slug: { type: "string" }, note: { type: "string" }, json: { type: "boolean" } } });
390
+ const file = resolveCompositionHtml(positionals[0]);
391
+ if (typeof values.layer !== "string" || !values.layer.trim())
392
+ throw new Error("set-identity requires --layer <layerKey|slug>.");
393
+ const result = setLayerIdentity(readFileSync(file, "utf8"), values.layer, {
394
+ slug: typeof values.slug === "string" ? values.slug : undefined,
395
+ note: typeof values.note === "string" ? values.note : undefined
396
+ });
397
+ writeFileSync(file, result.html, "utf8");
398
+ if (values.json) {
399
+ console.log(JSON.stringify({ file, layerKey: result.layerKey }, null, 2));
400
+ return;
401
+ }
402
+ console.log(`Set identity on ${result.layerKey}.`);
403
+ console.log(`Wrote ${file}`);
404
+ }
405
+ // ── duplicate ─────────────────────────────────────────────────────────────────
406
+ const DUPLICATE_HELP = `vidfarm duplicate — clone a layer (local file write)
407
+ vidfarm duplicate [dir|composition.html] --layer <key|slug> [--start <sec>] [--track <n>] [--new-key <id>] [--json]`;
408
+ export async function runDuplicateCommand(argv) {
409
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
410
+ console.log(DUPLICATE_HELP);
411
+ return;
412
+ }
413
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { layer: { type: "string" }, start: { type: "string" }, track: { type: "string" }, "new-key": { type: "string" }, json: { type: "boolean" } } });
414
+ const file = resolveCompositionHtml(positionals[0]);
415
+ if (typeof values.layer !== "string" || !values.layer.trim())
416
+ throw new Error("duplicate requires --layer <layerKey|slug>.");
417
+ const result = duplicateLayer(readFileSync(file, "utf8"), values.layer, { start: num(values, "start"), track: num(values, "track"), newKey: strVal(values, "new-key") });
418
+ writeFileSync(file, result.html, "utf8");
419
+ if (values.json) {
420
+ console.log(JSON.stringify({ file, layerKey: result.layerKey }, null, 2));
421
+ return;
422
+ }
423
+ console.log(`Duplicated → ${result.layerKey}.`);
424
+ console.log(`Wrote ${file}`);
425
+ }
426
+ // ── split ─────────────────────────────────────────────────────────────────────
427
+ const SPLIT_HELP = `vidfarm split — cut a clip in two at a time (local file write)
428
+ vidfarm split [dir|composition.html] --layer <key|slug> --at <sec> [--json]`;
429
+ export async function runSplitCommand(argv) {
430
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
431
+ console.log(SPLIT_HELP);
432
+ return;
433
+ }
434
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { layer: { type: "string" }, at: { type: "string" }, json: { type: "boolean" } } });
435
+ const file = resolveCompositionHtml(positionals[0]);
436
+ if (typeof values.layer !== "string" || !values.layer.trim())
437
+ throw new Error("split requires --layer <layerKey|slug>.");
438
+ const at = num(values, "at");
439
+ if (at === undefined)
440
+ throw new Error("split requires --at <sec>.");
441
+ const result = splitLayer(readFileSync(file, "utf8"), values.layer, at);
442
+ writeFileSync(file, result.html, "utf8");
443
+ if (values.json) {
444
+ console.log(JSON.stringify({ file, firstKey: result.firstKey, secondKey: result.secondKey }, null, 2));
445
+ return;
446
+ }
447
+ console.log(`Split ${result.firstKey} at ${at}s → tail ${result.secondKey}.`);
448
+ console.log(`Wrote ${file}`);
449
+ }
450
+ // ── retime (absolute set of start/duration/track/playback-start) ──────────────
451
+ const RETIME_HELP = `vidfarm retime — set a layer's timing directly (local file write)
452
+ vidfarm retime [dir|composition.html] --layer <key|slug> [--start <sec>] [--duration <sec>] [--track <n>] [--playback-start <sec>] [--json]`;
453
+ export async function runRetimeCommand(argv) {
454
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
455
+ console.log(RETIME_HELP);
456
+ return;
457
+ }
458
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { layer: { type: "string" }, start: { type: "string" }, duration: { type: "string" }, track: { type: "string" }, "playback-start": { type: "string" }, json: { type: "boolean" } } });
459
+ const file = resolveCompositionHtml(positionals[0]);
460
+ if (typeof values.layer !== "string" || !values.layer.trim())
461
+ throw new Error("retime requires --layer <layerKey|slug>.");
462
+ const result = setLayerTiming(readFileSync(file, "utf8"), values.layer, { start: num(values, "start"), duration: num(values, "duration"), track: num(values, "track"), playbackStart: num(values, "playback-start") });
463
+ writeFileSync(file, result.html, "utf8");
464
+ if (values.json) {
465
+ console.log(JSON.stringify({ file, layerKey: result.layerKey, track: result.track }, null, 2));
466
+ return;
467
+ }
468
+ console.log(`Retimed ${result.layerKey} (track ${result.track}).`);
469
+ console.log(`Wrote ${file}`);
470
+ }
471
+ // ── set-composition (canvas / theme) ──────────────────────────────────────────
472
+ const SET_COMPOSITION_HELP = `vidfarm set-composition — resize the canvas / retarget length / recolor background (local file write)
473
+ vidfarm set-composition [dir|composition.html] [--width <px>] [--height <px>] [--duration <sec>] [--background <css>] [--json]`;
474
+ export async function runSetCompositionCommand(argv) {
475
+ if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
476
+ console.log(SET_COMPOSITION_HELP);
477
+ return;
478
+ }
479
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { width: { type: "string" }, height: { type: "string" }, duration: { type: "string" }, background: { type: "string" }, json: { type: "boolean" } } });
480
+ const file = resolveCompositionHtml(positionals[0]);
481
+ const result = setComposition(readFileSync(file, "utf8"), { width: num(values, "width"), height: num(values, "height"), duration: num(values, "duration"), backgroundColor: strVal(values, "background") });
482
+ writeFileSync(file, result.html, "utf8");
483
+ if (values.json) {
484
+ console.log(JSON.stringify({ file, changed: result.changed }, null, 2));
485
+ return;
486
+ }
487
+ console.log(`Updated composition (${result.changed.join(" ")}).`);
488
+ console.log(`Wrote ${file}`);
489
+ }
490
+ //# sourceMappingURL=timeline-edit.js.map