@elah/cli 0.1.0 → 0.1.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/README.md +57 -0
- package/dist/bin.js +62 -1
- package/dist/index.js +52 -0
- package/dist/types/lib/serve.d.ts +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
Headless command-line runtime for the Elah video engine. A thin consumer of
|
|
4
4
|
`@elah/core`'s public APIs — no rendering or timeline logic lives here.
|
|
5
5
|
|
|
6
|
+
[](https://www.npmjs.com/package/@elah/cli)
|
|
7
|
+
[](https://www.npmjs.com/package/@elah/cli)
|
|
8
|
+
[](https://github.com/elahlabs/elah/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @elah/cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or without installing, via `npx @elah/cli <command>`. Requires Node >= 18.17.
|
|
19
|
+
`export`/`build --export`/`serve` additionally require a system Google Chrome
|
|
20
|
+
(or `--browser <path>` / `ELAH_BROWSER`) — see [Serve mode](#serve-mode) and
|
|
21
|
+
[Docker](#docker) for headless environments.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
6
27
|
```
|
|
7
28
|
elah split --project <in.json> --clip <clipId> --at <frame|timecode> [--out <out.json>]
|
|
8
29
|
elah trim --project <in.json> --clip <clipId> [--start <frame|timecode>] [--duration <frames|timecode>] [--out <out.json>]
|
|
@@ -18,6 +39,8 @@ output is identical to Editor output by construction. Exit codes: `0` success,
|
|
|
18
39
|
`1` validation/runtime failure, `2` usage error. Diagnostics go to stderr;
|
|
19
40
|
`split`/`trim` print the resulting project JSON to stdout unless `--out` is given.
|
|
20
41
|
|
|
42
|
+
---
|
|
43
|
+
|
|
21
44
|
## The build spec — the AI-generation contract
|
|
22
45
|
|
|
23
46
|
`elah build` consumes a seconds-based spec, probes each media asset's real
|
|
@@ -68,6 +91,8 @@ Rules:
|
|
|
68
91
|
Then: `elah build --spec spec.json --export final.mp4` (add `--out project.json`
|
|
69
92
|
to keep the editable project; export options like `--height` pass through).
|
|
70
93
|
|
|
94
|
+
---
|
|
95
|
+
|
|
71
96
|
## Library API
|
|
72
97
|
|
|
73
98
|
`@elah/cli` is also importable — no shelling out, no stderr parsing:
|
|
@@ -100,6 +125,8 @@ await session.close()
|
|
|
100
125
|
Errors throw `CliError` (aliased `ElahError`) with a `.message` that is
|
|
101
126
|
already the human-readable, path-addressed text (`clips[2].duration must be …`).
|
|
102
127
|
|
|
128
|
+
---
|
|
129
|
+
|
|
103
130
|
## Serve mode
|
|
104
131
|
|
|
105
132
|
`elah serve` runs a long-lived HTTP render server with a warm browser, so
|
|
@@ -123,12 +150,16 @@ There is no job queue — this is a synchronous, retry-on-503 contract. See
|
|
|
123
150
|
[docs/deploy-render-server.md](../../docs/deploy-render-server.md) for the
|
|
124
151
|
full contract, security notes, and deployment options.
|
|
125
152
|
|
|
153
|
+
---
|
|
154
|
+
|
|
126
155
|
## Docker
|
|
127
156
|
|
|
128
157
|
A Dockerfile that installs branded Chrome + fonts and runs `elah serve` is at
|
|
129
158
|
[`Dockerfile`](./Dockerfile) (build from the repo root — see
|
|
130
159
|
[docs/deploy-render-server.md](../../docs/deploy-render-server.md)).
|
|
131
160
|
|
|
161
|
+
---
|
|
162
|
+
|
|
132
163
|
## Validation tooling
|
|
133
164
|
|
|
134
165
|
- `ELAH_E2E=1 npm test --workspace=packages/cli` additionally runs a browser
|
|
@@ -142,3 +173,29 @@ A Dockerfile that installs branded Chrome + fonts and runs `elah serve` is at
|
|
|
142
173
|
both pipelines are individually deterministic).
|
|
143
174
|
|
|
144
175
|
Full documentation lands with the release phase; see `elah --help`.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Package layers
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
@elah/core — engine, playback, resolver, stores, media, export (framework-agnostic)
|
|
183
|
+
@elah/timeline — React timeline UI components and hooks
|
|
184
|
+
@elah/editor — EditorProvider, Preview, AssetPanel + re-exports everything above
|
|
185
|
+
@elah/cli — headless split/trim/build/export/serve on top of @elah/core (this package)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Use `@elah/cli` for automation, AI-generation pipelines, and server-side rendering.
|
|
189
|
+
Use `@elah/editor` when you need an interactive, in-browser editing UI.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Links
|
|
194
|
+
|
|
195
|
+
- [Website](https://www.elah.dev)
|
|
196
|
+
- [GitHub](https://github.com/elahlabs/elah)
|
|
197
|
+
- [Engine — @elah/core](https://www.npmjs.com/package/@elah/core)
|
|
198
|
+
- [React timeline UI — @elah/timeline](https://www.npmjs.com/package/@elah/timeline)
|
|
199
|
+
- [Full editor SDK — @elah/editor](https://www.npmjs.com/package/@elah/editor)
|
|
200
|
+
- [License](https://github.com/elahlabs/elah/blob/main/LICENSE)
|
|
201
|
+
- [Commercial licensing](mailto:paul@elah.dev)
|
package/dist/bin.js
CHANGED
|
@@ -39879,6 +39879,46 @@ var init_build = __esm({
|
|
|
39879
39879
|
// src/lib/serve.ts
|
|
39880
39880
|
import { createServer as createServer2 } from "node:http";
|
|
39881
39881
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
39882
|
+
function curlExample(origin) {
|
|
39883
|
+
return `curl -X POST ${origin}/render -H 'content-type: application/json' -d '${HELLO_SPEC}' -o hello.mp4`;
|
|
39884
|
+
}
|
|
39885
|
+
function powershellExample(origin) {
|
|
39886
|
+
return `Invoke-RestMethod -Uri "${origin}/render" -Method Post -ContentType "application/json" -Body '${HELLO_SPEC}' -OutFile hello.mp4`;
|
|
39887
|
+
}
|
|
39888
|
+
function welcomePage(origin, browserConnected) {
|
|
39889
|
+
const escapedCurl = curlExample(origin).replace(/</g, "<");
|
|
39890
|
+
const escapedPowershell = powershellExample(origin).replace(/</g, "<");
|
|
39891
|
+
return `<!doctype html>
|
|
39892
|
+
<meta charset="utf-8">
|
|
39893
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
39894
|
+
<title>elah serve</title>
|
|
39895
|
+
<style>
|
|
39896
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
39897
|
+
max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.6;
|
|
39898
|
+
background: #0d1117; color: #e6edf3; }
|
|
39899
|
+
h1 { font-size: 1.3rem; } h1 span { color: #7ee787; }
|
|
39900
|
+
code, pre { background: #161b22; border: 1px solid #30363d; border-radius: 6px; }
|
|
39901
|
+
code { padding: 0.1em 0.4em; }
|
|
39902
|
+
pre { padding: 0.8em 1em; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
|
39903
|
+
table { border-collapse: collapse; } td { padding: 0.15em 1em 0.15em 0; vertical-align: top; }
|
|
39904
|
+
a { color: #58a6ff; }
|
|
39905
|
+
.muted { color: #8b949e; }
|
|
39906
|
+
</style>
|
|
39907
|
+
<h1>elah serve <span>● ${browserConnected ? "browser connected" : "browser disconnected"}</span></h1>
|
|
39908
|
+
<p>Headless render server for the <a href="https://www.elah.dev">elah</a> video engine.
|
|
39909
|
+
POST a build spec, get an MP4 back.</p>
|
|
39910
|
+
<table>
|
|
39911
|
+
<tr><td><code>GET /healthz</code></td><td class="muted">status + browser state</td></tr>
|
|
39912
|
+
<tr><td><code>POST /render</code></td><td class="muted">build spec JSON in, <code>video/mp4</code> out</td></tr>
|
|
39913
|
+
</table>
|
|
39914
|
+
<p>Render your first video (no media files needed):</p>
|
|
39915
|
+
<pre>${escapedCurl}</pre>
|
|
39916
|
+
<p class="muted">On Windows PowerShell, <code>curl</code> is aliased to Invoke-WebRequest and won't accept <code>-H</code>/<code>-d</code> the curl way — use this instead:</p>
|
|
39917
|
+
<pre>${escapedPowershell}</pre>
|
|
39918
|
+
<p class="muted">Spec schema: assets map + clips array — see the
|
|
39919
|
+
<a href="https://github.com/elahlabs/elah/tree/main/packages/cli#readme">@elah/cli README</a>.</p>
|
|
39920
|
+
`;
|
|
39921
|
+
}
|
|
39882
39922
|
function createServeHandler(deps) {
|
|
39883
39923
|
return (req, res) => {
|
|
39884
39924
|
void handle(req, res, deps).catch((err) => {
|
|
@@ -39890,6 +39930,17 @@ function createServeHandler(deps) {
|
|
|
39890
39930
|
}
|
|
39891
39931
|
async function handle(req, res, deps) {
|
|
39892
39932
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
39933
|
+
if (url2.pathname === "/") {
|
|
39934
|
+
if (req.method !== "GET") {
|
|
39935
|
+
writeJson(res, 405, { error: "method not allowed" });
|
|
39936
|
+
return;
|
|
39937
|
+
}
|
|
39938
|
+
const origin = `http://${req.headers.host ?? "localhost"}`;
|
|
39939
|
+
const html = welcomePage(origin, deps.browserConnected());
|
|
39940
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": Buffer.byteLength(html) });
|
|
39941
|
+
res.end(html);
|
|
39942
|
+
return;
|
|
39943
|
+
}
|
|
39893
39944
|
if (url2.pathname === "/healthz") {
|
|
39894
39945
|
if (req.method !== "GET") {
|
|
39895
39946
|
writeJson(res, 405, { error: "method not allowed" });
|
|
@@ -40029,7 +40080,7 @@ async function startServe(options) {
|
|
|
40029
40080
|
})
|
|
40030
40081
|
};
|
|
40031
40082
|
}
|
|
40032
|
-
var Semaphore, DEFAULT_MAX_BODY_BYTES;
|
|
40083
|
+
var Semaphore, DEFAULT_MAX_BODY_BYTES, HELLO_SPEC;
|
|
40033
40084
|
var init_serve = __esm({
|
|
40034
40085
|
"src/lib/serve.ts"() {
|
|
40035
40086
|
"use strict";
|
|
@@ -40053,6 +40104,7 @@ var init_serve = __esm({
|
|
|
40053
40104
|
}
|
|
40054
40105
|
};
|
|
40055
40106
|
DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
40107
|
+
HELLO_SPEC = '{"clips":[{"track":"text","text":"Hello from elah","start":0,"duration":3,"fontSize":96}]}';
|
|
40056
40108
|
}
|
|
40057
40109
|
});
|
|
40058
40110
|
|
|
@@ -40097,6 +40149,15 @@ async function runServe(args) {
|
|
|
40097
40149
|
if (host !== "127.0.0.1") {
|
|
40098
40150
|
stderr("warning: no authentication \u2014 expose only on a trusted network");
|
|
40099
40151
|
}
|
|
40152
|
+
stderr("");
|
|
40153
|
+
const renderOrigin = `http://${host}:${handle2.port}`;
|
|
40154
|
+
stderr("render your first video:");
|
|
40155
|
+
if (process.platform === "win32") {
|
|
40156
|
+
stderr(` ${powershellExample(renderOrigin)}`);
|
|
40157
|
+
stderr(` (or, with curl.exe: ${curlExample(renderOrigin)})`);
|
|
40158
|
+
} else {
|
|
40159
|
+
stderr(` ${curlExample(renderOrigin)}`);
|
|
40160
|
+
}
|
|
40100
40161
|
await new Promise((resolve4) => {
|
|
40101
40162
|
let shuttingDown = false;
|
|
40102
40163
|
const shutdown = () => {
|
package/dist/index.js
CHANGED
|
@@ -39395,6 +39395,47 @@ var Semaphore = class {
|
|
|
39395
39395
|
}
|
|
39396
39396
|
};
|
|
39397
39397
|
var DEFAULT_MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
39398
|
+
var HELLO_SPEC = '{"clips":[{"track":"text","text":"Hello from elah","start":0,"duration":3,"fontSize":96}]}';
|
|
39399
|
+
function curlExample(origin) {
|
|
39400
|
+
return `curl -X POST ${origin}/render -H 'content-type: application/json' -d '${HELLO_SPEC}' -o hello.mp4`;
|
|
39401
|
+
}
|
|
39402
|
+
function powershellExample(origin) {
|
|
39403
|
+
return `Invoke-RestMethod -Uri "${origin}/render" -Method Post -ContentType "application/json" -Body '${HELLO_SPEC}' -OutFile hello.mp4`;
|
|
39404
|
+
}
|
|
39405
|
+
function welcomePage(origin, browserConnected) {
|
|
39406
|
+
const escapedCurl = curlExample(origin).replace(/</g, "<");
|
|
39407
|
+
const escapedPowershell = powershellExample(origin).replace(/</g, "<");
|
|
39408
|
+
return `<!doctype html>
|
|
39409
|
+
<meta charset="utf-8">
|
|
39410
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
39411
|
+
<title>elah serve</title>
|
|
39412
|
+
<style>
|
|
39413
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
39414
|
+
max-width: 720px; margin: 3rem auto; padding: 0 1rem; line-height: 1.6;
|
|
39415
|
+
background: #0d1117; color: #e6edf3; }
|
|
39416
|
+
h1 { font-size: 1.3rem; } h1 span { color: #7ee787; }
|
|
39417
|
+
code, pre { background: #161b22; border: 1px solid #30363d; border-radius: 6px; }
|
|
39418
|
+
code { padding: 0.1em 0.4em; }
|
|
39419
|
+
pre { padding: 0.8em 1em; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
|
39420
|
+
table { border-collapse: collapse; } td { padding: 0.15em 1em 0.15em 0; vertical-align: top; }
|
|
39421
|
+
a { color: #58a6ff; }
|
|
39422
|
+
.muted { color: #8b949e; }
|
|
39423
|
+
</style>
|
|
39424
|
+
<h1>elah serve <span>● ${browserConnected ? "browser connected" : "browser disconnected"}</span></h1>
|
|
39425
|
+
<p>Headless render server for the <a href="https://www.elah.dev">elah</a> video engine.
|
|
39426
|
+
POST a build spec, get an MP4 back.</p>
|
|
39427
|
+
<table>
|
|
39428
|
+
<tr><td><code>GET /healthz</code></td><td class="muted">status + browser state</td></tr>
|
|
39429
|
+
<tr><td><code>POST /render</code></td><td class="muted">build spec JSON in, <code>video/mp4</code> out</td></tr>
|
|
39430
|
+
</table>
|
|
39431
|
+
<p>Render your first video (no media files needed):</p>
|
|
39432
|
+
<pre>${escapedCurl}</pre>
|
|
39433
|
+
<p class="muted">On Windows PowerShell, <code>curl</code> is aliased to Invoke-WebRequest and won't accept <code>-H</code>/<code>-d</code> the curl way — use this instead:</p>
|
|
39434
|
+
<pre>${escapedPowershell}</pre>
|
|
39435
|
+
<p class="muted">Spec schema: assets map + clips array — see the
|
|
39436
|
+
<a href="https://github.com/elahlabs/elah/tree/main/packages/cli#readme">@elah/cli README</a>.</p>
|
|
39437
|
+
`;
|
|
39438
|
+
}
|
|
39398
39439
|
function createServeHandler(deps) {
|
|
39399
39440
|
return (req, res) => {
|
|
39400
39441
|
void handle(req, res, deps).catch((err) => {
|
|
@@ -39406,6 +39447,17 @@ function createServeHandler(deps) {
|
|
|
39406
39447
|
}
|
|
39407
39448
|
async function handle(req, res, deps) {
|
|
39408
39449
|
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
39450
|
+
if (url2.pathname === "/") {
|
|
39451
|
+
if (req.method !== "GET") {
|
|
39452
|
+
writeJson(res, 405, { error: "method not allowed" });
|
|
39453
|
+
return;
|
|
39454
|
+
}
|
|
39455
|
+
const origin = `http://${req.headers.host ?? "localhost"}`;
|
|
39456
|
+
const html = welcomePage(origin, deps.browserConnected());
|
|
39457
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": Buffer.byteLength(html) });
|
|
39458
|
+
res.end(html);
|
|
39459
|
+
return;
|
|
39460
|
+
}
|
|
39409
39461
|
if (url2.pathname === "/healthz") {
|
|
39410
39462
|
if (req.method !== "GET") {
|
|
39411
39463
|
writeJson(res, 405, { error: "method not allowed" });
|
|
@@ -17,6 +17,18 @@ export interface ServeHandlerDeps {
|
|
|
17
17
|
maxBodyBytes: number;
|
|
18
18
|
log(line: string): void;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Text-only spec (no media assets) so the copy-paste example renders on a
|
|
22
|
+
* machine with nothing but the server running. fps/stage fall back to spec
|
|
23
|
+
* defaults (30, 1920x1080).
|
|
24
|
+
*/
|
|
25
|
+
export declare const HELLO_SPEC = "{\"clips\":[{\"track\":\"text\",\"text\":\"Hello from elah\",\"start\":0,\"duration\":3,\"fontSize\":96}]}";
|
|
26
|
+
export declare function curlExample(origin: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* PowerShell aliases `curl` to Invoke-WebRequest, which doesn't accept -H/-d
|
|
29
|
+
* the curl way. Point Windows users at curl.exe (or Invoke-RestMethod).
|
|
30
|
+
*/
|
|
31
|
+
export declare function powershellExample(origin: string): string;
|
|
20
32
|
export declare function createServeHandler(deps: ServeHandlerDeps): (req: IncomingMessage, res: ServerResponse) => void;
|
|
21
33
|
export interface ServeOptions {
|
|
22
34
|
host: string;
|