@rse/specbook 1.2.15 → 1.2.16
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 +98 -11
- package/dst/specbook-api.d.ts +22 -21
- package/dst/specbook-api.js +80 -27
- package/dst/specbook-cli.js +50 -23
- package/dst/specbook-cmd-describe.js +8 -4
- package/dst/specbook-cmd-export.d.ts +4 -1
- package/dst/specbook-cmd-export.js +22 -12
- package/dst/specbook-cmd-init.js +2 -8
- package/dst/specbook-cmd-lint.d.ts +2 -1
- package/dst/specbook-cmd-lint.js +31 -28
- package/dst/specbook-config.d.ts +7 -0
- package/dst/specbook-config.js +6 -1
- package/dst/specbook-coverage.js +3 -4
- package/dst/specbook-diagram.d.ts +3 -1
- package/dst/specbook-diagram.js +78 -27
- package/dst/specbook-export-ast.d.ts +2 -1
- package/dst/specbook-export-ast.js +16 -8
- package/dst/specbook-export-common.d.ts +7 -0
- package/dst/specbook-export-common.js +36 -3
- package/dst/specbook-export-html.css +64 -18
- package/dst/specbook-export-html.d.ts +2 -1
- package/dst/specbook-export-html.js +517 -275
- package/dst/specbook-export-image.d.ts +3 -0
- package/dst/specbook-export-image.js +121 -0
- package/dst/specbook-export-md.d.ts +8 -2
- package/dst/specbook-export-md.js +67 -6
- package/dst/specbook-export-pdf.js +7 -4
- package/dst/specbook-format.md +35 -8
- package/dst/specbook-format.yaml +12 -9
- package/dst/specbook-ignore.js +5 -5
- package/dst/specbook-link.js +1 -1
- package/dst/specbook-mcp.js +51 -34
- package/dst/specbook-parse-common.d.ts +4 -1
- package/dst/specbook-parse-common.js +15 -3
- package/dst/specbook-parse-semantic.d.ts +1 -1
- package/dst/specbook-parse-syntax.d.ts +1 -1
- package/dst/specbook-parse-syntax.js +28 -8
- package/dst/specbook-parse.js +6 -2
- package/dst/specbook-project.d.ts +7 -0
- package/dst/specbook-project.js +93 -0
- package/dst/specbook-theme.d.ts +1 -1
- package/dst/specbook-theme.js +9 -8
- package/dst/specbook-verbose.js +4 -9
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -149,12 +149,14 @@ $ specbook export \
|
|
|
149
149
|
[-b|--basedir <spec-md-file-basedir>] \
|
|
150
150
|
[-o|--output [<format>:]<output-file>] \
|
|
151
151
|
[-w|--watch] \
|
|
152
|
+
[-O|--omit <aspect>[,...]] \
|
|
152
153
|
[...]
|
|
153
154
|
|
|
154
155
|
$ specbook preview \
|
|
155
156
|
[-v|--verbose [<level>]] \
|
|
156
157
|
[-c|--config <schema-yaml-file>] \
|
|
157
158
|
[-b|--basedir <spec-md-file-basedir>] \
|
|
159
|
+
[-O|--omit <aspect>[,...]] \
|
|
158
160
|
[-a|--addr <ip-addr>] \
|
|
159
161
|
[-p|--port <tcp-port>]
|
|
160
162
|
|
|
@@ -182,8 +184,9 @@ Options:
|
|
|
182
184
|
of a reference coverage.
|
|
183
185
|
|
|
184
186
|
- `-c|--config <schema-yaml-file>`:
|
|
185
|
-
The YAML schema configuration (default: the
|
|
186
|
-
configuration
|
|
187
|
+
The YAML schema configuration (default: the `config` entry of the
|
|
188
|
+
project configuration file `.specbook.yaml`, else the bundled standard
|
|
189
|
+
schema configuration `specbook-format.yaml`) determines the specification:
|
|
187
190
|
exactly the artifact files its `file` fields reference are loaded and
|
|
188
191
|
parsed; all other Markdown files below the base directory are ignored.
|
|
189
192
|
A referenced file which is absent is reported, unless all of its
|
|
@@ -210,12 +213,13 @@ Options:
|
|
|
210
213
|
the schema of the configuration.
|
|
211
214
|
|
|
212
215
|
- `-b|--basedir <spec-md-file-basedir>`:
|
|
213
|
-
The base directory (default:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
The base directory (default: the `basedir` entry of the project
|
|
217
|
+
configuration file `.specbook.yaml`, else `.`) is the directory the
|
|
218
|
+
referenced artifact files are resolved against, and generated
|
|
219
|
+
specification Markdown files are placed inside it, too.
|
|
216
220
|
|
|
217
221
|
- `-o|--output [<format>:]<output-file>` (`export` only):
|
|
218
|
-
The output file (
|
|
222
|
+
The output file (`-` for stdout) is required and can be given multiple times.
|
|
219
223
|
The format (`json`, `json5`, `yaml`, `toon`, `html`, `pdf`, or `md`)
|
|
220
224
|
is inferred from the filename extension, unless it is explicitly given
|
|
221
225
|
as a `<format>:` prefix, and plain `-` (stdout) defaults to JSON.
|
|
@@ -234,6 +238,23 @@ Options:
|
|
|
234
238
|
file itself, as its own write would re-trigger the observation
|
|
235
239
|
endlessly.
|
|
236
240
|
|
|
241
|
+
- `-O|--omit <aspect>[,...]` (`export` and `preview` only):
|
|
242
|
+
Omit content aspects (comma-separated, repeatable) from the HTML and
|
|
243
|
+
PDF outputs at generation time, instead of just leaving them foldable
|
|
244
|
+
in the browser. The aspects match the folding controls of the HTML
|
|
245
|
+
export: `diagram:graph`, `diagram:hub`, and `diagram:grid` omit all
|
|
246
|
+
diagrams of a type, `diagram:1` (alias: `diagram`), `diagram:2`, and
|
|
247
|
+
`diagram:3` omit all diagrams from an object tree nesting level on
|
|
248
|
+
(level 1 hence all, including the "Diagram of Contents" page), and
|
|
249
|
+
`text:long` cuts every table cell whose estimated number of text
|
|
250
|
+
lines towers over all other cells of its row (by the `maxCellHeight`
|
|
251
|
+
percentage of the object kind, default 40) at a word boundary, ending
|
|
252
|
+
it in a grey `[...]`. An omitted diagram leaves nothing behind and is not even
|
|
253
|
+
rendered, the folding controls of the omitted aspects leave the brand
|
|
254
|
+
bar, and the AST outputs (`json`, `json5`, `yaml`, `toon`) drop the
|
|
255
|
+
`diagram` fields of the omitted diagrams, too, while the `md` output
|
|
256
|
+
ignores the option.
|
|
257
|
+
|
|
237
258
|
- `-a|--addr <ip-addr>`, `-p|--port <tcp-port>` (`preview` only):
|
|
238
259
|
The IP address (default: `127.0.0.1`) and TCP port (default: `12345`)
|
|
239
260
|
the live preview listens on. The HTML export is served on
|
|
@@ -262,8 +283,10 @@ Options:
|
|
|
262
283
|
schema configuration (embedded into the Markdown or emitted as the
|
|
263
284
|
raw file content), so the configuration costs fewer tokens: `0`
|
|
264
285
|
emits it verbatim, `1` re-emits it with 2-space indentation, unwrapped
|
|
265
|
-
lines, and without comments, `2` additionally leaves out its `refs
|
|
266
|
-
`
|
|
286
|
+
lines, and without comments, `2` additionally leaves out its `refs`,
|
|
287
|
+
`diagram`, and `format` fields, and `3` additionally leaves out its
|
|
288
|
+
`desc` fields of objects and properties. The MCP tool `specbook_describe` defaults to
|
|
289
|
+
`2` instead, as its consumer is an LLM.
|
|
267
290
|
|
|
268
291
|
- `-f|--format <format>` (`describe` only):
|
|
269
292
|
The output format (default: `md`) switches from the rendered Markdown
|
|
@@ -278,13 +301,35 @@ Options:
|
|
|
278
301
|
or embedded with `-e|--embed`, else the bundled standard one,
|
|
279
302
|
embedded), or `spec` for the reference to the base directory.
|
|
280
303
|
|
|
304
|
+
The defaults of `-c|--config` and `-b|--basedir` are, unless
|
|
305
|
+
`SPECBOOK_CONFIG` and `SPECBOOK_BASEDIR` provide them (see below), taken
|
|
306
|
+
from the optional project configuration file `.specbook.yaml`, which is searched
|
|
307
|
+
in the current working directory and then upwards in all of its parent
|
|
308
|
+
directories (the closest one wins), so SpecBook works from any directory
|
|
309
|
+
below the project root:
|
|
310
|
+
|
|
311
|
+
```yaml
|
|
312
|
+
config: docs/spec/schema.yaml
|
|
313
|
+
basedir: docs/spec
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The entry `config` is a single file or glob pattern or a list of them
|
|
317
|
+
(exactly like the repeatable `-c|--config`, including the literal
|
|
318
|
+
`std`), and the entry `basedir` is a directory. Relative paths resolve
|
|
319
|
+
against the directory of the `.specbook.yaml` itself, the file is
|
|
320
|
+
validated (an unknown entry or a wrongly typed value fails the command),
|
|
321
|
+
and both an explicitly supplied option and its environment variable
|
|
322
|
+
win over its entry.
|
|
323
|
+
|
|
281
324
|
The default value of every CLI option `--xxx` can be overridden
|
|
282
325
|
by a corresponding `SPECBOOK_XXX` environment variable (e.g.
|
|
283
326
|
`SPECBOOK_BASEDIR`, `SPECBOOK_CONFIG`, `SPECBOOK_OUTPUT`,
|
|
284
327
|
`SPECBOOK_VERBOSE`, `SPECBOOK_ADDR`, `SPECBOOK_PORT`), while an
|
|
285
328
|
explicitly supplied option always wins. As `-c|--config` is repeatable,
|
|
286
329
|
`SPECBOOK_CONFIG` carries a list of patterns separated by the path
|
|
287
|
-
delimiter of the platform (`:` on Unix, `;` on Windows).
|
|
330
|
+
delimiter of the platform (`:` on Unix, `;` on Windows). Unlike all
|
|
331
|
+
others, `SPECBOOK_CONFIG` and `SPECBOOK_BASEDIR` are resolved by the API
|
|
332
|
+
itself, so they apply to the API methods and the MCP tools, too.
|
|
288
333
|
|
|
289
334
|
Beyond those, the option-less `SPECBOOK_BROWSER` selects the browser
|
|
290
335
|
printing the PDF export: a value carrying a path separator is taken as
|
|
@@ -298,6 +343,48 @@ Chrome* (the equivalent of `chrome`). An explicitly configured browser
|
|
|
298
343
|
failing to launch fails the export instead of falling back onto another
|
|
299
344
|
browser.
|
|
300
345
|
|
|
346
|
+
Agent Skill
|
|
347
|
+
-----------
|
|
348
|
+
|
|
349
|
+
For *Claude Code*, SpecBook ships the plugin `specbook`, which registers
|
|
350
|
+
the MCP service `specbook mcp` and provides the skill `/specbook` on top
|
|
351
|
+
of it:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
$ claude plugin marketplace add rse/specbook
|
|
355
|
+
$ claude plugin install specbook@specbook
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
```text
|
|
359
|
+
/specbook [-c|--config <yaml-file>] [-b|--basedir <basedir>] [<query>]
|
|
360
|
+
/specbook init [-c|--config <yaml-file>] [-b|--basedir <basedir>]
|
|
361
|
+
/specbook lint [-c|--config <yaml-file>] [-b|--basedir <basedir>] [-g|--gitignore]
|
|
362
|
+
/specbook export [-c|--config <yaml-file>] [-b|--basedir <basedir>] [-g|--gitignore]
|
|
363
|
+
[-O|--omit <aspect>[,...]] [-o|--output [<format>:]<output-file>] [...]
|
|
364
|
+
/specbook edit [-c|--config <yaml-file>] [-b|--basedir <basedir>] [-g|--grill]
|
|
365
|
+
[-r|--grill-rounds <n>] [-v|--verify] [-l|--loop] [<query>]
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Without a command, the skill *activates* the SpecBook know-how: it
|
|
369
|
+
learns the format and the schema configuration through the MCP tool
|
|
370
|
+
`specbook_describe`, so the specification can be queried and changed
|
|
371
|
+
ad-hoc in plain conversation for the remainder of the session, and it
|
|
372
|
+
directly serves an optional *query*. The agent invokes the skill this
|
|
373
|
+
way on its own, as soon as you want to read or change the specification.
|
|
374
|
+
|
|
375
|
+
The commands `init`, `lint`, and `export` just pass their options
|
|
376
|
+
through to the corresponding MCP tools. The command `edit` edits the
|
|
377
|
+
specification in one shot from a *query*: it learns the format and the
|
|
378
|
+
schema configuration through the MCP tool `specbook_describe`, reads the
|
|
379
|
+
related specification files, optionally interviews you about the open
|
|
380
|
+
points of the query first (`--grill`, for `--grill-rounds` rounds),
|
|
381
|
+
applies the change set, optionally lints the result and fixes the
|
|
382
|
+
reported diagnostics (`--verify`), and optionally asks for the next
|
|
383
|
+
query (`--loop`). All commands leave an absent `--config` and
|
|
384
|
+
`--basedir` to the MCP service, which resolves them through
|
|
385
|
+
`SPECBOOK_CONFIG` and `SPECBOOK_BASEDIR` and then through the
|
|
386
|
+
`.specbook.yaml` found from the working directory of the session upwards.
|
|
387
|
+
|
|
301
388
|
Example: Simple
|
|
302
389
|
---------------
|
|
303
390
|
|
|
@@ -312,7 +399,7 @@ example data model:
|
|
|
312
399
|
### Generation
|
|
313
400
|
|
|
314
401
|
```bash
|
|
315
|
-
$ specbook
|
|
402
|
+
$ specbook export -v \
|
|
316
403
|
-b smp/sample \
|
|
317
404
|
-o smp/sample/sample.html \
|
|
318
405
|
-o smp/sample/sample.pdf
|
|
@@ -337,7 +424,7 @@ application, based on **SpecBook**'s built-in "standard" schema:
|
|
|
337
424
|
### Generation
|
|
338
425
|
|
|
339
426
|
```bash
|
|
340
|
-
$ specbook
|
|
427
|
+
$ specbook export -v \
|
|
341
428
|
-b smp/broadcast \
|
|
342
429
|
-o smp/broadcast/broadcast.html \
|
|
343
430
|
-o smp/broadcast/broadcast.pdf
|
package/dst/specbook-api.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { projectFile } from "./specbook-project.js";
|
|
1
2
|
import { renderDiagnostic, renderDiagnosticVerbose, type Diagnostic, type DiagnosticSeverity } from "./specbook-diagnostic.js";
|
|
2
3
|
import { type LintResult } from "./specbook-cmd-lint.js";
|
|
3
|
-
import { parseOutputSpec, formats, type ExportFormat } from "./specbook-cmd-export.js";
|
|
4
|
+
import { parseOutputSpec, formats, omitAspects, type ExportFormat } from "./specbook-cmd-export.js";
|
|
4
5
|
import { previewAddr, previewPort } from "./specbook-cmd-preview.js";
|
|
5
6
|
import { describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel, type DescribeFormat, type DescribePart, type CompressLevel } from "./specbook-cmd-describe.js";
|
|
6
7
|
import { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity, type Verbose, type VerboseLevel, type Verbosity } from "./specbook-verbose.js";
|
|
7
8
|
export { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity };
|
|
8
9
|
export { type Verbose, type VerboseLevel, type Verbosity };
|
|
9
|
-
export { formats, parseOutputSpec, type ExportFormat };
|
|
10
|
+
export { formats, parseOutputSpec, omitAspects, type ExportFormat };
|
|
10
11
|
export { previewAddr, previewPort };
|
|
12
|
+
export { projectFile };
|
|
11
13
|
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
12
14
|
export { compressLevels, parseCompressLevel };
|
|
13
15
|
export { type DescribeFormat, type DescribePart, type CompressLevel };
|
|
@@ -21,49 +23,48 @@ export type VerboseSink = (cmd: string, msg: string, level: VerboseLevel) => voi
|
|
|
21
23
|
export interface SpecBookOptions {
|
|
22
24
|
verbose?: VerboseSink;
|
|
23
25
|
}
|
|
26
|
+
export interface ProjectOptions {
|
|
27
|
+
config?: string[];
|
|
28
|
+
basedir?: string;
|
|
29
|
+
cwd?: string;
|
|
30
|
+
}
|
|
24
31
|
export declare class SpecBook {
|
|
25
32
|
private verbose;
|
|
26
33
|
constructor(options?: SpecBookOptions);
|
|
27
34
|
private verboseOf;
|
|
35
|
+
private project;
|
|
28
36
|
private configFiles;
|
|
29
37
|
private requireConfig;
|
|
30
|
-
init(options:
|
|
31
|
-
|
|
32
|
-
basedir?: string;
|
|
33
|
-
}): Promise<string[]>;
|
|
34
|
-
lint(options: {
|
|
35
|
-
config?: string[];
|
|
36
|
-
basedir?: string;
|
|
38
|
+
init(options: ProjectOptions): Promise<string[]>;
|
|
39
|
+
lint(options: ProjectOptions & {
|
|
37
40
|
gitignore?: boolean;
|
|
38
41
|
}): Promise<LintResult>;
|
|
39
42
|
private renderFormats;
|
|
40
|
-
export(options: {
|
|
41
|
-
config?: string[];
|
|
42
|
-
basedir?: string;
|
|
43
|
+
export(options: ProjectOptions & {
|
|
43
44
|
formats?: ExportFormat[];
|
|
44
45
|
realtime?: boolean;
|
|
46
|
+
omit?: string[];
|
|
45
47
|
gitignore?: boolean;
|
|
48
|
+
rebase?: (string | undefined)[];
|
|
46
49
|
}): Promise<Buffer[]>;
|
|
50
|
+
private rebaseOf;
|
|
47
51
|
private observe;
|
|
48
|
-
watch(options: {
|
|
49
|
-
config?: string[];
|
|
50
|
-
basedir?: string;
|
|
52
|
+
watch(options: ProjectOptions & {
|
|
51
53
|
formats?: ExportFormat[];
|
|
52
54
|
realtime?: boolean;
|
|
55
|
+
omit?: string[];
|
|
53
56
|
gitignore?: boolean;
|
|
54
57
|
outputs?: string[];
|
|
58
|
+
rebase?: (string | undefined)[];
|
|
55
59
|
onExport: (buffers: Buffer[]) => void | Promise<void>;
|
|
56
60
|
}): Promise<void>;
|
|
57
|
-
preview(options: {
|
|
58
|
-
config?: string[];
|
|
59
|
-
basedir?: string;
|
|
61
|
+
preview(options: ProjectOptions & {
|
|
60
62
|
addr?: string;
|
|
61
63
|
port?: number;
|
|
64
|
+
omit?: string[];
|
|
62
65
|
gitignore?: boolean;
|
|
63
66
|
}): Promise<void>;
|
|
64
|
-
describe(options: {
|
|
65
|
-
config?: string[];
|
|
66
|
-
basedir?: string;
|
|
67
|
+
describe(options: ProjectOptions & {
|
|
67
68
|
embed?: boolean;
|
|
68
69
|
compress?: CompressLevel;
|
|
69
70
|
format?: DescribeFormat;
|
package/dst/specbook-api.js
CHANGED
|
@@ -8,17 +8,19 @@ import * as path from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { glob } from "glob";
|
|
10
10
|
import { loadConfig, configStatistics } from "./specbook-config.js";
|
|
11
|
+
import { loadProject, projectFile } from "./specbook-project.js";
|
|
11
12
|
import { renderDiagnostic, renderDiagnosticVerbose } from "./specbook-diagnostic.js";
|
|
12
13
|
import { initSpecification } from "./specbook-cmd-init.js";
|
|
13
14
|
import { lint } from "./specbook-cmd-lint.js";
|
|
14
|
-
import { exportSpecification, watchSpecification, parseOutputSpec, formats, requireBrowser } from "./specbook-cmd-export.js";
|
|
15
|
+
import { exportSpecification, watchSpecification, parseOutputSpec, formats, requireBrowser, parseOmit, omitAspects } from "./specbook-cmd-export.js";
|
|
15
16
|
import { servePreview, previewAddr, previewPort } from "./specbook-cmd-preview.js";
|
|
16
17
|
import { describeFormat, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, compressLevels, parseCompressLevel } from "./specbook-cmd-describe.js";
|
|
17
18
|
import { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity } from "./specbook-verbose.js";
|
|
18
19
|
/* re-export the central types for API consumers */
|
|
19
20
|
export { literal, renderVerbose, verbosities, verbosityOf, parseVerbosity };
|
|
20
|
-
export { formats, parseOutputSpec };
|
|
21
|
+
export { formats, parseOutputSpec, omitAspects };
|
|
21
22
|
export { previewAddr, previewPort };
|
|
23
|
+
export { projectFile };
|
|
22
24
|
export { describeFormats, describeParts, parseDescribeFormat, parseDescribePart };
|
|
23
25
|
export { compressLevels, parseCompressLevel };
|
|
24
26
|
export { renderDiagnostic, renderDiagnosticVerbose };
|
|
@@ -39,6 +41,35 @@ export class SpecBook {
|
|
|
39
41
|
verboseOf(cmd) {
|
|
40
42
|
return (msg, level = "notice") => this.verbose(cmd, msg, level);
|
|
41
43
|
}
|
|
44
|
+
/* determine the effective project options: a given option wins over
|
|
45
|
+
its environment variable (SPECBOOK_CONFIG, a path-delimiter-separated
|
|
46
|
+
list of patterns, and SPECBOOK_BASEDIR), which in turn wins over
|
|
47
|
+
its entry in the project configuration file. A given working
|
|
48
|
+
directory anchors the given relative paths and keeps all paths
|
|
49
|
+
absolute, else the project ones are rendered relative to ours */
|
|
50
|
+
project(options, verbose) {
|
|
51
|
+
const cwd = path.resolve(options.cwd ?? ".");
|
|
52
|
+
const project = loadProject(cwd);
|
|
53
|
+
if (project !== undefined)
|
|
54
|
+
verbose(`using project configuration "${literal(project.file)}"`);
|
|
55
|
+
const env = (name) => {
|
|
56
|
+
const value = process.env[name];
|
|
57
|
+
return value !== undefined && value !== "" ? value : undefined;
|
|
58
|
+
};
|
|
59
|
+
const config = options.config !== undefined && options.config.length > 0 ? options.config :
|
|
60
|
+
env("SPECBOOK_CONFIG")?.split(path.delimiter).filter((pattern) => pattern !== "");
|
|
61
|
+
const basedir = options.basedir ?? env("SPECBOOK_BASEDIR");
|
|
62
|
+
const given = (value) => options.cwd !== undefined ? path.resolve(cwd, value) : value;
|
|
63
|
+
const derived = (value) => options.cwd !== undefined ? value : path.relative(cwd, value) || ".";
|
|
64
|
+
return {
|
|
65
|
+
config: config !== undefined && config.length > 0 ?
|
|
66
|
+
config.map((pattern) => pattern === "std" ? pattern : given(pattern)) :
|
|
67
|
+
project?.config?.map((pattern) => pattern === "std" ? pattern : derived(pattern)),
|
|
68
|
+
basedir: basedir !== undefined ? given(basedir) :
|
|
69
|
+
project?.basedir !== undefined ? derived(project.basedir) :
|
|
70
|
+
options.cwd !== undefined ? cwd : undefined
|
|
71
|
+
};
|
|
72
|
+
}
|
|
42
73
|
/* determine the files of the YAML schema configuration out of the
|
|
43
74
|
glob patterns, in the order of the patterns and alphabetically
|
|
44
75
|
within a pattern, where the literal "std" names the bundled
|
|
@@ -78,23 +109,27 @@ export class SpecBook {
|
|
|
78
109
|
below the base directory */
|
|
79
110
|
async init(options) {
|
|
80
111
|
const verbose = this.verboseOf("init");
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
const project = this.project(options, verbose);
|
|
113
|
+
return initSpecification({ config: this.requireConfig(await this.configFiles(project.config), verbose),
|
|
114
|
+
basedir: project.basedir ?? ".", verbose });
|
|
83
115
|
}
|
|
84
116
|
/* lint the specification Markdown files below the base directory,
|
|
85
117
|
where "gitignore" treats an artifact file Git excludes from its
|
|
86
118
|
project exactly like an absent one */
|
|
87
119
|
async lint(options) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
120
|
+
const verbose = this.verboseOf("lint");
|
|
121
|
+
const project = this.project(options, verbose);
|
|
122
|
+
return lint({ config: await this.configFiles(project.config),
|
|
123
|
+
basedir: project.basedir ?? ".", gitignore: options.gitignore === true, verbose });
|
|
91
124
|
}
|
|
92
125
|
/* render an already parsed specification into the requested formats
|
|
93
126
|
(strict: any error diagnostic prevents the export, as a partial or
|
|
94
127
|
invalid specification must never be emitted, while the warnings
|
|
95
|
-
are just surfaced as notices), where "
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
are just surfaced as notices), where "rendering" carries the
|
|
129
|
+
rendering options (the live preview script, the omitted aspects)
|
|
130
|
+
and "rebase" names, per requested format, the directory the image
|
|
131
|
+
references of the normalized Markdown are re-based onto */
|
|
132
|
+
async renderFormats(result, requested, verbose, rendering, rebase) {
|
|
98
133
|
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error"))
|
|
99
134
|
throw new Error("invalid specification:\n" +
|
|
100
135
|
result.diagnostics.map(renderDiagnostic).join("\n"));
|
|
@@ -103,24 +138,38 @@ export class SpecBook {
|
|
|
103
138
|
if (result.specification.artifacts.length === 0)
|
|
104
139
|
throw new Error("unexportable specification: no artifacts found");
|
|
105
140
|
const buffers = new Array();
|
|
106
|
-
for (const format of requested)
|
|
107
|
-
|
|
141
|
+
for (const [i, format] of requested.entries()) {
|
|
142
|
+
const dir = rebase?.[i];
|
|
143
|
+
buffers.push(await exportSpecification(result.specification, format, verbose, result.config, rendering, dir !== undefined ? { origins: result.origins, dir } : undefined));
|
|
144
|
+
}
|
|
108
145
|
return buffers;
|
|
109
146
|
}
|
|
110
147
|
/* export the specification Markdown files below the base directory
|
|
111
148
|
as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown,
|
|
112
149
|
parsing the input just once and returning one buffer per
|
|
113
|
-
requested format
|
|
150
|
+
requested format, where "rebase" names, per requested format, the
|
|
151
|
+
directory of the output file, onto which the image references of
|
|
152
|
+
the normalized Markdown are re-based (an absent one keeps them),
|
|
153
|
+
and "omit" names the (comma-separated) aspects the HTML, the PDF,
|
|
154
|
+
and (the diagrams) the AST leave out (see "omitAspects") */
|
|
114
155
|
async export(options) {
|
|
115
156
|
const verbose = this.verboseOf("export");
|
|
116
157
|
const requested = options.formats ?? ["json"];
|
|
158
|
+
const omit = parseOmit(options.omit);
|
|
159
|
+
const project = this.project(options, verbose);
|
|
117
160
|
/* a missing browser is an environment problem, so let the PDF
|
|
118
161
|
export fail before the specification is even parsed */
|
|
119
162
|
if (requested.includes("pdf"))
|
|
120
163
|
await requireBrowser(verbose);
|
|
121
|
-
const result = lint({ config: await this.configFiles(
|
|
122
|
-
basedir:
|
|
123
|
-
return this.renderFormats(result, requested, verbose, options.realtime === true);
|
|
164
|
+
const result = lint({ config: await this.configFiles(project.config),
|
|
165
|
+
basedir: project.basedir ?? ".", gitignore: options.gitignore === true, verbose });
|
|
166
|
+
return this.renderFormats(result, requested, verbose, { realtime: options.realtime === true, omit }, this.rebaseOf(options));
|
|
167
|
+
}
|
|
168
|
+
/* anchor the given relative re-basing directories
|
|
169
|
+
at a given working directory */
|
|
170
|
+
rebaseOf(options) {
|
|
171
|
+
const cwd = options.cwd;
|
|
172
|
+
return cwd === undefined ? options.rebase : options.rebase?.map((dir) => dir !== undefined ? path.resolve(cwd, dir) : dir);
|
|
124
173
|
}
|
|
125
174
|
/* keep an export in sync with its sources (the shared core of
|
|
126
175
|
"watch" and "preview"): every change of a configuration file, of an
|
|
@@ -132,24 +181,26 @@ export class SpecBook {
|
|
|
132
181
|
once the initial export is done, while the active watcher keeps the
|
|
133
182
|
process alive afterwards */
|
|
134
183
|
async observe(options, verbose) {
|
|
135
|
-
const
|
|
184
|
+
const omit = parseOmit(options.omit);
|
|
185
|
+
const project = this.project(options, verbose);
|
|
186
|
+
const config = await this.configFiles(project.config);
|
|
136
187
|
return watchSpecification(async () => {
|
|
137
188
|
/* the lint result carries the files to observe even for an
|
|
138
189
|
invalid specification, and the configuration files are
|
|
139
190
|
observed, too, so a failing export (even one due to an
|
|
140
191
|
invalid configuration) still keeps the observe loop fed */
|
|
141
|
-
const result = lint({ config, basedir:
|
|
192
|
+
const result = lint({ config, basedir: project.basedir ?? ".",
|
|
142
193
|
gitignore: options.gitignore === true, verbose });
|
|
143
194
|
const files = [...config.map((file) => path.resolve(file)), ...result.files];
|
|
144
195
|
/* an output which is itself an observed source would re-trigger
|
|
145
196
|
the observation with its own write and hence feed an endless
|
|
146
197
|
re-export loop, so refuse it before it is ever written */
|
|
147
|
-
const collision = options.outputs?.find((output) => files.includes(path.resolve(output)));
|
|
198
|
+
const collision = options.outputs?.find((output) => files.includes(path.resolve(options.cwd ?? ".", output)));
|
|
148
199
|
if (collision !== undefined)
|
|
149
200
|
throw new Error(`the output "${collision}" is an observed source file, ` +
|
|
150
201
|
"which would re-trigger the observation endlessly");
|
|
151
202
|
try {
|
|
152
|
-
await options.onExport(await this.renderFormats(result, options.formats, verbose, options.realtime));
|
|
203
|
+
await options.onExport(await this.renderFormats(result, options.formats, verbose, { realtime: options.realtime, omit }, this.rebaseOf(options)));
|
|
153
204
|
}
|
|
154
205
|
catch (err) {
|
|
155
206
|
verbose("export failed: " +
|
|
@@ -161,7 +212,7 @@ export class SpecBook {
|
|
|
161
212
|
/* export the specification like "export" and then keep the export
|
|
162
213
|
in sync with its sources (see "observe"), where "outputs" names the
|
|
163
214
|
files "onExport" writes, so an output which is itself an observed
|
|
164
|
-
source can be refused */
|
|
215
|
+
source can be refused, and "rebase" and "omit" are the ones of "export" */
|
|
165
216
|
async watch(options) {
|
|
166
217
|
const verbose = this.verboseOf("export");
|
|
167
218
|
const requested = options.formats ?? ["json"];
|
|
@@ -174,13 +225,14 @@ export class SpecBook {
|
|
|
174
225
|
"http://<addr>:<port>/": the export is kept in sync with its
|
|
175
226
|
sources (see "observe") and every fresh export is pushed to the
|
|
176
227
|
connected browsers as an in-place document update, through the
|
|
177
|
-
client-side script the "realtime" export injects into the HTML
|
|
228
|
+
client-side script the "realtime" export injects into the HTML,
|
|
229
|
+
where "omit" is the one of "export" */
|
|
178
230
|
async preview(options) {
|
|
179
231
|
const verbose = this.verboseOf("preview");
|
|
180
232
|
const server = await servePreview({ addr: options.addr ?? previewAddr,
|
|
181
233
|
port: options.port ?? previewPort, verbose });
|
|
182
|
-
return this.observe({ config: options.config, basedir: options.basedir,
|
|
183
|
-
formats: ["html"], realtime: true, gitignore: options.gitignore,
|
|
234
|
+
return this.observe({ config: options.config, basedir: options.basedir, cwd: options.cwd,
|
|
235
|
+
formats: ["html"], realtime: true, omit: options.omit, gitignore: options.gitignore,
|
|
184
236
|
onExport: ([html]) => server.update(html) }, verbose);
|
|
185
237
|
}
|
|
186
238
|
/* describe the generic SpecBook models and formats as Markdown (or
|
|
@@ -192,14 +244,15 @@ export class SpecBook {
|
|
|
192
244
|
const verbose = this.verboseOf("describe");
|
|
193
245
|
const format = options.format ?? "md";
|
|
194
246
|
const part = options.part ?? "all";
|
|
247
|
+
const project = this.project(options, verbose);
|
|
195
248
|
/* the schema-bearing parts require a schema configuration, so
|
|
196
249
|
they fall back onto the bundled standard one, which is always
|
|
197
250
|
embedded, as its bundled file is no meaningful reference */
|
|
198
|
-
const given =
|
|
251
|
+
const given = project.config !== undefined;
|
|
199
252
|
const standard = !given && (part === "all" || part === "schema");
|
|
200
|
-
const config = given || standard ? await this.configFiles(
|
|
253
|
+
const config = given || standard ? await this.configFiles(project.config) : undefined;
|
|
201
254
|
const embed = options.embed === true || standard;
|
|
202
|
-
const basedir =
|
|
255
|
+
const basedir = project.basedir ?? (part === "spec" ? "." : undefined);
|
|
203
256
|
const schema = config !== undefined ? this.requireConfig(config, verbose) : undefined;
|
|
204
257
|
verbose(`describing the "${literal(part)}" part of the SpecBook models and formats ` +
|
|
205
258
|
`in the "${literal(format)}" format`);
|
package/dst/specbook-cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from "node:fs";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { Command, CommanderError } from "commander";
|
|
10
10
|
import chalk from "chalk";
|
|
11
|
-
import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, previewAddr, previewPort, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, parseCompressLevel, parseVerbosity, verbosityOf, version } from "./specbook-api.js";
|
|
11
|
+
import { SpecBook, renderDiagnostic, renderDiagnosticVerbose, renderVerbose, literal, parseOutputSpec, previewAddr, previewPort, describeFormats, describeParts, parseDescribeFormat, parseDescribePart, parseCompressLevel, parseVerbosity, verbosityOf, projectFile, omitAspects, version } from "./specbook-api.js";
|
|
12
12
|
import { serveMcp } from "./specbook-mcp.js";
|
|
13
13
|
/* route verbose messages to stderr, keeping stdout reserved for the
|
|
14
14
|
command outputs and the MCP protocol, and qualify every message with
|
|
@@ -69,17 +69,23 @@ const withVerboseOption = (command) => command
|
|
|
69
69
|
"3: also the traces like the unreferenced objects; default: 0, bare flag: 1)", envDefault("verbose", "0"));
|
|
70
70
|
/* provide the repeatable schema configuration option, whose files or
|
|
71
71
|
glob patterns are merged in order (with "std" naming the bundled
|
|
72
|
-
standard one), and determine its value, where
|
|
73
|
-
|
|
72
|
+
standard one), and determine its value, where an absent one is left
|
|
73
|
+
to the API, which resolves it through the environment variable
|
|
74
|
+
SPECBOOK_CONFIG and the project configuration file */
|
|
74
75
|
const withConfigOption = (command, fallback) => command
|
|
75
76
|
.option("-c, --config <yaml-file>", "YAML schema configuration file or glob pattern " +
|
|
76
77
|
"(repeatable, merged in order, \"std\" for the bundled standard schema configuration; " +
|
|
77
|
-
`default: ${fallback})`, (value, previous) => previous.concat(value), new Array());
|
|
78
|
-
const configOf = (opts) => opts.config.length > 0 ? opts.config :
|
|
78
|
+
`default: SPECBOOK_CONFIG, else the "config" entry of "${projectFile}", else ${fallback})`, (value, previous) => previous.concat(value), new Array());
|
|
79
|
+
const configOf = (opts) => opts.config.length > 0 ? opts.config : undefined;
|
|
80
|
+
/* provide the base directory option, where an absent one is left to
|
|
81
|
+
the API, which resolves it through the environment variable
|
|
82
|
+
SPECBOOK_BASEDIR and the project configuration file */
|
|
83
|
+
const withBasedirOption = (command, fallback) => command
|
|
84
|
+
.option("-b, --basedir <directory>", "base directory of the specification Markdown files " +
|
|
85
|
+
`(default: SPECBOOK_BASEDIR, else the "basedir" entry of "${projectFile}", else ${fallback})`);
|
|
79
86
|
/* provide the common options of the specification processing sub-commands,
|
|
80
87
|
for which the YAML schema configuration falls back onto the standard one */
|
|
81
|
-
const withCommonOptions = (command) => withConfigOption(withVerboseOption(command), "the bundled standard schema configuration")
|
|
82
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir", "."));
|
|
88
|
+
const withCommonOptions = (command) => withBasedirOption(withConfigOption(withVerboseOption(command), "the bundled standard schema configuration"), "\".\"");
|
|
83
89
|
/* provide the Git exclude option of the specification *reading*
|
|
84
90
|
sub-commands (not of "init", which creates artifact files instead of
|
|
85
91
|
reading them), which treats an artifact file Git excludes from its
|
|
@@ -87,6 +93,16 @@ const withCommonOptions = (command) => withConfigOption(withVerboseOption(comman
|
|
|
87
93
|
const withGitignoreOption = (command) => command
|
|
88
94
|
.option("-g, --gitignore", "skip the artifact files excluded by the Git exclude rules " +
|
|
89
95
|
"(the \".gitignore\" files, \"info/exclude\", and the global excludes file)", envDefaultFlag("gitignore", false));
|
|
96
|
+
/* provide the repeatable omit option of the HTML-rendering sub-commands,
|
|
97
|
+
whose aspects are left out of the output instead of being foldable,
|
|
98
|
+
and determine its value (validated by the API) */
|
|
99
|
+
const withOmitOption = (command) => command
|
|
100
|
+
.option("-O, --omit <aspect>[,...]", "omit content aspects from the HTML, the PDF, and (the " +
|
|
101
|
+
`diagrams) the AST outputs (repeatable): "diagram" (all) or "${omitAspects.join("\", \"")}"`, (value, previous) => previous.concat(value), new Array());
|
|
102
|
+
const omitOf = (opts) => {
|
|
103
|
+
const fallback = envDefault("omit");
|
|
104
|
+
return opts.omit.length > 0 ? opts.omit : (fallback !== undefined ? [fallback] : undefined);
|
|
105
|
+
};
|
|
90
106
|
/* the help, version, and usage-error output Commander produces, which
|
|
91
107
|
is collected instead of written directly, as Commander writes it
|
|
92
108
|
synchronously and then terminates the process, truncating a piped
|
|
@@ -131,8 +147,9 @@ withGitignoreOption(withCommonOptions(program.command("lint")))
|
|
|
131
147
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
132
148
|
const result = await specbook.lint({ config: configOf(opts), basedir: opts.basedir,
|
|
133
149
|
gitignore: opts.gitignore });
|
|
150
|
+
const detailed = parseVerbosity(opts.verbose) > 0;
|
|
134
151
|
for (const diagnostic of result.diagnostics)
|
|
135
|
-
await writeStdout(
|
|
152
|
+
await writeStdout(detailed ?
|
|
136
153
|
renderDiagnosticVerbose(diagnostic, process.stdout.isTTY === true) :
|
|
137
154
|
`${renderDiagnostic(diagnostic)}\n`);
|
|
138
155
|
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error"))
|
|
@@ -141,37 +158,48 @@ withGitignoreOption(withCommonOptions(program.command("lint")))
|
|
|
141
158
|
verboseOf(opts)("lint", "specification valid", "notice");
|
|
142
159
|
});
|
|
143
160
|
/* the export command parses the input once and writes every output */
|
|
144
|
-
withGitignoreOption(withCommonOptions(program.command("export")))
|
|
161
|
+
withOmitOption(withGitignoreOption(withCommonOptions(program.command("export"))))
|
|
145
162
|
.description("export the specification Markdown files below the base directory " +
|
|
146
163
|
"as JSON, JSON5, YAML, TOON, HTML, PDF, or normalized Markdown")
|
|
147
164
|
.option("-w, --watch", "keep the outputs in sync by re-exporting on every source change", envDefaultFlag("watch", false))
|
|
148
165
|
.option("-o, --output [<format>:]<output-file>", "output file (\"-\" for stdout, repeatable), with the format inferred " +
|
|
149
166
|
"from the filename extension unless explicitly prefixed", (value, previous) => previous.concat(value), new Array())
|
|
150
167
|
.action(async (opts) => {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
168
|
+
const verbose = verboseOf(opts);
|
|
169
|
+
const specbook = new SpecBook({ verbose });
|
|
170
|
+
const fallback = envDefault("output");
|
|
171
|
+
const given = opts.output.length > 0 ? opts.output : (fallback !== undefined ? [fallback] : []);
|
|
172
|
+
if (given.length === 0)
|
|
173
|
+
throw new Error("the export requires at least one output " +
|
|
174
|
+
"(use option \"-o [<format>:]<output-file>\", with \"-\" for stdout)");
|
|
175
|
+
const outputs = given.map(parseOutputSpec);
|
|
154
176
|
/* a re-export has to land somewhere it can be picked up again,
|
|
155
177
|
which a one-shot stdout stream cannot provide */
|
|
156
178
|
if (opts.watch && outputs.some(({ output }) => output === "-"))
|
|
157
179
|
throw new Error("the watch mode requires regular output files " +
|
|
158
180
|
"(\"-\" for stdout is not supported)");
|
|
159
|
-
/* parse the input once and export each distinct format once
|
|
160
|
-
|
|
181
|
+
/* parse the input once and export each distinct format once, the
|
|
182
|
+
normalized Markdown once per output directory, as its image
|
|
183
|
+
references are re-based onto it (but never for stdout) */
|
|
184
|
+
const rebaseOf = ({ format, output }) => format === "md" && output !== "-" ? path.dirname(path.resolve(output)) : undefined;
|
|
185
|
+
const keyOf = (spec) => `${spec.format}:${rebaseOf(spec) ?? ""}`;
|
|
186
|
+
const distinct = outputs.filter((spec, i) => outputs.findIndex((other) => keyOf(other) === keyOf(spec)) === i);
|
|
161
187
|
const write = async (buffers) => {
|
|
162
|
-
for (const
|
|
163
|
-
await writeOutput(output, buffers[distinct.
|
|
188
|
+
for (const spec of outputs)
|
|
189
|
+
await writeOutput(spec.output, buffers[distinct.findIndex((other) => keyOf(other) === keyOf(spec))], "export", verbose);
|
|
164
190
|
};
|
|
191
|
+
const formats = distinct.map(({ format }) => format);
|
|
192
|
+
const rebase = distinct.map(rebaseOf);
|
|
165
193
|
if (opts.watch)
|
|
166
194
|
await specbook.watch({ config: configOf(opts), basedir: opts.basedir,
|
|
167
|
-
formats:
|
|
195
|
+
formats, rebase, omit: omitOf(opts), gitignore: opts.gitignore,
|
|
168
196
|
outputs: outputs.map(({ output }) => output), onExport: write });
|
|
169
197
|
else
|
|
170
198
|
await write(await specbook.export({ config: configOf(opts), basedir: opts.basedir,
|
|
171
|
-
formats:
|
|
199
|
+
formats, rebase, omit: omitOf(opts), gitignore: opts.gitignore }));
|
|
172
200
|
});
|
|
173
201
|
/* the preview command serves the HTML export live in the browser */
|
|
174
|
-
withGitignoreOption(withCommonOptions(program.command("preview")))
|
|
202
|
+
withOmitOption(withGitignoreOption(withCommonOptions(program.command("preview"))))
|
|
175
203
|
.description("serve the HTML export of the specification Markdown files below the base " +
|
|
176
204
|
"directory as a live preview, re-exported and reloaded on every source change")
|
|
177
205
|
.option("-a, --addr <ip-addr>", "IP address to listen on", envDefault("addr", previewAddr))
|
|
@@ -182,17 +210,16 @@ withGitignoreOption(withCommonOptions(program.command("preview")))
|
|
|
182
210
|
throw new Error(`invalid TCP port "${opts.port}"`);
|
|
183
211
|
const specbook = new SpecBook({ verbose: verboseOf(opts) });
|
|
184
212
|
await specbook.preview({ config: configOf(opts), basedir: opts.basedir, addr: opts.addr, port,
|
|
185
|
-
gitignore: opts.gitignore });
|
|
213
|
+
omit: omitOf(opts), gitignore: opts.gitignore });
|
|
186
214
|
});
|
|
187
215
|
/* the describe command also describes the generic SpecBook models and
|
|
188
216
|
formats alone, so its YAML schema configuration stays optional */
|
|
189
|
-
withConfigOption(withVerboseOption(program.command("describe")), "the bundled standard schema configuration, embedded")
|
|
217
|
+
withBasedirOption(withConfigOption(withVerboseOption(program.command("describe")), "the bundled standard schema configuration, embedded"), "none")
|
|
190
218
|
.description("describe the SpecBook models and formats as Markdown")
|
|
191
|
-
.option("-b, --basedir <directory>", "base directory of the specification Markdown files", envDefault("basedir"))
|
|
192
219
|
.option("-e, --embed", "embed the given YAML schema configuration instead of just referencing it", envDefaultFlag("embed", false))
|
|
193
220
|
.option("-z, --compress [level]", "compression level of the emitted YAML schema configuration " +
|
|
194
221
|
"(0: verbatim, 1: re-emitted with 2-space indentation and without comments, " +
|
|
195
|
-
"2: also without \"refs\" fields, " +
|
|
222
|
+
"2: also without \"refs\", \"diagram\", and \"format\" fields, " +
|
|
196
223
|
"3: also without \"desc\" fields; default and bare flag: 1)", envDefault("compress", "1"))
|
|
197
224
|
.option("-f, --format <format>", `output format (${describeFormats.join(", ")})`, envDefault("format", "md"))
|
|
198
225
|
.option("-p, --part <part>", `document part (${describeParts.join(", ")})`, envDefault("part", "all"))
|