@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.
@@ -1,13 +1,11 @@
1
1
  import { resolve } from "node:path";
2
- import { availableParallelism } from "node:os";
3
- import { parseFrameRange } from "@vibeo/renderer";
4
- import { renderComposition } from "@vibeo/renderer";
2
+ import { parseFrameRange, renderComposition } from "@vibeo/renderer";
5
3
  import type { Codec, ImageFormat, RenderProgress } from "@vibeo/renderer";
6
4
 
7
- interface RenderArgs {
5
+ interface RenderOptions {
8
6
  entry: string;
9
7
  composition: string;
10
- output: string | null;
8
+ output: string | undefined;
11
9
  fps: number | null;
12
10
  frames: string | null;
13
11
  codec: Codec;
@@ -16,100 +14,6 @@ interface RenderArgs {
16
14
  quality: number;
17
15
  }
18
16
 
19
- function parseArgs(args: string[]): RenderArgs {
20
- const result: RenderArgs = {
21
- entry: "",
22
- composition: "",
23
- output: null,
24
- fps: null,
25
- frames: null,
26
- codec: "h264",
27
- concurrency: Math.max(1, Math.floor(availableParallelism() / 2)),
28
- imageFormat: "png",
29
- quality: 80,
30
- };
31
-
32
- for (let i = 0; i < args.length; i++) {
33
- const arg = args[i]!;
34
- const next = args[i + 1];
35
-
36
- if (arg === "--entry" && next) {
37
- result.entry = next;
38
- i++;
39
- } else if (arg.startsWith("--entry=")) {
40
- result.entry = arg.slice("--entry=".length);
41
- } else if (arg === "--composition" && next) {
42
- result.composition = next;
43
- i++;
44
- } else if (arg.startsWith("--composition=")) {
45
- result.composition = arg.slice("--composition=".length);
46
- } else if (arg === "--output" && next) {
47
- result.output = next;
48
- i++;
49
- } else if (arg.startsWith("--output=")) {
50
- result.output = arg.slice("--output=".length);
51
- } else if (arg === "--fps" && next) {
52
- result.fps = parseInt(next, 10);
53
- i++;
54
- } else if (arg.startsWith("--fps=")) {
55
- result.fps = parseInt(arg.slice("--fps=".length), 10);
56
- } else if (arg === "--frames" && next) {
57
- result.frames = next;
58
- i++;
59
- } else if (arg.startsWith("--frames=")) {
60
- result.frames = arg.slice("--frames=".length);
61
- } else if (arg === "--codec" && next) {
62
- result.codec = next as Codec;
63
- i++;
64
- } else if (arg.startsWith("--codec=")) {
65
- result.codec = arg.slice("--codec=".length) as Codec;
66
- } else if (arg === "--concurrency" && next) {
67
- result.concurrency = parseInt(next, 10);
68
- i++;
69
- } else if (arg.startsWith("--concurrency=")) {
70
- result.concurrency = parseInt(arg.slice("--concurrency=".length), 10);
71
- } else if (arg === "--image-format" && next) {
72
- result.imageFormat = next as ImageFormat;
73
- i++;
74
- } else if (arg.startsWith("--image-format=")) {
75
- result.imageFormat = arg.slice("--image-format=".length) as ImageFormat;
76
- } else if (arg === "--quality" && next) {
77
- result.quality = parseInt(next, 10);
78
- i++;
79
- } else if (arg.startsWith("--quality=")) {
80
- result.quality = parseInt(arg.slice("--quality=".length), 10);
81
- } else if (arg === "--help" || arg === "-h") {
82
- printHelp();
83
- process.exit(0);
84
- }
85
- }
86
-
87
- return result;
88
- }
89
-
90
- function printHelp(): void {
91
- console.log(`
92
- vibeo render - Render a composition to video
93
-
94
- Usage:
95
- vibeo render --entry <path> --composition <id> [options]
96
-
97
- Required:
98
- --entry <path> Path to the root file with compositions
99
- --composition <id> Composition ID to render
100
-
101
- Options:
102
- --output <path> Output file path (default: out/<compositionId>.mp4)
103
- --fps <number> Override fps
104
- --frames <range> Frame range "start-end" (e.g., "0-100")
105
- --codec <codec> h264 | h265 | vp9 | prores (default: h264)
106
- --concurrency <number> Parallel browser tabs (default: cpu count / 2)
107
- --image-format <format> png | jpeg (default: png)
108
- --quality <number> 0-100 for jpeg quality / crf (default: 80)
109
- --help Show this help
110
- `);
111
- }
112
-
113
17
  function formatTime(ms: number): string {
114
18
  const seconds = Math.floor(ms / 1000);
115
19
  const minutes = Math.floor(seconds / 60);
@@ -131,76 +35,49 @@ function renderProgressBar(progress: RenderProgress): void {
131
35
  );
132
36
  }
133
37
 
134
- /**
135
- * Execute the render command.
136
- */
137
- export async function renderCommand(args: string[]): Promise<void> {
138
- const parsed = parseArgs(args);
139
-
140
- if (!parsed.entry) {
141
- console.error("Error: --entry is required");
142
- printHelp();
143
- process.exit(1);
144
- }
145
-
146
- if (!parsed.composition) {
147
- console.error("Error: --composition is required");
148
- printHelp();
149
- process.exit(1);
150
- }
38
+ export async function renderVideo(
39
+ opts: RenderOptions,
40
+ ): Promise<{ output: string; elapsed: string }> {
41
+ const ext = opts.codec === "vp9" ? "webm" : opts.codec === "prores" ? "mov" : "mp4";
42
+ const output = opts.output
43
+ ? resolve(opts.output)
44
+ : resolve(`out/${opts.composition}.${ext}`);
151
45
 
152
- const entry = resolve(parsed.entry);
153
- const compositionId = parsed.composition;
154
-
155
- // TODO: In a full implementation, we would bundle the entry, extract
156
- // composition metadata, and use it here. For now we require the user
157
- // to have the composition info available.
158
- // This is a simplified flow that demonstrates the pipeline.
159
-
160
- const ext = parsed.codec === "vp9" ? "webm" : parsed.codec === "prores" ? "mov" : "mp4";
161
- const output = parsed.output
162
- ? resolve(parsed.output)
163
- : resolve(`out/${compositionId}.${ext}`);
164
-
165
- console.log(`\nRendering composition "${compositionId}"`);
166
- console.log(` Entry: ${entry}`);
46
+ console.log(`\nRendering composition "${opts.composition}"`);
47
+ console.log(` Entry: ${opts.entry}`);
167
48
  console.log(` Output: ${output}`);
168
- console.log(` Codec: ${parsed.codec}`);
169
- console.log(` Image format: ${parsed.imageFormat}`);
170
- console.log(` Concurrency: ${parsed.concurrency}`);
49
+ console.log(` Codec: ${opts.codec}`);
50
+ console.log(` Concurrency: ${opts.concurrency}`);
171
51
  console.log();
172
52
 
173
- // For a full render, we need composition info from the bundle.
174
- // This would normally be extracted by bundling and evaluating the entry.
175
- // Here we set up the render config and delegate to renderComposition.
176
53
  const compositionInfo = {
177
54
  width: 1920,
178
55
  height: 1080,
179
- fps: parsed.fps ?? 30,
56
+ fps: opts.fps ?? 30,
180
57
  durationInFrames: 300,
181
58
  };
182
59
 
183
- const frameRange = parseFrameRange(parsed.frames, compositionInfo.durationInFrames);
184
-
60
+ const frameRange = parseFrameRange(opts.frames, compositionInfo.durationInFrames);
185
61
  const startTime = Date.now();
186
62
 
187
63
  await renderComposition(
188
64
  {
189
- entry,
190
- compositionId,
65
+ entry: opts.entry,
66
+ compositionId: opts.composition,
191
67
  outputPath: output,
192
- codec: parsed.codec,
193
- imageFormat: parsed.imageFormat,
194
- quality: parsed.quality,
195
- fps: parsed.fps,
68
+ codec: opts.codec,
69
+ imageFormat: opts.imageFormat,
70
+ quality: opts.quality,
71
+ fps: opts.fps,
196
72
  frameRange,
197
- concurrency: parsed.concurrency,
73
+ concurrency: opts.concurrency,
198
74
  pixelFormat: "yuv420p",
199
75
  onProgress: renderProgressBar,
200
76
  },
201
77
  compositionInfo,
202
78
  );
203
79
 
204
- const elapsed = Date.now() - startTime;
205
- console.log(`\n\nDone in ${formatTime(elapsed)}. Output: ${output}`);
80
+ const elapsed = formatTime(Date.now() - startTime);
81
+ console.log(`\n\nDone in ${elapsed}. Output: ${output}`);
82
+ return { output, elapsed };
206
83
  }
package/src/index.ts CHANGED
@@ -1,63 +1,134 @@
1
- import { renderCommand } from "./commands/render.js";
2
- import { previewCommand } from "./commands/preview.js";
3
- import { listCommand } from "./commands/list.js";
4
- import { createCommand } from "./commands/create.js";
1
+ import { Cli, z } from "incur";
2
+ import { resolve } from "node:path";
3
+ import { availableParallelism } from "node:os";
4
+ import { createProject } from "./commands/create.js";
5
+ import { startPreview } from "./commands/preview.js";
6
+ import { listCompositions } from "./commands/list.js";
7
+ import { renderVideo } from "./commands/render.js";
8
+ import { installSkills } from "./commands/install-skills.js";
5
9
 
6
- const args = process.argv.slice(2);
7
- const command = args[0];
8
-
9
- function printUsage(): void {
10
- console.log(`
11
- vibeo - React video framework CLI
12
-
13
- Usage:
14
- vibeo <command> [options]
15
-
16
- Commands:
17
- create Create a new project from a template
18
- render Render a composition to video
19
- preview Start a dev server with live preview
20
- list List registered compositions
10
+ const cli = Cli.create("vibeo", {
11
+ description: "React-based programmatic video framework CLI",
12
+ sync: {
13
+ suggestions: [
14
+ "create a new video project",
15
+ "preview a composition in the browser",
16
+ "render a composition to video",
17
+ "list all registered compositions",
18
+ ],
19
+ },
20
+ });
21
21
 
22
- Options:
23
- --help Show help for a command
22
+ cli.command("create", {
23
+ description: "Create a new Vibeo project from a template",
24
+ args: z.object({
25
+ name: z.string().describe("Project directory name"),
26
+ }),
27
+ options: z.object({
28
+ template: z
29
+ .enum(["basic", "audio-reactive", "transitions", "subtitles"])
30
+ .default("basic")
31
+ .describe("Template to scaffold from"),
32
+ }),
33
+ examples: [
34
+ { args: { name: "my-video" }, description: "Create with basic template" },
35
+ { args: { name: "viz" }, options: { template: "audio-reactive" }, description: "Create with audio-reactive template" },
36
+ ],
37
+ async run(c) {
38
+ return await createProject(c.args.name, c.options.template);
39
+ },
40
+ });
24
41
 
25
- Examples:
26
- vibeo create my-video
27
- vibeo create music-viz --template audio-reactive
28
- vibeo render --entry src/index.tsx --composition MyComp --output out.mp4
29
- vibeo preview --entry src/index.tsx
30
- vibeo list --entry src/index.tsx
31
- `);
32
- }
42
+ cli.command("preview", {
43
+ description: "Start a dev server with live preview in the browser",
44
+ options: z.object({
45
+ entry: z.string().describe("Path to the root file with compositions"),
46
+ port: z.number().default(3000).describe("Port for the dev server"),
47
+ }),
48
+ examples: [
49
+ { options: { entry: "src/index.tsx" }, description: "Preview on default port" },
50
+ ],
51
+ async run(c) {
52
+ await startPreview(resolve(c.options.entry), c.options.port);
53
+ },
54
+ });
33
55
 
34
- async function main(): Promise<void> {
35
- if (!command || command === "--help" || command === "-h") {
36
- printUsage();
37
- process.exit(0);
38
- }
56
+ cli.command("render", {
57
+ description: "Render a composition to a video file",
58
+ options: z.object({
59
+ entry: z.string().describe("Path to the root file with compositions"),
60
+ composition: z.string().describe("Composition ID to render"),
61
+ output: z.string().optional().describe("Output file path (default: out/<id>.mp4)"),
62
+ fps: z.number().optional().describe("Override frames per second"),
63
+ frames: z.string().optional().describe('Frame range, e.g. "0-100" or "50"'),
64
+ codec: z
65
+ .enum(["h264", "h265", "vp9", "prores"])
66
+ .default("h264")
67
+ .describe("Video codec"),
68
+ concurrency: z
69
+ .number()
70
+ .default(Math.max(1, Math.floor(availableParallelism() / 2)))
71
+ .describe("Number of parallel browser tabs"),
72
+ imageFormat: z
73
+ .enum(["png", "jpeg"])
74
+ .default("png")
75
+ .describe("Intermediate frame image format"),
76
+ quality: z.number().default(80).describe("JPEG quality / CRF value (0-100)"),
77
+ }),
78
+ examples: [
79
+ { options: { entry: "src/index.tsx", composition: "MyComp" }, description: "Render with defaults" },
80
+ { options: { entry: "src/index.tsx", composition: "MyComp", codec: "vp9", frames: "0-100" }, description: "Render a frame range as WebM" },
81
+ ],
82
+ async run(c) {
83
+ return await renderVideo({
84
+ entry: resolve(c.options.entry),
85
+ composition: c.options.composition,
86
+ output: c.options.output,
87
+ fps: c.options.fps ?? null,
88
+ frames: c.options.frames ?? null,
89
+ codec: c.options.codec,
90
+ concurrency: c.options.concurrency,
91
+ imageFormat: c.options.imageFormat,
92
+ quality: c.options.quality,
93
+ });
94
+ },
95
+ });
39
96
 
40
- switch (command) {
41
- case "create":
42
- await createCommand(args.slice(1));
43
- break;
44
- case "render":
45
- await renderCommand(args.slice(1));
46
- break;
47
- case "preview":
48
- await previewCommand(args.slice(1));
49
- break;
50
- case "list":
51
- await listCommand(args.slice(1));
52
- break;
53
- default:
54
- console.error(`Unknown command: ${command}`);
55
- printUsage();
56
- process.exit(1);
57
- }
58
- }
97
+ cli.command("list", {
98
+ description: "List registered compositions in an entry file",
99
+ options: z.object({
100
+ entry: z.string().describe("Path to the root file with compositions"),
101
+ }),
102
+ examples: [
103
+ { options: { entry: "src/index.tsx" }, description: "List all compositions" },
104
+ ],
105
+ async run(c) {
106
+ const compositions = await listCompositions(resolve(c.options.entry));
107
+ return { compositions };
108
+ },
109
+ });
59
110
 
60
- main().catch((err) => {
61
- console.error(err);
62
- process.exit(1);
111
+ cli.command("install-skills", {
112
+ description:
113
+ "Install Vibeo skill/rule files for all supported LLM coding tools (Claude, Codex, Cursor, Gemini, OpenCode, Aider)",
114
+ options: z.object({
115
+ targets: z
116
+ .string()
117
+ .optional()
118
+ .describe(
119
+ 'Comma-separated list of targets (default: all). Options: claude, codex, cursor, gemini, opencode, aider',
120
+ ),
121
+ }),
122
+ examples: [
123
+ { description: "Install for all supported tools" },
124
+ { options: { targets: "claude,cursor" }, description: "Install for Claude and Cursor only" },
125
+ ],
126
+ async run(c) {
127
+ const targets = c.options.targets
128
+ ? c.options.targets.split(",").map((t: string) => t.trim())
129
+ : [];
130
+ return await installSkills(targets, process.cwd());
131
+ },
63
132
  });
133
+
134
+ cli.serve();