@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 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
+ [![npm](https://img.shields.io/npm/v/@elah/cli)](https://www.npmjs.com/package/@elah/cli)
7
+ [![node](https://img.shields.io/node/v/@elah/cli)](https://www.npmjs.com/package/@elah/cli)
8
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](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, "&lt;");
39890
+ const escapedPowershell = powershellExample(origin).replace(/</g, "&lt;");
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>&#9679; ${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 &mdash; use this instead:</p>
39917
+ <pre>${escapedPowershell}</pre>
39918
+ <p class="muted">Spec schema: assets map + clips array &mdash; 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, "&lt;");
39407
+ const escapedPowershell = powershellExample(origin).replace(/</g, "&lt;");
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>&#9679; ${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 &mdash; use this instead:</p>
39434
+ <pre>${escapedPowershell}</pre>
39435
+ <p class="muted">Spec schema: assets map + clips array &mdash; 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elah/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Headless command-line runtime for the Elah video engine — split, trim, and export projects using @elah/core",
5
5
  "type": "module",
6
6
  "bin": {