@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,34 @@
1
+ import { EXIT } from '../../core/errors.js'
2
+ import { engineVersion } from '../../core/version.js'
3
+ import { resolveHome } from '../../delivery/home.js'
4
+ import { readTemplatePackages } from '../../delivery/templates.js'
5
+ import { readServeRecords } from '../../delivery/run.js'
6
+ import { probeBrowser } from '../../export/browser.js'
7
+ import { registerBuiltinTemplates } from '../registry.js'
8
+
9
+ /**
10
+ * `kamishibai debug [--json]` — SPEC §10.1「診斷(環境、依賴、模板解析過程)」.
11
+ *
12
+ * One object that answers "what does this installation actually have?". The
13
+ * browser line is **probed**, never inferred from a package being installed:
14
+ * the same reasoning that governs `setup` applies here, and a diagnostic that
15
+ * guesses is worse than none, because it is believed.
16
+ *
17
+ * @returns {Promise<{result: object, exitCode: number}>}
18
+ */
19
+ export async function debugCommand(options = {}, env = process.env) {
20
+ registerBuiltinTemplates(env)
21
+ const probe = await probeBrowser()
22
+
23
+ return {
24
+ result: {
25
+ ok: true,
26
+ home: resolveHome(env),
27
+ templates: readTemplatePackages(env).length,
28
+ browser: probe.available ? 'present' : 'missing',
29
+ engine: engineVersion(),
30
+ servers: readServeRecords(env).length,
31
+ },
32
+ exitCode: EXIT.OK,
33
+ }
34
+ }
@@ -0,0 +1,22 @@
1
+ import { exampleFor, exampleKinds } from '../../core/example.js'
2
+ import { CODES, EXIT, KsbError } from '../../core/errors.js'
3
+
4
+ /**
5
+ * `kamishibai example [doc|<block>]` — the anti-trial-and-error channel.
6
+ * Human output is the example verbatim, so it can be piped straight into
7
+ * `render -`.
8
+ */
9
+ export function exampleCommand(kind = 'doc') {
10
+ const found = exampleFor(kind)
11
+ if (found === null) {
12
+ throw new KsbError({
13
+ code: CODES.USAGE,
14
+ message: `unknown example kind "${kind}"; available: ${exampleKinds().join(', ')}`,
15
+ exitCode: EXIT.USAGE,
16
+ })
17
+ }
18
+ return {
19
+ result: { ok: true, kind: found.kind, example: found.example },
20
+ exitCode: EXIT.OK,
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ import { exportArtifact } from '../../export/index.js'
2
+
3
+ /**
4
+ * `kamishibai export <artifact> --to pdf|pptx [-o out]`
5
+ *
6
+ * @returns {Promise<{result: object, exitCode: number}>}
7
+ */
8
+ export async function exportCommand(target, options = {}) {
9
+ return await exportArtifact({ target, format: options.to, out: options.out })
10
+ }
@@ -0,0 +1,53 @@
1
+ import { EXIT } from '../../core/errors.js'
2
+ import { engineVersion } from '../../core/version.js'
3
+ import { ensureStoreRoot } from '../../delivery/store.js'
4
+ import {
5
+ scaffoldTemplatePackage,
6
+ validateScaffoldRequest,
7
+ } from '../../delivery/template-scaffold.js'
8
+ import { reservedNamespaces } from '../../render/index.js'
9
+ import { registerBuiltinTemplates } from '../registry.js'
10
+
11
+ /**
12
+ * `kamishibai init <namespace>/<name> [--layout <文體>]` (CONTRACT D4).
13
+ *
14
+ * A template package's entry cost is what decides whether the pure-data path is
15
+ * the default path or a thing the documentation claims. This writes the whole
16
+ * format — manifest, stylesheet, 預設 md — into the store and hands back the
17
+ * exact command that draws it, so the first render is a copy-paste rather than
18
+ * a guess.
19
+ *
20
+ * The reserved namespaces come from the render layer's live registry rather
21
+ * than a list typed here: a second copy would be free to disagree with the one
22
+ * `resolveTemplate` actually enforces, and the direction it would fail in is
23
+ * the bad one — `init` cheerfully creating a package that can never be loaded.
24
+ *
25
+ * @returns {{result: object, exitCode: number}}
26
+ */
27
+ export function initCommand(key, options = {}, env = process.env) {
28
+ // Everything judgeable without the filesystem is judged **first** (複驗 F-2).
29
+ // `ensureStoreRoot` creates `~/.kamishibai` and `registerBuiltinTemplates`
30
+ // writes two manifests into it; running either before the key is validated
31
+ // means a refused `init ../evil` still built a store on its way to saying no.
32
+ // A command that rejects your input must leave the disk exactly as it found it.
33
+ const request = validateScaffoldRequest({
34
+ key,
35
+ layout: options.layout,
36
+ reserved: reservedNamespaces(),
37
+ })
38
+
39
+ ensureStoreRoot(env)
40
+ // First touch registers the built-ins, so a brand-new store lists the factory
41
+ // family alongside whatever this command is about to add (E3「首觸」).
42
+ registerBuiltinTemplates(env)
43
+
44
+ const result = scaffoldTemplatePackage({
45
+ key: request.key,
46
+ layout: request.layout,
47
+ engine: engineVersion(),
48
+ reserved: reservedNamespaces(),
49
+ env,
50
+ })
51
+
52
+ return { result: { ok: true, ...result }, exitCode: EXIT.OK }
53
+ }
@@ -0,0 +1,93 @@
1
+ import { readArtifact } from '../../delivery/read.js'
2
+ import { extractIrPayloads } from '../../parser/artifact.js'
3
+ import { lintArtifact, lintRules } from '../../core/lint.js'
4
+ import { templateKeyOf } from '../../core/ir.js'
5
+ import { CODES, EXIT, usageError } from '../../core/errors.js'
6
+
7
+ /** The single wording for "lint needs something to work on", both paths alike. */
8
+ const NO_TARGET_MESSAGE = 'lint 需要產物路徑;只想看規則請用 `kamishibai lint --rules`'
9
+
10
+ /**
11
+ * The single wording for "you asked for both at once".
12
+ *
13
+ * Accepting both and quietly listing the rules would hand back exit 0 while the
14
+ * artifact went unchecked — a gate that reports success without gating is worse
15
+ * than one that refuses, because CI believes it.
16
+ */
17
+ const RULES_WITH_TARGET_MESSAGE =
18
+ '`--rules` 只列規則、不驗證產物;請擇一:去掉產物路徑看規則,或去掉 `--rules` 驗證該產物'
19
+
20
+ /**
21
+ * Load the package the artifact says drew it, so the IR is judged against the
22
+ * vocabulary it was written in (CONTRACT F3 D3).
23
+ *
24
+ * The IR schema is generated from the block registry, and a package's `x-*`
25
+ * plugin blocks only enter that registry when the package is loaded. Without
26
+ * this, `render` would succeed and `lint` — a *fresh process* that never opened
27
+ * the package — would then report the plugin block as a schema violation: two
28
+ * commands disagreeing about what is legal, with the gate on the losing side.
29
+ *
30
+ * A key nobody has is passed over rather than raised: the artifact may have been
31
+ * drawn on another machine, and the schema check will say so in its own words,
32
+ * naming the block type it does not know. Every *other* failure is re-thrown —
33
+ * a package that exists and will not load is a real problem, and swallowing it
34
+ * here would turn "your package is broken" into "your artifact is fine".
35
+ *
36
+ * Both modules are imported *here* rather than at the top of the file, for the
37
+ * reason F4 gave the whole CLI: a static import would drag the render layer and
38
+ * both factory template packages into every `lint --rules`, which reads no
39
+ * artifact at all and needs neither.
40
+ */
41
+ async function loadArtifactVocabulary(html, env) {
42
+ const payloads = extractIrPayloads(html)
43
+ if (payloads.length !== 1) return undefined
44
+ let key
45
+ try {
46
+ key = templateKeyOf(JSON.parse(payloads[0]))
47
+ } catch {
48
+ // An unparsable payload is `lintArtifact`'s finding to report, not ours.
49
+ return undefined
50
+ }
51
+ if (key === undefined) return undefined
52
+
53
+ const { installTemplateStore } = await import('../../delivery/template-package.js')
54
+ const { resolveTemplate } = await import('../../render/index.js')
55
+ installTemplateStore(env)
56
+ try {
57
+ return await resolveTemplate(key)
58
+ } catch (error) {
59
+ if (error?.code !== CODES.TEMPLATE_NOT_FOUND) throw error
60
+ return undefined
61
+ }
62
+ }
63
+
64
+ /**
65
+ * `kamishibai lint <artifact>` — and `kamishibai lint --rules`, which answers
66
+ * "what does this gate actually scan, and over which region" without an agent
67
+ * having to read the linter's source (量測報告 §3 R1).
68
+ *
69
+ * @returns {Promise<{result: object|Array, exitCode: number}>}
70
+ */
71
+ export async function lintCommand(target, options = {}, env = process.env) {
72
+ const hasTarget = typeof target === 'string' && target.length > 0
73
+ if (options.rules && hasTarget) throw usageError(RULES_WITH_TARGET_MESSAGE)
74
+ if (options.rules) return { result: lintRules(), exitCode: EXIT.OK }
75
+ if (!hasTarget) throw usageError(NO_TARGET_MESSAGE)
76
+
77
+ const { html, path } = readArtifact(target)
78
+ // F7b — the same load answers a second question. A `[gates]` rule is only
79
+ // meaningful against the package that declared it, and the artifact says which
80
+ // package drew it; reading the manifest here is what keeps `core/lint.js` fed
81
+ // by its caller instead of growing an I/O path of its own (AGENTS §3.1).
82
+ const template = await loadArtifactVocabulary(html, env)
83
+ const errors = lintArtifact({
84
+ html,
85
+ irPayloads: extractIrPayloads(html),
86
+ gates: template?.manifest?.gates,
87
+ })
88
+
89
+ if (errors.length > 0) {
90
+ return { result: { ok: false, errors }, exitCode: EXIT.VALIDATION }
91
+ }
92
+ return { result: { ok: true, artifact: path, errors: [] }, exitCode: EXIT.OK }
93
+ }
@@ -0,0 +1,54 @@
1
+ import { extractIrPayloads } from '../../parser/artifact.js'
2
+ import { templateLabelOf } from '../../core/ir.js'
3
+ import { EXIT } from '../../core/errors.js'
4
+ import { resolveProject } from '../../delivery/project.js'
5
+ import { readEntries } from '../../delivery/store.js'
6
+
7
+ /** CONTRACT B4 — the six keys of a list entry, in output order. */
8
+ export const LIST_KEYS = Object.freeze([
9
+ 'name',
10
+ 'template',
11
+ 'createdAt',
12
+ 'generator',
13
+ 'artifact',
14
+ 'copies',
15
+ ])
16
+
17
+ /**
18
+ * An artifact whose IR cannot be read is still a record in the store: it is
19
+ * listed with empty descriptive fields rather than dropped, because a silently
20
+ * shorter list is indistinguishable from "no such artifact".
21
+ */
22
+ const irOf = (html) => {
23
+ const payloads = extractIrPayloads(html)
24
+ if (payloads.length !== 1) return null
25
+ try {
26
+ return JSON.parse(payloads[0])
27
+ } catch {
28
+ return null
29
+ }
30
+ }
31
+
32
+ const toEntry = (record) => {
33
+ const ir = irOf(record.html)
34
+ return {
35
+ name: record.name,
36
+ template: ir === null ? '' : templateLabelOf(ir),
37
+ createdAt: ir?.createdAt ?? '',
38
+ generator: ir?.generator ?? '',
39
+ artifact: record.path,
40
+ copies: record.copies,
41
+ }
42
+ }
43
+
44
+ /**
45
+ * `kamishibai list [--project name]` — the project's presentation history
46
+ * (SPEC §9.2). An empty project is an empty array with exit 0, not an error.
47
+ *
48
+ * @returns {{result: Array<object>, exitCode: number}}
49
+ */
50
+ export function listCommand(options = {}, env = process.env, cwd = process.cwd()) {
51
+ const project = resolveProject({ project: options.project, cwd })
52
+ const entries = readEntries({ project: project.name, env }).map(toEntry)
53
+ return { result: entries, exitCode: EXIT.OK }
54
+ }
@@ -0,0 +1,31 @@
1
+ import { CODES, EXIT, KsbError } from '../../core/errors.js'
2
+ import { resolveProject } from '../../delivery/project.js'
3
+ import { findArtifact, LATEST } from '../../delivery/store.js'
4
+ import { openPath } from '../../delivery/open.js'
5
+
6
+ /**
7
+ * `kamishibai open <name|latest> [--dry-run]`
8
+ *
9
+ * Resolves a name against the current project's central store and hands it to
10
+ * the single cross-platform opener (SPEC §8). `--dry-run` resolves only —
11
+ * the pinned surface is the absolute path, so an Agent can use `open` as a
12
+ * lookup without ever spawning a browser.
13
+ *
14
+ * @returns {{result: {ok: true, path: string}, exitCode: number}}
15
+ */
16
+ export function openCommand(nameSpec = LATEST, options = {}, env = process.env, cwd = process.cwd()) {
17
+ const project = resolveProject({ project: options.project, cwd })
18
+ const found = findArtifact({ project: project.name, name: nameSpec, env })
19
+
20
+ if (found === null) {
21
+ throw new KsbError({
22
+ code: CODES.ARTIFACT_NOT_FOUND,
23
+ message: `no artifact "${nameSpec}" in project "${project.name}"`,
24
+ exitCode: EXIT.VALIDATION,
25
+ })
26
+ }
27
+
28
+ if (options.dryRun !== true) openPath(found.path)
29
+
30
+ return { result: { ok: true, path: found.path }, exitCode: EXIT.OK }
31
+ }
@@ -0,0 +1,38 @@
1
+ import { EXIT } from '../../core/errors.js'
2
+ import { ensureStoreRoot } from '../../delivery/store.js'
3
+ import {
4
+ promoteTemplatePackage,
5
+ validatePromoteRequest,
6
+ } from '../../delivery/template-promote.js'
7
+ import { reservedNamespaces } from '../../render/index.js'
8
+ import { registerBuiltinTemplates } from '../registry.js'
9
+
10
+ /**
11
+ * `kamishibai promote <draft/名> <namespace/名>` (CONTRACT F7a H1).
12
+ *
13
+ * The exit of the 臨摹 workspace. `init draft/<名>` opens one, the Agent reads
14
+ * the original and writes the skin, `render` + `snapshot` close the loop by eye,
15
+ * `lint` closes it mechanically — and this is what turns the result into a
16
+ * package other things may depend on.
17
+ *
18
+ * The reserved namespaces come from the render layer's live registry rather
19
+ * than a list typed here, for the reason `init` records next door: a second copy
20
+ * would be free to disagree with the one `resolveTemplate` enforces, and it
21
+ * would fail in the bad direction — a `promote` that cheerfully lands a package
22
+ * at a key that can never be loaded.
23
+ *
24
+ * @returns {{result: object, exitCode: number}}
25
+ */
26
+ export function promoteCommand(from, to, options = {}, env = process.env) {
27
+ // Everything judgeable without the filesystem is judged **first** (複驗 F-2),
28
+ // and `validatePromoteRequest` is called ahead of `ensureStoreRoot` for the
29
+ // same reason `init` does it: a refused `promote ../evil x/y` that still built
30
+ // a store on its way to saying no is a rejected command that changed the world.
31
+ validatePromoteRequest({ from, to, reserved: reservedNamespaces(), env })
32
+
33
+ ensureStoreRoot(env)
34
+ registerBuiltinTemplates(env)
35
+
36
+ const result = promoteTemplatePackage({ from, to, reserved: reservedNamespaces(), env })
37
+ return { result: { ok: true, ...result }, exitCode: EXIT.OK }
38
+ }
@@ -0,0 +1,31 @@
1
+ import { readSource } from '../../delivery/read.js'
2
+ import { resolveCreatedAt } from '../../core/ir.js'
3
+ import { compileDocument } from '../../render/compile.js'
4
+ import { installTemplateStore } from '../../delivery/template-package.js'
5
+ import { deliver } from '../deliver.js'
6
+
7
+ /**
8
+ * `kamishibai render <input> [-o out.html] [--project name]`
9
+ *
10
+ * Renders, archives the canonical copy into the central store, and writes the
11
+ * delivery copy — the two are byte-identical by construction (CONTRACT B3).
12
+ *
13
+ * @returns {Promise<{result: object, exitCode: number}>}
14
+ */
15
+ export async function renderCommand(inputSpec, options = {}, env = process.env) {
16
+ // The store joins the resolution chain before anything is resolved (F3 D1):
17
+ // without this, a package sitting in the namespace answers
18
+ // `KSB_TEMPLATE_NOT_FOUND` — the exact failure 量測報告 §5.2 measured.
19
+ installTemplateStore(env)
20
+ const { source, format, slug } = readSource(inputSpec)
21
+
22
+ const { html, slides } = await compileDocument({
23
+ source,
24
+ format,
25
+ template: options.template,
26
+ generator: options.generator,
27
+ createdAt: resolveCreatedAt(env),
28
+ })
29
+
30
+ return deliver({ html, slides, slug, options, env })
31
+ }
@@ -0,0 +1,49 @@
1
+ import { basename, extname } from 'node:path'
2
+ import { readArtifact } from '../../delivery/read.js'
3
+ import { readEmbeddedIr } from '../../parser/artifact.js'
4
+ import { validateIr } from '../../core/validate.js'
5
+ import { replayCreatedAt, templateKeyOf } from '../../core/ir.js'
6
+ import { renderWithTemplatePackage, resolveTemplate } from '../../render/index.js'
7
+ import { installTemplateStore } from '../../delivery/template-package.js'
8
+ import { EXIT } from '../../core/errors.js'
9
+ import { deliver } from '../deliver.js'
10
+
11
+ /**
12
+ * `kamishibai replay <artifact> [-o out.html]`
13
+ *
14
+ * Re-renders an artifact from its own embedded IR (SPEC §7.3). With
15
+ * `KAMISHIBAI_BUILD_TIME` pinned the result is byte-identical to the original
16
+ * render (CONTRACT B6) — which is what makes re-skinning and engine upgrades
17
+ * verifiable rather than hopeful.
18
+ *
19
+ * @returns {Promise<{result: object, exitCode: number}>}
20
+ */
21
+ export async function replayCommand(target, options = {}, env = process.env) {
22
+ // 換皮 is the whole point of `replay`, so the skins the store holds have to be
23
+ // reachable from here too (F3 D1) — a chain wired only into `render` would
24
+ // make store packages re-renderable exactly never.
25
+ installTemplateStore(env)
26
+ const { html: source, path } = readArtifact(target)
27
+ const ir = readEmbeddedIr(source, path)
28
+
29
+ // The template is resolved **before** the IR is validated, because resolving
30
+ // it is what loads the package's `x-*` plugin blocks into the registry the IR
31
+ // schema is generated from (F3 D3). Validating first would judge an artifact
32
+ // against a vocabulary smaller than the one it was drawn in, and reject its
33
+ // own plugin blocks as unknown types — 換皮 failing on exactly the artifacts
34
+ // the extension mechanism exists to produce.
35
+ const template = await resolveTemplate(options.template ?? templateKeyOf(ir))
36
+
37
+ const { errors } = validateIr(ir)
38
+ if (errors.length > 0) return { result: { ok: false, errors }, exitCode: EXIT.VALIDATION }
39
+
40
+ const { html, slides } = await renderWithTemplatePackage({
41
+ template,
42
+ doc: ir.doc,
43
+ createdAt: replayCreatedAt(env, ir),
44
+ generator: options.generator ?? ir.generator,
45
+ })
46
+
47
+ const slug = basename(path, extname(path)) || 'document'
48
+ return deliver({ html, slides, slug, options, env })
49
+ }
@@ -0,0 +1,10 @@
1
+ import { irSchema } from '../../core/schema.js'
2
+ import { EXIT } from '../../core/errors.js'
3
+
4
+ /**
5
+ * `kamishibai schema` — emits the IR JSON Schema itself, so both the human and
6
+ * the `--json` path show the same object.
7
+ */
8
+ export function schemaCommand() {
9
+ return { result: irSchema(), exitCode: EXIT.OK }
10
+ }
@@ -0,0 +1,178 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { CODES, EXIT, KsbError, usageError } from '../../core/errors.js'
6
+ import { STDIN_SPEC } from '../../delivery/read.js'
7
+ import { resolveProject } from '../../delivery/project.js'
8
+ import { registerServe } from '../../delivery/run.js'
9
+ import { registerBuiltinTemplates } from '../registry.js'
10
+
11
+ /**
12
+ * `kamishibai serve <input> [--port N]` (CONTRACT E1).
13
+ *
14
+ * The command starts a **detached** preview server and returns: the shell (and
15
+ * the Agent driving it) gets its prompt back, and the server's existence is
16
+ * recorded in the pidfile so `close` can end it later. The alternative — a
17
+ * foreground process — makes `serve` unusable from an Agent loop and makes
18
+ * "stop it" mean "find the terminal you started it in".
19
+ *
20
+ * Failure to *start* is reported synchronously, with the daemon's own KSB_
21
+ * finding: printing a URL that nothing answers on would be the reassuring lie
22
+ * this SDK keeps refusing to tell.
23
+ */
24
+
25
+ const DAEMON = fileURLToPath(new URL('../../serve/daemon.js', import.meta.url))
26
+
27
+ /** How long to wait for the daemon to report a listening port. */
28
+ export const HANDSHAKE_TIMEOUT_MS = 20_000
29
+
30
+ /** `--port` arrives as a string; the boundary is where it stops being one. */
31
+ const parsePort = (value) => {
32
+ if (value === undefined) return 0
33
+ const text = String(value)
34
+ if (!/^\d+$/.test(text) || Number(text) < 1 || Number(text) > 65535) {
35
+ throw new KsbError({
36
+ code: CODES.SERVE_PORT_INVALID,
37
+ message: `--port 必須是 1–65535 的整數,收到 ${JSON.stringify(value)}`,
38
+ exitCode: EXIT.USAGE,
39
+ })
40
+ }
41
+ return Number(text)
42
+ }
43
+
44
+ const startFailed = (message) =>
45
+ new KsbError({ code: CODES.SERVE_START_FAILED, message, exitCode: EXIT.VALIDATION })
46
+
47
+ /**
48
+ * Spawn the daemon and wait for its one handshake line.
49
+ *
50
+ * stdout is a pipe only until the handshake lands; it is then destroyed and the
51
+ * child unref'd, so the parent's event loop is free to exit while the server
52
+ * keeps running. stderr is discarded for the same reason a detached process
53
+ * must never hold a pipe nobody drains: it would eventually block on a full
54
+ * buffer and the preview would freeze for no visible reason.
55
+ */
56
+ const spawnDaemon = (args, env) =>
57
+ new Promise((resolvePromise, rejectPromise) => {
58
+ const child = spawn(process.execPath, [DAEMON, ...args], {
59
+ detached: true,
60
+ stdio: ['ignore', 'pipe', 'ignore'],
61
+ env,
62
+ })
63
+
64
+ let buffer = ''
65
+ let settled = false
66
+
67
+ const finish = (fn, value) => {
68
+ if (settled) return
69
+ settled = true
70
+ clearTimeout(timer)
71
+ child.stdout.removeAllListeners()
72
+ child.removeAllListeners('error')
73
+ child.removeAllListeners('exit')
74
+ fn(value)
75
+ }
76
+
77
+ const timer = setTimeout(() => {
78
+ try {
79
+ child.kill('SIGTERM')
80
+ } catch {
81
+ // Already gone; nothing to stop.
82
+ }
83
+ finish(rejectPromise, startFailed(`daemon 在 ${HANDSHAKE_TIMEOUT_MS}ms 內沒有回報監聽位址`))
84
+ }, HANDSHAKE_TIMEOUT_MS)
85
+
86
+ child.on('error', (cause) => finish(rejectPromise, startFailed(`無法啟動 daemon:${cause.message}`)))
87
+
88
+ // The give-up signal is the *stream* ending, not the process exiting. A
89
+ // daemon that reports a failed start writes its finding and then exits, and
90
+ // those two events race: keying off `exit` would sometimes discard the real
91
+ // KSB_ diagnosis and report a generic "it died" instead. Stream order does
92
+ // guarantee that every buffered byte is emitted before `close`.
93
+ let exitCode = null
94
+ child.on('exit', (code) => {
95
+ exitCode = code
96
+ })
97
+ child.stdout.on('close', () =>
98
+ finish(rejectPromise, startFailed(`daemon 在回報監聽位址前就結束了(exit ${exitCode})`)),
99
+ )
100
+
101
+ child.stdout.setEncoding('utf8')
102
+ child.stdout.on('data', (chunk) => {
103
+ buffer += chunk
104
+ const newline = buffer.indexOf('\n')
105
+ if (newline === -1) return
106
+ const line = buffer.slice(0, newline)
107
+ let payload
108
+ try {
109
+ payload = JSON.parse(line)
110
+ } catch (cause) {
111
+ finish(rejectPromise, startFailed(`daemon 回報了無法解析的訊息:${cause.message}`))
112
+ return
113
+ }
114
+ finish(resolvePromise, { child, payload })
115
+ })
116
+ })
117
+
118
+ /**
119
+ * @returns {Promise<{result: object, exitCode: number}>}
120
+ */
121
+ export async function serveCommand(inputSpec, options = {}, env = process.env, cwd = process.cwd()) {
122
+ if (inputSpec === STDIN_SPEC) {
123
+ throw usageError('serve 需要一個可監看的來源檔,stdin(`-`)無法監看')
124
+ }
125
+ const input = resolve(String(inputSpec))
126
+ if (!existsSync(input)) {
127
+ throw new KsbError({
128
+ code: CODES.INPUT_NOT_FOUND,
129
+ message: `input not found: ${inputSpec}`,
130
+ exitCode: EXIT.USAGE,
131
+ })
132
+ }
133
+
134
+ const port = parsePort(options.port)
135
+ const project = resolveProject({ project: options.project, cwd })
136
+ registerBuiltinTemplates(env)
137
+
138
+ const args = ['--input', input, '--project', project.name, '--port', String(port)]
139
+ if (options.template !== undefined) args.push('--template', options.template)
140
+ if (options.generator !== undefined) args.push('--generator', options.generator)
141
+
142
+ const { child, payload } = await spawnDaemon(args, env)
143
+
144
+ if (payload?.ok !== true) {
145
+ child.stdout.destroy()
146
+ child.unref()
147
+ return {
148
+ result: { ok: false, errors: payload?.errors ?? [] },
149
+ exitCode: payload?.exitCode ?? EXIT.VALIDATION,
150
+ }
151
+ }
152
+
153
+ registerServe(
154
+ {
155
+ pid: payload.pid,
156
+ port: payload.port,
157
+ url: payload.url,
158
+ input,
159
+ artifact: payload.artifact,
160
+ startedAt: new Date().toISOString(),
161
+ },
162
+ env,
163
+ )
164
+
165
+ child.stdout.destroy()
166
+ child.unref()
167
+
168
+ return {
169
+ result: {
170
+ ok: true,
171
+ url: payload.url,
172
+ pid: payload.pid,
173
+ port: payload.port,
174
+ artifact: payload.artifact,
175
+ },
176
+ exitCode: EXIT.OK,
177
+ }
178
+ }
@@ -0,0 +1,64 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { EXIT } from '../../core/errors.js'
3
+ import { resolveHome } from '../../delivery/home.js'
4
+ import { ensureStoreRoot } from '../../delivery/store.js'
5
+ import { BROWSER_SETUP_COMMAND, browserMissingError, probeBrowser } from '../../export/browser.js'
6
+ import { registerBuiltinTemplates } from '../registry.js'
7
+
8
+ /**
9
+ * `kamishibai setup [--dry-run]` — SPEC §10.1「初始化環境(建中央儲存庫、裝渲染依賴)」.
10
+ *
11
+ * The browser state is *probed*, never assumed: this command exists so that the
12
+ * answer to "can this machine export?" is one command away, and a `setup` that
13
+ * reported success without launching a browser would be exactly the reassuring
14
+ * lie that D4 forbids.
15
+ */
16
+
17
+ /** The three states this command can honestly report. */
18
+ export const BROWSER_STATES = Object.freeze(['present', 'installed', 'missing'])
19
+
20
+ const runInstall = () => {
21
+ const [command, ...args] = BROWSER_SETUP_COMMAND.split(' ')
22
+ execFileSync(command, args, { stdio: 'ignore' })
23
+ }
24
+
25
+ /**
26
+ * @returns {Promise<{result: {ok: true, home: string, browser: string, command: string},
27
+ * exitCode: number}>}
28
+ */
29
+ export async function setupCommand(options = {}, env = process.env) {
30
+ const dryRun = options.dryRun === true
31
+ const home = dryRun ? resolveHome(env) : ensureStoreRoot(env).root
32
+
33
+ // Initialising the environment includes populating the template namespace
34
+ // (CONTRACT E3): a store without `templates/` is only half a store. Skipped
35
+ // under `--dry-run`, which must not create so much as a directory.
36
+ if (!dryRun) registerBuiltinTemplates(env)
37
+
38
+ const probe = await probeBrowser()
39
+ if (probe.available) {
40
+ return {
41
+ result: { ok: true, home, browser: 'present', command: BROWSER_SETUP_COMMAND },
42
+ exitCode: EXIT.OK,
43
+ }
44
+ }
45
+ if (dryRun) {
46
+ return {
47
+ result: { ok: true, home, browser: 'missing', command: BROWSER_SETUP_COMMAND },
48
+ exitCode: EXIT.OK,
49
+ }
50
+ }
51
+
52
+ try {
53
+ runInstall()
54
+ } catch (cause) {
55
+ throw browserMissingError(`\`${BROWSER_SETUP_COMMAND}\` 執行失敗(${cause.message})`)
56
+ }
57
+ const after = await probeBrowser()
58
+ if (!after.available) throw browserMissingError('安裝指令跑完了,瀏覽器仍然啟動不起來')
59
+
60
+ return {
61
+ result: { ok: true, home, browser: 'installed', command: BROWSER_SETUP_COMMAND },
62
+ exitCode: EXIT.OK,
63
+ }
64
+ }