@devmotion/cli 0.0.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.
- package/README.md +195 -0
- package/dist/index.js +1002 -0
- package/package.json +59 -0
- package/scripts/postinstall.mjs +60 -0
- package/templates/bare.html +9 -0
- package/templates/default.html +27 -0
- package/templates/gsap.html +24 -0
- package/templates/lottie.html +26 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { defineCommand as defineCommand8, runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var DEVMOTION_JSON_TEMPLATE = (name) => JSON.stringify(
|
|
11
|
+
{
|
|
12
|
+
version: 1,
|
|
13
|
+
name,
|
|
14
|
+
width: 1920,
|
|
15
|
+
height: 1080,
|
|
16
|
+
fps: 30,
|
|
17
|
+
duration: 10,
|
|
18
|
+
background: { type: "solid", color: "#0a0a0a" },
|
|
19
|
+
scenes: [{ id: "intro", file: "scenes/intro.html", start: 0, duration: 10 }]
|
|
20
|
+
},
|
|
21
|
+
null,
|
|
22
|
+
2
|
|
23
|
+
) + "\n";
|
|
24
|
+
var INTRO_HTML_TEMPLATE = `<!doctype html>
|
|
25
|
+
<html>
|
|
26
|
+
<head><meta charset="utf-8" /></head>
|
|
27
|
+
<body>
|
|
28
|
+
<!-- Each element with data-dm-* becomes a layer in the timeline -->
|
|
29
|
+
<div data-dm-id="title"
|
|
30
|
+
data-dm-enter="fade-in"
|
|
31
|
+
data-dm-enter-time="0.5"
|
|
32
|
+
data-dm-duration="0.8"
|
|
33
|
+
data-dm-easing="ease-out"
|
|
34
|
+
data-dm-keyframe-opacity="0:0; 0.5:1; 9:1; 10:0"
|
|
35
|
+
style="display: flex; align-items: center; justify-content: center;
|
|
36
|
+
width: 100%; height: 100%; color: #fff; font-size: 48px;
|
|
37
|
+
font-family: system-ui, sans-serif;">
|
|
38
|
+
Hello DevMotion
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- <dm-*> custom elements for premium layers -->
|
|
42
|
+
<dm-phone data-dm-id="phone-demo"
|
|
43
|
+
data-dm-enter="slide-in-right"
|
|
44
|
+
data-dm-enter-time="1.5"
|
|
45
|
+
data-dm-duration="0.6"
|
|
46
|
+
data-dm-easing="ease-out"
|
|
47
|
+
src="./assets/screen.mp4">
|
|
48
|
+
</dm-phone>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
`;
|
|
52
|
+
var GITIGNORE_TEMPLATE = `# DevMotion build artifacts
|
|
53
|
+
build/
|
|
54
|
+
dist/
|
|
55
|
+
out/
|
|
56
|
+
*.mp4
|
|
57
|
+
*.webm
|
|
58
|
+
|
|
59
|
+
# Dependencies
|
|
60
|
+
node_modules/
|
|
61
|
+
|
|
62
|
+
# OS
|
|
63
|
+
.DS_Store
|
|
64
|
+
Thumbs.db
|
|
65
|
+
`;
|
|
66
|
+
var README_TEMPLATE = (name) => `# ${name}
|
|
67
|
+
|
|
68
|
+
Created with [DevMotion](https://devmotion.com) \u2014 HTML-first video tooling.
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
# Preview in the Studio
|
|
74
|
+
devmotion preview
|
|
75
|
+
|
|
76
|
+
# Render to MP4
|
|
77
|
+
devmotion render
|
|
78
|
+
|
|
79
|
+
# Add a new scene
|
|
80
|
+
devmotion add-scene outro
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
## Project structure
|
|
84
|
+
|
|
85
|
+
- \`devmotion.json\` \u2014 project configuration (resolution, FPS, duration)
|
|
86
|
+
- \`scenes/*.html\` \u2014 one HTML file per scene, layers = elements with \`data-dm-*\`
|
|
87
|
+
- \`assets/\` \u2014 images, videos, fonts
|
|
88
|
+
|
|
89
|
+
## How it works
|
|
90
|
+
|
|
91
|
+
Every HTML element with \`data-dm-*\` attributes becomes a layer on the timeline.
|
|
92
|
+
Use \`<dm-*>\` custom elements for premium layers (phone, browser, captions, etc.).
|
|
93
|
+
Edit in any text editor or the DevMotion Studio.
|
|
94
|
+
`;
|
|
95
|
+
var initCommand = defineCommand({
|
|
96
|
+
meta: {
|
|
97
|
+
name: "init",
|
|
98
|
+
description: "Scaffold a new DevMotion project"
|
|
99
|
+
},
|
|
100
|
+
args: {
|
|
101
|
+
name: {
|
|
102
|
+
type: "positional",
|
|
103
|
+
description: "Project name (folder name)",
|
|
104
|
+
required: true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async run({ args }) {
|
|
108
|
+
const projectName = args.name;
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
const projectDir = join(cwd, projectName);
|
|
111
|
+
await mkdir(join(projectDir, "scenes"), { recursive: true });
|
|
112
|
+
await mkdir(join(projectDir, "assets"), { recursive: true });
|
|
113
|
+
await writeFile(join(projectDir, "devmotion.json"), DEVMOTION_JSON_TEMPLATE(projectName));
|
|
114
|
+
await writeFile(join(projectDir, "scenes", "intro.html"), INTRO_HTML_TEMPLATE);
|
|
115
|
+
await writeFile(join(projectDir, ".gitignore"), GITIGNORE_TEMPLATE);
|
|
116
|
+
await writeFile(join(projectDir, "README.md"), README_TEMPLATE(projectName));
|
|
117
|
+
console.log(`
|
|
118
|
+
\u2728 DevMotion project created at ./${projectName}/
|
|
119
|
+
`);
|
|
120
|
+
console.log(" Next steps:");
|
|
121
|
+
console.log(` cd ${projectName}`);
|
|
122
|
+
console.log(" devmotion preview");
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/commands/preview.ts
|
|
128
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
129
|
+
import { existsSync as existsSync3 } from "fs";
|
|
130
|
+
import { join as join4 } from "path";
|
|
131
|
+
|
|
132
|
+
// src/lib/studio.ts
|
|
133
|
+
import { spawn } from "child_process";
|
|
134
|
+
import { existsSync } from "fs";
|
|
135
|
+
import { dirname, join as join2 } from "path";
|
|
136
|
+
import { fileURLToPath } from "url";
|
|
137
|
+
import { createRequire } from "module";
|
|
138
|
+
var require2 = createRequire(import.meta.url);
|
|
139
|
+
function findStudioBuild() {
|
|
140
|
+
try {
|
|
141
|
+
const pkgJsonPath = require2.resolve("@devmotion/studio/package.json");
|
|
142
|
+
const studioDir = dirname(pkgJsonPath);
|
|
143
|
+
const distEntry = join2(studioDir, "dist", "index.js");
|
|
144
|
+
if (existsSync(distEntry)) return join2(studioDir, "dist");
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
let dir = process.cwd();
|
|
148
|
+
for (let i = 0; i < 10; i++) {
|
|
149
|
+
const buildEntry = join2(dir, "build", "index.js");
|
|
150
|
+
if (existsSync(buildEntry)) return join2(dir, "build");
|
|
151
|
+
const parent = join2(dir, "..");
|
|
152
|
+
if (parent === dir) break;
|
|
153
|
+
dir = parent;
|
|
154
|
+
}
|
|
155
|
+
const srcDir = fileURLToPath(new URL(".", import.meta.url));
|
|
156
|
+
let walk = srcDir;
|
|
157
|
+
for (let i = 0; i < 8; i++) {
|
|
158
|
+
const buildEntry = join2(walk, "build", "index.js");
|
|
159
|
+
if (existsSync(buildEntry)) return join2(walk, "build");
|
|
160
|
+
const parent = join2(walk, "..");
|
|
161
|
+
if (parent === walk) break;
|
|
162
|
+
walk = parent;
|
|
163
|
+
}
|
|
164
|
+
throw new Error(
|
|
165
|
+
"Studio bundle not found. If you installed @devmotion/cli from npm, try reinstalling. If you are developing from source, run `pnpm build` at the repo root first."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
function findFfmpegPath() {
|
|
169
|
+
try {
|
|
170
|
+
const installer = require2("@ffmpeg-installer/ffmpeg");
|
|
171
|
+
if (installer.path && existsSync(installer.path)) return installer.path;
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
function spawnStudio(opts) {
|
|
177
|
+
const buildDir = findStudioBuild();
|
|
178
|
+
const ffmpegPath = findFfmpegPath();
|
|
179
|
+
const env = {
|
|
180
|
+
...process.env,
|
|
181
|
+
PORT: opts.port,
|
|
182
|
+
HOST: "127.0.0.1",
|
|
183
|
+
PUBLIC_LOCAL_PROJECT_DIR: opts.projectDir,
|
|
184
|
+
PUBLIC_DESKTOP_MODE: "false",
|
|
185
|
+
PUBLIC_NO_AUTH: "true"
|
|
186
|
+
};
|
|
187
|
+
if (ffmpegPath) env.DEVMOTION_FFMPEG_PATH = ffmpegPath;
|
|
188
|
+
return spawn("node", [join2(buildDir, "index.js")], {
|
|
189
|
+
cwd: buildDir,
|
|
190
|
+
env,
|
|
191
|
+
stdio: opts.verbose ? "inherit" : ["ignore", "ignore", "pipe"]
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function waitForStudio(baseUrl, timeoutMs = 3e4) {
|
|
195
|
+
const start = Date.now();
|
|
196
|
+
const probeUrl = `${baseUrl}/api/local/project`;
|
|
197
|
+
while (Date.now() - start < timeoutMs) {
|
|
198
|
+
try {
|
|
199
|
+
const res = await fetch(probeUrl, { method: "GET" });
|
|
200
|
+
if (res.ok || res.status === 404) return;
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`Studio did not become ready within ${timeoutMs}ms at ${baseUrl}`);
|
|
206
|
+
}
|
|
207
|
+
async function isStudioRunning(baseUrl) {
|
|
208
|
+
try {
|
|
209
|
+
const res = await fetch(`${baseUrl}/api/local/project`, { method: "GET" });
|
|
210
|
+
return res.ok || res.status === 404;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/lib/doctor.ts
|
|
217
|
+
import { existsSync as existsSync2 } from "fs";
|
|
218
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
219
|
+
import { createRequire as createRequire2 } from "module";
|
|
220
|
+
import { spawnSync } from "child_process";
|
|
221
|
+
var require3 = createRequire2(import.meta.url);
|
|
222
|
+
function checkFfmpeg() {
|
|
223
|
+
try {
|
|
224
|
+
const installer = require3("@ffmpeg-installer/ffmpeg");
|
|
225
|
+
if (installer.path && existsSync2(installer.path)) {
|
|
226
|
+
return {
|
|
227
|
+
name: "ffmpeg",
|
|
228
|
+
ok: true,
|
|
229
|
+
detail: `${installer.version ?? "bundled"} \u2014 ${installer.path}`
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
name: "ffmpeg",
|
|
234
|
+
ok: false,
|
|
235
|
+
detail: "bundled binary missing on disk",
|
|
236
|
+
fix: "reinstall @devmotion/cli"
|
|
237
|
+
};
|
|
238
|
+
} catch {
|
|
239
|
+
return {
|
|
240
|
+
name: "ffmpeg",
|
|
241
|
+
ok: false,
|
|
242
|
+
detail: "@ffmpeg-installer/ffmpeg not installed",
|
|
243
|
+
fix: "reinstall @devmotion/cli (it ships ffmpeg as a dependency)"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function checkPlaywright() {
|
|
248
|
+
try {
|
|
249
|
+
const { chromium } = require3("playwright");
|
|
250
|
+
const execPath = chromium.executablePath();
|
|
251
|
+
if (execPath && existsSync2(execPath)) {
|
|
252
|
+
return { name: "playwright (chromium)", ok: true, detail: execPath };
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
name: "playwright (chromium)",
|
|
256
|
+
ok: false,
|
|
257
|
+
detail: "browser not downloaded",
|
|
258
|
+
fix: "npx playwright install chromium"
|
|
259
|
+
};
|
|
260
|
+
} catch {
|
|
261
|
+
return {
|
|
262
|
+
name: "playwright (chromium)",
|
|
263
|
+
ok: false,
|
|
264
|
+
detail: "playwright not installed",
|
|
265
|
+
fix: "reinstall @devmotion/cli (it ships playwright as a dependency)"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function runAllChecks() {
|
|
270
|
+
return [checkFfmpeg(), checkPlaywright()];
|
|
271
|
+
}
|
|
272
|
+
function formatCheck(c) {
|
|
273
|
+
const status = c.ok ? "\u2705" : "\u274C";
|
|
274
|
+
const detail = c.detail ? ` \u2014 ${c.detail}` : "";
|
|
275
|
+
const fix = !c.ok && c.fix ? `
|
|
276
|
+
fix: ${c.fix}` : "";
|
|
277
|
+
return ` ${status} ${c.name}${detail}${fix}`;
|
|
278
|
+
}
|
|
279
|
+
function isChromiumInstalled() {
|
|
280
|
+
try {
|
|
281
|
+
const { chromium } = require3("playwright");
|
|
282
|
+
const execPath = chromium.executablePath();
|
|
283
|
+
return !!execPath && existsSync2(execPath);
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function ensureChromium() {
|
|
289
|
+
if (isChromiumInstalled()) return true;
|
|
290
|
+
let cliPath;
|
|
291
|
+
try {
|
|
292
|
+
const pkgJson = require3.resolve("playwright/package.json");
|
|
293
|
+
cliPath = join3(dirname2(pkgJson), "cli.js");
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (!existsSync2(cliPath)) return false;
|
|
298
|
+
console.log(" Downloading Playwright Chromium (~150 MB, one-time)...");
|
|
299
|
+
const res = spawnSync(process.execPath, [cliPath, "install", "chromium"], {
|
|
300
|
+
stdio: "inherit"
|
|
301
|
+
});
|
|
302
|
+
return res.status === 0 && isChromiumInstalled();
|
|
303
|
+
}
|
|
304
|
+
function warnIfRenderDepsMissing() {
|
|
305
|
+
const checks = runAllChecks();
|
|
306
|
+
const missing = checks.filter((c) => !c.ok);
|
|
307
|
+
if (missing.length === 0) return;
|
|
308
|
+
console.warn("\n\u26A0\uFE0F Missing dependencies required for `devmotion render`:");
|
|
309
|
+
for (const c of missing) console.warn(formatCheck(c));
|
|
310
|
+
console.warn(" (preview still works \u2014 fix before running `devmotion render`)\n");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/commands/preview.ts
|
|
314
|
+
var previewCommand = defineCommand2({
|
|
315
|
+
meta: {
|
|
316
|
+
name: "preview",
|
|
317
|
+
description: "Open the DevMotion Studio for the current project (local, no auth)"
|
|
318
|
+
},
|
|
319
|
+
args: {
|
|
320
|
+
port: {
|
|
321
|
+
type: "string",
|
|
322
|
+
description: "Port to listen on (default: 5173)",
|
|
323
|
+
default: "5173"
|
|
324
|
+
},
|
|
325
|
+
open: {
|
|
326
|
+
type: "boolean",
|
|
327
|
+
description: "Open browser automatically",
|
|
328
|
+
default: true
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
async run({ args }) {
|
|
332
|
+
const cwd = process.cwd();
|
|
333
|
+
const configPath = join4(cwd, "devmotion.json");
|
|
334
|
+
if (!existsSync3(configPath)) {
|
|
335
|
+
console.error(
|
|
336
|
+
`
|
|
337
|
+
\u274C No devmotion.json found in ${cwd}
|
|
338
|
+
Run 'devmotion init <name>' first.
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const port = args.port || "5173";
|
|
344
|
+
warnIfRenderDepsMissing();
|
|
345
|
+
console.log(`
|
|
346
|
+
\u{1F680} Starting DevMotion Studio...`);
|
|
347
|
+
console.log(` Project: ${cwd}`);
|
|
348
|
+
console.log(` URL: http://localhost:${port}/editor/local
|
|
349
|
+
`);
|
|
350
|
+
const serverProcess = spawnStudio({ projectDir: cwd, port, verbose: true });
|
|
351
|
+
serverProcess.on("error", (err) => {
|
|
352
|
+
console.error("Failed to start Studio server:", err.message);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
});
|
|
355
|
+
serverProcess.on("exit", (code) => {
|
|
356
|
+
process.exit(code ?? 0);
|
|
357
|
+
});
|
|
358
|
+
process.on("SIGINT", () => serverProcess.kill("SIGINT"));
|
|
359
|
+
process.on("SIGTERM", () => serverProcess.kill("SIGTERM"));
|
|
360
|
+
if (args.open) {
|
|
361
|
+
const { exec } = await import("child_process");
|
|
362
|
+
const url = `http://localhost:${port}/editor/local`;
|
|
363
|
+
const platform = process.platform;
|
|
364
|
+
if (platform === "darwin") {
|
|
365
|
+
exec(`open "${url}"`);
|
|
366
|
+
} else if (platform === "win32") {
|
|
367
|
+
exec(`start "" "${url}"`);
|
|
368
|
+
} else {
|
|
369
|
+
exec(`xdg-open "${url}"`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// src/commands/render.ts
|
|
376
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
377
|
+
import { existsSync as existsSync4, createWriteStream } from "fs";
|
|
378
|
+
import { join as join5 } from "path";
|
|
379
|
+
import { pipeline } from "stream/promises";
|
|
380
|
+
import { Readable } from "stream";
|
|
381
|
+
import { randomUUID } from "crypto";
|
|
382
|
+
import { createInterface } from "readline";
|
|
383
|
+
var renderCommand = defineCommand3({
|
|
384
|
+
meta: {
|
|
385
|
+
name: "render",
|
|
386
|
+
description: "Render the current project to MP4 (auto-spawns Studio if not running)"
|
|
387
|
+
},
|
|
388
|
+
args: {
|
|
389
|
+
output: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "Output file path (default: ./output.mp4)",
|
|
392
|
+
default: "output.mp4"
|
|
393
|
+
},
|
|
394
|
+
studio: {
|
|
395
|
+
type: "string",
|
|
396
|
+
description: "Studio base URL (default: http://localhost:5173)",
|
|
397
|
+
default: "http://localhost:5173"
|
|
398
|
+
},
|
|
399
|
+
port: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Port for auto-spawned Studio (default: 5173)",
|
|
402
|
+
default: "5173"
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
async run({ args }) {
|
|
406
|
+
const cwd = process.cwd();
|
|
407
|
+
if (!existsSync4(join5(cwd, "devmotion.json"))) {
|
|
408
|
+
console.error(
|
|
409
|
+
`
|
|
410
|
+
\u274C No devmotion.json found in ${cwd}
|
|
411
|
+
Run 'devmotion init <name>' first.
|
|
412
|
+
`
|
|
413
|
+
);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
const ffmpeg = checkFfmpeg();
|
|
417
|
+
if (!ffmpeg.ok) {
|
|
418
|
+
console.error("\n\u274C Cannot render \u2014 ffmpeg is unavailable:");
|
|
419
|
+
console.error(formatCheck(ffmpeg));
|
|
420
|
+
console.error("\nRun `devmotion doctor` for details.\n");
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
if (!ensureChromium()) {
|
|
424
|
+
console.error("\n\u274C Cannot render \u2014 Playwright Chromium could not be installed.");
|
|
425
|
+
console.error(" Try manually: npx playwright install chromium\n");
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
const output = String(args.output || "output.mp4");
|
|
429
|
+
const studioUrl = String(args.studio || "http://localhost:5173");
|
|
430
|
+
const port = String(args.port || "5173");
|
|
431
|
+
const outputPath = join5(cwd, output);
|
|
432
|
+
const renderId = randomUUID();
|
|
433
|
+
console.log(`
|
|
434
|
+
DevMotion Render`);
|
|
435
|
+
console.log(` Project: ${cwd}`);
|
|
436
|
+
console.log(` Studio: ${studioUrl}`);
|
|
437
|
+
console.log(` Output: ${outputPath}
|
|
438
|
+
`);
|
|
439
|
+
let spawnedStudio = null;
|
|
440
|
+
if (!await isStudioRunning(studioUrl)) {
|
|
441
|
+
const expectedUrl = `http://localhost:${port}`;
|
|
442
|
+
if (studioUrl !== expectedUrl && studioUrl !== `http://127.0.0.1:${port}`) {
|
|
443
|
+
console.error(`
|
|
444
|
+
\u274C Cannot connect to Studio at ${studioUrl}`);
|
|
445
|
+
console.error(" Start it manually: devmotion preview\n");
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
console.log(" Studio not detected \u2014 starting an ephemeral instance...");
|
|
449
|
+
spawnedStudio = spawnStudio({ projectDir: cwd, port, verbose: false });
|
|
450
|
+
try {
|
|
451
|
+
await waitForStudio(studioUrl, 3e4);
|
|
452
|
+
console.log(" Studio ready.\n");
|
|
453
|
+
} catch (err) {
|
|
454
|
+
spawnedStudio.kill();
|
|
455
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
456
|
+
console.error(`
|
|
457
|
+
\u274C ${msg}
|
|
458
|
+
`);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const cleanup = () => {
|
|
463
|
+
if (spawnedStudio && !spawnedStudio.killed) spawnedStudio.kill();
|
|
464
|
+
};
|
|
465
|
+
process.on("SIGINT", () => {
|
|
466
|
+
cleanup();
|
|
467
|
+
process.exit(130);
|
|
468
|
+
});
|
|
469
|
+
process.on("SIGTERM", () => {
|
|
470
|
+
cleanup();
|
|
471
|
+
process.exit(143);
|
|
472
|
+
});
|
|
473
|
+
try {
|
|
474
|
+
const postRes = await fetch(`${studioUrl}/api/local/render?renderId=${renderId}`, {
|
|
475
|
+
method: "POST"
|
|
476
|
+
});
|
|
477
|
+
if (!postRes.ok) {
|
|
478
|
+
console.error(`
|
|
479
|
+
\u274C Render failed (${postRes.status}): ${await postRes.text()}
|
|
480
|
+
`);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
await new Promise((resolve2, reject) => {
|
|
484
|
+
fetch(`${studioUrl}/api/local/render?renderId=${renderId}`).then(async (sseRes) => {
|
|
485
|
+
if (!sseRes.body) {
|
|
486
|
+
reject(new Error("No SSE body"));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const rl = createInterface({
|
|
490
|
+
input: Readable.fromWeb(sseRes.body)
|
|
491
|
+
});
|
|
492
|
+
rl.on("line", (line) => {
|
|
493
|
+
if (!line.startsWith("data:")) return;
|
|
494
|
+
try {
|
|
495
|
+
const data = JSON.parse(line.slice(5).trim());
|
|
496
|
+
const { phase, percent, currentFrame, totalFrames } = data;
|
|
497
|
+
if (phase === "capturing") {
|
|
498
|
+
process.stdout.write(
|
|
499
|
+
`\r Capturing frames... ${currentFrame}/${totalFrames} (${percent}%)`
|
|
500
|
+
);
|
|
501
|
+
} else if (phase === "encoding") {
|
|
502
|
+
process.stdout.write(
|
|
503
|
+
`\r Encoding... `
|
|
504
|
+
);
|
|
505
|
+
} else if (phase === "initializing") {
|
|
506
|
+
process.stdout.write(`\r Initializing render engine...`);
|
|
507
|
+
} else if (phase === "done") {
|
|
508
|
+
process.stdout.write("\n");
|
|
509
|
+
rl.close();
|
|
510
|
+
resolve2();
|
|
511
|
+
} else if (phase === "error") {
|
|
512
|
+
rl.close();
|
|
513
|
+
reject(new Error(data.error || "Render failed on server"));
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
rl.on("close", () => resolve2());
|
|
519
|
+
}).catch(reject);
|
|
520
|
+
});
|
|
521
|
+
console.log(` Downloading...`);
|
|
522
|
+
const dlRes = await fetch(`${studioUrl}/api/local/render?renderId=${renderId}&download=1`);
|
|
523
|
+
if (!dlRes.ok || !dlRes.body) {
|
|
524
|
+
console.error(`
|
|
525
|
+
\u274C Download failed (${dlRes.status})
|
|
526
|
+
`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
const out = createWriteStream(outputPath);
|
|
530
|
+
await pipeline(Readable.fromWeb(dlRes.body), out);
|
|
531
|
+
console.log(`
|
|
532
|
+
\u2705 Render complete!`);
|
|
533
|
+
console.log(` Output: ${outputPath}
|
|
534
|
+
`);
|
|
535
|
+
} finally {
|
|
536
|
+
cleanup();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// src/commands/mcp.ts
|
|
542
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
543
|
+
import { existsSync as existsSync5 } from "fs";
|
|
544
|
+
import { join as join6 } from "path";
|
|
545
|
+
var mcpCommand = defineCommand4({
|
|
546
|
+
meta: {
|
|
547
|
+
name: "mcp",
|
|
548
|
+
description: "Print MCP server config for Claude Code / Cursor / Windsurf"
|
|
549
|
+
},
|
|
550
|
+
args: {
|
|
551
|
+
port: {
|
|
552
|
+
type: "string",
|
|
553
|
+
description: "Studio port (default: 5173)",
|
|
554
|
+
default: "5173"
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
async run({ args }) {
|
|
558
|
+
const cwd = process.cwd();
|
|
559
|
+
const port = String(args.port || "5173");
|
|
560
|
+
if (!existsSync5(join6(cwd, "devmotion.json"))) {
|
|
561
|
+
console.error(`
|
|
562
|
+
\u274C No devmotion.json found in ${cwd}
|
|
563
|
+
Run 'devmotion init <name>' first.
|
|
564
|
+
`);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
const url = `http://localhost:${port}/mcp`;
|
|
568
|
+
console.log(`
|
|
569
|
+
DevMotion MCP`);
|
|
570
|
+
console.log(` URL: ${url}`);
|
|
571
|
+
console.log(` Requires: devmotion preview running in this directory
|
|
572
|
+
`);
|
|
573
|
+
console.log(`Claude Code (.claude/settings.json):`);
|
|
574
|
+
console.log(JSON.stringify({ mcpServers: { devmotion: { type: "http", url } } }, null, 2));
|
|
575
|
+
console.log(`
|
|
576
|
+
Cursor / Windsurf (mcp.json):`);
|
|
577
|
+
console.log(JSON.stringify({ mcpServers: { devmotion: { url } } }, null, 2));
|
|
578
|
+
console.log(`
|
|
579
|
+
Start the server: devmotion preview
|
|
580
|
+
`);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// src/commands/setup.ts
|
|
585
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
586
|
+
import { writeFile as writeFile2, mkdir as mkdir2, readFile } from "fs/promises";
|
|
587
|
+
import { join as join7 } from "path";
|
|
588
|
+
import { existsSync as existsSync6 } from "fs";
|
|
589
|
+
var SKILLS = {
|
|
590
|
+
"devmotion.md": `# DevMotion \u2014 HTML-first Video Tooling
|
|
591
|
+
|
|
592
|
+
DevMotion is a code-driven video creation platform. Projects are defined as HTML files with \`data-dm-*\` attributes \u2014 every annotated element becomes a layer on the timeline.
|
|
593
|
+
|
|
594
|
+
## Project Structure
|
|
595
|
+
|
|
596
|
+
\`\`\`
|
|
597
|
+
my-video/
|
|
598
|
+
\u251C\u2500\u2500 devmotion.json # Project config (resolution, FPS, duration, scenes)
|
|
599
|
+
\u251C\u2500\u2500 scenes/
|
|
600
|
+
\u2502 \u251C\u2500\u2500 intro.html # One HTML file per scene
|
|
601
|
+
\u2502 \u2514\u2500\u2500 outro.html
|
|
602
|
+
\u2514\u2500\u2500 assets/ # Images, videos, fonts
|
|
603
|
+
\`\`\`
|
|
604
|
+
|
|
605
|
+
## Layers
|
|
606
|
+
|
|
607
|
+
Each HTML element with \`data-dm-*\` attributes is a layer. Two syntaxes:
|
|
608
|
+
|
|
609
|
+
### 1. Generic HTML (\`data-dm-*\` attributes)
|
|
610
|
+
\`\`\`html
|
|
611
|
+
<div data-dm-id="my-layer"
|
|
612
|
+
data-dm-enter="fade-in"
|
|
613
|
+
data-dm-enter-time="0.5"
|
|
614
|
+
data-dm-duration="0.8"
|
|
615
|
+
data-dm-easing="ease-out"
|
|
616
|
+
data-dm-keyframe-opacity="0:0; 0.5:1; 4:1; 5:0"
|
|
617
|
+
style="color: white; font-size: 48px;">
|
|
618
|
+
Hello World
|
|
619
|
+
</div>
|
|
620
|
+
\`\`\`
|
|
621
|
+
|
|
622
|
+
### 2. Premium \`<dm-*>\` Custom Elements
|
|
623
|
+
16 specialized layer types: dm-phone, dm-video, dm-audio, dm-text, dm-image, dm-browser, dm-laptop, dm-terminal, dm-captions, dm-code, dm-button, dm-icon, dm-shape, dm-html, dm-mouse, dm-group
|
|
624
|
+
|
|
625
|
+
\`\`\`html
|
|
626
|
+
<dm-phone data-dm-id="demo"
|
|
627
|
+
data-dm-enter="slide-in-right"
|
|
628
|
+
data-dm-enter-time="1.5"
|
|
629
|
+
data-dm-duration="0.6"
|
|
630
|
+
src="./assets/screen.mp4">
|
|
631
|
+
</dm-phone>
|
|
632
|
+
\`\`\`
|
|
633
|
+
|
|
634
|
+
## Key Attributes
|
|
635
|
+
|
|
636
|
+
| Attribute | Description | Example |
|
|
637
|
+
|-----------|-------------|---------|
|
|
638
|
+
| \`data-dm-id\` | Unique layer identifier | \`"title"\` |
|
|
639
|
+
| \`data-dm-enter\` | Entry animation preset | \`"fade-in"\` |
|
|
640
|
+
| \`data-dm-exit\` | Exit animation preset | \`"fade-out"\` |
|
|
641
|
+
| \`data-dm-enter-time\` | Start time in seconds | \`"0.5"\` |
|
|
642
|
+
| \`data-dm-duration\` | How long on screen | \`"2.0"\` |
|
|
643
|
+
| \`data-dm-easing\` | Animation easing | \`"ease-out"\` |
|
|
644
|
+
| \`data-dm-track\` | Timeline track (z-order) | \`"0"\` |
|
|
645
|
+
| \`data-dm-locked\` | Lock layer from edits | \`"true"\` |
|
|
646
|
+
| \`data-dm-hidden\` | Hide layer | (boolean attribute) |
|
|
647
|
+
| \`data-dm-keyframe-*\` | Property animation | \`data-dm-keyframe-opacity="0:0; 1:1"\` |
|
|
648
|
+
|
|
649
|
+
## Presets
|
|
650
|
+
|
|
651
|
+
fade-in, fade-out, reveal-up, reveal-right, reveal-left, slide-in-left, slide-in-right, slide-in-top, slide-in-bottom, slide-out-left, slide-out-right, wipe-down, wipe-right, scale-in, scale-out, pop, bounce, bounce-in, rotate-in, spin, pulse, shake, float, glow, zoom-in, zoom-out, flip-in-x, flip-in-y, swing
|
|
652
|
+
|
|
653
|
+
## Easing Functions
|
|
654
|
+
|
|
655
|
+
linear, ease-in, ease-out, ease-in-out, ease-in-quad/out/in-out, ease-in-cubic/out/in-out, ease-in-quart/out/in-out, ease-in-quint/out/in-out, ease-in-sine/out/in-out, ease-in-expo/out/in-out, ease-in-circ/out/in-out, ease-in-back/out/in-out, ease-in-bounce/out/in-out, ease-in-elastic/out/in-out
|
|
656
|
+
|
|
657
|
+
## MCP Tools (available when devmotion MCP is connected)
|
|
658
|
+
|
|
659
|
+
- \`get_project\` \u2014 Get current project state with all layers
|
|
660
|
+
- \`create_layer\` \u2014 Add a new layer to the timeline
|
|
661
|
+
- \`edit_layer\` \u2014 Modify an existing layer
|
|
662
|
+
- \`remove_layer\` \u2014 Delete a layer
|
|
663
|
+
- \`configure_project\` \u2014 Change project settings (resolution, FPS, etc.)
|
|
664
|
+
|
|
665
|
+
## Commands
|
|
666
|
+
|
|
667
|
+
- \`devmotion init <name>\` \u2014 Scaffold new project
|
|
668
|
+
- \`devmotion preview\` \u2014 Open Studio preview
|
|
669
|
+
- \`devmotion render\` \u2014 Render to MP4
|
|
670
|
+
- \`devmotion mcp\` \u2014 Start MCP server over stdio
|
|
671
|
+
`,
|
|
672
|
+
"devmotion-init.md": `# /devmotion-init \u2014 Scaffold a New DevMotion Project
|
|
673
|
+
|
|
674
|
+
Run \`devmotion init <name>\` to create a new video project from scratch.
|
|
675
|
+
|
|
676
|
+
## What it does
|
|
677
|
+
|
|
678
|
+
1. Creates a project directory with:
|
|
679
|
+
- \`devmotion.json\` \u2014 1920\xD71080, 30fps, 10s, dark background
|
|
680
|
+
- \`scenes/intro.html\` \u2014 Starter scene with a title and a phone demo
|
|
681
|
+
- \`assets/\` \u2014 Empty directory for media files
|
|
682
|
+
- \`.gitignore\` \u2014 Ignores build artifacts
|
|
683
|
+
- \`README.md\` \u2014 Quick start guide
|
|
684
|
+
|
|
685
|
+
2. The generated \`intro.html\` includes:
|
|
686
|
+
- A centered title div with \`data-dm-*\` attributes (fade-in, keyframed opacity)
|
|
687
|
+
- A \`<dm-phone>\` element with slide-in-right preset
|
|
688
|
+
|
|
689
|
+
## Usage
|
|
690
|
+
|
|
691
|
+
\`\`\`
|
|
692
|
+
/devmotion-init my-product-demo
|
|
693
|
+
\`\`\`
|
|
694
|
+
|
|
695
|
+
Always run this as the FIRST step when a user asks to create a video. After scaffolding:
|
|
696
|
+
- Use the MCP \`create_layer\` tool (or edit HTML directly) to add content
|
|
697
|
+
- Use \`devmotion preview\` to see the result in the Studio
|
|
698
|
+
- Use \`devmotion render\` to export MP4
|
|
699
|
+
|
|
700
|
+
## Important
|
|
701
|
+
|
|
702
|
+
- The command creates a subdirectory in the current working directory
|
|
703
|
+
- After init, cd into the new directory before using other devmotion commands
|
|
704
|
+
`,
|
|
705
|
+
"devmotion-add-layer.md": `# /devmotion-add-layer \u2014 Add a Layer to the Timeline
|
|
706
|
+
|
|
707
|
+
Adds a new animated layer to the current DevMotion project.
|
|
708
|
+
|
|
709
|
+
## When MCP is connected
|
|
710
|
+
|
|
711
|
+
Use the \`create_layer\` MCP tool. Required parameters:
|
|
712
|
+
|
|
713
|
+
| Parameter | Type | Description |
|
|
714
|
+
|-----------|------|-------------|
|
|
715
|
+
| \`layerType\` | string | One of: phone, video, audio, text, image, browser, laptop, terminal, captions, code, button, icon, shape, html, mouse, group |
|
|
716
|
+
| \`enterTime\` | number | Start time in seconds (default: 0) |
|
|
717
|
+
| \`exitTime\` | number | End time (optional) |
|
|
718
|
+
| \`track\` | integer | Z-order track (default: 0) |
|
|
719
|
+
| \`name\` | string | Human-readable name |
|
|
720
|
+
| \`props\` | object | Layer-specific properties |
|
|
721
|
+
| \`preset\` | string | Entry animation preset |
|
|
722
|
+
| \`presetDuration\` | number | Preset duration in seconds (default: 0.5) |
|
|
723
|
+
| \`easing\` | string | Easing function |
|
|
724
|
+
|
|
725
|
+
## Layer-specific props
|
|
726
|
+
|
|
727
|
+
### text
|
|
728
|
+
\`\`\`json
|
|
729
|
+
{ "content": "Hello World", "fontSize": 48, "color": "#ffffff", "fontFamily": "system-ui", "textAlign": "center" }
|
|
730
|
+
\`\`\`
|
|
731
|
+
|
|
732
|
+
### video / audio
|
|
733
|
+
\`\`\`json
|
|
734
|
+
{ "src": "./assets/demo.mp4", "volume": 100 }
|
|
735
|
+
\`\`\`
|
|
736
|
+
|
|
737
|
+
### image
|
|
738
|
+
\`\`\`json
|
|
739
|
+
{ "src": "./assets/logo.png", "fit": "cover" }
|
|
740
|
+
\`\`\`
|
|
741
|
+
|
|
742
|
+
### phone / browser / laptop
|
|
743
|
+
\`\`\`json
|
|
744
|
+
{ "src": "./assets/screen.mp4", "url": "https://example.com" }
|
|
745
|
+
\`\`\`
|
|
746
|
+
|
|
747
|
+
### shape
|
|
748
|
+
\`\`\`json
|
|
749
|
+
{ "shape": "circle", "color": "#3b82f6", "size": 200 }
|
|
750
|
+
\`\`\`
|
|
751
|
+
|
|
752
|
+
### button
|
|
753
|
+
\`\`\`json
|
|
754
|
+
{ "label": "Click Me", "url": "https://example.com", "color": "#3b82f6" }
|
|
755
|
+
\`\`\`
|
|
756
|
+
|
|
757
|
+
### code
|
|
758
|
+
\`\`\`json
|
|
759
|
+
{ "code": "console.log('hello')", "language": "javascript", "showLineNumbers": true }
|
|
760
|
+
\`\`\`
|
|
761
|
+
|
|
762
|
+
### icon
|
|
763
|
+
\`\`\`json
|
|
764
|
+
{ "icon": "star", "color": "#fbbf24", "size": 64 }
|
|
765
|
+
\`\`\`
|
|
766
|
+
|
|
767
|
+
## Best Practices
|
|
768
|
+
|
|
769
|
+
- Layer IDs must be unique within a project
|
|
770
|
+
- Use enterTime to sequence layers (stagger them for professional look)
|
|
771
|
+
- Keep text layers between 0.5-3s for readability
|
|
772
|
+
- Use track to control z-ordering (higher = on top)
|
|
773
|
+
`,
|
|
774
|
+
"devmotion-render.md": `# /devmotion-render \u2014 Render Project to MP4
|
|
775
|
+
|
|
776
|
+
Renders the current DevMotion project to an MP4 video file.
|
|
777
|
+
|
|
778
|
+
## Usage
|
|
779
|
+
|
|
780
|
+
\`\`\`
|
|
781
|
+
/devmotion-render
|
|
782
|
+
\`\`\`
|
|
783
|
+
|
|
784
|
+
## How it works
|
|
785
|
+
|
|
786
|
+
1. Validates the project structure (devmotion.json + all scene HTML files)
|
|
787
|
+
2. Opens a headless browser (Playwright)
|
|
788
|
+
3. Renders each frame deterministically using the virtualized clock
|
|
789
|
+
4. Pipes frames through ffmpeg to produce H.264 MP4
|
|
790
|
+
5. Output: build/out.mp4
|
|
791
|
+
|
|
792
|
+
## Prerequisites
|
|
793
|
+
|
|
794
|
+
- \`devmotion render\` command available (from \`@devmotion/cli\`)
|
|
795
|
+
- ffmpeg installed on the system
|
|
796
|
+
- All asset paths in layers are valid (relative to project root)
|
|
797
|
+
|
|
798
|
+
## Current Status
|
|
799
|
+
|
|
800
|
+
Full headless rendering requires the DevMotion Studio's render pipeline (Playwright + ffmpeg + virtualized clock). To render now:
|
|
801
|
+
1. Run \`devmotion preview\` to open the Studio
|
|
802
|
+
2. Use the Studio's Render button to export MP4
|
|
803
|
+
3. Or use the cloud render API at https://devmotion.app/api/v1/renders
|
|
804
|
+
|
|
805
|
+
## After Render
|
|
806
|
+
|
|
807
|
+
The output MP4 is saved to build/out.mp4. You can play it, upload to social media, or use as input for another DevMotion project.
|
|
808
|
+
`
|
|
809
|
+
};
|
|
810
|
+
var setupCommand = defineCommand5({
|
|
811
|
+
meta: {
|
|
812
|
+
name: "setup",
|
|
813
|
+
description: "Install Claude Code skills and wire MCP server"
|
|
814
|
+
},
|
|
815
|
+
args: {
|
|
816
|
+
dir: {
|
|
817
|
+
type: "positional",
|
|
818
|
+
description: "Target project directory (default: cwd)",
|
|
819
|
+
required: false
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
async run({ args }) {
|
|
823
|
+
const projectDir = args.dir || process.cwd();
|
|
824
|
+
const skillsDir = join7(projectDir, ".claude", "skills");
|
|
825
|
+
const settingsPath = join7(projectDir, ".claude", "settings.local.json");
|
|
826
|
+
await mkdir2(skillsDir, { recursive: true });
|
|
827
|
+
console.log(`\u{1F4C1} ${skillsDir}/`);
|
|
828
|
+
for (const [filename, content] of Object.entries(SKILLS)) {
|
|
829
|
+
const path = join7(skillsDir, filename);
|
|
830
|
+
await writeFile2(path, content);
|
|
831
|
+
console.log(` \u2705 ${filename}`);
|
|
832
|
+
}
|
|
833
|
+
let settings = {};
|
|
834
|
+
if (existsSync6(settingsPath)) {
|
|
835
|
+
try {
|
|
836
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
837
|
+
settings = JSON.parse(raw);
|
|
838
|
+
} catch {
|
|
839
|
+
console.log("\u26A0\uFE0F Could not parse existing settings.local.json, overwriting.");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
settings.mcpServers = {
|
|
843
|
+
...settings.mcpServers || {},
|
|
844
|
+
devmotion: {
|
|
845
|
+
command: "npx",
|
|
846
|
+
args: ["devmotion", "mcp"],
|
|
847
|
+
description: "DevMotion \u2014 HTML-first video tooling. Create, edit, and render video layers via MCP."
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
await writeFile2(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
851
|
+
console.log(` \u2705 settings.local.json (MCP wired)`);
|
|
852
|
+
console.log(`
|
|
853
|
+
\u2728 DevMotion Claude Code integration installed!
|
|
854
|
+
`);
|
|
855
|
+
console.log(" Available slash commands:");
|
|
856
|
+
console.log(" /devmotion-init \u2014 Scaffold new project");
|
|
857
|
+
console.log(" /devmotion-add-layer \u2014 Add a layer");
|
|
858
|
+
console.log(" /devmotion-render \u2014 Render to MP4");
|
|
859
|
+
console.log();
|
|
860
|
+
console.log(" MCP tools (when in a devmotion project dir):");
|
|
861
|
+
console.log(" get_project, create_layer, edit_layer, remove_layer, configure_project");
|
|
862
|
+
console.log();
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// src/commands/add-scene.ts
|
|
867
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
868
|
+
import { readFileSync, writeFileSync, existsSync as existsSync7 } from "fs";
|
|
869
|
+
import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
870
|
+
import { join as join8, dirname as dirname3, resolve } from "path";
|
|
871
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
872
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
873
|
+
function findTemplatesDir() {
|
|
874
|
+
let dir = __dirname;
|
|
875
|
+
for (let i = 0; i < 6; i++) {
|
|
876
|
+
const candidate = resolve(dir, "templates");
|
|
877
|
+
if (existsSync7(join8(candidate, "default.html"))) return candidate;
|
|
878
|
+
const parent = resolve(dir, "..");
|
|
879
|
+
if (parent === dir) break;
|
|
880
|
+
dir = parent;
|
|
881
|
+
}
|
|
882
|
+
throw new Error("DevMotion CLI: could not locate templates/ directory");
|
|
883
|
+
}
|
|
884
|
+
var templatesDir = findTemplatesDir();
|
|
885
|
+
var VALID_TEMPLATES = ["bare", "default", "gsap", "lottie"];
|
|
886
|
+
function loadTemplate(name) {
|
|
887
|
+
const templatePath = join8(templatesDir, `${name}.html`);
|
|
888
|
+
if (!existsSync7(templatePath)) {
|
|
889
|
+
console.error(`\u274C Template "${name}" not found at ${templatePath}`);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
return readFileSync(templatePath, "utf-8");
|
|
893
|
+
}
|
|
894
|
+
var SCENE_TEMPLATES = Object.fromEntries(
|
|
895
|
+
VALID_TEMPLATES.map((name) => [name, loadTemplate(name)])
|
|
896
|
+
);
|
|
897
|
+
var addSceneCommand = defineCommand6({
|
|
898
|
+
meta: {
|
|
899
|
+
name: "add-scene",
|
|
900
|
+
description: "Add a new scene to the project"
|
|
901
|
+
},
|
|
902
|
+
args: {
|
|
903
|
+
name: {
|
|
904
|
+
type: "positional",
|
|
905
|
+
description: "Scene name (used as filename stem)",
|
|
906
|
+
required: true
|
|
907
|
+
},
|
|
908
|
+
with: {
|
|
909
|
+
type: "string",
|
|
910
|
+
description: "Template: bare, gsap, lottie (default: standard scene)",
|
|
911
|
+
default: "default"
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
async run({ args }) {
|
|
915
|
+
const sceneName = args.name;
|
|
916
|
+
const template = args.with || "default";
|
|
917
|
+
const cwd = process.cwd();
|
|
918
|
+
const configPath = join8(cwd, "devmotion.json");
|
|
919
|
+
if (!existsSync7(configPath)) {
|
|
920
|
+
console.error("\u274C No devmotion.json found in current directory.");
|
|
921
|
+
console.error(" Run 'devmotion init <name>' first.");
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
925
|
+
const scenes = config.scenes || [];
|
|
926
|
+
const lastScene = scenes[scenes.length - 1];
|
|
927
|
+
const start = lastScene ? (lastScene.start || 0) + (lastScene.duration || 10) : 0;
|
|
928
|
+
const sceneId = sceneName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
929
|
+
if (scenes.find((s) => s.id === sceneId)) {
|
|
930
|
+
console.error(`\u274C Scene "${sceneId}" already exists.`);
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
const scenesDir = join8(cwd, "scenes");
|
|
934
|
+
await mkdir3(scenesDir, { recursive: true });
|
|
935
|
+
const filename = `scenes/${sceneName.toLowerCase().replace(/\s+/g, "-")}.html`;
|
|
936
|
+
const htmlContent = SCENE_TEMPLATES[template] || SCENE_TEMPLATES.default;
|
|
937
|
+
await writeFile3(join8(cwd, filename), htmlContent);
|
|
938
|
+
scenes.push({
|
|
939
|
+
id: sceneId,
|
|
940
|
+
file: filename,
|
|
941
|
+
start,
|
|
942
|
+
duration: 10
|
|
943
|
+
});
|
|
944
|
+
config.scenes = scenes;
|
|
945
|
+
const totalEnd = start + 10;
|
|
946
|
+
if (!config.duration || config.duration < totalEnd) {
|
|
947
|
+
config.duration = totalEnd;
|
|
948
|
+
}
|
|
949
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
950
|
+
console.log(`
|
|
951
|
+
\u2728 Scene "${sceneId}" added at ${start}s
|
|
952
|
+
`);
|
|
953
|
+
console.log(` File: ${filename}`);
|
|
954
|
+
console.log(` Template: ${template}`);
|
|
955
|
+
console.log(` Total duration: ${config.duration}s`);
|
|
956
|
+
console.log();
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// src/commands/doctor.ts
|
|
961
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
962
|
+
var doctorCommand = defineCommand7({
|
|
963
|
+
meta: {
|
|
964
|
+
name: "doctor",
|
|
965
|
+
description: "Check that ffmpeg and Playwright are installed and ready"
|
|
966
|
+
},
|
|
967
|
+
async run() {
|
|
968
|
+
console.log("\nDevMotion Doctor\n");
|
|
969
|
+
const results = runAllChecks();
|
|
970
|
+
for (const r of results) console.log(formatCheck(r));
|
|
971
|
+
const failed = results.filter((r) => !r.ok);
|
|
972
|
+
if (failed.length === 0) {
|
|
973
|
+
console.log("\nAll checks passed \u2014 you can run `devmotion render`.\n");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
console.log(
|
|
977
|
+
`
|
|
978
|
+
${failed.length} check(s) failed. Install the missing dependencies above, then re-run \`devmotion doctor\`.
|
|
979
|
+
`
|
|
980
|
+
);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// src/index.ts
|
|
986
|
+
var main = defineCommand8({
|
|
987
|
+
meta: {
|
|
988
|
+
name: "devmotion",
|
|
989
|
+
version: "0.0.1",
|
|
990
|
+
description: "HTML-first, agent-friendly video tooling. Write HTML. Render video."
|
|
991
|
+
},
|
|
992
|
+
subCommands: {
|
|
993
|
+
init: () => initCommand,
|
|
994
|
+
preview: () => previewCommand,
|
|
995
|
+
render: () => renderCommand,
|
|
996
|
+
mcp: () => mcpCommand,
|
|
997
|
+
setup: () => setupCommand,
|
|
998
|
+
"add-scene": () => addSceneCommand,
|
|
999
|
+
doctor: () => doctorCommand
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
runMain(main);
|