specleap-framework 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/.agents/backend.md +419 -0
- package/.agents/frontend.md +577 -0
- package/.agents/producto.md +516 -0
- package/.commands/adoptar.md +323 -0
- package/.commands/ayuda.md +142 -0
- package/.commands/crear-tickets.md +55 -0
- package/.commands/documentar.md +285 -0
- package/.commands/explicar.md +234 -0
- package/.commands/implementar.md +383 -0
- package/.commands/inicio.md +824 -0
- package/.commands/nuevo/README.md +292 -0
- package/.commands/nuevo/questions-base.yaml +320 -0
- package/.commands/nuevo/responses-example.yaml +53 -0
- package/.commands/planificar.md +253 -0
- package/.commands/refinar.md +306 -0
- package/LICENSE +21 -0
- package/README.md +603 -0
- package/SETUP.md +351 -0
- package/install.sh +152 -0
- package/package.json +60 -0
- package/proyectos/_template/.gitkeep +1 -0
- package/proyectos/_template/ANEXOS.md +21 -0
- package/proyectos/_template/CONTRATO.md +26 -0
- package/proyectos/_template/context/.gitkeep +1 -0
- package/rules/development-rules.md +113 -0
- package/rules/environment-protection.md +97 -0
- package/rules/git-workflow.md +142 -0
- package/rules/session-protocol.md +121 -0
- package/scripts/README.md +129 -0
- package/scripts/analyze-project.sh +826 -0
- package/scripts/create-asana-tasks.sh +133 -0
- package/scripts/detect-project-type.sh +141 -0
- package/scripts/estimate-effort.sh +290 -0
- package/scripts/generate-asana-structure.sh +262 -0
- package/scripts/generate-contract.sh +360 -0
- package/scripts/generate-contrato.sh +555 -0
- package/scripts/install-git-hooks.sh +141 -0
- package/scripts/install-skills.sh +130 -0
- package/scripts/lib/asana-utils.sh +191 -0
- package/scripts/lib/jira-project-utils.sh +222 -0
- package/scripts/lib/questions.json +831 -0
- package/scripts/lib/render-contrato.py +195 -0
- package/scripts/lib/validate.sh +325 -0
- package/scripts/parse-contrato.sh +190 -0
- package/scripts/setup-mcp.sh +654 -0
- package/scripts/test-cuestionario.sh +428 -0
- package/setup.sh +458 -0
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Cargar sistema i18n
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
source "$SCRIPT_DIR/../.specleap/i18n.sh"
|
|
6
|
+
|
|
7
|
+
# analyze-project.sh
|
|
8
|
+
# Analiza un proyecto legacy para adopciΓ³n en SpecLeap
|
|
9
|
+
# Uso: bash analyze-project.sh /ruta/al/proyecto
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
PROJECT_PATH="$1"
|
|
14
|
+
OUTPUT_FILE="/tmp/specleap-analysis.json"
|
|
15
|
+
|
|
16
|
+
if [ -z "$PROJECT_PATH" ]; then
|
|
17
|
+
echo "$(t "errors.no_path")"
|
|
18
|
+
echo "$(t "errors.usage"): bash analyze-project.sh /ruta/al/proyecto"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [ ! -d "$PROJECT_PATH" ]; then
|
|
23
|
+
echo "$(t 'errors.invalid_path'): $PROJECT_PATH"
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
echo "$(t "analyze.scanning"): $PROJECT_PATH"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
cd "$PROJECT_PATH" || exit 1
|
|
31
|
+
|
|
32
|
+
# ==============================================================================
|
|
33
|
+
# VARIABLES GLOBALES
|
|
34
|
+
# ==============================================================================
|
|
35
|
+
|
|
36
|
+
PROJECT_NAME=$(basename "$PROJECT_PATH")
|
|
37
|
+
DETECTION_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
38
|
+
|
|
39
|
+
# ==============================================================================
|
|
40
|
+
# FUNCIΓN: Detectar Stack Backend
|
|
41
|
+
# ==============================================================================
|
|
42
|
+
|
|
43
|
+
detect_backend() {
|
|
44
|
+
local framework="Unknown"
|
|
45
|
+
local version="Unknown"
|
|
46
|
+
local language="Unknown"
|
|
47
|
+
local lang_version="Unknown"
|
|
48
|
+
|
|
49
|
+
echo "π¦ Detectando stack backend..."
|
|
50
|
+
|
|
51
|
+
# Laravel
|
|
52
|
+
if [ -f "composer.json" ]; then
|
|
53
|
+
if grep -q "laravel/framework" composer.json; then
|
|
54
|
+
framework="Laravel"
|
|
55
|
+
version=$(grep '"laravel/framework"' composer.json | sed 's/.*"[^0-9]*\([0-9.]*\)".*/\1/')
|
|
56
|
+
language="PHP"
|
|
57
|
+
|
|
58
|
+
if command -v php &> /dev/null; then
|
|
59
|
+
lang_version=$(php -v | head -n 1 | awk '{print $2}')
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Symfony
|
|
65
|
+
if [ -f "composer.json" ]; then
|
|
66
|
+
if grep -q "symfony/framework-bundle" composer.json; then
|
|
67
|
+
framework="Symfony"
|
|
68
|
+
version=$(grep '"symfony/framework-bundle"' composer.json | sed 's/.*"[^0-9]*\([0-9.]*\)".*/\1/')
|
|
69
|
+
language="PHP"
|
|
70
|
+
|
|
71
|
+
if command -v php &> /dev/null; then
|
|
72
|
+
lang_version=$(php -v | head -n 1 | awk '{print $2}')
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Node.js/Express
|
|
78
|
+
if [ -f "package.json" ] && [ ! -f "composer.json" ]; then
|
|
79
|
+
if grep -q '"express"' package.json; then
|
|
80
|
+
framework="Express"
|
|
81
|
+
version=$(grep '"express"' package.json | sed 's/.*"[^0-9]*\([0-9.]*\)".*/\1/')
|
|
82
|
+
language="JavaScript"
|
|
83
|
+
|
|
84
|
+
if command -v node &> /dev/null; then
|
|
85
|
+
lang_version=$(node -v | sed 's/v//')
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Django
|
|
91
|
+
if [ -f "requirements.txt" ]; then
|
|
92
|
+
if grep -q "Django" requirements.txt; then
|
|
93
|
+
framework="Django"
|
|
94
|
+
version=$(grep "Django" requirements.txt | sed 's/.*==\([0-9.]*\).*/\1/')
|
|
95
|
+
language="Python"
|
|
96
|
+
|
|
97
|
+
if command -v python3 &> /dev/null; then
|
|
98
|
+
lang_version=$(python3 --version | awk '{print $2}')
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
echo " Framework: $framework $version"
|
|
104
|
+
echo " Language: $language $lang_version"
|
|
105
|
+
echo ""
|
|
106
|
+
|
|
107
|
+
# Guardar en variables globales
|
|
108
|
+
BACKEND_FRAMEWORK="$framework"
|
|
109
|
+
BACKEND_VERSION="$version"
|
|
110
|
+
BACKEND_LANGUAGE="$language"
|
|
111
|
+
BACKEND_LANG_VERSION="$lang_version"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ==============================================================================
|
|
115
|
+
# FUNCIΓN: Detectar Stack Frontend
|
|
116
|
+
# ==============================================================================
|
|
117
|
+
|
|
118
|
+
detect_frontend() {
|
|
119
|
+
local framework="Unknown"
|
|
120
|
+
local version="Unknown"
|
|
121
|
+
local build_tool="Unknown"
|
|
122
|
+
local ui_library="Unknown"
|
|
123
|
+
|
|
124
|
+
echo "π¨ Detectando stack frontend..."
|
|
125
|
+
|
|
126
|
+
if [ -f "package.json" ]; then
|
|
127
|
+
# React
|
|
128
|
+
if grep -q '"react"' package.json; then
|
|
129
|
+
framework="React"
|
|
130
|
+
version=$(grep '"react"' package.json | sed 's/.*"[^0-9]*\([0-9.]*\)".*/\1/')
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Vue
|
|
134
|
+
if grep -q '"vue"' package.json; then
|
|
135
|
+
framework="Vue"
|
|
136
|
+
version=$(grep '"vue"' package.json | sed 's/.*"[^0-9]*\([0-9.]*\)".*/\1/')
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Angular
|
|
140
|
+
if grep -q '"@angular/core"' package.json; then
|
|
141
|
+
framework="Angular"
|
|
142
|
+
version=$(grep '"@angular/core"' package.json | sed 's/.*"[^0-9]*\([0-9.]*\)".*/\1/')
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Svelte
|
|
146
|
+
if grep -q '"svelte"' package.json; then
|
|
147
|
+
framework="Svelte"
|
|
148
|
+
version=$(grep '"svelte"' package.json | sed 's/.*"[^0-9]*\([0-9.]*\)".*/\1/')
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Build tools
|
|
152
|
+
if grep -q '"vite"' package.json; then
|
|
153
|
+
build_tool="Vite"
|
|
154
|
+
elif grep -q '"webpack"' package.json; then
|
|
155
|
+
build_tool="Webpack"
|
|
156
|
+
elif grep -q '"parcel"' package.json; then
|
|
157
|
+
build_tool="Parcel"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# UI Libraries
|
|
161
|
+
if grep -q '"tailwindcss"' package.json; then
|
|
162
|
+
ui_library="Tailwind CSS"
|
|
163
|
+
elif grep -q '"@mui/material"' package.json; then
|
|
164
|
+
ui_library="Material-UI"
|
|
165
|
+
elif grep -q '"bootstrap"' package.json; then
|
|
166
|
+
ui_library="Bootstrap"
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
echo " Framework: $framework $version"
|
|
171
|
+
echo " Build Tool: $build_tool"
|
|
172
|
+
echo " UI Library: $ui_library"
|
|
173
|
+
echo ""
|
|
174
|
+
|
|
175
|
+
FRONTEND_FRAMEWORK="$framework"
|
|
176
|
+
FRONTEND_VERSION="$version"
|
|
177
|
+
FRONTEND_BUILD_TOOL="$build_tool"
|
|
178
|
+
FRONTEND_UI_LIBRARY="$ui_library"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# ==============================================================================
|
|
182
|
+
# FUNCIΓN: Detectar Base de Datos
|
|
183
|
+
# ==============================================================================
|
|
184
|
+
|
|
185
|
+
detect_database() {
|
|
186
|
+
local db_type="Unknown"
|
|
187
|
+
local db_version="Unknown"
|
|
188
|
+
local tables_count=0
|
|
189
|
+
|
|
190
|
+
echo "ποΈ Detectando base de datos..."
|
|
191
|
+
|
|
192
|
+
# Laravel migrations
|
|
193
|
+
if [ -d "database/migrations" ]; then
|
|
194
|
+
tables_count=$(grep -r "Schema::create" database/migrations/ 2>/dev/null | wc -l | tr -d ' ')
|
|
195
|
+
|
|
196
|
+
# Detectar tipo por driver en .env.example o config
|
|
197
|
+
if [ -f ".env.example" ]; then
|
|
198
|
+
if grep -q "DB_CONNECTION=mysql" .env.example; then
|
|
199
|
+
db_type="MySQL"
|
|
200
|
+
elif grep -q "DB_CONNECTION=pgsql" .env.example; then
|
|
201
|
+
db_type="PostgreSQL"
|
|
202
|
+
elif grep -q "DB_CONNECTION=sqlite" .env.example; then
|
|
203
|
+
db_type="SQLite"
|
|
204
|
+
fi
|
|
205
|
+
fi
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Django migrations
|
|
209
|
+
if [ -d "migrations" ] || find . -type d -name "migrations" -not -path "./node_modules/*" | grep -q .; then
|
|
210
|
+
tables_count=$(find . -type f -name "*.py" -path "*/migrations/*" -not -path "./node_modules/*" | wc -l | tr -d ' ')
|
|
211
|
+
db_type="PostgreSQL" # Asumido para Django
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
echo " Type: $db_type"
|
|
215
|
+
echo " Tables: $tables_count"
|
|
216
|
+
echo ""
|
|
217
|
+
|
|
218
|
+
DATABASE_TYPE="$db_type"
|
|
219
|
+
DATABASE_VERSION="$db_version"
|
|
220
|
+
DATABASE_TABLES_COUNT="$tables_count"
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# ==============================================================================
|
|
224
|
+
# FUNCIΓN: Detectar Servicios Externos
|
|
225
|
+
# ==============================================================================
|
|
226
|
+
|
|
227
|
+
detect_services() {
|
|
228
|
+
echo "π Detectando servicios externos..."
|
|
229
|
+
|
|
230
|
+
local services=()
|
|
231
|
+
|
|
232
|
+
# Buscar en archivos de configuraciΓ³n y cΓ³digo
|
|
233
|
+
if grep -rq "stripe" . --include="*.php" --include="*.js" --include="*.env*" 2>/dev/null; then
|
|
234
|
+
services+=("Stripe")
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
if grep -rq "sendgrid" . --include="*.php" --include="*.js" --include="*.env*" 2>/dev/null; then
|
|
238
|
+
services+=("SendGrid")
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
if grep -rq "mailgun" . --include="*.php" --include="*.js" --include="*.env*" 2>/dev/null; then
|
|
242
|
+
services+=("Mailgun")
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
if grep -rq "aws" . --include="*.php" --include="*.js" --include="*.env*" 2>/dev/null; then
|
|
246
|
+
services+=("AWS S3")
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
if grep -rq "firebase" . --include="*.php" --include="*.js" --include="*.json" 2>/dev/null; then
|
|
250
|
+
services+=("Firebase")
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
if grep -rq "pusher" . --include="*.php" --include="*.js" --include="*.env*" 2>/dev/null; then
|
|
254
|
+
services+=("Pusher")
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
if [ ${#services[@]} -gt 0 ]; then
|
|
258
|
+
SERVICES=$(printf '"%s",' "${services[@]}" | sed 's/,$//')
|
|
259
|
+
echo " Services: ${services[*]}"
|
|
260
|
+
else
|
|
261
|
+
SERVICES=""
|
|
262
|
+
echo " Services: None detected"
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
echo ""
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# ==============================================================================
|
|
269
|
+
# FUNCIΓN: Analizar Estructura del Proyecto
|
|
270
|
+
# ==============================================================================
|
|
271
|
+
|
|
272
|
+
analyze_structure() {
|
|
273
|
+
echo "π Analizando estructura..."
|
|
274
|
+
|
|
275
|
+
local total_files=0
|
|
276
|
+
local php_files=0
|
|
277
|
+
local js_files=0
|
|
278
|
+
local blade_files=0
|
|
279
|
+
local controllers=0
|
|
280
|
+
local models=0
|
|
281
|
+
local migrations=0
|
|
282
|
+
local components=0
|
|
283
|
+
|
|
284
|
+
total_files=$(find . -type f -not -path "./node_modules/*" -not -path "./vendor/*" -not -path "./.git/*" | wc -l | tr -d ' ')
|
|
285
|
+
|
|
286
|
+
php_files=$(find . -type f -name "*.php" -not -path "./vendor/*" | wc -l | tr -d ' ')
|
|
287
|
+
js_files=$(find . -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) -not -path "./node_modules/*" | wc -l | tr -d ' ')
|
|
288
|
+
blade_files=$(find . -type f -name "*.blade.php" 2>/dev/null | wc -l | tr -d ' ')
|
|
289
|
+
|
|
290
|
+
# Laravel especΓfico
|
|
291
|
+
if [ -d "app/Http/Controllers" ]; then
|
|
292
|
+
controllers=$(find app/Http/Controllers -type f -name "*Controller.php" 2>/dev/null | wc -l | tr -d ' ')
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
if [ -d "app/Models" ]; then
|
|
296
|
+
models=$(find app/Models -type f -name "*.php" 2>/dev/null | wc -l | tr -d ' ')
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if [ -d "database/migrations" ]; then
|
|
300
|
+
migrations=$(find database/migrations -type f -name "*.php" 2>/dev/null | wc -l | tr -d ' ')
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# React/Vue components
|
|
304
|
+
if [ -d "resources/js/components" ]; then
|
|
305
|
+
components=$(find resources/js/components -type f \( -name "*.jsx" -o -name "*.tsx" -o -name "*.vue" \) 2>/dev/null | wc -l | tr -d ' ')
|
|
306
|
+
elif [ -d "src/components" ]; then
|
|
307
|
+
components=$(find src/components -type f \( -name "*.jsx" -o -name "*.tsx" -o -name "*.vue" \) 2>/dev/null | wc -l | tr -d ' ')
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# Contar lΓneas de cΓ³digo (si cloc estΓ‘ instalado)
|
|
311
|
+
local total_lines=0
|
|
312
|
+
if command -v cloc &> /dev/null; then
|
|
313
|
+
total_lines=$(cloc . --json --quiet 2>/dev/null | grep -o '"code":[0-9]*' | head -1 | grep -o '[0-9]*')
|
|
314
|
+
else
|
|
315
|
+
# EstimaciΓ³n simple
|
|
316
|
+
total_lines=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) -not -path "./node_modules/*" -not -path "./vendor/*" -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}')
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
echo " Total files: $total_files"
|
|
320
|
+
echo " PHP files: $php_files"
|
|
321
|
+
echo " JS files: $js_files"
|
|
322
|
+
echo " Total lines: $total_lines"
|
|
323
|
+
echo " Controllers: $controllers"
|
|
324
|
+
echo " Models: $models"
|
|
325
|
+
echo " Migrations: $migrations"
|
|
326
|
+
echo " Components: $components"
|
|
327
|
+
echo ""
|
|
328
|
+
|
|
329
|
+
STRUCTURE_TOTAL_FILES="$total_files"
|
|
330
|
+
STRUCTURE_PHP_FILES="$php_files"
|
|
331
|
+
STRUCTURE_JS_FILES="$js_files"
|
|
332
|
+
STRUCTURE_BLADE_FILES="$blade_files"
|
|
333
|
+
STRUCTURE_TOTAL_LINES="$total_lines"
|
|
334
|
+
STRUCTURE_CONTROLLERS="$controllers"
|
|
335
|
+
STRUCTURE_MODELS="$models"
|
|
336
|
+
STRUCTURE_MIGRATIONS="$migrations"
|
|
337
|
+
STRUCTURE_COMPONENTS="$components"
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# ==============================================================================
|
|
341
|
+
# FUNCIΓN: AnΓ‘lisis EstΓ‘tico de Calidad (PHPStan)
|
|
342
|
+
# ==============================================================================
|
|
343
|
+
|
|
344
|
+
analyze_quality_phpstan() {
|
|
345
|
+
echo "π AnΓ‘lisis estΓ‘tico PHP (PHPStan)..."
|
|
346
|
+
|
|
347
|
+
local phpstan_errors=0
|
|
348
|
+
local phpstan_warnings=0
|
|
349
|
+
|
|
350
|
+
if [ "$BACKEND_LANGUAGE" = "PHP" ]; then
|
|
351
|
+
# Verificar si PHPStan estΓ‘ instalado
|
|
352
|
+
if [ -f "vendor/bin/phpstan" ] || command -v phpstan &> /dev/null; then
|
|
353
|
+
echo " Ejecutando PHPStan nivel 8..."
|
|
354
|
+
|
|
355
|
+
# Ejecutar PHPStan
|
|
356
|
+
if vendor/bin/phpstan analyse --level=8 --error-format=json --no-progress > /tmp/phpstan-report.json 2>/dev/null; then
|
|
357
|
+
phpstan_errors=0
|
|
358
|
+
else
|
|
359
|
+
# Contar errores del JSON
|
|
360
|
+
phpstan_errors=$(grep -o '"message"' /tmp/phpstan-report.json 2>/dev/null | wc -l | tr -d ' ')
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
echo " Errors: $phpstan_errors"
|
|
364
|
+
else
|
|
365
|
+
echo " β οΈ PHPStan no instalado (recomendado instalarlo)"
|
|
366
|
+
phpstan_errors=-1
|
|
367
|
+
fi
|
|
368
|
+
else
|
|
369
|
+
echo " Skipped (no es proyecto PHP)"
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
echo ""
|
|
373
|
+
|
|
374
|
+
QUALITY_PHPSTAN_ERRORS="$phpstan_errors"
|
|
375
|
+
QUALITY_PHPSTAN_WARNINGS="$phpstan_warnings"
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# ==============================================================================
|
|
379
|
+
# FUNCIΓN: AnΓ‘lisis EstΓ‘tico de Calidad (ESLint)
|
|
380
|
+
# ==============================================================================
|
|
381
|
+
|
|
382
|
+
analyze_quality_eslint() {
|
|
383
|
+
echo "π AnΓ‘lisis estΓ‘tico JavaScript (ESLint)..."
|
|
384
|
+
|
|
385
|
+
local eslint_errors=0
|
|
386
|
+
local eslint_warnings=0
|
|
387
|
+
|
|
388
|
+
if [ "$FRONTEND_FRAMEWORK" != "Unknown" ]; then
|
|
389
|
+
# Verificar si ESLint estΓ‘ instalado
|
|
390
|
+
if [ -f "node_modules/.bin/eslint" ] || command -v eslint &> /dev/null; then
|
|
391
|
+
echo " Ejecutando ESLint..."
|
|
392
|
+
|
|
393
|
+
# Ejecutar ESLint
|
|
394
|
+
if npx eslint . --format=json > /tmp/eslint-report.json 2>/dev/null; then
|
|
395
|
+
eslint_errors=0
|
|
396
|
+
eslint_warnings=0
|
|
397
|
+
else
|
|
398
|
+
# Contar errores y warnings
|
|
399
|
+
eslint_errors=$(grep -o '"severity":2' /tmp/eslint-report.json 2>/dev/null | wc -l | tr -d ' ')
|
|
400
|
+
eslint_warnings=$(grep -o '"severity":1' /tmp/eslint-report.json 2>/dev/null | wc -l | tr -d ' ')
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
echo " Errors: $eslint_errors"
|
|
404
|
+
echo " Warnings: $eslint_warnings"
|
|
405
|
+
else
|
|
406
|
+
echo " β οΈ ESLint no instalado (recomendado instalarlo)"
|
|
407
|
+
eslint_errors=-1
|
|
408
|
+
eslint_warnings=-1
|
|
409
|
+
fi
|
|
410
|
+
else
|
|
411
|
+
echo " Skipped (no es proyecto frontend)"
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
echo ""
|
|
415
|
+
|
|
416
|
+
QUALITY_ESLINT_ERRORS="$eslint_errors"
|
|
417
|
+
QUALITY_ESLINT_WARNINGS="$eslint_warnings"
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# ==============================================================================
|
|
421
|
+
# FUNCIΓN: Analizar Cobertura de Tests
|
|
422
|
+
# ==============================================================================
|
|
423
|
+
|
|
424
|
+
analyze_test_coverage() {
|
|
425
|
+
echo "π§ͺ Analizando cobertura de tests..."
|
|
426
|
+
|
|
427
|
+
local php_coverage="0%"
|
|
428
|
+
local js_coverage="0%"
|
|
429
|
+
|
|
430
|
+
# PHP (PHPUnit)
|
|
431
|
+
if [ -f "phpunit.xml" ] && [ -f "vendor/bin/phpunit" ]; then
|
|
432
|
+
echo " Ejecutando PHPUnit con cobertura..."
|
|
433
|
+
|
|
434
|
+
# Ejecutar tests con cobertura (si Xdebug estΓ‘ disponible)
|
|
435
|
+
if php -m | grep -q xdebug; then
|
|
436
|
+
coverage_output=$(vendor/bin/phpunit --coverage-text --colors=never 2>/dev/null | grep "Lines:" | awk '{print $2}')
|
|
437
|
+
if [ -n "$coverage_output" ]; then
|
|
438
|
+
php_coverage="$coverage_output"
|
|
439
|
+
fi
|
|
440
|
+
else
|
|
441
|
+
echo " β οΈ Xdebug no disponible, no se puede medir cobertura"
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
echo " PHP Coverage: $php_coverage"
|
|
445
|
+
else
|
|
446
|
+
echo " β οΈ PHPUnit no configurado"
|
|
447
|
+
fi
|
|
448
|
+
|
|
449
|
+
# JavaScript (Jest)
|
|
450
|
+
if [ -f "jest.config.js" ] || [ -f "jest.config.json" ]; then
|
|
451
|
+
echo " Ejecutando Jest con cobertura..."
|
|
452
|
+
|
|
453
|
+
coverage_output=$(npm test -- --coverage --silent 2>/dev/null | grep "All files" | awk '{print $10}')
|
|
454
|
+
if [ -n "$coverage_output" ]; then
|
|
455
|
+
js_coverage="$coverage_output"
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
echo " JS Coverage: $js_coverage"
|
|
459
|
+
else
|
|
460
|
+
echo " β οΈ Jest no configurado"
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
echo ""
|
|
464
|
+
|
|
465
|
+
TEST_COVERAGE_PHP="$php_coverage"
|
|
466
|
+
TEST_COVERAGE_JS="$js_coverage"
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# ==============================================================================
|
|
470
|
+
# FUNCIΓN: Detectar Dependencias Obsoletas
|
|
471
|
+
# ==============================================================================
|
|
472
|
+
|
|
473
|
+
detect_outdated_dependencies() {
|
|
474
|
+
echo "π¦ Detectando dependencias obsoletas..."
|
|
475
|
+
|
|
476
|
+
local composer_outdated=0
|
|
477
|
+
local npm_outdated=0
|
|
478
|
+
|
|
479
|
+
# Composer
|
|
480
|
+
if [ -f "composer.json" ] && command -v composer &> /dev/null; then
|
|
481
|
+
echo " Verificando composer..."
|
|
482
|
+
composer_outdated=$(composer outdated --direct 2>/dev/null | grep -c "^" || echo "0")
|
|
483
|
+
echo " Composer outdated: $composer_outdated packages"
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
# npm
|
|
487
|
+
if [ -f "package.json" ] && command -v npm &> /dev/null; then
|
|
488
|
+
echo " Verificando npm..."
|
|
489
|
+
npm_outdated=$(npm outdated 2>/dev/null | grep -c "^" || echo "0")
|
|
490
|
+
echo " npm outdated: $npm_outdated packages"
|
|
491
|
+
fi
|
|
492
|
+
|
|
493
|
+
echo ""
|
|
494
|
+
|
|
495
|
+
OUTDATED_COMPOSER="$composer_outdated"
|
|
496
|
+
OUTDATED_NPM="$npm_outdated"
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# ==============================================================================
|
|
500
|
+
# FUNCIΓN: Detectar Deuda TΓ©cnica
|
|
501
|
+
# ==============================================================================
|
|
502
|
+
|
|
503
|
+
detect_technical_debt() {
|
|
504
|
+
echo "β οΈ Detectando deuda tΓ©cnica..."
|
|
505
|
+
|
|
506
|
+
# Array para almacenar issues
|
|
507
|
+
TECH_DEBT_ISSUES=()
|
|
508
|
+
|
|
509
|
+
# TD-001: Tests insuficientes
|
|
510
|
+
php_cov_num=$(echo "$TEST_COVERAGE_PHP" | sed 's/%//')
|
|
511
|
+
js_cov_num=$(echo "$TEST_COVERAGE_JS" | sed 's/%//')
|
|
512
|
+
|
|
513
|
+
if [ "$php_cov_num" != "0" ] && [ "$php_cov_num" -lt 90 ] 2>/dev/null; then
|
|
514
|
+
echo " π΄ TD-001: Cobertura de tests insuficiente (PHP: $TEST_COVERAGE_PHP, JS: $TEST_COVERAGE_JS)"
|
|
515
|
+
TECH_DEBT_ISSUES+=("TD-001")
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# TD-002: Errores PHPStan
|
|
519
|
+
if [ "$QUALITY_PHPSTAN_ERRORS" -gt 0 ] 2>/dev/null; then
|
|
520
|
+
echo " π‘ TD-002: Errores PHPStan detectados ($QUALITY_PHPSTAN_ERRORS errores)"
|
|
521
|
+
TECH_DEBT_ISSUES+=("TD-002")
|
|
522
|
+
fi
|
|
523
|
+
|
|
524
|
+
# TD-003: N+1 queries (buscar patrones)
|
|
525
|
+
if grep -rq "foreach.*->get()" app/Http/Controllers/ 2>/dev/null; then
|
|
526
|
+
echo " π΄ TD-003: Posibles N+1 queries detectadas"
|
|
527
|
+
TECH_DEBT_ISSUES+=("TD-003")
|
|
528
|
+
fi
|
|
529
|
+
|
|
530
|
+
# TD-004: CΓ³digo duplicado
|
|
531
|
+
# (RequerirΓa phpcpd o similar - placeholder)
|
|
532
|
+
echo " π‘ TD-004: Verificar cΓ³digo duplicado (requiere anΓ‘lisis manual)"
|
|
533
|
+
TECH_DEBT_ISSUES+=("TD-004")
|
|
534
|
+
|
|
535
|
+
# TD-005: Rate limiting
|
|
536
|
+
if [ -f "routes/api.php" ]; then
|
|
537
|
+
if ! grep -q "throttle" routes/api.php 2>/dev/null; then
|
|
538
|
+
echo " π΄ TD-005: API sin rate limiting"
|
|
539
|
+
TECH_DEBT_ISSUES+=("TD-005")
|
|
540
|
+
fi
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
# TD-006: Dependencias obsoletas
|
|
544
|
+
if [ "$OUTDATED_COMPOSER" -gt 0 ] || [ "$OUTDATED_NPM" -gt 0 ]; then
|
|
545
|
+
echo " π‘ TD-006: Dependencias obsoletas ($OUTDATED_COMPOSER composer, $OUTDATED_NPM npm)"
|
|
546
|
+
TECH_DEBT_ISSUES+=("TD-006")
|
|
547
|
+
fi
|
|
548
|
+
|
|
549
|
+
# TD-007: Complejidad ciclomΓ‘tica
|
|
550
|
+
# (RequerirΓa phpmetrics o similar - placeholder)
|
|
551
|
+
echo " π’ TD-007: Verificar complejidad ciclomΓ‘tica (requiere anΓ‘lisis manual)"
|
|
552
|
+
TECH_DEBT_ISSUES+=("TD-007")
|
|
553
|
+
|
|
554
|
+
# TD-008: DocumentaciΓ³n API
|
|
555
|
+
if [ -f "routes/api.php" ] && [ ! -f "storage/api-docs/api-docs.json" ]; then
|
|
556
|
+
echo " π‘ TD-008: API sin documentaciΓ³n OpenAPI"
|
|
557
|
+
TECH_DEBT_ISSUES+=("TD-008")
|
|
558
|
+
fi
|
|
559
|
+
|
|
560
|
+
echo " Total issues: ${#TECH_DEBT_ISSUES[@]}"
|
|
561
|
+
echo ""
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
# ==============================================================================
|
|
565
|
+
# FUNCIΓN: Detectar Monorepo
|
|
566
|
+
# ==============================================================================
|
|
567
|
+
|
|
568
|
+
detect_monorepo() {
|
|
569
|
+
echo "π¦ Detectando monorepo..."
|
|
570
|
+
|
|
571
|
+
local is_monorepo=false
|
|
572
|
+
local packages=()
|
|
573
|
+
|
|
574
|
+
if [ -d "packages" ]; then
|
|
575
|
+
package_count=$(find packages -maxdepth 1 -type d | tail -n +2 | wc -l | tr -d ' ')
|
|
576
|
+
|
|
577
|
+
if [ "$package_count" -gt 0 ]; then
|
|
578
|
+
is_monorepo=true
|
|
579
|
+
echo " β
Monorepo detectado ($package_count packages)"
|
|
580
|
+
|
|
581
|
+
# Listar packages
|
|
582
|
+
while IFS= read -r pkg; do
|
|
583
|
+
pkg_name=$(basename "$pkg")
|
|
584
|
+
packages+=("$pkg_name")
|
|
585
|
+
done < <(find packages -maxdepth 1 -type d | tail -n +2)
|
|
586
|
+
fi
|
|
587
|
+
fi
|
|
588
|
+
|
|
589
|
+
if [ "$is_monorepo" = false ]; then
|
|
590
|
+
echo " No es monorepo"
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
echo ""
|
|
594
|
+
|
|
595
|
+
MONOREPO="$is_monorepo"
|
|
596
|
+
MONOREPO_PACKAGES=$(printf '"%s",' "${packages[@]}" | sed 's/,$//')
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
# ==============================================================================
|
|
600
|
+
# FUNCIΓN: Generar JSON de Salida
|
|
601
|
+
# ==============================================================================
|
|
602
|
+
|
|
603
|
+
generate_json_output() {
|
|
604
|
+
echo "π Generando reporte JSON..."
|
|
605
|
+
|
|
606
|
+
# Escapar comillas en strings
|
|
607
|
+
PROJECT_PATH_ESC=$(echo "$PROJECT_PATH" | sed 's/"/\\"/g')
|
|
608
|
+
|
|
609
|
+
cat > "$OUTPUT_FILE" <<EOF
|
|
610
|
+
{
|
|
611
|
+
"project_name": "$PROJECT_NAME",
|
|
612
|
+
"project_path": "$PROJECT_PATH_ESC",
|
|
613
|
+
"detection_date": "$DETECTION_DATE",
|
|
614
|
+
|
|
615
|
+
"stack": {
|
|
616
|
+
"backend": {
|
|
617
|
+
"framework": "$BACKEND_FRAMEWORK",
|
|
618
|
+
"version": "$BACKEND_VERSION",
|
|
619
|
+
"language": "$BACKEND_LANGUAGE",
|
|
620
|
+
"language_version": "$BACKEND_LANG_VERSION"
|
|
621
|
+
},
|
|
622
|
+
"frontend": {
|
|
623
|
+
"framework": "$FRONTEND_FRAMEWORK",
|
|
624
|
+
"version": "$FRONTEND_VERSION",
|
|
625
|
+
"build_tool": "$FRONTEND_BUILD_TOOL",
|
|
626
|
+
"ui_library": "$FRONTEND_UI_LIBRARY"
|
|
627
|
+
},
|
|
628
|
+
"database": {
|
|
629
|
+
"type": "$DATABASE_TYPE",
|
|
630
|
+
"version": "$DATABASE_VERSION",
|
|
631
|
+
"tables_count": $DATABASE_TABLES_COUNT
|
|
632
|
+
},
|
|
633
|
+
"services": [$SERVICES]
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
"structure": {
|
|
637
|
+
"total_files": $STRUCTURE_TOTAL_FILES,
|
|
638
|
+
"php_files": $STRUCTURE_PHP_FILES,
|
|
639
|
+
"js_files": $STRUCTURE_JS_FILES,
|
|
640
|
+
"blade_files": $STRUCTURE_BLADE_FILES,
|
|
641
|
+
"total_lines": $STRUCTURE_TOTAL_LINES,
|
|
642
|
+
"controllers": $STRUCTURE_CONTROLLERS,
|
|
643
|
+
"models": $STRUCTURE_MODELS,
|
|
644
|
+
"migrations": $STRUCTURE_MIGRATIONS,
|
|
645
|
+
"components": $STRUCTURE_COMPONENTS
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
"quality": {
|
|
649
|
+
"phpstan": {
|
|
650
|
+
"errors": $QUALITY_PHPSTAN_ERRORS,
|
|
651
|
+
"warnings": $QUALITY_PHPSTAN_WARNINGS
|
|
652
|
+
},
|
|
653
|
+
"eslint": {
|
|
654
|
+
"errors": $QUALITY_ESLINT_ERRORS,
|
|
655
|
+
"warnings": $QUALITY_ESLINT_WARNINGS
|
|
656
|
+
},
|
|
657
|
+
"test_coverage": {
|
|
658
|
+
"php": "$TEST_COVERAGE_PHP",
|
|
659
|
+
"js": "$TEST_COVERAGE_JS"
|
|
660
|
+
},
|
|
661
|
+
"outdated_dependencies": {
|
|
662
|
+
"composer": $OUTDATED_COMPOSER,
|
|
663
|
+
"npm": $OUTDATED_NPM
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
"technical_debt": [
|
|
668
|
+
$(generate_tech_debt_json)
|
|
669
|
+
],
|
|
670
|
+
|
|
671
|
+
"monorepo": $MONOREPO,
|
|
672
|
+
"packages": [$MONOREPO_PACKAGES]
|
|
673
|
+
}
|
|
674
|
+
EOF
|
|
675
|
+
|
|
676
|
+
echo " β
Reporte guardado en: $OUTPUT_FILE"
|
|
677
|
+
echo ""
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
# ==============================================================================
|
|
681
|
+
# FUNCIΓN: Generar JSON de Deuda TΓ©cnica
|
|
682
|
+
# ==============================================================================
|
|
683
|
+
|
|
684
|
+
generate_tech_debt_json() {
|
|
685
|
+
local json_items=()
|
|
686
|
+
|
|
687
|
+
# TD-001: Tests
|
|
688
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-001 " ]]; then
|
|
689
|
+
json_items+=(' {
|
|
690
|
+
"id": "TD-001",
|
|
691
|
+
"type": "tests",
|
|
692
|
+
"severity": "high",
|
|
693
|
+
"title": "Insufficient test coverage",
|
|
694
|
+
"description": "PHP coverage: '"$TEST_COVERAGE_PHP"', JS coverage: '"$TEST_COVERAGE_JS"'. Target: 90%+",
|
|
695
|
+
"affected_files": ["all"],
|
|
696
|
+
"estimated_effort_hours": 40
|
|
697
|
+
}')
|
|
698
|
+
fi
|
|
699
|
+
|
|
700
|
+
# TD-002: PHPStan
|
|
701
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-002 " ]]; then
|
|
702
|
+
json_items+=(' {
|
|
703
|
+
"id": "TD-002",
|
|
704
|
+
"type": "code_quality",
|
|
705
|
+
"severity": "medium",
|
|
706
|
+
"title": "PHPStan errors and warnings",
|
|
707
|
+
"description": "'"$QUALITY_PHPSTAN_ERRORS"' errors at level 8",
|
|
708
|
+
"affected_files": [],
|
|
709
|
+
"estimated_effort_hours": 8
|
|
710
|
+
}')
|
|
711
|
+
fi
|
|
712
|
+
|
|
713
|
+
# TD-003: N+1
|
|
714
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-003 " ]]; then
|
|
715
|
+
json_items+=(' {
|
|
716
|
+
"id": "TD-003",
|
|
717
|
+
"type": "performance",
|
|
718
|
+
"severity": "high",
|
|
719
|
+
"title": "N+1 query problems",
|
|
720
|
+
"description": "Detected potential N+1 queries in controllers",
|
|
721
|
+
"affected_files": [],
|
|
722
|
+
"estimated_effort_hours": 6
|
|
723
|
+
}')
|
|
724
|
+
fi
|
|
725
|
+
|
|
726
|
+
# TD-004: Code duplication
|
|
727
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-004 " ]]; then
|
|
728
|
+
json_items+=(' {
|
|
729
|
+
"id": "TD-004",
|
|
730
|
+
"type": "code_duplication",
|
|
731
|
+
"severity": "medium",
|
|
732
|
+
"title": "Duplicated code",
|
|
733
|
+
"description": "Requires manual analysis with phpcpd",
|
|
734
|
+
"affected_files": [],
|
|
735
|
+
"estimated_effort_hours": 4
|
|
736
|
+
}')
|
|
737
|
+
fi
|
|
738
|
+
|
|
739
|
+
# TD-005: Rate limiting
|
|
740
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-005 " ]]; then
|
|
741
|
+
json_items+=(' {
|
|
742
|
+
"id": "TD-005",
|
|
743
|
+
"type": "security",
|
|
744
|
+
"severity": "high",
|
|
745
|
+
"title": "Missing rate limiting",
|
|
746
|
+
"description": "API endpoints lack rate limiting configuration",
|
|
747
|
+
"affected_files": ["routes/api.php"],
|
|
748
|
+
"estimated_effort_hours": 3
|
|
749
|
+
}')
|
|
750
|
+
fi
|
|
751
|
+
|
|
752
|
+
# TD-006: Outdated deps
|
|
753
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-006 " ]]; then
|
|
754
|
+
json_items+=(' {
|
|
755
|
+
"id": "TD-006",
|
|
756
|
+
"type": "dependencies",
|
|
757
|
+
"severity": "medium",
|
|
758
|
+
"title": "Outdated dependencies",
|
|
759
|
+
"description": "'"$OUTDATED_COMPOSER"' composer packages and '"$OUTDATED_NPM"' npm packages outdated",
|
|
760
|
+
"affected_files": ["composer.json", "package.json"],
|
|
761
|
+
"estimated_effort_hours": 5
|
|
762
|
+
}')
|
|
763
|
+
fi
|
|
764
|
+
|
|
765
|
+
# TD-007: Complexity
|
|
766
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-007 " ]]; then
|
|
767
|
+
json_items+=(' {
|
|
768
|
+
"id": "TD-007",
|
|
769
|
+
"type": "code_complexity",
|
|
770
|
+
"severity": "low",
|
|
771
|
+
"title": "High cyclomatic complexity",
|
|
772
|
+
"description": "Requires analysis with phpmetrics",
|
|
773
|
+
"affected_files": [],
|
|
774
|
+
"estimated_effort_hours": 3
|
|
775
|
+
}')
|
|
776
|
+
fi
|
|
777
|
+
|
|
778
|
+
# TD-008: API docs
|
|
779
|
+
if [[ " ${TECH_DEBT_ISSUES[@]} " =~ " TD-008 " ]]; then
|
|
780
|
+
json_items+=(' {
|
|
781
|
+
"id": "TD-008",
|
|
782
|
+
"type": "documentation",
|
|
783
|
+
"severity": "medium",
|
|
784
|
+
"title": "Missing API documentation",
|
|
785
|
+
"description": "No OpenAPI/Swagger specs for API endpoints",
|
|
786
|
+
"affected_files": ["routes/api.php"],
|
|
787
|
+
"estimated_effort_hours": 8
|
|
788
|
+
}')
|
|
789
|
+
fi
|
|
790
|
+
|
|
791
|
+
# Join con comas
|
|
792
|
+
printf '%s' "$(IFS=,; echo "${json_items[*]}")"
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
# ==============================================================================
|
|
796
|
+
# EJECUCIΓN PRINCIPAL
|
|
797
|
+
# ==============================================================================
|
|
798
|
+
|
|
799
|
+
main() {
|
|
800
|
+
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
|
801
|
+
echo " SpecLeap β AnΓ‘lisis de Proyecto Legacy"
|
|
802
|
+
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
|
803
|
+
echo ""
|
|
804
|
+
|
|
805
|
+
detect_backend
|
|
806
|
+
detect_frontend
|
|
807
|
+
detect_database
|
|
808
|
+
detect_services
|
|
809
|
+
analyze_structure
|
|
810
|
+
analyze_quality_phpstan
|
|
811
|
+
analyze_quality_eslint
|
|
812
|
+
analyze_test_coverage
|
|
813
|
+
detect_outdated_dependencies
|
|
814
|
+
detect_technical_debt
|
|
815
|
+
detect_monorepo
|
|
816
|
+
generate_json_output
|
|
817
|
+
|
|
818
|
+
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
|
819
|
+
echo " β
AnΓ‘lisis completado"
|
|
820
|
+
echo "ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
|
|
821
|
+
echo ""
|
|
822
|
+
echo "π Reporte JSON: $OUTPUT_FILE"
|
|
823
|
+
echo ""
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
main
|