artifacty 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/README.md +34 -8
- package/docs/artifact-schema-v1.md +27 -5
- package/docs/integrations.md +21 -4
- package/docs/sarif-csv-artifact-plan.md +44 -65
- package/package.json +1 -1
- package/src/cli.js +62 -10
- package/src/client/editor.js +76 -4
- package/src/lib/converters.js +429 -44
- package/src/lib/i18n.js +8 -0
- package/src/lib/installer.js +58 -4
- package/src/lib/render.js +492 -4
- package/src/lib/storage.js +490 -33
- package/src/mcp-server.js +18 -8
- package/src/server.js +69 -10
package/src/lib/render.js
CHANGED
|
@@ -2,17 +2,26 @@ import { EDITOR_CLIENT_PATH, VIEWER_CLIENT_PATH, editorImportMapJson } from "./e
|
|
|
2
2
|
import { createI18n, DEFAULT_LOCALE, editorMessages, localizedHref, switchLocaleHref } from "./i18n.js";
|
|
3
3
|
import { ARTIFACT_FORMATS, ARTIFACT_TYPES } from "./storage.js";
|
|
4
4
|
|
|
5
|
-
export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
5
|
+
export function renderDashboard({ artifacts, baseUrl, filters = {}, pagination, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
6
6
|
const view = viewContext(locale, currentPath);
|
|
7
|
+
const total = pagination?.total ?? artifacts.length;
|
|
8
|
+
const start = artifacts.length ? (pagination?.offset ?? 0) + 1 : 0;
|
|
9
|
+
const end = artifacts.length ? (pagination?.offset ?? 0) + artifacts.length : 0;
|
|
10
|
+
const searchBackend = pagination?.search?.backend;
|
|
11
|
+
const pager = renderDashboardPager({ pagination, filters, view });
|
|
7
12
|
const rows = artifacts
|
|
8
13
|
.map((artifact) => {
|
|
9
14
|
const tags = artifact.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
|
|
10
15
|
const status = artifact.archivedAt ? statusBadge("archived") : "";
|
|
16
|
+
const snippet = artifact.searchSnippet
|
|
17
|
+
? `<span class="row-snippet">${escapeHtml(artifact.searchSnippet)}</span>`
|
|
18
|
+
: "";
|
|
11
19
|
return `
|
|
12
20
|
<a class="artifact-row" href="${view.href(`/artifacts/${encodeURIComponent(artifact.id)}`)}">
|
|
13
21
|
<span class="row-main">
|
|
14
22
|
<strong>${escapeHtml(artifact.title)}</strong>
|
|
15
23
|
<span>${escapeHtml(artifact.id)}</span>
|
|
24
|
+
${snippet}
|
|
16
25
|
</span>
|
|
17
26
|
<span>${escapeHtml(artifact.sourceAgent)}</span>
|
|
18
27
|
${typeBadge(artifact.artifactType || "document")}
|
|
@@ -44,10 +53,12 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEF
|
|
|
44
53
|
</header>
|
|
45
54
|
<main class="dashboard">
|
|
46
55
|
<section class="toolbar">
|
|
47
|
-
<span>${view.text("dashboard.
|
|
56
|
+
<span>${view.text("dashboard.range", { start, end, total })}</span>
|
|
57
|
+
${searchBackend ? `<span>${view.text("dashboard.searchBackend", { backend: searchBackend })}</span>` : ""}
|
|
48
58
|
</section>
|
|
49
59
|
<form class="filter-form" method="get" action="/">
|
|
50
60
|
${localeInput(view.locale)}
|
|
61
|
+
${filters.limit ? `<input type="hidden" name="limit" value="${escapeAttribute(filters.limit)}">` : ""}
|
|
51
62
|
<input name="q" value="${escapeAttribute(filters.query || "")}" placeholder="${view.attr("filter.search")}">
|
|
52
63
|
<input name="tag" value="${escapeAttribute(filters.tag || "")}" placeholder="${view.attr("filter.tag")}">
|
|
53
64
|
<input name="sourceAgent" value="${escapeAttribute(filters.sourceAgent || "")}" placeholder="${view.attr("filter.source")}">
|
|
@@ -58,12 +69,52 @@ export function renderDashboard({ artifacts, baseUrl, filters = {}, locale = DEF
|
|
|
58
69
|
<section class="artifact-list">
|
|
59
70
|
${rows || `<div class="empty">${view.text("dashboard.empty")}</div>`}
|
|
60
71
|
</section>
|
|
72
|
+
${pager}
|
|
61
73
|
</main>
|
|
62
74
|
`,
|
|
63
75
|
locale: view.locale
|
|
64
76
|
});
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
function renderDashboardPager({ pagination, filters, view }) {
|
|
80
|
+
if (!pagination || pagination.total <= pagination.limit) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const previous = pagination.previousOffset === null
|
|
85
|
+
? `<span class="pager-disabled">${view.text("dashboard.previous")}</span>`
|
|
86
|
+
: `<a href="${view.href(dashboardPageHref(filters, pagination.previousOffset))}">${view.text("dashboard.previous")}</a>`;
|
|
87
|
+
const next = pagination.nextOffset === null
|
|
88
|
+
? `<span class="pager-disabled">${view.text("dashboard.next")}</span>`
|
|
89
|
+
: `<a href="${view.href(dashboardPageHref(filters, pagination.nextOffset))}">${view.text("dashboard.next")}</a>`;
|
|
90
|
+
|
|
91
|
+
return `<nav class="pager" aria-label="Pagination">${previous}${next}</nav>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function dashboardPageHref(filters, offset) {
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
if (filters.query) {
|
|
97
|
+
params.set("q", filters.query);
|
|
98
|
+
}
|
|
99
|
+
if (filters.tag) {
|
|
100
|
+
params.set("tag", filters.tag);
|
|
101
|
+
}
|
|
102
|
+
if (filters.sourceAgent) {
|
|
103
|
+
params.set("sourceAgent", filters.sourceAgent);
|
|
104
|
+
}
|
|
105
|
+
if (filters.includeArchived) {
|
|
106
|
+
params.set("includeArchived", "true");
|
|
107
|
+
}
|
|
108
|
+
if (filters.limit) {
|
|
109
|
+
params.set("limit", filters.limit);
|
|
110
|
+
}
|
|
111
|
+
if (offset > 0) {
|
|
112
|
+
params.set("offset", String(offset));
|
|
113
|
+
}
|
|
114
|
+
const query = params.toString();
|
|
115
|
+
return query ? `/?${query}` : "/";
|
|
116
|
+
}
|
|
117
|
+
|
|
67
118
|
export function renderArtifactFormPage({ mode, baseUrl, artifact, version, content, authToken = "", locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
68
119
|
const view = viewContext(locale, currentPath);
|
|
69
120
|
const isEdit = mode === "edit";
|
|
@@ -163,6 +214,8 @@ export function renderImportArtifactPage({ baseUrl, authToken = "", locale = DEF
|
|
|
163
214
|
<option value="claude">Claude</option>
|
|
164
215
|
<option value="codex">Codex</option>
|
|
165
216
|
<option value="gemini">Gemini</option>
|
|
217
|
+
<option value="copilot">GitHub Copilot</option>
|
|
218
|
+
<option value="cursor">Cursor</option>
|
|
166
219
|
<option value="generic">Generic</option>
|
|
167
220
|
</select>
|
|
168
221
|
</label>
|
|
@@ -208,8 +261,10 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
|
|
|
208
261
|
const rendered = renderContent(version.format, content, version.metadata || {}, {
|
|
209
262
|
reactFrameUrl: reactRendererEnabled()
|
|
210
263
|
? view.href(`/artifacts/${encodeURIComponent(artifact.id)}/react-frame?version=${version.version}`)
|
|
211
|
-
: ""
|
|
264
|
+
: "",
|
|
265
|
+
rawUrl: view.href(`/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${version.version}`)
|
|
212
266
|
});
|
|
267
|
+
const viewClass = artifactViewClass({ artifact, version });
|
|
213
268
|
const needsViewerScript = version.format === "code";
|
|
214
269
|
const rawUrl = `/artifacts/${encodeURIComponent(artifact.id)}/raw?version=${version.version}`;
|
|
215
270
|
const archiveAction = artifact.archivedAt ? "restore" : "archive";
|
|
@@ -231,7 +286,7 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
|
|
|
231
286
|
${languageSwitcher(view)}
|
|
232
287
|
</nav>
|
|
233
288
|
</header>
|
|
234
|
-
<main class="
|
|
289
|
+
<main class="${viewClass}">
|
|
235
290
|
<section class="meta-strip">
|
|
236
291
|
${formatBadge(version.format)}
|
|
237
292
|
${typeBadge(artifact.artifactType || "document")}
|
|
@@ -256,6 +311,16 @@ export function renderArtifactPage({ artifact, version, content, baseUrl, authTo
|
|
|
256
311
|
});
|
|
257
312
|
}
|
|
258
313
|
|
|
314
|
+
function artifactViewClass({ artifact, version }) {
|
|
315
|
+
const wideFormats = new Set(["html", "svg", "mermaid", "react", "sarif", "csv", "image", "video"]);
|
|
316
|
+
const wideTypes = new Set(["dashboard", "design-option", "diff-walkthrough"]);
|
|
317
|
+
const classes = ["artifact-view"];
|
|
318
|
+
if (wideFormats.has(version.format) || wideTypes.has(artifact.artifactType)) {
|
|
319
|
+
classes.push("artifact-view-wide");
|
|
320
|
+
}
|
|
321
|
+
return classes.join(" ");
|
|
322
|
+
}
|
|
323
|
+
|
|
259
324
|
export function renderDiffPage({ artifact, fromVersion, toVersion, fromContent, toContent, diffRows, baseUrl, locale = DEFAULT_LOCALE, currentPath = "/" }) {
|
|
260
325
|
const view = viewContext(locale, currentPath);
|
|
261
326
|
const title = view.raw("diff.title", { title: artifact.title });
|
|
@@ -342,6 +407,22 @@ export function renderContent(format, content, metadata = {}, options = {}) {
|
|
|
342
407
|
return `<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>`;
|
|
343
408
|
}
|
|
344
409
|
|
|
410
|
+
if (format === "sarif") {
|
|
411
|
+
return renderSarif(content);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (format === "csv") {
|
|
415
|
+
return renderCsv(content);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (format === "image") {
|
|
419
|
+
return renderMediaArtifact("image", content, metadata, options);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (format === "video") {
|
|
423
|
+
return renderMediaArtifact("video", content, metadata, options);
|
|
424
|
+
}
|
|
425
|
+
|
|
345
426
|
if (format === "code") {
|
|
346
427
|
const language = metadata.language || metadata.artifactyImport?.language || "";
|
|
347
428
|
return `<section class="artifact-code-viewer" data-artifacty-code-viewer data-language="${escapeAttribute(language)}">
|
|
@@ -598,6 +679,9 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
598
679
|
width: min(1180px, calc(100vw - 32px));
|
|
599
680
|
margin: 28px auto 72px;
|
|
600
681
|
}
|
|
682
|
+
.artifact-view-wide {
|
|
683
|
+
width: min(1760px, calc(100vw - 24px));
|
|
684
|
+
}
|
|
601
685
|
.toolbar {
|
|
602
686
|
font-family: var(--mono);
|
|
603
687
|
font-size: 12px;
|
|
@@ -672,6 +756,34 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
672
756
|
background: var(--panel);
|
|
673
757
|
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
674
758
|
}
|
|
759
|
+
.pager {
|
|
760
|
+
display: flex;
|
|
761
|
+
justify-content: flex-end;
|
|
762
|
+
gap: 8px;
|
|
763
|
+
margin-top: 14px;
|
|
764
|
+
font-family: var(--mono);
|
|
765
|
+
font-size: 12.5px;
|
|
766
|
+
}
|
|
767
|
+
.pager a,
|
|
768
|
+
.pager-disabled {
|
|
769
|
+
display: inline-flex;
|
|
770
|
+
min-height: 34px;
|
|
771
|
+
align-items: center;
|
|
772
|
+
justify-content: center;
|
|
773
|
+
padding: 5px 12px;
|
|
774
|
+
border: 1px solid var(--line-2);
|
|
775
|
+
border-radius: 8px;
|
|
776
|
+
background: var(--panel);
|
|
777
|
+
color: var(--muted);
|
|
778
|
+
}
|
|
779
|
+
.pager a:hover {
|
|
780
|
+
border-color: var(--accent);
|
|
781
|
+
color: var(--text);
|
|
782
|
+
text-decoration: none;
|
|
783
|
+
}
|
|
784
|
+
.pager-disabled {
|
|
785
|
+
opacity: 0.55;
|
|
786
|
+
}
|
|
675
787
|
.editor-form {
|
|
676
788
|
display: grid;
|
|
677
789
|
gap: 16px;
|
|
@@ -856,6 +968,13 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
856
968
|
font-size: 12px;
|
|
857
969
|
color: var(--faint);
|
|
858
970
|
}
|
|
971
|
+
.row-main .row-snippet {
|
|
972
|
+
color: var(--muted);
|
|
973
|
+
white-space: normal;
|
|
974
|
+
overflow: visible;
|
|
975
|
+
text-overflow: clip;
|
|
976
|
+
overflow-wrap: anywhere;
|
|
977
|
+
}
|
|
859
978
|
.artifact-row > span:not(.row-main):not(.tags):not(.badge) {
|
|
860
979
|
font-family: var(--mono);
|
|
861
980
|
font-size: 12.5px;
|
|
@@ -905,6 +1024,8 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
905
1024
|
.badge.t-diagram { --bh: #0ea5e9; }
|
|
906
1025
|
.badge.t-component { --bh: #7c3aed; }
|
|
907
1026
|
.badge.t-snippet { --bh: #64748b; }
|
|
1027
|
+
.badge.t-analysis-report { --bh: #dc2626; }
|
|
1028
|
+
.badge.t-table { --bh: #0891b2; }
|
|
908
1029
|
.badge.t-unknown { --bh: #94a3b8; }
|
|
909
1030
|
.badge.f-html { --bh: #e0795b; }
|
|
910
1031
|
.badge.f-markdown { --bh: #3b82f6; }
|
|
@@ -914,6 +1035,10 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
914
1035
|
.badge.f-svg { --bh: #0ea5e9; }
|
|
915
1036
|
.badge.f-mermaid { --bh: #14b8a6; }
|
|
916
1037
|
.badge.f-react { --bh: #7c3aed; }
|
|
1038
|
+
.badge.f-sarif { --bh: #dc2626; }
|
|
1039
|
+
.badge.f-csv { --bh: #0891b2; }
|
|
1040
|
+
.badge.f-image { --bh: #f59e0b; }
|
|
1041
|
+
.badge.f-video { --bh: #db2777; }
|
|
917
1042
|
.badge.s-archived { --bh: #94a3b8; }
|
|
918
1043
|
.empty {
|
|
919
1044
|
padding: 28px;
|
|
@@ -1076,6 +1201,105 @@ export function pageShell({ title, body, head = "", afterBody = "", locale = DEF
|
|
|
1076
1201
|
}
|
|
1077
1202
|
.artifact-table .align-center { text-align: center; }
|
|
1078
1203
|
.artifact-table .align-right { text-align: right; }
|
|
1204
|
+
.artifact-csv,
|
|
1205
|
+
.artifact-sarif {
|
|
1206
|
+
display: grid;
|
|
1207
|
+
gap: 14px;
|
|
1208
|
+
}
|
|
1209
|
+
.artifact-csv-note,
|
|
1210
|
+
.artifact-sarif-note {
|
|
1211
|
+
margin: 0;
|
|
1212
|
+
padding: 10px 12px;
|
|
1213
|
+
border: 1px solid var(--line);
|
|
1214
|
+
border-radius: 8px;
|
|
1215
|
+
background: var(--panel);
|
|
1216
|
+
color: var(--muted);
|
|
1217
|
+
font-size: 13px;
|
|
1218
|
+
}
|
|
1219
|
+
.artifact-summary-grid {
|
|
1220
|
+
display: grid;
|
|
1221
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
1222
|
+
gap: 10px;
|
|
1223
|
+
}
|
|
1224
|
+
.summary-card {
|
|
1225
|
+
display: grid;
|
|
1226
|
+
gap: 3px;
|
|
1227
|
+
min-height: 72px;
|
|
1228
|
+
padding: 12px;
|
|
1229
|
+
border: 1px solid var(--line);
|
|
1230
|
+
border-radius: 8px;
|
|
1231
|
+
background: var(--panel);
|
|
1232
|
+
}
|
|
1233
|
+
.summary-card span {
|
|
1234
|
+
color: var(--muted);
|
|
1235
|
+
font-family: var(--mono);
|
|
1236
|
+
font-size: 11.5px;
|
|
1237
|
+
text-transform: uppercase;
|
|
1238
|
+
letter-spacing: 0.06em;
|
|
1239
|
+
}
|
|
1240
|
+
.summary-card strong {
|
|
1241
|
+
font-size: 20px;
|
|
1242
|
+
line-height: 1.2;
|
|
1243
|
+
}
|
|
1244
|
+
.sarif-level {
|
|
1245
|
+
display: inline-flex;
|
|
1246
|
+
min-width: 68px;
|
|
1247
|
+
justify-content: center;
|
|
1248
|
+
padding: 2px 8px;
|
|
1249
|
+
border-radius: 999px;
|
|
1250
|
+
background: color-mix(in srgb, var(--bh, var(--muted)) 14%, transparent);
|
|
1251
|
+
color: color-mix(in srgb, var(--bh, var(--muted)) 70%, var(--text));
|
|
1252
|
+
font-family: var(--mono);
|
|
1253
|
+
font-size: 12px;
|
|
1254
|
+
}
|
|
1255
|
+
.sarif-level.error { --bh: #dc2626; }
|
|
1256
|
+
.sarif-level.warning { --bh: #d97706; }
|
|
1257
|
+
.sarif-level.note { --bh: #2563eb; }
|
|
1258
|
+
.sarif-level.none { --bh: #64748b; }
|
|
1259
|
+
.artifact-raw-details {
|
|
1260
|
+
border: 1px solid var(--line);
|
|
1261
|
+
border-radius: 12px;
|
|
1262
|
+
background: var(--panel);
|
|
1263
|
+
overflow: hidden;
|
|
1264
|
+
}
|
|
1265
|
+
.artifact-raw-details summary {
|
|
1266
|
+
padding: 10px 14px;
|
|
1267
|
+
cursor: pointer;
|
|
1268
|
+
color: var(--muted);
|
|
1269
|
+
font-family: var(--mono);
|
|
1270
|
+
font-size: 12.5px;
|
|
1271
|
+
}
|
|
1272
|
+
.artifact-raw-details .artifact-code {
|
|
1273
|
+
border: 0;
|
|
1274
|
+
border-top: 1px solid var(--line);
|
|
1275
|
+
border-radius: 0;
|
|
1276
|
+
}
|
|
1277
|
+
.artifact-media {
|
|
1278
|
+
display: grid;
|
|
1279
|
+
gap: 12px;
|
|
1280
|
+
margin: 0;
|
|
1281
|
+
padding: 14px;
|
|
1282
|
+
border: 1px solid var(--line);
|
|
1283
|
+
border-radius: 12px;
|
|
1284
|
+
background: var(--panel);
|
|
1285
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
1286
|
+
}
|
|
1287
|
+
.artifact-media img,
|
|
1288
|
+
.artifact-media video {
|
|
1289
|
+
display: block;
|
|
1290
|
+
width: 100%;
|
|
1291
|
+
max-height: 76vh;
|
|
1292
|
+
object-fit: contain;
|
|
1293
|
+
border-radius: 8px;
|
|
1294
|
+
background: var(--panel-2);
|
|
1295
|
+
}
|
|
1296
|
+
.artifact-media figcaption,
|
|
1297
|
+
.artifact-media-note {
|
|
1298
|
+
color: var(--muted);
|
|
1299
|
+
font-family: var(--mono);
|
|
1300
|
+
font-size: 12.5px;
|
|
1301
|
+
overflow-wrap: anywhere;
|
|
1302
|
+
}
|
|
1079
1303
|
.artifact-code {
|
|
1080
1304
|
padding: 22px;
|
|
1081
1305
|
overflow: auto;
|
|
@@ -1512,6 +1736,270 @@ function alignmentAttribute(alignment) {
|
|
|
1512
1736
|
return alignment ? ` class="align-${alignment}"` : "";
|
|
1513
1737
|
}
|
|
1514
1738
|
|
|
1739
|
+
const CSV_RENDER_ROW_LIMIT = 1000;
|
|
1740
|
+
const CSV_RENDER_COLUMN_LIMIT = 80;
|
|
1741
|
+
const SARIF_RENDER_RESULT_LIMIT = 500;
|
|
1742
|
+
|
|
1743
|
+
function renderMediaArtifact(format, content, metadata = {}, options = {}) {
|
|
1744
|
+
const mimeType = mediaMimeType(metadata, format);
|
|
1745
|
+
const source = options.rawUrl || mediaDataUrl(content, mimeType);
|
|
1746
|
+
if (!source) {
|
|
1747
|
+
return `<section class="artifact-media-note">
|
|
1748
|
+
Media preview is unavailable because this artifact does not contain valid base64 media content.
|
|
1749
|
+
</section>
|
|
1750
|
+
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
const label = [
|
|
1754
|
+
mimeType || format,
|
|
1755
|
+
metadata.encoding ? `encoding: ${metadata.encoding}` : "",
|
|
1756
|
+
metadata.originalEncoding ? `source: ${metadata.originalEncoding}` : ""
|
|
1757
|
+
].filter(Boolean).join(" · ");
|
|
1758
|
+
|
|
1759
|
+
if (format === "video") {
|
|
1760
|
+
return `<figure class="artifact-media artifact-video">
|
|
1761
|
+
<video controls preload="metadata" src="${escapeAttribute(source)}"></video>
|
|
1762
|
+
<figcaption>${escapeHtml(label || "video artifact")}</figcaption>
|
|
1763
|
+
</figure>`;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return `<figure class="artifact-media artifact-image">
|
|
1767
|
+
<img src="${escapeAttribute(source)}" alt="Image artifact">
|
|
1768
|
+
<figcaption>${escapeHtml(label || "image artifact")}</figcaption>
|
|
1769
|
+
</figure>`;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function mediaMimeType(metadata = {}, format) {
|
|
1773
|
+
const explicit = String(metadata.mimeType || "").toLowerCase();
|
|
1774
|
+
if (explicit.startsWith("image/") || explicit.startsWith("video/")) {
|
|
1775
|
+
return explicit;
|
|
1776
|
+
}
|
|
1777
|
+
return format === "video" ? "video/mp4" : "image/png";
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function mediaDataUrl(content, mimeType) {
|
|
1781
|
+
const value = String(content || "").trim();
|
|
1782
|
+
if (/^data:[^;,]+;base64,/i.test(value)) {
|
|
1783
|
+
return value;
|
|
1784
|
+
}
|
|
1785
|
+
const base64 = value.replace(/\s+/g, "");
|
|
1786
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64)) {
|
|
1787
|
+
return "";
|
|
1788
|
+
}
|
|
1789
|
+
return `data:${mimeType};base64,${base64}`;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function renderCsv(content) {
|
|
1793
|
+
const parsed = parseCsv(content);
|
|
1794
|
+
if (!parsed.ok) {
|
|
1795
|
+
return `<section class="artifact-csv">
|
|
1796
|
+
<p class="artifact-csv-note">CSV parsing failed: ${escapeHtml(parsed.error)}. Showing escaped source.</p>
|
|
1797
|
+
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
1798
|
+
</section>`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (parsed.rows.length === 0) {
|
|
1802
|
+
return `<section class="artifact-csv"><p class="artifact-csv-note">Empty CSV artifact.</p></section>`;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const columnCount = Math.max(...parsed.rows.map((row) => row.length));
|
|
1806
|
+
const visibleColumns = Math.min(columnCount, CSV_RENDER_COLUMN_LIMIT);
|
|
1807
|
+
const header = parsed.rows[0];
|
|
1808
|
+
const bodyRows = parsed.rows.slice(1, CSV_RENDER_ROW_LIMIT + 1);
|
|
1809
|
+
const rowTruncated = parsed.rows.length - 1 > CSV_RENDER_ROW_LIMIT;
|
|
1810
|
+
const columnTruncated = columnCount > CSV_RENDER_COLUMN_LIMIT;
|
|
1811
|
+
const headerHtml = Array.from({ length: visibleColumns }, (_, index) =>
|
|
1812
|
+
`<th>${escapeHtml(header[index] || `Column ${index + 1}`)}</th>`
|
|
1813
|
+
).join("");
|
|
1814
|
+
const bodyHtml = bodyRows.map((row) =>
|
|
1815
|
+
`<tr>${Array.from({ length: visibleColumns }, (_, index) =>
|
|
1816
|
+
`<td>${escapeHtml(row[index] || "")}</td>`
|
|
1817
|
+
).join("")}</tr>`
|
|
1818
|
+
).join("\n");
|
|
1819
|
+
const notes = [
|
|
1820
|
+
`${parsed.rows.length - 1} data rows`,
|
|
1821
|
+
`${columnCount} columns`,
|
|
1822
|
+
rowTruncated ? `showing first ${CSV_RENDER_ROW_LIMIT} rows` : "",
|
|
1823
|
+
columnTruncated ? `showing first ${CSV_RENDER_COLUMN_LIMIT} columns` : ""
|
|
1824
|
+
].filter(Boolean).join(" · ");
|
|
1825
|
+
|
|
1826
|
+
return `<section class="artifact-csv">
|
|
1827
|
+
<p class="artifact-csv-note">${escapeHtml(notes)}</p>
|
|
1828
|
+
<div class="artifact-table-scroll">
|
|
1829
|
+
<table class="artifact-table">
|
|
1830
|
+
<thead><tr>${headerHtml}</tr></thead>
|
|
1831
|
+
<tbody>${bodyHtml}</tbody>
|
|
1832
|
+
</table>
|
|
1833
|
+
</div>
|
|
1834
|
+
</section>`;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function parseCsv(content) {
|
|
1838
|
+
const text = String(content || "");
|
|
1839
|
+
const rows = [];
|
|
1840
|
+
let row = [];
|
|
1841
|
+
let cell = "";
|
|
1842
|
+
let inQuotes = false;
|
|
1843
|
+
|
|
1844
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1845
|
+
const char = text[index];
|
|
1846
|
+
if (char === "\"") {
|
|
1847
|
+
if (inQuotes && text[index + 1] === "\"") {
|
|
1848
|
+
cell += "\"";
|
|
1849
|
+
index += 1;
|
|
1850
|
+
} else {
|
|
1851
|
+
inQuotes = !inQuotes;
|
|
1852
|
+
}
|
|
1853
|
+
} else if (char === "," && !inQuotes) {
|
|
1854
|
+
row.push(cell);
|
|
1855
|
+
cell = "";
|
|
1856
|
+
} else if ((char === "\n" || char === "\r") && !inQuotes) {
|
|
1857
|
+
if (char === "\r" && text[index + 1] === "\n") {
|
|
1858
|
+
index += 1;
|
|
1859
|
+
}
|
|
1860
|
+
row.push(cell);
|
|
1861
|
+
if (row.length > 1 || row[0] !== "") {
|
|
1862
|
+
rows.push(row);
|
|
1863
|
+
}
|
|
1864
|
+
row = [];
|
|
1865
|
+
cell = "";
|
|
1866
|
+
} else {
|
|
1867
|
+
cell += char;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
if (inQuotes) {
|
|
1872
|
+
return { ok: false, error: "unterminated quoted field", rows: [] };
|
|
1873
|
+
}
|
|
1874
|
+
row.push(cell);
|
|
1875
|
+
if (row.length > 1 || row[0] !== "") {
|
|
1876
|
+
rows.push(row);
|
|
1877
|
+
}
|
|
1878
|
+
return { ok: true, rows };
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function renderSarif(content) {
|
|
1882
|
+
let sarif;
|
|
1883
|
+
try {
|
|
1884
|
+
sarif = JSON.parse(content);
|
|
1885
|
+
} catch {
|
|
1886
|
+
return `<section class="artifact-sarif">
|
|
1887
|
+
<p class="artifact-sarif-note">Invalid SARIF JSON. Showing escaped source.</p>
|
|
1888
|
+
<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>
|
|
1889
|
+
</section>`;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
if (!isSarifDocument(sarif)) {
|
|
1893
|
+
return `<section class="artifact-sarif">
|
|
1894
|
+
<p class="artifact-sarif-note">This JSON does not match the expected SARIF top-level shape. Showing formatted JSON.</p>
|
|
1895
|
+
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
1896
|
+
</section>`;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const results = collectSarifResults(sarif);
|
|
1900
|
+
const counts = countBy(results, (result) => result.level);
|
|
1901
|
+
const visibleResults = results.slice(0, SARIF_RENDER_RESULT_LIMIT);
|
|
1902
|
+
const rowHtml = visibleResults.map((result) => `<tr>
|
|
1903
|
+
<td><span class="sarif-level ${escapeAttribute(result.level)}">${escapeHtml(result.level)}</span></td>
|
|
1904
|
+
<td>${escapeHtml(result.ruleId)}</td>
|
|
1905
|
+
<td>${escapeHtml(result.message)}</td>
|
|
1906
|
+
<td>${escapeHtml(result.location)}</td>
|
|
1907
|
+
<td>${escapeHtml(result.tool)}</td>
|
|
1908
|
+
</tr>`).join("\n");
|
|
1909
|
+
const note = results.length > SARIF_RENDER_RESULT_LIMIT
|
|
1910
|
+
? `Showing first ${SARIF_RENDER_RESULT_LIMIT} of ${results.length} results.`
|
|
1911
|
+
: `${results.length} results.`;
|
|
1912
|
+
|
|
1913
|
+
return `<section class="artifact-sarif">
|
|
1914
|
+
<div class="artifact-summary-grid">
|
|
1915
|
+
${summaryCard("Runs", sarif.runs.length)}
|
|
1916
|
+
${summaryCard("Results", results.length)}
|
|
1917
|
+
${summaryCard("Errors", counts.error || 0)}
|
|
1918
|
+
${summaryCard("Warnings", counts.warning || 0)}
|
|
1919
|
+
${summaryCard("Notes", counts.note || 0)}
|
|
1920
|
+
</div>
|
|
1921
|
+
<p class="artifact-sarif-note">${escapeHtml(note)}</p>
|
|
1922
|
+
<div class="artifact-table-scroll">
|
|
1923
|
+
<table class="artifact-table">
|
|
1924
|
+
<thead><tr><th>Level</th><th>Rule</th><th>Message</th><th>Location</th><th>Tool</th></tr></thead>
|
|
1925
|
+
<tbody>${rowHtml}</tbody>
|
|
1926
|
+
</table>
|
|
1927
|
+
</div>
|
|
1928
|
+
<details class="artifact-raw-details">
|
|
1929
|
+
<summary>Raw SARIF JSON</summary>
|
|
1930
|
+
<pre class="artifact-code"><code>${escapeHtml(formatJson(content))}</code></pre>
|
|
1931
|
+
</details>
|
|
1932
|
+
</section>`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function isSarifDocument(value) {
|
|
1936
|
+
return value &&
|
|
1937
|
+
typeof value === "object" &&
|
|
1938
|
+
!Array.isArray(value) &&
|
|
1939
|
+
Array.isArray(value.runs) &&
|
|
1940
|
+
(typeof value.version === "string" || String(value.$schema || "").toLowerCase().includes("sarif"));
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function collectSarifResults(sarif) {
|
|
1944
|
+
return sarif.runs.flatMap((run) => {
|
|
1945
|
+
const tool = run?.tool?.driver?.name || run?.tool?.driver?.fullName || "unknown";
|
|
1946
|
+
const rules = new Map();
|
|
1947
|
+
for (const [index, rule] of (run?.tool?.driver?.rules || []).entries()) {
|
|
1948
|
+
if (rule?.id) {
|
|
1949
|
+
rules.set(rule.id, rule);
|
|
1950
|
+
}
|
|
1951
|
+
rules.set(index, rule);
|
|
1952
|
+
}
|
|
1953
|
+
return (run?.results || []).map((result) => {
|
|
1954
|
+
const rule = rules.get(result.ruleId) || rules.get(result.ruleIndex);
|
|
1955
|
+
return {
|
|
1956
|
+
level: normalizeSarifLevel(result.level || rule?.defaultConfiguration?.level),
|
|
1957
|
+
ruleId: result.ruleId || rule?.id || (Number.isInteger(result.ruleIndex) ? `#${result.ruleIndex}` : "unknown"),
|
|
1958
|
+
message: sarifMessage(result.message) || sarifMessage(rule?.shortDescription) || sarifMessage(rule?.fullDescription) || "",
|
|
1959
|
+
location: sarifLocation(result),
|
|
1960
|
+
tool
|
|
1961
|
+
};
|
|
1962
|
+
});
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function normalizeSarifLevel(value) {
|
|
1967
|
+
const normalized = String(value || "warning").toLowerCase();
|
|
1968
|
+
return ["error", "warning", "note", "none"].includes(normalized) ? normalized : "warning";
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function sarifMessage(message) {
|
|
1972
|
+
if (!message || typeof message !== "object") {
|
|
1973
|
+
return "";
|
|
1974
|
+
}
|
|
1975
|
+
return String(message.text || message.markdown || "").trim();
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
function sarifLocation(result) {
|
|
1979
|
+
const physical = result?.locations?.[0]?.physicalLocation;
|
|
1980
|
+
const uri = physical?.artifactLocation?.uri || physical?.artifactLocation?.uriBaseId || "";
|
|
1981
|
+
const region = physical?.region || {};
|
|
1982
|
+
const line = Number.isInteger(region.startLine) ? region.startLine : "";
|
|
1983
|
+
const column = Number.isInteger(region.startColumn) ? region.startColumn : "";
|
|
1984
|
+
return [
|
|
1985
|
+
uri || "unknown",
|
|
1986
|
+
line ? `:${line}` : "",
|
|
1987
|
+
column ? `:${column}` : ""
|
|
1988
|
+
].join("");
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function countBy(values, keyFn) {
|
|
1992
|
+
return values.reduce((counts, value) => {
|
|
1993
|
+
const key = keyFn(value);
|
|
1994
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
1995
|
+
return counts;
|
|
1996
|
+
}, {});
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function summaryCard(label, value) {
|
|
2000
|
+
return `<div class="summary-card"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1515
2003
|
function formatJson(content) {
|
|
1516
2004
|
try {
|
|
1517
2005
|
return JSON.stringify(JSON.parse(content), null, 2);
|