Skip to content

CLI reference

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

Terminal window
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).

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).

Terminal window
liebstoeckel new <name> [--brand <brand>] [--dir <parent>] [--no-org-brand]
FlagMeaning
--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-branddon’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.

Terminal window
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
FlagMeaning
--dir <deck>target deck directory (default: current directory)
--dryprint the plan (files + dependencies) without writing
--forceoverwrite files that already exist (default: skip them)
--no-installdon’t run bun add for the items’ dependencies
--jsonstructured 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.

Browse the chart/component registry: the discovery surface for add, and the machine-readable contract agents read before scaffolding.

Terminal window
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 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.

Terminal window
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

Thumbnails 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.

FlagMeaning
--no-inline-packagedon’t embed the deck’s source (smaller file, not ejectable)
--no-inline-licensesdon’t embed the third-party license notices (not recommended)
--allow-secretoverride the secret gate that aborts if a packed file looks like a credential
--checkvalidate the deck bundles and resolves one copy of each framework package (no output written); reports diagnostics + exits non-zero on error
--trustpre-approve this deck’s build-time code (a deck is code; remembered after the first build)
--jsonmachine-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.

Terminal window
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):

Terminal window
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.

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>.

Terminal window
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

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.

Terminal window
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), 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).

Terminal window
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
FlagMeaning
--port NLAN port (default: auto)
--relay <url>present through a relay instead of LAN
--relay-token <tok>relay account token (or LIEBSTOECKEL_RELAY_TOKEN)
--no-thumbnailsskip thumbnail capture (also via LIEBSTOECKEL_NO_THUMBS=1)
--format / --width / --quality / --scaletune thumbnails, as in thumbs

Run a public relay server.

Terminal window
liebstoeckel relay [--port N] [--tokens tok1,tok2] [--public-url https://…]
FlagMeaning
--port Nlisten port (or PORT)
--tokenscomma-separated account tokens (or PRESENT_RELAY_TOKENS); one is generated if omitted
--public-urlthe public https:// origin (so links/WebSocket use wss://)

(Re)generate thumbnails for a built deck.

Terminal window
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 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>.

Terminal window
liebstoeckel export [deck.html|deck-dir|--dir <deck>] [opts]
flagmeaning
--format png|pdfoutput 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)
--rasterPDF only: image-per-page instead of the default vector PDF
--quality <n>JPEG quality for the raster PDF (default 92)
Terminal window
# 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).

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.

Terminal window
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
FlagMeaning
--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.

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.

Terminal window
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.

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).

Terminal window
liebstoeckel login --api https://<your-control-plane-host>
FlagMeaning
--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).

Terminal window
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.

FlagMeaning
--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)
--newcreate 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.

Terminal window
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.

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.

Terminal window
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 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.

FlagMeaning
--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.

Terminal window
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.

Comments

liebstoeckel

Code-first presentations your agent can author. One file out, no server.

© 2026 Leon Kaucher