#!/usr/bin/env python3
"""
sipi_cli.py - CLI profesional de SiPi.

Uso:
    sipi ejecutar archivo.sipi      Ejecuta un programa SiPi
    sipi crear nombre_proyecto      Crea un proyecto nuevo con estructura estandar
    sipi compilar archivo.sipi      Compila un programa a un ejecutable independiente
    sipi doc archivo.sipi           Genera documentacion HTML desde comentarios //! del codigo
    sipi instalar nombre_o_url      Instala un modulo .sipi suelto (un solo archivo)
    sipi paquete crear "nombre"     Arma un sipi.json para publicar un paquete multi-archivo
    sipi paquete publicar           Empaqueta y publica en el registro de paquetes
    sipi paquete instalar nombre    Instala un paquete del registro (con dependencias, semver)
    sipi paquete buscar texto       Busca paquetes en el registro configurado
    sipi cache tamaño               Muestra cuanto ocupa la cache (.sipic) en esta carpeta
    sipi cache limpiar               Borra los .sipic encontrados (pide confirmacion, o --todo)
    sipi benchmarks                  Corre los benchmarks oficiales (loops, funciones, strings, listas, archivos, hilos)
    sipi publicar                   Genera la carpeta PUBLICACION/ lista para distribuir
    sipi tutorial                   Corre el tutorial interactivo para principiantes
    sipi tutorial avanzado          Tutorial de temas intermedios/avanzados (POO, BD, hilos)
    sipi test / sipi probar         Corre la bateria de pruebas automatizadas (regresion)
    sipi repl                       Abre la consola interactiva de SiPi
    sipi formato archivo.sipi       Reindenta el archivo (4 espacios por nivel)
    sipi corregir archivo.sipi      Corrige errores tipograficos chicos y guarda el archivo
    sipi analizar archivo.sipi      Revisa bugs, seguridad y estilo (sin ejecutar el programa)
    sipi depurar archivo.sipi       Ejecuta mostrando cada linea antes de correrla
    sipi ayuda                      Muestra esta ayuda
    sipi ayuda mostrar comando      Ver que hace un comando puntual, con ejemplo
    sipi ayuda buscar texto         Buscar comandos por palabra clave

(En Windows, "sipi" es el nombre del .bat que llama a este script; en
Linux/macOS se usa "python3 sipi_cli.py <subcomando> ...".)
"""
import os
import sys
import json
import subprocess
import tempfile

AQUI = os.path.dirname(os.path.abspath(__file__))


def _advertir_si_carpeta_volatil():
    """Misma deteccion que editor_sipi.py: si 'sipi_cli.py' se esta
    corriendo desde adentro de la carpeta temporal del sistema (tipico de
    abrir el .zip descargado con doble clic sin extraerlo antes), avisa
    con causa y solucion en vez de dejar que aparezca despues un
    '[Errno 2] No such file or directory' sin contexto."""
    try:
        temporal = os.path.normcase(os.path.abspath(tempfile.gettempdir()))
        actual = os.path.normcase(AQUI)
        if actual.startswith(temporal):
            print("=" * 70)
            print("[SiPi] Aviso: estas ejecutando SiPi desde una carpeta temporal:")
            print(f"       {AQUI}")
            print("       Es probable que hayas abierto el .zip descargado sin")
            print("       extraerlo primero. Extrae el .zip completo a una carpeta")
            print("       normal del disco y volve a correr SiPi desde ahi -- si no,")
            print("       Windows puede borrar estos archivos en cualquier momento,")
            print("       incluso a mitad de la ejecucion.")
            print("=" * 70)
    except OSError:
        pass


def _error_motor_no_encontrado(nombre_normal, nombre_protegido):
    print(f"[SiPi] No se encontro '{nombre_normal}' ni '{nombre_protegido}' en esta carpeta.")
    print(f"       Carpeta donde se busco: {AQUI}")
    print("       Causa mas probable: SiPi se esta ejecutando desde una copia")
    print("       incompleta (por ejemplo, un .zip sin extraer del todo) o un")
    print("       archivo fue borrado por un antivirus/limpiador de temporales.")
    print("       Extrae el proyecto completo a una carpeta normal e intenta de nuevo.")


def _ruta_motor():
    normal = os.path.join(AQUI, "sipi.py")
    protegido = os.path.join(AQUI, "sipi_protegido.py")
    if os.path.exists(normal):
        return normal
    if os.path.exists(protegido):
        return protegido
    _error_motor_no_encontrado("sipi.py", "sipi_protegido.py")
    sys.exit(1)


def _ruta_generar_exe():
    normal = os.path.join(AQUI, "generar_exe.py")
    protegido = os.path.join(AQUI, "generar_exe_protegido.py")
    if os.path.exists(normal):
        return normal
    if os.path.exists(protegido):
        return protegido
    _error_motor_no_encontrado("generar_exe.py", "generar_exe_protegido.py")
    sys.exit(1)


def _cargar_motor_como_modulo():
    """Carga el motor de SiPi (sipi.py o sipi_protegido.py) como modulo de
    Python, sin depender de que se llame literalmente 'sipi', para poder
    reutilizar su logica interna (instalar modulos, etc.) sin duplicarla."""
    import importlib.util
    ruta = _ruta_motor()
    spec = importlib.util.spec_from_file_location("motor_sipi_cli", ruta)
    modulo = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(modulo)
    return modulo


def cmd_ejecutar(args):
    if not args:
        print("Uso: sipi ejecutar archivo.sipi")
        sys.exit(1)
    archivo = args[0]
    if not os.path.exists(archivo):
        print(f"[SiPi] Error: no se encontro el archivo '{archivo}'")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, _ruta_motor(), archivo] + args[1:])
    sys.exit(resultado.returncode)


def cmd_compilar(args):
    if not args:
        print("Uso: sipi compilar archivo.sipi")
        sys.exit(1)
    archivo = args[0]
    if not os.path.exists(archivo):
        print(f"[SiPi] Error: no se encontro el archivo '{archivo}'")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, _ruta_generar_exe(), archivo])
    sys.exit(resultado.returncode)


def cmd_doc(args):
    if not args:
        print("Uso: sipi doc archivo.sipi")
        sys.exit(1)
    archivo = args[0]
    if not os.path.exists(archivo):
        print(f"[SiPi] Error: no se encontro el archivo '{archivo}'")
        sys.exit(1)
    ruta_generador = os.path.join(AQUI, "generar_docs.py")
    resultado = subprocess.run([sys.executable, ruta_generador, archivo])
    sys.exit(resultado.returncode)


def cmd_instalar(args):
    if not args:
        print('Uso: sipi instalar "nombre_o_url"')
        print("     sipi instalar --dependencias   (instala todo lo declarado en sipi_paquetes.json)")
        sys.exit(1)
    motor = _cargar_motor_como_modulo()
    interp = motor.Interprete(os.path.join(os.getcwd(), "_sipi_cli_temporal.sipi"))
    interp.entorno = motor.Entorno()
    if args[0] in ("--dependencias", "-d", "dependencias"):
        interp._instalar_dependencias()
        return
    for nombre_o_url in args:
        try:
            interp._instalar_modulo(nombre_o_url)
        except motor.SiPiError as e:
            print(f"[SiPi] Error instalando '{nombre_o_url}': {e}")
            sys.exit(1)


def cmd_publicar(args):
    ruta_publicar = os.path.join(AQUI, "publicar.py")
    if not os.path.exists(ruta_publicar):
        print("[SiPi] Error: no se encontro 'publicar.py' en esta carpeta (esta es una carpeta de desarrollo?).")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, ruta_publicar])
    sys.exit(resultado.returncode)


PLANTILLA_MAIN = '''programa "{nombre}"

// Punto de entrada de tu proyecto. Corre con: sipi ejecutar main.sipi
decir "Hola desde {nombre}!"
'''

PLANTILLA_PAQUETES = '''{
  "modulos": {}
}
'''

PLANTILLA_README = '''# {nombre}

Proyecto creado con `sipi crear`.

## Estructura

- `main.sipi` — punto de entrada del programa.
- `ejemplos/` — programas de ejemplo o pruebas sueltas.
- `modulos_instalados/` — modulos de terceros (administrados con `sipi instalar`).
- `sipi_paquetes.json` — dependencias del proyecto (instalar todas con `sipi instalar --dependencias`).

## Como correrlo

```
sipi ejecutar main.sipi
```
'''


def cmd_crear(args):
    if not args:
        print("Uso: sipi crear nombre_proyecto")
        sys.exit(1)
    nombre = args[0]
    if os.path.exists(nombre):
        print(f"[SiPi] Error: ya existe una carpeta o archivo llamado '{nombre}'.")
        sys.exit(1)
    os.makedirs(nombre)
    os.makedirs(os.path.join(nombre, "ejemplos"))
    os.makedirs(os.path.join(nombre, "modulos_instalados"))
    with open(os.path.join(nombre, "main.sipi"), "w", encoding="utf-8") as f:
        f.write(PLANTILLA_MAIN.format(nombre=nombre))
    with open(os.path.join(nombre, "sipi_paquetes.json"), "w", encoding="utf-8") as f:
        f.write(PLANTILLA_PAQUETES)
    with open(os.path.join(nombre, "README.md"), "w", encoding="utf-8") as f:
        f.write(PLANTILLA_README.format(nombre=nombre))
    print(f"[SiPi] Proyecto '{nombre}' creado.")
    print("[SiPi] Para empezar:")
    print(f"         cd {nombre}")
    print("         sipi ejecutar main.sipi")


def cmd_ayuda(args):
    if args and args[0] in ("mostrar", "ver"):
        if len(args) < 2:
            print('Uso: sipi ayuda mostrar nombre_comando')
            sys.exit(1)
        nombre_comando = args[1]
        motor = _cargar_motor_como_modulo()
        ayuda_comandos = getattr(motor, "AYUDA_COMANDOS", {})
        if nombre_comando in ayuda_comandos:
            resumen, ejemplo = ayuda_comandos[nombre_comando]
            print(f"{nombre_comando}: {resumen}\n")
            print("Ejemplo:")
            print(ejemplo)
        elif nombre_comando in getattr(motor, "COMANDOS_CONOCIDOS", []):
            print(f"'{nombre_comando}' es un comando valido de SiPi, pero todavia no tiene una ficha corta.")
            print("Buscalo en DOCUMENTACION.md para ver su sintaxis completa.")
        else:
            import difflib
            candidatos = list(ayuda_comandos.keys()) + list(getattr(motor, "COMANDOS_CONOCIDOS", []))
            sugerencia = difflib.get_close_matches(nombre_comando, candidatos, n=1, cutoff=0.6)
            if sugerencia:
                print(f"No encontre el comando '{nombre_comando}'. ¿Quisiste decir '{sugerencia[0]}'?")
            else:
                print(f"No encontre el comando '{nombre_comando}'.")
        return
    if args and args[0] in ("buscar",):
        if len(args) < 2:
            print("Uso: sipi ayuda buscar texto_a_buscar")
            sys.exit(1)
        texto = " ".join(args[1:]).lower()
        motor = _cargar_motor_como_modulo()
        ayuda_comandos = getattr(motor, "AYUDA_COMANDOS", {})
        encontrados = [
            nombre for nombre, (resumen, _) in ayuda_comandos.items()
            if texto in nombre.lower() or texto in resumen.lower()
        ]
        if encontrados:
            print(f"Comandos que coinciden con '{texto}':")
            for nombre in sorted(encontrados):
                print(f"  - {nombre}: {ayuda_comandos[nombre][0]}")
        else:
            print(f"No encontre ningun comando relacionado con '{texto}'.")
        return
    print(__doc__)
    try:
        motor = _cargar_motor_como_modulo()
        comandos = sorted(getattr(motor, "COMANDOS_CONOCIDOS", []))
        print(f"El lenguaje SiPi tiene {len(comandos)} comandos. Referencia completa en DOCUMENTACION.md.")
        print("Tambien podes usar:")
        print("  sipi ayuda mostrar nombre_comando   Ver que hace un comando y un ejemplo")
        print("  sipi ayuda buscar texto             Buscar comandos por palabra clave")
    except Exception:
        pass


def cmd_tutorial(args):
    # El tutorial interactivo vive dentro de examples/ (categoria
    # 01_Fundamentos), no en una carpeta "ejemplos/" aparte -- se
    # prueban varias rutas por si el archivo se reorganiza en el
    # futuro, para no romper este comando de nuevo en silencio.
    quiere_avanzado = args and args[0] in ("avanzado", "avanzada", "2", "intermedio")
    nombre_archivo = "tutorial_interactivo_avanzado.sipi" if quiere_avanzado else "tutorial_interactivo.sipi"
    candidatos = [
        os.path.join(os.path.dirname(AQUI), "examples", "01_Fundamentos", nombre_archivo),
        os.path.join(os.path.dirname(AQUI), "examples", nombre_archivo),
        os.path.join(AQUI, "ejemplos", nombre_archivo),
    ]
    ruta_tutorial = next((c for c in candidatos if os.path.exists(c)), None)
    if ruta_tutorial is None:
        print(f"[SiPi] No se encontro el tutorial ({nombre_archivo}) -- se busco en examples/01_Fundamentos/.")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, _ruta_motor(), ruta_tutorial])
    if not quiere_avanzado and resultado.returncode == 0:
        print("\n[SiPi] Para seguir con temas mas avanzados (POO, base de datos, hilos):")
        print("       sipi tutorial avanzado")
    sys.exit(resultado.returncode)


def _descubrir_archivos_test(carpeta_raiz):
    """Busca todo archivo que termine en '_test.sipi' (convencion tipo
    pytest/'*_test.py') recursivamente, salteando carpetas de control de
    versiones, dependencias y ocultas para no tardar de mas en proyectos
    grandes."""
    encontrados = []
    for actual, carpetas, archivos in os.walk(carpeta_raiz):
        carpetas[:] = [c for c in carpetas if c not in (".git", "node_modules", "__pycache__")
                       and not c.startswith(".")]
        for nombre in sorted(archivos):
            if nombre.endswith("_test.sipi"):
                encontrados.append(os.path.join(actual, nombre))
    return encontrados


def _correr_archivo_test(ruta_motor, ruta_archivo):
    """Corre un '*_test.sipi' con el motor real y parsea su salida en
    busca de las lineas que 'afirmar' ya imprime en modo pruebas
    ('[SiPi] OK: ...' / '[SiPi] FALLO: ...'), sin inventar un protocolo
    nuevo: son las mismas aserciones que el usuario ya usa a mano con
    'iniciar_pruebas'/'afirmar'/'resumen_pruebas'. Si el archivo no llama
    a 'iniciar_pruebas', cualquier 'afirmar' que falle corta el programa
    con SiPiError -- eso tambien se reporta como un fallo (el mensaje de
    error de stderr pasa a ser el detalle del caso).
    """
    import subprocess as _sp
    import time as _time
    inicio = _time.time()
    resultado = _sp.run(
        [sys.executable, ruta_motor, ruta_archivo],
        capture_output=True, text=True, timeout=60,
        encoding="utf-8", errors="replace",
    )
    duracion = _time.time() - inicio
    casos = []
    for linea in resultado.stdout.splitlines():
        if linea.startswith("[SiPi] OK: "):
            casos.append({"nombre": linea[len("[SiPi] OK: "):], "ok": True, "detalle": ""})
        elif linea.startswith("[SiPi] FALLO: "):
            casos.append({"nombre": linea[len("[SiPi] FALLO: "):], "ok": False, "detalle": linea})
    crasheo = resultado.returncode not in (0, 1)  # 0 = todo bien, 1 = resumen_pruebas con fallos (esperado)
    if crasheo or (not casos and resultado.returncode != 0):
        casos.append({
            "nombre": f"{os.path.basename(ruta_archivo)} (ejecucion)",
            "ok": False,
            "detalle": (resultado.stderr or resultado.stdout or "El programa termino con error.").strip(),
        })
    return casos, duracion


def _escribir_junit_xml(ruta_salida, resultados_por_archivo):
    """Genera un JUnit XML estandar (el formato que Jenkins, GitHub
    Actions, GitLab CI y practicamente cualquier sistema de CI/CD sabe
    leer sin plugins especiales) para que 'sipi test' se pueda integrar
    directo a un pipeline."""
    import xml.etree.ElementTree as ET
    raiz = ET.Element("testsuites")
    for ruta_archivo, (casos, duracion) in resultados_por_archivo.items():
        suite = ET.SubElement(raiz, "testsuite", {
            "name": os.path.basename(ruta_archivo),
            "tests": str(len(casos)),
            "failures": str(sum(1 for c in casos if not c["ok"])),
            "time": f"{duracion:.3f}",
        })
        for caso in casos:
            caso_xml = ET.SubElement(suite, "testcase", {
                "name": caso["nombre"], "classname": os.path.basename(ruta_archivo),
            })
            if not caso["ok"]:
                fallo = ET.SubElement(caso_xml, "failure", {"message": caso["nombre"]})
                fallo.text = caso["detalle"]
    ET.ElementTree(raiz).write(ruta_salida, encoding="utf-8", xml_declaration=True)


def cmd_test(args):
    ruta_junit = None
    args_restantes = []
    i = 0
    while i < len(args):
        if args[i] == "--junit" and i + 1 < len(args):
            ruta_junit = args[i + 1]
            i += 2
            continue
        args_restantes.append(args[i])
        i += 1

    ruta_suite_interna = os.path.join(AQUI, "tests", "test_suite.py")
    archivos_proyecto = _descubrir_archivos_test(os.getcwd())

    if not archivos_proyecto and not os.path.exists(ruta_suite_interna):
        print("[SiPi] No se encontro ningun '*_test.sipi' en esta carpeta (ni tests/test_suite.py del motor).")
        print("[SiPi] Convencion: nombra tus archivos de prueba 'algo_test.sipi' y usa 'iniciar_pruebas'/'afirmar'/'resumen_pruebas'.")
        sys.exit(1)

    if archivos_proyecto:
        print(f"[SiPi] {len(archivos_proyecto)} archivo(s) de prueba encontrados en el proyecto:")
        ruta_motor = _ruta_motor()
        resultados_por_archivo = {}
        total_ok = total_fallos = 0
        for ruta_archivo in archivos_proyecto:
            casos, duracion = _correr_archivo_test(ruta_motor, ruta_archivo)
            resultados_por_archivo[ruta_archivo] = (casos, duracion)
            fallos = sum(1 for c in casos if not c["ok"])
            total_ok += len(casos) - fallos
            total_fallos += fallos
            estado = "OK" if fallos == 0 else f"FALLO ({fallos})"
            print(f"  [{estado}] {os.path.relpath(ruta_archivo)}  ({len(casos)} caso(s), {duracion:.2f}s)")
            for caso in casos:
                if not caso["ok"]:
                    print(f"        - {caso['nombre']}")
        print(f"[SiPi] Total: {total_ok} exitosas, {total_fallos} fallidas, en {len(archivos_proyecto)} archivo(s).")
        if ruta_junit:
            _escribir_junit_xml(ruta_junit, resultados_por_archivo)
            print(f"[SiPi] Reporte JUnit escrito en: {ruta_junit}")
        if total_fallos > 0:
            sys.exit(1)
        # Si tambien existe la suite interna del motor, la corremos a
        # continuacion (compatibilidad con el uso anterior de 'sipi test'
        # dentro del propio repo de SiPi).
        if os.path.exists(ruta_suite_interna):
            resultado = subprocess.run([sys.executable, ruta_suite_interna] + args_restantes)
            sys.exit(resultado.returncode)
        sys.exit(0)

    resultado = subprocess.run([sys.executable, ruta_suite_interna] + args_restantes)
    sys.exit(resultado.returncode)


def cmd_repl(args):
    resultado = subprocess.run([sys.executable, _ruta_motor(), "--repl"])
    sys.exit(resultado.returncode)


def cmd_formato(args):
    if not args:
        print("Uso: sipi formato archivo.sipi")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, _ruta_motor(), "--formatear"] + args)
    sys.exit(resultado.returncode)


def cmd_corregir(args):
    if not args:
        print("Uso: sipi corregir archivo.sipi")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, _ruta_motor(), "--corregir"] + args)
    sys.exit(resultado.returncode)


def cmd_analizar(args):
    if not args:
        print("Uso: sipi analizar archivo.sipi")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, _ruta_motor(), "--revisar"] + args)
    sys.exit(resultado.returncode)


def cmd_depurar(args):
    if not args:
        print("Uso: sipi depurar archivo.sipi [--breakpoint N ...]")
        print("Dentro del depurador: 'ayuda' para ver todos los comandos (paso, continuar, b <linea>, variables, ver <nombre>, etc.)")
        sys.exit(1)
    ruta_depurador = os.path.join(AQUI, "sipi_depurador.py")
    if not os.path.exists(ruta_depurador):
        print("[SiPi] No se encontro 'sipi_depurador.py' junto al CLI.")
        sys.exit(1)
    # Se corre como subproceso (no se importa in-process) para que la
    # entrada/salida interactiva (input()/print() del depurador) se
    # comporte exactamente igual que corriendo cualquier otro programa de
    # SiPi desde la terminal, sin pelearse con el 'sys.path'/estado global
    # del propio 'sipi_cli.py'.
    resultado = subprocess.run([sys.executable, ruta_depurador] + args)
    sys.exit(resultado.returncode)


def _buscar_archivos_cache(carpeta_raiz):
    """Recorre 'carpeta_raiz' buscando todos los '.sipic' (ver CACHE.md:
    son archivos chicos, uno por cada '.sipi', al lado del original -- no
    existe una carpeta central de cache). Se salta '.git', 'node_modules'
    y carpetas ocultas para no tardar de mas en proyectos grandes."""
    encontrados = []
    for actual, carpetas, archivos in os.walk(carpeta_raiz):
        carpetas[:] = [c for c in carpetas if c not in (".git", "node_modules") and not c.startswith(".")]
        for nombre in archivos:
            if nombre.endswith(".sipic"):
                encontrados.append(os.path.join(actual, nombre))
    return encontrados


def cmd_cache(args):
    """Items 37/38/39 del feedback: 'sipi cache tamaño', 'sipi cache
    limpiar' (con confirmacion) y 'sipi cache limpiar --todo' (sin
    preguntar, para scripts/CI). Busca desde el directorio actual hacia
    abajo, ya que la cache de SiPi no vive en una carpeta central sino
    repartida junto a cada '.sipi' (documentado en CACHE.md)."""
    accion = args[0] if args else None
    raiz = os.getcwd()
    if accion == "tamaño" or accion == "tamano":
        archivos = _buscar_archivos_cache(raiz)
        total = sum(os.path.getsize(a) for a in archivos)
        print(f"[SiPi] {len(archivos)} archivo(s) .sipic encontrados bajo {raiz}")
        print(f"[SiPi] Tamaño total: {total / 1024:.1f} KB")
        return
    if accion == "limpiar":
        archivos = _buscar_archivos_cache(raiz)
        if not archivos:
            print("[SiPi] No hay archivos .sipic para limpiar.")
            return
        total = sum(os.path.getsize(a) for a in archivos)
        forzar = "--todo" in args
        if not forzar:
            respuesta = input(
                f"[SiPi] Se van a borrar {len(archivos)} archivo(s) .sipic "
                f"({total / 1024:.1f} KB). ¿Confirmar? (s/n): "
            ).strip().lower()
            if respuesta != "s":
                print("[SiPi] Cancelado, no se borro nada.")
                return
        borrados = 0
        for archivo in archivos:
            try:
                os.remove(archivo)
                borrados += 1
            except OSError as error:
                print(f"[SiPi] No se pudo borrar {archivo}: {error}")
        print(f"[SiPi] {borrados} archivo(s) .sipic borrados ({total / 1024:.1f} KB liberados).")
        print("[SiPi] Se regeneraran solos en la proxima ejecucion de cada .sipi -- no rompe nada.")
        return
    print("Uso:")
    print("    sipi cache tamaño            Muestra cuanto ocupa la cache (.sipic) en esta carpeta")
    print("    sipi cache limpiar           Borra los .sipic encontrados (pide confirmacion)")
    print("    sipi cache limpiar --todo    Borra los .sipic sin preguntar")
    sys.exit(1)


def cmd_benchmarks(args):
    """Item #65 del feedback: benchmarks oficiales. Delega en
    benchmarks.py (motor + generador de programas .sipi reales), pasando
    los argumentos tal cual para no duplicar el parser de opciones."""
    ruta_benchmarks = os.path.join(AQUI, "benchmarks.py")
    if not os.path.exists(ruta_benchmarks):
        print("[SiPi] No se encontro 'benchmarks.py' en esta carpeta.")
        sys.exit(1)
    resultado = subprocess.run([sys.executable, ruta_benchmarks] + args)
    sys.exit(resultado.returncode)


def cmd_paquete(args):
    """sipi paquete crear|publicar|instalar|buscar ... -- cliente real
    del registro central de paquetes (item 1 del roadmap). Ver
    'src/sipi_registro.py' para la implementacion y
    'herramientas/plantilla_registro/' para levantar tu propio
    registro en 5 minutos."""
    sys.path.insert(0, AQUI)
    import sipi_registro as reg

    if not args or args[0] in ("-h", "--help", "ayuda"):
        print("Uso:")
        print('  sipi paquete crear "nombre"                    Arma un sipi.json nuevo aca')
        print('  sipi paquete publicar [--registro carpeta]     Empaqueta y publica el paquete de esta carpeta')
        print('  sipi paquete instalar nombre[@rango]           Instala desde el registro (ej: nombre@^1.2.0)')
        print('  sipi paquete buscar "texto"                    Busca en el indice del registro configurado')
        return

    sub = args[0]
    resto = args[1:]

    if sub == "crear":
        nombre = resto[0] if resto else os.path.basename(os.getcwd())
        ruta_manifiesto = os.path.join(os.getcwd(), "sipi.json")
        if os.path.exists(ruta_manifiesto):
            print(f"[SiPi] Ya existe {ruta_manifiesto}, no lo piso.")
            sys.exit(1)
        manifiesto = {
            "nombre": nombre, "version": "1.0.0", "descripcion": "",
            "punto_entrada": f"{nombre}.sipi", "dependencias": {},
        }
        with open(ruta_manifiesto, "w", encoding="utf-8") as f:
            json.dump(manifiesto, f, indent=2, ensure_ascii=False)
        print(f"[SiPi] Creado {ruta_manifiesto}. Completa 'descripcion' y 'punto_entrada', y 'sipi paquete publicar' cuando este listo.")
        return

    if sub == "publicar":
        clon_registro = None
        if "--registro" in resto:
            i = resto.index("--registro")
            clon_registro = resto[i + 1] if i + 1 < len(resto) else None
        try:
            reg.publicar(os.getcwd(), clon_local_registro=clon_registro)
        except reg.ErrorRegistro as e:
            print(f"[SiPi] Error: {e}")
            sys.exit(1)
        return

    if sub == "instalar":
        if not resto:
            print('Uso: sipi paquete instalar nombre[@rango]')
            sys.exit(1)
        cliente = reg.ClienteRegistro(carpeta_proyecto=os.getcwd())
        for objetivo in resto:
            nombre, _, rango = objetivo.partition("@")
            try:
                cliente.instalar(nombre, rango or "*")
            except reg.ErrorRegistro as e:
                print(f"[SiPi] Error instalando '{nombre}': {e}")
                sys.exit(1)
        return

    if sub == "buscar":
        if not resto:
            print('Uso: sipi paquete buscar "texto"')
            sys.exit(1)
        cliente = reg.ClienteRegistro(carpeta_proyecto=os.getcwd())
        try:
            resultados = cliente.buscar(" ".join(resto))
        except reg.ErrorRegistro as e:
            print(f"[SiPi] Error: {e}")
            sys.exit(1)
        if not resultados:
            print("[SiPi] Sin resultados.")
            return
        for nombre, descripcion, ultima in resultados:
            print(f"  {nombre} ({ultima}) - {descripcion}")
        return

    print(f"[SiPi] Subcomando desconocido: 'sipi paquete {sub}'. Usa 'sipi paquete ayuda'.")
    sys.exit(1)



SUBCOMANDOS = {
    "ejecutar": cmd_ejecutar,
    "crear": cmd_crear,
    "compilar": cmd_compilar,
    "doc": cmd_doc,
    "instalar": cmd_instalar,
    "cache": cmd_cache,
    "benchmarks": cmd_benchmarks,
    "publicar": cmd_publicar,
    "paquete": cmd_paquete,
    "tutorial": cmd_tutorial,
    "test": cmd_test,
    "probar": cmd_test,
    "repl": cmd_repl,
    "formato": cmd_formato,
    "corregir": cmd_corregir,
    "analizar": cmd_analizar,
    "depurar": cmd_depurar,
    "ayuda": cmd_ayuda,
    "help": cmd_ayuda,
    "-h": cmd_ayuda,
    "--help": cmd_ayuda,
}



def main():
    _advertir_si_carpeta_volatil()
    if len(sys.argv) < 2:
        cmd_ayuda([])
        sys.exit(1)
    subcomando = sys.argv[1]
    resto = sys.argv[2:]
    funcion = SUBCOMANDOS.get(subcomando)
    if funcion is None:
        print(f"[SiPi] Subcomando desconocido: '{subcomando}'")
        cmd_ayuda([])
        sys.exit(1)
    funcion(resto)


if __name__ == "__main__":
    main()
