@kamishibai/sdk 0.1.1

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 (136) hide show
  1. package/README.md +192 -0
  2. package/package.json +54 -0
  3. package/src/blocks/board.js +210 -0
  4. package/src/blocks/callout.js +63 -0
  5. package/src/blocks/code.js +28 -0
  6. package/src/blocks/deck.js +76 -0
  7. package/src/blocks/diagram.js +265 -0
  8. package/src/blocks/element.js +51 -0
  9. package/src/blocks/graph.js +264 -0
  10. package/src/blocks/grid.js +156 -0
  11. package/src/blocks/index.js +106 -0
  12. package/src/blocks/list.js +50 -0
  13. package/src/blocks/placement.js +119 -0
  14. package/src/blocks/prose.js +28 -0
  15. package/src/blocks/quote.js +25 -0
  16. package/src/blocks/raw.js +47 -0
  17. package/src/blocks/registry.js +158 -0
  18. package/src/blocks/schema-parts.js +19 -0
  19. package/src/blocks/section.js +53 -0
  20. package/src/blocks/slide.js +104 -0
  21. package/src/blocks/stat.js +83 -0
  22. package/src/blocks/table.js +50 -0
  23. package/src/blocks/timeline.js +80 -0
  24. package/src/cli/commands/close.js +51 -0
  25. package/src/cli/commands/comments.js +73 -0
  26. package/src/cli/commands/debug.js +34 -0
  27. package/src/cli/commands/example.js +22 -0
  28. package/src/cli/commands/export.js +10 -0
  29. package/src/cli/commands/init.js +53 -0
  30. package/src/cli/commands/lint.js +93 -0
  31. package/src/cli/commands/list.js +54 -0
  32. package/src/cli/commands/open.js +31 -0
  33. package/src/cli/commands/promote.js +38 -0
  34. package/src/cli/commands/render.js +31 -0
  35. package/src/cli/commands/replay.js +49 -0
  36. package/src/cli/commands/schema.js +10 -0
  37. package/src/cli/commands/serve.js +178 -0
  38. package/src/cli/commands/setup.js +64 -0
  39. package/src/cli/commands/snapshot.js +29 -0
  40. package/src/cli/commands/templates.js +75 -0
  41. package/src/cli/deliver.js +54 -0
  42. package/src/cli/emit.js +29 -0
  43. package/src/cli/format.js +153 -0
  44. package/src/cli/index.js +365 -0
  45. package/src/cli/registry.js +18 -0
  46. package/src/core/blocks.js +147 -0
  47. package/src/core/diagram.js +282 -0
  48. package/src/core/errors.js +156 -0
  49. package/src/core/example.js +109 -0
  50. package/src/core/ir.js +62 -0
  51. package/src/core/lint-gates.js +427 -0
  52. package/src/core/lint.js +281 -0
  53. package/src/core/scan.js +84 -0
  54. package/src/core/schema.js +88 -0
  55. package/src/core/spec-check.js +33 -0
  56. package/src/core/validate.js +44 -0
  57. package/src/core/version.js +18 -0
  58. package/src/core/vocabulary.js +140 -0
  59. package/src/delivery/atomic.js +71 -0
  60. package/src/delivery/comments.js +180 -0
  61. package/src/delivery/home.js +70 -0
  62. package/src/delivery/open.js +31 -0
  63. package/src/delivery/project.js +141 -0
  64. package/src/delivery/read.js +109 -0
  65. package/src/delivery/run.js +95 -0
  66. package/src/delivery/scaffold-blueprints.js +728 -0
  67. package/src/delivery/store.js +219 -0
  68. package/src/delivery/template-extensions.js +183 -0
  69. package/src/delivery/template-format.js +112 -0
  70. package/src/delivery/template-package.js +376 -0
  71. package/src/delivery/template-promote.js +240 -0
  72. package/src/delivery/template-scaffold.js +181 -0
  73. package/src/delivery/templates.js +192 -0
  74. package/src/delivery/toml.js +195 -0
  75. package/src/delivery/write.js +35 -0
  76. package/src/export/browser.js +130 -0
  77. package/src/export/index.js +96 -0
  78. package/src/export/pdf.js +25 -0
  79. package/src/export/png.js +40 -0
  80. package/src/export/pptx.js +48 -0
  81. package/src/export/slides.js +33 -0
  82. package/src/export/snapshot.js +33 -0
  83. package/src/layouts/article.js +103 -0
  84. package/src/layouts/canvas.js +144 -0
  85. package/src/layouts/card.js +128 -0
  86. package/src/layouts/deck.js +88 -0
  87. package/src/layouts/index.js +90 -0
  88. package/src/layouts/one-page.js +161 -0
  89. package/src/layouts/registry.js +251 -0
  90. package/src/layouts/resume.js +172 -0
  91. package/src/layouts/template-index.js +78 -0
  92. package/src/parser/artifact.js +38 -0
  93. package/src/parser/container.js +103 -0
  94. package/src/parser/index.js +223 -0
  95. package/src/parser/tokens.js +265 -0
  96. package/src/render/board-filter.client.js +80 -0
  97. package/src/render/compile.js +29 -0
  98. package/src/render/context.js +98 -0
  99. package/src/render/element.js +32 -0
  100. package/src/render/fonts.js +129 -0
  101. package/src/render/graph-hover.client.js +148 -0
  102. package/src/render/html.js +52 -0
  103. package/src/render/index.js +241 -0
  104. package/src/render/measure.js +60 -0
  105. package/src/render/placement.js +136 -0
  106. package/src/render/playback.client.js +74 -0
  107. package/src/render/scale-to-fit.client.js +136 -0
  108. package/src/render/scale.js +41 -0
  109. package/src/render/skeleton.js +131 -0
  110. package/src/render/ssr.js +24 -0
  111. package/src/render/styles.js +56 -0
  112. package/src/render/templates.js +191 -0
  113. package/src/serve/daemon.js +117 -0
  114. package/src/serve/overlay.js +213 -0
  115. package/src/serve/protocol.js +36 -0
  116. package/src/serve/server.js +264 -0
  117. package/templates/kami/cards/components.js +40 -0
  118. package/templates/kami/cards/index.js +25 -0
  119. package/templates/kami/cards/manifest.js +67 -0
  120. package/templates/kami/cards/styles.css +389 -0
  121. package/templates/kami/long-form/components.js +102 -0
  122. package/templates/kami/long-form/index.js +25 -0
  123. package/templates/kami/long-form/manifest.js +87 -0
  124. package/templates/kami/long-form/styles.css +481 -0
  125. package/templates/kami/one-page/components.js +48 -0
  126. package/templates/kami/one-page/index.js +27 -0
  127. package/templates/kami/one-page/manifest.js +65 -0
  128. package/templates/kami/one-page/styles.css +375 -0
  129. package/templates/kami/resume/components.js +51 -0
  130. package/templates/kami/resume/index.js +27 -0
  131. package/templates/kami/resume/manifest.js +65 -0
  132. package/templates/kami/resume/styles.css +424 -0
  133. package/templates/kami/slides/components.js +41 -0
  134. package/templates/kami/slides/index.js +26 -0
  135. package/templates/kami/slides/manifest.js +64 -0
  136. package/templates/kami/slides/styles.css +406 -0
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env node
2
+ import { Command, CommanderError } from 'commander'
3
+ import { engineVersion } from '../core/version.js'
4
+ import { EXIT, usageError, commandLoadError } from '../core/errors.js'
5
+ import { emit, toFailure } from './emit.js'
6
+
7
+ /**
8
+ * Command modules are loaded *inside* their action, never at the top level.
9
+ *
10
+ * Eagerly importing all fifteen dragged every command's dependency tree into
11
+ * every invocation: `--version` paid ~208 ms to answer from a string it already
12
+ * had (量測報告 §1.2), and CLI-spawning tests paid it once per spawn until they
13
+ * blew the 5 s default budget (§1.5). An `import()` inside the action loads
14
+ * exactly the one module the invocation actually runs.
15
+ *
16
+ * Every one of those loads goes through `load`, because a rejected `import()`
17
+ * would otherwise reach the generic handler and be reported as
18
+ * `KSB_PARSE_FAILED` — telling an agent its *document* failed to parse when the
19
+ * truth is a broken install.
20
+ */
21
+ const load = async (command, importer) => {
22
+ try {
23
+ return await importer()
24
+ } catch (cause) {
25
+ throw commandLoadError(command, cause)
26
+ }
27
+ }
28
+
29
+ const argv = process.argv
30
+ const wantsJson = argv.includes('--json')
31
+
32
+ const HELP_CODES = new Set(['commander.helpDisplayed', 'commander.help'])
33
+ const VERSION_CODE = 'commander.version'
34
+
35
+ const HELP_FLAGS = new Set(['-h', '--help', 'help'])
36
+ const VERSION_FLAGS = new Set(['-v', '--version'])
37
+
38
+ /** The subcommands. Presence of one is what makes an invocation a command. */
39
+ const COMMANDS = Object.freeze([
40
+ 'render',
41
+ 'lint',
42
+ 'example',
43
+ 'schema',
44
+ 'list',
45
+ 'open',
46
+ 'replay',
47
+ 'export',
48
+ 'snapshot',
49
+ 'setup',
50
+ 'serve',
51
+ 'close',
52
+ 'comments',
53
+ 'templates',
54
+ 'init',
55
+ 'promote',
56
+ 'debug',
57
+ ])
58
+
59
+ /** The single wording for "you gave no command", shared by both output paths. */
60
+ const NO_COMMAND_MESSAGE = 'no command given; try `kamishibai --help`'
61
+
62
+ /**
63
+ * Commander raises the same help error whether the user *asked* for help or
64
+ * merely gave no command. Only the former is a success: treating both as
65
+ * exit 0 turned `kamishibai --json` into exit 0 with empty stdout, which any
66
+ * downstream `JSON.parse` would crash on while looking like success (A7).
67
+ */
68
+ const askedForHelp = argv.slice(2).some((a) => HELP_FLAGS.has(a))
69
+ const askedForVersion = argv.slice(2).some((a) => VERSION_FLAGS.has(a))
70
+
71
+ /**
72
+ * Detect "no command" by looking for a subcommand token, not by counting argv:
73
+ * a bare `--json` makes argv長 enough to slip past a length check and then falls
74
+ * through to commander, whose help error message is the internal token
75
+ * `(outputHelp)` — leaking implementation detail and making the two output
76
+ * paths disagree on the wording (A7).
77
+ */
78
+ const hasCommand = argv.slice(2).some((a) => COMMANDS.includes(a))
79
+
80
+ /** Commander's own output, captured so `--json` can return it as a JSON field. */
81
+ const captured = []
82
+
83
+ const buildProgram = () => {
84
+ const program = new Command()
85
+ program
86
+ .name('kamishibai')
87
+ .description('Agent Presentation SDK — 結構化內容渲染為離線單檔產物')
88
+ .version(engineVersion(), '-v, --version')
89
+ .option('--json', '以 JSON 輸出結果(可置於指令前或後)')
90
+ .exitOverride()
91
+
92
+ // With --json, stdout must stay a single JSON object, so commander's own
93
+ // help/usage text is captured instead of printed — help then comes back as
94
+ // a JSON field, and usage errors as a KSB_ error object.
95
+ if (wantsJson) {
96
+ program.configureOutput({
97
+ writeOut: (str) => captured.push(str),
98
+ writeErr: (str) => captured.push(str),
99
+ })
100
+ }
101
+
102
+ program
103
+ .command('render')
104
+ .description('把 Markdown 超集或 block tree JSON 渲染成離線單檔 HTML(`-` 讀 stdin)')
105
+ .argument('<input>', 'Markdown 超集/block tree JSON 檔路徑,或 `-` 表示 stdin')
106
+ .option('-o, --out <path>', '產物輸出路徑')
107
+ .option('-t, --template <key>', '覆寫模板 <namespace>/<name>')
108
+ .option('-g, --generator <name>', 'IR generator 欄位值')
109
+ .option('-p, --project <name>', '覆寫中央產物庫的專案名(預設依四層解析)')
110
+ .option('--json', '以 JSON 輸出結果')
111
+ .action(async (input, options) => {
112
+ const { renderCommand } = await load('render', () => import('./commands/render.js'))
113
+ const { result, exitCode } = await renderCommand(input, options)
114
+ emit({ command: 'render', result, json: wantsJson, exitCode })
115
+ })
116
+
117
+ program
118
+ .command('replay')
119
+ .description('由產物內嵌 IR 重繪(換模板/升版/換皮)')
120
+ .argument('<artifact>', '既有產物 HTML 路徑')
121
+ .option('-o, --out <path>', '產物輸出路徑')
122
+ .option('-t, --template <key>', '覆寫模板 <namespace>/<name>')
123
+ .option('-g, --generator <name>', 'IR generator 欄位值')
124
+ .option('-p, --project <name>', '覆寫中央產物庫的專案名(預設依四層解析)')
125
+ .option('--json', '以 JSON 輸出結果')
126
+ .action(async (artifact, options) => {
127
+ const { replayCommand } = await load('replay', () => import('./commands/replay.js'))
128
+ const { result, exitCode } = await replayCommand(artifact, options)
129
+ emit({ command: 'replay', result, json: wantsJson, exitCode })
130
+ })
131
+
132
+ program
133
+ .command('list')
134
+ .description('列出當前專案的中央產物庫呈現史')
135
+ .option('-p, --project <name>', '覆寫專案名(預設 .kamishibai.toml → git root → cwd)')
136
+ .option('--json', '以 JSON 輸出結果')
137
+ .action(async (options) => {
138
+ const { listCommand } = await load('list', () => import('./commands/list.js'))
139
+ const { result, exitCode } = listCommand(options)
140
+ emit({ command: 'list', result, json: wantsJson, exitCode })
141
+ })
142
+
143
+ program
144
+ .command('open')
145
+ .description('由中央產物庫解析並開啟產物(跨平台開啟鏈)')
146
+ .argument('<name>', '產物名稱,或 `latest` 表示最新一份')
147
+ .option('-p, --project <name>', '覆寫專案名(預設 .kamishibai.toml → git root → cwd)')
148
+ .option('--dry-run', '只解析路徑、不開啟瀏覽器')
149
+ .option('--json', '以 JSON 輸出結果')
150
+ .action(async (name, options) => {
151
+ const { openCommand } = await load('open', () => import('./commands/open.js'))
152
+ const { result, exitCode } = openCommand(name, options)
153
+ emit({ command: 'open', result, json: wantsJson, exitCode })
154
+ })
155
+
156
+ program
157
+ .command('export')
158
+ .description('把產物匯出成附屬格式(document → pdf、deck → pptx)')
159
+ .argument('<artifact>', '既有產物 HTML 路徑')
160
+ .requiredOption('--to <format>', '匯出格式:pdf 或 pptx')
161
+ .option('-o, --out <path>', '匯出檔輸出路徑(預設 out/<產物名>.<格式>)')
162
+ .option('--json', '以 JSON 輸出結果')
163
+ .action(async (artifact, options) => {
164
+ const { exportCommand } = await load('export', () => import('./commands/export.js'))
165
+ const { result, exitCode } = await exportCommand(artifact, options)
166
+ emit({ command: 'export', result, json: wantsJson, exitCode })
167
+ })
168
+
169
+ program
170
+ .command('snapshot')
171
+ .description('把產物截成 PNG,讓 Agent(與人)看得到視覺效果')
172
+ .argument('<artifact>', '既有產物 HTML 路徑')
173
+ .option('-o, --out <path>', 'PNG 輸出路徑(預設 out/<產物名>.png)')
174
+ .option('--slide <n>', 'deck 產物要截第幾張(預設第 1 張)')
175
+ .option('--json', '以 JSON 輸出結果')
176
+ .action(async (artifact, options) => {
177
+ const { snapshotCommand } = await load('snapshot', () => import('./commands/snapshot.js'))
178
+ const { result, exitCode } = await snapshotCommand(artifact, options)
179
+ emit({ command: 'snapshot', result, json: wantsJson, exitCode })
180
+ })
181
+
182
+ program
183
+ .command('setup')
184
+ .description('初始化環境:建中央儲存庫、確認(必要時安裝)渲染用瀏覽器')
185
+ .option('--dry-run', '只回報狀態、不建目錄也不安裝')
186
+ .option('--json', '以 JSON 輸出結果')
187
+ .action(async (options) => {
188
+ const { setupCommand } = await load('setup', () => import('./commands/setup.js'))
189
+ const { result, exitCode } = await setupCommand(options)
190
+ emit({ command: 'setup', result, json: wantsJson, exitCode })
191
+ })
192
+
193
+ program
194
+ .command('serve')
195
+ .description('起本地預覽伺服器:來源檔一變就重繪並推播 reload(SPEC §10.2 watch 模式)')
196
+ .argument('<input>', 'Markdown 超集/block tree JSON 檔路徑')
197
+ .option('--port <n>', '指定連接埠(預設由系統挑一個空的)')
198
+ .option('-t, --template <key>', '覆寫模板 <namespace>/<name>')
199
+ .option('-g, --generator <name>', 'IR generator 欄位值')
200
+ .option('-p, --project <name>', '覆寫中央產物庫的專案名(預設依四層解析)')
201
+ .option('--json', '以 JSON 輸出結果')
202
+ .action(async (input, options) => {
203
+ const { serveCommand } = await load('serve', () => import('./commands/serve.js'))
204
+ const { result, exitCode } = await serveCommand(input, options)
205
+ emit({ command: 'serve', result, json: wantsJson, exitCode })
206
+ })
207
+
208
+ program
209
+ .command('close')
210
+ .description('終止本 SDK 起的預覽伺服器(沒有在跑也算成功)')
211
+ .option('--json', '以 JSON 輸出結果')
212
+ .action(async (options) => {
213
+ const { closeCommand } = await load('close', () => import('./commands/close.js'))
214
+ const { result, exitCode } = closeCommand(options)
215
+ emit({ command: 'close', result, json: wantsJson, exitCode })
216
+ })
217
+
218
+ program
219
+ .command('comments')
220
+ .description('讀寫產物留言(block id 錨定):列出/`resolve <產物> <id>`/`add <產物> <blockId> <文字>`')
221
+ .argument('<args...>', '`<產物>`、`resolve <產物> <id>` 或 `add <產物> <blockId> <文字>`')
222
+ .option('--json', '以 JSON 輸出結果')
223
+ .action(async (args) => {
224
+ const { commentsCommand } = await load('comments', () => import('./commands/comments.js'))
225
+ const { result, exitCode } = commentsCommand(args)
226
+ emit({ command: 'comments', result, json: wantsJson, exitCode })
227
+ })
228
+
229
+ program
230
+ .command('templates')
231
+ .description('列出中央儲存庫已註冊的模板包')
232
+ .option('--json', '以 JSON 輸出結果')
233
+ .action(async (options) => {
234
+ const { templatesCommand } = await load('templates', () => import('./commands/templates.js'))
235
+ const { result, exitCode } = templatesCommand(options)
236
+ emit({ command: 'templates', result, json: wantsJson, exitCode })
237
+ })
238
+
239
+ program
240
+ .command('init')
241
+ .description('產生模板包骨架(manifest+stylesheet+預設 md),開箱即可 render')
242
+ .argument('<key>', '模板鍵 <namespace>/<name>(出廠 namespace 保留)')
243
+ // 這一句必須列出腳手架**真的**做得出來的每一個文體(封緘 F2):help 是
244
+ // 使用者唯一的能力面目錄,而它原本只說「預設 article」——於是 F2f 讓腳手架
245
+ // 學會 deck 的那天,能力長了、目錄沒長,讀 help 的人除了猜沒有別的辦法。
246
+ // 這裡不 import 那份清單:F4 已把序幕的 static import 釘成白名單,為了一句
247
+ // 說明拉一個模組進每一次 `--version` 是划不來的交易。改由
248
+ // `test_f3_init_is_in_help` 拿 SCAFFOLD_SUPPORTED_LAYOUTS 組出這一整行來對,
249
+ // 清單長大而這裡沒跟上就會紅。
250
+ .option(
251
+ '-l, --layout <name>',
252
+ '所騎的文體(article/card/deck/one-page/resume;預設 article)',
253
+ )
254
+ .option('--json', '以 JSON 輸出結果')
255
+ .action(async (key, options) => {
256
+ const { initCommand } = await load('init', () => import('./commands/init.js'))
257
+ const { result, exitCode } = initCommand(key, options)
258
+ emit({ command: 'init', result, json: wantsJson, exitCode })
259
+ })
260
+
261
+ program
262
+ .command('promote')
263
+ .description('把臨摹草稿 `draft/<名>` 轉正成正式模板包(目錄搬移+manifest 改寫)')
264
+ .argument('<from>', '來源模板鍵,必須是 `draft/<名>`')
265
+ .argument('<to>', '目標模板鍵 <namespace>/<name>(出廠 namespace 保留)')
266
+ .option('--json', '以 JSON 輸出結果')
267
+ .action(async (from, to, options) => {
268
+ const { promoteCommand } = await load('promote', () => import('./commands/promote.js'))
269
+ const { result, exitCode } = promoteCommand(from, to, options)
270
+ emit({ command: 'promote', result, json: wantsJson, exitCode })
271
+ })
272
+
273
+ program
274
+ .command('debug')
275
+ .description('診斷:儲存庫位置、模板包數、瀏覽器狀態、引擎版本')
276
+ .option('--json', '以 JSON 輸出結果')
277
+ .action(async (options) => {
278
+ const { debugCommand } = await load('debug', () => import('./commands/debug.js'))
279
+ const { result, exitCode } = await debugCommand(options)
280
+ emit({ command: 'debug', result, json: wantsJson, exitCode })
281
+ })
282
+
283
+ program
284
+ .command('lint')
285
+ .description('驗證產物:零外部請求、內嵌 IR 齊備且通過 schema')
286
+ .argument('[artifact]', '產物 HTML 路徑(`--rules` 時可省略)')
287
+ .option('--rules', '列出 lint 規則(code/掃描範圍/pattern/說明),不驗證產物')
288
+ .option('--json', '以 JSON 輸出結果')
289
+ .action(async (artifact, options) => {
290
+ const { lintCommand } = await load('lint', () => import('./commands/lint.js'))
291
+ const { result, exitCode } = await lintCommand(artifact, options)
292
+ emit({ command: 'lint', result, json: wantsJson, exitCode })
293
+ })
294
+
295
+ program
296
+ .command('example')
297
+ .description('輸出合法範例(`doc`/`deck` 為 Markdown 超集,其餘為 block JSON)')
298
+ .argument('[kind]', '範例類型:doc、deck 或 block 名稱', 'doc')
299
+ .option('--json', '以 JSON 輸出結果')
300
+ .action(async (kind) => {
301
+ const { exampleCommand } = await load('example', () => import('./commands/example.js'))
302
+ const { result, exitCode } = exampleCommand(kind)
303
+ emit({ command: 'example', result, json: wantsJson, exitCode })
304
+ })
305
+
306
+ program
307
+ .command('schema')
308
+ .description('輸出 IR 的 JSON Schema(draft 2020-12)')
309
+ .option('--json', '以 JSON 輸出結果')
310
+ .action(async () => {
311
+ const { schemaCommand } = await load('schema', () => import('./commands/schema.js'))
312
+ const { result, exitCode } = schemaCommand()
313
+ emit({ command: 'schema', result, json: wantsJson, exitCode })
314
+ })
315
+
316
+ return program
317
+ }
318
+
319
+ async function main() {
320
+ if (!hasCommand && !askedForHelp && !askedForVersion) throw usageError(NO_COMMAND_MESSAGE)
321
+ await buildProgram().parseAsync(argv)
322
+ }
323
+
324
+ /**
325
+ * `--help` / `--version` succeed only when the user actually asked for them.
326
+ * Without `--json` commander has already written the text to stdout itself;
327
+ * with `--json` that text was captured instead, and comes back as a field so
328
+ * stdout stays a single JSON object.
329
+ */
330
+ const emitHelp = (field, value) => {
331
+ if (wantsJson) {
332
+ emit({ command: 'cli', result: { ok: true, [field]: value }, json: true, exitCode: EXIT.OK })
333
+ return
334
+ }
335
+ process.exitCode = EXIT.OK
336
+ }
337
+
338
+ /**
339
+ * Commander sets some error messages to bare internal tokens such as
340
+ * `(outputHelp)`. Those are event names, not diagnostics — never show them.
341
+ */
342
+ const INTERNAL_TOKEN = /^\(\w+\)$/
343
+
344
+ const userFacingMessage = (raw) => {
345
+ const message = String(raw ?? '').replace(/^error:\s*/, '').trim()
346
+ return message.length === 0 || INTERNAL_TOKEN.test(message) ? NO_COMMAND_MESSAGE : message
347
+ }
348
+
349
+ main().catch((error) => {
350
+ if (error instanceof CommanderError) {
351
+ if (error.code === VERSION_CODE && askedForVersion) {
352
+ emitHelp('version', engineVersion())
353
+ return
354
+ }
355
+ if (HELP_CODES.has(error.code) && askedForHelp) {
356
+ emitHelp('help', captured.join('').trimEnd())
357
+ return
358
+ }
359
+ const { result } = toFailure(usageError(userFacingMessage(error.message)))
360
+ emit({ command: 'cli', result, json: wantsJson, exitCode: EXIT.USAGE })
361
+ return
362
+ }
363
+ const { result, exitCode } = toFailure(error)
364
+ emit({ command: 'cli', result, json: wantsJson, exitCode })
365
+ })
@@ -0,0 +1,18 @@
1
+ import { engineVersion } from '../core/version.js'
2
+ import { builtinManifests } from '../render/index.js'
3
+ import { registerTemplatePackage } from '../delivery/templates.js'
4
+
5
+ /**
6
+ * The one place where "what the engine can draw" (render layer) meets "what the
7
+ * store records" (delivery layer). Wiring them here, at the CLI boundary, is
8
+ * what keeps the store from importing templates and the renderer from knowing
9
+ * where `~/.kamishibai` is.
10
+ *
11
+ * It runs on `setup`, on every delivery, and on `templates`/`debug` — a first
12
+ * touch of any kind registers the built-ins, so an Agent that never ran `setup`
13
+ * still finds the namespace populated (CONTRACT E3「`setup`(或首觸)」).
14
+ */
15
+ export function registerBuiltinTemplates(env = process.env) {
16
+ const engine = engineVersion()
17
+ return builtinManifests().map((manifest) => registerTemplatePackage({ manifest, engine, env }))
18
+ }
@@ -0,0 +1,147 @@
1
+ import { blockModule, blockModules, blockTypes, registryVersion } from '../blocks/index.js'
2
+ import { CALLOUT_VARIANTS } from '../blocks/callout.js'
3
+ import { RAW_SUBTYPES } from '../blocks/raw.js'
4
+ import { diagramParseError } from '../blocks/diagram.js'
5
+
6
+ /**
7
+ * Tree-shaped operations over the canonical block vocabulary.
8
+ *
9
+ * What a block *is* no longer lives here: each type owns one module under
10
+ * `src/blocks/`, and this file derives everything type-specific from the
11
+ * registry. Only genuinely cross-type work stays — id assignment, the document
12
+ * walk, and the `doc` envelope, none of which any single block can own.
13
+ */
14
+
15
+ /** The canonical block vocabulary, in registration order (SPEC §3). */
16
+ export const BLOCK_TYPES = Object.freeze(blockTypes())
17
+
18
+ export { CALLOUT_VARIANTS, RAW_SUBTYPES, diagramParseError }
19
+
20
+ export const META_KEY_ORDER = Object.freeze(['title', 'kicker', 'subtitle', 'author', 'date'])
21
+
22
+ /** The `doc` envelope — the one node that is not a block and has no module. */
23
+ export const doc = ({ meta, children }) => Object.freeze({ type: 'doc', meta, children })
24
+
25
+ /**
26
+ * Block factories, resolved through the registry on every access.
27
+ *
28
+ * `create.section({…})` reads like the plain factory it replaced, but the
29
+ * function comes from the section module rather than from a list maintained
30
+ * here — the list is exactly what CONTRACT A3 forbids the core to keep. Live
31
+ * lookup rather than a snapshot, so a block registered after this module loaded
32
+ * is still constructible.
33
+ */
34
+ export const create = new Proxy(
35
+ // Extensible on purpose: a Proxy over a frozen target may not report a
36
+ // property the target lacks, which would make every lookup return undefined.
37
+ {},
38
+ {
39
+ get: (_target, type) => (typeof type === 'string' ? blockModule(type)?.create : undefined),
40
+ has: (_target, type) => blockModule(String(type)) !== undefined,
41
+ },
42
+ )
43
+
44
+ /**
45
+ * A block nests its sub-blocks under one of two field shapes, and which key a
46
+ * type uses is the type's own knowledge — declared by its module's `nesting`,
47
+ * never listed again here. Every generic walk (id assignment, traversal) is
48
+ * written against these derived sets rather than against `children` alone: a
49
+ * new container type that forgot to declare its nesting would otherwise
50
+ * silently lose ids for everything inside it.
51
+ *
52
+ * - arrays (`children` / `slides`) — a flat array of blocks
53
+ * - matrices (`items`) — an array *of arrays* of blocks
54
+ */
55
+ let nestingCache = { version: -1, arrays: [], matrices: [] }
56
+
57
+ function nestingKeys() {
58
+ const version = registryVersion()
59
+ if (nestingCache.version === version) return nestingCache
60
+ const arrays = new Set(['children']) // the `doc` envelope's own shape
61
+ const matrices = new Set()
62
+ for (const mod of blockModules()) {
63
+ for (const key of mod.nesting?.arrays ?? []) arrays.add(key)
64
+ for (const key of mod.nesting?.matrices ?? []) matrices.add(key)
65
+ }
66
+ nestingCache = { version, arrays: [...arrays], matrices: [...matrices] }
67
+ return nestingCache
68
+ }
69
+
70
+ /** The nesting field names currently in the vocabulary — exported for inspection. */
71
+ export const blockArrayKeys = () => Object.freeze([...nestingKeys().arrays])
72
+ export const blockMatrixKeys = () => Object.freeze([...nestingKeys().matrices])
73
+
74
+ /**
75
+ * Assign deterministic document-order ids (b1, b2, …) to a block tree.
76
+ * Returns a new tree; the input is untouched.
77
+ */
78
+ export function withIds(node, counter = { n: 0 }, keys = nestingKeys()) {
79
+ counter.n += 1
80
+ const id = `b${counter.n}`
81
+ const { id: _previousId, ...rest } = node
82
+ // Own fields first, nested ones after — key order here is the key order of
83
+ // the embedded IR JSON, and therefore of the artifact's bytes.
84
+ const isNested = (key, value) =>
85
+ Array.isArray(value) && (keys.matrices.includes(key) || keys.arrays.includes(key))
86
+ const out = { id }
87
+ for (const [key, value] of Object.entries(rest)) {
88
+ if (!isNested(key, value)) out[key] = value
89
+ }
90
+ for (const key of keys.matrices) {
91
+ const value = rest[key]
92
+ if (!Array.isArray(value)) continue
93
+ out[key] = value.map((group) =>
94
+ Array.isArray(group) ? group.map((block) => withIds(block, counter, keys)) : group,
95
+ )
96
+ }
97
+ for (const key of keys.arrays) {
98
+ const value = rest[key]
99
+ if (!Array.isArray(value)) continue
100
+ out[key] = value.map((block) => withIds(block, counter, keys))
101
+ }
102
+ return out
103
+ }
104
+
105
+ /** Depth-first walk in document order, across every nesting shape. */
106
+ export function* walkBlocks(node, path = 'doc', keys = nestingKeys()) {
107
+ yield { block: node, path }
108
+ for (const key of keys.matrices) {
109
+ if (!Array.isArray(node[key])) continue
110
+ for (const [group, blocks] of node[key].entries()) {
111
+ if (!Array.isArray(blocks)) continue
112
+ for (const [index, child] of blocks.entries()) {
113
+ yield* walkBlocks(child, `${path}.${key}[${group}][${index}]`, keys)
114
+ }
115
+ }
116
+ }
117
+ for (const key of keys.arrays) {
118
+ if (!Array.isArray(node[key])) continue
119
+ for (const [index, child] of node[key].entries()) {
120
+ yield* walkBlocks(child, `${path}.${key}[${index}]`, keys)
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Every block id a tree carries — the set of anchors a comment may name
127
+ * (CONTRACT E2). Derived from the same walk that assigns the ids, so an anchor
128
+ * can never be legal in one place and unknown in the other.
129
+ *
130
+ * @returns {Set<string>}
131
+ */
132
+ export function collectBlockIds(node) {
133
+ const ids = new Set()
134
+ if (node === undefined || node === null) return ids
135
+ for (const { block } of walkBlocks(node)) {
136
+ if (typeof block?.id === 'string') ids.add(block.id)
137
+ }
138
+ return ids
139
+ }
140
+
141
+ /** The first `deck` block in a tree, or undefined for a document artifact. */
142
+ export function findDeck(node) {
143
+ for (const { block } of walkBlocks(node)) {
144
+ if (block?.type === 'deck') return block
145
+ }
146
+ return undefined
147
+ }