CLI
Every deckard command and flag, with its defaults, what it prints, its exit codes, and the environment variables it reads, taken from the CLI source.
deckard <command> [options]
deckard is the only binary @thebuilder/deckard-cli puts on the path. Every command except init takes the current working directory as the deck, which is how a generated app wires its own scripts:
{
"scripts": {
"deck:validate": "deckard validate",
"deck:check-overflow": "deckard check-overflow",
"export:pdf": "deckard export pdf"
}
}
deckard --help (or -h, or no arguments) prints the command list. deckard --version (or -v) prints the version.
Commands
| Command | What it does |
|---|---|
deckard init <dir> |
Write a deck, install it, commit it, typecheck it. |
deckard validate |
Load the deck, check its slides and its theme. |
deckard doctor |
Check node, the package, the stylesheet, the deck, and the routes. |
deckard check-overflow |
Fail listing the slides that lose content to the canvas, the chrome, or a clipped box. |
deckard screenshots |
One PNG per slide at canvas size. |
deckard contact-sheet |
Every screenshot in one labelled grid. |
deckard export pdf |
One PDF page per slide. |
deckard add theme <name> |
Point deck/deck.ts at another built-in theme. |
deckard add block <name> |
Install a registry block into app/slides/blocks. |
deckard eject theme |
Copy the built-in the deck uses into deck/theme. |
How flags parse
Only long --flags. There are no short aliases anywhere except the top-level -h and -v.
- String flags take
--flag valueand--flag=valuealike. A missing value, or a next token starting with--, is an error. - Boolean flags never consume the next token.
--lightistrue;--light truesets the flag and leavestrueas a stray positional. --no-<name>sets a declared boolean tofalse. It does not work on string flags.- Unknown flags are rejected with the list of flags that command does take.
The list it prints:
deckard --help
deckard 0.0.1
deckard init <dir> create a deck: a Next.js app with slides, a theme, and the checks
--theme <name> meridian, atelier, aurora, blueprint, cotton,
ledger, nexus, noir, phosphor, or quorum
(default meridian)
--empty two slides instead of the sample deck
--package-manager <name> npm, pnpm, yarn, or bun (default: the one that ran init)
--no-install write the files and stop
--no-git skip git init and the first commit
--core-tarball <path> install @thebuilder/deckard-core from a local tarball
--cli-tarball <path> install @thebuilder/deckard-cli from a local tarball
--themes-tarball <path> install @thebuilder/deckard-themes from a local tarball
--registry <url> the @deckard registry for components.json
deckard validate load the deck, check its slides and its theme
--registry <path> also check a shadcn registry.json
deckard doctor check node, the package, the stylesheet, the deck, and the routes
deckard check-overflow fail on slides the canvas clips
deckard screenshots one PNG per slide at canvas size, into out/screenshots
--max <n> stop after n slides
deckard contact-sheet every screenshot in one grid, into out/contact-sheet.png
--columns <n> columns in the grid (default 4)
deckard export pdf one PDF page per slide, into out/slides.pdf
--dark export the dark deck instead of the light one
The four checks above build the app and serve it. They take --port <n> and
--skip-build, and every one but the PDF export takes --light.
deckard add theme <name> point deck/deck.ts at another built-in theme
deckard eject theme copy the built-in the deck uses into deck/theme, yours to edit
--theme <name> eject that built-in instead of the imported one
deckard add block <name> install a block from the registry, into app/slides/blocks
--registry <url> a registry URL carrying a {name} placeholder
--yes overwrite the files it installs without asking
Every command except init runs against the deck in the current directory.
deckard init
deckard init my-talk --theme phosphor
Takes exactly one positional, the target directory, and refuses one that already has files in it.
| Flag | Value | Default |
|---|---|---|
--theme <name> |
any built-in, from the theme gallery | meridian |
--empty |
boolean | off, so the sample deck |
--package-manager <name> |
bun, npm, pnpm, yarn |
detected |
--no-install |
boolean | install runs |
--no-git |
boolean | git runs |
--registry <url> |
a URL with a {name} placeholder |
https://deckard.thebuilder.dk/r/{name}.json |
--core-tarball <path> |
a local .tgz |
^<cli version> from npm |
--themes-tarball <path> |
a local .tgz |
^<cli version> from npm |
--cli-tarball <path> |
a local .tgz |
^<cli version> from npm |
The package name comes from the directory basename, lowercased with anything outside [a-z0-9-] collapsed to a hyphen.
Package manager precedence for init: the --package-manager flag, then npm_config_user_agent, then the first lockfile found walking up from the target’s parent (pnpm-lock.yaml, bun.lock, bun.lockb, yarn.lock, package-lock.json), then npm. Whichever wins is what the install runs with. Through npx it is npm:
No packageManager field is written into the generated package.json. Corepack reads that field as an enforcement lock, so recording the manager that happened to run init would refuse every other manager afterwards. Later deckard commands resolve the deck’s manager from a packageManager field if you add one yourself, then the lockfile, then npm_config_user_agent, then npm.
npx @thebuilder/deckard-cli init my-talk
Created my-talk in ~/my-talk
meridian theme, sample deck
npm is the package manager for this deck
Installing dependencies with npm
added 453 packages, and audited 454 packages in 14s
Typechecking the new deck
> my-talk@0.1.0 typecheck
> next typegen && tsc --noEmit
Generating route types...
✓ Types generated successfully
cd my-talk
npm run dev
Slides are deck/slides.tsx. The theme is imported from @thebuilder/deckard-themes; run deckard eject theme to own a copy of it.
The export commands drive Chromium. Download it once with npm exec -- playwright install chromium.
init writes playwright and pdf-lib into the deck’s devDependencies. The capture commands load both from the deck rather than from the CLI, so the CLI install itself carries neither.
--no-install skips the typecheck too, since there is nothing installed to typecheck against. A git failure is a warning, not an exit, and the commit prints nothing when it works. A typecheck failure exits 1 after everything is written, so the files are on disk either way.
deckard validate
deckard validate
deckard validate --registry ../../registry.json
| Flag | Value | Default |
|---|---|---|
--registry <path> |
path to a shadcn registry.json |
none |
Loads the real deck/deck.ts through a throwaway in-memory Vite SSR server, in about a second, and reports three sections:
- deck: the deck resolves, every slide has a body, every
sourcePatha discovered module reports is a file on disk. A duplicate slug, an unsafe slug, or a module with no default export comes back as one line naming the slide or the file, not a build-time stack trace. - theme: the theme class is a real selector in the stylesheet the deck renders, the dark block defines nothing the light block does not, and
colorModesmatches the blocks that stylesheet carries. It readsdeck/theme/theme.cssfor an ejected theme and the copy inside the installed package for an imported one, and names which it read. - registry, only with
--registry: everyitems[].files[].pathexists, resolved against that file’s own directory.
Exits 1 on any problem. Prints slide counts otherwise:
deckard validate
deck 6 slides: 4 inline, 2 discovered
3 slugs, 3 numbered
canvas 1920x1080, margin 0
theme meridian (.meridian-theme): light and dark, default system
57 tokens in the light block, 25 dark overrides, from @thebuilder/deckard-themes/meridian
deckard validate passed
Two slides that resolve to the same route, for instance:
deckard validate
deck FAILED
Slides 4 and 5 both use the slug "keyboard". Slide ids must be unique.
Raised while loading deck/deck.ts, which resolves deck/slides.tsx and every deck/slides/*.slide.tsx module.
deckard validate found 1 problem
Only a repository that publishes blocks needs --registry. It is how this repository checks its own:
deckard validate --registry=../../registry.json
The report adds a registry row to the deck and theme checks. It names missing source files and duplicate registry targets before returning a failure status.
deckard doctor
deckard doctor
Takes no flags at all. Checks the shape of the app rather than the content of the deck:
| Check | Passes when |
|---|---|
node |
Node is at least 20.9.0. |
core |
@thebuilder/deckard-core/package.json resolves from the project. |
tooling |
playwright and pdf-lib resolve from the project. The capture commands load them from the deck. |
styles |
app/globals.css contains @import "@thebuilder/deckard-core/styles.css". |
routes |
Each of the four route files re-exports its adapter. |
deck |
deck/deck.ts loads. |
deckard doctor
ok node node 24.14.0
ok core @thebuilder/deckard-core 0.0.1
ok tooling playwright 1.62.1, pdf-lib 1.17.1
ok styles app/globals.css imports the slide token contract
ok routes 4 route files re-export their adapter
ok deck deck/deck.ts loads 5 slides for "My talk"
deckard doctor found nothing wrong
Each failure prints what to do about it, and the run keeps going, so one run shows all of them. Exits 1 if anything failed.
deckard doctor
ok node node 24.14.0
ok core @thebuilder/deckard-core 0.0.1
ok tooling playwright 1.62.1, pdf-lib 1.17.1
FAIL styles app/globals.css never imports @import "@thebuilder/deckard-core/styles.css"
Add @import "@thebuilder/deckard-core/styles.css"; to app/globals.css. It carries the --slide-* tokens and registers the runtime's own Tailwind source.
ok routes 4 route files re-export their adapter
ok deck deck/deck.ts loads 5 slides for "My talk"
deckard doctor found 1 problem
Reach for doctor when a deck that used to work stops, and for validate when the slides changed.
deckard check-overflow
deckard check-overflow --light
| Flag | Value | Default |
|---|---|---|
--light |
boolean | off, so dark |
--skip-build |
boolean | off |
--port <n> |
positive integer | 3412 |
Builds the deck, serves it, and opens every slide, then runs three measurements over it with a 1px tolerance, in canvas pixels:
- The canvas edge. The slide frame, the header, and the footer against the 1920x1080 canvas.
- The chrome band. Nothing inside
[data-slide-frame]may cross the top of[data-slide-footer]or the bottom of[data-slide-header]. Absolutely positioned parts are skipped, and so is a slide whoselayoutis"fullscreen", which is handed the whole canvas with the chrome out of the way. - Clipping. Nothing inside the frame may hide its own overflow: a box with a non-visible
overflowwhose content is taller or wider than it is has lost that content silently.
Anything inside a SlideScrollArea is exempt from the last two, because outgrowing its box is what a scroll area is for.
deckard check-overflow
5 slides fit the 1920x1080 canvas in dark mode, chrome included.
Exits 1 listing every slide that loses content, naming the part by its data-slide-* or data-stat-* attribute, which band or which axis, and by how many pixels. A container and the rows it drags with it are one finding, reported against the outermost part.
deckard check-overflow
/slides/roadmap slide content runs 794px below the canvas
1 of 6 slides overflow the canvas and are clipped. Trim the content, or wrap the part that has to scroll in SlideScrollArea.
next dev takes the same measurement after every render, rings the canvas in amber, and logs the same sentence:
Slide content runs 794px below the canvas and is clipped.
The ring is development only. Both run measureSlideLayout from @thebuilder/deckard-core, so this gate and next dev cannot disagree.
deckard screenshots
deckard screenshots --max 5
| Flag | Value | Default |
|---|---|---|
--light |
boolean | off, so dark |
--max <n> |
positive integer | all slides |
--skip-build |
boolean | off |
--port <n> |
positive integer | 3411 |
Writes out/screenshots/<id>.png per slide plus out/screenshots/manifest.json. Wipes the directory first.
deckard screenshots
Captured intro.png
Captured 2.png
Captured 3.png
Captured keyboard.png
Captured 5.png
5 slides at 1920x1080 in ~/my-talk/out/screenshots
deckard contact-sheet
deckard contact-sheet --columns 5
| Flag | Value | Default |
|---|---|---|
--columns <n> |
positive integer | 4 |
Reads out/screenshots/manifest.json and composes out/contact-sheet.png. It builds and serves nothing, so run deckard screenshots first, and the sheet comes out in whichever color mode those were captured in.
deckard contact-sheet
Contact sheet with 5 slides written to ~/my-talk/out/contact-sheet.png
--port, --skip-build and --light belong to the commands that build and serve. This one takes none of them and rejects them by name.
deckard export pdf
deckard export pdf --dark
The positional must be exactly pdf. Anything else, including nothing, is an error.
| Flag | Value | Default |
|---|---|---|
--dark |
boolean | off, so light |
--skip-build |
boolean | off |
--port <n> |
positive integer | PDF_EXPORT_PORT, else 3410 |
The checks default to dark and the PDF defaults to light, so pass --dark for a PDF meant for a screen. Writes out/slides.pdf, or wherever PDF_EXPORT_OUTPUT points, one slide to a page and sized from the canvas.
deckard export pdf
Exported slide: intro
Exported slide: 2
Exported slide: 3
Exported slide: keyboard
Exported slide: 5
PDF written to ~/my-talk/out/slides.pdf
deckard add theme
deckard add theme ledger
A pure source edit, with no install. Rewrites the @thebuilder/deckard-themes import and the theme property in defineDeck, keeping import order (package imports, then @/ imports, alphabetical within each). It no-ops when the deck is already on that theme, rejects a name that is not a built-in, and warns when a stale deck/theme directory is still sitting there.
deckard add theme ledger
deck/deck.ts now uses the ledger theme.
A name that is not a built-in comes back with the ones that are:
deckard add theme brutalist
"brutalist" is not a Deckard theme. The built-ins are atelier, aurora, blueprint, cotton, ledger, meridian, nexus, noir, phosphor, quorum.
--registry and --yes are accepted on this path and do nothing.
deckard add block
deckard add block metrics
deckard add block metrics --registry https://example.com/r/{name}.json --yes
| Flag | Value | Default |
|---|---|---|
--registry <url> |
a URL carrying a {name} placeholder |
components.json -> registries["@deckard"] |
--yes |
boolean | off |
Builds the item id as block-<name>, probes the registry with a 5-second timeout, and then runs the shadcn binary installed alongside the CLI as shadcn add <target>. --yes adds -y -o, overwriting without asking. The block families in app/slides/blocks arrive with init and are yours to edit, so this writes over the copy you have, skipping a file that already matches.
deckard add block metrics
Installing @deckard/block-metrics from https://deckard.thebuilder.dk/r/block-metrics.json
✔ Checking registry.
✔ Created 1 file:
- app/slides/blocks/metrics.tsx
With no --registry and no registries["@deckard"] in components.json, it errors naming the shape it expects:
deckard add block metrics
No @deckard registry in components.json and no --registry flag.
Add one: { "registries": { "@deckard": "https://deckard.thebuilder.dk/r/{name}.json" } }
The URL init writes points at the Deckard docs site, which serves every registry item at /r/{name}.json. Point --registry at another URL with a {name} placeholder to install from your own.
deckard eject theme
deckard eject theme
deckard eject theme --theme meridian
| Flag | Value | Default |
|---|---|---|
--theme <name> |
a built-in theme name | the theme deck/deck.ts imports |
Creates deck/theme/ with theme.css and THEME.md copied out of the installed @thebuilder/deckard-themes, generates an index.ts exporting a SlideTheme literal, and rewrites deck/deck.ts to import { theme } from "@/deck/theme".
deckard eject theme
Ejected the ledger theme into deck/theme/
deck/theme/theme.css
deck/theme/index.ts
deck/theme/THEME.md
deck/deck.ts now imports it from "@/deck/theme". The three files are yours: read THEME.md, then edit theme.css.
validate reads whichever stylesheet the deck renders, so afterwards it names your copy rather than the package:
deckard validate
deck 5 slides: 4 inline, 1 discovered
2 slugs, 3 numbered
canvas 1920x1080, margin 0
theme ledger (.ledger-theme): light and dark, default system
57 tokens in the light block, 25 dark overrides, from deck/theme/theme.css
deckard validate passed
It refuses when deck/theme/ already exists, when the deck already imports from @/deck/theme and no --theme was given, and when the loaded deck’s theme.id disagrees with the name you asked for.
Environment variables
| Variable | Read by | What it does |
|---|---|---|
PDF_EXPORT_OUTPUT |
export pdf |
Output path, resolved against the deck directory. Default out/slides.pdf. |
PDF_EXPORT_PORT |
export pdf |
Default port when --port is absent. |
npm_config_user_agent |
init |
Package-manager detection. |
NEXT_PUBLIC_PDF_EXPORT and NEXT_PUBLIC_PDF_THEME are set by the export build, not read by the CLI.
Exit codes
| Code | When |
|---|---|
0 |
Success. |
1 |
Any error, including validate and doctor finding problems, and bin/deckard.mjs running before dist/ exists. |
130 |
Interrupted with Ctrl-C during a build-and-serve session. |
The build harness
check-overflow, screenshots, and export pdf share one harness, so Next’s own build output scrolls past before the lines those three print:
Build, unless it is fresh
A stamp at .next/deckard-build.json is compared against the newest file
under app/, assets/, components/, deck/, hooks/, lib/,
public/, plus components.json, next.config.*, package.json,
postcss.config.*, tsconfig.json, and the resolved @thebuilder/deckard-core and
its src/. --skip-build skips the comparison entirely.
Refuse an occupied port
Anything already answering on the port is an error telling you to pass
--port.
Serve and wait
next start with NODE_ENV=production, polled every 500ms for up to 60
seconds.
Read the slide list
Slide ids come from /sitemap.xml, which is app/sitemap.ts. An empty list
is an error naming that file.
Open at scale 1
Headless Chromium with the requested colorScheme, then the viewport is
resized to the canvas size read off the rendered page, so nothing is
captured scaled. Every slide asserts the applied color mode matches the one
requested.
A Chromium launch failure prints the playwright install chromium command for your package manager.