@vibeo/cli 0.1.2 → 0.3.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.
@@ -3,7 +3,7 @@ import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
 
5
5
  // ---------------------------------------------------------------------------
6
- // Embedded templates (so `create` works from npm without the examples/ dir)
6
+ // Embedded templates
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
9
  const TEMPLATE_BASIC = `import React from "react";
@@ -214,112 +214,31 @@ export function Root() {
214
214
  // ---------------------------------------------------------------------------
215
215
 
216
216
  const TEMPLATES: Record<string, { description: string; source: string }> = {
217
- basic: {
218
- description: "Minimal composition with text animation and two scenes",
219
- source: TEMPLATE_BASIC,
220
- },
221
- "audio-reactive": {
222
- description: "Audio visualization with frequency bars and amplitude-driven effects",
223
- source: TEMPLATE_AUDIO_REACTIVE,
224
- },
225
- transitions: {
226
- description: "Scene transitions (fade, slide) between multiple scenes",
227
- source: TEMPLATE_TRANSITIONS,
228
- },
229
- subtitles: {
230
- description: "Video with SRT subtitle overlay",
231
- source: TEMPLATE_SUBTITLES,
232
- },
217
+ basic: { description: "Minimal composition with text animation and two scenes", source: TEMPLATE_BASIC },
218
+ "audio-reactive": { description: "Audio visualization with frequency bars", source: TEMPLATE_AUDIO_REACTIVE },
219
+ transitions: { description: "Scene transitions (fade, slide)", source: TEMPLATE_TRANSITIONS },
220
+ subtitles: { description: "Video with SRT subtitle overlay", source: TEMPLATE_SUBTITLES },
233
221
  };
234
222
 
235
- interface CreateArgs {
236
- name: string;
237
- template: string;
238
- }
239
-
240
- function parseArgs(args: string[]): CreateArgs {
241
- const result: CreateArgs = { name: "", template: "basic" };
242
-
243
- for (let i = 0; i < args.length; i++) {
244
- const arg = args[i]!;
245
- const next = args[i + 1];
246
-
247
- if (arg === "--template" && next) {
248
- result.template = next;
249
- i++;
250
- } else if (arg.startsWith("--template=")) {
251
- result.template = arg.slice("--template=".length);
252
- } else if (arg === "--help" || arg === "-h") {
253
- printHelp();
254
- process.exit(0);
255
- } else if (!arg.startsWith("-") && !result.name) {
256
- result.name = arg;
257
- }
258
- }
259
-
260
- return result;
261
- }
262
-
263
- function printHelp(): void {
264
- console.log(`
265
- vibeo create - Create a new Vibeo project
266
-
267
- Usage:
268
- vibeo create <project-name> [options]
269
-
270
- Options:
271
- --template <name> Template to use (default: basic)
272
- --help Show this help
273
-
274
- Templates:`);
223
+ export const TEMPLATE_NAMES = Object.keys(TEMPLATES);
275
224
 
276
- for (const [name, { description }] of Object.entries(TEMPLATES)) {
277
- console.log(` ${name.padEnd(18)} ${description}`);
278
- }
225
+ export async function createProject(
226
+ name: string,
227
+ template: string,
228
+ ): Promise<{ project: string; template: string; files: string[] }> {
229
+ const tmpl = TEMPLATES[template];
230
+ if (!tmpl) throw new Error(`Unknown template: ${template}`);
279
231
 
280
- console.log(`
281
- Examples:
282
- vibeo create my-video
283
- vibeo create music-viz --template audio-reactive
284
- vibeo create intro --template transitions
285
- `);
286
- }
287
-
288
- export async function createCommand(args: string[]): Promise<void> {
289
- const parsed = parseArgs(args);
290
-
291
- if (!parsed.name) {
292
- console.error("Error: project name is required\n");
293
- printHelp();
294
- process.exit(1);
295
- }
296
-
297
- const template = TEMPLATES[parsed.template];
298
- if (!template) {
299
- console.error(`Error: unknown template "${parsed.template}"`);
300
- console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
301
- process.exit(1);
302
- }
232
+ const projectDir = resolve(name);
233
+ if (existsSync(projectDir)) throw new Error(`Directory "${name}" already exists`);
303
234
 
304
- const projectDir = resolve(parsed.name);
305
- if (existsSync(projectDir)) {
306
- console.error(`Error: directory "${parsed.name}" already exists`);
307
- process.exit(1);
308
- }
309
-
310
- console.log(`\nCreating Vibeo project: ${parsed.name}`);
311
- console.log(`Template: ${parsed.template}\n`);
312
-
313
- // Create project structure
314
235
  await mkdir(join(projectDir, "src"), { recursive: true });
315
236
  await mkdir(join(projectDir, "public"), { recursive: true });
316
237
 
317
- // Write template source
318
- await writeFile(join(projectDir, "src", "index.tsx"), template.source);
238
+ await writeFile(join(projectDir, "src", "index.tsx"), tmpl.source);
319
239
 
320
- // Write package.json
321
240
  const pkg = {
322
- name: parsed.name,
241
+ name,
323
242
  version: "0.0.1",
324
243
  private: true,
325
244
  type: "module",
@@ -347,7 +266,6 @@ export async function createCommand(args: string[]): Promise<void> {
347
266
  };
348
267
  await writeFile(join(projectDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
349
268
 
350
- // Write tsconfig.json
351
269
  const tsconfig = {
352
270
  compilerOptions: {
353
271
  target: "ES2022",
@@ -366,29 +284,8 @@ export async function createCommand(args: string[]): Promise<void> {
366
284
  };
367
285
  await writeFile(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
368
286
 
369
- // Write .gitignore
370
- await writeFile(
371
- join(projectDir, ".gitignore"),
372
- `node_modules/
373
- dist/
374
- out/
375
- *.tmp
376
- .DS_Store
377
- `,
378
- );
287
+ await writeFile(join(projectDir, ".gitignore"), "node_modules/\ndist/\nout/\n*.tmp\n.DS_Store\n");
379
288
 
380
- console.log(` Created ${parsed.name}/`);
381
- console.log(` ├── src/index.tsx`);
382
- console.log(` ├── public/`);
383
- console.log(` ├── package.json`);
384
- console.log(` ├── tsconfig.json`);
385
- console.log(` └── .gitignore`);
386
-
387
- console.log(`
388
- Next steps:
389
- cd ${parsed.name}
390
- bun install
391
- bun run dev # preview in browser
392
- bun run build # render to video
393
- `);
289
+ const files = ["src/index.tsx", "package.json", "tsconfig.json", ".gitignore", "public/"];
290
+ return { project: name, template, files };
394
291
  }
@@ -0,0 +1,246 @@
1
+ import { resolve, join, dirname } from "node:path";
2
+ import { mkdir, writeFile, readFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Embedded skill content (so it works from npm without the skills/ dir)
8
+ // ---------------------------------------------------------------------------
9
+
10
+ async function loadSkills(): Promise<Record<string, string>> {
11
+ // Try to load from the repo's skills/ directory first
12
+ const candidates = [
13
+ join(dirname(import.meta.url.replace("file://", "")), "../../../../skills"),
14
+ join(process.cwd(), "skills"),
15
+ ];
16
+
17
+ for (const dir of candidates) {
18
+ const names = ["vibeo-core", "vibeo-audio", "vibeo-effects", "vibeo-extras", "vibeo-rendering"];
19
+ const skills: Record<string, string> = {};
20
+ let found = true;
21
+
22
+ for (const name of names) {
23
+ const path = join(dir, name, "SKILL.md");
24
+ if (existsSync(path)) {
25
+ skills[name] = await readFile(path, "utf-8");
26
+ } else {
27
+ found = false;
28
+ break;
29
+ }
30
+ }
31
+
32
+ if (found) return skills;
33
+ }
34
+
35
+ // Fallback: generate a minimal combined skill from --llms-full output
36
+ return {
37
+ vibeo: getEmbeddedSkill(),
38
+ };
39
+ }
40
+
41
+ function getEmbeddedSkill(): string {
42
+ return `# Vibeo — React Video Framework
43
+
44
+ Vibeo is a React-based programmatic video framework. Write video compositions as React components, preview in the browser, and render to video with FFmpeg.
45
+
46
+ ## Quick Reference
47
+
48
+ \`\`\`bash
49
+ # Get full CLI docs
50
+ bunx @vibeo/cli --llms-full
51
+
52
+ # Create a project
53
+ bunx @vibeo/cli create my-video --template basic
54
+
55
+ # Preview
56
+ bunx @vibeo/cli preview --entry src/index.tsx
57
+
58
+ # Render
59
+ bunx @vibeo/cli render --entry src/index.tsx --composition MyComp
60
+
61
+ # List compositions
62
+ bunx @vibeo/cli list --entry src/index.tsx
63
+ \`\`\`
64
+
65
+ ## Packages
66
+
67
+ - \`@vibeo/core\` — Composition, Sequence, Loop, useCurrentFrame, useVideoConfig, interpolate, easing
68
+ - \`@vibeo/audio\` — Audio/Video components, 48kHz sync, volume curves, audio mixing
69
+ - \`@vibeo/effects\` — useKeyframes, useSpring, Transition (fade/wipe/slide/dissolve), useAudioData
70
+ - \`@vibeo/extras\` — Subtitle (SRT/VTT), AudioWaveform, AudioSpectrogram, SceneGraph, AudioMix
71
+ - \`@vibeo/player\` — Interactive Player component with controls
72
+ - \`@vibeo/renderer\` — Headless rendering via Playwright + FFmpeg
73
+ - \`@vibeo/cli\` — CLI with incur (supports --llms, --mcp, --schema)
74
+
75
+ ## Core Pattern
76
+
77
+ \`\`\`tsx
78
+ import { Composition, Sequence, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate } from "@vibeo/core";
79
+
80
+ function MyScene() {
81
+ const frame = useCurrentFrame();
82
+ const { width, height, fps } = useVideoConfig();
83
+ const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
84
+ return <div style={{ width, height, opacity }}>Hello</div>;
85
+ }
86
+
87
+ export function Root() {
88
+ return (
89
+ <VibeoRoot>
90
+ <Composition id="MyComp" component={MyScene} width={1920} height={1080} fps={30} durationInFrames={150} />
91
+ </VibeoRoot>
92
+ );
93
+ }
94
+ \`\`\`
95
+
96
+ ## Key Math
97
+
98
+ - Frame to time: \`time = frame / fps\`
99
+ - Samples per frame (audio): \`(48000 * 2) / fps\`
100
+ - Media time with playback rate: uses interpolation with rate scaling
101
+ - Sequence relative frame: \`absoluteFrame - (cumulatedFrom + relativeFrom)\`
102
+ - Loop iteration: \`floor(currentFrame / durationInFrames)\`
103
+
104
+ For full API details, run \`bunx @vibeo/cli --llms-full\` or \`bunx @vibeo/cli <command> --schema\`.
105
+ `;
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // LLM tool targets
110
+ // ---------------------------------------------------------------------------
111
+
112
+ interface Target {
113
+ name: string;
114
+ description: string;
115
+ install: (skills: Record<string, string>, cwd: string) => Promise<string[]>;
116
+ }
117
+
118
+ const home = homedir();
119
+
120
+ const TARGETS: Target[] = [
121
+ {
122
+ name: "claude",
123
+ description: "Claude Code (~/.claude/skills/ + project CLAUDE.md)",
124
+ async install(skills, cwd) {
125
+ const files: string[] = [];
126
+
127
+ // Global skills directory
128
+ const globalDir = join(home, ".claude", "skills");
129
+ await mkdir(globalDir, { recursive: true });
130
+ for (const [name, content] of Object.entries(skills)) {
131
+ const path = join(globalDir, `${name}.md`);
132
+ await writeFile(path, content);
133
+ files.push(path);
134
+ }
135
+
136
+ // Project-level .claude/skills/
137
+ const projectDir = join(cwd, ".claude", "skills");
138
+ await mkdir(projectDir, { recursive: true });
139
+ for (const [name, content] of Object.entries(skills)) {
140
+ const path = join(projectDir, `${name}.md`);
141
+ await writeFile(path, content);
142
+ files.push(path);
143
+ }
144
+
145
+ return files;
146
+ },
147
+ },
148
+ {
149
+ name: "codex",
150
+ description: "OpenAI Codex CLI (project AGENTS.md)",
151
+ async install(skills, cwd) {
152
+ const combined = Object.values(skills).join("\n\n---\n\n");
153
+ const path = join(cwd, "AGENTS.md");
154
+ const header = "# Vibeo — Agent Instructions\n\nThis file is auto-generated by `vibeo install-skills`. It helps Codex CLI understand the Vibeo framework.\n\n";
155
+ await writeFile(path, header + combined);
156
+ return [path];
157
+ },
158
+ },
159
+ {
160
+ name: "cursor",
161
+ description: "Cursor (.cursor/rules/*.mdc)",
162
+ async install(skills, cwd) {
163
+ const dir = join(cwd, ".cursor", "rules");
164
+ await mkdir(dir, { recursive: true });
165
+ const files: string[] = [];
166
+ for (const [name, content] of Object.entries(skills)) {
167
+ const mdcContent = `---\ndescription: ${name} skill for Vibeo video framework\nglobs: **/*.{ts,tsx}\nalwaysApply: false\n---\n\n${content}`;
168
+ const path = join(dir, `${name}.mdc`);
169
+ await writeFile(path, mdcContent);
170
+ files.push(path);
171
+ }
172
+ return files;
173
+ },
174
+ },
175
+ {
176
+ name: "gemini",
177
+ description: "Gemini CLI (GEMINI.md)",
178
+ async install(skills, cwd) {
179
+ const combined = Object.values(skills).join("\n\n---\n\n");
180
+ const path = join(cwd, "GEMINI.md");
181
+ const header = "# Vibeo — Gemini Instructions\n\nThis file is auto-generated by `vibeo install-skills`. It helps Gemini CLI understand the Vibeo framework.\n\n";
182
+ await writeFile(path, header + combined);
183
+ return [path];
184
+ },
185
+ },
186
+ {
187
+ name: "opencode",
188
+ description: "OpenCode (AGENTS.md)",
189
+ async install(skills, cwd) {
190
+ // OpenCode also reads AGENTS.md — same as codex, but we don't double-write
191
+ const path = join(cwd, "AGENTS.md");
192
+ if (existsSync(path)) return []; // Already written by codex target
193
+ const combined = Object.values(skills).join("\n\n---\n\n");
194
+ const header = "# Vibeo — Agent Instructions\n\nThis file is auto-generated by `vibeo install-skills`. It helps AI agents understand the Vibeo framework.\n\n";
195
+ await writeFile(path, header + combined);
196
+ return [path];
197
+ },
198
+ },
199
+ {
200
+ name: "aider",
201
+ description: "Aider (.aider.conf.yml conventions file reference)",
202
+ async install(skills, cwd) {
203
+ const combined = Object.values(skills).join("\n\n---\n\n");
204
+ const path = join(cwd, ".aider.vibeo.md");
205
+ await writeFile(path, combined);
206
+ return [path];
207
+ },
208
+ },
209
+ ];
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Public API
213
+ // ---------------------------------------------------------------------------
214
+
215
+ export async function installSkills(
216
+ targets: string[],
217
+ cwd: string,
218
+ ): Promise<{ installed: Record<string, string[]> }> {
219
+ const skills = await loadSkills();
220
+ const installed: Record<string, string[]> = {};
221
+
222
+ const selectedTargets =
223
+ targets.length === 0
224
+ ? TARGETS // all
225
+ : TARGETS.filter((t) => targets.includes(t.name));
226
+
227
+ if (selectedTargets.length === 0) {
228
+ throw new Error(
229
+ `No matching targets. Available: ${TARGETS.map((t) => t.name).join(", ")}`,
230
+ );
231
+ }
232
+
233
+ for (const target of selectedTargets) {
234
+ const files = await target.install(skills, cwd);
235
+ if (files.length > 0) {
236
+ installed[target.name] = files;
237
+ }
238
+ }
239
+
240
+ return { installed };
241
+ }
242
+
243
+ export const AVAILABLE_TARGETS = TARGETS.map((t) => ({
244
+ name: t.name,
245
+ description: t.description,
246
+ }));
@@ -1,49 +1,6 @@
1
- import { resolve } from "node:path";
2
1
  import { bundle } from "@vibeo/renderer";
3
2
  import { launchBrowser, createPage, closeBrowser } from "@vibeo/renderer";
4
3
 
5
- interface ListArgs {
6
- entry: string;
7
- }
8
-
9
- function parseArgs(args: string[]): ListArgs {
10
- const result: ListArgs = {
11
- entry: "",
12
- };
13
-
14
- for (let i = 0; i < args.length; i++) {
15
- const arg = args[i]!;
16
- const next = args[i + 1];
17
-
18
- if (arg === "--entry" && next) {
19
- result.entry = next;
20
- i++;
21
- } else if (arg.startsWith("--entry=")) {
22
- result.entry = arg.slice("--entry=".length);
23
- } else if (arg === "--help" || arg === "-h") {
24
- printHelp();
25
- process.exit(0);
26
- }
27
- }
28
-
29
- return result;
30
- }
31
-
32
- function printHelp(): void {
33
- console.log(`
34
- vibeo list - List registered compositions
35
-
36
- Usage:
37
- vibeo list --entry <path>
38
-
39
- Required:
40
- --entry <path> Path to the root file with compositions
41
-
42
- Options:
43
- --help Show this help
44
- `);
45
- }
46
-
47
4
  interface CompositionMeta {
48
5
  id: string;
49
6
  width: number;
@@ -52,21 +9,7 @@ interface CompositionMeta {
52
9
  durationInFrames: number;
53
10
  }
54
11
 
55
- /**
56
- * Bundle the entry, launch a browser to evaluate it,
57
- * extract registered compositions, and print a table.
58
- */
59
- export async function listCommand(args: string[]): Promise<void> {
60
- const parsed = parseArgs(args);
61
-
62
- if (!parsed.entry) {
63
- console.error("Error: --entry is required");
64
- printHelp();
65
- process.exit(1);
66
- }
67
-
68
- const entry = resolve(parsed.entry);
69
-
12
+ export async function listCompositions(entry: string): Promise<CompositionMeta[]> {
70
13
  console.log(`Bundling ${entry}...`);
71
14
  const bundleResult = await bundle(entry);
72
15
 
@@ -75,11 +18,8 @@ export async function listCommand(args: string[]): Promise<void> {
75
18
  const page = await createPage(browser, 1920, 1080);
76
19
 
77
20
  await page.goto(bundleResult.url, { waitUntil: "networkidle" });
78
-
79
- // Wait briefly for React to register compositions
80
21
  await page.waitForTimeout(2000);
81
22
 
82
- // Extract composition data from the page
83
23
  const compositions = await page.evaluate(() => {
84
24
  const win = window as typeof window & {
85
25
  vibeo_getCompositions?: () => CompositionMeta[];
@@ -93,44 +33,8 @@ export async function listCommand(args: string[]): Promise<void> {
93
33
  await page.close();
94
34
  await closeBrowser();
95
35
 
96
- if (compositions.length === 0) {
97
- console.log("\nNo compositions found.");
98
- console.log(
99
- "Make sure your entry file exports compositions via <Composition /> components.",
100
- );
101
- return;
102
- }
103
-
104
- // Print table
105
- console.log("\nRegistered compositions:\n");
106
- console.log(
107
- padRight("ID", 25) +
108
- padRight("Width", 8) +
109
- padRight("Height", 8) +
110
- padRight("FPS", 6) +
111
- padRight("Frames", 8) +
112
- "Duration",
113
- );
114
- console.log("-".repeat(70));
115
-
116
- for (const comp of compositions) {
117
- const duration = (comp.durationInFrames / comp.fps).toFixed(1) + "s";
118
- console.log(
119
- padRight(comp.id, 25) +
120
- padRight(String(comp.width), 8) +
121
- padRight(String(comp.height), 8) +
122
- padRight(String(comp.fps), 6) +
123
- padRight(String(comp.durationInFrames), 8) +
124
- duration,
125
- );
126
- }
127
-
128
- console.log();
36
+ return compositions;
129
37
  } finally {
130
38
  await bundleResult.cleanup();
131
39
  }
132
40
  }
133
-
134
- function padRight(str: string, len: number): string {
135
- return str.length >= len ? str : str + " ".repeat(len - str.length);
136
- }
@@ -1,70 +1,6 @@
1
- import { resolve } from "node:path";
2
1
  import { bundle } from "@vibeo/renderer";
3
2
 
4
- interface PreviewArgs {
5
- entry: string;
6
- port: number;
7
- }
8
-
9
- function parseArgs(args: string[]): PreviewArgs {
10
- const result: PreviewArgs = {
11
- entry: "",
12
- port: 3000,
13
- };
14
-
15
- for (let i = 0; i < args.length; i++) {
16
- const arg = args[i]!;
17
- const next = args[i + 1];
18
-
19
- if (arg === "--entry" && next) {
20
- result.entry = next;
21
- i++;
22
- } else if (arg.startsWith("--entry=")) {
23
- result.entry = arg.slice("--entry=".length);
24
- } else if (arg === "--port" && next) {
25
- result.port = parseInt(next, 10);
26
- i++;
27
- } else if (arg.startsWith("--port=")) {
28
- result.port = parseInt(arg.slice("--port=".length), 10);
29
- } else if (arg === "--help" || arg === "-h") {
30
- printHelp();
31
- process.exit(0);
32
- }
33
- }
34
-
35
- return result;
36
- }
37
-
38
- function printHelp(): void {
39
- console.log(`
40
- vibeo preview - Start a dev server with live preview
41
-
42
- Usage:
43
- vibeo preview --entry <path> [options]
44
-
45
- Required:
46
- --entry <path> Path to the root file with compositions
47
-
48
- Options:
49
- --port <number> Port for the dev server (default: 3000)
50
- --help Show this help
51
- `);
52
- }
53
-
54
- /**
55
- * Start a dev server hosting the Player with hot reload.
56
- */
57
- export async function previewCommand(args: string[]): Promise<void> {
58
- const parsed = parseArgs(args);
59
-
60
- if (!parsed.entry) {
61
- console.error("Error: --entry is required");
62
- printHelp();
63
- process.exit(1);
64
- }
65
-
66
- const entry = resolve(parsed.entry);
67
-
3
+ export async function startPreview(entry: string, _port: number): Promise<void> {
68
4
  console.log(`Starting preview server...`);
69
5
  console.log(` Entry: ${entry}`);
70
6
 
@@ -73,7 +9,6 @@ export async function previewCommand(args: string[]): Promise<void> {
73
9
  console.log(`\n Preview running at ${bundleResult.url}`);
74
10
  console.log(` Press Ctrl+C to stop\n`);
75
11
 
76
- // Keep the process alive until interrupted
77
12
  const shutdown = async () => {
78
13
  console.log("\nShutting down preview server...");
79
14
  await bundleResult.cleanup();