@hajimism/dek 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.ja.md +3 -1
  3. package/README.md +3 -1
  4. package/package.json +4 -1
  5. package/skills/dek/SKILL.md +44 -0
  6. package/src/cli/annotations.ts +26 -0
  7. package/src/cli/commands.ts +66 -0
  8. package/src/cli/contract.ts +75 -4
  9. package/src/cli/env-file.ts +78 -0
  10. package/src/cli/files.ts +3 -3
  11. package/src/cli/flags.ts +1 -1
  12. package/src/cli/format.ts +3 -0
  13. package/src/cli/pptx.ts +20 -0
  14. package/src/cli/show.ts +17 -2
  15. package/src/cli/text.ts +43 -0
  16. package/src/cli/theme.ts +7 -1
  17. package/src/cli.ts +5 -1
  18. package/src/core/annotation-rows.ts +176 -0
  19. package/src/core/annotations.ts +362 -0
  20. package/src/core/diagnostic.ts +1 -0
  21. package/src/core/github.ts +27 -24
  22. package/src/core/html-scan.ts +54 -0
  23. package/src/core/html.ts +53 -8
  24. package/src/core/lint/slide-html.ts +92 -2
  25. package/src/core/lint.ts +8 -1
  26. package/src/core/live-protocol.ts +4 -1
  27. package/src/core/page.ts +6 -0
  28. package/src/core/playwright-module.d.ts +7 -3
  29. package/src/core/playwright-visual.ts +76 -1
  30. package/src/core/playwright.ts +65 -2
  31. package/src/core/pptx-package.ts +312 -0
  32. package/src/core/pptx-text.ts +363 -0
  33. package/src/core/pptx.ts +75 -0
  34. package/src/core/slide-measure.ts +1 -0
  35. package/src/core/slide-script-eval.ts +2 -1
  36. package/src/core/spawn.ts +8 -1
  37. package/src/core/sync.ts +33 -6
  38. package/src/core/theme-excerpt.ts +61 -9
  39. package/src/core/theme-facts.ts +12 -2
  40. package/src/core/zip.ts +77 -0
  41. package/src/runtime/annotate-pick.ts +2 -6
  42. package/src/runtime/annotate-view.ts +115 -101
  43. package/src/runtime/annotate.ts +5 -1
  44. package/src/runtime/browser.ts +5 -1
  45. package/src/runtime/live.ts +1 -0
  46. package/src/runtime/routes.ts +3 -0
  47. package/src/server/auth.ts +3 -1
  48. package/src/server/routes.ts +68 -1
  49. package/src/video/worker.ts +2 -0
  50. package/tsconfig.json +26 -0
  51. package/src/runtime/annotate-notes.ts +0 -207
package/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  Release notes for `@hajimism/dek`. From 0.1.0 on, release-please writes this file from the Conventional Commits on `main`.
4
4
 
5
+ ## [0.3.0](https://github.com/hajimism/dek/compare/v0.2.2...v0.3.0) (2026-10-07)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * a DEK_ variable kept in a project's .env, such as DEK_VOICE_URL, no longer applies; set it in the shell, or use `engine` in voice.toml.
11
+ * reading a private ref needs GITHUB_TOKEN or GH_TOKEN; being signed in to gh is not enough, and DEK_GH is gone. `GITHUB_TOKEN=$(gh auth token) dekc ref ...` does what dek did before. No token is sent to DEK_GITHUB_API.
12
+
13
+ ### Bug Fixes
14
+
15
+ * draw pages in Chromium's sandbox with the network off ([550cc3a](https://github.com/hajimism/dek/commit/550cc3ab1c44005d4ef89420d3bb92a44a7da3a9))
16
+ * ignore what the .env.*.local files set, and NODE_TLS_REJECT_UNAUTHORIZED from any .env ([da02b7c](https://github.com/hajimism/dek/commit/da02b7c4375b41ddd9ea2d879714950e5017cbc2))
17
+ * keep a ref's title to one line of AGENTS.md ([c1b0b60](https://github.com/hajimism/dek/commit/c1b0b609a8710985c32988cff19b8cb65a0e6bc6))
18
+ * run dekc, and every bun it starts, without the working directory's bunfig.toml ([0418909](https://github.com/hajimism/dek/commit/04189098b6975ba37506f039e865388205c5817b))
19
+ * ship tsconfig.json, so a project's paths cannot take over dek's imports ([3eed8f0](https://github.com/hajimism/dek/commit/3eed8f0961a232a48be1cb06cd08dc03e121d026))
20
+ * stop asking gh for a token, and send one only to api.github.com ([62c896b](https://github.com/hajimism/dek/commit/62c896ba1b69424e5426534040a087a2d42bef1a))
21
+ * take DEK_ variables only from the environment, never from a .env ([7f4629b](https://github.com/hajimism/dek/commit/7f4629b3099e23a4c46b6261fa95627659fbfe51))
22
+
23
+
24
+ ### Documentation
25
+
26
+ * **adr:** number the GHSA-mj6q-67q7-q923 decision after the ones on main ([4a888b0](https://github.com/hajimism/dek/commit/4a888b06594c188bf0efa88fb9111f48f70263e9))
27
+ * **adr:** take GitHub tokens and DEK_ variables only from what the user sets ([c07fa4c](https://github.com/hajimism/dek/commit/c07fa4ceb7e1df69fb184ee8955f1819ee58a069))
28
+ * record the ref title and shot fixes in ADR 12 and the architecture guide ([a6bfb93](https://github.com/hajimism/dek/commit/a6bfb9383c9c614c5c39f0e2f6817db24dc83e06))
29
+ * say what a project cannot set for the Bun that runs dek, and what stays out of scope ([632efbb](https://github.com/hajimism/dek/commit/632efbbe4697a2175bf9cd0c8b5db77e53dbfa17))
30
+
31
+ ## [0.2.2](https://github.com/hajimism/dek/compare/v0.2.1...v0.2.2) (2026-10-06)
32
+
33
+
34
+ ### Features
35
+
36
+ * **agents:** generate llms.txt and ship the official dek skill ([79d8abe](https://github.com/hajimism/dek/commit/79d8abea5d3c8822a865e2e58b687ec898e9f729))
37
+ * **agents:** generate llms.txt and ship the official dek skill ([b9b66ba](https://github.com/hajimism/dek/commit/b9b66ba43b556e35738a66ed492ad699556573ce))
38
+ * **dev:** keep annotations in .dek/ and list them with dekc annotations ([e22671f](https://github.com/hajimism/dek/commit/e22671f2816b517c62b827ca111aadd2edc02dfe))
39
+ * **dev:** keep annotations in .dek/ and list them with dekc annotations ([10195e3](https://github.com/hajimism/dek/commit/10195e3060d5754d36e91f5276b9964ed620ccb8))
40
+ * **lint:** keep runtime state classes out of a theme's vocabulary and slide markup ([83fd36c](https://github.com/hajimism/dek/commit/83fd36c8d2312c709d939bdc2e344d906b13ff82))
41
+ * **lint:** require every picture to say what it shows (DEK034) ([95ebbc2](https://github.com/hajimism/dek/commit/95ebbc2830b67a13288709d593b7d39c006ea806))
42
+ * **lint:** require every picture to say what it shows (DEK034) ([9926f4b](https://github.com/hajimism/dek/commit/9926f4b22c3d9be1abf6fd8681fdc101383c11b3))
43
+ * **pptx:** export a talk as PPTX with editable text over each slide ([12c2ec2](https://github.com/hajimism/dek/commit/12c2ec21b67e0b713e326d9300eff787a5a8ed16))
44
+ * **pptx:** export a talk as PPTX with editable text over each slide ([46d5468](https://github.com/hajimism/dek/commit/46d54686e5ce60d4718015a964791401a2345b3b))
45
+
46
+
47
+ ### Bug Fixes
48
+
49
+ * **agents:** write the agent guidance as what the checks and commands do ([97447ca](https://github.com/hajimism/dek/commit/97447cabf543c66cc6231b9b0e5a2378d7060615))
50
+
5
51
  ## [0.2.1](https://github.com/hajimism/dek/compare/v0.2.0...v0.2.1) (2026-10-06)
6
52
 
7
53
 
package/README.ja.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  [ドキュメント](https://hajimism.github.io/dek/ja/) · [English README](./README.md)
8
8
 
9
- dek は、ビルドシステムがコードを扱うように発表を扱う CLI です。ソースは Markdown で書くトークの台本で、`##` の見出しひとつが 1 枚の HTML スライドになります。同じソースから、尺を見積もり、どのスライドも lint して描画を測り、1 ファイルの HTML、PDF、読み上げ付きの動画をビルドします。プロジェクトが複数のデッキを持つので、テーマも規約も前回のトークから次のトークへ引き継がれます。AI エージェントと一緒に作ることを、最初から前提に設計しています。
9
+ dek は、ビルドシステムがコードを扱うように発表を扱う CLI です。ソースは Markdown で書くトークの台本で、`##` の見出しひとつが 1 枚の HTML スライドになります。同じソースから、尺を見積もり、どのスライドも lint して描画を測り、1 ファイルの HTML、PDF、文字を編集できる PowerPoint ファイル、読み上げ付きの動画をビルドします。プロジェクトが複数のデッキを持つので、テーマも規約も前回のトークから次のトークへ引き継がれます。AI エージェントと一緒に作ることを、最初から前提に設計しています。
10
10
 
11
11
  ## なぜ dek か
12
12
 
@@ -44,6 +44,8 @@ bunx dekc build # 3. dist/2026-04-vite.html — トーク全体がこの 1
44
44
 
45
45
  台本を書いて `dekc` を叩けば、HTML を一行も書かずに同梱テーマで発表できます。
46
46
 
47
+ エージェントと作るなら、公式スキルを `npx skills add hajimism/dek` で、Claude Code なら `/plugin marketplace add hajimism/dek` と `/plugin install dek@dek` で入れてください。プロジェクトの外にいるエージェントは [llms.txt](https://hajimism.github.io/dek/llms.txt) から始められます。[AI エージェントと作る](https://hajimism.github.io/dek/ja/guide/ai.html)を参照してください。
48
+
47
49
  続きは [はじめる](https://hajimism.github.io/dek/ja/guide/getting-started.html)へ。通しで 1 本作るなら [チュートリアル](https://hajimism.github.io/dek/ja/guide/tutorial.html)。コマンドの一覧は [CLI リファレンス](https://hajimism.github.io/dek/ja/reference/cli.html)にあります。
48
50
 
49
51
  ## サンプル
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  [Documentation](https://hajimism.github.io/dek/) · [日本語 README](./README.ja.md)
8
8
 
9
- dek treats a talk the way a build system treats code. The source is your talk script, in Markdown; every `##` heading becomes one HTML slide, and from the same source dek estimates the timing, lints and measures every slide, and builds a single HTML file, a PDF, and a narrated video. A project holds many decks, so the theme and conventions carry forward from one talk to the next. It is designed from the start to be worked on together with AI agents.
9
+ dek treats a talk the way a build system treats code. The source is your talk script, in Markdown; every `##` heading becomes one HTML slide, and from the same source dek estimates the timing, lints and measures every slide, and builds a single HTML file, a PDF, a PowerPoint file with its text editable, and a narrated video. A project holds many decks, so the theme and conventions carry forward from one talk to the next. It is designed from the start to be worked on together with AI agents.
10
10
 
11
11
  ## Why
12
12
 
@@ -44,6 +44,8 @@ bunx dekc build # 3. dist/2026-04-vite.html — the whole talk in one file
44
44
 
45
45
  Write the script, run `dekc`, and you can present with the bundled theme without writing a line of HTML.
46
46
 
47
+ Working with an agent? Install the official skill with `npx skills add hajimism/dek`, or in Claude Code with `/plugin marketplace add hajimism/dek` and `/plugin install dek@dek`; an agent outside any project can start from [llms.txt](https://hajimism.github.io/dek/llms.txt). See [Working with AI Agents](https://hajimism.github.io/dek/guide/ai.html).
48
+
47
49
  Continue with [Getting Started](https://hajimism.github.io/dek/guide/getting-started.html), or build a real deck end to end in the [Tutorial](https://hajimism.github.io/dek/guide/tutorial.html). Every command is listed in the [CLI reference](https://hajimism.github.io/dek/reference/cli.html).
48
50
 
49
51
  ## Sample
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hajimism/dek",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "A build system for talks.",
5
5
  "license": "MIT",
6
6
  "author": "Hajime Nakagawa (https://github.com/hajimism)",
@@ -10,6 +10,8 @@
10
10
  },
11
11
  "files": [
12
12
  "src",
13
+ "skills",
14
+ "tsconfig.json",
13
15
  "LICENSE",
14
16
  "README.md",
15
17
  "README.ja.md",
@@ -46,6 +48,7 @@
46
48
  "typecheck": "tsc --noEmit",
47
49
  "knip": "knip",
48
50
  "schema": "bun scripts/cli-schema.ts",
51
+ "llms": "bun scripts/llms.ts",
49
52
  "format": "biome format --write .",
50
53
  "lint": "biome lint .",
51
54
  "check": "biome check --write .",
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: dek
3
+ description: Write and fix talks with dek, a build system for talks run as `dekc` on Bun - script first in script.md, one small HTML file per slide, checked by lint and screenshots until lint passes. Use when a project has dek.toml or decks/*/script.md, or when asked to make, fix, or hand over slides for a talk with dek.
4
+ ---
5
+
6
+ # dek
7
+
8
+ dek builds a talk from its script. `script.md` holds what will be said, in order; each `##` heading is one slide, written as one small HTML file, and lint checks everything a rule can measure. This skill is the loop. The installed dek tells you the rest, and it wins whenever the two disagree.
9
+
10
+ ## Run it as `dekc`, on Bun
11
+
12
+ - The package is `@hajimism/dek`; the command is `dekc`. Never `bunx dek` or `npx dek`: `dek` on npm is an unrelated package that would be downloaded and run.
13
+ - dek runs on Bun. Inside a project, `bunx dekc`; outside one, `bunx @hajimism/dek`. Not `npx`, not `node`.
14
+ - No project yet: `bunx @hajimism/dek init <dir>`, then `cd <dir>`; where dek is already installed, `bunx dekc init <dir>` does the same.
15
+ - Decide from `--json`: every result command takes it. Branch on `ok`; an error carries a `hint` with the next command to run. Never scrape a verdict from the text output. `show` and `theme` print text meant to be read, and take `--json` too.
16
+
17
+ ## First, ask the dek you have
18
+
19
+ 1. `bunx dekc --version` and `bunx dekc help --agent`: every command and flag of the dek installed in this project. This file names few on purpose; use only what that list shows.
20
+ 2. Read dek's block in `AGENTS.md`. dek rewrites it for the installed version: the conventions lint checks, how to check a slide, and what to do before reporting a deck as done. Follow it as written.
21
+ 3. In the deck, `bunx dekc theme`: the classes, tokens, and layouts this deck's theme defines, and `bunx dekc theme <layout>` for a layout's markup. Use nothing else.
22
+
23
+ ## Script first
24
+
25
+ Write or change `script.md` before any slide: `##` is a slide, `###` a beat, and paragraphs are what is said. `bunx dekc sync` writes a skeleton for each new section, and removes the skeletons nobody edited whose section is gone; it never touches a slide you edited. Keep the order and what is said in the script; a slide shows it. `bunx dekc ls` estimates the talk's length from the script against the `duration` in its frontmatter.
26
+
27
+ ## One slide at a time
28
+
29
+ 1. `bunx dekc show <slug>`: the slide's section of the script, its files, and the theme rules it uses.
30
+ 2. Edit `slides/<slug>.html`, and `slides/<slug>.css` for what only this slide needs.
31
+ 3. `bunx dekc check <slug> --shot --json`: fix every error, then open the shot it names. Read its `fill` before judging the shot.
32
+ 4. Next slide. When all are written, `bunx dekc lint --visual --json`, which reports every problem in one run.
33
+
34
+ A hint that sends a fix to `theme.css` means every slide of the deck shares the problem: fix it in the deck's own `theme.css`.
35
+
36
+ ## When to stop
37
+
38
+ - `bunx dekc lint --visual` reports no errors. Warnings do not fail it; read them and decide. A check listed in `skipped` did not run: say so, with its hint, rather than count it as passed.
39
+ - You have looked at `bunx dekc shot --sheet` and judged the deck's balance.
40
+ - Say what you could not judge, such as the argument and how the timing feels when spoken, and hand it back to the author. Passing lint means nothing measurable is wrong, not that the talk is good.
41
+
42
+ ## When the human points at something
43
+
44
+ If the human marked beats while rehearsing or pointed at elements of a slide, `bunx dekc help --agent` lists the commands that read those notes, and `AGENTS.md` says how to use them. Leave clearing them to the human.
@@ -0,0 +1,26 @@
1
+ import type { AnnotationRow } from "../core/annotation-rows.ts";
2
+ import { clearAnnotations, listAnnotations } from "../core/annotations.ts";
3
+ import type { DeckTarget } from "./scope.ts";
4
+
5
+ export type AnnotationsCliResult =
6
+ | { action: "list"; annotations: AnnotationRow[] }
7
+ | { action: "clear"; cleared: number };
8
+
9
+ /**
10
+ * `dekc annotations`: the notes a human wrote on elements of the slides in annotate mode, each
11
+ * element where the file has it now. It reads the project's file, so no dev server is needed.
12
+ */
13
+ export async function annotationsCommand({
14
+ project,
15
+ deck,
16
+ }: DeckTarget): Promise<AnnotationsCliResult> {
17
+ return { action: "list", annotations: listAnnotations(project.root, deck) };
18
+ }
19
+
20
+ /** `dekc annotations clear`: drop the deck's notes, once the human has seen them dealt with. */
21
+ export async function clearAnnotationsCommand({
22
+ project,
23
+ deck,
24
+ }: DeckTarget): Promise<AnnotationsCliResult> {
25
+ return { action: "clear", cleared: clearAnnotations(project.root, deck) };
26
+ }
@@ -9,6 +9,7 @@ import type { DeckScope, DecksTarget, ReadableDeck } from "./scope.ts";
9
9
  import {
10
10
  countSummary,
11
11
  displayDiagnostics,
12
+ formatAnnotations,
12
13
  formatBuild,
13
14
  formatCheck,
14
15
  formatCues,
@@ -540,6 +541,51 @@ export const COMMANDS = {
540
541
  : data,
541
542
  },
542
543
  }),
544
+ annotations: result({
545
+ scope: "deck",
546
+ args: [],
547
+ flags: [],
548
+ usage: ["dekc annotations [deck]"],
549
+ summary:
550
+ "List the notes written on elements of the slides in annotate mode, by pressing a on the\ndev server's page: each element where the file has it now, and whether the slide changed since.",
551
+ group: "Slide",
552
+ overview: [
553
+ ["dekc annotations", "list the notes written on slide elements in annotate mode (a)"],
554
+ ["dekc annotations clear", "drop the deck's annotations"],
555
+ ],
556
+ agent: [
557
+ "dekc annotations [clear] notes a human wrote on slide elements (a): path:line:column, text, shot, status open|edited|gone",
558
+ ],
559
+ run: async ({ target }) => {
560
+ const { annotationsCommand } = await import("./annotations.ts");
561
+ return annotationsCommand(target);
562
+ },
563
+ subcommands: {
564
+ clear: {
565
+ args: [],
566
+ flags: [],
567
+ usage: ["dekc annotations [deck] clear"],
568
+ run: async ({ target }) => {
569
+ const { clearAnnotationsCommand } = await import("./annotations.ts");
570
+ return clearAnnotationsCommand(target);
571
+ },
572
+ },
573
+ },
574
+ output: {
575
+ text: formatAnnotations,
576
+ paths: (data, display) =>
577
+ data.action === "list"
578
+ ? {
579
+ ...data,
580
+ annotations: data.annotations.map((row) => ({
581
+ ...row,
582
+ targets: row.targets.map((target) => ({ ...target, path: display(target.path) })),
583
+ changed: row.changed.map(display),
584
+ })),
585
+ }
586
+ : data,
587
+ },
588
+ }),
543
589
  sync: result({
544
590
  scope: "decks",
545
591
  args: [],
@@ -738,6 +784,26 @@ export const COMMANDS = {
738
784
  },
739
785
  output: { text: formatPdf },
740
786
  }),
787
+ pptx: result({
788
+ scope: "decks",
789
+ args: [],
790
+ flags: ["root-dist", "public"],
791
+ usage: ["dekc pptx [deck] [--root-dist] [--public]"],
792
+ summary:
793
+ "Write dist/<deck>.pptx, one slide per slide at its last beat: a picture of it with its text\nin text boxes to edit, and its script in the notes. Needs Playwright.",
794
+ group: "Output",
795
+ overview: [
796
+ ["dekc pptx [--root-dist] [--public]", "write a PowerPoint file with editable text"],
797
+ ],
798
+ agent: [
799
+ "dekc pptx [--root-dist] [--public] text editable over a picture of each slide; script in notes",
800
+ ],
801
+ run: async ({ target, flags }) => {
802
+ const { pptxCommand } = await import("./pptx.ts");
803
+ return pptxCommand(target, { rootDist: flags["root-dist"], public: flags.public });
804
+ },
805
+ output: { text: formatPdf },
806
+ }),
741
807
  help: {
742
808
  kind: "help",
743
809
  args: ["command?"],
@@ -189,13 +189,25 @@ export const RESULT_FIELDS = {
189
189
  html: z.string().nullable(),
190
190
  css: z.string().nullable(),
191
191
  ts: z.string().nullable(),
192
- theme: z.string().nullable().describe("The rules of the deck's theme.css this slide uses."),
192
+ theme: z
193
+ .string()
194
+ .nullable()
195
+ .describe(
196
+ "The rules of the deck's theme.css this slide uses: those its classes, layout, elements, and attributes match; with a slide script, every element and attribute rule under .slide.",
197
+ ),
193
198
  assets: paths,
194
199
  ref: RefInfo.optional(),
195
200
  }),
196
201
  theme: z.strictObject({
197
202
  path: z.string(),
198
- classes: z.array(z.string()),
203
+ classes: z
204
+ .array(z.string())
205
+ .describe("The classes slide markup may use; what max_classes counts."),
206
+ stateClasses: z
207
+ .array(z.string())
208
+ .describe(
209
+ "State classes the theme selects that the player sets at runtime (is-current, is-shown); never written in markup.",
210
+ ),
199
211
  tokens: z.array(z.strictObject({ name: z.string(), value: z.string() })),
200
212
  layouts: z.array(z.strictObject({ name: z.string(), example: z.string().optional() })),
201
213
  layout: z.strictObject({ name: z.string(), example: z.string() }).optional(),
@@ -249,7 +261,9 @@ export const RESULT_FIELDS = {
249
261
  .optional(),
250
262
  }),
251
263
  shot: z.strictObject({
252
- shots: z.array(shotFile),
264
+ shots: z
265
+ .array(shotFile)
266
+ .describe("Each still or morph frame; with --motion, empty: the frames are in motion."),
253
267
  sheets: paths.optional().describe("Contact sheets, from --sheet or --motion."),
254
268
  motion: z
255
269
  .array(
@@ -260,7 +274,8 @@ export const RESULT_FIELDS = {
260
274
  ),
261
275
  }),
262
276
  )
263
- .optional(),
277
+ .optional()
278
+ .describe("With --motion, each beat in order with its frames, the settled end last."),
264
279
  }),
265
280
  mv: z.strictObject({
266
281
  from: z.string(),
@@ -294,6 +309,61 @@ export const RESULT_FIELDS = {
294
309
  }),
295
310
  z.strictObject({ action: z.literal("clear"), cleared: z.number().int() }),
296
311
  ]),
312
+ annotations: z.union([
313
+ z.strictObject({
314
+ action: z.literal("list"),
315
+ annotations: z.array(
316
+ z.strictObject({
317
+ number: z.number().int().describe("As the dev page's markers and Copy number it."),
318
+ id: z.string(),
319
+ slug: z.string(),
320
+ step: z
321
+ .string()
322
+ .describe("The stop as `dekc shot --step` takes it; 0 as the slide arrives."),
323
+ status: z
324
+ .enum(["open", "edited", "gone"])
325
+ .describe(
326
+ "edited once the slide's own html, css, or ts changed since the note; gone once none of its elements is on the slide.",
327
+ ),
328
+ text: z.string().describe("What the human wrote."),
329
+ targets: z.array(
330
+ z.strictObject({
331
+ path: z.string(),
332
+ line: z
333
+ .number()
334
+ .int()
335
+ .describe("Where the start tag is now; where it was last found when not found."),
336
+ column: z.number().int().describe("1-based, in UTF-16 units as editors count."),
337
+ found: z.boolean(),
338
+ name: z
339
+ .string()
340
+ .describe(
341
+ "The tag and its classes as written where it was last found, as div.chevron.",
342
+ ),
343
+ was: z.string().describe("Its text as written when the note was written, cut short."),
344
+ text: z.string().nullable().describe("Its text as written now; null when not found."),
345
+ box: z
346
+ .strictObject({
347
+ x: z.number(),
348
+ y: z.number(),
349
+ width: z.number(),
350
+ height: z.number(),
351
+ })
352
+ .describe("On the slide in logical pixels, when the note was written."),
353
+ point: z
354
+ .strictObject({ x: z.number(), y: z.number() })
355
+ .optional()
356
+ .describe("Where the slide itself was clicked."),
357
+ }),
358
+ ),
359
+ changed: paths.describe("The slide's own files that changed since the note."),
360
+ shot: z.string().describe("The command that shows the slide at the note's beat."),
361
+ createdAt: z.string(),
362
+ }),
363
+ ),
364
+ }),
365
+ z.strictObject({ action: z.literal("clear"), cleared: z.number().int() }),
366
+ ]),
297
367
  sync: z.strictObject({ created: paths, updated: paths, removed: paths, kept: paths }),
298
368
  lint: z.strictObject({ diagnostics, skipped }),
299
369
  cues: z.strictObject({
@@ -342,6 +412,7 @@ export const RESULT_FIELDS = {
342
412
  credits: z.string().optional(),
343
413
  }),
344
414
  pdf: z.strictObject({ outs: paths }),
415
+ pptx: z.strictObject({ outs: paths }),
345
416
  } satisfies Record<ResultCommand, FieldsSchema>;
346
417
 
347
418
  type FieldsSchema = z.ZodObject | z.ZodUnion<readonly z.ZodObject[]>;
@@ -0,0 +1,78 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ansi, shouldColor, terminalSafe } from "./tty.ts";
4
+
5
+ /**
6
+ * The files Bun may read into process.env from the working directory before dek starts: `.env`,
7
+ * `.env.local`, and the two for NODE_ENV, plain and `.local`. All of them are read whatever
8
+ * NODE_ENV is, so a change in which ones Bun picks cannot let a variable through.
9
+ */
10
+ const ENV_FILES = [
11
+ ".env",
12
+ ".env.local",
13
+ ...["development", "production", "test"].flatMap((mode) => [
14
+ `.env.${mode}`,
15
+ `.env.${mode}.local`,
16
+ ]),
17
+ ];
18
+
19
+ /**
20
+ * Every name dek takes only from the environment that a file holds, however the line around it is
21
+ * written: `K=v`, `export K=v`, `K: v`. Besides DEK_ variables, NODE_TLS_REJECT_UNAUTHORIZED: Bun
22
+ * reads it on each request, and with it a proxy that a `.env` named could read the token `dekc ref`
23
+ * sends. Other variables Bun acts on, such as HTTPS_PROXY, take effect before dek starts, and
24
+ * deleting them changes nothing.
25
+ */
26
+ const NAME_RE = /DEK_[A-Z0-9_]*|NODE_TLS_REJECT_UNAUTHORIZED/gi;
27
+
28
+ /**
29
+ * Takes out of `env` every variable in NAME_RE that a `.env` file in `cwd` may have set, and
30
+ * returns their names. Bun loads those files before dek runs, so a repository someone else wrote
31
+ * could otherwise choose the programs dek runs and the host `dekc ref` calls. These variables come
32
+ * only from the environment dek was started in. Under `--no-env-file`, which the dekc bin passes,
33
+ * Bun loaded nothing, so every one there is the user's own.
34
+ */
35
+ export function dropEnvFileVariables(
36
+ cwd: string,
37
+ env: NodeJS.ProcessEnv = process.env,
38
+ execArgv: readonly string[] = process.execArgv,
39
+ ): string[] {
40
+ if (execArgv.includes("--no-env-file")) {
41
+ return [];
42
+ }
43
+ const names = new Set<string>();
44
+ for (const file of ENV_FILES) {
45
+ let text: string;
46
+ try {
47
+ text = readFileSync(join(cwd, file), "utf8");
48
+ } catch {
49
+ continue;
50
+ }
51
+ for (const [name] of text.matchAll(NAME_RE)) {
52
+ names.add(name.toUpperCase());
53
+ }
54
+ }
55
+ const dropped = [...names].filter((name) => env[name] !== undefined).sort();
56
+ for (const name of dropped) {
57
+ delete env[name];
58
+ }
59
+ return dropped;
60
+ }
61
+
62
+ /** What dek says on stderr about the variables `dropEnvFileVariables` took out. */
63
+ export function envFileWarning(names: readonly string[], color = false): string {
64
+ const c = ansi(color);
65
+ const list = names.join(", ");
66
+ return [
67
+ `${c.yellow("warning:")} ignored ${list}: a .env file in this directory sets it, and dek takes it only from the environment`,
68
+ ` ${c.yellow("help:")} set it in the shell that runs dekc instead`,
69
+ ].join("\n");
70
+ }
71
+
72
+ /** The bin's first step: drop what a `.env` set, and say so. */
73
+ export function ignoreEnvFileVariables(cwd: string): void {
74
+ const dropped = dropEnvFileVariables(cwd);
75
+ if (dropped.length > 0) {
76
+ process.stderr.write(`${terminalSafe(envFileWarning(dropped, shouldColor(process.stderr)))}\n`);
77
+ }
78
+ }
package/src/cli/files.ts CHANGED
@@ -18,10 +18,10 @@ export function defaultToml(): string {
18
18
  }
19
19
 
20
20
  /**
21
- * What dek keeps for itself under .dek/ while it runs: the dev server's lock, and the marks left
22
- * while rehearsing. Neither belongs in the repository.
21
+ * What dek keeps for itself under .dek/ while it runs: the dev server's lock, the marks left
22
+ * while rehearsing, and the notes written in annotate mode. None belongs in the repository.
23
23
  */
24
- const LOCAL_STATE = [".dek/server.json", ".dek/marks.json"];
24
+ const LOCAL_STATE = [".dek/server.json", ".dek/marks.json", ".dek/annotations.json"];
25
25
 
26
26
  export function defaultGitignore(): string {
27
27
  return `# dek
package/src/cli/flags.ts CHANGED
@@ -89,7 +89,7 @@ export const FLAGS = {
89
89
  },
90
90
  public: {
91
91
  type: "boolean",
92
- text: "a page for anyone with the link, without the script's stage directions and comments",
92
+ text: "for anyone the file reaches: the script without its stage directions and comments",
93
93
  },
94
94
  } as const satisfies Record<string, FlagSpec>;
95
95
 
package/src/cli/format.ts CHANGED
@@ -141,6 +141,9 @@ function formatLiveEvent(event: LiveEvent, cwd: string | undefined): string | nu
141
141
  return event.diagnostics.length === 0 ? null : formatDiagnostics(event.diagnostics, { cwd });
142
142
  case "timeline":
143
143
  return "timeline";
144
+ // The page that changed them already shows it; the terminal has nothing to add.
145
+ case "annotations":
146
+ return null;
144
147
  }
145
148
  }
146
149
 
@@ -0,0 +1,20 @@
1
+ import { pptxDeck } from "../core/pptx.ts";
2
+ import type { DecksTarget } from "./scope.ts";
3
+
4
+ /** One file per deck, a single deck included, as `dekc pdf` answers. */
5
+ export type PptxCliResult = { outs: string[] };
6
+
7
+ export async function pptxCommand(
8
+ { project, decks }: DecksTarget,
9
+ options: { rootDist?: boolean; public?: boolean } = {},
10
+ ): Promise<PptxCliResult> {
11
+ const outs: string[] = [];
12
+ for (const entry of decks) {
13
+ const done = await pptxDeck(
14
+ { project, deck: entry },
15
+ { rootDist: options.rootDist, public: options.public },
16
+ );
17
+ outs.push(done.outPath);
18
+ }
19
+ return { outs };
20
+ }
package/src/cli/show.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { classifyAssetRef } from "../core/assets.ts";
2
2
  import { cssUrls, parseCss } from "../core/css.ts";
3
3
  import { deckPaths } from "../core/deck-paths.ts";
4
- import { scanSlideHtml } from "../core/html-scan.ts";
4
+ import { type HtmlScan, scanSlideHtml } from "../core/html-scan.ts";
5
5
  import { sectionChunks, splitLines } from "../core/lines.ts";
6
6
  import { readDeckFile } from "../core/resolve.ts";
7
7
  import type { Section } from "../core/schema.ts";
@@ -24,7 +24,11 @@ export type ShowResult = {
24
24
  css: string | null;
25
25
  /** `slides/<slug>.ts`. */
26
26
  ts: string | null;
27
- /** The rules of the deck's theme.css this slide uses; null when the deck has no theme.css. */
27
+ /**
28
+ * The rules of the deck's theme.css this slide uses, for the classes, layout, elements, and
29
+ * attributes its markup has; with a slide script, every element and attribute rule under
30
+ * `.slide`. Null when the deck has no theme.css.
31
+ */
28
32
  theme: string | null;
29
33
  /** Deck-relative paths of the files the slide references that exist. */
30
34
  assets: string[];
@@ -52,6 +56,8 @@ export function showCommand({ deck, ref }: ReadableDeck, slug: string): ShowResu
52
56
  classes: scan?.classes ?? [],
53
57
  ...(scan?.layout !== undefined ? { layout: scan.layout } : {}),
54
58
  ...(sheet ? { css: sheet } : {}),
59
+ // A slide script may add elements and attributes the markup lacks: keep their rules.
60
+ ...(scan && ts === null ? { markup: slideMarkup(scan) } : {}),
55
61
  });
56
62
 
57
63
  const refs = [
@@ -73,6 +79,15 @@ export function showCommand({ deck, ref }: ReadableDeck, slug: string): ShowResu
73
79
  };
74
80
  }
75
81
 
82
+ /** The tags and attribute names of the slide and the elements inside it. */
83
+ function slideMarkup(scan: HtmlScan): { tags: string[]; attributes: string[] } {
84
+ const elements = scan.elements.filter((element) => element.inSlide);
85
+ return {
86
+ tags: elements.map((element) => element.tag),
87
+ attributes: elements.flatMap((element) => element.attributes.map((attr) => attr.name)),
88
+ };
89
+ }
90
+
76
91
  /**
77
92
  * Local references that name a file inside the deck, as sorted deck-relative
78
93
  * paths. theme.css and the slide's own files are left out: show returns them.
package/src/cli/text.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // The pieces the dev server's log shares, such as diagnostics and tables, are in format.ts.
3
3
  import type { Diagnostic, SkippedCheck } from "../core/diagnostic.ts";
4
4
  import { formatClock } from "../core/timing.ts";
5
+ import type { AnnotationsCliResult } from "./annotations.ts";
5
6
  import type { BuildCliResult } from "./build.ts";
6
7
  import type { CheckCliResult } from "./check.ts";
7
8
  import type { CuesResult } from "./cues.ts";
@@ -171,6 +172,41 @@ export function formatMarks(data: MarksCliResult): string {
171
172
  .join("\n");
172
173
  }
173
174
 
175
+ /**
176
+ * Each note by its number, as the page's markers show it, with its slide and beat, each element
177
+ * at its line the way an editor jumps to it, the human's words, and the shot that shows it.
178
+ */
179
+ export function formatAnnotations(data: AnnotationsCliResult): string {
180
+ if (data.action === "clear") {
181
+ return data.cleared === 1 ? "cleared 1 annotation" : `cleared ${data.cleared} annotations`;
182
+ }
183
+ if (data.annotations.length === 0) {
184
+ return "no annotations: press a on the dev server's page to point at an element and write a note";
185
+ }
186
+ return data.annotations
187
+ .flatMap((row) => {
188
+ const beat = row.step === "0" ? row.slug : `${row.slug} › ${row.step}`;
189
+ const status = row.status === "open" ? "" : ` (${row.status})`;
190
+ const targets = row.targets.map((target) => {
191
+ const where = `${target.path}:${target.line}:${target.column}`;
192
+ if (!target.found) {
193
+ return ` ${where} ${target.name} (not found)`;
194
+ }
195
+ const text =
196
+ target.text === "" || target.text === null ? "" : ` ${JSON.stringify(target.text)}`;
197
+ const was = target.text !== target.was ? ` (was ${JSON.stringify(target.was)})` : "";
198
+ return ` ${where} ${target.name}${text}${was}`;
199
+ });
200
+ const words = row.text
201
+ .trim()
202
+ .split(/\r?\n/)
203
+ .filter((line) => line.trim() !== "")
204
+ .map((line) => ` > ${line}`);
205
+ return [`${row.number} ${beat}${status}`, ...targets, ...words, ` ${row.shot}`];
206
+ })
207
+ .join("\n");
208
+ }
209
+
174
210
  /** The contact sheets when there are any, else each screenshot. */
175
211
  export function formatShot(data: ShotCliResult): string {
176
212
  return (data.sheets ?? data.shots.map((shot) => shot.path)).join("\n");
@@ -307,6 +343,13 @@ export function formatTheme(data: ThemeResult): string {
307
343
  "CLASSES",
308
344
  ` ${data.classes.join(" ")}`,
309
345
  "",
346
+ ...(data.stateClasses.length > 0
347
+ ? [
348
+ "STATE CLASSES (the player sets these; select them in CSS, never write them in markup)",
349
+ ` ${data.stateClasses.join(" ")}`,
350
+ "",
351
+ ]
352
+ : []),
310
353
  "TOKENS",
311
354
  ...data.tokens.map((token) => ` ${token.name.padEnd(width)} ${token.value}`),
312
355
  "",