@rse/specbook 1.0.1 → 1.0.3
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 +272 -121
- package/dst/specbook-api.d.ts +3 -3
- package/dst/specbook-api.js +8 -2
- package/dst/specbook-cli.js +9 -7
- package/dst/specbook-cmd-export.d.ts +2 -1
- package/dst/specbook-cmd-init.d.ts +2 -1
- package/dst/specbook-cmd-lint.d.ts +2 -1
- package/dst/specbook-export-pdf.d.ts +4 -1
- package/dst/specbook-export-pdf.js +46 -16
- package/dst/specbook-mcp.js +2 -2
- package/dst/specbook-verbose.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,63 +22,63 @@ CLI with commands `specbook <xxx>`, and an MCP service with tools
|
|
|
22
22
|
|
|
23
23
|
**SpecBook** provides the following distinct features:
|
|
24
24
|
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
25
|
+
- **Hierarchical Object Model**:
|
|
26
|
+
A specification is a tree of typed objects, each with a kind and
|
|
27
|
+
name, an optional id, optional properties, optional description, and
|
|
28
|
+
optional child objects. The allowed object hierarchy is defined per
|
|
29
|
+
context by the YAML schema configuration.
|
|
30
|
+
|
|
31
|
+
- **Versatile Object to Markdown Mapping**:
|
|
32
|
+
Objects are authored as plain Markdown headings, property lists,
|
|
33
|
+
and description prose. The objects can be mapped to nested sections
|
|
34
|
+
("complex" format) or compact bullet point lists ("concise" and
|
|
35
|
+
"group" format).
|
|
36
|
+
|
|
37
|
+
- **Wiki-Style Object Linking**:
|
|
38
|
+
Objects can hierarchically reference each other through Wiki-style
|
|
39
|
+
`[[xxx]]` references, which are resolved against the locally-unique
|
|
40
|
+
ids and names of all objects. In the HTML and PDF exports they become
|
|
41
|
+
navigable links onto precise anchors.
|
|
42
|
+
|
|
43
|
+
- **Strict Validation with Custom Schema**:
|
|
44
|
+
The YAML schema configuration strictly defines the allowed object
|
|
45
|
+
kinds, hierarchies, and properties, whose values are constrained by an
|
|
46
|
+
expression language (regex, enum, tags, list, and reference). Violations
|
|
47
|
+
are reported as file- and line-precise diagnostics. A bundled standard
|
|
48
|
+
schema configuration applies if no particular one is given.
|
|
49
|
+
|
|
50
|
+
- **Object Model Diagram Visualisation**:
|
|
51
|
+
Object kinds can declare "graph", "hub", or "grid" diagrams in the
|
|
52
|
+
schema, whose nodes and edges are automatically derived from the
|
|
53
|
+
object model and its references. The diagrams are rendered with the
|
|
54
|
+
sibling project [Gradia](https://github.com/rse/gradia), which is
|
|
55
|
+
specialized in rendering object models.
|
|
56
|
+
|
|
57
|
+
- **CLI, MCP, and API Interface**:
|
|
58
|
+
All commands are implemented once in the API class `SpecBook`, on
|
|
59
|
+
which both the CLI and the MCP service are just thin wrappers.
|
|
60
|
+
This way, humans, scripts, and AI agents use exactly the same
|
|
61
|
+
functionality.
|
|
62
|
+
|
|
63
|
+
- **AST Exports for AI/LLMs**:
|
|
64
|
+
The parsed specification Abstract Syntax Tree (AST) can be exported in
|
|
65
|
+
JSON, JSON5, YAML, or TOON format for machine consumption. Together
|
|
66
|
+
with the `describe` command, which explains the models and formats,
|
|
67
|
+
this enables LLMs to both read and write specifications.
|
|
68
|
+
|
|
69
|
+
- **HTML Export for Developers**:
|
|
70
|
+
The HTML export is a self-contained single document with a
|
|
71
|
+
table-of-contents, full-text search, embedded images, and a light/dark
|
|
72
|
+
theme toggle. It is intended for the day-to-day online reading during
|
|
73
|
+
development. The HTML export can map all object kinds to nested
|
|
74
|
+
sections or compact tables, or an automatic mixture which collapses
|
|
75
|
+
only the deepest level into tables.
|
|
76
|
+
|
|
77
|
+
- **PDF Export for Customers**:
|
|
78
|
+
The PDF export prints the HTML rendering via Chromium and post-processes
|
|
79
|
+
it with page numbers, headers/footers, and a hierarchical PDF outline.
|
|
80
|
+
It is intended as a polished, paginated offline document for handing
|
|
81
|
+
over to customers.
|
|
82
82
|
|
|
83
83
|
Overview
|
|
84
84
|
--------
|
|
@@ -97,84 +97,235 @@ $ npm install -g @rse/specbook
|
|
|
97
97
|
Usage
|
|
98
98
|
-----
|
|
99
99
|
|
|
100
|
-
###
|
|
100
|
+
### API
|
|
101
101
|
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
```ts
|
|
103
|
+
/* the SpecBook API, providing all commands the CLI and MCP service expose */
|
|
104
|
+
export class SpecBook {
|
|
105
|
+
constructor (options?: SpecBookOptions)
|
|
106
|
+
|
|
107
|
+
/* initialize the configured specification artifact files below the
|
|
108
|
+
base directory and return the list of the generated files */
|
|
109
|
+
init (options: {
|
|
110
|
+
config?: string, /* YAML schema configuration (default: bundled standard one) */
|
|
111
|
+
basedir?: string /* base directory (default: ".") */
|
|
112
|
+
}): Promise<string[]>
|
|
113
|
+
|
|
114
|
+
/* parse and validate the specification Markdown files below the base directory */
|
|
115
|
+
lint (options: {
|
|
116
|
+
config?: string,
|
|
117
|
+
basedir?: string
|
|
118
|
+
}): Promise<LintResult>
|
|
119
|
+
|
|
120
|
+
/* export the specification, parsing the input just once and
|
|
121
|
+
returning one buffer per requested format (strict: any diagnostic
|
|
122
|
+
rejects the export with an "Error") */
|
|
123
|
+
export (options: {
|
|
124
|
+
config?: string,
|
|
125
|
+
basedir?: string,
|
|
126
|
+
formats?: ExportFormat[] /* requested output formats (default: [ "json" ]) */
|
|
127
|
+
}): Promise<Buffer[]>
|
|
128
|
+
|
|
129
|
+
/* describe the SpecBook models and formats, optionally pointing to
|
|
130
|
+
the artifacts of the particular project */
|
|
131
|
+
describe (options: {
|
|
132
|
+
config?: string,
|
|
133
|
+
basedir?: string,
|
|
134
|
+
embed?: boolean, /* embed the schema configuration instead of referencing it */
|
|
135
|
+
format?: DescribeFormat, /* output format (default: "md") */
|
|
136
|
+
part?: DescribePart /* document part (default: "all") */
|
|
137
|
+
}): Promise<string>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* the constructor options and the sink of the verbose messages,
|
|
141
|
+
receiving the emitting command and the message */
|
|
142
|
+
export interface SpecBookOptions {
|
|
143
|
+
verbose?: VerboseSink
|
|
144
|
+
}
|
|
145
|
+
export type VerboseSink = (cmd: string, msg: string) => void
|
|
146
|
+
|
|
147
|
+
/* the result of the "lint" command and its file- and line-precise diagnostics */
|
|
148
|
+
export interface LintResult {
|
|
149
|
+
specification: Spec
|
|
150
|
+
diagnostics: Diagnostic[]
|
|
151
|
+
config?: Schema
|
|
152
|
+
}
|
|
153
|
+
export interface Diagnostic {
|
|
154
|
+
file: string
|
|
155
|
+
line: number
|
|
156
|
+
column: number
|
|
157
|
+
message: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* the supported export formats and describe formats/parts */
|
|
161
|
+
export const formats: readonly [ "json", "json5", "yaml", "toon", "html", "pdf", "md" ]
|
|
162
|
+
export const describeFormats: readonly [ "md", "raw" ]
|
|
163
|
+
export const describeParts: readonly [ "all", "meta", "schema", "spec" ]
|
|
164
|
+
export type ExportFormat = typeof formats[number]
|
|
165
|
+
export type DescribeFormat = typeof describeFormats[number]
|
|
166
|
+
export type DescribePart = typeof describeParts[number]
|
|
167
|
+
|
|
168
|
+
/* the Abstract Syntax Tree (AST) of a parsed specification */
|
|
169
|
+
export type Spec = {
|
|
170
|
+
artifacts: SpecArtifact[]
|
|
171
|
+
}
|
|
172
|
+
export type SpecArtifact = {
|
|
173
|
+
created: Date
|
|
174
|
+
modified: Date
|
|
175
|
+
objects: SpecObject[]
|
|
176
|
+
}
|
|
177
|
+
export type SpecObject = {
|
|
178
|
+
kind: string
|
|
179
|
+
id: string
|
|
180
|
+
anchor?: string
|
|
181
|
+
paren?: string
|
|
182
|
+
name: string
|
|
183
|
+
primary?: boolean
|
|
184
|
+
description?: SpecDescription
|
|
185
|
+
properties: SpecProperty[]
|
|
186
|
+
childs: SpecObject[]
|
|
187
|
+
}
|
|
188
|
+
export type SpecDescription = {
|
|
189
|
+
description: string
|
|
190
|
+
rationale?: string
|
|
191
|
+
embedding?: string[]
|
|
192
|
+
}
|
|
193
|
+
export type SpecProperty = {
|
|
194
|
+
key: string
|
|
195
|
+
value: string
|
|
196
|
+
embedding?: string[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* the YAML schema configuration, where the nested "diagram" and
|
|
200
|
+
"format" structures are detailed in the "describe" output */
|
|
201
|
+
export type Schema = SchemaObject[]
|
|
202
|
+
export type SchemaObject = {
|
|
203
|
+
kind: string
|
|
204
|
+
name?: string
|
|
205
|
+
id?: string
|
|
206
|
+
file?: string
|
|
207
|
+
desc?: string
|
|
208
|
+
optional?: boolean
|
|
209
|
+
diagram?: SchemaDiagram
|
|
210
|
+
format?: SchemaFormat
|
|
211
|
+
props?: SchemaProperty[]
|
|
212
|
+
childs?: SchemaObject[]
|
|
213
|
+
}
|
|
214
|
+
export type SchemaProperty = {
|
|
215
|
+
name: string
|
|
216
|
+
desc?: string
|
|
217
|
+
value?: string
|
|
218
|
+
optional?: boolean
|
|
219
|
+
}
|
|
108
220
|
```
|
|
109
221
|
|
|
110
|
-
|
|
111
|
-
standard schema configuration `specbook-format.yaml`) determines the
|
|
112
|
-
specification: exactly the artifact files its `file` fields reference
|
|
113
|
-
are loaded and parsed; all other Markdown files below the base
|
|
114
|
-
directory are ignored. A referenced
|
|
115
|
-
file which is absent is reported, unless all of its artifacts are
|
|
116
|
-
`optional`, and so is an artifact absent from its present file, unless
|
|
117
|
-
it is `optional` itself. Both `lint` and `export` report all
|
|
118
|
-
diagnostics and fail on any of them, so a partial or invalid
|
|
119
|
-
specification is never exported.
|
|
120
|
-
|
|
121
|
-
The base directory `-b`/`--basedir` (default: `.`) is the directory the
|
|
122
|
-
referenced artifact files are resolved against, and generated
|
|
123
|
-
specification Markdown files are placed inside it, too.
|
|
124
|
-
|
|
125
|
-
The export output option `-o`/`--output` (default: `-` for stdout) can
|
|
126
|
-
occur multiple times. The format (`json`, `json5`, `yaml`, `toon`,
|
|
127
|
-
`html`, `pdf`, or `md`) is inferred from the filename extension, unless
|
|
128
|
-
it is explicitly given as a `<format>:` prefix, and plain `-` (stdout)
|
|
129
|
-
defaults to JSON.
|
|
130
|
-
|
|
131
|
-
The `md` format normalizes the entire corpus into a *single* Markdown
|
|
132
|
-
document, whose single frontmatter block carries the earliest `Created:`
|
|
133
|
-
and the latest `Modified:` timestamp of all artifacts, so the
|
|
134
|
-
per-artifact timestamps do not survive this round-trip.
|
|
135
|
-
|
|
136
|
-
The `describe` command outputs the description of the SpecBook models
|
|
137
|
-
and formats, extended by a *SpecBook Project Instantiation* section which
|
|
138
|
-
points to the YAML schema configuration `-c`/`--config` and the base
|
|
139
|
-
directory `-b`/`--basedir`. The schema configuration falls back onto the
|
|
140
|
-
bundled standard one, embedded verbatim, as long as no particular
|
|
141
|
-
configuration file `-c`/`--config` is given. With `-e`/`--embed` the
|
|
142
|
-
given YAML schema configuration is embedded verbatim instead of just
|
|
143
|
-
being referenced, so the resulting document describes the specification
|
|
144
|
-
format entirely on its own.
|
|
145
|
-
|
|
146
|
-
The `describe` document part `-p`/`--part` (default: `all`) reduces the
|
|
147
|
-
output to a single part: `meta` for the description of the generic
|
|
148
|
-
SpecBook models and formats, `schema` for the YAML schema configuration
|
|
149
|
-
(the given one, referenced or embedded with `-e`/`--embed`, else the
|
|
150
|
-
bundled standard one, embedded), or `spec` for the reference to
|
|
151
|
-
the base directory. The output format `-f`/`--format` (default: `md`)
|
|
152
|
-
switches from this rendered Markdown onto the raw original file content
|
|
153
|
-
with `raw`, which is available for the file-backed parts `meta` and
|
|
154
|
-
`schema` only.
|
|
155
|
-
|
|
156
|
-
The default value of every CLI option `--xxx` can be overridden by a
|
|
157
|
-
corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
158
|
-
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
|
159
|
-
`SPECBOOK_VERBOSE`), while an explicitly supplied option always wins.
|
|
160
|
-
|
|
161
|
-
### API
|
|
222
|
+
Example:
|
|
162
223
|
|
|
163
224
|
```ts
|
|
164
|
-
import { SpecBook
|
|
225
|
+
import { SpecBook } from "@rse/specbook"
|
|
165
226
|
|
|
166
227
|
const specbook = new SpecBook({
|
|
167
|
-
verbose: (cmd, msg) => console.error(`specbook: ${cmd}: ${
|
|
228
|
+
verbose: (cmd, msg) => console.error(`specbook: ${cmd}: ${msg}`)
|
|
168
229
|
})
|
|
169
230
|
const result = await specbook.lint({
|
|
170
231
|
basedir: "smp/broadcast"
|
|
171
232
|
})
|
|
172
233
|
```
|
|
173
234
|
|
|
174
|
-
###
|
|
235
|
+
### CLI
|
|
236
|
+
|
|
237
|
+
Commands:
|
|
175
238
|
|
|
176
|
-
|
|
177
|
-
|
|
239
|
+
```bash
|
|
240
|
+
$ specbook init \
|
|
241
|
+
[-v|--verbose] \
|
|
242
|
+
[-c|--config <schema-yaml-file>] \
|
|
243
|
+
[-b|--basedir <spec-md-file-basedir>]
|
|
244
|
+
|
|
245
|
+
$ specbook lint \
|
|
246
|
+
[-v|--verbose] \
|
|
247
|
+
[-c|--config <schema-yaml-file>] \
|
|
248
|
+
[-b|--basedir <spec-md-file-basedir>]
|
|
249
|
+
|
|
250
|
+
$ specbook export \
|
|
251
|
+
[-v|--verbose] \
|
|
252
|
+
[-c|--config <schema-yaml-file>] \
|
|
253
|
+
[-b|--basedir <spec-md-file-basedir>] \
|
|
254
|
+
[-o|--output [<format>:]<output-file>] [...]
|
|
255
|
+
|
|
256
|
+
$ specbook describe \
|
|
257
|
+
[-v|--verbose] \
|
|
258
|
+
[-c|--config <schema-yaml-file>] \
|
|
259
|
+
[-b|--basedir <spec-md-file-basedir>] \
|
|
260
|
+
[-e|--embed] \
|
|
261
|
+
[-f|--format <format>] \
|
|
262
|
+
[-p|--part <part>] \
|
|
263
|
+
[-o|--output <output-file>]
|
|
264
|
+
|
|
265
|
+
$ specbook mcp \
|
|
266
|
+
[-v|--verbose]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Options:
|
|
270
|
+
|
|
271
|
+
- `-v|--verbose`:
|
|
272
|
+
Enable verbose logging of processing information to `stderr`.
|
|
273
|
+
|
|
274
|
+
- `-c|--config <schema-yaml-file>`:
|
|
275
|
+
The YAML schema configuration (default: the bundled standard schema
|
|
276
|
+
configuration `specbook-format.yaml`) determines the specification:
|
|
277
|
+
exactly the artifact files its `file` fields reference are loaded and
|
|
278
|
+
parsed; all other Markdown files below the base directory are ignored.
|
|
279
|
+
A referenced file which is absent is reported, unless all of its
|
|
280
|
+
artifacts are `optional`, and so is an artifact absent from its present
|
|
281
|
+
file, unless it is `optional` itself. Both `lint` and `export` report
|
|
282
|
+
all diagnostics and fail on any of them, so a partial or invalid
|
|
283
|
+
specification is never exported.
|
|
284
|
+
|
|
285
|
+
- `-b|--basedir <spec-md-file-basedir>`:
|
|
286
|
+
The base directory (default: `.`) is the directory the referenced
|
|
287
|
+
artifact files are resolved against, and generated specification
|
|
288
|
+
Markdown files are placed inside it, too.
|
|
289
|
+
|
|
290
|
+
- `-o|--output [<format>:]<output-file>` (`export` only):
|
|
291
|
+
The output file (default: `-` for stdout) can be given multiple times.
|
|
292
|
+
The format (`json`, `json5`, `yaml`, `toon`, `html`, `pdf`, or `md`)
|
|
293
|
+
is inferred from the filename extension, unless it is explicitly given
|
|
294
|
+
as a `<format>:` prefix, and plain `-` (stdout) defaults to JSON.
|
|
295
|
+
The `md` export format normalizes the entire corpus into a *single*
|
|
296
|
+
Markdown document.
|
|
297
|
+
|
|
298
|
+
- `-o|--output <output-file>` (`describe` only):
|
|
299
|
+
The output file (default: `-` for stdout) receives the described
|
|
300
|
+
Markdown document.
|
|
301
|
+
|
|
302
|
+
- `-e|--embed` (`describe` only):
|
|
303
|
+
Embed the given YAML schema configuration verbatim instead of just
|
|
304
|
+
referencing it, so the resulting document describes the specification
|
|
305
|
+
format entirely on its own.
|
|
306
|
+
|
|
307
|
+
- `-f|--format <format>` (`describe` only):
|
|
308
|
+
The output format (default: `md`) switches from the rendered Markdown
|
|
309
|
+
onto the raw original file content with `raw`, which is available for
|
|
310
|
+
the file-backed parts `meta` and `schema` only.
|
|
311
|
+
|
|
312
|
+
- `-p|--part <part>` (`describe` only):
|
|
313
|
+
The document part (default: `all`) reduces the output to a single part:
|
|
314
|
+
`meta` for the description of the generic SpecBook models and formats,
|
|
315
|
+
`schema` for the YAML schema configuration (the given one, referenced
|
|
316
|
+
or embedded with `-e|--embed`, else the bundled standard one,
|
|
317
|
+
embedded), or `spec` for the reference to the base directory.
|
|
318
|
+
|
|
319
|
+
The default value of every CLI option `--xxx` can be overridden
|
|
320
|
+
by a corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
321
|
+
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
|
322
|
+
`SPECBOOK_VERBOSE`), while an explicitly supplied option always wins.
|
|
323
|
+
|
|
324
|
+
Example:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
$ specbook lint -v -b smp/broadcast
|
|
328
|
+
```
|
|
178
329
|
|
|
179
330
|
Rendering Examples
|
|
180
331
|
------------------
|
package/dst/specbook-api.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic } from "./sp
|
|
|
2
2
|
import { type LintResult } from "./specbook-cmd-lint.js";
|
|
3
3
|
import { parseOutputSpec, formats, type ExportFormat } from "./specbook-cmd-export.js";
|
|
4
4
|
import { describeFormats, describeParts, parseDescribeFormat, parseDescribePart, type DescribeFormat, type DescribePart } from "./specbook-cmd-describe.js";
|
|
5
|
-
import { literal, renderVerbose } from "./specbook-verbose.js";
|
|
6
|
-
export { literal, renderVerbose };
|
|
5
|
+
import { literal, renderVerbose, type Verbose, type VerboseLevel } from "./specbook-verbose.js";
|
|
6
|
+
export { literal, renderVerbose, type Verbose, type VerboseLevel };
|
|
7
7
|
export { formats, parseOutputSpec, type ExportFormat };
|
|
8
8
|
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
9
9
|
export { type DescribeFormat, type DescribePart };
|
|
@@ -13,7 +13,7 @@ export type { Spec, SpecArtifact, SpecObject, SpecDescription, SpecProperty } fr
|
|
|
13
13
|
export type { Schema, SchemaObject, SchemaProperty } from "./specbook-format-schema.js";
|
|
14
14
|
export declare const version: string;
|
|
15
15
|
export declare const standardConfig: string;
|
|
16
|
-
export type VerboseSink = (cmd: string, msg: string) => void;
|
|
16
|
+
export type VerboseSink = (cmd: string, msg: string, level: VerboseLevel) => void;
|
|
17
17
|
export interface SpecBookOptions {
|
|
18
18
|
verbose?: VerboseSink;
|
|
19
19
|
}
|
package/dst/specbook-api.js
CHANGED
|
@@ -11,6 +11,7 @@ import { initSpecification } from "./specbook-cmd-init.js";
|
|
|
11
11
|
import { lint } from "./specbook-cmd-lint.js";
|
|
12
12
|
import { exportSpecification, parseOutputSpec, formats } from "./specbook-cmd-export.js";
|
|
13
13
|
import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart } from "./specbook-cmd-describe.js";
|
|
14
|
+
import { requireBrowser } from "./specbook-export-pdf.js";
|
|
14
15
|
import { literal, renderVerbose } from "./specbook-verbose.js";
|
|
15
16
|
/* re-export the central types for API consumers */
|
|
16
17
|
export { literal, renderVerbose };
|
|
@@ -32,7 +33,7 @@ export class SpecBook {
|
|
|
32
33
|
}
|
|
33
34
|
/* bind the verbose sink to a particular command */
|
|
34
35
|
verboseOf(cmd) {
|
|
35
|
-
return (msg) => this.verbose(cmd, msg);
|
|
36
|
+
return (msg, level = "debug") => this.verbose(cmd, msg, level);
|
|
36
37
|
}
|
|
37
38
|
/* determine the file of the YAML schema configuration, falling
|
|
38
39
|
back onto the bundled standard schema configuration */
|
|
@@ -68,6 +69,11 @@ export class SpecBook {
|
|
|
68
69
|
as a partial or invalid specification must never be emitted) */
|
|
69
70
|
async export(options) {
|
|
70
71
|
const verbose = this.verboseOf("export");
|
|
72
|
+
const requested = options.formats ?? ["json"];
|
|
73
|
+
/* a missing browser is an environment problem, so let the PDF
|
|
74
|
+
export fail before the specification is even parsed */
|
|
75
|
+
if (requested.includes("pdf"))
|
|
76
|
+
await requireBrowser(verbose);
|
|
71
77
|
const result = lint({ config: this.configFile(options.config),
|
|
72
78
|
basedir: options.basedir ?? ".", verbose });
|
|
73
79
|
if (result.diagnostics.length > 0)
|
|
@@ -76,7 +82,7 @@ export class SpecBook {
|
|
|
76
82
|
if (result.specification.artifacts.length === 0)
|
|
77
83
|
throw new Error("unexportable specification: no artifacts found");
|
|
78
84
|
const buffers = new Array();
|
|
79
|
-
for (const format of
|
|
85
|
+
for (const format of requested)
|
|
80
86
|
buffers.push(await exportSpecification(result.specification, format, verbose, result.config));
|
|
81
87
|
return buffers;
|
|
82
88
|
}
|
package/dst/specbook-cli.js
CHANGED
|
@@ -12,9 +12,11 @@ import { serveMcp } from "./specbook-mcp.js";
|
|
|
12
12
|
/* route verbose messages to stderr, keeping stdout reserved for the
|
|
13
13
|
command outputs and the MCP protocol, and qualify every message with
|
|
14
14
|
the tool name plus the scope path of the emitting command, where
|
|
15
|
-
Chalk styles the output only if the terminal supports colors
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Chalk styles the output only if the terminal supports colors; the
|
|
16
|
+
"notice" messages pass regardless of the verbose option, as they
|
|
17
|
+
report environment problems the user has to see */
|
|
18
|
+
const verboseOf = (opts, ...scope) => (cmd, msg, level) => {
|
|
19
|
+
if (opts.verbose || level === "notice") {
|
|
18
20
|
const scopePath = [...scope, cmd].map((segment) => chalk.bold(segment)).join(": ");
|
|
19
21
|
process.stderr.write(`specbook: ${scopePath}: ${renderVerbose(msg, chalk.blue)}\n`);
|
|
20
22
|
}
|
|
@@ -38,7 +40,7 @@ const writeOutput = async (output, data, cmd, verbose) => {
|
|
|
38
40
|
await writeStdout(data);
|
|
39
41
|
else {
|
|
40
42
|
await fs.promises.writeFile(output, data);
|
|
41
|
-
verbose(cmd, `wrote "${literal(output)}" (${literal(Buffer.byteLength(data))} bytes)
|
|
43
|
+
verbose(cmd, `wrote "${literal(output)}" (${literal(Buffer.byteLength(data))} bytes)`, "debug");
|
|
42
44
|
}
|
|
43
45
|
};
|
|
44
46
|
/* determine an option default value, overridable via a
|
|
@@ -71,7 +73,7 @@ program.name("specbook")
|
|
|
71
73
|
withVerboseOption(program.command("mcp"))
|
|
72
74
|
.description("run as MCP stdio server")
|
|
73
75
|
.action(async (opts) => {
|
|
74
|
-
verboseOf(opts)("mcp", "starting MCP server on stdio");
|
|
76
|
+
verboseOf(opts)("mcp", "starting MCP server on stdio", "debug");
|
|
75
77
|
await serveMcp(verboseOf(opts, "mcp"));
|
|
76
78
|
});
|
|
77
79
|
/* the init command creates the configured artifact files */
|
|
@@ -97,7 +99,7 @@ withCommonOptions(program.command("lint"))
|
|
|
97
99
|
if (result.diagnostics.length > 0)
|
|
98
100
|
process.exitCode = 1;
|
|
99
101
|
else
|
|
100
|
-
verboseOf(opts)("lint", "specification valid");
|
|
102
|
+
verboseOf(opts)("lint", "specification valid", "debug");
|
|
101
103
|
});
|
|
102
104
|
/* the export command parses the input once and writes every output */
|
|
103
105
|
withCommonOptions(program.command("export"))
|
|
@@ -143,5 +145,5 @@ catch (err) {
|
|
|
143
145
|
let msg = err instanceof Error ? err.message : String(err);
|
|
144
146
|
for (let cause = err instanceof Error ? err.cause : undefined; cause instanceof Error; cause = cause.cause)
|
|
145
147
|
msg += `: ${cause.message}`;
|
|
146
|
-
process.stderr.write(`specbook: ERROR: ${msg}\n`, () => process.exit(1));
|
|
148
|
+
process.stderr.write(`specbook: ${chalk.red("ERROR:")} ${renderVerbose(msg, chalk.blue)}\n`, () => process.exit(1));
|
|
147
149
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Spec } from "./specbook-format-spec.js";
|
|
2
2
|
import type { Schema } from "./specbook-format-schema.js";
|
|
3
|
+
import type { Verbose } from "./specbook-verbose.js";
|
|
3
4
|
export declare const formats: readonly ["json", "json5", "yaml", "toon", "html", "pdf", "md"];
|
|
4
5
|
export type ExportFormat = typeof formats[number];
|
|
5
6
|
export declare const parseOutputSpec: (spec: string) => {
|
|
6
7
|
format: ExportFormat;
|
|
7
8
|
output: string;
|
|
8
9
|
};
|
|
9
|
-
export declare const exportSpecification: (specification: Spec, format: ExportFormat, verbose:
|
|
10
|
+
export declare const exportSpecification: (specification: Spec, format: ExportFormat, verbose: Verbose, config?: Schema) => Promise<Buffer>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type Verbose } from "./specbook-verbose.js";
|
|
1
2
|
import { type Schema } from "./specbook-format-schema.js";
|
|
2
3
|
export interface InitOptions {
|
|
3
4
|
config: Schema;
|
|
4
5
|
basedir: string;
|
|
5
|
-
verbose:
|
|
6
|
+
verbose: Verbose;
|
|
6
7
|
}
|
|
7
8
|
export declare const initSpecification: (options: InitOptions) => string[];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { type Verbose } from "./specbook-verbose.js";
|
|
1
2
|
import { type Diagnostic } from "./specbook-diagnostic.js";
|
|
2
3
|
import { type Spec } from "./specbook-format-spec.js";
|
|
3
4
|
import { type Schema } from "./specbook-format-schema.js";
|
|
4
5
|
export interface LintOptions {
|
|
5
6
|
config: string;
|
|
6
7
|
basedir: string;
|
|
7
|
-
verbose:
|
|
8
|
+
verbose: Verbose;
|
|
8
9
|
}
|
|
9
10
|
export interface LintResult {
|
|
10
11
|
specification: Spec;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { LaunchOptions } from "playwright";
|
|
1
2
|
import type { OutlineEntry } from "./specbook-export-html.js";
|
|
2
3
|
import type { ThemeMapping } from "./specbook-theme.js";
|
|
4
|
+
import { type Verbose } from "./specbook-verbose.js";
|
|
3
5
|
type Heading = {
|
|
4
6
|
title: string;
|
|
5
7
|
subtitle?: string;
|
|
6
8
|
logo: string;
|
|
7
9
|
};
|
|
8
|
-
export declare const
|
|
10
|
+
export declare const requireBrowser: (verbose: Verbose) => Promise<LaunchOptions>;
|
|
11
|
+
export declare const htmlToPdf: (renderHtmlPass: (tocPages?: Map<string, number>) => Promise<string>, heading: Heading, outline: OutlineEntry[], verbose: Verbose, css: string, theme: ThemeMapping, paper?: string) => Promise<Buffer>;
|
|
9
12
|
export {};
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
+
import fs from "node:fs";
|
|
6
7
|
import { escapeHtml, paperSetup, paperLength, paperSizeDefault } from "./specbook-export-common.js";
|
|
8
|
+
import { literal } from "./specbook-verbose.js";
|
|
7
9
|
/* extract the per-anchor page numbers from a Chromium-generated PDF,
|
|
8
10
|
which records the internal link targets as named PDF destinations */
|
|
9
11
|
const anchorPages = async (pdf) => {
|
|
@@ -121,6 +123,48 @@ const drawBrandBar = async (doc, accent) => {
|
|
|
121
123
|
page.drawRectangle({ x: 0, y: 0, width: 5.4,
|
|
122
124
|
height: page.getHeight(), color: rgb(r, g, b) });
|
|
123
125
|
};
|
|
126
|
+
/* the launch options of the Chromium-class browser printing the PDF,
|
|
127
|
+
resolved just once per process, as probing a system-installed Google
|
|
128
|
+
Chrome has to actually launch it and as the export preflight and the
|
|
129
|
+
renderer both ask for the very same browser */
|
|
130
|
+
let browserOptions;
|
|
131
|
+
/* resolve the browser printing the PDF: the downloaded Playwright
|
|
132
|
+
Chromium is a plain file check, while a system-installed Google
|
|
133
|
+
Chrome is only detectable by launching it */
|
|
134
|
+
const resolveBrowser = async (verbose) => {
|
|
135
|
+
const { chromium } = await import("playwright");
|
|
136
|
+
let executable = "";
|
|
137
|
+
try {
|
|
138
|
+
executable = chromium.executablePath();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* Playwright knows no Chromium distribution at all */
|
|
142
|
+
}
|
|
143
|
+
if (executable !== "" && fs.existsSync(executable))
|
|
144
|
+
return {};
|
|
145
|
+
try {
|
|
146
|
+
const chrome = await chromium.launch({ channel: "chrome" });
|
|
147
|
+
await chrome.close();
|
|
148
|
+
verbose("Playwright Chromium unavailable -- falling back to the " +
|
|
149
|
+
"system-installed Google Chrome", "notice");
|
|
150
|
+
return { channel: "chrome" };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
/* ensure a browser printing the PDF is available, so a missing browser
|
|
157
|
+
fails the export early and with an actionable remedy instead of deep
|
|
158
|
+
inside the rendering and with a Playwright-internal message */
|
|
159
|
+
export const requireBrowser = async (verbose) => {
|
|
160
|
+
browserOptions ??= resolveBrowser(verbose);
|
|
161
|
+
const options = await browserOptions;
|
|
162
|
+
if (options === undefined)
|
|
163
|
+
throw new Error("the PDF export requires a Chromium-class browser, but neither the " +
|
|
164
|
+
"Playwright Chromium nor a system-installed Google Chrome was found -- run " +
|
|
165
|
+
`"${literal("npx playwright install chromium")}" once to download the Playwright Chromium`);
|
|
166
|
+
return options;
|
|
167
|
+
};
|
|
124
168
|
/* render a self-contained HTML document into a PDF via Playwright,
|
|
125
169
|
re-rendering the HTML with the discovered ToC page numbers */
|
|
126
170
|
export const htmlToPdf = async (renderHtmlPass, heading, outline, verbose, css, theme, paper = paperSizeDefault) => {
|
|
@@ -128,22 +172,8 @@ export const htmlToPdf = async (renderHtmlPass, heading, outline, verbose, css,
|
|
|
128
172
|
/* the page geometry of the chosen paper size */
|
|
129
173
|
const setup = paperSetup(paper);
|
|
130
174
|
const margin = setup.margin;
|
|
131
|
-
/* launch the
|
|
132
|
-
|
|
133
|
-
let browser;
|
|
134
|
-
try {
|
|
135
|
-
browser = await chromium.launch();
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
verbose("Playwright Chromium unavailable -- falling back to installed Google Chrome");
|
|
139
|
-
try {
|
|
140
|
-
browser = await chromium.launch({ channel: "chrome" });
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
/* report the original, more instructive error */
|
|
144
|
-
throw err;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
175
|
+
/* launch the browser resolved for this process */
|
|
176
|
+
const browser = await chromium.launch(await requireBrowser(verbose));
|
|
147
177
|
try {
|
|
148
178
|
const page = await browser.newPage();
|
|
149
179
|
/* render an HTML document into a paginated PDF */
|
package/dst/specbook-mcp.js
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { SpecBook, renderDiagnostic, formats, parseOutputSpec, describeFormats, describeParts, version } from "./specbook-api.js";
|
|
10
|
+
import { SpecBook, renderDiagnostic, renderVerbose, formats, parseOutputSpec, describeFormats, describeParts, version } from "./specbook-api.js";
|
|
11
11
|
/* render an error with its cause chain into a tool error result */
|
|
12
12
|
const errorResult = (err) => {
|
|
13
13
|
let msg = err instanceof Error ? err.message : String(err);
|
|
@@ -15,7 +15,7 @@ const errorResult = (err) => {
|
|
|
15
15
|
msg += `: ${cause.message}`;
|
|
16
16
|
return {
|
|
17
17
|
isError: true,
|
|
18
|
-
content: [{ type: "text", text: `ERROR: ${msg}` }]
|
|
18
|
+
content: [{ type: "text", text: `ERROR: ${renderVerbose(msg)}` }]
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
21
|
/* serve the SpecBook functionality as "specbook_<cmd>" MCP tools over stdio */
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
export type VerboseLevel = "debug" | "notice";
|
|
2
|
+
export type Verbose = (msg: string, level?: VerboseLevel) => void;
|
|
1
3
|
export declare const literal: (value: string | number) => string;
|
|
2
4
|
export declare const renderVerbose: (msg: string, style?: (value: string) => string) => string;
|