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.
- package/README.md +142 -33
- package/docs/artifact-schema-v1.md +26 -1
- package/docs/integrations.md +84 -8
- package/docs/network-sharing.md +41 -0
- package/docs/release-checklist.md +13 -0
- package/docs/sarif-csv-artifact-plan.md +65 -0
- package/package.json +15 -3
- package/scripts/smoke.sh +2 -1
- package/src/cli.js +77 -8
- package/src/client/editor.js +35 -2
- package/src/client/viewer.js +67 -0
- package/src/lib/background.js +344 -0
- package/src/lib/converters.js +448 -10
- package/src/lib/editor-assets.js +45 -2
- package/src/lib/installer.js +13 -4
- package/src/lib/render.js +348 -8
- package/src/lib/storage.js +60 -4
- package/src/lib/token.js +25 -0
- package/src/mcp-server.js +8 -6
- package/src/server.js +104 -25
package/src/lib/installer.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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 (
|
|
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);
|
package/src/lib/storage.js
CHANGED
|
@@ -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 === "
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/token.js
ADDED
|
@@ -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
|
+
}
|