Skip to content
Deckard
Esc
navigateopen⌘Jpreview
On this page

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 value and --flag=value alike. A missing value, or a next token starting with --, is an error.
  • Boolean flags never consume the next token. --light is true; --light true sets the flag and leaves true as a stray positional.
  • --no-<name> sets a declared boolean to false. 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 sourcePath a 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 colorModes matches the blocks that stylesheet carries. It reads deck/theme/theme.css for an ejected theme and the copy inside the installed package for an imported one, and names which it read.
  • registry, only with --registry: every items[].files[].path exists, 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 whose layout is "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 overflow whose 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.

Was this page helpful?