CLI reference
One binary, liebstoeckel, with subcommands. A bare path is shorthand for live.
liebstoeckel <command> [args]liebstoeckel <deck> # shorthand for: liebstoeckel live <deck>liebstoeckel --help # the command surfaceliebstoeckel <command> --help # arguments and options for one commandliebstoeckel --versionEvery command has its own --help listing its arguments, options, and defaults.
The short alias lst is interchangeable with liebstoeckel (e.g. lst build).
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).
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). 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.
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.
liebstoeckel add bar-chart # one item into the current deckliebstoeckel add chart bar-chart line-chart # several at once (optional category word)liebstoeckel add donut-chart --dir ./presentations/demoliebstoeckel 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
Section titled “registry”Browse the chart/component registry: the discovery surface for add, and the
machine-readable contract agents read before scaffolding.
liebstoeckel registry list # the catalog (name · type · data shape)liebstoeckel registry view bar-chart # one item: exports, props, dataShape, exampleliebstoeckel 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 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 ejected 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.
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.
liebstoeckel build # the deck in the current directoryliebstoeckel build --dir <deck> # or a positional [dir]liebstoeckel build [dir] --no-inline-package # build WITHOUT the recoverable sourceliebstoeckel build [dir] --check # validate only: no artifact, structured errorsliebstoeckel build [dir] --trust # pre-approve an unfamiliar deck's build-time codeLIEBSTOECKEL_NO_THUMBS=1 liebstoeckel build [dir] # skip thumbnailsThumbnails need a headless Chromium; the build skips them (never fails) when none is
found. See Chromium setup 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:
{ "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).
Recover a built deck’s editable source from its .html.
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):
cd <outdir> && bun install --ignore-scripts && liebstoeckel buildFor the full recover → edit (by hand or with an agent) → rebuild loop, see Editing a built deck.
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>.
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 gatelicenses
Section titled “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.
liebstoeckel licenses dist/my-deck.html # print the notices embedded in a built deckliebstoeckel licenses # compute the report for the deck in cwdliebstoeckel 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 bundledThe 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), so you catch it before shipping.
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.
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).
liebstoeckel live # the deck in the current directoryliebstoeckel 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 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 |
Run a public relay server.
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
Section titled “thumbs”(Re)generate thumbnails for a built deck.
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).
export
Section titled “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>.
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) |
# slide 3 of a built deck → PNGliebstoeckel export ./dist/my-deck.html --slides 3 -o ./out
# whole deck as a PDF, building the source dir on demandliebstoeckel export ./presentations/demo --format pdf -o demo.pdfPDF 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).
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.
liebstoeckel skill install # all agents + AGENTS.md, into the current dirliebstoeckel skill install --target codex --dir ./my-deckliebstoeckel 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 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
Section titled “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.
bun update --latest @liebstoeckel/cli # update the CLI itselfliebstoeckel skill update # then refresh a deck's agent skillReminders 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)
Section titled “Cloud commands (coming soon)”The remaining commands talk to liebstoeckel cloud, the hosted control plane.
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).
liebstoeckel login --api https://<your-control-plane-host>| Flag | Meaning |
|---|---|
--api <url> | the control-plane base URL (or set LIEBSTOECKEL_API) |
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).
liebstoeckel build # produces ./dist/<deck-slug>.htmlliebstoeckel push # no path → that built deck in ./distliebstoeckel push --org acme # push into a team you belong toliebstoeckel push ./dist/my-deck.html --title "My deck" # explicit path + title overrideRe-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 |
List the organizations (personal workspace + teams) you belong to, and choose which one
push targets by default.
liebstoeckel orgs # list workspaces; → marks the push defaultliebstoeckel orgs use acme # make "acme" the default for pushTeams, 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.
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.
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 deckliebstoeckel new my-deck # auto-applies the org's default brandbrand 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 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 |
List your cloud decks in an organization, with view counts.
liebstoeckel decks # decks in your default workspaceliebstoeckel 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.
Code-first presentations your agent can author. One file out, no server.
Comments