sdd-es 2.5.0 → 2.6.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/.claude/settings.json +24 -0
- package/.claude/settings.local.json +10 -0
- package/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +119 -0
- package/.gitignore +20 -0
- package/.mcp.json +8 -0
- package/README.md +27 -20
- package/agents/architecture-designer.md +37 -0
- package/agents/desarrollador-frontend.md +8 -15
- package/agents/product-designer.md +36 -0
- package/claude-hooks/agent-memory.js +137 -3
- package/claude-hooks/pre-tool-guard.js +61 -9
- package/commands/sdd.adr.md +196 -0
- package/commands/sdd.ayuda.md +13 -0
- package/commands/sdd.compliance.md +5 -0
- package/commands/sdd.configurar.md +1 -1
- package/commands/sdd.crear-mcp.md +2 -0
- package/commands/sdd.defect-report.md +134 -0
- package/commands/sdd.descubrir.md +19 -0
- package/commands/sdd.estado.md +52 -2
- package/commands/sdd.implementar.md +71 -31
- package/commands/sdd.md +23 -3
- package/commands/sdd.optimizar-memoria.md +47 -0
- package/commands/sdd.retro.md +74 -0
- package/commands/sdd.verificar.md +71 -0
- package/configuracion-ejemplo/.claude/CLAUDE.md +106 -0
- package/configuracion-ejemplo/sdd.config.yaml +10 -0
- package/docs/CASO-COMPLETO.md +206 -0
- package/docs/EJEMPLOS.md +88 -0
- package/docs/FABRICA.md +5 -6
- package/docs/INICIO-RAPIDO.md +27 -79
- package/docs/MEMORIA-Y-OBSERVABILIDAD.md +12 -10
- package/docs/README.md +43 -0
- package/docs/RELACION-CON-CLAUDE-CODE.md +38 -0
- package/package.json +11 -8
- package/plantillas/job-story-mejorar-prompt.md +107 -0
- package/presets/enterprise.yaml +6 -0
- package/presets/lean.yaml +4 -0
- package/presets/startup.yaml +6 -0
- package/skills/adr-indexer/SKILL.md +181 -0
- package/skills/cache-audit/SKILL.md +1 -1
- package/skills/critica-diseno/SKILL.md +1 -1
- package/skills/descubrir-idea/SKILL.md +1 -1
- package/skills/effort-router/SKILL.md +1 -1
- package/skills/elegir-direccion/SKILL.md +1 -1
- package/skills/interpretar-idea/SKILL.md +1 -1
- package/skills/mejorar-prompt/SKILL.md +237 -0
- package/skills/memory-compactor/SKILL.md +34 -80
- package/skills/mutation-detector/SKILL.md +134 -0
- package/skills/observabilidad-consumo/SKILL.md +1 -1
- package/skills/token-budget/SKILL.md +24 -1
- package/skills/wireframe-mvp/SKILL.md +1 -1
- package/mcp-figma/README.md +0 -158
- package/mcp-figma/package.json +0 -7
- package/mcp-figma/src/component-generator.js +0 -162
- package/mcp-figma/src/design-system-analyzer.js +0 -247
- package/mcp-figma/src/figma-client.js +0 -75
- package/mcp-figma/src/index.js +0 -114
- package/mcp-figma/src/mcp.js +0 -97
- package/mcp-figma/src/style-mapper.js +0 -85
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
3
|
-
import { join, extname, basename } from "path";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {{ framework:string, cssApproach:string, hasTokenFile:boolean }} Stack
|
|
7
|
-
* @typedef {{ colors:Record<string,string>, spacing:Record<string,string>, typography:{ fontFamilies:string[], fontSizes:Record<string,string>, fontWeights:Record<string,string> }, breakpoints:Record<string,string>, borderRadius:Record<string,string>, shadows:Record<string,string>, existingComponents:string[], stack:Stack, rawSources:string[] }} DesignProfile
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Escanea un nivel de directorio buscando archivos por extensión (sin recursión profunda).
|
|
12
|
-
* Versión ligera usada solo por detectCSSApproach antes de que findFiles esté disponible.
|
|
13
|
-
* @param {string} dir
|
|
14
|
-
* @param {string} ext
|
|
15
|
-
* @returns {string[]}
|
|
16
|
-
*/
|
|
17
|
-
function shallowFind(dir, ext) {
|
|
18
|
-
if (!existsSync(dir)) return [];
|
|
19
|
-
try {
|
|
20
|
-
return readdirSync(dir)
|
|
21
|
-
.filter(e => e.endsWith(ext))
|
|
22
|
-
.map(e => join(dir, e));
|
|
23
|
-
} catch { return []; }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** @param {string} projectRoot */
|
|
27
|
-
function detectCSSApproach(projectRoot) {
|
|
28
|
-
const srcDir = join(projectRoot, "src");
|
|
29
|
-
|
|
30
|
-
// Si algún componente importa *.module.css → CSS Modules
|
|
31
|
-
const srcFiles = [
|
|
32
|
-
...shallowFind(srcDir, ".tsx"),
|
|
33
|
-
...shallowFind(srcDir, ".jsx"),
|
|
34
|
-
...shallowFind(join(srcDir, "components"), ".tsx"),
|
|
35
|
-
...shallowFind(join(srcDir, "components"), ".jsx"),
|
|
36
|
-
];
|
|
37
|
-
for (const file of srcFiles.slice(0, 20)) {
|
|
38
|
-
try {
|
|
39
|
-
if (/from\s+['"][^'"]+\.module\.css['"]/.test(readFileSync(file, "utf-8"))) return "css-modules";
|
|
40
|
-
} catch {}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// CSS global presente → CSS puro
|
|
44
|
-
const globalCandidates = [
|
|
45
|
-
join(projectRoot, "src/index.css"),
|
|
46
|
-
join(projectRoot, "src/global.css"),
|
|
47
|
-
join(projectRoot, "src/styles.css"),
|
|
48
|
-
join(projectRoot, "src/main.css"),
|
|
49
|
-
join(projectRoot, "styles/main.css"),
|
|
50
|
-
join(projectRoot, "styles/global.css"),
|
|
51
|
-
];
|
|
52
|
-
if (globalCandidates.some(f => existsSync(f))) return "css";
|
|
53
|
-
|
|
54
|
-
// Cualquier .css en src/ → CSS puro
|
|
55
|
-
if (shallowFind(srcDir, ".css").length > 0) return "css";
|
|
56
|
-
|
|
57
|
-
return "css"; // default seguro: CSS puro
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** @param {string} projectRoot */
|
|
61
|
-
function detectFramework(projectRoot) {
|
|
62
|
-
let pkg = null;
|
|
63
|
-
try { pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8")); } catch {}
|
|
64
|
-
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
65
|
-
|
|
66
|
-
const framework =
|
|
67
|
-
"react" in deps ? "react" :
|
|
68
|
-
"vue" in deps ? "vue" :
|
|
69
|
-
"@angular/core" in deps ? "angular" :
|
|
70
|
-
"svelte" in deps ? "svelte" :
|
|
71
|
-
"solid-js" in deps ? "solid" : "unknown";
|
|
72
|
-
|
|
73
|
-
const cssApproach =
|
|
74
|
-
"tailwindcss" in deps ? "tailwind" :
|
|
75
|
-
"styled-components" in deps ? "styled-components" :
|
|
76
|
-
"@emotion/react" in deps ? "emotion" :
|
|
77
|
-
"sass" in deps || "node-sass" in deps ? "sass" :
|
|
78
|
-
detectCSSApproach(projectRoot);
|
|
79
|
-
|
|
80
|
-
return { framework, cssApproach };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @param {string} raw
|
|
85
|
-
* @param {string} section
|
|
86
|
-
* @returns {Record<string,string>}
|
|
87
|
-
*/
|
|
88
|
-
function extractTailwindSection(raw, section) {
|
|
89
|
-
/** @type {Record<string,string>} */
|
|
90
|
-
const result = {};
|
|
91
|
-
const sectionRe = new RegExp(`['"]?${section}['"]?\\s*:\\s*\\{([^}]+)\\}`, "s");
|
|
92
|
-
const match = raw.match(sectionRe);
|
|
93
|
-
if (!match) return result;
|
|
94
|
-
const pairRe = /['"]?([\w-]+)['"]?\s*:\s*['"]([^'"]+)['"]/g;
|
|
95
|
-
let m;
|
|
96
|
-
while ((m = pairRe.exec(match[1])) !== null) result[m[1]] = m[2];
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** @param {string} projectRoot */
|
|
101
|
-
function parseTailwindConfig(projectRoot) {
|
|
102
|
-
const candidates = ["tailwind.config.js","tailwind.config.ts","tailwind.config.cjs","tailwind.config.mjs"];
|
|
103
|
-
for (const filename of candidates) {
|
|
104
|
-
const path = join(projectRoot, filename);
|
|
105
|
-
if (!existsSync(path)) continue;
|
|
106
|
-
const raw = readFileSync(path, "utf-8");
|
|
107
|
-
return {
|
|
108
|
-
colors: extractTailwindSection(raw, "colors"),
|
|
109
|
-
spacing: extractTailwindSection(raw, "spacing"),
|
|
110
|
-
fontSizes: extractTailwindSection(raw, "fontSize"),
|
|
111
|
-
breakpoints: extractTailwindSection(raw, "screens"),
|
|
112
|
-
borderRadius: extractTailwindSection(raw, "borderRadius"),
|
|
113
|
-
shadows: extractTailwindSection(raw, "boxShadow"),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
return {};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* @param {string} dir
|
|
121
|
-
* @param {string[]} extensions
|
|
122
|
-
* @param {number} maxDepth
|
|
123
|
-
* @param {number} [depth]
|
|
124
|
-
* @returns {string[]}
|
|
125
|
-
*/
|
|
126
|
-
function findFiles(dir, extensions, maxDepth, depth = 0) {
|
|
127
|
-
if (depth > maxDepth || !existsSync(dir)) return [];
|
|
128
|
-
const results = [];
|
|
129
|
-
try {
|
|
130
|
-
for (const entry of readdirSync(dir)) {
|
|
131
|
-
if ([".", "node_modules", "dist", ".next", ".git"].some(x => entry === x || entry.startsWith("."))) continue;
|
|
132
|
-
const full = join(dir, entry);
|
|
133
|
-
if (statSync(full).isDirectory()) results.push(...findFiles(full, extensions, maxDepth, depth + 1));
|
|
134
|
-
else if (extensions.includes(extname(full))) results.push(full);
|
|
135
|
-
}
|
|
136
|
-
} catch {}
|
|
137
|
-
return results;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** @param {string} projectRoot */
|
|
141
|
-
function parseCSSVariables(projectRoot) {
|
|
142
|
-
/** @type {Record<string,string>} */
|
|
143
|
-
const colors = {};
|
|
144
|
-
/** @type {Record<string,string>} */
|
|
145
|
-
const spacing = {};
|
|
146
|
-
/** @type {Record<string,string>} */
|
|
147
|
-
const fontSizes = {};
|
|
148
|
-
/** @type {Record<string,string>} */
|
|
149
|
-
const fontWeights = {};
|
|
150
|
-
/** @type {string[]} */
|
|
151
|
-
const fontFamilies = [];
|
|
152
|
-
const cssFiles = findFiles(projectRoot, [".css", ".scss", ".sass"], 4);
|
|
153
|
-
for (const file of cssFiles.slice(0, 20)) {
|
|
154
|
-
try {
|
|
155
|
-
const content = readFileSync(file, "utf-8");
|
|
156
|
-
const varRe = /--([\w-]+)\s*:\s*([^;]+);/g;
|
|
157
|
-
let m;
|
|
158
|
-
while ((m = varRe.exec(content)) !== null) {
|
|
159
|
-
const name = m[1].toLowerCase(), value = m[2].trim();
|
|
160
|
-
if (/(color|primary|secondary|accent|bg|text|border|surface|brand)/.test(name)) colors[`--${m[1]}`] = value;
|
|
161
|
-
else if (/(spacing|gap|padding|margin|size)/.test(name)) spacing[`--${m[1]}`] = value;
|
|
162
|
-
else if (/(font-size|text-size)/.test(name)) fontSizes[`--${m[1]}`] = value;
|
|
163
|
-
else if (/font-weight/.test(name)) fontWeights[`--${m[1]}`] = value;
|
|
164
|
-
else if (/font-family/.test(name)) fontFamilies.push(value);
|
|
165
|
-
}
|
|
166
|
-
} catch {}
|
|
167
|
-
}
|
|
168
|
-
return { colors, spacing, typography: { fontFamilies, fontSizes, fontWeights } };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** @param {string} projectRoot */
|
|
172
|
-
function findExistingComponents(projectRoot) {
|
|
173
|
-
const found = [];
|
|
174
|
-
for (const dir of ["src/components","src/ui","components","ui","src/shared"]) {
|
|
175
|
-
const full = join(projectRoot, dir);
|
|
176
|
-
if (!existsSync(full)) continue;
|
|
177
|
-
for (const f of findFiles(full, [".tsx",".jsx",".vue",".svelte"], 3))
|
|
178
|
-
found.push(basename(f, extname(f)));
|
|
179
|
-
}
|
|
180
|
-
return [...new Set(found)].sort();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* @param {string} projectRoot
|
|
185
|
-
* @returns {DesignProfile}
|
|
186
|
-
*/
|
|
187
|
-
function analyzeDesignSystem(projectRoot) {
|
|
188
|
-
const { framework, cssApproach } = detectFramework(projectRoot);
|
|
189
|
-
const tailwindData = cssApproach === "tailwind" ? parseTailwindConfig(projectRoot) : {};
|
|
190
|
-
const cssData = parseCSSVariables(projectRoot);
|
|
191
|
-
|
|
192
|
-
const tokenCandidates = ["src/tokens.ts","src/tokens.js","src/design-tokens.ts","tokens.json","src/theme.ts"];
|
|
193
|
-
const hasTokenFile = tokenCandidates.some(t => existsSync(join(projectRoot, t)));
|
|
194
|
-
|
|
195
|
-
const rawSources = [];
|
|
196
|
-
if (cssApproach === "tailwind") rawSources.push("tailwind.config");
|
|
197
|
-
if (Object.keys(cssData.colors).length > 0) rawSources.push("css-variables");
|
|
198
|
-
if (hasTokenFile) rawSources.push("token-file");
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
stack: { framework, cssApproach, hasTokenFile },
|
|
202
|
-
colors: { ...(cssData.colors ?? {}), ...(tailwindData.colors ?? {}) },
|
|
203
|
-
typography: {
|
|
204
|
-
fontFamilies: cssData.typography?.fontFamilies ?? [],
|
|
205
|
-
fontSizes: { ...(cssData.typography?.fontSizes ?? {}), ...(tailwindData.fontSizes ?? {}) },
|
|
206
|
-
fontWeights: cssData.typography?.fontWeights ?? {},
|
|
207
|
-
},
|
|
208
|
-
spacing: { ...(cssData.spacing ?? {}), ...(tailwindData.spacing ?? {}) },
|
|
209
|
-
breakpoints: tailwindData.breakpoints ?? {},
|
|
210
|
-
borderRadius: tailwindData.borderRadius ?? {},
|
|
211
|
-
shadows: tailwindData.shadows ?? {},
|
|
212
|
-
existingComponents: findExistingComponents(projectRoot),
|
|
213
|
-
rawSources,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* @param {string} projectRoot
|
|
219
|
-
* @param {DesignProfile} profile
|
|
220
|
-
*/
|
|
221
|
-
function evaluateUIConsistency(projectRoot, profile) {
|
|
222
|
-
/** @type {string[]} */
|
|
223
|
-
const issues = [];
|
|
224
|
-
/** @type {string[]} */
|
|
225
|
-
const suggestions = [];
|
|
226
|
-
let score = 100;
|
|
227
|
-
|
|
228
|
-
if (Object.keys(profile.colors).length === 0) { issues.push("No se detectaron tokens de color — los colores pueden estar hardcodeados"); score -= 20; }
|
|
229
|
-
if (Object.keys(profile.spacing).length === 0) { issues.push("No se detectaron tokens de espaciado"); score -= 10; }
|
|
230
|
-
if (profile.typography.fontFamilies.length === 0) { issues.push("No se detectó fuente tipográfica declarada como token"); score -= 10; }
|
|
231
|
-
if (profile.existingComponents.length === 0) { issues.push("No se encontró librería de componentes en src/components o src/ui"); score -= 15; suggestions.push("Crea src/components con componentes base (Button, Input, Card)"); }
|
|
232
|
-
if (!profile.stack.hasTokenFile) suggestions.push("Centraliza los tokens en src/tokens.ts para tener una única fuente de verdad");
|
|
233
|
-
if (profile.stack.cssApproach === "unknown") suggestions.push("Define un enfoque de estilos claro: Tailwind, CSS Modules, o CSS Variables");
|
|
234
|
-
|
|
235
|
-
const componentCoverage = profile.existingComponents.slice(0, 20).map(name => ({
|
|
236
|
-
name,
|
|
237
|
-
hasStory: existsSync(join(projectRoot, `src/components/${name}.stories.tsx`)) || existsSync(join(projectRoot, `src/components/${name}.stories.ts`)),
|
|
238
|
-
hasTest: existsSync(join(projectRoot, `src/components/${name}.test.tsx`)) || existsSync(join(projectRoot, `src/__tests__/${name}.test.tsx`)),
|
|
239
|
-
}));
|
|
240
|
-
|
|
241
|
-
const withoutTests = componentCoverage.filter(c => !c.hasTest).length;
|
|
242
|
-
if (withoutTests > 0) { suggestions.push(`${withoutTests} componente(s) sin tests`); score -= Math.min(15, withoutTests * 3); }
|
|
243
|
-
|
|
244
|
-
return { score: Math.max(0, score), issues, suggestions, componentCoverage };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export { analyzeDesignSystem, evaluateUIConsistency };
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
const FIGMA_API = "https://api.figma.com/v1";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {{ type: string, color?: { r:number, g:number, b:number, a?:number } }} Fill
|
|
6
|
-
* @typedef {{ id:string, name:string, type:string, fills?:Fill[], style?:object, children?:FigmaNode[], absoluteBoundingBox?:{width:number,height:number} }} FigmaNode
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
function pat() {
|
|
10
|
-
const token = process.env.FIGMA_PAT;
|
|
11
|
-
if (!token) throw new Error("FIGMA_PAT no está definido en las variables de entorno");
|
|
12
|
-
return token;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function headers() {
|
|
16
|
-
return { "X-Figma-Token": pat(), "Content-Type": "application/json" };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** @param {string} path */
|
|
20
|
-
async function get(path) {
|
|
21
|
-
const res = await fetch(`${FIGMA_API}${path}`, { headers: headers() });
|
|
22
|
-
if (!res.ok) {
|
|
23
|
-
const body = await res.text();
|
|
24
|
-
throw new Error(`Figma API ${res.status}: ${body}`);
|
|
25
|
-
}
|
|
26
|
-
return res.json();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** @param {string} fileKey */
|
|
30
|
-
async function getFileMeta(fileKey) {
|
|
31
|
-
const data = await get(`/files/${fileKey}?depth=1`);
|
|
32
|
-
return {
|
|
33
|
-
name: data.name,
|
|
34
|
-
lastModified: data.lastModified,
|
|
35
|
-
version: data.version,
|
|
36
|
-
componentCount: Object.keys(data.components ?? {}).length,
|
|
37
|
-
styleCount: Object.keys(data.styles ?? {}).length,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @param {string} fileKey */
|
|
42
|
-
async function getFileComponents(fileKey) {
|
|
43
|
-
const data = await get(`/components/file/${fileKey}`);
|
|
44
|
-
return data.meta?.components ?? [];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {string} fileKey
|
|
49
|
-
* @param {string} nodeId
|
|
50
|
-
*/
|
|
51
|
-
async function getNodeById(fileKey, nodeId) {
|
|
52
|
-
return get(`/files/${fileKey}/nodes?ids=${encodeURIComponent(nodeId)}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @param {number} r
|
|
57
|
-
* @param {number} g
|
|
58
|
-
* @param {number} b
|
|
59
|
-
* @param {number} [a]
|
|
60
|
-
*/
|
|
61
|
-
function rgbaToHex(r, g, b, a = 1) {
|
|
62
|
-
const toHex = (/** @type {number} */ n) => Math.round(n * 255).toString(16).padStart(2, "0");
|
|
63
|
-
const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
64
|
-
return a < 1 ? `${hex}${toHex(a)}` : hex;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** @param {Fill} fill */
|
|
68
|
-
function extractColorFromFill(fill) {
|
|
69
|
-
if (fill.type === "SOLID" && fill.color) {
|
|
70
|
-
return rgbaToHex(fill.color.r, fill.color.g, fill.color.b, fill.color.a ?? 1);
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export { getFileMeta, getFileComponents, getNodeById, rgbaToHex, extractColorFromFill };
|
package/mcp-figma/src/index.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createServer } from "./mcp.js";
|
|
3
|
-
import { analyzeDesignSystem, evaluateUIConsistency } from "./design-system-analyzer.js";
|
|
4
|
-
import { getFileMeta, getFileComponents, getNodeById } from "./figma-client.js";
|
|
5
|
-
import { mapColors, mapTypography, buildMappingReport } from "./style-mapper.js";
|
|
6
|
-
import { generateComponent, suggestImprovements } from "./component-generator.js";
|
|
7
|
-
|
|
8
|
-
const server = createServer({ name: "sdd-figma-mcp", version: "1.0.0" });
|
|
9
|
-
|
|
10
|
-
// ── 1. analizar_sistema_diseño ────────────────────────────────────────────────
|
|
11
|
-
server.tool({
|
|
12
|
-
name: "analizar_sistema_diseño",
|
|
13
|
-
description: "Analiza el sistema de diseño del proyecto local: framework, CSS, tokens de color, tipografía, espaciado y componentes existentes. Ejecutar SIEMPRE antes de generar código UI.",
|
|
14
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
15
|
-
inputSchema: { type: "object", properties: { project_root: { type: "string", description: "Ruta absoluta a la raíz del proyecto (donde está package.json)" } }, required: ["project_root"] },
|
|
16
|
-
}, async ({ project_root }) => {
|
|
17
|
-
const p = analyzeDesignSystem(project_root);
|
|
18
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, stack: p.stack, colores: Object.keys(p.colors).length ? p.colors : "(no detectados)", tipografia: { fuentes: p.typography.fontFamilies, tamaños: Object.keys(p.typography.fontSizes).length ? p.typography.fontSizes : "(no detectados)", pesos: p.typography.fontWeights }, espaciado: Object.keys(p.spacing).length ? p.spacing : "(no detectado)", breakpoints: Object.keys(p.breakpoints).length ? p.breakpoints : "(no detectados)", borderRadius: p.borderRadius, sombras: p.shadows, componentesExistentes: p.existingComponents, fuentesLeidas: p.rawSources }, null, 2) }] };
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// ── 2. evaluar_ui_existente ───────────────────────────────────────────────────
|
|
22
|
-
server.tool({
|
|
23
|
-
name: "evaluar_ui_existente",
|
|
24
|
-
description: "Score 0-100 de la calidad del sistema de diseño + lista de problemas y sugerencias priorizadas.",
|
|
25
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
26
|
-
inputSchema: { type: "object", properties: { project_root: { type: "string" } }, required: ["project_root"] },
|
|
27
|
-
}, async ({ project_root }) => {
|
|
28
|
-
const p = analyzeDesignSystem(project_root);
|
|
29
|
-
const e = evaluateUIConsistency(project_root, p);
|
|
30
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, score: `${e.score}/100`, problemas: e.issues, sugerencias: e.suggestions, coberturaComponentes: e.componentCoverage }, null, 2) }] };
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// ── 3. conectar_figma ─────────────────────────────────────────────────────────
|
|
34
|
-
server.tool({
|
|
35
|
-
name: "conectar_figma",
|
|
36
|
-
description: "Verifica PAT y devuelve metadata del archivo Figma (nombre, versión, cantidad de componentes y estilos).",
|
|
37
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
38
|
-
inputSchema: { type: "object", properties: { file_key: { type: "string", description: "Clave del archivo — extraída de la URL: figma.com/file/AQUI/nombre" } }, required: ["file_key"] },
|
|
39
|
-
}, async ({ file_key }) => {
|
|
40
|
-
const meta = await getFileMeta(file_key);
|
|
41
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, ...meta }, null, 2) }] };
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ── 4. listar_componentes ─────────────────────────────────────────────────────
|
|
45
|
-
server.tool({
|
|
46
|
-
name: "listar_componentes",
|
|
47
|
-
description: "Lista todos los componentes publicados en el archivo Figma. Acepta filtro opcional por nombre.",
|
|
48
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
49
|
-
inputSchema: { type: "object", properties: { file_key: { type: "string" }, filter: { type: "string", description: "Texto para filtrar por nombre (opcional)" } }, required: ["file_key"] },
|
|
50
|
-
}, async ({ file_key, filter }) => {
|
|
51
|
-
const all = await getFileComponents(file_key);
|
|
52
|
-
const list = filter ? all.filter(c => c.name.toLowerCase().includes(filter.toLowerCase())) : all;
|
|
53
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, total: list.length, components: list.map(c => ({ name: c.name, key: c.key, description: c.description || "(sin descripción)" })) }, null, 2) }] };
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// ── 5. traer_componente ───────────────────────────────────────────────────────
|
|
57
|
-
server.tool({
|
|
58
|
-
name: "traer_componente",
|
|
59
|
-
description: "Trae el detalle completo de un nodo Figma: estructura, fills, estilos de texto, dimensiones e hijos directos.",
|
|
60
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
61
|
-
inputSchema: { type: "object", properties: { file_key: { type: "string" }, node_id: { type: "string", description: "ID del nodo — visible en la URL al seleccionar un frame: ?node-id=AQUI" } }, required: ["file_key", "node_id"] },
|
|
62
|
-
}, async ({ file_key, node_id }) => {
|
|
63
|
-
const detail = await getNodeById(file_key, node_id);
|
|
64
|
-
const nd = detail.nodes[node_id];
|
|
65
|
-
if (!nd) throw new Error(`Nodo ${node_id} no encontrado`);
|
|
66
|
-
const n = nd.document;
|
|
67
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, id: n.id, name: n.name, type: n.type, dimensiones: n.absoluteBoundingBox ? { ancho: n.absoluteBoundingBox.width, alto: n.absoluteBoundingBox.height } : null, fills: n.fills ?? [], estiloTexto: n.style ?? null, hijos: (n.children ?? []).map(c => ({ id: c.id, name: c.name, type: c.type, fills: c.fills ?? [] })) }, null, 2) }] };
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// ── 6. mapear_estilos ─────────────────────────────────────────────────────────
|
|
71
|
-
server.tool({
|
|
72
|
-
name: "mapear_estilos",
|
|
73
|
-
description: "Cruza colores y tipografía de un nodo Figma con los tokens del proyecto. Devuelve mapeo con nivel de confianza y lista de tokens sin equivalente.",
|
|
74
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
75
|
-
inputSchema: { type: "object", properties: { file_key: { type: "string" }, node_id: { type: "string" }, project_root: { type: "string" } }, required: ["file_key", "node_id", "project_root"] },
|
|
76
|
-
}, async ({ file_key, node_id, project_root }) => {
|
|
77
|
-
const [detail, profile] = await Promise.all([getNodeById(file_key, node_id), Promise.resolve(analyzeDesignSystem(project_root))]);
|
|
78
|
-
const nd = detail.nodes[node_id];
|
|
79
|
-
if (!nd) throw new Error(`Nodo ${node_id} no encontrado`);
|
|
80
|
-
const all = [nd.document, ...(nd.document.children ?? [])];
|
|
81
|
-
const colorMappings = mapColors(all.filter(n => n.fills?.length).map(n => ({ name: n.name, fills: n.fills })), profile);
|
|
82
|
-
const typographyMappings = mapTypography(all.filter(n => n.style && n.type === "TEXT").map(n => ({ name: n.name, style: n.style })), profile);
|
|
83
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, ...buildMappingReport(colorMappings, typographyMappings) }, null, 2) }] };
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// ── 7. generar_componente ─────────────────────────────────────────────────────
|
|
87
|
-
server.tool({
|
|
88
|
-
name: "generar_componente",
|
|
89
|
-
description: "Genera código del componente (React/Vue) usando los tokens del proyecto. No hardcodea valores de Figma.",
|
|
90
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
91
|
-
inputSchema: { type: "object", properties: { file_key: { type: "string" }, node_id: { type: "string" }, project_root: { type: "string" } }, required: ["file_key", "node_id", "project_root"] },
|
|
92
|
-
}, async ({ file_key, node_id, project_root }) => {
|
|
93
|
-
const [detail, profile] = await Promise.all([getNodeById(file_key, node_id), Promise.resolve(analyzeDesignSystem(project_root))]);
|
|
94
|
-
const nd = detail.nodes[node_id];
|
|
95
|
-
if (!nd) throw new Error(`Nodo ${node_id} no encontrado`);
|
|
96
|
-
const node = nd.document;
|
|
97
|
-
const all = [node, ...(node.children ?? [])];
|
|
98
|
-
const colorMappings = mapColors(all.filter(n => n.fills?.length).map(n => ({ name: n.name, fills: n.fills })), profile);
|
|
99
|
-
const g = generateComponent(node, profile, colorMappings);
|
|
100
|
-
const cssExt = profile.stack.cssApproach === "css-modules" ? ".module.css" : ".css";
|
|
101
|
-
const cssFile = g.cssSnippet ? `src/components/${g.filename.replace(/\.tsx$/, cssExt)}` : null;
|
|
102
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, filename: g.filename, framework: profile.stack.framework, cssApproach: profile.stack.cssApproach, code: g.code, cssSnippet: g.cssSnippet, advertencias: g.warnings, instrucciones: [`Guarda en src/components/${g.filename}`, ...(cssFile ? [`Crea ${cssFile} con el CSS snippet`] : []), "Revisa los colores y agrega props de accesibilidad según el contexto"] }, null, 2) }] };
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// ── 8. sugerir_mejoras ────────────────────────────────────────────────────────
|
|
106
|
-
server.tool({
|
|
107
|
-
name: "sugerir_mejoras",
|
|
108
|
-
description: "Lista priorizada (alta/media/baja) de mejoras al sistema de diseño del proyecto.",
|
|
109
|
-
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
110
|
-
inputSchema: { type: "object", properties: { project_root: { type: "string" } }, required: ["project_root"] },
|
|
111
|
-
}, async ({ project_root }) => {
|
|
112
|
-
const improvements = suggestImprovements(analyzeDesignSystem(project_root));
|
|
113
|
-
return { content: [{ type: "text", text: JSON.stringify({ ok: true, total: improvements.length, porPrioridad: { alta: improvements.filter(i => i.priority === "alta"), media: improvements.filter(i => i.priority === "media"), baja: improvements.filter(i => i.priority === "baja") } }, null, 2) }] };
|
|
114
|
-
});
|
package/mcp-figma/src/mcp.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Implementación mínima del protocolo MCP sobre stdio (JSON-RPC 2.0)
|
|
3
|
-
// Sin dependencias externas — solo Node.js built-ins
|
|
4
|
-
|
|
5
|
-
import { createInterface } from "readline";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {{ name:string, description:string, inputSchema:object, annotations?:object }} ToolDefinition
|
|
9
|
-
* @typedef {(args: Record<string,any>) => Promise<object>} ToolHandler
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param {{ name:string, version:string }} serverInfo
|
|
14
|
-
*/
|
|
15
|
-
export function createServer({ name, version }) {
|
|
16
|
-
/** @type {ToolDefinition[]} */
|
|
17
|
-
const tools = [];
|
|
18
|
-
/** @type {Record<string, ToolHandler>} */
|
|
19
|
-
const handlers = {};
|
|
20
|
-
|
|
21
|
-
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
22
|
-
|
|
23
|
-
/** @param {object} obj */
|
|
24
|
-
function send(obj) {
|
|
25
|
-
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {any} id
|
|
30
|
-
* @param {object} result
|
|
31
|
-
*/
|
|
32
|
-
function reply(id, result) {
|
|
33
|
-
send({ jsonrpc: "2.0", id, result });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @param {any} id
|
|
38
|
-
* @param {number} code
|
|
39
|
-
* @param {string} message
|
|
40
|
-
*/
|
|
41
|
-
function replyError(id, code, message) {
|
|
42
|
-
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
rl.on("line", async (line) => {
|
|
46
|
-
/** @type {{ id:any, method:string, params:any }} */
|
|
47
|
-
let msg;
|
|
48
|
-
try { msg = JSON.parse(line); } catch { return; }
|
|
49
|
-
|
|
50
|
-
const { id, method, params } = msg;
|
|
51
|
-
|
|
52
|
-
if (method === "initialize") {
|
|
53
|
-
reply(id, {
|
|
54
|
-
protocolVersion: "2024-11-05",
|
|
55
|
-
serverInfo: { name, version },
|
|
56
|
-
capabilities: { tools: {} },
|
|
57
|
-
});
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (method === "notifications/initialized") return;
|
|
62
|
-
if (method === "ping") { reply(id, {}); return; }
|
|
63
|
-
|
|
64
|
-
if (method === "tools/list") {
|
|
65
|
-
reply(id, { tools });
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (method === "tools/call") {
|
|
70
|
-
const handler = handlers[params?.name];
|
|
71
|
-
if (!handler) { replyError(id, -32601, `Tool not found: ${params?.name}`); return; }
|
|
72
|
-
try {
|
|
73
|
-
const result = await handler(params?.arguments ?? {});
|
|
74
|
-
reply(id, result);
|
|
75
|
-
} catch (err) {
|
|
76
|
-
reply(id, {
|
|
77
|
-
content: [{ type: "text", text: JSON.stringify({ ok: false, error: /** @type {Error} */ (err).message }) }],
|
|
78
|
-
isError: true,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (id !== undefined) replyError(id, -32601, `Method not found: ${method}`);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
/**
|
|
89
|
-
* @param {ToolDefinition} definition
|
|
90
|
-
* @param {ToolHandler} handler
|
|
91
|
-
*/
|
|
92
|
-
tool(definition, handler) {
|
|
93
|
-
tools.push(definition);
|
|
94
|
-
handlers[definition.name] = handler;
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { extractColorFromFill } from "./figma-client.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {{ figmaName: string, figmaValue: string, localToken: string|null, localValue: string, matchType: "exact"|"approximate"|"new", confidence: number }} ColorMapping
|
|
6
|
-
* @typedef {{ type: string, color?: { r:number, g:number, b:number, a?:number } }} Fill
|
|
7
|
-
* @typedef {{ name: string, fills: Fill[] }} FillItem
|
|
8
|
-
* @typedef {{ name: string, style: { fontSize?: number } }} TextItem
|
|
9
|
-
* @typedef {{ colors: Record<string,string>, typography: { fontSizes: Record<string,string> } }} DesignProfile
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** @param {string} hex */
|
|
13
|
-
function hexToRgb(hex) {
|
|
14
|
-
const clean = hex.replace("#", "");
|
|
15
|
-
if (clean.length !== 6) return null;
|
|
16
|
-
return [parseInt(clean.slice(0,2),16), parseInt(clean.slice(2,4),16), parseInt(clean.slice(4,6),16)];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** @param {string} a @param {string} b */
|
|
20
|
-
function colorDistance(a, b) {
|
|
21
|
-
const ra = hexToRgb(a), rb = hexToRgb(b);
|
|
22
|
-
if (!ra || !rb) return Infinity;
|
|
23
|
-
return Math.sqrt((ra[0]-rb[0])**2 + (ra[1]-rb[1])**2 + (ra[2]-rb[2])**2);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {FillItem[]} fillItems
|
|
28
|
-
* @param {DesignProfile} profile
|
|
29
|
-
* @returns {ColorMapping[]}
|
|
30
|
-
*/
|
|
31
|
-
function mapColors(fillItems, profile) {
|
|
32
|
-
const localEntries = Object.entries(profile.colors);
|
|
33
|
-
return fillItems.map(({ name, fills }) => {
|
|
34
|
-
const figmaHex = fills.map(extractColorFromFill).find(Boolean) ?? null;
|
|
35
|
-
if (!figmaHex) return { figmaName: name, figmaValue: "desconocido", localToken: null, localValue: "desconocido", matchType: "new", confidence: 0 };
|
|
36
|
-
|
|
37
|
-
const exact = localEntries.find(([,v]) => v.toLowerCase() === figmaHex.toLowerCase());
|
|
38
|
-
if (exact) return { figmaName: name, figmaValue: figmaHex, localToken: exact[0], localValue: exact[1], matchType: "exact", confidence: 1 };
|
|
39
|
-
|
|
40
|
-
let best = null, bestDist = Infinity;
|
|
41
|
-
for (const [token, value] of localEntries) {
|
|
42
|
-
const dist = colorDistance(figmaHex, value);
|
|
43
|
-
if (dist < bestDist) { bestDist = dist; best = [token, value]; }
|
|
44
|
-
}
|
|
45
|
-
if (best && bestDist < 30) return { figmaName: name, figmaValue: figmaHex, localToken: best[0], localValue: best[1], matchType: "approximate", confidence: Math.max(0, 1 - bestDist/30) };
|
|
46
|
-
|
|
47
|
-
return { figmaName: name, figmaValue: figmaHex, localToken: null, localValue: figmaHex, matchType: "new", confidence: 0 };
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {TextItem[]} textItems
|
|
53
|
-
* @param {DesignProfile} profile
|
|
54
|
-
* @returns {ColorMapping[]}
|
|
55
|
-
*/
|
|
56
|
-
function mapTypography(textItems, profile) {
|
|
57
|
-
return textItems.map(({ name, style }) => {
|
|
58
|
-
const figmaSize = style.fontSize ? `${style.fontSize}px` : null;
|
|
59
|
-
if (!figmaSize) return { figmaName: name, figmaValue: "desconocido", localToken: null, localValue: "desconocido", matchType: "new", confidence: 0 };
|
|
60
|
-
const match = Object.entries(profile.typography.fontSizes).find(([,v]) => v === figmaSize);
|
|
61
|
-
if (match) return { figmaName: name, figmaValue: figmaSize, localToken: match[0], localValue: match[1], matchType: "exact", confidence: 1 };
|
|
62
|
-
return { figmaName: name, figmaValue: figmaSize, localToken: null, localValue: figmaSize, matchType: "new", confidence: 0 };
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @param {ColorMapping[]} colorMappings
|
|
68
|
-
* @param {ColorMapping[]} typographyMappings
|
|
69
|
-
*/
|
|
70
|
-
function buildMappingReport(colorMappings, typographyMappings) {
|
|
71
|
-
const all = [...colorMappings, ...typographyMappings];
|
|
72
|
-
const unmapped = [
|
|
73
|
-
...colorMappings.filter(m => m.matchType === "new").map(m => `color: ${m.figmaName} (${m.figmaValue})`),
|
|
74
|
-
...typographyMappings.filter(m => m.matchType === "new").map(m => `tipografía: ${m.figmaName} (${m.figmaValue})`),
|
|
75
|
-
];
|
|
76
|
-
const total = all.length;
|
|
77
|
-
let recommendation = "";
|
|
78
|
-
if (total === 0) recommendation = "No hay estilos en Figma para mapear.";
|
|
79
|
-
else if (unmapped.length === 0) recommendation = "✅ Todos los estilos de Figma coinciden con tokens existentes.";
|
|
80
|
-
else if (unmapped.length <= 3) recommendation = `⚠️ ${unmapped.length} estilo(s) nuevo(s) sin equivalente en el proyecto.`;
|
|
81
|
-
else recommendation = `❌ ${unmapped.length} estilos sin equivalente — considera sincronizar el design system con Figma.`;
|
|
82
|
-
return { colors: colorMappings, typography: typographyMappings, unmapped, recommendation };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export { mapColors, mapTypography, buildMappingReport };
|