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,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* post-write-conventions.js — Hook PostToolUse global de Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Se activa después de Write, Edit, o MultiEdit sobre cualquier archivo.
|
|
6
|
+
* Detecta las convenciones del proyecto (dinámicamente + desde la constitución
|
|
7
|
+
* SDD-ES si existe) y valida el archivo recién modificado contra ellas.
|
|
8
|
+
*
|
|
9
|
+
* Resultado:
|
|
10
|
+
* - Violación bloqueante → exit 2 + mensaje claro
|
|
11
|
+
* - Sugerencia → exit 0 + mensaje a stderr (Claude lo ve y puede corregir)
|
|
12
|
+
* - OK → exit 0 silencioso
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
16
|
+
import { extname, basename, dirname, join, resolve } from "node:path";
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
18
|
+
|
|
19
|
+
// ── Leer evento desde stdin ────────────────────────────────────────────────
|
|
20
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
21
|
+
let raw = "";
|
|
22
|
+
rl.on("line", (l) => (raw += l + "\n"));
|
|
23
|
+
rl.on("close", () => main(raw.trim()));
|
|
24
|
+
|
|
25
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function tryRead(path) {
|
|
28
|
+
try { return readFileSync(path, "utf8"); } catch { return ""; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function tryJSON(path) {
|
|
32
|
+
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findUp(filename, from) {
|
|
36
|
+
let dir = resolve(from);
|
|
37
|
+
for (let i = 0; i < 8; i++) {
|
|
38
|
+
const candidate = join(dir, filename);
|
|
39
|
+
if (existsSync(candidate)) return candidate;
|
|
40
|
+
const parent = dirname(dir);
|
|
41
|
+
if (parent === dir) break;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findWorkspaceRoot(startFile) {
|
|
48
|
+
const dir = dirname(resolve(startFile));
|
|
49
|
+
const markers = ["package.json","pyproject.toml","Cargo.toml","go.mod","pom.xml",".git",".sdd"];
|
|
50
|
+
for (const m of markers) {
|
|
51
|
+
const p = findUp(m, dir);
|
|
52
|
+
if (p) return dirname(p);
|
|
53
|
+
}
|
|
54
|
+
return dir;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Detección dinámica de convenciones ─────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function detectConventions(root, ext) {
|
|
60
|
+
const conv = {
|
|
61
|
+
// Naming
|
|
62
|
+
namingStyle: null, // "camelCase" | "snake_case" | "PascalCase" | "kebab-case"
|
|
63
|
+
// Indentación
|
|
64
|
+
indentChar: null, // "space" | "tab"
|
|
65
|
+
indentSize: null, // 2 | 4
|
|
66
|
+
// Comillas
|
|
67
|
+
quoteStyle: null, // "single" | "double"
|
|
68
|
+
// Punto y coma
|
|
69
|
+
semicolons: null, // true | false
|
|
70
|
+
// Longitud máxima de función (desde constitución)
|
|
71
|
+
maxFunctionLines: null,
|
|
72
|
+
// Cobertura mínima (desde constitución)
|
|
73
|
+
minCoverage: null,
|
|
74
|
+
// Prohibidos (desde constitución)
|
|
75
|
+
prohibitedPatterns: [],
|
|
76
|
+
// Fuente de detección
|
|
77
|
+
sources: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ── 1. Desde constitución SDD-ES ────────────────────────────────────────
|
|
81
|
+
const constitucionPath = join(root, ".sdd", "memoria", "constitucion.md");
|
|
82
|
+
const constitucion = tryRead(constitucionPath);
|
|
83
|
+
if (constitucion) {
|
|
84
|
+
conv.sources.push("constitucion.md");
|
|
85
|
+
|
|
86
|
+
const maxFn = constitucion.match(/longitud[_\s-]*función[_\s-]*máxima[:\s]+(\d+)/i)
|
|
87
|
+
|| constitucion.match(/max[_\s-]*function[_\s-]*lines?[:\s]+(\d+)/i)
|
|
88
|
+
|| constitucion.match(/funciones?\s*[≤<=]\s*(\d+)\s*líneas/i);
|
|
89
|
+
if (maxFn) conv.maxFunctionLines = parseInt(maxFn[1]);
|
|
90
|
+
|
|
91
|
+
const cov = constitucion.match(/cobertura[_\s-]*(?:mínima|tests?)[:\s]+(\d+)/i);
|
|
92
|
+
if (cov) conv.minCoverage = parseInt(cov[1]);
|
|
93
|
+
|
|
94
|
+
// Patrones prohibidos declarados en constitución
|
|
95
|
+
const prohibited = [...constitucion.matchAll(/(?:NUNCA|PROHIBIDO|NO\s+USAR)[:\s]+`([^`]+)`/gi)];
|
|
96
|
+
conv.prohibitedPatterns = prohibited.map(m => m[1].trim());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── 2. Desde sdd.config.yaml ────────────────────────────────────────────
|
|
100
|
+
const configPath = join(root, ".sdd", "sdd.config.yaml");
|
|
101
|
+
const config = tryRead(configPath);
|
|
102
|
+
if (config) {
|
|
103
|
+
conv.sources.push("sdd.config.yaml");
|
|
104
|
+
const maxFn = config.match(/longitud_funcion_maxima:\s*(\d+)/);
|
|
105
|
+
if (maxFn && !conv.maxFunctionLines) conv.maxFunctionLines = parseInt(maxFn[1]);
|
|
106
|
+
const cov = config.match(/cobertura_tests_minima:\s*(\d+)/);
|
|
107
|
+
if (cov && !conv.minCoverage) conv.minCoverage = parseInt(cov[1]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 3. Desde ESLint ─────────────────────────────────────────────────────
|
|
111
|
+
const eslintFiles = [".eslintrc.json",".eslintrc.js",".eslintrc.yml",".eslintrc",
|
|
112
|
+
"eslint.config.js","eslint.config.mjs"];
|
|
113
|
+
for (const f of eslintFiles) {
|
|
114
|
+
const p = join(root, f);
|
|
115
|
+
if (!existsSync(p)) continue;
|
|
116
|
+
const content = tryRead(p);
|
|
117
|
+
conv.sources.push(f);
|
|
118
|
+
if (content.includes('"quotes"') || content.includes("'quotes'")) {
|
|
119
|
+
conv.quoteStyle = content.includes('"single"') || content.includes("'single'")
|
|
120
|
+
? "single" : "double";
|
|
121
|
+
}
|
|
122
|
+
if (content.includes('"semi"') || content.includes("'semi'")) {
|
|
123
|
+
conv.semicolons = !content.includes('"never"') && !content.includes("'never'");
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── 4. Desde Prettier ───────────────────────────────────────────────────
|
|
129
|
+
const prettierFiles = [".prettierrc",".prettierrc.json",".prettierrc.js",
|
|
130
|
+
".prettierrc.yml","prettier.config.js","prettier.config.mjs"];
|
|
131
|
+
for (const f of prettierFiles) {
|
|
132
|
+
const p = join(root, f);
|
|
133
|
+
if (!existsSync(p)) continue;
|
|
134
|
+
const content = tryRead(p);
|
|
135
|
+
conv.sources.push(f);
|
|
136
|
+
if (content.includes("singleQuote")) {
|
|
137
|
+
conv.quoteStyle = content.includes("true") ? "single" : "double";
|
|
138
|
+
}
|
|
139
|
+
if (content.includes("semi")) {
|
|
140
|
+
conv.semicolons = !content.includes('"semi": false') && !content.includes("semi: false");
|
|
141
|
+
}
|
|
142
|
+
if (content.includes("tabWidth")) {
|
|
143
|
+
const m = content.match(/tabWidth["\s:]+(\d)/);
|
|
144
|
+
if (m) conv.indentSize = parseInt(m[1]);
|
|
145
|
+
}
|
|
146
|
+
if (content.includes("useTabs")) {
|
|
147
|
+
conv.indentChar = content.includes("true") ? "tab" : "space";
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── 5. Desde .editorconfig ──────────────────────────────────────────────
|
|
153
|
+
const editorconfig = tryRead(join(root, ".editorconfig"));
|
|
154
|
+
if (editorconfig) {
|
|
155
|
+
conv.sources.push(".editorconfig");
|
|
156
|
+
if (!conv.indentChar) {
|
|
157
|
+
if (editorconfig.includes("indent_style = tab")) conv.indentChar = "tab";
|
|
158
|
+
if (editorconfig.includes("indent_style = space")) conv.indentChar = "space";
|
|
159
|
+
}
|
|
160
|
+
if (!conv.indentSize) {
|
|
161
|
+
const m = editorconfig.match(/indent_size\s*=\s*(\d)/);
|
|
162
|
+
if (m) conv.indentSize = parseInt(m[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── 6. Desde tsconfig.json ──────────────────────────────────────────────
|
|
167
|
+
if ([".ts",".tsx"].includes(ext)) {
|
|
168
|
+
const tsconfig = tryJSON(join(root, "tsconfig.json"));
|
|
169
|
+
if (tsconfig) {
|
|
170
|
+
conv.sources.push("tsconfig.json");
|
|
171
|
+
// strict mode implica tipos estrictos obligatorios
|
|
172
|
+
if (tsconfig.compilerOptions?.strict) conv.strictTypes = true;
|
|
173
|
+
if (tsconfig.compilerOptions?.noImplicitAny) conv.noImplicitAny = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── 7. Desde ruff.toml (Python) ─────────────────────────────────────────
|
|
178
|
+
if (ext === ".py") {
|
|
179
|
+
const ruff = tryRead(join(root, "ruff.toml")) || tryRead(join(root, ".ruff.toml"));
|
|
180
|
+
if (ruff) {
|
|
181
|
+
conv.sources.push("ruff.toml");
|
|
182
|
+
const lineLen = ruff.match(/line-length\s*=\s*(\d+)/);
|
|
183
|
+
if (lineLen) conv.maxLineLength = parseInt(lineLen[1]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── 8. Inferir desde código existente (si nada detectado aún) ───────────
|
|
188
|
+
if (!conv.namingStyle || !conv.indentChar) {
|
|
189
|
+
inferFromExistingCode(root, ext, conv);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return conv;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function inferFromExistingCode(root, ext, conv) {
|
|
196
|
+
// Busca hasta 3 archivos del mismo tipo para inferir patrones
|
|
197
|
+
const candidates = [];
|
|
198
|
+
try {
|
|
199
|
+
const dirs = ["src","lib","app","pkg","internal"];
|
|
200
|
+
for (const d of dirs) {
|
|
201
|
+
const dp = join(root, d);
|
|
202
|
+
if (!existsSync(dp)) continue;
|
|
203
|
+
const files = readdirSync(dp).filter(f => f.endsWith(ext)).slice(0, 2);
|
|
204
|
+
candidates.push(...files.map(f => join(dp, f)));
|
|
205
|
+
if (candidates.length >= 3) break;
|
|
206
|
+
}
|
|
207
|
+
} catch { /* sin acceso */ }
|
|
208
|
+
|
|
209
|
+
for (const f of candidates.slice(0, 3)) {
|
|
210
|
+
const content = tryRead(f);
|
|
211
|
+
if (!content) continue;
|
|
212
|
+
|
|
213
|
+
// Indentación: mirar primeras líneas indentadas
|
|
214
|
+
if (!conv.indentChar) {
|
|
215
|
+
const lines = content.split("\n").slice(0, 30);
|
|
216
|
+
const tabLine = lines.find(l => l.startsWith("\t"));
|
|
217
|
+
const spaceLine = lines.find(l => l.match(/^ {2,}/));
|
|
218
|
+
if (tabLine && !spaceLine) conv.indentChar = "tab";
|
|
219
|
+
else if (spaceLine && !tabLine) {
|
|
220
|
+
conv.indentChar = "space";
|
|
221
|
+
const m = spaceLine.match(/^( +)/);
|
|
222
|
+
if (m) conv.indentSize = m[1].length === 2 ? 2 : 4;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Comillas JS/TS
|
|
227
|
+
if (!conv.quoteStyle && [".js",".ts",".jsx",".tsx"].includes(ext)) {
|
|
228
|
+
const singleCount = (content.match(/'/g) || []).length;
|
|
229
|
+
const doubleCount = (content.match(/"/g) || []).length;
|
|
230
|
+
if (singleCount > doubleCount * 1.5) conv.quoteStyle = "single";
|
|
231
|
+
else if (doubleCount > singleCount * 1.5) conv.quoteStyle = "double";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
conv.sources.push(`inferido de ${basename(f)}`);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Validación del archivo ─────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
function validate(filePath, content, conv, ext) {
|
|
242
|
+
const errors = []; // bloqueantes
|
|
243
|
+
const warnings = []; // sugerencias
|
|
244
|
+
|
|
245
|
+
const lines = content.split("\n");
|
|
246
|
+
const name = basename(filePath);
|
|
247
|
+
|
|
248
|
+
// ── Indentación ─────────────────────────────────────────────────────────
|
|
249
|
+
if (conv.indentChar === "tab") {
|
|
250
|
+
const spacedLines = lines.filter((l, i) => i < 100 && /^ {2,}/.test(l) && !/^\s*\*/.test(l));
|
|
251
|
+
if (spacedLines.length > 3) {
|
|
252
|
+
errors.push(`indentación: el proyecto usa TABS pero se encontraron ${spacedLines.length} líneas con espacios`);
|
|
253
|
+
}
|
|
254
|
+
} else if (conv.indentChar === "space") {
|
|
255
|
+
const tabbedLines = lines.filter((l, i) => i < 100 && l.startsWith("\t"));
|
|
256
|
+
if (tabbedLines.length > 3) {
|
|
257
|
+
errors.push(`indentación: el proyecto usa ESPACIOS pero se encontraron ${tabbedLines.length} líneas con tabs`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Comillas JS/TS ──────────────────────────────────────────────────────
|
|
262
|
+
if (conv.quoteStyle && [".js",".ts",".jsx",".tsx",".mjs",".cjs"].includes(ext)) {
|
|
263
|
+
const wrong = conv.quoteStyle === "single"
|
|
264
|
+
? lines.filter((l,i) => i < 200 && /"[^"]*"/.test(l) && !l.includes("//") && !l.trim().startsWith("*")).length
|
|
265
|
+
: lines.filter((l,i) => i < 200 && /'[^']*'/.test(l) && !l.includes("//") && !l.trim().startsWith("*")).test;
|
|
266
|
+
if (wrong > 5) {
|
|
267
|
+
const expected = conv.quoteStyle === "single" ? "comillas simples" : "comillas dobles";
|
|
268
|
+
warnings.push(`comillas: el proyecto usa ${expected} pero se encontraron ${wrong} líneas con el estilo opuesto`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Longitud de funciones ───────────────────────────────────────────────
|
|
273
|
+
if (conv.maxFunctionLines) {
|
|
274
|
+
const fnStarts = [];
|
|
275
|
+
lines.forEach((l, i) => {
|
|
276
|
+
if (/^\s*(export\s+)?(async\s+)?function\s+\w|=>\s*\{|^\s*(public|private|protected|async)\s+\w+\s*\(/.test(l)) {
|
|
277
|
+
fnStarts.push(i);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
for (const start of fnStarts.slice(0, 20)) {
|
|
282
|
+
let depth = 0, end = start;
|
|
283
|
+
for (let i = start; i < Math.min(start + conv.maxFunctionLines * 2, lines.length); i++) {
|
|
284
|
+
depth += (lines[i].match(/\{/g) || []).length;
|
|
285
|
+
depth -= (lines[i].match(/\}/g) || []).length;
|
|
286
|
+
if (depth <= 0 && i > start) { end = i; break; }
|
|
287
|
+
}
|
|
288
|
+
const len = end - start;
|
|
289
|
+
if (len > conv.maxFunctionLines) {
|
|
290
|
+
warnings.push(`función en línea ${start + 1}: ${len} líneas > límite de ${conv.maxFunctionLines} (constitución)`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Patrones prohibidos (desde constitución) ────────────────────────────
|
|
296
|
+
for (const pattern of conv.prohibitedPatterns) {
|
|
297
|
+
if (content.includes(pattern)) {
|
|
298
|
+
errors.push(`patrón prohibido en constitución: \`${pattern}\` encontrado en el archivo`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── console.log / print de debug en archivos no-test ───────────────────
|
|
303
|
+
const isTest = /\.(test|spec)\.|__tests__|test_/.test(filePath);
|
|
304
|
+
if (!isTest) {
|
|
305
|
+
if ([".ts",".js",".tsx",".jsx"].includes(ext)) {
|
|
306
|
+
const debugLines = lines.filter((l,i) => /console\.(log|warn|error|debug|info)\(/.test(l) && !l.trim().startsWith("//")).length;
|
|
307
|
+
if (debugLines > 2) {
|
|
308
|
+
warnings.push(`${debugLines} llamadas a console.log/warn/error en archivo no-test — ¿son logs de producción o debug olvidado?`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (ext === ".py") {
|
|
312
|
+
const printLines = lines.filter(l => /^\s*print\s*\(/.test(l) && !l.trim().startsWith("#")).length;
|
|
313
|
+
if (printLines > 2) {
|
|
314
|
+
warnings.push(`${printLines} llamadas a print() en archivo no-test — usa logging en lugar de print en producción`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── TypeScript: any implícito ───────────────────────────────────────────
|
|
320
|
+
if (conv.noImplicitAny || conv.strictTypes) {
|
|
321
|
+
if ([".ts",".tsx"].includes(ext)) {
|
|
322
|
+
const anyCount = lines.filter(l => /:\s*any\b/.test(l) && !l.trim().startsWith("//")).length;
|
|
323
|
+
if (anyCount > 0) {
|
|
324
|
+
warnings.push(`${anyCount} usos de \`any\` en TypeScript estricto — reemplaza con tipos concretos`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Secretos hardcodeados ───────────────────────────────────────────────
|
|
330
|
+
const secretPatterns = [
|
|
331
|
+
{ re: /password\s*=\s*['"][^'"]{4,}/i, label: "password hardcodeado" },
|
|
332
|
+
{ re: /secret\s*=\s*['"][^'"]{8,}/i, label: "secret hardcodeado" },
|
|
333
|
+
{ re: /api[_-]?key\s*=\s*['"][^'"]{8,}/i, label: "API key hardcodeada" },
|
|
334
|
+
{ re: /sk-[a-zA-Z0-9]{20,}/, label: "OpenAI key" },
|
|
335
|
+
{ re: /ghp_[a-zA-Z0-9]{36}/, label: "GitHub PAT" },
|
|
336
|
+
{ re: /AKIA[0-9A-Z]{16}/, label: "AWS Access Key" },
|
|
337
|
+
{ re: /BEGIN (RSA|EC|OPENSSH) PRIVATE KEY/, label: "clave privada" },
|
|
338
|
+
];
|
|
339
|
+
for (const { re, label } of secretPatterns) {
|
|
340
|
+
if (re.test(content)) {
|
|
341
|
+
errors.push(`${label} detectado en el archivo — usa variables de entorno`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Archivos de test: verificar que tienen al menos una aserción ─────────
|
|
346
|
+
if (isTest) {
|
|
347
|
+
const hasAssertion = /expect\(|assert\.|assertEquals|pytest\.raises|#\[test\]|func Test/.test(content);
|
|
348
|
+
if (!hasAssertion) {
|
|
349
|
+
warnings.push("archivo de test sin aserciones detectadas — ¿el test verifica algo real?");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { errors, warnings };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
function main(raw) {
|
|
359
|
+
let event;
|
|
360
|
+
try { event = JSON.parse(raw); } catch { process.exit(0); }
|
|
361
|
+
|
|
362
|
+
const toolName = event?.tool_name ?? "";
|
|
363
|
+
if (!["Write", "Edit", "MultiEdit"].includes(toolName)) process.exit(0);
|
|
364
|
+
|
|
365
|
+
// Extraer ruta del archivo
|
|
366
|
+
const filePath = event?.tool_input?.file_path
|
|
367
|
+
?? event?.tool_input?.path
|
|
368
|
+
?? event?.tool_response?.file_path
|
|
369
|
+
?? "";
|
|
370
|
+
|
|
371
|
+
if (!filePath) process.exit(0);
|
|
372
|
+
|
|
373
|
+
const ext = extname(filePath).toLowerCase();
|
|
374
|
+
|
|
375
|
+
// Solo validar archivos de código fuente
|
|
376
|
+
const CODE_EXTS = new Set([
|
|
377
|
+
".ts",".tsx",".js",".jsx",".mjs",".cjs",
|
|
378
|
+
".py",".rs",".go",".java",".kt",".cs",
|
|
379
|
+
".rb",".php",".swift",".cpp",".c",".h",
|
|
380
|
+
]);
|
|
381
|
+
if (!CODE_EXTS.has(ext)) process.exit(0);
|
|
382
|
+
|
|
383
|
+
const content = tryRead(filePath);
|
|
384
|
+
if (!content || content.length < 10) process.exit(0);
|
|
385
|
+
|
|
386
|
+
const root = findWorkspaceRoot(filePath);
|
|
387
|
+
const conv = detectConventions(root, ext);
|
|
388
|
+
const { errors, warnings } = validate(filePath, content, conv, ext);
|
|
389
|
+
|
|
390
|
+
if (errors.length === 0 && warnings.length === 0) process.exit(0);
|
|
391
|
+
|
|
392
|
+
const relPath = filePath.replace(root, "").replace(/^[/\\]/, "");
|
|
393
|
+
const sources = conv.sources.length ? `(fuentes: ${conv.sources.slice(0,3).join(", ")})` : "";
|
|
394
|
+
|
|
395
|
+
let msg = `\n── Validación de convenciones: ${relPath} ${sources}\n`;
|
|
396
|
+
|
|
397
|
+
if (errors.length > 0) {
|
|
398
|
+
msg += `\n🔴 VIOLACIONES BLOQUEANTES (corregir antes de continuar):\n`;
|
|
399
|
+
errors.forEach((e, i) => { msg += ` ${i+1}. ${e}\n`; });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (warnings.length > 0) {
|
|
403
|
+
msg += `\n🟡 SUGERENCIAS (revisar antes del merge):\n`;
|
|
404
|
+
warnings.forEach((w, i) => { msg += ` ${i+1}. ${w}\n`; });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
msg += "\n";
|
|
408
|
+
process.stderr.write(msg);
|
|
409
|
+
|
|
410
|
+
// Errores → bloquear; solo warnings → dejar pasar (Claude los ve y puede corregir)
|
|
411
|
+
process.exit(errors.length > 0 ? 2 : 0);
|
|
412
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pre-tool-guard.js — Hook PreToolUse global de Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Bloquea operaciones destructivas o fuera del workspace antes
|
|
6
|
+
* de que Claude Code las ejecute. Se registra en settings.json
|
|
7
|
+
* bajo "hooks.PreToolUse".
|
|
8
|
+
*
|
|
9
|
+
* Protocolo de hooks de Claude Code:
|
|
10
|
+
* - Lee el evento JSON desde stdin
|
|
11
|
+
* - Exit 0 → permitir
|
|
12
|
+
* - Exit 2 → bloquear (Claude Code muestra el stderr al usuario)
|
|
13
|
+
* - Stderr → mensaje de error mostrado al usuario
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createInterface } from "node:readline";
|
|
17
|
+
|
|
18
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
19
|
+
let raw = "";
|
|
20
|
+
rl.on("line", (l) => (raw += l + "\n"));
|
|
21
|
+
rl.on("close", () => main(raw.trim()));
|
|
22
|
+
|
|
23
|
+
// ── Comandos prohibidos — bloqueo duro, sin confirmación ───────────────────
|
|
24
|
+
const PROHIBIDOS = [
|
|
25
|
+
// Eliminación masiva
|
|
26
|
+
/rm\s+-rf?\s+\/(?!\w)/, // rm -rf / (raíz)
|
|
27
|
+
/rm\s+-rf?\s+~\b/, // rm -rf ~
|
|
28
|
+
/rm\s+-rf?\s+\.\s*$/, // rm -rf . (cwd)
|
|
29
|
+
/rm\s+-rf?\s+\.\./, // rm -rf ..
|
|
30
|
+
/Remove-Item\s+.*-Recurse.*-Force\s+[/\\]/, // PowerShell rm raíz
|
|
31
|
+
|
|
32
|
+
// Git destructivo remoto
|
|
33
|
+
/git\s+push\s+.*--force(?!-with-lease)/, // push --force (no --force-with-lease)
|
|
34
|
+
/git\s+push\s+-f\b/,
|
|
35
|
+
/git\s+push\s+\w+\s+:\w+/, // git push origin :rama (borrar rama remota)
|
|
36
|
+
|
|
37
|
+
// Git destructivo local irreversible
|
|
38
|
+
/git\s+reset\s+--hard/,
|
|
39
|
+
/git\s+clean\s+.*-[xf]{1,3}d/,
|
|
40
|
+
/git\s+reflog\s+expire/,
|
|
41
|
+
/git\s+gc\s+.*--prune=now/,
|
|
42
|
+
|
|
43
|
+
// Base de datos
|
|
44
|
+
/DROP\s+DATABASE\b/i,
|
|
45
|
+
/DROP\s+SCHEMA\b/i,
|
|
46
|
+
|
|
47
|
+
// Credenciales en git config
|
|
48
|
+
/git\s+config\s+.*password/i,
|
|
49
|
+
/git\s+config\s+.*credential.*store/i,
|
|
50
|
+
|
|
51
|
+
// Operaciones fuera del workspace — acceso a rutas de sistema
|
|
52
|
+
/rm\s+.*\/etc\//,
|
|
53
|
+
/rm\s+.*\/usr\//,
|
|
54
|
+
/rm\s+.*\/bin\//,
|
|
55
|
+
/rm\s+.*C:\\Windows\\/i,
|
|
56
|
+
/rm\s+.*C:\\Program Files\\/i,
|
|
57
|
+
|
|
58
|
+
// Exposición de secrets
|
|
59
|
+
/cat\s+.*\.env(?!\.example|\.template)/,
|
|
60
|
+
/type\s+.*\.env(?!\.example|\.template)/i, // Windows
|
|
61
|
+
/Get-Content\s+.*\.env(?!\.example|\.template)/i,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// ── Operaciones que requieren confirmación explícita ───────────────────────
|
|
65
|
+
// (Para estas Claude Code debe preguntar al usuario — no las bloqueamos aquí
|
|
66
|
+
// porque el hook PreToolUse no puede hacer interacción. Las marcamos en stderr
|
|
67
|
+
// con un aviso y exit 0 para que Claude Code muestre la advertencia antes de pedir
|
|
68
|
+
// permiso al usuario mediante el flujo normal de permisos.)
|
|
69
|
+
const ADVERTENCIAS = [
|
|
70
|
+
{ re: /git\s+push\b/, msg: "git push — sube código al remoto" },
|
|
71
|
+
{ re: /git\s+merge\b/, msg: "git merge — modifica historial" },
|
|
72
|
+
{ re: /git\s+rebase\b/, msg: "git rebase — reescribe historial" },
|
|
73
|
+
{ re: /git\s+reset\b/, msg: "git reset — descarta cambios" },
|
|
74
|
+
{ re: /git\s+branch\s+-D\b/, msg: "git branch -D — borra rama local" },
|
|
75
|
+
{ re: /DROP\s+TABLE\b/i, msg: "DROP TABLE — elimina tabla de BD" },
|
|
76
|
+
{ re: /DELETE\s+FROM\b/i, msg: "DELETE FROM — elimina filas de BD" },
|
|
77
|
+
{ re: /TRUNCATE\b/i, msg: "TRUNCATE — vacía tabla de BD" },
|
|
78
|
+
{ re: /npm\s+publish\b/, msg: "npm publish — publica al registro público" },
|
|
79
|
+
{ re: /terraform\s+apply\b/, msg: "terraform apply — modifica infraestructura real" },
|
|
80
|
+
{ re: /terraform\s+destroy\b/, msg: "terraform destroy — destruye infraestructura" },
|
|
81
|
+
{ re: /kubectl\s+delete\b/, msg: "kubectl delete — elimina recursos de k8s" },
|
|
82
|
+
{ re: /helm\s+uninstall\b/, msg: "helm uninstall — elimina release de k8s" },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// ── Patrones de secrets hardcodeados ──────────────────────────────────────
|
|
86
|
+
// Detecta si el comando intenta escribir un secret literal en un archivo
|
|
87
|
+
const SECRET_PATTERNS = [
|
|
88
|
+
/password\s*=\s*['"][^'"]{4,}/i,
|
|
89
|
+
/secret\s*=\s*['"][^'"]{8,}/i,
|
|
90
|
+
/api[_-]?key\s*=\s*['"][^'"]{8,}/i,
|
|
91
|
+
/token\s*=\s*['"][^'"]{10,}/i,
|
|
92
|
+
/sk-[a-zA-Z0-9]{20,}/, // OpenAI key
|
|
93
|
+
/xox[baprs]-[0-9]{10,}/, // Slack token
|
|
94
|
+
/ghp_[a-zA-Z0-9]{36}/, // GitHub PAT
|
|
95
|
+
/AKIA[0-9A-Z]{16}/, // AWS Access Key
|
|
96
|
+
/BEGIN (RSA|EC|OPENSSH) PRIVATE KEY/,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
function main(raw) {
|
|
100
|
+
let event;
|
|
101
|
+
try {
|
|
102
|
+
event = JSON.parse(raw);
|
|
103
|
+
} catch {
|
|
104
|
+
// Si no podemos parsear, dejamos pasar (no bloquear por error del hook)
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const toolName = event?.tool_name ?? "";
|
|
109
|
+
const toolInput = event?.tool_input ?? {};
|
|
110
|
+
|
|
111
|
+
// Solo nos importan Bash y PowerShell
|
|
112
|
+
if (toolName !== "Bash" && toolName !== "PowerShell") {
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const cmd = String(toolInput?.command ?? "").trim();
|
|
117
|
+
if (!cmd) process.exit(0);
|
|
118
|
+
|
|
119
|
+
// ── 1. Verificar prohibidos ─────────────────────────────────────────────
|
|
120
|
+
for (const re of PROHIBIDOS) {
|
|
121
|
+
if (re.test(cmd)) {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`🚫 BLOQUEADO por guardia de seguridad SDD-ES\n` +
|
|
124
|
+
` Comando: ${cmd.slice(0, 120)}\n` +
|
|
125
|
+
` Razón: coincide con patrón prohibido (${re.source.slice(0, 60)})\n` +
|
|
126
|
+
` Esta operación nunca debe ejecutarse automáticamente.\n`
|
|
127
|
+
);
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── 2. Verificar secrets hardcodeados en el comando ────────────────────
|
|
133
|
+
for (const re of SECRET_PATTERNS) {
|
|
134
|
+
if (re.test(cmd)) {
|
|
135
|
+
process.stderr.write(
|
|
136
|
+
`🚫 BLOQUEADO — secret hardcodeado detectado en el comando\n` +
|
|
137
|
+
` Patrón: ${re.source.slice(0, 60)}\n` +
|
|
138
|
+
` Usa variables de entorno o un secret manager.\n`
|
|
139
|
+
);
|
|
140
|
+
process.exit(2);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── 3. Advertencias (no bloquean, pero se loguean) ─────────────────────
|
|
145
|
+
for (const { re, msg } of ADVERTENCIAS) {
|
|
146
|
+
if (re.test(cmd)) {
|
|
147
|
+
// Escribir a stderr — Claude Code lo muestra como contexto antes de pedir permiso
|
|
148
|
+
process.stderr.write(
|
|
149
|
+
`⚠️ Operación sensible detectada: ${msg}\n` +
|
|
150
|
+
` Comando: ${cmd.slice(0, 120)}\n`
|
|
151
|
+
);
|
|
152
|
+
// Exit 0 — dejamos que el flujo normal de permisos de Claude Code actúe
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Todo bien
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|