@vibeo/cli 0.1.1 → 0.2.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,145 +1,261 @@
1
- import { resolve, join, basename } from "node:path";
2
- import { cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { resolve, join } from "node:path";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
 
5
- const TEMPLATES: Record<string, { description: string; example: string }> = {
6
- basic: {
7
- description: "Minimal composition with text animation and two scenes",
8
- example: "basic-composition.tsx",
9
- },
10
- "audio-reactive": {
11
- description: "Audio visualization with frequency bars and amplitude-driven effects",
12
- example: "audio-reactive-viz.tsx",
13
- },
14
- transitions: {
15
- description: "Scene transitions (fade, slide) between multiple scenes",
16
- example: "transition-demo.tsx",
17
- },
18
- subtitles: {
19
- description: "Video with SRT subtitle overlay",
20
- example: "subtitle-overlay.tsx",
21
- },
22
- };
5
+ // ---------------------------------------------------------------------------
6
+ // Embedded templates
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const TEMPLATE_BASIC = `import React from "react";
10
+ import {
11
+ Composition, Sequence, VibeoRoot,
12
+ useCurrentFrame, useVideoConfig, interpolate, easeInOut,
13
+ } from "@vibeo/core";
14
+
15
+ function TitleScene() {
16
+ const frame = useCurrentFrame();
17
+ const { width, height } = useVideoConfig();
18
+ const opacity = interpolate(frame, [0, 30], [0, 1], { extrapolateRight: "clamp" });
19
+ const y = interpolate(frame, [0, 30], [40, 0], { easing: easeInOut, extrapolateRight: "clamp" });
20
+
21
+ return (
22
+ <div style={{ width, height, display: "flex", justifyContent: "center", alignItems: "center", background: "linear-gradient(135deg, #0f0c29, #302b63, #24243e)" }}>
23
+ <h1 style={{ color: "white", fontSize: 72, fontFamily: "sans-serif", opacity, transform: \`translateY(\${y}px)\` }}>Hello, Vibeo!</h1>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ function ContentScene() {
29
+ const frame = useCurrentFrame();
30
+ const { width, height, fps } = useVideoConfig();
31
+ const seconds = (frame / fps).toFixed(1);
32
+ const scale = interpolate(frame, [0, 20], [0.8, 1], { easing: easeInOut, extrapolateRight: "clamp" });
33
+
34
+ return (
35
+ <div style={{ width, height, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", background: "#24243e" }}>
36
+ <div style={{ transform: \`scale(\${scale})\`, color: "white", fontSize: 48, fontFamily: "sans-serif", textAlign: "center" }}>
37
+ <p>Scene 2</p>
38
+ <p style={{ fontSize: 32, opacity: 0.7 }}>{seconds}s elapsed</p>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ function MyVideo() {
45
+ return (
46
+ <>
47
+ <Sequence from={0} durationInFrames={75} name="Title"><TitleScene /></Sequence>
48
+ <Sequence from={75} durationInFrames={75} name="Content"><ContentScene /></Sequence>
49
+ </>
50
+ );
51
+ }
52
+
53
+ export function Root() {
54
+ return (
55
+ <VibeoRoot>
56
+ <Composition id="BasicComposition" component={MyVideo} width={1920} height={1080} fps={30} durationInFrames={150} />
57
+ </VibeoRoot>
58
+ );
59
+ }
60
+ `;
61
+
62
+ const TEMPLATE_AUDIO_REACTIVE = `import React from "react";
63
+ import { Composition, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate } from "@vibeo/core";
64
+ import { Audio } from "@vibeo/audio";
65
+ import { useAudioData } from "@vibeo/effects";
66
+
67
+ const AUDIO_SRC = "/music.mp3";
68
+
69
+ function FrequencyBars() {
70
+ const { width, height } = useVideoConfig();
71
+ const audio = useAudioData(AUDIO_SRC, { fftSize: 1024 });
72
+ if (!audio) return <div style={{ width, height }} />;
73
+
74
+ const barCount = 48;
75
+ const step = Math.floor(audio.frequencies.length / barCount);
76
+ const barWidth = (width * 0.8) / barCount;
77
+ const maxBarHeight = height * 0.6;
78
+
79
+ return (
80
+ <div style={{ position: "absolute", bottom: 60, left: width * 0.1, display: "flex", alignItems: "flex-end", gap: 2 }}>
81
+ {Array.from({ length: barCount }, (_, i) => {
82
+ const db = audio.frequencies[i * step];
83
+ const normalized = Math.max(0, (db + 100) / 100);
84
+ const hue = interpolate(i, [0, barCount - 1], [220, 340]);
85
+ return (
86
+ <div key={i} style={{ width: barWidth - 2, height: Math.max(2, normalized * maxBarHeight), background: \`hsl(\${hue}, 80%, 60%)\`, borderRadius: 2 }} />
87
+ );
88
+ })}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function AudioViz() {
94
+ const frame = useCurrentFrame();
95
+ const { width, height, fps } = useVideoConfig();
96
+ const audio = useAudioData(AUDIO_SRC);
97
+ const hue = 240 + (audio ? audio.amplitude * 60 : 0);
98
+ const lightness = audio ? 8 + audio.amplitude * 12 : 8;
99
+
100
+ return (
101
+ <div style={{ width, height, background: \`radial-gradient(ellipse at center, hsl(\${hue}, 40%, \${lightness + 5}%), hsl(\${hue}, 30%, \${lightness}%))\`, position: "relative", overflow: "hidden" }}>
102
+ <div style={{ position: "absolute", top: 40, left: 40, color: "white", fontFamily: "sans-serif" }}>
103
+ <h1 style={{ fontSize: 36, margin: 0, opacity: 0.9 }}>Audio Visualizer</h1>
104
+ <p style={{ fontSize: 18, margin: "8px 0 0", opacity: 0.5 }}>{(frame / fps).toFixed(1)}s</p>
105
+ </div>
106
+ <FrequencyBars />
107
+ <Audio src={AUDIO_SRC} volume={0.8} />
108
+ </div>
109
+ );
110
+ }
111
+
112
+ export function Root() {
113
+ return (
114
+ <VibeoRoot>
115
+ <Composition id="AudioReactiveViz" component={AudioViz} width={1920} height={1080} fps={30} durationInFrames={900} />
116
+ </VibeoRoot>
117
+ );
118
+ }
119
+ `;
120
+
121
+ const TEMPLATE_TRANSITIONS = `import React from "react";
122
+ import { Composition, Sequence, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate, easeOut } from "@vibeo/core";
123
+ import { Transition } from "@vibeo/effects";
124
+
125
+ function ColorScene({ title, color }: { title: string; color: string }) {
126
+ const frame = useCurrentFrame();
127
+ const { width, height } = useVideoConfig();
128
+ const opacity = interpolate(frame, [0, 20], [0, 1], { easing: easeOut, extrapolateRight: "clamp" });
129
+
130
+ return (
131
+ <div style={{ width, height, background: color, display: "flex", justifyContent: "center", alignItems: "center" }}>
132
+ <h1 style={{ color: "white", fontSize: 80, fontFamily: "sans-serif", opacity }}>{title}</h1>
133
+ </div>
134
+ );
135
+ }
136
+
137
+ function SceneA() { return <ColorScene title="Scene One" color="linear-gradient(135deg, #667eea, #764ba2)" />; }
138
+ function SceneB() { return <ColorScene title="Scene Two" color="linear-gradient(135deg, #f093fb, #f5576c)" />; }
139
+ function SceneC() { return <ColorScene title="Scene Three" color="linear-gradient(135deg, #4facfe, #00f2fe)" />; }
23
140
 
24
- interface CreateArgs {
25
- name: string;
26
- template: string;
141
+ function TransitionDemo() {
142
+ return (
143
+ <>
144
+ <Sequence from={0} durationInFrames={85}><SceneA /></Sequence>
145
+ <Sequence from={65} durationInFrames={20}>
146
+ <Transition type="fade" durationInFrames={20}><SceneA /><SceneB /></Transition>
147
+ </Sequence>
148
+ <Sequence from={85} durationInFrames={85}><SceneB /></Sequence>
149
+ <Sequence from={150} durationInFrames={20}>
150
+ <Transition type="slide" durationInFrames={20} direction="left"><SceneB /><SceneC /></Transition>
151
+ </Sequence>
152
+ <Sequence from={170} durationInFrames={70}><SceneC /></Sequence>
153
+ </>
154
+ );
27
155
  }
28
156
 
29
- function parseArgs(args: string[]): CreateArgs {
30
- const result: CreateArgs = { name: "", template: "basic" };
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 === "--template" && next) {
37
- result.template = next;
38
- i++;
39
- } else if (arg.startsWith("--template=")) {
40
- result.template = arg.slice("--template=".length);
41
- } else if (arg === "--help" || arg === "-h") {
42
- printHelp();
43
- process.exit(0);
44
- } else if (!arg.startsWith("-") && !result.name) {
45
- result.name = arg;
46
- }
47
- }
48
-
49
- return result;
157
+ export function Root() {
158
+ return (
159
+ <VibeoRoot>
160
+ <Composition id="TransitionDemo" component={TransitionDemo} width={1920} height={1080} fps={30} durationInFrames={240} />
161
+ </VibeoRoot>
162
+ );
50
163
  }
164
+ `;
165
+
166
+ const TEMPLATE_SUBTITLES = `import React from "react";
167
+ import { Composition, Sequence, VibeoRoot, useCurrentFrame, useVideoConfig, interpolate } from "@vibeo/core";
168
+ import { Subtitle } from "@vibeo/extras";
51
169
 
52
- function printHelp(): void {
53
- console.log(`
54
- vibeo create - Create a new Vibeo project
170
+ const SUBTITLES_SRT = \`1
171
+ 00:00:00,500 --> 00:00:03,000
172
+ Welcome to the Vibeo demo.
55
173
 
56
- Usage:
57
- vibeo create <project-name> [options]
174
+ 2
175
+ 00:00:03,500 --> 00:00:06,000
176
+ This shows subtitle overlays.
58
177
 
59
- Options:
60
- --template <name> Template to use (default: basic)
61
- --help Show this help
178
+ 3
179
+ 00:00:06,500 --> 00:00:09,000
180
+ Subtitles are synced to the frame timeline.
62
181
 
63
- Templates:`);
182
+ 4
183
+ 00:00:09,500 --> 00:00:12,000
184
+ You can use <b>bold</b> and <i>italic</i> text.
64
185
 
65
- for (const [name, { description }] of Object.entries(TEMPLATES)) {
66
- console.log(` ${name.padEnd(18)} ${description}`);
67
- }
186
+ 5
187
+ 00:00:13,000 --> 00:00:16,000
188
+ The end. Thanks for watching!\`;
68
189
 
69
- console.log(`
70
- Examples:
71
- vibeo create my-video
72
- vibeo create music-viz --template audio-reactive
73
- vibeo create intro --template transitions
74
- `);
190
+ function SubtitleVideo() {
191
+ const frame = useCurrentFrame();
192
+ const { width, height } = useVideoConfig();
193
+ const hue = interpolate(frame, [0, 480], [200, 280]);
194
+
195
+ return (
196
+ <div style={{ width, height, position: "relative" }}>
197
+ <div style={{ width, height, background: \`linear-gradient(135deg, hsl(\${hue}, 50%, 15%), hsl(\${hue + 40}, 40%, 10%))\` }} />
198
+ <div style={{ position: "absolute", top: 0, left: 0, width, height }}>
199
+ <Subtitle src={SUBTITLES_SRT} format="srt" position="bottom" fontSize={36} color="white" outlineColor="black" outlineWidth={2} style={{ padding: "0 80px 60px" }} />
200
+ </div>
201
+ </div>
202
+ );
75
203
  }
76
204
 
77
- // Find the examples directory relative to the CLI package
78
- function findExamplesDir(): string {
79
- // Walk up from this file to find the repo root with examples/
80
- let dir = import.meta.dir;
81
- for (let i = 0; i < 6; i++) {
82
- const candidate = join(dir, "examples");
83
- if (existsSync(candidate)) return candidate;
84
- dir = resolve(dir, "..");
85
- }
86
- throw new Error("Could not find examples directory");
205
+ export function Root() {
206
+ return (
207
+ <VibeoRoot>
208
+ <Composition id="SubtitleOverlay" component={SubtitleVideo} width={1920} height={1080} fps={30} durationInFrames={480} />
209
+ </VibeoRoot>
210
+ );
87
211
  }
212
+ `;
88
213
 
89
- export async function createCommand(args: string[]): Promise<void> {
90
- const parsed = parseArgs(args);
214
+ // ---------------------------------------------------------------------------
91
215
 
92
- if (!parsed.name) {
93
- console.error("Error: project name is required\n");
94
- printHelp();
95
- process.exit(1);
96
- }
216
+ const TEMPLATES: Record<string, { description: string; source: string }> = {
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 },
221
+ };
97
222
 
98
- const template = TEMPLATES[parsed.template];
99
- if (!template) {
100
- console.error(`Error: unknown template "${parsed.template}"`);
101
- console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
102
- process.exit(1);
103
- }
223
+ export const TEMPLATE_NAMES = Object.keys(TEMPLATES);
104
224
 
105
- const projectDir = resolve(parsed.name);
106
- if (existsSync(projectDir)) {
107
- console.error(`Error: directory "${parsed.name}" already exists`);
108
- process.exit(1);
109
- }
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}`);
110
231
 
111
- console.log(`\nCreating Vibeo project: ${parsed.name}`);
112
- console.log(`Template: ${parsed.template}\n`);
232
+ const projectDir = resolve(name);
233
+ if (existsSync(projectDir)) throw new Error(`Directory "${name}" already exists`);
113
234
 
114
- // Create project structure
115
235
  await mkdir(join(projectDir, "src"), { recursive: true });
116
236
  await mkdir(join(projectDir, "public"), { recursive: true });
117
237
 
118
- // Copy example as src/index.tsx
119
- const examplesDir = findExamplesDir();
120
- const exampleSrc = await readFile(join(examplesDir, template.example), "utf-8");
121
- await writeFile(join(projectDir, "src", "index.tsx"), exampleSrc);
238
+ await writeFile(join(projectDir, "src", "index.tsx"), tmpl.source);
122
239
 
123
- // Write package.json
124
240
  const pkg = {
125
- name: parsed.name,
241
+ name,
126
242
  version: "0.0.1",
127
243
  private: true,
128
244
  type: "module",
129
245
  scripts: {
130
- dev: "vibeo preview --entry src/index.tsx",
131
- build: "vibeo render --entry src/index.tsx",
132
- list: "vibeo list --entry src/index.tsx",
246
+ dev: "bunx @vibeo/cli preview --entry src/index.tsx",
247
+ build: "bunx @vibeo/cli render --entry src/index.tsx",
248
+ list: "bunx @vibeo/cli list --entry src/index.tsx",
133
249
  typecheck: "bunx tsc --noEmit",
134
250
  },
135
251
  dependencies: {
136
- "@vibeo/core": "workspace:*",
137
- "@vibeo/audio": "workspace:*",
138
- "@vibeo/effects": "workspace:*",
139
- "@vibeo/extras": "workspace:*",
140
- "@vibeo/player": "workspace:*",
141
- "@vibeo/renderer": "workspace:*",
142
- "@vibeo/cli": "workspace:*",
252
+ "@vibeo/core": "^0.1.0",
253
+ "@vibeo/audio": "^0.1.0",
254
+ "@vibeo/effects": "^0.1.0",
255
+ "@vibeo/extras": "^0.1.0",
256
+ "@vibeo/player": "^0.1.0",
257
+ "@vibeo/renderer": "^0.1.0",
258
+ "@vibeo/cli": "^0.1.0",
143
259
  react: "^19.0.0",
144
260
  "react-dom": "^19.0.0",
145
261
  },
@@ -150,7 +266,6 @@ export async function createCommand(args: string[]): Promise<void> {
150
266
  };
151
267
  await writeFile(join(projectDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
152
268
 
153
- // Write tsconfig.json
154
269
  const tsconfig = {
155
270
  compilerOptions: {
156
271
  target: "ES2022",
@@ -169,29 +284,8 @@ export async function createCommand(args: string[]): Promise<void> {
169
284
  };
170
285
  await writeFile(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
171
286
 
172
- // Write .gitignore
173
- await writeFile(
174
- join(projectDir, ".gitignore"),
175
- `node_modules/
176
- dist/
177
- out/
178
- *.tmp
179
- .DS_Store
180
- `,
181
- );
287
+ await writeFile(join(projectDir, ".gitignore"), "node_modules/\ndist/\nout/\n*.tmp\n.DS_Store\n");
182
288
 
183
- console.log(` Created ${parsed.name}/`);
184
- console.log(` ├── src/index.tsx`);
185
- console.log(` ├── public/`);
186
- console.log(` ├── package.json`);
187
- console.log(` ├── tsconfig.json`);
188
- console.log(` └── .gitignore`);
189
-
190
- console.log(`
191
- Next steps:
192
- cd ${parsed.name}
193
- bun install
194
- bun run dev # preview in browser
195
- bun run build # render to video
196
- `);
289
+ const files = ["src/index.tsx", "package.json", "tsconfig.json", ".gitignore", "public/"];
290
+ return { project: name, template, files };
197
291
  }
@@ -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();