# CLI reference

One binary, `liebstoeckel`, with subcommands. A bare path is shorthand for `live`.

```bash
liebstoeckel <command> [args]
liebstoeckel <deck>            # shorthand for: liebstoeckel live <deck>
liebstoeckel --help            # the command surface
liebstoeckel <command> --help  # arguments and options for one command
liebstoeckel --version
```

Every command has its own `--help` listing its arguments, options, and defaults.
The short alias `lst` is interchangeable with `liebstoeckel` (e.g. `lst build`).

## `new`

Scaffold a new deck. It materializes in the **current directory** as `./<name>`;
pass `--dir <parent>` to put it elsewhere (e.g. `--dir presentations` to drop it
into a monorepo's workspace glob).

```bash
liebstoeckel new <name> [--brand <brand>] [--dir <parent>] [--no-org-brand]
```

| Flag | Meaning |
| --- | --- |
| `--brand <brand>` | brand theme to wire in (default: `liebstoeckel`, or your org's default brand when logged in) |
| `--dir <parent>` | parent directory for the deck (default: the current directory) |
| `--no-org-brand` | don't bake the logged-in org's default brand; use the plain `liebstoeckel` brand |

Generates `index.html`, `main.tsx`, `build.ts` (using `buildDeck` from `@liebstoeckel/thumbnails/build`), `server.ts`, `bunfig.toml`, and a starter `slides/01-intro.tsx`. The name must be lower-case letters/digits/hyphens; it won't overwrite an existing directory.

When you're **logged in** (and didn't pass `--brand`), `new` automatically bakes your
org's **default brand** into the deck as owned source: `brands/<name>.ts`, wired into
`<Present brandThemes={…}>`, with `data-brand` set, so new decks are on-brand
instantly (see [`brand`](#brand)). Pass `--no-org-brand` to opt out, or `--brand` to
choose a specific one. It's best-effort: offline or signed-out, you just get the
default `liebstoeckel` brand.

## `add`

Scaffold registry items (charts, hooks, layouts, …) into a deck as **owned
source**: the code is copied into your project, not imported from a package, so
you can edit it freely. Leaf dependencies the items need (e.g. the relevant
`@visx/*` packages) are installed into the deck.

```bash
liebstoeckel add bar-chart                       # one item into the current deck
liebstoeckel add chart bar-chart line-chart      # several at once (optional category word)
liebstoeckel add donut-chart --dir ./presentations/demo
liebstoeckel add bar-chart --dry                 # show the plan, write nothing
```

| Flag | Meaning |
| --- | --- |
| `--dir <deck>` | target deck directory (default: current directory) |
| `--dry` | print the plan (files + dependencies) without writing |
| `--force` | overwrite files that already exist (default: skip them) |
| `--no-install` | don't run `bun add` for the items' dependencies |
| `--json` | structured plan/result output (default when stdout isn't a TTY) |

`add` always **prints its plan before writing**: which files it will create,
overwrite, or skip, plus the dependencies it will install. Files land under the
deck's `charts/` (or the item's own path); existing files are skipped unless
`--force` (so re-running is idempotent). Dependencies are installed with
`bun add --ignore-scripts` (the registry trust model), or skipped with
`--no-install` so you can run it yourself. With `--json` (or when piped), the plan
(`--dry`) or the result emits as JSON for an agent to consume.

Items resolve through a registry **namespace**. A bare name (`bar-chart`) comes
from the bundled default registry (`@liebstoeckel`); a scoped ref (`@acme/heatmap`)
comes from a registry you map in the deck's `liebstoeckel.json` under
`"registries"`, which today accepts a `"default"` keyword or a local path, with
npm/git/HTTP transports planned. Categories are `chart`, `hook`, `element`,
`component`, `layout`, `motion`; the leading category word is optional sugar.

## `registry`

Browse the chart/component registry: the discovery surface for [`add`](#add), and the
machine-readable contract agents read before scaffolding.

```bash
liebstoeckel registry list                 # the catalog (name · type · data shape)
liebstoeckel registry view bar-chart       # one item: exports, props, dataShape, example
liebstoeckel registry list --json          # JSON (default when piped / not a TTY)
```

`view` returns each component's **`dataShape`**, the exact type of its `data` prop,
so you (or an agent) can wire data without reading the source. Output is JSON when
`--json` is passed or stdout isn't a TTY, and a compact human view otherwise. This
reads the bundled `@liebstoeckel` registry.

## `build`

Build a deck to a single self-contained `.html` (+ thumbnails), written to
`./dist/<deck-slug>.html`. The slug is the deck folder name, e.g. `poll-demo/` →
`dist/poll-demo.html`. By default the build also
embeds a compressed copy of the deck's **own source** (slides, `package.json`, etc., not
`node_modules`), so the compiled `.html` can later be [`eject`](#eject)ed back to an editable
project. Source collection uses `bun pm pack`, so your deck's `package.json` `"files"` list
controls exactly what's embedded.

The deck defaults to the **current directory**; pass it as a positional `[dir]` or
with `--dir <deck>` (the convention shared by `pack`/`live`/`export`).

The build also embeds a **third-party license notice** built from the modules that
actually end up in the bundle (React, Motion, Yjs, the fonts, and the MPL-2.0 engine).
Minification strips license comments, so this puts the required attribution back. It is
recomputed on every build, so it stays correct when you swap a font or a library. Read
it with [`licenses`](#licenses).

The build requires a **single copy of each `@liebstoeckel/*` package**. A deck compiles
into one file, so if two framework packages disagree on a version — for example after
bumping `@liebstoeckel/engine` but leaving a plugin on an older release — the bundle would
embed two incompatible copies side by side. The build stops, prints the conflicting
versions, and asks you to update the framework packages together (e.g. `bun update`) so
they resolve to one version. `--check` reports the same conflict without writing anything.

```bash
liebstoeckel build                             # the deck in the current directory
liebstoeckel build --dir <deck>                # or a positional [dir]
liebstoeckel build [dir] --no-inline-package   # build WITHOUT the recoverable source
liebstoeckel build [dir] --check               # validate only: no artifact, structured errors
liebstoeckel build [dir] --trust               # pre-approve an unfamiliar deck's build-time code
LIEBSTOECKEL_NO_THUMBS=1 liebstoeckel build [dir]   # skip thumbnails
```
**Building a deck runs its code:** A deck is **code**. Building it runs the deck's build-time modules (Bun macros such as the
animated-code macro, and build plugins) in this process with full access to your files and
network — so building a deck you did not write is arbitrary code execution on your machine.
The first time you build an unfamiliar deck, `build` asks you to confirm trust and remembers
your answer. Decks you scaffold with [`new`](#new) are trusted automatically. In a
non-interactive shell (CI) it refuses unless you pass `--trust` or set
`LIEBSTOECKEL_TRUST_BUILD=1`. **Only build decks you trust.** (Note: `--ignore-scripts`
blocks npm lifecycle scripts, not a deck's macros — it is not a substitute for this.)

Thumbnails need a headless Chromium; the build skips them (never fails) when none is
found. See [Chromium setup](https://docs.liebstoeckel.app/guides/chromium/) to install one or point at an existing
binary with `LIEBSTOECKEL_CHROMIUM`.

| Flag | Meaning |
| --- | --- |
| `--no-inline-package` | don't embed the deck's source (smaller file, not ejectable) |
| `--no-inline-licenses` | don't embed the third-party license notices (not recommended) |
| `--allow-secret` | override the secret gate that aborts if a packed file looks like a credential |
| `--check` | validate the deck **bundles** and resolves one copy of each framework package (no output written); reports diagnostics + exits non-zero on error |
| `--trust` | pre-approve this deck's build-time code (a deck is code; remembered after the first build) |
| `--json` | machine-readable JSON result (default when stdout isn't a TTY); progress goes to stderr |

`--check` is the fast validation gate (no Chromium, no thumbnails, nothing written):
it returns `{ ok, diagnostics }`, each diagnostic carrying `message`/`file`/`line`,
and exits non-zero when the deck doesn't bundle. It checks that imports resolve and
MDX/TSX transforms; it does not type-check. Output is JSON when piped or with `--json`.

With `--json` (or piped), a normal **write** build prints the human progress to stderr and
emits a single result object on stdout, so an agent can read the artifact path without
scraping prose:

```json
{ "ok": true, "artifact": "/abs/path/dist/my-talk.html", "outfile": "my-talk.html", "thumbnails": 6 }
```

`thumbnails` is `null` (with a `thumbnailsSkipped` reason) when no Chromium is available.
A refused untrusted build emits `{ "ok": false, "error": "untrusted deck", "hint": "…" }`
and exits non-zero. Trusting a deck you didn't write means agreeing to run its build-time
code on your machine — a human decision; an AI agent should surface it rather than approve
on its own. A human who trusts the deck approves it with `--trust` (or `LIEBSTOECKEL_TRUST_BUILD=1`).
**Anyone with the file has the source:** The embedded source is recoverable by anyone with the `.html`. A `"files"` allowlist in the deck's
`package.json` (the scaffold ships one) keeps secrets out by construction; the build also refuses to
embed a `.env` or a file matching a credential signature. Use `--no-inline-package` to ship an opaque deck.

## `eject`

Recover a built deck's editable source from its `.html`.

```bash
liebstoeckel eject <deck.html> [outdir] [--force]
```

`outdir` defaults to `<deck>-source/`. Eject refuses a non-empty directory unless `--force`, and
errors clearly if the HTML carries no embedded source (e.g. it was built `--no-inline-package`).
Rebuilding runs the deck's own build-time code (macros/build plugins) — `--ignore-scripts`
only blocks npm lifecycle scripts, not that — so **only rebuild a deck you trust**.
`liebstoeckel build` confirms an unfamiliar deck once (see the warning under [`build`](#build)):

```bash
cd <outdir> && bun install --ignore-scripts && liebstoeckel build
```

For the full recover → edit (by hand or with an agent) → rebuild loop, see
[Editing a built deck](https://docs.liebstoeckel.app/guides/editing-a-built-deck/).

## `pack`

Inspect, or emit, exactly the source a build would embed, without running a full build.
The deck defaults to the **current directory**; pass it as `[dir]` or `--dir <deck>`.

```bash
liebstoeckel pack [dir]                  # print the file list (defaults to cwd)
liebstoeckel pack [dir] -o deck.tgz      # write a gzip tarball (bun add-compatible)
liebstoeckel pack [dir] --allow-secret   # override the secret gate
```

## `licenses`

Report the third-party licenses bundled into a deck. Pass a built `.html` to print
the notices it already carries, or a deck `[dir]` / `--dir <deck>` (default: cwd) to
compute the report fresh from the deck's real module graph.

```bash
liebstoeckel licenses dist/my-deck.html   # print the notices embedded in a built deck
liebstoeckel licenses                     # compute the report for the deck in cwd
liebstoeckel licenses --dir <deck>        # or a positional [dir]
liebstoeckel licenses [dir] --json        # structured: { ok, packages, firstParty, flagged }
liebstoeckel licenses [dir] --check       # exit non-zero if any non-standard license is bundled
```

The report lists only what is actually inlined into the `.html`: the client bundle and
the embedded fonts. Build-time tools like the MDX compiler, Tailwind, and the syntax
highlighter run during the build and aren't redistributed, so they're left out.

A built **`.html`** carries its notices as rendered text, so the command prints them
(with `--json`, as `{ source, notices }`). A deck **directory** is recomputed from a
fresh bundle, so it gives the full structured report (`{ ok, packages, firstParty,
flagged }`) and is the only input that works with `--check`. As with `build --check`,
the output is JSON when piped or with `--json`.

`--check` is a CI gate: it fails when a bundled package carries a license outside the
permissive/embeddable set (for example a copyleft chart library pulled in via
[`add`](#add)), so you catch it before shipping.
**Notices are embedded automatically:** Every `build` embeds these notices into the `.html` (see [`build`](#build)); `licenses`
is for inspecting and gating them. Disable the embed with `build --no-inline-licenses`
only if you have another way to satisfy the attribution requirements.

Useful for verifying your `"files"` allowlist before shipping. The `-o` tarball is standard gzip
(`bun add ./deck.tgz`-installable); the in-HTML embed uses zstd internally.

## `live`

Present a deck to a room. Builds the deck if given a project dir, and captures
slide thumbnails by default (so the overview grid is instant); the capture is
skipped, not failed, when no Chromium is available.

The deck defaults to the **current directory**; pass it as a positional or with
`--dir <deck>` (the same convention as `build`/`pack`/`export`).

```bash
liebstoeckel live                               # the deck in the current directory
liebstoeckel live <deck.html | deck-dir> [--port N]
liebstoeckel live --dir <deck> --relay <url> --relay-token <token>
liebstoeckel live <deck> --no-thumbnails        # skip the capture step
```

| Flag | Meaning |
| --- | --- |
| `--port N` | LAN port (default: auto) |
| `--relay <url>` | present through a [relay](https://docs.liebstoeckel.app/guides/relay/) instead of LAN |
| `--relay-token <tok>` | relay account token (or `LIEBSTOECKEL_RELAY_TOKEN`) |
| `--no-thumbnails` | skip thumbnail capture (also via `LIEBSTOECKEL_NO_THUMBS=1`) |
| `--format` / `--width` / `--quality` / `--scale` | tune thumbnails, as in [`thumbs`](#thumbs) |

## `relay`

Run a public relay server.

```bash
liebstoeckel relay [--port N] [--tokens tok1,tok2] [--public-url https://…]
```

| Flag | Meaning |
| --- | --- |
| `--port N` | listen port (or `PORT`) |
| `--tokens` | comma-separated account tokens (or `PRESENT_RELAY_TOKENS`); one is generated if omitted |
| `--public-url` | the public `https://` origin (so links/WebSocket use `wss://`) |

## `thumbs`

(Re)generate thumbnails for a built deck.

```bash
liebstoeckel thumbs <built-deck.html> [--format webp|jpeg|png] [--width 640] [--quality 80] [--scale 2]
```

Needs Chromium and fails if none is found ([Chromium setup](https://docs.liebstoeckel.app/guides/chromium/)).

## `export`

Export slides to **PNG** files or a single **PDF** (one slide, a range, or the
whole deck), rendered headless on the native 1280×720 canvas, the same pipeline as
thumbnails. The input is a built `.html` **or** a deck source directory (built on
demand); it defaults to the **current directory**, or pass it as a positional or
with `--dir <deck>`.

```bash
liebstoeckel export [deck.html|deck-dir|--dir <deck>] [opts]
```

| flag | meaning |
| --- | --- |
| `--format png\|pdf` | output format (default: inferred from `-o`'s extension, else `png`) |
| `--slides <spec>` | 1-based, inclusive: `3`, `2-5`, `1,3,5-7`, `3-`, `-4` (default: all) |
| `-o`, `--out <path>` | PDF: the `.pdf` file. PNG: an output directory |
| `--width <n>` | viewport / page width (default `1280`, the authoring canvas) |
| `--scale <n>` | device-scale factor for PNG / raster PDF (default `2` → 2560×1440) |
| `--raster` | PDF only: image-per-page instead of the default vector PDF |
| `--quality <n>` | JPEG quality for the raster PDF (default `92`) |

```bash
# slide 3 of a built deck → PNG
liebstoeckel export ./dist/my-deck.html --slides 3 -o ./out

# whole deck as a PDF, building the source dir on demand
liebstoeckel export ./presentations/demo --format pdf -o demo.pdf
```

**PDF is vector by default**: selectable, searchable text with embedded fonts and
vector charts/gradients, produced in a single `page.pdf()` over a print view that
stacks one slide per page (no merge, no extra dependency). Pass `--raster` for an
image-per-page PDF instead: no text layer, but pixel-exact fidelity for a slide
whose effects don't reproduce under print.

PNGs come straight off the page (lossless, native resolution), named
`<deck>-slide-NN.png` (1-based, zero-padded). Like `thumbs`, `export` needs Chromium
and fails loudly if none is found ([Chromium setup](https://docs.liebstoeckel.app/guides/chromium/)).

## `skill`

Install the **`liebstoeckel-deck` agent skill** into a project so AI coding agents can
create and edit decks for you (scaffold → add charts → wire data → build). The skill
ships inside the CLI package, version-pinned so it always matches your installed
`liebstoeckel`, and is placed where each agent looks for it.

```bash
liebstoeckel skill install                        # all agents + AGENTS.md, into the current dir
liebstoeckel skill install --target codex --dir ./my-deck
liebstoeckel skill update                         # refresh the installed skill to the CLI's version
```

| Flag | Meaning |
| --- | --- |
| `--target <list>` | (install) `claude`, `codex`, `cursor`, `gemini`, or `all` (default); comma-separated |
| `--dir <deck>` | where to install (default: current directory) |

It writes the same skill to each agent's path (`.claude/skills/`, `.agents/skills/` for
Codex/Gemini, `.cursor/skills/` + a rule) and merges a managed block into `AGENTS.md`,
the universal fallback for agents without skill support. Re-running is idempotent. See
the [Scaffolding guide](https://docs.liebstoeckel.app/guides/scaffolding/) for how an agent uses it.

The installed skill is **version-pinned to the CLI** that wrote it. `skill update`
rewrites the skill for the agent paths already present in the deck (plus the
`AGENTS.md` block) without adding new ones; when a deck's skill is older than the CLI
you run, commands print a one-line reminder to do exactly that.

## Staying up to date

The CLI checks its registry for a newer `@liebstoeckel/cli` at most **once a day**, in
a detached background process, and prints a one-line note on a later run when an update
exists. The check shells out to `bun pm view`, so it follows the same registry
configuration as your installs (a scoped registry in `.npmrc`/`bunfig.toml`, or the
public npm registry); no command ever waits on the network for it.

```bash
bun update --latest @liebstoeckel/cli   # update the CLI itself
liebstoeckel skill update               # then refresh a deck's agent skill
```

Reminders appear only on an interactive terminal. They are suppressed with `--json`,
when output is piped (agents), when `CI` is set, and entirely with
`LIEBSTOECKEL_NO_UPDATE_CHECK=1`.

## Cloud commands (coming soon)

The remaining commands talk to **liebstoeckel cloud**, the hosted control plane.
**Coming soon:** liebstoeckel cloud is not generally available yet. `login`, `push`, `orgs`, `decks`, and
`brand` ship in the CLI today, but without a hosted service to sign in to they exit with
a "coming soon" notice. They work against a private control-plane deployment via
`--api` / `LIEBSTOECKEL_API`. See the [Cloud & teams guide](https://docs.liebstoeckel.app/guides/cloud/).

### `login`

Sign in to **liebstoeckel cloud** (the hosted control plane) from the terminal using the
OAuth 2.0 device-authorization grant (RFC 8628), with no password. The CLI prints a URL;
open it in a browser, sign in with an email one-time code, and approve. The resulting
token is stored in `~/.config/liebstoeckel/credentials.json` (mode `600`).

```bash
liebstoeckel login --api https://<your-control-plane-host>
```

| Flag | Meaning |
| --- | --- |
| `--api <url>` | the control-plane base URL (or set `LIEBSTOECKEL_API`) |

### `push`

Upload a built single-file deck to your cloud dashboard. Authenticates with the token
from `login`; the deck appears in your dashboard's deck list, and for a **team org** it's
visible to every member (the shared library).

```bash
liebstoeckel build                       # produces ./dist/<deck-slug>.html
liebstoeckel push                        # no path → that built deck in ./dist
liebstoeckel push --org acme             # push into a team you belong to
liebstoeckel push ./dist/my-deck.html --title "My deck"   # explicit path + title override
```

**Re-push = a new version.** `push` identifies a deck by a stable **key** (the deck
folder name, or `--name <key>`); pushing the same key again updates that deck **in place
at the same URL** (a new version on Pro; the latest is replaced on free). `--new` forces a
separate deck. With **no path**, `push` uploads the single built `.html` in `./dist`
(run `build` first). The **title** comes from `--title` → the deck's own embedded
`<title>` → the deck key: the server reads the `<title>` straight from the
uploaded file, so any Unicode (em-dashes, smart quotes, emoji) just works. Only an
explicit `--title` override travels as a header (URL-encoded). A **cover** is read from
the deck's built-in thumbnails.

| Flag | Meaning |
| --- | --- |
| `--title <title>` | override the title (default: the deck's embedded `<title>`, then its key) |
| `--name <key>` | the deck key to upsert (default: the deck folder name) |
| `--new` | create a separate deck instead of updating the matching one |
| `--org <slug>` | push into this organization (default: the one set by `orgs use`, else personal) |
| `--api <url>` | override the stored control-plane host |

### `orgs`

List the organizations (personal workspace + teams) you belong to, and choose which one
`push` targets by default.

```bash
liebstoeckel orgs                        # list workspaces; → marks the push default
liebstoeckel orgs use acme               # make "acme" the default for push
```

Teams, members, roles (`owner` / `admin` / `member`), and invitations are managed in the
dashboard's **Team** page; the CLI only needs to pick which org a deck lands in.

### `brand`

Share governed **brand token sets** across a team. Your org is an authenticated registry
(the same protocol as `add`/`registry`); a brand is pulled into a deck as **owned source**
(`brands/<name>.ts`) and baked at build, so the deck stays self-contained and WYSIWYG.

```bash
liebstoeckel brand list                  # brands in your org (→ marks the default)
liebstoeckel brand push ./brand.ts --default   # upload a theme token file (admins/owners)
liebstoeckel brand pull acme             # write brands/acme.ts into the current deck
liebstoeckel new my-deck                 # auto-applies the org's default brand
```

`brand push` accepts a `defineTheme(...)` module or a flat tokens JSON. Editing is also
available in the dashboard's **Brand** page. Brand sharing is a paid (Pro/Team) feature.

A brand's fonts are picked from a curated [Fontsource](https://fontsource.org) catalog in
the Brand page. `brand pull` writes the `import "@fontsource-variable/…"` into
`brands/<name>.ts` **and runs `bun add`** to install those font packages, so the webfont is
inlined into your single-file deck at build (pass `--no-install` to install them yourself).
A **Custom…** font carries no package and falls back to the system font unless you supply
its own `@font-face`.

| Flag | Meaning |
| --- | --- |
| `--default` | (push) make this the org default that `new` applies |
| `--name <key>` | (push) brand name (default: the theme's `name`, then the file name) |
| `--dir <deck>` | (pull) the deck to write `brands/<name>.ts` into (default: cwd) |
| `--no-install` | (pull) write the files but don't `bun add` the brand's font packages |
| `--org <slug>` | the organization to act on |

### `decks`

List your cloud decks in an organization, with view counts.

```bash
liebstoeckel decks                       # decks in your default workspace
liebstoeckel decks --org acme            # decks in a team you belong to
```

`--org` works the same on every cloud command (`push`, `decks`, …): explicit `--org`
wins, else the default from `orgs use`, else your personal workspace.