zombie-mermaid 5.3.0 → 5.4.0
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 +2 -1
- package/dist/cli.js +180 -170
- package/package.json +13 -8
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Ultra-fast, fully themeable, zero DOM dependencies. A maintained fork of [`beaut
|
|
|
30
30
|
|
|
31
31
|
`beautiful-mermaid` is a genuinely good library — fast, beautiful, and works everywhere from rich UIs to plain terminals. Checkable facts, as of 2026-10-13: the last push to upstream's `main` was 2026-05-06 and its latest release is v1.1.2 (2026-02-26); it has [43 open pull requests](https://github.com/lukilabs/beautiful-mermaid/pulls) (19 opened before 2026-04-13) and [50 open issues](https://github.com/lukilabs/beautiful-mermaid/issues) (live counts at those links; the figures above are a dated snapshot). I maintain `zombie-mermaid` as the fork that won't stay buried: pulling in upstream fixes, giving PRs stuck in the upstream queue a home, and actually shipping releases. Craft and Craft Agents aren't part of this project's process going forward; this is an independently maintained continuation.
|
|
32
32
|
|
|
33
|
-
This isn't just a claim — see **[what this fork fixes](https://dfadler.github.io/zombie-mermaid/fork-fixes.html)** for an evidence-based before/after showcase (
|
|
33
|
+
This isn't just a claim — see **[what this fork fixes](https://dfadler.github.io/zombie-mermaid/fork-fixes.html)** for an evidence-based before/after showcase (38 fixes): every bug listed there is rendered by the actual pre-fix and post-fix code, not described from memory.
|
|
34
34
|
|
|
35
35
|
It's still MIT-licensed, still built on the same foundation, and still credits the original authors — including the ASCII rendering engine's origins — in [Attribution](#attribution) below.
|
|
36
36
|
|
|
@@ -223,6 +223,7 @@ zombie-mermaid --help # Show all options
|
|
|
223
223
|
`--hyperlinks` (with `--ascii`) turns `click` hrefs into OSC 8 terminal
|
|
224
224
|
hyperlinks — off by default, since terminal and pager support varies (see
|
|
225
225
|
[ASCII terminal hyperlinks](docs/diagrams.md#ascii-terminal-hyperlinks)).
|
|
226
|
+
`--profile markdown` (with `--ascii`) emits plain ASCII characters with no color, for pasting into issues, commits and fenced blocks (the `profile` option in the library and the MCP `render_mermaid_ascii` tool).
|
|
226
227
|
|
|
227
228
|
**Output rules.** `-o` is the destination for the run's _file_ output — SVG, HTML, or PNG when `--svg`/`--html`/`--png` is given, otherwise the ASCII rendering — and `-o -` sends it to stdout instead. A recognised extension (`.svg`, `.html`/`.htm`, `.txt`, `.png`) picks the format on its own, so the flag can be dropped; an extension that contradicts an explicit flag (`--svg -o out.txt`) is an error, while unrecognised ones are simply used as given. `--svg`/`--html`/`--png` with no `-o` writes `<input stem>.svg`/`.html`/`.png` beside the input (stdin input has no name to derive from, so it must pass `-o`). Only one of `--svg`/`--html`/`--png` can be set at a time — run the command twice for more than one. ASCII always prints to the terminal when one of them is also requested (`--ascii --svg -o out.svg`), and never carries ANSI colour codes when written to a file. **An existing output file is never overwritten unless you pass `--force`/`-f`.**
|
|
228
229
|
|
package/dist/cli.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire as
|
|
3
|
-
import { extname as H, format as
|
|
4
|
-
import { MermaidParseError as
|
|
5
|
-
import { access as
|
|
6
|
-
import { diagramColorsToAsciiTheme as
|
|
2
|
+
import { createRequire as q } from "node:module";
|
|
3
|
+
import { extname as H, format as j, parse as F } from "node:path";
|
|
4
|
+
import { MermaidParseError as K, THEMES as O, escapeXml as J, isDirection as Z, isWideChar as Q } from "@zombie-mermaid/core";
|
|
5
|
+
import { access as ee, readFile as G, writeFile as te } from "node:fs/promises";
|
|
6
|
+
import { diagramColorsToAsciiTheme as ne, renderMermaidASCII as _ } from "@zombie-mermaid/ascii-renderer";
|
|
7
7
|
import "@zombie-mermaid/mermaid-parser";
|
|
8
|
-
import { registerElk as
|
|
9
|
-
import { createServer as
|
|
10
|
-
import { StdioServerTransport as
|
|
11
|
-
import { createMcpServer as
|
|
12
|
-
import
|
|
13
|
-
var
|
|
14
|
-
var
|
|
8
|
+
import { registerElk as re, renderMermaidSVG as z, validateMermaid as oe } from "@zombie-mermaid/svg-renderer";
|
|
9
|
+
import { createServer as ie } from "node:http";
|
|
10
|
+
import { StdioServerTransport as ae } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { createMcpServer as se } from "@zombie-mermaid/mcp";
|
|
12
|
+
import de from "elkjs/lib/elk.bundled.js";
|
|
13
|
+
var ce = 3e3;
|
|
14
|
+
var W = {
|
|
15
15
|
".svg": "svg",
|
|
16
16
|
".txt": "ascii",
|
|
17
17
|
".html": "html",
|
|
18
18
|
".htm": "html",
|
|
19
19
|
".png": "png"
|
|
20
20
|
};
|
|
21
|
-
function
|
|
21
|
+
function le(e) {
|
|
22
22
|
if (!(e === void 0 || e === "-"))
|
|
23
|
-
return
|
|
23
|
+
return W[H(e).toLowerCase()];
|
|
24
24
|
}
|
|
25
|
-
var
|
|
25
|
+
var b = {
|
|
26
26
|
svg: {
|
|
27
27
|
flag: "--svg",
|
|
28
28
|
ext: ".svg",
|
|
@@ -44,22 +44,22 @@ var w = {
|
|
|
44
44
|
display: "PNG",
|
|
45
45
|
label: "the PNG rasterization"
|
|
46
46
|
}
|
|
47
|
-
},
|
|
48
|
-
function
|
|
49
|
-
const t =
|
|
50
|
-
return
|
|
47
|
+
}, I = Object.keys(b);
|
|
48
|
+
function me(e, n) {
|
|
49
|
+
const t = F(e);
|
|
50
|
+
return j({
|
|
51
51
|
dir: t.dir,
|
|
52
52
|
name: t.name,
|
|
53
53
|
ext: n
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
function
|
|
56
|
+
function ue(e) {
|
|
57
57
|
const [n, ...t] = e;
|
|
58
58
|
if (n === void 0) return { command: "help" };
|
|
59
59
|
if (n === "--help" || n === "-h") return { command: "help" };
|
|
60
60
|
if (n === "--version" || n === "-v") return { command: "version" };
|
|
61
61
|
if (n === "themes") return { command: "themes" };
|
|
62
|
-
if (n === "render") return
|
|
62
|
+
if (n === "render") return he(t);
|
|
63
63
|
if (n === "validate") {
|
|
64
64
|
const r = t.find((a, d) => d > 0 || a.startsWith("-"));
|
|
65
65
|
if (r !== void 0) throw new Error(`Unexpected argument: ${r}`);
|
|
@@ -68,17 +68,17 @@ function me(e) {
|
|
|
68
68
|
input: t[0]
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
if (n === "web") return
|
|
71
|
+
if (n === "web") return ge(t);
|
|
72
72
|
if (n === "mcp") return { command: "mcp" };
|
|
73
73
|
throw new Error(`Unknown command: ${n}`);
|
|
74
74
|
}
|
|
75
|
-
function
|
|
75
|
+
function $(e, n, t) {
|
|
76
76
|
const r = e[n + 1];
|
|
77
77
|
if (n + 1 >= e.length || r === void 0) throw new Error(`${t} requires a numeric value`);
|
|
78
78
|
if (!/^\d+$/.test(r)) throw new Error(`${t} requires a non-negative integer, got: "${r}"`);
|
|
79
79
|
return Number(r);
|
|
80
80
|
}
|
|
81
|
-
function
|
|
81
|
+
function pe(e, n, t) {
|
|
82
82
|
const r = e[n + 1];
|
|
83
83
|
if (n + 1 >= e.length || r === void 0) throw new Error(`${t} requires a value ("auto" or a positive integer)`);
|
|
84
84
|
if (r === "auto") return "auto";
|
|
@@ -86,15 +86,15 @@ function ue(e, n, t) {
|
|
|
86
86
|
if (!/^\d+$/.test(r) || a === 0 || !Number.isSafeInteger(a)) throw new Error(`${t} requires "auto" or a positive integer, got: "${r}"`);
|
|
87
87
|
return a;
|
|
88
88
|
}
|
|
89
|
-
function
|
|
89
|
+
function fe(e, n, t) {
|
|
90
90
|
const r = e[n + 1];
|
|
91
91
|
if (n + 1 >= e.length || r === void 0) throw new Error(`${t} requires a value (one of TD, TB, BT, LR, RL)`);
|
|
92
92
|
const a = r.toUpperCase();
|
|
93
|
-
if (!
|
|
93
|
+
if (!Z(a)) throw new Error(`${t} requires one of TD, TB, BT, LR, RL, got: "${r}"`);
|
|
94
94
|
return a;
|
|
95
95
|
}
|
|
96
|
-
function
|
|
97
|
-
let n, t = !1, r = !1, a = !1, d = !1, l = !1, m, g = !1, E, S, k, s, u = !1, p = !1, h,
|
|
96
|
+
function he(e) {
|
|
97
|
+
let n, t = !1, r = !1, a = !1, d = !1, l = !1, m, g = !1, E, S, k, s, u = !1, p = !1, h, v, y, i = 0;
|
|
98
98
|
for (; i < e.length; ) {
|
|
99
99
|
const o = e[i];
|
|
100
100
|
if (o === void 0) throw new Error(`parseArgs: index ${i} out of range while parsing arguments`);
|
|
@@ -117,72 +117,77 @@ function fe(e) {
|
|
|
117
117
|
if (i + 1 >= e.length) throw new Error("--theme requires a theme name");
|
|
118
118
|
E = e[i + 1], i += 2;
|
|
119
119
|
} else if (o === "-x" || o === "--paddingX")
|
|
120
|
-
S =
|
|
120
|
+
S = $(e, i, o), i += 2;
|
|
121
121
|
else if (o === "-y" || o === "--paddingY")
|
|
122
|
-
k =
|
|
122
|
+
k = $(e, i, o), i += 2;
|
|
123
123
|
else if (o === "-p" || o === "--borderPadding")
|
|
124
|
-
s =
|
|
124
|
+
s = $(e, i, o), i += 2;
|
|
125
125
|
else if (o === "--coords")
|
|
126
126
|
u = !0, i++;
|
|
127
127
|
else if (o === "--hyperlinks")
|
|
128
128
|
p = !0, i++;
|
|
129
129
|
else if (o === "-w" || o === "--max-width")
|
|
130
|
-
h =
|
|
131
|
-
else if (o === "--
|
|
132
|
-
|
|
130
|
+
h = pe(e, i, o), i += 2;
|
|
131
|
+
else if (o === "--profile") {
|
|
132
|
+
const c = e[i + 1];
|
|
133
|
+
if (c !== "markdown") throw new Error(`--profile requires one of: markdown, got: "${c ?? ""}"`);
|
|
134
|
+
v = c, i += 2;
|
|
135
|
+
} else if (o === "--direction")
|
|
136
|
+
y = fe(e, i, o), i += 2;
|
|
133
137
|
else if (!o.startsWith("-") || o === "-") {
|
|
134
138
|
if (o === "-") throw new Error("Unexpected argument: - (use -o - to write output to stdout; omit the input file to read stdin)");
|
|
135
139
|
if (n !== void 0) throw new Error(`Unexpected argument: ${o} (input file already set to "${n}")`);
|
|
136
140
|
n = o, i++;
|
|
137
141
|
} else throw new Error(`Unknown flag: ${o}`);
|
|
138
142
|
}
|
|
139
|
-
const
|
|
143
|
+
const w = {
|
|
140
144
|
svg: r,
|
|
141
145
|
html: d,
|
|
142
146
|
png: l
|
|
143
|
-
},
|
|
144
|
-
if (
|
|
145
|
-
const [o, c] =
|
|
146
|
-
throw new Error(`${
|
|
147
|
+
}, C = I.filter((o) => w[o]);
|
|
148
|
+
if (C.length > 1) {
|
|
149
|
+
const [o, c] = C;
|
|
150
|
+
throw new Error(`${b[o].flag} and ${b[c].flag} cannot both be set — they would write different content to the same output path. Run the command twice with different -o paths for each.`);
|
|
147
151
|
}
|
|
148
|
-
const f =
|
|
152
|
+
const f = le(m);
|
|
149
153
|
if (f !== void 0 && f !== "ascii") {
|
|
150
|
-
const o =
|
|
154
|
+
const o = I.find((c) => c !== f && w[c]);
|
|
151
155
|
if (o !== void 0) {
|
|
152
|
-
const c =
|
|
153
|
-
throw new Error(`-o ${m} has ${c.article} ${c.ext} extension, but ${
|
|
156
|
+
const c = b[f], P = b[o];
|
|
157
|
+
throw new Error(`-o ${m} has ${c.article} ${c.ext} extension, but ${P.flag} output would be written to it. Use a ${P.ext} path (or -o - for stdout) for ${P.label}.`);
|
|
154
158
|
}
|
|
155
159
|
f === "svg" ? r = !0 : f === "html" ? d = !0 : l = !0;
|
|
156
160
|
} else if (f === "ascii") {
|
|
157
|
-
const o =
|
|
161
|
+
const o = I.find((c) => w[c]);
|
|
158
162
|
if (o !== void 0) {
|
|
159
|
-
const c =
|
|
163
|
+
const c = b[o];
|
|
160
164
|
throw new Error(`-o ${m} has a .txt extension, but ${c.flag} output would be written to it. Use a ${c.ext} path (or -o - for stdout) instead.`);
|
|
161
165
|
}
|
|
162
166
|
t = !0;
|
|
163
167
|
}
|
|
164
168
|
if (!t && !r && !d && !l) {
|
|
165
169
|
if (m !== void 0 && m !== "-") {
|
|
166
|
-
const o = Object.keys(
|
|
170
|
+
const o = Object.keys(W).join(", ");
|
|
167
171
|
throw new Error(`Cannot infer an output format from "${m}" — pass --ascii, --svg, --html, or --png, or use a recognised extension (${o})`);
|
|
168
172
|
}
|
|
169
173
|
throw new Error("Specify --ascii, --svg, --html, and/or --png (or -o <path> with a .svg/.txt/.html/.png extension)");
|
|
170
174
|
}
|
|
171
|
-
const
|
|
175
|
+
const T = {
|
|
172
176
|
svg: r,
|
|
173
177
|
html: d,
|
|
174
178
|
png: l
|
|
175
|
-
}, x =
|
|
179
|
+
}, x = I.find((o) => T[o]);
|
|
176
180
|
if (m === "-" && t && x !== void 0) {
|
|
177
|
-
const o =
|
|
181
|
+
const o = b[x].display;
|
|
178
182
|
throw new Error(`-o - would send both ASCII and ${o} to stdout; drop --ascii, or write the ${o} to a file path`);
|
|
179
183
|
}
|
|
180
184
|
if (x !== void 0 && m === void 0) {
|
|
181
|
-
const { flag: o, ext: c } =
|
|
185
|
+
const { flag: o, ext: c } = b[x];
|
|
182
186
|
if (n === void 0) throw new Error(`${o} needs -o <path> (or -o - for stdout) when reading from stdin — there is no input file name to derive an output name from`);
|
|
183
|
-
m =
|
|
187
|
+
m = me(n, c);
|
|
184
188
|
}
|
|
185
189
|
if (h !== void 0 && !t) throw new Error("-w/--max-width requires --ascii");
|
|
190
|
+
if (v !== void 0 && !t) throw new Error("--profile requires --ascii");
|
|
186
191
|
if (p && !t) throw new Error("--hyperlinks requires --ascii");
|
|
187
192
|
if (a && !r) throw new Error("--resolve-colors requires --svg");
|
|
188
193
|
return {
|
|
@@ -202,16 +207,17 @@ function fe(e) {
|
|
|
202
207
|
coords: u,
|
|
203
208
|
hyperlinks: p,
|
|
204
209
|
maxWidth: h,
|
|
205
|
-
|
|
210
|
+
profile: v,
|
|
211
|
+
direction: y
|
|
206
212
|
};
|
|
207
213
|
}
|
|
208
|
-
function
|
|
209
|
-
let n =
|
|
214
|
+
function ge(e) {
|
|
215
|
+
let n = ce, t = 0;
|
|
210
216
|
for (; t < e.length; ) {
|
|
211
217
|
const r = e[t];
|
|
212
218
|
if (r === void 0) throw new Error(`parseArgs: index ${t} out of range while parsing arguments`);
|
|
213
219
|
if (r === "--port")
|
|
214
|
-
n =
|
|
220
|
+
n = $(e, t, r), t += 2;
|
|
215
221
|
else throw new Error(`Unknown flag: ${r}`);
|
|
216
222
|
}
|
|
217
223
|
return {
|
|
@@ -219,37 +225,37 @@ function he(e) {
|
|
|
219
225
|
port: n
|
|
220
226
|
};
|
|
221
227
|
}
|
|
222
|
-
var
|
|
223
|
-
function
|
|
224
|
-
return
|
|
228
|
+
var ve = /\p{Mn}|\p{Me}/u;
|
|
229
|
+
function we(e) {
|
|
230
|
+
return ve.test(e);
|
|
225
231
|
}
|
|
226
|
-
var
|
|
227
|
-
function
|
|
232
|
+
var be = new Intl.Segmenter(void 0, { granularity: "grapheme" }), ye = "️";
|
|
233
|
+
function xe(e) {
|
|
228
234
|
if (/^[\x20-\x7e]*$/.test(e)) return e.split("");
|
|
229
235
|
const n = [];
|
|
230
|
-
for (const { segment: t } of
|
|
236
|
+
for (const { segment: t } of be.segment(e)) n.push(t);
|
|
231
237
|
return n;
|
|
232
238
|
}
|
|
233
|
-
function
|
|
234
|
-
if (e.includes(
|
|
239
|
+
function Ee(e) {
|
|
240
|
+
if (e.includes(ye)) return 2;
|
|
235
241
|
let n = !1, t = !1;
|
|
236
242
|
for (const r of e)
|
|
237
|
-
|
|
243
|
+
Q(r) && (n = !0), we(r) || (t = !0);
|
|
238
244
|
return n ? 2 : t ? 1 : 0;
|
|
239
245
|
}
|
|
240
|
-
function
|
|
246
|
+
function Se(e) {
|
|
241
247
|
let n = 0;
|
|
242
|
-
for (const t of
|
|
248
|
+
for (const t of xe(e)) n += Ee(t);
|
|
243
249
|
return n;
|
|
244
250
|
}
|
|
245
|
-
var
|
|
246
|
-
function
|
|
247
|
-
return e.replace(
|
|
251
|
+
var ke = /\x1b\]8;[^\x07\x1b]*(?:\x1b\\|\x07)/g;
|
|
252
|
+
function Ce(e) {
|
|
253
|
+
return e.replace(ke, "");
|
|
248
254
|
}
|
|
249
|
-
var
|
|
250
|
-
function
|
|
251
|
-
if (!(e instanceof
|
|
252
|
-
const { line: t, message: r } = e, a =
|
|
255
|
+
var Te = /^Line (\d+): ([\s\S]*)$/;
|
|
256
|
+
function Ie(e, n) {
|
|
257
|
+
if (!(e instanceof K) || e.line === void 0) return;
|
|
258
|
+
const { line: t, message: r } = e, a = Te.exec(r), d = n.split(/\r?\n/)[t - 1];
|
|
253
259
|
if (!(t < 1 || d === void 0))
|
|
254
260
|
return {
|
|
255
261
|
line: t,
|
|
@@ -257,9 +263,9 @@ function Te(e, n) {
|
|
|
257
263
|
message: a?.[2] ?? r
|
|
258
264
|
};
|
|
259
265
|
}
|
|
260
|
-
function
|
|
266
|
+
function X(e, n) {
|
|
261
267
|
if (!(e instanceof Error)) return e;
|
|
262
|
-
const t =
|
|
268
|
+
const t = Ie(e, n);
|
|
263
269
|
return t === void 0 ? e : /* @__PURE__ */ new Error(`${e.message}
|
|
264
270
|
${t.line} | ${t.sourceLine}`);
|
|
265
271
|
}
|
|
@@ -555,7 +561,7 @@ var Ae = `// @ts-check
|
|
|
555
561
|
fit()
|
|
556
562
|
stage.focus({ preventScroll: true })
|
|
557
563
|
})()
|
|
558
|
-
`,
|
|
564
|
+
`, Me = `
|
|
559
565
|
:root {
|
|
560
566
|
color-scheme: light dark;
|
|
561
567
|
--bg: #f4f4f5;
|
|
@@ -662,16 +668,16 @@ var Ae = `// @ts-check
|
|
|
662
668
|
#diagram { position: static; transform: none !important; }
|
|
663
669
|
}
|
|
664
670
|
`.trim();
|
|
665
|
-
function
|
|
671
|
+
function ze(e) {
|
|
666
672
|
const n = e.title?.trim() || "Mermaid diagram";
|
|
667
673
|
return `<!doctype html>
|
|
668
674
|
<html lang="en">
|
|
669
675
|
<head>
|
|
670
676
|
<meta charset="utf-8" />
|
|
671
677
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
672
|
-
<title>${
|
|
678
|
+
<title>${J(n)}</title>
|
|
673
679
|
<style>
|
|
674
|
-
${
|
|
680
|
+
${Me}
|
|
675
681
|
</style>
|
|
676
682
|
</head>
|
|
677
683
|
<body>
|
|
@@ -695,7 +701,7 @@ ${Ae}
|
|
|
695
701
|
</html>
|
|
696
702
|
`;
|
|
697
703
|
}
|
|
698
|
-
async function
|
|
704
|
+
async function $e(e) {
|
|
699
705
|
let n;
|
|
700
706
|
try {
|
|
701
707
|
n = await import("@resvg/resvg-js");
|
|
@@ -706,34 +712,34 @@ ${r}`);
|
|
|
706
712
|
}
|
|
707
713
|
return (await n.renderAsync(e)).asPng();
|
|
708
714
|
}
|
|
709
|
-
var
|
|
710
|
-
function
|
|
715
|
+
var Pe = 80, L = 1, R = 1, B = 0, Le = /\x1b\[[0-9;]*m/g;
|
|
716
|
+
function N(e) {
|
|
711
717
|
return e.split(`
|
|
712
|
-
`).reduce((n, t) => Math.max(n,
|
|
718
|
+
`).reduce((n, t) => Math.max(n, Se(Ce(t.replace(Le, "")))), 0);
|
|
713
719
|
}
|
|
714
|
-
function
|
|
715
|
-
return e !== "auto" ? e : process.stdout.columns ??
|
|
720
|
+
function Re(e) {
|
|
721
|
+
return e !== "auto" ? e : process.stdout.columns ?? Pe;
|
|
716
722
|
}
|
|
717
|
-
async function
|
|
723
|
+
async function Be(e, n, t, r) {
|
|
718
724
|
const a = n ?? process.stdout, d = r ?? process.stderr;
|
|
719
725
|
let l;
|
|
720
|
-
if (e.input !== void 0) l = await
|
|
726
|
+
if (e.input !== void 0) l = await G(e.input, "utf-8");
|
|
721
727
|
else if (t !== void 0) l = t;
|
|
722
728
|
else {
|
|
723
729
|
if (process.stdin.isTTY) throw new Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
|
|
724
|
-
l = await
|
|
730
|
+
l = await U();
|
|
725
731
|
}
|
|
726
732
|
if (l.trim().length === 0) throw new Error("Empty input — provide a Mermaid diagram via file or stdin");
|
|
727
733
|
try {
|
|
728
|
-
await
|
|
734
|
+
await Oe(e, l, a, d);
|
|
729
735
|
} catch (m) {
|
|
730
|
-
throw
|
|
736
|
+
throw X(m, l);
|
|
731
737
|
}
|
|
732
738
|
}
|
|
733
|
-
async function
|
|
739
|
+
async function Oe(e, n, t, r) {
|
|
734
740
|
let a;
|
|
735
|
-
if (e.theme !== void 0 && (a =
|
|
736
|
-
const s = Object.keys(
|
|
741
|
+
if (e.theme !== void 0 && (a = O[e.theme], a === void 0)) {
|
|
742
|
+
const s = Object.keys(O).join(", ");
|
|
737
743
|
throw new Error(`Unknown theme: "${e.theme}". Available themes: ${s}`);
|
|
738
744
|
}
|
|
739
745
|
const d = e.svg && e.output === "-", l = e.html && e.output === "-", m = e.png && e.output === "-", g = e.ascii && !e.svg && !e.html && !e.png && e.output !== void 0 && e.output !== "-" ? e.output : void 0, E = e.svg && !d ? e.output : void 0, S = e.html && !l ? e.output : void 0, k = e.png && !m ? e.output : void 0;
|
|
@@ -742,37 +748,37 @@ async function Be(e, n, t, r) {
|
|
|
742
748
|
E,
|
|
743
749
|
S,
|
|
744
750
|
k
|
|
745
|
-
]) s !== void 0 && await
|
|
751
|
+
]) s !== void 0 && await _e(s, e.force);
|
|
746
752
|
if (e.ascii) {
|
|
747
753
|
const s = a && g === void 0 ? {
|
|
748
754
|
colorMode: "auto",
|
|
749
|
-
theme:
|
|
755
|
+
theme: ne(a)
|
|
750
756
|
} : { colorMode: "none" };
|
|
751
|
-
e.paddingX !== void 0 && (s.paddingX = e.paddingX), e.paddingY !== void 0 && (s.paddingY = e.paddingY), e.borderPadding !== void 0 && (s.boxBorderPadding = e.borderPadding), e.coords && (s.showCoords = !0), e.direction !== void 0 && (s.direction = e.direction), e.hyperlinks && g === void 0 && (s.hyperlinks = !0);
|
|
752
|
-
let u =
|
|
757
|
+
e.paddingX !== void 0 && (s.paddingX = e.paddingX), e.paddingY !== void 0 && (s.paddingY = e.paddingY), e.borderPadding !== void 0 && (s.boxBorderPadding = e.borderPadding), e.coords && (s.showCoords = !0), e.profile !== void 0 && (s.profile = e.profile), e.direction !== void 0 && (s.direction = e.direction), e.hyperlinks && g === void 0 && (s.hyperlinks = !0);
|
|
758
|
+
let u = _(n, s);
|
|
753
759
|
if (e.maxWidth !== void 0) {
|
|
754
|
-
const p =
|
|
755
|
-
let h =
|
|
760
|
+
const p = Re(e.maxWidth);
|
|
761
|
+
let h = N(u), v = !1;
|
|
756
762
|
if (h > p) {
|
|
757
|
-
const
|
|
758
|
-
if (
|
|
759
|
-
const
|
|
763
|
+
const y = s.paddingX ?? 5, i = s.paddingY ?? 5, w = s.boxBorderPadding ?? 1, C = Math.min(y, L), f = Math.min(i, R), T = Math.min(w, B);
|
|
764
|
+
if (C < y || f < i || T < w) {
|
|
765
|
+
const x = _(n, {
|
|
760
766
|
...s,
|
|
761
|
-
paddingX:
|
|
762
|
-
paddingY:
|
|
763
|
-
boxBorderPadding:
|
|
764
|
-
}),
|
|
765
|
-
|
|
767
|
+
paddingX: C,
|
|
768
|
+
paddingY: f,
|
|
769
|
+
boxBorderPadding: T
|
|
770
|
+
}), o = N(x);
|
|
771
|
+
o < h && (u = x, h = o, v = !0);
|
|
766
772
|
}
|
|
767
773
|
}
|
|
768
774
|
if (h > p) {
|
|
769
|
-
const
|
|
770
|
-
r.write(`Warning: ASCII output is ${h} columns wide, exceeding ${
|
|
775
|
+
const y = e.maxWidth === "auto" ? "detected terminal width" : "--max-width", i = v ? `Compact spacing (-x ${L} -y ${R} -p ${B}) was already applied automatically. ` : "", w = v ? "Try a narrower direction (LR vs TD), shorter labels, or widen your terminal." : "Try -x/-y/-p for tighter spacing, a narrower direction (LR vs TD), or widen your terminal.";
|
|
776
|
+
r.write(`Warning: ASCII output is ${h} columns wide, exceeding ${y} of ${p}. ${i}zombie-mermaid does not reflow diagram structure to fit (no label wrapping or direction-flip cascade — see https://github.com/dfadler/zombie-mermaid/issues/335). ${w}
|
|
771
777
|
`);
|
|
772
|
-
} else
|
|
778
|
+
} else v && r.write(`Note: ASCII output exceeded ${p} columns at the requested spacing; applied compact spacing automatically (-x ${L} -y ${R} -p ${B}) to fit.
|
|
773
779
|
`);
|
|
774
780
|
}
|
|
775
|
-
await
|
|
781
|
+
await M(A(g === void 0, g), u + `
|
|
776
782
|
`, t, e.force);
|
|
777
783
|
}
|
|
778
784
|
if (e.svg) {
|
|
@@ -781,15 +787,15 @@ async function Be(e, n, t, r) {
|
|
|
781
787
|
resolveColors: e.resolveColors
|
|
782
788
|
};
|
|
783
789
|
e.direction !== void 0 && (s.direction = e.direction);
|
|
784
|
-
const u =
|
|
785
|
-
await
|
|
790
|
+
const u = z(n, s);
|
|
791
|
+
await M(A(d, E), u, t, e.force);
|
|
786
792
|
}
|
|
787
793
|
if (e.html) {
|
|
788
|
-
const s =
|
|
794
|
+
const s = z(n, a ?? {}), u = e.input ? F(e.input).name : void 0, p = ze({
|
|
789
795
|
svg: s,
|
|
790
796
|
title: u
|
|
791
797
|
});
|
|
792
|
-
await
|
|
798
|
+
await M(A(l, S), p, t, e.force);
|
|
793
799
|
}
|
|
794
800
|
if (e.png) {
|
|
795
801
|
const s = {
|
|
@@ -797,8 +803,8 @@ async function Be(e, n, t, r) {
|
|
|
797
803
|
resolveColors: !0
|
|
798
804
|
};
|
|
799
805
|
e.direction !== void 0 && (s.direction = e.direction);
|
|
800
|
-
const u =
|
|
801
|
-
await
|
|
806
|
+
const u = z(n, s), p = await $e(u);
|
|
807
|
+
await M(A(m, k), p, t, e.force);
|
|
802
808
|
}
|
|
803
809
|
}
|
|
804
810
|
function A(e, n) {
|
|
@@ -807,68 +813,68 @@ function A(e, n) {
|
|
|
807
813
|
path: n
|
|
808
814
|
} : { kind: "skip" };
|
|
809
815
|
}
|
|
810
|
-
async function
|
|
816
|
+
async function M(e, n, t, r) {
|
|
811
817
|
switch (e.kind) {
|
|
812
818
|
case "stdout":
|
|
813
819
|
t.write(n);
|
|
814
820
|
break;
|
|
815
821
|
case "file":
|
|
816
|
-
await
|
|
822
|
+
await Ne(e.path, n, r);
|
|
817
823
|
}
|
|
818
824
|
}
|
|
819
|
-
function
|
|
825
|
+
function Y(e) {
|
|
820
826
|
return /* @__PURE__ */ new Error(`Refusing to overwrite existing file "${e}" — pass --force to replace it`);
|
|
821
827
|
}
|
|
822
|
-
async function
|
|
828
|
+
async function _e(e, n) {
|
|
823
829
|
if (n) return;
|
|
824
830
|
let t = !0;
|
|
825
831
|
try {
|
|
826
|
-
await
|
|
832
|
+
await ee(e);
|
|
827
833
|
} catch {
|
|
828
834
|
t = !1;
|
|
829
835
|
}
|
|
830
|
-
if (t) throw
|
|
836
|
+
if (t) throw Y(e);
|
|
831
837
|
}
|
|
832
|
-
async function
|
|
838
|
+
async function Ne(e, n, t) {
|
|
833
839
|
try {
|
|
834
|
-
await
|
|
840
|
+
await te(e, n, {
|
|
835
841
|
encoding: "utf-8",
|
|
836
842
|
flag: t ? "w" : "wx"
|
|
837
843
|
});
|
|
838
844
|
} catch (r) {
|
|
839
|
-
throw !t && r instanceof Error && "code" in r && r.code === "EEXIST" ?
|
|
845
|
+
throw !t && r instanceof Error && "code" in r && r.code === "EEXIST" ? Y(e) : r;
|
|
840
846
|
}
|
|
841
847
|
}
|
|
842
|
-
async function
|
|
848
|
+
async function U() {
|
|
843
849
|
const e = [], n = process.stdin;
|
|
844
850
|
return n.setEncoding("utf-8"), new Promise((t, r) => {
|
|
845
851
|
n.on("data", (a) => e.push(a)), n.on("end", () => t(e.join(""))), n.on("error", r);
|
|
846
852
|
});
|
|
847
853
|
}
|
|
848
|
-
async function
|
|
854
|
+
async function De(e, n) {
|
|
849
855
|
let t;
|
|
850
|
-
if (e.input !== void 0) t = await
|
|
856
|
+
if (e.input !== void 0) t = await G(e.input, "utf-8");
|
|
851
857
|
else if (n !== void 0) t = n;
|
|
852
858
|
else {
|
|
853
859
|
if (process.stdin.isTTY) throw new Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
|
|
854
|
-
t = await
|
|
860
|
+
t = await U();
|
|
855
861
|
}
|
|
856
862
|
const r = t;
|
|
857
863
|
if (r.trim().length === 0) throw new Error("Empty input — provide a Mermaid diagram via file or stdin");
|
|
858
864
|
try {
|
|
859
|
-
|
|
865
|
+
oe(r);
|
|
860
866
|
} catch (a) {
|
|
861
|
-
throw
|
|
867
|
+
throw X(a, r);
|
|
862
868
|
}
|
|
863
869
|
console.log("valid");
|
|
864
870
|
}
|
|
865
|
-
var
|
|
866
|
-
function
|
|
871
|
+
var Fe = 1e6;
|
|
872
|
+
function Ge(e) {
|
|
867
873
|
return new Promise((n, t) => {
|
|
868
874
|
const r = [];
|
|
869
875
|
let a = 0;
|
|
870
876
|
e.on("data", (d) => {
|
|
871
|
-
if (a += d.length, a >
|
|
877
|
+
if (a += d.length, a > Fe) {
|
|
872
878
|
t(/* @__PURE__ */ new Error("Request body too large")), e.destroy();
|
|
873
879
|
return;
|
|
874
880
|
}
|
|
@@ -876,17 +882,17 @@ function Fe(e) {
|
|
|
876
882
|
}), e.on("end", () => n(Buffer.concat(r).toString("utf-8"))), e.on("error", t);
|
|
877
883
|
});
|
|
878
884
|
}
|
|
879
|
-
function
|
|
885
|
+
function V(e) {
|
|
880
886
|
return typeof e == "object" && e !== null;
|
|
881
887
|
}
|
|
882
|
-
function
|
|
888
|
+
function We(e) {
|
|
883
889
|
let n;
|
|
884
890
|
try {
|
|
885
891
|
n = JSON.parse(e);
|
|
886
892
|
} catch {
|
|
887
893
|
throw new Error("Invalid JSON in request body");
|
|
888
894
|
}
|
|
889
|
-
if (!
|
|
895
|
+
if (!V(n)) throw new Error("Request body must be a JSON object");
|
|
890
896
|
const t = n.source;
|
|
891
897
|
if (typeof t != "string" || t.trim().length === 0) throw new Error('Provide Mermaid source as a non-empty "source" string');
|
|
892
898
|
return {
|
|
@@ -894,25 +900,25 @@ function Ge(e) {
|
|
|
894
900
|
format: n.format === "svg" ? "svg" : "ascii"
|
|
895
901
|
};
|
|
896
902
|
}
|
|
897
|
-
function
|
|
903
|
+
function D(e, n, t) {
|
|
898
904
|
const r = JSON.stringify(t);
|
|
899
905
|
e.writeHead(n, {
|
|
900
906
|
"Content-Type": "application/json; charset=utf-8",
|
|
901
907
|
"Content-Length": Buffer.byteLength(r)
|
|
902
908
|
}), e.end(r);
|
|
903
909
|
}
|
|
904
|
-
async function
|
|
910
|
+
async function Xe(e, n) {
|
|
905
911
|
try {
|
|
906
|
-
const { source: t, format: r } =
|
|
907
|
-
|
|
908
|
-
output: r === "svg" ?
|
|
912
|
+
const { source: t, format: r } = We(await Ge(e));
|
|
913
|
+
D(n, 200, {
|
|
914
|
+
output: r === "svg" ? z(t) : _(t),
|
|
909
915
|
format: r
|
|
910
916
|
});
|
|
911
917
|
} catch (t) {
|
|
912
|
-
|
|
918
|
+
D(n, 400, { error: t instanceof Error ? t.message : String(t) });
|
|
913
919
|
}
|
|
914
920
|
}
|
|
915
|
-
var
|
|
921
|
+
var Ye = `<!doctype html>
|
|
916
922
|
<html lang="en">
|
|
917
923
|
<head>
|
|
918
924
|
<meta charset="utf-8" />
|
|
@@ -978,79 +984,79 @@ var Xe = `<!doctype html>
|
|
|
978
984
|
</body>
|
|
979
985
|
</html>
|
|
980
986
|
`;
|
|
981
|
-
function
|
|
982
|
-
return
|
|
987
|
+
function Ue() {
|
|
988
|
+
return ie((e, n) => {
|
|
983
989
|
const t = (e.url ?? "/").split("?")[0];
|
|
984
990
|
if (e.method === "GET" && (t === "/" || t === "")) {
|
|
985
|
-
n.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), n.end(
|
|
991
|
+
n.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), n.end(Ye);
|
|
986
992
|
return;
|
|
987
993
|
}
|
|
988
994
|
if (e.method === "POST" && t === "/render") {
|
|
989
|
-
|
|
995
|
+
Xe(e, n);
|
|
990
996
|
return;
|
|
991
997
|
}
|
|
992
998
|
n.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }), n.end("Not found");
|
|
993
999
|
});
|
|
994
1000
|
}
|
|
995
|
-
function
|
|
1001
|
+
function Ve(e) {
|
|
996
1002
|
return new Promise((n, t) => {
|
|
997
|
-
const r =
|
|
1003
|
+
const r = Ue();
|
|
998
1004
|
r.once("error", t), r.listen(e, "127.0.0.1", () => {
|
|
999
1005
|
r.removeListener("error", t), n(r);
|
|
1000
1006
|
});
|
|
1001
1007
|
});
|
|
1002
1008
|
}
|
|
1003
|
-
async function
|
|
1004
|
-
const t = await
|
|
1005
|
-
return n(`zombie-mermaid web running at http://localhost:${
|
|
1009
|
+
async function qe(e, n = console.log) {
|
|
1010
|
+
const t = await Ve(e.port), r = t.address();
|
|
1011
|
+
return n(`zombie-mermaid web running at http://localhost:${V(r) && typeof r.port == "number" ? r.port : e.port}`), t;
|
|
1006
1012
|
}
|
|
1007
1013
|
async function He(e = (n) => console.error(n)) {
|
|
1008
|
-
const n =
|
|
1014
|
+
const n = se(), t = new ae();
|
|
1009
1015
|
return await n.connect(t), e("zombie-mermaid MCP server running on stdio (experimental — feedback welcome: https://github.com/dfadler/zombie-mermaid/issues)"), n;
|
|
1010
1016
|
}
|
|
1011
|
-
function
|
|
1012
|
-
const e =
|
|
1017
|
+
function je() {
|
|
1018
|
+
const e = q(import.meta.url)("../package.json");
|
|
1013
1019
|
return typeof e == "object" && e !== null && "version" in e && typeof e.version == "string" ? e.version : "unknown";
|
|
1014
1020
|
}
|
|
1015
|
-
|
|
1016
|
-
var
|
|
1017
|
-
async function
|
|
1021
|
+
re(de);
|
|
1022
|
+
var Ke = je();
|
|
1023
|
+
async function Je() {
|
|
1018
1024
|
const e = process.argv.slice(2);
|
|
1019
1025
|
let n;
|
|
1020
1026
|
try {
|
|
1021
|
-
n =
|
|
1027
|
+
n = ue(e);
|
|
1022
1028
|
} catch (t) {
|
|
1023
1029
|
console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
|
|
1024
1030
|
}
|
|
1025
1031
|
switch (n.command) {
|
|
1026
1032
|
case "help":
|
|
1027
|
-
|
|
1033
|
+
Ze();
|
|
1028
1034
|
break;
|
|
1029
1035
|
case "version":
|
|
1030
|
-
console.log(`zombie-mermaid ${
|
|
1036
|
+
console.log(`zombie-mermaid ${Ke}`);
|
|
1031
1037
|
break;
|
|
1032
1038
|
case "themes":
|
|
1033
1039
|
console.log(`Available themes:
|
|
1034
1040
|
`);
|
|
1035
|
-
for (const t of Object.keys(
|
|
1041
|
+
for (const t of Object.keys(O)) console.log(` ${t}`);
|
|
1036
1042
|
break;
|
|
1037
1043
|
case "render":
|
|
1038
1044
|
try {
|
|
1039
|
-
await
|
|
1045
|
+
await Be(n);
|
|
1040
1046
|
} catch (t) {
|
|
1041
1047
|
console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
|
|
1042
1048
|
}
|
|
1043
1049
|
break;
|
|
1044
1050
|
case "validate":
|
|
1045
1051
|
try {
|
|
1046
|
-
await
|
|
1052
|
+
await De(n);
|
|
1047
1053
|
} catch (t) {
|
|
1048
1054
|
console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
|
|
1049
1055
|
}
|
|
1050
1056
|
break;
|
|
1051
1057
|
case "web":
|
|
1052
1058
|
try {
|
|
1053
|
-
await
|
|
1059
|
+
await qe(n);
|
|
1054
1060
|
} catch (t) {
|
|
1055
1061
|
console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
|
|
1056
1062
|
}
|
|
@@ -1063,7 +1069,7 @@ async function Ke() {
|
|
|
1063
1069
|
}
|
|
1064
1070
|
}
|
|
1065
1071
|
}
|
|
1066
|
-
function
|
|
1072
|
+
function Ze() {
|
|
1067
1073
|
console.log(`
|
|
1068
1074
|
zombie-mermaid — render Mermaid diagrams from the command line
|
|
1069
1075
|
|
|
@@ -1128,6 +1134,10 @@ Options:
|
|
|
1128
1134
|
VTE-based terminals render them as clickable links;
|
|
1129
1135
|
others may show the URL or nothing. Pagers need
|
|
1130
1136
|
OSC 8 support too (\`less -R\`, recent versions).
|
|
1137
|
+
--profile <name> ASCII preset: 'markdown' = plain ASCII characters, no
|
|
1138
|
+
color, for pasting into issues/commits/fenced blocks
|
|
1139
|
+
(explicit flags like --coords still win). Requires
|
|
1140
|
+
--ascii.
|
|
1131
1141
|
-w, --max-width <n|auto> Fit ASCII output within this many columns
|
|
1132
1142
|
('auto' detects the terminal width): applies compact
|
|
1133
1143
|
spacing automatically when needed, and warns on
|
|
@@ -1138,7 +1148,7 @@ Options:
|
|
|
1138
1148
|
-v, --version Show version
|
|
1139
1149
|
`.trim());
|
|
1140
1150
|
}
|
|
1141
|
-
|
|
1151
|
+
Je();
|
|
1142
1152
|
export {
|
|
1143
|
-
|
|
1153
|
+
Je as main
|
|
1144
1154
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zombie-mermaid",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Render Mermaid diagrams as beautiful SVGs or ASCII art. Ultra-fast, fully themeable, zero DOM dependencies. Maintained fork of beautiful-mermaid.",
|
|
6
6
|
"type": "module",
|
|
@@ -88,22 +88,24 @@
|
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
90
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
91
|
-
"elkjs": "^0.
|
|
91
|
+
"elkjs": "^0.12.0",
|
|
92
92
|
"entities": "^7.0.1",
|
|
93
|
-
"@zombie-mermaid/ascii-renderer": "5.
|
|
94
|
-
"@zombie-mermaid/
|
|
95
|
-
"@zombie-mermaid/mcp": "5.
|
|
96
|
-
"@zombie-mermaid/
|
|
97
|
-
"@zombie-mermaid/svg-renderer": "5.
|
|
93
|
+
"@zombie-mermaid/ascii-renderer": "5.4.0",
|
|
94
|
+
"@zombie-mermaid/core": "5.4.0",
|
|
95
|
+
"@zombie-mermaid/mcp": "5.4.0",
|
|
96
|
+
"@zombie-mermaid/mermaid-parser": "5.4.0",
|
|
97
|
+
"@zombie-mermaid/svg-renderer": "5.4.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@arethetypeswrong/cli": "0.18.5",
|
|
101
101
|
"@axe-core/playwright": "^4.13.0",
|
|
102
|
-
"@fast-check/vitest": "0.5.0",
|
|
103
102
|
"@changesets/changelog-github": "^1.0.0",
|
|
104
103
|
"@changesets/cli": "^3.0.1",
|
|
104
|
+
"@fast-check/vitest": "0.5.0",
|
|
105
105
|
"@microsoft/api-extractor": "^7.59.0",
|
|
106
106
|
"@playwright/test": "1.62.1",
|
|
107
|
+
"@stryker-mutator/core": "10.0.0",
|
|
108
|
+
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
107
109
|
"@testing-library/jest-dom": "^7.0.1",
|
|
108
110
|
"@testing-library/react": "^16.3.3",
|
|
109
111
|
"@testing-library/user-event": "^14.6.7",
|
|
@@ -171,12 +173,15 @@
|
|
|
171
173
|
"check:changesets": "tsx scripts/check-changesets.ts",
|
|
172
174
|
"check:docs": "tsx scripts/check-docs.ts",
|
|
173
175
|
"smoke:packed": "node scripts/smoke-test-packed.mjs",
|
|
176
|
+
"check:pack-dist": "node scripts/check-pack-dist.mjs",
|
|
174
177
|
"check:package-types": "scripts/check-package-types.sh",
|
|
175
178
|
"check:bundle-size": "tsx scripts/check-bundle-size.ts",
|
|
176
179
|
"generate:hero": "tsx scripts/generate-hero.ts",
|
|
177
180
|
"build:mono-font": "tsx scripts/build-mono-font-subset.ts && prettier --write demo/components/generated/mono-font-subset.ts demo/styles.css packages/svg-renderer/src/generated/mono-font-subset.ts",
|
|
178
181
|
"generate:primitives-classes": "tsx scripts/generate-primitives-classes.ts && prettier --write demo/components/generated/primitives-classes.ts",
|
|
179
182
|
"generate:logo-lockup": "tsx scripts/generate-logo-lockup.ts",
|
|
183
|
+
"api:report": "tsx scripts/api-report.ts",
|
|
184
|
+
"mutation": "stryker run",
|
|
180
185
|
"docs:scripts": "tsx scripts/generate-development-scripts-doc.ts && prettier --write docs/development-scripts.md",
|
|
181
186
|
"badge:bundle-size": "tsx scripts/generate-bundle-badge.ts",
|
|
182
187
|
"visual-diff": "tsx scripts/visual-diff.ts",
|