@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/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);