@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.
- package/.agents/skills/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +43 -4
- package/SKILL.director.md +190 -18
- package/SKILL.platform.md +8 -4
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +3031 -300
- package/dist/src/cli.js +1549 -69
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +55 -9
- package/dist/src/devcli/composition-edit.js +658 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/editor-chat.js +56 -17
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +995 -0
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +28 -22
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +26 -2
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/agency-page.js +1 -1
- package/dist/src/reskin/calendar-page.js +2 -1
- package/dist/src/reskin/chat-page.js +420 -85
- package/dist/src/reskin/discover-page.js +731 -39
- package/dist/src/reskin/document.js +1311 -387
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -446
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +1168 -228
- package/dist/src/reskin/login-page.js +6 -6
- package/dist/src/reskin/pricing-page.js +2 -0
- package/dist/src/reskin/settings-page.js +55 -10
- package/dist/src/reskin/theme.js +365 -20
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +81 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +42 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +117 -0
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +47 -2
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/favicon.ico +0 -0
- 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
|