mcp-stitch 0.1.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/README.md +122 -0
- package/dist/config/stitch.d.ts +11 -0
- package/dist/config/stitch.d.ts.map +1 -0
- package/dist/config/stitch.js +38 -0
- package/dist/config/stitch.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/models/stitch.d.ts +20 -0
- package/dist/models/stitch.d.ts.map +1 -0
- package/dist/models/stitch.js +2 -0
- package/dist/models/stitch.js.map +1 -0
- package/dist/services/stitchClient.d.ts +8 -0
- package/dist/services/stitchClient.d.ts.map +1 -0
- package/dist/services/stitchClient.js +189 -0
- package/dist/services/stitchClient.js.map +1 -0
- package/dist/tools/status.d.ts +3 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +50 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/stitchDesignSystems.d.ts +3 -0
- package/dist/tools/stitchDesignSystems.d.ts.map +1 -0
- package/dist/tools/stitchDesignSystems.js +340 -0
- package/dist/tools/stitchDesignSystems.js.map +1 -0
- package/dist/tools/stitchExport.d.ts +3 -0
- package/dist/tools/stitchExport.d.ts.map +1 -0
- package/dist/tools/stitchExport.js +923 -0
- package/dist/tools/stitchExport.js.map +1 -0
- package/dist/tools/stitchProjects.d.ts +3 -0
- package/dist/tools/stitchProjects.d.ts.map +1 -0
- package/dist/tools/stitchProjects.js +125 -0
- package/dist/tools/stitchProjects.js.map +1 -0
- package/dist/tools/stitchScreens.d.ts +3 -0
- package/dist/tools/stitchScreens.d.ts.map +1 -0
- package/dist/tools/stitchScreens.js +320 -0
- package/dist/tools/stitchScreens.js.map +1 -0
- package/dist/utils/redact.d.ts +3 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +14 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/safePath.d.ts +3 -0
- package/dist/utils/safePath.d.ts.map +1 -0
- package/dist/utils/safePath.js +81 -0
- package/dist/utils/safePath.js.map +1 -0
- package/dist/utils/stitchIds.d.ts +12 -0
- package/dist/utils/stitchIds.d.ts.map +1 -0
- package/dist/utils/stitchIds.js +48 -0
- package/dist/utils/stitchIds.js.map +1 -0
- package/dist/utils/stitchResponse.d.ts +11 -0
- package/dist/utils/stitchResponse.d.ts.map +1 -0
- package/dist/utils/stitchResponse.js +71 -0
- package/dist/utils/stitchResponse.js.map +1 -0
- package/dist/utils/stitchScreenResolver.d.ts +26 -0
- package/dist/utils/stitchScreenResolver.d.ts.map +1 -0
- package/dist/utils/stitchScreenResolver.js +159 -0
- package/dist/utils/stitchScreenResolver.js.map +1 -0
- package/dist/utils/stitchToolHelpers.d.ts +6 -0
- package/dist/utils/stitchToolHelpers.d.ts.map +1 -0
- package/dist/utils/stitchToolHelpers.js +25 -0
- package/dist/utils/stitchToolHelpers.js.map +1 -0
- package/docs/stitch-tools.md +389 -0
- package/package.json +40 -0
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { lstat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getStitchConfig } from "../config/stitch.js";
|
|
6
|
+
import { StitchClient } from "../services/stitchClient.js";
|
|
7
|
+
import { prepareSafeOutputPath, sanitizeFileName, } from "../utils/safePath.js";
|
|
8
|
+
import { toScreenIdentifier } from "../utils/stitchIds.js";
|
|
9
|
+
import { resolveScreenInput } from "../utils/stitchScreenResolver.js";
|
|
10
|
+
const ARTIFACT_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
11
|
+
const ARTIFACT_FILES = [
|
|
12
|
+
"raw.json",
|
|
13
|
+
"screen-summary.md",
|
|
14
|
+
"implementation-context.md",
|
|
15
|
+
"implementation-plan.md",
|
|
16
|
+
"component-map.json",
|
|
17
|
+
"copy.md",
|
|
18
|
+
"style-notes.md",
|
|
19
|
+
"build-prompt.md",
|
|
20
|
+
"acceptance-criteria.md",
|
|
21
|
+
"test-plan.md",
|
|
22
|
+
"questions.md",
|
|
23
|
+
"manifest.json",
|
|
24
|
+
];
|
|
25
|
+
const DEFAULT_ARTIFACT_ROOT = ".artifacts/stitch";
|
|
26
|
+
function isSafeRelativePathInput(value) {
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
return false;
|
|
30
|
+
if (path.isAbsolute(trimmed))
|
|
31
|
+
return false;
|
|
32
|
+
const segments = trimmed.split(/[\\/]+/);
|
|
33
|
+
if (segments.length === 0)
|
|
34
|
+
return false;
|
|
35
|
+
for (const segment of segments) {
|
|
36
|
+
if (!segment || segment === "." || segment === "..")
|
|
37
|
+
return false;
|
|
38
|
+
if (segment.includes(":"))
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function nowStamp() {
|
|
44
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
45
|
+
}
|
|
46
|
+
function toPrettyJson(value) {
|
|
47
|
+
return JSON.stringify(value, null, 2);
|
|
48
|
+
}
|
|
49
|
+
function defaultArtifactName(screenId) {
|
|
50
|
+
const suffix = screenId ? sanitizeFileName(screenId) : "screen";
|
|
51
|
+
return `stitch-${suffix}-${nowStamp()}`;
|
|
52
|
+
}
|
|
53
|
+
function slugifyArtifactSegment(value) {
|
|
54
|
+
const slug = value
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
58
|
+
.replace(/-+/g, "-")
|
|
59
|
+
.replace(/^-|-$/g, "");
|
|
60
|
+
return slug || "screen";
|
|
61
|
+
}
|
|
62
|
+
async function getBaseRoot(configOutputDir) {
|
|
63
|
+
const projectRoot = process.env.PROJECT_ROOT?.trim();
|
|
64
|
+
if (projectRoot) {
|
|
65
|
+
const resolved = path.resolve(projectRoot);
|
|
66
|
+
try {
|
|
67
|
+
const stats = await lstat(resolved);
|
|
68
|
+
if (!stats.isDirectory()) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: `Invalid PROJECT_ROOT: path is not a directory: ${resolved}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: `Invalid PROJECT_ROOT: path does not exist or is not accessible: ${resolved}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, baseRoot: resolved, source: "PROJECT_ROOT" };
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, baseRoot: path.resolve(configOutputDir), source: "STITCH_OUTPUT_DIR" };
|
|
84
|
+
}
|
|
85
|
+
function defaultArtifactPath(payload, screenId) {
|
|
86
|
+
const screen = getScreen(payload);
|
|
87
|
+
const title = getString(screen, "title");
|
|
88
|
+
const id = getString(screen, "id") ?? getScreenIdFromName(getString(screen, "name")) ?? screenId;
|
|
89
|
+
const suffix = slugifyArtifactSegment(title ?? id ?? "screen");
|
|
90
|
+
return path.join(DEFAULT_ARTIFACT_ROOT, suffix);
|
|
91
|
+
}
|
|
92
|
+
function asRecord(value) {
|
|
93
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
94
|
+
? value
|
|
95
|
+
: null;
|
|
96
|
+
}
|
|
97
|
+
function getString(obj, key) {
|
|
98
|
+
if (!obj)
|
|
99
|
+
return undefined;
|
|
100
|
+
const value = obj[key];
|
|
101
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
102
|
+
}
|
|
103
|
+
function parseContentJson(payload) {
|
|
104
|
+
const obj = asRecord(payload);
|
|
105
|
+
const content = obj?.content;
|
|
106
|
+
if (!Array.isArray(content))
|
|
107
|
+
return null;
|
|
108
|
+
for (const item of content) {
|
|
109
|
+
const itemObj = asRecord(item);
|
|
110
|
+
const text = getString(itemObj, "text");
|
|
111
|
+
if (!text)
|
|
112
|
+
continue;
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(text);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function getStructuredPayload(payload) {
|
|
123
|
+
const obj = asRecord(payload);
|
|
124
|
+
const structuredContent = obj?.structuredContent;
|
|
125
|
+
if (structuredContent && typeof structuredContent === "object") {
|
|
126
|
+
return structuredContent;
|
|
127
|
+
}
|
|
128
|
+
return parseContentJson(payload) ?? payload;
|
|
129
|
+
}
|
|
130
|
+
function getScreen(payload) {
|
|
131
|
+
const structured = getStructuredPayload(payload);
|
|
132
|
+
const direct = asRecord(structured);
|
|
133
|
+
if (!direct)
|
|
134
|
+
return null;
|
|
135
|
+
if (getString(direct, "name")?.includes("/screens/")) {
|
|
136
|
+
return direct;
|
|
137
|
+
}
|
|
138
|
+
const screen = asRecord(direct.screen);
|
|
139
|
+
if (screen)
|
|
140
|
+
return screen;
|
|
141
|
+
const screens = direct.screens;
|
|
142
|
+
if (Array.isArray(screens) && screens.length > 0) {
|
|
143
|
+
return asRecord(screens[0]);
|
|
144
|
+
}
|
|
145
|
+
return direct;
|
|
146
|
+
}
|
|
147
|
+
function getNestedRecord(obj, key) {
|
|
148
|
+
return asRecord(obj?.[key]);
|
|
149
|
+
}
|
|
150
|
+
function getScreenIdFromName(name) {
|
|
151
|
+
if (!name)
|
|
152
|
+
return undefined;
|
|
153
|
+
const marker = "/screens/";
|
|
154
|
+
const index = name.indexOf(marker);
|
|
155
|
+
return index >= 0 ? name.slice(index + marker.length) : undefined;
|
|
156
|
+
}
|
|
157
|
+
function getProjectIdFromName(name) {
|
|
158
|
+
if (!name || !name.startsWith("projects/"))
|
|
159
|
+
return undefined;
|
|
160
|
+
const [, projectId] = name.match(/^projects\/([^/]+)/) ?? [];
|
|
161
|
+
return projectId;
|
|
162
|
+
}
|
|
163
|
+
function listStringValues(value, maxItems = 12) {
|
|
164
|
+
if (!Array.isArray(value))
|
|
165
|
+
return [];
|
|
166
|
+
const strings = [];
|
|
167
|
+
for (const item of value) {
|
|
168
|
+
if (typeof item === "string" && item.trim()) {
|
|
169
|
+
strings.push(item.trim());
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const obj = asRecord(item);
|
|
173
|
+
const label = getString(obj, "title") ?? getString(obj, "name") ?? getString(obj, "text");
|
|
174
|
+
if (label)
|
|
175
|
+
strings.push(label);
|
|
176
|
+
}
|
|
177
|
+
if (strings.length >= maxItems)
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
return strings;
|
|
181
|
+
}
|
|
182
|
+
function bulletList(items, fallback) {
|
|
183
|
+
return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : `- ${fallback}`;
|
|
184
|
+
}
|
|
185
|
+
function uniqueStrings(values) {
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const result = [];
|
|
188
|
+
for (const value of values) {
|
|
189
|
+
const trimmed = value.trim();
|
|
190
|
+
if (!trimmed || seen.has(trimmed))
|
|
191
|
+
continue;
|
|
192
|
+
seen.add(trimmed);
|
|
193
|
+
result.push(trimmed);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
function collectTextContent(screen) {
|
|
198
|
+
const values = [
|
|
199
|
+
getString(screen, "title"),
|
|
200
|
+
getString(screen, "prompt"),
|
|
201
|
+
getString(getNestedRecord(screen, "screenMetadata"), "summary"),
|
|
202
|
+
getString(getNestedRecord(screen, "screenMetadata"), "statusMessage"),
|
|
203
|
+
];
|
|
204
|
+
return values.filter((value) => Boolean(value));
|
|
205
|
+
}
|
|
206
|
+
function collectCopyFacts(screen) {
|
|
207
|
+
const metadata = getNestedRecord(screen, "screenMetadata");
|
|
208
|
+
const title = getString(screen, "title");
|
|
209
|
+
const prompt = getString(screen, "prompt");
|
|
210
|
+
const summary = getString(metadata, "summary");
|
|
211
|
+
const statusMessage = getString(metadata, "statusMessage");
|
|
212
|
+
const suggestions = listStringValues(metadata?.suggestions);
|
|
213
|
+
return {
|
|
214
|
+
visibleText: uniqueStrings([...(title ? [title] : [])]),
|
|
215
|
+
possibleUserFacingText: uniqueStrings([
|
|
216
|
+
...(statusMessage ? [statusMessage] : []),
|
|
217
|
+
...suggestions,
|
|
218
|
+
]),
|
|
219
|
+
generationContextText: uniqueStrings([
|
|
220
|
+
...(prompt ? [prompt] : []),
|
|
221
|
+
...(summary ? [summary] : []),
|
|
222
|
+
]),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function describeTokenRecord(record, maxItems = 16) {
|
|
226
|
+
if (!record)
|
|
227
|
+
return [];
|
|
228
|
+
return Object.entries(record)
|
|
229
|
+
.filter(([, value]) => typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
230
|
+
.slice(0, maxItems)
|
|
231
|
+
.map(([key, value]) => `${key}: ${String(value)}`);
|
|
232
|
+
}
|
|
233
|
+
function getTheme(screen) {
|
|
234
|
+
const directTheme = getNestedRecord(screen, "theme");
|
|
235
|
+
if (directTheme)
|
|
236
|
+
return directTheme;
|
|
237
|
+
const designSystem = getNestedRecord(screen, "designSystem");
|
|
238
|
+
const designSystemBody = getNestedRecord(designSystem, "designSystem");
|
|
239
|
+
return getNestedRecord(designSystemBody, "theme");
|
|
240
|
+
}
|
|
241
|
+
function buildScreenSummary(payload) {
|
|
242
|
+
const screen = getScreen(payload);
|
|
243
|
+
const name = getString(screen, "name");
|
|
244
|
+
const metadata = getNestedRecord(screen, "screenMetadata");
|
|
245
|
+
const title = getString(screen, "title") ?? "(untitled)";
|
|
246
|
+
const prompt = getString(screen, "prompt");
|
|
247
|
+
const status = getString(metadata, "status") ?? "(unknown)";
|
|
248
|
+
const deviceType = getString(screen, "deviceType") ?? "(unknown)";
|
|
249
|
+
const screenType = getString(screen, "screenType") ?? "(unknown)";
|
|
250
|
+
const generatedBy = getString(screen, "generatedBy") ?? "(unknown)";
|
|
251
|
+
const summary = getString(metadata, "summary");
|
|
252
|
+
const suggestions = listStringValues(metadata?.suggestions);
|
|
253
|
+
return [
|
|
254
|
+
`# ${title}`,
|
|
255
|
+
"",
|
|
256
|
+
"## IDs",
|
|
257
|
+
`- Screen name: ${name ?? "(unknown)"}`,
|
|
258
|
+
`- Project ID: ${getProjectIdFromName(name) ?? "(unknown)"}`,
|
|
259
|
+
`- Screen ID: ${getString(screen, "id") ?? getScreenIdFromName(name) ?? "(unknown)"}`,
|
|
260
|
+
"",
|
|
261
|
+
"## Purpose",
|
|
262
|
+
prompt ?? summary ?? "No purpose or prompt was included in the Stitch response.",
|
|
263
|
+
"",
|
|
264
|
+
"## Screen Metadata",
|
|
265
|
+
`- Status: ${status}`,
|
|
266
|
+
`- Device type: ${deviceType}`,
|
|
267
|
+
`- Screen type: ${screenType}`,
|
|
268
|
+
`- Generated by: ${generatedBy}`,
|
|
269
|
+
`- Width: ${getString(screen, "width") ?? "(unknown)"}`,
|
|
270
|
+
`- Height: ${getString(screen, "height") ?? "(unknown)"}`,
|
|
271
|
+
"",
|
|
272
|
+
"## Notable UI Sections",
|
|
273
|
+
bulletList(suggestions, "No explicit section/suggestion list was included in the Stitch response. Inspect implementation-context.md and raw.json for rendered assets and code references."),
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function buildImplementationContext(payload) {
|
|
277
|
+
const screen = getScreen(payload);
|
|
278
|
+
const metadata = getNestedRecord(screen, "screenMetadata");
|
|
279
|
+
const theme = getTheme(screen);
|
|
280
|
+
const namedColors = getNestedRecord(theme, "namedColors");
|
|
281
|
+
const designSystem = getNestedRecord(screen, "designSystem");
|
|
282
|
+
const designSystemBody = getNestedRecord(designSystem, "designSystem");
|
|
283
|
+
const htmlCode = getNestedRecord(screen, "htmlCode");
|
|
284
|
+
const screenshot = getNestedRecord(screen, "screenshot");
|
|
285
|
+
const figmaExport = getNestedRecord(screen, "figmaExport");
|
|
286
|
+
const textContent = collectTextContent(screen);
|
|
287
|
+
const colorTokens = describeTokenRecord(namedColors);
|
|
288
|
+
const themeTokens = describeTokenRecord(theme, 20);
|
|
289
|
+
return [
|
|
290
|
+
`# Implementation Context: ${getString(screen, "title") ?? "Stitch Screen"}`,
|
|
291
|
+
"",
|
|
292
|
+
"## Source",
|
|
293
|
+
`- Screen name: ${getString(screen, "name") ?? "(unknown)"}`,
|
|
294
|
+
`- HTML artifact: ${getString(htmlCode, "name") ?? "(none)"}`,
|
|
295
|
+
`- HTML download URL: ${getString(htmlCode, "downloadUrl") ?? "(none)"}`,
|
|
296
|
+
`- Screenshot artifact: ${getString(screenshot, "name") ?? "(none)"}`,
|
|
297
|
+
`- Screenshot download URL: ${getString(screenshot, "downloadUrl") ?? "(none)"}`,
|
|
298
|
+
`- Figma artifact: ${getString(figmaExport, "name") ?? "(none)"}`,
|
|
299
|
+
"",
|
|
300
|
+
"## Layout",
|
|
301
|
+
`- Device type: ${getString(screen, "deviceType") ?? "(unknown)"}`,
|
|
302
|
+
`- Canvas size: ${getString(screen, "width") ?? "unknown"} x ${getString(screen, "height") ?? "unknown"}`,
|
|
303
|
+
`- Display mode: ${getString(metadata, "displayMode") ?? "(unknown)"}`,
|
|
304
|
+
"",
|
|
305
|
+
"## Components And Content",
|
|
306
|
+
bulletList(textContent, "No structured text content was included in the Stitch response."),
|
|
307
|
+
"",
|
|
308
|
+
"## Interactions",
|
|
309
|
+
bulletList(listStringValues(metadata?.suggestions), "No explicit interactions were included. Infer behavior from the screen purpose, linked HTML artifact, and raw.json."),
|
|
310
|
+
"",
|
|
311
|
+
"## Design Tokens",
|
|
312
|
+
`- Design system: ${getString(designSystem, "name") ?? getString(designSystemBody, "displayName") ?? "(unknown)"}`,
|
|
313
|
+
"",
|
|
314
|
+
"### Theme",
|
|
315
|
+
bulletList(themeTokens, "No theme tokens were included."),
|
|
316
|
+
"",
|
|
317
|
+
"### Named Colors",
|
|
318
|
+
bulletList(colorTokens, "No named color tokens were included."),
|
|
319
|
+
"",
|
|
320
|
+
"## Unknowns",
|
|
321
|
+
"- Assets referenced by download URLs are not fetched by this export.",
|
|
322
|
+
"- Fine-grained DOM/component hierarchy may require inspecting the linked HTML artifact or raw.json.",
|
|
323
|
+
].join("\n");
|
|
324
|
+
}
|
|
325
|
+
function getScreenFacts(payload) {
|
|
326
|
+
const screen = getScreen(payload);
|
|
327
|
+
const metadata = getNestedRecord(screen, "screenMetadata");
|
|
328
|
+
const theme = getTheme(screen);
|
|
329
|
+
const namedColors = getNestedRecord(theme, "namedColors");
|
|
330
|
+
const htmlCode = getNestedRecord(screen, "htmlCode");
|
|
331
|
+
const screenshot = getNestedRecord(screen, "screenshot");
|
|
332
|
+
const figmaExport = getNestedRecord(screen, "figmaExport");
|
|
333
|
+
const title = getString(screen, "title");
|
|
334
|
+
const prompt = getString(screen, "prompt");
|
|
335
|
+
const summary = getString(metadata, "summary");
|
|
336
|
+
const statusMessage = getString(metadata, "statusMessage");
|
|
337
|
+
const suggestions = listStringValues(metadata?.suggestions);
|
|
338
|
+
const copyFacts = collectCopyFacts(screen);
|
|
339
|
+
return {
|
|
340
|
+
screen,
|
|
341
|
+
metadata,
|
|
342
|
+
theme,
|
|
343
|
+
namedColors,
|
|
344
|
+
htmlCode,
|
|
345
|
+
screenshot,
|
|
346
|
+
figmaExport,
|
|
347
|
+
title,
|
|
348
|
+
prompt,
|
|
349
|
+
summary,
|
|
350
|
+
statusMessage,
|
|
351
|
+
suggestions,
|
|
352
|
+
copyFacts,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function buildImplementationPlan(payload) {
|
|
356
|
+
const facts = getScreenFacts(payload);
|
|
357
|
+
const screenName = getString(facts.screen, "name");
|
|
358
|
+
const deviceType = getString(facts.screen, "deviceType");
|
|
359
|
+
const width = getString(facts.screen, "width");
|
|
360
|
+
const height = getString(facts.screen, "height");
|
|
361
|
+
const hasHtml = Boolean(getString(facts.htmlCode, "downloadUrl"));
|
|
362
|
+
const hasScreenshot = Boolean(getString(facts.screenshot, "downloadUrl"));
|
|
363
|
+
const hasTokens = Boolean(facts.theme || facts.namedColors);
|
|
364
|
+
return [
|
|
365
|
+
`# Implementation Plan: ${facts.title ?? "Stitch Screen"}`,
|
|
366
|
+
"",
|
|
367
|
+
"## Source Basis",
|
|
368
|
+
`- Extracted screen name: ${screenName ?? "(missing)"}`,
|
|
369
|
+
`- Extracted device/canvas: ${deviceType ?? "unknown"} ${width ?? "unknown"} x ${height ?? "unknown"}`,
|
|
370
|
+
`- Extracted HTML artifact present: ${hasHtml ? "yes" : "no"}`,
|
|
371
|
+
`- Extracted screenshot artifact present: ${hasScreenshot ? "yes" : "no"}`,
|
|
372
|
+
`- Extracted design tokens present: ${hasTokens ? "yes" : "no"}`,
|
|
373
|
+
"",
|
|
374
|
+
"## Likely Page / Component Structure",
|
|
375
|
+
"- Inferred: create a page-level screen container that matches the extracted canvas/device type.",
|
|
376
|
+
"- Inferred: add a header/title region if the visible title appears in copy.md.",
|
|
377
|
+
"- Inferred: group related controls/content into semantic sections after inspecting the screenshot or HTML artifact.",
|
|
378
|
+
"- Inferred: keep data, navigation actions, and form controls isolated in small reusable components where the UI repeats.",
|
|
379
|
+
"",
|
|
380
|
+
"## Suggested Build Order",
|
|
381
|
+
"1. Read `screen-summary.md`, `copy.md`, and `style-notes.md`.",
|
|
382
|
+
"2. Inspect the screenshot and/or linked HTML artifact from `implementation-context.md`.",
|
|
383
|
+
"3. Scaffold the route/page and set responsive canvas constraints.",
|
|
384
|
+
"4. Implement layout sections from largest containers to smallest controls.",
|
|
385
|
+
"5. Apply extracted tokens from `style-notes.md`; use existing app tokens when Stitch tokens are absent.",
|
|
386
|
+
"6. Add interactions and states that are explicit in `component-map.json`; mark any inferred behavior in code review notes.",
|
|
387
|
+
"7. Check keyboard navigation, labels, focus states, and responsive behavior.",
|
|
388
|
+
"",
|
|
389
|
+
"## Unknowns / Assumptions",
|
|
390
|
+
bulletList([
|
|
391
|
+
!hasHtml ? "HTML artifact was not present in the Stitch payload." : "",
|
|
392
|
+
!hasScreenshot ? "Screenshot artifact was not present in the Stitch payload." : "",
|
|
393
|
+
!hasTokens ? "Design tokens were not present; style values must be inferred from visual assets or existing app tokens." : "",
|
|
394
|
+
facts.copyFacts.visibleText.length === 0 ? "No confident visible UI text was extractable from structured screen fields." : "",
|
|
395
|
+
"Inferred structure should be validated against the screenshot/HTML before implementation is considered complete.",
|
|
396
|
+
].filter(Boolean), "No major unknowns were detected from structured payload fields."),
|
|
397
|
+
"",
|
|
398
|
+
"## Accessibility Notes",
|
|
399
|
+
"- Inferred: use semantic landmarks for page/header/main/footer regions where applicable.",
|
|
400
|
+
"- Inferred: ensure every icon-only control has an accessible name.",
|
|
401
|
+
"- Inferred: preserve visible text from `copy.md` as real text, not baked into images.",
|
|
402
|
+
"- Inferred: verify color contrast after applying tokens or app theme colors.",
|
|
403
|
+
"- Inferred: support keyboard focus order matching visual order.",
|
|
404
|
+
"",
|
|
405
|
+
"## Responsive / Layout Notes",
|
|
406
|
+
`- Extracted device type: ${deviceType ?? "(unknown)"}.`,
|
|
407
|
+
`- Extracted canvas: ${width ?? "unknown"} x ${height ?? "unknown"}.`,
|
|
408
|
+
"- Inferred: treat extracted canvas dimensions as a reference, not a fixed viewport unless the product requires it.",
|
|
409
|
+
"- Inferred: define behavior for narrow, standard, and wide breakpoints before final polish.",
|
|
410
|
+
].join("\n");
|
|
411
|
+
}
|
|
412
|
+
function buildComponentMap(payload) {
|
|
413
|
+
const facts = getScreenFacts(payload);
|
|
414
|
+
const screenName = getString(facts.screen, "name");
|
|
415
|
+
const source = {
|
|
416
|
+
screenName,
|
|
417
|
+
title: facts.title,
|
|
418
|
+
deviceType: getString(facts.screen, "deviceType"),
|
|
419
|
+
width: getString(facts.screen, "width"),
|
|
420
|
+
height: getString(facts.screen, "height"),
|
|
421
|
+
htmlArtifact: getString(facts.htmlCode, "name"),
|
|
422
|
+
screenshotArtifact: getString(facts.screenshot, "name"),
|
|
423
|
+
};
|
|
424
|
+
const sections = [
|
|
425
|
+
{
|
|
426
|
+
name: facts.title ? `${facts.title} screen container` : "Screen container",
|
|
427
|
+
provenance: "inferred",
|
|
428
|
+
rationale: "Stitch payload did not include a detailed component tree in structured fields.",
|
|
429
|
+
likelyComponents: [
|
|
430
|
+
{
|
|
431
|
+
name: "PageShell",
|
|
432
|
+
provenance: "inferred",
|
|
433
|
+
labels: facts.copyFacts.visibleText,
|
|
434
|
+
interactions: [],
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
];
|
|
439
|
+
if (facts.suggestions.length > 0) {
|
|
440
|
+
sections.push({
|
|
441
|
+
name: "Suggested follow-up actions",
|
|
442
|
+
provenance: "inferred",
|
|
443
|
+
rationale: "Derived from screenMetadata.suggestions; not clearly marked as rendered screen nodes.",
|
|
444
|
+
likelyComponents: facts.suggestions.map((suggestion) => ({
|
|
445
|
+
name: "Action",
|
|
446
|
+
provenance: "inferred",
|
|
447
|
+
labels: [suggestion],
|
|
448
|
+
interactions: ["Uncertain suggested next action"],
|
|
449
|
+
})),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
source,
|
|
454
|
+
sections,
|
|
455
|
+
textContent: facts.copyFacts.visibleText.map((text) => ({
|
|
456
|
+
text,
|
|
457
|
+
provenance: "extracted:screen.title",
|
|
458
|
+
})),
|
|
459
|
+
interactions: facts.suggestions.map((suggestion) => ({
|
|
460
|
+
label: suggestion,
|
|
461
|
+
provenance: "uncertain:screenMetadata.suggestions",
|
|
462
|
+
})),
|
|
463
|
+
notes: [
|
|
464
|
+
"Fields marked inferred should be validated against the screenshot or linked HTML artifact.",
|
|
465
|
+
"No precise component hierarchy is invented when the Stitch payload does not include one.",
|
|
466
|
+
],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function buildCopyMarkdown(payload) {
|
|
470
|
+
const facts = getScreenFacts(payload);
|
|
471
|
+
return [
|
|
472
|
+
`# Copy: ${facts.title ?? "Stitch Screen"}`,
|
|
473
|
+
"",
|
|
474
|
+
"## Visible / UI Text",
|
|
475
|
+
bulletList(facts.copyFacts.visibleText, "No confident visible UI text was extractable from structured screen fields."),
|
|
476
|
+
"",
|
|
477
|
+
"## Possible User-Facing Text From Metadata",
|
|
478
|
+
bulletList(facts.copyFacts.possibleUserFacingText, "No possible user-facing metadata text was present."),
|
|
479
|
+
"",
|
|
480
|
+
"These items may be displayed in some Stitch/client surfaces, but the payload did not clearly mark them as rendered screen nodes.",
|
|
481
|
+
"",
|
|
482
|
+
"## Generation / Context Text (Not UI Copy)",
|
|
483
|
+
bulletList(facts.copyFacts.generationContextText, "No generation/context text was present."),
|
|
484
|
+
"",
|
|
485
|
+
"## Provenance",
|
|
486
|
+
"- Visible/UI text is limited to structured screen fields that are reasonably likely to be rendered, currently `title`.",
|
|
487
|
+
"- Metadata text comes from fields such as `screenMetadata.statusMessage` and `screenMetadata.suggestions`; it is uncertain unless raw.json clearly marks it as rendered.",
|
|
488
|
+
"- Generation/context text comes from fields such as `prompt` and `screenMetadata.summary`; it should not be implemented as visible UI copy without separate confirmation.",
|
|
489
|
+
"- This file does not OCR screenshots or fetch linked HTML assets.",
|
|
490
|
+
].join("\n");
|
|
491
|
+
}
|
|
492
|
+
function buildStyleNotes(payload) {
|
|
493
|
+
const facts = getScreenFacts(payload);
|
|
494
|
+
const themeTokens = describeTokenRecord(facts.theme, 30);
|
|
495
|
+
const colorTokens = describeTokenRecord(facts.namedColors, 40);
|
|
496
|
+
return [
|
|
497
|
+
`# Style Notes: ${facts.title ?? "Stitch Screen"}`,
|
|
498
|
+
"",
|
|
499
|
+
"## Extracted Layout Values",
|
|
500
|
+
`- Device type: ${getString(facts.screen, "deviceType") ?? "(not present)"}`,
|
|
501
|
+
`- Width: ${getString(facts.screen, "width") ?? "(not present)"}`,
|
|
502
|
+
`- Height: ${getString(facts.screen, "height") ?? "(not present)"}`,
|
|
503
|
+
"",
|
|
504
|
+
"## Extracted Theme Tokens",
|
|
505
|
+
bulletList(themeTokens, "No theme token object was present in the structured Stitch payload."),
|
|
506
|
+
"",
|
|
507
|
+
"## Extracted Color Tokens",
|
|
508
|
+
bulletList(colorTokens, "No named color token object was present in the structured Stitch payload."),
|
|
509
|
+
"",
|
|
510
|
+
"## Typography / Spacing",
|
|
511
|
+
bulletList([
|
|
512
|
+
getString(facts.theme, "font") ? `font: ${getString(facts.theme, "font")}` : "",
|
|
513
|
+
getString(facts.theme, "headlineFont") ? `headlineFont: ${getString(facts.theme, "headlineFont")}` : "",
|
|
514
|
+
getString(facts.theme, "bodyFont") ? `bodyFont: ${getString(facts.theme, "bodyFont")}` : "",
|
|
515
|
+
getString(facts.theme, "labelFont") ? `labelFont: ${getString(facts.theme, "labelFont")}` : "",
|
|
516
|
+
getString(facts.theme, "spacingScale") ? `spacingScale: ${getString(facts.theme, "spacingScale")}` : "",
|
|
517
|
+
getString(facts.theme, "roundness") ? `roundness: ${getString(facts.theme, "roundness")}` : "",
|
|
518
|
+
].filter(Boolean), "No typography, spacing, or roundness tokens were present in the structured Stitch payload."),
|
|
519
|
+
"",
|
|
520
|
+
"## Inferred Style Guidance",
|
|
521
|
+
"- Inferred: if tokens are absent, map visual styling to the host app's existing design system rather than inventing exact values.",
|
|
522
|
+
"- Inferred: validate spacing, typography, and colors against the screenshot or linked HTML artifact before final implementation.",
|
|
523
|
+
].join("\n");
|
|
524
|
+
}
|
|
525
|
+
function buildBuildPrompt(payload) {
|
|
526
|
+
const facts = getScreenFacts(payload);
|
|
527
|
+
const screenName = getString(facts.screen, "name") ?? "(unknown)";
|
|
528
|
+
const title = facts.title ?? "Stitch Screen";
|
|
529
|
+
const hasTokens = Boolean(facts.theme || facts.namedColors);
|
|
530
|
+
const htmlUrl = getString(facts.htmlCode, "downloadUrl");
|
|
531
|
+
const screenshotUrl = getString(facts.screenshot, "downloadUrl");
|
|
532
|
+
return [
|
|
533
|
+
`# Build Prompt: ${title}`,
|
|
534
|
+
"",
|
|
535
|
+
"You are implementing a screen from a Stitch artifact bundle. Use the files in this directory as source material. Do not invent exact design values that are not present in the artifacts; when values are missing, use the host app's existing design system and mark assumptions in your implementation notes.",
|
|
536
|
+
"",
|
|
537
|
+
"## Source Files To Read First",
|
|
538
|
+
"1. `screen-summary.md`",
|
|
539
|
+
"2. `implementation-context.md`",
|
|
540
|
+
"3. `implementation-plan.md`",
|
|
541
|
+
"4. `component-map.json`",
|
|
542
|
+
"5. `copy.md`",
|
|
543
|
+
"6. `style-notes.md`",
|
|
544
|
+
"7. `raw.json` only when structured details are missing from the markdown/json summaries",
|
|
545
|
+
"",
|
|
546
|
+
"## Screen To Implement",
|
|
547
|
+
`- Extracted title: ${title}`,
|
|
548
|
+
`- Extracted screen name: ${screenName}`,
|
|
549
|
+
`- Extracted device/canvas: ${getString(facts.screen, "deviceType") ?? "unknown"} ${getString(facts.screen, "width") ?? "unknown"} x ${getString(facts.screen, "height") ?? "unknown"}`,
|
|
550
|
+
`- Extracted HTML URL: ${htmlUrl ?? "(not present)"}`,
|
|
551
|
+
`- Extracted screenshot URL: ${screenshotUrl ?? "(not present)"}`,
|
|
552
|
+
`- Extracted design tokens present: ${hasTokens ? "yes" : "no"}`,
|
|
553
|
+
"",
|
|
554
|
+
"## Implementation Instructions",
|
|
555
|
+
"- Build the screen in the current app's existing framework and design conventions.",
|
|
556
|
+
"- Preserve confident visible UI text from `copy.md`.",
|
|
557
|
+
"- Treat metadata and generation/context text in `copy.md` as non-UI unless confirmed elsewhere.",
|
|
558
|
+
"- Use `component-map.json` for extracted facts and inferred component organization; validate inferred items against available visual/source artifacts.",
|
|
559
|
+
"- Use `style-notes.md` for extracted tokens. If tokens are absent, map to existing app tokens rather than inventing precise colors, spacing, or typography.",
|
|
560
|
+
"- Implement responsive behavior appropriate for the extracted device/canvas, but do not hard-code the canvas as the only supported viewport unless the product requires it.",
|
|
561
|
+
"- Add or update tests using `test-plan.md`.",
|
|
562
|
+
"",
|
|
563
|
+
"## Important Constraints",
|
|
564
|
+
"- Do not generate unrelated pages or features.",
|
|
565
|
+
"- Do not treat inferred structure as guaranteed design truth.",
|
|
566
|
+
"- Do not implement prompt, summary, status, or suggestions as visible UI copy unless the payload clearly marks them as rendered.",
|
|
567
|
+
].join("\n");
|
|
568
|
+
}
|
|
569
|
+
function buildAcceptanceCriteria(payload) {
|
|
570
|
+
const facts = getScreenFacts(payload);
|
|
571
|
+
const title = facts.title ?? "Stitch Screen";
|
|
572
|
+
const hasTokens = Boolean(facts.theme || facts.namedColors);
|
|
573
|
+
return [
|
|
574
|
+
`# Acceptance Criteria: ${title}`,
|
|
575
|
+
"",
|
|
576
|
+
"- The implemented screen corresponds to the extracted Stitch screen id/name in `manifest.json`.",
|
|
577
|
+
"- Confident visible UI text from `copy.md` is present in the implementation.",
|
|
578
|
+
"- Metadata/context text from `copy.md` is not rendered as UI unless confirmed by `raw.json`, screenshot, linked HTML, or product direction.",
|
|
579
|
+
"- Layout follows the extracted device/canvas as a reference and remains responsive for relevant app breakpoints.",
|
|
580
|
+
"- Inferred sections/components from `component-map.json` are validated against available source artifacts before being treated as final.",
|
|
581
|
+
hasTokens
|
|
582
|
+
? "- Extracted design tokens from `style-notes.md` are applied or mapped to equivalent app tokens."
|
|
583
|
+
: "- Because no design tokens were extracted, styling uses the app's existing design system without inventing exact Stitch values.",
|
|
584
|
+
"- Accessibility basics are covered: semantic structure, keyboard navigation, focus visibility, accessible names, and text contrast.",
|
|
585
|
+
"- Implementation avoids unrelated feature work and keeps changes scoped to the requested screen.",
|
|
586
|
+
"- Tests from `test-plan.md` are implemented where appropriate for the app's test stack.",
|
|
587
|
+
"- Open questions in `questions.md` are resolved or explicitly documented as assumptions.",
|
|
588
|
+
].join("\n");
|
|
589
|
+
}
|
|
590
|
+
function buildTestPlan(payload) {
|
|
591
|
+
const facts = getScreenFacts(payload);
|
|
592
|
+
const title = facts.title ?? "Stitch Screen";
|
|
593
|
+
const visibleText = facts.copyFacts.visibleText;
|
|
594
|
+
return [
|
|
595
|
+
`# Test Plan: ${title}`,
|
|
596
|
+
"",
|
|
597
|
+
"## Unit / Utility Tests",
|
|
598
|
+
"- Test any data formatting, routing helpers, or state helpers introduced for this screen.",
|
|
599
|
+
"- Test conditional rendering for empty/loading/error states if those states are part of the implementation.",
|
|
600
|
+
"",
|
|
601
|
+
"## Component Tests",
|
|
602
|
+
...(visibleText.length > 0
|
|
603
|
+
? visibleText.map((text) => `- Assert visible UI text is rendered: ${JSON.stringify(text)}.`)
|
|
604
|
+
: ["- No confident visible UI text was extracted; test stable semantic landmarks, roles, or labels from the implemented design."]),
|
|
605
|
+
"- Assert primary sections/components render without crashing.",
|
|
606
|
+
"- Assert interactive controls have accessible names and expected enabled/disabled states.",
|
|
607
|
+
"- Assert metadata/context text from `copy.md` is not accidentally rendered as visible UI copy unless explicitly confirmed.",
|
|
608
|
+
"",
|
|
609
|
+
"## Responsive / Visual Checks",
|
|
610
|
+
`- Check layout at the extracted reference size: ${getString(facts.screen, "width") ?? "unknown"} x ${getString(facts.screen, "height") ?? "unknown"}.`,
|
|
611
|
+
"- Check at the app's common small, medium, and large breakpoints.",
|
|
612
|
+
"- Verify text does not overlap, clip, or escape containers.",
|
|
613
|
+
"",
|
|
614
|
+
"## E2E / Flow Tests",
|
|
615
|
+
"- Add navigation smoke coverage for reaching this screen if it is route-accessible.",
|
|
616
|
+
"- Exercise primary interactions if they are confirmed by product requirements or source artifacts.",
|
|
617
|
+
"- Include an accessibility smoke pass if the project has an automated a11y tool.",
|
|
618
|
+
].join("\n");
|
|
619
|
+
}
|
|
620
|
+
function buildQuestions(payload) {
|
|
621
|
+
const facts = getScreenFacts(payload);
|
|
622
|
+
const title = facts.title ?? "Stitch Screen";
|
|
623
|
+
const questions = [
|
|
624
|
+
facts.copyFacts.visibleText.length === 0
|
|
625
|
+
? "No confident visible UI text was extracted. Should copy be taken from screenshot/HTML, product specs, or existing app content?"
|
|
626
|
+
: "",
|
|
627
|
+
facts.copyFacts.possibleUserFacingText.length > 0
|
|
628
|
+
? "Should any possible user-facing metadata in `copy.md` be rendered in the app, or is it only Stitch/client metadata?"
|
|
629
|
+
: "",
|
|
630
|
+
facts.copyFacts.generationContextText.length > 0
|
|
631
|
+
? "Should any generation/context text in `copy.md` influence UI content, or remain implementation context only?"
|
|
632
|
+
: "",
|
|
633
|
+
!(facts.theme || facts.namedColors)
|
|
634
|
+
? "No design tokens were present. Which app theme/tokens should be used for final color, typography, spacing, and radius?"
|
|
635
|
+
: "",
|
|
636
|
+
!getString(facts.htmlCode, "downloadUrl")
|
|
637
|
+
? "No linked HTML artifact was present. Is screenshot/raw payload enough to implement the component hierarchy?"
|
|
638
|
+
: "",
|
|
639
|
+
!getString(facts.screenshot, "downloadUrl")
|
|
640
|
+
? "No screenshot artifact was present. What visual reference should be used for layout validation?"
|
|
641
|
+
: "",
|
|
642
|
+
"What route, navigation entry point, and surrounding app shell should host this screen?",
|
|
643
|
+
"Which interactions are required versus decorative or suggested by Stitch metadata?",
|
|
644
|
+
"Are there loading, empty, error, or permission states that should be implemented for this screen?",
|
|
645
|
+
].filter(Boolean);
|
|
646
|
+
return [
|
|
647
|
+
`# Questions: ${title}`,
|
|
648
|
+
"",
|
|
649
|
+
"These questions should be answered before or during implementation. Some are inferred from missing artifact data.",
|
|
650
|
+
"",
|
|
651
|
+
...questions.map((question) => `- ${question}`),
|
|
652
|
+
].join("\n");
|
|
653
|
+
}
|
|
654
|
+
function createManifest(options) {
|
|
655
|
+
const screen = getScreen(options.payload);
|
|
656
|
+
const name = getString(screen, "name");
|
|
657
|
+
return {
|
|
658
|
+
generatedAt: options.generatedAt,
|
|
659
|
+
source: {
|
|
660
|
+
screenName: name,
|
|
661
|
+
projectId: getProjectIdFromName(name),
|
|
662
|
+
screenId: getString(screen, "id") ?? getScreenIdFromName(name),
|
|
663
|
+
title: getString(screen, "title"),
|
|
664
|
+
},
|
|
665
|
+
input: options.input,
|
|
666
|
+
resolver: options.resolver,
|
|
667
|
+
output: {
|
|
668
|
+
artifactPath: options.artifactPath,
|
|
669
|
+
resolvedOutputDir: options.resolvedOutputDir,
|
|
670
|
+
baseRoot: options.baseRoot,
|
|
671
|
+
baseRootSource: options.baseRootSource,
|
|
672
|
+
mode: options.outputMode,
|
|
673
|
+
},
|
|
674
|
+
paths: options.paths,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
export function registerStitchExportTool(server) {
|
|
678
|
+
server.registerTool("stitch_export_screen_artifact", {
|
|
679
|
+
description: "Exports Stitch screen data as an artifact bundle. artifactPath is the preferred workspace-relative output directory. PROJECT_ROOT is preferred as the base root; STITCH_OUTPUT_DIR is fallback/testing only when PROJECT_ROOT is unset. artifactName and relativePath are legacy fallback inputs. If screenData is provided, it is exported directly. Otherwise rawGetScreenInput takes precedence over screenId when fetching via get_screen.",
|
|
680
|
+
inputSchema: {
|
|
681
|
+
screenId: z.string().optional(),
|
|
682
|
+
projectId: z.string().optional(),
|
|
683
|
+
artifactPath: z
|
|
684
|
+
.string()
|
|
685
|
+
.trim()
|
|
686
|
+
.min(1)
|
|
687
|
+
.max(240)
|
|
688
|
+
.refine((value) => isSafeRelativePathInput(value), {
|
|
689
|
+
message: "artifactPath must be non-empty, relative, and must not include traversal or suspicious segments.",
|
|
690
|
+
})
|
|
691
|
+
.optional()
|
|
692
|
+
.describe("Workspace-relative artifact bundle directory, for example .artifacts/features/settings/design."),
|
|
693
|
+
artifactName: z
|
|
694
|
+
.string()
|
|
695
|
+
.min(1)
|
|
696
|
+
.max(80)
|
|
697
|
+
.regex(ARTIFACT_NAME_PATTERN, "artifactName may include only letters, numbers, ., _, and -")
|
|
698
|
+
.optional(),
|
|
699
|
+
relativePath: z
|
|
700
|
+
.string()
|
|
701
|
+
.trim()
|
|
702
|
+
.min(1)
|
|
703
|
+
.max(200)
|
|
704
|
+
.refine((value) => isSafeRelativePathInput(value), {
|
|
705
|
+
message: "relativePath must be non-empty, relative, and must not include traversal or suspicious segments.",
|
|
706
|
+
})
|
|
707
|
+
.optional()
|
|
708
|
+
.describe("Legacy workspace-relative artifact bundle directory, for example exports/my-screen."),
|
|
709
|
+
rawGetScreenInput: z.record(z.string(), z.unknown()).optional(),
|
|
710
|
+
screenData: z.unknown().optional(),
|
|
711
|
+
},
|
|
712
|
+
}, async ({ screenId, projectId, artifactPath, artifactName, relativePath, rawGetScreenInput, screenData }) => {
|
|
713
|
+
if (!screenData && !screenId && !rawGetScreenInput) {
|
|
714
|
+
return {
|
|
715
|
+
content: [
|
|
716
|
+
{
|
|
717
|
+
type: "text",
|
|
718
|
+
text: "Invalid input: provide screenData directly, or provide screenId/rawGetScreenInput to fetch from Stitch.",
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const config = getStitchConfig();
|
|
724
|
+
let payload = screenData;
|
|
725
|
+
let fetchInput;
|
|
726
|
+
let resolverInfo;
|
|
727
|
+
if (!payload) {
|
|
728
|
+
const client = new StitchClient(config);
|
|
729
|
+
const screenIdentifier = screenId ? toScreenIdentifier(screenId) : null;
|
|
730
|
+
const resolved = screenIdentifier
|
|
731
|
+
? {
|
|
732
|
+
ok: true,
|
|
733
|
+
input: screenIdentifier,
|
|
734
|
+
resolver: {
|
|
735
|
+
requested: screenId,
|
|
736
|
+
projectId: screenIdentifier.projectId,
|
|
737
|
+
strategy: "full-resource-name",
|
|
738
|
+
matchedName: screenIdentifier.name,
|
|
739
|
+
matchedScreenId: screenIdentifier.screenId,
|
|
740
|
+
},
|
|
741
|
+
}
|
|
742
|
+
: screenId
|
|
743
|
+
? await resolveScreenInput({
|
|
744
|
+
client,
|
|
745
|
+
screenIdOrName: screenId,
|
|
746
|
+
projectIdOrName: projectId,
|
|
747
|
+
})
|
|
748
|
+
: null;
|
|
749
|
+
if (!rawGetScreenInput && !resolved) {
|
|
750
|
+
return {
|
|
751
|
+
content: [
|
|
752
|
+
{
|
|
753
|
+
type: "text",
|
|
754
|
+
text: "Invalid input: provide a full screenId path, rawGetScreenInput, screenData directly, or projectId with a screen id/title fragment.",
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (resolved && !resolved.ok) {
|
|
760
|
+
return {
|
|
761
|
+
content: [
|
|
762
|
+
{
|
|
763
|
+
type: "text",
|
|
764
|
+
text: resolved.error,
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const getScreenInput = rawGetScreenInput ?? resolved?.input;
|
|
770
|
+
if (!getScreenInput) {
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: "text",
|
|
775
|
+
text: "Invalid input: provide a full screenId path, rawGetScreenInput, screenData directly, or projectId with a screen id/title fragment.",
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
fetchInput = getScreenInput;
|
|
781
|
+
resolverInfo = resolved?.ok ? resolved.resolver : undefined;
|
|
782
|
+
const fetchResult = await client.callTool({
|
|
783
|
+
toolName: "get_screen",
|
|
784
|
+
input: getScreenInput,
|
|
785
|
+
});
|
|
786
|
+
if (!fetchResult.ok) {
|
|
787
|
+
return {
|
|
788
|
+
content: [
|
|
789
|
+
{
|
|
790
|
+
type: "text",
|
|
791
|
+
text: `stitch_export_screen_artifact failed while fetching screen.\n\n${fetchResult.error}`,
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
payload = fetchResult.data;
|
|
797
|
+
}
|
|
798
|
+
const baseRootInfo = await getBaseRoot(config.outputDir);
|
|
799
|
+
if (!baseRootInfo.ok) {
|
|
800
|
+
return {
|
|
801
|
+
content: [
|
|
802
|
+
{
|
|
803
|
+
type: "text",
|
|
804
|
+
text: baseRootInfo.error,
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const outputMode = artifactPath || relativePath ? "artifactPath" : artifactName ? "artifactName" : "default";
|
|
810
|
+
const relativeDir = artifactPath ??
|
|
811
|
+
relativePath ??
|
|
812
|
+
(artifactName
|
|
813
|
+
? path.join("exports", sanitizeFileName(artifactName))
|
|
814
|
+
: defaultArtifactPath(payload, screenId));
|
|
815
|
+
const outputPaths = {
|
|
816
|
+
"raw.json": "",
|
|
817
|
+
"screen-summary.md": "",
|
|
818
|
+
"implementation-context.md": "",
|
|
819
|
+
"implementation-plan.md": "",
|
|
820
|
+
"component-map.json": "",
|
|
821
|
+
"copy.md": "",
|
|
822
|
+
"style-notes.md": "",
|
|
823
|
+
"build-prompt.md": "",
|
|
824
|
+
"acceptance-criteria.md": "",
|
|
825
|
+
"test-plan.md": "",
|
|
826
|
+
"questions.md": "",
|
|
827
|
+
"manifest.json": "",
|
|
828
|
+
};
|
|
829
|
+
try {
|
|
830
|
+
for (const fileName of ARTIFACT_FILES) {
|
|
831
|
+
outputPaths[fileName] = await prepareSafeOutputPath(baseRootInfo.baseRoot, path.join(relativeDir, fileName));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
836
|
+
return {
|
|
837
|
+
content: [
|
|
838
|
+
{
|
|
839
|
+
type: "text",
|
|
840
|
+
text: `Invalid output path.\n\n${message}`,
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
const generatedAt = new Date().toISOString();
|
|
847
|
+
const rawJson = toPrettyJson(payload);
|
|
848
|
+
const screenSummary = buildScreenSummary(payload);
|
|
849
|
+
const implementationContext = buildImplementationContext(payload);
|
|
850
|
+
const implementationPlan = buildImplementationPlan(payload);
|
|
851
|
+
const componentMap = buildComponentMap(payload);
|
|
852
|
+
const copy = buildCopyMarkdown(payload);
|
|
853
|
+
const styleNotes = buildStyleNotes(payload);
|
|
854
|
+
const buildPrompt = buildBuildPrompt(payload);
|
|
855
|
+
const acceptanceCriteria = buildAcceptanceCriteria(payload);
|
|
856
|
+
const testPlan = buildTestPlan(payload);
|
|
857
|
+
const questions = buildQuestions(payload);
|
|
858
|
+
const manifest = createManifest({
|
|
859
|
+
payload,
|
|
860
|
+
input: {
|
|
861
|
+
...(screenId ? { screenId } : {}),
|
|
862
|
+
...(projectId ? { projectId } : {}),
|
|
863
|
+
...(artifactPath ? { artifactPath } : {}),
|
|
864
|
+
...(artifactName ? { artifactName } : {}),
|
|
865
|
+
...(relativePath ? { relativePath } : {}),
|
|
866
|
+
...(rawGetScreenInput ? { rawGetScreenInput } : {}),
|
|
867
|
+
...(screenData ? { screenDataProvided: true } : {}),
|
|
868
|
+
...(fetchInput ? { fetchInput } : {}),
|
|
869
|
+
},
|
|
870
|
+
resolver: resolverInfo,
|
|
871
|
+
generatedAt,
|
|
872
|
+
paths: outputPaths,
|
|
873
|
+
artifactPath: relativeDir,
|
|
874
|
+
resolvedOutputDir: path.dirname(outputPaths["manifest.json"]),
|
|
875
|
+
baseRoot: baseRootInfo.baseRoot,
|
|
876
|
+
baseRootSource: baseRootInfo.source,
|
|
877
|
+
outputMode,
|
|
878
|
+
});
|
|
879
|
+
await writeFile(outputPaths["raw.json"], `${rawJson}\n`, "utf8");
|
|
880
|
+
await writeFile(outputPaths["screen-summary.md"], `${screenSummary}\n`, "utf8");
|
|
881
|
+
await writeFile(outputPaths["implementation-context.md"], `${implementationContext}\n`, "utf8");
|
|
882
|
+
await writeFile(outputPaths["implementation-plan.md"], `${implementationPlan}\n`, "utf8");
|
|
883
|
+
await writeFile(outputPaths["component-map.json"], `${toPrettyJson(componentMap)}\n`, "utf8");
|
|
884
|
+
await writeFile(outputPaths["copy.md"], `${copy}\n`, "utf8");
|
|
885
|
+
await writeFile(outputPaths["style-notes.md"], `${styleNotes}\n`, "utf8");
|
|
886
|
+
await writeFile(outputPaths["build-prompt.md"], `${buildPrompt}\n`, "utf8");
|
|
887
|
+
await writeFile(outputPaths["acceptance-criteria.md"], `${acceptanceCriteria}\n`, "utf8");
|
|
888
|
+
await writeFile(outputPaths["test-plan.md"], `${testPlan}\n`, "utf8");
|
|
889
|
+
await writeFile(outputPaths["questions.md"], `${questions}\n`, "utf8");
|
|
890
|
+
await writeFile(outputPaths["manifest.json"], `${toPrettyJson(manifest)}\n`, "utf8");
|
|
891
|
+
const screen = getScreen(payload);
|
|
892
|
+
const title = getString(screen, "title") ?? "(untitled)";
|
|
893
|
+
const screenName = getString(screen, "name") ?? "(unknown)";
|
|
894
|
+
return {
|
|
895
|
+
content: [
|
|
896
|
+
{
|
|
897
|
+
type: "text",
|
|
898
|
+
text: "Stitch screen artifact bundle exported successfully.\n\n" +
|
|
899
|
+
`Title: ${title}\n` +
|
|
900
|
+
`Screen: ${screenName}\n` +
|
|
901
|
+
`Artifact path: ${relativeDir}\n` +
|
|
902
|
+
`Base root: ${baseRootInfo.baseRoot} (${baseRootInfo.source})\n` +
|
|
903
|
+
`Output directory: ${path.dirname(outputPaths["manifest.json"])}\n\n` +
|
|
904
|
+
"Files:\n" +
|
|
905
|
+
ARTIFACT_FILES.map((fileName) => `- ${fileName}: ${outputPaths[fileName]}`).join("\n"),
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
912
|
+
return {
|
|
913
|
+
content: [
|
|
914
|
+
{
|
|
915
|
+
type: "text",
|
|
916
|
+
text: `Failed to write artifact.\n\n${message}`,
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
//# sourceMappingURL=stitchExport.js.map
|