@hajimism/dek 0.3.2 → 0.4.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 (77) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.ja.md +1 -1
  3. package/README.md +1 -1
  4. package/package.json +2 -1
  5. package/skills/dek/SKILL.md +10 -3
  6. package/skills/dek-design/SKILL.md +42 -0
  7. package/src/cli/address.ts +84 -14
  8. package/src/cli/build.ts +2 -2
  9. package/src/cli/check.ts +2 -1
  10. package/src/cli/commands.ts +54 -13
  11. package/src/cli/contract.ts +18 -1
  12. package/src/cli/files.ts +9 -6
  13. package/src/cli/flags.ts +1 -1
  14. package/src/cli/goto.ts +3 -1
  15. package/src/cli/init.ts +10 -5
  16. package/src/cli/lint.ts +4 -3
  17. package/src/cli/main.ts +7 -5
  18. package/src/cli/new.ts +41 -10
  19. package/src/cli/pack.ts +21 -0
  20. package/src/cli/pptx.ts +9 -4
  21. package/src/cli/ref.ts +243 -9
  22. package/src/cli/result.ts +5 -2
  23. package/src/cli/scope.ts +25 -27
  24. package/src/cli/shot.ts +2 -1
  25. package/src/cli/text.ts +45 -4
  26. package/src/cli/theme.ts +16 -2
  27. package/src/cli/voice.ts +4 -3
  28. package/src/core/alignment.ts +320 -0
  29. package/src/core/annotation-rows.ts +16 -4
  30. package/src/core/annotations.ts +19 -1
  31. package/src/core/config-keys.ts +6 -2
  32. package/src/core/config.ts +88 -12
  33. package/src/core/diagnostic.ts +5 -1
  34. package/src/core/escape.ts +43 -0
  35. package/src/core/licenses/CC-BY-4.0.txt +396 -0
  36. package/src/core/licenses/CC0-1.0.txt +121 -0
  37. package/src/core/licenses.ts +20 -0
  38. package/src/core/line-break.ts +275 -0
  39. package/src/core/lint/asset-refs.ts +6 -2
  40. package/src/core/lint/files.ts +30 -5
  41. package/src/core/lint/keys.ts +7 -3
  42. package/src/core/lint/slide-html.ts +3 -1
  43. package/src/core/lint/slide-script.ts +7 -5
  44. package/src/core/mv.ts +30 -10
  45. package/src/core/pack.ts +137 -0
  46. package/src/core/playwright-module.d.ts +1 -0
  47. package/src/core/playwright-visual.ts +180 -20
  48. package/src/core/playwright.ts +202 -25
  49. package/src/core/pptx-package.ts +461 -33
  50. package/src/core/pptx-paint.ts +1168 -0
  51. package/src/core/pptx-plan.ts +879 -0
  52. package/src/core/pptx-text.ts +204 -47
  53. package/src/core/pptx-theme.ts +222 -0
  54. package/src/core/pptx.ts +55 -8
  55. package/src/core/prose.ts +23 -0
  56. package/src/core/pseudo-text.ts +32 -10
  57. package/src/core/ref-name.ts +100 -17
  58. package/src/core/ref.ts +318 -34
  59. package/src/core/resolve.ts +20 -9
  60. package/src/core/shot/morph.ts +1 -1
  61. package/src/core/shot/motion.ts +2 -3
  62. package/src/core/shot/still.ts +2 -2
  63. package/src/core/shot-cache.ts +25 -17
  64. package/src/core/slide-measure.ts +51 -1
  65. package/src/core/slide-script-eval.ts +10 -8
  66. package/src/core/slide-script.ts +114 -20
  67. package/src/core/sync.ts +3 -3
  68. package/src/core/visual-cache.ts +2 -0
  69. package/src/core/visual.ts +154 -15
  70. package/src/core/voice.ts +3 -5
  71. package/src/core/zod.ts +9 -1
  72. package/src/runtime/annotate-view.ts +4 -2
  73. package/src/server/dev.ts +15 -4
  74. package/src/server/lock.ts +48 -5
  75. package/src/server/routes.ts +7 -2
  76. package/src/theme/default.css +4 -0
  77. package/src/video/bake.ts +6 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,96 @@
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.4.0](https://github.com/hajimism/dek/compare/v0.3.4...v0.4.0) (2026-10-27)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * **theme:** `dekc theme --json` lists in `layouts` only layouts whose names are plain words: letters, marks, and digits of any script and `@%+=:,./_-`, not starting with `=`. A layout whose name holds anything else, such as a space, a quote, `$`, `;`, or a backtick, is no longer in `layouts`; `unprintedLayouts`, present only when there are some, says how many were left out. `dekc theme <layout>` still prints such a layout's example.
11
+ * **ref:** a dek.toml whose [refs] pins a deck name with any other character, starting with a dash, not in NFC, or longer than 64 characters or 200 bytes, such as `owner/repo/talk(1)` or `file/a+b`, no longer loads. Delete the line the error points at, and for a pack its packs/<deck>.tgz and refs/file/<deck>, then pin the deck again under a name a ref can carry.
12
+
13
+ ### Bug Fixes
14
+
15
+ * **annotations:** show a deck, slide, or beat that is no plain word as a placeholder in the notes ([3b04e89](https://github.com/hajimism/dek/commit/3b04e89dc22f9ea421919c920594f53a9c02d3be))
16
+ * **cli:** say once where to find a deck's name a retyped command leaves as &lt;deck&gt; ([d4a57d9](https://github.com/hajimism/dek/commit/d4a57d9f7836b2ba2433dc634a136bb7e97be509))
17
+ * **config:** show a key that is no plain word as &lt;key&gt; in an error ([22476a7](https://github.com/hajimism/dek/commit/22476a712ecf0ee4e3c552b1e0fe6f35ac10cacf))
18
+ * **dev:** read .dek/server.json only as the dev server writes it ([c402cb6](https://github.com/hajimism/dek/commit/c402cb6282783425611274fbc0337cd6652add7e))
19
+ * **dev:** read the lock a dev server on port 80 writes ([665e0e2](https://github.com/hajimism/dek/commit/665e0e237c3a639a45fa36dc14ed8bd2e7ad9168))
20
+ * keep reads, writes, and suggested commands inside what the project and dek wrote ([23a667d](https://github.com/hajimism/dek/commit/23a667d199ee5f0b34040c4c33f0c0bcfd47f819))
21
+ * **lint:** name a remote file in DEK020's hint only when it is a plain word ([fb77d02](https://github.com/hajimism/dek/commit/fb77d028db1bb38bea6b43695838d52776b192a7))
22
+ * **lint:** name a slide or asset file in a hint only when it is a plain word ([eeb240c](https://github.com/hajimism/dek/commit/eeb240c418b615067bd66ff45ac6b70361d3bd1b))
23
+ * **lint:** show an unknown key that is no plain word as &lt;key&gt; in DEK008 ([f950def](https://github.com/hajimism/dek/commit/f950def7f80f06da46bd8c913ae84c8c849d5ddf))
24
+ * never follow a link under refs/ when writing a snapshot ([0c843ea](https://github.com/hajimism/dek/commit/0c843ea4193270dd1cc0fde4a04768e210efd911))
25
+ * never write or remove a pack's copy through a link under packs/ ([95980a6](https://github.com/hajimism/dek/commit/95980a631e90ee97eda24784ded50b042111512d))
26
+ * quote every name in a command dek suggests ([d1e608d](https://github.com/hajimism/dek/commit/d1e608d5676e46b7947bd5f43a3315269fd27773))
27
+ * read a project's files only through links that stay inside it ([7ae3815](https://github.com/hajimism/dek/commit/7ae38152eadd6f58d67034169ade9dd749038002))
28
+ * **ref:** accept only letters, digits, dots, dashes, and underscores in a ref's deck name ([1b430e1](https://github.com/hajimism/dek/commit/1b430e1ea3d0f66af2642a5f257af1cefcae3ac0))
29
+ * **ref:** fold names as macOS does when telling whether two refs share a folder ([fdb32ee](https://github.com/hajimism/dek/commit/fdb32ee103facc5a661f20dabf79fe8f91ae48cd))
30
+ * **ref:** refuse a pin whose folders another ref already takes ([0d550d8](https://github.com/hajimism/dek/commit/0d550d897b0cdad86ac3de181bf7fd031061bd66))
31
+ * refuse a decks folder that links out of the project ([83f2ff0](https://github.com/hajimism/dek/commit/83f2ff00079893b8e2dbb2fc5d5b7588435aa4dd))
32
+ * refuse to serve or write inside a ref, as every other command does ([665b2de](https://github.com/hajimism/dek/commit/665b2de7a3e6aa3b67ce6140d3eb2a15fb398ee6))
33
+ * serve a deck's assets only from inside the project ([86743cf](https://github.com/hajimism/dek/commit/86743cf731d97f664e95b75f1441dd238f0eeb11))
34
+ * take a folder for a ref only where dek wrote one, and name it from there ([62cc8df](https://github.com/hajimism/dek/commit/62cc8df0a509a99a55d9db7698683bc3be5523d4))
35
+ * **theme:** leave layouts whose names are no plain word out of --json ([625f6d3](https://github.com/hajimism/dek/commit/625f6d3dab0dfe16dacb272e371b551214621a86))
36
+ * **theme:** print only layout names that are plain words ([62b15d1](https://github.com/hajimism/dek/commit/62b15d18afb91d6adbc2641376d9ca899cb6bf20))
37
+
38
+
39
+ ### Documentation
40
+
41
+ * say dek follows no link under packs/ either ([5a11b2c](https://github.com/hajimism/dek/commit/5a11b2ca9960a134e261f09f80408fc770aa44da))
42
+
43
+ ## [0.3.4](https://github.com/hajimism/dek/compare/v0.3.3...v0.3.4) (2026-10-27)
44
+
45
+
46
+ ### Features
47
+
48
+ * **pptx:** lay out boxes, pictures, text, the theme, and backgrounds as PowerPoint's own ([#27](https://github.com/hajimism/dek/issues/27)) ([eb854e9](https://github.com/hajimism/dek/commit/eb854e9835508792d3e8cfb9dd4046e8750dab7f))
49
+ * **skills:** teach the review in the browser, and add an optional dek-design skill ([2c18b0e](https://github.com/hajimism/dek/commit/2c18b0e339160fb03ca6ce3915b89e37d82c9522))
50
+ * **skills:** teach the review in the browser, and add an optional dek-design skill ([1b04394](https://github.com/hajimism/dek/commit/1b04394023a239706a2fa29d168811e1a1be05d2))
51
+ * **visual:** report awkward line breaks and edges a few pixels off (DEK035, DEK036) ([#28](https://github.com/hajimism/dek/issues/28)) ([ce75fc1](https://github.com/hajimism/dek/commit/ce75fc14937d76ab457298a5aca9116a0fe3778e))
52
+
53
+
54
+ ### Bug Fixes
55
+
56
+ * refuse a slide script's imports before Bun reads it, so none runs while dek compiles ([2752356](https://github.com/hajimism/dek/commit/275235647869f1e0a040c080476141d3aaa1aaa6))
57
+ * refuse a slide script's imports before Bun reads it, so none runs while dek compiles ([198930c](https://github.com/hajimism/dek/commit/198930c1d41639cc9943f36a3a187ff6eab0f364))
58
+
59
+
60
+ ### Documentation
61
+
62
+ * **adr:** keep dependency code away from the tokens that publish ([103f17c](https://github.com/hajimism/dek/commit/103f17cebb37dd1750caa4cc242acedf55703809))
63
+ * **adr:** renumber the PPTX records after the line-break ones ([ca43352](https://github.com/hajimism/dek/commit/ca4335267b6eeed7131c4123a8a543a230d65875))
64
+ * **adr:** renumber the PPTX records after the line-break ones ([bef6e12](https://github.com/hajimism/dek/commit/bef6e12915ac301843297fb972cf36288bbf612c))
65
+ * **adr:** ship an optional design skill and raise the official skill's floor to 0.2.2 ([6f39dd1](https://github.com/hajimism/dek/commit/6f39dd1e8234297cb0fb72d93722a419d1921f23))
66
+ * name the optional dek-design skill, and the review the official skill teaches ([6908fa1](https://github.com/hajimism/dek/commit/6908fa195e16d79c75d5b77cda72f708798de34c))
67
+
68
+ ## [0.3.3](https://github.com/hajimism/dek/compare/v0.3.2...v0.3.3) (2026-10-21)
69
+
70
+
71
+ ### Features
72
+
73
+ * **new:** start a deck on a ref's theme with `--theme-from <ref>` ([6d0ffc9](https://github.com/hajimism/dek/commit/6d0ffc9c45a54092375ca73e305294e71e005655))
74
+ * **new:** start a deck on a ref's theme with `--theme-from <ref>` ([54be6db](https://github.com/hajimism/dek/commit/54be6db71b2521fda1bec3ddb967a4a7545bc36c))
75
+ * **pack:** hand a deck over under CC0 1.0 unless dek.toml says otherwise ([b7e2565](https://github.com/hajimism/dek/commit/b7e2565fd83a8d82c8d8416f6ae3551ba5040427))
76
+ * **pack:** hand a deck over under CC0 1.0 unless dek.toml says otherwise ([46cd7b9](https://github.com/hajimism/dek/commit/46cd7b9424d9e76570c2e017f2345b41f85506b3))
77
+ * **ref:** hand a deck over with `dekc pack` and read it with `dekc ref <file>.tgz` ([9e77bbc](https://github.com/hajimism/dek/commit/9e77bbc7bdf9f2e232c115903c73bf8fa8f078fe))
78
+ * **ref:** hand a deck over with `dekc pack` and read it with `dekc ref <file>.tgz` ([a0c67bc](https://github.com/hajimism/dek/commit/a0c67bc8258a2132dd77eadb2b87c838e6e9f866))
79
+
80
+
81
+ ### Bug Fixes
82
+
83
+ * **dev:** leave the Enter and Escape that end a conversion to the input method ([f148c54](https://github.com/hajimism/dek/commit/f148c54377460b4df11e8fedd059554f00c06889))
84
+ * **dev:** leave the Enter and Escape that end a conversion to the input method ([e2b3940](https://github.com/hajimism/dek/commit/e2b3940cf48168b5a7c2b95250df982566222b60))
85
+
86
+
87
+ ### Documentation
88
+
89
+ * **adr:** hand a deck over as a pack and read it as a ref ([4b3ffc6](https://github.com/hajimism/dek/commit/4b3ffc6e653757f3976a581acff1a2bf76862748))
90
+ * **adr:** start a deck from a ref's theme ([f836941](https://github.com/hajimism/dek/commit/f836941cb2faf9b33faaab63abd16f66878e4767))
91
+ * describe packs in the structure guide, the reference, and ADR 15 ([020d4a0](https://github.com/hajimism/dek/commit/020d4a0254138c55e0a78c41e2e28f8a839bf8d1))
92
+ * record the pack's license in ADR 16, the guide, and the reference ([a0b4ea7](https://github.com/hajimism/dek/commit/a0b4ea74b6d896d102c1c0e2c06a040b75905962))
93
+ * say a deck can start on a ref's theme, in the guides and the reference ([49791ff](https://github.com/hajimism/dek/commit/49791ff46f77e5fd37e9d21f4061669de31af1f9))
94
+
5
95
  ## [0.3.2](https://github.com/hajimism/dek/compare/v0.3.1...v0.3.2) (2026-10-20)
6
96
 
7
97
 
package/README.ja.md CHANGED
@@ -44,7 +44,7 @@ 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)を参照してください。
47
+ エージェントと作るなら、公式スキルを `npx skills add hajimism/dek --skill dek` で、Claude Code なら `/plugin marketplace add hajimism/dek` と `/plugin install dek@dek` で入れてください。`dek-design` も、同じ方法で選んで入れられます。プロジェクトの外にいるエージェントは [llms.txt](https://hajimism.github.io/dek/llms.txt) から始められます。[AI エージェントと作る](https://hajimism.github.io/dek/ja/guide/ai.html)を参照してください。
48
48
 
49
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)にあります。
50
50
 
package/README.md CHANGED
@@ -44,7 +44,7 @@ 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).
47
+ Working with an agent? Install the official skill with `npx skills add hajimism/dek --skill dek`, or in Claude Code with `/plugin marketplace add hajimism/dek` and `/plugin install dek@dek`; `dek-design` is an optional skill installed the same way; 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
48
 
49
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).
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hajimism/dek",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "A build system for talks.",
5
5
  "license": "MIT",
6
6
  "author": "Hajime Nakagawa (https://github.com/hajimism)",
@@ -67,6 +67,7 @@
67
67
  "happy-dom": "21.0.1",
68
68
  "knip": "6.41.0",
69
69
  "playwright": "^1.63.0",
70
+ "publint": "0.3.25",
70
71
  "typescript": "^7.0.2",
71
72
  "vitepress": "^1.6.4"
72
73
  },
@@ -16,7 +16,7 @@ dek builds a talk from its script. `script.md` holds what will be said, in order
16
16
 
17
17
  ## First, ask the dek you have
18
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.
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. It is written for dek 0.2.2 or newer; with an older one, follow only that list and `AGENTS.md`.
20
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
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
22
 
@@ -39,6 +39,13 @@ A hint that sends a fix to `theme.css` means every slide of the deck shares the
39
39
  - You have looked at `bunx dekc shot --sheet` and judged the deck's balance.
40
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
41
 
42
- ## When the human points at something
42
+ ## When the human reviews the deck
43
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.
44
+ The human sees what lint cannot. Let them point at it:
45
+
46
+ 1. Start the dev server in the background, since it keeps running: `bunx dekc <deck> --port <free port>`, or your harness's preview tool if it has one.
47
+ 2. Give them the URL and ask them to press `a`, click what they want changed, write a note, and tell you when they are done. They may not know dek: name the keys, not the commands.
48
+ 3. `bunx dekc annotations <deck> --json`, and fix every note. Shoot the slide again with `bunx dekc shot <slug>` and compare it with the note's `shot`. Tell them in short what you changed.
49
+ 4. Ask them to look again, until they say the deck is done. Stop the dev server you started.
50
+
51
+ Leave `dekc annotations clear` to the human. If they marked beats while rehearsing, `AGENTS.md` says how to read those too.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: dek-design
3
+ description: Make dek slides that read at a glance - a heading that states a claim, one focal point, and no visual difference without a meaning. Use when writing or reviewing the look of slides in a dek deck, or when a deck lints clean but still looks off.
4
+ ---
5
+
6
+ # dek-design
7
+
8
+ The audience sees a slide, holds it in mind, then understands it. Make each step cost as little as it can. Lint measures what a rule can decide, such as overflow and contrast; this skill is for what it cannot. Use only the classes, tokens, and layouts `bunx dekc theme` lists.
9
+
10
+ ## Before you start
11
+
12
+ - Projected or read: a talk is spoken, so the words live in `script.md` and a slide shows what the voice cannot. A deck sent to be read alone needs sentences that stand without the speaker.
13
+ - What the audience knows: do not use a term they do not know. If you must, tie it to something they do.
14
+
15
+ ## One slide
16
+
17
+ - The `##` heading is the slide's claim, in one sentence: "Builds fail on cold caches", not "Problems". Read in order, the headings tell the whole talk.
18
+ - The body is the evidence for the heading. Show it as a figure, a chart, or a short table where you can.
19
+ - Keep about four groups on a slide at most. With more, nest them or split the slide.
20
+
21
+ ## How it looks
22
+
23
+ - One thing stands out most on each slide, and it is the main point. Emphasis used everywhere is emphasis nowhere.
24
+ - Change color, type, or layout only when the meaning changes; the audience looks for a reason in every change, so never vary for variety. Mark sections with one layout used the same way each time.
25
+ - When things differ, make them differ clearly. A small difference is not seen as one.
26
+ - Line things up. Items side by side share their top, their bottom, and the line their text starts on; a row of cards is one height even when one holds less. Items stacked share their left edge. An edge a little off reads as a mistake, not a choice, and a shot shows it where markup does not.
27
+ - What sits close, aligns, or shares a color reads as one group. Do not set items of different rank as a row of equal cards.
28
+ - Position means something: time and steps run left to right, more or better goes up, the general sits above the specific. A person or anything with a direction faces into the slide or toward what it points at.
29
+ - The eye goes from the heading at top left, to the body in the middle, to the conclusion or next step at bottom right.
30
+
31
+ ## Charts
32
+
33
+ - A line for a trend, bars to compare values. Show amounts by position or length, not by area, angle, or hue.
34
+ - Label lines and bars directly instead of using a legend. Put each note next to what it explains.
35
+ - No icon or picture that only decorates.
36
+
37
+ ## Check
38
+
39
+ 1. `bunx dekc ls <deck>` lists the headings in order. Read only them: does the talk hold?
40
+ 2. `bunx dekc shot --sheet`, then each slide's own shot: where does the eye land first, and is that the main point? Read `fill` in `bunx dekc check <slug> --json` for where the slide is empty.
41
+ 3. In each slide's shot, edges line up: in a row, tops, bottoms, and the first lines of text; in a column, left edges. Fix any that do not.
42
+ 4. Every difference in position, color, size, and look has a reason you can say. Remove the ones that do not.
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
+ import { shellName } from "../core/escape.ts";
3
4
  import { isInside } from "../core/path.ts";
4
- import { COMMANDS } from "./commands.ts";
5
+ import { type AnySpec, COMMANDS, subcommandsOf } from "./commands.ts";
5
6
 
6
7
  /** A deck a hint is about: the name a command takes, and the directory that makes it implicit. */
7
8
  export type HintDeck = { name: string; dir: string };
@@ -10,20 +11,72 @@ export type HintDeck = { name: string; dir: string };
10
11
  * A hint's commands as they run from `cwd`. Hints are written as if run inside the deck they are
11
12
  * about, where a command finds its deck by itself. Run anywhere else, a command that takes a deck
12
13
  * needs its name: as the first word outside every deck, and as `--deck` inside another deck,
13
- * where a first word names something in that deck instead. A command that already names a deck,
14
- * or takes none, is left as written.
14
+ * where a first word names something in that deck instead, or where the name would read as a
15
+ * subcommand or a flag. The name is one shell word, however it is spelled, or `<deck>` when no
16
+ * word holds it alike in every shell, with where to find it unless `note` is false, for a hint
17
+ * that joins several and says it once. A command that already names a deck, or takes none, is
18
+ * left as written.
15
19
  */
16
- export function addressHint(hint: string, deck: HintDeck | undefined, cwd: string): string {
20
+ export function addressHint(
21
+ hint: string,
22
+ deck: HintDeck | undefined,
23
+ cwd: string,
24
+ { note: noted = true }: { note?: boolean } = {},
25
+ ): string {
17
26
  if (deck === undefined || isInside(cwd, deck.dir)) {
18
27
  return hint;
19
28
  }
20
- const naming = deckAround(cwd) === undefined ? deck.name : `--deck ${deck.name}`;
21
- return hint.replace(COMMAND_RE, (whole, command: string, rest: string) => {
22
- if (!DECK_COMMANDS.has(command) || namesDeck(rest, deck.name)) {
23
- return whole;
24
- }
25
- return `\`dekc ${command} ${naming}${rest}\``;
26
- });
29
+ const word = shellName(deck.name) ?? DECK_PLACEHOLDER;
30
+ const inOther = deckAround(cwd) !== undefined;
31
+ // The note a placeholder adds names `dekc ls` for the project, not for the deck.
32
+ const note = hint.indexOf(PLACEHOLDER_NOTE);
33
+ let placed = false;
34
+ const addressed = hint.replace(
35
+ COMMAND_RE,
36
+ (whole, command: string, rest: string, offset: number) => {
37
+ const subcommand = SUBCOMMAND_WORDS.get(command)?.has(deck.name) === true;
38
+ // A first word that is a subcommand, or a flag, is never the deck's name.
39
+ const positional = !subcommand && !deck.name.startsWith("-");
40
+ if (
41
+ !DECK_COMMANDS.has(command) ||
42
+ namesDeck(rest, positional ? word : undefined) ||
43
+ (note >= 0 && offset >= note && offset < note + PLACEHOLDER_NOTE.length)
44
+ ) {
45
+ return whole;
46
+ }
47
+ placed ||= word === DECK_PLACEHOLDER;
48
+ const flag = inOther || subcommand;
49
+ return `\`dekc ${command} ${flag ? deckFlag(deck.name) : deckWord(deck.name)}${rest}\``;
50
+ },
51
+ );
52
+ return placed && note < 0 && noted ? `${addressed}${PLACEHOLDER_NOTE}` : addressed;
53
+ }
54
+
55
+ /** `hint` with where to find a deck's name, once, when one of its commands has `<deck>` for it. */
56
+ export function withPlaceholderNote(hint: string): string {
57
+ return hint.includes(DECK_PLACEHOLDER) && !hint.includes(PLACEHOLDER_NOTE)
58
+ ? `${hint}${PLACEHOLDER_NOTE}`
59
+ : hint;
60
+ }
61
+
62
+ /** Where a hint with `<deck>` in it says to find the deck's name. */
63
+ const PLACEHOLDER_NOTE = "; for <deck>, name the deck as `dekc ls` lists it";
64
+
65
+ /** What a command shows for a deck no word holds alike in every shell. */
66
+ const DECK_PLACEHOLDER = "<deck>";
67
+
68
+ /**
69
+ * A deck's name as the first word of a command: one shell word, `<deck>` when there is none, or
70
+ * `--deck=` when the name starts with a dash, which a first word would read as a flag.
71
+ */
72
+ export function deckWord(name: string): string {
73
+ return name.startsWith("-") ? deckFlag(name) : (shellName(name) ?? DECK_PLACEHOLDER);
74
+ }
75
+
76
+ /** A deck's name as `--deck`, joined with `=` when it starts with a dash, as a value must be. */
77
+ export function deckFlag(name: string): string {
78
+ const word = shellName(name) ?? DECK_PLACEHOLDER;
79
+ return name.startsWith("-") ? `--deck=${word}` : `--deck ${word}`;
27
80
  }
28
81
 
29
82
  /** A backquoted command: `dekc`, a command word, and what follows it up to the closing quote. */
@@ -33,9 +86,26 @@ const DECK_COMMANDS = new Set(
33
86
  Object.entries(COMMANDS).flatMap(([name, spec]) => ("scope" in spec && spec.scope ? [name] : [])),
34
87
  );
35
88
 
36
- function namesDeck(rest: string, name: string): boolean {
37
- const [first] = rest.trim().split(/\s+/);
38
- return first === name || first?.includes("/") === true || /(?:^| )--deck(?: |$)/.test(rest);
89
+ /** The first word of each command's subcommands, which a deck's name in its place would read as. */
90
+ const SUBCOMMAND_WORDS = new Map(
91
+ Object.entries(COMMANDS).map(([name, spec]) => [
92
+ name,
93
+ new Set(Object.keys(subcommandsOf(spec as AnySpec)).map((sub) => sub.split(" ")[0])),
94
+ ]),
95
+ );
96
+
97
+ /**
98
+ * Whether a command's words already name a deck: as `word`, the deck's name as one shell word,
99
+ * when the deck can be named first, as a ref, or with --deck.
100
+ */
101
+ function namesDeck(rest: string, word: string | undefined): boolean {
102
+ const words = rest.trim();
103
+ const [first] = words.split(/\s+/);
104
+ return (
105
+ (word !== undefined && (words === word || words.startsWith(`${word} `))) ||
106
+ first?.includes("/") === true ||
107
+ /(?:^| )--deck(?:[ =]|$)/.test(rest)
108
+ );
39
109
  }
40
110
 
41
111
  /**
package/src/cli/build.ts CHANGED
@@ -5,7 +5,7 @@ import { lintDeck, lintProject } from "../core/lint.ts";
5
5
  import { parsePublicUrl } from "../core/ogp.ts";
6
6
  import { PLAYWRIGHT_INSTALL, type PlaywrightRunner } from "../core/playwright.ts";
7
7
  import { playerScript } from "../runtime/player.ts";
8
- import { type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
8
+ import { RENDERED_RULES, type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
9
9
  import type { DecksTarget } from "./scope.ts";
10
10
 
11
11
  /**
@@ -77,7 +77,7 @@ export async function buildCommand(
77
77
  ...skippedChecks([
78
78
  ...skipped,
79
79
  visualSkipped("dekc lint --visual", {
80
- reason: "overflow and contrast are measured only by `dekc lint --visual`",
80
+ reason: `${RENDERED_RULES} are measured only by \`dekc lint --visual\``,
81
81
  ...(options.runner ? { runner: options.runner } : {}),
82
82
  }),
83
83
  ]),
package/src/cli/check.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Diagnostic } from "../core/diagnostic.ts";
2
2
  import { DekError } from "../core/error.ts";
3
+ import { shellQuote } from "../core/escape.ts";
3
4
  import type { Fill } from "../core/fill.ts";
4
5
  import { lintDeck } from "../core/lint.ts";
5
6
  import { type PlaywrightRunner, playwrightMissingError } from "../core/playwright.ts";
@@ -53,7 +54,7 @@ export async function checkCommand(
53
54
 
54
55
  const skipped: SkippedCheck[] = visual
55
56
  ? []
56
- : [visualSkipped(`dekc check ${slug}`, { reason: "Playwright is not installed" })];
57
+ : [visualSkipped(`dekc check ${shellQuote(slug)}`, { reason: "Playwright is not installed" })];
57
58
 
58
59
  // A live-only deck is done without voice, so asking for it skips the check rather than failing,
59
60
  // the way a missing Playwright skips visual.
@@ -1,4 +1,6 @@
1
1
  import { hasErrors } from "../core/diagnostic.ts";
2
+ import { shellQuote } from "../core/escape.ts";
3
+ import { isRefName } from "../core/ref-name.ts";
2
4
  import { mergeSarif, toSarif } from "../core/sarif.ts";
3
5
  import type { CheckCliResult } from "./check.ts";
4
6
  import type { ResultFields } from "./contract.ts";
@@ -21,7 +23,9 @@ import {
21
23
  formatMv,
22
24
  formatNav,
23
25
  formatNew,
26
+ formatPack,
24
27
  formatPdf,
28
+ formatPptx,
25
29
  formatRef,
26
30
  formatReport,
27
31
  formatShot,
@@ -266,14 +270,22 @@ export const COMMANDS = {
266
270
  new: result({
267
271
  args: ["name"],
268
272
  flags: ["theme-from"],
269
- usage: ["dekc new <name> [--theme-from DECK]"],
270
- summary: "Add a deck with the project's theme, or another deck's, and its skeleton slides.",
273
+ usage: ["dekc new <name> [--theme-from DECK|REF]"],
274
+ summary:
275
+ "Add a deck with the project's theme, another deck's, or a ref's, and its skeleton slides.",
271
276
  group: "Project",
272
- overview: [["dekc new <name> [--theme-from DECK]", "add a deck"]],
273
- agent: ["dekc new <name> [--theme-from DECK]"],
277
+ overview: [["dekc new <name> [--theme-from DECK|REF]", "add a deck"]],
278
+ agent: [
279
+ "dekc new <name> [--theme-from DECK|REF] a ref's theme to build on its layouts; copied, never linked",
280
+ ],
274
281
  run: async ({ cwd, args, flags }) => {
282
+ const themeFrom = flags["theme-from"];
275
283
  const { newCommand } = await import("./new.ts");
276
- return newCommand({ cwd, name: args.name, themeFrom: flags["theme-from"] });
284
+ if (themeFrom !== undefined && isRefName(themeFrom)) {
285
+ const { restoreRef } = await import("./ref.ts");
286
+ await restoreRef(cwd, themeFrom);
287
+ }
288
+ return newCommand({ cwd, name: args.name, themeFrom });
277
289
  },
278
290
  output: {
279
291
  text: formatNew,
@@ -313,7 +325,7 @@ export const COMMANDS = {
313
325
  ref: result({
314
326
  args: ["source?"],
315
327
  flags: [],
316
- usage: ["dekc ref <owner/repo/deck>[@rev]", "dekc ref"],
328
+ usage: ["dekc ref <owner/repo/deck>[@rev]", "dekc ref <file>.tgz", "dekc ref"],
317
329
  summary:
318
330
  "Pin another project's deck to read as a model, list the pins, or drop one.\nls, show, theme, and shot read a ref as they read a deck.",
319
331
  group: "Refs",
@@ -322,11 +334,13 @@ export const COMMANDS = {
322
334
  "dekc ref owner/repo/deck[@rev]",
323
335
  "pin and fetch one (a GitHub link works too); again moves the pin",
324
336
  ],
337
+ ["dekc ref <file>.tgz", "pin a pack someone sent as file/<deck>; commit packs/"],
325
338
  ["dekc ref", "list pinned refs"],
326
339
  ["dekc ref rm <ref>", "drop one"],
327
340
  ],
328
341
  agent: [
329
342
  "dekc ref [owner/repo/deck[@rev] | github-link] pin + fetch; no args lists; dekc ref rm <ref>",
343
+ "dekc ref <file>.tgz pin a pack as file/<deck>; commit packs/<deck>.tgz, which a clone unpacks",
330
344
  ],
331
345
  run: async ({ cwd, args }) => {
332
346
  const { addRef, listRefs } = await import("./ref.ts");
@@ -343,7 +357,31 @@ export const COMMANDS = {
343
357
  },
344
358
  },
345
359
  },
346
- output: { text: formatRef },
360
+ output: {
361
+ text: formatRef,
362
+ paths: (data, display) =>
363
+ data.action === "add" && data.pack !== undefined
364
+ ? { ...data, pack: display(data.pack) }
365
+ : data,
366
+ },
367
+ }),
368
+ pack: result({
369
+ scope: "decks",
370
+ args: [],
371
+ flags: ["root-dist"],
372
+ usage: ["dekc pack [deck] [--root-dist]"],
373
+ summary:
374
+ "Write dist/<deck>.tgz: the deck as a file to hand to someone who reads it as a ref with\n`dekc ref <file>.tgz`, without access to where it lives.",
375
+ group: "Refs",
376
+ overview: [["dekc pack [--root-dist]", "write the deck as a file for `dekc ref` elsewhere"]],
377
+ agent: [
378
+ "dekc pack [--root-dist] dist/<deck>.tgz, for another project's `dekc ref <file>.tgz`",
379
+ ],
380
+ run: async ({ target, flags }) => {
381
+ const { packCommand } = await import("./pack.ts");
382
+ return packCommand(target, { rootDist: flags["root-dist"] });
383
+ },
384
+ output: { text: formatPack },
347
385
  }),
348
386
  show: result({
349
387
  scope: "deck",
@@ -408,7 +446,7 @@ export const COMMANDS = {
408
446
  text: formatCheck,
409
447
  notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
410
448
  paths: withDisplayDiagnostics,
411
- failure: (data) => diagnosticFailure("check", data, `dekc check ${data.slug}`),
449
+ failure: (data) => diagnosticFailure("check", data, `dekc check ${shellQuote(data.slug)}`),
412
450
  },
413
451
  }),
414
452
  shot: result({
@@ -623,7 +661,7 @@ export const COMMANDS = {
623
661
  group: "Output",
624
662
  overview: [
625
663
  ["dekc lint [--fix]", "check script.md against slides; --fix syncs first"],
626
- ["dekc lint --visual", "add overflow and contrast rules"],
664
+ ["dekc lint --visual", "add overflow, contrast, line-break, and alignment rules"],
627
665
  ["dekc lint --format sarif", "print diagnostics as SARIF, for code scanning"],
628
666
  ],
629
667
  agent: ["dekc lint [--fix] [--visual] [--format sarif]"],
@@ -791,19 +829,22 @@ export const COMMANDS = {
791
829
  flags: ["root-dist", "public"],
792
830
  usage: ["dekc pptx [deck] [--root-dist] [--public]"],
793
831
  summary:
794
- "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.",
832
+ "Write dist/<deck>.pptx, one slide per slide at its last beat: its plain boxes, pictures, and\ntext as PowerPoint's own over a picture of the rest, in the deck's theme, and its script in\nthe notes. Needs Playwright.",
795
833
  group: "Output",
796
834
  overview: [
797
- ["dekc pptx [--root-dist] [--public]", "write a PowerPoint file with editable text"],
835
+ [
836
+ "dekc pptx [--root-dist] [--public]",
837
+ "write a PowerPoint file with editable shapes and text",
838
+ ],
798
839
  ],
799
840
  agent: [
800
- "dekc pptx [--root-dist] [--public] text editable over a picture of each slide; script in notes",
841
+ "dekc pptx [--root-dist] [--public] boxes, pictures, and text editable over a picture of the rest; script in notes",
801
842
  ],
802
843
  run: async ({ target, flags }) => {
803
844
  const { pptxCommand } = await import("./pptx.ts");
804
845
  return pptxCommand(target, { rootDist: flags["root-dist"], public: flags.public });
805
846
  },
806
- output: { text: formatPdf },
847
+ output: { text: formatPptx, json: ({ outs }) => ({ outs }) },
807
848
  }),
808
849
  help: {
809
850
  kind: "help",
@@ -111,6 +111,7 @@ export const RESULT_FIELDS = {
111
111
  created: paths,
112
112
  updated: paths,
113
113
  next: z.array(z.string()),
114
+ warnings: z.array(z.string()).describe("Set when the theme came from a ref with no license."),
114
115
  }),
115
116
  ls: z.union([
116
117
  z.strictObject({
@@ -159,6 +160,10 @@ export const RESULT_FIELDS = {
159
160
  changed: z.boolean(),
160
161
  dir: z.string(),
161
162
  license: z.string().nullable(),
163
+ pack: z
164
+ .string()
165
+ .optional()
166
+ .describe("A pack's ref: the copy in packs/ to commit, which a clone unpacks it from."),
162
167
  warnings: z.array(z.string()),
163
168
  }),
164
169
  z.strictObject({
@@ -209,7 +214,15 @@ export const RESULT_FIELDS = {
209
214
  "State classes the theme selects that the player sets at runtime (is-current, is-shown); never written in markup.",
210
215
  ),
211
216
  tokens: z.array(z.strictObject({ name: z.string(), value: z.string() })),
212
- layouts: z.array(z.strictObject({ name: z.string(), example: z.string().optional() })),
217
+ layouts: z
218
+ .array(z.strictObject({ name: z.string(), example: z.string().optional() }))
219
+ .describe("The layouts whose names are plain words, which dek prints."),
220
+ unprintedLayouts: z
221
+ .number()
222
+ .optional()
223
+ .describe(
224
+ "How many layouts the theme has whose names are no plain word, left out of layouts; absent when none.",
225
+ ),
213
226
  layout: z.strictObject({ name: z.string(), example: z.string() }).optional(),
214
227
  ref: RefInfo.optional(),
215
228
  }),
@@ -413,6 +426,10 @@ export const RESULT_FIELDS = {
413
426
  }),
414
427
  pdf: z.strictObject({ outs: paths }),
415
428
  pptx: z.strictObject({ outs: paths }),
429
+ pack: z.strictObject({
430
+ outs: paths,
431
+ warnings: z.array(z.string()).describe("Once per project: the packs carry no license."),
432
+ }),
416
433
  } satisfies Record<ResultCommand, FieldsSchema>;
417
434
 
418
435
  type FieldsSchema = z.ZodObject | z.ZodUnion<readonly z.ZodObject[]>;
package/src/cli/files.ts CHANGED
@@ -4,6 +4,7 @@ import { bundledTheme } from "../core/bundled-theme.ts";
4
4
  import type { DekConfig } from "../core/config.ts";
5
5
  import { deckPaths } from "../core/deck-paths.ts";
6
6
  import { DekError } from "../core/error.ts";
7
+ import { shellQuote } from "../core/escape.ts";
7
8
  import { walkUp } from "../core/optional.ts";
8
9
  import { PLAYWRIGHT_INSTALL } from "../core/playwright.ts";
9
10
  import { readSourceIfExists, writeInside } from "../core/safe-fs.ts";
@@ -14,7 +15,14 @@ export function defaultTheme(): string {
14
15
  }
15
16
 
16
17
  export function defaultToml(): string {
17
- return "# dek project\n";
18
+ return `# dek project
19
+
20
+ [pack]
21
+ # The license \`dekc pack\` hands a deck over under. CC0-1.0 lets the reader reuse it
22
+ # freely, with no credit owed, as a template is meant to be used; CC-BY-4.0 asks for
23
+ # credit. Delete the line to hand decks over with no license.
24
+ license = "CC0-1.0"
25
+ `;
18
26
  }
19
27
 
20
28
  /**
@@ -309,8 +317,3 @@ function inNodeModules(root: string, ...path: string[]): boolean {
309
317
  walkUp(root, (dir) => existsSync(join(dir, "node_modules", ...path)) || undefined) === true
310
318
  );
311
319
  }
312
-
313
- /** `word` as one shell word: as it is when that is safe, else single-quoted. */
314
- export function shellQuote(word: string): string {
315
- return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
316
- }
package/src/cli/flags.ts CHANGED
@@ -41,7 +41,7 @@ export const FLAGS = {
41
41
  fix: { type: "boolean", text: "create missing skeleton slides before linting" },
42
42
  visual: {
43
43
  type: "boolean",
44
- text: "add the rendered rules: overflow and contrast (needs Playwright)",
44
+ text: "add the rendered rules: overflow, contrast, line breaks, and alignment (needs Playwright)",
45
45
  },
46
46
  shot: { type: "boolean", text: "also write a screenshot and return its path" },
47
47
  voice: { type: "boolean", text: "also return each sentence's reading and duration" },
package/src/cli/goto.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { DekError } from "../core/error.ts";
2
2
  import { deckRoutePath } from "../runtime/routes.ts";
3
3
  import { isPidAlive, readDevServerLock } from "../server/lock.ts";
4
+ import { deckFlag, deckWord } from "./address.ts";
5
+ import { isTypedCommand } from "./commands.ts";
4
6
  import { type DeckTarget, requireSection } from "./scope.ts";
5
7
 
6
8
  export type NavResult = {
@@ -82,7 +84,7 @@ function refusal(
82
84
  ): DekError {
83
85
  if (status === 404 && lock.deck !== undefined && lock.deck !== deckName) {
84
86
  return new DekError(`the running dev server serves deck "${lock.deck}", not "${deckName}"`, {
85
- hint: `pass --deck ${lock.deck}, or stop the server at ${lock.url} and run \`dekc ${deckName}\``,
87
+ hint: `pass ${deckFlag(lock.deck)}, or stop the server at ${lock.url} and run \`dekc ${isTypedCommand(deckName) ? deckFlag(deckName) : deckWord(deckName)}\``,
86
88
  });
87
89
  }
88
90
  if (status === 401) {