@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
|
@@ -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
|
+
}
|