What is deck?
A presentation is a JSON list of markdown slides. That is the entire content model.
presentation.json
[
"# Quarterly review\n\nOctober",
"## Where the time went\n\n- Migrations\n- Meetings\n- Actual work"
]
Two commands work on it. deck writes decks and is built for an agent to
drive: every command is one edit to one record, and a deck is a folder on disk — no database, no server,
no account. deckui renders them, and never writes.
Installation
bash
npm i -g @apisurf/deck # writing
npm i -g @apisurf/deckui # building and serving
Published to GitHub Packages under the @apisurf scope, so installing
needs a token in your ~/.npmrc:
~/.npmrc
@apisurf:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=<a PAT with read:packages>
They are separate packages so an agent that only writes slides never installs Astro, marked or mermaid.
Five minutes
bash
deck new deck "Quarterly Review" # slug quarterly-review, title on slide 1
deck new slide --deck quarterly-review --file ./where-the-time-went.md
deck img add ~/Desktop/revenue.png --deck quarterly-review
deck new slide --deck quarterly-review --content ""
deck ls slides --deck quarterly-review # the slides, numbered
deckui serve quarterly-review # build it and open it in a browser
Where decks live
In one global folder, not per project, so every shell and agent session sees the same decks. Each deck is a
folder named by its slug; there is no index, so a folder copied in is a deck and one deleted is gone.
~/.apisurf/deck
~/.apisurf/deck/
quarterly-review/
presentation.json
images/
revenue.png
arch-talk/
presentation.json
images/
Point any command elsewhere with --home <dir>, or
$DECK_HOME for a whole session.
The format
presentation.json is a JSON array of strings. One string is one slide,
written in markdown; the order of the array is the order of the talk. There is no wrapper object, no ids and
no per-slide settings. Four things follow from that:
| What follows | Why |
| The title is the first heading of the first slide | There is nowhere else to put one. deck new deck writes the name there, so retitling is deck set slide 1 |
| Slides are addressed by position, from 1 | A slide has no identity beyond where it sits, so deck mv slide changes what deck set slide 3 means |
A slide opening with an # heading is centred | As a title or a section break. Everything else is flush left |
| Anything that can edit JSON can edit the deck | The commands are a convenience over an array, not a database with a shell around it |
Markdown is GitHub flavoured. Two things are worth calling out, both of which are just markdown:
markdown

```mermaid
flowchart LR
deck[presentation.json] --> build[deckui build] --> serve[deckui serve]
```
Images live in the deck's own images/ folder and are linked relatively, so
one link works in a build served at the root and under a sub-path. A fenced block whose language is
mermaid is drawn as a diagram; a deck with no diagram in it never loads the
library.
Commands
| Command | Does |
deck new deck "<name>" | Create a deck. --slug picks the slug instead of deriving it from the name |
deck new slide | Append a slide, or insert it before --at <n> |
deck ls decks|slides | The decks in the store, or one deck's slides, numbered |
deck get deck|slide <ref...> | A deck's facts, or a slide's markdown |
deck set slide <ref> | Replace a slide |
deck set deck <slug> --slug <new> | Rename a deck |
deck mv slide <ref> --to <n> | Reorder a slide. --into <deck> moves it to another deck, without its images |
deck rm slide <ref...> | Delete slides |
deck rm deck <slug...> | Delete decks. A non-empty one needs --force; --all --force empties the store |
deck cat <ref...> | The markdown of a whole deck, or of the slides named |
deck img [add|rm] | List, add or delete a deck's images |
deck howto [name] | Worked examples: write, many, images, diagram |
deck help [command] | One command's page. Topics: refs, store, format, images |
And on the other side
| Command | Does |
deckui ls [decks|slides] | What is in the store, and which decks are built |
deckui build [deck] | Render into static HTML, in ./dist/<slug> unless --out says otherwise |
deckui serve [deck] | Build into .builds/<slug> in the store, serve it and open the browser |
deckui open [dir] | Serve a folder that is already built, without reading the store |
deckui howto [name] | Worked examples: present, publish, snapshot |
deckui help [command] | One command's page. Topics: keys, snapshot, store |
A deck is named by its slug. A slide is its position, or last, and its deck
comes from --deck or the ref itself
(q3/3); when the store holds a single deck, neither is needed. A slide's
body comes from exactly one of --content,
--file <path>, or a bare - for
stdin.
bash
deck set slide 3 --deck q3 --content "## A heading"
deck set slide q3/3 --file ./slide.md
printf '# Questions?' | deck new slide --deck q3 -
deck rm slide last --deck q3
Typing one tool's command at the other is answered rather than refused:
deck build prints the deckui line with
the flags unchanged, and deckui new points back at
deck.
The built deck
deckui build writes one HTML page, a stylesheet, a small script, the
diagram library if a slide needs it, and images/. Every slide is in that
one page, so any static file server can host it and it needs no network once loaded.
keys
→ ↓ Space PageDown next slide
← ↑ Backspace PageUp previous slide
Home End first, last
F fullscreen
The URL carries the slide number, so a reload keeps your place, a link can point at one slide, and the back
button steps backwards through the talk. Printing the page gives one slide per sheet, which is how you get a
PDF. There are no transitions, no presenter view, no speaker notes and no timer.
deckui serve and open listen on
127.0.0.1:4200, or the next free port; --port
and --host change that, and serve --no-open
skips the browser.
bash
deckui build q3 --out ./public --base /talk # for a sub-path on a static host
deckui open ./public --base /talk # check it before uploading
deckui serve q3 --host 0.0.0.0 # present to a screen on the network
deckui build q3 --snapshot-only --snapshot ./deck.json # the JSON, for another renderer