claude-git-hooks 1.1.0 → 1.2.4
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/CHANGELOG.md +131 -0
- package/LICENSE +0 -0
- package/README.md +57 -36
- package/bin/claude-hooks +567 -57
- package/package.json +2 -1
- package/templates/CLAUDE_PRE_COMMIT.md +2 -1
- package/templates/CLAUDE_PRE_COMMIT_SONAR.md +12 -2
- package/templates/pre-commit +33 -25
- package/templates/prepare-commit-msg +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Todos los cambios notables en este proyecto se documentarán en este archivo.
|
|
4
|
+
|
|
5
|
+
El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.2.4] - 2025-08-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- 🐛 Corregido el análisis de SonarQube para mostrar correctamente el formato detallado
|
|
12
|
+
- 📊 Arreglado el parsing de JSON para buscar `QUALITY_GATE` en mayúsculas según las pautas
|
|
13
|
+
- 🔧 Actualizado el mapeo de campos JSON para coincidir con la estructura esperada
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- 📈 Añadidas métricas de coverage, duplications y complexity al formato SonarQube
|
|
17
|
+
- 📝 Actualizado el archivo de pautas SonarQube para incluir todas las métricas
|
|
18
|
+
|
|
19
|
+
## [1.2.3] - 2025-08-22
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- 🚀 Actualización automática de `.gitignore` durante la instalación
|
|
23
|
+
- 📝 Claude Hooks ahora agrega automáticamente las entradas necesarias a `.gitignore`
|
|
24
|
+
- 🔍 El comando `status` ahora muestra el estado de las entradas en `.gitignore`
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- 🎯 Mejorado el proceso de instalación para ser más completo y automatizado
|
|
28
|
+
- 📊 El comando `status` ahora proporciona información más detallada sobre la configuración
|
|
29
|
+
|
|
30
|
+
## [1.2.2] - 2025-08-22
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- 🐛 Corregido problema donde los archivos markdown de pautas no se instalaban correctamente desde npm
|
|
34
|
+
- 📁 Los archivos de pautas ahora se instalan en el directorio `.claude/` en lugar de la raíz del proyecto
|
|
35
|
+
- 🔧 Actualizado el hook pre-commit para buscar los archivos de pautas en la nueva ubicación `.claude/`
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- 📂 Los archivos `CLAUDE_PRE_COMMIT.md` y `CLAUDE_PRE_COMMIT_SONAR.md` ahora se almacenan en `.claude/`
|
|
39
|
+
- 🎯 Mejorada la organización del proyecto manteniendo los archivos de configuración separados del código fuente
|
|
40
|
+
- 📝 Actualizada la documentación para reflejar la nueva estructura de directorios
|
|
41
|
+
|
|
42
|
+
## [1.2.1] - 2024-07-24
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- 🐛 Corregido problema de bloqueo del spinner durante verificación de autenticación Claude
|
|
46
|
+
- 🎨 Arreglado sistema de entretenimiento para mostrar correctamente spinner, chistes y countdown
|
|
47
|
+
- 🔧 Solucionado problema de líneas duplicadas en consola después del segundo chiste
|
|
48
|
+
- 🧹 Mejorada limpieza de pantalla para evitar superposición de texto al finalizar spinner
|
|
49
|
+
- ⏱️ Aumentado timeout de verificación de autenticación a 2 minutos
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
- 🔄 Reemplazado `execSync` por `spawn` en verificación de autenticación para evitar bloqueos
|
|
53
|
+
- 📍 Mejorado sistema de posicionamiento del cursor para renderizado consistente
|
|
54
|
+
- 🎯 Optimizada lógica de actualización del spinner para evitar race conditions
|
|
55
|
+
|
|
56
|
+
### Technical
|
|
57
|
+
- 🏗️ Uso de `spawn` con `stdio: 'ignore'` para mantener compatibilidad con comportamiento original
|
|
58
|
+
- 🎮 Sistema de reserva de espacio (3 líneas) para evitar problemas de renderizado
|
|
59
|
+
- 🧮 Contador de chistes cada 10 segundos real (sin drift de tiempo)
|
|
60
|
+
- 🔍 Limpieza precisa línea por línea al finalizar para evitar artefactos visuales
|
|
61
|
+
|
|
62
|
+
## [1.2.0] - 2024-07-24
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
- ✨ Sistema de entretenimiento con spinner animado y chistes durante operaciones largas
|
|
66
|
+
- 🔐 Lectura segura y no-persistente de contraseña sudo para instalación automática
|
|
67
|
+
- 🛠️ Verificación completa de dependencias del sistema (jq, curl, herramientas Unix)
|
|
68
|
+
- 🚀 Instalación automática de dependencias faltantes con contraseña sudo
|
|
69
|
+
- ⚙️ Configuración automática de Git (line endings para WSL/Windows)
|
|
70
|
+
- 🎭 Chistes rotativos cada 10 segundos durante verificación de autenticación Claude
|
|
71
|
+
|
|
72
|
+
### Enhanced
|
|
73
|
+
- 📦 Función `install` ahora incluye verificación e instalación completa de dependencias
|
|
74
|
+
- 🔍 Verificación de autenticación Claude con entretenimiento visual
|
|
75
|
+
- 💻 Detección inteligente de plataforma (Linux/macOS) para instalación de paquetes
|
|
76
|
+
|
|
77
|
+
### Removed
|
|
78
|
+
- 🗑️ Eliminados archivos `setup-wsl.sh` y `setup-wsl.js` (redundantes)
|
|
79
|
+
- 🧹 Simplificación de la arquitectura eliminando duplicación de código
|
|
80
|
+
|
|
81
|
+
### Technical
|
|
82
|
+
- 🔧 Añadido módulo `https` para obtener chistes de API externa
|
|
83
|
+
- 🎨 Clase `Entertainment` para manejo de spinner y chistes
|
|
84
|
+
- 🔐 Funciones `readPassword()` y `testSudoPassword()` para manejo seguro de credenciales
|
|
85
|
+
- ⚡ Conversión a funciones async/await para mejor manejo de procesos asíncronos
|
|
86
|
+
|
|
87
|
+
## [1.1.0] - 2024-07-24
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
- 🎯 Comando `set-mode` para cambiar entre análisis estándar y SonarQube
|
|
91
|
+
- 📊 Modo SonarQube con métricas detalladas y quality gate
|
|
92
|
+
- 📈 Visualización del modo de análisis actual en comando `status`
|
|
93
|
+
- 🔧 Configuración persistente del modo de análisis en archivo `.claude-analysis-mode`
|
|
94
|
+
|
|
95
|
+
### Enhanced
|
|
96
|
+
- 📋 Comando `status` ahora muestra modo de análisis y archivos de pautas
|
|
97
|
+
- 📚 Documentación mejorada con ejemplos de ambos modos
|
|
98
|
+
- 🎨 Mejor formateo de salida en comandos informativos
|
|
99
|
+
|
|
100
|
+
### Technical
|
|
101
|
+
- 📁 Detección automática de archivos de pautas (CLAUDE_PRE_COMMIT.md, CLAUDE_PRE_COMMIT_SONAR.md)
|
|
102
|
+
- 🏗️ Estructura preparada para múltiples modos de análisis
|
|
103
|
+
|
|
104
|
+
## [1.0.0] - 2024-07-24
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
- 🎉 Primera versión estable del paquete npm `claude-git-hooks`
|
|
108
|
+
- 📦 CLI global `claude-hooks` para gestión de hooks de Git
|
|
109
|
+
- 🪝 Instalación automática de hooks `pre-commit` y `prepare-commit-msg`
|
|
110
|
+
- 🔍 Verificación de dependencias (Claude CLI, jq)
|
|
111
|
+
- 📋 Comandos principales: `install`, `uninstall`, `enable`, `disable`, `status`
|
|
112
|
+
- 📖 Sistema de ayuda completo con ejemplos
|
|
113
|
+
- 🎛️ Gestión individual de hooks (habilitar/deshabilitar por separado)
|
|
114
|
+
- 📂 Creación automática de archivos de pautas si no existen
|
|
115
|
+
- 🔄 Sistema de backup automático de hooks existentes
|
|
116
|
+
|
|
117
|
+
### Technical
|
|
118
|
+
- 🏗️ Arquitectura modular con funciones especializadas
|
|
119
|
+
- 🎨 Sistema de colores para output legible
|
|
120
|
+
- 🛡️ Validaciones de entorno (repositorio Git, dependencias)
|
|
121
|
+
- 📝 Documentación completa en README
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Tipos de cambios
|
|
126
|
+
- `Added` - para nuevas funcionalidades
|
|
127
|
+
- `Changed` - para cambios en funcionalidades existentes
|
|
128
|
+
- `Deprecated` - para funcionalidades que serán removidas
|
|
129
|
+
- `Removed` - para funcionalidades removidas
|
|
130
|
+
- `Fixed` - para corrección de bugs
|
|
131
|
+
- `Security` - para vulnerabilidades corregidas
|
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -7,10 +7,9 @@ Este directorio contiene un pre-commit hook que utiliza Claude CLI para revisar
|
|
|
7
7
|
- **`README.md`** - Esta documentación
|
|
8
8
|
- **`pre-commit`** - Hook principal para análisis de código (con auto-actualización)
|
|
9
9
|
- **`prepare-commit-msg`** - Hook para generar mensajes de commit automáticos
|
|
10
|
-
- **`setup-wsl.sh`** - Script de instalación inicial para WSL
|
|
11
10
|
- **`.gitattributes`** - Configuración para mantener line endings correctos
|
|
12
|
-
-
|
|
13
|
-
-
|
|
11
|
+
- **`.claude/CLAUDE_PRE_COMMIT.md`** - Pautas de evaluación formato estándar
|
|
12
|
+
- **`.claude/CLAUDE_PRE_COMMIT_SONAR.md`** - Pautas de evaluación formato SonarQube
|
|
14
13
|
|
|
15
14
|
## 🔧 Configuración Previa Importante
|
|
16
15
|
|
|
@@ -51,28 +50,32 @@ git config --global credential.helper store
|
|
|
51
50
|
|
|
52
51
|
## 🚀 Instalación
|
|
53
52
|
|
|
54
|
-
###
|
|
53
|
+
### Paquete NPM Global (Recomendado)
|
|
55
54
|
|
|
56
55
|
**IMPORTANTE**: Claude CLI corre en WSL, por lo que toda la instalación y uso de git debe hacerse desde la terminal WSL.
|
|
57
56
|
|
|
58
|
-
#### Instalación Global
|
|
59
|
-
|
|
60
57
|
```bash
|
|
61
58
|
# Instalar el paquete globalmente
|
|
62
|
-
|
|
59
|
+
npm install -g claude-git-hooks
|
|
63
60
|
|
|
64
61
|
# En cualquier repositorio, instalar los hooks
|
|
65
62
|
cd tu-proyecto
|
|
66
63
|
claude-hooks install
|
|
67
64
|
```
|
|
68
65
|
|
|
66
|
+
El comando `claude-hooks install` ahora incluye:
|
|
67
|
+
- ✅ Verificación completa de dependencias del sistema
|
|
68
|
+
- ✅ Instalación automática de paquetes faltantes (jq, curl)
|
|
69
|
+
- ✅ Configuración de Git (line endings WSL/Windows)
|
|
70
|
+
- ✅ Verificación de autenticación Claude con entretenimiento
|
|
71
|
+
- ✅ Instalación de hooks y archivos de pautas
|
|
72
|
+
- ✅ Actualización automática de .gitignore con archivos de Claude
|
|
73
|
+
|
|
69
74
|
#### Añadir como Dependencia de Desarrollo
|
|
70
75
|
|
|
71
76
|
```bash
|
|
72
77
|
# Instalar como devDependency
|
|
73
78
|
npm install --save-dev claude-git-hooks
|
|
74
|
-
|
|
75
|
-
# Añadir script al package.json
|
|
76
79
|
```
|
|
77
80
|
|
|
78
81
|
Luego añade esto a tu `package.json`:
|
|
@@ -83,30 +86,11 @@ Luego añade esto a tu `package.json`:
|
|
|
83
86
|
"postinstall": "claude-hooks install"
|
|
84
87
|
},
|
|
85
88
|
"devDependencies": {
|
|
86
|
-
"claude-git-hooks": "^1.
|
|
89
|
+
"claude-git-hooks": "^1.2.0"
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
### Opción 2: Instalación Local con Scripts
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
# Desde WSL - Instalación completa
|
|
95
|
-
./git-hooks/setup-wsl.sh
|
|
96
|
-
|
|
97
|
-
# Ver opciones disponibles
|
|
98
|
-
./git-hooks/setup-wsl.sh --help
|
|
99
|
-
|
|
100
|
-
# Solo sincronizar hooks (útil después de modificar archivos fuente)
|
|
101
|
-
./git-hooks/setup-wsl.sh --sync
|
|
102
|
-
|
|
103
|
-
# Habilitar/deshabilitar hooks
|
|
104
|
-
./git-hooks/setup-wsl.sh --enable-hooks # Habilita todos los hooks
|
|
105
|
-
./git-hooks/setup-wsl.sh --disable-hooks # Deshabilita todos los hooks
|
|
106
|
-
./git-hooks/setup-wsl.sh --enable-hooks pre-commit # Habilita solo pre-commit
|
|
107
|
-
./git-hooks/setup-wsl.sh --disable-hooks prepare-commit-msg # Deshabilita solo prepare-commit-msg
|
|
108
|
-
```
|
|
109
|
-
|
|
110
94
|
## 🤖 Características
|
|
111
95
|
|
|
112
96
|
**✨ Auto-actualización incorporada**: Los hooks se actualizan automáticamente en cada commit.
|
|
@@ -116,6 +100,37 @@ Luego añade esto a tu `package.json`:
|
|
|
116
100
|
- `pre-commit`: Análisis de código con Claude (solo archivos Java/config)
|
|
117
101
|
- `prepare-commit-msg`: Generación automática de mensajes de commit
|
|
118
102
|
|
|
103
|
+
## 📁 Gestión de Archivos
|
|
104
|
+
|
|
105
|
+
### Archivos creados durante la instalación
|
|
106
|
+
|
|
107
|
+
El comando `claude-hooks install` crea los siguientes archivos y directorios:
|
|
108
|
+
|
|
109
|
+
1. **`.git/hooks/pre-commit`** - Hook de análisis de código
|
|
110
|
+
2. **`.git/hooks/prepare-commit-msg`** - Hook de generación de mensajes
|
|
111
|
+
3. **`.claude/`** - Directorio para archivos de configuración
|
|
112
|
+
- `CLAUDE_PRE_COMMIT.md` - Pautas de evaluación estándar
|
|
113
|
+
- `CLAUDE_PRE_COMMIT_SONAR.md` - Pautas de evaluación SonarQube
|
|
114
|
+
4. **`.claude-analysis-mode`** - Archivo de preferencia de modo (creado al primer uso)
|
|
115
|
+
|
|
116
|
+
### Actualización automática de .gitignore
|
|
117
|
+
|
|
118
|
+
Durante la instalación, Claude Hooks actualiza automáticamente tu `.gitignore` para incluir:
|
|
119
|
+
|
|
120
|
+
```gitignore
|
|
121
|
+
# Claude Git Hooks
|
|
122
|
+
.claude/
|
|
123
|
+
.claude-analysis-mode
|
|
124
|
+
debug-claude-response.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Esto asegura que:
|
|
128
|
+
- Los archivos de configuración de Claude específicos del proyecto no se suban al repositorio
|
|
129
|
+
- Los archivos de debug temporales se ignoren
|
|
130
|
+
- Cada desarrollador pueda tener sus propias preferencias de análisis
|
|
131
|
+
|
|
132
|
+
Si no existe un `.gitignore`, se creará uno nuevo. Si ya existe, las entradas se agregarán al final solo si no están presentes.
|
|
133
|
+
|
|
119
134
|
## 🎯 Funcionamiento
|
|
120
135
|
|
|
121
136
|
### Hook pre-commit (Análisis de código)
|
|
@@ -129,7 +144,7 @@ Luego añade esto a tu `package.json`:
|
|
|
129
144
|
- **Estándar**: Formato clásico con score y recomendaciones detalladas
|
|
130
145
|
- **SonarQube**: Simula salida de SonarQube con Quality Gate, métricas y clasificación de issues
|
|
131
146
|
4. **Construye prompt inteligente**:
|
|
132
|
-
- Lee las pautas desde
|
|
147
|
+
- Lee las pautas desde `.claude/CLAUDE_PRE_COMMIT.md` o `.claude/CLAUDE_PRE_COMMIT_SONAR.md`
|
|
133
148
|
- Incluye el diff completo para archivos nuevos
|
|
134
149
|
- Muestra solo cambios para archivos existentes
|
|
135
150
|
5. **Envía a Claude CLI para revisión**
|
|
@@ -301,8 +316,8 @@ En el archivo `pre-commit`:
|
|
|
301
316
|
- **`MAX_FILE_SIZE`**: Tamaño máximo de archivo a analizar (default: 100KB)
|
|
302
317
|
- **`MAX_FILES`**: Número máximo de archivos por commit (default: 10)
|
|
303
318
|
- **`CLAUDE_CLI`**: Comando de Claude CLI (default: "claude")
|
|
304
|
-
- **`GUIDELINES_FILE`**: Archivo de pautas para modo estándar (default: "CLAUDE_PRE_COMMIT.md")
|
|
305
|
-
- **`GUIDELINES_FILE_SONAR`**: Archivo de pautas para modo SonarQube (default: "CLAUDE_PRE_COMMIT_SONAR.md")
|
|
319
|
+
- **`GUIDELINES_FILE`**: Archivo de pautas para modo estándar (default: ".claude/CLAUDE_PRE_COMMIT.md")
|
|
320
|
+
- **`GUIDELINES_FILE_SONAR`**: Archivo de pautas para modo SonarQube (default: ".claude/CLAUDE_PRE_COMMIT_SONAR.md")
|
|
306
321
|
|
|
307
322
|
En el archivo `prepare-commit-msg`:
|
|
308
323
|
|
|
@@ -368,7 +383,11 @@ Este problema suele ocurrir por conflictos de configuración de line endings:
|
|
|
368
383
|
|
|
369
384
|
## 📝 Personalización
|
|
370
385
|
|
|
371
|
-
Puedes modificar las pautas de evaluación editando
|
|
386
|
+
Puedes modificar las pautas de evaluación editando los archivos en el directorio `.claude/`:
|
|
387
|
+
- `.claude/CLAUDE_PRE_COMMIT.md` - Para el modo estándar
|
|
388
|
+
- `.claude/CLAUDE_PRE_COMMIT_SONAR.md` - Para el modo SonarQube
|
|
389
|
+
|
|
390
|
+
Estos archivos son específicos de tu proyecto y puedes adaptarlos a los estándares de tu equipo.
|
|
372
391
|
|
|
373
392
|
## 🔄 Arquitectura del Sistema
|
|
374
393
|
|
|
@@ -405,16 +424,18 @@ WSL Terminal
|
|
|
405
424
|
```
|
|
406
425
|
claude-git-hooks/
|
|
407
426
|
├── bin/
|
|
408
|
-
│ └── claude-hooks
|
|
427
|
+
│ └── claude-hooks # CLI principal con verificación completa
|
|
409
428
|
├── templates/
|
|
410
429
|
│ ├── pre-commit # Hook de análisis de código
|
|
411
430
|
│ ├── prepare-commit-msg # Hook de generación de mensajes
|
|
412
431
|
│ ├── CLAUDE_PRE_COMMIT.md # Pautas estándar
|
|
413
432
|
│ └── CLAUDE_PRE_COMMIT_SONAR.md # Pautas SonarQube
|
|
414
|
-
├──
|
|
433
|
+
├── .claude/ # Directorio creado en el repo del usuario
|
|
434
|
+
│ ├── CLAUDE_PRE_COMMIT.md # Pautas copiadas aquí durante la instalación
|
|
435
|
+
│ └── CLAUDE_PRE_COMMIT_SONAR.md
|
|
415
436
|
├── package.json # Configuración NPM
|
|
416
437
|
├── README.md # Este archivo
|
|
417
|
-
├──
|
|
438
|
+
├── CHANGELOG.md # Historial de versiones
|
|
418
439
|
└── PUBLISH.md # Guía de publicación
|
|
419
440
|
```
|
|
420
441
|
|
package/bin/claude-hooks
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { execSync } = require('child_process');
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const https = require('https');
|
|
7
9
|
|
|
8
10
|
// Colores para output
|
|
9
11
|
const colors = {
|
|
@@ -35,6 +37,206 @@ function warning(message) {
|
|
|
35
37
|
log(`⚠️ ${message}`, 'yellow');
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// Función para leer contraseña de forma segura
|
|
41
|
+
function readPassword(prompt) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const rl = readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Deshabilitar echo
|
|
49
|
+
rl.stdoutMuted = true;
|
|
50
|
+
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
|
51
|
+
if (rl.stdoutMuted)
|
|
52
|
+
rl.output.write("*");
|
|
53
|
+
else
|
|
54
|
+
rl.output.write(stringToWrite);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
rl.question(prompt, (password) => {
|
|
58
|
+
rl.close();
|
|
59
|
+
console.log(); // Nueva línea
|
|
60
|
+
resolve(password);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Verificar si la contraseña sudo es correcta
|
|
66
|
+
function testSudoPassword(password) {
|
|
67
|
+
try {
|
|
68
|
+
execSync('echo "' + password + '" | sudo -S true', {
|
|
69
|
+
stdio: 'ignore',
|
|
70
|
+
timeout: 5000
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Instalar paquete con sudo automático
|
|
79
|
+
function installPackage(packageName, sudoPassword = null) {
|
|
80
|
+
try {
|
|
81
|
+
if (sudoPassword) {
|
|
82
|
+
if (os.platform() === 'linux') {
|
|
83
|
+
execSync(`echo "${sudoPassword}" | sudo -S apt-get update && echo "${sudoPassword}" | sudo -S apt-get install -y ${packageName}`, {
|
|
84
|
+
stdio: 'inherit'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
if (os.platform() === 'linux') {
|
|
89
|
+
execSync(`sudo apt-get update && sudo apt-get install -y ${packageName}`, {
|
|
90
|
+
stdio: 'inherit'
|
|
91
|
+
});
|
|
92
|
+
} else if (os.platform() === 'darwin') {
|
|
93
|
+
execSync(`brew install ${packageName}`, {
|
|
94
|
+
stdio: 'inherit'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sistema de entretenimiento
|
|
105
|
+
class Entertainment {
|
|
106
|
+
static jokes = [
|
|
107
|
+
"¿Por qué los programadores prefieren el modo oscuro? Porque la luz atrae bugs!",
|
|
108
|
+
"Un QA entra a un bar. Pide 1 cerveza. Pide 0 cervezas. Pide -1 cervezas.",
|
|
109
|
+
"¿Cuál es el lenguaje favorito de los piratas? R!",
|
|
110
|
+
"Hay 10 tipos de personas: las que entienden binario y las que no.",
|
|
111
|
+
"¿Por qué los programadores confunden Halloween con Navidad? Porque Oct 31 = Dec 25",
|
|
112
|
+
"¿Qué le dice un bit al otro? Nos vemos en el bus!",
|
|
113
|
+
"¿Por qué Java y C++ no se llevan bien? Porque tienen diferentes puntos de vista sobre los punteros.",
|
|
114
|
+
"Mi código no tiene bugs, solo features no documentadas."
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
static async getJoke() {
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
// Intentar obtener chiste de API
|
|
120
|
+
const req = https.get('https://icanhazdadjoke.com/', {
|
|
121
|
+
headers: { 'Accept': 'text/plain' },
|
|
122
|
+
timeout: 3000
|
|
123
|
+
}, (res) => {
|
|
124
|
+
let data = '';
|
|
125
|
+
res.on('data', chunk => data += chunk);
|
|
126
|
+
res.on('end', () => resolve(data.trim()));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
req.on('error', () => {
|
|
130
|
+
// Si falla, usar chiste local
|
|
131
|
+
const randomJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
132
|
+
resolve(randomJoke);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
req.on('timeout', () => {
|
|
136
|
+
req.abort();
|
|
137
|
+
const randomJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
138
|
+
resolve(randomJoke);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static async showSpinner(promise, message = 'Procesando') {
|
|
144
|
+
const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
145
|
+
let spinnerIndex = 0;
|
|
146
|
+
let jokeCountdown = 10;
|
|
147
|
+
let currentJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
148
|
+
let isFinished = false;
|
|
149
|
+
let isFirstRender = true;
|
|
150
|
+
|
|
151
|
+
// Obtener primer chiste de la API sin bloquear
|
|
152
|
+
this.getJoke().then(joke => {
|
|
153
|
+
if (!isFinished) currentJoke = joke;
|
|
154
|
+
}).catch(() => {}); // Si falla, mantener el local
|
|
155
|
+
|
|
156
|
+
// Ocultar cursor
|
|
157
|
+
process.stdout.write('\x1B[?25l');
|
|
158
|
+
|
|
159
|
+
// Reservar espacio para las 3 líneas
|
|
160
|
+
process.stdout.write('\n\n\n');
|
|
161
|
+
|
|
162
|
+
const interval = setInterval(() => {
|
|
163
|
+
if (isFinished) {
|
|
164
|
+
clearInterval(interval);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
spinnerIndex++;
|
|
169
|
+
|
|
170
|
+
// Actualizar countdown cada segundo (10 iteraciones de 100ms)
|
|
171
|
+
if (spinnerIndex % 10 === 0) {
|
|
172
|
+
jokeCountdown--;
|
|
173
|
+
|
|
174
|
+
// Renovar chiste cada 10 segundos
|
|
175
|
+
if (jokeCountdown <= 0) {
|
|
176
|
+
this.getJoke().then(joke => {
|
|
177
|
+
if (!isFinished) currentJoke = joke;
|
|
178
|
+
}).catch(() => {
|
|
179
|
+
if (!isFinished) {
|
|
180
|
+
currentJoke = this.jokes[Math.floor(Math.random() * this.jokes.length)];
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
jokeCountdown = 10;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Siempre volver exactamente 3 líneas arriba
|
|
188
|
+
process.stdout.write('\x1B[3A');
|
|
189
|
+
|
|
190
|
+
// Renderizar las 3 líneas desde el inicio
|
|
191
|
+
const spinner = spinners[spinnerIndex % spinners.length];
|
|
192
|
+
|
|
193
|
+
// Línea 1: Spinner
|
|
194
|
+
process.stdout.write('\r\x1B[2K' + `${colors.yellow}${spinner} ${message}${colors.reset}\n`);
|
|
195
|
+
|
|
196
|
+
// Línea 2: Chiste
|
|
197
|
+
process.stdout.write('\r\x1B[2K' + `${colors.green}🎭 ${currentJoke}${colors.reset}\n`);
|
|
198
|
+
|
|
199
|
+
// Línea 3: Countdown
|
|
200
|
+
process.stdout.write('\r\x1B[2K' + `${colors.yellow}⏱️ Próximo chiste en: ${jokeCountdown}s${colors.reset}\n`);
|
|
201
|
+
}, 100);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const result = await promise;
|
|
205
|
+
isFinished = true;
|
|
206
|
+
clearInterval(interval);
|
|
207
|
+
|
|
208
|
+
// Limpiar exactamente 3 líneas completamente
|
|
209
|
+
process.stdout.write('\x1B[3A'); // Subir 3 líneas
|
|
210
|
+
process.stdout.write('\r\x1B[2K'); // Limpiar línea 1
|
|
211
|
+
process.stdout.write('\n\r\x1B[2K'); // Bajar y limpiar línea 2
|
|
212
|
+
process.stdout.write('\n\r\x1B[2K'); // Bajar y limpiar línea 3
|
|
213
|
+
process.stdout.write('\x1B[2A'); // Subir 2 líneas para quedar en la primera
|
|
214
|
+
process.stdout.write('\r'); // Ir al inicio de línea
|
|
215
|
+
|
|
216
|
+
// Mostrar cursor
|
|
217
|
+
process.stdout.write('\x1B[?25h');
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
isFinished = true;
|
|
222
|
+
clearInterval(interval);
|
|
223
|
+
|
|
224
|
+
// Limpiar exactamente 3 líneas completamente
|
|
225
|
+
process.stdout.write('\x1B[3A'); // Subir 3 líneas
|
|
226
|
+
process.stdout.write('\r\x1B[2K'); // Limpiar línea 1
|
|
227
|
+
process.stdout.write('\n\r\x1B[2K'); // Bajar y limpiar línea 2
|
|
228
|
+
process.stdout.write('\n\r\x1B[2K'); // Bajar y limpiar línea 3
|
|
229
|
+
process.stdout.write('\x1B[2A'); // Subir 2 líneas para quedar en la primera
|
|
230
|
+
process.stdout.write('\r'); // Ir al inicio de línea
|
|
231
|
+
|
|
232
|
+
// Mostrar cursor
|
|
233
|
+
process.stdout.write('\x1B[?25h');
|
|
234
|
+
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
38
240
|
// Verificar si estamos en un repositorio git
|
|
39
241
|
function checkGitRepo() {
|
|
40
242
|
try {
|
|
@@ -51,13 +253,33 @@ function getTemplatesPath() {
|
|
|
51
253
|
}
|
|
52
254
|
|
|
53
255
|
// Comando install
|
|
54
|
-
function install(args) {
|
|
256
|
+
async function install(args) {
|
|
55
257
|
if (!checkGitRepo()) {
|
|
56
258
|
error('No estás en un repositorio Git. Por favor, ejecuta este comando desde la raíz de un repositorio.');
|
|
57
259
|
}
|
|
58
260
|
|
|
59
261
|
info('Instalando Claude Git Hooks...');
|
|
60
262
|
|
|
263
|
+
// Solicitar contraseña sudo al inicio si es necesario
|
|
264
|
+
let sudoPassword = null;
|
|
265
|
+
if (os.platform() === 'linux') {
|
|
266
|
+
const needsInstall = await checkIfInstallationNeeded();
|
|
267
|
+
if (needsInstall) {
|
|
268
|
+
info('Para la instalación automática de dependencias se necesita acceso sudo, por favor ingrese contraseña');
|
|
269
|
+
sudoPassword = await readPassword('Introduce tu contraseña de Ubuntu para sudo: ');
|
|
270
|
+
|
|
271
|
+
if (sudoPassword && !testSudoPassword(sudoPassword)) {
|
|
272
|
+
warning('Contraseña incorrecta. Continuando sin instalación automática.');
|
|
273
|
+
sudoPassword = null;
|
|
274
|
+
} else if (sudoPassword) {
|
|
275
|
+
success('Contraseña verificada. Procediendo con instalación automática.');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Verificar dependencias con instalación automática
|
|
281
|
+
await checkAndInstallDependencies(sudoPassword);
|
|
282
|
+
|
|
61
283
|
const templatesPath = getTemplatesPath();
|
|
62
284
|
const hooksPath = '.git/hooks';
|
|
63
285
|
|
|
@@ -86,18 +308,31 @@ function install(args) {
|
|
|
86
308
|
success(`${hook} instalado`);
|
|
87
309
|
});
|
|
88
310
|
|
|
89
|
-
//
|
|
311
|
+
// Crear directorio .claude si no existe
|
|
312
|
+
const claudeDir = '.claude';
|
|
313
|
+
if (!fs.existsSync(claudeDir)) {
|
|
314
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
315
|
+
success('.claude directory created');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Copiar archivos de pautas a .claude si no existen
|
|
90
319
|
const guidelines = ['CLAUDE_PRE_COMMIT.md', 'CLAUDE_PRE_COMMIT_SONAR.md'];
|
|
91
320
|
guidelines.forEach(guideline => {
|
|
92
|
-
|
|
321
|
+
const destPath = path.join(claudeDir, guideline);
|
|
322
|
+
if (!fs.existsSync(destPath)) {
|
|
93
323
|
const sourcePath = path.join(templatesPath, guideline);
|
|
94
|
-
fs.
|
|
95
|
-
|
|
324
|
+
if (fs.existsSync(sourcePath)) {
|
|
325
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
326
|
+
success(`${guideline} creado en .claude/`);
|
|
327
|
+
}
|
|
96
328
|
}
|
|
97
329
|
});
|
|
98
330
|
|
|
99
|
-
//
|
|
100
|
-
|
|
331
|
+
// Configurar Git
|
|
332
|
+
configureGit();
|
|
333
|
+
|
|
334
|
+
// Actualizar .gitignore
|
|
335
|
+
updateGitignore();
|
|
101
336
|
|
|
102
337
|
success('¡Claude Git Hooks instalado exitosamente! 🎉');
|
|
103
338
|
console.log('\nUso:');
|
|
@@ -106,31 +341,271 @@ function install(args) {
|
|
|
106
341
|
console.log('\nPara más opciones: claude-hooks --help');
|
|
107
342
|
}
|
|
108
343
|
|
|
109
|
-
// Verificar dependencias
|
|
110
|
-
function
|
|
111
|
-
info('
|
|
344
|
+
// Verificar dependencias completas (como setup-wsl.sh)
|
|
345
|
+
async function checkAndInstallDependencies(sudoPassword = null) {
|
|
346
|
+
info('Verificando dependencias del sistema...');
|
|
347
|
+
|
|
348
|
+
// Verificar Node.js
|
|
349
|
+
try {
|
|
350
|
+
const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim();
|
|
351
|
+
success(`Node.js ${nodeVersion}`);
|
|
352
|
+
} catch (e) {
|
|
353
|
+
error('Node.js no está instalado. Instala Node.js e intenta de nuevo.');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Verificar npm
|
|
357
|
+
try {
|
|
358
|
+
const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim();
|
|
359
|
+
success(`npm ${npmVersion}`);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
error('npm no está instalado.');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Verificar e instalar jq
|
|
365
|
+
try {
|
|
366
|
+
const jqVersion = execSync('jq --version', { encoding: 'utf8' }).trim();
|
|
367
|
+
success(`jq ${jqVersion}`);
|
|
368
|
+
} catch (e) {
|
|
369
|
+
warning('jq no está instalado. Instalando...');
|
|
370
|
+
if (installPackage('jq', sudoPassword)) {
|
|
371
|
+
success('jq instalado correctamente');
|
|
372
|
+
} else {
|
|
373
|
+
warning('No se pudo instalar jq automáticamente');
|
|
374
|
+
if (os.platform() === 'linux') {
|
|
375
|
+
console.log('Instálalo manualmente con: sudo apt install jq');
|
|
376
|
+
} else if (os.platform() === 'darwin') {
|
|
377
|
+
console.log('Instálalo manualmente con: brew install jq');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Verificar e instalar curl
|
|
383
|
+
try {
|
|
384
|
+
const curlVersion = execSync('curl --version', { encoding: 'utf8' }).split('\n')[0];
|
|
385
|
+
success(`curl ${curlVersion.split(' ')[1]}`);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
warning('curl no está instalado. Instalando...');
|
|
388
|
+
if (installPackage('curl', sudoPassword)) {
|
|
389
|
+
success('curl instalado correctamente');
|
|
390
|
+
} else {
|
|
391
|
+
warning('No se pudo instalar curl automáticamente');
|
|
392
|
+
if (os.platform() === 'linux') {
|
|
393
|
+
console.log('Instálalo manualmente con: sudo apt install curl');
|
|
394
|
+
} else if (os.platform() === 'darwin') {
|
|
395
|
+
console.log('Instálalo manualmente con: brew install curl');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Verificar Git
|
|
401
|
+
try {
|
|
402
|
+
const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim();
|
|
403
|
+
success(`${gitVersion}`);
|
|
404
|
+
} catch (e) {
|
|
405
|
+
error('Git no está instalado. Instala Git e intenta de nuevo.');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Verificar herramientas Unix estándar
|
|
409
|
+
const unixTools = ['sed', 'awk', 'grep', 'head', 'tail', 'stat', 'tput'];
|
|
410
|
+
const missingTools = [];
|
|
411
|
+
|
|
412
|
+
unixTools.forEach(tool => {
|
|
413
|
+
try {
|
|
414
|
+
execSync(`which ${tool}`, { stdio: 'ignore' });
|
|
415
|
+
} catch (e) {
|
|
416
|
+
missingTools.push(tool);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (missingTools.length === 0) {
|
|
421
|
+
success('Herramientas Unix estándar verificadas');
|
|
422
|
+
} else {
|
|
423
|
+
error(`Faltan herramientas Unix estándar: ${missingTools.join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Verificar e instalar Claude CLI
|
|
427
|
+
await checkAndInstallClaude();
|
|
428
|
+
|
|
429
|
+
// Verificar autenticación de Claude
|
|
430
|
+
await checkClaudeAuth();
|
|
431
|
+
|
|
432
|
+
// Limpiar contraseña de memoria
|
|
433
|
+
sudoPassword = null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Verificar si necesitamos instalar dependencias
|
|
437
|
+
async function checkIfInstallationNeeded() {
|
|
438
|
+
const dependencies = ['jq', 'curl'];
|
|
439
|
+
|
|
440
|
+
for (const dep of dependencies) {
|
|
441
|
+
try {
|
|
442
|
+
execSync(`which ${dep}`, { stdio: 'ignore' });
|
|
443
|
+
} catch (e) {
|
|
444
|
+
return true; // Necesita instalación
|
|
445
|
+
}
|
|
446
|
+
}
|
|
112
447
|
|
|
113
448
|
// Verificar Claude CLI
|
|
114
449
|
try {
|
|
115
450
|
execSync('claude --version', { stdio: 'ignore' });
|
|
116
|
-
success('Claude CLI encontrado');
|
|
117
451
|
} catch (e) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
452
|
+
return true; // Necesita instalación de Claude
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Verificar e instalar Claude CLI
|
|
459
|
+
async function checkAndInstallClaude() {
|
|
460
|
+
try {
|
|
461
|
+
execSync('claude --version', { stdio: 'ignore' });
|
|
462
|
+
success('Claude CLI detectado');
|
|
463
|
+
} catch (e) {
|
|
464
|
+
info('Claude CLI no detectado. Instalando...');
|
|
465
|
+
try {
|
|
466
|
+
execSync('npm install -g @anthropic-ai/claude-cli', { stdio: 'inherit' });
|
|
467
|
+
success('Claude CLI instalado correctamente');
|
|
468
|
+
} catch (installError) {
|
|
469
|
+
error('Error al instalar Claude CLI. Instálalo manualmente: npm install -g @anthropic-ai/claude-cli');
|
|
470
|
+
}
|
|
121
471
|
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Verificar autenticación de Claude con entretenimiento
|
|
475
|
+
async function checkClaudeAuth() {
|
|
476
|
+
info('Verificando autenticación de Claude...');
|
|
122
477
|
|
|
123
|
-
//
|
|
478
|
+
// Usar spawn para no bloquear, pero con stdio: 'ignore' como el original
|
|
479
|
+
const authPromise = new Promise((resolve, reject) => {
|
|
480
|
+
const child = spawn('claude', ['auth', 'status'], {
|
|
481
|
+
stdio: 'ignore', // Igual que el original
|
|
482
|
+
detached: false,
|
|
483
|
+
windowsHide: true
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Timeout manual ya que spawn no tiene timeout nativo
|
|
487
|
+
const timeout = setTimeout(() => {
|
|
488
|
+
child.kill();
|
|
489
|
+
reject(new Error('timeout'));
|
|
490
|
+
}, 120000); // 2 minutos
|
|
491
|
+
|
|
492
|
+
child.on('exit', (code) => {
|
|
493
|
+
clearTimeout(timeout);
|
|
494
|
+
if (code === 0) {
|
|
495
|
+
resolve('success');
|
|
496
|
+
} else {
|
|
497
|
+
reject(new Error('not_authenticated'));
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
child.on('error', (err) => {
|
|
502
|
+
clearTimeout(timeout);
|
|
503
|
+
reject(err);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
124
507
|
try {
|
|
125
|
-
|
|
126
|
-
success('
|
|
508
|
+
await Entertainment.showSpinner(authPromise, 'Verificando autenticación de Claude');
|
|
509
|
+
success('Autenticado en Claude');
|
|
127
510
|
} catch (e) {
|
|
128
|
-
warning('
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
511
|
+
warning('No estás autenticado en Claude');
|
|
512
|
+
console.log('Ejecuta: claude auth login');
|
|
513
|
+
console.log('Después vuelve a ejecutar este comando');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Actualizar .gitignore con las entradas de Claude
|
|
518
|
+
function updateGitignore() {
|
|
519
|
+
info('Actualizando .gitignore...');
|
|
520
|
+
|
|
521
|
+
const gitignorePath = '.gitignore';
|
|
522
|
+
const claudeEntries = [
|
|
523
|
+
'# Claude Git Hooks',
|
|
524
|
+
'.claude/',
|
|
525
|
+
'.claude-analysis-mode',
|
|
526
|
+
'debug-claude-response.json'
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
let gitignoreContent = '';
|
|
530
|
+
let fileExists = false;
|
|
531
|
+
|
|
532
|
+
// Leer .gitignore existente si existe
|
|
533
|
+
if (fs.existsSync(gitignorePath)) {
|
|
534
|
+
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
535
|
+
fileExists = true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Verificar qué entradas faltan
|
|
539
|
+
const missingEntries = [];
|
|
540
|
+
claudeEntries.forEach(entry => {
|
|
541
|
+
if (entry.startsWith('#')) {
|
|
542
|
+
// Para comentarios, verificar si ya existe algún comentario de Claude
|
|
543
|
+
if (!gitignoreContent.includes('# Claude')) {
|
|
544
|
+
missingEntries.push(entry);
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
// Para entradas normales, verificar si ya están presentes
|
|
548
|
+
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
549
|
+
if (!regex.test(gitignoreContent)) {
|
|
550
|
+
missingEntries.push(entry);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Si hay entradas faltantes, agregarlas
|
|
556
|
+
if (missingEntries.length > 0) {
|
|
557
|
+
// Asegurar que hay una nueva línea al final si el archivo existe y no está vacío
|
|
558
|
+
if (fileExists && gitignoreContent.length > 0 && !gitignoreContent.endsWith('\n')) {
|
|
559
|
+
gitignoreContent += '\n';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Si el archivo no está vacío, agregar una línea en blanco antes
|
|
563
|
+
if (gitignoreContent.length > 0) {
|
|
564
|
+
gitignoreContent += '\n';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Agregar las entradas faltantes
|
|
568
|
+
gitignoreContent += missingEntries.join('\n') + '\n';
|
|
569
|
+
|
|
570
|
+
// Escribir el archivo actualizado
|
|
571
|
+
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
572
|
+
|
|
573
|
+
if (fileExists) {
|
|
574
|
+
success('.gitignore actualizado con entradas de Claude');
|
|
575
|
+
} else {
|
|
576
|
+
success('.gitignore creado con entradas de Claude');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Mostrar qué se agregó
|
|
580
|
+
missingEntries.forEach(entry => {
|
|
581
|
+
if (!entry.startsWith('#')) {
|
|
582
|
+
info(` + ${entry}`);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
} else {
|
|
586
|
+
info('.gitignore ya contiene todas las entradas necesarias');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Configurar Git (line endings, etc.)
|
|
591
|
+
function configureGit() {
|
|
592
|
+
info('Configurando Git...');
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Configurar line endings para WSL
|
|
596
|
+
execSync('git config core.autocrlf input', { stdio: 'ignore' });
|
|
597
|
+
success('Line endings configurados para WSL (core.autocrlf = input)');
|
|
598
|
+
|
|
599
|
+
// Intentar configurar en Windows a través de PowerShell
|
|
600
|
+
try {
|
|
601
|
+
execSync('powershell.exe -Command "git config core.autocrlf true"', { stdio: 'ignore' });
|
|
602
|
+
success('Line endings configurados para Windows (core.autocrlf = true)');
|
|
603
|
+
} catch (psError) {
|
|
604
|
+
info('No se pudo configurar automáticamente en Windows');
|
|
133
605
|
}
|
|
606
|
+
|
|
607
|
+
} catch (e) {
|
|
608
|
+
warning('Error al configurar Git');
|
|
134
609
|
}
|
|
135
610
|
}
|
|
136
611
|
|
|
@@ -240,12 +715,40 @@ function status() {
|
|
|
240
715
|
console.log('\nArchivos de pautas:');
|
|
241
716
|
const guidelines = ['CLAUDE_PRE_COMMIT.md', 'CLAUDE_PRE_COMMIT_SONAR.md'];
|
|
242
717
|
guidelines.forEach(guideline => {
|
|
243
|
-
|
|
244
|
-
|
|
718
|
+
const claudePath = path.join('.claude', guideline);
|
|
719
|
+
if (fs.existsSync(claudePath)) {
|
|
720
|
+
success(`${guideline}: presente en .claude/`);
|
|
721
|
+
} else if (fs.existsSync(guideline)) {
|
|
722
|
+
warning(`${guideline}: presente en raíz (debería estar en .claude/)`);
|
|
245
723
|
} else {
|
|
246
724
|
warning(`${guideline}: faltante`);
|
|
247
725
|
}
|
|
248
726
|
});
|
|
727
|
+
|
|
728
|
+
// Verificar entradas en .gitignore
|
|
729
|
+
console.log('\n.gitignore:');
|
|
730
|
+
const gitignorePath = '.gitignore';
|
|
731
|
+
if (fs.existsSync(gitignorePath)) {
|
|
732
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
733
|
+
const claudeIgnores = ['.claude/', '.claude-analysis-mode', 'debug-claude-response.json'];
|
|
734
|
+
let allPresent = true;
|
|
735
|
+
|
|
736
|
+
claudeIgnores.forEach(entry => {
|
|
737
|
+
const regex = new RegExp(`^${entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm');
|
|
738
|
+
if (regex.test(gitignoreContent)) {
|
|
739
|
+
success(`${entry}: incluido`);
|
|
740
|
+
} else {
|
|
741
|
+
warning(`${entry}: faltante`);
|
|
742
|
+
allPresent = false;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
if (!allPresent) {
|
|
747
|
+
info('\nEjecuta "claude-hooks install" para actualizar .gitignore');
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
warning('.gitignore no existe');
|
|
751
|
+
}
|
|
249
752
|
}
|
|
250
753
|
|
|
251
754
|
// Comando set-mode
|
|
@@ -315,35 +818,42 @@ Más información: https://github.com/pablorovito/claude-git-hooks
|
|
|
315
818
|
}
|
|
316
819
|
|
|
317
820
|
// Main
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
install
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
uninstall
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
enable
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
disable
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
status
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
821
|
+
async function main() {
|
|
822
|
+
const args = process.argv.slice(2);
|
|
823
|
+
const command = args[0];
|
|
824
|
+
|
|
825
|
+
switch (command) {
|
|
826
|
+
case 'install':
|
|
827
|
+
await install(args.slice(1));
|
|
828
|
+
break;
|
|
829
|
+
case 'uninstall':
|
|
830
|
+
uninstall();
|
|
831
|
+
break;
|
|
832
|
+
case 'enable':
|
|
833
|
+
enable(args[1]);
|
|
834
|
+
break;
|
|
835
|
+
case 'disable':
|
|
836
|
+
disable(args[1]);
|
|
837
|
+
break;
|
|
838
|
+
case 'set-mode':
|
|
839
|
+
setMode(args[1]);
|
|
840
|
+
break;
|
|
841
|
+
case 'status':
|
|
842
|
+
status();
|
|
843
|
+
break;
|
|
844
|
+
case 'help':
|
|
845
|
+
case '--help':
|
|
846
|
+
case '-h':
|
|
847
|
+
case undefined:
|
|
848
|
+
showHelp();
|
|
849
|
+
break;
|
|
850
|
+
default:
|
|
851
|
+
error(`Comando desconocido: ${command}`);
|
|
852
|
+
showHelp();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Ejecutar main
|
|
857
|
+
main().catch(err => {
|
|
858
|
+
error(`Error inesperado: ${err.message}`);
|
|
859
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-git-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Git hooks con Claude CLI para análisis de código y generación automática de mensajes de commit",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"lib/",
|
|
36
36
|
"templates/",
|
|
37
37
|
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
38
39
|
"LICENSE"
|
|
39
40
|
]
|
|
40
41
|
}
|
|
@@ -23,6 +23,11 @@ Evaluar el código modificado siguiendo métricas similares a SonarQube para ase
|
|
|
23
23
|
- Duplicación de código
|
|
24
24
|
- Deuda técnica
|
|
25
25
|
|
|
26
|
+
### Métricas Adicionales
|
|
27
|
+
- **Coverage**: Porcentaje estimado de cobertura de pruebas (0-100)
|
|
28
|
+
- **Duplications**: Porcentaje estimado de código duplicado (0-100)
|
|
29
|
+
- **Complexity**: Complejidad ciclomática promedio del código
|
|
30
|
+
|
|
26
31
|
## Clasificación de Issues
|
|
27
32
|
|
|
28
33
|
### Severidad
|
|
@@ -42,7 +47,10 @@ Responde ÚNICAMENTE con un JSON válido siguiendo esta estructura:
|
|
|
42
47
|
"metrics": {
|
|
43
48
|
"reliability": "A/B/C/D/E",
|
|
44
49
|
"security": "A/B/C/D/E",
|
|
45
|
-
"maintainability": "A/B/C/D/E"
|
|
50
|
+
"maintainability": "A/B/C/D/E",
|
|
51
|
+
"coverage": 75,
|
|
52
|
+
"duplications": 5,
|
|
53
|
+
"complexity": 15
|
|
46
54
|
},
|
|
47
55
|
"issues": {
|
|
48
56
|
"blocker": 0,
|
|
@@ -60,7 +68,9 @@ Responde ÚNICAMENTE con un JSON válido siguiendo esta estructura:
|
|
|
60
68
|
"line": 42
|
|
61
69
|
}
|
|
62
70
|
],
|
|
63
|
-
"securityHotspots": 0
|
|
71
|
+
"securityHotspots": 0,
|
|
72
|
+
"approved": true/false,
|
|
73
|
+
"blockingIssues": ["Lista de problemas bloqueantes si existen"]
|
|
64
74
|
}
|
|
65
75
|
```
|
|
66
76
|
|
package/templates/pre-commit
CHANGED
|
@@ -64,10 +64,10 @@ if [ "$ANALYSIS_MODE" != "standard" ] && [ "$ANALYSIS_MODE" != "sonar" ]; then
|
|
|
64
64
|
|
|
65
65
|
if [ "$REPLY" = "2" ]; then
|
|
66
66
|
ANALYSIS_MODE="sonar"
|
|
67
|
-
GUIDELINES_FILE="CLAUDE_PRE_COMMIT_SONAR.md"
|
|
67
|
+
GUIDELINES_FILE=".claude/CLAUDE_PRE_COMMIT_SONAR.md"
|
|
68
68
|
else
|
|
69
69
|
ANALYSIS_MODE="standard"
|
|
70
|
-
GUIDELINES_FILE="CLAUDE_PRE_COMMIT.md"
|
|
70
|
+
GUIDELINES_FILE=".claude/CLAUDE_PRE_COMMIT.md"
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
73
|
# Guardar preferencia para futuros commits
|
|
@@ -78,10 +78,10 @@ if [ "$ANALYSIS_MODE" != "standard" ] && [ "$ANALYSIS_MODE" != "sonar" ]; then
|
|
|
78
78
|
else
|
|
79
79
|
# Usar modo guardado
|
|
80
80
|
if [ "$ANALYSIS_MODE" = "sonar" ]; then
|
|
81
|
-
GUIDELINES_FILE="CLAUDE_PRE_COMMIT_SONAR.md"
|
|
81
|
+
GUIDELINES_FILE=".claude/CLAUDE_PRE_COMMIT_SONAR.md"
|
|
82
82
|
log "Usando modo de análisis: SonarQube"
|
|
83
83
|
else
|
|
84
|
-
GUIDELINES_FILE="CLAUDE_PRE_COMMIT.md"
|
|
84
|
+
GUIDELINES_FILE=".claude/CLAUDE_PRE_COMMIT.md"
|
|
85
85
|
log "Usando modo de análisis: Estándar"
|
|
86
86
|
fi
|
|
87
87
|
fi
|
|
@@ -233,7 +233,7 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
|
233
233
|
BLOCKING_ISSUES=$(echo "$JSON_RESPONSE" | jq -r '.blockingIssues[]?' 2>/dev/null | sed '/^$/d')
|
|
234
234
|
|
|
235
235
|
# Verificar si estamos en modo SonarQube
|
|
236
|
-
QUALITY_GATE=$(echo "$JSON_RESPONSE" | jq -r '.
|
|
236
|
+
QUALITY_GATE=$(echo "$JSON_RESPONSE" | jq -r '.QUALITY_GATE // ""' 2>/dev/null)
|
|
237
237
|
|
|
238
238
|
if [ "$ANALYSIS_MODE" = "sonar" ] && [ -n "$QUALITY_GATE" ] && [ "$QUALITY_GATE" != "null" ]; then
|
|
239
239
|
# Mostrar resultados estilo SonarQube
|
|
@@ -265,15 +265,15 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
|
265
265
|
fi
|
|
266
266
|
|
|
267
267
|
# Issues Summary
|
|
268
|
-
|
|
269
|
-
if [ "$
|
|
268
|
+
ISSUES=$(echo "$JSON_RESPONSE" | jq -r '.issues // {}' 2>/dev/null)
|
|
269
|
+
if [ "$ISSUES" != "{}" ] && [ "$ISSUES" != "null" ]; then
|
|
270
270
|
echo "📋 ISSUES SUMMARY"
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
271
|
+
BLOCKER=$(echo "$ISSUES" | jq -r '.blocker // 0' 2>/dev/null)
|
|
272
|
+
CRITICAL=$(echo "$ISSUES" | jq -r '.critical // 0' 2>/dev/null)
|
|
273
|
+
MAJOR=$(echo "$ISSUES" | jq -r '.major // 0' 2>/dev/null)
|
|
274
|
+
MINOR=$(echo "$ISSUES" | jq -r '.minor // 0' 2>/dev/null)
|
|
275
|
+
INFO=$(echo "$ISSUES" | jq -r '.info // 0' 2>/dev/null)
|
|
276
|
+
TOTAL=$((BLOCKER + CRITICAL + MAJOR + MINOR + INFO))
|
|
277
277
|
|
|
278
278
|
echo "Total: $TOTAL issues found"
|
|
279
279
|
[ "$BLOCKER" -gt 0 ] && echo -e " ${RED}🔴 Blocker: $BLOCKER${NC}"
|
|
@@ -285,18 +285,19 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
|
285
285
|
fi
|
|
286
286
|
|
|
287
287
|
# Detailed Issues
|
|
288
|
-
|
|
289
|
-
if [ "$
|
|
288
|
+
DETAILS_COUNT=$(echo "$JSON_RESPONSE" | jq -r '.details | length' 2>/dev/null)
|
|
289
|
+
if [ "$DETAILS_COUNT" -gt 0 ] 2>/dev/null; then
|
|
290
290
|
echo "🔍 DETAILED ISSUES"
|
|
291
|
-
echo "$JSON_RESPONSE" | jq -r '.
|
|
292
|
-
"[\(.severity)] \(.type) in \(.file):\(.line // "?")\n \(.message)\n
|
|
291
|
+
echo "$JSON_RESPONSE" | jq -r '.details[]? |
|
|
292
|
+
"[\(.severity)] \(.type) in \(.file):\(.line // "?")\n \(.message)\n"' 2>/dev/null
|
|
293
293
|
fi
|
|
294
294
|
|
|
295
295
|
# Security Hotspots
|
|
296
|
-
|
|
297
|
-
if [ "$
|
|
298
|
-
echo "🔥 SECURITY HOTSPOTS"
|
|
299
|
-
echo "
|
|
296
|
+
HOTSPOTS=$(echo "$JSON_RESPONSE" | jq -r '.securityHotspots // 0' 2>/dev/null)
|
|
297
|
+
if [ "$HOTSPOTS" -gt 0 ] 2>/dev/null; then
|
|
298
|
+
echo "🔥 SECURITY HOTSPOTS: $HOTSPOTS found"
|
|
299
|
+
echo " Review security-sensitive code carefully"
|
|
300
|
+
|
|
300
301
|
echo
|
|
301
302
|
fi
|
|
302
303
|
|
|
@@ -332,11 +333,18 @@ if $CLAUDE_CLI < "$PROMPT_FILE" > "$RESPONSE_FILE" 2>&1; then
|
|
|
332
333
|
exit 1
|
|
333
334
|
fi
|
|
334
335
|
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
# Mostrar detalles adicionales si existen
|
|
337
|
+
DETAILS=$(echo "$JSON_RESPONSE" | jq -r '.details // null')
|
|
338
|
+
if [ -n "$DETAILS" ] && [ "$DETAILS" != "null" ]; then
|
|
337
339
|
echo
|
|
338
|
-
echo "=== DETALLES
|
|
339
|
-
|
|
340
|
+
echo "=== DETALLES ADICIONALES ==="
|
|
341
|
+
# Si details es un string, imprimirlo directamente
|
|
342
|
+
if echo "$DETAILS" | jq -e 'type == "string"' >/dev/null 2>&1; then
|
|
343
|
+
echo "$DETAILS" | jq -r '.'
|
|
344
|
+
# Si es un objeto o array, formatearlo
|
|
345
|
+
else
|
|
346
|
+
echo "$DETAILS" | jq '.'
|
|
347
|
+
fi
|
|
340
348
|
fi
|
|
341
349
|
|
|
342
350
|
log "✅ Revisión completada. Commit aprobado (Score: $SCORE/10)"
|
|
File without changes
|