@danielsimonjr/mathts-workbook 0.4.10 → 0.4.11

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