@fragments-sdk/mcp 0.8.1 → 0.10.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 +33 -17
- package/dist/bin.js +7 -21
- package/dist/bin.js.map +1 -1
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/{chunk-VV2PJ75X.js → chunk-WDQPNHZ2.js} +37 -6
- package/dist/chunk-WDQPNHZ2.js.map +1 -0
- package/dist/chunk-YJTMK4JY.js +4270 -0
- package/dist/chunk-YJTMK4JY.js.map +1 -0
- package/dist/{constants-YXOTMY3I.js → constants-BLN4SSNH.js} +2 -1
- package/dist/dist-TTCI6TME.js +60962 -0
- package/dist/dist-TTCI6TME.js.map +1 -0
- package/dist/index.js +75 -11
- package/dist/index.js.map +1 -1
- package/dist/init.js +36 -0
- package/dist/init.js.map +1 -1
- package/dist/rules-JUZ3RABB.js +8 -0
- package/dist/rules-JUZ3RABB.js.map +1 -0
- package/dist/{sass.node-4XJK6YBF-2NJM7G64.js → sass.node-4XJK6YBF-CPK77BO6.js} +2 -1
- package/dist/{sass.node-4XJK6YBF-2NJM7G64.js.map → sass.node-4XJK6YBF-CPK77BO6.js.map} +1 -1
- package/dist/server.js +2 -2
- package/package.json +7 -7
- package/dist/chunk-6JMX4AMO.js +0 -4885
- package/dist/chunk-6JMX4AMO.js.map +0 -1
- package/dist/chunk-VV2PJ75X.js.map +0 -1
- package/dist/chunk-YSRGQDEB.js +0 -93
- package/dist/chunk-YSRGQDEB.js.map +0 -1
- package/dist/dist-V7D67NXS.js +0 -1093
- package/dist/dist-V7D67NXS.js.map +0 -1
- package/dist/rules-CKBRD3UL.js +0 -8
- /package/dist/{constants-YXOTMY3I.js.map → chunk-7D4SUZUM.js.map} +0 -0
- /package/dist/{rules-CKBRD3UL.js.map → constants-BLN4SSNH.js.map} +0 -0
|
@@ -0,0 +1,4270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BRAND
|
|
3
|
+
} from "./chunk-4SVS3AA3.js";
|
|
4
|
+
|
|
5
|
+
// src/server.ts
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import {
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
ErrorCode,
|
|
11
|
+
ListResourcesRequestSchema,
|
|
12
|
+
ListToolsRequestSchema,
|
|
13
|
+
McpError,
|
|
14
|
+
ReadResourceRequestSchema
|
|
15
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
|
+
import { existsSync as existsSync8 } from "fs";
|
|
17
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
18
|
+
import { join as join7 } from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
22
|
+
import { readFileSync, existsSync } from "fs";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
function loadConfigFile(projectRoot) {
|
|
25
|
+
const configPath = join(projectRoot, "ds-mcp.config.json");
|
|
26
|
+
if (existsSync(configPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const content = readFileSync(configPath, "utf-8");
|
|
29
|
+
return JSON.parse(content);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw new Error(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
35
|
+
if (existsSync(pkgPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const content = readFileSync(pkgPath, "utf-8");
|
|
38
|
+
const pkg = JSON.parse(content);
|
|
39
|
+
if (pkg.dsMcp) return pkg.dsMcp;
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/server.ts
|
|
47
|
+
import { buildMcpTools, buildToolNames, MCP_TOOL_DEFINITIONS } from "@fragments-sdk/context/mcp-tools";
|
|
48
|
+
|
|
49
|
+
// src/version.ts
|
|
50
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
51
|
+
function readPackageVersion() {
|
|
52
|
+
try {
|
|
53
|
+
const raw = readFileSync2(new URL("../package.json", import.meta.url), "utf-8");
|
|
54
|
+
const pkg = JSON.parse(raw);
|
|
55
|
+
return pkg.version ?? "0.0.0";
|
|
56
|
+
} catch {
|
|
57
|
+
return "0.0.0";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var MCP_SERVER_VERSION = readPackageVersion();
|
|
61
|
+
|
|
62
|
+
// src/catalog-meta.ts
|
|
63
|
+
function getCatalogMeta(data) {
|
|
64
|
+
const rawUpdatedAt = data.validateFixContext?.updatedAt ?? data.snapshot.metadata.updatedAt;
|
|
65
|
+
const updatedAt = typeof rawUpdatedAt === "number" ? new Date(rawUpdatedAt).toISOString() : rawUpdatedAt;
|
|
66
|
+
return {
|
|
67
|
+
catalogRevision: data.validateFixContext?.catalogRevision ?? data.snapshot.metadata.revision,
|
|
68
|
+
updatedAt
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/token-suggestions.ts
|
|
73
|
+
function propertyFamilyFor(property, value) {
|
|
74
|
+
const prop = property.toLowerCase().trim();
|
|
75
|
+
const normalizedValue = value?.toLowerCase().trim();
|
|
76
|
+
if (prop.includes("shadow")) return "shadow";
|
|
77
|
+
if (prop.includes("z-index")) return "z-index";
|
|
78
|
+
if (prop.includes("transition") || prop.includes("duration") || prop.includes("animation")) {
|
|
79
|
+
return "duration";
|
|
80
|
+
}
|
|
81
|
+
if (prop.includes("font") || prop.includes("line-height") || prop === "letter-spacing") {
|
|
82
|
+
return "typography";
|
|
83
|
+
}
|
|
84
|
+
if (prop.includes("radius")) return "radius";
|
|
85
|
+
if (prop.endsWith("border-width") || prop === "border-width" || prop === "outline-width" || prop === "stroke-width") {
|
|
86
|
+
return "border-width";
|
|
87
|
+
}
|
|
88
|
+
if (prop.includes("color") || prop === "background" || prop.startsWith("background-") || prop === "fill" || prop === "stroke" || prop === "caret-color" || prop === "accent-color" || (prop === "border" || prop === "outline") && normalizedValue && looksLikeColor(normalizedValue)) {
|
|
89
|
+
return "color";
|
|
90
|
+
}
|
|
91
|
+
if (prop === "margin" || prop.startsWith("margin-") || prop === "padding" || prop.startsWith("padding-") || prop === "gap" || prop === "row-gap" || prop === "column-gap" || prop === "inset" || prop === "top" || prop === "right" || prop === "bottom" || prop === "left" || prop.endsWith("width") || prop.endsWith("height")) {
|
|
92
|
+
return "spacing";
|
|
93
|
+
}
|
|
94
|
+
return "other";
|
|
95
|
+
}
|
|
96
|
+
function suggestToken(input) {
|
|
97
|
+
const family = propertyFamilyFor(input.property, input.value);
|
|
98
|
+
const limit = Math.min(Math.max(input.limit ?? 5, 1), 10);
|
|
99
|
+
const candidates = input.tokens ? scoreCandidates(input.tokens, family, input) : [];
|
|
100
|
+
const top = candidates.slice(0, limit).map(
|
|
101
|
+
(candidate) => presentCandidate(candidate, input.tokens)
|
|
102
|
+
);
|
|
103
|
+
const meta = {
|
|
104
|
+
propertyFamily: family,
|
|
105
|
+
catalogRevision: input.catalogRevision,
|
|
106
|
+
updatedAt: input.updatedAt,
|
|
107
|
+
candidateCount: candidates.length
|
|
108
|
+
};
|
|
109
|
+
if (family === "other") {
|
|
110
|
+
return {
|
|
111
|
+
alternatives: [],
|
|
112
|
+
noSuggestion: true,
|
|
113
|
+
noSuggestionReason: `No token family is known for CSS property "${input.property}".`,
|
|
114
|
+
_meta: meta
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (!input.tokens || input.tokens.total === 0) {
|
|
118
|
+
return {
|
|
119
|
+
alternatives: [],
|
|
120
|
+
noSuggestion: true,
|
|
121
|
+
noSuggestionReason: "No design tokens are available in the active catalog.",
|
|
122
|
+
_meta: meta
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (top.length === 0) {
|
|
126
|
+
return {
|
|
127
|
+
alternatives: [],
|
|
128
|
+
noSuggestion: true,
|
|
129
|
+
noSuggestionReason: `No ${family} tokens are available for CSS property "${input.property}".`,
|
|
130
|
+
_meta: meta
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const [recommended, ...alternatives] = top;
|
|
134
|
+
return {
|
|
135
|
+
recommended,
|
|
136
|
+
alternatives,
|
|
137
|
+
_meta: meta
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function scoreCandidates(tokenData, family, input) {
|
|
141
|
+
if (family === "other") return [];
|
|
142
|
+
const value = normalizeComparableValue(input.value);
|
|
143
|
+
const flatTokens = tokenData.flat.length > 0 ? tokenData.flat : Object.values(tokenData.categories).flat();
|
|
144
|
+
const familyTokens = flatTokens.filter((token) => !isGarbageToken(token)).map((token) => ({ token, family: tokenFamily(token) })).filter((entry) => entry.family === family);
|
|
145
|
+
const scored = familyTokens.map(({ token }) => {
|
|
146
|
+
const tokenValue = normalizeComparableValue(token.value);
|
|
147
|
+
let score = 50;
|
|
148
|
+
let reason = "family-match";
|
|
149
|
+
if (value && tokenValue && value === tokenValue) {
|
|
150
|
+
score += 60;
|
|
151
|
+
reason = "exact-value-match";
|
|
152
|
+
} else if (value && tokenValue && family !== "color") {
|
|
153
|
+
const distanceScore = lengthDistanceScore(value, tokenValue);
|
|
154
|
+
if (distanceScore > 0) {
|
|
155
|
+
score += distanceScore;
|
|
156
|
+
reason = "nearest-neighbor";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
score += nameRelevanceScore(token, input.property, input.context);
|
|
160
|
+
return { token, family, score, reason };
|
|
161
|
+
});
|
|
162
|
+
return scored.filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score || a.token.name.localeCompare(b.token.name));
|
|
163
|
+
}
|
|
164
|
+
function presentCandidate(candidate, tokenData) {
|
|
165
|
+
const cssVar = cssVarForToken(candidate.token);
|
|
166
|
+
const resolvedValue = resolvedValueForToken(candidate.token);
|
|
167
|
+
const confidence = candidate.score >= 105 ? "high" : candidate.score >= 65 ? "medium" : "low";
|
|
168
|
+
return {
|
|
169
|
+
name: dottedNameForToken(candidate.token, tokenData),
|
|
170
|
+
...cssVar && { cssVar },
|
|
171
|
+
...cssVar && {
|
|
172
|
+
cssValue: resolvedValue ? `var(${cssVar}, ${resolvedValue})` : `var(${cssVar})`
|
|
173
|
+
},
|
|
174
|
+
...resolvedValue && { resolvedValue },
|
|
175
|
+
confidence,
|
|
176
|
+
reason: candidate.reason
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function isGarbageToken(token) {
|
|
180
|
+
const value = token.value?.trim();
|
|
181
|
+
if (!value) return false;
|
|
182
|
+
if (value.includes("#{") || value.includes("$")) return true;
|
|
183
|
+
if (/^\$[\w-]+/.test(token.name)) return true;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function tokenFamily(token) {
|
|
187
|
+
const haystack = [
|
|
188
|
+
token.type,
|
|
189
|
+
token.category,
|
|
190
|
+
...token.path ?? [],
|
|
191
|
+
token.name
|
|
192
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
193
|
+
if (/\b(color|colour|background|foreground|surface|palette)\b/.test(haystack)) {
|
|
194
|
+
return "color";
|
|
195
|
+
}
|
|
196
|
+
if (/\b(radius|radii|rounded|corner)\b/.test(haystack)) return "radius";
|
|
197
|
+
if (/\b(border-width|border width|stroke-width|stroke width)\b/.test(haystack)) {
|
|
198
|
+
return "border-width";
|
|
199
|
+
}
|
|
200
|
+
if (/\b(shadow|elevation)\b/.test(haystack)) return "shadow";
|
|
201
|
+
if (/\b(font|type|typography|line-height|letter-spacing)\b/.test(haystack)) {
|
|
202
|
+
return "typography";
|
|
203
|
+
}
|
|
204
|
+
if (/\b(duration|transition|animation)\b/.test(haystack)) return "duration";
|
|
205
|
+
if (/\b(z-index|zindex)\b/.test(haystack)) return "z-index";
|
|
206
|
+
if (/\b(space|spacing|size|sizing|width|height|gap|padding|margin|inset)\b/.test(haystack)) {
|
|
207
|
+
return "spacing";
|
|
208
|
+
}
|
|
209
|
+
if (/\bborder\b/.test(haystack)) return "border-width";
|
|
210
|
+
return "other";
|
|
211
|
+
}
|
|
212
|
+
function cssVarForToken(token) {
|
|
213
|
+
if (token.name.startsWith("--")) return token.name;
|
|
214
|
+
const match = token.value?.match(/var\((--[\w-]+)/);
|
|
215
|
+
return match?.[1];
|
|
216
|
+
}
|
|
217
|
+
function dottedNameForToken(token, tokenData) {
|
|
218
|
+
if (!token.name.startsWith("--")) return token.name;
|
|
219
|
+
let name = token.name.slice(2);
|
|
220
|
+
const prefix = tokenData?.prefix?.replace(/^--/, "").replace(/-$/, "");
|
|
221
|
+
if (prefix && name.startsWith(`${prefix}-`)) {
|
|
222
|
+
name = name.slice(prefix.length + 1);
|
|
223
|
+
}
|
|
224
|
+
return name.replace(/-/g, ".");
|
|
225
|
+
}
|
|
226
|
+
function resolvedValueForToken(token) {
|
|
227
|
+
const value = token.value?.trim();
|
|
228
|
+
if (!value) return void 0;
|
|
229
|
+
const fallback = value.match(/var\(--[\w-]+,\s*([^)]+)\)/)?.[1]?.trim();
|
|
230
|
+
if (fallback) return fallback;
|
|
231
|
+
if (value.startsWith("var(")) return void 0;
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
function normalizeComparableValue(value) {
|
|
235
|
+
if (!value) return void 0;
|
|
236
|
+
const trimmed = value.trim().toLowerCase();
|
|
237
|
+
const color = normalizeColor(trimmed);
|
|
238
|
+
if (color) return color;
|
|
239
|
+
const length = parseLength(trimmed);
|
|
240
|
+
if (length) return `${length.value}${length.unit}`;
|
|
241
|
+
return trimmed.replace(/\s+/g, " ");
|
|
242
|
+
}
|
|
243
|
+
function looksLikeColor(value) {
|
|
244
|
+
return /^#([\da-f]{3,8})$/i.test(value) || /^rgba?\(/i.test(value) || /^hsla?\(/i.test(value);
|
|
245
|
+
}
|
|
246
|
+
function normalizeColor(value) {
|
|
247
|
+
const hex = value.match(/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i);
|
|
248
|
+
if (!hex) return void 0;
|
|
249
|
+
const body = hex[1].toLowerCase();
|
|
250
|
+
if (body.length === 3) {
|
|
251
|
+
return `#${body[0]}${body[0]}${body[1]}${body[1]}${body[2]}${body[2]}`;
|
|
252
|
+
}
|
|
253
|
+
return `#${body}`;
|
|
254
|
+
}
|
|
255
|
+
function parseLength(value) {
|
|
256
|
+
const match = value.match(/^(-?\d+(?:\.\d+)?)(px|rem|em|%)$/);
|
|
257
|
+
if (!match) return void 0;
|
|
258
|
+
return { value: Number(match[1]), unit: match[2] };
|
|
259
|
+
}
|
|
260
|
+
function lengthDistanceScore(inputValue, tokenValue) {
|
|
261
|
+
const input = parseLength(inputValue);
|
|
262
|
+
const token = parseLength(tokenValue);
|
|
263
|
+
if (!input || !token || input.unit !== token.unit) return 0;
|
|
264
|
+
const distance = Math.abs(input.value - token.value);
|
|
265
|
+
if (distance === 0) return 60;
|
|
266
|
+
if (distance <= 2) return 35;
|
|
267
|
+
if (distance <= 4) return 20;
|
|
268
|
+
if (distance <= 8) return 10;
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
function nameRelevanceScore(token, property, context) {
|
|
272
|
+
const haystack = [token.name, token.category, ...token.path ?? []].join(" ").toLowerCase();
|
|
273
|
+
const prop = property.toLowerCase();
|
|
274
|
+
let score = 0;
|
|
275
|
+
for (const part of prop.split(/[^a-z0-9]+/).filter((part2) => part2.length > 2)) {
|
|
276
|
+
if (haystack.includes(part)) score += 4;
|
|
277
|
+
}
|
|
278
|
+
if (context === "component" && haystack.includes("component")) score += 3;
|
|
279
|
+
if (context === "global" && haystack.includes("global")) score += 3;
|
|
280
|
+
return score;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/tools/tokens-suggest.ts
|
|
284
|
+
var tokensSuggestHandler = async (args, ctx) => {
|
|
285
|
+
const property = args.property;
|
|
286
|
+
if (!property || typeof property !== "string") {
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: JSON.stringify({ error: "property is required." })
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
isError: true
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const context = args.context;
|
|
298
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
299
|
+
const result2 = suggestToken({
|
|
300
|
+
tokens: ctx.data.tokens,
|
|
301
|
+
property,
|
|
302
|
+
value: typeof args.value === "string" ? args.value : void 0,
|
|
303
|
+
context: context === "component" || context === "block" || context === "global" ? context : void 0,
|
|
304
|
+
catalogRevision: catalogMeta.catalogRevision,
|
|
305
|
+
updatedAt: catalogMeta.updatedAt
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: "text", text: JSON.stringify(result2) }],
|
|
309
|
+
_meta: result2._meta
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/tools/spec-govern.ts
|
|
314
|
+
var SEVERITY_WEIGHTS = {
|
|
315
|
+
critical: 10,
|
|
316
|
+
serious: 5,
|
|
317
|
+
moderate: 2,
|
|
318
|
+
minor: 1
|
|
319
|
+
};
|
|
320
|
+
function isRuleEnabled(rule, defaultEnabled = true) {
|
|
321
|
+
if (rule === void 0) return defaultEnabled;
|
|
322
|
+
if (typeof rule === "boolean") return rule;
|
|
323
|
+
return rule.enabled ?? defaultEnabled;
|
|
324
|
+
}
|
|
325
|
+
function ruleSeverity(rule, fallback) {
|
|
326
|
+
return typeof rule === "object" && rule?.severity ? rule.severity : fallback;
|
|
327
|
+
}
|
|
328
|
+
function ruleOptions(rule) {
|
|
329
|
+
return typeof rule === "object" && rule?.options ? rule.options : {};
|
|
330
|
+
}
|
|
331
|
+
function nodeId(node, fallback) {
|
|
332
|
+
return typeof node.id === "string" ? node.id : `node-${fallback}`;
|
|
333
|
+
}
|
|
334
|
+
function nodeType(node) {
|
|
335
|
+
return typeof node.type === "string" ? node.type : "_unknown";
|
|
336
|
+
}
|
|
337
|
+
function parentType(type) {
|
|
338
|
+
const dotIndex = type.indexOf(".");
|
|
339
|
+
return dotIndex > 0 ? type.slice(0, dotIndex) : type;
|
|
340
|
+
}
|
|
341
|
+
function walkNodes(nodes, visitor) {
|
|
342
|
+
if (!Array.isArray(nodes)) return;
|
|
343
|
+
for (const [index, node] of nodes.entries()) {
|
|
344
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const specNode = node;
|
|
348
|
+
visitor(specNode, index);
|
|
349
|
+
walkNodes(specNode.children, visitor);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function flattenNodes(spec) {
|
|
353
|
+
const flattened = [];
|
|
354
|
+
walkNodes(spec.nodes, (node) => flattened.push(node));
|
|
355
|
+
return flattened;
|
|
356
|
+
}
|
|
357
|
+
function worstSeverity(violations) {
|
|
358
|
+
const order = ["critical", "serious", "moderate", "minor"];
|
|
359
|
+
return violations.reduce(
|
|
360
|
+
(worst, violation) => order.indexOf(violation.severity) < order.indexOf(worst) ? violation.severity : worst,
|
|
361
|
+
"minor"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
function result(validator, violations) {
|
|
365
|
+
return {
|
|
366
|
+
validator,
|
|
367
|
+
severity: violations.length > 0 ? worstSeverity(violations) : "minor",
|
|
368
|
+
passed: violations.length === 0,
|
|
369
|
+
violations
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
var SEVERITY_SCORE_CAPS = {
|
|
373
|
+
critical: 25,
|
|
374
|
+
serious: 50,
|
|
375
|
+
moderate: 80,
|
|
376
|
+
minor: 95
|
|
377
|
+
};
|
|
378
|
+
function verdictFor(violations) {
|
|
379
|
+
if (violations.length === 0) return "pass";
|
|
380
|
+
const worst = worstSeverity(violations);
|
|
381
|
+
return worst === "critical" || worst === "serious" ? "fail" : "warn";
|
|
382
|
+
}
|
|
383
|
+
function computeScore(violations) {
|
|
384
|
+
if (violations.length === 0) return 100;
|
|
385
|
+
const penalty = violations.reduce(
|
|
386
|
+
(sum, violation) => sum + SEVERITY_WEIGHTS[violation.severity],
|
|
387
|
+
0
|
|
388
|
+
);
|
|
389
|
+
const cap = SEVERITY_SCORE_CAPS[worstSeverity(violations)];
|
|
390
|
+
return Math.min(cap, Math.max(0, 100 - penalty));
|
|
391
|
+
}
|
|
392
|
+
function validateComponents(nodes, options) {
|
|
393
|
+
const rules = options.policy?.rules ?? {};
|
|
394
|
+
const allowRule = rules["components/allow"];
|
|
395
|
+
const denyRule = rules["components/deny"];
|
|
396
|
+
const allowOptions = ruleOptions(allowRule);
|
|
397
|
+
const denyOptions = ruleOptions(denyRule);
|
|
398
|
+
const allowed = new Set(
|
|
399
|
+
(allowOptions.components ?? options.allowedComponents).filter(Boolean)
|
|
400
|
+
);
|
|
401
|
+
const denied = new Set(denyOptions.components ?? []);
|
|
402
|
+
const violations = [];
|
|
403
|
+
for (const [index, node] of nodes.entries()) {
|
|
404
|
+
const type = nodeType(node);
|
|
405
|
+
const parent = parentType(type);
|
|
406
|
+
if (allowed.size > 0 && isRuleEnabled(allowRule) && !allowed.has(type) && !allowed.has(parent)) {
|
|
407
|
+
violations.push({
|
|
408
|
+
nodeId: nodeId(node, index),
|
|
409
|
+
nodeType: type,
|
|
410
|
+
rule: "components/allow",
|
|
411
|
+
severity: ruleSeverity(allowRule, "serious"),
|
|
412
|
+
message: `Component "${type}" is not in the allowed list`,
|
|
413
|
+
suggestion: `Use one of the allowed components: ${[...allowed].slice(0, 10).join(", ")}${allowed.size > 10 ? "..." : ""}`
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (denied.size > 0 && isRuleEnabled(denyRule) && (denied.has(type) || denied.has(parent))) {
|
|
417
|
+
violations.push({
|
|
418
|
+
nodeId: nodeId(node, index),
|
|
419
|
+
nodeType: type,
|
|
420
|
+
rule: "components/deny",
|
|
421
|
+
severity: ruleSeverity(denyRule, "serious"),
|
|
422
|
+
message: `Component "${type}" is blocked by the deny list`,
|
|
423
|
+
suggestion: `Remove "${type}" or replace it with an allowed alternative`
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return result("components", violations);
|
|
428
|
+
}
|
|
429
|
+
function isEventHandlerProp(prop) {
|
|
430
|
+
return /^on[A-Z]/.test(prop);
|
|
431
|
+
}
|
|
432
|
+
function validateSafety(nodes) {
|
|
433
|
+
const violations = [];
|
|
434
|
+
const dangerousProps = /* @__PURE__ */ new Set(["dangerouslySetInnerHTML", "innerHTML"]);
|
|
435
|
+
for (const [index, node] of nodes.entries()) {
|
|
436
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
437
|
+
if (dangerousProps.has(prop)) {
|
|
438
|
+
violations.push({
|
|
439
|
+
nodeId: nodeId(node, index),
|
|
440
|
+
nodeType: nodeType(node),
|
|
441
|
+
rule: "safety/no-dangerous-props",
|
|
442
|
+
severity: "critical",
|
|
443
|
+
message: `Dangerous prop "${prop}" is not allowed`,
|
|
444
|
+
suggestion: `Remove "${prop}" or use a safe rendering pattern`,
|
|
445
|
+
prop
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
if (isEventHandlerProp(prop) && typeof value === "string") {
|
|
449
|
+
violations.push({
|
|
450
|
+
nodeId: nodeId(node, index),
|
|
451
|
+
nodeType: nodeType(node),
|
|
452
|
+
rule: "safety/no-string-handlers",
|
|
453
|
+
severity: "serious",
|
|
454
|
+
message: `Event handler prop "${prop}" must not be a string`,
|
|
455
|
+
suggestion: `Remove "${prop}" from generated specs`,
|
|
456
|
+
prop
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return result("safety", violations);
|
|
462
|
+
}
|
|
463
|
+
function editDistance(a, b) {
|
|
464
|
+
const rows = a.length + 1;
|
|
465
|
+
const cols = b.length + 1;
|
|
466
|
+
const distances = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
467
|
+
for (let i = 0; i < rows; i++) distances[i][0] = i;
|
|
468
|
+
for (let j = 0; j < cols; j++) distances[0][j] = j;
|
|
469
|
+
for (let i = 1; i < rows; i++) {
|
|
470
|
+
for (let j = 1; j < cols; j++) {
|
|
471
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
472
|
+
distances[i][j] = Math.min(
|
|
473
|
+
distances[i - 1][j] + 1,
|
|
474
|
+
distances[i][j - 1] + 1,
|
|
475
|
+
distances[i - 1][j - 1] + cost
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return distances[a.length][b.length];
|
|
480
|
+
}
|
|
481
|
+
function normalizePropValue(value) {
|
|
482
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
483
|
+
}
|
|
484
|
+
function closestAllowedValue(rawValue, allowedValues) {
|
|
485
|
+
const normalizedRaw = normalizePropValue(rawValue);
|
|
486
|
+
const ranked = allowedValues.map((value) => {
|
|
487
|
+
const normalizedValue = normalizePropValue(value);
|
|
488
|
+
const distance = editDistance(normalizedRaw, normalizedValue);
|
|
489
|
+
const prefixMatch = normalizedRaw.length >= 4 && (normalizedValue.startsWith(normalizedRaw) || normalizedRaw.startsWith(normalizedValue));
|
|
490
|
+
return { value, distance, prefixMatch };
|
|
491
|
+
}).sort((a, b) => {
|
|
492
|
+
if (a.prefixMatch !== b.prefixMatch) return a.prefixMatch ? -1 : 1;
|
|
493
|
+
return a.distance - b.distance || a.value.localeCompare(b.value);
|
|
494
|
+
});
|
|
495
|
+
const best = ranked[0];
|
|
496
|
+
if (!best) return void 0;
|
|
497
|
+
if (best.prefixMatch || best.distance <= 2) return best.value;
|
|
498
|
+
return void 0;
|
|
499
|
+
}
|
|
500
|
+
function validateProps(nodes, options) {
|
|
501
|
+
const rules = options.policy?.rules ?? {};
|
|
502
|
+
const propRule = rules["props/valid-values"];
|
|
503
|
+
const componentProps = options.componentProps ?? {};
|
|
504
|
+
const violations = [];
|
|
505
|
+
if (!isRuleEnabled(propRule)) {
|
|
506
|
+
return result("props", violations);
|
|
507
|
+
}
|
|
508
|
+
for (const [index, node] of nodes.entries()) {
|
|
509
|
+
const type = nodeType(node);
|
|
510
|
+
const propSchema = componentProps[type] ?? componentProps[parentType(type)];
|
|
511
|
+
if (!propSchema) continue;
|
|
512
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
513
|
+
const allowedValues = propSchema[prop]?.values?.filter(Boolean) ?? [];
|
|
514
|
+
if (allowedValues.length === 0) continue;
|
|
515
|
+
const rawValue = typeof value === "string" ? value : void 0;
|
|
516
|
+
if (!rawValue || allowedValues.includes(rawValue)) continue;
|
|
517
|
+
const closest = closestAllowedValue(rawValue, allowedValues);
|
|
518
|
+
violations.push({
|
|
519
|
+
nodeId: nodeId(node, index),
|
|
520
|
+
nodeType: type,
|
|
521
|
+
rule: "props/invalid-value",
|
|
522
|
+
severity: ruleSeverity(propRule, "moderate"),
|
|
523
|
+
message: `Prop "${prop}" on ${type} has invalid value "${rawValue}"`,
|
|
524
|
+
suggestion: closest ? `Use "${closest}" for prop "${prop}" on ${type}.` : `Use one of: ${allowedValues.join(", ")}`,
|
|
525
|
+
prop,
|
|
526
|
+
rawValue
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return result("props", violations);
|
|
531
|
+
}
|
|
532
|
+
function hasHardcodedCssValue(value) {
|
|
533
|
+
if (value.includes("var(")) return false;
|
|
534
|
+
return /#[0-9a-f]{3,8}\b/i.test(value) || /\b\d+(?:px|rem|em)\b/.test(value);
|
|
535
|
+
}
|
|
536
|
+
function validateTokens(nodes, options) {
|
|
537
|
+
const rules = options.policy?.rules ?? {};
|
|
538
|
+
const requireRule = rules["tokens/require-design-tokens"];
|
|
539
|
+
const prefixRule = rules["tokens/allowed-prefixes"];
|
|
540
|
+
const requireTokens = isRuleEnabled(requireRule);
|
|
541
|
+
const defaultTokenPrefix = options.tokenPrefix?.replace(/^--/, "");
|
|
542
|
+
const allowedPrefixes = ruleOptions(prefixRule).prefixes ?? (defaultTokenPrefix ? [defaultTokenPrefix] : []);
|
|
543
|
+
const violations = [];
|
|
544
|
+
for (const [index, node] of nodes.entries()) {
|
|
545
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
546
|
+
if (typeof value !== "string") continue;
|
|
547
|
+
if (requireTokens && hasHardcodedCssValue(value)) {
|
|
548
|
+
violations.push({
|
|
549
|
+
nodeId: nodeId(node, index),
|
|
550
|
+
nodeType: nodeType(node),
|
|
551
|
+
rule: "tokens/require-design-tokens",
|
|
552
|
+
severity: ruleSeverity(requireRule, "moderate"),
|
|
553
|
+
message: `Hardcoded CSS value "${value}" in prop "${prop}" - use a design token instead`,
|
|
554
|
+
suggestion: "Use a design token instead of a hardcoded CSS value",
|
|
555
|
+
prop,
|
|
556
|
+
rawValue: value
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (allowedPrefixes.length > 0 && isRuleEnabled(prefixRule, Boolean(options.tokenPrefix))) {
|
|
560
|
+
const matches = value.matchAll(/var\(--([^)]+)\)/g);
|
|
561
|
+
for (const match of matches) {
|
|
562
|
+
const tokenName = match[1];
|
|
563
|
+
if (allowedPrefixes.some((prefix) => tokenName.startsWith(prefix))) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
violations.push({
|
|
567
|
+
nodeId: nodeId(node, index),
|
|
568
|
+
nodeType: nodeType(node),
|
|
569
|
+
rule: "tokens/allowed-prefixes",
|
|
570
|
+
severity: ruleSeverity(prefixRule, "moderate"),
|
|
571
|
+
message: `Token "--${tokenName}" does not use an allowed prefix (${allowedPrefixes.join(", ")})`,
|
|
572
|
+
suggestion: `Use a token with one of the allowed prefixes: ${allowedPrefixes.map((prefix) => `--${prefix}*`).join(", ")}`,
|
|
573
|
+
prop,
|
|
574
|
+
rawValue: value
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return result("tokens", violations);
|
|
581
|
+
}
|
|
582
|
+
function textFromUnknown(value) {
|
|
583
|
+
if (typeof value === "string") return value.trim();
|
|
584
|
+
if (Array.isArray(value)) return textFromChildren(value);
|
|
585
|
+
if (typeof value === "object" && value !== null) {
|
|
586
|
+
return textFromNode(value);
|
|
587
|
+
}
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
function textFromNode(node) {
|
|
591
|
+
const type = nodeType(node).toLowerCase();
|
|
592
|
+
const props = node.props ?? {};
|
|
593
|
+
const propText = [
|
|
594
|
+
textFromUnknown(props.children),
|
|
595
|
+
type === "text" ? textFromUnknown(props.value) : ""
|
|
596
|
+
].filter(Boolean);
|
|
597
|
+
const childText = textFromChildren(node.children);
|
|
598
|
+
return [...propText, childText].join(" ").trim();
|
|
599
|
+
}
|
|
600
|
+
function textFromChildren(children) {
|
|
601
|
+
if (typeof children === "string") return children.trim();
|
|
602
|
+
if (!Array.isArray(children)) return "";
|
|
603
|
+
return children.map((child) => {
|
|
604
|
+
return textFromUnknown(child);
|
|
605
|
+
}).join(" ").trim();
|
|
606
|
+
}
|
|
607
|
+
function validateA11y(nodes) {
|
|
608
|
+
const violations = [];
|
|
609
|
+
for (const [index, node] of nodes.entries()) {
|
|
610
|
+
const type = nodeType(node);
|
|
611
|
+
if (!/button/i.test(type)) continue;
|
|
612
|
+
const props = node.props ?? {};
|
|
613
|
+
const label = props["aria-label"] ?? props["aria-labelledby"] ?? props.title;
|
|
614
|
+
const childText = textFromNode(node);
|
|
615
|
+
if (typeof label === "string" && label.trim().length > 0) continue;
|
|
616
|
+
if (childText.length > 0) continue;
|
|
617
|
+
violations.push({
|
|
618
|
+
nodeId: nodeId(node, index),
|
|
619
|
+
nodeType: type,
|
|
620
|
+
rule: "button-name",
|
|
621
|
+
severity: "serious",
|
|
622
|
+
message: "Buttons must have discernible text (button-name)",
|
|
623
|
+
suggestion: "Add visible text or an aria-label"
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return result("a11y", violations);
|
|
627
|
+
}
|
|
628
|
+
function runSpecGovern(spec, options) {
|
|
629
|
+
const startedAt = Date.now();
|
|
630
|
+
const nodes = flattenNodes(spec);
|
|
631
|
+
const results = [
|
|
632
|
+
validateSafety(nodes),
|
|
633
|
+
validateComponents(nodes, options),
|
|
634
|
+
validateProps(nodes, options),
|
|
635
|
+
validateTokens(nodes, options),
|
|
636
|
+
validateA11y(nodes)
|
|
637
|
+
];
|
|
638
|
+
const violations = results.flatMap((entry) => entry.violations);
|
|
639
|
+
const componentTypes = Array.from(
|
|
640
|
+
new Set(nodes.map((node) => nodeType(node)))
|
|
641
|
+
);
|
|
642
|
+
return {
|
|
643
|
+
verdict: verdictFor(violations),
|
|
644
|
+
passed: results.every((entry) => entry.passed),
|
|
645
|
+
score: computeScore(violations),
|
|
646
|
+
results,
|
|
647
|
+
metadata: {
|
|
648
|
+
runner: "mcp",
|
|
649
|
+
duration: Date.now() - startedAt,
|
|
650
|
+
nodeCount: nodes.length,
|
|
651
|
+
componentTypes
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function formatVerdict(verdict, format = "summary") {
|
|
656
|
+
if (format === "json") {
|
|
657
|
+
return JSON.stringify(verdict, null, 2);
|
|
658
|
+
}
|
|
659
|
+
const lines = [];
|
|
660
|
+
const icon = verdict.passed ? "ok" : "fail";
|
|
661
|
+
lines.push(`${icon} Governance check: verdict ${verdict.verdict}, score ${verdict.score}/100`);
|
|
662
|
+
lines.push("");
|
|
663
|
+
for (const entry of verdict.results) {
|
|
664
|
+
const resultIcon = entry.passed ? "ok" : "fail";
|
|
665
|
+
const count = entry.violations.length;
|
|
666
|
+
lines.push(` ${resultIcon} ${entry.validator}: ${count === 0 ? "passed" : `${count} violation(s)`}`);
|
|
667
|
+
for (const violation of entry.violations) {
|
|
668
|
+
lines.push(` - [${violation.severity}] ${violation.nodeType}#${violation.nodeId}: ${violation.message}`);
|
|
669
|
+
if (violation.suggestion) {
|
|
670
|
+
lines.push(` -> ${violation.suggestion}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
lines.push("");
|
|
675
|
+
lines.push(`Duration: ${verdict.metadata.duration.toFixed(0)}ms | Nodes: ${verdict.metadata.nodeCount}`);
|
|
676
|
+
return lines.join("\n");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/tools/govern.ts
|
|
680
|
+
function buildComponentProps(ctx) {
|
|
681
|
+
return Object.fromEntries(
|
|
682
|
+
Object.values(ctx.data.components).map((component) => [
|
|
683
|
+
component.name,
|
|
684
|
+
component.props
|
|
685
|
+
])
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
var governHandler = async (args, ctx) => {
|
|
689
|
+
const spec = args?.spec;
|
|
690
|
+
if (!spec || typeof spec !== "object") {
|
|
691
|
+
return {
|
|
692
|
+
content: [
|
|
693
|
+
{
|
|
694
|
+
type: "text",
|
|
695
|
+
text: JSON.stringify({
|
|
696
|
+
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }], root?, metadata? }. See the govern.schema MCP resource for the full schema."
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
],
|
|
700
|
+
isError: true
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const policyOverrides = args?.policy;
|
|
704
|
+
const format = args?.format ?? "json";
|
|
705
|
+
const allowedComponents = Object.values(ctx.data.components).map(
|
|
706
|
+
(component) => component.name
|
|
707
|
+
);
|
|
708
|
+
try {
|
|
709
|
+
const verdict = runSpecGovern(spec, {
|
|
710
|
+
allowedComponents,
|
|
711
|
+
tokenPrefix: ctx.data.tokens?.prefix,
|
|
712
|
+
policy: policyOverrides,
|
|
713
|
+
componentProps: buildComponentProps(ctx)
|
|
714
|
+
});
|
|
715
|
+
const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
|
|
716
|
+
return {
|
|
717
|
+
content: [{ type: "text", text }],
|
|
718
|
+
_meta: {
|
|
719
|
+
...getCatalogMeta(ctx.data),
|
|
720
|
+
verdict: verdict.verdict,
|
|
721
|
+
score: verdict.score,
|
|
722
|
+
passed: verdict.passed,
|
|
723
|
+
violationCount: verdict.results.reduce(
|
|
724
|
+
(sum, r) => sum + r.violations.length,
|
|
725
|
+
0
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
} catch (error) {
|
|
730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
731
|
+
const isSpecError = message.includes("Expected") || message.includes("Required");
|
|
732
|
+
return {
|
|
733
|
+
content: [
|
|
734
|
+
{
|
|
735
|
+
type: "text",
|
|
736
|
+
text: JSON.stringify({
|
|
737
|
+
error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id?: string, type: string, props?: object, children?: array|string }], root?: string, metadata?: object }. See the govern.schema MCP resource for examples.` : message
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
],
|
|
741
|
+
isError: true
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
// src/tools/validate-and-fix.ts
|
|
747
|
+
function classifyComponent(component) {
|
|
748
|
+
if (component.status === "deprecated") return "discouraged";
|
|
749
|
+
if (component.isCanonical && component.tier === "core") return "preferred";
|
|
750
|
+
return "allowed";
|
|
751
|
+
}
|
|
752
|
+
function buildEffectiveComponents(ctx) {
|
|
753
|
+
const validateFixByKey = new Map(
|
|
754
|
+
(ctx.data.validateFixContext?.components ?? []).map((component) => [
|
|
755
|
+
component.componentKey,
|
|
756
|
+
component
|
|
757
|
+
])
|
|
758
|
+
);
|
|
759
|
+
return Object.entries(ctx.data.components).map(([componentKey, component]) => ({
|
|
760
|
+
component,
|
|
761
|
+
selection: validateFixByKey.get(componentKey)?.selection ?? classifyComponent(component),
|
|
762
|
+
isActive: validateFixByKey.get(componentKey)?.isActive ?? true,
|
|
763
|
+
reasons: validateFixByKey.get(componentKey)?.reasons ?? []
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
766
|
+
function cloneSpec(spec) {
|
|
767
|
+
return structuredClone(spec);
|
|
768
|
+
}
|
|
769
|
+
function isPlainObject(value) {
|
|
770
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
771
|
+
}
|
|
772
|
+
function validateNodeShape(node, path) {
|
|
773
|
+
if (!isPlainObject(node)) {
|
|
774
|
+
return `${path} must be an object`;
|
|
775
|
+
}
|
|
776
|
+
if ("id" in node && node.id !== void 0 && typeof node.id !== "string") {
|
|
777
|
+
return `${path}.id must be a string`;
|
|
778
|
+
}
|
|
779
|
+
if ("type" in node && node.type !== void 0 && typeof node.type !== "string") {
|
|
780
|
+
return `${path}.type must be a string`;
|
|
781
|
+
}
|
|
782
|
+
if ("props" in node && node.props !== void 0 && !isPlainObject(node.props)) {
|
|
783
|
+
return `${path}.props must be an object`;
|
|
784
|
+
}
|
|
785
|
+
if ("children" in node && node.children !== void 0) {
|
|
786
|
+
if (typeof node.children === "string") {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
if (!Array.isArray(node.children)) {
|
|
790
|
+
return `${path}.children must be an array or string`;
|
|
791
|
+
}
|
|
792
|
+
for (const [index, child] of node.children.entries()) {
|
|
793
|
+
if (!isPlainObject(child)) continue;
|
|
794
|
+
const childError = validateNodeShape(
|
|
795
|
+
child,
|
|
796
|
+
`${path}.children[${index}]`
|
|
797
|
+
);
|
|
798
|
+
if (childError) return childError;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
function validateSpecShape(spec) {
|
|
804
|
+
if (!isPlainObject(spec)) {
|
|
805
|
+
return "spec must be an object";
|
|
806
|
+
}
|
|
807
|
+
if ("root" in spec && spec.root !== void 0 && typeof spec.root !== "string") {
|
|
808
|
+
return "spec.root must be a string";
|
|
809
|
+
}
|
|
810
|
+
if ("metadata" in spec && spec.metadata !== void 0 && !isPlainObject(spec.metadata)) {
|
|
811
|
+
return "spec.metadata must be an object";
|
|
812
|
+
}
|
|
813
|
+
if ("nodes" in spec && spec.nodes !== void 0) {
|
|
814
|
+
if (!Array.isArray(spec.nodes)) {
|
|
815
|
+
return "spec.nodes must be an array";
|
|
816
|
+
}
|
|
817
|
+
for (const [index, node] of spec.nodes.entries()) {
|
|
818
|
+
const error = validateNodeShape(node, `spec.nodes[${index}]`);
|
|
819
|
+
if (error) return error;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
function getSelectionNames(effectiveComponents, selection) {
|
|
825
|
+
const selections = Array.isArray(selection) ? selection : [selection];
|
|
826
|
+
return Array.from(
|
|
827
|
+
new Set(
|
|
828
|
+
effectiveComponents.filter((entry) => selections.includes(entry.selection)).map((entry) => entry.component.name)
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
function normalizeTokens(value) {
|
|
833
|
+
return value.toLowerCase().split(/[^a-z0-9]+/g).map((token) => token.trim()).filter((token) => token.length >= 3);
|
|
834
|
+
}
|
|
835
|
+
function buildComponentText(component) {
|
|
836
|
+
return [
|
|
837
|
+
component.name,
|
|
838
|
+
component.category,
|
|
839
|
+
component.description,
|
|
840
|
+
component.guidance.usageGuidance,
|
|
841
|
+
...component.guidance.when,
|
|
842
|
+
...component.guidance.whenNot,
|
|
843
|
+
...component.guidance.dos,
|
|
844
|
+
...component.guidance.donts
|
|
845
|
+
].filter(Boolean).join(" ");
|
|
846
|
+
}
|
|
847
|
+
function rankReplacementCandidates(args) {
|
|
848
|
+
const nodePropKeys = Object.keys(args.node.props ?? {});
|
|
849
|
+
const sourcePropKeys = new Set(Object.keys(args.source.component.props ?? {}));
|
|
850
|
+
const sourceTokens = new Set(normalizeTokens(buildComponentText(args.source.component)));
|
|
851
|
+
return args.candidates.map((candidate) => {
|
|
852
|
+
let score = 0;
|
|
853
|
+
const reasons = [];
|
|
854
|
+
const candidatePropKeys = new Set(Object.keys(candidate.component.props ?? {}));
|
|
855
|
+
if (candidate.selection === "preferred") {
|
|
856
|
+
score += 12;
|
|
857
|
+
reasons.push("preferred selection");
|
|
858
|
+
}
|
|
859
|
+
if (candidate.component.isCanonical) {
|
|
860
|
+
score += 6;
|
|
861
|
+
reasons.push("canonical component");
|
|
862
|
+
}
|
|
863
|
+
if (candidate.component.tier === "core") {
|
|
864
|
+
score += 4;
|
|
865
|
+
reasons.push("core tier");
|
|
866
|
+
}
|
|
867
|
+
if (candidate.isActive) {
|
|
868
|
+
score += 3;
|
|
869
|
+
reasons.push("active component");
|
|
870
|
+
}
|
|
871
|
+
const supportedNodeProps = nodePropKeys.filter(
|
|
872
|
+
(key) => candidatePropKeys.has(key)
|
|
873
|
+
);
|
|
874
|
+
if (supportedNodeProps.length > 0) {
|
|
875
|
+
score += supportedNodeProps.length * 5;
|
|
876
|
+
reasons.push(`supports node props: ${supportedNodeProps.join(", ")}`);
|
|
877
|
+
}
|
|
878
|
+
const missingNodeProps = nodePropKeys.filter(
|
|
879
|
+
(key) => !candidatePropKeys.has(key)
|
|
880
|
+
);
|
|
881
|
+
if (missingNodeProps.length > 0) {
|
|
882
|
+
score -= missingNodeProps.length * 6;
|
|
883
|
+
reasons.push(`missing node props: ${missingNodeProps.join(", ")}`);
|
|
884
|
+
}
|
|
885
|
+
const sharedContractProps = [...sourcePropKeys].filter(
|
|
886
|
+
(key) => candidatePropKeys.has(key)
|
|
887
|
+
);
|
|
888
|
+
if (sharedContractProps.length > 0) {
|
|
889
|
+
score += sharedContractProps.length * 2;
|
|
890
|
+
reasons.push(`shares source props: ${sharedContractProps.join(", ")}`);
|
|
891
|
+
}
|
|
892
|
+
const candidateTokens = new Set(
|
|
893
|
+
normalizeTokens(buildComponentText(candidate.component))
|
|
894
|
+
);
|
|
895
|
+
const tokenOverlap = [...candidateTokens].filter(
|
|
896
|
+
(token) => sourceTokens.has(token)
|
|
897
|
+
);
|
|
898
|
+
if (tokenOverlap.length > 0) {
|
|
899
|
+
score += Math.min(tokenOverlap.length, 4);
|
|
900
|
+
reasons.push(`guidance overlap: ${tokenOverlap.join(", ")}`);
|
|
901
|
+
}
|
|
902
|
+
if (candidate.component.parentComponentId && candidate.component.parentComponentId === args.source.component.parentComponentId) {
|
|
903
|
+
score += 2;
|
|
904
|
+
reasons.push("matches parent component");
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
name: candidate.component.name,
|
|
908
|
+
score,
|
|
909
|
+
reasons
|
|
910
|
+
};
|
|
911
|
+
}).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
912
|
+
}
|
|
913
|
+
function chooseDeterministicCandidate(rankedCandidates) {
|
|
914
|
+
if (rankedCandidates.length === 0) return null;
|
|
915
|
+
if (rankedCandidates.length === 1) return rankedCandidates[0].name;
|
|
916
|
+
const [first, second] = rankedCandidates;
|
|
917
|
+
if (first.score <= 0) return null;
|
|
918
|
+
if (first.score - second.score < 4) return null;
|
|
919
|
+
return first.name;
|
|
920
|
+
}
|
|
921
|
+
function countViolations(verdict) {
|
|
922
|
+
return verdict.results.reduce(
|
|
923
|
+
(sum, result2) => sum + result2.violations.length,
|
|
924
|
+
0
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
function getNextAction(status, replacements, unresolvedAmbiguityCount) {
|
|
928
|
+
if (status === "pass") return "none";
|
|
929
|
+
if (status === "fixed") return "apply_fixed_spec";
|
|
930
|
+
if (status === "partial_fix") return "review_partial_fix";
|
|
931
|
+
if (unresolvedAmbiguityCount > 0) return "resolve_ambiguities";
|
|
932
|
+
if (replacements.length > 0) return "review_partial_fix";
|
|
933
|
+
return "revise_input";
|
|
934
|
+
}
|
|
935
|
+
function authorizationForAmbiguity(args) {
|
|
936
|
+
const required = [];
|
|
937
|
+
if (!args.applyFixes) {
|
|
938
|
+
required.push("applyFixes");
|
|
939
|
+
}
|
|
940
|
+
if (!args.allowElicitation) {
|
|
941
|
+
required.push("allowElicitation");
|
|
942
|
+
} else if (!args.supportsElicitation) {
|
|
943
|
+
required.push("clientCapabilities.elicitation.form");
|
|
944
|
+
}
|
|
945
|
+
if (!args.allowSampling) {
|
|
946
|
+
required.push("allowSampling");
|
|
947
|
+
} else if (!args.supportsSampling) {
|
|
948
|
+
required.push("clientCapabilities.sampling");
|
|
949
|
+
}
|
|
950
|
+
return required;
|
|
951
|
+
}
|
|
952
|
+
function buildWouldFixIfAuthorized(args) {
|
|
953
|
+
const entries = [];
|
|
954
|
+
if (!args.applyFixes) {
|
|
955
|
+
entries.push(
|
|
956
|
+
...args.deterministicPreview.map((replacement) => ({
|
|
957
|
+
action: "replace_component",
|
|
958
|
+
nodeId: replacement.nodeId,
|
|
959
|
+
from: replacement.from,
|
|
960
|
+
to: replacement.to,
|
|
961
|
+
reason: replacement.reason,
|
|
962
|
+
requiredAuthorization: ["applyFixes"]
|
|
963
|
+
}))
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
const requiredAuthorization = authorizationForAmbiguity({
|
|
967
|
+
applyFixes: args.applyFixes,
|
|
968
|
+
allowElicitation: args.allowElicitation,
|
|
969
|
+
allowSampling: args.allowSampling,
|
|
970
|
+
supportsElicitation: args.supportsElicitation,
|
|
971
|
+
supportsSampling: args.supportsSampling
|
|
972
|
+
});
|
|
973
|
+
if (requiredAuthorization.length > 0) {
|
|
974
|
+
entries.push(
|
|
975
|
+
...args.unresolvedAmbiguities.map((ambiguity) => ({
|
|
976
|
+
action: "resolve_ambiguous_component",
|
|
977
|
+
nodeId: ambiguity.nodeId,
|
|
978
|
+
from: ambiguity.from,
|
|
979
|
+
candidates: ambiguity.candidates,
|
|
980
|
+
rankedCandidates: ambiguity.rankedCandidates,
|
|
981
|
+
reason: ambiguity.reason,
|
|
982
|
+
requiredAuthorization
|
|
983
|
+
}))
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return entries;
|
|
987
|
+
}
|
|
988
|
+
async function emitValidateAndFixTelemetry(args) {
|
|
989
|
+
if (!args.ctx.mcp?.server) return;
|
|
990
|
+
try {
|
|
991
|
+
await args.ctx.mcp.server.sendLoggingMessage({
|
|
992
|
+
level: "info",
|
|
993
|
+
data: JSON.stringify({
|
|
994
|
+
tool: "validate_and_fix",
|
|
995
|
+
status: args.status,
|
|
996
|
+
nextAction: args.nextAction,
|
|
997
|
+
resolutionPath: args.resolutionPath,
|
|
998
|
+
evaluation: args.evaluation,
|
|
999
|
+
attestation: args.attestation
|
|
1000
|
+
}),
|
|
1001
|
+
logger: "fragments-mcp"
|
|
1002
|
+
});
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function buildAllowedComponents(ctx) {
|
|
1007
|
+
return buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
|
|
1008
|
+
}
|
|
1009
|
+
function buildComponentProps2(ctx) {
|
|
1010
|
+
return Object.fromEntries(
|
|
1011
|
+
Object.values(ctx.data.components).map((component) => [
|
|
1012
|
+
component.name,
|
|
1013
|
+
component.props
|
|
1014
|
+
])
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
function runGovern(spec, ctx, policyOverrides) {
|
|
1018
|
+
return runSpecGovern(spec, {
|
|
1019
|
+
allowedComponents: buildAllowedComponents(ctx),
|
|
1020
|
+
tokenPrefix: ctx.data.tokens?.prefix,
|
|
1021
|
+
policy: policyOverrides,
|
|
1022
|
+
componentProps: buildComponentProps2(ctx)
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
function walkNodes2(nodes, visitor, path = "nodes") {
|
|
1026
|
+
for (const [index, node] of nodes.entries()) {
|
|
1027
|
+
if (!isPlainObject(node)) continue;
|
|
1028
|
+
const nodeRef = `${path}[${index}]`;
|
|
1029
|
+
visitor(node, nodeRef);
|
|
1030
|
+
if (Array.isArray(node.children)) {
|
|
1031
|
+
walkNodes2(node.children, visitor, `${nodeRef}.children`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function applyDeterministicReplacements(spec, ctx) {
|
|
1036
|
+
const effectiveComponents = buildEffectiveComponents(ctx);
|
|
1037
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1038
|
+
const preferredByCategory = /* @__PURE__ */ new Map();
|
|
1039
|
+
for (const entry of effectiveComponents) {
|
|
1040
|
+
const list = byName.get(entry.component.name) ?? [];
|
|
1041
|
+
list.push(entry);
|
|
1042
|
+
byName.set(entry.component.name, list);
|
|
1043
|
+
if (entry.selection !== "preferred") continue;
|
|
1044
|
+
const category = entry.component.category ?? "uncategorized";
|
|
1045
|
+
const names = preferredByCategory.get(category) ?? [];
|
|
1046
|
+
if (!names.includes(entry.component.name)) {
|
|
1047
|
+
names.push(entry.component.name);
|
|
1048
|
+
}
|
|
1049
|
+
preferredByCategory.set(category, names);
|
|
1050
|
+
}
|
|
1051
|
+
const fixedSpec = cloneSpec(spec);
|
|
1052
|
+
const nodes = Array.isArray(fixedSpec.nodes) ? fixedSpec.nodes : [];
|
|
1053
|
+
const replacements = [];
|
|
1054
|
+
const ambiguities = [];
|
|
1055
|
+
walkNodes2(nodes, (node, nodeRef) => {
|
|
1056
|
+
if (!node.type) return;
|
|
1057
|
+
const componentEntries = byName.get(node.type) ?? [];
|
|
1058
|
+
const component = componentEntries[0];
|
|
1059
|
+
if (!component || component.selection !== "discouraged" && component.selection !== "forbidden") {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const candidateEntries = (preferredByCategory.get(component.component.category ?? "uncategorized") ?? []).filter((candidate) => candidate !== component.component.name).map(
|
|
1063
|
+
(candidateName) => effectiveComponents.find(
|
|
1064
|
+
(entry) => entry.component.name === candidateName
|
|
1065
|
+
)
|
|
1066
|
+
).filter((entry) => Boolean(entry));
|
|
1067
|
+
const rankedCandidates = rankReplacementCandidates({
|
|
1068
|
+
source: component,
|
|
1069
|
+
candidates: candidateEntries,
|
|
1070
|
+
node
|
|
1071
|
+
});
|
|
1072
|
+
const replacement = chooseDeterministicCandidate(rankedCandidates);
|
|
1073
|
+
if (!replacement) {
|
|
1074
|
+
if (rankedCandidates.length > 1) {
|
|
1075
|
+
ambiguities.push({
|
|
1076
|
+
nodeId: node.id ?? node.type,
|
|
1077
|
+
nodeRef,
|
|
1078
|
+
from: component.component.name,
|
|
1079
|
+
candidates: rankedCandidates.map((candidate) => candidate.name),
|
|
1080
|
+
rankedCandidates,
|
|
1081
|
+
reason: "ambiguous_preferred_components"
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
replacements.push({
|
|
1087
|
+
nodeId: node.id ?? node.type,
|
|
1088
|
+
from: component.component.name,
|
|
1089
|
+
to: replacement,
|
|
1090
|
+
reason: "deprecated_component",
|
|
1091
|
+
mode: "deterministic"
|
|
1092
|
+
});
|
|
1093
|
+
node.type = replacement;
|
|
1094
|
+
});
|
|
1095
|
+
return {
|
|
1096
|
+
fixedSpec,
|
|
1097
|
+
replacements,
|
|
1098
|
+
ambiguities
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function applyReplacementAtRef(spec, nodeRef, replacement) {
|
|
1102
|
+
const nodes = Array.isArray(spec.nodes) ? spec.nodes : [];
|
|
1103
|
+
let applied = false;
|
|
1104
|
+
walkNodes2(nodes, (node, currentRef) => {
|
|
1105
|
+
if (applied || currentRef !== nodeRef) return;
|
|
1106
|
+
node.type = replacement;
|
|
1107
|
+
applied = true;
|
|
1108
|
+
});
|
|
1109
|
+
return applied;
|
|
1110
|
+
}
|
|
1111
|
+
function supportsFormElicitation(ctx) {
|
|
1112
|
+
const capabilities = ctx.mcp?.clientCapabilities?.elicitation;
|
|
1113
|
+
if (!capabilities) return false;
|
|
1114
|
+
return capabilities.form !== void 0 || capabilities.url === void 0;
|
|
1115
|
+
}
|
|
1116
|
+
function supportsSampling(ctx) {
|
|
1117
|
+
return Boolean(ctx.mcp?.clientCapabilities?.sampling);
|
|
1118
|
+
}
|
|
1119
|
+
function getSamplingText(response) {
|
|
1120
|
+
if (Array.isArray(response.content)) {
|
|
1121
|
+
return response.content.find((item) => item.type === "text")?.text;
|
|
1122
|
+
}
|
|
1123
|
+
return response.content.type === "text" ? response.content.text : void 0;
|
|
1124
|
+
}
|
|
1125
|
+
function buildComponentGuidanceMap(ctx) {
|
|
1126
|
+
return new Map(
|
|
1127
|
+
Object.values(ctx.data.components).map((component) => {
|
|
1128
|
+
const guidance = [];
|
|
1129
|
+
if (component.description) {
|
|
1130
|
+
guidance.push(`description: ${component.description}`);
|
|
1131
|
+
}
|
|
1132
|
+
if (component.guidance.usageGuidance) {
|
|
1133
|
+
guidance.push(`usage: ${component.guidance.usageGuidance}`);
|
|
1134
|
+
}
|
|
1135
|
+
if (component.guidance.dos.length > 0) {
|
|
1136
|
+
guidance.push(`dos: ${component.guidance.dos.join("; ")}`);
|
|
1137
|
+
}
|
|
1138
|
+
if (component.guidance.donts.length > 0) {
|
|
1139
|
+
guidance.push(`donts: ${component.guidance.donts.join("; ")}`);
|
|
1140
|
+
}
|
|
1141
|
+
return [component.name, guidance];
|
|
1142
|
+
})
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
function buildAmbiguityGuidanceLines(candidates, guidanceByName) {
|
|
1146
|
+
return candidates.map((candidate) => {
|
|
1147
|
+
const guidance = guidanceByName.get(candidate) ?? [];
|
|
1148
|
+
return guidance.length > 0 ? `${candidate}: ${guidance.join(" | ")}` : `${candidate}: no additional guidance available`;
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
async function resolveAmbiguitiesWithElicitation(ambiguities, spec, ctx) {
|
|
1152
|
+
if (!ctx.mcp?.server || ambiguities.length === 0) return [];
|
|
1153
|
+
const replacements = [];
|
|
1154
|
+
const guidanceByName = buildComponentGuidanceMap(ctx);
|
|
1155
|
+
for (const ambiguity of ambiguities) {
|
|
1156
|
+
const guidanceLines = buildAmbiguityGuidanceLines(
|
|
1157
|
+
ambiguity.candidates,
|
|
1158
|
+
guidanceByName
|
|
1159
|
+
);
|
|
1160
|
+
const result2 = await ctx.mcp.server.elicitInput({
|
|
1161
|
+
mode: "form",
|
|
1162
|
+
message: [
|
|
1163
|
+
`Fragments found multiple safe replacements for ${ambiguity.from} on node ${ambiguity.nodeId}.`,
|
|
1164
|
+
"Choose the best replacement, or skip this change.",
|
|
1165
|
+
"Candidate guidance:",
|
|
1166
|
+
...guidanceLines
|
|
1167
|
+
].join("\n"),
|
|
1168
|
+
requestedSchema: {
|
|
1169
|
+
type: "object",
|
|
1170
|
+
properties: {
|
|
1171
|
+
replacement: {
|
|
1172
|
+
type: "string",
|
|
1173
|
+
title: "Replacement",
|
|
1174
|
+
description: [
|
|
1175
|
+
"Select the best replacement for this node.",
|
|
1176
|
+
...guidanceLines,
|
|
1177
|
+
"Choose Skip to leave this component unchanged."
|
|
1178
|
+
].join("\n"),
|
|
1179
|
+
oneOf: [
|
|
1180
|
+
...ambiguity.candidates.map((candidate) => ({
|
|
1181
|
+
const: candidate,
|
|
1182
|
+
title: candidate
|
|
1183
|
+
})),
|
|
1184
|
+
{
|
|
1185
|
+
const: "__skip__",
|
|
1186
|
+
title: "Skip"
|
|
1187
|
+
}
|
|
1188
|
+
],
|
|
1189
|
+
default: "__skip__"
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
required: ["replacement"]
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
if (result2.action !== "accept" || !result2.content || typeof result2.content.replacement !== "string" || result2.content.replacement === "__skip__") {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (!ambiguity.candidates.includes(result2.content.replacement)) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (!applyReplacementAtRef(spec, ambiguity.nodeRef, result2.content.replacement)) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
replacements.push({
|
|
1205
|
+
nodeId: ambiguity.nodeId,
|
|
1206
|
+
from: ambiguity.from,
|
|
1207
|
+
to: result2.content.replacement,
|
|
1208
|
+
reason: "deprecated_component",
|
|
1209
|
+
mode: "elicitation"
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
return replacements;
|
|
1213
|
+
}
|
|
1214
|
+
async function resolveAmbiguitiesWithSampling(ambiguities, spec, ctx) {
|
|
1215
|
+
if (!ctx.mcp?.server || ambiguities.length === 0) return [];
|
|
1216
|
+
const guidanceByName = buildComponentGuidanceMap(ctx);
|
|
1217
|
+
const prompt = [
|
|
1218
|
+
"You are choosing design-system component replacements.",
|
|
1219
|
+
'Return strict JSON only in the form {"choices":[{"nodeId":"...","replacement":"...|__skip__"}]}.',
|
|
1220
|
+
"Only choose from the provided candidates.",
|
|
1221
|
+
"Prefer the option whose guidance best matches the original component intent.",
|
|
1222
|
+
JSON.stringify(
|
|
1223
|
+
{
|
|
1224
|
+
ambiguities: ambiguities.map((ambiguity) => ({
|
|
1225
|
+
nodeId: ambiguity.nodeId,
|
|
1226
|
+
from: ambiguity.from,
|
|
1227
|
+
candidates: ambiguity.candidates.map((candidate) => ({
|
|
1228
|
+
name: candidate,
|
|
1229
|
+
guidance: guidanceByName.get(candidate) ?? []
|
|
1230
|
+
}))
|
|
1231
|
+
}))
|
|
1232
|
+
},
|
|
1233
|
+
null,
|
|
1234
|
+
2
|
|
1235
|
+
)
|
|
1236
|
+
].join("\n\n");
|
|
1237
|
+
const response = await ctx.mcp.server.createMessage({
|
|
1238
|
+
messages: [
|
|
1239
|
+
{
|
|
1240
|
+
role: "user",
|
|
1241
|
+
content: {
|
|
1242
|
+
type: "text",
|
|
1243
|
+
text: prompt
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
],
|
|
1247
|
+
maxTokens: 600
|
|
1248
|
+
});
|
|
1249
|
+
const text = getSamplingText(response);
|
|
1250
|
+
if (!text) return [];
|
|
1251
|
+
let parsed;
|
|
1252
|
+
try {
|
|
1253
|
+
parsed = JSON.parse(text);
|
|
1254
|
+
} catch {
|
|
1255
|
+
return [];
|
|
1256
|
+
}
|
|
1257
|
+
const replacements = [];
|
|
1258
|
+
for (const choice of parsed.choices ?? []) {
|
|
1259
|
+
if (!choice.nodeId || !choice.replacement || choice.replacement === "__skip__") {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const ambiguity = ambiguities.find((entry) => entry.nodeId === choice.nodeId);
|
|
1263
|
+
if (!ambiguity || !ambiguity.candidates.includes(choice.replacement)) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (!applyReplacementAtRef(spec, ambiguity.nodeRef, choice.replacement)) {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
replacements.push({
|
|
1270
|
+
nodeId: ambiguity.nodeId,
|
|
1271
|
+
from: ambiguity.from,
|
|
1272
|
+
to: choice.replacement,
|
|
1273
|
+
reason: "deprecated_component",
|
|
1274
|
+
mode: "sampling"
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
return replacements;
|
|
1278
|
+
}
|
|
1279
|
+
function buildSummary(args) {
|
|
1280
|
+
const lines = [
|
|
1281
|
+
`status: ${args.status}`,
|
|
1282
|
+
`original: ${formatVerdict(args.originalVerdict, "summary")}`
|
|
1283
|
+
];
|
|
1284
|
+
if (args.replacements.length > 0) {
|
|
1285
|
+
lines.push(
|
|
1286
|
+
`replacements: ${args.replacements.map((item) => `${item.from} -> ${item.to}`).join(", ")}`
|
|
1287
|
+
);
|
|
1288
|
+
lines.push(`final: ${formatVerdict(args.finalVerdict, "summary")}`);
|
|
1289
|
+
}
|
|
1290
|
+
return lines.join("\n");
|
|
1291
|
+
}
|
|
1292
|
+
var validateAndFixHandler = async (args, ctx) => {
|
|
1293
|
+
const spec = args?.spec;
|
|
1294
|
+
if (!spec || typeof spec !== "object") {
|
|
1295
|
+
return {
|
|
1296
|
+
content: [
|
|
1297
|
+
{
|
|
1298
|
+
type: "text",
|
|
1299
|
+
text: JSON.stringify({
|
|
1300
|
+
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
|
|
1301
|
+
})
|
|
1302
|
+
}
|
|
1303
|
+
],
|
|
1304
|
+
isError: true
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
const specError = validateSpecShape(spec);
|
|
1308
|
+
if (specError) {
|
|
1309
|
+
return {
|
|
1310
|
+
content: [
|
|
1311
|
+
{
|
|
1312
|
+
type: "text",
|
|
1313
|
+
text: JSON.stringify({
|
|
1314
|
+
error: `Invalid spec format: ${specError}. Expected: { nodes: [{ id?: string, type?: string, props?: object, children?: object[] }] }`
|
|
1315
|
+
})
|
|
1316
|
+
}
|
|
1317
|
+
],
|
|
1318
|
+
isError: true
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
const policyOverrides = args?.policy;
|
|
1322
|
+
const applyFixes = args?.applyFixes !== false;
|
|
1323
|
+
const allowElicitation = args?.allowElicitation !== false;
|
|
1324
|
+
const allowSampling = args?.allowSampling !== false;
|
|
1325
|
+
const format = args?.format ?? "json";
|
|
1326
|
+
const startedAt = Date.now();
|
|
1327
|
+
try {
|
|
1328
|
+
const effectiveComponents = buildEffectiveComponents(ctx);
|
|
1329
|
+
const originalVerdict = await runGovern(spec, ctx, policyOverrides);
|
|
1330
|
+
let finalVerdict = originalVerdict;
|
|
1331
|
+
let workingSpec;
|
|
1332
|
+
let fixedSpec;
|
|
1333
|
+
let replacements = [];
|
|
1334
|
+
let ambiguities = [];
|
|
1335
|
+
const resolutionPath = [];
|
|
1336
|
+
const supportsElicitation = supportsFormElicitation(ctx);
|
|
1337
|
+
const supportsModelSampling = supportsSampling(ctx);
|
|
1338
|
+
const preview = !originalVerdict.passed ? applyDeterministicReplacements(spec, ctx) : void 0;
|
|
1339
|
+
if (!applyFixes && preview) {
|
|
1340
|
+
ambiguities = preview.ambiguities;
|
|
1341
|
+
}
|
|
1342
|
+
if (!originalVerdict.passed && applyFixes) {
|
|
1343
|
+
const result2 = preview ?? applyDeterministicReplacements(spec, ctx);
|
|
1344
|
+
replacements = result2.replacements;
|
|
1345
|
+
ambiguities = result2.ambiguities;
|
|
1346
|
+
if (replacements.length > 0) {
|
|
1347
|
+
workingSpec = result2.fixedSpec;
|
|
1348
|
+
resolutionPath.push("deterministic");
|
|
1349
|
+
}
|
|
1350
|
+
const unresolvedAmbiguities2 = ambiguities.filter(
|
|
1351
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
1352
|
+
);
|
|
1353
|
+
if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsElicitation) {
|
|
1354
|
+
workingSpec ??= cloneSpec(spec);
|
|
1355
|
+
const elicitedReplacements = await resolveAmbiguitiesWithElicitation(
|
|
1356
|
+
unresolvedAmbiguities2,
|
|
1357
|
+
workingSpec,
|
|
1358
|
+
ctx
|
|
1359
|
+
);
|
|
1360
|
+
if (elicitedReplacements.length > 0) {
|
|
1361
|
+
resolutionPath.push("elicitation");
|
|
1362
|
+
}
|
|
1363
|
+
replacements.push(...elicitedReplacements);
|
|
1364
|
+
}
|
|
1365
|
+
const remainingAmbiguities = ambiguities.filter(
|
|
1366
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
1367
|
+
);
|
|
1368
|
+
if (remainingAmbiguities.length > 0 && allowSampling && supportsModelSampling) {
|
|
1369
|
+
workingSpec ??= cloneSpec(spec);
|
|
1370
|
+
const sampledReplacements = await resolveAmbiguitiesWithSampling(
|
|
1371
|
+
remainingAmbiguities,
|
|
1372
|
+
workingSpec,
|
|
1373
|
+
ctx
|
|
1374
|
+
);
|
|
1375
|
+
if (sampledReplacements.length > 0) {
|
|
1376
|
+
resolutionPath.push("sampling");
|
|
1377
|
+
}
|
|
1378
|
+
replacements.push(...sampledReplacements);
|
|
1379
|
+
}
|
|
1380
|
+
fixedSpec = replacements.length > 0 ? workingSpec : void 0;
|
|
1381
|
+
if (fixedSpec) {
|
|
1382
|
+
finalVerdict = await runGovern(fixedSpec, ctx, policyOverrides);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const status = originalVerdict.passed ? "pass" : replacements.length > 0 ? finalVerdict.passed ? "fixed" : "partial_fix" : "fail";
|
|
1386
|
+
const unresolvedAmbiguities = ambiguities.filter(
|
|
1387
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
1388
|
+
).map(({ nodeRef: _nodeRef, ...ambiguity }) => ambiguity);
|
|
1389
|
+
const unresolvedAmbiguitiesWithRefs = ambiguities.filter(
|
|
1390
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
1391
|
+
);
|
|
1392
|
+
const wouldFixIfAuthorized = buildWouldFixIfAuthorized({
|
|
1393
|
+
deterministicPreview: preview?.replacements ?? [],
|
|
1394
|
+
unresolvedAmbiguities: unresolvedAmbiguitiesWithRefs,
|
|
1395
|
+
applyFixes,
|
|
1396
|
+
allowElicitation,
|
|
1397
|
+
allowSampling,
|
|
1398
|
+
supportsElicitation,
|
|
1399
|
+
supportsSampling: supportsModelSampling
|
|
1400
|
+
});
|
|
1401
|
+
const attestation = {
|
|
1402
|
+
sourceType: ctx.data.snapshot.sourceType,
|
|
1403
|
+
sourceLabel: ctx.data.snapshot.sourceLabel,
|
|
1404
|
+
designSystemName: ctx.data.snapshot.metadata.designSystemName,
|
|
1405
|
+
packageName: ctx.data.snapshot.metadata.packageName,
|
|
1406
|
+
importPath: ctx.data.snapshot.metadata.importPath,
|
|
1407
|
+
catalogRevision: ctx.data.validateFixContext?.catalogRevision ?? ctx.data.snapshot.metadata.revision,
|
|
1408
|
+
catalogUpdatedAt: ctx.data.validateFixContext?.updatedAt ?? ctx.data.snapshot.metadata.updatedAt,
|
|
1409
|
+
policy: {
|
|
1410
|
+
mode: ctx.data.validateFixContext?.policy.mode ?? "embedded",
|
|
1411
|
+
endpoint: ctx.data.validateFixContext?.policy.endpoint,
|
|
1412
|
+
overrideApplied: Boolean(policyOverrides)
|
|
1413
|
+
},
|
|
1414
|
+
clientCapabilities: {
|
|
1415
|
+
sampling: supportsModelSampling,
|
|
1416
|
+
samplingTools: Boolean(ctx.mcp?.clientCapabilities?.sampling?.tools),
|
|
1417
|
+
elicitationForm: supportsElicitation,
|
|
1418
|
+
roots: Boolean(ctx.mcp?.clientCapabilities?.roots)
|
|
1419
|
+
},
|
|
1420
|
+
capabilitiesUsed: {
|
|
1421
|
+
deterministic: replacements.some((replacement) => replacement.mode === "deterministic"),
|
|
1422
|
+
elicitation: replacements.some((replacement) => replacement.mode === "elicitation"),
|
|
1423
|
+
sampling: replacements.some((replacement) => replacement.mode === "sampling")
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1426
|
+
const evaluation = {
|
|
1427
|
+
durationMs: Date.now() - startedAt,
|
|
1428
|
+
originalViolationCount: countViolations(originalVerdict),
|
|
1429
|
+
finalViolationCount: countViolations(finalVerdict),
|
|
1430
|
+
originalPassed: originalVerdict.passed,
|
|
1431
|
+
finalPassed: finalVerdict.passed,
|
|
1432
|
+
replacementCount: replacements.length,
|
|
1433
|
+
replacementsByMode: {
|
|
1434
|
+
deterministic: replacements.filter((replacement) => replacement.mode === "deterministic").length,
|
|
1435
|
+
elicitation: replacements.filter((replacement) => replacement.mode === "elicitation").length,
|
|
1436
|
+
sampling: replacements.filter((replacement) => replacement.mode === "sampling").length
|
|
1437
|
+
},
|
|
1438
|
+
ambiguityCount: ambiguities.length,
|
|
1439
|
+
unresolvedAmbiguityCount: unresolvedAmbiguities.length,
|
|
1440
|
+
candidateRankingUsed: true
|
|
1441
|
+
};
|
|
1442
|
+
const nextAction = getNextAction(
|
|
1443
|
+
status,
|
|
1444
|
+
replacements,
|
|
1445
|
+
unresolvedAmbiguities.length
|
|
1446
|
+
);
|
|
1447
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1448
|
+
const payload = {
|
|
1449
|
+
status,
|
|
1450
|
+
nextAction,
|
|
1451
|
+
applyFixes,
|
|
1452
|
+
allowElicitation,
|
|
1453
|
+
allowSampling,
|
|
1454
|
+
resolutionPath,
|
|
1455
|
+
replacements,
|
|
1456
|
+
originalVerdict,
|
|
1457
|
+
finalVerdict,
|
|
1458
|
+
...fixedSpec ? { fixedSpec } : {},
|
|
1459
|
+
attestation,
|
|
1460
|
+
evaluation,
|
|
1461
|
+
preferredComponents: getSelectionNames(
|
|
1462
|
+
effectiveComponents,
|
|
1463
|
+
"preferred"
|
|
1464
|
+
),
|
|
1465
|
+
discouragedComponents: getSelectionNames(
|
|
1466
|
+
effectiveComponents,
|
|
1467
|
+
"discouraged"
|
|
1468
|
+
),
|
|
1469
|
+
forbiddenComponents: getSelectionNames(
|
|
1470
|
+
effectiveComponents,
|
|
1471
|
+
"forbidden"
|
|
1472
|
+
),
|
|
1473
|
+
unresolvedAmbiguities,
|
|
1474
|
+
wouldFixIfAuthorized
|
|
1475
|
+
};
|
|
1476
|
+
await emitValidateAndFixTelemetry({
|
|
1477
|
+
ctx,
|
|
1478
|
+
status,
|
|
1479
|
+
resolutionPath,
|
|
1480
|
+
evaluation,
|
|
1481
|
+
attestation,
|
|
1482
|
+
nextAction
|
|
1483
|
+
});
|
|
1484
|
+
return {
|
|
1485
|
+
content: [
|
|
1486
|
+
{
|
|
1487
|
+
type: "text",
|
|
1488
|
+
text: format === "summary" ? buildSummary({
|
|
1489
|
+
status,
|
|
1490
|
+
originalVerdict,
|
|
1491
|
+
finalVerdict,
|
|
1492
|
+
replacements
|
|
1493
|
+
}) : JSON.stringify(payload)
|
|
1494
|
+
}
|
|
1495
|
+
],
|
|
1496
|
+
_meta: {
|
|
1497
|
+
...catalogMeta,
|
|
1498
|
+
status,
|
|
1499
|
+
nextAction,
|
|
1500
|
+
replacementCount: replacements.length,
|
|
1501
|
+
passed: finalVerdict.passed,
|
|
1502
|
+
unresolvedAmbiguityCount: unresolvedAmbiguities.length,
|
|
1503
|
+
wouldFixIfAuthorizedCount: wouldFixIfAuthorized.length,
|
|
1504
|
+
resolutionPath
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1509
|
+
return {
|
|
1510
|
+
content: [
|
|
1511
|
+
{
|
|
1512
|
+
type: "text",
|
|
1513
|
+
text: JSON.stringify({
|
|
1514
|
+
error: message
|
|
1515
|
+
})
|
|
1516
|
+
}
|
|
1517
|
+
],
|
|
1518
|
+
isError: true
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
// src/cloud-http.ts
|
|
1524
|
+
var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
|
|
1525
|
+
function normalizeCloudUrl(url) {
|
|
1526
|
+
if (!url) return DEFAULT_CLOUD_URL;
|
|
1527
|
+
return url.replace(/\/+$/, "");
|
|
1528
|
+
}
|
|
1529
|
+
async function cloudFetchJson(args) {
|
|
1530
|
+
const base = normalizeCloudUrl(args.cloudUrl);
|
|
1531
|
+
const url = new URL(`${base}${args.path}`);
|
|
1532
|
+
if (args.query) {
|
|
1533
|
+
for (const [key, value] of Object.entries(args.query)) {
|
|
1534
|
+
if (value !== void 0) url.searchParams.set(key, String(value));
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const response = await fetch(url.toString(), {
|
|
1538
|
+
headers: { "X-API-Key": args.apiKey }
|
|
1539
|
+
});
|
|
1540
|
+
if (!response.ok) {
|
|
1541
|
+
const body = await response.text();
|
|
1542
|
+
let message;
|
|
1543
|
+
try {
|
|
1544
|
+
const parsed = JSON.parse(body);
|
|
1545
|
+
message = parsed.error ?? body;
|
|
1546
|
+
} catch {
|
|
1547
|
+
message = body;
|
|
1548
|
+
}
|
|
1549
|
+
throw new Error(
|
|
1550
|
+
`Cloud ${args.resource} API error (${response.status}): ${message}`
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
return await response.json();
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/findings-service.ts
|
|
1557
|
+
async function fetchFindings(apiKey, params, cloudUrl) {
|
|
1558
|
+
return cloudFetchJson({
|
|
1559
|
+
apiKey,
|
|
1560
|
+
cloudUrl,
|
|
1561
|
+
path: "/api/findings",
|
|
1562
|
+
resource: "findings",
|
|
1563
|
+
query: {
|
|
1564
|
+
status: params.status,
|
|
1565
|
+
severity: params.severity,
|
|
1566
|
+
category: params.category,
|
|
1567
|
+
ruleId: params.ruleId,
|
|
1568
|
+
filePath: params.filePath,
|
|
1569
|
+
limit: params.limit
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
|
|
1574
|
+
return fetchFindings(
|
|
1575
|
+
apiKey,
|
|
1576
|
+
{ status: "open", filePath, limit: 200 },
|
|
1577
|
+
cloudUrl
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
function buildFindingSourceUrl(finding) {
|
|
1581
|
+
const { repoFullName, commitSha, filePath, line } = finding;
|
|
1582
|
+
if (!repoFullName || !commitSha || !filePath) return void 0;
|
|
1583
|
+
if (!/^[-\w.]+\/[-\w.]+$/.test(repoFullName)) return void 0;
|
|
1584
|
+
if (!/^[a-fA-F0-9]{7,64}$/.test(commitSha)) return void 0;
|
|
1585
|
+
const normalizedPath = filePath.replace(/^\/+/, "");
|
|
1586
|
+
if (!normalizedPath) return void 0;
|
|
1587
|
+
const encodedPath = normalizedPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1588
|
+
const lineSuffix = line != null && line > 0 ? `#L${Math.floor(line)}` : "";
|
|
1589
|
+
return `https://github.com/${repoFullName}/blob/${commitSha}/${encodedPath}${lineSuffix}`;
|
|
1590
|
+
}
|
|
1591
|
+
async function fetchFindingsSummary(apiKey, params, cloudUrl) {
|
|
1592
|
+
const { findings } = await fetchFindings(
|
|
1593
|
+
apiKey,
|
|
1594
|
+
{ ...params, limit: params.limit ?? 200 },
|
|
1595
|
+
cloudUrl
|
|
1596
|
+
);
|
|
1597
|
+
return summarizeFindings(findings, params);
|
|
1598
|
+
}
|
|
1599
|
+
function summarizeFindings(findings, params) {
|
|
1600
|
+
const bySeverity = { error: 0, warning: 0, info: 0 };
|
|
1601
|
+
const byStatus = { open: 0, resolved: 0, ignored: 0 };
|
|
1602
|
+
const byCategory = {};
|
|
1603
|
+
const byRuleId = {};
|
|
1604
|
+
const byFilePath = {};
|
|
1605
|
+
for (const finding of findings) {
|
|
1606
|
+
bySeverity[finding.severity] += 1;
|
|
1607
|
+
byStatus[finding.status] += 1;
|
|
1608
|
+
increment(byRuleId, finding.ruleId);
|
|
1609
|
+
if (finding.category) increment(byCategory, finding.category);
|
|
1610
|
+
if (finding.filePath) increment(byFilePath, finding.filePath);
|
|
1611
|
+
}
|
|
1612
|
+
const { limit: _limit, ...filters } = params;
|
|
1613
|
+
return {
|
|
1614
|
+
total: findings.length,
|
|
1615
|
+
filters,
|
|
1616
|
+
bySeverity,
|
|
1617
|
+
byStatus,
|
|
1618
|
+
byCategory,
|
|
1619
|
+
byRuleId,
|
|
1620
|
+
byFilePath,
|
|
1621
|
+
topFiles: topEntries(byFilePath, "filePath"),
|
|
1622
|
+
topRules: topEntries(byRuleId, "ruleId")
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function increment(counts, key) {
|
|
1626
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1627
|
+
}
|
|
1628
|
+
function topEntries(counts, keyName) {
|
|
1629
|
+
return Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10).map(([key, count]) => ({ [keyName]: key, count }));
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/tools/cloud-auth.ts
|
|
1633
|
+
function resolveCloudApiKey(ctx) {
|
|
1634
|
+
return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
|
|
1635
|
+
}
|
|
1636
|
+
function resolveCloudUrl(ctx) {
|
|
1637
|
+
return ctx.config.fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
|
|
1638
|
+
}
|
|
1639
|
+
function missingKeyError() {
|
|
1640
|
+
return {
|
|
1641
|
+
content: [
|
|
1642
|
+
{
|
|
1643
|
+
type: "text",
|
|
1644
|
+
text: JSON.stringify({
|
|
1645
|
+
error: "Cloud API key required. Set FRAGMENTS_API_KEY env var, pass --cloud-api-key, or configure cloud.apiKey in ds-mcp.config.json."
|
|
1646
|
+
})
|
|
1647
|
+
}
|
|
1648
|
+
],
|
|
1649
|
+
isError: true
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// src/tools/findings.ts
|
|
1654
|
+
function enrichFindings(findings, ctx) {
|
|
1655
|
+
return findings.map((finding) => {
|
|
1656
|
+
const sourceUrl = finding.sourceUrl ?? buildFindingSourceUrl(finding);
|
|
1657
|
+
const withSourceUrl = sourceUrl ? { ...finding, sourceUrl } : finding;
|
|
1658
|
+
if (!finding.prop || !finding.rawValue || !ctx.data.tokens) {
|
|
1659
|
+
return withSourceUrl;
|
|
1660
|
+
}
|
|
1661
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1662
|
+
const suggestion = suggestToken({
|
|
1663
|
+
tokens: ctx.data.tokens,
|
|
1664
|
+
property: finding.prop,
|
|
1665
|
+
value: finding.rawValue,
|
|
1666
|
+
catalogRevision: catalogMeta.catalogRevision,
|
|
1667
|
+
updatedAt: catalogMeta.updatedAt
|
|
1668
|
+
});
|
|
1669
|
+
if (!suggestion.recommended) {
|
|
1670
|
+
const {
|
|
1671
|
+
suggestedToken: _discarded,
|
|
1672
|
+
suggestedTokenDetails: _details,
|
|
1673
|
+
...rest
|
|
1674
|
+
} = withSourceUrl;
|
|
1675
|
+
return rest;
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
...withSourceUrl,
|
|
1679
|
+
suggestedToken: suggestion.recommended.cssVar ?? suggestion.recommended.name,
|
|
1680
|
+
suggestedTokenDetails: suggestion.recommended
|
|
1681
|
+
};
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
var findingsListHandler = async (args, ctx) => {
|
|
1685
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1686
|
+
if (!apiKey) return missingKeyError();
|
|
1687
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1688
|
+
const params = {};
|
|
1689
|
+
if (args.status) params.status = args.status;
|
|
1690
|
+
if (args.severity)
|
|
1691
|
+
params.severity = args.severity;
|
|
1692
|
+
if (args.category) params.category = String(args.category);
|
|
1693
|
+
if (args.ruleId) params.ruleId = String(args.ruleId);
|
|
1694
|
+
if (args.filePath) params.filePath = String(args.filePath);
|
|
1695
|
+
if (args.limit != null) params.limit = Number(args.limit);
|
|
1696
|
+
try {
|
|
1697
|
+
const result2 = await fetchFindings(apiKey, params, cloudUrl);
|
|
1698
|
+
const findings = enrichFindings(result2.findings, ctx);
|
|
1699
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1700
|
+
return {
|
|
1701
|
+
content: [
|
|
1702
|
+
{
|
|
1703
|
+
type: "text",
|
|
1704
|
+
text: JSON.stringify({ ...result2, findings })
|
|
1705
|
+
}
|
|
1706
|
+
],
|
|
1707
|
+
_meta: {
|
|
1708
|
+
...catalogMeta,
|
|
1709
|
+
count: findings.length,
|
|
1710
|
+
tokenSuggestionCount: findings.filter(
|
|
1711
|
+
(finding) => finding.suggestedTokenDetails
|
|
1712
|
+
).length
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
return {
|
|
1717
|
+
content: [
|
|
1718
|
+
{
|
|
1719
|
+
type: "text",
|
|
1720
|
+
text: JSON.stringify({
|
|
1721
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1722
|
+
})
|
|
1723
|
+
}
|
|
1724
|
+
],
|
|
1725
|
+
isError: true
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
var findingsSummaryHandler = async (args, ctx) => {
|
|
1730
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1731
|
+
if (!apiKey) return missingKeyError();
|
|
1732
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1733
|
+
const params = {};
|
|
1734
|
+
if (args.status) params.status = args.status;
|
|
1735
|
+
if (args.severity)
|
|
1736
|
+
params.severity = args.severity;
|
|
1737
|
+
if (args.category) params.category = String(args.category);
|
|
1738
|
+
if (args.ruleId) params.ruleId = String(args.ruleId);
|
|
1739
|
+
if (args.filePath) params.filePath = String(args.filePath);
|
|
1740
|
+
if (args.limit != null) params.limit = Number(args.limit);
|
|
1741
|
+
try {
|
|
1742
|
+
const summary = await fetchFindingsSummary(apiKey, params, cloudUrl);
|
|
1743
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1744
|
+
return {
|
|
1745
|
+
content: [
|
|
1746
|
+
{
|
|
1747
|
+
type: "text",
|
|
1748
|
+
text: JSON.stringify(summary)
|
|
1749
|
+
}
|
|
1750
|
+
],
|
|
1751
|
+
_meta: {
|
|
1752
|
+
...catalogMeta,
|
|
1753
|
+
total: summary.total,
|
|
1754
|
+
topFileCount: summary.topFiles.length,
|
|
1755
|
+
topRuleCount: summary.topRules.length
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
return {
|
|
1760
|
+
content: [
|
|
1761
|
+
{
|
|
1762
|
+
type: "text",
|
|
1763
|
+
text: JSON.stringify({
|
|
1764
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1765
|
+
})
|
|
1766
|
+
}
|
|
1767
|
+
],
|
|
1768
|
+
isError: true
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
var findingsForFileHandler = async (args, ctx) => {
|
|
1773
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1774
|
+
if (!apiKey) return missingKeyError();
|
|
1775
|
+
const filePath = args.filePath;
|
|
1776
|
+
if (!filePath || typeof filePath !== "string") {
|
|
1777
|
+
return {
|
|
1778
|
+
content: [
|
|
1779
|
+
{
|
|
1780
|
+
type: "text",
|
|
1781
|
+
text: JSON.stringify({ error: "filePath is required." })
|
|
1782
|
+
}
|
|
1783
|
+
],
|
|
1784
|
+
isError: true
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1788
|
+
try {
|
|
1789
|
+
const result2 = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
|
|
1790
|
+
const findings = enrichFindings(
|
|
1791
|
+
result2.findings.filter((f) => f.filePath === filePath),
|
|
1792
|
+
ctx
|
|
1793
|
+
);
|
|
1794
|
+
const catalogMeta = getCatalogMeta(ctx.data);
|
|
1795
|
+
return {
|
|
1796
|
+
content: [
|
|
1797
|
+
{
|
|
1798
|
+
type: "text",
|
|
1799
|
+
text: JSON.stringify({ findings, filePath })
|
|
1800
|
+
}
|
|
1801
|
+
],
|
|
1802
|
+
_meta: {
|
|
1803
|
+
...catalogMeta,
|
|
1804
|
+
count: findings.length,
|
|
1805
|
+
filePath,
|
|
1806
|
+
tokenSuggestionCount: findings.filter(
|
|
1807
|
+
(finding) => finding.suggestedTokenDetails
|
|
1808
|
+
).length
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
return {
|
|
1813
|
+
content: [
|
|
1814
|
+
{
|
|
1815
|
+
type: "text",
|
|
1816
|
+
text: JSON.stringify({
|
|
1817
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1818
|
+
})
|
|
1819
|
+
}
|
|
1820
|
+
],
|
|
1821
|
+
isError: true
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// src/tools/swap-to-canonical.ts
|
|
1827
|
+
import * as ts from "typescript";
|
|
1828
|
+
import {
|
|
1829
|
+
resolveCanonicalForHtmlElement,
|
|
1830
|
+
formatRawHtmlElement
|
|
1831
|
+
} from "@fragments-sdk/classifier";
|
|
1832
|
+
|
|
1833
|
+
// src/canonical-mappings-service.ts
|
|
1834
|
+
async function fetchCanonicalMappings(apiKey, cloudUrl) {
|
|
1835
|
+
const catalog = await cloudFetchJson({
|
|
1836
|
+
apiKey,
|
|
1837
|
+
cloudUrl,
|
|
1838
|
+
path: "/api/catalog",
|
|
1839
|
+
resource: "catalog"
|
|
1840
|
+
});
|
|
1841
|
+
return catalog.canonicalMappings ?? [];
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/tools/swap-to-canonical.ts
|
|
1845
|
+
function errorResult(message) {
|
|
1846
|
+
return {
|
|
1847
|
+
content: [
|
|
1848
|
+
{
|
|
1849
|
+
type: "text",
|
|
1850
|
+
text: JSON.stringify({ error: message })
|
|
1851
|
+
}
|
|
1852
|
+
],
|
|
1853
|
+
isError: true
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
var resolveCanonicalForElement = resolveCanonicalForHtmlElement;
|
|
1857
|
+
function walkRawJsxElements(source) {
|
|
1858
|
+
const results = [];
|
|
1859
|
+
const visit = (node) => {
|
|
1860
|
+
let opening;
|
|
1861
|
+
if (ts.isJsxSelfClosingElement(node)) {
|
|
1862
|
+
opening = node;
|
|
1863
|
+
} else if (ts.isJsxElement(node)) {
|
|
1864
|
+
opening = node.openingElement;
|
|
1865
|
+
}
|
|
1866
|
+
if (opening) {
|
|
1867
|
+
const tagNameNode = opening.tagName;
|
|
1868
|
+
if (ts.isIdentifier(tagNameNode)) {
|
|
1869
|
+
const tagName = tagNameNode.text;
|
|
1870
|
+
if (/^[a-z]/.test(tagName)) {
|
|
1871
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
1872
|
+
for (const attr of opening.attributes.properties) {
|
|
1873
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
1874
|
+
if (!attr.name || !ts.isIdentifier(attr.name)) continue;
|
|
1875
|
+
const name = attr.name.text;
|
|
1876
|
+
if (!attr.initializer) {
|
|
1877
|
+
attrs.set(name, { value: "", dynamic: false });
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
1881
|
+
attrs.set(name, {
|
|
1882
|
+
value: attr.initializer.text,
|
|
1883
|
+
dynamic: false
|
|
1884
|
+
});
|
|
1885
|
+
} else if (ts.isJsxExpression(attr.initializer)) {
|
|
1886
|
+
attrs.set(name, { value: "", dynamic: true });
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
const start = opening.getStart(source);
|
|
1890
|
+
const { line } = source.getLineAndCharacterOfPosition(start);
|
|
1891
|
+
results.push({ tagName, attrs, line: line + 1 });
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
ts.forEachChild(node, visit);
|
|
1896
|
+
};
|
|
1897
|
+
visit(source);
|
|
1898
|
+
return results;
|
|
1899
|
+
}
|
|
1900
|
+
var formatRawElement = formatRawHtmlElement;
|
|
1901
|
+
function buildPropMapping(attrs, rowMapping) {
|
|
1902
|
+
const transforms = new Map(
|
|
1903
|
+
rowMapping.map((m) => [m.rawProp, m])
|
|
1904
|
+
);
|
|
1905
|
+
const mapping = [];
|
|
1906
|
+
for (const rawProp of attrs.keys()) {
|
|
1907
|
+
if (rawProp === "type") continue;
|
|
1908
|
+
mapping.push(
|
|
1909
|
+
transforms.get(rawProp) ?? { rawProp, canonicalProp: rawProp }
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
return mapping;
|
|
1913
|
+
}
|
|
1914
|
+
function groupMappingsByCanonical(mappings) {
|
|
1915
|
+
const out = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (const m of mappings) {
|
|
1917
|
+
if (!m.importPath) continue;
|
|
1918
|
+
const list = out.get(m.canonical) ?? [];
|
|
1919
|
+
list.push(m);
|
|
1920
|
+
out.set(m.canonical, list);
|
|
1921
|
+
}
|
|
1922
|
+
for (const list of out.values()) {
|
|
1923
|
+
list.sort((a, b) => {
|
|
1924
|
+
const ac = a.confidence ?? 0;
|
|
1925
|
+
const bc = b.confidence ?? 0;
|
|
1926
|
+
if (ac !== bc) return bc - ac;
|
|
1927
|
+
return a.name.localeCompare(b.name);
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
return out;
|
|
1931
|
+
}
|
|
1932
|
+
function buildSwapSuggestions(args) {
|
|
1933
|
+
const eligible = args.mappings.filter(
|
|
1934
|
+
(m) => m.canonicalConfidence === "auto" || m.canonicalConfidence === "overridden"
|
|
1935
|
+
);
|
|
1936
|
+
if (eligible.length === 0) return [];
|
|
1937
|
+
const byCanonical = groupMappingsByCanonical(eligible);
|
|
1938
|
+
if (byCanonical.size === 0) return [];
|
|
1939
|
+
const source = ts.createSourceFile(
|
|
1940
|
+
args.filePath,
|
|
1941
|
+
args.fileContent,
|
|
1942
|
+
ts.ScriptTarget.Latest,
|
|
1943
|
+
/* setParentNodes */
|
|
1944
|
+
true,
|
|
1945
|
+
ts.ScriptKind.TSX
|
|
1946
|
+
);
|
|
1947
|
+
const elements = walkRawJsxElements(source);
|
|
1948
|
+
const suggestions = [];
|
|
1949
|
+
for (const el of elements) {
|
|
1950
|
+
const canonical = resolveCanonicalForElement(el.tagName, el.attrs);
|
|
1951
|
+
if (!canonical) continue;
|
|
1952
|
+
const candidates = byCanonical.get(canonical);
|
|
1953
|
+
if (!candidates || candidates.length === 0) continue;
|
|
1954
|
+
const primary = candidates[0];
|
|
1955
|
+
const importPath = primary.importPath;
|
|
1956
|
+
if (!importPath) continue;
|
|
1957
|
+
const suggestion = {
|
|
1958
|
+
rawElement: formatRawElement(el.tagName, el.attrs),
|
|
1959
|
+
canonical,
|
|
1960
|
+
componentName: primary.name,
|
|
1961
|
+
importPath,
|
|
1962
|
+
propMapping: buildPropMapping(el.attrs, primary.propMapping),
|
|
1963
|
+
line: el.line
|
|
1964
|
+
};
|
|
1965
|
+
if (candidates.length > 1) {
|
|
1966
|
+
suggestion.alternates = candidates.slice(1).filter((c) => c.importPath).map((c) => ({
|
|
1967
|
+
name: c.name,
|
|
1968
|
+
importPath: c.importPath,
|
|
1969
|
+
confidence: c.confidence ?? 0
|
|
1970
|
+
}));
|
|
1971
|
+
}
|
|
1972
|
+
suggestions.push(suggestion);
|
|
1973
|
+
}
|
|
1974
|
+
return suggestions;
|
|
1975
|
+
}
|
|
1976
|
+
var swapToCanonicalHandler = async (args, ctx) => {
|
|
1977
|
+
const apiKey = resolveCloudApiKey(ctx);
|
|
1978
|
+
if (!apiKey) return missingKeyError();
|
|
1979
|
+
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
1980
|
+
const fileContent = typeof args.fileContent === "string" ? args.fileContent : "";
|
|
1981
|
+
if (!filePath || !fileContent) {
|
|
1982
|
+
return errorResult("filePath and fileContent are required.");
|
|
1983
|
+
}
|
|
1984
|
+
const cloudUrl = resolveCloudUrl(ctx);
|
|
1985
|
+
let mappings;
|
|
1986
|
+
try {
|
|
1987
|
+
mappings = await fetchCanonicalMappings(apiKey, cloudUrl);
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
1990
|
+
}
|
|
1991
|
+
const suggestions = buildSwapSuggestions({
|
|
1992
|
+
filePath,
|
|
1993
|
+
fileContent,
|
|
1994
|
+
mappings
|
|
1995
|
+
});
|
|
1996
|
+
return {
|
|
1997
|
+
content: [
|
|
1998
|
+
{
|
|
1999
|
+
type: "text",
|
|
2000
|
+
text: JSON.stringify({ suggestions, filePath })
|
|
2001
|
+
}
|
|
2002
|
+
],
|
|
2003
|
+
_meta: {
|
|
2004
|
+
count: suggestions.length,
|
|
2005
|
+
mappingCount: mappings.length,
|
|
2006
|
+
filePath
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
// src/tools/index.ts
|
|
2012
|
+
var CORE_TOOLS = {
|
|
2013
|
+
"tokens.suggest": tokensSuggestHandler,
|
|
2014
|
+
govern: governHandler,
|
|
2015
|
+
validate_and_fix: validateAndFixHandler,
|
|
2016
|
+
findings_list: findingsListHandler,
|
|
2017
|
+
findings_summary: findingsSummaryHandler,
|
|
2018
|
+
findings_for_file: findingsForFileHandler,
|
|
2019
|
+
swap_to_canonical: swapToCanonicalHandler
|
|
2020
|
+
};
|
|
2021
|
+
var VIEWER_TOOLS = {};
|
|
2022
|
+
var INFRA_TOOLS = {};
|
|
2023
|
+
var BUILTIN_TOOLS = {
|
|
2024
|
+
...CORE_TOOLS,
|
|
2025
|
+
...VIEWER_TOOLS,
|
|
2026
|
+
...INFRA_TOOLS
|
|
2027
|
+
};
|
|
2028
|
+
var TOOL_CAPABILITIES = {
|
|
2029
|
+
"tokens.suggest": ["tokens"],
|
|
2030
|
+
validate_and_fix: ["components"]
|
|
2031
|
+
};
|
|
2032
|
+
|
|
2033
|
+
// src/registry.ts
|
|
2034
|
+
var ToolRegistry = class {
|
|
2035
|
+
tools = /* @__PURE__ */ new Map();
|
|
2036
|
+
prefix;
|
|
2037
|
+
onChanged;
|
|
2038
|
+
constructor(prefix, opts) {
|
|
2039
|
+
this.prefix = prefix;
|
|
2040
|
+
this.onChanged = opts?.onChanged;
|
|
2041
|
+
}
|
|
2042
|
+
/** Register a single tool */
|
|
2043
|
+
register(key, handler, definition, availability = "always", requiredCapabilities = []) {
|
|
2044
|
+
this.tools.set(key, {
|
|
2045
|
+
key,
|
|
2046
|
+
handler,
|
|
2047
|
+
definition,
|
|
2048
|
+
availability,
|
|
2049
|
+
requiredCapabilities
|
|
2050
|
+
});
|
|
2051
|
+
this.onChanged?.();
|
|
2052
|
+
}
|
|
2053
|
+
/** Unregister a tool by key */
|
|
2054
|
+
unregister(key) {
|
|
2055
|
+
if (this.tools.delete(key)) {
|
|
2056
|
+
this.onChanged?.();
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
/** Bulk register built-in tools with availability metadata */
|
|
2060
|
+
registerBuiltins(toolSets, definitions, capabilityByKey = {}) {
|
|
2061
|
+
const defMap = new Map(definitions.map((d) => [d.key, d]));
|
|
2062
|
+
for (const [key, handler] of Object.entries(toolSets.core)) {
|
|
2063
|
+
this.tools.set(key, {
|
|
2064
|
+
key,
|
|
2065
|
+
handler,
|
|
2066
|
+
definition: defMap.get(key),
|
|
2067
|
+
availability: "always",
|
|
2068
|
+
requiredCapabilities: capabilityByKey[key] ?? []
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
for (const [key, handler] of Object.entries(toolSets.viewer)) {
|
|
2072
|
+
this.tools.set(key, {
|
|
2073
|
+
key,
|
|
2074
|
+
handler,
|
|
2075
|
+
definition: defMap.get(key),
|
|
2076
|
+
availability: "viewer",
|
|
2077
|
+
requiredCapabilities: capabilityByKey[key] ?? []
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
for (const [key, handler] of Object.entries(toolSets.infra)) {
|
|
2081
|
+
this.tools.set(key, {
|
|
2082
|
+
key,
|
|
2083
|
+
handler,
|
|
2084
|
+
definition: defMap.get(key),
|
|
2085
|
+
availability: "playground",
|
|
2086
|
+
requiredCapabilities: capabilityByKey[key] ?? []
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
/** Get handler for a tool (by unprefixed key or prefixed name) */
|
|
2091
|
+
getHandler(nameOrKey) {
|
|
2092
|
+
const direct = this.tools.get(nameOrKey);
|
|
2093
|
+
if (direct) return direct.handler;
|
|
2094
|
+
if (!this.prefix) return void 0;
|
|
2095
|
+
const prefixStr = this.prefix + "_";
|
|
2096
|
+
if (nameOrKey.startsWith(prefixStr)) {
|
|
2097
|
+
const key = nameOrKey.slice(prefixStr.length);
|
|
2098
|
+
return this.tools.get(key)?.handler;
|
|
2099
|
+
}
|
|
2100
|
+
return void 0;
|
|
2101
|
+
}
|
|
2102
|
+
/** Resolve unprefixed key from a potentially prefixed tool name */
|
|
2103
|
+
resolveKey(name) {
|
|
2104
|
+
if (!this.prefix) return name;
|
|
2105
|
+
const prefixStr = this.prefix + "_";
|
|
2106
|
+
return name.startsWith(prefixStr) ? name.slice(prefixStr.length) : name;
|
|
2107
|
+
}
|
|
2108
|
+
/** List available tools as MCP Tool[] based on current availability context */
|
|
2109
|
+
listTools(ctx, allToolSchemas) {
|
|
2110
|
+
const availableKeys = /* @__PURE__ */ new Set();
|
|
2111
|
+
for (const [key, tool] of this.tools) {
|
|
2112
|
+
const hasCapabilities = tool.requiredCapabilities.every(
|
|
2113
|
+
(capability) => ctx.capabilities.has(capability)
|
|
2114
|
+
);
|
|
2115
|
+
if (!hasCapabilities) continue;
|
|
2116
|
+
if (tool.availability === "always") {
|
|
2117
|
+
availableKeys.add(key);
|
|
2118
|
+
} else if (tool.availability === "viewer" && ctx.hasViewer) {
|
|
2119
|
+
availableKeys.add(key);
|
|
2120
|
+
} else if (tool.availability === "playground" && ctx.hasPlayground) {
|
|
2121
|
+
availableKeys.add(key);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
if (!this.prefix) {
|
|
2125
|
+
return allToolSchemas.filter((t) => availableKeys.has(t.name));
|
|
2126
|
+
}
|
|
2127
|
+
const prefixStr = this.prefix + "_";
|
|
2128
|
+
return allToolSchemas.filter((t) => {
|
|
2129
|
+
const key = t.name.startsWith(prefixStr) ? t.name.slice(prefixStr.length) : t.name;
|
|
2130
|
+
return availableKeys.has(key);
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
/** Execute a tool by name, dispatching to its handler */
|
|
2134
|
+
async execute(name, args, ctx) {
|
|
2135
|
+
const key = this.resolveKey(name);
|
|
2136
|
+
const registered = this.tools.get(key);
|
|
2137
|
+
if (!registered) {
|
|
2138
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2139
|
+
}
|
|
2140
|
+
return registered.handler(args, ctx);
|
|
2141
|
+
}
|
|
2142
|
+
/** Get the number of registered tools */
|
|
2143
|
+
get size() {
|
|
2144
|
+
return this.tools.size;
|
|
2145
|
+
}
|
|
2146
|
+
/** Get all registered keys */
|
|
2147
|
+
keys() {
|
|
2148
|
+
return Array.from(this.tools.keys());
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
// src/middleware.ts
|
|
2153
|
+
function executeWithMiddleware(middlewares, mCtx, handler) {
|
|
2154
|
+
const chain = [...middlewares].reverse().reduce(
|
|
2155
|
+
(next, mw) => () => mw(mCtx, next),
|
|
2156
|
+
handler
|
|
2157
|
+
);
|
|
2158
|
+
return chain();
|
|
2159
|
+
}
|
|
2160
|
+
function telemetryMiddleware(logger) {
|
|
2161
|
+
return async (mCtx, next) => {
|
|
2162
|
+
const start = Date.now();
|
|
2163
|
+
const result2 = await next();
|
|
2164
|
+
logger(`${mCtx.toolKey}: ${Date.now() - start}ms`);
|
|
2165
|
+
return result2;
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// src/source-selection.ts
|
|
2170
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2171
|
+
import { join as join6 } from "path";
|
|
2172
|
+
|
|
2173
|
+
// src/adapters/fragments-json.ts
|
|
2174
|
+
import { readFile } from "fs/promises";
|
|
2175
|
+
|
|
2176
|
+
// src/discovery.ts
|
|
2177
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
2178
|
+
import { join as join2, dirname, resolve } from "path";
|
|
2179
|
+
import { createRequire } from "module";
|
|
2180
|
+
function resolveWorkspaceGlob(baseDir, pattern) {
|
|
2181
|
+
const parts = pattern.split("/");
|
|
2182
|
+
let dirs = [baseDir];
|
|
2183
|
+
for (const part of parts) {
|
|
2184
|
+
if (part === "**") continue;
|
|
2185
|
+
const next = [];
|
|
2186
|
+
for (const d of dirs) {
|
|
2187
|
+
if (part === "*") {
|
|
2188
|
+
try {
|
|
2189
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
2190
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2191
|
+
next.push(join2(d, entry.name));
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
} catch {
|
|
2195
|
+
}
|
|
2196
|
+
} else {
|
|
2197
|
+
const candidate = join2(d, part);
|
|
2198
|
+
if (existsSync2(candidate)) next.push(candidate);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
dirs = next;
|
|
2202
|
+
}
|
|
2203
|
+
return dirs;
|
|
2204
|
+
}
|
|
2205
|
+
function getWorkspaceDirs(rootDir) {
|
|
2206
|
+
const dirs = [];
|
|
2207
|
+
const rootPkgPath = join2(rootDir, "package.json");
|
|
2208
|
+
if (existsSync2(rootPkgPath)) {
|
|
2209
|
+
try {
|
|
2210
|
+
const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
|
|
2211
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
|
|
2212
|
+
if (Array.isArray(workspaces)) {
|
|
2213
|
+
for (const pattern of workspaces) {
|
|
2214
|
+
dirs.push(...resolveWorkspaceGlob(rootDir, pattern));
|
|
2215
|
+
}
|
|
2216
|
+
return dirs;
|
|
2217
|
+
}
|
|
2218
|
+
} catch {
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
const pnpmWsPath = join2(rootDir, "pnpm-workspace.yaml");
|
|
2222
|
+
if (existsSync2(pnpmWsPath)) {
|
|
2223
|
+
try {
|
|
2224
|
+
const content = readFileSync3(pnpmWsPath, "utf-8");
|
|
2225
|
+
const lines = content.split("\n");
|
|
2226
|
+
let inPackages = false;
|
|
2227
|
+
for (const line of lines) {
|
|
2228
|
+
if (/^packages\s*:/.test(line)) {
|
|
2229
|
+
inPackages = true;
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (inPackages) {
|
|
2233
|
+
const match = line.match(/^\s+-\s+['"]?([^'"#\n]+)['"]?/);
|
|
2234
|
+
if (match) {
|
|
2235
|
+
dirs.push(...resolveWorkspaceGlob(rootDir, match[1].trim()));
|
|
2236
|
+
} else if (/^\S/.test(line) && line.trim()) {
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
} catch {
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return dirs;
|
|
2245
|
+
}
|
|
2246
|
+
function resolveDepPackageJson(localRequire, depName) {
|
|
2247
|
+
try {
|
|
2248
|
+
return localRequire.resolve(`${depName}/package.json`);
|
|
2249
|
+
} catch {
|
|
2250
|
+
}
|
|
2251
|
+
try {
|
|
2252
|
+
const mainPath = localRequire.resolve(depName);
|
|
2253
|
+
let dir = dirname(mainPath);
|
|
2254
|
+
while (true) {
|
|
2255
|
+
const candidate = join2(dir, "package.json");
|
|
2256
|
+
if (existsSync2(candidate)) {
|
|
2257
|
+
const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
|
|
2258
|
+
if (pkg.name === depName) return candidate;
|
|
2259
|
+
}
|
|
2260
|
+
const parent = dirname(dir);
|
|
2261
|
+
if (parent === dir) break;
|
|
2262
|
+
dir = parent;
|
|
2263
|
+
}
|
|
2264
|
+
} catch {
|
|
2265
|
+
}
|
|
2266
|
+
return null;
|
|
2267
|
+
}
|
|
2268
|
+
function findFragmentsInDeps(dir, found, depField) {
|
|
2269
|
+
const pkgJsonPath = join2(dir, "package.json");
|
|
2270
|
+
if (!existsSync2(pkgJsonPath)) return;
|
|
2271
|
+
try {
|
|
2272
|
+
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
|
|
2273
|
+
const allDeps = {
|
|
2274
|
+
...pkgJson.dependencies,
|
|
2275
|
+
...pkgJson.devDependencies
|
|
2276
|
+
};
|
|
2277
|
+
const localRequire = createRequire(join2(dir, "noop.js"));
|
|
2278
|
+
for (const depName of Object.keys(allDeps)) {
|
|
2279
|
+
try {
|
|
2280
|
+
const depPkgPath = resolveDepPackageJson(localRequire, depName);
|
|
2281
|
+
if (!depPkgPath) continue;
|
|
2282
|
+
const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
|
|
2283
|
+
if (depPkg[depField]) {
|
|
2284
|
+
const fragmentsPath = join2(dirname(depPkgPath), depPkg[depField]);
|
|
2285
|
+
if (existsSync2(fragmentsPath) && !found.includes(fragmentsPath)) {
|
|
2286
|
+
found.push(fragmentsPath);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
} catch {
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
} catch {
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function findDesignSystemJson(startDir, outFile, depField) {
|
|
2296
|
+
const found = [];
|
|
2297
|
+
const resolvedStart = resolve(startDir);
|
|
2298
|
+
let dir = resolvedStart;
|
|
2299
|
+
while (true) {
|
|
2300
|
+
const candidate = join2(dir, outFile);
|
|
2301
|
+
if (existsSync2(candidate)) {
|
|
2302
|
+
found.push(candidate);
|
|
2303
|
+
break;
|
|
2304
|
+
}
|
|
2305
|
+
const parent = dirname(dir);
|
|
2306
|
+
if (parent === dir) break;
|
|
2307
|
+
dir = parent;
|
|
2308
|
+
}
|
|
2309
|
+
findFragmentsInDeps(resolvedStart, found, depField);
|
|
2310
|
+
if (found.length === 0 || existsSync2(join2(resolvedStart, "pnpm-workspace.yaml"))) {
|
|
2311
|
+
const workspaceDirs = getWorkspaceDirs(resolvedStart);
|
|
2312
|
+
for (const wsDir of workspaceDirs) {
|
|
2313
|
+
findFragmentsInDeps(wsDir, found, depField);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return found;
|
|
2317
|
+
}
|
|
2318
|
+
function findFragmentsJson(startDir) {
|
|
2319
|
+
return findDesignSystemJson(startDir, BRAND.outFile, "fragments");
|
|
2320
|
+
}
|
|
2321
|
+
function findBundleManifest(startDir) {
|
|
2322
|
+
const found = [];
|
|
2323
|
+
let dir = resolve(startDir);
|
|
2324
|
+
while (true) {
|
|
2325
|
+
const candidate = join2(dir, BRAND.dataDir, BRAND.manifestFile);
|
|
2326
|
+
if (existsSync2(candidate)) {
|
|
2327
|
+
found.push(candidate);
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
const parent = dirname(dir);
|
|
2331
|
+
if (parent === dir) break;
|
|
2332
|
+
dir = parent;
|
|
2333
|
+
}
|
|
2334
|
+
return found;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/adapters/snapshot-converters.ts
|
|
2338
|
+
import { mcpSnapshotSchema } from "@fragments-sdk/core";
|
|
2339
|
+
function slugify(value) {
|
|
2340
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
2341
|
+
}
|
|
2342
|
+
function buildComponentId(args) {
|
|
2343
|
+
const base = args.filePath ?? args.packageName ?? args.name;
|
|
2344
|
+
return `${slugify(args.name)}:${base}`;
|
|
2345
|
+
}
|
|
2346
|
+
function valueToString(value) {
|
|
2347
|
+
if (value === void 0) return void 0;
|
|
2348
|
+
if (typeof value === "string") return value;
|
|
2349
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
2350
|
+
return String(value);
|
|
2351
|
+
}
|
|
2352
|
+
try {
|
|
2353
|
+
return JSON.stringify(value);
|
|
2354
|
+
} catch {
|
|
2355
|
+
return void 0;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
function buildCompoundChildren(contract, ai) {
|
|
2359
|
+
const contractChildren = Object.entries(contract?.compoundChildren ?? {});
|
|
2360
|
+
if (contractChildren.length > 0) {
|
|
2361
|
+
return contractChildren.map(([childName, child]) => ({
|
|
2362
|
+
name: childName,
|
|
2363
|
+
description: child.description,
|
|
2364
|
+
required: child.required,
|
|
2365
|
+
accepts: child.accepts,
|
|
2366
|
+
visibility: "public"
|
|
2367
|
+
}));
|
|
2368
|
+
}
|
|
2369
|
+
const subs = ai?.subComponents;
|
|
2370
|
+
if (!subs || subs.length === 0) return [];
|
|
2371
|
+
const requiredSet = new Set(ai?.requiredChildren ?? []);
|
|
2372
|
+
return subs.map((name) => ({
|
|
2373
|
+
name,
|
|
2374
|
+
required: requiredSet.has(name) || void 0,
|
|
2375
|
+
visibility: "public"
|
|
2376
|
+
}));
|
|
2377
|
+
}
|
|
2378
|
+
function componentFromCompiledFragment(args) {
|
|
2379
|
+
const { fragment, sourceType } = args;
|
|
2380
|
+
const name = fragment.meta.name;
|
|
2381
|
+
const description = fragment.meta.description ?? "";
|
|
2382
|
+
const usage = fragment.usage ?? { when: [], whenNot: [] };
|
|
2383
|
+
const contract = fragment.contract;
|
|
2384
|
+
const ai = fragment.ai;
|
|
2385
|
+
const guidancePatterns = (fragment._cloudPatterns ?? []).map((pattern) => ({
|
|
2386
|
+
name: pattern.name ?? "Pattern",
|
|
2387
|
+
description: pattern.description
|
|
2388
|
+
}));
|
|
2389
|
+
return {
|
|
2390
|
+
id: args.id ?? buildComponentId({
|
|
2391
|
+
name,
|
|
2392
|
+
filePath: fragment.sourcePath ?? fragment.filePath,
|
|
2393
|
+
packageName: args.packageName
|
|
2394
|
+
}),
|
|
2395
|
+
name,
|
|
2396
|
+
description,
|
|
2397
|
+
category: fragment.meta.category ?? "uncategorized",
|
|
2398
|
+
status: fragment.meta.status ?? "stable",
|
|
2399
|
+
tags: fragment.meta.tags ?? [],
|
|
2400
|
+
props: Object.fromEntries(
|
|
2401
|
+
Object.entries(fragment.props ?? {}).map(([propName, prop]) => [
|
|
2402
|
+
propName,
|
|
2403
|
+
{
|
|
2404
|
+
type: prop.type,
|
|
2405
|
+
description: prop.description ?? "",
|
|
2406
|
+
required: prop.required ?? false,
|
|
2407
|
+
default: prop.default,
|
|
2408
|
+
values: prop.values ? [...prop.values] : void 0,
|
|
2409
|
+
constraints: prop.constraints ? [...prop.constraints] : void 0
|
|
2410
|
+
}
|
|
2411
|
+
])
|
|
2412
|
+
),
|
|
2413
|
+
propsSummary: fragment.propsSummary ?? contract?.propsSummary ?? Object.entries(fragment.props ?? {}).map(
|
|
2414
|
+
([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
|
|
2415
|
+
),
|
|
2416
|
+
examples: (fragment.variants ?? []).map((variant) => ({
|
|
2417
|
+
name: variant.name,
|
|
2418
|
+
description: variant.description,
|
|
2419
|
+
code: variant.code,
|
|
2420
|
+
kind: "example"
|
|
2421
|
+
})),
|
|
2422
|
+
relations: (fragment.relations ?? []).map((relation) => ({
|
|
2423
|
+
componentName: relation.component,
|
|
2424
|
+
relationship: relation.relationship,
|
|
2425
|
+
note: relation.note
|
|
2426
|
+
})),
|
|
2427
|
+
compoundChildren: buildCompoundChildren(contract, ai),
|
|
2428
|
+
guidance: {
|
|
2429
|
+
when: usage.when ?? [],
|
|
2430
|
+
whenNot: usage.whenNot ?? [],
|
|
2431
|
+
guidelines: usage.guidelines ?? [],
|
|
2432
|
+
accessibility: usage.accessibility ?? [],
|
|
2433
|
+
dos: usage.when ?? [],
|
|
2434
|
+
donts: usage.whenNot ?? [],
|
|
2435
|
+
patterns: guidancePatterns.length > 0 ? guidancePatterns : (ai?.commonPatterns ?? []).map((pattern) => ({
|
|
2436
|
+
name: pattern
|
|
2437
|
+
}))
|
|
2438
|
+
},
|
|
2439
|
+
sourceType,
|
|
2440
|
+
sourcePath: fragment.sourcePath ?? fragment.filePath,
|
|
2441
|
+
packageName: args.packageName,
|
|
2442
|
+
importPath: args.importPath ?? args.packageName,
|
|
2443
|
+
performance: fragment.performance ? {
|
|
2444
|
+
bundleSize: fragment.performance.bundleSize,
|
|
2445
|
+
rawSize: fragment.performance.rawSize,
|
|
2446
|
+
complexity: fragment.performance.complexity,
|
|
2447
|
+
budgetPercent: fragment.performance.budgetPercent,
|
|
2448
|
+
overBudget: fragment.performance.overBudget,
|
|
2449
|
+
measuredAt: fragment.performance.measuredAt,
|
|
2450
|
+
imports: fragment.performance.imports?.map((entry) => ({
|
|
2451
|
+
path: entry.path,
|
|
2452
|
+
bytes: entry.bytes,
|
|
2453
|
+
percent: entry.percent
|
|
2454
|
+
}))
|
|
2455
|
+
} : void 0,
|
|
2456
|
+
metadata: {
|
|
2457
|
+
a11yRules: contract?.a11yRules ?? [],
|
|
2458
|
+
scenarioTags: contract?.scenarioTags ?? []
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
function blockFromCompiledBlock(key, block) {
|
|
2463
|
+
return {
|
|
2464
|
+
id: key,
|
|
2465
|
+
name: block.name,
|
|
2466
|
+
description: block.description ?? "",
|
|
2467
|
+
category: block.category ?? "uncategorized",
|
|
2468
|
+
components: block.components ?? [],
|
|
2469
|
+
tags: block.tags ?? [],
|
|
2470
|
+
code: block.code ?? ""
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
function tokensFromCompiledTokenData(tokens) {
|
|
2474
|
+
const flat = [];
|
|
2475
|
+
const categories = {};
|
|
2476
|
+
for (const [category, entries] of Object.entries(tokens.categories)) {
|
|
2477
|
+
const normalized = entries.map((entry) => ({
|
|
2478
|
+
name: entry.name,
|
|
2479
|
+
category,
|
|
2480
|
+
value: valueToString(entry.value),
|
|
2481
|
+
description: entry.description
|
|
2482
|
+
})).filter((token) => !isGarbageToken(token));
|
|
2483
|
+
if (normalized.length > 0) {
|
|
2484
|
+
categories[category] = normalized;
|
|
2485
|
+
flat.push(...normalized);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return {
|
|
2489
|
+
prefix: tokens.prefix,
|
|
2490
|
+
total: flat.length,
|
|
2491
|
+
categories,
|
|
2492
|
+
flat
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
function buildCapabilities(args) {
|
|
2496
|
+
const capabilities = /* @__PURE__ */ new Set();
|
|
2497
|
+
if (Object.keys(args.components).length > 0) {
|
|
2498
|
+
capabilities.add("components");
|
|
2499
|
+
}
|
|
2500
|
+
if (args.tokens && args.tokens.total > 0) {
|
|
2501
|
+
capabilities.add("tokens");
|
|
2502
|
+
}
|
|
2503
|
+
if (args.blocks && Object.keys(args.blocks).length > 0) {
|
|
2504
|
+
capabilities.add("blocks");
|
|
2505
|
+
}
|
|
2506
|
+
if (args.graph) {
|
|
2507
|
+
capabilities.add("graph");
|
|
2508
|
+
}
|
|
2509
|
+
const hasPerformance = Boolean(args.performanceSummary) || Object.values(args.components).some((component) => Boolean(component.performance));
|
|
2510
|
+
if (hasPerformance) {
|
|
2511
|
+
capabilities.add("performance");
|
|
2512
|
+
}
|
|
2513
|
+
return Array.from(capabilities);
|
|
2514
|
+
}
|
|
2515
|
+
function validateSnapshot(snapshot) {
|
|
2516
|
+
return mcpSnapshotSchema.parse(snapshot);
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// src/adapters/fragments-json.ts
|
|
2520
|
+
var FragmentsJsonAdapter = class {
|
|
2521
|
+
name = "fragments-json";
|
|
2522
|
+
discover(startDir) {
|
|
2523
|
+
return findFragmentsJson(startDir);
|
|
2524
|
+
}
|
|
2525
|
+
async load(projectRoot) {
|
|
2526
|
+
const paths = this.discover(projectRoot);
|
|
2527
|
+
if (paths.length === 0) {
|
|
2528
|
+
throw new Error(
|
|
2529
|
+
`No ${BRAND.outFile} found. Searched ${projectRoot} and package.json dependencies.
|
|
2530
|
+
|
|
2531
|
+
Fix: Add a project-level MCP config so the server runs from your workspace root:
|
|
2532
|
+
|
|
2533
|
+
Cursor: .cursor/mcp.json
|
|
2534
|
+
VS Code: .vscode/mcp.json
|
|
2535
|
+
Claude: claude mcp add ${BRAND.nameLower} -- npx @fragments-sdk/mcp
|
|
2536
|
+
Windsurf: .windsurf/mcp.json
|
|
2537
|
+
|
|
2538
|
+
Or pass --project-root: npx @fragments-sdk/mcp -p /path/to/project
|
|
2539
|
+
|
|
2540
|
+
If you're a library author, run \`${BRAND.cliCommand} build\` first.`
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
const content = await readFile(paths[0], "utf-8");
|
|
2544
|
+
const primary = JSON.parse(content);
|
|
2545
|
+
if (!primary.blocks && primary.recipes) {
|
|
2546
|
+
primary.blocks = primary.recipes;
|
|
2547
|
+
}
|
|
2548
|
+
const packageMap = {};
|
|
2549
|
+
if (primary.packageName) {
|
|
2550
|
+
for (const name of Object.keys(primary.fragments)) {
|
|
2551
|
+
packageMap[name] = primary.packageName;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
for (let i = 1; i < paths.length; i++) {
|
|
2555
|
+
const extra = JSON.parse(await readFile(paths[i], "utf-8"));
|
|
2556
|
+
if (extra.packageName) {
|
|
2557
|
+
for (const name of Object.keys(extra.fragments)) {
|
|
2558
|
+
packageMap[name] = extra.packageName;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
Object.assign(primary.fragments, extra.fragments);
|
|
2562
|
+
const extraBlocks = extra.blocks ?? extra.recipes;
|
|
2563
|
+
if (extraBlocks) {
|
|
2564
|
+
primary.blocks = { ...primary.blocks, ...extraBlocks };
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
const components = Object.fromEntries(
|
|
2568
|
+
Object.entries(primary.fragments).map(([key, fragment]) => [
|
|
2569
|
+
key,
|
|
2570
|
+
componentFromCompiledFragment({
|
|
2571
|
+
id: key,
|
|
2572
|
+
fragment,
|
|
2573
|
+
sourceType: "fragments-json",
|
|
2574
|
+
packageName: packageMap[fragment.meta.name] ?? primary.packageName,
|
|
2575
|
+
importPath: packageMap[fragment.meta.name] ?? primary.packageName
|
|
2576
|
+
})
|
|
2577
|
+
])
|
|
2578
|
+
);
|
|
2579
|
+
const blocks = primary.blocks ? Object.fromEntries(
|
|
2580
|
+
Object.entries(primary.blocks).map(([key, block]) => [
|
|
2581
|
+
key,
|
|
2582
|
+
blockFromCompiledBlock(key, block)
|
|
2583
|
+
])
|
|
2584
|
+
) : void 0;
|
|
2585
|
+
const tokens = primary.tokens ? tokensFromCompiledTokenData(primary.tokens) : void 0;
|
|
2586
|
+
const snapshot = validateSnapshot({
|
|
2587
|
+
schemaVersion: 1,
|
|
2588
|
+
sourceType: "fragments-json",
|
|
2589
|
+
sourceLabel: BRAND.outFile,
|
|
2590
|
+
capabilities: buildCapabilities({
|
|
2591
|
+
components,
|
|
2592
|
+
blocks,
|
|
2593
|
+
tokens,
|
|
2594
|
+
graph: primary.graph,
|
|
2595
|
+
performanceSummary: primary.performanceSummary
|
|
2596
|
+
}),
|
|
2597
|
+
metadata: {
|
|
2598
|
+
designSystemName: primary.packageName,
|
|
2599
|
+
packageName: primary.packageName,
|
|
2600
|
+
importPath: primary.packageName
|
|
2601
|
+
},
|
|
2602
|
+
components,
|
|
2603
|
+
blocks,
|
|
2604
|
+
tokens,
|
|
2605
|
+
graph: primary.graph,
|
|
2606
|
+
performanceSummary: primary.performanceSummary,
|
|
2607
|
+
packageMap,
|
|
2608
|
+
defaultPackageName: primary.packageName
|
|
2609
|
+
});
|
|
2610
|
+
return {
|
|
2611
|
+
snapshot,
|
|
2612
|
+
components: snapshot.components,
|
|
2613
|
+
blocks: snapshot.blocks,
|
|
2614
|
+
tokens: snapshot.tokens,
|
|
2615
|
+
graph: primary.graph,
|
|
2616
|
+
performanceSummary: primary.performanceSummary,
|
|
2617
|
+
packageMap: snapshot.packageMap,
|
|
2618
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
2619
|
+
capabilities: new Set(snapshot.capabilities)
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
// src/adapters/auto-extract.ts
|
|
2625
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
2626
|
+
import { join as join5, relative, sep } from "path";
|
|
2627
|
+
|
|
2628
|
+
// src/adapters/discover-components.ts
|
|
2629
|
+
import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
|
|
2630
|
+
import { join as join3, extname, basename } from "path";
|
|
2631
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
2632
|
+
"node_modules",
|
|
2633
|
+
"dist",
|
|
2634
|
+
"build",
|
|
2635
|
+
".next",
|
|
2636
|
+
".nuxt",
|
|
2637
|
+
"coverage",
|
|
2638
|
+
"__tests__",
|
|
2639
|
+
"__mocks__",
|
|
2640
|
+
".git",
|
|
2641
|
+
".cache",
|
|
2642
|
+
".turbo",
|
|
2643
|
+
"out"
|
|
2644
|
+
]);
|
|
2645
|
+
var EXCLUDED_PATTERNS = [
|
|
2646
|
+
/\.test\./,
|
|
2647
|
+
/\.spec\./,
|
|
2648
|
+
/\.stories\./,
|
|
2649
|
+
/\.story\./,
|
|
2650
|
+
/\.fragment\./,
|
|
2651
|
+
/\.d\.ts$/,
|
|
2652
|
+
/\.config\./,
|
|
2653
|
+
/\.mock\./,
|
|
2654
|
+
/\.fixture\./
|
|
2655
|
+
];
|
|
2656
|
+
function discoverComponentFiles(projectRoot) {
|
|
2657
|
+
const results = [];
|
|
2658
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2659
|
+
const scanDirs = [
|
|
2660
|
+
"src/components",
|
|
2661
|
+
"components",
|
|
2662
|
+
"lib/components",
|
|
2663
|
+
"src/ui",
|
|
2664
|
+
"lib/ui",
|
|
2665
|
+
"packages"
|
|
2666
|
+
].map((d) => join3(projectRoot, d)).filter((d) => existsSync3(d));
|
|
2667
|
+
if (scanDirs.length === 0) {
|
|
2668
|
+
const srcDir = join3(projectRoot, "src");
|
|
2669
|
+
if (existsSync3(srcDir)) scanDirs.push(srcDir);
|
|
2670
|
+
}
|
|
2671
|
+
for (const dir of scanDirs) {
|
|
2672
|
+
walkDir(dir, results, seen);
|
|
2673
|
+
}
|
|
2674
|
+
return results;
|
|
2675
|
+
}
|
|
2676
|
+
function walkDir(dir, results, seen, depth = 0) {
|
|
2677
|
+
if (depth > 6) return;
|
|
2678
|
+
let entries;
|
|
2679
|
+
try {
|
|
2680
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
2681
|
+
} catch {
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
for (const entry of entries) {
|
|
2685
|
+
if (entry.name.startsWith(".")) continue;
|
|
2686
|
+
if (entry.isDirectory()) {
|
|
2687
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
2688
|
+
walkDir(join3(dir, entry.name), results, seen, depth + 1);
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
if (!entry.isFile()) continue;
|
|
2692
|
+
const ext = extname(entry.name);
|
|
2693
|
+
if (ext !== ".tsx" && ext !== ".jsx") continue;
|
|
2694
|
+
if (EXCLUDED_PATTERNS.some((p) => p.test(entry.name))) continue;
|
|
2695
|
+
const filePath = join3(dir, entry.name);
|
|
2696
|
+
if (seen.has(filePath)) continue;
|
|
2697
|
+
seen.add(filePath);
|
|
2698
|
+
const name = inferComponentName(entry.name, dir);
|
|
2699
|
+
if (name) {
|
|
2700
|
+
results.push({ filePath, componentName: name });
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function inferComponentName(fileName, dirPath) {
|
|
2705
|
+
const withoutExt = fileName.replace(/\.(tsx|jsx)$/, "");
|
|
2706
|
+
if (withoutExt === "index") {
|
|
2707
|
+
return basename(dirPath);
|
|
2708
|
+
}
|
|
2709
|
+
return withoutExt;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// src/adapters/scan-tokens.ts
|
|
2713
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
2714
|
+
import { join as join4, extname as extname2 } from "path";
|
|
2715
|
+
function scanTokens(projectRoot) {
|
|
2716
|
+
const cssFiles = discoverCssFiles(projectRoot);
|
|
2717
|
+
if (cssFiles.length === 0) return void 0;
|
|
2718
|
+
const allTokens = [];
|
|
2719
|
+
let prefix = "";
|
|
2720
|
+
for (const filePath of cssFiles) {
|
|
2721
|
+
try {
|
|
2722
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
2723
|
+
const tokens = extractCustomProperties(content);
|
|
2724
|
+
allTokens.push(...tokens);
|
|
2725
|
+
} catch {
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
if (allTokens.length === 0) return void 0;
|
|
2730
|
+
prefix = detectPrefix(allTokens.map((t) => t.name));
|
|
2731
|
+
const categories = {};
|
|
2732
|
+
for (const token of allTokens) {
|
|
2733
|
+
const category = inferTokenCategory(token.name);
|
|
2734
|
+
if (!categories[category]) categories[category] = [];
|
|
2735
|
+
if (!categories[category].some((t) => t.name === token.name)) {
|
|
2736
|
+
categories[category].push(token);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return {
|
|
2740
|
+
prefix,
|
|
2741
|
+
total: Object.values(categories).reduce((sum, arr) => sum + arr.length, 0),
|
|
2742
|
+
categories
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
function discoverCssFiles(projectRoot) {
|
|
2746
|
+
const files = [];
|
|
2747
|
+
const searchDirs = [
|
|
2748
|
+
"src",
|
|
2749
|
+
"styles",
|
|
2750
|
+
"css",
|
|
2751
|
+
"app"
|
|
2752
|
+
].map((d) => join4(projectRoot, d)).filter((d) => existsSync4(d));
|
|
2753
|
+
searchDirs.push(projectRoot);
|
|
2754
|
+
for (const dir of searchDirs) {
|
|
2755
|
+
try {
|
|
2756
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
2757
|
+
for (const entry of entries) {
|
|
2758
|
+
if (!entry.isFile()) continue;
|
|
2759
|
+
const ext = extname2(entry.name);
|
|
2760
|
+
if (ext === ".css" || ext === ".scss") {
|
|
2761
|
+
files.push(join4(dir, entry.name));
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
} catch {
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const srcDir = join4(projectRoot, "src");
|
|
2769
|
+
if (existsSync4(srcDir)) {
|
|
2770
|
+
try {
|
|
2771
|
+
for (const subEntry of readdirSync3(srcDir, { withFileTypes: true })) {
|
|
2772
|
+
if (subEntry.isDirectory() && ["styles", "css", "theme", "tokens"].includes(subEntry.name)) {
|
|
2773
|
+
const subDir = join4(srcDir, subEntry.name);
|
|
2774
|
+
for (const file of readdirSync3(subDir, { withFileTypes: true })) {
|
|
2775
|
+
if (file.isFile() && (file.name.endsWith(".css") || file.name.endsWith(".scss"))) {
|
|
2776
|
+
files.push(join4(subDir, file.name));
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
} catch {
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return [...new Set(files)];
|
|
2785
|
+
}
|
|
2786
|
+
function extractCustomProperties(content) {
|
|
2787
|
+
const tokens = [];
|
|
2788
|
+
let inRelevantBlock = false;
|
|
2789
|
+
let braceDepth = 0;
|
|
2790
|
+
const relevantSelectors = [":root", ".dark", "[data-theme", "@theme"];
|
|
2791
|
+
const lines = content.split("\n");
|
|
2792
|
+
for (const line of lines) {
|
|
2793
|
+
const trimmed = line.trim();
|
|
2794
|
+
if (!inRelevantBlock && braceDepth === 0) {
|
|
2795
|
+
if (relevantSelectors.some((sel) => trimmed.startsWith(sel) || trimmed.includes(sel))) {
|
|
2796
|
+
if (trimmed.includes("{")) {
|
|
2797
|
+
inRelevantBlock = true;
|
|
2798
|
+
braceDepth = 1;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
if (inRelevantBlock) {
|
|
2803
|
+
for (const ch of trimmed) {
|
|
2804
|
+
if (ch === "{") braceDepth++;
|
|
2805
|
+
else if (ch === "}") {
|
|
2806
|
+
braceDepth--;
|
|
2807
|
+
if (braceDepth <= 0) {
|
|
2808
|
+
inRelevantBlock = false;
|
|
2809
|
+
braceDepth = 0;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
if (!inRelevantBlock && braceDepth === 0) continue;
|
|
2815
|
+
const match = trimmed.match(/^(--[\w-]+)\s*:\s*(.+?)\s*;/);
|
|
2816
|
+
if (match) {
|
|
2817
|
+
const [, name, value] = match;
|
|
2818
|
+
const commentMatch = value.match(/\/\*\s*(.+?)\s*\*\//);
|
|
2819
|
+
const cleanValue = value.replace(/\/\*.*?\*\//, "").trim();
|
|
2820
|
+
tokens.push({
|
|
2821
|
+
name,
|
|
2822
|
+
value: cleanValue,
|
|
2823
|
+
description: commentMatch?.[1]
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
return tokens;
|
|
2828
|
+
}
|
|
2829
|
+
function detectPrefix(names) {
|
|
2830
|
+
if (names.length === 0) return "--";
|
|
2831
|
+
const stripped = names.map((n) => n.slice(2));
|
|
2832
|
+
let prefix = "";
|
|
2833
|
+
const first = stripped[0];
|
|
2834
|
+
for (let i = 0; i < first.length; i++) {
|
|
2835
|
+
const ch = first[i];
|
|
2836
|
+
if (stripped.every((n) => n[i] === ch)) {
|
|
2837
|
+
prefix += ch;
|
|
2838
|
+
} else {
|
|
2839
|
+
break;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
const lastDash = prefix.lastIndexOf("-");
|
|
2843
|
+
if (lastDash > 0) {
|
|
2844
|
+
return "--" + prefix.slice(0, lastDash + 1);
|
|
2845
|
+
}
|
|
2846
|
+
return "--";
|
|
2847
|
+
}
|
|
2848
|
+
function inferTokenCategory(name) {
|
|
2849
|
+
const n = name.toLowerCase();
|
|
2850
|
+
if (n.includes("color") || n.includes("background") || n.includes("foreground") || n.includes("primary") || n.includes("secondary") || n.includes("accent") || n.includes("muted") || n.includes("destructive") || n.includes("popover") || n.includes("card") && !n.includes("card-") || n.includes("chart")) {
|
|
2851
|
+
return "colors";
|
|
2852
|
+
}
|
|
2853
|
+
if (n.includes("font") || n.includes("text") || n.includes("letter") || n.includes("line-height")) {
|
|
2854
|
+
return "typography";
|
|
2855
|
+
}
|
|
2856
|
+
if (n.includes("space") || n.includes("gap") || n.includes("padding") || n.includes("margin")) {
|
|
2857
|
+
return "spacing";
|
|
2858
|
+
}
|
|
2859
|
+
if (n.includes("radius")) {
|
|
2860
|
+
return "radius";
|
|
2861
|
+
}
|
|
2862
|
+
if (n.includes("shadow")) {
|
|
2863
|
+
return "shadows";
|
|
2864
|
+
}
|
|
2865
|
+
if (n.includes("border")) {
|
|
2866
|
+
return "borders";
|
|
2867
|
+
}
|
|
2868
|
+
if (n.includes("ring")) {
|
|
2869
|
+
return "focus";
|
|
2870
|
+
}
|
|
2871
|
+
if (n.includes("sidebar")) {
|
|
2872
|
+
return "sidebar";
|
|
2873
|
+
}
|
|
2874
|
+
if (n.includes("input")) {
|
|
2875
|
+
return "forms";
|
|
2876
|
+
}
|
|
2877
|
+
return "other";
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// src/adapters/auto-extract.ts
|
|
2881
|
+
var AutoExtractionAdapter = class {
|
|
2882
|
+
name = "auto-extract";
|
|
2883
|
+
discover(startDir) {
|
|
2884
|
+
return discoverComponentFiles(startDir).map((f) => f.filePath);
|
|
2885
|
+
}
|
|
2886
|
+
async load(projectRoot) {
|
|
2887
|
+
let extractMod;
|
|
2888
|
+
try {
|
|
2889
|
+
extractMod = await loadExtractorModule();
|
|
2890
|
+
} catch (e) {
|
|
2891
|
+
throw new Error(
|
|
2892
|
+
"Auto-extraction requires @fragments-sdk/extract and TypeScript.\n\nIf you see this error, the MCP server may not be installed correctly.\nAlternative: pre-build your design system with `npx fragments build`"
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
const tsconfigPath = findTsConfig(projectRoot);
|
|
2896
|
+
const extractor = extractMod.createComponentExtractor(tsconfigPath ?? void 0);
|
|
2897
|
+
try {
|
|
2898
|
+
const discovered = discoverComponentFiles(projectRoot);
|
|
2899
|
+
if (discovered.length === 0) {
|
|
2900
|
+
throw new Error(
|
|
2901
|
+
`No component files found in ${projectRoot}.
|
|
2902
|
+
Searched: src/components/, components/, lib/components/, src/ui/
|
|
2903
|
+
|
|
2904
|
+
If your components are elsewhere, create a fragments.json with:
|
|
2905
|
+
npx fragments build`
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
const components = {};
|
|
2909
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
2910
|
+
const defaultPackageName = readPackageName(projectRoot);
|
|
2911
|
+
let extractedCount = 0;
|
|
2912
|
+
const fileToComponents = /* @__PURE__ */ new Map();
|
|
2913
|
+
for (const { filePath, componentName } of discovered) {
|
|
2914
|
+
try {
|
|
2915
|
+
const metas = extractor.extractAll(filePath);
|
|
2916
|
+
for (const meta of metas) {
|
|
2917
|
+
const fragment = mapToCompiledFragment(meta, filePath, projectRoot);
|
|
2918
|
+
components[meta.name] = fragment;
|
|
2919
|
+
if (defaultPackageName) {
|
|
2920
|
+
packageMap.set(meta.name, defaultPackageName);
|
|
2921
|
+
}
|
|
2922
|
+
extractedCount++;
|
|
2923
|
+
const relPath = relative(projectRoot, filePath);
|
|
2924
|
+
if (!fileToComponents.has(relPath)) fileToComponents.set(relPath, []);
|
|
2925
|
+
fileToComponents.get(relPath).push(meta.name);
|
|
2926
|
+
}
|
|
2927
|
+
if (metas.length === 0) {
|
|
2928
|
+
const meta = extractor.extract(filePath, componentName);
|
|
2929
|
+
if (meta) {
|
|
2930
|
+
const fragment = mapToCompiledFragment(meta, filePath, projectRoot);
|
|
2931
|
+
components[meta.name] = fragment;
|
|
2932
|
+
if (defaultPackageName) {
|
|
2933
|
+
packageMap.set(meta.name, defaultPackageName);
|
|
2934
|
+
}
|
|
2935
|
+
extractedCount++;
|
|
2936
|
+
const relPath = relative(projectRoot, filePath);
|
|
2937
|
+
if (!fileToComponents.has(relPath)) fileToComponents.set(relPath, []);
|
|
2938
|
+
fileToComponents.get(relPath).push(meta.name);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
} catch {
|
|
2942
|
+
continue;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
inferRelations(components, fileToComponents);
|
|
2946
|
+
console.error(`[fragments-mcp] Extracted ${extractedCount} components from ${discovered.length} files.`);
|
|
2947
|
+
if (extractedCount === 0) {
|
|
2948
|
+
throw new Error(
|
|
2949
|
+
`Found ${discovered.length} component files but could not extract any props.
|
|
2950
|
+
This usually means TypeScript cannot parse the files.
|
|
2951
|
+
Check that your tsconfig.json includes the component directories.`
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
const tokens = scanTokens(projectRoot);
|
|
2955
|
+
if (tokens) {
|
|
2956
|
+
console.error(`[fragments-mcp] Found ${tokens.total} design tokens across ${Object.keys(tokens.categories).length} categories.`);
|
|
2957
|
+
}
|
|
2958
|
+
const snapshotComponents = Object.fromEntries(
|
|
2959
|
+
Object.entries(components).map(([key, fragment]) => [
|
|
2960
|
+
key,
|
|
2961
|
+
componentFromCompiledFragment({
|
|
2962
|
+
id: key,
|
|
2963
|
+
fragment,
|
|
2964
|
+
sourceType: "auto-extract",
|
|
2965
|
+
packageName: packageMap.get(fragment.meta.name) ?? defaultPackageName,
|
|
2966
|
+
importPath: packageMap.get(fragment.meta.name) ?? defaultPackageName
|
|
2967
|
+
})
|
|
2968
|
+
])
|
|
2969
|
+
);
|
|
2970
|
+
const snapshotTokens = tokens ? tokensFromCompiledTokenData(tokens) : void 0;
|
|
2971
|
+
const packageMapRecord = Object.fromEntries(packageMap.entries());
|
|
2972
|
+
const snapshot = validateSnapshot({
|
|
2973
|
+
schemaVersion: 1,
|
|
2974
|
+
sourceType: "auto-extract",
|
|
2975
|
+
sourceLabel: "Auto-extracted source files",
|
|
2976
|
+
capabilities: buildCapabilities({
|
|
2977
|
+
components: snapshotComponents,
|
|
2978
|
+
tokens: snapshotTokens
|
|
2979
|
+
}),
|
|
2980
|
+
metadata: {
|
|
2981
|
+
packageName: defaultPackageName,
|
|
2982
|
+
importPath: defaultPackageName
|
|
2983
|
+
},
|
|
2984
|
+
components: snapshotComponents,
|
|
2985
|
+
tokens: snapshotTokens,
|
|
2986
|
+
packageMap: packageMapRecord,
|
|
2987
|
+
defaultPackageName
|
|
2988
|
+
});
|
|
2989
|
+
return {
|
|
2990
|
+
snapshot,
|
|
2991
|
+
components: snapshot.components,
|
|
2992
|
+
blocks: snapshot.blocks,
|
|
2993
|
+
tokens: snapshot.tokens,
|
|
2994
|
+
graph: void 0,
|
|
2995
|
+
performanceSummary: void 0,
|
|
2996
|
+
packageMap: snapshot.packageMap,
|
|
2997
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
2998
|
+
capabilities: new Set(snapshot.capabilities)
|
|
2999
|
+
};
|
|
3000
|
+
} finally {
|
|
3001
|
+
extractor.dispose();
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
var extractorModulePromise = null;
|
|
3006
|
+
async function loadExtractorModule() {
|
|
3007
|
+
if (!extractorModulePromise) {
|
|
3008
|
+
extractorModulePromise = import("./dist-TTCI6TME.js");
|
|
3009
|
+
}
|
|
3010
|
+
return extractorModulePromise;
|
|
3011
|
+
}
|
|
3012
|
+
var UNIVERSAL_INHERITED = /* @__PURE__ */ new Set(["children", "className", "id", "disabled"]);
|
|
3013
|
+
var FORM_INPUT_INHERITED = /* @__PURE__ */ new Set([
|
|
3014
|
+
"placeholder",
|
|
3015
|
+
"value",
|
|
3016
|
+
"defaultValue",
|
|
3017
|
+
"onChange",
|
|
3018
|
+
"name",
|
|
3019
|
+
"required",
|
|
3020
|
+
"autoFocus",
|
|
3021
|
+
"autoComplete",
|
|
3022
|
+
"checked",
|
|
3023
|
+
"defaultChecked",
|
|
3024
|
+
"type"
|
|
3025
|
+
]);
|
|
3026
|
+
var LABEL_INHERITED = /* @__PURE__ */ new Set(["htmlFor"]);
|
|
3027
|
+
var INPUT_LIKE_NAMES = /input|select|textarea|checkbox|radio|switch|slider|combobox/i;
|
|
3028
|
+
var LABEL_LIKE_NAMES = /label/i;
|
|
3029
|
+
function getRelevantInheritedProps(componentName) {
|
|
3030
|
+
const allowed = new Set(UNIVERSAL_INHERITED);
|
|
3031
|
+
if (INPUT_LIKE_NAMES.test(componentName)) {
|
|
3032
|
+
for (const p of FORM_INPUT_INHERITED) allowed.add(p);
|
|
3033
|
+
}
|
|
3034
|
+
if (LABEL_LIKE_NAMES.test(componentName)) {
|
|
3035
|
+
for (const p of LABEL_INHERITED) allowed.add(p);
|
|
3036
|
+
}
|
|
3037
|
+
return allowed;
|
|
3038
|
+
}
|
|
3039
|
+
function mapToCompiledFragment(meta, filePath, projectRoot) {
|
|
3040
|
+
const relevantInherited = getRelevantInheritedProps(meta.name);
|
|
3041
|
+
const props = {};
|
|
3042
|
+
for (const [name, propMeta] of Object.entries(meta.props)) {
|
|
3043
|
+
if (propMeta.source === "inherited" && !relevantInherited.has(name)) continue;
|
|
3044
|
+
props[name] = {
|
|
3045
|
+
type: propMeta.type,
|
|
3046
|
+
description: propMeta.description ?? "",
|
|
3047
|
+
required: propMeta.required,
|
|
3048
|
+
...propMeta.values && { values: propMeta.values },
|
|
3049
|
+
...propMeta.default !== void 0 && { default: propMeta.default }
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
const relativePath = relative(projectRoot, filePath);
|
|
3053
|
+
const category = inferCategory(relativePath);
|
|
3054
|
+
const importPath = buildImportPath(relativePath);
|
|
3055
|
+
const description = buildDescription(meta, props);
|
|
3056
|
+
const propsSummary = buildPropsSummary(props);
|
|
3057
|
+
const codeExample = buildCodeExample(meta.name, props);
|
|
3058
|
+
const fragmentMeta = {
|
|
3059
|
+
name: meta.name,
|
|
3060
|
+
description,
|
|
3061
|
+
category
|
|
3062
|
+
};
|
|
3063
|
+
return {
|
|
3064
|
+
filePath: relativePath,
|
|
3065
|
+
meta: fragmentMeta,
|
|
3066
|
+
usage: {
|
|
3067
|
+
when: [`Use ${meta.name} when you need a ${category} ${meta.name.toLowerCase()} element.`],
|
|
3068
|
+
whenNot: [],
|
|
3069
|
+
guidelines: importPath ? [`import { ${meta.name} } from "${importPath}"`] : []
|
|
3070
|
+
},
|
|
3071
|
+
props,
|
|
3072
|
+
propsSummary,
|
|
3073
|
+
variants: codeExample ? [{ name: "Default", description: `Basic ${meta.name} usage`, code: codeExample }] : [],
|
|
3074
|
+
sourcePath: relativePath,
|
|
3075
|
+
exportName: meta.name,
|
|
3076
|
+
_generated: {
|
|
3077
|
+
source: "extracted",
|
|
3078
|
+
verified: false,
|
|
3079
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3080
|
+
},
|
|
3081
|
+
...meta.composition && {
|
|
3082
|
+
ai: {
|
|
3083
|
+
compositionPattern: meta.composition.pattern
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
function buildImportPath(relativePath) {
|
|
3089
|
+
const withoutExt = relativePath.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
3090
|
+
if (withoutExt.startsWith(`src${sep}`)) {
|
|
3091
|
+
return "@/" + withoutExt.slice(4).split(sep).join("/");
|
|
3092
|
+
}
|
|
3093
|
+
return "./" + withoutExt.split(sep).join("/");
|
|
3094
|
+
}
|
|
3095
|
+
function buildDescription(meta, localProps) {
|
|
3096
|
+
if (meta.description && meta.description !== meta.name && !meta.description.endsWith(" component")) {
|
|
3097
|
+
return meta.description;
|
|
3098
|
+
}
|
|
3099
|
+
const propEntries = Object.entries(localProps);
|
|
3100
|
+
if (propEntries.length === 0) {
|
|
3101
|
+
return `${meta.name} component`;
|
|
3102
|
+
}
|
|
3103
|
+
const details = [];
|
|
3104
|
+
for (const [name, prop] of propEntries) {
|
|
3105
|
+
if (prop.values && prop.values.length > 0) {
|
|
3106
|
+
details.push(`${prop.values.length} ${name} options`);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
if (details.length > 0) {
|
|
3110
|
+
return `${meta.name} component with ${details.join(" and ")}.`;
|
|
3111
|
+
}
|
|
3112
|
+
return `${meta.name} component with ${propEntries.length} configurable prop${propEntries.length === 1 ? "" : "s"}.`;
|
|
3113
|
+
}
|
|
3114
|
+
function buildPropsSummary(props) {
|
|
3115
|
+
const summaries = [];
|
|
3116
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
3117
|
+
let summary = `${name}`;
|
|
3118
|
+
if (prop.values && prop.values.length > 0) {
|
|
3119
|
+
summary += `: ${prop.values.map((v) => `"${v}"`).join(" | ")}`;
|
|
3120
|
+
} else {
|
|
3121
|
+
summary += `: ${prop.type}`;
|
|
3122
|
+
}
|
|
3123
|
+
if (prop.default !== void 0) {
|
|
3124
|
+
summary += ` (default: ${JSON.stringify(prop.default)})`;
|
|
3125
|
+
}
|
|
3126
|
+
if (prop.required) {
|
|
3127
|
+
summary += " (required)";
|
|
3128
|
+
}
|
|
3129
|
+
summaries.push(summary);
|
|
3130
|
+
}
|
|
3131
|
+
return summaries;
|
|
3132
|
+
}
|
|
3133
|
+
function buildCodeExample(name, props) {
|
|
3134
|
+
const propsStr = [];
|
|
3135
|
+
for (const [pName, prop] of Object.entries(props)) {
|
|
3136
|
+
if (pName === "children" || pName === "asChild" || pName === "className") continue;
|
|
3137
|
+
if (pName === "onChange" || pName === "onSubmit" || pName === "value" || pName === "defaultValue" || pName === "checked" || pName === "defaultChecked" || pName === "autoFocus" || pName === "autoComplete" || pName === "required" || pName === "disabled" || pName === "name") continue;
|
|
3138
|
+
if (prop.default !== void 0) {
|
|
3139
|
+
if (prop.values && prop.values.length > 1) {
|
|
3140
|
+
const nonDefault = prop.values.find((v) => v !== String(prop.default));
|
|
3141
|
+
if (nonDefault) {
|
|
3142
|
+
propsStr.push(`${pName}="${nonDefault}"`);
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
if (pName === "placeholder" && INPUT_LIKE_NAMES.test(name)) {
|
|
3148
|
+
propsStr.push(`placeholder="Enter ${name.toLowerCase()}..."`);
|
|
3149
|
+
continue;
|
|
3150
|
+
}
|
|
3151
|
+
if (pName === "htmlFor" && LABEL_LIKE_NAMES.test(name)) {
|
|
3152
|
+
propsStr.push(`htmlFor="field-id"`);
|
|
3153
|
+
continue;
|
|
3154
|
+
}
|
|
3155
|
+
if (pName === "id" || pName === "className" || pName === "children") continue;
|
|
3156
|
+
if (prop.required) {
|
|
3157
|
+
if (prop.values && prop.values.length > 0) {
|
|
3158
|
+
propsStr.push(`${pName}="${prop.values[0]}"`);
|
|
3159
|
+
} else if (prop.type === "string") {
|
|
3160
|
+
propsStr.push(`${pName}="..."`);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
const propsJsx = propsStr.length > 0 ? " " + propsStr.join(" ") : "";
|
|
3165
|
+
const nameLower = name.toLowerCase();
|
|
3166
|
+
const selfClosing = nameLower.includes("input") || nameLower.includes("separator") || nameLower.includes("slider") || nameLower.includes("switch");
|
|
3167
|
+
if (selfClosing) {
|
|
3168
|
+
return `<${name}${propsJsx} />`;
|
|
3169
|
+
}
|
|
3170
|
+
return `<${name}${propsJsx}>${name}</${name}>`;
|
|
3171
|
+
}
|
|
3172
|
+
function buildCompositionExample(root, subs) {
|
|
3173
|
+
const header = subs.filter((s) => s.includes("Header"));
|
|
3174
|
+
const title = subs.filter((s) => s.includes("Title"));
|
|
3175
|
+
const description = subs.filter((s) => s.includes("Description"));
|
|
3176
|
+
const content = subs.filter((s) => s.includes("Content") || s.includes("Body"));
|
|
3177
|
+
const footer = subs.filter((s) => s.includes("Footer"));
|
|
3178
|
+
const action = subs.filter((s) => s.includes("Action"));
|
|
3179
|
+
const other = subs.filter(
|
|
3180
|
+
(s) => !header.includes(s) && !title.includes(s) && !description.includes(s) && !content.includes(s) && !footer.includes(s) && !action.includes(s)
|
|
3181
|
+
);
|
|
3182
|
+
const lines = [`<${root}>`];
|
|
3183
|
+
if (header.length > 0) {
|
|
3184
|
+
lines.push(` <${header[0]}>`);
|
|
3185
|
+
for (const t of title) lines.push(` <${t}>Title</${t}>`);
|
|
3186
|
+
for (const d of description) lines.push(` <${d}>Description</${d}>`);
|
|
3187
|
+
lines.push(` </${header[0]}>`);
|
|
3188
|
+
} else {
|
|
3189
|
+
for (const t of title) lines.push(` <${t}>Title</${t}>`);
|
|
3190
|
+
for (const d of description) lines.push(` <${d}>Description</${d}>`);
|
|
3191
|
+
}
|
|
3192
|
+
for (const c of content) lines.push(` <${c}>Content</${c}>`);
|
|
3193
|
+
for (const o of other) lines.push(` <${o}>...</${o}>`);
|
|
3194
|
+
if (footer.length > 0) {
|
|
3195
|
+
lines.push(` <${footer[0]}>`);
|
|
3196
|
+
for (const a of action) lines.push(` <${a}>Action</${a}>`);
|
|
3197
|
+
lines.push(` </${footer[0]}>`);
|
|
3198
|
+
} else {
|
|
3199
|
+
for (const a of action) lines.push(` <${a}>Action</${a}>`);
|
|
3200
|
+
}
|
|
3201
|
+
lines.push(`</${root}>`);
|
|
3202
|
+
return lines.join("\n");
|
|
3203
|
+
}
|
|
3204
|
+
function inferRelations(components, fileToComponents) {
|
|
3205
|
+
for (const [_file, names] of fileToComponents) {
|
|
3206
|
+
if (names.length <= 1) continue;
|
|
3207
|
+
const sorted = [...names].sort((a, b) => a.length - b.length);
|
|
3208
|
+
const root = sorted[0];
|
|
3209
|
+
const subs = sorted.slice(1).filter((n) => n.startsWith(root));
|
|
3210
|
+
if (subs.length === 0) continue;
|
|
3211
|
+
const rootComp = components[root];
|
|
3212
|
+
if (rootComp) {
|
|
3213
|
+
rootComp.ai = {
|
|
3214
|
+
...rootComp.ai,
|
|
3215
|
+
compositionPattern: "compound",
|
|
3216
|
+
subComponents: subs
|
|
3217
|
+
};
|
|
3218
|
+
rootComp.relations = subs.map((sub) => ({
|
|
3219
|
+
component: sub,
|
|
3220
|
+
relationship: "parent-of",
|
|
3221
|
+
note: `${sub} is a sub-component of ${root}`
|
|
3222
|
+
}));
|
|
3223
|
+
const compositionCode = buildCompositionExample(root, subs);
|
|
3224
|
+
rootComp.variants = [
|
|
3225
|
+
...rootComp.variants ?? [],
|
|
3226
|
+
{ name: "Composition", description: `${root} with all sub-components`, code: compositionCode }
|
|
3227
|
+
];
|
|
3228
|
+
}
|
|
3229
|
+
const rootImportPath = rootComp?.usage?.guidelines?.[0];
|
|
3230
|
+
for (const sub of subs) {
|
|
3231
|
+
const subComp = components[sub];
|
|
3232
|
+
if (subComp) {
|
|
3233
|
+
subComp.relations = [{
|
|
3234
|
+
component: root,
|
|
3235
|
+
relationship: "child-of",
|
|
3236
|
+
note: `Use inside <${root}>`
|
|
3237
|
+
}];
|
|
3238
|
+
if (rootImportPath) {
|
|
3239
|
+
const allNames = [root, ...subs].join(", ");
|
|
3240
|
+
const fromPath = rootImportPath.match(/from\s+"([^"]+)"/)?.[1] ?? "";
|
|
3241
|
+
if (fromPath) {
|
|
3242
|
+
subComp.usage = {
|
|
3243
|
+
...subComp.usage,
|
|
3244
|
+
guidelines: [`import { ${allNames} } from "${fromPath}"`]
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
if (rootComp && rootComp.usage?.guidelines?.[0]) {
|
|
3251
|
+
const fromPath = rootComp.usage.guidelines[0].match(/from\s+"([^"]+)"/)?.[1] ?? "";
|
|
3252
|
+
if (fromPath) {
|
|
3253
|
+
const allNames = [root, ...subs].join(", ");
|
|
3254
|
+
rootComp.usage.guidelines = [`import { ${allNames} } from "${fromPath}"`];
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
function inferCategory(relativePath) {
|
|
3260
|
+
const parts = relativePath.split(sep);
|
|
3261
|
+
const componentsIdx = parts.findIndex(
|
|
3262
|
+
(p) => p === "components" || p === "ui"
|
|
3263
|
+
);
|
|
3264
|
+
if (componentsIdx >= 0 && componentsIdx + 1 < parts.length - 1) {
|
|
3265
|
+
const nextPart = parts[componentsIdx + 1];
|
|
3266
|
+
if (/^[A-Z]/.test(nextPart)) return parts[componentsIdx] || "uncategorized";
|
|
3267
|
+
return nextPart;
|
|
3268
|
+
}
|
|
3269
|
+
return "uncategorized";
|
|
3270
|
+
}
|
|
3271
|
+
function findTsConfig(projectRoot) {
|
|
3272
|
+
const candidates = ["tsconfig.json", "tsconfig.app.json"];
|
|
3273
|
+
for (const name of candidates) {
|
|
3274
|
+
const p = join5(projectRoot, name);
|
|
3275
|
+
if (existsSync5(p)) return p;
|
|
3276
|
+
}
|
|
3277
|
+
return null;
|
|
3278
|
+
}
|
|
3279
|
+
function readPackageName(projectRoot) {
|
|
3280
|
+
try {
|
|
3281
|
+
const pkgPath = join5(projectRoot, "package.json");
|
|
3282
|
+
if (!existsSync5(pkgPath)) return void 0;
|
|
3283
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
3284
|
+
return pkg.name;
|
|
3285
|
+
} catch {
|
|
3286
|
+
return void 0;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
// src/adapters/cloud-catalog.ts
|
|
3291
|
+
var DEFAULT_CLOUD_URL2 = "https://app.usefragments.com/api/catalog";
|
|
3292
|
+
function mergeDesignSystemMetadata(catalogDesignSystem, contextDesignSystem) {
|
|
3293
|
+
return {
|
|
3294
|
+
name: catalogDesignSystem?.name ?? contextDesignSystem?.name,
|
|
3295
|
+
packageName: catalogDesignSystem?.packageName ?? contextDesignSystem?.packageName ?? null,
|
|
3296
|
+
importPath: catalogDesignSystem?.importPath ?? contextDesignSystem?.importPath ?? catalogDesignSystem?.packageName ?? contextDesignSystem?.packageName ?? null
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
function chooseComponentSource(args) {
|
|
3300
|
+
if (args.contextComponents.length > args.catalogComponents.length) {
|
|
3301
|
+
return args.contextComponents;
|
|
3302
|
+
}
|
|
3303
|
+
return args.catalogComponents;
|
|
3304
|
+
}
|
|
3305
|
+
function dominantString(values) {
|
|
3306
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3307
|
+
for (const value of values) {
|
|
3308
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
3309
|
+
if (!trimmed) continue;
|
|
3310
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
3311
|
+
}
|
|
3312
|
+
const ranked = [...counts.entries()].sort(
|
|
3313
|
+
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0])
|
|
3314
|
+
);
|
|
3315
|
+
if (ranked.length === 0) return void 0;
|
|
3316
|
+
if (ranked[1] && ranked[1][1] === ranked[0][1]) return void 0;
|
|
3317
|
+
return ranked[0][0];
|
|
3318
|
+
}
|
|
3319
|
+
function inferDesignSystemMetadataFromComponents(components) {
|
|
3320
|
+
const packageName = dominantString(
|
|
3321
|
+
components.map((component) => component.packageName)
|
|
3322
|
+
);
|
|
3323
|
+
const packageComponents = packageName ? components.filter((component) => component.packageName === packageName) : components;
|
|
3324
|
+
const importPath = dominantString(
|
|
3325
|
+
packageComponents.map(
|
|
3326
|
+
(component) => component.importPath ?? component.packageName
|
|
3327
|
+
)
|
|
3328
|
+
);
|
|
3329
|
+
return {
|
|
3330
|
+
packageName: packageName ?? null,
|
|
3331
|
+
importPath: importPath ?? packageName ?? null
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
var TOKEN_CATEGORY_ALIASES = {
|
|
3335
|
+
color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
|
|
3336
|
+
spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
|
|
3337
|
+
typography: ["typography", "font", "text", "copy", "line-height", "letter"],
|
|
3338
|
+
border: ["border", "borders", "stroke", "outline"],
|
|
3339
|
+
radius: ["radius", "radii", "corner", "corners", "rounded"],
|
|
3340
|
+
shadow: ["shadow", "shadows", "elevation"],
|
|
3341
|
+
layout: ["layout", "grid", "container", "breakpoint"],
|
|
3342
|
+
focus: ["focus", "ring", "focus-ring"],
|
|
3343
|
+
surface: ["surface", "surfaces", "canvas", "card", "background"]
|
|
3344
|
+
};
|
|
3345
|
+
function normalizeCatalogUrl(url) {
|
|
3346
|
+
if (!url) return DEFAULT_CLOUD_URL2;
|
|
3347
|
+
if (url.endsWith("/api/catalog")) return url;
|
|
3348
|
+
return `${url.replace(/\/+$/, "")}/api/catalog`;
|
|
3349
|
+
}
|
|
3350
|
+
function normalizeValidateFixUrl(url) {
|
|
3351
|
+
const base = normalizeCatalogUrl(url).replace(/\/api\/catalog$/, "");
|
|
3352
|
+
return `${base}/api/context?target=validate-fix`;
|
|
3353
|
+
}
|
|
3354
|
+
function normalizeValue(value) {
|
|
3355
|
+
return value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() ?? "";
|
|
3356
|
+
}
|
|
3357
|
+
function canonicalizeTokenCategory(token) {
|
|
3358
|
+
const candidates = [
|
|
3359
|
+
token.category,
|
|
3360
|
+
token.path?.[0],
|
|
3361
|
+
token.name.split(/[.:/-]/)[0]
|
|
3362
|
+
].map(normalizeValue).filter(Boolean);
|
|
3363
|
+
for (const candidate of candidates) {
|
|
3364
|
+
for (const [canonical, aliases] of Object.entries(
|
|
3365
|
+
TOKEN_CATEGORY_ALIASES
|
|
3366
|
+
)) {
|
|
3367
|
+
if (candidate === canonical || aliases.some(
|
|
3368
|
+
(alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
|
|
3369
|
+
)) {
|
|
3370
|
+
return canonical;
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
return candidates[0] || "other";
|
|
3375
|
+
}
|
|
3376
|
+
function groupTokens(flat) {
|
|
3377
|
+
const categories = {};
|
|
3378
|
+
const normalizedFlat = (flat ?? []).filter((token) => !isGarbageToken(token)).map((token) => {
|
|
3379
|
+
const category = canonicalizeTokenCategory(token);
|
|
3380
|
+
const normalized = {
|
|
3381
|
+
name: token.name,
|
|
3382
|
+
category,
|
|
3383
|
+
value: token.value,
|
|
3384
|
+
description: token.description,
|
|
3385
|
+
path: token.path,
|
|
3386
|
+
type: token.type
|
|
3387
|
+
};
|
|
3388
|
+
if (!categories[category]) {
|
|
3389
|
+
categories[category] = [];
|
|
3390
|
+
}
|
|
3391
|
+
categories[category].push(normalized);
|
|
3392
|
+
return normalized;
|
|
3393
|
+
});
|
|
3394
|
+
return {
|
|
3395
|
+
prefix: "",
|
|
3396
|
+
total: normalizedFlat.length,
|
|
3397
|
+
categories,
|
|
3398
|
+
flat: normalizedFlat
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
function mapComponent(component, designSystem) {
|
|
3402
|
+
return {
|
|
3403
|
+
id: component.componentKey,
|
|
3404
|
+
name: component.name,
|
|
3405
|
+
description: component.description ?? "",
|
|
3406
|
+
category: component.category ?? "uncategorized",
|
|
3407
|
+
status: component.status ?? "stable",
|
|
3408
|
+
tags: [],
|
|
3409
|
+
props: Object.fromEntries(
|
|
3410
|
+
Object.entries(component.props ?? {}).map(([propName, prop]) => [
|
|
3411
|
+
propName,
|
|
3412
|
+
{
|
|
3413
|
+
type: prop.type ?? "unknown",
|
|
3414
|
+
description: prop.description ?? "",
|
|
3415
|
+
required: prop.required ?? false,
|
|
3416
|
+
default: prop.default,
|
|
3417
|
+
values: prop.values
|
|
3418
|
+
}
|
|
3419
|
+
])
|
|
3420
|
+
),
|
|
3421
|
+
propsSummary: Object.entries(component.props ?? {}).map(
|
|
3422
|
+
([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type ?? "unknown"}`
|
|
3423
|
+
),
|
|
3424
|
+
examples: (component.examples ?? []).map((example) => ({
|
|
3425
|
+
name: example.title ?? "Example",
|
|
3426
|
+
description: example.kind,
|
|
3427
|
+
code: example.code,
|
|
3428
|
+
kind: example.kind
|
|
3429
|
+
})),
|
|
3430
|
+
relations: (component.relations ?? []).map((relation) => ({
|
|
3431
|
+
componentName: relation.component ?? relation.componentKey ?? "Unknown",
|
|
3432
|
+
componentId: relation.componentKey,
|
|
3433
|
+
relationship: relation.relationship ?? relation.type ?? "related",
|
|
3434
|
+
note: relation.note,
|
|
3435
|
+
description: relation.description
|
|
3436
|
+
})),
|
|
3437
|
+
compoundChildren: (component.compoundChildren ?? []).filter((child) => child.subcomponentVisibility !== "internal").map((child) => ({
|
|
3438
|
+
name: child.name,
|
|
3439
|
+
componentId: child.componentKey,
|
|
3440
|
+
description: child.description,
|
|
3441
|
+
required: child.required,
|
|
3442
|
+
accepts: child.accepts,
|
|
3443
|
+
visibility: child.subcomponentVisibility === "internal" ? "internal" : "public"
|
|
3444
|
+
})),
|
|
3445
|
+
guidance: {
|
|
3446
|
+
when: component.usageGuidance ? [component.usageGuidance] : [],
|
|
3447
|
+
whenNot: component.donts ?? [],
|
|
3448
|
+
guidelines: component.usageGuidance ? [component.usageGuidance] : [],
|
|
3449
|
+
accessibility: [],
|
|
3450
|
+
usageGuidance: component.usageGuidance,
|
|
3451
|
+
dos: component.dos ?? [],
|
|
3452
|
+
donts: component.donts ?? [],
|
|
3453
|
+
patterns: []
|
|
3454
|
+
},
|
|
3455
|
+
sourceType: "cloud",
|
|
3456
|
+
sourcePath: component.sourcePath,
|
|
3457
|
+
sourceRepoFullName: component.sourceRepoFullName ?? void 0,
|
|
3458
|
+
packageName: component.packageName ?? designSystem?.packageName ?? void 0,
|
|
3459
|
+
importPath: component.importPath ?? component.packageName ?? designSystem?.importPath ?? designSystem?.packageName ?? void 0,
|
|
3460
|
+
publicRef: component.publicRef,
|
|
3461
|
+
publicSlug: component.publicSlug ?? null,
|
|
3462
|
+
isCanonical: component.isCanonical ?? false,
|
|
3463
|
+
tier: component.tier,
|
|
3464
|
+
parentComponentId: component.parentComponentKey,
|
|
3465
|
+
parentComponentName: component.parentComponentName,
|
|
3466
|
+
metadata: {
|
|
3467
|
+
a11yRules: [],
|
|
3468
|
+
scenarioTags: []
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
function normalizeValidateFixContext(raw) {
|
|
3473
|
+
const content = raw.content;
|
|
3474
|
+
if (!content || !Array.isArray(content.components)) {
|
|
3475
|
+
return void 0;
|
|
3476
|
+
}
|
|
3477
|
+
const components = content.components.filter(
|
|
3478
|
+
(component) => Boolean(component?.componentKey) && Boolean(component?.publicRef) && Boolean(component?.name) && Boolean(component?.selection)
|
|
3479
|
+
).map((component) => ({
|
|
3480
|
+
componentKey: component.componentKey,
|
|
3481
|
+
publicRef: component.publicRef,
|
|
3482
|
+
name: component.name,
|
|
3483
|
+
category: component.category ?? "uncategorized",
|
|
3484
|
+
status: component.status ?? "stable",
|
|
3485
|
+
tier: component.tier ?? "composition",
|
|
3486
|
+
isCanonical: component.isCanonical ?? false,
|
|
3487
|
+
isActive: component.isActive ?? true,
|
|
3488
|
+
selection: component.selection ?? "allowed",
|
|
3489
|
+
reasons: component.reasons ?? [],
|
|
3490
|
+
usageGuidance: component.usageGuidance,
|
|
3491
|
+
dos: component.dos ?? [],
|
|
3492
|
+
donts: component.donts ?? []
|
|
3493
|
+
}));
|
|
3494
|
+
return {
|
|
3495
|
+
version: 1,
|
|
3496
|
+
catalogRevision: content.catalogRevision,
|
|
3497
|
+
updatedAt: content.updatedAt,
|
|
3498
|
+
policy: {
|
|
3499
|
+
mode: "cloud",
|
|
3500
|
+
endpoint: content.policy?.endpoint ?? "/api/govern/policy"
|
|
3501
|
+
},
|
|
3502
|
+
components
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
var CloudCatalogAdapter = class {
|
|
3506
|
+
constructor(options) {
|
|
3507
|
+
this.options = options;
|
|
3508
|
+
}
|
|
3509
|
+
options;
|
|
3510
|
+
name = "cloud";
|
|
3511
|
+
async load(_projectRoot) {
|
|
3512
|
+
const headers = {
|
|
3513
|
+
"X-API-Key": this.options.apiKey
|
|
3514
|
+
};
|
|
3515
|
+
const [response, validateFixResponse] = await Promise.all([
|
|
3516
|
+
fetch(normalizeCatalogUrl(this.options.url), { headers }),
|
|
3517
|
+
fetch(normalizeValidateFixUrl(this.options.url), { headers }).catch(
|
|
3518
|
+
() => null
|
|
3519
|
+
)
|
|
3520
|
+
]);
|
|
3521
|
+
if (!response.ok) {
|
|
3522
|
+
throw new Error(
|
|
3523
|
+
`Failed to load Cloud catalog (${response.status} ${response.statusText}).`
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
const raw = await response.json();
|
|
3527
|
+
const validateFixRaw = validateFixResponse && validateFixResponse.ok ? await validateFixResponse.json() : void 0;
|
|
3528
|
+
const sourceComponents = chooseComponentSource({
|
|
3529
|
+
catalogComponents: raw.components ?? [],
|
|
3530
|
+
contextComponents: validateFixRaw?.content?.components ?? []
|
|
3531
|
+
});
|
|
3532
|
+
const inferredDesignSystem = inferDesignSystemMetadataFromComponents(sourceComponents);
|
|
3533
|
+
const designSystem = mergeDesignSystemMetadata(
|
|
3534
|
+
{
|
|
3535
|
+
...raw.designSystem,
|
|
3536
|
+
packageName: raw.designSystem?.packageName ?? inferredDesignSystem.packageName,
|
|
3537
|
+
importPath: raw.designSystem?.importPath ?? inferredDesignSystem.importPath
|
|
3538
|
+
},
|
|
3539
|
+
validateFixRaw?.content?.designSystem
|
|
3540
|
+
);
|
|
3541
|
+
const components = Object.fromEntries(
|
|
3542
|
+
sourceComponents.map((component) => [
|
|
3543
|
+
component.componentKey,
|
|
3544
|
+
mapComponent(component, designSystem)
|
|
3545
|
+
])
|
|
3546
|
+
);
|
|
3547
|
+
const tokens = raw.tokens?.flat ? groupTokens(raw.tokens.flat) : void 0;
|
|
3548
|
+
const packageName = designSystem?.packageName ?? void 0;
|
|
3549
|
+
const importPath = designSystem?.importPath ?? designSystem?.packageName ?? void 0;
|
|
3550
|
+
const packageMap = Object.fromEntries(
|
|
3551
|
+
Object.values(components).map((component) => [
|
|
3552
|
+
component.name,
|
|
3553
|
+
component.importPath ?? component.packageName ?? packageName
|
|
3554
|
+
]).filter(
|
|
3555
|
+
(entry) => typeof entry[1] === "string" && entry[1].length > 0
|
|
3556
|
+
)
|
|
3557
|
+
);
|
|
3558
|
+
const snapshot = validateSnapshot({
|
|
3559
|
+
schemaVersion: 1,
|
|
3560
|
+
sourceType: "cloud",
|
|
3561
|
+
sourceLabel: "Fragments Cloud catalog",
|
|
3562
|
+
capabilities: buildCapabilities({
|
|
3563
|
+
components,
|
|
3564
|
+
tokens
|
|
3565
|
+
}),
|
|
3566
|
+
metadata: {
|
|
3567
|
+
designSystemName: designSystem?.name ?? raw.org.name,
|
|
3568
|
+
packageName,
|
|
3569
|
+
importPath,
|
|
3570
|
+
revision: raw.revision,
|
|
3571
|
+
updatedAt: typeof raw.updatedAt === "number" ? new Date(raw.updatedAt).toISOString() : void 0
|
|
3572
|
+
},
|
|
3573
|
+
components,
|
|
3574
|
+
tokens,
|
|
3575
|
+
packageMap,
|
|
3576
|
+
defaultPackageName: packageName
|
|
3577
|
+
});
|
|
3578
|
+
const validateFixContext = validateFixRaw ? normalizeValidateFixContext(validateFixRaw) : void 0;
|
|
3579
|
+
const hydratedComponents = Object.fromEntries(
|
|
3580
|
+
Object.entries(snapshot.components).map(([componentId, component]) => [
|
|
3581
|
+
componentId,
|
|
3582
|
+
{
|
|
3583
|
+
...component,
|
|
3584
|
+
isCanonical: components[componentId]?.isCanonical ?? false
|
|
3585
|
+
}
|
|
3586
|
+
])
|
|
3587
|
+
);
|
|
3588
|
+
return {
|
|
3589
|
+
snapshot: {
|
|
3590
|
+
...snapshot,
|
|
3591
|
+
components: hydratedComponents
|
|
3592
|
+
},
|
|
3593
|
+
components: hydratedComponents,
|
|
3594
|
+
blocks: snapshot.blocks,
|
|
3595
|
+
tokens: snapshot.tokens,
|
|
3596
|
+
graph: void 0,
|
|
3597
|
+
performanceSummary: void 0,
|
|
3598
|
+
packageMap: snapshot.packageMap,
|
|
3599
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
3600
|
+
validateFixContext,
|
|
3601
|
+
capabilities: new Set(snapshot.capabilities)
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
};
|
|
3605
|
+
|
|
3606
|
+
// src/adapters/bundle.ts
|
|
3607
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3608
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
3609
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
3610
|
+
import {
|
|
3611
|
+
bundleComponentShardSchema,
|
|
3612
|
+
bundleManifestSchema,
|
|
3613
|
+
bundleTokenFileSchema
|
|
3614
|
+
} from "@fragments-sdk/core";
|
|
3615
|
+
async function readJsonFile(path, parser, label) {
|
|
3616
|
+
const content = await readFile2(path, "utf-8");
|
|
3617
|
+
try {
|
|
3618
|
+
return parser.parse(JSON.parse(content));
|
|
3619
|
+
} catch (error) {
|
|
3620
|
+
throw new Error(
|
|
3621
|
+
`Invalid ${label} at ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
function normalizeProps(props) {
|
|
3626
|
+
return Object.fromEntries(
|
|
3627
|
+
Object.entries(props ?? {}).map(([propName, value]) => {
|
|
3628
|
+
const prop = value && typeof value === "object" ? value : {};
|
|
3629
|
+
return [
|
|
3630
|
+
propName,
|
|
3631
|
+
{
|
|
3632
|
+
type: typeof prop.type === "string" && prop.type.length > 0 ? prop.type : "unknown",
|
|
3633
|
+
description: typeof prop.description === "string" ? prop.description : "",
|
|
3634
|
+
required: Boolean(prop.required),
|
|
3635
|
+
default: prop.default,
|
|
3636
|
+
values: Array.isArray(prop.values) ? prop.values.filter((entry) => typeof entry === "string") : void 0,
|
|
3637
|
+
constraints: Array.isArray(prop.constraints) ? prop.constraints.filter(
|
|
3638
|
+
(entry) => typeof entry === "string"
|
|
3639
|
+
) : void 0
|
|
3640
|
+
}
|
|
3641
|
+
];
|
|
3642
|
+
})
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
function normalizeExamples(examples) {
|
|
3646
|
+
return examples.map((example) => {
|
|
3647
|
+
const record = example && typeof example === "object" ? example : {};
|
|
3648
|
+
return {
|
|
3649
|
+
name: typeof record.title === "string" && record.title || typeof record.name === "string" && record.name || "Example",
|
|
3650
|
+
description: typeof record.description === "string" ? record.description : void 0,
|
|
3651
|
+
code: typeof record.code === "string" ? record.code : void 0,
|
|
3652
|
+
kind: typeof record.kind === "string" ? record.kind : void 0
|
|
3653
|
+
};
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
function normalizeRelations(relations, fallback) {
|
|
3657
|
+
if (relations.length > 0) {
|
|
3658
|
+
return relations.map((relation) => {
|
|
3659
|
+
const record = relation && typeof relation === "object" ? relation : {};
|
|
3660
|
+
return {
|
|
3661
|
+
componentName: typeof record.component === "string" && record.component || typeof record.name === "string" && record.name || typeof record.componentName === "string" && record.componentName || typeof record.componentId === "string" && record.componentId || "Unknown",
|
|
3662
|
+
componentId: typeof record.componentId === "string" ? record.componentId : void 0,
|
|
3663
|
+
relationship: typeof record.relationship === "string" && record.relationship || typeof record.type === "string" && record.type || "related",
|
|
3664
|
+
note: typeof record.note === "string" ? record.note : void 0,
|
|
3665
|
+
description: typeof record.description === "string" ? record.description : void 0
|
|
3666
|
+
};
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
return fallback.map((relation) => ({
|
|
3670
|
+
componentName: relation.name,
|
|
3671
|
+
componentId: relation.componentId,
|
|
3672
|
+
relationship: relation.type
|
|
3673
|
+
}));
|
|
3674
|
+
}
|
|
3675
|
+
function buildComponent(manifest, entry, shard) {
|
|
3676
|
+
const sourcePackage = manifest.designSystem.importPath ?? manifest.designSystem.packageName ?? void 0;
|
|
3677
|
+
const component = shard.component;
|
|
3678
|
+
const componentRecord = component;
|
|
3679
|
+
const props = normalizeProps(component.props);
|
|
3680
|
+
return {
|
|
3681
|
+
id: shard.componentId,
|
|
3682
|
+
name: component.name,
|
|
3683
|
+
description: component.description,
|
|
3684
|
+
category: component.category,
|
|
3685
|
+
status: component.status,
|
|
3686
|
+
tags: [],
|
|
3687
|
+
props,
|
|
3688
|
+
propsSummary: Object.entries(props).map(
|
|
3689
|
+
([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}`
|
|
3690
|
+
),
|
|
3691
|
+
examples: normalizeExamples(component.examples),
|
|
3692
|
+
relations: normalizeRelations(component.relations, entry.relations),
|
|
3693
|
+
compoundChildren: entry.compoundChildren.map((child) => ({
|
|
3694
|
+
name: child.name,
|
|
3695
|
+
componentId: child.componentId,
|
|
3696
|
+
visibility: "public"
|
|
3697
|
+
})),
|
|
3698
|
+
guidance: {
|
|
3699
|
+
when: component.usageGuidance ? [component.usageGuidance] : [],
|
|
3700
|
+
whenNot: component.donts,
|
|
3701
|
+
guidelines: component.usageGuidance ? [component.usageGuidance] : [],
|
|
3702
|
+
accessibility: [],
|
|
3703
|
+
usageGuidance: component.usageGuidance || void 0,
|
|
3704
|
+
dos: component.dos,
|
|
3705
|
+
donts: component.donts,
|
|
3706
|
+
patterns: []
|
|
3707
|
+
},
|
|
3708
|
+
sourceType: "bundle",
|
|
3709
|
+
sourcePath: component.sourcePath,
|
|
3710
|
+
sourceRepoFullName: component.sourceRepoFullName,
|
|
3711
|
+
packageName: manifest.designSystem.packageName ?? void 0,
|
|
3712
|
+
importPath: sourcePackage,
|
|
3713
|
+
publicRef: component.publicRef,
|
|
3714
|
+
publicSlug: component.publicSlug,
|
|
3715
|
+
isCanonical: componentRecord.isCanonical ?? component.tier === "core",
|
|
3716
|
+
tier: component.tier,
|
|
3717
|
+
parentComponentName: component.parentComponentName,
|
|
3718
|
+
metadata: {
|
|
3719
|
+
a11yRules: [],
|
|
3720
|
+
scenarioTags: []
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
var BundleAdapter = class {
|
|
3725
|
+
name = "bundle";
|
|
3726
|
+
discover(startDir) {
|
|
3727
|
+
return findBundleManifest(startDir);
|
|
3728
|
+
}
|
|
3729
|
+
async load(projectRoot) {
|
|
3730
|
+
const manifests = this.discover(projectRoot);
|
|
3731
|
+
if (manifests.length === 0) {
|
|
3732
|
+
throw new Error(
|
|
3733
|
+
`No ${BRAND.dataDir}/${BRAND.manifestFile} found. Run \`${BRAND.cliCommand} context install --cloud\` or commit a Fragments bundle into your workspace.`
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
3736
|
+
const manifestPath = manifests[0];
|
|
3737
|
+
const manifest = await readJsonFile(
|
|
3738
|
+
manifestPath,
|
|
3739
|
+
bundleManifestSchema,
|
|
3740
|
+
"bundle manifest"
|
|
3741
|
+
);
|
|
3742
|
+
const bundleDir = dirname3(manifestPath);
|
|
3743
|
+
const repoRoot = dirname3(bundleDir);
|
|
3744
|
+
const tokensPath = resolve2(bundleDir, "tokens.json");
|
|
3745
|
+
const tokensFile = existsSync6(tokensPath) ? await readJsonFile(tokensPath, bundleTokenFileSchema, "bundle tokens") : void 0;
|
|
3746
|
+
const components = Object.fromEntries(
|
|
3747
|
+
await Promise.all(
|
|
3748
|
+
Object.values(manifest.components).map(async (entry) => {
|
|
3749
|
+
const shardPath = resolve2(repoRoot, entry.file);
|
|
3750
|
+
const shard = await readJsonFile(
|
|
3751
|
+
shardPath,
|
|
3752
|
+
bundleComponentShardSchema,
|
|
3753
|
+
`bundle component shard (${entry.name})`
|
|
3754
|
+
);
|
|
3755
|
+
return [entry.componentId, buildComponent(manifest, entry, shard)];
|
|
3756
|
+
})
|
|
3757
|
+
)
|
|
3758
|
+
);
|
|
3759
|
+
const packageName = manifest.designSystem.importPath ?? manifest.designSystem.packageName ?? void 0;
|
|
3760
|
+
const packageMap = packageName ? Object.fromEntries(
|
|
3761
|
+
Object.values(components).map((component) => [component.name, packageName])
|
|
3762
|
+
) : {};
|
|
3763
|
+
const tokens = tokensFile ? {
|
|
3764
|
+
prefix: "",
|
|
3765
|
+
total: tokensFile.flat.length,
|
|
3766
|
+
categories: Object.fromEntries(
|
|
3767
|
+
Object.entries(tokensFile.categories).map(([category, value]) => [
|
|
3768
|
+
category,
|
|
3769
|
+
value.tokens.map((token) => ({
|
|
3770
|
+
name: token.name,
|
|
3771
|
+
category: token.category,
|
|
3772
|
+
value: token.value,
|
|
3773
|
+
description: token.description,
|
|
3774
|
+
path: token.path,
|
|
3775
|
+
type: token.type
|
|
3776
|
+
}))
|
|
3777
|
+
])
|
|
3778
|
+
),
|
|
3779
|
+
flat: tokensFile.flat.map((token) => ({
|
|
3780
|
+
name: token.name,
|
|
3781
|
+
category: token.category,
|
|
3782
|
+
value: token.value,
|
|
3783
|
+
description: token.description,
|
|
3784
|
+
path: token.path,
|
|
3785
|
+
type: token.type
|
|
3786
|
+
}))
|
|
3787
|
+
} : void 0;
|
|
3788
|
+
const snapshot = validateSnapshot({
|
|
3789
|
+
schemaVersion: 1,
|
|
3790
|
+
sourceType: "bundle",
|
|
3791
|
+
sourceLabel: `${BRAND.dataDir}/${BRAND.manifestFile}`,
|
|
3792
|
+
capabilities: buildCapabilities({
|
|
3793
|
+
components,
|
|
3794
|
+
tokens
|
|
3795
|
+
}),
|
|
3796
|
+
metadata: {
|
|
3797
|
+
designSystemName: manifest.designSystem.name,
|
|
3798
|
+
packageName: manifest.designSystem.packageName ?? void 0,
|
|
3799
|
+
importPath: manifest.designSystem.importPath ?? void 0,
|
|
3800
|
+
revision: manifest.catalogRevision,
|
|
3801
|
+
updatedAt: manifest.catalogUpdatedAt
|
|
3802
|
+
},
|
|
3803
|
+
components,
|
|
3804
|
+
tokens,
|
|
3805
|
+
packageMap,
|
|
3806
|
+
defaultPackageName: packageName
|
|
3807
|
+
});
|
|
3808
|
+
const hydratedComponents = Object.fromEntries(
|
|
3809
|
+
Object.entries(snapshot.components).map(([componentId, component]) => [
|
|
3810
|
+
componentId,
|
|
3811
|
+
{
|
|
3812
|
+
...component,
|
|
3813
|
+
isCanonical: components[componentId]?.isCanonical ?? component.tier === "core"
|
|
3814
|
+
}
|
|
3815
|
+
])
|
|
3816
|
+
);
|
|
3817
|
+
return {
|
|
3818
|
+
snapshot: {
|
|
3819
|
+
...snapshot,
|
|
3820
|
+
components: hydratedComponents
|
|
3821
|
+
},
|
|
3822
|
+
components: hydratedComponents,
|
|
3823
|
+
blocks: snapshot.blocks,
|
|
3824
|
+
tokens: snapshot.tokens,
|
|
3825
|
+
graph: snapshot.graph,
|
|
3826
|
+
performanceSummary: snapshot.performanceSummary,
|
|
3827
|
+
packageMap: snapshot.packageMap,
|
|
3828
|
+
defaultPackageName: snapshot.defaultPackageName,
|
|
3829
|
+
capabilities: new Set(snapshot.capabilities)
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
};
|
|
3833
|
+
|
|
3834
|
+
// src/source-selection.ts
|
|
3835
|
+
function resolveCloudApiKey2(config, fileConfig) {
|
|
3836
|
+
return config.cloudApiKey ?? fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
|
|
3837
|
+
}
|
|
3838
|
+
function resolveCloudUrl2(fileConfig) {
|
|
3839
|
+
return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
|
|
3840
|
+
}
|
|
3841
|
+
function hasTsProject(projectRoot) {
|
|
3842
|
+
return existsSync7(join6(projectRoot, "tsconfig.json")) || existsSync7(join6(projectRoot, "tsconfig.app.json"));
|
|
3843
|
+
}
|
|
3844
|
+
function resolveDataAdapter(config, fileConfig) {
|
|
3845
|
+
const source = config.source ?? fileConfig?.source ?? "auto";
|
|
3846
|
+
const fragmentsJsonPaths = findFragmentsJson(config.projectRoot);
|
|
3847
|
+
const bundleManifestPaths = findBundleManifest(config.projectRoot);
|
|
3848
|
+
const cloudApiKey = resolveCloudApiKey2(config, fileConfig);
|
|
3849
|
+
const cloudUrl = resolveCloudUrl2(fileConfig);
|
|
3850
|
+
switch (source) {
|
|
3851
|
+
case "fragments-json":
|
|
3852
|
+
return { adapter: new FragmentsJsonAdapter(), mode: "fragments-json" };
|
|
3853
|
+
case "cloud":
|
|
3854
|
+
if (!cloudApiKey) {
|
|
3855
|
+
throw new Error(
|
|
3856
|
+
"Cloud source requires a Cloud API key. Pass --cloud-api-key, set FRAGMENTS_API_KEY, or configure cloud.apiKey in ds-mcp.config.json."
|
|
3857
|
+
);
|
|
3858
|
+
}
|
|
3859
|
+
return {
|
|
3860
|
+
adapter: new CloudCatalogAdapter({ apiKey: cloudApiKey, url: cloudUrl }),
|
|
3861
|
+
mode: "cloud"
|
|
3862
|
+
};
|
|
3863
|
+
case "bundle":
|
|
3864
|
+
return { adapter: new BundleAdapter(), mode: "bundle" };
|
|
3865
|
+
case "extract":
|
|
3866
|
+
return { adapter: new AutoExtractionAdapter(), mode: "extract" };
|
|
3867
|
+
case "auto":
|
|
3868
|
+
default:
|
|
3869
|
+
if (fragmentsJsonPaths.length > 0) {
|
|
3870
|
+
return {
|
|
3871
|
+
adapter: new FragmentsJsonAdapter(),
|
|
3872
|
+
mode: "fragments-json"
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
if (cloudApiKey) {
|
|
3876
|
+
return {
|
|
3877
|
+
adapter: new CloudCatalogAdapter({
|
|
3878
|
+
apiKey: cloudApiKey,
|
|
3879
|
+
url: cloudUrl
|
|
3880
|
+
}),
|
|
3881
|
+
mode: "cloud"
|
|
3882
|
+
};
|
|
3883
|
+
}
|
|
3884
|
+
if (bundleManifestPaths.length > 0) {
|
|
3885
|
+
return {
|
|
3886
|
+
adapter: new BundleAdapter(),
|
|
3887
|
+
mode: "bundle"
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
if (hasTsProject(config.projectRoot)) {
|
|
3891
|
+
return { adapter: new AutoExtractionAdapter(), mode: "extract" };
|
|
3892
|
+
}
|
|
3893
|
+
return {
|
|
3894
|
+
adapter: new FragmentsJsonAdapter(),
|
|
3895
|
+
mode: "fragments-json"
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
function resolveSearchApiKey(config, fileConfig) {
|
|
3900
|
+
return config.searchApiKey ?? fileConfig?.vectorSearch?.apiKey;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// src/spec-schema.ts
|
|
3904
|
+
var UI_SPEC_SCHEMA_URI = "fragments://schemas/govern.schema";
|
|
3905
|
+
var UI_SPEC_SCHEMA_NAME = "govern.schema";
|
|
3906
|
+
var UI_SPEC_SCHEMA_MIME_TYPE = "application/schema+json";
|
|
3907
|
+
var UI_SPEC_SCHEMA_RESOURCE = {
|
|
3908
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
3909
|
+
$id: UI_SPEC_SCHEMA_URI,
|
|
3910
|
+
title: "Fragments UI Spec",
|
|
3911
|
+
description: "Input shape accepted by the Fragments govern and validate_and_fix MCP tools.",
|
|
3912
|
+
type: "object",
|
|
3913
|
+
required: ["nodes"],
|
|
3914
|
+
additionalProperties: false,
|
|
3915
|
+
properties: {
|
|
3916
|
+
root: {
|
|
3917
|
+
type: "string",
|
|
3918
|
+
description: "Optional node id to treat as the root. When omitted, top-level nodes are evaluated in order."
|
|
3919
|
+
},
|
|
3920
|
+
metadata: {
|
|
3921
|
+
type: "object",
|
|
3922
|
+
description: "Optional caller metadata. The validator stores and echoes only fields that downstream tools understand.",
|
|
3923
|
+
additionalProperties: true
|
|
3924
|
+
},
|
|
3925
|
+
nodes: {
|
|
3926
|
+
type: "array",
|
|
3927
|
+
description: "Top-level UI nodes. Each node names a component type and may carry props plus nested children.",
|
|
3928
|
+
items: { $ref: "#/$defs/node" }
|
|
3929
|
+
}
|
|
3930
|
+
},
|
|
3931
|
+
$defs: {
|
|
3932
|
+
node: {
|
|
3933
|
+
type: "object",
|
|
3934
|
+
required: ["type"],
|
|
3935
|
+
additionalProperties: false,
|
|
3936
|
+
properties: {
|
|
3937
|
+
id: {
|
|
3938
|
+
type: "string",
|
|
3939
|
+
description: "Stable caller-provided id. Used in violations and fixedSpec patches."
|
|
3940
|
+
},
|
|
3941
|
+
type: {
|
|
3942
|
+
type: "string",
|
|
3943
|
+
description: 'Design-system component name, including compound names such as "DatePicker.Trigger".'
|
|
3944
|
+
},
|
|
3945
|
+
props: {
|
|
3946
|
+
type: "object",
|
|
3947
|
+
description: 'Component props. Put simple text labels in props.children, e.g. { "children": "Save" }.',
|
|
3948
|
+
additionalProperties: true
|
|
3949
|
+
},
|
|
3950
|
+
children: {
|
|
3951
|
+
description: "Nested child nodes, or a string text child for leaf content. Prefer props.children for simple button labels.",
|
|
3952
|
+
oneOf: [
|
|
3953
|
+
{ type: "string" },
|
|
3954
|
+
{
|
|
3955
|
+
type: "array",
|
|
3956
|
+
items: {
|
|
3957
|
+
oneOf: [{ type: "string" }, { $ref: "#/$defs/node" }]
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
]
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
},
|
|
3965
|
+
examples: {
|
|
3966
|
+
valid: [
|
|
3967
|
+
{
|
|
3968
|
+
nodes: [
|
|
3969
|
+
{
|
|
3970
|
+
id: "save",
|
|
3971
|
+
type: "Button",
|
|
3972
|
+
props: { variant: "primary", children: "Save" }
|
|
3973
|
+
}
|
|
3974
|
+
]
|
|
3975
|
+
},
|
|
3976
|
+
{
|
|
3977
|
+
root: "card",
|
|
3978
|
+
metadata: { source: "agent-draft" },
|
|
3979
|
+
nodes: [
|
|
3980
|
+
{
|
|
3981
|
+
id: "card",
|
|
3982
|
+
type: "Card",
|
|
3983
|
+
props: {},
|
|
3984
|
+
children: [
|
|
3985
|
+
{
|
|
3986
|
+
id: "title",
|
|
3987
|
+
type: "Heading",
|
|
3988
|
+
props: { level: 2, children: "Billing" }
|
|
3989
|
+
},
|
|
3990
|
+
{
|
|
3991
|
+
id: "submit",
|
|
3992
|
+
type: "Button",
|
|
3993
|
+
props: { children: "Update plan" }
|
|
3994
|
+
}
|
|
3995
|
+
]
|
|
3996
|
+
}
|
|
3997
|
+
]
|
|
3998
|
+
}
|
|
3999
|
+
],
|
|
4000
|
+
invalid: [
|
|
4001
|
+
{
|
|
4002
|
+
reason: "String event handlers are blocked by safety/no-string-handlers.",
|
|
4003
|
+
spec: {
|
|
4004
|
+
nodes: [
|
|
4005
|
+
{
|
|
4006
|
+
id: "danger",
|
|
4007
|
+
type: "Button",
|
|
4008
|
+
props: { children: "Save", onClick: 'alert("saved")' }
|
|
4009
|
+
}
|
|
4010
|
+
]
|
|
4011
|
+
}
|
|
4012
|
+
},
|
|
4013
|
+
{
|
|
4014
|
+
reason: "Raw CSS values should use design tokens; call tokens.suggest before writing them.",
|
|
4015
|
+
spec: {
|
|
4016
|
+
nodes: [
|
|
4017
|
+
{
|
|
4018
|
+
id: "panel",
|
|
4019
|
+
type: "Box",
|
|
4020
|
+
props: { style: { padding: "6px" } }
|
|
4021
|
+
}
|
|
4022
|
+
]
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
]
|
|
4026
|
+
},
|
|
4027
|
+
notes: [
|
|
4028
|
+
"Use component names from the active catalog; unknown components fail components/allow.",
|
|
4029
|
+
"Use props.children for simple text labels, especially Button text.",
|
|
4030
|
+
"Do not put JavaScript source strings in event handler props.",
|
|
4031
|
+
"Run validate_and_fix after govern when the verdict asks for revision or deterministic repair."
|
|
4032
|
+
]
|
|
4033
|
+
};
|
|
4034
|
+
function serializeUiSpecSchema() {
|
|
4035
|
+
return JSON.stringify(UI_SPEC_SCHEMA_RESOURCE, null, 2);
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// src/server.ts
|
|
4039
|
+
var TOOL_NAMES = buildToolNames();
|
|
4040
|
+
var TOOLS = buildMcpTools();
|
|
4041
|
+
var TOOL_DEFINITION_BY_KEY = new Map(
|
|
4042
|
+
MCP_TOOL_DEFINITIONS.map((definition) => [definition.key, definition])
|
|
4043
|
+
);
|
|
4044
|
+
function createMcpServer(config) {
|
|
4045
|
+
const server = new Server(
|
|
4046
|
+
{
|
|
4047
|
+
name: `${BRAND.nameLower}-mcp`,
|
|
4048
|
+
version: MCP_SERVER_VERSION
|
|
4049
|
+
},
|
|
4050
|
+
{
|
|
4051
|
+
capabilities: {
|
|
4052
|
+
tools: { listChanged: true },
|
|
4053
|
+
resources: { listChanged: false },
|
|
4054
|
+
logging: {}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
);
|
|
4058
|
+
const registry = new ToolRegistry("", {
|
|
4059
|
+
onChanged: () => {
|
|
4060
|
+
server.notification({ method: "notifications/tools/list_changed", params: {} });
|
|
4061
|
+
}
|
|
4062
|
+
});
|
|
4063
|
+
registry.registerBuiltins(
|
|
4064
|
+
{ core: CORE_TOOLS, viewer: VIEWER_TOOLS, infra: INFRA_TOOLS },
|
|
4065
|
+
MCP_TOOL_DEFINITIONS,
|
|
4066
|
+
TOOL_CAPABILITIES
|
|
4067
|
+
);
|
|
4068
|
+
const fileConfig = config.fileConfig ?? loadConfigFile(config.projectRoot) ?? void 0;
|
|
4069
|
+
const mergedConfig = {
|
|
4070
|
+
...fileConfig ? { ...config, fileConfig } : config,
|
|
4071
|
+
searchApiKey: resolveSearchApiKey(config, fileConfig)
|
|
4072
|
+
};
|
|
4073
|
+
const adapter = config.adapter ?? resolveDataAdapter(mergedConfig, fileConfig).adapter;
|
|
4074
|
+
config.onRegistry?.(registry);
|
|
4075
|
+
if (fileConfig?.tools?.exclude) {
|
|
4076
|
+
for (const key of fileConfig.tools.exclude) {
|
|
4077
|
+
registry.unregister(key);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
let cachedData = null;
|
|
4081
|
+
let loadDataPromise = null;
|
|
4082
|
+
let resolvedRoot = null;
|
|
4083
|
+
let resolveProjectRootPromise = null;
|
|
4084
|
+
async function resolveProjectRoot() {
|
|
4085
|
+
if (resolvedRoot) return resolvedRoot;
|
|
4086
|
+
if (resolveProjectRootPromise) return resolveProjectRootPromise;
|
|
4087
|
+
resolveProjectRootPromise = (async () => {
|
|
4088
|
+
if (server.getClientCapabilities()?.roots) {
|
|
4089
|
+
try {
|
|
4090
|
+
const result2 = await server.listRoots();
|
|
4091
|
+
if (result2.roots?.length > 0) {
|
|
4092
|
+
const rootUri = result2.roots[0].uri;
|
|
4093
|
+
resolvedRoot = fileURLToPath(rootUri);
|
|
4094
|
+
return resolvedRoot;
|
|
4095
|
+
}
|
|
4096
|
+
} catch {
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
resolvedRoot = config.projectRoot;
|
|
4100
|
+
return resolvedRoot;
|
|
4101
|
+
})();
|
|
4102
|
+
try {
|
|
4103
|
+
return await resolveProjectRootPromise;
|
|
4104
|
+
} finally {
|
|
4105
|
+
resolveProjectRootPromise = null;
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
async function loadData() {
|
|
4109
|
+
if (cachedData) return cachedData;
|
|
4110
|
+
if (loadDataPromise) return loadDataPromise;
|
|
4111
|
+
loadDataPromise = (async () => {
|
|
4112
|
+
const projectRoot = await resolveProjectRoot();
|
|
4113
|
+
const loaded = await adapter.load(projectRoot);
|
|
4114
|
+
cachedData = loaded;
|
|
4115
|
+
return loaded;
|
|
4116
|
+
})();
|
|
4117
|
+
try {
|
|
4118
|
+
return await loadDataPromise;
|
|
4119
|
+
} finally {
|
|
4120
|
+
loadDataPromise = null;
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4124
|
+
const data = await loadData();
|
|
4125
|
+
return {
|
|
4126
|
+
tools: registry.listTools(
|
|
4127
|
+
{
|
|
4128
|
+
hasViewer: false,
|
|
4129
|
+
hasPlayground: false,
|
|
4130
|
+
capabilities: data.capabilities
|
|
4131
|
+
},
|
|
4132
|
+
TOOLS
|
|
4133
|
+
)
|
|
4134
|
+
};
|
|
4135
|
+
});
|
|
4136
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
4137
|
+
resources: [
|
|
4138
|
+
{
|
|
4139
|
+
uri: UI_SPEC_SCHEMA_URI,
|
|
4140
|
+
name: UI_SPEC_SCHEMA_NAME,
|
|
4141
|
+
title: "Fragments govern UI spec schema",
|
|
4142
|
+
description: "JSON schema and examples for the spec argument accepted by govern and validate_and_fix.",
|
|
4143
|
+
mimeType: UI_SPEC_SCHEMA_MIME_TYPE
|
|
4144
|
+
}
|
|
4145
|
+
]
|
|
4146
|
+
}));
|
|
4147
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
4148
|
+
if (request.params.uri !== UI_SPEC_SCHEMA_URI) {
|
|
4149
|
+
throw new McpError(
|
|
4150
|
+
ErrorCode.InvalidParams,
|
|
4151
|
+
`Unknown resource URI: ${request.params.uri}`
|
|
4152
|
+
);
|
|
4153
|
+
}
|
|
4154
|
+
return {
|
|
4155
|
+
contents: [
|
|
4156
|
+
{
|
|
4157
|
+
uri: UI_SPEC_SCHEMA_URI,
|
|
4158
|
+
mimeType: UI_SPEC_SCHEMA_MIME_TYPE,
|
|
4159
|
+
text: serializeUiSpecSchema()
|
|
4160
|
+
}
|
|
4161
|
+
]
|
|
4162
|
+
};
|
|
4163
|
+
});
|
|
4164
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4165
|
+
const { name, arguments: args } = request.params;
|
|
4166
|
+
const data = await loadData();
|
|
4167
|
+
const toolContext = {
|
|
4168
|
+
data,
|
|
4169
|
+
config: mergedConfig,
|
|
4170
|
+
indexes: { componentIndex: null, blockIndex: null, tokenIndex: null },
|
|
4171
|
+
mcp: {
|
|
4172
|
+
server,
|
|
4173
|
+
clientCapabilities: server.getClientCapabilities()
|
|
4174
|
+
},
|
|
4175
|
+
resolvePackageName: (name2) => {
|
|
4176
|
+
if (name2) {
|
|
4177
|
+
const pkg = data.packageMap[name2];
|
|
4178
|
+
if (pkg) return pkg;
|
|
4179
|
+
}
|
|
4180
|
+
if (data.defaultPackageName) return data.defaultPackageName;
|
|
4181
|
+
if (data.snapshot.sourceType === "cloud") {
|
|
4182
|
+
return "your-component-library";
|
|
4183
|
+
}
|
|
4184
|
+
const root = resolvedRoot ?? config.projectRoot;
|
|
4185
|
+
const packageJsonPath = join7(root, "package.json");
|
|
4186
|
+
if (existsSync8(packageJsonPath)) {
|
|
4187
|
+
try {
|
|
4188
|
+
const content = readFileSync6(packageJsonPath, "utf-8");
|
|
4189
|
+
const pkg = JSON.parse(content);
|
|
4190
|
+
if (pkg.name) {
|
|
4191
|
+
return pkg.name;
|
|
4192
|
+
}
|
|
4193
|
+
} catch {
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
return "your-component-library";
|
|
4197
|
+
},
|
|
4198
|
+
toolNames: TOOL_NAMES
|
|
4199
|
+
};
|
|
4200
|
+
try {
|
|
4201
|
+
const toolKey = registry.resolveKey(name);
|
|
4202
|
+
const definition = TOOL_DEFINITION_BY_KEY.get(toolKey);
|
|
4203
|
+
const argumentKeys = Object.keys(args ?? {});
|
|
4204
|
+
const allowedKeys = new Set(Object.keys(definition?.params ?? {}));
|
|
4205
|
+
const unknownKeys = definition ? argumentKeys.filter((key) => !allowedKeys.has(key)) : [];
|
|
4206
|
+
if (unknownKeys.length > 0) {
|
|
4207
|
+
return {
|
|
4208
|
+
content: [
|
|
4209
|
+
{
|
|
4210
|
+
type: "text",
|
|
4211
|
+
text: JSON.stringify({
|
|
4212
|
+
error: `Unknown argument(s) for ${toolKey}: ${unknownKeys.join(", ")}`
|
|
4213
|
+
})
|
|
4214
|
+
}
|
|
4215
|
+
],
|
|
4216
|
+
isError: true
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
const mCtx = {
|
|
4220
|
+
toolName: name,
|
|
4221
|
+
toolKey,
|
|
4222
|
+
args: args ?? {},
|
|
4223
|
+
ctx: toolContext
|
|
4224
|
+
};
|
|
4225
|
+
return await executeWithMiddleware(
|
|
4226
|
+
config.middleware ?? [],
|
|
4227
|
+
mCtx,
|
|
4228
|
+
() => registry.execute(name, args ?? {}, toolContext)
|
|
4229
|
+
);
|
|
4230
|
+
} catch (error) {
|
|
4231
|
+
return {
|
|
4232
|
+
content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) }],
|
|
4233
|
+
isError: true
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
});
|
|
4237
|
+
return server;
|
|
4238
|
+
}
|
|
4239
|
+
async function startMcpServer(config) {
|
|
4240
|
+
const server = createMcpServer(config);
|
|
4241
|
+
const transport = new StdioServerTransport();
|
|
4242
|
+
await server.connect(transport);
|
|
4243
|
+
}
|
|
4244
|
+
function createSandboxServer() {
|
|
4245
|
+
return createMcpServer({ projectRoot: process.cwd() });
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4248
|
+
export {
|
|
4249
|
+
loadConfigFile,
|
|
4250
|
+
CORE_TOOLS,
|
|
4251
|
+
VIEWER_TOOLS,
|
|
4252
|
+
INFRA_TOOLS,
|
|
4253
|
+
BUILTIN_TOOLS,
|
|
4254
|
+
ToolRegistry,
|
|
4255
|
+
executeWithMiddleware,
|
|
4256
|
+
telemetryMiddleware,
|
|
4257
|
+
componentFromCompiledFragment,
|
|
4258
|
+
blockFromCompiledBlock,
|
|
4259
|
+
tokensFromCompiledTokenData,
|
|
4260
|
+
buildCapabilities,
|
|
4261
|
+
validateSnapshot,
|
|
4262
|
+
FragmentsJsonAdapter,
|
|
4263
|
+
AutoExtractionAdapter,
|
|
4264
|
+
resolveDataAdapter,
|
|
4265
|
+
resolveSearchApiKey,
|
|
4266
|
+
createMcpServer,
|
|
4267
|
+
startMcpServer,
|
|
4268
|
+
createSandboxServer
|
|
4269
|
+
};
|
|
4270
|
+
//# sourceMappingURL=chunk-YJTMK4JY.js.map
|