@svelteframe/cli 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sumeet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,41 @@
1
- # Temporary Holding Version
1
+ # @svelteframe/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Command-line interface for Svelteframe video compositions.
4
+
5
+ ## Prerequisites
6
+
7
+ - **ffmpeg** installed on your system (`brew install ffmpeg` on macOS, or via your Linux package manager)
8
+ - **Chromium** installed for Playwright (`npx playwright install chromium`)
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install -g @svelteframe/cli
14
+ # or install as project dev dependency
15
+ pnpm add -D @svelteframe/cli
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```sh
21
+ # Render a composition to MP4 video
22
+ svelteframe render <composition-id> [output.mp4]
23
+
24
+ # Render a single still frame to image
25
+ svelteframe still <composition-id> [output.png] --frame 30
26
+
27
+ # List available compositions
28
+ svelteframe compositions
29
+ ```
30
+
31
+ ## Options
32
+
33
+ - `--props <json|path>`: Pass props as a JSON string or JSON file path
34
+ - `--port <number>`: Port for the internal dev server
35
+ - `--root <path>`: Root folder of the project containing compositions
36
+ - `--quality <1-100>`: JPEG quality (default: 90)
37
+ - `--format <jpeg|png>`: Image format for intermediate frames
38
+
39
+ ## License
40
+
41
+ MIT
package/dist/args.js ADDED
@@ -0,0 +1,153 @@
1
+ import { parseArgs } from 'node:util';
2
+ /** A mistake in how the command was written; reported with the usage hint, exit code 2. */
3
+ export class UsageError extends Error {
4
+ name = 'UsageError';
5
+ }
6
+ export const HELP = `svelteframe: render videos made with Svelteframe
7
+
8
+ Usage:
9
+ svelteframe render <id> [options] Render a composition to a video
10
+ svelteframe still <id> [options] Capture one frame as an image
11
+ svelteframe compositions [options] List the project's compositions
12
+
13
+ Common options:
14
+ --root <dir> the project (default: the current directory)
15
+ --url <base url> use a server that is already running instead of starting one
16
+ --dev start the dev server instead of building and previewing the project
17
+ --chromium <path> Chromium executable to use
18
+
19
+ render options:
20
+ --out <file> output video (default <id>.mp4)
21
+ --props <file> JSON file with input props
22
+ --concurrency <n> pages rendering at once (default: half the CPU cores)
23
+ --quality <0-100> JPEG quality of captured frames (default 90)
24
+ --png capture PNG frames instead of JPEG
25
+
26
+ still options:
27
+ --frame <n> frame to capture (default 0)
28
+ --out <file> output image, .png or .jpg (default <id>.png)
29
+ --props <file> JSON file with input props
30
+ --quality <0-100> JPEG quality when writing .jpg (default 90)
31
+
32
+ compositions options:
33
+ --json print JSON instead of a table
34
+
35
+ Other:
36
+ -h, --help show this help
37
+ -v, --version show the version`;
38
+ const OPTIONS = {
39
+ root: { type: 'string' },
40
+ url: { type: 'string' },
41
+ dev: { type: 'boolean' },
42
+ chromium: { type: 'string' },
43
+ out: { type: 'string' },
44
+ props: { type: 'string' },
45
+ concurrency: { type: 'string' },
46
+ quality: { type: 'string' },
47
+ png: { type: 'boolean' },
48
+ frame: { type: 'string' },
49
+ json: { type: 'boolean' },
50
+ help: { type: 'boolean', short: 'h' },
51
+ version: { type: 'boolean', short: 'v' }
52
+ };
53
+ const SHARED = ['root', 'url', 'dev', 'chromium'];
54
+ const ALLOWED = {
55
+ render: [...SHARED, 'out', 'props', 'concurrency', 'quality', 'png'],
56
+ still: [...SHARED, 'out', 'props', 'frame', 'quality'],
57
+ compositions: [...SHARED, 'json']
58
+ };
59
+ function whole(value, flag, min) {
60
+ const n = Number(value);
61
+ if (value.trim() === '' || !Number.isInteger(n) || n < min) {
62
+ throw new UsageError(`${flag} must be a whole number of ${min} or more, got "${value}"`);
63
+ }
64
+ return n;
65
+ }
66
+ function quality(value) {
67
+ if (value === undefined)
68
+ return 90;
69
+ const n = Number(value);
70
+ if (value.trim() === '' || !Number.isFinite(n) || n < 0 || n > 100) {
71
+ throw new UsageError(`--quality must be a number from 0 to 100, got "${value}"`);
72
+ }
73
+ return n;
74
+ }
75
+ /** Turn command-line arguments (without `node` and the script) into a command. */
76
+ export function parseCommand(argv) {
77
+ let parsed;
78
+ try {
79
+ parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
80
+ }
81
+ catch (err) {
82
+ const message = err.message;
83
+ // A value that starts with "-" reads as another option.
84
+ const ambiguous = /Option '(--[\w-]+)[^']*' argument is ambiguous/.exec(message);
85
+ if (ambiguous) {
86
+ throw new UsageError(`${ambiguous[1]} needs a value. A value that starts with "-" must be written ${ambiguous[1]}=<value>`);
87
+ }
88
+ throw new UsageError(message.replace(/\. To specify a positional.*$/s, ''));
89
+ }
90
+ const { values, positionals } = parsed;
91
+ if (values.help)
92
+ return { name: 'help' };
93
+ if (values.version)
94
+ return { name: 'version' };
95
+ const [name, ...rest] = positionals;
96
+ if (name === undefined)
97
+ return { name: 'help' };
98
+ if (name !== 'render' && name !== 'still' && name !== 'compositions') {
99
+ throw new UsageError(`Unknown command "${name}". Commands: render, still, compositions`);
100
+ }
101
+ for (const option of Object.keys(values)) {
102
+ if (!ALLOWED[name].includes(option)) {
103
+ throw new UsageError(`--${option} does not apply to "${name}"`);
104
+ }
105
+ }
106
+ const shared = { root: values.root, url: values.url, dev: values.dev ?? false, chromium: values.chromium };
107
+ if (shared.url !== undefined) {
108
+ let protocol;
109
+ try {
110
+ protocol = new URL(shared.url).protocol;
111
+ }
112
+ catch {
113
+ // Reported below.
114
+ }
115
+ if (protocol !== 'http:' && protocol !== 'https:') {
116
+ throw new UsageError(`--url must be a full URL such as http://localhost:5173, got "${shared.url}"`);
117
+ }
118
+ if (shared.dev)
119
+ throw new UsageError('--dev has no effect with --url: no server is started');
120
+ }
121
+ if (name === 'compositions') {
122
+ if (rest.length > 0)
123
+ throw new UsageError(`"compositions" takes no arguments, got "${rest[0]}"`);
124
+ return { name, ...shared, json: values.json ?? false };
125
+ }
126
+ const [id, ...extra] = rest;
127
+ if (id === undefined) {
128
+ throw new UsageError(`Missing the composition id: svelteframe ${name} <id>. See "svelteframe compositions".`);
129
+ }
130
+ if (extra.length > 0)
131
+ throw new UsageError(`"${name}" takes one composition id, got extra "${extra[0]}"`);
132
+ if (name === 'render') {
133
+ return {
134
+ name,
135
+ ...shared,
136
+ id,
137
+ out: values.out ?? `${id}.mp4`,
138
+ props: values.props,
139
+ concurrency: values.concurrency === undefined ? undefined : whole(values.concurrency, '--concurrency', 1),
140
+ quality: quality(values.quality),
141
+ png: values.png ?? false
142
+ };
143
+ }
144
+ return {
145
+ name,
146
+ ...shared,
147
+ id,
148
+ frame: values.frame === undefined ? 0 : whole(values.frame, '--frame', 0),
149
+ out: values.out ?? `${id}.png`,
150
+ props: values.props,
151
+ quality: quality(values.quality)
152
+ };
153
+ }
package/dist/bin.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from 'node:fs/promises';
3
+ import { HELP, UsageError, parseCommand } from './args.js';
4
+ import { run } from './run.js';
5
+ async function main() {
6
+ let command;
7
+ try {
8
+ command = parseCommand(process.argv.slice(2));
9
+ }
10
+ catch (err) {
11
+ if (!(err instanceof UsageError))
12
+ throw err;
13
+ process.stderr.write(`${err.message}\nRun "svelteframe --help" for usage.\n`);
14
+ return 2;
15
+ }
16
+ if (command.name === 'help') {
17
+ process.stdout.write(`${HELP}\n`);
18
+ return 0;
19
+ }
20
+ if (command.name === 'version') {
21
+ const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
22
+ process.stdout.write(`${manifest.version}\n`);
23
+ return 0;
24
+ }
25
+ // Ctrl-C stops the render and shuts the server down before leaving; a second one leaves at once.
26
+ const controller = new AbortController();
27
+ process.once('SIGINT', () => {
28
+ process.stderr.write('\nInterrupted, cleaning up...\n');
29
+ controller.abort();
30
+ process.once('SIGINT', () => process.exit(130));
31
+ });
32
+ try {
33
+ await run(command, {
34
+ cwd: process.cwd(),
35
+ signal: controller.signal,
36
+ stdout: process.stdout,
37
+ stderr: process.stderr,
38
+ verbose: process.env.SVELTEFRAME_VERBOSE === '1'
39
+ });
40
+ return 0;
41
+ }
42
+ catch (err) {
43
+ if (process.stdout.isTTY)
44
+ process.stdout.write('\n');
45
+ if (controller.signal.aborted)
46
+ return 130;
47
+ process.stderr.write(`${err instanceof Error ? err.message : err}\n`);
48
+ return 1;
49
+ }
50
+ }
51
+ process.exit(await main());
package/dist/format.js ADDED
@@ -0,0 +1,45 @@
1
+ /** Check what a project served as its composition list. */
2
+ export function parseListing(value) {
3
+ if (!Array.isArray(value))
4
+ throw new Error('the composition list is not an array');
5
+ return value.map((entry, i) => {
6
+ const where = `composition list entry ${i}`;
7
+ if (typeof entry !== 'object' || entry === null)
8
+ throw new Error(`${where} is not an object`);
9
+ const { id, fps, width, height, durationInFrames, defaultProps, calculatesMetadata } = entry;
10
+ if (typeof id !== 'string' || id === '')
11
+ throw new Error(`${where}: id must be a string`);
12
+ for (const [name, v] of Object.entries({ fps, width, height, durationInFrames })) {
13
+ if (typeof v !== 'number' || !Number.isFinite(v))
14
+ throw new Error(`${where} (${id}): ${name} must be a number`);
15
+ }
16
+ return {
17
+ id,
18
+ fps: fps,
19
+ width: width,
20
+ height: height,
21
+ durationInFrames: durationInFrames,
22
+ defaultProps: typeof defaultProps === 'object' && defaultProps !== null ? defaultProps : {},
23
+ calculatesMetadata: calculatesMetadata === true
24
+ };
25
+ });
26
+ }
27
+ /** A table of compositions for the terminal. */
28
+ export function formatListing(list) {
29
+ if (list.length === 0)
30
+ return 'No compositions found.';
31
+ const rows = list.map((c) => [
32
+ c.id,
33
+ `${c.width}×${c.height}`,
34
+ String(c.fps),
35
+ String(c.durationInFrames),
36
+ `${(c.durationInFrames / c.fps).toFixed(1)}s${c.calculatesMetadata ? '*' : ''}`,
37
+ Object.keys(c.defaultProps).join(', ') || '-'
38
+ ]);
39
+ const table = [['ID', 'SIZE', 'FPS', 'FRAMES', 'LENGTH', 'PROPS'], ...rows];
40
+ const widths = table[0].map((_, column) => Math.max(...table.map((row) => row[column].length)));
41
+ const lines = table.map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(' ').trimEnd());
42
+ if (list.some((c) => c.calculatesMetadata))
43
+ lines.push('', '* the length depends on the props');
44
+ return lines.join('\n');
45
+ }
package/dist/run.js ADDED
@@ -0,0 +1,96 @@
1
+ import { resolve } from 'node:path';
2
+ import { buildRenderUrl, readPropsFile, renderStill, renderVideo } from '@svelteframe/renderer';
3
+ import { formatListing, parseListing } from './format.js';
4
+ import { startProjectServer } from './server.js';
5
+ /** Run `work` against the project's server: the one at `--url`, or one started for the occasion. */
6
+ async function withProject(command, io, work) {
7
+ if (command.url !== undefined)
8
+ return work(command.url);
9
+ const server = await startProjectServer({
10
+ root: resolve(io.cwd, command.root ?? '.'),
11
+ dev: command.dev,
12
+ verbose: io.verbose,
13
+ signal: io.signal,
14
+ onStatus: (message) => io.stderr.write(`${message}\n`)
15
+ });
16
+ try {
17
+ return await work(server.url);
18
+ }
19
+ finally {
20
+ await server.close();
21
+ }
22
+ }
23
+ async function readProps(command, io) {
24
+ return command.props === undefined ? undefined : readPropsFile(resolve(io.cwd, command.props));
25
+ }
26
+ const progressLine = (p) => `frame ${p.frame}/${p.total} ${(p.elapsedMs / 1000).toFixed(1)}s ${p.fps.toFixed(1)} fps`;
27
+ async function render(command, io) {
28
+ const output = resolve(io.cwd, command.out);
29
+ const props = await readProps(command, io);
30
+ await withProject(command, io, async (base) => {
31
+ const tty = io.stdout.isTTY;
32
+ const result = await renderVideo({
33
+ url: buildRenderUrl({ url: base, composition: command.id, props }),
34
+ output,
35
+ imageFormat: command.png ? 'png' : 'jpeg',
36
+ quality: command.quality,
37
+ chromiumPath: command.chromium,
38
+ concurrency: command.concurrency,
39
+ signal: io.signal,
40
+ onProgress: (p) => {
41
+ if (tty)
42
+ io.stdout.write(`\r${progressLine(p)}`);
43
+ else if (p.frame % 30 === 0 || p.frame === p.total)
44
+ io.stdout.write(`${progressLine(p)}\n`);
45
+ }
46
+ });
47
+ if (tty)
48
+ io.stdout.write('\n');
49
+ const audio = result.audioSegments > 0 ? ` with ${result.audioSegments} audio segment(s)` : '';
50
+ const workers = result.concurrency > 1 ? ` using ${result.concurrency} workers` : '';
51
+ io.stdout.write(`Rendered ${result.frame} frames${audio}${workers} to ${output}\n`);
52
+ });
53
+ }
54
+ async function still(command, io) {
55
+ const output = resolve(io.cwd, command.out);
56
+ const props = await readProps(command, io);
57
+ await withProject(command, io, async (base) => {
58
+ const result = await renderStill({
59
+ url: buildRenderUrl({ url: base, composition: command.id, props }),
60
+ output,
61
+ frame: command.frame,
62
+ quality: command.quality,
63
+ chromiumPath: command.chromium,
64
+ signal: io.signal
65
+ });
66
+ io.stdout.write(`Saved frame ${result.frame} of ${command.id} (${result.width}×${result.height}) to ${output}\n`);
67
+ });
68
+ }
69
+ async function compositions(command, io) {
70
+ const listing = await withProject(command, io, async (base) => {
71
+ const endpoint = new URL('svelteframe/compositions.json', base.endsWith('/') ? base : `${base}/`);
72
+ const response = await fetch(endpoint, { signal: io.signal });
73
+ if (!response.ok) {
74
+ throw new Error(`${endpoint} answered ${response.status}. The project needs a route at /svelteframe/compositions.json ` +
75
+ 'that returns describeCompositions(compositions) from @svelteframe/core.');
76
+ }
77
+ try {
78
+ return parseListing(await response.json());
79
+ }
80
+ catch (err) {
81
+ throw new Error(`${endpoint} did not return a composition list: ${err.message}`);
82
+ }
83
+ });
84
+ io.stdout.write(command.json ? `${JSON.stringify(listing, null, 2)}\n` : `${formatListing(listing)}\n`);
85
+ }
86
+ /** Run a command that talks to the project. Rejects with the reason when it fails. */
87
+ export async function run(command, io) {
88
+ switch (command.name) {
89
+ case 'render':
90
+ return render(command, io);
91
+ case 'still':
92
+ return still(command, io);
93
+ case 'compositions':
94
+ return compositions(command, io);
95
+ }
96
+ }
package/dist/server.js ADDED
@@ -0,0 +1,173 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { createServer as createNetServer } from 'node:net';
6
+ import { tmpdir } from 'node:os';
7
+ import { dirname, join, resolve } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { format } from 'node:util';
10
+ /**
11
+ * The directory that has `name` in its `node_modules`, searching from `from` upwards. Unlike
12
+ * `require.resolve`, this ignores `NODE_PATH` (which pnpm sets), so it cannot find some other copy.
13
+ */
14
+ function findInstalled(name, from) {
15
+ for (let dir = from;; dir = dirname(dir)) {
16
+ if (existsSync(join(dir, 'node_modules', name, 'package.json')))
17
+ return dir;
18
+ if (dirname(dir) === dir)
19
+ return undefined;
20
+ }
21
+ }
22
+ /** Import the Vite that is installed in the project, whatever version it is. */
23
+ async function loadProjectVite(root) {
24
+ const installedAt = findInstalled('vite', root);
25
+ if (!installedAt) {
26
+ throw new Error(`Could not find "vite" in ${root}. Are the project's dependencies installed?`);
27
+ }
28
+ const entry = createRequire(join(installedAt, 'package.json')).resolve('vite');
29
+ return (await import(pathToFileURL(entry).href));
30
+ }
31
+ /**
32
+ * Run `work` with console output held back. Build tools print progress straight to the console,
33
+ * which would end up in the middle of our own output (`--json` must stay clean). If `work` fails,
34
+ * what it printed is added to the error, as that is usually the explanation.
35
+ */
36
+ async function withQuietConsole(work) {
37
+ const methods = ['log', 'info', 'warn', 'error'];
38
+ const original = Object.fromEntries(methods.map((method) => [method, console[method]]));
39
+ const captured = [];
40
+ for (const method of methods)
41
+ console[method] = (...args) => void captured.push(format(...args));
42
+ try {
43
+ return await work();
44
+ }
45
+ catch (err) {
46
+ const output = captured.join('\n').trim();
47
+ if (err instanceof Error && output)
48
+ err.message += `\n\nOutput of the build:\n${output}`;
49
+ throw err;
50
+ }
51
+ finally {
52
+ Object.assign(console, original);
53
+ }
54
+ }
55
+ /** Where the lock for a project lives: outside the project, and always writable. */
56
+ export function lockPathFor(root) {
57
+ return join(tmpdir(), `svelteframe-lock-${createHash('sha1').update(resolve(root)).digest('hex').slice(0, 12)}`);
58
+ }
59
+ const isAlive = (pid) => {
60
+ try {
61
+ process.kill(pid, 0);
62
+ return true;
63
+ }
64
+ catch (err) {
65
+ // EPERM means it exists but belongs to someone else.
66
+ return err.code === 'EPERM';
67
+ }
68
+ };
69
+ /**
70
+ * Building writes to the project's own build folder, which the preview server then serves from.
71
+ * Two commands doing that at once would break each other, so they take turns: the lock is held
72
+ * from the build until the server is closed. A lock left behind by a process that is gone is ignored.
73
+ * Returns a function that releases it.
74
+ */
75
+ async function acquireBuildLock(root, options) {
76
+ const lock = lockPathFor(root);
77
+ let announced = false;
78
+ for (;;) {
79
+ options.signal?.throwIfAborted();
80
+ try {
81
+ await mkdir(lock);
82
+ await writeFile(join(lock, 'pid'), String(process.pid));
83
+ return () => rm(lock, { recursive: true, force: true });
84
+ }
85
+ catch (err) {
86
+ if (err.code !== 'EEXIST')
87
+ throw err;
88
+ }
89
+ const owner = Number(await readFile(join(lock, 'pid'), 'utf8').catch(() => ''));
90
+ // No pid yet means it was only just taken, unless it has been like that for a while.
91
+ const age = await stat(lock).then((info) => Date.now() - info.mtimeMs, () => 0);
92
+ if ((owner > 0 && !isAlive(owner)) || (!owner && age > 10_000)) {
93
+ await rm(lock, { recursive: true, force: true });
94
+ continue;
95
+ }
96
+ if (!announced) {
97
+ announced = true;
98
+ options.onStatus?.(`Another svelteframe command${owner ? ` (process ${owner})` : ''} is using this project; waiting for it...`);
99
+ }
100
+ await new Promise((done) => setTimeout(done, 250));
101
+ }
102
+ }
103
+ /** A port nothing is listening on right now. */
104
+ export function getFreePort() {
105
+ return new Promise((resolvePort, reject) => {
106
+ const probe = createNetServer();
107
+ probe.once('error', reject);
108
+ probe.listen(0, '127.0.0.1', () => {
109
+ const { port } = probe.address();
110
+ probe.close(() => resolvePort(port));
111
+ });
112
+ });
113
+ }
114
+ /**
115
+ * Serve the project with its own Vite: `vite build` then `vite preview` by default, which is
116
+ * steady and what you ship, or the dev server with `dev`. Returns once it answers.
117
+ *
118
+ * The working directory moves to the project while it runs, as it would if you had run Vite
119
+ * there; server routes that read files relative to it depend on that. `close()` moves it back.
120
+ * Building and previewing take turns between commands (see `acquireBuildLock`).
121
+ */
122
+ export async function startProjectServer(options) {
123
+ const root = resolve(options.root);
124
+ if (!existsSync(join(root, 'package.json'))) {
125
+ throw new Error(`No project at ${root}: there is no package.json. Run this in your project, or pass --root.`);
126
+ }
127
+ const vite = await loadProjectVite(root);
128
+ const port = await getFreePort();
129
+ const logLevel = options.verbose ? 'info' : 'warn';
130
+ const host = '127.0.0.1';
131
+ const releaseLock = options.dev ? async () => { } : await acquireBuildLock(root, options);
132
+ const previousDirectory = process.cwd();
133
+ process.chdir(root);
134
+ try {
135
+ let server;
136
+ if (options.dev) {
137
+ options.onStatus?.('Starting the dev server...');
138
+ const dev = await vite.createServer({ root, logLevel, server: { host, port, strictPort: true } });
139
+ await dev.listen();
140
+ server = dev;
141
+ }
142
+ else {
143
+ options.onStatus?.('Building the project...');
144
+ // `build()` alone builds only the client; an app with server code needs every environment.
145
+ const build = async () => {
146
+ if (vite.createBuilder)
147
+ await (await vite.createBuilder({ root, logLevel })).buildApp();
148
+ else
149
+ await vite.build({ root, logLevel });
150
+ };
151
+ await (options.verbose ? build() : withQuietConsole(build));
152
+ options.onStatus?.('Starting the preview server...');
153
+ server = await vite.preview({ root, logLevel, preview: { host, port, strictPort: true } });
154
+ }
155
+ return {
156
+ url: server.resolvedUrls?.local?.[0] ?? `http://${host}:${port}/`,
157
+ close: async () => {
158
+ try {
159
+ await server.close();
160
+ }
161
+ finally {
162
+ process.chdir(previousDirectory);
163
+ await releaseLock();
164
+ }
165
+ }
166
+ };
167
+ }
168
+ catch (err) {
169
+ process.chdir(previousDirectory);
170
+ await releaseLock();
171
+ throw err;
172
+ }
173
+ }
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@svelteframe/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "Command-line interface for rendering Svelteframe video compositions",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "bin": {
11
+ "svelteframe": "./dist/bin.js"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "@svelteframe/renderer": "^0.0.1"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^26.6.5",
21
+ "playwright": "^1.64.1",
22
+ "tsx": "^4.23.15",
23
+ "typescript": "^6.0.3"
24
+ },
25
+ "keywords": [
26
+ "svelte",
27
+ "svelteframe",
28
+ "video",
29
+ "cli"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json",
33
+ "precheck": "pnpm --filter @svelteframe/renderer build",
34
+ "check": "tsc --noEmit",
35
+ "pretest": "pnpm --filter @svelteframe/renderer build && pnpm run build",
36
+ "test": "tsx --test src/*.test.ts"
37
+ }
6
38
  }