CLI
Create a project with bunx @hajimism/dek init, then install dek into it with bun add -d @hajimism/dek; see Getting Started. The examples below write dekc for what is really bunx dekc inside that project, or bunx @hajimism/dek outside one. Type dekc, not dek: bunx dek runs dek from npm, an unrelated package.
Conventions
- Scope follows the working directory. The project root means every deck; a deck directory means that deck. From anywhere, name a deck as the first argument or with
--deck <name>:dekc lint why-dek,dekc show why-dek intro,dek why-dek. Details in Projects and Decks. - Result commands accept
--json, with one envelope. Success is{ "ok": true, ... }. Failure is{ "ok": false, "error": { "message", "hint", "path", "line" }, ... }with exit code 1, both when a command cannot run and whenlintorcheckfinds an error;hintis there whenever dek knows the next step,pathandlinewhen the failure has a place. Commands that report diagnostics (lint,check,build,ls,cues) carrydiagnosticspass or fail, so a reader branches onok, readserror, and then the diagnostics.dekcanddekc rehearsestay running and do not take--json. See The JSON contract. - Each command's shape is a JSON Schema, published at
cli.schema.jsonwith one entry under$defsper command. - The shape is not frozen yet. While dek is 0.x, the
--jsonshape and the rule ids may change between releases. A rule id is never reused for another rule. - Diagnostics are SARIF with
dekc lint --format sarif:file://URIs, line and column, the hint, slug, and data inproperties, and skipped checks as tool execution notifications. The default is ESLint-style text,path:line:column: id message. - Only errors fail. Each diagnostic has a
severity.lintandcheckexit 1 with"ok": falsewhen an error remains; warnings alone exit 0. initandsyncnever overwrite your work. They create what is missing and warn about what is left over. The only slides sync rewrites or removes are skeletons nobody has edited: rewritten when the script they came from changed, removed when their section is gone and no stylesheet or script sits beside them. InAGENTS.md, dek owns only the block between<!-- dek:begin … -->and<!-- dek:end -->. Neither renames.- Every error carries a hint naming the next command to run. Diagnostics carry one too when the fix is known: the beat ids a
data-stepmay use, the classes a slide may use, theassets/path for a remote image. - Paths into the source tree are relative to the working directory in text and
--jsonoutput: diagnostics, errors, and the filesinit,new, andsynccreate. Artifacts dek writes, such as a screenshot or a build, stay absolute. SARIF keeps absolute URIs. - A check that did not run is listed in
"skipped", each entry with thecheck, thereason, and ahintwhen there is a way to run it:dekc checkwithout Playwright skipsvisual,dekc check --voiceon a deck withoutvoice/skipsvoice,dekc lintwithout rumdl skipsrumdl,dekc buildwithout a URL or Playwright skipspreview(the link preview image), anddekc lson a ref skipslint. Text output prints<check>: skipped (<reason>)and the hint ashelp:;lint,check, andbuildprint them on stderr, so stdout holds only the result. The field is absent when every check ran. - Commands that write one file per deck return a list.
buildandpdfreturnouts, one path per deck in scope, one deck included. - A ref is read, never written.
ls,show,theme, andshottake a ref name (owner/repo/deck) where they take a deck; every other command refuses one. See Refs. - Help is per command.
dekc help <command>,dekc <command> --help, and-hprint its usage, what it does, and every flag it takes.dekc --version(-v) prints the version. A mistyped command or deck name getsdid you mean …?. - Flags are checked per command.
--json,--help, and--versiongo anywhere; every other flag only where its command takes it, and--deckeverywhere butnew,ref, andhelp. A misspelled flag, or one from another command, is an error whose hint lists the flags the command does take and namesdekc help <command>, not a silent no-op. Values are checked before anything runs:--porttakes a whole number from 1 to 65535,--fpsa number above 0,--accenta whole number from 0, and--formatonlysarif; anything else, an empty value included, fails asinvalid --<flag> "<value>"with what the flag takes. - Arguments are checked per command, too. A missing argument fails as
missing <slug> for dekc show, and one past what the command takes asunexpected argument "extra" for dekc ls, each with the command's usage anddekc help <command>as the hint. A mistyped subcommand getsdid you mean …?:dekc voice speakrsuggestsdekc voice speakers.
Development
| Command | Purpose |
|---|---|
dekc [deck] [--visual] [--port N] [--remote] | Start the dev server: sync on start and on every save, live reload, lint on save, presenter view. It answers only at 127.0.0.1 or localhost, and refuses a move (a WebSocket, goto) from another origin. --visual adds overflow and contrast on save. --port fixes the port; without it the OS picks a free one. It stops on Ctrl-C, or when the process that started it exits, so no server is left holding a port. |
dekc --remote | Serve on the LAN. The presenter view, goto, current, the voice timeline and audio, and the streamed diagnostics require the 10-letter password dek makes and prints on each start. On a terminal, a QR code under it opens the presenter view on a phone: once, within five minutes; Enter prints a new one. |
dekc rehearse [deck] [slug] [--remote] | Auto-advance from the Timeline. Records nothing. --remote serves it on the LAN as dekc --remote does. |
Project
| Command | Purpose |
|---|---|
dekc init [dir] [--deck NAME] | Create a project in dir (default: the current directory), optionally with a first deck, and print the commands to run next (next in --json). Writes dek.toml, theme.css, .gitignore, .rumdl.toml, tsconfig.json, assets/, decks/, AGENTS.md, .dek/schema.json, and .dek/slide.d.ts. A first deck comes with a short starter script and its skeleton slides, so it passes lint as created; once you replace the script with your own, dekc sync removes the example skeletons you left untouched. Nothing that exists is overwritten: a file already there is kept, and listed as kept when it differs (created and kept in --json), except dek's own files, which are brought up to date and listed as updated (updated); every input is checked before the first write. An AGENTS.md already there keeps what you wrote and gets dek's block added, as sync does. A theme.css already there is kept too, and every deck copies it: when it lacks tokens every deck needs, init names them on stderr (missingTokens in --json), before the first lint reports them as DEK015, whose hint gives the value dek's own theme uses. Refuses to run inside another project; add a deck with dekc new there. The next commands include bun add -d @hajimism/dek until the project has dek installed. |
dekc new <name> [--theme-from DECK] | Add a deck. Copies the project theme.css, or the named deck's, and creates its skeleton slides, so it passes lint as created. Refreshes dek's own files as sync does, and lists each one it brought up to date (updated in --json). Prints the commands to run next (next in --json). |
dekc ls [deck] | List decks, or summarize one: sections, slides, diagnostics, budget, estimate, and narrated length when a Timeline exists. Diagnostics are dek's own rules, without rumdl. The list counts each deck's findings on its row and the project's (dek.toml) once, on a project line and in the top-level diagnostics of --json. |
Refs
A ref is someone else's deck, pinned in dek.toml [refs] to read as a model. Its snapshot under refs/ is gitignored; whenever it is missing or at another commit, the next read fetches the pinned commit again. Public repositories need no token. For a private one, set GITHUB_TOKEN (or GH_TOKEN) to a token that can read it, such as GITHUB_TOKEN=$(gh auth token) dekc ref …. dek sends it only to api.github.com, and never asks gh for one itself.
| Command | Purpose |
|---|---|
dekc ref <owner/repo/deck>[@rev] | Pin a ref to the default branch, or to the tag, branch, or sha after @, and fetch its snapshot. A GitHub link to the deck's folder works too. Run it again to move the pin to the latest; --json returns rev, from, and changed. Warns when the repository has no license. |
dekc ref | List the pinned refs: rev, title, slides, and which are not fetched. |
dekc ref rm <ref> | Drop the pin and the snapshot. |
dekc ls <ref> | Summarize a ref, timed at its own speaking rate. Lists lint in skipped: a ref is not yours to fix. |
dekc show <ref> <slug> | The same bundle as for a slide of your own, plus ref: name, rev, snapshot directory, and license file. |
dekc theme <ref>, dekc shot <ref> <slug> | The ref's theme, and a screenshot of one of its slides. |
Slide
| Command | Purpose |
|---|---|
dekc show [deck] <slug> | Print everything one slide is made of, each part labeled with its file: the section as script.md has it, beat headings and their {#id}s included, slides/<slug>.html, .css, and .ts, the rules of the deck's theme.css the slide uses (with only the tokens and keyframes they reach), and the assets it references. A rule is the slide's when its classes, its layout, and the elements and attributes it needs are in the slide's markup, so a slide with no list and no data-step gets no .slide ul and no beat rules; with slides/<slug>.ts, whose draw may add elements, every element and attribute rule under .slide stays. A missing file is null. A file that is a symlink out of the project is an error, as it is for dekc build. With --json, beats lists the section's beats in order, each with its title, line, and id when it has one: what a data-step names. |
dekc theme [deck] [layout] | List the layouts, classes, and tokens the deck's own theme.css defines. The classes are the ones markup may use and max_classes counts; is-current and is-shown, which the player sets, are listed apart as state classes (stateClasses in --json), to select in CSS and never to write in markup (DEK010). The tokens are the ones the bare .slide rule sets, which every slide can var(). With a layout, print its example markup, ready to paste into slides/<id>.html. |
dekc check [deck] <slug> [--shot] [--voice] | Lint one slide: the findings with that slide's scope, including rendering rules when Playwright is available. Findings about the deck (theme.css, the frontmatter, the length) or the project are dekc lint's; see Scope. --shot writes a screenshot and returns its path. With Playwright, fill says how much of the frame the slide fills at its last beat: coverage, the share the content covers; box, the smallest box holding it; and rows and columns, the share of each tenth of the frame top to bottom and left to right, all from 0 to 1. It counts what the audience reads or looks at: each text's lines, each picture (an image, an SVG, a video, a canvas, a url() background image), and each painted box that holds nothing, as a chart's bar or a swatch. A painted box that holds something, a card or a window, counts by what it holds, so one whose words sit in its top half leaves the rest empty. Decoration under aria-hidden counts too, since the audience sees it take its place; ::before and ::after text and what the beat has not shown yet count for nothing. Judge a slide by rows, columns, and box, where an empty band is a run of zeros, not by coverage alone: a chapter door in large type and a dense slide can share it. The text output names each empty band as a margin at an edge or a gap between content. --voice returns kana and durations; on a deck without voice/ it lists voice in skipped with how to set it up, and the rest of the check still runs. |
dekc shot [deck] [slug] [--step ID|N] | Screenshot one slide, or every slide, at the last beat by default; --step 0 is the slide as it arrives, before any beat. Files are .cache/shots/<slug>~<step>.<hash>.png, where <step> is the beat id or number shot (the last beat by default), so --step hook, --step 1, and the default still share one file when they name the same beat; the hash is of the rendered content, so a changed theme or slide yields a new path and the stale image is removed. An unchanged slide keeps its image and is not shot again. |
dekc shot [deck] --sheet | Every slide at its last beat, tiled on contact sheets at .cache/shots/sheets/<hash>/sheet-<n>.png. The tiles are as large as fits the whole deck on one image of at most 1568 px a side and 1.15 megapixels, which vision models read without scaling it down; a longer deck takes more sheets. The tiles are the slides' own shots, so an unchanged deck starts no browser, and --json lists each shot too. |
dekc shot [deck] <slug> --motion [--step ID|N] | A row for the slide's arrival and one per beat, each played from the beat or slide before it and held at 0, 25, 50, and 75% of everything that move starts, then at its end, the still dekc shot takes. Written to .cache/shots/motion/<slug>.<hash>/: the sheets as sheet-<n>.png, the frames under frames/. With --json, motion lists each beat in order, its step and its frames, each with its path and ms and end: true on the settled one, and sheets the sheets; shots is empty. --step plays that beat alone. |
dekc shot [deck] <a> --to <b> [--at 0..1] | One frame of the View Transition from the last beat of a into b, frozen at --at (default 0.5) of the transition's length; an entrance on b may still be running at --at 1, which --motion shows. Written to .cache/shots/<a>~<b>~<at>.<hash>.png. Does not combine with --step. |
dekc mv [deck] <old> <new> | Rename a section id, its HTML file and data-slug, its .css and .ts if present, and its keys in voice/voice.toml. Heading text is untouched. Refuses if any destination exists; all files change or none do. |
dekc mv [deck] <slug> --before|--after <slug> | Reorder a section in script.md. |
dekc goto [deck] <slug> | Jump the open browser. Requires a running dev server. viewers in --json counts the pages that show the deck; with none, the position is where the next page opened lands, and dek says so on stderr. |
dekc current [deck] | Print the slide on screen. Requires a running dev server. viewers counts the pages that show it; with none, nothing is on screen, and the slide printed is where the next page lands, as stderr says. |
dekc marks [deck] | List the beats marked to rewrite while rehearsing: press m in the presenter view, or the mark button in its bar, on a beat you stumble over. Each mark gives the heading's line in script.md, was (the beat's words when it was marked), text (its words now), and status: open, edited once the words changed, or gone once no beat by that name is left. A mark follows its beat by the beat's id, or its title, so beats added before it do not move it. Marks are kept in .dek/marks.json; marking needs the dev server. |
dekc marks [deck] clear | Drop the deck's marks, once they are dealt with. cleared in --json counts them. |
dekc annotations [deck] | List the notes written on elements of the slides in annotate mode: press a on the dev server's page, or the annotate button in the presenter bar, then click what should change. Each note gives its number, as the page's markers show it, the step it was written at, the human's text, the shot command that shows that beat, and its targets: for each element the path, line, and column of its start tag in the file now, its name (the tag and its classes as written there), was (its text when the note was written), text (its text now), its box on the slide when noted, and whether it was found. status is open while the slide's own slides/<id>.html, .css, and .ts are as they were, edited once one of them changed (changed names which), or gone once none of the note's elements is on the slide or the slide left the script. edited says the slide changed, not that the note was dealt with: compare shot with the note. Each element is matched again as the file is now: at the same place with the same name and text, else the only one with that name and text, else at the same place with the same name, else at the same place with the same tag, its classes changed. Where it was found is written back, so a note follows its element from one edit to the next. Notes are kept in .dek/annotations.json; reading them needs no dev server, writing them needs the page. |
dekc annotations [deck] clear | Drop the deck's annotations, once the human has seen them dealt with. cleared in --json counts them. |
dekc sync [deck] | Create missing skeleton slides, refresh skeletons nobody has edited since, remove untouched skeletons whose section is gone (unless a slides/<id>.css or .ts sits beside them), and refresh dek's own files: dek's block in AGENTS.md, .dek/schema.json, and .dek/slide.d.ts. These follow the installed dek and the project's refs, not any deck, so they change only after either does, and the first command to run then, whichever it is, lists them as updated. A .gitignore that does not ignore the files dek keeps for itself, .dek/server.json, .dek/marks.json, and .dek/annotations.json, gets them added at its end and is listed as updated; a project with no .gitignore is left without one. Never touches a slide you edited, what you wrote in AGENTS.md outside dek's block, or tsconfig.json. A slide whose section is gone but that holds your edits is kept and named on stderr with what to do, and lint reports it as DEK002. --json returns created, updated, removed, and kept. |
Output
| Command | Purpose |
|---|---|
dekc lint [deck] [--fix] [--visual] [--format sarif] | Lint. --fix runs dekc sync first. --visual adds overflow and contrast. |
dekc cues [deck] | Print the spoken cues as Cue[]. Paragraphs only. No engine needed. |
dekc voice [deck] | Synthesize changed sentences into .cache/voice/. Also runs on save. |
dekc voice [deck] speakers | List the engine's speakers. |
dekc voice [deck] say <text> | Speak one sentence. |
dekc voice [deck] dict add <word> <kana> [--accent N] | Add a reading to voice/dict.toml, with the accent position when given. |
dekc voice [deck] pin | Copy the master audio and timeline.json into voice/pin/. |
dekc build [deck] [--root-dist] [--url <url>] [--public] | Write one HTML file to decks/<deck>/dist/<deck>.html, or <root>/dist/<deck>.html with --root-dist. Slide stylesheets and scripts are inlined. Lint never stops a build: a section with no HTML is built from its skeleton, a slide script that cannot run (DEK016) is left out and its slide built without motion, and when dek's rules find something (rumdl is dekc lint's alone), the output says how many and --json includes the diagnostics. The page carries link preview tags; given the URL dist/ is served from (url in dek.toml, or --url, which wins), the first slide also becomes dist/<deck>.png for og:image. See On the web. --public builds a page for anyone who has the link: its presenter view leaves out the script's stage directions and HTML comments. Without it, the presenter view carries the whole script, a URL or not. |
dekc video [deck] [slug] [--fps N] [--root-dist] | Bake dist/<deck>.mp4 with .vtt, .chapters.txt, and .credits.txt, from the Timeline dekc voice writes. One slide goes to .cache/video/<slug>.mp4. --fps defaults to 30. |
dekc pptx [deck] [--root-dist] [--public] | Write dist/<deck>.pptx: one slide per ## at its last beat, a picture of it drawn at twice its size with its text laid over it in text boxes, a line each, to edit, and its script in the notes. --public leaves the stage directions and comments out of the notes, as build --public does. Each box names the font Chromium drew it with; no font is embedded. SVG, aria-hidden decoration, pseudo-element text, rotated text, and shadows stay in the picture. A slide script that cannot run stops it, as it stops dekc pdf. --json returns outs. Requires Playwright. |
dekc pdf [deck] [--root-dist] | Write dist/<deck>.pdf with every slide at its last beat. A slide script that cannot run stops it, as it stops dekc video: neither has findings to carry the news, and each would show the slide as if its script never ran. |
Help
| Command | Purpose |
|---|---|
dekc help [command] [--agent] | Help, or one command's usage and flags. --agent is the compact reference for agents. A word that is no command fails as dekc <word> does, with did you mean …?. |
Environment variables
dek reads its DEK_ variables only from the environment it starts in, never from a project's files. dekc runs Bun with --no-env-file --config=/dev/null, as does every bun dek starts, so neither a .env nor a bunfig.toml in the working directory applies. When Bun loads .env files anyway, as bun ./node_modules/.bin/dekc does, dek ignores every DEK_ variable, and NODE_TLS_REJECT_UNAUTHORIZED, that .env, .env.local, or the .env.<mode> and .env.<mode>.local files for development, production, and test name, and says so. That launch still reads bunfig.toml; see A project you did not write.
| Variable | Purpose |
|---|---|
DEK_FFMPEG | Path to the ffmpeg binary that dekc video muxes with, instead of ffmpeg on PATH. |
DEK_GITHUB_API | Base URL of the GitHub API that dekc ref fetches from. No token is sent to it. |
DEK_PLAYWRIGHT | Path to an alternative Playwright worker script. |
GITHUB_TOKEN | Token dekc ref sends to api.github.com, for private repositories and a higher rate limit. |
GH_TOKEN | Read as GITHUB_TOKEN when that is unset. |
DEK_RUMDL | Path to the rumdl binary, ahead of PATH and the node_modules/.bin beside dek's own install. |
DEK_VIDEO | Path to an alternative video capture worker, used instead of the Playwright worker. |
DEK_VOICE_PLAY | Set to 0 to keep dekc voice say from playing the audio it synthesized. |
DEK_VOICE_URL | Base URL of the speech engine, overriding voice.toml. |
NO_COLOR | Set to turn colored output off, ahead of FORCE_COLOR and CI. |
FORCE_COLOR | Set to color output even when it is not a terminal. |
CI | Set, as CI services do, to turn colored output off unless NO_COLOR or FORCE_COLOR decides first. |