@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.
- package/README.md +192 -0
- package/package.json +54 -0
- package/src/blocks/board.js +210 -0
- package/src/blocks/callout.js +63 -0
- package/src/blocks/code.js +28 -0
- package/src/blocks/deck.js +76 -0
- package/src/blocks/diagram.js +265 -0
- package/src/blocks/element.js +51 -0
- package/src/blocks/graph.js +264 -0
- package/src/blocks/grid.js +156 -0
- package/src/blocks/index.js +106 -0
- package/src/blocks/list.js +50 -0
- package/src/blocks/placement.js +119 -0
- package/src/blocks/prose.js +28 -0
- package/src/blocks/quote.js +25 -0
- package/src/blocks/raw.js +47 -0
- package/src/blocks/registry.js +158 -0
- package/src/blocks/schema-parts.js +19 -0
- package/src/blocks/section.js +53 -0
- package/src/blocks/slide.js +104 -0
- package/src/blocks/stat.js +83 -0
- package/src/blocks/table.js +50 -0
- package/src/blocks/timeline.js +80 -0
- package/src/cli/commands/close.js +51 -0
- package/src/cli/commands/comments.js +73 -0
- package/src/cli/commands/debug.js +34 -0
- package/src/cli/commands/example.js +22 -0
- package/src/cli/commands/export.js +10 -0
- package/src/cli/commands/init.js +53 -0
- package/src/cli/commands/lint.js +93 -0
- package/src/cli/commands/list.js +54 -0
- package/src/cli/commands/open.js +31 -0
- package/src/cli/commands/promote.js +38 -0
- package/src/cli/commands/render.js +31 -0
- package/src/cli/commands/replay.js +49 -0
- package/src/cli/commands/schema.js +10 -0
- package/src/cli/commands/serve.js +178 -0
- package/src/cli/commands/setup.js +64 -0
- package/src/cli/commands/snapshot.js +29 -0
- package/src/cli/commands/templates.js +75 -0
- package/src/cli/deliver.js +54 -0
- package/src/cli/emit.js +29 -0
- package/src/cli/format.js +153 -0
- package/src/cli/index.js +365 -0
- package/src/cli/registry.js +18 -0
- package/src/core/blocks.js +147 -0
- package/src/core/diagram.js +282 -0
- package/src/core/errors.js +156 -0
- package/src/core/example.js +109 -0
- package/src/core/ir.js +62 -0
- package/src/core/lint-gates.js +427 -0
- package/src/core/lint.js +281 -0
- package/src/core/scan.js +84 -0
- package/src/core/schema.js +88 -0
- package/src/core/spec-check.js +33 -0
- package/src/core/validate.js +44 -0
- package/src/core/version.js +18 -0
- package/src/core/vocabulary.js +140 -0
- package/src/delivery/atomic.js +71 -0
- package/src/delivery/comments.js +180 -0
- package/src/delivery/home.js +70 -0
- package/src/delivery/open.js +31 -0
- package/src/delivery/project.js +141 -0
- package/src/delivery/read.js +109 -0
- package/src/delivery/run.js +95 -0
- package/src/delivery/scaffold-blueprints.js +728 -0
- package/src/delivery/store.js +219 -0
- package/src/delivery/template-extensions.js +183 -0
- package/src/delivery/template-format.js +112 -0
- package/src/delivery/template-package.js +376 -0
- package/src/delivery/template-promote.js +240 -0
- package/src/delivery/template-scaffold.js +181 -0
- package/src/delivery/templates.js +192 -0
- package/src/delivery/toml.js +195 -0
- package/src/delivery/write.js +35 -0
- package/src/export/browser.js +130 -0
- package/src/export/index.js +96 -0
- package/src/export/pdf.js +25 -0
- package/src/export/png.js +40 -0
- package/src/export/pptx.js +48 -0
- package/src/export/slides.js +33 -0
- package/src/export/snapshot.js +33 -0
- package/src/layouts/article.js +103 -0
- package/src/layouts/canvas.js +144 -0
- package/src/layouts/card.js +128 -0
- package/src/layouts/deck.js +88 -0
- package/src/layouts/index.js +90 -0
- package/src/layouts/one-page.js +161 -0
- package/src/layouts/registry.js +251 -0
- package/src/layouts/resume.js +172 -0
- package/src/layouts/template-index.js +78 -0
- package/src/parser/artifact.js +38 -0
- package/src/parser/container.js +103 -0
- package/src/parser/index.js +223 -0
- package/src/parser/tokens.js +265 -0
- package/src/render/board-filter.client.js +80 -0
- package/src/render/compile.js +29 -0
- package/src/render/context.js +98 -0
- package/src/render/element.js +32 -0
- package/src/render/fonts.js +129 -0
- package/src/render/graph-hover.client.js +148 -0
- package/src/render/html.js +52 -0
- package/src/render/index.js +241 -0
- package/src/render/measure.js +60 -0
- package/src/render/placement.js +136 -0
- package/src/render/playback.client.js +74 -0
- package/src/render/scale-to-fit.client.js +136 -0
- package/src/render/scale.js +41 -0
- package/src/render/skeleton.js +131 -0
- package/src/render/ssr.js +24 -0
- package/src/render/styles.js +56 -0
- package/src/render/templates.js +191 -0
- package/src/serve/daemon.js +117 -0
- package/src/serve/overlay.js +213 -0
- package/src/serve/protocol.js +36 -0
- package/src/serve/server.js +264 -0
- package/templates/kami/cards/components.js +40 -0
- package/templates/kami/cards/index.js +25 -0
- package/templates/kami/cards/manifest.js +67 -0
- package/templates/kami/cards/styles.css +389 -0
- package/templates/kami/long-form/components.js +102 -0
- package/templates/kami/long-form/index.js +25 -0
- package/templates/kami/long-form/manifest.js +87 -0
- package/templates/kami/long-form/styles.css +481 -0
- package/templates/kami/one-page/components.js +48 -0
- package/templates/kami/one-page/index.js +27 -0
- package/templates/kami/one-page/manifest.js +65 -0
- package/templates/kami/one-page/styles.css +375 -0
- package/templates/kami/resume/components.js +51 -0
- package/templates/kami/resume/index.js +27 -0
- package/templates/kami/resume/manifest.js +65 -0
- package/templates/kami/resume/styles.css +424 -0
- package/templates/kami/slides/components.js +41 -0
- package/templates/kami/slides/index.js +26 -0
- package/templates/kami/slides/manifest.js +64 -0
- package/templates/kami/slides/styles.css +406 -0
package/src/cli/index.js
ADDED
|
@@ -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
|
+
}
|