Skip to content

Upgrading to 0.4

0.4 brings the live audience work: the join card and stable join link, a phone-first audience view, clear audience states in English and German, and presenter links that keep their token out of the address bar. It builds decks with a new build core, and the CLI brings its own Bun, so you no longer need Bun installed. It also tightens the CLI’s machine output. This page lists what to do and what behaves differently from 0.3.12.

  1. In each deck, run:

    Terminal window
    liebstoeckel update

    It moves every @liebstoeckel/* package the deck declares to its latest release, updates a global CLI install, and refreshes the agent skill. It prints one line per package that moved. Then it brings the deck’s scaffold files along (see The build core): it removes the 0.3 bunfig.toml, server.ts and build.ts where you never changed them, and says what to do with one you did change. A plain bun update keeps a deck on 0.3: before 1.0, ^0.3.10 does not include 0.4.

  2. Rebuild the deck (liebstoeckel build), and rebuild any .html you present through a relay (see Presenting an older deck).

  3. If the deck defines plugins of its own and presents them through a relay, declare their audience fields (see Plugins defined inside a deck).

All @liebstoeckel/* packages in a deck have to be on the same release line. A deck is one inlined .html, so two copies of a package (say a 0.3 and a 0.4 plugin-sdk) would both be bundled, and the build refuses that. The error names the package that pulled in the older copy.

Exact pins stay exact. A dependency written as "0.3.10" becomes "0.4.0", not "^0.4.0", so a deck that pins keeps pinning. Ranges move as bun moves them.

Plugin authors: a plugin package that depends on @liebstoeckel/plugin-sdk or @liebstoeckel/plugin-ui with ^0.3.x pulls in a 0.3 copy next to the deck’s 0.4 one. Publish a release that accepts ^0.4.0.

A 0.4 relay moves a presenter link’s token from the address into a cookie on the first load, so a projected presenter tab shows nothing that grants control. A deck built with an engine before 0.4 builds its “drive from your phone” link from the address bar, so the relay leaves the token in the address for such a deck, as before, and the phone link keeps working. The relay tells the two apart by the engine in the deck’s generator stamp. Rebuild the deck with 0.4 to get the cookie.

The other way round, a 0.4 deck on a 0.3 relay works, with the token left in the address bar (only the relay can move it), and a viewer’s address may gain a ?pid= (a 0.3 relay does not keep the participant id in a cookie, so the page keeps it in its address).

A relay lets the audience write only the state fields a plugin declares. A plugin package declares them in its own package.json; a plugin defined with definePlugin inside the deck had no way to, so on a relay every audience write to it was lost (0.3 relays showed the viewer their own vote while the presenter saw none; 0.4 relays refuse it and say so). Declare them in the deck’s package.json, by plugin id:

package.json (the deck)
"liebstoeckel": {
"plugins": {
"mood": { "audienceWrites": ["votes"] }
}
}

See a plugin inside the deck. Add "plugins" to the deck’s files list too (decks made with liebstoeckel new from 0.4 on have it), or the built deck’s embedded source, and with it eject, lacks the plugin. The build warns when it is missing.

The build core replaces the scaffold’s tooling files

Section titled “The build core replaces the scaffold’s tooling files”

0.4 builds and serves decks with its own build core on esbuild instead of Bun’s bundler and dev server. For a deck author that means:

  • Three files fewer. A 0.3 deck carries a bunfig.toml, a server.ts and a build.ts, with dev and build scripts that run them. liebstoeckel update (and liebstoeckel dev) removes the ones you never changed, points the scripts at liebstoeckel dev and liebstoeckel build, and drops them from files. One you changed stays, and the CLI says what to do with it: move what you added elsewhere, then delete it. Nothing breaks while it is there, the CLI never runs it. This is migration 0003-scaffold-tooling-files; see scaffold migrations.
  • No Bun plugins, no other macros. The build runs nothing of the deck. A Bun plugin of your own in bunfig.toml is reported by dev, build and doctor, with what to do instead (see Building); a macro import other than codeStory, and Tailwind’s @plugin or @config, are errors.
  • Animated code moves to code files. Write the states of a <CodeMagic> block as fenced blocks in a .code.md file and import it. The codeStory macro still builds, with a warning that it is deprecated; it goes in a later release. See Animated code for how to move a slide.
  • The same bytes everywhere. The same source builds to the same file on every machine and from any folder, and hot reload keeps the slide, the step and component state across edits.

From 0.4 the liebstoeckel command runs on its own Bun, one exact version (Bun 1.4.3) installed from npm with the CLI, and never on the Bun you have installed. You no longer need Bun at all: npm install -g @liebstoeckel/cli is enough. liebstoeckel new installs a new deck’s dependencies itself, and liebstoeckel install replaces bun install and bun add in a deck. liebstoeckel --version and doctor show which Bun and esbuild run. If you installed the CLI with --omit=optional, or through a registry mirror that does not proxy the @oven scope, the command says what is missing; see Installing the CLI. On Alpine (musl) install the musl build of Bun next to the CLI, as the message says.

A deck you installed with Bun 1.3 needs nothing: the CLI reads its bun.lock and keeps it in that format. A deck the CLI installs gets a bun.lock in Bun 1.4’s format. If you then run bun install in it yourself with Bun 1.3, that Bun prints “Unknown lockfile version” once, ignores the lockfile and writes it again in its own format, which the CLI then keeps. To avoid the extra change in your history, use liebstoeckel install in the deck, or move your own Bun to 1.4 (bun upgrade).

A deck that still runs its own 0.3 server.ts or build.ts needs bun-plugin-tailwind in the deck now (liebstoeckel install --dev bun-plugin-tailwind), or moves to liebstoeckel dev and liebstoeckel build, which need nothing.

The CLI’s machine output follows one set of rules on every command. What a 0.3.12 script may notice:

Change0.3.120.4
Unknown optionsignored (build --check --thumbs=false ran)refused before the command does anything, exit 2, code unknown_option, with the closest option it takes
Unknown commandsthe whole usage, exit 1one line with the closest command, exit 2, code unknown_command
--versionaccepted after a command; prints the versiononly liebstoeckel --version; prints liebstoeckel <version>, then the Bun and esbuild lines
Usage errors (a missing or invalid argument, e.g. add with nothing to add, registry view without a name, build --visual without --check, eject without a deck, push without a file, new without a name)exit 1exit 2
JSON when pipedbuild, licenses, add, registry, doctoralso pack, push, decks, orgs list, brand list, and the new pull, sync, folders
--no-json in a pipehad no effect on the JSON commandsprints the human output
Error output in JSON mode{ "error": "..." }, or prose on stderr for build, pack and cloud HTTP errorsone document on stdout: { "ok": false, "error", "code", "hint" }
dev poll errorson stderr, { "error": "poll_failed", "message": ... } for a refused host or token tooon stdout with a code of their own: unauthorized, forbidden_host, no_dev_server, poll_failed; a bad --reply or --data exits 2
orgs use, brand push, brand pull without credentialsJSON on stdout when pipedprose on stderr (they have no JSON mode)

Everything a 0.3.12 command did successfully it still does; the codes are listed under error codes.

The agent skill follows the deck. The CLI refreshes an installed skill to its own version on any command, except in a deck whose engine is an older minor (a deck still on 0.3 under a 0.4 CLI): the 0.4 skill tells agents to use features such as <JoinCard /> that a 0.3 engine does not have, so there the skill stays as it is until liebstoeckel update moves the deck. liebstoeckel doctor --json reports it as skill.held.

  • Default brand. A deck that names no brands now uses the liebstoeckel house brand. 0.3 defaulted to a brand name with no theme behind it, so such a deck rendered without theme tokens. An unknown brand name now prints a warning in development.
  • Smaller files. Builds always use React’s production build (before, the builder’s NODE_ENV decided, and it was usually the development build): a typical deck is about 17 % smaller.
  • Generator stamp. The engine version in a built file is the engine the deck bundles, not the one the CLI builds with.
  • Eject gets the deck’s bun.lock and .gitignore back, so liebstoeckel install in the ejected project restores the versions the deck was built with. The lockfile travels inside the built .html; a build refuses a lockfile with a password in a URL and names any registry other than npm’s that it lists.
  • Live connections send a keepalive every 10 s (was 25 s), on the relay and on liebstoeckel live, and a client gives up on a silent connection after 35 s (was 60 s), so a dropped phone reconnects sooner.
  • Relay sessions can be requested for up to 24 h (the default stays 6 h). The relay takes up to 10 000 entries in a plugin field holding objects and 50 000 in one holding single values (was 5 000 for both); PRESENT_RELAY_AUDIENCE_ENTRY_CAP lowers both, for testing what a full session looks like.

Comments

liebstoeckel

Live, programmable presentations. Your deck is a program; run it with the room.

© 2026 Leon Kaucher