mcp-stitch 0.1.2 → 0.1.4
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 +145 -2
- package/dist/config/stitch.js +1 -1
- package/dist/config/stitch.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/packageInfo.d.ts +6 -0
- package/dist/packageInfo.d.ts.map +1 -0
- package/dist/packageInfo.js +18 -0
- package/dist/packageInfo.js.map +1 -0
- package/dist/tools/status.d.ts.map +1 -1
- package/dist/tools/status.js +27 -0
- package/dist/tools/status.js.map +1 -1
- package/dist/tools/stitchExport.d.ts.map +1 -1
- package/dist/tools/stitchExport.js +419 -6
- package/dist/tools/stitchExport.js.map +1 -1
- package/dist/tools/stitchScreens.d.ts.map +1 -1
- package/dist/tools/stitchScreens.js +208 -19
- package/dist/tools/stitchScreens.js.map +1 -1
- package/docs/stitch-tools.md +38 -17
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { lstat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getStitchConfig } from "../config/stitch.js";
|
|
@@ -23,6 +23,9 @@ const ARTIFACT_FILES = [
|
|
|
23
23
|
"manifest.json",
|
|
24
24
|
];
|
|
25
25
|
const DEFAULT_ARTIFACT_ROOT = ".artifacts/stitch";
|
|
26
|
+
const DEFAULT_MAX_LINKED_ASSETS = 50;
|
|
27
|
+
const DEFAULT_MAX_DOWNLOAD_BYTES = 25 * 1024 * 1024;
|
|
28
|
+
const VERSION_DIR_PATTERN = /^v(\d{3})$/;
|
|
26
29
|
function isSafeRelativePathInput(value) {
|
|
27
30
|
const trimmed = value.trim();
|
|
28
31
|
if (!trimmed)
|
|
@@ -46,6 +49,206 @@ function nowStamp() {
|
|
|
46
49
|
function toPrettyJson(value) {
|
|
47
50
|
return JSON.stringify(value, null, 2);
|
|
48
51
|
}
|
|
52
|
+
function safeUrl(url) {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(url);
|
|
55
|
+
if (parsed.protocol !== "https:")
|
|
56
|
+
return null;
|
|
57
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
58
|
+
if (hostname === "localhost" ||
|
|
59
|
+
hostname.endsWith(".localhost") ||
|
|
60
|
+
hostname.endsWith(".local") ||
|
|
61
|
+
hostname === "0.0.0.0" ||
|
|
62
|
+
hostname === "127.0.0.1" ||
|
|
63
|
+
hostname === "::1" ||
|
|
64
|
+
hostname.startsWith("10.") ||
|
|
65
|
+
hostname.startsWith("192.168.") ||
|
|
66
|
+
/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function extensionFromContentType(contentType) {
|
|
76
|
+
const normalized = contentType?.split(";")[0]?.trim().toLowerCase();
|
|
77
|
+
switch (normalized) {
|
|
78
|
+
case "text/html":
|
|
79
|
+
return ".html";
|
|
80
|
+
case "image/jpeg":
|
|
81
|
+
return ".jpg";
|
|
82
|
+
case "image/png":
|
|
83
|
+
return ".png";
|
|
84
|
+
case "image/webp":
|
|
85
|
+
return ".webp";
|
|
86
|
+
case "image/gif":
|
|
87
|
+
return ".gif";
|
|
88
|
+
case "image/svg+xml":
|
|
89
|
+
return ".svg";
|
|
90
|
+
case "image/avif":
|
|
91
|
+
return ".avif";
|
|
92
|
+
case "font/woff":
|
|
93
|
+
return ".woff";
|
|
94
|
+
case "font/woff2":
|
|
95
|
+
return ".woff2";
|
|
96
|
+
case "text/css":
|
|
97
|
+
return ".css";
|
|
98
|
+
case "application/javascript":
|
|
99
|
+
case "text/javascript":
|
|
100
|
+
return ".js";
|
|
101
|
+
default:
|
|
102
|
+
return ".bin";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function extensionFromUrl(url) {
|
|
106
|
+
const extension = path.extname(url.pathname).toLowerCase();
|
|
107
|
+
return /^[a-z0-9.]{2,12}$/.test(extension) ? extension : "";
|
|
108
|
+
}
|
|
109
|
+
function fileNameWithExtension(stem, url, contentType) {
|
|
110
|
+
const extension = extensionFromUrl(url) || extensionFromContentType(contentType);
|
|
111
|
+
return `${stem}${extension}`;
|
|
112
|
+
}
|
|
113
|
+
function assetFileNameStem(prefix, index) {
|
|
114
|
+
return `${prefix}-${String(index).padStart(3, "0")}`;
|
|
115
|
+
}
|
|
116
|
+
function dedupeUrls(urls) {
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const result = [];
|
|
119
|
+
for (const url of urls) {
|
|
120
|
+
if (seen.has(url))
|
|
121
|
+
continue;
|
|
122
|
+
seen.add(url);
|
|
123
|
+
result.push(url);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function extractLinkedAssetUrls(html, baseUrl) {
|
|
128
|
+
const candidates = [];
|
|
129
|
+
const attrPattern = /\b(?:src|href)=["']([^"']+)["']/gi;
|
|
130
|
+
const cssUrlPattern = /url\(["']?([^"')]+)["']?\)/gi;
|
|
131
|
+
for (const match of html.matchAll(attrPattern)) {
|
|
132
|
+
if (match[1])
|
|
133
|
+
candidates.push(match[1]);
|
|
134
|
+
}
|
|
135
|
+
for (const match of html.matchAll(cssUrlPattern)) {
|
|
136
|
+
if (match[1])
|
|
137
|
+
candidates.push(match[1]);
|
|
138
|
+
}
|
|
139
|
+
const urls = candidates
|
|
140
|
+
.map((candidate) => {
|
|
141
|
+
try {
|
|
142
|
+
return new URL(candidate, baseUrl).toString();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
.filter((url) => Boolean(url))
|
|
149
|
+
.filter((url) => safeUrl(url) !== null);
|
|
150
|
+
return dedupeUrls(urls);
|
|
151
|
+
}
|
|
152
|
+
function isLikelyLinkedAsset(contentType) {
|
|
153
|
+
const normalized = contentType?.split(";")[0]?.trim().toLowerCase();
|
|
154
|
+
return Boolean(normalized &&
|
|
155
|
+
(normalized.startsWith("image/") ||
|
|
156
|
+
normalized.startsWith("font/") ||
|
|
157
|
+
normalized === "text/css" ||
|
|
158
|
+
normalized === "application/javascript" ||
|
|
159
|
+
normalized === "text/javascript"));
|
|
160
|
+
}
|
|
161
|
+
async function downloadUrl(options) {
|
|
162
|
+
const parsed = safeUrl(options.url);
|
|
163
|
+
if (!parsed) {
|
|
164
|
+
return {
|
|
165
|
+
asset: {
|
|
166
|
+
kind: options.kind,
|
|
167
|
+
sourceUrl: options.url,
|
|
168
|
+
status: "skipped",
|
|
169
|
+
reason: "Only safe https URLs are downloaded.",
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const response = await fetch(parsed);
|
|
175
|
+
const contentType = response.headers.get("content-type") ?? undefined;
|
|
176
|
+
const contentLength = response.headers.get("content-length");
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
return {
|
|
179
|
+
asset: {
|
|
180
|
+
kind: options.kind,
|
|
181
|
+
sourceUrl: options.url,
|
|
182
|
+
contentType,
|
|
183
|
+
status: "failed",
|
|
184
|
+
reason: `HTTP ${response.status}`,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (options.requireLinkedAssetType && !isLikelyLinkedAsset(contentType)) {
|
|
189
|
+
return {
|
|
190
|
+
asset: {
|
|
191
|
+
kind: options.kind,
|
|
192
|
+
sourceUrl: options.url,
|
|
193
|
+
contentType,
|
|
194
|
+
status: "skipped",
|
|
195
|
+
reason: "Response content type is not a linked asset type.",
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (contentLength && Number(contentLength) > options.maxBytes) {
|
|
200
|
+
return {
|
|
201
|
+
asset: {
|
|
202
|
+
kind: options.kind,
|
|
203
|
+
sourceUrl: options.url,
|
|
204
|
+
contentType,
|
|
205
|
+
status: "skipped",
|
|
206
|
+
reason: `Content length exceeds ${options.maxBytes} bytes.`,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
211
|
+
if (arrayBuffer.byteLength > options.maxBytes) {
|
|
212
|
+
return {
|
|
213
|
+
asset: {
|
|
214
|
+
kind: options.kind,
|
|
215
|
+
sourceUrl: options.url,
|
|
216
|
+
contentType,
|
|
217
|
+
bytes: arrayBuffer.byteLength,
|
|
218
|
+
status: "skipped",
|
|
219
|
+
reason: `Downloaded content exceeds ${options.maxBytes} bytes.`,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
224
|
+
const fileName = options.forcedFileName ?? fileNameWithExtension(options.fileNameStem, parsed, contentType);
|
|
225
|
+
const relativePath = path.join(options.relativeDir, fileName);
|
|
226
|
+
const outputPath = await prepareSafeOutputPath(options.baseRoot, relativePath);
|
|
227
|
+
await writeFile(outputPath, buffer);
|
|
228
|
+
return {
|
|
229
|
+
asset: {
|
|
230
|
+
kind: options.kind,
|
|
231
|
+
sourceUrl: options.url,
|
|
232
|
+
path: outputPath,
|
|
233
|
+
relativePath,
|
|
234
|
+
contentType,
|
|
235
|
+
bytes: buffer.byteLength,
|
|
236
|
+
status: "saved",
|
|
237
|
+
},
|
|
238
|
+
text: contentType?.startsWith("text/") ? buffer.toString("utf8") : undefined,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
asset: {
|
|
244
|
+
kind: options.kind,
|
|
245
|
+
sourceUrl: options.url,
|
|
246
|
+
status: "failed",
|
|
247
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
49
252
|
function defaultArtifactName(screenId) {
|
|
50
253
|
const suffix = screenId ? sanitizeFileName(screenId) : "screen";
|
|
51
254
|
return `stitch-${suffix}-${nowStamp()}`;
|
|
@@ -82,6 +285,26 @@ async function getBaseRoot(configOutputDir) {
|
|
|
82
285
|
}
|
|
83
286
|
return { ok: true, baseRoot: path.resolve(configOutputDir), source: "STITCH_OUTPUT_DIR" };
|
|
84
287
|
}
|
|
288
|
+
async function nextVersionedArtifactPath(options) {
|
|
289
|
+
const basePath = await prepareSafeOutputPath(options.baseRoot, path.join(options.artifactPath, ".version-probe"));
|
|
290
|
+
const versionBaseDir = path.dirname(basePath);
|
|
291
|
+
await mkdir(versionBaseDir, { recursive: true });
|
|
292
|
+
let maxVersion = 0;
|
|
293
|
+
const entries = await readdir(versionBaseDir, { withFileTypes: true });
|
|
294
|
+
for (const entry of entries) {
|
|
295
|
+
if (!entry.isDirectory())
|
|
296
|
+
continue;
|
|
297
|
+
const match = entry.name.match(VERSION_DIR_PATTERN);
|
|
298
|
+
if (!match?.[1])
|
|
299
|
+
continue;
|
|
300
|
+
maxVersion = Math.max(maxVersion, Number(match[1]));
|
|
301
|
+
}
|
|
302
|
+
const version = `v${String(maxVersion + 1).padStart(3, "0")}`;
|
|
303
|
+
return {
|
|
304
|
+
artifactPath: path.join(options.artifactPath, version),
|
|
305
|
+
version,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
85
308
|
function defaultArtifactPath(payload, screenId) {
|
|
86
309
|
const screen = getScreen(payload);
|
|
87
310
|
const title = getString(screen, "title");
|
|
@@ -672,6 +895,14 @@ function createManifest(options) {
|
|
|
672
895
|
mode: options.outputMode,
|
|
673
896
|
},
|
|
674
897
|
paths: options.paths,
|
|
898
|
+
downloadedAssets: options.downloadedAssets,
|
|
899
|
+
version: options.versionInfo.enabled
|
|
900
|
+
? {
|
|
901
|
+
enabled: true,
|
|
902
|
+
baseArtifactPath: options.versionInfo.baseArtifactPath,
|
|
903
|
+
version: options.versionInfo.version,
|
|
904
|
+
}
|
|
905
|
+
: { enabled: false },
|
|
675
906
|
};
|
|
676
907
|
}
|
|
677
908
|
export function registerStitchExportTool(server) {
|
|
@@ -708,8 +939,47 @@ export function registerStitchExportTool(server) {
|
|
|
708
939
|
.describe("Legacy workspace-relative artifact bundle directory, for example exports/my-screen."),
|
|
709
940
|
rawGetScreenInput: z.record(z.string(), z.unknown()).optional(),
|
|
710
941
|
screenData: z.unknown().optional(),
|
|
942
|
+
versioned: z
|
|
943
|
+
.boolean()
|
|
944
|
+
.optional()
|
|
945
|
+
.default(false)
|
|
946
|
+
.describe("When true, writes into the next nested version folder such as artifactPath/v001, then v002."),
|
|
947
|
+
includeHtml: z
|
|
948
|
+
.boolean()
|
|
949
|
+
.optional()
|
|
950
|
+
.default(true)
|
|
951
|
+
.describe("Download the screen htmlCode.downloadUrl into screen.html when present."),
|
|
952
|
+
includeScreenshot: z
|
|
953
|
+
.boolean()
|
|
954
|
+
.optional()
|
|
955
|
+
.default(true)
|
|
956
|
+
.describe("Download the screen screenshot.downloadUrl into screenshot.* when present."),
|
|
957
|
+
includeLinkedAssets: z
|
|
958
|
+
.boolean()
|
|
959
|
+
.optional()
|
|
960
|
+
.default(false)
|
|
961
|
+
.describe("Download safe HTTPS assets referenced by the exported HTML into assets/."),
|
|
962
|
+
rewriteHtmlAssetUrls: z
|
|
963
|
+
.boolean()
|
|
964
|
+
.optional()
|
|
965
|
+
.default(false)
|
|
966
|
+
.describe("Rewrite saved screen.html to point at downloaded local asset files."),
|
|
967
|
+
maxLinkedAssets: z
|
|
968
|
+
.number()
|
|
969
|
+
.int()
|
|
970
|
+
.min(1)
|
|
971
|
+
.max(200)
|
|
972
|
+
.optional()
|
|
973
|
+
.default(DEFAULT_MAX_LINKED_ASSETS),
|
|
974
|
+
maxDownloadBytes: z
|
|
975
|
+
.number()
|
|
976
|
+
.int()
|
|
977
|
+
.min(1024)
|
|
978
|
+
.max(100 * 1024 * 1024)
|
|
979
|
+
.optional()
|
|
980
|
+
.default(DEFAULT_MAX_DOWNLOAD_BYTES),
|
|
711
981
|
},
|
|
712
|
-
}, async ({ screenId, projectId, artifactPath, artifactName, relativePath, rawGetScreenInput, screenData }) => {
|
|
982
|
+
}, async ({ screenId, projectId, artifactPath, artifactName, relativePath, rawGetScreenInput, screenData, versioned = false, includeHtml = true, includeScreenshot = true, includeLinkedAssets = false, rewriteHtmlAssetUrls = false, maxLinkedAssets = DEFAULT_MAX_LINKED_ASSETS, maxDownloadBytes = DEFAULT_MAX_DOWNLOAD_BYTES, }) => {
|
|
713
983
|
if (!screenData && !screenId && !rawGetScreenInput) {
|
|
714
984
|
return {
|
|
715
985
|
content: [
|
|
@@ -807,11 +1077,38 @@ export function registerStitchExportTool(server) {
|
|
|
807
1077
|
};
|
|
808
1078
|
}
|
|
809
1079
|
const outputMode = artifactPath || relativePath ? "artifactPath" : artifactName ? "artifactName" : "default";
|
|
810
|
-
const
|
|
1080
|
+
const baseRelativeDir = artifactPath ??
|
|
811
1081
|
relativePath ??
|
|
812
1082
|
(artifactName
|
|
813
1083
|
? path.join("exports", sanitizeFileName(artifactName))
|
|
814
1084
|
: defaultArtifactPath(payload, screenId));
|
|
1085
|
+
let relativeDir = baseRelativeDir;
|
|
1086
|
+
let versionInfo = { enabled: false };
|
|
1087
|
+
if (versioned) {
|
|
1088
|
+
try {
|
|
1089
|
+
const versionedPath = await nextVersionedArtifactPath({
|
|
1090
|
+
baseRoot: baseRootInfo.baseRoot,
|
|
1091
|
+
artifactPath: baseRelativeDir,
|
|
1092
|
+
});
|
|
1093
|
+
relativeDir = versionedPath.artifactPath;
|
|
1094
|
+
versionInfo = {
|
|
1095
|
+
enabled: true,
|
|
1096
|
+
baseArtifactPath: baseRelativeDir,
|
|
1097
|
+
version: versionedPath.version,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1102
|
+
return {
|
|
1103
|
+
content: [
|
|
1104
|
+
{
|
|
1105
|
+
type: "text",
|
|
1106
|
+
text: `Invalid versioned output path.\n\n${message}`,
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
815
1112
|
const outputPaths = {
|
|
816
1113
|
"raw.json": "",
|
|
817
1114
|
"screen-summary.md": "",
|
|
@@ -855,6 +1152,107 @@ export function registerStitchExportTool(server) {
|
|
|
855
1152
|
const acceptanceCriteria = buildAcceptanceCriteria(payload);
|
|
856
1153
|
const testPlan = buildTestPlan(payload);
|
|
857
1154
|
const questions = buildQuestions(payload);
|
|
1155
|
+
const allOutputPaths = { ...outputPaths };
|
|
1156
|
+
const downloadedAssets = [];
|
|
1157
|
+
const screen = getScreen(payload);
|
|
1158
|
+
const htmlCode = getNestedRecord(screen, "htmlCode");
|
|
1159
|
+
const screenshot = getNestedRecord(screen, "screenshot");
|
|
1160
|
+
const htmlUrl = getString(htmlCode, "downloadUrl");
|
|
1161
|
+
const screenshotUrl = getString(screenshot, "downloadUrl");
|
|
1162
|
+
let savedHtmlPath;
|
|
1163
|
+
let savedHtmlText;
|
|
1164
|
+
if (includeHtml && htmlUrl) {
|
|
1165
|
+
const htmlDownload = await downloadUrl({
|
|
1166
|
+
url: htmlUrl,
|
|
1167
|
+
baseRoot: baseRootInfo.baseRoot,
|
|
1168
|
+
relativeDir,
|
|
1169
|
+
fileNameStem: "screen",
|
|
1170
|
+
forcedFileName: "screen.html",
|
|
1171
|
+
kind: "html",
|
|
1172
|
+
maxBytes: maxDownloadBytes,
|
|
1173
|
+
});
|
|
1174
|
+
downloadedAssets.push(htmlDownload.asset);
|
|
1175
|
+
if (htmlDownload.asset.status === "saved") {
|
|
1176
|
+
savedHtmlPath = htmlDownload.asset.path;
|
|
1177
|
+
savedHtmlText = htmlDownload.text;
|
|
1178
|
+
if (htmlDownload.asset.relativePath) {
|
|
1179
|
+
allOutputPaths["screen.html"] = htmlDownload.asset.path ?? "";
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
if (includeScreenshot && screenshotUrl) {
|
|
1184
|
+
const screenshotDownload = await downloadUrl({
|
|
1185
|
+
url: screenshotUrl,
|
|
1186
|
+
baseRoot: baseRootInfo.baseRoot,
|
|
1187
|
+
relativeDir,
|
|
1188
|
+
fileNameStem: "screenshot",
|
|
1189
|
+
kind: "screenshot",
|
|
1190
|
+
maxBytes: maxDownloadBytes,
|
|
1191
|
+
});
|
|
1192
|
+
downloadedAssets.push(screenshotDownload.asset);
|
|
1193
|
+
if (screenshotDownload.asset.status === "saved" && screenshotDownload.asset.path) {
|
|
1194
|
+
allOutputPaths[path.basename(screenshotDownload.asset.path)] = screenshotDownload.asset.path;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (includeLinkedAssets && htmlUrl) {
|
|
1198
|
+
if (!savedHtmlText) {
|
|
1199
|
+
const htmlDownload = await downloadUrl({
|
|
1200
|
+
url: htmlUrl,
|
|
1201
|
+
baseRoot: baseRootInfo.baseRoot,
|
|
1202
|
+
relativeDir,
|
|
1203
|
+
fileNameStem: "screen",
|
|
1204
|
+
forcedFileName: "screen.html",
|
|
1205
|
+
kind: "html",
|
|
1206
|
+
maxBytes: maxDownloadBytes,
|
|
1207
|
+
});
|
|
1208
|
+
downloadedAssets.push(htmlDownload.asset);
|
|
1209
|
+
if (htmlDownload.asset.status === "saved") {
|
|
1210
|
+
savedHtmlPath = htmlDownload.asset.path;
|
|
1211
|
+
savedHtmlText = htmlDownload.text;
|
|
1212
|
+
if (htmlDownload.asset.path) {
|
|
1213
|
+
allOutputPaths["screen.html"] = htmlDownload.asset.path;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const linkedUrls = savedHtmlText
|
|
1218
|
+
? extractLinkedAssetUrls(savedHtmlText, htmlUrl).slice(0, maxLinkedAssets)
|
|
1219
|
+
: [];
|
|
1220
|
+
const rewriteMap = new Map();
|
|
1221
|
+
for (const [index, linkedUrl] of linkedUrls.entries()) {
|
|
1222
|
+
const linkedAsset = await downloadUrl({
|
|
1223
|
+
url: linkedUrl,
|
|
1224
|
+
baseRoot: baseRootInfo.baseRoot,
|
|
1225
|
+
relativeDir: path.join(relativeDir, "assets"),
|
|
1226
|
+
fileNameStem: assetFileNameStem("asset", index + 1),
|
|
1227
|
+
kind: "linked-asset",
|
|
1228
|
+
maxBytes: maxDownloadBytes,
|
|
1229
|
+
requireLinkedAssetType: true,
|
|
1230
|
+
});
|
|
1231
|
+
downloadedAssets.push(linkedAsset.asset);
|
|
1232
|
+
if (linkedAsset.asset.status === "saved" && linkedAsset.asset.path) {
|
|
1233
|
+
allOutputPaths[`assets/${path.basename(linkedAsset.asset.path)}`] = linkedAsset.asset.path;
|
|
1234
|
+
if (savedHtmlPath) {
|
|
1235
|
+
const localReference = path
|
|
1236
|
+
.relative(path.dirname(savedHtmlPath), linkedAsset.asset.path)
|
|
1237
|
+
.split(path.sep)
|
|
1238
|
+
.join("/");
|
|
1239
|
+
rewriteMap.set(linkedUrl, localReference.startsWith(".") ? localReference : `./${localReference}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (rewriteHtmlAssetUrls && savedHtmlPath && savedHtmlText && rewriteMap.size > 0) {
|
|
1244
|
+
let rewrittenHtml = savedHtmlText;
|
|
1245
|
+
for (const [remoteUrl, localReference] of rewriteMap) {
|
|
1246
|
+
rewrittenHtml = rewrittenHtml.split(remoteUrl).join(localReference);
|
|
1247
|
+
}
|
|
1248
|
+
await writeFile(savedHtmlPath, rewrittenHtml, "utf8");
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (downloadedAssets.length > 0) {
|
|
1252
|
+
const assetManifestPath = await prepareSafeOutputPath(baseRootInfo.baseRoot, path.join(relativeDir, "asset-manifest.json"));
|
|
1253
|
+
allOutputPaths["asset-manifest.json"] = assetManifestPath;
|
|
1254
|
+
await writeFile(assetManifestPath, `${toPrettyJson({ generatedAt, assets: downloadedAssets })}\n`, "utf8");
|
|
1255
|
+
}
|
|
858
1256
|
const manifest = createManifest({
|
|
859
1257
|
payload,
|
|
860
1258
|
input: {
|
|
@@ -866,10 +1264,19 @@ export function registerStitchExportTool(server) {
|
|
|
866
1264
|
...(rawGetScreenInput ? { rawGetScreenInput } : {}),
|
|
867
1265
|
...(screenData ? { screenDataProvided: true } : {}),
|
|
868
1266
|
...(fetchInput ? { fetchInput } : {}),
|
|
1267
|
+
versioned,
|
|
1268
|
+
includeHtml,
|
|
1269
|
+
includeScreenshot,
|
|
1270
|
+
includeLinkedAssets,
|
|
1271
|
+
rewriteHtmlAssetUrls,
|
|
1272
|
+
maxLinkedAssets,
|
|
1273
|
+
maxDownloadBytes,
|
|
869
1274
|
},
|
|
870
1275
|
resolver: resolverInfo,
|
|
871
1276
|
generatedAt,
|
|
872
|
-
paths:
|
|
1277
|
+
paths: allOutputPaths,
|
|
1278
|
+
downloadedAssets,
|
|
1279
|
+
versionInfo,
|
|
873
1280
|
artifactPath: relativeDir,
|
|
874
1281
|
resolvedOutputDir: path.dirname(outputPaths["manifest.json"]),
|
|
875
1282
|
baseRoot: baseRootInfo.baseRoot,
|
|
@@ -888,9 +1295,11 @@ export function registerStitchExportTool(server) {
|
|
|
888
1295
|
await writeFile(outputPaths["test-plan.md"], `${testPlan}\n`, "utf8");
|
|
889
1296
|
await writeFile(outputPaths["questions.md"], `${questions}\n`, "utf8");
|
|
890
1297
|
await writeFile(outputPaths["manifest.json"], `${toPrettyJson(manifest)}\n`, "utf8");
|
|
891
|
-
const screen = getScreen(payload);
|
|
892
1298
|
const title = getString(screen, "title") ?? "(untitled)";
|
|
893
1299
|
const screenName = getString(screen, "name") ?? "(unknown)";
|
|
1300
|
+
const savedCount = downloadedAssets.filter((asset) => asset.status === "saved").length;
|
|
1301
|
+
const skippedCount = downloadedAssets.filter((asset) => asset.status === "skipped").length;
|
|
1302
|
+
const failedCount = downloadedAssets.filter((asset) => asset.status === "failed").length;
|
|
894
1303
|
return {
|
|
895
1304
|
content: [
|
|
896
1305
|
{
|
|
@@ -899,10 +1308,14 @@ export function registerStitchExportTool(server) {
|
|
|
899
1308
|
`Title: ${title}\n` +
|
|
900
1309
|
`Screen: ${screenName}\n` +
|
|
901
1310
|
`Artifact path: ${relativeDir}\n` +
|
|
1311
|
+
(versionInfo.enabled
|
|
1312
|
+
? `Version: ${versionInfo.version} (${versionInfo.baseArtifactPath})\n`
|
|
1313
|
+
: "") +
|
|
902
1314
|
`Base root: ${baseRootInfo.baseRoot} (${baseRootInfo.source})\n` +
|
|
903
1315
|
`Output directory: ${path.dirname(outputPaths["manifest.json"])}\n\n` +
|
|
1316
|
+
`Downloaded assets: ${savedCount} saved, ${skippedCount} skipped, ${failedCount} failed\n\n` +
|
|
904
1317
|
"Files:\n" +
|
|
905
|
-
|
|
1318
|
+
Object.entries(allOutputPaths).map(([fileName, filePath]) => `- ${fileName}: ${filePath}`).join("\n"),
|
|
906
1319
|
},
|
|
907
1320
|
],
|
|
908
1321
|
};
|