@vibeo/cli 0.3.3 → 0.3.5

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.
@@ -0,0 +1,364 @@
1
+ # Vibeo Rendering (`@vibeo/renderer` + `@vibeo/cli`)
2
+
3
+ ## Overview
4
+
5
+ `@vibeo/renderer` provides the headless rendering pipeline: bundling a Vibeo project, launching Playwright browser instances, capturing frames, and stitching them into video via FFmpeg. `@vibeo/cli` wraps this in a CLI.
6
+
7
+ **When to use**: When you need to render a composition to a video file, preview it, or list available compositions.
8
+
9
+ ---
10
+
11
+ ## CLI Commands
12
+
13
+ ### `vibeo render`
14
+
15
+ Render a composition to a video file.
16
+
17
+ ```bash
18
+ bunx vibeo render --entry src/index.tsx --composition MyComp --output out.mp4
19
+ ```
20
+
21
+ **Required flags**:
22
+ | Flag | Description |
23
+ |------|-------------|
24
+ | `--entry <path>` | Path to the root file with compositions |
25
+ | `--composition <id>` | Composition ID to render |
26
+
27
+ **Optional flags**:
28
+ | Flag | Default | Description |
29
+ |------|---------|-------------|
30
+ | `--output <path>` | `out/<compositionId>.<ext>` | Output file path |
31
+ | `--fps <number>` | Composition fps | Override frames per second |
32
+ | `--frames <range>` | Full duration | Frame range, e.g. `"0-100"` |
33
+ | `--codec <codec>` | `h264` | `h264 \| h265 \| vp9 \| prores` |
34
+ | `--concurrency <n>` | `CPU cores / 2` | Parallel browser tabs |
35
+ | `--image-format <fmt>` | `png` | `png \| jpeg` |
36
+ | `--quality <n>` | `80` | JPEG quality / CRF value (0-100) |
37
+
38
+ ### `vibeo preview`
39
+
40
+ Start a dev server with live preview in the browser.
41
+
42
+ ```bash
43
+ bunx vibeo preview --entry src/index.tsx --port 3000
44
+ ```
45
+
46
+ **Required flags**:
47
+ | Flag | Description |
48
+ |------|-------------|
49
+ | `--entry <path>` | Path to the root file with compositions |
50
+
51
+ **Optional flags**:
52
+ | Flag | Default | Description |
53
+ |------|---------|-------------|
54
+ | `--port <number>` | `3000` | Port for the dev server |
55
+ | `--help`, `-h` | — | Show help |
56
+
57
+ ### `vibeo list`
58
+
59
+ List all registered compositions in the project. Bundles the entry, launches a headless browser, and prints a table of composition IDs with their dimensions, FPS, and duration.
60
+
61
+ ```bash
62
+ bunx vibeo list --entry src/index.tsx
63
+ ```
64
+
65
+ **Required flags**:
66
+ | Flag | Description |
67
+ |------|-------------|
68
+ | `--entry <path>` | Path to the root file with compositions |
69
+
70
+ **Optional flags**:
71
+ | Flag | Description |
72
+ |------|-------------|
73
+ | `--help`, `-h` | Show help |
74
+
75
+ ---
76
+
77
+ ## Programmatic Rendering API
78
+
79
+ ### `renderComposition(config, compositionInfo): Promise<string>`
80
+
81
+ Full render orchestration: bundle → capture frames → stitch video. Returns the path to the final output file.
82
+
83
+ ```ts
84
+ import { renderComposition } from "@vibeo/renderer";
85
+
86
+ const outputPath = await renderComposition(
87
+ {
88
+ entry: "src/index.tsx",
89
+ compositionId: "MyComp",
90
+ outputPath: "out/video.mp4",
91
+ codec: "h264",
92
+ imageFormat: "png",
93
+ quality: 80,
94
+ fps: null, // use composition fps
95
+ frameRange: null, // render all frames
96
+ concurrency: 4,
97
+ pixelFormat: "yuv420p",
98
+ onProgress: (p) => console.log(`${(p.percent * 100).toFixed(1)}%`),
99
+ },
100
+ { width: 1920, height: 1080, fps: 30, durationInFrames: 300 },
101
+ );
102
+ console.log(`Rendered to ${outputPath}`);
103
+ ```
104
+
105
+ ### `RenderConfig`
106
+
107
+ | Field | Type | Description |
108
+ |-------|------|-------------|
109
+ | `entry` | `string` | Path to the entry file |
110
+ | `compositionId` | `string` | Composition ID |
111
+ | `outputPath` | `string` | Output video path |
112
+ | `codec` | `Codec` | Video codec |
113
+ | `imageFormat` | `ImageFormat` | `"png" \| "jpeg"` |
114
+ | `quality` | `number` | 0-100 quality/CRF |
115
+ | `fps` | `number \| null` | FPS override |
116
+ | `frameRange` | `FrameRange \| null` | `[start, end]` or null for all |
117
+ | `concurrency` | `number` | Parallel browser tabs |
118
+ | `pixelFormat` | `string` | FFmpeg pixel format |
119
+ | `onProgress?` | `(progress: RenderProgress) => void` | Progress callback |
120
+
121
+ ### `RenderProgress`
122
+
123
+ | Field | Type | Description |
124
+ |-------|------|-------------|
125
+ | `framesRendered` | `number` | Frames completed |
126
+ | `totalFrames` | `number` | Total frames |
127
+ | `percent` | `number` | 0-1 fraction |
128
+ | `etaMs` | `number \| null` | Estimated time remaining (ms) |
129
+
130
+ ### Browser Lifecycle
131
+
132
+ ```ts
133
+ import { launchBrowser, closeBrowser, createPage } from "@vibeo/renderer";
134
+
135
+ const browser = await launchBrowser();
136
+ const page = await createPage(browser, 1920, 1080); // browser, width, height
137
+ // ... render frames ...
138
+ await closeBrowser(); // closes the singleton browser, no argument needed
139
+ ```
140
+
141
+ ### Bundler
142
+
143
+ ```ts
144
+ import { bundle } from "@vibeo/renderer";
145
+
146
+ const result: BundleResult = await bundle(entryPath);
147
+ // result.outDir — bundled output directory
148
+ // result.url — URL to access bundled app
149
+ // result.cleanup() — stop server and remove temp files
150
+ ```
151
+
152
+ ### Frame Operations
153
+
154
+ ```ts
155
+ import { seekToFrame, loadBundle, captureFrame } from "@vibeo/renderer";
156
+
157
+ await loadBundle(page, bundleUrl);
158
+ await seekToFrame(page, 42, "MyComp");
159
+ const filePath = await captureFrame(page, outputDir, frameNumber, {
160
+ imageFormat: "png",
161
+ quality: 80, // optional, for jpeg
162
+ });
163
+ ```
164
+
165
+ ### Frame Range Utilities
166
+
167
+ ```ts
168
+ import { parseFrameRange, getRealFrameRange, validateFrameRange } from "@vibeo/renderer";
169
+
170
+ const range = parseFrameRange("0-100", 300); // parseFrameRange(input, durationInFrames) → [0, 100]
171
+ const real = getRealFrameRange(300, range); // getRealFrameRange(durationInFrames, frameRange) → [0, 100]
172
+ const fullRange = getRealFrameRange(300, null); // → [0, 299]
173
+ validateFrameRange([0, 100], 300); // throws on invalid
174
+ ```
175
+
176
+ ### FFmpeg Stitching
177
+
178
+ ```ts
179
+ import { stitchFrames, getContainerExt, stitchAudio } from "@vibeo/renderer";
180
+
181
+ await stitchFrames({
182
+ framesDir: "/tmp/frames",
183
+ outputPath: "out.mp4",
184
+ fps: 30,
185
+ codec: "h264",
186
+ imageFormat: "png",
187
+ pixelFormat: "yuv420p",
188
+ quality: 80,
189
+ width: 1920,
190
+ height: 1080,
191
+ });
192
+
193
+ const ext = getContainerExt("vp9"); // "webm"
194
+
195
+ await stitchAudio({
196
+ videoPath: "out.mp4",
197
+ audioPaths: ["/tmp/audio.wav"],
198
+ outputPath: "final.mp4",
199
+ });
200
+ ```
201
+
202
+ ### Parallel Rendering
203
+
204
+ ```ts
205
+ import { parallelRender } from "@vibeo/renderer";
206
+
207
+ await parallelRender({
208
+ browser, // Playwright Browser instance
209
+ bundleUrl, // URL from bundle()
210
+ compositionId: "MyComp",
211
+ frameRange: [0, 299], // [start, end] inclusive
212
+ outputDir: "/tmp/frames",
213
+ width: 1920,
214
+ height: 1080,
215
+ concurrency: 8,
216
+ imageFormat: "png",
217
+ quality: 80, // optional
218
+ onProgress: (p) => {}, // optional RenderProgress callback
219
+ });
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Codec Options
225
+
226
+ | Codec | Container | Use Case |
227
+ |-------|-----------|----------|
228
+ | `h264` | `.mp4` | General purpose, best compatibility |
229
+ | `h265` | `.mp4` | Better compression, less compatible |
230
+ | `vp9` | `.webm` | Web delivery, open format |
231
+ | `prores` | `.mov` | Professional editing, lossless quality |
232
+
233
+ ### When to use each
234
+
235
+ - **`h264`**: Default. Works everywhere. Good quality/size trade-off.
236
+ - **`h265`**: 30-50% smaller files than h264 at same quality. Use when file size matters and playback devices support it.
237
+ - **`vp9`**: Best for web embedding. Royalty-free.
238
+ - **`prores`**: Editing workflows (Premiere, DaVinci). Large files but no quality loss.
239
+
240
+ ---
241
+
242
+ ## Output Format Options
243
+
244
+ - **`png` frames**: Lossless intermediate frames. Larger but no quality loss during capture.
245
+ - **`jpeg` frames**: Lossy but much smaller intermediate files. Use `--quality` to control (80 is good default).
246
+ - **`pixelFormat: "yuv420p"`**: Standard for h264/h265. Use `"yuv444p"` for maximum color fidelity with prores.
247
+
248
+ ---
249
+
250
+ ## Parallel Rendering Config
251
+
252
+ ```bash
253
+ # Use 8 parallel browser tabs
254
+ bunx vibeo render --entry src/index.tsx --composition MyComp --concurrency 8
255
+ ```
256
+
257
+ The frame range is split evenly across tabs. Default concurrency is `CPU cores / 2`.
258
+
259
+ For a 300-frame video with `--concurrency 4`:
260
+ - Tab 1: frames 0-74
261
+ - Tab 2: frames 75-149
262
+ - Tab 3: frames 150-224
263
+ - Tab 4: frames 225-299
264
+
265
+ ---
266
+
267
+ ## Types
268
+
269
+ ```ts
270
+ import type {
271
+ RenderConfig,
272
+ RenderProgress,
273
+ Codec,
274
+ ImageFormat,
275
+ FrameRange,
276
+ StitchOptions,
277
+ AudioMuxOptions,
278
+ BundleResult,
279
+ } from "@vibeo/renderer";
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Gotchas and Tips
285
+
286
+ 1. **FFmpeg must be installed** and available on `PATH` for stitching to work.
287
+
288
+ 2. **Playwright is required** — the renderer uses it for headless browser rendering. Install with `bunx playwright install chromium`.
289
+
290
+ 3. **Frame capture is the bottleneck** — increase `--concurrency` on machines with many cores to speed up rendering.
291
+
292
+ 4. **`--frames` range is inclusive** — `--frames=0-100` renders 101 frames.
293
+
294
+ 5. **Output directory is created automatically** — you don't need to `mkdir` before rendering.
295
+
296
+ 6. **`pixelFormat: "yuv420p"`** is required for most players to handle h264/h265 correctly.
297
+
298
+
299
+ ---
300
+
301
+ ## LLM & Agent Integration
302
+
303
+ Vibeo's CLI is built with [incur](https://github.com/wevm/incur), making it natively discoverable by AI agents and LLMs.
304
+
305
+ ### Discovering the API
306
+
307
+ ```bash
308
+ # Get a compact summary of all CLI commands (ideal for LLM system prompts)
309
+ bunx @vibeo/cli --llms
310
+
311
+ # Get the full manifest with schemas, examples, and argument details
312
+ bunx @vibeo/cli --llms-full
313
+
314
+ # Get JSON Schema for a specific command (useful for structured tool calls)
315
+ bunx @vibeo/cli render --schema
316
+ bunx @vibeo/cli create --schema
317
+ ```
318
+
319
+ ### Using as an MCP Server
320
+
321
+ ```bash
322
+ # Start Vibeo as an MCP (Model Context Protocol) server
323
+ bunx @vibeo/cli --mcp
324
+
325
+ # Register as a persistent MCP server for your agent
326
+ bunx @vibeo/cli mcp add
327
+ ```
328
+
329
+ This lets LLMs call `create`, `render`, `preview`, and `list` as structured tool calls through the MCP protocol.
330
+
331
+ ### Generating Skill Files
332
+
333
+ ```bash
334
+ # Sync skill files to your agent's skill directory
335
+ bunx @vibeo/cli skills add
336
+ ```
337
+
338
+ This generates markdown skill files that agents like Claude Code can discover and use to write Vibeo code without reading source.
339
+
340
+ ### Agent-Friendly Output
341
+
342
+ ```bash
343
+ # Output as JSON for programmatic consumption
344
+ bunx @vibeo/cli list --entry src/index.tsx --format json
345
+
346
+ # Output as YAML
347
+ bunx @vibeo/cli list --entry src/index.tsx --format yaml
348
+
349
+ # Filter output to specific keys
350
+ bunx @vibeo/cli list --entry src/index.tsx --filter-output compositions[0].id
351
+
352
+ # Count tokens in output (useful for context window planning)
353
+ bunx @vibeo/cli render --schema --token-count
354
+ ```
355
+
356
+ ### How LLMs Should Use Vibeo
357
+
358
+ 1. **Discover commands**: Run `bunx @vibeo/cli --llms` to get the command manifest
359
+ 2. **Create a project**: `bunx @vibeo/cli create my-video --template basic`
360
+ 3. **Edit `src/index.tsx`**: Write React components using `@vibeo/core` hooks and components
361
+ 4. **Preview**: `bunx @vibeo/cli preview --entry src/index.tsx`
362
+ 5. **Render**: `bunx @vibeo/cli render --entry src/index.tsx --composition MyComp`
363
+
364
+ All commands accept `--format json` for structured output that LLMs can parse reliably.
@@ -1,25 +1,33 @@
1
1
  import { resolve, join, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { mkdir, writeFile, readFile } from "node:fs/promises";
3
4
  import { existsSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
- // Embedded skill content (so it works from npm without the skills/ dir)
8
+ // Skill loader resolves from the CLI package's own skills/ directory
8
9
  // ---------------------------------------------------------------------------
9
10
 
11
+ const SKILL_NAMES = ["vibeo-core", "vibeo-audio", "vibeo-effects", "vibeo-extras", "vibeo-rendering"];
12
+
10
13
  async function loadSkills(): Promise<Record<string, string>> {
11
- // Try to load from the repo's skills/ directory first
14
+ // Resolve from this file's location — skills/ is shipped alongside dist/ in the npm package
15
+ const thisFile = fileURLToPath(import.meta.url);
16
+ const thisDir = dirname(thisFile);
17
+
18
+ // From dist/commands/install-skills.js → ../../skills
19
+ // From src/commands/install-skills.ts → ../../skills
12
20
  const candidates = [
13
- join(dirname(import.meta.url.replace("file://", "")), "../../../../skills"),
14
- join(process.cwd(), "skills"),
21
+ join(thisDir, "..", "..", "skills"), // from dist/commands/ or src/commands/
22
+ join(thisDir, "..", "..", "..", "skills"), // from deeper nesting
23
+ join(process.cwd(), "skills"), // from project root
15
24
  ];
16
25
 
17
26
  for (const dir of candidates) {
18
- const names = ["vibeo-core", "vibeo-audio", "vibeo-effects", "vibeo-extras", "vibeo-rendering"];
19
27
  const skills: Record<string, string> = {};
20
28
  let found = true;
21
29
 
22
- for (const name of names) {
30
+ for (const name of SKILL_NAMES) {
23
31
  const path = join(dir, name, "SKILL.md");
24
32
  if (existsSync(path)) {
25
33
  skills[name] = await readFile(path, "utf-8");
@@ -32,78 +40,11 @@ async function loadSkills(): Promise<Record<string, string>> {
32
40
  if (found) return skills;
33
41
  }
34
42
 
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>
43
+ throw new Error(
44
+ "Could not find skill files. Make sure the @vibeo/cli package is installed correctly.",
92
45
  );
93
46
  }
94
- \`\`\`
95
-
96
- ## Key Math
97
47
 
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
48
 
108
49
  // ---------------------------------------------------------------------------
109
50
  // LLM tool targets
@@ -120,24 +61,26 @@ const home = homedir();
120
61
  const TARGETS: Target[] = [
121
62
  {
122
63
  name: "claude",
123
- description: "Claude Code (~/.claude/skills/ + project CLAUDE.md)",
64
+ description: "Claude Code (~/.claude/skills/<name>/SKILL.md + project .claude/skills/<name>/SKILL.md)",
124
65
  async install(skills, cwd) {
125
66
  const files: string[] = [];
126
67
 
127
- // Global skills directory
68
+ // Global skills directory — each skill in its own dir with SKILL.md
128
69
  const globalDir = join(home, ".claude", "skills");
129
- await mkdir(globalDir, { recursive: true });
130
70
  for (const [name, content] of Object.entries(skills)) {
131
- const path = join(globalDir, `${name}.md`);
71
+ const skillDir = join(globalDir, name);
72
+ await mkdir(skillDir, { recursive: true });
73
+ const path = join(skillDir, "SKILL.md");
132
74
  await writeFile(path, content);
133
75
  files.push(path);
134
76
  }
135
77
 
136
- // Project-level .claude/skills/
78
+ // Project-level .claude/skills/<name>/SKILL.md
137
79
  const projectDir = join(cwd, ".claude", "skills");
138
- await mkdir(projectDir, { recursive: true });
139
80
  for (const [name, content] of Object.entries(skills)) {
140
- const path = join(projectDir, `${name}.md`);
81
+ const skillDir = join(projectDir, name);
82
+ await mkdir(skillDir, { recursive: true });
83
+ const path = join(skillDir, "SKILL.md");
141
84
  await writeFile(path, content);
142
85
  files.push(path);
143
86
  }