mosage 0.2.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +16 -15
  2. package/bin.js +2 -0
  3. package/dist/build-C7NW_3Pk.js +14 -0
  4. package/dist/check-CP4873Wx.js +41 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +228 -0
  7. package/dist/config-DPm1BBAb.js +2619 -0
  8. package/dist/config-TlTe7Ona.d.ts +24 -0
  9. package/dist/context-BqsdSrAQ.js +1084 -0
  10. package/dist/dev-Biz42qlu.js +17 -0
  11. package/dist/diagram-xlVDekYk.js +763 -0
  12. package/dist/export-Bi6nuxjT.js +31 -0
  13. package/dist/import-D2jNB07F.js +25 -0
  14. package/dist/index.d.ts +455 -0
  15. package/dist/index.js +693 -0
  16. package/dist/{cli.js → init-Bbtj2pxF.js} +34 -68
  17. package/dist/preview-CLm51aRt.js +19 -0
  18. package/dist/sdk-DjpX6mCv.js +51 -0
  19. package/dist/vite/index.d.ts +25 -0
  20. package/dist/vite/index.js +2 -0
  21. package/env.d.ts +83 -0
  22. package/package.json +59 -12
  23. package/{template/.agents/skills → skills}/create-doc/SKILL.md +1 -1
  24. package/{template/.agents/skills → skills}/create-theme/SKILL.md +1 -1
  25. package/{template/.agents/skills → skills}/doc-authoring/SKILL.md +8 -9
  26. package/{template/.agents/skills → skills}/doc-authoring/references/assets.md +1 -1
  27. package/{template/.agents/skills → skills}/doc-authoring/references/design-system.md +1 -1
  28. package/{template/.agents/skills → skills}/doc-authoring/references/long-form.md +4 -4
  29. package/{template/.agents/skills → skills}/doc-authoring/references/pagination.md +1 -1
  30. package/src/app/app.tsx +42 -0
  31. package/src/app/components/data-table.tsx +196 -0
  32. package/src/app/components/design-panel/design-panel.tsx +318 -0
  33. package/src/app/components/design-panel/design-provider.tsx +121 -0
  34. package/src/app/components/design-panel/use-design.ts +85 -0
  35. package/src/app/components/diagram.tsx +76 -0
  36. package/src/app/components/doc-assets.tsx +129 -0
  37. package/src/app/components/doc-search.tsx +248 -0
  38. package/src/app/components/doc-sidebar.tsx +162 -0
  39. package/src/app/components/flow-page.tsx +93 -0
  40. package/src/app/components/footnote.tsx +204 -0
  41. package/src/app/components/image-placeholder.tsx +50 -0
  42. package/src/app/components/inspector/inspector.tsx +518 -0
  43. package/src/app/components/numbering.tsx +224 -0
  44. package/src/app/components/page-frame.tsx +70 -0
  45. package/src/app/components/sidebar/folder-item.tsx +212 -0
  46. package/src/app/components/sidebar/icon-picker.tsx +99 -0
  47. package/src/app/components/sidebar/sidebar.tsx +252 -0
  48. package/src/app/components/table-of-contents.tsx +93 -0
  49. package/src/app/components/theme-toggle.tsx +50 -0
  50. package/src/app/components/themes/markdown.tsx +249 -0
  51. package/src/app/components/themes/theme-preview.tsx +74 -0
  52. package/src/app/components/ui/menu.tsx +143 -0
  53. package/src/app/index.html +12 -0
  54. package/src/app/lib/agent-bridge.ts +140 -0
  55. package/src/app/lib/assets.ts +151 -0
  56. package/src/app/lib/design-presets.ts +109 -0
  57. package/src/app/lib/design.ts +88 -0
  58. package/src/app/lib/diagnostics.ts +282 -0
  59. package/src/app/lib/doc-preview.tsx +29 -0
  60. package/src/app/lib/docs.ts +26 -0
  61. package/src/app/lib/docx/extract.ts +1623 -0
  62. package/src/app/lib/docx/fonts.test.ts +136 -0
  63. package/src/app/lib/docx/fonts.ts +166 -0
  64. package/src/app/lib/docx/media.ts +102 -0
  65. package/src/app/lib/docx/model.ts +206 -0
  66. package/src/app/lib/docx/paragraph.test.ts +92 -0
  67. package/src/app/lib/docx/paragraph.ts +107 -0
  68. package/src/app/lib/docx/props.ts +187 -0
  69. package/src/app/lib/docx/styles.ts +306 -0
  70. package/src/app/lib/docx/units.ts +35 -0
  71. package/src/app/lib/docx/write.test.ts +507 -0
  72. package/src/app/lib/docx/write.ts +581 -0
  73. package/src/app/lib/docx/xml.ts +39 -0
  74. package/src/app/lib/export-docx.ts +289 -0
  75. package/src/app/lib/export-dom.ts +318 -0
  76. package/src/app/lib/export-html.ts +156 -0
  77. package/src/app/lib/export-image.ts +70 -0
  78. package/src/app/lib/export-pdf.ts +165 -0
  79. package/src/app/lib/flow-measure.test.ts +31 -0
  80. package/src/app/lib/flow-measure.ts +183 -0
  81. package/src/app/lib/flow.test.ts +110 -0
  82. package/src/app/lib/flow.ts +136 -0
  83. package/src/app/lib/folders.ts +192 -0
  84. package/src/app/lib/footnotes.test.tsx +102 -0
  85. package/src/app/lib/footnotes.ts +94 -0
  86. package/src/app/lib/inspector/fiber.ts +99 -0
  87. package/src/app/lib/labels.test.ts +18 -0
  88. package/src/app/lib/labels.ts +181 -0
  89. package/src/app/lib/outline.ts +118 -0
  90. package/src/app/lib/page-context.tsx +43 -0
  91. package/src/app/lib/page-range.test.ts +95 -0
  92. package/src/app/lib/page-range.ts +90 -0
  93. package/src/app/lib/print-ready.ts +69 -0
  94. package/src/app/lib/rasterize.ts +173 -0
  95. package/src/app/lib/scan.ts +26 -0
  96. package/src/app/lib/sdk.test.ts +32 -0
  97. package/src/app/lib/sdk.ts +115 -0
  98. package/src/app/lib/themes.ts +31 -0
  99. package/src/app/lib/use-doc-module.ts +53 -0
  100. package/src/app/lib/use-doc-pages.ts +147 -0
  101. package/src/app/lib/utils.ts +6 -0
  102. package/src/app/lib/view-mode.test.ts +91 -0
  103. package/src/app/lib/view-mode.ts +104 -0
  104. package/src/app/main.tsx +14 -0
  105. package/src/app/routes/assets.tsx +257 -0
  106. package/src/app/routes/doc.tsx +877 -0
  107. package/src/app/routes/home-shell.tsx +203 -0
  108. package/src/app/routes/home.tsx +269 -0
  109. package/src/app/routes/themes.tsx +121 -0
  110. package/src/app/styles.css +97 -0
  111. package/src/app/virtual.d.ts +30 -0
  112. package/template/AGENTS.md +1 -1
  113. package/template/README.md +24 -52
  114. package/template/docs/getting-started/index.tsx +2 -2
  115. package/template/mosage.config.ts +1 -1
  116. package/template/package.json +1 -1
  117. package/template/tsconfig.json +1 -1
  118. package/README.zh-TW.md +0 -28
  119. /package/{template/.agents/skills → skills}/apply-comments/SKILL.md +0 -0
  120. /package/{template/.agents/skills → skills}/current-doc/SKILL.md +0 -0
  121. /package/{template/.agents/skills → skills}/doc-authoring/references/tables-and-charts.md +0 -0
@@ -0,0 +1,1084 @@
1
+ import { a as PAGE_SIZE_NAMES, o as isOrientation, r as ORIENTATIONS, s as isPageSizeName } from "./sdk-DjpX6mCv.js";
2
+ import { r as readCoreVersion } from "./cli/bin.js";
3
+ import { a as resolveEntry, c as DOC_ID_RE, i as listDocIds, l as loadUserConfig, n as OpsError, o as makeContext, r as createDocument, s as validateAssetName, t as createViteConfig } from "./config-DPm1BBAb.js";
4
+ import path from "node:path";
5
+ import { existsSync } from "node:fs";
6
+ import fs from "node:fs/promises";
7
+ import "fast-glob";
8
+ import { createServer, mergeConfig } from "vite";
9
+ import { Buffer } from "node:buffer";
10
+ //#region src/import/markdown.ts
11
+ const FENCE_RE = /^(```|~~~)\s*([^\s`]*)\s*$/;
12
+ const ATX_RE = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
13
+ const HR_RE = /^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/;
14
+ const QUOTE_RE = /^ {0,3}> ?(.*)$/;
15
+ const UL_RE = /^(\s*)([-*+])\s+(.*)$/;
16
+ const OL_RE = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
17
+ const SETEXT_RE = /^ {0,3}(=+|-+)\s*$/;
18
+ const TABLE_DELIM_RE = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;
19
+ function splitLines(source) {
20
+ return source.replace(/^/, "").replace(/\r\n?/g, "\n").split("\n");
21
+ }
22
+ function parseFrontmatter(lines) {
23
+ if (lines[0]?.trim() !== "---") return {
24
+ frontmatter: {},
25
+ rest: lines
26
+ };
27
+ const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
28
+ if (end === -1) return {
29
+ frontmatter: {},
30
+ rest: lines
31
+ };
32
+ const frontmatter = {};
33
+ for (const line of lines.slice(1, end)) {
34
+ const match = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
35
+ if (!match) continue;
36
+ frontmatter[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, "");
37
+ }
38
+ return {
39
+ frontmatter,
40
+ rest: lines.slice(end + 1)
41
+ };
42
+ }
43
+ function splitRow(line) {
44
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
45
+ const cells = [];
46
+ let current = "";
47
+ for (let i = 0; i < trimmed.length; i++) {
48
+ const char = trimmed[i];
49
+ if (char === "\\" && trimmed[i + 1] === "|") {
50
+ current += "|";
51
+ i++;
52
+ continue;
53
+ }
54
+ if (char === "|") {
55
+ cells.push(current.trim());
56
+ current = "";
57
+ continue;
58
+ }
59
+ current += char;
60
+ }
61
+ cells.push(current.trim());
62
+ return cells;
63
+ }
64
+ function parseAlign(line) {
65
+ return splitRow(line).map((cell) => {
66
+ const left = cell.startsWith(":");
67
+ const right = cell.endsWith(":");
68
+ if (left && right) return "center";
69
+ if (right) return "right";
70
+ if (left) return "left";
71
+ return null;
72
+ });
73
+ }
74
+ function indentOf(line) {
75
+ return line.length - line.trimStart().length;
76
+ }
77
+ function isBlockStart(line) {
78
+ return ATX_RE.test(line) || HR_RE.test(line) || QUOTE_RE.test(line) || UL_RE.test(line) || OL_RE.test(line) || FENCE_RE.test(line);
79
+ }
80
+ function parseMarkdown(source) {
81
+ const { frontmatter, rest } = parseFrontmatter(splitLines(source));
82
+ return {
83
+ frontmatter,
84
+ blocks: parseBlocks(rest)
85
+ };
86
+ }
87
+ function parseBlocks(lines) {
88
+ const blocks = [];
89
+ let i = 0;
90
+ while (i < lines.length) {
91
+ const line = lines[i];
92
+ if (line.trim() === "") {
93
+ i++;
94
+ continue;
95
+ }
96
+ const fence = line.match(FENCE_RE);
97
+ if (fence) {
98
+ const marker = fence[1];
99
+ const value = [];
100
+ i++;
101
+ while (i < lines.length && !lines[i].trimStart().startsWith(marker)) {
102
+ value.push(lines[i]);
103
+ i++;
104
+ }
105
+ i++;
106
+ blocks.push({
107
+ type: "code",
108
+ lang: fence[2] || null,
109
+ value: value.join("\n")
110
+ });
111
+ continue;
112
+ }
113
+ const atx = line.match(ATX_RE);
114
+ if (atx) {
115
+ blocks.push({
116
+ type: "heading",
117
+ level: atx[1].length,
118
+ children: parseInline(atx[2])
119
+ });
120
+ i++;
121
+ continue;
122
+ }
123
+ if (HR_RE.test(line)) {
124
+ blocks.push({ type: "hr" });
125
+ i++;
126
+ continue;
127
+ }
128
+ if (QUOTE_RE.test(line)) {
129
+ const inner = [];
130
+ while (i < lines.length && (QUOTE_RE.test(lines[i]) || lines[i].trim() !== "")) {
131
+ const match = lines[i].match(QUOTE_RE);
132
+ if (!match && lines[i].trim() === "") break;
133
+ inner.push(match ? match[1] : lines[i]);
134
+ i++;
135
+ }
136
+ blocks.push({
137
+ type: "quote",
138
+ children: parseBlocks(inner)
139
+ });
140
+ continue;
141
+ }
142
+ if (UL_RE.test(line) || OL_RE.test(line)) {
143
+ const { block, next } = parseList(lines, i);
144
+ blocks.push(block);
145
+ i = next;
146
+ continue;
147
+ }
148
+ if (line.includes("|") && lines[i + 1] !== void 0 && TABLE_DELIM_RE.test(lines[i + 1])) {
149
+ const head = splitRow(line).map(parseInline);
150
+ const align = parseAlign(lines[i + 1]);
151
+ const rows = [];
152
+ i += 2;
153
+ while (i < lines.length && lines[i].includes("|") && lines[i].trim() !== "") {
154
+ rows.push(splitRow(lines[i]).map(parseInline));
155
+ i++;
156
+ }
157
+ blocks.push({
158
+ type: "table",
159
+ head,
160
+ rows,
161
+ align
162
+ });
163
+ continue;
164
+ }
165
+ const paragraph = [];
166
+ while (i < lines.length && lines[i].trim() !== "") {
167
+ const next = lines[i + 1];
168
+ if (next !== void 0 && SETEXT_RE.test(next)) {
169
+ paragraph.push(lines[i]);
170
+ i += 2;
171
+ blocks.push({
172
+ type: "heading",
173
+ level: next.trim().startsWith("=") ? 1 : 2,
174
+ children: parseInline(paragraph.join(" "))
175
+ });
176
+ paragraph.length = 0;
177
+ break;
178
+ }
179
+ if (paragraph.length > 0 && isBlockStart(lines[i])) break;
180
+ paragraph.push(lines[i]);
181
+ i++;
182
+ }
183
+ if (paragraph.length > 0) {
184
+ const text = paragraph.join("\n");
185
+ const figure = asStandaloneImage(text);
186
+ blocks.push(figure ?? {
187
+ type: "paragraph",
188
+ children: parseInline(text)
189
+ });
190
+ }
191
+ }
192
+ return blocks;
193
+ }
194
+ /** An image alone in its paragraph is a figure, not a run of inline content. */
195
+ function asStandaloneImage(text) {
196
+ const match = text.trim().match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
197
+ if (!match) return null;
198
+ return {
199
+ type: "figure",
200
+ alt: match[1],
201
+ src: match[2]
202
+ };
203
+ }
204
+ function parseList(lines, start) {
205
+ const first = lines[start];
206
+ const ordered = OL_RE.test(first);
207
+ const startNumber = ordered ? Number(first.match(OL_RE)?.[2] ?? 1) : 1;
208
+ const baseIndent = indentOf(first);
209
+ const items = [];
210
+ let i = start;
211
+ let current = null;
212
+ const flush = () => {
213
+ if (current) items.push(parseBlocks(current));
214
+ current = null;
215
+ };
216
+ while (i < lines.length) {
217
+ const line = lines[i];
218
+ if (line.trim() === "") {
219
+ const next = lines[i + 1];
220
+ if (next === void 0 || next.trim() !== "" && indentOf(next) <= baseIndent && !isItem(next)) break;
221
+ current?.push("");
222
+ i++;
223
+ continue;
224
+ }
225
+ const match = line.match(ordered ? OL_RE : UL_RE) ?? line.match(ordered ? UL_RE : OL_RE);
226
+ if (match && indentOf(line) <= baseIndent) {
227
+ flush();
228
+ current = [match[3]];
229
+ i++;
230
+ continue;
231
+ }
232
+ if (indentOf(line) > baseIndent && current) {
233
+ current.push(line.slice(Math.min(indentOf(line), baseIndent + 2)));
234
+ i++;
235
+ continue;
236
+ }
237
+ break;
238
+ }
239
+ flush();
240
+ return {
241
+ block: {
242
+ type: "list",
243
+ ordered,
244
+ start: startNumber,
245
+ items
246
+ },
247
+ next: i
248
+ };
249
+ function isItem(line) {
250
+ return UL_RE.test(line) || OL_RE.test(line);
251
+ }
252
+ }
253
+ const INLINE_RE = /(!\[[^\]]*\]\([^)\s]+(?:\s+"[^"]*")?\))|(\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\))|(`+)([^`]*?)\3|(\*\*|__)(?=\S)([\s\S]*?\S)\5|(\*|_)(?=\S)([\s\S]*?\S)\7|(<[^>\s]+@[^>\s]+>)|(https?:\/\/[^\s<>"')\]]+)/;
254
+ function parseInline(source) {
255
+ const out = [];
256
+ let rest = maskEscapes(source);
257
+ const pushText = (value) => {
258
+ if (value === "") return;
259
+ value.split(/(?: {2}|\\)\n|\n/).forEach((part, index) => {
260
+ if (index > 0) out.push({ type: "break" });
261
+ const text = unmask(part);
262
+ if (text !== "") out.push({
263
+ type: "text",
264
+ value: text
265
+ });
266
+ });
267
+ };
268
+ while (rest.length > 0) {
269
+ const match = rest.match(INLINE_RE);
270
+ if (!match || match.index === void 0) break;
271
+ pushText(rest.slice(0, match.index));
272
+ const token = match[0];
273
+ if (match[1]) {
274
+ const image = token.match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
275
+ if (image) out.push({
276
+ type: "image",
277
+ alt: unmask(image[1]),
278
+ src: unmask(image[2])
279
+ });
280
+ } else if (match[2]) {
281
+ const link = token.match(/^\[([^\]]*)\]\(([^)\s]*)(?:\s+"[^"]*")?\)$/);
282
+ if (link) out.push({
283
+ type: "link",
284
+ href: unmask(link[2]),
285
+ children: parseInline(link[1])
286
+ });
287
+ } else if (match[3]) out.push({
288
+ type: "code",
289
+ value: unmask(match[4].trim())
290
+ });
291
+ else if (match[5]) out.push({
292
+ type: "strong",
293
+ children: parseInline(match[6])
294
+ });
295
+ else if (match[7]) out.push({
296
+ type: "em",
297
+ children: parseInline(match[8])
298
+ });
299
+ else if (match[9]) {
300
+ const address = token.slice(1, -1);
301
+ out.push({
302
+ type: "link",
303
+ href: `mailto:${address}`,
304
+ children: [{
305
+ type: "text",
306
+ value: address
307
+ }]
308
+ });
309
+ } else if (match[10]) {
310
+ const url = unmask(token);
311
+ out.push({
312
+ type: "link",
313
+ href: url,
314
+ children: [{
315
+ type: "text",
316
+ value: url
317
+ }]
318
+ });
319
+ }
320
+ rest = rest.slice(match.index + token.length);
321
+ }
322
+ pushText(rest);
323
+ return out;
324
+ }
325
+ const ESCAPE_RE = /\\([\\`*_{}[\]()#+\-.!|>~])/g;
326
+ const MASK_RE = /\u0000(\d+)\u0000/g;
327
+ /**
328
+ * The inline scanner is a regex over delimiters, which cannot see a preceding
329
+ * backslash. Escaped characters are swapped for a sentinel before scanning and
330
+ * restored on the way out, so `\*not emphasis\*` survives as literal text.
331
+ */
332
+ function maskEscapes(value) {
333
+ return value.replace(ESCAPE_RE, (_, char) => `\u0000${char.charCodeAt(0)}\u0000`);
334
+ }
335
+ function unmask(value) {
336
+ return value.replace(MASK_RE, (_, code) => String.fromCharCode(Number(code)));
337
+ }
338
+ /** Flattens inline content to plain text — for titles, alt text, and outline labels. */
339
+ function inlineText(nodes) {
340
+ return nodes.map((node) => {
341
+ switch (node.type) {
342
+ case "text":
343
+ case "code": return node.value;
344
+ case "strong":
345
+ case "em":
346
+ case "link": return inlineText(node.children);
347
+ case "image": return node.alt;
348
+ case "break": return " ";
349
+ default: return "";
350
+ }
351
+ }).join("").replace(/\s+/g, " ").trim();
352
+ }
353
+ //#endregion
354
+ //#region src/import/to-tsx.ts
355
+ /** JSX text is written literally; only these characters have to be escaped. */
356
+ function jsxText(text) {
357
+ return text.replace(/[{}<>]/g, (char) => `{'${char}'}`);
358
+ }
359
+ function jsString(value) {
360
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n")}'`;
361
+ }
362
+ function attr(value) {
363
+ return `"${value.replace(/"/g, "&quot;")}"`;
364
+ }
365
+ function collectImageSources(blocks) {
366
+ const out = [];
367
+ const walkInline = (nodes) => {
368
+ for (const node of nodes) if (node.type === "image") out.push(node.src);
369
+ else if (node.type === "strong" || node.type === "em" || node.type === "link") walkInline(node.children);
370
+ };
371
+ const walk = (list) => {
372
+ for (const block of list) switch (block.type) {
373
+ case "figure":
374
+ out.push(block.src);
375
+ break;
376
+ case "heading":
377
+ case "paragraph":
378
+ walkInline(block.children);
379
+ break;
380
+ case "quote":
381
+ walk(block.children);
382
+ break;
383
+ case "list":
384
+ for (const item of block.items) walk(item);
385
+ break;
386
+ case "table":
387
+ for (const cell of block.head) walkInline(cell);
388
+ for (const row of block.rows) for (const cell of row) walkInline(cell);
389
+ }
390
+ };
391
+ walk(blocks);
392
+ return out;
393
+ }
394
+ function imageSrc(src, images) {
395
+ const known = images?.get(src);
396
+ return known ? `{${known.ident}}` : attr(src);
397
+ }
398
+ function renderInline(nodes, images) {
399
+ return nodes.map((node) => {
400
+ switch (node.type) {
401
+ case "text": return jsxText(node.value);
402
+ case "strong": return `<strong>${renderInline(node.children, images)}</strong>`;
403
+ case "em": return `<em>${renderInline(node.children, images)}</em>`;
404
+ case "code": return `<code style={codeInline}>${jsxText(node.value)}</code>`;
405
+ case "link": return `<a href=${attr(node.href)} style={link}>${renderInline(node.children, images)}</a>`;
406
+ case "image": return `<img src=${imageSrc(node.src, images)} alt=${attr(node.alt)} style={inlineImg} />`;
407
+ case "break": return "<br />";
408
+ default: return "";
409
+ }
410
+ }).join("");
411
+ }
412
+ const HEADING_STYLE = {
413
+ 1: "h1",
414
+ 2: "h2",
415
+ 3: "h3",
416
+ 4: "h4"
417
+ };
418
+ function alignStyle(align) {
419
+ return align && align !== "left" ? ` align={${jsString(align)}}` : "";
420
+ }
421
+ function renderBlock(block, images, indent) {
422
+ const pad = indent;
423
+ switch (block.type) {
424
+ case "heading": {
425
+ const level = Math.min(block.level, 4);
426
+ const tag = level >= 4 ? "h4" : `h${level}`;
427
+ return `${pad}<${tag} style={${HEADING_STYLE[level] ?? "h3"}}>${renderInline(block.children, images)}</${tag}>`;
428
+ }
429
+ case "paragraph": return `${pad}<p style={p}>${renderInline(block.children, images)}</p>`;
430
+ case "hr": return `${pad}<hr style={rule} />`;
431
+ case "code": return `${pad}<pre style={pre}>{${jsString(block.value)}}</pre>`;
432
+ case "quote": return [
433
+ `${pad}<blockquote style={quote}>`,
434
+ ...block.children.map((child) => renderBlock(child, images, `${pad} `)),
435
+ `${pad}</blockquote>`
436
+ ].join("\n");
437
+ case "figure": {
438
+ const caption = block.alt ? `\n${pad} <figcaption style={caption}>${jsxText(block.alt)}</figcaption>` : "";
439
+ return [
440
+ `${pad}<figure style={figure}>`,
441
+ `${pad} <img src=${imageSrc(block.src, images)} alt=${attr(block.alt)} style={img} />${caption}`,
442
+ `${pad}</figure>`
443
+ ].join("\n");
444
+ }
445
+ case "list": {
446
+ const tag = block.ordered ? "ol" : "ul";
447
+ const startAttr = block.ordered && block.start !== 1 ? ` start={${block.start}}` : "";
448
+ const items = block.items.map((item) => {
449
+ const inner = item.map((child, index) => child.type === "paragraph" && index === 0 ? `${renderInline(child.children, images)}` : `\n${renderBlock(child, images, `${pad} `)}`).join("");
450
+ return `${pad} <li style={li}>${inner.startsWith("\n") ? `${inner}\n${pad} ` : inner}</li>`;
451
+ });
452
+ return [
453
+ `${pad}<${tag} style={${tag}}${startAttr}>`,
454
+ ...items,
455
+ `${pad}</${tag}>`
456
+ ].join("\n");
457
+ }
458
+ case "table": {
459
+ const head = block.head.map((cell, i) => `${pad} <Th${alignStyle(block.align[i] ?? null)}>${renderInline(cell, images)}</Th>`).join("\n");
460
+ const rows = block.rows.map((row) => [
461
+ `${pad} <tr>`,
462
+ ...row.map((cell, i) => `${pad} <Td${alignStyle(block.align[i] ?? null)}>${renderInline(cell, images)}</Td>`),
463
+ `${pad} </tr>`
464
+ ].join("\n")).join("\n");
465
+ return [
466
+ `${pad}<table style={table}>`,
467
+ `${pad} <thead>`,
468
+ `${pad} <tr>`,
469
+ head,
470
+ `${pad} </tr>`,
471
+ `${pad} </thead>`,
472
+ `${pad} <tbody>`,
473
+ rows,
474
+ `${pad} </tbody>`,
475
+ `${pad}</table>`
476
+ ].join("\n");
477
+ }
478
+ }
479
+ }
480
+ function titleFrom(parsed, opts) {
481
+ if (opts.title) return opts.title;
482
+ if (parsed.frontmatter.title) return parsed.frontmatter.title;
483
+ const heading = parsed.blocks.find((block) => block.type === "heading" && block.level === 1);
484
+ if (heading && heading.type === "heading") return inlineText(heading.children);
485
+ return opts.docId.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
486
+ }
487
+ const STYLES = `const page = {
488
+ width: '100%',
489
+ height: '100%',
490
+ boxSizing: 'border-box' as const,
491
+ padding: 'var(--od-margin)',
492
+ background: 'var(--od-bg)',
493
+ color: 'var(--od-text)',
494
+ fontFamily: 'var(--od-font-body)',
495
+ fontSize: 'var(--od-size-body)',
496
+ lineHeight: 'var(--od-leading)',
497
+ position: 'relative' as const,
498
+ };
499
+
500
+ const h1 = {
501
+ fontFamily: 'var(--od-font-heading)',
502
+ fontSize: 'var(--od-size-h1)',
503
+ lineHeight: 1.2,
504
+ fontWeight: 650,
505
+ margin: '0 0 18px',
506
+ };
507
+
508
+ const h2 = { ...h1, fontSize: 'var(--od-size-h2)', margin: '26px 0 12px' };
509
+ const h3 = { ...h1, fontSize: 'var(--od-size-h3)', fontWeight: 600, margin: '20px 0 8px' };
510
+ const h4 = { ...h3, fontSize: 'var(--od-size-body)', margin: '16px 0 6px' };
511
+ const p = { margin: '0 0 12px' };
512
+ // The viewer's CSS reset strips list markers, so both are set back explicitly.
513
+ const ul = { margin: '0 0 12px', paddingLeft: 22, listStyle: 'disc outside' };
514
+ const ol = { ...ul, listStyle: 'decimal outside' };
515
+ const li = { margin: '0 0 5px' };
516
+ const link = { color: 'var(--od-accent)', textDecoration: 'none' };
517
+ const rule = { border: 0, borderTop: '1px solid var(--od-rule)', margin: '20px 0' };
518
+
519
+ const codeInline = {
520
+ fontFamily: 'var(--od-font-mono)',
521
+ fontSize: '0.92em',
522
+ background: 'var(--od-rule)',
523
+ borderRadius: 3,
524
+ padding: '1px 4px',
525
+ };
526
+
527
+ const pre = {
528
+ fontFamily: 'var(--od-font-mono)',
529
+ fontSize: 11,
530
+ lineHeight: 1.5,
531
+ background: 'var(--od-rule)',
532
+ borderRadius: 'var(--od-radius)',
533
+ padding: '10px 12px',
534
+ margin: '0 0 14px',
535
+ whiteSpace: 'pre-wrap' as const,
536
+ overflowWrap: 'anywhere' as const,
537
+ };
538
+
539
+ const quote = {
540
+ margin: '0 0 14px',
541
+ padding: '2px 0 2px 14px',
542
+ borderLeft: '2px solid var(--od-accent)',
543
+ color: 'var(--od-muted)',
544
+ };
545
+
546
+ const table = {
547
+ width: '100%',
548
+ borderCollapse: 'collapse' as const,
549
+ tableLayout: 'fixed' as const,
550
+ margin: '0 0 16px',
551
+ };
552
+
553
+ const figure = { margin: '0 0 16px' };
554
+ const img = { width: '100%', display: 'block' as const };
555
+ const inlineImg = { maxWidth: '100%', verticalAlign: 'middle' as const };
556
+ const caption = { fontSize: 'var(--od-size-caption)', color: 'var(--od-muted)', margin: '6px 0 0' };
557
+
558
+ const Th = ({ children, align = 'left' }: { children?: ReactNode; align?: 'left' | 'center' | 'right' }) => (
559
+ <th
560
+ style={{
561
+ textAlign: align,
562
+ fontFamily: 'var(--od-font-heading)',
563
+ fontSize: 11,
564
+ fontWeight: 600,
565
+ letterSpacing: '0.04em',
566
+ textTransform: 'uppercase',
567
+ color: 'var(--od-muted)',
568
+ borderBottom: '1px solid var(--od-rule)',
569
+ padding: '0 8px 6px',
570
+ }}
571
+ >
572
+ {children}
573
+ </th>
574
+ );
575
+
576
+ const Td = ({ children, align = 'left' }: { children?: ReactNode; align?: 'left' | 'center' | 'right' }) => (
577
+ <td
578
+ style={{
579
+ textAlign: align,
580
+ fontSize: 12,
581
+ padding: '7px 8px',
582
+ borderBottom: '1px solid var(--od-rule)',
583
+ fontVariantNumeric: align === 'right' ? 'tabular-nums' : undefined,
584
+ }}
585
+ >
586
+ {children}
587
+ </td>
588
+ );
589
+
590
+ const Footer = () => {
591
+ const n = useDocPageNumber();
592
+ const total = useDocPageCount();
593
+ return (
594
+ <div
595
+ style={{
596
+ position: 'absolute',
597
+ left: 'var(--od-margin)',
598
+ right: 'var(--od-margin)',
599
+ bottom: 40,
600
+ display: 'flex',
601
+ justifyContent: 'space-between',
602
+ fontSize: 'var(--od-size-caption)',
603
+ color: 'var(--od-muted)',
604
+ }}
605
+ >
606
+ <span>{DOC_TITLE}</span>
607
+ <span style={{ fontVariantNumeric: 'tabular-nums' }}>
608
+ {n} / {total}
609
+ </span>
610
+ </div>
611
+ );
612
+ };`;
613
+ const DESIGN = `export const design: DesignSystem = {
614
+ palette: {
615
+ bg: '#ffffff',
616
+ text: '#16181d',
617
+ muted: '#6b7280',
618
+ accent: '#2563eb',
619
+ rule: '#e5e7eb',
620
+ },
621
+ fonts: {
622
+ heading: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
623
+ body: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
624
+ mono: 'ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace',
625
+ },
626
+ typeScale: { title: 44, h1: 28, h2: 20, h3: 16, body: 14, caption: 10 },
627
+ margin: 76,
628
+ leading: 1.55,
629
+ radius: 6,
630
+ };`;
631
+ /**
632
+ * Turns parsed markdown into a document module. The output is ordinary authored
633
+ * TSX — inline styles, real heading tags, plain JSX text — so the outline, the
634
+ * inspector, and the design panel all work on an imported document exactly as
635
+ * they do on a hand-written one.
636
+ */
637
+ function generateDocumentSource(parsed, opts) {
638
+ const title = titleFrom(parsed, opts);
639
+ const subtitle = opts.subtitle ?? parsed.frontmatter.subtitle;
640
+ const author = opts.author ?? parsed.frontmatter.author;
641
+ const theme = opts.theme ?? parsed.frontmatter.theme;
642
+ const pageSize = opts.pageSize ?? parsed.frontmatter.pageSize;
643
+ const orientation = opts.orientation ?? parsed.frontmatter.orientation;
644
+ const cover = opts.cover !== false;
645
+ let blocks = parsed.blocks;
646
+ const first = blocks[0];
647
+ if (cover && first?.type === "heading" && first.level === 1 && inlineText(first.children) === title) blocks = blocks.slice(1);
648
+ const images = opts.images;
649
+ const imports = images ? [...images.values()] : [];
650
+ const head = [
651
+ `import type { ReactNode } from 'react';`,
652
+ `import {\n${[
653
+ "type DesignSystem",
654
+ "type DocEntry",
655
+ "type DocMeta",
656
+ ...cover || opts.contents ? ["type DocPage"] : [],
657
+ ...opts.contents ? ["TableOfContents"] : [],
658
+ "flow",
659
+ "useDocPageCount",
660
+ "useDocPageNumber"
661
+ ].map((name) => ` ${name},`).join("\n")}\n} from 'mosage';`,
662
+ ...imports.map((image) => `import ${image.ident} from './assets/${image.filename}';`)
663
+ ].join("\n");
664
+ const styles = STYLES.replace("{DOC_TITLE}", jsxText(title));
665
+ const coverPage = cover ? `const Cover: DocPage = () => (
666
+ <div style={{ ...page, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
667
+ ${subtitle ? ` <p style={{ fontSize: 12, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--od-accent)', margin: 0 }}>
668
+ ${jsxText(subtitle)}
669
+ </p>\n` : ""} <h1 style={{ ...h1, fontSize: 'var(--od-size-title)', margin: '16px 0 12px' }} data-od-outline="skip">
670
+ ${jsxText(title)}
671
+ </h1>
672
+ ${author ? ` <p style={{ color: 'var(--od-muted)', margin: 0 }}>${jsxText(author)}</p>\n` : ""} </div>
673
+ );` : "";
674
+ const contentsPage = opts.contents ? `const Contents: DocPage = () => (
675
+ <div style={page}>
676
+ <h1 style={h1} data-od-outline="skip">
677
+ Contents
678
+ </h1>
679
+ <TableOfContents maxLevel={2} />
680
+ </div>
681
+ );` : "";
682
+ const body = `const Body = flow(
683
+ <>
684
+ ${blocks.map((block) => renderBlock(block, images, " ")).join("\n")}
685
+ </>,
686
+ { footer: Footer },
687
+ );`;
688
+ const metaFields = [
689
+ ` title: ${jsString(title)},`,
690
+ ...subtitle ? [` subtitle: ${jsString(subtitle)},`] : [],
691
+ ...author ? [` author: ${jsString(author)},`] : [],
692
+ ...pageSize ? [` pageSize: ${jsString(pageSize)},`] : [],
693
+ ...orientation ? [` orientation: ${jsString(orientation)},`] : [],
694
+ ...theme ? [` theme: ${jsString(theme)},`] : [],
695
+ ` createdAt: ${jsString(opts.createdAt)},`
696
+ ];
697
+ const entries = [
698
+ ...cover ? ["Cover"] : [],
699
+ ...opts.contents ? ["Contents"] : [],
700
+ "Body"
701
+ ];
702
+ return {
703
+ source: [
704
+ head,
705
+ "",
706
+ DESIGN,
707
+ "",
708
+ styles,
709
+ "",
710
+ ...coverPage ? [coverPage, ""] : [],
711
+ ...contentsPage ? [contentsPage, ""] : [],
712
+ body,
713
+ "",
714
+ `export const meta: DocMeta = {\n${metaFields.join("\n")}\n};`,
715
+ `export default [${entries.join(", ")}] satisfies DocEntry[];`,
716
+ ""
717
+ ].join("\n"),
718
+ title,
719
+ blockCount: blocks.length
720
+ };
721
+ }
722
+ //#endregion
723
+ //#region src/ops/import.ts
724
+ const REMOTE_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
725
+ function slugify(value, fallback = "document") {
726
+ const slug = value.normalize("NFKD").toLowerCase().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
727
+ return DOC_ID_RE.test(slug) ? slug : fallback;
728
+ }
729
+ function uniqueId(base, taken) {
730
+ if (!taken.has(base)) return base;
731
+ for (let n = 2; n < 1e3; n++) {
732
+ const candidate = `${base}-${n}`;
733
+ if (!taken.has(candidate)) return candidate;
734
+ }
735
+ throw new OpsError(409, `could not find a free id for ${base}`);
736
+ }
737
+ function identFor(index) {
738
+ return `figure${index + 1}`;
739
+ }
740
+ /**
741
+ * Reads markdown and writes a real MoSage document: `flow()` body, inline
742
+ * styles, a cover, and any local images copied into the document's own assets
743
+ * folder. Nothing about the result is special-cased — it is the same shape an
744
+ * agent would have written by hand, and every editing surface works on it.
745
+ */
746
+ async function importMarkdown(ctx, opts) {
747
+ const sourceFile = opts.file ? path.resolve(ctx.userCwd, opts.file) : null;
748
+ if (sourceFile && !sourceFile.startsWith(ctx.userCwd + path.sep)) throw new OpsError(400, `file must sit inside the workspace: ${opts.file}`);
749
+ let markdown = opts.markdown;
750
+ if (markdown === void 0) {
751
+ if (!sourceFile) throw new OpsError(400, "pass either `markdown` or `file`");
752
+ try {
753
+ markdown = await fs.readFile(sourceFile, "utf8");
754
+ } catch {
755
+ throw new OpsError(404, `file not found: ${opts.file}`);
756
+ }
757
+ }
758
+ if (markdown.trim() === "") throw new OpsError(422, "the markdown is empty");
759
+ const parsed = parseMarkdown(markdown);
760
+ const pageSize = opts.pageSize ?? parsed.frontmatter.pageSize;
761
+ if (pageSize !== void 0 && !isPageSizeName(pageSize)) throw new OpsError(422, `unsupported pageSize \`${pageSize}\` — use ${PAGE_SIZE_NAMES.join(", ")}`);
762
+ const orientation = opts.orientation ?? parsed.frontmatter.orientation;
763
+ if (orientation !== void 0 && !isOrientation(orientation)) throw new OpsError(422, `unsupported orientation \`${orientation}\` — use ${ORIENTATIONS.join(", ")}`);
764
+ const fallbackName = sourceFile ? path.basename(sourceFile).replace(/\.mdx?$/i, "") : "document";
765
+ const requestedId = opts.docId ?? slugify(opts.title ?? parsed.frontmatter.title ?? fallbackName);
766
+ if (opts.docId && !DOC_ID_RE.test(opts.docId)) throw new OpsError(400, `invalid document id: ${opts.docId}`);
767
+ const docId = opts.docId ?? uniqueId(requestedId, new Set(await listDocIds(ctx)));
768
+ const baseDir = sourceFile ? path.dirname(sourceFile) : ctx.userCwd;
769
+ const images = /* @__PURE__ */ new Map();
770
+ const missing = [];
771
+ const staged = [];
772
+ const usedNames = /* @__PURE__ */ new Set();
773
+ for (const src of new Set(collectImageSources(parsed.blocks))) {
774
+ if (REMOTE_RE.test(src)) continue;
775
+ const from = path.resolve(baseDir, src.split(/[?#]/)[0]);
776
+ if (!from.startsWith(ctx.userCwd + path.sep) || !existsSync(from)) {
777
+ missing.push(src);
778
+ continue;
779
+ }
780
+ const safe = validateAssetName(path.basename(from));
781
+ if (!safe) {
782
+ missing.push(src);
783
+ continue;
784
+ }
785
+ const filename = usedNames.has(safe) ? `${images.size + 1}-${safe}` : safe;
786
+ usedNames.add(filename);
787
+ images.set(src, {
788
+ source: src,
789
+ ident: identFor(images.size),
790
+ filename
791
+ });
792
+ staged.push({
793
+ from,
794
+ filename
795
+ });
796
+ }
797
+ const generated = generateDocumentSource(parsed, {
798
+ docId,
799
+ ...opts.title !== void 0 ? { title: opts.title } : {},
800
+ ...opts.subtitle !== void 0 ? { subtitle: opts.subtitle } : {},
801
+ ...opts.author !== void 0 ? { author: opts.author } : {},
802
+ ...opts.theme !== void 0 ? { theme: opts.theme } : {},
803
+ ...opts.pageSize !== void 0 ? { pageSize: opts.pageSize } : {},
804
+ ...opts.orientation !== void 0 ? { orientation: opts.orientation } : {},
805
+ ...opts.cover !== void 0 ? { cover: opts.cover } : {},
806
+ ...opts.contents !== void 0 ? { contents: opts.contents } : {},
807
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
808
+ images
809
+ });
810
+ const created = await createDocument(ctx, docId, generated.source);
811
+ if (staged.length > 0) {
812
+ const assetsDir = path.join(ctx.docsRoot, docId, "assets");
813
+ await fs.mkdir(assetsDir, { recursive: true });
814
+ for (const { from, filename } of staged) await fs.copyFile(from, path.join(assetsDir, filename));
815
+ }
816
+ return {
817
+ id: created.id,
818
+ entry: created.entry,
819
+ title: generated.title,
820
+ blocks: generated.blockCount,
821
+ assets: staged.map((asset) => asset.filename),
822
+ missingAssets: missing
823
+ };
824
+ }
825
+ //#endregion
826
+ //#region src/render/session.ts
827
+ var RenderUnavailableError = class extends Error {
828
+ constructor(message) {
829
+ super(message);
830
+ this.name = "RenderUnavailableError";
831
+ }
832
+ };
833
+ const INSTALL_HINT = "Headless rendering needs Playwright. Install it in this workspace:\n pnpm add -D playwright && pnpm exec playwright install chromium";
834
+ async function loadPlaywright() {
835
+ try {
836
+ return await import("playwright");
837
+ } catch {
838
+ throw new RenderUnavailableError(INSTALL_HINT);
839
+ }
840
+ }
841
+ const DEFAULT_TIMEOUT = 6e4;
842
+ const PRINT_PAGE_SELECTOR = "#od-print-root .od-print-page";
843
+ async function bootServer(userCwd) {
844
+ const base = await createViteConfig({
845
+ userCwd,
846
+ headless: true
847
+ });
848
+ const config = mergeConfig(base, {
849
+ logLevel: "silent",
850
+ server: {
851
+ port: 0,
852
+ strictPort: false,
853
+ open: false
854
+ }
855
+ });
856
+ const server = await createServer(config);
857
+ await server.listen();
858
+ const origin = server.resolvedUrls?.local?.[0]?.replace(/\/$/, "");
859
+ if (!origin) {
860
+ await server.close();
861
+ throw new Error("Could not determine the dev server URL");
862
+ }
863
+ return {
864
+ server,
865
+ origin
866
+ };
867
+ }
868
+ async function createRenderSession(opts) {
869
+ const { chromium } = await loadPlaywright();
870
+ const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
871
+ let server = null;
872
+ let origin = opts.origin?.replace(/\/$/, "") ?? "";
873
+ if (!origin) {
874
+ const booted = await bootServer(opts.userCwd);
875
+ server = booted.server;
876
+ origin = booted.origin;
877
+ }
878
+ let browser;
879
+ try {
880
+ browser = await chromium.launch({ headless: true });
881
+ } catch (err) {
882
+ await server?.close();
883
+ throw new RenderUnavailableError(`${INSTALL_HINT}\n\nChromium failed to launch: ${err.message}`);
884
+ }
885
+ return {
886
+ origin,
887
+ async open(docId) {
888
+ const page = await browser.newPage({
889
+ viewport: {
890
+ width: 1280,
891
+ height: 1024
892
+ },
893
+ ...opts.deviceScaleFactor !== void 0 ? { deviceScaleFactor: opts.deviceScaleFactor } : {}
894
+ });
895
+ const errors = [];
896
+ page.on("pageerror", (err) => errors.push(String(err?.message ?? err)));
897
+ await page.goto(`${origin}/d/${encodeURIComponent(docId)}`, {
898
+ waitUntil: "load",
899
+ timeout
900
+ });
901
+ try {
902
+ await page.waitForFunction("globalThis.__mosage ? globalThis.__mosage.status().ready : false", void 0, { timeout });
903
+ } catch {
904
+ await page.close();
905
+ const detail = errors.length ? `\n${errors.join("\n")}` : "";
906
+ throw new Error(`Document "${docId}" never finished rendering.${detail}`);
907
+ }
908
+ const status = await page.evaluate("globalThis.__mosage.status()");
909
+ return {
910
+ status,
911
+ diagnose: () => page.evaluate("globalThis.__mosage.diagnose()"),
912
+ async pdf() {
913
+ await page.evaluate("globalThis.__mosage.preparePrint()");
914
+ try {
915
+ return await page.pdf({
916
+ printBackground: true,
917
+ preferCSSPageSize: true
918
+ });
919
+ } finally {
920
+ await page.evaluate("globalThis.__mosage.releasePrint()");
921
+ }
922
+ },
923
+ async screenshot(pageNumber) {
924
+ if (pageNumber < 1 || pageNumber > status.pageCount) throw new Error(`page ${pageNumber} is out of range — "${docId}" has ${status.pageCount}`);
925
+ await page.evaluate("globalThis.__mosage.preparePrint()");
926
+ await page.emulateMedia({ media: "print" });
927
+ try {
928
+ const sheet = await page.$(`${PRINT_PAGE_SELECTOR}:nth-child(${pageNumber})`);
929
+ if (!sheet) throw new Error(`page ${pageNumber} did not render`);
930
+ return await sheet.screenshot({ type: "png" });
931
+ } finally {
932
+ await page.emulateMedia({ media: null });
933
+ await page.evaluate("globalThis.__mosage.releasePrint()");
934
+ }
935
+ },
936
+ html: () => page.evaluate("globalThis.__mosage.htmlBundle()"),
937
+ docx: () => page.evaluate("globalThis.__mosage.docxBundle()"),
938
+ close: () => page.close()
939
+ };
940
+ },
941
+ async close() {
942
+ await browser.close();
943
+ await server?.close();
944
+ }
945
+ };
946
+ }
947
+ //#endregion
948
+ //#region src/ops/layout.ts
949
+ /**
950
+ * Chromium and a Vite server are expensive to start, and an agent checking its
951
+ * work calls these back to back. One session is kept warm and closed once
952
+ * nobody has asked for a while.
953
+ */
954
+ const IDLE_MS = 12e4;
955
+ let shared = null;
956
+ let pending = null;
957
+ let idleTimer = null;
958
+ let leases = 0;
959
+ function armIdleClose() {
960
+ if (idleTimer) clearTimeout(idleTimer);
961
+ idleTimer = setTimeout(() => {
962
+ if (leases > 0) return;
963
+ const current = shared;
964
+ shared = null;
965
+ idleTimer = null;
966
+ current?.session.close().catch(() => {});
967
+ }, IDLE_MS);
968
+ idleTimer.unref?.();
969
+ }
970
+ async function acquire(ctx) {
971
+ if (shared && shared.origin === ctx.serverOrigin) return shared.session;
972
+ if (!pending) {
973
+ const origin = ctx.serverOrigin;
974
+ pending = createRenderSession({
975
+ userCwd: ctx.userCwd,
976
+ origin,
977
+ deviceScaleFactor: 2
978
+ }).then((session) => {
979
+ shared = {
980
+ session,
981
+ origin
982
+ };
983
+ return session;
984
+ }).finally(() => {
985
+ pending = null;
986
+ });
987
+ }
988
+ return pending;
989
+ }
990
+ async function withDoc(ctx, docId, fn) {
991
+ if (!resolveEntry(ctx, docId)) throw new OpsError(404, `document not found: ${docId}`);
992
+ let session;
993
+ try {
994
+ session = await acquire(ctx);
995
+ } catch (err) {
996
+ if (err instanceof RenderUnavailableError) throw new OpsError(501, err.message);
997
+ throw new OpsError(500, err.message);
998
+ }
999
+ leases++;
1000
+ let renderer = null;
1001
+ try {
1002
+ renderer = await session.open(docId);
1003
+ return await fn(renderer);
1004
+ } catch (err) {
1005
+ if (err instanceof OpsError) throw err;
1006
+ throw new OpsError(422, err.message);
1007
+ } finally {
1008
+ await renderer?.close().catch(() => {});
1009
+ leases--;
1010
+ armIdleClose();
1011
+ }
1012
+ }
1013
+ /** Closes the warm browser. Call before the process exits so nothing lingers. */
1014
+ async function closeRenderSession() {
1015
+ if (idleTimer) clearTimeout(idleTimer);
1016
+ idleTimer = null;
1017
+ const current = shared;
1018
+ shared = null;
1019
+ await current?.session.close().catch(() => {});
1020
+ }
1021
+ /**
1022
+ * Renders the document headlessly at true page size and reports what is wrong
1023
+ * with the layout — the part of a document an agent writing React cannot see.
1024
+ */
1025
+ async function checkLayout(ctx, docId) {
1026
+ return withDoc(ctx, docId, async (renderer) => {
1027
+ const report = await renderer.diagnose();
1028
+ return {
1029
+ docId: report.docId || docId,
1030
+ title: report.title,
1031
+ pageCount: report.pageCount,
1032
+ errors: report.findings.filter((f) => f.severity === "error").length,
1033
+ warnings: report.findings.filter((f) => f.severity === "warn").length,
1034
+ findings: report.findings
1035
+ };
1036
+ });
1037
+ }
1038
+ function resolveOutDir(ctx, outDir) {
1039
+ const resolved = path.resolve(ctx.userCwd, outDir);
1040
+ if (resolved !== ctx.userCwd && !resolved.startsWith(ctx.userCwd + path.sep)) throw new OpsError(400, `outDir must stay inside the workspace: ${outDir}`);
1041
+ return resolved;
1042
+ }
1043
+ async function exportDocument(ctx, docId, opts = {}) {
1044
+ const format = opts.format ?? "pdf";
1045
+ const dir = resolveOutDir(ctx, opts.outDir ?? "out");
1046
+ return withDoc(ctx, docId, async (renderer) => {
1047
+ await fs.mkdir(dir, { recursive: true });
1048
+ const written = [];
1049
+ const write = async (name, bytes) => {
1050
+ const file = path.join(dir, name);
1051
+ await fs.writeFile(file, bytes);
1052
+ written.push(path.relative(ctx.userCwd, file));
1053
+ };
1054
+ if (format === "pdf") await write(`${docId}.pdf`, await renderer.pdf());
1055
+ else if (format === "html" || format === "docx") {
1056
+ const bundle = await renderer[format]();
1057
+ if (!bundle) throw new OpsError(422, `document has no pages: ${docId}`);
1058
+ await write(bundle.filename, Buffer.from(bundle.base64, "base64"));
1059
+ } else {
1060
+ const width = String(renderer.status.pageCount).length;
1061
+ for (let page = 1; page <= renderer.status.pageCount; page++) await write(`${docId}-${String(page).padStart(width, "0")}.png`, await renderer.screenshot(page));
1062
+ }
1063
+ return {
1064
+ docId,
1065
+ format,
1066
+ pageCount: renderer.status.pageCount,
1067
+ files: written
1068
+ };
1069
+ });
1070
+ }
1071
+ //#endregion
1072
+ //#region src/cli/context.ts
1073
+ /** The same context the dev API runs on, for commands that call `ops/`. */
1074
+ async function cliContext(userCwd = process.cwd()) {
1075
+ const config = await loadUserConfig(userCwd);
1076
+ return makeContext({
1077
+ userCwd,
1078
+ docsDir: config.docsDir ?? "docs",
1079
+ assetsDir: config.assetsDir ?? "assets",
1080
+ coreVersion: await readCoreVersion()
1081
+ });
1082
+ }
1083
+ //#endregion
1084
+ export { importMarkdown as a, exportDocument as i, checkLayout as n, closeRenderSession as r, cliContext as t };