artifacty 0.1.0 → 0.1.2

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.
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
 
6
6
  const SUPPORTED_AGENTS = new Set(["all", "claude", "codex", "gemini"]);
7
+ const DEFAULT_MCP_TIMEOUT_MS = 30000;
7
8
 
8
9
  export async function installAgent(agent, options = {}) {
9
10
  const normalized = normalizeAgent(agent);
@@ -81,7 +82,7 @@ export async function installGemini(options = {}) {
81
82
  ...(existing.mcpServers || {}),
82
83
  artifacty: {
83
84
  ...createMcpServerConfig(options),
84
- timeout: options.timeout || 30000,
85
+ timeout: normalizeTimeoutMs(options.timeout),
85
86
  trust: Boolean(options.trust)
86
87
  }
87
88
  }
@@ -98,7 +99,9 @@ export async function installGemini(options = {}) {
98
99
  export async function installCodex(options = {}) {
99
100
  const targetPath = path.resolve(options.configPath || path.join(homedir(), ".codex", "config.toml"));
100
101
  const existing = await readTextFile(targetPath, "");
101
- const block = codexTomlBlock(createMcpServerConfig(options));
102
+ const block = codexTomlBlock(createMcpServerConfig(options), {
103
+ timeoutMs: normalizeTimeoutMs(options.timeout)
104
+ });
102
105
  const next = replaceTomlBlock(existing, "mcp_servers.artifacty", block);
103
106
 
104
107
  return writeInstallFile({
@@ -109,16 +112,17 @@ export async function installCodex(options = {}) {
109
112
  });
110
113
  }
111
114
 
112
- export function codexTomlBlock(config) {
115
+ export function codexTomlBlock(config, options = {}) {
113
116
  const envPairs = Object.entries(config.env || {})
114
117
  .map(([key, value]) => `${key} = ${quoteTomlString(value)}`)
115
118
  .join(", ");
119
+ const startupTimeoutSec = normalizeTimeoutMs(options.timeoutMs) / 1000;
116
120
 
117
121
  return [
118
122
  "[mcp_servers.artifacty]",
119
123
  `command = ${quoteTomlString(config.command)}`,
120
124
  `args = [${config.args.map(quoteTomlString).join(", ")}]`,
121
- "startup_timeout_sec = 5.0",
125
+ `startup_timeout_sec = ${startupTimeoutSec.toFixed(1)}`,
122
126
  `env = { ${envPairs} }`,
123
127
  ""
124
128
  ].join("\n");
@@ -178,6 +182,11 @@ function normalizeAgent(agent) {
178
182
  return String(agent || "").trim().toLowerCase();
179
183
  }
180
184
 
185
+ function normalizeTimeoutMs(value) {
186
+ const timeout = Number(value ?? DEFAULT_MCP_TIMEOUT_MS);
187
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_MCP_TIMEOUT_MS;
188
+ }
189
+
181
190
  function quoteTomlString(value) {
182
191
  return JSON.stringify(String(value));
183
192
  }
package/src/lib/render.js CHANGED
@@ -1,5 +1,6 @@
1
- import { EDITOR_CLIENT_PATH, editorImportMapJson } from "./editor-assets.js";
1
+ import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorImportMapJson } from "./editor-assets.js";
2
2
  import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, switchLocaleHref } from "./i18n.js";
3
+ import { ARTIFACT_FORMATS, ARTIFACT_TYPES } from "./storage.js";
3
4
 
4
5
  export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEFAULT_LOCALE, currentPath = "/" }) {
5
6
  const view = viewContext(locale, currentPath);
@@ -204,7 +205,12 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
204
205
  })
205
206
  .join("");
206
207
 
207
- const rendered = renderContent(version.format, content);
208
+ const rendered = renderContent(version.format, content, version.metadata || {}, {
209
+ reactFrameUrl: reactRendererEnabled()
210
+ ? view.href(`/artifacts/${encodeURIComponent(artifact.id)}/react-frame?version=${version.version}`)
211
+ : ""
212
+ });
213
+ const needsViewerScript = version.format === "code";
208
214
  const rawUrl = `/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${version.version}`;
209
215
  const archiveAction = artifact.archivedAt ? "restore" : "archive";
210
216
  const archiveLabel = artifact.archivedAt ? view.text("artifact.restore") : view.text("artifact.archive");
@@ -244,7 +250,8 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
244
250
  ${rendered}
245
251
  </main>
246
252
  `,
247
- afterBody: frameResizeScript(),
253
+ head: needsViewerScript ? editorHead() : "",
254
+ afterBody: `${frameResizeScript()}${needsViewerScript ? viewerScript() : ""}`,
248
255
  locale: view.locale
249
256
  });
250
257
  }
@@ -314,11 +321,19 @@ export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent,
314
321
  });
315
322
  }
316
323
 
317
- export function renderContent(format, content) {
324
+ export function renderContent(format, content, metadata = {}, options = {}) {
318
325
  if (format === "html") {
319
326
  return `<iframe class="artifact-frame" sandbox="allow-scripts allow-forms allow-popups" srcdoc="${escapeAttribute(htmlFrameContent(content))}"></iframe>`;
320
327
  }
321
328
 
329
+ if (format === "svg") {
330
+ return `<iframe class="artifact-frame artifact-svg-frame" sandbox srcdoc="${escapeAttribute(svgFrameContent(content))}"></iframe>`;
331
+ }
332
+
333
+ if (format === "mermaid") {
334
+ return `<iframe class="artifact-frame artifact-mermaid-frame" sandbox="allow-scripts" srcdoc="${escapeAttribute(mermaidFrameContent(content))}"></iframe>`;
335
+ }
336
+
322
337
  if (format === "markdown") {
323
338
  return `<article class="artifact-doc">${markdownToHtml(content)}</article>`;
324
339
  }
@@ -327,9 +342,105 @@ export function renderContent(format, content) {
327
342
  return `<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>`;
328
343
  }
329
344
 
345
+ if (format === "code") {
346
+ const language = metadata.language || metadata.artifactyImport?.language || "";
347
+ return `<section class="artifact-code-viewer" data-artifacty-code-viewer data-language="${escapeAttribute(language)}">
348
+ <textarea hidden>${escapeHtml(content)}</textarea>
349
+ <pre class="artifact-code artifact-code-fallback"><code>${escapeHtml(content)}</code></pre>
350
+ </section>`;
351
+ }
352
+
353
+ if (format === "react") {
354
+ if (options.reactFrameUrl) {
355
+ return `<iframe class="artifact-frame artifact-react-frame" sandbox="allow-scripts" src="${escapeAttribute(options.reactFrameUrl)}"></iframe>`;
356
+ }
357
+ return `<section class="artifact-react-disabled">
358
+ <p>React rendering is disabled. Set <code>ARTIFACTY_ENABLE_REACT_RENDERER=true</code> to run this component in a sandboxed frame.</p>
359
+ <pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
360
+ </section>`;
361
+ }
362
+
330
363
  return `<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
331
364
  }
332
365
 
366
+ export function renderReactFramePage({ title, content }) {
367
+ const source = jsonForScript(content);
368
+ return `<!doctype html>
369
+ <html>
370
+ <head>
371
+ <meta charset="utf-8">
372
+ <meta name="viewport" content="width=device-width, initial-scale=1">
373
+ <title>${escapeHtml(title)}</title>
374
+ <style>
375
+ html, body { margin: 0; min-height: 100%; background: #fff; color: #111827; }
376
+ body { padding: 16px; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
377
+ #root { min-height: 120px; }
378
+ .error { white-space: pre-wrap; color: #991b1b; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 12px; }
379
+ </style>
380
+ </head>
381
+ <body>
382
+ <div id="root"></div>
383
+ <script type="application/json" id="artifacty-react-source">${source}</script>
384
+ <script src="/vendor/npm/react/umd/react.production.min.js"></script>
385
+ <script src="/vendor/npm/react-dom/umd/react-dom.production.min.js"></script>
386
+ <script src="/vendor/npm/@babel/standalone/babel.min.js"></script>
387
+ <script>
388
+ const rootElement = document.getElementById("root");
389
+ function report() {
390
+ const doc = document.documentElement;
391
+ const body = document.body;
392
+ const height = Math.max(doc.scrollHeight, doc.offsetHeight, body ? body.scrollHeight : 0, body ? body.offsetHeight : 0);
393
+ parent.postMessage({ __artifactyHeight: height }, "*");
394
+ }
395
+ function showError(error) {
396
+ rootElement.innerHTML = "";
397
+ const pre = document.createElement("pre");
398
+ pre.className = "error";
399
+ pre.textContent = error && error.stack ? error.stack : String(error);
400
+ rootElement.append(pre);
401
+ report();
402
+ }
403
+ try {
404
+ const source = JSON.parse(document.getElementById("artifacty-react-source").textContent);
405
+ const transformed = Babel.transform(source, {
406
+ filename: "artifact.jsx",
407
+ presets: [
408
+ ["typescript", { allExtensions: true, isTSX: true }],
409
+ ["react", { runtime: "classic" }]
410
+ ],
411
+ plugins: ["transform-modules-commonjs"]
412
+ }).code;
413
+ const module = { exports: {} };
414
+ const exports = module.exports;
415
+ const require = function(name) {
416
+ if (name === "react") return React;
417
+ if (name === "react-dom") return ReactDOM;
418
+ throw new Error("Unsupported import in React artifact: " + name);
419
+ };
420
+ new Function("React", "ReactDOM", "module", "exports", "require", transformed)(React, ReactDOM, module, exports, require);
421
+ const Component = module.exports.default || exports.default || module.exports;
422
+ if (typeof Component !== "function") {
423
+ throw new Error("React artifact must export a component as default.");
424
+ }
425
+ if (ReactDOM.createRoot) {
426
+ ReactDOM.createRoot(rootElement).render(React.createElement(Component, {}));
427
+ } else {
428
+ ReactDOM.render(React.createElement(Component, {}), rootElement);
429
+ }
430
+ window.addEventListener("resize", report);
431
+ if (window.ResizeObserver) {
432
+ try { new ResizeObserver(report).observe(document.documentElement); } catch (error) {}
433
+ }
434
+ setTimeout(report, 0);
435
+ setTimeout(report, 250);
436
+ } catch (error) {
437
+ showError(error);
438
+ }
439
+ </script>
440
+ </body>
441
+ </html>`;
442
+ }
443
+
333
444
  export function pageShell({ title, body, head = "", afterBody = "", locale = DEFAULT_LOCALE }) {
334
445
  return `<!doctype html>
335
446
  <html lang="${escapeAttribute(locale)}">
@@ -791,11 +902,18 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
791
902
  .badge.t-diff-walkthrough { --bh: #14b8a6; }
792
903
  .badge.t-bundle { --bh: #8a8d98; }
793
904
  .badge.t-asset { --bh: #f59e0b; }
905
+ .badge.t-diagram { --bh: #0ea5e9; }
906
+ .badge.t-component { --bh: #7c3aed; }
907
+ .badge.t-snippet { --bh: #64748b; }
794
908
  .badge.t-unknown { --bh: #94a3b8; }
795
909
  .badge.f-html { --bh: #e0795b; }
796
910
  .badge.f-markdown { --bh: #3b82f6; }
797
911
  .badge.f-text { --bh: #5b6b7f; }
798
912
  .badge.f-json { --bh: #16a34a; }
913
+ .badge.f-code { --bh: #64748b; }
914
+ .badge.f-svg { --bh: #0ea5e9; }
915
+ .badge.f-mermaid { --bh: #14b8a6; }
916
+ .badge.f-react { --bh: #7c3aed; }
799
917
  .badge.s-archived { --bh: #94a3b8; }
800
918
  .empty {
801
919
  padding: 28px;
@@ -920,6 +1038,44 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
920
1038
  background: var(--panel-2);
921
1039
  font-size: 0.88em;
922
1040
  }
1041
+ .artifact-table-scroll {
1042
+ width: 100%;
1043
+ margin: 20px 0;
1044
+ overflow-x: auto;
1045
+ border: 1px solid var(--line);
1046
+ border-radius: 10px;
1047
+ background: var(--bg);
1048
+ }
1049
+ .artifact-table {
1050
+ width: 100%;
1051
+ min-width: max-content;
1052
+ border-collapse: collapse;
1053
+ font-size: 14px;
1054
+ line-height: 1.45;
1055
+ }
1056
+ .artifact-table th,
1057
+ .artifact-table td {
1058
+ padding: 10px 12px;
1059
+ border-bottom: 1px solid var(--line);
1060
+ border-right: 1px solid var(--line);
1061
+ vertical-align: top;
1062
+ text-align: left;
1063
+ white-space: normal;
1064
+ }
1065
+ .artifact-table th:last-child,
1066
+ .artifact-table td:last-child {
1067
+ border-right: 0;
1068
+ }
1069
+ .artifact-table tbody tr:last-child td {
1070
+ border-bottom: 0;
1071
+ }
1072
+ .artifact-table th {
1073
+ background: var(--panel-2);
1074
+ color: var(--text);
1075
+ font-weight: 650;
1076
+ }
1077
+ .artifact-table .align-center { text-align: center; }
1078
+ .artifact-table .align-right { text-align: right; }
923
1079
  .artifact-code {
924
1080
  padding: 22px;
925
1081
  overflow: auto;
@@ -929,6 +1085,39 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
929
1085
  white-space: pre-wrap;
930
1086
  overflow-wrap: anywhere;
931
1087
  }
1088
+ .artifact-code-viewer {
1089
+ border: 1px solid var(--line);
1090
+ border-radius: 12px;
1091
+ overflow: hidden;
1092
+ background: var(--code);
1093
+ }
1094
+ .artifact-code-viewer .cm-editor {
1095
+ min-height: 50vh;
1096
+ background: var(--code);
1097
+ color: var(--code-text);
1098
+ }
1099
+ .artifact-code-viewer .cm-scroller {
1100
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
1101
+ font-size: 13px;
1102
+ line-height: 1.55;
1103
+ }
1104
+ .artifact-code-viewer .cm-gutters {
1105
+ background: color-mix(in srgb, var(--code) 84%, white);
1106
+ color: var(--muted);
1107
+ border-color: rgba(255, 255, 255, 0.08);
1108
+ }
1109
+ .artifact-react-disabled {
1110
+ display: grid;
1111
+ gap: 12px;
1112
+ }
1113
+ .artifact-react-disabled > p {
1114
+ margin: 0;
1115
+ padding: 12px 14px;
1116
+ border: 1px solid var(--line);
1117
+ border-radius: 8px;
1118
+ background: var(--panel-2);
1119
+ color: var(--muted);
1120
+ }
932
1121
  code,
933
1122
  pre {
934
1123
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
@@ -972,7 +1161,7 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
972
1161
 
973
1162
  function formatSelect(selected) {
974
1163
  return `<select name="format">
975
- ${["markdown", "html", "text", "json"].map((format) => {
1164
+ ${ARTIFACT_FORMATS.map((format) => {
976
1165
  const label = format[0].toUpperCase() + format.slice(1);
977
1166
  return `<option value="${format}"${format === selected ? " selected" : ""}>${label}</option>`;
978
1167
  }).join("")}
@@ -980,9 +1169,8 @@ function formatSelect(selected) {
980
1169
  }
981
1170
 
982
1171
  function artifactTypeSelect(selected) {
983
- const types = ["document", "html-page", "handoff", "code-review", "test-report", "dashboard", "design-option", "diff-walkthrough", "bundle", "asset", "unknown"];
984
1172
  return `<select name="artifactType">
985
- ${types.map((type) => `<option value="${type}"${type === selected ? " selected" : ""}>${escapeHtml(type)}</option>`).join("")}
1173
+ ${ARTIFACT_TYPES.map((type) => `<option value="${type}"${type === selected ? " selected" : ""}>${escapeHtml(type)}</option>`).join("")}
986
1174
  </select>`;
987
1175
  }
988
1176
 
@@ -1065,10 +1253,92 @@ function htmlFrameContent(content) {
1065
1253
  return `${content}${reporter}`;
1066
1254
  }
1067
1255
 
1256
+ function svgFrameContent(content) {
1257
+ const sanitized = sanitizeSvg(content);
1258
+ return `<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;min-height:100%;background:#fff;}body{display:grid;place-items:center;padding:16px;box-sizing:border-box;}svg{max-width:100%;height:auto;}</style></head><body>${sanitized}</body></html>`;
1259
+ }
1260
+
1261
+ function mermaidFrameContent(content) {
1262
+ const source = jsonForScript(content);
1263
+ return `<!doctype html>
1264
+ <html>
1265
+ <head>
1266
+ <meta charset="utf-8">
1267
+ <style>
1268
+ html, body { margin: 0; min-height: 100%; background: #fff; color: #111827; }
1269
+ body { padding: 16px; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
1270
+ #artifacty-mermaid { display: grid; place-items: center; min-height: 240px; }
1271
+ #artifacty-mermaid svg { max-width: 100%; height: auto; }
1272
+ .error { white-space: pre-wrap; color: #991b1b; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 12px; }
1273
+ </style>
1274
+ </head>
1275
+ <body>
1276
+ <div id="artifacty-mermaid"></div>
1277
+ <script type="application/json" id="artifacty-mermaid-source">${source}</script>
1278
+ <script type="module">
1279
+ import mermaid from "/vendor/npm/mermaid/dist/mermaid.esm.min.mjs";
1280
+ const source = JSON.parse(document.getElementById("artifacty-mermaid-source").textContent);
1281
+ const target = document.getElementById("artifacty-mermaid");
1282
+ function report() {
1283
+ const doc = document.documentElement;
1284
+ const body = document.body;
1285
+ const height = Math.max(doc.scrollHeight, doc.offsetHeight, body ? body.scrollHeight : 0, body ? body.offsetHeight : 0);
1286
+ parent.postMessage({ __artifactyHeight: height }, "*");
1287
+ }
1288
+ try {
1289
+ mermaid.initialize({ startOnLoad: false, securityLevel: "strict" });
1290
+ const result = await mermaid.render("artifacty-mermaid-svg", source);
1291
+ target.innerHTML = result.svg;
1292
+ } catch (error) {
1293
+ target.innerHTML = "";
1294
+ const pre = document.createElement("pre");
1295
+ pre.className = "error";
1296
+ pre.textContent = error && error.message ? error.message : String(error);
1297
+ target.append(pre);
1298
+ }
1299
+ window.addEventListener("resize", report);
1300
+ if (window.ResizeObserver) {
1301
+ try { new ResizeObserver(report).observe(document.documentElement); } catch (error) {}
1302
+ }
1303
+ report();
1304
+ setTimeout(report, 250);
1305
+ </script>
1306
+ </body>
1307
+ </html>`;
1308
+ }
1309
+
1310
+ function sanitizeSvg(content) {
1311
+ return String(content || "")
1312
+ .replace(/<script\b[\s\S]*?<\/script>/gi, "")
1313
+ .replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "")
1314
+ .replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "")
1315
+ .replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "")
1316
+ .replace(/\s(?:href|xlink:href)\s*=\s*"javascript:[^"]*"/gi, "")
1317
+ .replace(/\s(?:href|xlink:href)\s*=\s*'javascript:[^']*'/gi, "")
1318
+ .replace(/\s(?:href|xlink:href)\s*=\s*javascript:[^\s>]+/gi, "");
1319
+ }
1320
+
1321
+ function jsonForScript(value) {
1322
+ return JSON.stringify(String(value || ""))
1323
+ .replaceAll("<", "\\u003c")
1324
+ .replaceAll(">", "\\u003e")
1325
+ .replaceAll("&", "\\u0026")
1326
+ .replaceAll("\\u2028", "\\\\u2028")
1327
+ .replaceAll("\\u2029", "\\\\u2029");
1328
+ }
1329
+
1068
1330
  function frameResizeScript() {
1069
1331
  return `<script>(function(){var frame=document.querySelector(".artifact-frame");if(!frame){return;}window.addEventListener("message",function(event){if(event.source!==frame.contentWindow){return;}var data=event.data;if(!data||typeof data.__artifactyHeight!=="number"){return;}var height=Math.min(Math.max(Math.ceil(data.__artifactyHeight),200),200000);frame.style.height=height+"px";});})();</script>`;
1070
1332
  }
1071
1333
 
1334
+ function viewerScript() {
1335
+ return `<script type="module" src="${VIEWER_CLIENT_PATH}"></script>`;
1336
+ }
1337
+
1338
+ function reactRendererEnabled() {
1339
+ return process.env.ARTIFACTY_ENABLE_REACT_RENDERER === "true";
1340
+ }
1341
+
1072
1342
  function languageSwitcher(view) {
1073
1343
  const english = view.locale === "en"
1074
1344
  ? `<span class="active">${view.text("language.english")}</span>`
@@ -1112,7 +1382,8 @@ function markdownToHtml(markdown) {
1112
1382
  }
1113
1383
  };
1114
1384
 
1115
- for (const line of lines) {
1385
+ for (let index = 0; index < lines.length; index += 1) {
1386
+ const line = lines[index];
1116
1387
  if (line.startsWith("```")) {
1117
1388
  flushParagraph();
1118
1389
  closeList();
@@ -1137,6 +1408,15 @@ function markdownToHtml(markdown) {
1137
1408
  continue;
1138
1409
  }
1139
1410
 
1411
+ if (isMarkdownTableStart(lines, index)) {
1412
+ flushParagraph();
1413
+ closeList();
1414
+ const { html: tableHtml, nextIndex } = renderMarkdownTable(lines, index);
1415
+ html.push(tableHtml);
1416
+ index = nextIndex;
1417
+ continue;
1418
+ }
1419
+
1140
1420
  const heading = /^(#{1,3})\s+(.+)$/.exec(line);
1141
1421
  if (heading) {
1142
1422
  flushParagraph();
@@ -1172,6 +1452,66 @@ function inlineMarkdown(value) {
1172
1452
  return escapeHtml(value).replace(/`([^`]+)`/g, "<code>$1</code>");
1173
1453
  }
1174
1454
 
1455
+ function isMarkdownTableStart(lines, index) {
1456
+ return isMarkdownTableRow(lines[index]) && isMarkdownTableSeparator(lines[index + 1]);
1457
+ }
1458
+
1459
+ function renderMarkdownTable(lines, startIndex) {
1460
+ const headers = splitMarkdownTableRow(lines[startIndex]);
1461
+ const alignments = splitMarkdownTableRow(lines[startIndex + 1]).map(tableAlignment);
1462
+ const rows = [];
1463
+ let index = startIndex + 2;
1464
+
1465
+ for (; index < lines.length; index += 1) {
1466
+ if (!isMarkdownTableRow(lines[index])) {
1467
+ break;
1468
+ }
1469
+ rows.push(splitMarkdownTableRow(lines[index]));
1470
+ }
1471
+
1472
+ const headerHtml = headers.map((cell, cellIndex) =>
1473
+ `<th${alignmentAttribute(alignments[cellIndex])}>${inlineMarkdown(cell)}</th>`
1474
+ ).join("");
1475
+ const bodyHtml = rows.map((row) => `<tr>${headers.map((_, cellIndex) => {
1476
+ const alignment = alignments[cellIndex];
1477
+ return `<td${alignmentAttribute(alignment)}>${inlineMarkdown(row[cellIndex] || "")}</td>`;
1478
+ }).join("")}</tr>`).join("\n");
1479
+
1480
+ return {
1481
+ html: `<div class="artifact-table-scroll"><table class="artifact-table"><thead><tr>${headerHtml}</tr></thead><tbody>${bodyHtml}</tbody></table></div>`,
1482
+ nextIndex: index - 1
1483
+ };
1484
+ }
1485
+
1486
+ function isMarkdownTableRow(line = "") {
1487
+ return /^\s*\|.*\|\s*$/.test(line);
1488
+ }
1489
+
1490
+ function isMarkdownTableSeparator(line = "") {
1491
+ const cells = splitMarkdownTableRow(line);
1492
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
1493
+ }
1494
+
1495
+ function splitMarkdownTableRow(line = "") {
1496
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
1497
+ return trimmed.split("|").map((cell) => cell.trim());
1498
+ }
1499
+
1500
+ function tableAlignment(cell = "") {
1501
+ const trimmed = cell.trim();
1502
+ if (trimmed.startsWith(":") && trimmed.endsWith(":")) {
1503
+ return "center";
1504
+ }
1505
+ if (trimmed.endsWith(":")) {
1506
+ return "right";
1507
+ }
1508
+ return "left";
1509
+ }
1510
+
1511
+ function alignmentAttribute(alignment) {
1512
+ return alignment ? ` class="align-${alignment}"` : "";
1513
+ }
1514
+
1175
1515
  function formatJson(content) {
1176
1516
  try {
1177
1517
  return JSON.stringify(JSON.parse(content), null, 2);
@@ -9,6 +9,16 @@ import { assertNoSecrets, securityConfig } from "./security.js";
9
9
  export const STORE_VERSION = 3;
10
10
  export const ARTIFACT_SCHEMA_VERSION = 1;
11
11
  export const MAX_ARTIFACT_BYTES = 16 * 1024 * 1024;
12
+ export const ARTIFACT_FORMATS = [
13
+ "html",
14
+ "markdown",
15
+ "text",
16
+ "json",
17
+ "code",
18
+ "svg",
19
+ "mermaid",
20
+ "react"
21
+ ];
12
22
  export const ARTIFACT_TYPES = [
13
23
  "document",
14
24
  "html-page",
@@ -20,6 +30,9 @@ export const ARTIFACT_TYPES = [
20
30
  "diff-walkthrough",
21
31
  "bundle",
22
32
  "asset",
33
+ "diagram",
34
+ "component",
35
+ "snippet",
23
36
  "unknown"
24
37
  ];
25
38
 
@@ -27,14 +40,22 @@ const FORMAT_TO_EXTENSION = {
27
40
  html: "html",
28
41
  markdown: "md",
29
42
  text: "txt",
30
- json: "json"
43
+ json: "json",
44
+ code: "code",
45
+ svg: "svg",
46
+ mermaid: "mmd",
47
+ react: "jsx"
31
48
  };
32
49
 
33
50
  const FORMAT_TO_CONTENT_TYPE = {
34
51
  html: "text/html; charset=utf-8",
35
52
  markdown: "text/markdown; charset=utf-8",
36
53
  text: "text/plain; charset=utf-8",
37
- json: "application/json; charset=utf-8"
54
+ json: "application/json; charset=utf-8",
55
+ code: "text/x-source-code; charset=utf-8",
56
+ svg: "image/svg+xml; charset=utf-8",
57
+ mermaid: "text/vnd.mermaid; charset=utf-8",
58
+ react: "text/jsx; charset=utf-8"
38
59
  };
39
60
 
40
61
  export function createStore(options = {}) {
@@ -368,7 +389,16 @@ export function normalizeFormat(value = "text") {
368
389
  if (normalized === "md") {
369
390
  return "markdown";
370
391
  }
371
- if (normalized === "html" || normalized === "markdown" || normalized === "text" || normalized === "json") {
392
+ if (normalized === "svg+xml") {
393
+ return "svg";
394
+ }
395
+ if (normalized === "mmd") {
396
+ return "mermaid";
397
+ }
398
+ if (normalized === "jsx" || normalized === "tsx") {
399
+ return "react";
400
+ }
401
+ if (ARTIFACT_FORMATS.includes(normalized)) {
372
402
  return normalized;
373
403
  }
374
404
  throw Object.assign(new Error(`Unsupported artifact format: ${value}`), {
@@ -757,10 +787,24 @@ function normalizeSchemaVersion(value) {
757
787
  }
758
788
 
759
789
  function inferArtifactType(input) {
760
- const format = normalizeOptionalString(input.format || inferFormat(input.contentType));
790
+ let format;
791
+ try {
792
+ format = normalizeFormat(input.format || inferFormat(input.contentType));
793
+ } catch {
794
+ format = normalizeOptionalString(input.format || inferFormat(input.contentType));
795
+ }
761
796
  if (format === "html") {
762
797
  return "html-page";
763
798
  }
799
+ if (format === "svg" || format === "mermaid") {
800
+ return "diagram";
801
+ }
802
+ if (format === "react") {
803
+ return "component";
804
+ }
805
+ if (format === "code") {
806
+ return "snippet";
807
+ }
764
808
  return "document";
765
809
  }
766
810
 
@@ -787,6 +831,18 @@ function normalizeOptionalString(value) {
787
831
 
788
832
  function inferFormat(contentType) {
789
833
  const value = normalizeOptionalString(contentType).toLowerCase();
834
+ if (value.includes("vnd.ant.code") || value.includes("source-code")) {
835
+ return "code";
836
+ }
837
+ if (value.includes("svg")) {
838
+ return "svg";
839
+ }
840
+ if (value.includes("vnd.ant.mermaid") || value.includes("mermaid")) {
841
+ return "mermaid";
842
+ }
843
+ if (value.includes("vnd.ant.react") || value.includes("jsx")) {
844
+ return "react";
845
+ }
790
846
  if (value.includes("html")) {
791
847
  return "html";
792
848
  }
@@ -0,0 +1,25 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ export function generateToken(options = {}) {
4
+ const bytes = normalizeTokenBytes(options.bytes);
5
+ const token = randomBytes(bytes).toString("base64url");
6
+ return {
7
+ token,
8
+ bytes,
9
+ env: `ARTIFACTY_API_TOKEN=${quoteShellValue(token)}`,
10
+ header: `x-artifacty-token: ${token}`,
11
+ authorization: `Authorization: Bearer ${token}`
12
+ };
13
+ }
14
+
15
+ export function normalizeTokenBytes(value) {
16
+ const bytes = Number(value ?? 32);
17
+ if (!Number.isInteger(bytes) || bytes < 16 || bytes > 128) {
18
+ throw new Error("--bytes must be an integer between 16 and 128");
19
+ }
20
+ return bytes;
21
+ }
22
+
23
+ function quoteShellValue(value) {
24
+ return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
25
+ }