@rse/specbook 1.0.2 → 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/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/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;
|