sdd-es 2.0.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 +51 -0
- package/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +97 -0
- package/README.md +332 -0
- package/agents/arquitecto.md +148 -0
- package/agents/asesor-datos.md +163 -0
- package/agents/critico.md +142 -0
- package/agents/desarrollador-backend.md +242 -0
- package/agents/desarrollador-frontend.md +120 -0
- package/agents/disenador-api.md +108 -0
- package/agents/documentador.md +177 -0
- package/agents/investigador.md +174 -0
- package/agents/operaciones.md +105 -0
- package/agents/revisor.md +153 -0
- package/agents/seguridad.md +216 -0
- package/agents/tester.md +286 -0
- package/claude-hooks/post-write-conventions.js +412 -0
- package/claude-hooks/pre-tool-guard.js +159 -0
- package/cli/index.js +401 -0
- package/commands/sdd.aclarar.md +200 -0
- package/commands/sdd.analizar.md +241 -0
- package/commands/sdd.ayuda.md +227 -0
- package/commands/sdd.canary.md +60 -0
- package/commands/sdd.checklist.md +174 -0
- package/commands/sdd.comprimir.md +166 -0
- package/commands/sdd.configurar.md +195 -0
- package/commands/sdd.constitucion.md +343 -0
- package/commands/sdd.crear-app.md +168 -0
- package/commands/sdd.crear-mcp.md +174 -0
- package/commands/sdd.descubrir.md +269 -0
- package/commands/sdd.desplegar.md +155 -0
- package/commands/sdd.especificar.md +302 -0
- package/commands/sdd.estado.md +124 -0
- package/commands/sdd.glosario.md +108 -0
- package/commands/sdd.implementar.md +377 -0
- package/commands/sdd.importar.md +91 -0
- package/commands/sdd.mapear.md +120 -0
- package/commands/sdd.md +119 -0
- package/commands/sdd.planificar.md +372 -0
- package/commands/sdd.qa.md +108 -0
- package/commands/sdd.release.md +253 -0
- package/commands/sdd.retro.md +82 -0
- package/commands/sdd.snapshot.md +122 -0
- package/commands/sdd.tareas.md +300 -0
- package/commands/sdd.verificar.md +239 -0
- package/configuracion-ejemplo/hooks-ejemplo/antes_cada_tarea.sh +18 -0
- package/configuracion-ejemplo/hooks-ejemplo/antes_implementar.sh +45 -0
- package/configuracion-ejemplo/hooks-ejemplo/despues_especificar.sh +14 -0
- package/configuracion-ejemplo/hooks-ejemplo/despues_implementar.sh +36 -0
- package/configuracion-ejemplo/hooks-ejemplo/despues_planificar.sh +19 -0
- package/configuracion-ejemplo/hooks-ejemplo/guardia-seguridad.sh +367 -0
- package/configuracion-ejemplo/sdd.config.yaml +310 -0
- package/docs/AGENTES.md +74 -0
- package/docs/COMPRESION.md +155 -0
- package/docs/EJEMPLO-PRACTICA.md +383 -0
- package/docs/EJEMPLOS.md +212 -0
- package/docs/FABRICA.md +185 -0
- package/docs/FILOSOFIA.md +61 -0
- package/docs/FLUJO.md +149 -0
- package/docs/INICIO-RAPIDO.md +116 -0
- package/docs/MAPAS.md +113 -0
- package/docs/MODELOS.md +103 -0
- package/docs/PERSONALIZACION.md +152 -0
- package/instalar.ps1 +39 -0
- package/instalar.sh +22 -0
- package/mcp-figma/README.md +158 -0
- package/mcp-figma/package.json +7 -0
- package/mcp-figma/src/component-generator.js +162 -0
- package/mcp-figma/src/design-system-analyzer.js +247 -0
- package/mcp-figma/src/figma-client.js +75 -0
- package/mcp-figma/src/index.js +114 -0
- package/mcp-figma/src/mcp.js +97 -0
- package/mcp-figma/src/style-mapper.js +85 -0
- package/package.json +50 -0
- package/plantillas/analisis.md +57 -0
- package/plantillas/checklist-especificacion.md +66 -0
- package/plantillas/constitucion.md +104 -0
- package/plantillas/decision-arquitectura.md +39 -0
- package/plantillas/dependencias-mapa.md +89 -0
- package/plantillas/especificacion.md +108 -0
- package/plantillas/estructura-mapa.md +40 -0
- package/plantillas/glosario.md +22 -0
- package/plantillas/index-especificaciones.md +15 -0
- package/plantillas/mcp-server.md +147 -0
- package/plantillas/plan.md +152 -0
- package/plantillas/simbolos-mapa.md +57 -0
- package/plantillas/snapshot.md +54 -0
- package/plantillas/tareas.md +72 -0
- package/presets/enterprise.yaml +69 -0
- package/presets/lean.yaml +63 -0
- package/presets/startup.yaml +67 -0
- package/skills/compresion-tokens.md +264 -0
- package/skills/constitucion-constraint.md +78 -0
- package/skills/deteccion-stack.md +175 -0
- package/skills/enrutador-agentes.md +69 -0
- package/skills/gestion-estado.md +114 -0
- package/skills/indexador.md +199 -0
- package/skills/modo-guiado/SKILL.md +78 -0
- package/skills/orquestacion-ptc/SKILL.md +96 -0
- package/skills/validacion-spec.md +52 -0
- package/skills/verificador-implementacion.md +71 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ figmaName:string, figmaValue:string, localToken:string|null, matchType:string }} ColorMapping
|
|
7
|
+
* @typedef {{ type:string, color?:{r:number,g:number,b:number}, fills?:any[], children?:any[], name:string, absoluteBoundingBox?:{width:number,height:number}, style?:any }} FigmaNode
|
|
8
|
+
* @typedef {{ stack:{ framework:string, cssApproach:string, hasTokenFile:boolean }, colors:Record<string,string>, spacing:Record<string,string>, typography:{ fontFamilies:string[], fontSizes:Record<string,string>, fontWeights:Record<string,string> }, existingComponents:string[], breakpoints:Record<string,string>, shadows:Record<string,string> }} DesignProfile
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @param {string} str */
|
|
12
|
+
function toPascalCase(str) {
|
|
13
|
+
return str.replace(/[^a-zA-Z0-9\s]/g, " ").split(/\s+/).filter(Boolean)
|
|
14
|
+
.map(w => w[0].toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @param {string} str */
|
|
18
|
+
function toKebabCase(str) {
|
|
19
|
+
return str.replace(/[^a-zA-Z0-9\s]/g, " ").split(/\s+/).filter(Boolean).join("-").toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @param {FigmaNode} node */
|
|
23
|
+
function inferProps(node) {
|
|
24
|
+
/** @type {Record<string,string>} */
|
|
25
|
+
const props = {};
|
|
26
|
+
const name = node.name.toLowerCase();
|
|
27
|
+
const hasText = (node.children ?? []).some(/** @param {any} c */ c => c.type === "TEXT");
|
|
28
|
+
if (hasText || /(button|label|title|heading)/.test(name)) props["children"] = "ReactNode";
|
|
29
|
+
if (/(button|btn|cta|link)/.test(name)) { props["onClick"] = "() => void"; props["disabled"] = "boolean"; }
|
|
30
|
+
if ((node.children ?? []).some(/** @param {any} c */ c => /(image|img|avatar)/.test(c.name.toLowerCase()))) { props["src"] = "string"; props["alt"] = "string"; }
|
|
31
|
+
if (/(card|button|badge|chip)/.test(name)) props["variant"] = '"primary" | "secondary" | "ghost"';
|
|
32
|
+
return props;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {ColorMapping|null|undefined} mapping
|
|
37
|
+
* @param {string} prefix
|
|
38
|
+
*/
|
|
39
|
+
function resolveColorClass(mapping, prefix) {
|
|
40
|
+
if (!mapping) return "";
|
|
41
|
+
return mapping.localToken ? `${prefix}-[var(${mapping.localToken})]` : `${prefix}-[${mapping.figmaValue}]`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Construye el bloque CSS usando variables del proyecto o valores hex como fallback.
|
|
46
|
+
* @param {string} selector
|
|
47
|
+
* @param {FigmaNode} node
|
|
48
|
+
* @param {ColorMapping[]} colorMappings
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function buildCSSBlock(selector, node, colorMappings) {
|
|
52
|
+
const dims = node.absoluteBoundingBox;
|
|
53
|
+
const lines = [];
|
|
54
|
+
|
|
55
|
+
if (dims) {
|
|
56
|
+
lines.push(` width: ${Math.round(dims.width)}px;`);
|
|
57
|
+
lines.push(` height: ${Math.round(dims.height)}px;`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const bgFill = (node.fills ?? []).find(/** @param {any} f */ f => f.type === "SOLID");
|
|
61
|
+
if (bgFill?.color) {
|
|
62
|
+
const hex = `#${Math.round(bgFill.color.r*255).toString(16).padStart(2,"0")}${Math.round(bgFill.color.g*255).toString(16).padStart(2,"0")}${Math.round(bgFill.color.b*255).toString(16).padStart(2,"0")}`;
|
|
63
|
+
const mapping = colorMappings.find(m => m.figmaValue === hex);
|
|
64
|
+
lines.push(` background-color: ${mapping?.localToken ? `var(${mapping.localToken})` : hex};`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const textChild = (node.children ?? []).find(/** @param {any} c */ c => c.type === "TEXT" && c.style);
|
|
68
|
+
if (textChild?.style) {
|
|
69
|
+
const s = textChild.style;
|
|
70
|
+
if (s.fontSize) lines.push(` font-size: ${s.fontSize}px;`);
|
|
71
|
+
if (s.fontWeight) lines.push(` font-weight: ${s.fontWeight};`);
|
|
72
|
+
if (s.lineHeightPx) lines.push(` line-height: ${Math.round(s.lineHeightPx)}px;`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (lines.length === 0) lines.push(" /* estilos del sistema de diseño */");
|
|
76
|
+
|
|
77
|
+
return `.${selector} {\n${lines.join("\n")}\n}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {FigmaNode} node
|
|
82
|
+
* @param {string} componentName
|
|
83
|
+
* @param {Record<string,string>} props
|
|
84
|
+
* @param {ColorMapping[]} colorMappings
|
|
85
|
+
* @param {string} cssApproach
|
|
86
|
+
*/
|
|
87
|
+
function generateReact(node, componentName, props, colorMappings, cssApproach) {
|
|
88
|
+
const warnings = [];
|
|
89
|
+
const dims = node.absoluteBoundingBox;
|
|
90
|
+
if (!dims) warnings.push("No se pudo leer el bounding box — dimensiones aproximadas.");
|
|
91
|
+
if (colorMappings.some(m => m.matchType === "new")) warnings.push("Algunos colores de Figma no tienen token equivalente en el proyecto. Se usó el valor hex directo.");
|
|
92
|
+
|
|
93
|
+
const hasChildren = "children" in props;
|
|
94
|
+
const propsStr = Object.entries(props).map(([k,v]) => `${k}${v==="boolean"?"?":""}: ${v}`).join("; ");
|
|
95
|
+
const propsKeys = Object.keys(props).join(", ") || "className";
|
|
96
|
+
const cssClass = toKebabCase(componentName);
|
|
97
|
+
|
|
98
|
+
/** @type {string|null} */
|
|
99
|
+
let cssSnippet = null;
|
|
100
|
+
let code = "";
|
|
101
|
+
|
|
102
|
+
if (cssApproach === "tailwind") {
|
|
103
|
+
const bgFill = (node.fills ?? []).find(/** @param {any} f */ f => f.type === "SOLID");
|
|
104
|
+
const bgHex = bgFill?.color
|
|
105
|
+
? `#${Math.round(bgFill.color.r*255).toString(16).padStart(2,"0")}${Math.round(bgFill.color.g*255).toString(16).padStart(2,"0")}${Math.round(bgFill.color.b*255).toString(16).padStart(2,"0")}`
|
|
106
|
+
: null;
|
|
107
|
+
const bgMapping = bgHex ? colorMappings.find(m => m.figmaValue === bgHex) : null;
|
|
108
|
+
const bgClass = bgMapping ? resolveColorClass(bgMapping, "bg") : "";
|
|
109
|
+
code = `import React from "react";\n\ninterface ${componentName}Props {\n ${propsStr || "className?: string"}\n}\n\nexport function ${componentName}({ ${propsKeys} }: ${componentName}Props) {\n return (\n <div className="${bgClass} rounded p-4">\n ${hasChildren ? "{children}" : `{/* contenido de ${node.name} */}`}\n </div>\n );\n}\n\nexport default ${componentName};\n`;
|
|
110
|
+
|
|
111
|
+
} else if (cssApproach === "css-modules") {
|
|
112
|
+
code = `import React from "react";\nimport styles from "./${cssClass}.module.css";\n\ninterface ${componentName}Props {\n ${propsStr || "className?: string"}\n}\n\nexport function ${componentName}({ ${propsKeys} }: ${componentName}Props) {\n return (\n <div className={styles.root}>\n ${hasChildren ? "{children}" : `{/* contenido de ${node.name} */}`}\n </div>\n );\n}\n\nexport default ${componentName};\n`;
|
|
113
|
+
cssSnippet = buildCSSBlock(".root", node, colorMappings);
|
|
114
|
+
|
|
115
|
+
} else {
|
|
116
|
+
// CSS puro — clase global, snippet CSS separado
|
|
117
|
+
code = `import React from "react";\nimport "./${cssClass}.css";\n\ninterface ${componentName}Props {\n ${propsStr || "className?: string"}\n}\n\nexport function ${componentName}({ ${propsKeys} }: ${componentName}Props) {\n return (\n <div className="${cssClass}">\n ${hasChildren ? "{children}" : `{/* contenido de ${node.name} */}`}\n </div>\n );\n}\n\nexport default ${componentName};\n`;
|
|
118
|
+
cssSnippet = buildCSSBlock(`.${cssClass}`, node, colorMappings);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { filename: `${componentName}.tsx`, code, warnings, cssSnippet };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {FigmaNode} node
|
|
126
|
+
* @param {string} componentName
|
|
127
|
+
* @param {Record<string,string>} props
|
|
128
|
+
*/
|
|
129
|
+
function generateVue(node, componentName, props) {
|
|
130
|
+
const propsBlock = Object.entries(props).map(([k,v]) => `${k}?: ${v}`).join("\n ");
|
|
131
|
+
const hasChildren = "children" in props;
|
|
132
|
+
const code = `<template>\n <div class="root">\n ${hasChildren ? "<slot />" : `<!-- contenido de ${node.name} -->`}\n </div>\n</template>\n\n<script setup lang="ts">\ninterface Props {\n ${propsBlock}\n}\ndefineProps<Props>();\n</script>\n\n<style scoped>\n.root {\n /* estilos del sistema de diseño */\n}\n</style>\n`;
|
|
133
|
+
return { filename: `${componentName}.vue`, code, warnings: /** @type {string[]} */ ([]), cssSnippet: /** @type {string|null} */ (null) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {FigmaNode} node
|
|
138
|
+
* @param {DesignProfile} profile
|
|
139
|
+
* @param {ColorMapping[]} colorMappings
|
|
140
|
+
*/
|
|
141
|
+
function generateComponent(node, profile, colorMappings) {
|
|
142
|
+
const componentName = toPascalCase(node.name) || "FigmaComponent";
|
|
143
|
+
const props = inferProps(node);
|
|
144
|
+
return profile.stack.framework === "vue"
|
|
145
|
+
? generateVue(node, componentName, props)
|
|
146
|
+
: generateReact(node, componentName, props, colorMappings, profile.stack.cssApproach);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @param {DesignProfile} profile */
|
|
150
|
+
function suggestImprovements(profile) {
|
|
151
|
+
const improvements = [];
|
|
152
|
+
if (Object.keys(profile.colors).length === 0) improvements.push({ priority: "alta", area: "Tokens de color", description: "No se detectaron tokens de color", action: "Define colores como CSS variables o en tailwind.config.js" });
|
|
153
|
+
if (!profile.stack.hasTokenFile) improvements.push({ priority: "alta", area: "Token file", description: "No existe archivo único de tokens", action: "Crea src/tokens.ts con exports de colores, tipografía y espaciado" });
|
|
154
|
+
if (profile.existingComponents.length === 0) improvements.push({ priority: "alta", area: "Componentes base", description: "No se encontró librería de componentes", action: "Crea src/components con Button, Input, Card" });
|
|
155
|
+
if (Object.keys(profile.spacing).length === 0) improvements.push({ priority: "media", area: "Espaciado", description: "No se detectaron tokens de espaciado", action: "Define escala de espaciado (4, 8, 16, 24, 32px)" });
|
|
156
|
+
if (Object.keys(profile.breakpoints).length === 0) improvements.push({ priority: "media", area: "Breakpoints", description: "No se detectaron breakpoints como tokens", action: "Define breakpoints en tailwind.config.js o custom media queries" });
|
|
157
|
+
if (profile.typography.fontFamilies.length === 0) improvements.push({ priority: "media", area: "Tipografía", description: "No se detectó fuente declarada como token", action: "Define --font-sans o fontFamily en tailwind.config.js" });
|
|
158
|
+
if (Object.keys(profile.shadows).length === 0) improvements.push({ priority: "baja", area: "Sombras", description: "No se detectaron tokens de sombra", action: "Define sombras estándar (sm, md, lg)" });
|
|
159
|
+
return improvements;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { generateComponent, suggestImprovements };
|
|
@@ -0,0 +1,247 @@
|
|
|
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 };
|
|
@@ -0,0 +1,75 @@
|
|
|
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 };
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
});
|