@danielsimonjr/mathts-workbook 0.1.7 → 0.2.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 +222 -117
- package/dist/chunk-TS2WDJ7W.js +1239 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +1650 -79
- package/dist/index.d.ts +255 -9
- package/dist/index.js +29 -1
- package/package.json +64 -62
- package/dist/chunk-L7UWFWMV.js +0 -269
package/dist/cli.js
CHANGED
|
@@ -1,107 +1,1678 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
SCHEMA_VERSION,
|
|
4
|
+
Session,
|
|
5
|
+
VERSION,
|
|
6
|
+
addCell,
|
|
7
|
+
buildDependencyGraph,
|
|
8
|
+
capabilitiesInfo,
|
|
3
9
|
createExecutor,
|
|
4
|
-
|
|
5
|
-
|
|
10
|
+
describeData,
|
|
11
|
+
detectCycles,
|
|
12
|
+
editCell,
|
|
13
|
+
formatResult,
|
|
14
|
+
handleRequest,
|
|
15
|
+
importWorkbook,
|
|
16
|
+
listFunctions,
|
|
17
|
+
moveCell,
|
|
18
|
+
parseWorkbook,
|
|
19
|
+
parseYamlHardened,
|
|
20
|
+
removeCell,
|
|
21
|
+
renameCell,
|
|
22
|
+
serializeWorkbook,
|
|
23
|
+
setMetadata,
|
|
24
|
+
stripOutputs,
|
|
25
|
+
toDOT,
|
|
26
|
+
toMermaid,
|
|
27
|
+
writeFileAtomic
|
|
28
|
+
} from "./chunk-TS2WDJ7W.js";
|
|
6
29
|
|
|
7
30
|
// src/cli.ts
|
|
31
|
+
import { readFileSync, writeFileSync, lstatSync, realpathSync } from "fs";
|
|
32
|
+
import { pathToFileURL } from "url";
|
|
33
|
+
import { basename } from "path";
|
|
34
|
+
import { createInterface } from "readline";
|
|
35
|
+
import * as mathFunctions from "@danielsimonjr/mathts-functions";
|
|
36
|
+
|
|
37
|
+
// src/html.ts
|
|
38
|
+
import { mathMLDocument, mathMLError } from "@danielsimonjr/mathts-expression";
|
|
39
|
+
|
|
40
|
+
// src/markdown.ts
|
|
41
|
+
function esc(s) {
|
|
42
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
43
|
+
}
|
|
44
|
+
function sanitizeHrefRaw(href) {
|
|
45
|
+
const h = href.trim();
|
|
46
|
+
let probe = h.replace(/&/g, "&");
|
|
47
|
+
try {
|
|
48
|
+
probe = decodeURIComponent(probe);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
probe = probe.trim().toLowerCase();
|
|
52
|
+
if (probe.startsWith("//")) return null;
|
|
53
|
+
if (probe.startsWith("http://") || probe.startsWith("https://") || probe.startsWith("mailto:"))
|
|
54
|
+
return h;
|
|
55
|
+
if (probe.startsWith("/") || probe.startsWith("./") || probe.startsWith("../") || probe.startsWith("#"))
|
|
56
|
+
return h;
|
|
57
|
+
if (/^[a-z][a-z0-9+.-]*:/.test(probe)) return null;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function sanitizeHref(href) {
|
|
61
|
+
const raw = sanitizeHrefRaw(href);
|
|
62
|
+
return raw === null ? null : raw.replace(/"/g, """);
|
|
63
|
+
}
|
|
64
|
+
function inline(escaped) {
|
|
65
|
+
let s = escaped.replace(/`([^`]+)`/g, (_m, code) => `<code>${code}</code>`);
|
|
66
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
67
|
+
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
68
|
+
s = s.replace(/\[([^\]]*)\]\(([^)\s]+)\)/g, (_m, text, href) => {
|
|
69
|
+
const safe = sanitizeHref(href);
|
|
70
|
+
return safe ? `<a href="${safe}">${text}</a>` : text;
|
|
71
|
+
});
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
var HR = /^ {0,3}(-{3,}|\*{3,}|_{3,})\s*$/;
|
|
75
|
+
var HEADING = /^(#{1,6})\s+(.*)$/;
|
|
76
|
+
var UL = /^\s*[-*+]\s+/;
|
|
77
|
+
var OL = /^\s*\d+\.\s+/;
|
|
78
|
+
function isBlockStart(line2) {
|
|
79
|
+
return line2.trim() === "" || /^```/.test(line2.trim()) || HEADING.test(line2) || UL.test(line2) || OL.test(line2) || HR.test(line2);
|
|
80
|
+
}
|
|
81
|
+
function markdownToHtml(src) {
|
|
82
|
+
if (!src) return "";
|
|
83
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
84
|
+
const out = [];
|
|
85
|
+
let i = 0;
|
|
86
|
+
while (i < lines.length) {
|
|
87
|
+
const line2 = lines[i];
|
|
88
|
+
if (/^```/.test(line2.trim())) {
|
|
89
|
+
const buf = [];
|
|
90
|
+
i++;
|
|
91
|
+
while (i < lines.length && !/^```/.test(lines[i].trim())) {
|
|
92
|
+
buf.push(lines[i]);
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
i++;
|
|
96
|
+
out.push(`<pre><code>${esc(buf.join("\n"))}</code></pre>`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (line2.trim() === "") {
|
|
100
|
+
i++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (HR.test(line2)) {
|
|
104
|
+
out.push("<hr>");
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const h = HEADING.exec(line2);
|
|
109
|
+
if (h) {
|
|
110
|
+
const lvl = h[1].length;
|
|
111
|
+
out.push(`<h${lvl}>${inline(esc(h[2].trim()))}</h${lvl}>`);
|
|
112
|
+
i++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (UL.test(line2)) {
|
|
116
|
+
const items = [];
|
|
117
|
+
while (i < lines.length && UL.test(lines[i])) {
|
|
118
|
+
items.push(`<li>${inline(esc(lines[i].replace(UL, "")))}</li>`);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
out.push(`<ul>${items.join("")}</ul>`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (OL.test(line2)) {
|
|
125
|
+
const items = [];
|
|
126
|
+
while (i < lines.length && OL.test(lines[i])) {
|
|
127
|
+
items.push(`<li>${inline(esc(lines[i].replace(OL, "")))}</li>`);
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
out.push(`<ol>${items.join("")}</ol>`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const para = [];
|
|
134
|
+
while (i < lines.length && !isBlockStart(lines[i])) {
|
|
135
|
+
para.push(lines[i]);
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
out.push(`<p>${inline(esc(para.join(" ")))}</p>`);
|
|
139
|
+
}
|
|
140
|
+
return out.join("\n");
|
|
141
|
+
}
|
|
142
|
+
function texEscape(s) {
|
|
143
|
+
return s.replace(/[\\&%$#_{}~^]/g, (c) => {
|
|
144
|
+
switch (c) {
|
|
145
|
+
case "\\":
|
|
146
|
+
return "\\textbackslash{}";
|
|
147
|
+
case "~":
|
|
148
|
+
return "\\textasciitilde{}";
|
|
149
|
+
case "^":
|
|
150
|
+
return "\\textasciicircum{}";
|
|
151
|
+
default:
|
|
152
|
+
return `\\${c}`;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function escapeInlineNonLink(text) {
|
|
157
|
+
let s = texEscape(text);
|
|
158
|
+
s = s.replace(/`([^`]+)`/g, (_m, code) => `\\texttt{${code}}`);
|
|
159
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "\\textbf{$1}");
|
|
160
|
+
s = s.replace(/\*([^*]+)\*/g, "\\emph{$1}");
|
|
161
|
+
return s;
|
|
162
|
+
}
|
|
163
|
+
function inlineTex(text) {
|
|
164
|
+
const linkRe = /\[([^\]]*)\]\(([^)\s]+)\)/g;
|
|
165
|
+
let out = "";
|
|
166
|
+
let last = 0;
|
|
167
|
+
let m;
|
|
168
|
+
while ((m = linkRe.exec(text)) !== null) {
|
|
169
|
+
out += escapeInlineNonLink(text.slice(last, m.index));
|
|
170
|
+
const raw = sanitizeHrefRaw(m[2]);
|
|
171
|
+
out += raw ? `\\href{${texEscape(raw)}}{${escapeInlineNonLink(m[1])}}` : escapeInlineNonLink(m[1]);
|
|
172
|
+
last = linkRe.lastIndex;
|
|
173
|
+
}
|
|
174
|
+
out += escapeInlineNonLink(text.slice(last));
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function markdownToTex(src) {
|
|
178
|
+
if (!src) return "";
|
|
179
|
+
const lines = src.replace(/\r\n?/g, "\n").split("\n");
|
|
180
|
+
const out = [];
|
|
181
|
+
let i = 0;
|
|
182
|
+
const SEC = ["\\section*", "\\subsection*", "\\subsubsection*", "\\paragraph"];
|
|
183
|
+
while (i < lines.length) {
|
|
184
|
+
const line2 = lines[i];
|
|
185
|
+
if (/^```/.test(line2.trim())) {
|
|
186
|
+
const buf = [];
|
|
187
|
+
i++;
|
|
188
|
+
while (i < lines.length && !/^```/.test(lines[i].trim())) {
|
|
189
|
+
buf.push(lines[i]);
|
|
190
|
+
i++;
|
|
191
|
+
}
|
|
192
|
+
i++;
|
|
193
|
+
out.push(`\\begin{lstlisting}
|
|
194
|
+
${buf.join("\n")}
|
|
195
|
+
\\end{lstlisting}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (line2.trim() === "") {
|
|
199
|
+
i++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (HR.test(line2)) {
|
|
203
|
+
out.push("\\par\\noindent\\rule{\\linewidth}{0.4pt}\\par");
|
|
204
|
+
i++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const h = HEADING.exec(line2);
|
|
208
|
+
if (h) {
|
|
209
|
+
const lvl = Math.min(h[1].length, 4);
|
|
210
|
+
out.push(`${SEC[lvl - 1]}{${inlineTex(h[2].trim())}}`);
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (UL.test(line2)) {
|
|
215
|
+
const items = [];
|
|
216
|
+
while (i < lines.length && UL.test(lines[i])) {
|
|
217
|
+
items.push(` \\item ${inlineTex(lines[i].replace(UL, ""))}`);
|
|
218
|
+
i++;
|
|
219
|
+
}
|
|
220
|
+
out.push(`\\begin{itemize}
|
|
221
|
+
${items.join("\n")}
|
|
222
|
+
\\end{itemize}`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (OL.test(line2)) {
|
|
226
|
+
const items = [];
|
|
227
|
+
while (i < lines.length && OL.test(lines[i])) {
|
|
228
|
+
items.push(` \\item ${inlineTex(lines[i].replace(OL, ""))}`);
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
out.push(`\\begin{enumerate}
|
|
232
|
+
${items.join("\n")}
|
|
233
|
+
\\end{enumerate}`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const para = [];
|
|
237
|
+
while (i < lines.length && !isBlockStart(lines[i])) {
|
|
238
|
+
para.push(lines[i]);
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
out.push(inlineTex(para.join(" ")));
|
|
242
|
+
}
|
|
243
|
+
return out.join("\n\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/html.ts
|
|
247
|
+
function esc2(s) {
|
|
248
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
249
|
+
}
|
|
250
|
+
function renderEquation(content, parse3) {
|
|
251
|
+
let math;
|
|
252
|
+
if (!parse3) {
|
|
253
|
+
math = mathMLError(content);
|
|
254
|
+
} else {
|
|
255
|
+
try {
|
|
256
|
+
math = mathMLDocument(parse3(content));
|
|
257
|
+
} catch {
|
|
258
|
+
math = mathMLError(content);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return `<figure class="cell cell-eq">${math}</figure>`;
|
|
262
|
+
}
|
|
263
|
+
function renderCell(cell, parse3) {
|
|
264
|
+
const cap = cell.id ? `<figcaption>${esc2(cell.id)}</figcaption>` : "";
|
|
265
|
+
switch (cell.type) {
|
|
266
|
+
case "markdown":
|
|
267
|
+
return `<section class="cell cell-md">${markdownToHtml(cell.content)}</section>`;
|
|
268
|
+
case "equation":
|
|
269
|
+
return renderEquation(cell.content, parse3);
|
|
270
|
+
case "code": {
|
|
271
|
+
const src = `<pre class="src"><code>${esc2(cell.content)}</code></pre>`;
|
|
272
|
+
let res = "";
|
|
273
|
+
if (cell.error !== void 0) res = `<div class="cell-error">${esc2(cell.error)}</div>`;
|
|
274
|
+
else if (cell.output !== void 0)
|
|
275
|
+
res = `<div class="cell-output">${esc2(cell.output)}</div>`;
|
|
276
|
+
return `<figure class="cell cell-code">${cap}${src}${res}</figure>`;
|
|
277
|
+
}
|
|
278
|
+
case "test": {
|
|
279
|
+
const err = cell.error;
|
|
280
|
+
let badge;
|
|
281
|
+
let cls;
|
|
282
|
+
if (err !== void 0) {
|
|
283
|
+
badge = "\u26A0";
|
|
284
|
+
cls = "err";
|
|
285
|
+
} else if (cell.passed === true) {
|
|
286
|
+
badge = "\u2713";
|
|
287
|
+
cls = "pass";
|
|
288
|
+
} else if (cell.passed === false) {
|
|
289
|
+
badge = "\u2717";
|
|
290
|
+
cls = "fail";
|
|
291
|
+
} else {
|
|
292
|
+
badge = "\u25CB";
|
|
293
|
+
cls = "nrun";
|
|
294
|
+
}
|
|
295
|
+
const note = err !== void 0 ? ` <span class="note">${esc2(err)}</span>` : "";
|
|
296
|
+
return `<figure class="cell cell-test ${cls}"><span class="badge">${badge}</span> <code>${esc2(cell.content)}</code>${note}</figure>`;
|
|
297
|
+
}
|
|
298
|
+
case "data": {
|
|
299
|
+
const val = cell.error !== void 0 ? esc2(cell.error) : esc2(cell.output ?? "");
|
|
300
|
+
return `<figure class="cell cell-data">${cap}<pre><code>${val}</code></pre></figure>`;
|
|
301
|
+
}
|
|
302
|
+
case "chart": {
|
|
303
|
+
const note = cell.note ? `<p class="note">\u26A0 ${esc2(cell.note)}</p>` : "";
|
|
304
|
+
return `<figure class="cell cell-chart">${cap}${cell.chartSvg ?? '<p class="note">no chart</p>'}${note}</figure>`;
|
|
305
|
+
}
|
|
306
|
+
default:
|
|
307
|
+
return `<figure class="cell"><pre><code>${esc2(cell.content)}</code></pre></figure>`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function toCSS() {
|
|
311
|
+
return [
|
|
312
|
+
":root{--fg:#1a1a2e;--muted:#5a5a72;--bg:#fff;--code-bg:#f4f4f8;--border:#e0e0ea;--accent:#2a4d8f;--pass:#1a7f37;--fail:#cf222e;}",
|
|
313
|
+
"*{box-sizing:border-box}",
|
|
314
|
+
"body{margin:0;color:var(--fg);background:var(--bg);font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}",
|
|
315
|
+
"main{max-width:48rem;margin:0 auto;padding:2rem 1.25rem}",
|
|
316
|
+
"header{border-bottom:2px solid var(--border);margin-bottom:1.5rem;padding-bottom:1rem}",
|
|
317
|
+
".doc-title{margin:0 0 .25rem;font-size:1.9rem}",
|
|
318
|
+
".doc-meta,.doc-desc{color:var(--muted);margin:.15rem 0}",
|
|
319
|
+
".tag{display:inline-block;background:var(--code-bg);border:1px solid var(--border);border-radius:1rem;padding:.05rem .55rem;font-size:.8rem;margin-right:.3rem}",
|
|
320
|
+
".cell{margin:1.1rem 0}",
|
|
321
|
+
"h1,h2,h3,h4{line-height:1.25}",
|
|
322
|
+
"code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}",
|
|
323
|
+
"pre{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:.7rem .9rem;overflow:auto}",
|
|
324
|
+
".cell-code{border-left:3px solid var(--accent);padding-left:.9rem}",
|
|
325
|
+
".cell-code figcaption{color:var(--muted);font-size:.8rem;font-family:ui-monospace,monospace;margin-bottom:.2rem}",
|
|
326
|
+
".cell-output{background:#fafaff;border:1px solid var(--border);border-radius:6px;padding:.5rem .9rem;margin-top:.4rem;white-space:pre-wrap}",
|
|
327
|
+
".cell-error{background:#fff0f0;border:1px solid #f3c0c0;color:var(--fail);border-radius:6px;padding:.5rem .9rem;margin-top:.4rem}",
|
|
328
|
+
".cell-eq{text-align:center;margin:1.3rem 0}",
|
|
329
|
+
"math{font-size:1.15rem}",
|
|
330
|
+
".cell-test{display:flex;align-items:center;gap:.5rem;font-size:.95rem}",
|
|
331
|
+
".cell-test .badge{font-weight:700;width:1.4rem;height:1.4rem;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;color:#fff}",
|
|
332
|
+
".cell-test.pass .badge{background:var(--pass)}",
|
|
333
|
+
".cell-test.fail .badge{background:var(--fail)}",
|
|
334
|
+
".cell-test.err .badge{background:#9a6700}",
|
|
335
|
+
".cell-test.nrun .badge{background:#8a8a9a}",
|
|
336
|
+
".cell-test .note{color:var(--fail);font-size:.85rem}",
|
|
337
|
+
".cell-chart{text-align:center}",
|
|
338
|
+
".note{color:var(--muted)}"
|
|
339
|
+
].join("");
|
|
340
|
+
}
|
|
341
|
+
function toHTML(doc, options = {}) {
|
|
342
|
+
const title = doc.title ?? "Workbook";
|
|
343
|
+
const headerBits = [`<h1 class="doc-title">${esc2(title)}</h1>`];
|
|
344
|
+
if (doc.author) headerBits.push(`<p class="doc-meta">by ${esc2(doc.author)}</p>`);
|
|
345
|
+
if (doc.description) headerBits.push(`<p class="doc-desc">${esc2(doc.description)}</p>`);
|
|
346
|
+
if (doc.tags && doc.tags.length) {
|
|
347
|
+
headerBits.push(
|
|
348
|
+
`<p class="doc-tags">${doc.tags.map((t) => `<span class="tag">${esc2(t)}</span>`).join("")}</p>`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const body = doc.cells.map((c) => renderCell(c, options.parse)).join("\n");
|
|
352
|
+
return [
|
|
353
|
+
"<!doctype html>",
|
|
354
|
+
'<html lang="en">',
|
|
355
|
+
"<head>",
|
|
356
|
+
'<meta charset="utf-8">',
|
|
357
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
358
|
+
`<title>${esc2(title)}</title>`,
|
|
359
|
+
`<style>${toCSS()}</style>`,
|
|
360
|
+
"</head>",
|
|
361
|
+
"<body>",
|
|
362
|
+
"<main>",
|
|
363
|
+
`<header>${headerBits.join("")}</header>`,
|
|
364
|
+
body,
|
|
365
|
+
"</main>",
|
|
366
|
+
"</body>",
|
|
367
|
+
"</html>"
|
|
368
|
+
].join("\n");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/tex.ts
|
|
372
|
+
var PREAMBLE = [
|
|
373
|
+
"\\documentclass{article}",
|
|
374
|
+
"\\usepackage{amsmath}",
|
|
375
|
+
"\\usepackage{tikz}",
|
|
376
|
+
"\\usepackage{listings}",
|
|
377
|
+
"\\usepackage{xcolor}",
|
|
378
|
+
"\\usepackage[margin=1in]{geometry}",
|
|
379
|
+
"\\usepackage{hyperref}"
|
|
380
|
+
].join("\n");
|
|
381
|
+
function renderEquationTex(content, parse3) {
|
|
382
|
+
if (parse3) {
|
|
383
|
+
try {
|
|
384
|
+
const node = parse3(content);
|
|
385
|
+
return `\\[ ${node.toTex()} \\]`;
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return `\\texttt{${texEscape(content)}}`;
|
|
390
|
+
}
|
|
391
|
+
function renderCellTex(cell, parse3) {
|
|
392
|
+
switch (cell.type) {
|
|
393
|
+
case "markdown":
|
|
394
|
+
return markdownToTex(cell.content);
|
|
395
|
+
case "equation":
|
|
396
|
+
return renderEquationTex(cell.content, parse3);
|
|
397
|
+
case "code": {
|
|
398
|
+
const cap = cell.id ? `\\textit{${texEscape(cell.id)}}\\\\
|
|
399
|
+
` : "";
|
|
400
|
+
const src = `\\begin{lstlisting}
|
|
401
|
+
${cell.content}
|
|
402
|
+
\\end{lstlisting}`;
|
|
403
|
+
let res = "";
|
|
404
|
+
if (cell.error !== void 0)
|
|
405
|
+
res = `
|
|
406
|
+
|
|
407
|
+
\\textcolor{red}{\\texttt{${texEscape(cell.error)}}}`;
|
|
408
|
+
else if (cell.output !== void 0)
|
|
409
|
+
res = `
|
|
410
|
+
|
|
411
|
+
\\begin{verbatim}
|
|
412
|
+
${cell.output}
|
|
413
|
+
\\end{verbatim}`;
|
|
414
|
+
return `${cap}${src}${res}`;
|
|
415
|
+
}
|
|
416
|
+
case "test": {
|
|
417
|
+
const label = `\\texttt{${texEscape(cell.content)}}`;
|
|
418
|
+
if (cell.error !== void 0)
|
|
419
|
+
return `\\textcolor{orange}{[ERROR]} ${label} --- ${texEscape(cell.error)}`;
|
|
420
|
+
if (cell.passed === true) return `\\textcolor{green!60!black}{[PASS]} ${label}`;
|
|
421
|
+
if (cell.passed === false) return `\\textcolor{red}{[FAIL]} ${label}`;
|
|
422
|
+
return `\\textcolor{gray}{[NOT RUN]} ${label}`;
|
|
423
|
+
}
|
|
424
|
+
case "data": {
|
|
425
|
+
const val = cell.error !== void 0 ? cell.error : cell.output ?? "";
|
|
426
|
+
const cap = cell.id ? `\\textit{${texEscape(cell.id)}}\\\\
|
|
427
|
+
` : "";
|
|
428
|
+
return `${cap}\\begin{verbatim}
|
|
429
|
+
${val}
|
|
430
|
+
\\end{verbatim}`;
|
|
431
|
+
}
|
|
432
|
+
case "chart": {
|
|
433
|
+
const chart = cell.chartTikz ?? "% no chart";
|
|
434
|
+
const note = cell.note ? `
|
|
435
|
+
|
|
436
|
+
\\textit{${texEscape(cell.note)}}` : "";
|
|
437
|
+
return `\\begin{center}
|
|
438
|
+
${chart}
|
|
439
|
+
\\end{center}${note}`;
|
|
440
|
+
}
|
|
441
|
+
default:
|
|
442
|
+
return `\\begin{verbatim}
|
|
443
|
+
${cell.content}
|
|
444
|
+
\\end{verbatim}`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function toTeX(doc, options = {}) {
|
|
448
|
+
const body = doc.cells.map((c) => renderCellTex(c, options.parse)).join("\n\n");
|
|
449
|
+
if (options.fragment) return body;
|
|
450
|
+
const meta = [];
|
|
451
|
+
if (doc.title) meta.push(`\\title{${texEscape(doc.title)}}`);
|
|
452
|
+
if (doc.author) meta.push(`\\author{${texEscape(doc.author)}}`);
|
|
453
|
+
const maketitle = doc.title ? "\\maketitle\n" : "";
|
|
454
|
+
return [PREAMBLE, ...meta, "\\begin{document}", `${maketitle}${body}`, "\\end{document}"].join(
|
|
455
|
+
"\n"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/pdf.ts
|
|
460
|
+
import { latexToPdf } from "@danielsimonjr/mathts-plot/render";
|
|
461
|
+
function toPDF(doc, outPath, options = {}) {
|
|
462
|
+
const { parse: parse3, ...renderOpts } = options;
|
|
463
|
+
return latexToPdf(toTeX(doc, { parse: parse3, fragment: false }), outPath, renderOpts);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/svg.ts
|
|
467
|
+
import { line, scatter, bar } from "@danielsimonjr/mathts-plot";
|
|
468
|
+
function coerce(v) {
|
|
469
|
+
if (typeof v === "number") return v;
|
|
470
|
+
if (typeof v === "bigint") return Number(v);
|
|
471
|
+
const u = v;
|
|
472
|
+
if (u && typeof u.toNumeric === "function") {
|
|
473
|
+
try {
|
|
474
|
+
return Number(u.toNumeric());
|
|
475
|
+
} catch {
|
|
476
|
+
return NaN;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return Number(v);
|
|
480
|
+
}
|
|
481
|
+
function toNums(raw) {
|
|
482
|
+
let arr = raw;
|
|
483
|
+
const m = raw;
|
|
484
|
+
if (m && typeof m.toArray === "function") {
|
|
485
|
+
try {
|
|
486
|
+
arr = m.toArray();
|
|
487
|
+
} catch {
|
|
488
|
+
arr = raw;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return Array.isArray(arr) ? arr.flat(Infinity).map(coerce) : [];
|
|
492
|
+
}
|
|
493
|
+
function renderChart(spec, xRaw, yRaw, format = "svg") {
|
|
494
|
+
const xs = toNums(xRaw);
|
|
495
|
+
const ys = toNums(yRaw);
|
|
496
|
+
const opts = format === "tikz" ? {
|
|
497
|
+
title: spec.title,
|
|
498
|
+
xLabel: spec.xLabel,
|
|
499
|
+
yLabel: spec.yLabel,
|
|
500
|
+
format: "tikz",
|
|
501
|
+
tikz: { standalone: false }
|
|
502
|
+
} : { title: spec.title, xLabel: spec.xLabel, yLabel: spec.yLabel };
|
|
503
|
+
if (spec.type === "scatter") return scatter(xs, ys, opts);
|
|
504
|
+
if (spec.type === "bar") return bar(xs, ys, opts);
|
|
505
|
+
return line(xs, ys, opts);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/cli.ts
|
|
509
|
+
var parse2 = mathFunctions.parse;
|
|
8
510
|
var HELP = `
|
|
9
511
|
mtsw - MathTS Workbook CLI
|
|
10
512
|
|
|
11
513
|
Usage:
|
|
12
|
-
mtsw run <file>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
mtsw
|
|
18
|
-
mtsw
|
|
19
|
-
mtsw
|
|
20
|
-
mtsw
|
|
21
|
-
mtsw new <name> -t
|
|
22
|
-
|
|
514
|
+
mtsw run <file> [-c <id>] [-v] [--json] [--write]
|
|
515
|
+
Execute a workbook (or one cell + its
|
|
516
|
+
deps with -c/--cell). -v: events,
|
|
517
|
+
--json: machine output, --write: persist
|
|
518
|
+
outputs back to the file.
|
|
519
|
+
mtsw describe <file> [--json] Structured document model (cells, graph)
|
|
520
|
+
mtsw validate <file> [--json] Validate structure (ids, deps, cycles)
|
|
521
|
+
mtsw graph <file> [-f mermaid|dot] Print the dependency graph
|
|
522
|
+
mtsw strip <file> [-w|--write] Strip outputs (stdout, or -w to rewrite)
|
|
523
|
+
mtsw new <name> [-t basic|empty|chart] [--empty] [-o <path>] [--force]
|
|
524
|
+
Scaffold a new workbook (-o for any path)
|
|
525
|
+
mtsw import [<file>] [-o out.mtsw] [--json]
|
|
526
|
+
Build a .mtsw from a JSON/YAML doc
|
|
527
|
+
({metadata,cells:[{id,type,content,dependsOn}]};
|
|
528
|
+
file or stdin; stdout if no -o)
|
|
529
|
+
mtsw capabilities [--json] Engine version + feature flags
|
|
530
|
+
mtsw templates [--json] List scaffold templates
|
|
531
|
+
mtsw cell <verb> <file> ... Edit cells (atomic, in-place):
|
|
532
|
+
cell add <file> --type <t> --id <id> [--content <s> | --content-file <p|->]
|
|
533
|
+
[--depends-on a,b] [--before|--after <id> | --at <n>]
|
|
534
|
+
cell edit <file> <id> [--content <s> | --content-file <p|->] [--type <t>] [--depends-on a,b]
|
|
535
|
+
cell rm <file> <id> [--force] (--force detaches dependents)
|
|
536
|
+
cell move <file> <id> (--before|--after <id> | --at <n>)
|
|
537
|
+
cell rename <file> <oldId> <newId>
|
|
538
|
+
(any cell verb accepts --json and --dry-run)
|
|
539
|
+
mtsw functions [--json] List functions/constants cells can call
|
|
540
|
+
mtsw meta get <file> [--json] Show workbook metadata
|
|
541
|
+
mtsw meta set <file> [--title s] [--author s] [--description s] [--tags a,b]
|
|
542
|
+
mtsw export <file> [--format html|tex|json] [--fragment] [-o out] [--no-run]
|
|
543
|
+
Render to a self-contained HTML or LaTeX
|
|
544
|
+
document (MathML/TikZ equations + charts,
|
|
545
|
+
no external deps), or emit the executed
|
|
546
|
+
run report as JSON. --fragment (tex only)
|
|
547
|
+
omits the preamble for \\input; --no-run is
|
|
548
|
+
incompatible with --format json.
|
|
549
|
+
mtsw serve [<file>] JSON-RPC over stdio (persistent session
|
|
550
|
+
w/ streaming events + incremental run)
|
|
23
551
|
|
|
24
552
|
Options:
|
|
25
553
|
-h, --help Show this help
|
|
26
|
-
-v, --verbose Verbose output
|
|
27
554
|
-V, --version Show version
|
|
555
|
+
|
|
556
|
+
Notes:
|
|
557
|
+
--json output is a single envelope on stdout: { schemaVersion, command, ok,
|
|
558
|
+
data, problems }. The envelope is emitted even on failure; exit code mirrors
|
|
559
|
+
'ok' (GUI clients should read 'ok', not the exit code).
|
|
560
|
+
Writes (run --write, strip -w, new) are atomic but drop YAML comments / key
|
|
561
|
+
order. Dependency scope is direct-only (non-transitive); cell ids must be
|
|
562
|
+
valid identifiers ([A-Za-z_][A-Za-z0-9_]*).
|
|
563
|
+
`.trimStart();
|
|
564
|
+
function jsonEnvelope(command, ok, data, problems = []) {
|
|
565
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
566
|
+
const replacer = (_key, value) => {
|
|
567
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
568
|
+
if (value !== null && typeof value === "object") {
|
|
569
|
+
if (seen.has(value)) return "[Circular]";
|
|
570
|
+
seen.add(value);
|
|
571
|
+
}
|
|
572
|
+
return value;
|
|
573
|
+
};
|
|
574
|
+
return JSON.stringify(
|
|
575
|
+
{ schemaVersion: SCHEMA_VERSION, command, ok, data, problems: problems ?? [] },
|
|
576
|
+
replacer,
|
|
577
|
+
2
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
581
|
+
"-f",
|
|
582
|
+
"--format",
|
|
583
|
+
"-t",
|
|
584
|
+
"--template",
|
|
585
|
+
"-c",
|
|
586
|
+
"--cell",
|
|
587
|
+
"--id",
|
|
588
|
+
"--content",
|
|
589
|
+
"--content-file",
|
|
590
|
+
"--depends-on",
|
|
591
|
+
"--before",
|
|
592
|
+
"--after",
|
|
593
|
+
"--at",
|
|
594
|
+
"--title",
|
|
595
|
+
"--author",
|
|
596
|
+
"--description",
|
|
597
|
+
"--tags",
|
|
598
|
+
"-o",
|
|
599
|
+
"--output"
|
|
600
|
+
]);
|
|
601
|
+
function firstPositional(args) {
|
|
602
|
+
for (let i = 0; i < args.length; i++) {
|
|
603
|
+
const arg = args[i];
|
|
604
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
605
|
+
i++;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (!arg.startsWith("-")) return arg;
|
|
609
|
+
}
|
|
610
|
+
return void 0;
|
|
611
|
+
}
|
|
612
|
+
function flagValue(args, flag) {
|
|
613
|
+
const i = args.indexOf(flag);
|
|
614
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
615
|
+
}
|
|
616
|
+
function readFile(file) {
|
|
617
|
+
try {
|
|
618
|
+
return { content: readFileSync(file, "utf-8") };
|
|
619
|
+
} catch (error) {
|
|
620
|
+
return {
|
|
621
|
+
error: `Cannot read file '${file}': ${error instanceof Error ? error.message : String(error)}`
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
var BASIC_TEMPLATE = `version: "1.0"
|
|
626
|
+
metadata:
|
|
627
|
+
title: "<NAME>"
|
|
628
|
+
runtime:
|
|
629
|
+
engine: mathts
|
|
630
|
+
execution: reactive
|
|
631
|
+
cells:
|
|
632
|
+
- markdown: |
|
|
633
|
+
# <NAME>
|
|
634
|
+
|
|
635
|
+
A new MathTS workbook. Edit the cells, then run:
|
|
636
|
+
mtsw run <NAME>.mtsw
|
|
637
|
+
id: intro
|
|
638
|
+
|
|
639
|
+
- code: "1 + 1"
|
|
640
|
+
id: example
|
|
641
|
+
|
|
642
|
+
- test: "example == 2"
|
|
643
|
+
id: checkExample
|
|
644
|
+
depends_on: [example]
|
|
28
645
|
`;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
console.log(HELP);
|
|
56
|
-
process.exit(1);
|
|
646
|
+
function bullets(items) {
|
|
647
|
+
return items.map((item) => ` - ${item}`).join("\n");
|
|
648
|
+
}
|
|
649
|
+
function statusMark(status) {
|
|
650
|
+
return status === "success" || status === "pass" ? "\u2713" : "\u2717";
|
|
651
|
+
}
|
|
652
|
+
function humanCellLine(cell) {
|
|
653
|
+
const mark = statusMark(cell.status);
|
|
654
|
+
const body = cell.status === "error" || cell.status === "fail" ? cell.error ?? "(failed)" : formatResult(cell.output);
|
|
655
|
+
return `${mark} ${cell.id} (${cell.type}): ${body}`;
|
|
656
|
+
}
|
|
657
|
+
function failureSummary(cells) {
|
|
658
|
+
const failures = cells.filter((c) => c.status === "error" || c.status === "fail");
|
|
659
|
+
if (failures.length === 0) return "";
|
|
660
|
+
return `${failures.length} cell(s) failed:
|
|
661
|
+
${bullets(failures.map((c) => `${c.id}: ${c.error ?? "(failed)"}`))}`;
|
|
662
|
+
}
|
|
663
|
+
function errMessage(error) {
|
|
664
|
+
return error instanceof Error ? error.message : String(error);
|
|
665
|
+
}
|
|
666
|
+
function applyResults(workbook, results) {
|
|
667
|
+
for (const result of results) {
|
|
668
|
+
const cell = workbook.cells.find((c) => c.id === result.id);
|
|
669
|
+
if (!cell) continue;
|
|
670
|
+
cell.output = result.output;
|
|
671
|
+
cell.error = result.error;
|
|
57
672
|
}
|
|
58
673
|
}
|
|
674
|
+
function jsonCells(cells) {
|
|
675
|
+
return cells.map((c) => ({
|
|
676
|
+
id: c.id,
|
|
677
|
+
type: c.type,
|
|
678
|
+
status: c.status,
|
|
679
|
+
output: c.output ?? null,
|
|
680
|
+
error: c.error ?? null
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
function failureList(cells) {
|
|
684
|
+
return cells.filter((c) => c.status === "error" || c.status === "fail").map((c) => `${c.id}: ${c.error ?? "(failed)"}`);
|
|
685
|
+
}
|
|
59
686
|
async function runCommand(args) {
|
|
60
|
-
const
|
|
687
|
+
const json = args.includes("--json");
|
|
688
|
+
const fail = (problems2, data = null) => json ? { stdout: jsonEnvelope("run", false, data, problems2), stderr: "", exitCode: 1 } : { stdout: "", stderr: problems2.join("\n"), exitCode: 1 };
|
|
689
|
+
const file = firstPositional(args);
|
|
61
690
|
if (!file) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const result = parseWorkbook(content);
|
|
68
|
-
if (!result.success) {
|
|
69
|
-
console.error("Parse errors:", result.errors);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
if (result.workbook) {
|
|
73
|
-
const executor = createExecutor(result.workbook);
|
|
74
|
-
executor.on((event) => {
|
|
75
|
-
console.log(`[${event.type}] ${event.cellId ?? ""}`);
|
|
76
|
-
});
|
|
77
|
-
await executor.runAll();
|
|
691
|
+
return json ? fail(["Usage: mtsw run <file> [-c <id>] [-v] [--json] [--write]"]) : {
|
|
692
|
+
stdout: "",
|
|
693
|
+
stderr: "Usage: mtsw run <file> [-c <id>] [-v] [--json] [--write]",
|
|
694
|
+
exitCode: 1
|
|
695
|
+
};
|
|
78
696
|
}
|
|
697
|
+
const read = readFile(file);
|
|
698
|
+
if (read.error) return fail([read.error]);
|
|
699
|
+
const parsed = parseWorkbook(read.content);
|
|
700
|
+
if (!parsed.success) {
|
|
701
|
+
const problems2 = parsed.errors ?? [];
|
|
702
|
+
return json ? fail(problems2, { cells: [] }) : {
|
|
703
|
+
stdout: "",
|
|
704
|
+
stderr: `Parse errors:
|
|
705
|
+
${bullets(problems2)}`,
|
|
706
|
+
exitCode: 1
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const cellId = flagValue(args, "-c") ?? flagValue(args, "--cell");
|
|
710
|
+
if (cellId !== void 0 && !parsed.workbook.cells.some((c) => c.id === cellId)) {
|
|
711
|
+
return json ? fail([`No such cell: '${cellId}'`], { cells: [] }) : {
|
|
712
|
+
stdout: "",
|
|
713
|
+
stderr: `No such cell: '${cellId}'`,
|
|
714
|
+
exitCode: 1
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
718
|
+
const executor = createExecutor(parsed.workbook);
|
|
719
|
+
const events = [];
|
|
720
|
+
if (verbose) {
|
|
721
|
+
executor.on((event) => events.push(`[${event.type}] ${event.cellId ?? ""}`.trimEnd()));
|
|
722
|
+
}
|
|
723
|
+
const report = await executor.runReport(cellId !== void 0 ? { only: cellId } : {});
|
|
724
|
+
const problems = report.ok ? [] : failureList(report.cells);
|
|
725
|
+
const humanFailures = report.ok ? "" : failureSummary(report.cells);
|
|
726
|
+
if (args.includes("--write")) {
|
|
727
|
+
applyResults(parsed.workbook, report.cells);
|
|
728
|
+
try {
|
|
729
|
+
writeFileAtomic(file, serializeWorkbook(parsed.workbook));
|
|
730
|
+
} catch (error) {
|
|
731
|
+
return fail([`Failed to write '${file}': ${errMessage(error)}`], {
|
|
732
|
+
cells: jsonCells(report.cells)
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (json) {
|
|
736
|
+
const stdout2 = jsonEnvelope(
|
|
737
|
+
"run",
|
|
738
|
+
report.ok,
|
|
739
|
+
{ cells: jsonCells(report.cells), written: file },
|
|
740
|
+
problems
|
|
741
|
+
);
|
|
742
|
+
return { stdout: stdout2, stderr: "", exitCode: report.ok ? 0 : 1 };
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
stdout: "",
|
|
746
|
+
stderr: [`Updated ${file}`, humanFailures].filter(Boolean).join("\n"),
|
|
747
|
+
exitCode: report.ok ? 0 : 1
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
if (json) {
|
|
751
|
+
const stdout2 = jsonEnvelope("run", report.ok, { cells: jsonCells(report.cells) }, problems);
|
|
752
|
+
return { stdout: stdout2, stderr: "", exitCode: report.ok ? 0 : 1 };
|
|
753
|
+
}
|
|
754
|
+
const body = report.cells.map(humanCellLine).join("\n");
|
|
755
|
+
const stdout = verbose && events.length > 0 ? `${events.join("\n")}
|
|
756
|
+
|
|
757
|
+
${body}` : body;
|
|
758
|
+
return { stdout, stderr: humanFailures, exitCode: report.ok ? 0 : 1 };
|
|
759
|
+
}
|
|
760
|
+
function validateChartSpecs(workbook) {
|
|
761
|
+
const problems = [];
|
|
762
|
+
const ids = new Set(workbook.cells.map((c) => c.id));
|
|
763
|
+
for (const cell of workbook.cells) {
|
|
764
|
+
if (cell.type !== "visualization") continue;
|
|
765
|
+
let spec;
|
|
766
|
+
try {
|
|
767
|
+
spec = parseYamlHardened(cell.content);
|
|
768
|
+
} catch (error) {
|
|
769
|
+
problems.push(`Cell "${cell.id}": invalid chart spec (${errMessage(error)})`);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
773
|
+
problems.push(`Cell "${cell.id}": chart spec must be a mapping with type/x/y`);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const s = spec;
|
|
777
|
+
if (s.type !== void 0 && !["line", "scatter", "bar"].includes(String(s.type))) {
|
|
778
|
+
problems.push(`Cell "${cell.id}": chart type must be line|scatter|bar`);
|
|
779
|
+
}
|
|
780
|
+
for (const axis of ["x", "y"]) {
|
|
781
|
+
const a = s[axis];
|
|
782
|
+
if (!a || typeof a !== "object") {
|
|
783
|
+
problems.push(`Cell "${cell.id}": chart "${axis}" must be a mapping with a "data" field`);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const data = a.data;
|
|
787
|
+
if (data === void 0) {
|
|
788
|
+
problems.push(`Cell "${cell.id}": chart ${axis}.data is required`);
|
|
789
|
+
} else if (typeof data === "string" && !ids.has(data)) {
|
|
790
|
+
problems.push(`Cell "${cell.id}": chart ${axis}.data references unknown cell "${data}"`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return problems;
|
|
79
795
|
}
|
|
80
|
-
|
|
81
|
-
const
|
|
796
|
+
function computeProblems(parsed) {
|
|
797
|
+
const problems = [...parsed.errors ?? []];
|
|
798
|
+
if (parsed.success && parsed.workbook) {
|
|
799
|
+
const graph = buildDependencyGraph(parsed.workbook.cells);
|
|
800
|
+
for (const cycle of detectCycles(graph)) {
|
|
801
|
+
problems.push(`Dependency cycle: ${cycle.join(" -> ")}`);
|
|
802
|
+
}
|
|
803
|
+
problems.push(...validateChartSpecs(parsed.workbook));
|
|
804
|
+
}
|
|
805
|
+
return problems;
|
|
806
|
+
}
|
|
807
|
+
function validateCommand(args) {
|
|
808
|
+
const json = args.includes("--json");
|
|
809
|
+
const file = firstPositional(args);
|
|
810
|
+
if (!file) {
|
|
811
|
+
return json ? {
|
|
812
|
+
stdout: jsonEnvelope("validate", false, null, ["Usage: mtsw validate <file>"]),
|
|
813
|
+
stderr: "",
|
|
814
|
+
exitCode: 1
|
|
815
|
+
} : { stdout: "", stderr: "Usage: mtsw validate <file>", exitCode: 1 };
|
|
816
|
+
}
|
|
817
|
+
const read = readFile(file);
|
|
818
|
+
if (read.error) {
|
|
819
|
+
return json ? { stdout: jsonEnvelope("validate", false, null, [read.error]), stderr: "", exitCode: 1 } : { stdout: "", stderr: read.error, exitCode: 1 };
|
|
820
|
+
}
|
|
821
|
+
const parsed = parseWorkbook(read.content);
|
|
822
|
+
const problems = computeProblems(parsed);
|
|
823
|
+
const ok = problems.length === 0;
|
|
824
|
+
const cellCount = parsed.workbook ? parsed.workbook.cells.length : 0;
|
|
825
|
+
if (json) {
|
|
826
|
+
return {
|
|
827
|
+
stdout: jsonEnvelope("validate", ok, { cellCount }, problems),
|
|
828
|
+
stderr: "",
|
|
829
|
+
exitCode: ok ? 0 : 1
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
if (ok) {
|
|
833
|
+
return { stdout: `OK: '${file}' is valid (${cellCount} cell(s))`, stderr: "", exitCode: 0 };
|
|
834
|
+
}
|
|
835
|
+
return { stdout: "", stderr: `Invalid workbook:
|
|
836
|
+
${bullets(problems)}`, exitCode: 1 };
|
|
837
|
+
}
|
|
838
|
+
function describeCommand(args) {
|
|
839
|
+
const json = args.includes("--json");
|
|
840
|
+
const file = firstPositional(args);
|
|
82
841
|
if (!file) {
|
|
83
|
-
|
|
84
|
-
|
|
842
|
+
return json ? {
|
|
843
|
+
stdout: jsonEnvelope("describe", false, null, ["Usage: mtsw describe <file>"]),
|
|
844
|
+
stderr: "",
|
|
845
|
+
exitCode: 1
|
|
846
|
+
} : { stdout: "", stderr: "Usage: mtsw describe <file>", exitCode: 1 };
|
|
847
|
+
}
|
|
848
|
+
const read = readFile(file);
|
|
849
|
+
if (read.error) {
|
|
850
|
+
return json ? { stdout: jsonEnvelope("describe", false, null, [read.error]), stderr: "", exitCode: 1 } : { stdout: "", stderr: read.error, exitCode: 1 };
|
|
851
|
+
}
|
|
852
|
+
const parsed = parseWorkbook(read.content);
|
|
853
|
+
const problems = computeProblems(parsed);
|
|
854
|
+
const ok = parsed.success && problems.length === 0;
|
|
855
|
+
const data = describeData(parsed.workbook);
|
|
856
|
+
const cells = data.cells;
|
|
857
|
+
if (json) {
|
|
858
|
+
return {
|
|
859
|
+
stdout: jsonEnvelope("describe", ok, data, problems),
|
|
860
|
+
stderr: "",
|
|
861
|
+
exitCode: ok ? 0 : 1
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
if (!parsed.success) {
|
|
865
|
+
return { stdout: "", stderr: `Parse errors:
|
|
866
|
+
${bullets(problems)}`, exitCode: 1 };
|
|
867
|
+
}
|
|
868
|
+
const byType = cells.reduce((acc, c) => {
|
|
869
|
+
acc[c.type] = (acc[c.type] ?? 0) + 1;
|
|
870
|
+
return acc;
|
|
871
|
+
}, {});
|
|
872
|
+
const summary = [
|
|
873
|
+
`${data.metadata.title ?? "(untitled)"} \u2014 ${cells.length} cell(s)`,
|
|
874
|
+
...Object.entries(byType).map(([t, n]) => ` ${t}: ${n}`),
|
|
875
|
+
problems.length ? `${problems.length} problem(s)` : "no problems"
|
|
876
|
+
].join("\n");
|
|
877
|
+
return { stdout: summary, stderr: "", exitCode: ok ? 0 : 1 };
|
|
878
|
+
}
|
|
879
|
+
function capabilitiesCommand(args) {
|
|
880
|
+
const data = capabilitiesInfo();
|
|
881
|
+
if (args.includes("--json")) {
|
|
882
|
+
return { stdout: jsonEnvelope("capabilities", true, data, []), stderr: "", exitCode: 0 };
|
|
883
|
+
}
|
|
884
|
+
const lines = [
|
|
885
|
+
`mtsw ${data.version} (schema ${SCHEMA_VERSION.major}.${SCHEMA_VERSION.minor})`,
|
|
886
|
+
`supported cells: ${data.cellTypes.supported.join(", ")}`,
|
|
887
|
+
`deferred cells: ${data.cellTypes.deferred.join(", ")}`,
|
|
888
|
+
`commands: ${data.commands.join(", ")}`
|
|
889
|
+
];
|
|
890
|
+
return { stdout: lines.join("\n"), stderr: "", exitCode: 0 };
|
|
891
|
+
}
|
|
892
|
+
function templatesCommand(args) {
|
|
893
|
+
const templates = Object.entries(TEMPLATES).map(([name, t]) => ({
|
|
894
|
+
name,
|
|
895
|
+
description: t.description
|
|
896
|
+
}));
|
|
897
|
+
if (args.includes("--json")) {
|
|
898
|
+
return { stdout: jsonEnvelope("templates", true, { templates }, []), stderr: "", exitCode: 0 };
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
stdout: templates.map((t) => `${t.name} - ${t.description}`).join("\n"),
|
|
902
|
+
stderr: "",
|
|
903
|
+
exitCode: 0
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function functionsCommand(args) {
|
|
907
|
+
const data = listFunctions();
|
|
908
|
+
if (args.includes("--json")) {
|
|
909
|
+
return { stdout: jsonEnvelope("functions", true, data, []), stderr: "", exitCode: 0 };
|
|
910
|
+
}
|
|
911
|
+
const lines = [
|
|
912
|
+
`functions (${data.functions.length}): ${data.functions.join(", ")}`,
|
|
913
|
+
`constants (${data.constants.length}): ${data.constants.join(", ")}`
|
|
914
|
+
];
|
|
915
|
+
return { stdout: lines.join("\n"), stderr: "", exitCode: 0 };
|
|
916
|
+
}
|
|
917
|
+
function metaCommand(args) {
|
|
918
|
+
const json = args.includes("--json");
|
|
919
|
+
const fail = (problems) => json ? { stdout: jsonEnvelope("meta", false, null, problems), stderr: "", exitCode: 1 } : { stdout: "", stderr: problems.join("\n"), exitCode: 1 };
|
|
920
|
+
const verb = args[0];
|
|
921
|
+
if (verb !== "get" && verb !== "set") return fail(["Usage: mtsw meta <get|set> <file> ..."]);
|
|
922
|
+
const vargs = args.slice(1);
|
|
923
|
+
const file = positionals(vargs)[0];
|
|
924
|
+
if (!file) return fail([`Usage: mtsw meta ${verb} <file> ...`]);
|
|
925
|
+
const read = readFile(file);
|
|
926
|
+
if (read.error) return fail([read.error]);
|
|
927
|
+
const parsed = parseWorkbook(read.content);
|
|
928
|
+
if (!parsed.success) return fail(parsed.errors ?? ["Parse error"]);
|
|
929
|
+
const wb = parsed.workbook;
|
|
930
|
+
if (verb === "get") {
|
|
931
|
+
if (json)
|
|
932
|
+
return { stdout: jsonEnvelope("meta", true, wb.metadata, []), stderr: "", exitCode: 0 };
|
|
933
|
+
const lines = Object.entries(wb.metadata).map(
|
|
934
|
+
([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : String(v)}`
|
|
935
|
+
);
|
|
936
|
+
return { stdout: lines.join("\n") || "(no metadata)", stderr: "", exitCode: 0 };
|
|
937
|
+
}
|
|
938
|
+
const changes = {};
|
|
939
|
+
const title = flagValue(vargs, "--title");
|
|
940
|
+
if (title !== void 0) changes.title = title;
|
|
941
|
+
const author = flagValue(vargs, "--author");
|
|
942
|
+
if (author !== void 0) changes.author = author;
|
|
943
|
+
const description = flagValue(vargs, "--description");
|
|
944
|
+
if (description !== void 0) changes.description = description;
|
|
945
|
+
const tags = flagValue(vargs, "--tags");
|
|
946
|
+
if (tags !== void 0)
|
|
947
|
+
changes.tags = tags.split(",").map((s) => s.trim()).filter(Boolean);
|
|
948
|
+
if (Object.keys(changes).length === 0) {
|
|
949
|
+
return fail(["meta set requires at least one of --title/--author/--description/--tags"]);
|
|
950
|
+
}
|
|
951
|
+
let next;
|
|
952
|
+
try {
|
|
953
|
+
next = setMetadata(wb, changes);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
return fail([errMessage(error)]);
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
writeFileAtomic(file, serializeWorkbook(next));
|
|
959
|
+
} catch (error) {
|
|
960
|
+
return fail([`Failed to write '${file}': ${errMessage(error)}`]);
|
|
961
|
+
}
|
|
962
|
+
if (json)
|
|
963
|
+
return { stdout: jsonEnvelope("meta", true, next.metadata, []), stderr: "", exitCode: 0 };
|
|
964
|
+
return { stdout: "", stderr: `Updated metadata in ${file}`, exitCode: 0 };
|
|
965
|
+
}
|
|
966
|
+
function buildRenderDoc(workbook, byId, format = "svg") {
|
|
967
|
+
const lookup = (ref) => {
|
|
968
|
+
if (typeof ref !== "string") return ref;
|
|
969
|
+
return byId?.get(ref)?.output ?? workbook.cells.find((c) => c.id === ref)?.output;
|
|
970
|
+
};
|
|
971
|
+
const cells = workbook.cells.map((c) => {
|
|
972
|
+
const rc = { type: c.type, content: c.content, id: c.id };
|
|
973
|
+
if (c.type === "markdown" || c.type === "equation") return rc;
|
|
974
|
+
if (c.type === "visualization") {
|
|
975
|
+
rc.type = "chart";
|
|
976
|
+
try {
|
|
977
|
+
const spec = parseYamlHardened(c.content);
|
|
978
|
+
const rendered = renderChart(
|
|
979
|
+
{ type: spec?.type, title: spec?.title, xLabel: spec?.x?.label, yLabel: spec?.y?.label },
|
|
980
|
+
lookup(spec?.x?.data),
|
|
981
|
+
lookup(spec?.y?.data),
|
|
982
|
+
format
|
|
983
|
+
);
|
|
984
|
+
if (format === "tikz") rc.chartTikz = rendered;
|
|
985
|
+
else rc.chartSvg = rendered;
|
|
986
|
+
const unresolved = [];
|
|
987
|
+
for (const ref of [spec?.x?.data, spec?.y?.data]) {
|
|
988
|
+
if (typeof ref !== "string") continue;
|
|
989
|
+
if (byId) {
|
|
990
|
+
const r2 = byId.get(ref);
|
|
991
|
+
if (!r2 || r2.status === "error" || r2.status === "fail") unresolved.push(ref);
|
|
992
|
+
} else if (workbook.cells.find((x) => x.id === ref)?.output === void 0) {
|
|
993
|
+
unresolved.push(ref);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (unresolved.length > 0) {
|
|
997
|
+
rc.note = `chart data did not resolve: ${unresolved.join(", ")}${byId ? "" : " (try without --no-run)"}`;
|
|
998
|
+
}
|
|
999
|
+
} catch (error2) {
|
|
1000
|
+
const placeholder = renderChart({}, [], [], format);
|
|
1001
|
+
if (format === "tikz") rc.chartTikz = placeholder;
|
|
1002
|
+
else rc.chartSvg = placeholder;
|
|
1003
|
+
rc.note = `invalid chart spec: ${errMessage(error2)}`;
|
|
1004
|
+
}
|
|
1005
|
+
return rc;
|
|
1006
|
+
}
|
|
1007
|
+
const r = byId?.get(c.id);
|
|
1008
|
+
const status = r?.status;
|
|
1009
|
+
const output = r ? r.output : c.output;
|
|
1010
|
+
const error = r ? r.error : c.error;
|
|
1011
|
+
if (c.type === "test") {
|
|
1012
|
+
if (status === "error") rc.error = error ?? "error";
|
|
1013
|
+
else if (status !== void 0) rc.passed = status === "pass";
|
|
1014
|
+
else if (typeof output === "boolean") rc.passed = output;
|
|
1015
|
+
return rc;
|
|
1016
|
+
}
|
|
1017
|
+
if (status === "error" || status === "fail" || status === void 0 && error !== void 0) {
|
|
1018
|
+
rc.error = error ?? "error";
|
|
1019
|
+
} else if (output !== void 0) {
|
|
1020
|
+
rc.output = formatResult(output);
|
|
1021
|
+
}
|
|
1022
|
+
return rc;
|
|
1023
|
+
});
|
|
1024
|
+
return {
|
|
1025
|
+
title: workbook.metadata.title,
|
|
1026
|
+
author: workbook.metadata.author,
|
|
1027
|
+
description: workbook.metadata.description,
|
|
1028
|
+
tags: workbook.metadata.tags,
|
|
1029
|
+
cells
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
async function exportCommand(args) {
|
|
1033
|
+
const json = args.includes("--json");
|
|
1034
|
+
const fail = (problems) => json ? { stdout: jsonEnvelope("export", false, null, problems), stderr: "", exitCode: 1 } : { stdout: "", stderr: problems.join("\n"), exitCode: 1 };
|
|
1035
|
+
const format = flagValue(args, "--format") ?? "html";
|
|
1036
|
+
if (format !== "html" && format !== "tex" && format !== "json" && format !== "pdf") {
|
|
1037
|
+
return fail([`Unknown format '${format}' (supported: html, tex, json, pdf)`]);
|
|
1038
|
+
}
|
|
1039
|
+
const file = firstPositional(args);
|
|
1040
|
+
if (!file)
|
|
1041
|
+
return fail([
|
|
1042
|
+
"Usage: mtsw export <file> [--format html|tex|json|pdf] [--fragment] [-o out] [--no-run]"
|
|
1043
|
+
]);
|
|
1044
|
+
const read = readFile(file);
|
|
1045
|
+
if (read.error) return fail([read.error]);
|
|
1046
|
+
const parsed = parseWorkbook(read.content);
|
|
1047
|
+
if (!parsed.success) return fail(parsed.errors ?? ["Parse error"]);
|
|
1048
|
+
const workbook = parsed.workbook;
|
|
1049
|
+
let byId = null;
|
|
1050
|
+
let report = null;
|
|
1051
|
+
if (!args.includes("--no-run")) {
|
|
1052
|
+
report = await createExecutor(workbook).runReport();
|
|
1053
|
+
const fatal = report.cells.find((r) => r.id === "(workbook)");
|
|
1054
|
+
if (fatal) return fail([fatal.error ?? "workbook run failed"]);
|
|
1055
|
+
byId = new Map(report.cells.map((r) => [r.id, r]));
|
|
1056
|
+
}
|
|
1057
|
+
if (format === "json") {
|
|
1058
|
+
if (!report) {
|
|
1059
|
+
return fail(["--format json requires running the notebook (remove --no-run)"]);
|
|
1060
|
+
}
|
|
1061
|
+
const payload = {
|
|
1062
|
+
ok: report.ok,
|
|
1063
|
+
cells: report.cells.map((c) => ({
|
|
1064
|
+
id: c.id,
|
|
1065
|
+
type: c.type,
|
|
1066
|
+
status: c.status,
|
|
1067
|
+
output: c.output === void 0 ? void 0 : formatResult(c.output),
|
|
1068
|
+
error: c.error
|
|
1069
|
+
}))
|
|
1070
|
+
};
|
|
1071
|
+
const jsonStr = JSON.stringify(payload, null, 2);
|
|
1072
|
+
const outPath2 = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1073
|
+
if (outPath2) {
|
|
1074
|
+
try {
|
|
1075
|
+
writeFileAtomic(outPath2, jsonStr);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
return fail([`Failed to write '${outPath2}': ${errMessage(error)}`]);
|
|
1078
|
+
}
|
|
1079
|
+
if (json)
|
|
1080
|
+
return {
|
|
1081
|
+
stdout: jsonEnvelope(
|
|
1082
|
+
"export",
|
|
1083
|
+
true,
|
|
1084
|
+
{ path: outPath2, bytes: Buffer.byteLength(jsonStr) },
|
|
1085
|
+
[]
|
|
1086
|
+
),
|
|
1087
|
+
stderr: "",
|
|
1088
|
+
exitCode: 0
|
|
1089
|
+
};
|
|
1090
|
+
return {
|
|
1091
|
+
stdout: "",
|
|
1092
|
+
stderr: `Exported ${file} -> ${outPath2} (${Buffer.byteLength(jsonStr)} bytes)`,
|
|
1093
|
+
exitCode: 0
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
return { stdout: jsonStr, stderr: "", exitCode: 0 };
|
|
1097
|
+
}
|
|
1098
|
+
if (format === "pdf") {
|
|
1099
|
+
const outPath2 = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1100
|
+
if (!outPath2) return fail(["--format pdf requires an output path: -o <file.pdf>"]);
|
|
1101
|
+
try {
|
|
1102
|
+
await toPDF(buildRenderDoc(workbook, byId, "tikz"), outPath2, { parse: parse2 });
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
return fail([`PDF export failed: ${errMessage(error)}`]);
|
|
1105
|
+
}
|
|
1106
|
+
return json ? { stdout: jsonEnvelope("export", true, { path: outPath2 }, []), stderr: "", exitCode: 0 } : { stdout: "", stderr: `Exported ${file} -> ${outPath2}`, exitCode: 0 };
|
|
1107
|
+
}
|
|
1108
|
+
const fragment = args.includes("--fragment");
|
|
1109
|
+
const rendered = format === "tex" ? toTeX(buildRenderDoc(workbook, byId, "tikz"), { parse: parse2, fragment }) : toHTML(buildRenderDoc(workbook, byId), { parse: parse2 });
|
|
1110
|
+
const bytes = Buffer.byteLength(rendered, "utf-8");
|
|
1111
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1112
|
+
if (outPath) {
|
|
1113
|
+
try {
|
|
1114
|
+
writeFileAtomic(outPath, rendered);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
return fail([`Failed to write '${outPath}': ${errMessage(error)}`]);
|
|
1117
|
+
}
|
|
1118
|
+
if (json)
|
|
1119
|
+
return {
|
|
1120
|
+
stdout: jsonEnvelope("export", true, { path: outPath, bytes }, []),
|
|
1121
|
+
stderr: "",
|
|
1122
|
+
exitCode: 0
|
|
1123
|
+
};
|
|
1124
|
+
return { stdout: "", stderr: `Exported ${file} -> ${outPath} (${bytes} bytes)`, exitCode: 0 };
|
|
1125
|
+
}
|
|
1126
|
+
if (json) return { stdout: jsonEnvelope("export", true, { bytes }, []), stderr: "", exitCode: 0 };
|
|
1127
|
+
return { stdout: rendered, stderr: "", exitCode: 0 };
|
|
1128
|
+
}
|
|
1129
|
+
function runServer(input = process.stdin, output = process.stdout) {
|
|
1130
|
+
const session = new Session();
|
|
1131
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
1132
|
+
const write = (obj) => {
|
|
1133
|
+
output.write(`${JSON.stringify(obj)}
|
|
1134
|
+
`);
|
|
1135
|
+
};
|
|
1136
|
+
return new Promise((resolve) => {
|
|
1137
|
+
let done = false;
|
|
1138
|
+
const finish = () => {
|
|
1139
|
+
if (done) return;
|
|
1140
|
+
done = true;
|
|
1141
|
+
process.removeListener("SIGINT", finish);
|
|
1142
|
+
process.removeListener("SIGTERM", finish);
|
|
1143
|
+
rl.close();
|
|
1144
|
+
resolve();
|
|
1145
|
+
};
|
|
1146
|
+
const processLine = async (line2) => {
|
|
1147
|
+
if (done) return;
|
|
1148
|
+
const text = line2.trim();
|
|
1149
|
+
if (!text) return;
|
|
1150
|
+
let request;
|
|
1151
|
+
try {
|
|
1152
|
+
request = JSON.parse(text);
|
|
1153
|
+
} catch {
|
|
1154
|
+
write({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (Array.isArray(request)) {
|
|
1158
|
+
write({
|
|
1159
|
+
jsonrpc: "2.0",
|
|
1160
|
+
id: null,
|
|
1161
|
+
error: { code: -32600, message: "Batch requests are not supported" }
|
|
1162
|
+
});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1166
|
+
const { response, events, shutdown } = await handleRequest(
|
|
1167
|
+
session,
|
|
1168
|
+
request
|
|
1169
|
+
);
|
|
1170
|
+
for (const event of events) write(event);
|
|
1171
|
+
write(response);
|
|
1172
|
+
if (shutdown) finish();
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
write({
|
|
1175
|
+
jsonrpc: "2.0",
|
|
1176
|
+
id: null,
|
|
1177
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) }
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
let queue = Promise.resolve();
|
|
1182
|
+
rl.on("line", (line2) => {
|
|
1183
|
+
queue = queue.then(() => processLine(line2));
|
|
1184
|
+
});
|
|
1185
|
+
rl.on("close", () => {
|
|
1186
|
+
void queue.then(finish);
|
|
1187
|
+
});
|
|
1188
|
+
process.once("SIGINT", finish);
|
|
1189
|
+
process.once("SIGTERM", finish);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
async function serveCommand() {
|
|
1193
|
+
await runServer();
|
|
1194
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
1195
|
+
}
|
|
1196
|
+
function graphCommand(args) {
|
|
1197
|
+
if (args.includes("--json")) {
|
|
1198
|
+
return {
|
|
1199
|
+
stdout: jsonEnvelope("graph", false, null, [
|
|
1200
|
+
"graph has no --json output; use `describe --json` for structured graph data"
|
|
1201
|
+
]),
|
|
1202
|
+
stderr: "",
|
|
1203
|
+
exitCode: 1
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
const file = firstPositional(args);
|
|
1207
|
+
if (!file) {
|
|
1208
|
+
return { stdout: "", stderr: "Usage: mtsw graph <file> [-f mermaid|dot]", exitCode: 1 };
|
|
1209
|
+
}
|
|
1210
|
+
const read = readFile(file);
|
|
1211
|
+
if (read.error) return { stdout: "", stderr: read.error, exitCode: 1 };
|
|
1212
|
+
const parsed = parseWorkbook(read.content);
|
|
1213
|
+
if (!parsed.success) {
|
|
1214
|
+
return { stdout: "", stderr: `Parse errors:
|
|
1215
|
+
${bullets(parsed.errors ?? [])}`, exitCode: 1 };
|
|
85
1216
|
}
|
|
86
|
-
|
|
1217
|
+
const graph = buildDependencyGraph(parsed.workbook.cells);
|
|
1218
|
+
if (flagValue(args, "-f") === "mermaid" || flagValue(args, "--format") === "mermaid") {
|
|
1219
|
+
return { stdout: toMermaid(graph), stderr: "", exitCode: 0 };
|
|
1220
|
+
}
|
|
1221
|
+
if (flagValue(args, "-f") === "dot" || flagValue(args, "--format") === "dot") {
|
|
1222
|
+
return { stdout: toDOT(graph), stderr: "", exitCode: 0 };
|
|
1223
|
+
}
|
|
1224
|
+
const lines = [...graph.nodes].map(([id, node]) => `${id}: [${node.dependencies.join(", ")}]`);
|
|
1225
|
+
return { stdout: lines.join("\n"), stderr: "", exitCode: 0 };
|
|
87
1226
|
}
|
|
88
|
-
|
|
89
|
-
const file = args
|
|
1227
|
+
function stripCommand(args) {
|
|
1228
|
+
const file = firstPositional(args);
|
|
90
1229
|
if (!file) {
|
|
91
|
-
|
|
92
|
-
|
|
1230
|
+
return { stdout: "", stderr: "Usage: mtsw strip <file> [-w|--write]", exitCode: 1 };
|
|
1231
|
+
}
|
|
1232
|
+
const read = readFile(file);
|
|
1233
|
+
if (read.error) return { stdout: "", stderr: read.error, exitCode: 1 };
|
|
1234
|
+
const parsed = parseWorkbook(read.content);
|
|
1235
|
+
if (!parsed.success) {
|
|
1236
|
+
return { stdout: "", stderr: `Parse errors:
|
|
1237
|
+
${bullets(parsed.errors ?? [])}`, exitCode: 1 };
|
|
1238
|
+
}
|
|
1239
|
+
const yaml = serializeWorkbook(stripOutputs(parsed.workbook));
|
|
1240
|
+
if (args.includes("-w") || args.includes("--write")) {
|
|
1241
|
+
try {
|
|
1242
|
+
writeFileAtomic(file, yaml);
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
return { stdout: "", stderr: `Failed to write '${file}': ${errMessage(error)}`, exitCode: 1 };
|
|
1245
|
+
}
|
|
1246
|
+
return { stdout: "", stderr: `Stripped outputs -> ${file}`, exitCode: 0 };
|
|
1247
|
+
}
|
|
1248
|
+
return { stdout: yaml, stderr: "", exitCode: 0 };
|
|
1249
|
+
}
|
|
1250
|
+
var EMPTY_TEMPLATE = `version: "1.0"
|
|
1251
|
+
metadata:
|
|
1252
|
+
title: "<NAME>"
|
|
1253
|
+
runtime:
|
|
1254
|
+
engine: mathts
|
|
1255
|
+
execution: reactive
|
|
1256
|
+
cells: []
|
|
1257
|
+
`;
|
|
1258
|
+
var CHART_TEMPLATE = `version: "1.0"
|
|
1259
|
+
metadata:
|
|
1260
|
+
title: "<NAME>"
|
|
1261
|
+
runtime:
|
|
1262
|
+
engine: mathts
|
|
1263
|
+
execution: reactive
|
|
1264
|
+
cells:
|
|
1265
|
+
- data: "[1, 2, 3, 4]"
|
|
1266
|
+
id: xs
|
|
1267
|
+
|
|
1268
|
+
- data: "[1, 4, 9, 16]"
|
|
1269
|
+
id: ys
|
|
1270
|
+
|
|
1271
|
+
- visualization: |
|
|
1272
|
+
type: line
|
|
1273
|
+
title: "Sample chart"
|
|
1274
|
+
x: { label: "x", data: xs }
|
|
1275
|
+
y: { label: "y", data: ys }
|
|
1276
|
+
id: chart
|
|
1277
|
+
depends_on: [xs, ys]
|
|
1278
|
+
`;
|
|
1279
|
+
var TEMPLATES = {
|
|
1280
|
+
basic: {
|
|
1281
|
+
content: BASIC_TEMPLATE,
|
|
1282
|
+
description: "Markdown intro + a code cell + a passing test cell"
|
|
1283
|
+
},
|
|
1284
|
+
empty: { content: EMPTY_TEMPLATE, description: "A blank workbook (no cells)" },
|
|
1285
|
+
chart: {
|
|
1286
|
+
content: CHART_TEMPLATE,
|
|
1287
|
+
description: "A line chart over two data cells (visualization example)"
|
|
93
1288
|
}
|
|
94
|
-
|
|
1289
|
+
};
|
|
1290
|
+
var RESERVED_DEVICE_NAMES = /* @__PURE__ */ new Set([
|
|
1291
|
+
"CON",
|
|
1292
|
+
"PRN",
|
|
1293
|
+
"AUX",
|
|
1294
|
+
"NUL",
|
|
1295
|
+
...Array.from({ length: 9 }, (_unused, i) => `COM${i + 1}`),
|
|
1296
|
+
...Array.from({ length: 9 }, (_unused, i) => `LPT${i + 1}`)
|
|
1297
|
+
]);
|
|
1298
|
+
function newCommand(args) {
|
|
1299
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1300
|
+
const name = firstPositional(args);
|
|
1301
|
+
const templateName = args.includes("--empty") ? "empty" : flagValue(args, "-t") ?? flagValue(args, "--template") ?? "basic";
|
|
1302
|
+
const template = TEMPLATES[templateName];
|
|
1303
|
+
if (!template) {
|
|
1304
|
+
return {
|
|
1305
|
+
stdout: "",
|
|
1306
|
+
stderr: `Unknown template '${templateName}'. Available: ${Object.keys(TEMPLATES).join(", ")}`,
|
|
1307
|
+
exitCode: 1
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
let target;
|
|
1311
|
+
let bare;
|
|
1312
|
+
if (outPath) {
|
|
1313
|
+
target = outPath.endsWith(".mtsw") ? outPath : `${outPath}.mtsw`;
|
|
1314
|
+
bare = basename(target).slice(0, -".mtsw".length);
|
|
1315
|
+
} else {
|
|
1316
|
+
if (!name) {
|
|
1317
|
+
return {
|
|
1318
|
+
stdout: "",
|
|
1319
|
+
stderr: "Usage: mtsw new <name> [-t basic|empty|chart] [--empty] [-o <path>] [--force]",
|
|
1320
|
+
exitCode: 1
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name)) {
|
|
1324
|
+
return {
|
|
1325
|
+
stdout: "",
|
|
1326
|
+
stderr: `Invalid name '${name}': must be a bare filename \u2014 or use -o <path> for an explicit location`,
|
|
1327
|
+
exitCode: 1
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
bare = name.endsWith(".mtsw") ? name.slice(0, -".mtsw".length) : name;
|
|
1331
|
+
if (bare === "" || bare === "." || bare === ".." || RESERVED_DEVICE_NAMES.has(bare.toUpperCase())) {
|
|
1332
|
+
return { stdout: "", stderr: `Invalid name '${name}'`, exitCode: 1 };
|
|
1333
|
+
}
|
|
1334
|
+
target = `${bare}.mtsw`;
|
|
1335
|
+
}
|
|
1336
|
+
const content = template.content.replace(/<NAME>/g, bare);
|
|
1337
|
+
const force = args.includes("--force");
|
|
1338
|
+
try {
|
|
1339
|
+
if (force) {
|
|
1340
|
+
try {
|
|
1341
|
+
if (lstatSync(target).isSymbolicLink()) {
|
|
1342
|
+
return { stdout: "", stderr: `Refusing to overwrite symlink '${target}'`, exitCode: 1 };
|
|
1343
|
+
}
|
|
1344
|
+
} catch {
|
|
1345
|
+
}
|
|
1346
|
+
writeFileAtomic(target, content);
|
|
1347
|
+
} else {
|
|
1348
|
+
writeFileSync(target, content, { encoding: "utf-8", flag: "wx" });
|
|
1349
|
+
}
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
const e = error;
|
|
1352
|
+
if (e.code === "EEXIST") {
|
|
1353
|
+
return {
|
|
1354
|
+
stdout: "",
|
|
1355
|
+
stderr: `File '${target}' already exists (use --force to overwrite)`,
|
|
1356
|
+
exitCode: 1
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
stdout: "",
|
|
1361
|
+
stderr: `Failed to create '${target}': ${errMessage(error)}`,
|
|
1362
|
+
exitCode: 1
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return { stdout: `Created ${target}`, stderr: "", exitCode: 0 };
|
|
1366
|
+
}
|
|
1367
|
+
function importCommand(args) {
|
|
1368
|
+
const json = args.includes("--json");
|
|
1369
|
+
const fail = (problems) => json ? { stdout: jsonEnvelope("import", false, null, problems), stderr: "", exitCode: 1 } : { stdout: "", stderr: problems.join("\n"), exitCode: 1 };
|
|
1370
|
+
const file = firstPositional(args);
|
|
1371
|
+
let input;
|
|
1372
|
+
if (!file || file === "-") {
|
|
1373
|
+
if (process.stdin.isTTY) return fail(["Provide an input file, or pipe JSON/YAML to stdin"]);
|
|
1374
|
+
try {
|
|
1375
|
+
input = readFileSync(0, "utf-8");
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
return fail([`Failed to read stdin: ${errMessage(error)}`]);
|
|
1378
|
+
}
|
|
1379
|
+
} else {
|
|
1380
|
+
const read = readFile(file);
|
|
1381
|
+
if (read.error) return fail([read.error]);
|
|
1382
|
+
input = read.content;
|
|
1383
|
+
}
|
|
1384
|
+
const result = importWorkbook(input);
|
|
1385
|
+
if (!result.success || !result.workbook) return fail(result.errors ?? ["Import failed"]);
|
|
1386
|
+
const content = serializeWorkbook(result.workbook);
|
|
1387
|
+
const count = result.workbook.cells.length;
|
|
1388
|
+
const outPath = flagValue(args, "-o") ?? flagValue(args, "--output");
|
|
1389
|
+
if (outPath) {
|
|
1390
|
+
try {
|
|
1391
|
+
writeFileAtomic(outPath, content);
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
return fail([`Failed to write '${outPath}': ${errMessage(error)}`]);
|
|
1394
|
+
}
|
|
1395
|
+
if (json)
|
|
1396
|
+
return {
|
|
1397
|
+
stdout: jsonEnvelope("import", true, { path: outPath, cells: count }, []),
|
|
1398
|
+
stderr: "",
|
|
1399
|
+
exitCode: 0
|
|
1400
|
+
};
|
|
1401
|
+
return { stdout: "", stderr: `Imported ${count} cell(s) -> ${outPath}`, exitCode: 0 };
|
|
1402
|
+
}
|
|
1403
|
+
if (json)
|
|
1404
|
+
return {
|
|
1405
|
+
stdout: jsonEnvelope("import", true, { content, cells: count }, []),
|
|
1406
|
+
stderr: "",
|
|
1407
|
+
exitCode: 0
|
|
1408
|
+
};
|
|
1409
|
+
return { stdout: content, stderr: "", exitCode: 0 };
|
|
1410
|
+
}
|
|
1411
|
+
function positionals(args) {
|
|
1412
|
+
const out = [];
|
|
1413
|
+
for (let i = 0; i < args.length; i++) {
|
|
1414
|
+
const a = args[i];
|
|
1415
|
+
if (VALUE_FLAGS.has(a)) {
|
|
1416
|
+
i++;
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (!a.startsWith("-")) out.push(a);
|
|
1420
|
+
}
|
|
1421
|
+
return out;
|
|
1422
|
+
}
|
|
1423
|
+
function parseDependsOn(args) {
|
|
1424
|
+
const value = flagValue(args, "--depends-on");
|
|
1425
|
+
if (value === void 0) return void 0;
|
|
1426
|
+
return [
|
|
1427
|
+
...new Set(
|
|
1428
|
+
value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
1429
|
+
)
|
|
1430
|
+
];
|
|
95
1431
|
}
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
1432
|
+
function parsePosition(args) {
|
|
1433
|
+
const before = flagValue(args, "--before");
|
|
1434
|
+
if (before !== void 0) return { before };
|
|
1435
|
+
const after = flagValue(args, "--after");
|
|
1436
|
+
if (after !== void 0) return { after };
|
|
1437
|
+
const at = flagValue(args, "--at");
|
|
1438
|
+
if (at !== void 0) {
|
|
1439
|
+
const n = Number(at);
|
|
1440
|
+
if (!Number.isInteger(n)) throw new Error(`--at must be an integer (got '${at}')`);
|
|
1441
|
+
return { at: n };
|
|
101
1442
|
}
|
|
102
|
-
|
|
1443
|
+
return void 0;
|
|
1444
|
+
}
|
|
1445
|
+
function resolveContent(args) {
|
|
1446
|
+
const inline2 = flagValue(args, "--content");
|
|
1447
|
+
const fromFile = flagValue(args, "--content-file");
|
|
1448
|
+
if (inline2 !== void 0 && fromFile !== void 0) {
|
|
1449
|
+
return { error: "Use only one of --content / --content-file" };
|
|
1450
|
+
}
|
|
1451
|
+
if (inline2 !== void 0) return { content: inline2 };
|
|
1452
|
+
if (fromFile !== void 0) {
|
|
1453
|
+
if (fromFile === "-") {
|
|
1454
|
+
if (process.stdin.isTTY) {
|
|
1455
|
+
return {
|
|
1456
|
+
error: "No piped input for --content-file - (pipe content via stdin, or use --content)"
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
return { content: readFileSync(0, "utf-8") };
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
return { error: `Cannot read stdin: ${errMessage(error)}` };
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const r = readFile(fromFile);
|
|
1466
|
+
return r.error ? { error: r.error } : { content: r.content };
|
|
1467
|
+
}
|
|
1468
|
+
return {};
|
|
1469
|
+
}
|
|
1470
|
+
var CELL_VERBS = ["add", "edit", "rm", "move", "rename"];
|
|
1471
|
+
function cellCommand(args) {
|
|
1472
|
+
const json = args.includes("--json");
|
|
1473
|
+
const fail = (problems) => json ? { stdout: jsonEnvelope("cell", false, null, problems), stderr: "", exitCode: 1 } : { stdout: "", stderr: problems.join("\n"), exitCode: 1 };
|
|
1474
|
+
const verb = args[0];
|
|
1475
|
+
if (!verb || !CELL_VERBS.includes(verb)) {
|
|
1476
|
+
return fail([`Usage: mtsw cell <${CELL_VERBS.join("|")}> <file> ...`]);
|
|
1477
|
+
}
|
|
1478
|
+
const vargs = args.slice(1);
|
|
1479
|
+
const pos = positionals(vargs);
|
|
1480
|
+
const file = pos[0];
|
|
1481
|
+
if (!file) return fail([`Usage: mtsw cell ${verb} <file> ...`]);
|
|
1482
|
+
const read = readFile(file);
|
|
1483
|
+
if (read.error) return fail([read.error]);
|
|
1484
|
+
const parsed = parseWorkbook(read.content);
|
|
1485
|
+
if (!parsed.success) return fail(parsed.errors ?? ["Parse error"]);
|
|
1486
|
+
const wb = parsed.workbook;
|
|
1487
|
+
let result;
|
|
1488
|
+
let changedCells = [];
|
|
1489
|
+
try {
|
|
1490
|
+
switch (verb) {
|
|
1491
|
+
case "add": {
|
|
1492
|
+
const id = flagValue(vargs, "--id");
|
|
1493
|
+
const type = flagValue(vargs, "--type") ?? flagValue(vargs, "-t");
|
|
1494
|
+
if (!id) return fail(["cell add requires --id"]);
|
|
1495
|
+
if (!type) return fail(["cell add requires --type"]);
|
|
1496
|
+
const content = resolveContent(vargs);
|
|
1497
|
+
if (content.error) return fail([content.error]);
|
|
1498
|
+
result = addCell(
|
|
1499
|
+
wb,
|
|
1500
|
+
{
|
|
1501
|
+
id,
|
|
1502
|
+
type,
|
|
1503
|
+
content: content.content,
|
|
1504
|
+
dependsOn: parseDependsOn(vargs)
|
|
1505
|
+
},
|
|
1506
|
+
parsePosition(vargs)
|
|
1507
|
+
);
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
case "edit": {
|
|
1511
|
+
const id = pos[1];
|
|
1512
|
+
if (!id) return fail(["cell edit requires <id>"]);
|
|
1513
|
+
const content = resolveContent(vargs);
|
|
1514
|
+
if (content.error) return fail([content.error]);
|
|
1515
|
+
const type = flagValue(vargs, "--type") ?? flagValue(vargs, "-t");
|
|
1516
|
+
const deps = parseDependsOn(vargs);
|
|
1517
|
+
const changes = {};
|
|
1518
|
+
if (content.content !== void 0) changes.content = content.content;
|
|
1519
|
+
if (type !== void 0) changes.type = type;
|
|
1520
|
+
if (deps !== void 0) changes.dependsOn = deps;
|
|
1521
|
+
result = editCell(wb, id, changes);
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1524
|
+
case "rm": {
|
|
1525
|
+
const id = pos[1];
|
|
1526
|
+
if (!id) return fail(["cell rm requires <id>"]);
|
|
1527
|
+
const removed = removeCell(wb, id, { force: vargs.includes("--force") });
|
|
1528
|
+
result = removed.workbook;
|
|
1529
|
+
changedCells = removed.changedCells;
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
case "move": {
|
|
1533
|
+
const id = pos[1];
|
|
1534
|
+
if (!id) return fail(["cell move requires <id>"]);
|
|
1535
|
+
const position = parsePosition(vargs);
|
|
1536
|
+
if (!position) return fail(["cell move requires --before/--after/--at"]);
|
|
1537
|
+
result = moveCell(wb, id, position);
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
case "rename": {
|
|
1541
|
+
const oldId = pos[1];
|
|
1542
|
+
const newId = pos[2];
|
|
1543
|
+
if (!oldId || !newId) return fail(["cell rename requires <oldId> <newId>"]);
|
|
1544
|
+
result = renameCell(wb, oldId, newId);
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
default:
|
|
1548
|
+
return fail([`Unknown cell verb: ${verb}`]);
|
|
1549
|
+
}
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
return fail([errMessage(error)]);
|
|
1552
|
+
}
|
|
1553
|
+
const after = serializeWorkbook(result);
|
|
1554
|
+
const changed = after !== serializeWorkbook(wb);
|
|
1555
|
+
if (vargs.includes("--dry-run")) {
|
|
1556
|
+
if (json) {
|
|
1557
|
+
const data = {
|
|
1558
|
+
dryRun: true,
|
|
1559
|
+
changed,
|
|
1560
|
+
content: after,
|
|
1561
|
+
...changedCells.length ? { changedCells } : {}
|
|
1562
|
+
};
|
|
1563
|
+
return { stdout: jsonEnvelope("cell", true, data, []), stderr: "", exitCode: 0 };
|
|
1564
|
+
}
|
|
1565
|
+
return { stdout: after, stderr: "", exitCode: 0 };
|
|
1566
|
+
}
|
|
1567
|
+
if (changed) {
|
|
1568
|
+
try {
|
|
1569
|
+
writeFileAtomic(file, after);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
return fail([`Failed to write '${file}': ${errMessage(error)}`]);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
if (json) {
|
|
1575
|
+
const data = {
|
|
1576
|
+
changed,
|
|
1577
|
+
...changedCells.length ? { changedCells } : {},
|
|
1578
|
+
...describeData(result)
|
|
1579
|
+
};
|
|
1580
|
+
return { stdout: jsonEnvelope("cell", true, data, []), stderr: "", exitCode: 0 };
|
|
1581
|
+
}
|
|
1582
|
+
const note = changed ? `Updated ${file}` : `No change (${file})`;
|
|
1583
|
+
const detached = changedCells.length ? `
|
|
1584
|
+
Detached from: ${changedCells.join(", ")}` : "";
|
|
1585
|
+
return { stdout: "", stderr: note + detached, exitCode: 0 };
|
|
1586
|
+
}
|
|
1587
|
+
async function dispatch(argv) {
|
|
1588
|
+
if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
|
|
1589
|
+
return { stdout: HELP, stderr: "", exitCode: 0 };
|
|
1590
|
+
}
|
|
1591
|
+
if (argv.includes("-V") || argv.includes("--version")) {
|
|
1592
|
+
return { stdout: `mtsw version ${VERSION}`, stderr: "", exitCode: 0 };
|
|
1593
|
+
}
|
|
1594
|
+
const [command, ...rest] = argv;
|
|
1595
|
+
try {
|
|
1596
|
+
switch (command) {
|
|
1597
|
+
case "run":
|
|
1598
|
+
return await runCommand(rest);
|
|
1599
|
+
case "validate":
|
|
1600
|
+
return validateCommand(rest);
|
|
1601
|
+
case "graph":
|
|
1602
|
+
return graphCommand(rest);
|
|
1603
|
+
case "describe":
|
|
1604
|
+
return describeCommand(rest);
|
|
1605
|
+
case "strip":
|
|
1606
|
+
return stripCommand(rest);
|
|
1607
|
+
case "new":
|
|
1608
|
+
return newCommand(rest);
|
|
1609
|
+
case "capabilities":
|
|
1610
|
+
return capabilitiesCommand(rest);
|
|
1611
|
+
case "templates":
|
|
1612
|
+
return templatesCommand(rest);
|
|
1613
|
+
case "cell":
|
|
1614
|
+
return cellCommand(rest);
|
|
1615
|
+
case "functions":
|
|
1616
|
+
return functionsCommand(rest);
|
|
1617
|
+
case "meta":
|
|
1618
|
+
return metaCommand(rest);
|
|
1619
|
+
case "import":
|
|
1620
|
+
return importCommand(rest);
|
|
1621
|
+
case "export":
|
|
1622
|
+
return exportCommand(rest);
|
|
1623
|
+
case "serve":
|
|
1624
|
+
return serveCommand();
|
|
1625
|
+
default:
|
|
1626
|
+
return { stdout: "", stderr: `Unknown command: ${command}
|
|
1627
|
+
${HELP}`, exitCode: 1 };
|
|
1628
|
+
}
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1631
|
+
if (rest.includes("--json")) {
|
|
1632
|
+
return {
|
|
1633
|
+
stdout: jsonEnvelope(command, false, null, [`Internal error: ${message}`]),
|
|
1634
|
+
stderr: "",
|
|
1635
|
+
exitCode: 1
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
return { stdout: "", stderr: `Internal error: ${message}`, exitCode: 1 };
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
async function main() {
|
|
1642
|
+
const result = await dispatch(process.argv.slice(2));
|
|
1643
|
+
if (result.stdout) process.stdout.write(`${result.stdout}
|
|
1644
|
+
`);
|
|
1645
|
+
if (result.stderr) process.stderr.write(`${result.stderr}
|
|
1646
|
+
`);
|
|
1647
|
+
process.exit(result.exitCode);
|
|
1648
|
+
}
|
|
1649
|
+
function isEntryPoint() {
|
|
1650
|
+
const entry = process.argv[1];
|
|
1651
|
+
if (!entry) return false;
|
|
1652
|
+
try {
|
|
1653
|
+
return import.meta.url === pathToFileURL(realpathSync(entry)).href;
|
|
1654
|
+
} catch {
|
|
1655
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (isEntryPoint()) {
|
|
1659
|
+
void main();
|
|
103
1660
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
1661
|
+
export {
|
|
1662
|
+
capabilitiesCommand,
|
|
1663
|
+
cellCommand,
|
|
1664
|
+
describeCommand,
|
|
1665
|
+
dispatch,
|
|
1666
|
+
exportCommand,
|
|
1667
|
+
functionsCommand,
|
|
1668
|
+
graphCommand,
|
|
1669
|
+
importCommand,
|
|
1670
|
+
metaCommand,
|
|
1671
|
+
newCommand,
|
|
1672
|
+
runCommand,
|
|
1673
|
+
runServer,
|
|
1674
|
+
serveCommand,
|
|
1675
|
+
stripCommand,
|
|
1676
|
+
templatesCommand,
|
|
1677
|
+
validateCommand
|
|
1678
|
+
};
|