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.
Files changed (60) hide show
  1. package/.claude/settings.json +24 -0
  2. package/.claude/settings.local.json +10 -0
  3. package/.claude-plugin/marketplace.json +34 -0
  4. package/.claude-plugin/plugin.json +119 -0
  5. package/.gitignore +20 -0
  6. package/.mcp.json +8 -0
  7. package/README.md +27 -20
  8. package/agents/architecture-designer.md +37 -0
  9. package/agents/desarrollador-frontend.md +8 -15
  10. package/agents/product-designer.md +36 -0
  11. package/claude-hooks/agent-memory.js +137 -3
  12. package/claude-hooks/pre-tool-guard.js +61 -9
  13. package/commands/sdd.adr.md +196 -0
  14. package/commands/sdd.ayuda.md +13 -0
  15. package/commands/sdd.compliance.md +5 -0
  16. package/commands/sdd.configurar.md +1 -1
  17. package/commands/sdd.crear-mcp.md +2 -0
  18. package/commands/sdd.defect-report.md +134 -0
  19. package/commands/sdd.descubrir.md +19 -0
  20. package/commands/sdd.estado.md +52 -2
  21. package/commands/sdd.implementar.md +71 -31
  22. package/commands/sdd.md +23 -3
  23. package/commands/sdd.optimizar-memoria.md +47 -0
  24. package/commands/sdd.retro.md +74 -0
  25. package/commands/sdd.verificar.md +71 -0
  26. package/configuracion-ejemplo/.claude/CLAUDE.md +106 -0
  27. package/configuracion-ejemplo/sdd.config.yaml +10 -0
  28. package/docs/CASO-COMPLETO.md +206 -0
  29. package/docs/EJEMPLOS.md +88 -0
  30. package/docs/FABRICA.md +5 -6
  31. package/docs/INICIO-RAPIDO.md +27 -79
  32. package/docs/MEMORIA-Y-OBSERVABILIDAD.md +12 -10
  33. package/docs/README.md +43 -0
  34. package/docs/RELACION-CON-CLAUDE-CODE.md +38 -0
  35. package/package.json +11 -8
  36. package/plantillas/job-story-mejorar-prompt.md +107 -0
  37. package/presets/enterprise.yaml +6 -0
  38. package/presets/lean.yaml +4 -0
  39. package/presets/startup.yaml +6 -0
  40. package/skills/adr-indexer/SKILL.md +181 -0
  41. package/skills/cache-audit/SKILL.md +1 -1
  42. package/skills/critica-diseno/SKILL.md +1 -1
  43. package/skills/descubrir-idea/SKILL.md +1 -1
  44. package/skills/effort-router/SKILL.md +1 -1
  45. package/skills/elegir-direccion/SKILL.md +1 -1
  46. package/skills/interpretar-idea/SKILL.md +1 -1
  47. package/skills/mejorar-prompt/SKILL.md +237 -0
  48. package/skills/memory-compactor/SKILL.md +34 -80
  49. package/skills/mutation-detector/SKILL.md +134 -0
  50. package/skills/observabilidad-consumo/SKILL.md +1 -1
  51. package/skills/token-budget/SKILL.md +24 -1
  52. package/skills/wireframe-mvp/SKILL.md +1 -1
  53. package/mcp-figma/README.md +0 -158
  54. package/mcp-figma/package.json +0 -7
  55. package/mcp-figma/src/component-generator.js +0 -162
  56. package/mcp-figma/src/design-system-analyzer.js +0 -247
  57. package/mcp-figma/src/figma-client.js +0 -75
  58. package/mcp-figma/src/index.js +0 -114
  59. package/mcp-figma/src/mcp.js +0 -97
  60. 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 };
@@ -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
- });
@@ -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 };