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.
Files changed (47) hide show
  1. package/.agents/backend.md +419 -0
  2. package/.agents/frontend.md +577 -0
  3. package/.agents/producto.md +516 -0
  4. package/.commands/adoptar.md +323 -0
  5. package/.commands/ayuda.md +142 -0
  6. package/.commands/crear-tickets.md +55 -0
  7. package/.commands/documentar.md +285 -0
  8. package/.commands/explicar.md +234 -0
  9. package/.commands/implementar.md +383 -0
  10. package/.commands/inicio.md +824 -0
  11. package/.commands/nuevo/README.md +292 -0
  12. package/.commands/nuevo/questions-base.yaml +320 -0
  13. package/.commands/nuevo/responses-example.yaml +53 -0
  14. package/.commands/planificar.md +253 -0
  15. package/.commands/refinar.md +306 -0
  16. package/LICENSE +21 -0
  17. package/README.md +603 -0
  18. package/SETUP.md +351 -0
  19. package/install.sh +152 -0
  20. package/package.json +60 -0
  21. package/proyectos/_template/.gitkeep +1 -0
  22. package/proyectos/_template/ANEXOS.md +21 -0
  23. package/proyectos/_template/CONTRATO.md +26 -0
  24. package/proyectos/_template/context/.gitkeep +1 -0
  25. package/rules/development-rules.md +113 -0
  26. package/rules/environment-protection.md +97 -0
  27. package/rules/git-workflow.md +142 -0
  28. package/rules/session-protocol.md +121 -0
  29. package/scripts/README.md +129 -0
  30. package/scripts/analyze-project.sh +826 -0
  31. package/scripts/create-asana-tasks.sh +133 -0
  32. package/scripts/detect-project-type.sh +141 -0
  33. package/scripts/estimate-effort.sh +290 -0
  34. package/scripts/generate-asana-structure.sh +262 -0
  35. package/scripts/generate-contract.sh +360 -0
  36. package/scripts/generate-contrato.sh +555 -0
  37. package/scripts/install-git-hooks.sh +141 -0
  38. package/scripts/install-skills.sh +130 -0
  39. package/scripts/lib/asana-utils.sh +191 -0
  40. package/scripts/lib/jira-project-utils.sh +222 -0
  41. package/scripts/lib/questions.json +831 -0
  42. package/scripts/lib/render-contrato.py +195 -0
  43. package/scripts/lib/validate.sh +325 -0
  44. package/scripts/parse-contrato.sh +190 -0
  45. package/scripts/setup-mcp.sh +654 -0
  46. package/scripts/test-cuestionario.sh +428 -0
  47. package/setup.sh +458 -0
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ArchSpec — Renderizador de CONTRATO.md
4
+ Convierte respuestas planas (project.name) a estructura anidada y renderiza con Jinja2
5
+ """
6
+
7
+ import json
8
+ import sys
9
+ from datetime import datetime
10
+ from jinja2 import Environment, FileSystemLoader
11
+ import re
12
+
13
+ def flatten_to_nested(flat_dict):
14
+ """
15
+ Convierte dict plano {'project.name': 'foo'} a anidado {'project': {'name': 'foo'}}
16
+ """
17
+ nested = {}
18
+
19
+ for key, value in flat_dict.items():
20
+ parts = key.split('.')
21
+ current = nested
22
+
23
+ for i, part in enumerate(parts):
24
+ if i == len(parts) - 1:
25
+ # Último nivel, asignar valor
26
+ current[part] = value
27
+ else:
28
+ # Crear nivel intermedio si no existe
29
+ if part not in current:
30
+ current[part] = {}
31
+ current = current[part]
32
+
33
+ return nested
34
+
35
+ def convert_string_to_type(value, expected_type):
36
+ """
37
+ Convierte strings a su tipo apropiado (bool, int, array)
38
+ """
39
+ if expected_type == 'boolean':
40
+ if isinstance(value, bool):
41
+ return value
42
+ return value.lower() in ['true', '1', 'yes', 's', 'si']
43
+
44
+ if expected_type == 'number':
45
+ try:
46
+ return int(value)
47
+ except:
48
+ return 0
49
+
50
+ if expected_type == 'array':
51
+ if isinstance(value, list):
52
+ return value
53
+ # Si es string vacío, devolver array vacío
54
+ if value == "":
55
+ return []
56
+ # Si es JSON array string
57
+ if isinstance(value, str) and value.startswith('['):
58
+ try:
59
+ return json.loads(value)
60
+ except:
61
+ return []
62
+ return [value]
63
+
64
+ return value
65
+
66
+ def normalize_answers(answers_flat, questions_meta):
67
+ """
68
+ Normaliza respuestas según metadata de preguntas (tipos, defaults)
69
+ """
70
+ normalized = {}
71
+
72
+ for q in questions_meta:
73
+ q_id = q['id']
74
+ q_type = q['type']
75
+ q_default = q.get('default', None)
76
+
77
+ # Obtener valor o usar default
78
+ value = answers_flat.get(q_id, q_default)
79
+
80
+ # Convertir a tipo apropiado
81
+ if value is not None:
82
+ value = convert_string_to_type(value, q_type)
83
+
84
+ normalized[q_id] = value
85
+
86
+ return normalized
87
+
88
+ def add_computed_fields(nested_dict):
89
+ """
90
+ Agrega campos computados (fecha actual, contadores, etc.)
91
+ """
92
+ # Agregar fecha actual si no existe
93
+ if 'project' in nested_dict:
94
+ if not nested_dict['project'].get('created_at'):
95
+ nested_dict['project']['created_at'] = datetime.now().strftime('%Y-%m-%d')
96
+
97
+ if not nested_dict['project'].get('status'):
98
+ nested_dict['project']['status'] = 'draft'
99
+
100
+ if not nested_dict['project'].get('version'):
101
+ nested_dict['project']['version'] = '1.0'
102
+
103
+ # Contar features
104
+ if 'features' in nested_dict and 'core' in nested_dict['features']:
105
+ core_count = len(nested_dict['features']['core']) if nested_dict['features']['core'] else 0
106
+ nested_dict['_computed'] = {'core_count': core_count}
107
+
108
+ return nested_dict
109
+
110
+ def main():
111
+ if len(sys.argv) < 4:
112
+ print("Uso: render-contrato.py <answers.json> <questions.json> <template.md> [output.md]")
113
+ sys.exit(1)
114
+
115
+ answers_file = sys.argv[1]
116
+ questions_file = sys.argv[2]
117
+ template_file = sys.argv[3]
118
+ output_file = sys.argv[4] if len(sys.argv) > 4 else None
119
+
120
+ # Leer archivos
121
+ with open(answers_file, 'r') as f:
122
+ answers_flat = json.load(f)
123
+
124
+ with open(questions_file, 'r') as f:
125
+ questions_data = json.load(f)
126
+ questions_meta = questions_data['questions']
127
+
128
+ # Normalizar respuestas según tipos
129
+ answers_normalized = normalize_answers(answers_flat, questions_meta)
130
+
131
+ # Convertir plano a anidado
132
+ answers_nested = flatten_to_nested(answers_normalized)
133
+
134
+ # Agregar campos computados
135
+ answers_nested = add_computed_fields(answers_nested)
136
+
137
+ # Configurar Jinja2
138
+ template_dir = '/'.join(template_file.split('/')[:-1])
139
+ template_name = template_file.split('/')[-1]
140
+
141
+ env = Environment(
142
+ loader=FileSystemLoader(template_dir),
143
+ trim_blocks=True,
144
+ lstrip_blocks=True,
145
+ keep_trailing_newline=True
146
+ )
147
+
148
+ # Leer template (separar YAML frontmatter del contenido Jinja2)
149
+ with open(template_file, 'r') as f:
150
+ template_content = f.read()
151
+
152
+ # Convertir placeholders tipo {VAR} a sintaxis Jinja2 {{ var }} para templates legacy
153
+ # Solo si no es el template principal (que ya usa Jinja2)
154
+ if 'LEGACY' in template_file or '{PROJECT_NAME}' in template_content:
155
+ import re
156
+ # Convertir {VAR_NAME} -> {{ var_name }}
157
+ def convert_placeholder(match):
158
+ var_name = match.group(1).lower().replace('_', '.')
159
+ return '{{ ' + var_name + ' }}'
160
+ template_content = re.sub(r'\{([A-Z_]+)\}', convert_placeholder, template_content)
161
+
162
+ # Detectar frontmatter YAML
163
+ if template_content.startswith('---'):
164
+ parts = template_content.split('---', 2)
165
+ if len(parts) >= 3:
166
+ yaml_front = parts[1]
167
+ jinja_content = parts[2]
168
+ else:
169
+ yaml_front = ""
170
+ jinja_content = template_content
171
+ else:
172
+ yaml_front = ""
173
+ jinja_content = template_content
174
+
175
+ # Renderizar YAML frontmatter
176
+ yaml_template = env.from_string(yaml_front)
177
+ yaml_rendered = yaml_template.render(**answers_nested)
178
+
179
+ # Renderizar contenido Jinja2
180
+ content_template = env.from_string(jinja_content)
181
+ content_rendered = content_template.render(**answers_nested)
182
+
183
+ # Combinar
184
+ final_output = f"---\n{yaml_rendered}---{content_rendered}"
185
+
186
+ # Escribir output
187
+ if output_file:
188
+ with open(output_file, 'w') as f:
189
+ f.write(final_output)
190
+ print(f"✅ CONTRATO.md generado: {output_file}")
191
+ else:
192
+ print(final_output)
193
+
194
+ if __name__ == '__main__':
195
+ main()
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # SpecLeap — Funciones de validación avanzadas
4
+ # Fase 2.2
5
+
6
+ # ============================================================================
7
+ # VALIDACIONES POR TIPO
8
+ # ============================================================================
9
+
10
+ validate_string_advanced() {
11
+ local value="$1"
12
+ local pattern="${2:-}"
13
+ local min_length="${3:-0}"
14
+ local max_length="${4:-999999}"
15
+
16
+ local length=${#value}
17
+
18
+ # Validar longitud mínima
19
+ if [[ $length -lt $min_length ]]; then
20
+ echo "ERROR:Debe tener al menos $min_length caracteres (actualmente: $length)"
21
+ return 1
22
+ fi
23
+
24
+ # Validar longitud máxima
25
+ if [[ $length -gt $max_length ]]; then
26
+ echo "ERROR:Debe tener máximo $max_length caracteres (actualmente: $length)"
27
+ return 1
28
+ fi
29
+
30
+ # Validar patrón regex
31
+ if [[ -n "$pattern" ]] && ! [[ "$value" =~ $pattern ]]; then
32
+ case "$pattern" in
33
+ '^[a-z0-9-]+$')
34
+ echo "ERROR:Solo se permiten letras minúsculas, números y guiones (sin espacios ni mayúsculas)"
35
+ ;;
36
+ '^#[0-9A-Fa-f]{6}$')
37
+ echo "ERROR:Debe ser un código hexadecimal válido (ejemplo: #3B82F6)"
38
+ ;;
39
+ *)
40
+ echo "ERROR:Formato inválido"
41
+ ;;
42
+ esac
43
+ return 1
44
+ fi
45
+
46
+ return 0
47
+ }
48
+
49
+ validate_select_strict() {
50
+ local value="$1"
51
+ shift
52
+ local options=("$@")
53
+
54
+ # Normalizar a minúsculas para comparación
55
+ local value_lower="${value,,}"
56
+
57
+ for opt in "${options[@]}"; do
58
+ if [[ "${opt,,}" == "$value_lower" ]]; then
59
+ # Devolver el valor normalizado (minúsculas)
60
+ echo "$opt"
61
+ return 0
62
+ fi
63
+ done
64
+
65
+ echo "ERROR:Opción inválida. Opciones válidas: ${options[*]}"
66
+ return 1
67
+ }
68
+
69
+ validate_multiselect() {
70
+ local value="$1"
71
+ shift
72
+ local valid_options=("$@")
73
+
74
+ # Si está vacío, es válido (si no es required)
75
+ if [[ -z "$value" ]]; then
76
+ return 0
77
+ fi
78
+
79
+ # Separar por comas
80
+ IFS=',' read -ra selected <<< "$value"
81
+
82
+ for item in "${selected[@]}"; do
83
+ # Trim espacios
84
+ item="$(echo "$item" | xargs)"
85
+ item_lower="${item,,}"
86
+
87
+ local found=false
88
+ for opt in "${valid_options[@]}"; do
89
+ if [[ "${opt,,}" == "$item_lower" ]]; then
90
+ found=true
91
+ break
92
+ fi
93
+ done
94
+
95
+ if [[ "$found" == false ]]; then
96
+ echo "ERROR:Opción inválida '$item'. Opciones válidas: ${valid_options[*]}"
97
+ return 1
98
+ fi
99
+ done
100
+
101
+ return 0
102
+ }
103
+
104
+ validate_boolean_strict() {
105
+ local value="$1"
106
+
107
+ case "${value,,}" in
108
+ true|s|si|yes|y|1)
109
+ echo "true"
110
+ return 0
111
+ ;;
112
+ false|n|no|0)
113
+ echo "false"
114
+ return 0
115
+ ;;
116
+ *)
117
+ echo "ERROR:Respuesta inválida. Usa: s/n, true/false, yes/no, 1/0"
118
+ return 1
119
+ ;;
120
+ esac
121
+ }
122
+
123
+ validate_number_range() {
124
+ local value="$1"
125
+ local min="${2:-0}"
126
+ local max="${3:-999999999}"
127
+
128
+ # Validar que sea número
129
+ if ! [[ "$value" =~ ^[0-9]+$ ]]; then
130
+ echo "ERROR:Debe ser un número entero"
131
+ return 1
132
+ fi
133
+
134
+ # Validar rango
135
+ if [[ $value -lt $min ]]; then
136
+ echo "ERROR:Debe ser al menos $min"
137
+ return 1
138
+ fi
139
+
140
+ if [[ $value -gt $max ]]; then
141
+ echo "ERROR:Debe ser máximo $max"
142
+ return 1
143
+ fi
144
+
145
+ return 0
146
+ }
147
+
148
+ validate_array_items() {
149
+ local value="$1"
150
+ local min_items="${2:-0}"
151
+ local max_items="${3:-999}"
152
+ local separator="${4:-,}"
153
+
154
+ # Contar items
155
+ if [[ -z "$value" ]]; then
156
+ local count=0
157
+ else
158
+ IFS="$separator" read -ra items <<< "$value"
159
+ local count=${#items[@]}
160
+ fi
161
+
162
+ if [[ $count -lt $min_items ]]; then
163
+ echo "ERROR:Debes proporcionar al menos $min_items elemento(s)"
164
+ return 1
165
+ fi
166
+
167
+ if [[ $count -gt $max_items ]]; then
168
+ echo "ERROR:Puedes proporcionar máximo $max_items elemento(s)"
169
+ return 1
170
+ fi
171
+
172
+ return 0
173
+ }
174
+
175
+ # ============================================================================
176
+ # CONDICIONES Y DEPENDENCIAS
177
+ # ============================================================================
178
+
179
+ should_skip_question() {
180
+ local skip_if_json="$1"
181
+ local answers_file="$2"
182
+
183
+ # Si no hay skip_if, no skipear
184
+ if [[ "$skip_if_json" == "null" ]] || [[ -z "$skip_if_json" ]]; then
185
+ return 1
186
+ fi
187
+
188
+ # Extraer campo y valor esperado
189
+ local skip_field=$(echo "$skip_if_json" | jq -r 'keys[0]')
190
+ local skip_value=$(echo "$skip_if_json" | jq -r '.[]')
191
+
192
+ # Obtener valor actual del campo
193
+ local current_value=$(jq -r ".\"$skip_field\" // \"\"" "$answers_file")
194
+
195
+ # Si el valor actual coincide con el valor de skip, saltamos
196
+ if [[ "$current_value" == "$skip_value" ]]; then
197
+ return 0 # Sí, skipear
198
+ fi
199
+
200
+ return 1 # No skipear
201
+ }
202
+
203
+ get_auto_suggest() {
204
+ local auto_suggest_json="$1"
205
+ local dependent_field="$2"
206
+ local answers_file="$3"
207
+
208
+ # Si no hay auto_suggest, devolver vacío
209
+ if [[ "$auto_suggest_json" == "null" ]] || [[ -z "$auto_suggest_json" ]]; then
210
+ echo ""
211
+ return
212
+ fi
213
+
214
+ # Obtener valor del campo del que depende
215
+ local dep_value=$(jq -r ".\"$dependent_field\" // \"\"" "$answers_file")
216
+
217
+ # Obtener sugerencia basada en ese valor
218
+ local suggestion=$(echo "$auto_suggest_json" | jq -r ".\"$dep_value\" // \"\"")
219
+
220
+ echo "$suggestion"
221
+ }
222
+
223
+ # ============================================================================
224
+ # VALIDADOR PRINCIPAL
225
+ # ============================================================================
226
+
227
+ validate_answer() {
228
+ local value="$1"
229
+ local question_json="$2"
230
+ local answers_file="$3"
231
+
232
+ local q_type=$(echo "$question_json" | jq -r '.type')
233
+ local q_required=$(echo "$question_json" | jq -r '.required')
234
+ local q_validation=$(echo "$question_json" | jq -r '.validation // {}')
235
+ local q_options=$(echo "$question_json" | jq -r '.options // [] | join(" ")')
236
+
237
+ # Si es vacío y no es requerido, OK
238
+ if [[ -z "$value" ]] && [[ "$q_required" == "false" ]]; then
239
+ echo "OK"
240
+ return 0
241
+ fi
242
+
243
+ # Si es vacío y es requerido, ERROR
244
+ if [[ -z "$value" ]] && [[ "$q_required" == "true" ]]; then
245
+ echo "ERROR:Esta pregunta es obligatoria"
246
+ return 1
247
+ fi
248
+
249
+ # Validar según tipo
250
+ case "$q_type" in
251
+ string)
252
+ local pattern=$(echo "$q_validation" | jq -r '.pattern // ""')
253
+ local min_length=$(echo "$q_validation" | jq -r '.min_length // 0')
254
+ local max_length=$(echo "$q_validation" | jq -r '.max_length // 999999')
255
+
256
+ if ! validate_string_advanced "$value" "$pattern" "$min_length" "$max_length"; then
257
+ return 1
258
+ fi
259
+ echo "OK"
260
+ ;;
261
+
262
+ text)
263
+ local max_length=$(echo "$q_validation" | jq -r '.max_length // 999999')
264
+
265
+ if ! validate_string_advanced "$value" "" "0" "$max_length"; then
266
+ return 1
267
+ fi
268
+ echo "OK"
269
+ ;;
270
+
271
+ select)
272
+ local opts_array=($(echo "$q_options"))
273
+ local result
274
+ if ! result=$(validate_select_strict "$value" "${opts_array[@]}"); then
275
+ echo "$result"
276
+ return 1
277
+ fi
278
+ echo "OK:$result"
279
+ ;;
280
+
281
+ multiselect)
282
+ local opts_array=($(echo "$q_options"))
283
+ if ! validate_multiselect "$value" "${opts_array[@]}"; then
284
+ return 1
285
+ fi
286
+ echo "OK"
287
+ ;;
288
+
289
+ boolean)
290
+ local result
291
+ if ! result=$(validate_boolean_strict "$value"); then
292
+ echo "$result"
293
+ return 1
294
+ fi
295
+ echo "OK:$result"
296
+ ;;
297
+
298
+ number)
299
+ local min=$(echo "$q_validation" | jq -r '.min // 0')
300
+ local max=$(echo "$q_validation" | jq -r '.max // 999999999')
301
+
302
+ if ! validate_number_range "$value" "$min" "$max"; then
303
+ return 1
304
+ fi
305
+ echo "OK"
306
+ ;;
307
+
308
+ array)
309
+ local min_items=$(echo "$q_validation" | jq -r '.min_items // 0')
310
+ local max_items=$(echo "$q_validation" | jq -r '.max_items // 999')
311
+ local separator=$(echo "$q_validation" | jq -r '.separator // ","')
312
+
313
+ if ! validate_array_items "$value" "$min_items" "$max_items" "$separator"; then
314
+ return 1
315
+ fi
316
+ echo "OK"
317
+ ;;
318
+
319
+ *)
320
+ echo "OK"
321
+ ;;
322
+ esac
323
+
324
+ return 0
325
+ }
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Cargar sistema i18n
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/../.specleap/i18n.sh"
6
+
7
+ # SpecLeap — Parser CONTRATO.md → JSON
8
+ # Extrae YAML frontmatter y lo convierte a JSON para procesamiento
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+
14
+ # Colores
15
+ RED='\033[0;31m'
16
+ GREEN='\033[0;32m'
17
+ CYAN='\033[0;36m'
18
+ RESET='\033[0m'
19
+
20
+ print_error() {
21
+ echo -e "${RED}$(t "scripts.error"): $1${RESET}" >&2
22
+ }
23
+
24
+ print_success() {
25
+ echo -e "${GREEN}$(t "scripts.success") $1${RESET}" >&2
26
+ }
27
+
28
+ print_info() {
29
+ echo -e "${CYAN}$(t "scripts.info") $1${RESET}" >&2
30
+ }
31
+
32
+ # ============================================================================
33
+ # PARSER
34
+ # ============================================================================
35
+
36
+ parse_contrato() {
37
+ local contrato_file="$1"
38
+ local output_file="${2:-}"
39
+
40
+ if [[ ! -f "$contrato_file" ]]; then
41
+ print_error "Archivo no encontrado: $contrato_file"
42
+ return 1
43
+ fi
44
+
45
+ # Verificar que yq esté instalado
46
+ if ! command -v yq &> /dev/null; then
47
+ print_error "yq no está instalado. Instala con: brew install yq"
48
+ return 1
49
+ fi
50
+
51
+ print_info "Parseando $contrato_file..."
52
+
53
+ # Extraer solo el YAML frontmatter (entre --- ---)
54
+ # Usa awk para capturar solo el primer bloque entre --- (líneas 2 hasta el segundo ---)
55
+ local json_output
56
+ if json_output=$(awk '/^---$/{if(++n==2) exit; next} n==1' "$contrato_file" | yq eval -o=json '.' - 2>/dev/null); then
57
+ if [[ -n "$output_file" ]]; then
58
+ echo "$json_output" > "$output_file"
59
+ print_success "JSON exportado a: $output_file"
60
+ else
61
+ echo "$json_output"
62
+ fi
63
+ return 0
64
+ else
65
+ print_error "Fallo al parsear YAML frontmatter"
66
+ return 1
67
+ fi
68
+ }
69
+
70
+ # ============================================================================
71
+ # VALIDACIÓN
72
+ # ============================================================================
73
+
74
+ validate_contrato() {
75
+ local contrato_file="$1"
76
+
77
+ print_info "Validando estructura CONTRATO.md..."
78
+
79
+ # Parsear a JSON
80
+ local json
81
+ if ! json=$(parse_contrato "$contrato_file"); then
82
+ return 1
83
+ fi
84
+
85
+ # Validar campos obligatorios
86
+ local required_fields=(
87
+ ".project.name"
88
+ ".project.display_name"
89
+ ".identity.objective"
90
+ ".identity.problem_solved"
91
+ ".stack.backend.framework"
92
+ ".features.core"
93
+ )
94
+
95
+ local errors=0
96
+
97
+ for field in "${required_fields[@]}"; do
98
+ local value=$(echo "$json" | jq -r "$field // empty")
99
+ if [[ -z "$value" ]] || [[ "$value" == "null" ]]; then
100
+ print_error "Campo obligatorio faltante: $field"
101
+ errors=$((errors + 1))
102
+ fi
103
+ done
104
+
105
+ if [[ $errors -eq 0 ]]; then
106
+ print_success "CONTRATO válido"
107
+ return 0
108
+ else
109
+ print_error "CONTRATO inválido: $errors campos faltantes"
110
+ return 1
111
+ fi
112
+ }
113
+
114
+ # ============================================================================
115
+ # EXTRACCIÓN DE CAMPOS ESPECÍFICOS
116
+ # ============================================================================
117
+
118
+ get_project_name() {
119
+ local contrato_file="$1"
120
+ parse_contrato "$contrato_file" | jq -r '.project.name'
121
+ }
122
+
123
+ get_core_features() {
124
+ local contrato_file="$1"
125
+ parse_contrato "$contrato_file" | jq -r '.features.core[]'
126
+ }
127
+
128
+ get_backend_framework() {
129
+ local contrato_file="$1"
130
+ parse_contrato "$contrato_file" | jq -r '.stack.backend.framework'
131
+ }
132
+
133
+ get_frontend_framework() {
134
+ local contrato_file="$1"
135
+ parse_contrato "$contrato_file" | jq -r '.stack.frontend.framework'
136
+ }
137
+
138
+ # ============================================================================
139
+ # MAIN
140
+ # ============================================================================
141
+
142
+ main() {
143
+ local command="${1:-parse}"
144
+ local contrato_file="${2:-}"
145
+ local output_file="${3:-}"
146
+
147
+ case "$command" in
148
+ parse)
149
+ if [[ -z "$contrato_file" ]]; then
150
+ echo "Uso: $0 parse <CONTRATO.md> [output.json]"
151
+ exit 1
152
+ fi
153
+ parse_contrato "$contrato_file" "$output_file"
154
+ ;;
155
+ validate)
156
+ if [[ -z "$contrato_file" ]]; then
157
+ echo "Uso: $0 validate <CONTRATO.md>"
158
+ exit 1
159
+ fi
160
+ validate_contrato "$contrato_file"
161
+ ;;
162
+ get-name)
163
+ if [[ -z "$contrato_file" ]]; then
164
+ echo "Uso: $0 get-name <CONTRATO.md>"
165
+ exit 1
166
+ fi
167
+ get_project_name "$contrato_file"
168
+ ;;
169
+ get-features)
170
+ if [[ -z "$contrato_file" ]]; then
171
+ echo "Uso: $0 get-features <CONTRATO.md>"
172
+ exit 1
173
+ fi
174
+ get_core_features "$contrato_file"
175
+ ;;
176
+ *)
177
+ echo "Comandos disponibles:"
178
+ echo " parse <CONTRATO.md> [output.json] — Parsear YAML → JSON"
179
+ echo " validate <CONTRATO.md> — Validar estructura"
180
+ echo " get-name <CONTRATO.md> — Obtener nombre proyecto"
181
+ echo " get-features <CONTRATO.md> — Listar features core"
182
+ exit 1
183
+ ;;
184
+ esac
185
+ }
186
+
187
+ # Ejecutar si se llama directamente
188
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
189
+ main "$@"
190
+ fi