# functions

id: inspect_ssl_cert_bash_cybersecurity
name: inspect_ssl_cert
kind: function
lang: bash
domain: cybersecurity
version: 1.0.0
purity: impure
signature: inspect_ssl_cert(host: string) -> void
tags: ["bash","cybersecurity","ssl","tls","certificate","web","openssl","security"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/cybersecurity/inspect_ssl_cert.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: a5507dbab5e0ef47b4539050df093b1e7cc4255256155fe0f48fdd3d25846f37
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/ciberseguridad/web/ssl_cert_info.sh
description:
  Inspecciona el certificado SSL/TLS de un host: muestra sujeto, emisor, fechas de validez, días hasta expiración, SANs (Subject Alternative Names), cadena de confianza completa y detecta soporte de versiones inseguras TLS 1.0/1.1.

example:
  ```bash
  source bash/functions/cybersecurity/inspect_ssl_cert.sh

  # Puerto 443 por defecto
  inspect_ssl_cert example.com

  # Puerto personalizado
  inspect_ssl_cert example.com:8443

  # API interna
  inspect_ssl_cert api.internal.example.com:4443
  ```

notes:
  Requiere `openssl` y `timeout`. Usa `openssl s_client` con SNI (`-servername`) para soportar virtual hosting. La alerta de expiración se activa a 30 días o menos. La detección de TLS 1.0/1.1 usa flags `-tls1` y `-tls1_1` de openssl s_client — si el servidor acepta la conexión y negocia un cipher, el protocolo inseguro está habilitado. Cada conexión tiene timeout de 10 segundos para evitar cuelgues en hosts sin respuesta.

code:
  #!/usr/bin/env bash
  # inspect_ssl_cert
  # ----------------
  # Inspecciona el certificado SSL/TLS de un host: sujeto, emisor, fechas de validez,
  # SANs, cadena de confianza y versiones de TLS aceptadas (detecta TLS 1.0/1.1 inseguros).
  #
  # USO (directo):
  #   inspect_ssl_cert <host[:puerto]>
  #   inspect_ssl_cert example.com
  #   inspect_ssl_cert example.com:8443
  #
  # Depende de: openssl, timeout

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR/../shell/bash_colors.sh"
  source "$SCRIPT_DIR/../shell/bash_log.sh"
  bash_colors
  bash_log_init

  # ─── Constantes ───────────────────────────────────────────────────────────────

  _SSL_WARN_DAYS=30

  # ─── Funciones puras ──────────────────────────────────────────────────────────

  _ssl_parse_host_port() {
      local input="$1"
      if [[ "$input" =~ ^(.+):([0-9]+)$ ]]; then
          echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"
      else
          echo "${input} 443"
      fi
  }

  _ssl_days_until_expiry() {
      local expiry_str="$1"
      local expiry_epoch
      expiry_epoch="$(date -d "$expiry_str" +%s 2>/dev/null || echo 0)"
      local now_epoch
      now_epoch="$(date +%s)"
      echo $(( (expiry_epoch - now_epoch) / 86400 ))
  }

  # ─── Funciones de efecto ──────────────────────────────────────────────────────

  _ssl_fetch_subject() {
      local host="$1"
      local port="$2"
      echo | timeout 10 openssl s_client -connect "${host}:${port}" -servername "$host" 2>/dev/null \
          | openssl x509 -noout -subject -issuer 2>/dev/null
  }

  _ssl_fetch_dates() {
      local host="$1"
      local port="$2"
      echo | timeout 10 openssl s_client -connect "${host}:${port}" -servername "$host" 2>/dev/null \
          | openssl x509 -noout -dates 2>/dev/null
  }

  _ssl_fetch_san() {
      local host="$1"
      local port="$2"
      echo | timeout 10 openssl s_client -connect "${host}:${port}" -servername "$host" 2>/dev/null \
          | openssl x509 -noout -ext subjectAltName 2>/dev/null \
          | grep -oE 'DNS:[^,]+' | sed 's/DNS://g' | tr '\n' ' '
  }

  _ssl_fetch_chain() {
      local host="$1"
      local port="$2"
      echo | timeout 10 openssl s_client -connect "${host}:${port}" -servername "$host" \
          -showcerts 2>/dev/null \
          | grep -E "^(subject|issuer)=" | sed 's/^/    /'
  }

  _ssl_check_tls_version() {
      local host="$1"
      local port="$2"
      local proto="$3"
      local label="$4"
      if echo | timeout 5 openssl s_client -connect "${host}:${port}" \
          -servername "$host" "${proto}" 2>/dev/null | grep -q "Cipher"; then
          echo -e "  ${RED}[x]${NC} ${label} -- soportado (inseguro)"
      else
          echo -e "  ${GREEN}[ok]${NC} ${label} no soportado"
      fi
  }

  # ─── Punto de entrada ─────────────────────────────────────────────────────────

  inspect_ssl_cert() {
      local input="$1"

      if [[ -z "$input" ]]; then
          error "inspect_ssl_cert: se requiere un host como argumento (ej: example.com o example.com:8443)" >&2
          return 1
      fi

      if ! command -v openssl &>/dev/null; then
          error "inspect_ssl_cert: 'openssl' no está instalado (sudo apt install openssl)" >&2
          return 1
      fi

      local host port
      read -r host port <<< "$(_ssl_parse_host_port "$input")"

      info "Conectando a ${host}:${port}..."
      echo ""

      local subj_issuer
      subj_issuer="$(_ssl_fetch_subject "$host" "$port")"
      if [[ -z "$subj_issuer" ]]; then
          error "inspect_ssl_cert: no se pudo obtener el certificado. ¿El host está disponible?" >&2
          return 1
      fi

      local subject issuer
      subject="$(echo "$subj_issuer" | grep ^subject | cut -d= -f2- | xargs)"
      issuer="$(echo "$subj_issuer"  | grep ^issuer  | cut -d= -f2- | xargs)"

      local dates
      dates="$(_ssl_fetch_dates "$host" "$port")"
      local not_before not_after
      not_before="$(echo "$dates" | grep notBefore | cut -d= -f2)"
      not_after="$(echo "$dates"  | grep notAfter  | cut -d= -f2)"

      local days_left
      days_left="$(_ssl_days_until_expiry "$not_after")"

      local sans
      sans="$(_ssl_fetch_san "$host" "$port")"

      echo -e "${PURPLE}════════ Certificado SSL/TLS ════════════════════${NC}"
      echo -e "  ${CYAN}Sujeto:${NC}        ${subject}"
      echo -e "  ${CYAN}Emisor:${NC}        ${issuer}"
      echo -e "  ${CYAN}Válido desde:${NC}  ${not_before}"
      echo -e "  ${CYAN}Válido hasta:${NC}  ${not_after}"
      echo ""

      if [[ $days_left -le 0 ]]; then
          echo -e "  ${RED}[x] CERTIFICADO EXPIRADO${NC}"
      elif [[ $days_left -le $_SSL_WARN_DAYS ]]; then
          echo -e "  ${YELLOW}[!] Expira en ${days_left} días -- renovar pronto${NC}"
      else
          echo -e "  ${GREEN}[ok] Válido -- expira en ${days_left} días${NC}"
      fi

      echo ""
      echo -e "  ${CYAN}SANs:${NC}"
      echo "$sans" | tr ' ' '\n' | grep -v '^$' | while IFS= read -r san; do
          echo -e "    ${GREEN}*${NC} ${san}"
      done

      echo ""
      echo -e "${PURPLE}════════ Cadena de confianza ════════════════════${NC}"
      _ssl_fetch_chain "$host" "$port"

      echo ""
      echo -e "${PURPLE}════════ Versiones TLS aceptadas ════════════════${NC}"
      _ssl_check_tls_version "$host" "$port" "-tls1"   "TLS 1.0"
      _ssl_check_tls_version "$host" "$port" "-tls1_1" "TLS 1.1"
      echo -e "  ${GREEN}[ok]${NC} TLS 1.2 / 1.3 (estándar)"
      echo -e "${PURPLE}═════════════════════════════════════════════════${NC}"
  }

  # Ejecutar si se llama directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      inspect_ssl_cert "$@"
  fi

params_schema:
  {"params":[{"name":"host","desc":"host a inspeccionar, acepta formato host o host:puerto (por defecto puerto 443), ej: example.com o example.com:8443"}],"output":"imprime detalles del certificado SSL/TLS, días hasta expiración con nivel de alerta, SANs, cadena de confianza y resultado de checks de versiones TLS"}


id: list_active_connections_bash_cybersecurity
name: list_active_connections
kind: function
lang: bash
domain: cybersecurity
version: 1.0.0
purity: impure
signature: list_active_connections(mode: string) -> void
tags: ["bash","cybersecurity","network","connections","monitoring","ss","ports"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/cybersecurity/list_active_connections.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 5f5d050fdaf6abb6597b8fc83a04bdb38c6cc54d886b5f806eb8f484cce43778
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/ciberseguridad/redes/conexiones_activas.sh
description:
  Muestra conexiones de red activas del sistema usando ss: puertos en escucha, conexiones establecidas y detección de conexiones hacia IPs externas (excluye RFC1918, loopback y link-local).

example:
  ```bash
  source bash/functions/cybersecurity/list_active_connections.sh

  # Todas las conexiones
  list_active_connections

  # Solo puertos en escucha
  list_active_connections listening

  # Solo conexiones hacia internet
  list_active_connections external
  ```

notes:
  Requiere `ss` del paquete iproute2 (disponible por defecto en la mayoría de distribuciones modernas). La detección de IPs externas excluye: 127.x, ::1, 0.0.0.0, rangos RFC1918 (10.x, 172.16-31.x, 192.168.x) y link-local (fe80:). Usa `ss -tnp` para mostrar el proceso asociado a cada conexión (puede requerir sudo para ver procesos de otros usuarios).

code:
  #!/usr/bin/env bash
  # list_active_connections
  # -----------------------
  # Muestra conexiones de red activas del sistema: puertos en escucha,
  # conexiones establecidas y detección de IPs externas (no RFC1918).
  #
  # USO (directo):
  #   list_active_connections [listening|established|external|all]
  #
  # Depende de: ss (iproute2)

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR/../shell/bash_colors.sh"
  source "$SCRIPT_DIR/../shell/bash_log.sh"
  bash_colors
  bash_log_init

  # ─── Funciones puras ──────────────────────────────────────────────────────────

  _conn_is_external_ip() {
      local ip="$1"
      # Devuelve 0 (verdadero) si no es loopback, link-local ni RFC1918
      [[ ! "$ip" =~ ^(127\.|::1|0\.0\.0\.0|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|fe80:) ]]
  }

  # ─── Funciones de efecto ──────────────────────────────────────────────────────

  _conn_show_listening() {
      info "Puertos en escucha con proceso asociado..."
      echo ""
      echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
      ss -tlnp 2>/dev/null \
          | awk 'NR==1 {printf "%-6s %-25s %-25s %s\n", "Proto", "Local", "Peer", "Proceso"} NR>1 {printf "%-6s %-25s %-25s %s\n", $1, $4, $5, $7}' \
          | while IFS= read -r line; do echo -e "  ${DIM_GRAY}${line}${NC}"; done
      echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
  }

  _conn_show_established() {
      info "Conexiones establecidas..."
      echo ""
      echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
      ss -tnp state established 2>/dev/null \
          | awk 'NR==1 {printf "%-6s %-25s %-25s %s\n", "Proto", "Local", "Peer", "Proceso"} NR>1 {printf "%-6s %-25s %-25s %s\n", $1, $4, $5, $6}' \
          | while IFS= read -r line; do echo -e "  ${DIM_GRAY}${line}${NC}"; done
      echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
  }

  _conn_show_external() {
      info "Conexiones hacia IPs externas..."
      echo ""
      echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"

      local found=0
      while IFS= read -r line; do
          local peer
          peer="$(echo "$line" | awk '{print $5}' | cut -d: -f1)"
          if _conn_is_external_ip "$peer"; then
              echo -e "  ${YELLOW}*${NC} $line"
              found=$((found + 1))
          fi
      done < <(ss -tnp state established 2>/dev/null | tail -n +2)

      echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
      echo ""
      if [[ $found -eq 0 ]]; then
          success "No se detectaron conexiones hacia IPs externas"
      else
          info "Total conexiones externas: $found"
      fi
  }

  # ─── Punto de entrada ─────────────────────────────────────────────────────────

  list_active_connections() {
      local mode="${1:-all}"

      if ! command -v ss &>/dev/null; then
          error "list_active_connections: 'ss' no está disponible (sudo apt install iproute2)" >&2
          return 1
      fi

      case "$mode" in
          listening)
              _conn_show_listening
              ;;
          established)
              _conn_show_established
              ;;
          external)
              _conn_show_external
              ;;
          all)
              _conn_show_listening
              echo ""
              _conn_show_established
              echo ""
              _conn_show_external
              ;;
          *)
              error "list_active_connections: modo no válido '$mode'. Use: listening|established|external|all" >&2
              return 1
              ;;
      esac
  }

  # Ejecutar si se llama directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      list_active_connections "$@"
  fi

params_schema:
  {"params":[{"name":"mode","desc":"modo de visualización: listening (puertos en escucha), established (conexiones activas), external (solo IPs externas) o all (todo, por defecto)"}],"output":"imprime tabla de conexiones de red a stdout con colores ANSI; las conexiones a IPs externas se resaltan en amarillo"}


id: verify_file_hash_bash_cybersecurity
name: verify_file_hash
kind: function
lang: bash
domain: cybersecurity
version: 1.0.0
purity: impure
signature: verify_file_hash(file: string, algorithm: string, expected_hash: string) -> void
tags: ["bash","cybersecurity","hash","integrity","checksum","md5","sha256","sha512"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/cybersecurity/verify_file_hash.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: f2139f531913b1163696636398bcae670175133ced8af836495c182441392657
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/ciberseguridad/utilidades/verificar_hash.sh
description:
  Calcula el hash criptográfico de un archivo con el algoritmo especificado (md5, sha1, sha256, sha512) y opcionalmente lo compara con un hash esperado para verificar integridad. Retorna exit code 1 si los hashes no coinciden.

example:
  ```bash
  source bash/functions/cybersecurity/verify_file_hash.sh

  # Solo calcular el hash
  verify_file_hash archivo.iso sha256

  # Verificar contra un hash conocido
  verify_file_hash archivo.iso sha256 "a1b2c3d4e5f6..."

  # MD5 (solo para compatibilidad, no recomendado para seguridad)
  verify_file_hash documento.pdf md5
  ```

notes:
  La comparación de hashes es case-insensitive (normaliza a minúsculas). SHA256 es el algoritmo recomendado para verificación de integridad. MD5 y SHA1 están deprecados para uso en seguridad pero se incluyen para compatibilidad con sumas publicadas en sistemas legacy. Retorna exit code 1 cuando los hashes no coinciden, lo que permite usar la función en scripts con `set -e`.

code:
  #!/usr/bin/env bash
  # verify_file_hash
  # ----------------
  # Calcula el hash de un archivo con el algoritmo indicado (md5, sha1, sha256, sha512)
  # y opcionalmente lo compara con un hash esperado.
  #
  # USO (directo):
  #   verify_file_hash <archivo> <md5|sha1|sha256|sha512> [hash_esperado]
  #
  # Depende de: md5sum, sha1sum, sha256sum, sha512sum

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR/../shell/bash_colors.sh"
  source "$SCRIPT_DIR/../shell/bash_log.sh"
  bash_colors
  bash_log_init

  # ─── Funciones puras ──────────────────────────────────────────────────────────

  _hash_select_cmd() {
      local algo="$1"
      case "$algo" in
          md5)    echo "md5sum" ;;
          sha1)   echo "sha1sum" ;;
          sha256) echo "sha256sum" ;;
          sha512) echo "sha512sum" ;;
          *)      echo "" ;;
      esac
  }

  _hash_hashes_match() {
      local a="${1,,}"
      local b="${2,,}"
      [[ "$a" == "$b" ]]
  }

  # ─── Funciones de efecto ──────────────────────────────────────────────────────

  _hash_compute() {
      local cmd="$1"
      local file="$2"
      "$cmd" "$file" 2>/dev/null | awk '{print $1}'
  }

  # ─── Punto de entrada ─────────────────────────────────────────────────────────

  verify_file_hash() {
      local file="$1"
      local algorithm="$2"
      local expected_hash="${3:-}"

      if [[ -z "$file" || -z "$algorithm" ]]; then
          error "verify_file_hash: uso: verify_file_hash <archivo> <md5|sha1|sha256|sha512> [hash_esperado]" >&2
          return 1
      fi

      if [[ ! -f "$file" ]]; then
          error "verify_file_hash: archivo no encontrado: $file" >&2
          return 1
      fi

      local cmd
      cmd="$(_hash_select_cmd "$algorithm")"

      if [[ -z "$cmd" ]]; then
          error "verify_file_hash: algoritmo no válido '$algorithm'. Use: md5|sha1|sha256|sha512" >&2
          return 1
      fi

      if ! command -v "$cmd" &>/dev/null; then
          error "verify_file_hash: '$cmd' no está disponible" >&2
          return 1
      fi

      info "Calculando ${algorithm^^} de: $(basename "$file")"
      local hash
      hash="$(_hash_compute "$cmd" "$file")"

      echo ""
      echo -e "  ${CYAN}Archivo:${NC} ${file}"
      echo -e "  ${CYAN}${algorithm^^}:${NC}    ${hash}"
      echo ""

      if [[ -n "$expected_hash" ]]; then
          if _hash_hashes_match "$hash" "$expected_hash"; then
              echo -e "  ${GREEN}[COINCIDE]${NC} La integridad del archivo es correcta"
          else
              echo -e "  ${RED}[NO COINCIDE]${NC} El archivo puede estar corrupto o modificado"
              echo ""
              echo -e "  ${CYAN}Calculado:${NC} ${hash}"
              echo -e "  ${CYAN}Esperado: ${NC} ${expected_hash}"
              return 1
          fi
      fi
  }

  # Ejecutar si se llama directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      verify_file_hash "$@"
  fi

params_schema:
  {"params":[{"name":"file","desc":"ruta al archivo del que calcular el hash"},{"name":"algorithm","desc":"algoritmo de hash a usar: md5, sha1, sha256 (recomendado) o sha512"},{"name":"expected_hash","desc":"hash esperado en hexadecimal para verificar integridad (opcional; si se omite, solo calcula e imprime)"}],"output":"imprime el hash calculado; si se proporcionó expected_hash, imprime COINCIDE o NO COINCIDE y retorna exit code 1 si no coinciden"}


id: analyze_disk_space_bash_infra
name: analyze_disk_space
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: analyze_disk_space([target_dir: string], [mode: string]) -> void
tags: ["bash","disk","space","analysis","filesystem"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/analyze_disk_space.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 00768368fbd681ae0f0f74fd414dc245c75fe58e5b0443e5afd383b153f92386
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/gestion_linux/espacio_disponible.sh
description:
  Analiza el uso de espacio en disco. Modos: partitions (df con filtros), top-dirs (du top 10), top-files (find top 20), inodes (df -i), all (todos). Emite advertencias si el uso supera el 90%.

example:
  ```bash
  source bash/functions/infra/analyze_disk_space.sh

  # Análisis completo del directorio raíz
  analyze_disk_space

  # Solo particiones
  analyze_disk_space / partitions

  # Top directorios en home
  analyze_disk_space "$HOME" top-dirs

  # Solo inodos
  analyze_disk_space / inodes
  ```

notes:
  Excluye tmpfs, devtmpfs y loop de los resultados de df. No realiza ninguna limpieza destructiva. El modo top-files puede tardar en sistemas con muchos archivos.

code:
  #!/usr/bin/env bash
  # analyze_disk_space
  # ------------------
  # Analiza el uso de espacio en disco: particiones, directorios más grandes,
  # archivos más grandes e inodos.
  #
  # USO:
  #   source analyze_disk_space.sh
  #   analyze_disk_space [target_dir] [mode]
  #
  # ARGUMENTOS:
  #   target_dir   Directorio a analizar (default: /)
  #   mode         Modo de análisis: partitions|top-dirs|top-files|inodes|all (default: all)

  analyze_disk_space() {
      local target_dir="${1:-/}"
      local mode="${2:-all}"

      _ads_partitions() {
          echo "=== Espacio en sistemas de archivos ==="
          df -h --output=source,fstype,size,used,avail,pcent,target 2>/dev/null \
              | grep -v "tmpfs\|devtmpfs\|loop" | column -t
          echo ""

          local high_usage
          high_usage="$(df -h | awk 'NR>1 && $5+0 > 90 {print $6, $5}' | grep -v "tmpfs\|devtmpfs" || true)"
          if [[ -n "$high_usage" ]]; then
              echo "ADVERTENCIA: Discos con uso >90%:"
              echo "$high_usage"
              echo ""
          fi
      }

      _ads_top_dirs() {
          local dir="${1:-.}"
          echo "=== Top 10 carpetas más grandes en: $(realpath "$dir") ==="
          du -h --max-depth=1 "$dir" 2>/dev/null | sort -rh | head -11 \
              | awk '{printf "%-10s %s\n", $1, $2}'
          echo ""
      }

      _ads_top_files() {
          local dir="${1:-.}"
          echo "=== Top 20 archivos más grandes en: $(realpath "$dir") ==="
          find "$dir" -type f -exec du -h {} + 2>/dev/null | sort -rh | head -20 \
              | awk '{printf "%-10s %s\n", $1, $2}'
          echo ""
      }

      _ads_inodes() {
          echo "=== Inodos disponibles ==="
          df -i 2>/dev/null | grep -v "tmpfs\|devtmpfs\|loop" | column -t
          echo ""

          local high_inodes
          high_inodes="$(df -i | awk 'NR>1 && $5+0 > 90 {print $6, $5}' | grep -v "tmpfs\|devtmpfs" || true)"
          if [[ -n "$high_inodes" ]]; then
              echo "ADVERTENCIA: Sistemas de archivos con >90% de inodos usados:"
              echo "$high_inodes"
              echo ""
          fi
      }

      case "$mode" in
          partitions)
              _ads_partitions
              ;;
          top-dirs)
              _ads_top_dirs "$target_dir"
              ;;
          top-files)
              _ads_top_files "$target_dir"
              ;;
          inodes)
              _ads_inodes
              ;;
          all)
              _ads_partitions
              _ads_top_dirs "$target_dir"
              _ads_top_files "$target_dir"
              _ads_inodes
              ;;
          *)
              echo "analyze_disk_space: modo desconocido '${mode}'. Usa: partitions|top-dirs|top-files|inodes|all" >&2
              return 1
              ;;
      esac
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      analyze_disk_space "$@"
  fi

params_schema:
  {"params":[{"name":"target_dir","desc":"directorio a analizar para top-dirs y top-files (default: /)"},{"name":"mode","desc":"qué analizar: partitions|top-dirs|top-files|inodes|all (default: all)"}],"output":"informe de uso de disco a stdout; advertencias a stdout si uso \u003e90%; exit code 1 si modo desconocido"}


id: assert_docker_container_running_bash_infra
name: assert_docker_container_running
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: assert_docker_container_running(container_name: string) -> void
tags: ["assert","docker","container","running","validation","infra","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/assert_docker_container_running.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 145d610fbb3cbe56b2beee02a4de3ff4d79a866082f84bf94d03c6b571681d97
description:
  Verifica que un contenedor Docker está corriendo. Sale con exit code 1 si no está activo, con mensaje a stderr.

example:
  ```bash
  source functions/infra/assert_docker_container_running.sh

  assert_docker_container_running metabase
  echo "Contenedor activo, continuando..."
  ```

notes:
  Usa `docker ps --format '{{.Names}}'` con grep anclado (`^name$`) para evitar matches parciales (ej: "metabase" no matchea "metabase-test").

  Output limpio: void en éxito. El mensaje de error en stderr no incluye lista de contenedores activos — eso es responsabilidad del pipeline/caller.

  Requiere que `docker` esté en PATH. Combinar con `assert_command_exists` antes de llamar.

code:
  # assert_docker_container_running
  # --------------------------------
  # Verifica que un contenedor Docker está corriendo.
  # No produce output a stdout en caso de éxito.
  # Sale con exit code 1 si el contenedor no está corriendo,
  # con mensaje descriptivo a stderr.
  #
  # USO (sourced):
  #   source assert_docker_container_running.sh
  #   assert_docker_container_running metabase

  assert_docker_container_running() {
      local container_name="$1"

      if ! docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then
          echo "assert_docker_container_running: el contenedor '$container_name' no está corriendo" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"container_name","desc":"nombre del contenedor Docker a verificar"}],"output":"sin salida; exit code 0 si existe y está corriendo, 1 si no"}


id: build_cpp_linux_bash_infra
name: build_cpp_linux
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: build_cpp_linux(target?: string) -> void
description: Compila las funciones y apps C++ del registry para Linux nativo usando cmake
tags: ["cpp","build","cmake","linux","imgui"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/build_cpp_linux.sh
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
variant: []
content_hash: 7aad296717375fac47ac533cbf3d258df7104d27a04da06322b871d5adaf9c82
documentation:
  # build_cpp_linux

  Configura y compila el proyecto C++ (ImGui/ImPlot) para Linux nativo.

  Usa cmake con compilacion paralela (`-j$(nproc)`). Si no se ha configurado antes, ejecuta `cmake -B` automaticamente.

  ```bash

  fn run build_cpp_linux              # Compilar todo

  fn run build_cpp_linux chart_demo   # Compilar solo chart_demo

  ```

code:
  #!/usr/bin/env bash
  set -euo pipefail

  REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
  CPP_ROOT="$REGISTRY_ROOT/cpp"
  BUILD_DIR="$CPP_ROOT/build/linux"
  TARGET="${1:-}"

  # Configure if needed
  if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
      echo "[build_cpp_linux] Configuring cmake..."
      cmake -B "$BUILD_DIR" -S "$CPP_ROOT"
  fi

  # Build
  if [ -n "$TARGET" ]; then
      echo "[build_cpp_linux] Building target: $TARGET"
      cmake --build "$BUILD_DIR" --target "$TARGET" -- -j"$(nproc)"
  else
      echo "[build_cpp_linux] Building all targets..."
      cmake --build "$BUILD_DIR" -- -j"$(nproc)"
  fi

  echo "[build_cpp_linux] Done. Binaries in $BUILD_DIR"

params_schema:
  {"params":[{"name":"target","desc":"Nombre del target cmake a compilar (opcional, sin argumento compila todo)"}],"output":"Compila los binarios en cpp/build/linux/"}


id: build_cpp_windows_bash_infra
name: build_cpp_windows
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: build_cpp_windows(target?: string) -> void
description: Cross-compila las funciones y apps C++ del registry para Windows usando mingw-w64
tags: ["cpp","build","cmake","windows","cross-compile","mingw","imgui"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/build_cpp_windows.sh
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
variant: []
content_hash: ee946317d4f02273b0c7e28b686e422c24b8e197d91895a07e6407a47f7cb95d
documentation:
  # build_cpp_windows

  Cross-compila el proyecto C++ para Windows desde Linux usando el toolchain mingw-w64.

  Los .exe resultantes incluyen runtime linkado estaticamente (self-contained).

  ```bash

  fn run build_cpp_windows              # Compilar todo

  fn run build_cpp_windows chart_demo   # Compilar solo chart_demo

  ```

  Requiere `mingw-w64`: `sudo apt install mingw-w64`

code:
  #!/usr/bin/env bash
  set -euo pipefail

  REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
  CPP_ROOT="$REGISTRY_ROOT/cpp"
  BUILD_DIR="$CPP_ROOT/build/windows"
  TOOLCHAIN="$CPP_ROOT/toolchains/mingw-w64.cmake"
  TARGET="${1:-}"

  # Check mingw is available
  if ! command -v x86_64-w64-mingw32-g++ &>/dev/null; then
      echo "[build_cpp_windows] Error: mingw-w64 not found. Install with: sudo apt install mingw-w64"
      exit 1
  fi

  # Configure if needed
  if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
      echo "[build_cpp_windows] Configuring cmake with mingw-w64 toolchain..."
      cmake -B "$BUILD_DIR" -S "$CPP_ROOT" -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN"
  fi

  # Build
  if [ -n "$TARGET" ]; then
      echo "[build_cpp_windows] Cross-compiling target: $TARGET"
      cmake --build "$BUILD_DIR" --target "$TARGET" -- -j"$(nproc)"
  else
      echo "[build_cpp_windows] Cross-compiling all targets..."
      cmake --build "$BUILD_DIR" -- -j"$(nproc)"
  fi

  echo "[build_cpp_windows] Done. Windows binaries in $BUILD_DIR"
  if [ -n "$TARGET" ]; then
      file "$BUILD_DIR"/**/"$TARGET".exe 2>/dev/null || file "$BUILD_DIR/$TARGET".exe 2>/dev/null || true
  fi

params_schema:
  {"params":[{"name":"target","desc":"Nombre del target cmake a compilar (opcional, sin argumento compila todo)"}],"output":"Produce binarios .exe de Windows en cpp/build/windows/"}


id: detect_wsl_bash_infra
name: detect_wsl
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: detect_wsl([--check]) -> void
tags: ["bash","wsl","windows","detect","integration"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/detect_wsl.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 03fb4e6885b8b3e1909b9fcf16ed10821d1dc31e91025506de0672e173c394cf
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/gestion_linux/wsl_host.sh
description:
  Detecta si el sistema es WSL (Windows Subsystem for Linux). Con --check retorna solo exit code (0=WSL, 1=no WSL) sin output. Sin argumentos imprime versión WSL, usuario Windows, distribución, hostname, unidades montadas y ruta Windows del directorio actual.

example:
  ```bash
  source bash/functions/infra/detect_wsl.sh

  # Verificar si es WSL en scripts (sin output)
  if detect_wsl --check; then
      echo "Estamos en WSL"
  fi

  # Mostrar información completa del entorno WSL
  detect_wsl
  ```

notes:
  Usa tres métodos de detección en orden: /proc/version, /proc/sys/kernel/osrelease, y la presencia de /mnt/c + WSLInterop. No incluye las acciones interactivas del script original (abrir PowerShell, CMD, Explorer, VS Code).

code:
  #!/usr/bin/env bash
  # detect_wsl
  # ----------
  # Detecta si el sistema actual es WSL (Windows Subsystem for Linux).
  # Con --check solo retorna exit code (0=WSL, 1=no WSL) sin output.
  # Sin argumentos, imprime información completa del entorno WSL.
  #
  # USO:
  #   source detect_wsl.sh
  #   detect_wsl [--check]

  detect_wsl() {
      local check_only=false
      [[ "${1:-}" == "--check" ]] && check_only=true

      # Detección interna de WSL
      _is_wsl() {
          if [[ -f /proc/version ]] && grep -qi "microsoft\|wsl" /proc/version; then
              return 0
          fi
          if [[ -f /proc/sys/kernel/osrelease ]] && grep -qi "microsoft\|wsl" /proc/sys/kernel/osrelease; then
              return 0
          fi
          if [[ -d /mnt/c ]] && [[ -f /proc/sys/fs/binfmt_misc/WSLInterop ]]; then
              return 0
          fi
          return 1
      }

      _get_wsl_version() {
          if [[ -f /proc/version ]]; then
              if grep -qi "WSL2" /proc/version; then
                  echo "WSL2"
              elif grep -qi "microsoft" /proc/version; then
                  echo "WSL1"
              else
                  echo "Unknown"
              fi
          else
              echo "Unknown"
          fi
      }

      _get_windows_username() {
          if [[ -n "${WSLENV:-}" ]]; then
              cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r\n' || echo "Unknown"
          else
              echo "Unknown"
          fi
      }

      # Modo --check: solo exit code
      if [[ "$check_only" == true ]]; then
          _is_wsl && return 0 || return 1
      fi

      # Modo informativo
      if ! _is_wsl; then
          echo "detect_wsl: este sistema NO es WSL" >&2
          return 1
      fi

      local wsl_version
      wsl_version="$(_get_wsl_version)"

      local win_user
      win_user="$(_get_windows_username)"

      local distro="Unknown"
      if [[ -f /etc/os-release ]]; then
          distro="$(. /etc/os-release && echo "${PRETTY_NAME:-${ID:-Unknown}}")"
      fi

      echo "=== Entorno WSL ==="
      echo "  Versión de WSL:    ${wsl_version}"
      echo "  Usuario Windows:   ${win_user}"
      echo "  Distribución:      ${distro}"
      echo "  Hostname:          $(hostname)"
      echo ""

      echo "=== Unidades de Windows montadas ==="
      ls /mnt/ 2>/dev/null | grep -E "^[a-z]$" | while IFS= read -r drive; do
          echo "  ${drive}: → /mnt/${drive}"
      done
      echo ""

      local current_win_path
      current_win_path="$(wslpath -w "$(pwd)" 2>/dev/null || echo "N/A")"
      echo "=== Directorio actual en Windows ==="
      echo "  ${current_win_path}"
      echo ""
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      detect_wsl "$@"
  fi

params_schema:
  {"params":[{"name":"--check","desc":"flag: solo detecta y retorna exit code sin producir output (0=WSL, 1=no WSL)"}],"output":"sin output con --check; informe del entorno WSL a stdout sin argumentos; exit code 1 si no es WSL"}


id: docker_compose_remote_deploy_bash_infra
name: docker_compose_remote_deploy
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
tags: ["docker","compose","deploy","ssh","remote","git","infra","cicd"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/docker_compose_remote_deploy.sh
created_at: 2026-04-12T18:29:33Z
updated_at: 2026-04-12T18:29:33Z
props: []
emits: []
variant: []
content_hash: 494783ceb3e5f7e91430ee0fcb6aa655ad88900833952fe257df2b6f235237cc
signature:
  docker_compose_remote_deploy(host: string, remote_dir: string, branch: string, compose_files: string) -> json

description:
  Despliega un stack Docker Compose en un host remoto via SSH. Verifica conectividad, hace git pull del branch indicado, actualiza imagenes con docker-compose pull y levanta/recrea los servicios modificados con docker-compose up -d. Soporta compose files adicionales. Retorna JSON con status, containers corriendo y duracion.

example:
  ```bash
  source bash/functions/infra/docker_compose_remote_deploy.sh

  # Deploy basico (solo docker-compose.yml, branch main)
  result=$(docker_compose_remote_deploy "prod-server" "/opt/apps/element")
  echo "$result"
  # {"status":"ok","host":"prod-server","remote_dir":"/opt/apps/element","branch":"main","containers":["element-web","synapse","postgres"],"duration_ms":4200}

  # Deploy con compose files adicionales y branch especifico
  result=$(docker_compose_remote_deploy "prod-server" "/opt/apps/element" "release" "docker-compose.livekit.yml,docker-compose.monitoring.yml")
  echo "$result"
  # {"status":"ok","host":"prod-server","remote_dir":"/opt/apps/element","branch":"release","containers":[...],"duration_ms":8100}

  # Uso desde un pipeline CI/CD
  source bash/functions/infra/docker_compose_remote_deploy.sh
  docker_compose_remote_deploy "$SSH_HOST" "$REMOTE_DIR" "$GIT_BRANCH" "$EXTRA_COMPOSE" || exit 1
  ```

notes:
  - Flujo: verificar SSH → git pull → docker-compose pull → docker-compose up -d → listar containers.
  - La verificacion SSH usa `-o BatchMode=yes -o ConnectTimeout=5` para fallar rapido sin pedir password.
  - Los compose files adicionales se pasan como `-f file1.yml -f file2.yml` a todos los subcomandos compose.
  - `docker-compose up -d` solo recrea los servicios cuya imagen o config cambio (comportamiento nativo de compose).
  - La lista de containers al final incluye TODOS los containers corriendo en el host, no solo los del stack.
  - Requiere `jq` instalado en el host remoto para serializar la lista de containers. Si no esta, `containers` sera `[]`.
  - Los mensajes de progreso van a stderr; el JSON final va a stdout.
  - Exit code 1 en cualquier fallo (SSH, git pull, compose pull, compose up); el JSON de error NO se emite — el caller debe manejar el exit code.
  - El `host` se resuelve con `~/.ssh/config` incluyendo host, user, identityfile y puerto.
  - Diferencia con `rsync_deploy`: este flujo asume que el codigo ya esta en el remoto (via git) y usa compose. `rsync_deploy` sube archivos locales sin git.

code:
  #!/usr/bin/env bash
  # docker_compose_remote_deploy — Despliega un stack Docker Compose en un host remoto via SSH
  set -euo pipefail

  docker_compose_remote_deploy() {
      local host="$1"
      local remote_dir="$2"
      local branch="${3:-main}"
      local compose_files="${4:-}"

      if [[ -z "$host" || -z "$remote_dir" ]]; then
          echo "docker_compose_remote_deploy: se requieren host y remote_dir" >&2
          return 1
      fi

      local start_ts
      start_ts=$(date +%s)

      # 1. Verificar conectividad SSH
      echo "docker_compose_remote_deploy: verificando conectividad SSH a '$host'..." >&2
      if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" true 2>/dev/null; then
          echo "docker_compose_remote_deploy: no se puede conectar a '$host' via SSH" >&2
          return 1
      fi

      # 2. Git pull en el host remoto
      echo "docker_compose_remote_deploy: git pull origin $branch en '$remote_dir'..." >&2
      if ! ssh "$host" "cd '$remote_dir' && git pull origin '$branch'" >&2; then
          echo "docker_compose_remote_deploy: git pull falló en '$host:$remote_dir'" >&2
          return 1
      fi

      # 3. Construir los argumentos -f para docker-compose
      local compose_args="-f docker-compose.yml"
      if [[ -n "$compose_files" ]]; then
          local IFS=","
          local extra_file
          for extra_file in $compose_files; do
              extra_file="${extra_file// /}"  # trim spaces
              if [[ -n "$extra_file" ]]; then
                  compose_args="$compose_args -f $extra_file"
              fi
          done
          unset IFS
      fi

      # 4. docker-compose pull
      echo "docker_compose_remote_deploy: actualizando imagenes ($compose_args)..." >&2
      if ! ssh "$host" "cd '$remote_dir' && docker-compose $compose_args pull" >&2; then
          echo "docker_compose_remote_deploy: docker-compose pull falló en '$host:$remote_dir'" >&2
          return 1
      fi

      # 5. docker-compose up -d
      echo "docker_compose_remote_deploy: levantando servicios ($compose_args)..." >&2
      if ! ssh "$host" "cd '$remote_dir' && docker-compose $compose_args up -d" >&2; then
          echo "docker_compose_remote_deploy: docker-compose up -d falló en '$host:$remote_dir'" >&2
          return 1
      fi

      # 6. Recopilar containers corriendo tras el deploy
      local containers_json
      containers_json=$(ssh "$host" \
          "docker ps --format '{{.Names}}' 2>/dev/null | jq -R . | jq -sc ." 2>/dev/null || echo '[]')

      local end_ts
      end_ts=$(date +%s)
      local duration_ms=$(( (end_ts - start_ts) * 1000 ))

      # Emitir JSON a stdout
      printf '{"status":"ok","host":"%s","remote_dir":"%s","branch":"%s","containers":%s,"duration_ms":%d}\n' \
          "$host" \
          "$remote_dir" \
          "$branch" \
          "$containers_json" \
          "$duration_ms"
  }

  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
      docker_compose_remote_deploy "$@"
  fi

params_schema:
  {"params":[{"name":"host","desc":"alias SSH del host remoto definido en ~/.ssh/config (ej: prod-server)"},{"name":"remote_dir","desc":"ruta absoluta en el host donde esta el repo con docker-compose.yml (ej: /opt/apps/element)"},{"name":"branch","desc":"branch de git a hacer pull; default 'main'"},{"name":"compose_files","desc":"archivos compose adicionales separados por coma (ej: 'docker-compose.livekit.yml,docker-compose.monitoring.yml'); si vacio usa solo docker-compose.yml"}],"output":"JSON con status ('ok'), host, remote_dir, branch, containers (array de nombres corriendo tras el deploy), duration_ms"}


id: docker_cp_file_bash_infra
name: docker_cp_file
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: docker_cp_file(local_path: string, container_name: string, dest_path: string) -> string
tags: ["docker","cp","copy","file","container","transfer","infra","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/docker_cp_file.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 34cf8d182c9154609e1f09f14513194ccc85fc91b55b155bf06f0bd6ffe90528
description:
  Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide. Imprime JSON con local_size y remote_size a stdout. Sale con exit code 1 si docker cp falla o los tamaños difieren.

example:
  ```bash
  source functions/infra/docker_cp_file.sh

  result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db)
  echo "$result"
  # {"local_size":524288,"remote_size":524288}

  local_size=$(echo "$result" | grep -o '"local_size":[0-9]*' | cut -d: -f2)
  ```

notes:
  La verificación de tamaño usa `docker exec stat -c%s` sobre el contenedor destino. Si `stat` no está disponible en el contenedor, `remote_size` será -1 y la función fallará.

  Output a stdout: JSON minificado con campos `local_size` y `remote_size` (enteros, bytes).

  Usa `printf` en lugar de `echo` para garantizar que no haya newline extra en el JSON.

code:
  # docker_cp_file
  # --------------
  # Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide.
  # Imprime JSON con local_size y remote_size a stdout si la copia es exitosa.
  # Sale con exit code 1 si docker cp falla o si los tamaños no coinciden.
  #
  # USO (sourced):
  #   source docker_cp_file.sh
  #   result=$(docker_cp_file /ruta/local.db metabase /dest/path.db)

  docker_cp_file() {
      local local_path="$1"
      local container_name="$2"
      local dest_path="$3"

      if ! docker cp "$local_path" "${container_name}:${dest_path}" 2>/dev/null; then
          echo "docker_cp_file: fallo al copiar '$local_path' a '${container_name}:${dest_path}'" >&2
          return 1
      fi

      local local_size
      local_size=$(stat -c%s "$local_path")

      local remote_size
      remote_size=$(docker exec "$container_name" stat -c%s "$dest_path" 2>/dev/null || echo "-1")

      if [ "$local_size" != "$remote_size" ]; then
          echo "docker_cp_file: tamaños no coinciden (local=${local_size}, remoto=${remote_size})" >&2
          return 1
      fi

      printf '{"local_size":%s,"remote_size":%s}' "$local_size" "$remote_size"
  }

params_schema:
  {"params":[{"name":"local_path","desc":"ruta del archivo local a copiar"},{"name":"container_name","desc":"nombre del contenedor Docker destino"},{"name":"dest_path","desc":"ruta destino dentro del contenedor"}],"output":"JSON con local_size y remote_size en bytes"}


id: frontend_doctor_bash_infra
name: frontend_doctor
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: frontend_doctor(project_dir: string) -> diagnostics_stdout
tags: ["frontend","mantine","doctor","diagnostics","health","validation"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/frontend_doctor.sh
created_at: 2026-04-05T23:51:12Z
updated_at: 2026-04-05T23:51:12Z
props: []
emits: []
variant: []
content_hash: eb61d0f0248ba6d23bbde74add06154c387aed91578408f487ae46479474794f
description:
  Diagnostica la salud de un proyecto frontend Mantine. Verifica Node, React, Mantine, PostCSS, TypeScript, vite.config y detecta residuos de shadcn/@base-ui. Imprime tabla de checks con exit code 0/1.

example:
  ```bash
  # Diagnosticar un proyecto
  bash frontend_doctor.sh ./apps/rapid_dashboards/frontend

  # Output:
  # === Frontend Doctor: ./apps/rapid_dashboards/frontend ===
  #
  #   ✓  Node >= 18                          22.12.0
  #   ✓  Package manager detected            pnpm
  #   ✓  node_modules present
  #   ✓  @mantine/core                       7.17.0
  #   ✓  @mantine/hooks
  #   ✓  @mantine/charts
  #   ✓  React >= 18                         19.2.4
  #   ✓  postcss.config present
  #   ✓  TypeScript >= 5                     6.0.2
  #   ✓  vite.config present
  #   ✓  No shadcn residual
  #   ✓  No @base-ui residual
  #
  #   Resultado: todo OK
  ```

notes:
  Checks informativos, no modifica nada. Util para validar que un proyecto esta correctamente configurado despues de instalar Mantine o migrar desde shadcn. Exit code 0 si todo OK, 1 si hay problemas.

code:
  # frontend_doctor
  # ----------------
  # Diagnostica la salud de un proyecto frontend Mantine.
  # Verifica dependencias, configuracion y versiones.
  # Imprime tabla de checks y retorna exit code 0 (ok) o 1 (fallos).
  #
  # USO (sourced):
  #   source frontend_doctor.sh
  #   frontend_doctor /path/to/frontend
  #
  # USO (directo):
  #   bash frontend_doctor.sh /path/to/frontend

  frontend_doctor() {
      local project_dir="$1"
      local failures=0

      if [ -z "$project_dir" ]; then
          echo "frontend_doctor: se requiere project_dir" >&2
          return 1
      fi

      if [ ! -f "$project_dir/package.json" ]; then
          echo "frontend_doctor: no existe package.json en $project_dir" >&2
          return 1
      fi

      echo "=== Frontend Doctor: $project_dir ==="
      echo ""

      # Helper: check y reportar
      _check() {
          local label="$1"
          local ok="$2"
          local detail="$3"
          if [ "$ok" = "1" ]; then
              printf "  ✓  %-35s %s\n" "$label" "$detail"
          else
              printf "  ✗  %-35s %s\n" "$label" "$detail"
              ((failures++))
          fi
      }

      # 1. Node >= 18
      local node_ver=""
      local node_ok=0
      if command -v node &>/dev/null; then
          node_ver=$(node -v 2>/dev/null | sed 's/v//')
          local node_major=$(echo "$node_ver" | cut -d. -f1)
          [ "$node_major" -ge 18 ] 2>/dev/null && node_ok=1
      fi
      _check "Node >= 18" "$node_ok" "${node_ver:-not found}"

      # 2. Package manager
      local pm_ok=0
      local pm_name="none"
      if [ -f "$project_dir/pnpm-lock.yaml" ]; then
          pm_name="pnpm"; pm_ok=1
      elif [ -f "$project_dir/yarn.lock" ]; then
          pm_name="yarn"; pm_ok=1
      elif [ -f "$project_dir/package-lock.json" ]; then
          pm_name="npm"; pm_ok=1
      fi
      _check "Package manager detected" "$pm_ok" "$pm_name"

      # 3. node_modules existe
      local nm_ok=0
      [ -d "$project_dir/node_modules" ] && nm_ok=1
      _check "node_modules present" "$nm_ok" ""

      # 4. @mantine/core instalado
      local mantine_ok=0
      local mantine_ver=""
      if [ -f "$project_dir/node_modules/@mantine/core/package.json" ]; then
          mantine_ver=$(node -e "console.log(require('$project_dir/node_modules/@mantine/core/package.json').version)" 2>/dev/null)
          mantine_ok=1
      fi
      _check "@mantine/core" "$mantine_ok" "${mantine_ver:-not installed}"

      # 5. @mantine/hooks
      local hooks_ok=0
      [ -d "$project_dir/node_modules/@mantine/hooks" ] && hooks_ok=1
      _check "@mantine/hooks" "$hooks_ok" ""

      # 6. @mantine/charts
      local charts_ok=0
      [ -d "$project_dir/node_modules/@mantine/charts" ] && charts_ok=1
      _check "@mantine/charts" "$charts_ok" ""

      # 7. React >= 18
      local react_ok=0
      local react_ver=""
      if [ -f "$project_dir/node_modules/react/package.json" ]; then
          react_ver=$(node -e "console.log(require('$project_dir/node_modules/react/package.json').version)" 2>/dev/null)
          local react_major=$(echo "$react_ver" | cut -d. -f1)
          [ "$react_major" -ge 18 ] 2>/dev/null && react_ok=1
      fi
      _check "React >= 18" "$react_ok" "${react_ver:-not found}"

      # 8. postcss.config presente
      local postcss_ok=0
      if [ -f "$project_dir/postcss.config.cjs" ] || [ -f "$project_dir/postcss.config.js" ] || [ -f "$project_dir/postcss.config.mjs" ]; then
          postcss_ok=1
      fi
      _check "postcss.config present" "$postcss_ok" ""

      # 9. TypeScript >= 5
      local ts_ok=0
      local ts_ver=""
      if [ -f "$project_dir/node_modules/typescript/package.json" ]; then
          ts_ver=$(node -e "console.log(require('$project_dir/node_modules/typescript/package.json').version)" 2>/dev/null)
          local ts_major=$(echo "$ts_ver" | cut -d. -f1)
          [ "$ts_major" -ge 5 ] 2>/dev/null && ts_ok=1
      fi
      _check "TypeScript >= 5" "$ts_ok" "${ts_ver:-not found}"

      # 10. vite.config presente
      local vite_ok=0
      if [ -f "$project_dir/vite.config.ts" ] || [ -f "$project_dir/vite.config.js" ]; then
          vite_ok=1
      fi
      _check "vite.config present" "$vite_ok" ""

      # 11. Shadcn residual (warning)
      local shadcn_clean=1
      if [ -f "$project_dir/components.json" ]; then
          shadcn_clean=0
      fi
      _check "No shadcn residual" "$shadcn_clean" "$([ "$shadcn_clean" = "0" ] && echo 'components.json found')"

      # 12. @base-ui residual (warning)
      local baseui_clean=1
      if [ -d "$project_dir/node_modules/@base-ui" ]; then
          baseui_clean=0
      fi
      _check "No @base-ui residual" "$baseui_clean" "$([ "$baseui_clean" = "0" ] && echo '@base-ui still installed')"

      echo ""
      if [ "$failures" -eq 0 ]; then
          echo "  Resultado: todo OK"
          return 0
      else
          echo "  Resultado: $failures problema(s) encontrado(s)"
          return 1
      fi
  }

  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      frontend_doctor "$@"
  fi

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto frontend con package.json"}],"output":"tabla de checks con ✓/✗ por cada validación y resumen final"}


id: gitea_add_collaborator_bash_infra
name: gitea_add_collaborator
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: gitea_add_collaborator(owner: string, repo: string, username: string, permission: string) -> void
tags: ["gitea","git","collaborator","permission","repo","api","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/gitea_add_collaborator.sh
created_at: 2026-04-05T23:01:37Z
updated_at: 2026-04-05T23:01:37Z
props: []
emits: []
variant: []
content_hash: 26ad480468b3d06fc2b01b02a9c287654037e07b880cf98c216235437a081bf2
description:
  Añade un colaborador a un repositorio Gitea con el nivel de permisos indicado. Silencioso si el colaborador ya existe (422).

example:
  ```bash
  source bash/functions/infra/gitea_add_collaborator.sh

  export GITEA_URL="https://git.example.com"
  export GITEA_TOKEN="$(pass agentes/dataforge-token)"

  # Añadir colaborador con permiso admin (default)
  gitea_add_collaborator "myorg" "my-app" "egutierrez"

  # Añadir colaborador con permiso de solo lectura
  gitea_add_collaborator "myorg" "my-app" "reviewer" "read"
  ```

notes:
  - Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
  - Un 422 de la API indica que el usuario ya es colaborador — se trata como éxito silencioso.
  - La función no produce salida a stdout; los mensajes informativos van a stderr.
  - Nivel `admin` da acceso completo al repo incluyendo settings.

code:
  #!/usr/bin/env bash
  # gitea_add_collaborator — Añade un colaborador a un repositorio Gitea

  gitea_add_collaborator() {
      local owner="$1"
      local repo="$2"
      local username="$3"
      local permission="${4:-admin}"

      if [[ -z "${GITEA_URL:-}" ]]; then
          echo "gitea_add_collaborator: GITEA_URL no está seteada" >&2
          return 1
      fi
      if [[ -z "${GITEA_TOKEN:-}" ]]; then
          echo "gitea_add_collaborator: GITEA_TOKEN no está seteado" >&2
          return 1
      fi
      if [[ -z "$owner" || -z "$repo" || -z "$username" ]]; then
          echo "gitea_add_collaborator: se requieren owner, repo y username" >&2
          return 1
      fi

      local payload
      payload=$(printf '{"permission":"%s"}' "$permission")

      echo "gitea_add_collaborator: añadiendo '$username' a '$owner/$repo' con permiso '$permission'..." >&2

      local response http_code
      response=$(curl -s -w "\n%{http_code}" \
          -X PUT \
          -H "Content-Type: application/json" \
          -H "Authorization: token ${GITEA_TOKEN}" \
          -d "$payload" \
          "${GITEA_URL}/api/v1/repos/${owner}/${repo}/collaborators/${username}")

      http_code=$(echo "$response" | tail -n1)
      local body
      body=$(echo "$response" | head -n -1)

      if [[ "$http_code" == "204" || "$http_code" == "200" ]]; then
          echo "gitea_add_collaborator: '$username' añadido a '$owner/$repo'" >&2
          return 0
      fi

      if [[ "$http_code" == "422" ]]; then
          echo "gitea_add_collaborator: '$username' ya es colaborador de '$owner/$repo' (silencioso)" >&2
          return 0
      fi

      echo "gitea_add_collaborator: error (HTTP ${http_code}): ${body}" >&2
      return 1
  }

params_schema:
  {"params":[{"name":"owner","desc":"usuario u organización propietaria del repositorio"},{"name":"repo","desc":"nombre del repositorio"},{"name":"username","desc":"nombre de usuario del colaborador a añadir"},{"name":"permission","desc":"nivel de permisos: 'read', 'write' o 'admin' (default: admin)"}],"output":"vacío — efectos observables a través de la API de Gitea"}


id: gitea_create_repo_bash_infra
name: gitea_create_repo
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: gitea_create_repo(owner: string, name: string, private: string, description: string) -> string
tags: ["gitea","git","repo","create","infra","api"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/gitea_create_repo.sh
created_at: 2026-04-05T23:01:37Z
updated_at: 2026-04-05T23:01:37Z
props: []
emits: []
variant: []
content_hash: e321b9937e6be33965e10b271b2f1a9b834d10112a1312af00cb6df272a8938d
description:
  Crea un repositorio en Gitea para un owner. Intenta crearlo en org primero; si el owner no es una org (404/422), lo crea en el usuario autenticado. No falla fatalmente si el repo ya existe (409).

example:
  ```bash
  source bash/functions/infra/gitea_create_repo.sh

  export GITEA_URL="https://git.example.com"
  export GITEA_TOKEN="$(pass agentes/dataforge-token)"

  # Crear repo público en org o usuario
  repo_json=$(gitea_create_repo "myorg" "my-app")

  # Crear repo privado con descripción
  repo_json=$(gitea_create_repo "myorg" "my-app" "true" "Mi aplicación principal")

  # Extraer la URL del clon
  clone_url=$(echo "$repo_json" | jq -r '.clone_url')
  ```

notes:
  - Requiere variables de entorno `GITEA_URL` y `GITEA_TOKEN` seteadas antes de invocar.
  - El fallback org → usuario ocurre con HTTP 404 o 422 en el endpoint de orgs.
  - Un 409 se reporta a stderr pero la función retorna 0 — el repo ya existe es una condición aceptable para idempotencia.
  - Los mensajes informativos van a stderr; el JSON de respuesta va a stdout.

code:
  #!/usr/bin/env bash
  # gitea_create_repo — Crea un repositorio en Gitea para un owner (org o usuario)

  gitea_create_repo() {
      local owner="$1"
      local name="$2"
      local private="${3:-false}"
      local description="${4:-}"

      if [[ -z "${GITEA_URL:-}" ]]; then
          echo "gitea_create_repo: GITEA_URL no está seteada" >&2
          return 1
      fi
      if [[ -z "${GITEA_TOKEN:-}" ]]; then
          echo "gitea_create_repo: GITEA_TOKEN no está seteado" >&2
          return 1
      fi
      if [[ -z "$owner" || -z "$name" ]]; then
          echo "gitea_create_repo: se requieren owner y name" >&2
          return 1
      fi

      local payload
      payload=$(printf '{"name":"%s","private":%s,"description":"%s","auto_init":false}' \
          "$name" "$private" "$description")

      echo "gitea_create_repo: intentando crear '$owner/$name' en org..." >&2

      local response http_code
      response=$(curl -s -w "\n%{http_code}" \
          -X POST \
          -H "Content-Type: application/json" \
          -H "Authorization: token ${GITEA_TOKEN}" \
          -d "$payload" \
          "${GITEA_URL}/api/v1/orgs/${owner}/repos")

      http_code=$(echo "$response" | tail -n1)
      local body
      body=$(echo "$response" | head -n -1)

      if [[ "$http_code" == "201" ]]; then
          echo "gitea_create_repo: repo '$owner/$name' creado en org" >&2
          echo "$body"
          return 0
      fi

      if [[ "$http_code" == "409" ]]; then
          echo "gitea_create_repo: repo '$owner/$name' ya existe (409)" >&2
          echo "$body"
          return 0
      fi

      if [[ "$http_code" == "404" || "$http_code" == "422" ]]; then
          echo "gitea_create_repo: org no encontrada (${http_code}), intentando en usuario..." >&2
          response=$(curl -s -w "\n%{http_code}" \
              -X POST \
              -H "Content-Type: application/json" \
              -H "Authorization: token ${GITEA_TOKEN}" \
              -d "$payload" \
              "${GITEA_URL}/api/v1/user/repos")

          http_code=$(echo "$response" | tail -n1)
          body=$(echo "$response" | head -n -1)

          if [[ "$http_code" == "201" ]]; then
              echo "gitea_create_repo: repo '$owner/$name' creado en usuario" >&2
              echo "$body"
              return 0
          fi

          if [[ "$http_code" == "409" ]]; then
              echo "gitea_create_repo: repo '$owner/$name' ya existe (409)" >&2
              echo "$body"
              return 0
          fi

          echo "gitea_create_repo: error al crear en usuario (HTTP ${http_code}): ${body}" >&2
          return 1
      fi

      echo "gitea_create_repo: error inesperado (HTTP ${http_code}): ${body}" >&2
      return 1
  }

params_schema:
  {"params":[{"name":"owner","desc":"usuario u organización propietaria del repo"},{"name":"name","desc":"nombre del repositorio a crear"},{"name":"private","desc":"si el repo es privado, 'true' o 'false' (default: false)"},{"name":"description","desc":"descripción del repositorio (opcional)"}],"output":"JSON del repositorio creado según la API de Gitea"}


id: gitea_create_webhook_bash_infra
name: gitea_create_webhook
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: gitea_create_webhook(owner: string, repo: string, target_url: string, secret?: string) -> json
description: Crea un webhook de push en un repositorio Gitea. El webhook notifica a target_url en cada push.
tags: ["gitea","webhook","push","deploy","ci","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/gitea_create_webhook.sh
created_at: 2026-04-12T12:48:21Z
updated_at: 2026-04-12T12:48:21Z
props: []
emits: []
variant: []
content_hash: 094cae1ada836289be900634ee31c4a71b6d1b05a6f3ea4d64e7472ef9079db3
example:
  ```bash
  source bash/functions/infra/gitea_create_webhook.sh

  export GITEA_URL="https://git.example.com"
  export GITEA_TOKEN="$(pass agentes/dataforge-token)"

  # Crear webhook para auto-deploy
  gitea_create_webhook "myorg" "dag_engine" "http://vps:9090/webhook/push" "mi_secreto"
  # {"webhook_id":42,"owner":"myorg","repo":"dag_engine","target_url":"http://vps:9090/webhook/push"}
  ```

notes:
  - Requiere `GITEA_URL` y `GITEA_TOKEN` como variables de entorno.
  - Solo escucha eventos `push`. Para otros eventos, modificar el array `events` en el payload.
  - Si el webhook ya existe para la misma URL, Gitea crea uno duplicado (no es idempotente).

code:
  #!/usr/bin/env bash
  # gitea_create_webhook — Crea un webhook de push en un repositorio Gitea
  set -euo pipefail

  gitea_create_webhook() {
      local owner="$1"
      local repo="$2"
      local target_url="$3"
      local secret="${4:-}"

      if [[ -z "$owner" || -z "$repo" || -z "$target_url" ]]; then
          echo "usage: gitea_create_webhook <owner> <repo> <target_url> [secret]" >&2
          return 1
      fi

      local gitea_url="${GITEA_URL:?GITEA_URL no seteada}"
      local gitea_token="${GITEA_TOKEN:?GITEA_TOKEN no seteada}"

      # Payload JSON para el webhook
      local payload
      payload=$(cat <<EOF
  {
      "type": "gitea",
      "active": true,
      "events": ["push"],
      "config": {
          "url": "$target_url",
          "content_type": "json",
          "secret": "$secret"
      }
  }
  EOF
  )

      local response http_code body
      response=$(curl -s -w "\n%{http_code}" \
          -X POST \
          -H "Authorization: token $gitea_token" \
          -H "Content-Type: application/json" \
          -d "$payload" \
          "${gitea_url}/api/v1/repos/${owner}/${repo}/hooks")

      http_code=$(echo "$response" | tail -1)
      body=$(echo "$response" | sed '$d')

      if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
          # Extraer webhook ID del response
          local webhook_id
          webhook_id=$(echo "$body" | grep -oP '"id":\s*\K[0-9]+' | head -1)
          printf '{"webhook_id":%s,"owner":"%s","repo":"%s","target_url":"%s"}\n' \
              "$webhook_id" "$owner" "$repo" "$target_url"
      else
          echo "gitea_create_webhook: HTTP $http_code — $body" >&2
          return 1
      fi
  }

  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
      gitea_create_webhook "$@"
  fi

params_schema:
  {"params":[{"name":"owner","desc":"usuario u organización propietaria del repositorio"},{"name":"repo","desc":"nombre del repositorio"},{"name":"target_url","desc":"URL que recibirá el POST del webhook en cada push"},{"name":"secret","desc":"secreto compartido para firmar el payload (opcional)"}],"output":"JSON con webhook_id, owner, repo, target_url"}


id: gitea_list_repos_bash_infra
name: gitea_list_repos
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: gitea_list_repos(owner: string) -> string
tags: ["gitea","git","repo","list","org","user","api","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/gitea_list_repos.sh
created_at: 2026-04-05T23:01:37Z
updated_at: 2026-04-05T23:01:37Z
props: []
emits: []
variant: []
content_hash: a8464110043a3a2761afa00e41da77627c4a5adb949259165ecb89da9303e428
description:
  Lista repositorios de un owner en Gitea. Intenta listar como org primero; si falla, lista como usuario. Imprime una línea por repo en formato name<TAB>html_url<TAB>description.

example:
  ```bash
  source bash/functions/infra/gitea_list_repos.sh

  export GITEA_URL="https://git.example.com"
  export GITEA_TOKEN="$(pass agentes/dataforge-token)"

  # Listar todos los repos de una org
  gitea_list_repos "myorg"
  # my-app    https://git.example.com/myorg/my-app    Mi aplicación principal
  # infra     https://git.example.com/myorg/infra

  # Iterar sobre los repos
  while IFS=$'\t' read -r name url desc; do
      echo "Repo: $name — $url"
  done < <(gitea_list_repos "myorg")
  ```

notes:
  - Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
  - Usa `jq` si está disponible; fallback con grep/sed en caso contrario.
  - El límite es 50 repos por página. Para owners con más de 50 repos habría que implementar paginación.
  - Los mensajes informativos van a stderr; los datos tabulados van a stdout.

code:
  #!/usr/bin/env bash
  # gitea_list_repos — Lista repositorios de un owner (org o usuario) en Gitea

  gitea_list_repos() {
      local owner="$1"

      if [[ -z "${GITEA_URL:-}" ]]; then
          echo "gitea_list_repos: GITEA_URL no está seteada" >&2
          return 1
      fi
      if [[ -z "${GITEA_TOKEN:-}" ]]; then
          echo "gitea_list_repos: GITEA_TOKEN no está seteado" >&2
          return 1
      fi
      if [[ -z "$owner" ]]; then
          echo "gitea_list_repos: se requiere owner" >&2
          return 1
      fi

      echo "gitea_list_repos: listando repos de '$owner' (intentando org)..." >&2

      local response http_code body
      response=$(curl -s -w "\n%{http_code}" \
          -H "Authorization: token ${GITEA_TOKEN}" \
          "${GITEA_URL}/api/v1/orgs/${owner}/repos?limit=50")

      http_code=$(echo "$response" | tail -n1)
      body=$(echo "$response" | head -n -1)

      if [[ "$http_code" != "200" ]]; then
          echo "gitea_list_repos: org no encontrada (${http_code}), intentando usuario..." >&2
          response=$(curl -s -w "\n%{http_code}" \
              -H "Authorization: token ${GITEA_TOKEN}" \
              "${GITEA_URL}/api/v1/users/${owner}/repos?limit=50")

          http_code=$(echo "$response" | tail -n1)
          body=$(echo "$response" | head -n -1)

          if [[ "$http_code" != "200" ]]; then
              echo "gitea_list_repos: error listando repos de usuario (HTTP ${http_code}): ${body}" >&2
              return 1
          fi
      fi

      # Formatear salida como name\thtml_url\tdescription
      if command -v jq &>/dev/null; then
          echo "$body" | jq -r '.[] | [.name, .html_url, (.description // "")] | @tsv'
      else
          # Fallback sin jq: extraer campos básicos con grep/sed
          echo "$body" | grep -o '"name":"[^"]*"\|"html_url":"[^"]*"\|"description":"[^"]*"' \
              | paste - - - | sed 's/"name":"//;s/"html_url":"//;s/"description":"//;s/"//g'
      fi
  }

params_schema:
  {"params":[{"name":"owner","desc":"nombre del usuario u organización cuyos repos se listan"}],"output":"una línea por repositorio con columnas separadas por tabulador: name, html_url, description"}


id: gitea_push_directory_bash_infra
name: gitea_push_directory
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: gitea_push_directory(directory: string, owner: string, repo: string, branch: string) -> void
tags: ["gitea","git","push","directory","sync","infra","repo"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/gitea_push_directory.sh
created_at: 2026-04-05T23:01:37Z
updated_at: 2026-04-05T23:01:37Z
props: []
emits: []
variant: []
content_hash: 3f0868b69167a6bac201dd5ba6e19b94461be9e510d14d55f629fc778c0cfde3
description:
  Inicializa git en un directorio local y lo sube a un repositorio Gitea existente. Si el directorio ya tiene .git, actualiza el remote y pushea cambios pendientes. Protege registry.db añadiéndolo al .gitignore antes del commit.

example:
  ```bash
  source bash/functions/infra/gitea_push_directory.sh

  export GITEA_URL="https://git.example.com"
  export GITEA_TOKEN="$(pass agentes/dataforge-token)"

  # Subir directorio a repo existente
  gitea_push_directory "/home/lucas/myproject" "myorg" "my-app"

  # Subir a rama específica
  gitea_push_directory "/home/lucas/myproject" "myorg" "my-app" "develop"
  ```

notes:
  - Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
  - El token se embebe en la URL del remote para autenticación (nunca se imprime a stdout/stderr, se enmascara con ***).
  - Si `registry.db` existe en el directorio, se añade automáticamente al `.gitignore` local.
  - Si el `.git` ya existe con un remote diferente, se redirige al repo indicado sin perder el historial local.
  - Usa `--force-with-lease` para el primer push y fallback a push normal (para repos vacíos recién creados).
  - El commit se firma con `agent@fn-registry` si no hay configuración git en el entorno.

code:
  #!/usr/bin/env bash
  # gitea_push_directory — Inicializa git en un directorio y lo sube a un repo Gitea existente

  gitea_push_directory() {
      local directory="$1"
      local owner="$2"
      local repo="$3"
      local branch="${4:-main}"

      if [[ -z "${GITEA_URL:-}" ]]; then
          echo "gitea_push_directory: GITEA_URL no está seteada" >&2
          return 1
      fi
      if [[ -z "${GITEA_TOKEN:-}" ]]; then
          echo "gitea_push_directory: GITEA_TOKEN no está seteado" >&2
          return 1
      fi
      if [[ -z "$directory" || -z "$owner" || -z "$repo" ]]; then
          echo "gitea_push_directory: se requieren directory, owner y repo" >&2
          return 1
      fi
      if [[ ! -d "$directory" ]]; then
          echo "gitea_push_directory: directorio '$directory' no existe" >&2
          return 1
      fi

      # Construir URL con credenciales embebidas para autenticación
      local gitea_host
      gitea_host=$(echo "$GITEA_URL" | sed 's|https\?://||')
      local remote_url="https://${GITEA_TOKEN}@${gitea_host}/${owner}/${repo}.git"
      local display_url="https://***@${gitea_host}/${owner}/${repo}.git"

      echo "gitea_push_directory: procesando '$directory' → '$owner/$repo' (rama: $branch)..." >&2

      # Añadir registry.db al .gitignore local si existe en el directorio
      if [[ -f "$directory/registry.db" ]]; then
          echo "gitea_push_directory: añadiendo registry.db al .gitignore..." >&2
          if [[ ! -f "$directory/.gitignore" ]] || ! grep -qxF "registry.db" "$directory/.gitignore"; then
              echo "registry.db" >> "$directory/.gitignore"
          fi
      fi

      # Gestionar estado del repositorio git
      if [[ -d "$directory/.git" ]]; then
          local existing_remote
          existing_remote=$(git -C "$directory" remote get-url origin 2>/dev/null || echo "")

          if [[ -z "$existing_remote" ]]; then
              echo "gitea_push_directory: añadiendo remote origin..." >&2
              git -C "$directory" remote add origin "$remote_url"
          else
              # Comparar remote sin token para detectar si apunta al mismo repo
              local clean_existing
              clean_existing=$(echo "$existing_remote" | sed 's|https://[^@]*@||;s|https://||')
              local clean_target="${gitea_host}/${owner}/${repo}.git"

              if [[ "$clean_existing" != "$clean_target" ]]; then
                  echo "gitea_push_directory: remote apunta a otro destino ('$clean_existing'), actualizando..." >&2
                  git -C "$directory" remote set-url origin "$remote_url"
              else
                  echo "gitea_push_directory: remote ya apunta al destino correcto, actualizando token..." >&2
                  git -C "$directory" remote set-url origin "$remote_url"
              fi
          fi
      else
          echo "gitea_push_directory: inicializando nuevo repositorio git..." >&2
          git -C "$directory" init
          git -C "$directory" remote add origin "$remote_url"
      fi

      # Configurar rama por defecto
      git -C "$directory" checkout -B "$branch" 2>/dev/null || true

      # Añadir y commitear cambios si los hay
      git -C "$directory" add -A

      local status
      status=$(git -C "$directory" status --porcelain)

      if [[ -n "$status" ]]; then
          echo "gitea_push_directory: commiteando cambios..." >&2
          git -C "$directory" -c user.email="agent@fn-registry" -c user.name="fn-registry agent" \
              commit -m "chore: sync from fn-registry agent"
      else
          echo "gitea_push_directory: sin cambios pendientes, solo haciendo push..." >&2
      fi

      echo "gitea_push_directory: haciendo push a $display_url..." >&2
      git -C "$directory" push --set-upstream origin "$branch" --force-with-lease 2>&1 \
          | sed "s|${GITEA_TOKEN}|***|g" >&2 \
          || git -C "$directory" push --set-upstream origin "$branch" 2>&1 \
              | sed "s|${GITEA_TOKEN}|***|g" >&2

      echo "gitea_push_directory: push completado a '$owner/$repo' rama '$branch'" >&2
  }

params_schema:
  {"params":[{"name":"directory","desc":"ruta absoluta o relativa al directorio local a subir"},{"name":"owner","desc":"usuario u organización propietaria del repositorio Gitea destino"},{"name":"repo","desc":"nombre del repositorio Gitea destino (debe existir previamente)"},{"name":"branch","desc":"rama en la que hacer push (default: main)"}],"output":"vacío — efectos observables en el repositorio Gitea destino"}


id: init_uv_venv_bash_infra
name: init_uv_venv
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: init_uv_venv([project_dir: string]) -> string
tags: ["python","venv","uv","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/init_uv_venv.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f0316721482ff0715ed3b38820d44506b9124ab7dbecc48b878802fe579c3dad
description:
  Crea un virtualenv Python con uv en el directorio dado si no existe. Fallback a python3 -m venv. Retorna la ruta del venv.

example:
  ```bash
  source init_uv_venv.sh
  venv=$(init_uv_venv /home/lucas/analysis/finanzas)
  echo "Venv creado en: $venv"

  # Idempotente — si ya existe, retorna la ruta sin recrear
  venv=$(init_uv_venv .)
  ```

notes:
  Idempotente: si el venv ya existe con un python valido, retorna la ruta sin hacer nada. Prefiere uv por velocidad, usa python3 como fallback.

code:
  # init_uv_venv
  # -------------
  # Crea un venv con uv en el directorio especificado si no existe.
  # Fallback a python -m venv si uv no esta disponible.
  # Imprime la ruta del venv a stdout.
  #
  # USO (sourced):
  #   source init_uv_venv.sh
  #   venv_path=$(init_uv_venv /path/to/project)

  init_uv_venv() {
      local project_dir="${1:-.}"
      local venv_path="${project_dir}/.venv"

      if [ -d "$venv_path" ] && [ -f "$venv_path/bin/python" ]; then
          echo "$venv_path"
          return 0
      fi

      if command -v uv &>/dev/null; then
          (cd "$project_dir" && uv venv) >/dev/null 2>&1
      elif command -v python3 &>/dev/null; then
          python3 -m venv "$venv_path"
      else
          echo "init_uv_venv: ni uv ni python3 disponibles" >&2
          return 1
      fi

      if [ ! -f "$venv_path/bin/python" ]; then
          echo "init_uv_venv: fallo al crear venv en $venv_path" >&2
          return 1
      fi

      echo "$venv_path"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto donde crear el venv (default: directorio actual)"}],"output":"ruta absoluta del venv creado o existente"}


id: install_android_sdk_bash_infra
name: install_android_sdk
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_android_sdk() -> void
tags: ["android","sdk","jdk","java","install","infra","mobile"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_android_sdk.sh
created_at: 2026-04-06T16:40:15Z
updated_at: 2026-04-06T16:40:15Z
props: []
emits: []
variant: []
content_hash: 93656abf9e21932065937e90abf5816f1a8830a61018b6276952c7e3101d48a7
params_schema: {"output":"sin salida estructurada; imprime progreso y resumen final con rutas de instalacion"}
description:
  Descarga e instala Android SDK command-line tools y JDK 17 localmente (sin root/sudo) en $ANDROID_SDK_DIR (default: $HOME/android-sdk). Idempotente: detecta instalacion existente y sale sin hacer nada. Genera env.sh con JAVA_HOME, ANDROID_HOME y PATH listos para hacer source.

example:
  ```bash
  # Instalacion en directorio por defecto ($HOME/android-sdk)
  source install_android_sdk.sh

  # Instalacion en directorio personalizado
  ANDROID_SDK_DIR=/opt/android source install_android_sdk.sh

  # Si ya esta instalado:
  # Android SDK ya instalado en: /home/user/android-sdk

  # Instalacion completa imprime:
  # Descargando JDK 17...
  # JDK 17 instalado: /home/user/android-sdk/jdk-17/jdk-17.0.x+y
  # Descargando Android cmdline-tools...
  # cmdline-tools instalados
  # Aceptando licencias de Android SDK...
  # Instalando platform-tools, platforms;android-34, build-tools;34.0.0...
  #
  # Android SDK instalado en: /home/user/android-sdk
  # JDK 17: /home/user/android-sdk/jdk-17/jdk-17.0.x+y
  # Para activar: source /home/user/android-sdk/env.sh

  # Activar entorno en sesion actual
  source ~/android-sdk/env.sh
  ```

notes:
  Requiere `curl` y `unzip` (disponibles en la mayoria de distros Linux). No requiere root ni sudo.

  El JDK se descarga desde Adoptium (Eclipse Temurin) via su API oficial. La URL de cmdline-tools apunta a la version 11076708 (2024). Si Google actualiza la version, cambiar la URL con el nuevo numero de build.

  La reorganizacion del zip es necesaria porque Google distribuye cmdline-tools con estructura `cmdline-tools/bin/...` pero sdkmanager espera estar en `cmdline-tools/latest/bin/sdkmanager` para que Android Studio y otras herramientas lo detecten correctamente.

  El archivo `env.sh` generado en `$ANDROID_SDK_DIR/env.sh` contiene las variables de entorno necesarias (`JAVA_HOME`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `PATH`) y puede hacerse source desde `.bashrc`, `.zshrc` o desde scripts de CI.

  Paquetes instalados: `platform-tools` (adb, fastboot), `platforms;android-34` (API 34), `build-tools;34.0.0`.

code:
  #!/usr/bin/env bash
  # install_android_sdk — Descarga e instala Android SDK command-line tools y JDK 17
  # localmente (sin root/sudo) en $ANDROID_SDK_DIR (default: $HOME/android-sdk).
  set -euo pipefail

  install_android_sdk() {
      local sdk_dir="${ANDROID_SDK_DIR:-$HOME/android-sdk}"
      local tmp_dir
      tmp_dir="$(mktemp -d)"

      # Limpia temporales al salir
      trap 'rm -rf "$tmp_dir"' EXIT

      # 1. Verifica si ya está instalado
      if [[ -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
          if JAVA_HOME="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1)" \
             "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" --version &>/dev/null; then
              echo "Android SDK ya instalado en: $sdk_dir"
              return 0
          fi
      fi

      mkdir -p "$sdk_dir"

      # 2. Descarga JDK 17 si no existe
      local jdk_dir
      jdk_dir="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1 || true)"

      if [[ -z "$jdk_dir" ]]; then
          echo "Descargando JDK 17..."
          local jdk_tar="$tmp_dir/jdk17.tar.gz"
          local jdk_url="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jdk/hotspot/normal/eclipse"

          if ! curl -fL --progress-bar -o "$jdk_tar" "$jdk_url"; then
              echo "ERROR: fallo al descargar JDK 17 desde $jdk_url" >&2
              return 1
          fi

          mkdir -p "$sdk_dir/jdk-17"
          echo "Extrayendo JDK 17..."
          if ! tar -xzf "$jdk_tar" -C "$sdk_dir/jdk-17"; then
              echo "ERROR: fallo al extraer JDK 17" >&2
              return 1
          fi

          jdk_dir="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1 || true)"
          if [[ -z "$jdk_dir" ]]; then
              echo "ERROR: no se encontro directorio jdk-17* tras la extraccion" >&2
              return 1
          fi

          if ! JAVA_HOME="$jdk_dir" "$jdk_dir/bin/java" -version &>/dev/null; then
              echo "ERROR: java -version fallo tras instalar JDK" >&2
              return 1
          fi
          echo "JDK 17 instalado: $jdk_dir"
      else
          echo "JDK 17 ya presente: $jdk_dir"
      fi

      export JAVA_HOME="$jdk_dir"

      # 3. Descarga Android cmdline-tools si no existen
      if [[ ! -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
          echo "Descargando Android cmdline-tools..."
          local tools_zip="$tmp_dir/cmdline-tools.zip"
          local tools_url="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"

          if ! curl -fL --progress-bar -o "$tools_zip" "$tools_url"; then
              echo "ERROR: fallo al descargar Android cmdline-tools desde $tools_url" >&2
              return 1
          fi

          local tools_tmp="$tmp_dir/cmdline-tools-extracted"
          mkdir -p "$tools_tmp"
          echo "Extrayendo cmdline-tools..."
          if ! unzip -q "$tools_zip" -d "$tools_tmp"; then
              echo "ERROR: fallo al extraer cmdline-tools" >&2
              return 1
          fi

          # La estructura del zip es cmdline-tools/bin/..., reorganizar a cmdline-tools/latest/
          mkdir -p "$sdk_dir/cmdline-tools"
          if [[ -d "$tools_tmp/cmdline-tools" ]]; then
              mv "$tools_tmp/cmdline-tools" "$sdk_dir/cmdline-tools/latest"
          else
              echo "ERROR: estructura inesperada en el zip de cmdline-tools" >&2
              return 1
          fi

          if [[ ! -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
              echo "ERROR: sdkmanager no encontrado tras extraer cmdline-tools" >&2
              return 1
          fi
          echo "cmdline-tools instalados"
      else
          echo "cmdline-tools ya presentes"
      fi

      local sdkmanager="$sdk_dir/cmdline-tools/latest/bin/sdkmanager"
      export ANDROID_HOME="$sdk_dir"
      export ANDROID_SDK_ROOT="$sdk_dir"
      export PATH="$JAVA_HOME/bin:$sdk_dir/cmdline-tools/latest/bin:$sdk_dir/platform-tools:$PATH"

      # 4. Acepta licencias e instala paquetes necesarios
      echo "Aceptando licencias de Android SDK..."
      if ! yes | "$sdkmanager" --licenses; then
          echo "ERROR: fallo al aceptar licencias de Android SDK" >&2
          return 1
      fi

      echo "Instalando platform-tools, platforms;android-34, build-tools;34.0.0..."
      if ! "$sdkmanager" "platform-tools" "platforms;android-34" "build-tools;34.0.0"; then
          echo "ERROR: fallo al instalar paquetes de Android SDK" >&2
          return 1
      fi

      # 5. Genera archivo de entorno
      local env_file="$sdk_dir/env.sh"
      cat > "$env_file" <<EOF
  export JAVA_HOME="$JAVA_HOME"
  export ANDROID_HOME="$sdk_dir"
  export ANDROID_SDK_ROOT="$sdk_dir"
  export PATH="\$JAVA_HOME/bin:$sdk_dir/cmdline-tools/latest/bin:$sdk_dir/platform-tools:\$PATH"
  EOF

      # 6. Resumen final
      echo ""
      echo "Android SDK instalado en: $sdk_dir"
      echo "JDK 17: $JAVA_HOME"
      echo "Para activar: source $sdk_dir/env.sh"
  }

  install_android_sdk


id: install_cpp_deps_bash_infra
name: install_cpp_deps
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_cpp_deps() -> void
tags: ["cpp","dependencies","setup","cmake","imgui"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_cpp_deps.sh
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
variant: []
content_hash: 012666be7f45769da894947f5e9ab7d3946f9377405d3e3204c4c12d8e1b11f1
params_schema: {"output":"Instala paquetes faltantes via apt o confirma que todo esta instalado"}
description:
  Verifica e instala las dependencias de sistema necesarias para compilar C++ con ImGui (cmake, g++, glfw, mesa)

documentation:
  # install_cpp_deps

  Verifica las dependencias necesarias para el build C++:

  - `cmake` — sistema de build

  - `g++` / `build-essential` — compilador

  - `libglfw3-dev` — windowing (GLFW)

  - `libgl1-mesa-dev` — OpenGL headers

  Tambien reporta si `mingw-w64` esta disponible para cross-compile a Windows.

  ```bash

  fn run install_cpp_deps

  ```

code:
  #!/usr/bin/env bash
  set -euo pipefail

  echo "[install_cpp_deps] Checking C++ build dependencies..."

  MISSING=()

  if ! command -v cmake &>/dev/null; then
      MISSING+=(cmake)
  else
      echo "  cmake: $(cmake --version | head -1)"
  fi

  if ! command -v g++ &>/dev/null; then
      MISSING+=(g++ build-essential)
  else
      echo "  g++: $(g++ --version | head -1)"
  fi

  if ! dpkg -s libglfw3-dev &>/dev/null 2>&1; then
      MISSING+=(libglfw3-dev)
  else
      echo "  libglfw3-dev: installed"
  fi

  if ! dpkg -s libgl1-mesa-dev &>/dev/null 2>&1; then
      MISSING+=(libgl1-mesa-dev)
  else
      echo "  libgl1-mesa-dev: installed"
  fi

  # Optional: mingw for cross-compile
  if command -v x86_64-w64-mingw32-g++ &>/dev/null; then
      echo "  mingw-w64: $(x86_64-w64-mingw32-g++ --version | head -1)"
  else
      echo "  mingw-w64: not installed (optional, for Windows cross-compile)"
  fi

  if [ ${#MISSING[@]} -eq 0 ]; then
      echo "[install_cpp_deps] All dependencies satisfied."
      exit 0
  fi

  echo ""
  echo "[install_cpp_deps] Missing packages: ${MISSING[*]}"
  echo "[install_cpp_deps] Installing..."
  sudo apt-get update -qq
  sudo apt-get install -y -qq "${MISSING[@]}"
  echo "[install_cpp_deps] Done."


id: install_go_bash_infra
name: install_go
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_go([version: string], [--force]) -> void
tags: ["bash","install","go","golang"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_go.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: d58d81b2aea781069360273400ef122cbaab3b1dabb6a990d03d0349dd9f3d5b
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/instaladores/instalar_go.sh
description:
  Instala Go en Linux descargando desde go.dev/dl. Detecta arquitectura automáticamente (amd64/arm64/armv6l). Idempotente: omite la instalación si Go ya está presente (a menos que se use --force). Configura PATH en ~/.bashrc o ~/.zshrc.

example:
  ```bash
  source bash/functions/infra/install_go.sh

  # Instalar versión por defecto (1.22.0)
  install_go

  # Instalar versión específica
  install_go 1.23.0

  # Reinstalar aunque ya esté instalado
  install_go 1.22.0 --force
  ```

notes:
  Requiere `curl` y `sudo`. Instala en `/usr/local/go`. Crea `$HOME/go/{bin,src,pkg}` como GOPATH. Después de instalar, hay que recargar el shell (`source ~/.bashrc`) o abrir una nueva terminal.

code:
  #!/usr/bin/env bash
  # install_go
  # ----------
  # Instala Go en Linux. Detecta arquitectura automáticamente (amd64/arm64/armv6l).
  # Descarga desde go.dev/dl, instala en /usr/local y configura PATH en el shell config.
  #
  # USO:
  #   source install_go.sh
  #   install_go [version] [--force]
  #
  # ARGUMENTOS:
  #   version   Versión de Go a instalar (default: 1.22.0)
  #   --force   Reinstala aunque Go ya esté instalado

  install_go() {
      local version="1.22.0"
      local force=false

      for arg in "$@"; do
          case "$arg" in
              --force) force=true ;;
              *)       [[ "$arg" =~ ^[0-9] ]] && version="$arg" ;;
          esac
      done

      local go_os="linux"
      local go_arch

      # Detectar arquitectura
      local arch
      arch="$(uname -m)"
      case "$arch" in
          x86_64)        go_arch="amd64" ;;
          aarch64|arm64) go_arch="arm64" ;;
          armv6l)        go_arch="armv6l" ;;
          *)
              echo "install_go: arquitectura no soportada: ${arch}" >&2
              return 1
              ;;
      esac

      # Verificar si ya está instalado
      if command -v go &>/dev/null && [[ "$force" != true ]]; then
          local current_version
          current_version="$(go version | awk '{print $3}' | sed 's/go//')"
          echo "install_go: Go ya está instalado (versión: ${current_version}). Usa --force para reinstalar."
          return 0
      fi

      local tarball="go${version}.${go_os}-${go_arch}.tar.gz"
      local url="https://go.dev/dl/${tarball}"
      local install_dir="/usr/local"

      echo "Instalando Go ${version} para ${go_os}-${go_arch}..."

      # Eliminar versión anterior si existe
      if command -v go &>/dev/null; then
          echo "Eliminando versión anterior..."
          sudo rm -rf "${install_dir}/go"
      fi

      # Descargar en directorio temporal
      local tmp_dir
      tmp_dir="$(mktemp -d)"

      echo "Descargando ${url}..."
      if ! curl -LO --output-dir "$tmp_dir" "$url"; then
          echo "install_go: error al descargar Go ${version}. Verifica la versión en: https://go.dev/dl/" >&2
          rm -rf "$tmp_dir"
          return 1
      fi

      echo "Instalando en ${install_dir}..."
      sudo tar -C "$install_dir" -xzf "${tmp_dir}/${tarball}"
      rm -rf "$tmp_dir"

      # Configurar PATH en shell config
      local shell_config=""
      if [[ -f "$HOME/.bashrc" ]]; then
          shell_config="$HOME/.bashrc"
      elif [[ -f "$HOME/.zshrc" ]]; then
          shell_config="$HOME/.zshrc"
      fi

      if [[ -n "$shell_config" ]]; then
          if ! grep -q "export PATH=\$PATH:${install_dir}/go/bin" "$shell_config"; then
              {
                  echo ""
                  echo "# Go configuration"
                  echo "export PATH=\$PATH:${install_dir}/go/bin"
                  echo "export GOPATH=\$HOME/go"
                  echo "export PATH=\$PATH:\$GOPATH/bin"
              } >> "$shell_config"
              echo "Variables de PATH añadidas a ${shell_config}"
          else
              echo "Variables de entorno ya configuradas en ${shell_config}"
          fi
      fi

      # Crear estructura GOPATH
      mkdir -p "$HOME/go"/{bin,src,pkg}

      # Verificar instalación
      export PATH="$PATH:${install_dir}/go/bin"
      local installed_version
      installed_version="$("${install_dir}/go/bin/go" version)"
      echo ""
      echo "Go instalado correctamente: ${installed_version}"
      echo "Reinicia tu terminal o ejecuta: source ${shell_config:-~/.bashrc}"
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_go "$@"
  fi

params_schema:
  {"params":[{"name":"version","desc":"versión de Go a instalar, ej: 1.22.0 (default: 1.22.0)"},{"name":"--force","desc":"flag para reinstalar aunque Go ya esté instalado"}],"output":"progreso a stdout; exit code 1 si la arquitectura no es soportada o falla la descarga"}


id: install_mantine_bash_infra
name: install_mantine
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_mantine(project_dir: string) -> void
tags: ["mantine","frontend","install","react","ui","postcss"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_mantine.sh
created_at: 2026-04-05T23:51:12Z
updated_at: 2026-04-05T23:51:12Z
props: []
emits: []
variant: []
content_hash: ff562680c5e966c914ae32e8f04b4ee0d3107b2cb786eaadb9463600bb87841c
description:
  Instala Mantine UI con todas sus dependencias (@mantine/core, hooks, charts, notifications, form) y PostCSS en un proyecto frontend. Detecta package manager por lockfile. Genera postcss.config.cjs si no existe. Idempotente.

example:
  ```bash
  # Instalar Mantine en un proyecto con pnpm
  source install_mantine.sh
  install_mantine ./apps/rapid_dashboards/frontend

  # Uso directo
  bash install_mantine.sh ./frontend
  ```

notes:
  Detecta el package manager por lockfile: pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm. Instala las dependencias core de Mantine v7+ y el stack PostCSS necesario. Si postcss.config.cjs ya existe no lo sobreescribe.

code:
  # install_mantine
  # ---------------
  # Instala dependencias de Mantine UI en un proyecto frontend.
  # Detecta package manager por lockfile (pnpm > yarn > npm).
  # Genera postcss.config.cjs si no existe.
  # Idempotente: no reinstala si ya estan presentes.
  #
  # USO (sourced):
  #   source install_mantine.sh
  #   install_mantine /path/to/frontend
  #
  # USO (directo):
  #   bash install_mantine.sh /path/to/frontend

  install_mantine() {
      local project_dir="$1"

      if [ -z "$project_dir" ]; then
          echo "install_mantine: se requiere project_dir" >&2
          return 1
      fi

      if [ ! -f "$project_dir/package.json" ]; then
          echo "install_mantine: no existe package.json en $project_dir" >&2
          return 1
      fi

      # Detectar package manager
      local pm="npm"
      local add_cmd="install"
      local add_dev_flag="--save-dev"
      if [ -f "$project_dir/pnpm-lock.yaml" ] || [ -f "$project_dir/pnpm-workspace.yaml" ]; then
          pm="pnpm"
          add_cmd="add"
          add_dev_flag="-D"
      elif [ -f "$project_dir/yarn.lock" ]; then
          pm="yarn"
          add_cmd="add"
          add_dev_flag="--dev"
      elif [ -f "$project_dir/package-lock.json" ]; then
          pm="npm"
          add_cmd="install"
          add_dev_flag="--save-dev"
      fi

      echo "Detectado package manager: $pm"

      # Dependencias runtime
      local runtime_deps="@mantine/core @mantine/hooks @mantine/charts @mantine/notifications @mantine/form"
      echo "Instalando dependencias Mantine..."
      (cd "$project_dir" && $pm $add_cmd $runtime_deps 2>&1)

      if [ $? -ne 0 ]; then
          echo "install_mantine: fallo instalando dependencias runtime" >&2
          return 1
      fi

      # Dependencias PostCSS (dev)
      local dev_deps="postcss postcss-preset-mantine postcss-simple-vars"
      echo "Instalando dependencias PostCSS..."
      (cd "$project_dir" && $pm $add_cmd $add_dev_flag $dev_deps 2>&1)

      if [ $? -ne 0 ]; then
          echo "install_mantine: fallo instalando dependencias PostCSS" >&2
          return 1
      fi

      # Generar postcss.config.cjs si no existe
      if [ ! -f "$project_dir/postcss.config.cjs" ]; then
          echo "Generando postcss.config.cjs..."
          cat > "$project_dir/postcss.config.cjs" << 'POSTCSS'
  module.exports = {
    plugins: {
      'postcss-preset-mantine': {},
      'postcss-simple-vars': {
        variables: {
          'mantine-breakpoint-xs': '36em',
          'mantine-breakpoint-sm': '48em',
          'mantine-breakpoint-md': '62em',
          'mantine-breakpoint-lg': '75em',
          'mantine-breakpoint-xl': '88em',
        },
      },
    },
  };
  POSTCSS
          echo "postcss.config.cjs creado"
      else
          echo "postcss.config.cjs ya existe, no se sobreescribe"
      fi

      echo "Mantine instalado correctamente en $project_dir"
  }

  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_mantine "$@"
  fi

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto frontend con package.json"}],"output":"sin salida; muestra progreso de instalación"}


id: install_nbconvert_bash_infra
name: install_nbconvert
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_nbconvert(project_dir: string) -> void
tags: ["jupyter","nbconvert","pdf","export","playwright","python","uv"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source install_nbconvert.sh
  install_nbconvert /home/lucas/analysis/finanzas
  ```

tests: []
file_path: bash/functions/infra/install_nbconvert.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: be6ee004a968c0023937c202d2e84019de4b1d71368e03571d611deed2063d3b
description:
  Instala nbconvert y playwright con chromium en un proyecto uv existente. Idempotente: uv add no reinstala si los paquetes ya estan presentes.

notes:
  Requiere que el venv ya exista (usa `init_uv_venv` antes). La instalacion de chromium via `uv run playwright install chromium` puede tardar la primera vez. La salida de playwright se suprime si tiene exito — solo se muestra si hay un error.

code:
  # install_nbconvert
  # ------------------
  # Instala nbconvert y playwright con chromium en un proyecto uv existente.
  # Idempotente: uv add no reinstala si los paquetes ya estan presentes.
  #
  # USO (sourced):
  #   source install_nbconvert.sh
  #   install_nbconvert /path/to/project

  install_nbconvert() {
      local project_dir="$1"

      if [ -z "$project_dir" ]; then
          echo "install_nbconvert: se requiere project_dir" >&2
          return 1
      fi

      if [ ! -d "$project_dir/.venv" ]; then
          echo "install_nbconvert: no existe .venv en $project_dir — ejecuta init_uv_venv primero" >&2
          return 1
      fi

      # Instalar nbconvert y playwright via uv add
      (cd "$project_dir" && uv add nbconvert playwright 2>&1)

      # Instalar chromium — capturar output, solo mostrar si hay error
      local playwright_output
      if ! playwright_output=$(cd "$project_dir" && uv run playwright install chromium 2>&1); then
          echo "$playwright_output" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto con venv existente"}],"output":"sin salida"}


id: install_nodejs_bash_infra
name: install_nodejs
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_nodejs([version: string]) -> void
tags: ["bash","install","nodejs","nvm"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_nodejs.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: f2b757c412590d7ed59ee033dc3c08bf57af4b31466b6baf091659f8ecb18a74
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/instaladores/instalar_nodejs.sh
description:
  Instala Node.js en Linux usando nvm. Instala nvm v0.39.7 si no está presente. Instala la versión de Node indicada, la activa con 'nvm use' y la configura como default. Idempotente si nvm ya está instalado.

example:
  ```bash
  source bash/functions/infra/install_nodejs.sh

  # Instalar Node.js 20 (LTS por defecto)
  install_nodejs

  # Instalar versión específica
  install_nodejs 18
  install_nodejs 21
  ```

notes:
  Requiere `curl`. nvm se instala en `$HOME/.nvm`. Después de instalar en una sesión nueva, hay que recargar el shell para que los comandos `node` y `npm` queden disponibles globalmente.

code:
  #!/usr/bin/env bash
  # install_nodejs
  # --------------
  # Instala Node.js en Linux usando nvm (Node Version Manager).
  # Instala nvm si no está presente, luego instala la versión de Node indicada
  # y la configura como default.
  #
  # USO:
  #   source install_nodejs.sh
  #   install_nodejs [version]
  #
  # ARGUMENTOS:
  #   version   Versión principal de Node.js (default: 20)

  install_nodejs() {
      local node_version="${1:-20}"

      echo "Instalando Node.js v${node_version} mediante nvm..."
      echo ""

      # Informar si Node ya está instalado
      if command -v node &>/dev/null; then
          local current_version
          current_version="$(node --version)"
          echo "Node.js ya está instalado: ${current_version}"
          echo "Continuando con la instalación/actualización..."
      fi

      # Instalar nvm si no está presente
      if [[ -d "$HOME/.nvm" ]]; then
          echo "nvm ya está instalado."
          export NVM_DIR="$HOME/.nvm"
          # shellcheck disable=SC1091
          [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh"
      else
          echo "Descargando e instalando nvm..."
          curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

          export NVM_DIR="$HOME/.nvm"
          # shellcheck disable=SC1091
          [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh"
          echo "nvm instalado correctamente."
      fi

      # Verificar que nvm esté disponible
      if ! command -v nvm &>/dev/null; then
          echo "install_nodejs: nvm no está disponible después de la instalación" >&2
          echo "  Ejecuta: source ~/.bashrc  (o abre una nueva terminal)" >&2
          return 1
      fi

      echo ""
      echo "Instalando Node.js v${node_version}..."
      nvm install "$node_version"
      nvm use "$node_version"
      nvm alias default "$node_version"

      local installed_node
      local installed_npm
      installed_node="$(node --version)"
      installed_npm="$(npm --version)"

      echo ""
      echo "Node.js instalado correctamente:"
      echo "  Node.js: ${installed_node}"
      echo "  npm:     ${installed_npm}"
      echo ""
      echo "Si es una instalación nueva, reinicia tu terminal o ejecuta:"
      echo "  source ~/.bashrc  # o ~/.zshrc según tu shell"
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_nodejs "$@"
  fi

params_schema:
  {"params":[{"name":"version","desc":"versión principal de Node.js a instalar (default: 20)"}],"output":"progreso a stdout con versión instalada; exit code 1 si nvm no queda disponible"}


id: install_nordvpn_bash_infra
name: install_nordvpn
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_nordvpn() -> void
tags: ["vpn","nordvpn","install","infra","wsl2"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_nordvpn.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 57b7aebbc251da1f0d0c4680aef0f2734275a1932a0bfb85c596f41f481f047d
params_schema: {"output":"sin salida; muestra estado de instalación"}
description:
  Instala NordVPN CLI en Ubuntu/Debian (incluido WSL2). Configura repositorio oficial, instala paquete y habilita servicio nordvpnd. Idempotente.

example:
  ```bash
  source install_nordvpn.sh
  install_nordvpn
  # nordvpn ya instalado: NordVPN Version 3.x.x
  # — o —
  # Instalando NordVPN CLI...
  # NordVPN instalado: NordVPN Version 3.x.x
  # NOTA: ejecuta 'nordvpn login' para autenticarte
  ```

notes:
  Usa el script de instalacion oficial de NordVPN. En WSL2 sin systemd, levanta nordvpnd manualmente. Agrega el usuario al grupo nordvpn para evitar sudo en comandos posteriores. Despues de instalar, se requiere `nordvpn login` para autenticarse.

code:
  # install_nordvpn
  # ---------------
  # Instala NordVPN CLI en Ubuntu/Debian (incluido WSL2).
  # Configura el repositorio oficial, instala el paquete y habilita el servicio.
  # Si ya esta instalado, no hace nada.
  #
  # USO (sourced):
  #   source install_nordvpn.sh
  #   install_nordvpn

  install_nordvpn() {
      if command -v nordvpn &>/dev/null; then
          echo "nordvpn ya instalado: $(nordvpn version 2>/dev/null)"
          return 0
      fi

      echo "Instalando NordVPN CLI..."

      # Descargar e instalar via script oficial
      sh <(curl -sSf https://downloads.nordcdn.com/apps/linux/install.sh) 2>&1

      if ! command -v nordvpn &>/dev/null; then
          echo "install_nordvpn: fallo la instalacion" >&2
          return 1
      fi

      # Agregar usuario al grupo nordvpn para evitar sudo
      sudo usermod -aG nordvpn "$USER" 2>/dev/null || true

      # Habilitar servicio (systemd o manual para WSL2)
      if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
          sudo systemctl enable --now nordvpnd 2>/dev/null || true
      else
          # WSL2 sin systemd — levantar daemon manualmente
          sudo nordvpnd &>/dev/null &
          sleep 2
      fi

      echo "NordVPN instalado: $(nordvpn version 2>/dev/null)"
      echo "NOTA: ejecuta 'nordvpn login' para autenticarte"
  }


id: install_pnpm_bash_infra
name: install_pnpm
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_pnpm() -> void
tags: ["bash","install","pnpm","node"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source bash/functions/infra/install_pnpm.sh

  install_pnpm
  ```

tests: []
file_path: bash/functions/infra/install_pnpm.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 10e2a6b831fd3764f1ec46842c918788834969cbc02f1de6e008af917d1c06ca
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/instaladores/instalar_pnpm.sh
description:
  Instala pnpm globalmente usando npm (npm install -g pnpm). Verifica que npm esté disponible. Idempotente: si pnpm ya está instalado, informa y termina sin hacer nada.

notes:
  Requiere Node.js/npm instalado previamente. Si la instalación global falla por permisos, usar `sudo npm install -g pnpm` manualmente. Idempotente: vuelve a ejecutarse sin error si pnpm ya existe.

code:
  #!/usr/bin/env bash
  # install_pnpm
  # ------------
  # Instala pnpm globalmente usando npm.
  # Verifica que npm esté disponible antes de instalar.
  # Idempotente: informa si pnpm ya está instalado.
  #
  # USO:
  #   source install_pnpm.sh
  #   install_pnpm

  install_pnpm() {
      echo "Instalando pnpm..."
      echo ""

      # Verificar si pnpm ya está instalado
      if command -v pnpm &>/dev/null; then
          local current_version
          current_version="$(pnpm --version 2>/dev/null || echo "desconocida")"
          echo "pnpm ya está instalado (versión: ${current_version})."
          return 0
      fi

      # Verificar que npm esté disponible
      if ! command -v npm &>/dev/null; then
          echo "install_pnpm: npm no está instalado (requerido para instalar pnpm)" >&2
          echo "  Instala Node.js primero con install_nodejs" >&2
          return 1
      fi

      local npm_version
      npm_version="$(npm --version 2>/dev/null || echo "?")"
      echo "npm detectado: ${npm_version}"
      echo ""

      echo "Instalando pnpm globalmente (npm install -g pnpm)..."
      if ! npm install -g pnpm; then
          echo "install_pnpm: falló la instalación de pnpm" >&2
          echo "  Intenta con sudo: sudo npm install -g pnpm" >&2
          return 1
      fi

      # Verificar instalación
      if ! command -v pnpm &>/dev/null; then
          echo "install_pnpm: pnpm no está disponible después de la instalación" >&2
          echo "  Verifica que npm/bin esté en tu PATH" >&2
          return 1
      fi

      local installed_version
      installed_version="$(pnpm --version)"
      echo ""
      echo "pnpm instalado correctamente: ${installed_version}"
      echo ""
      echo "Comandos útiles:"
      echo "  pnpm install    - Instalar dependencias"
      echo "  pnpm add <pkg>  - Agregar paquete"
      echo "  pnpm run <cmd>  - Ejecutar script"
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_pnpm "$@"
  fi

params_schema:
  {"params":[{"name":"(ninguno)","desc":"no acepta argumentos"}],"output":"progreso a stdout con versión instalada; exit code 1 si npm no está disponible o falla la instalación"}


id: install_python312_bash_infra
name: install_python312
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_python312() -> void
tags: ["bash","install","python","python312"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source bash/functions/infra/install_python312.sh

  install_python312
  ```

tests: []
file_path: bash/functions/infra/install_python312.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 149be2695c431448362ecff78849bf5aac7883de742c05ac305993be979b4e4f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/instaladores/instalar_python312.sh
description:
  Instala Python 3.12 detectando la distribución Linux automáticamente. Ubuntu/Debian/Mint usan deadsnakes PPA; Fedora/RHEL usan dnf; Arch/Manjaro usan pacman. Instala también python3.12-venv, python3.12-dev y verifica pip. Idempotente.

notes:
  Requiere `sudo`. Para distribuciones no soportadas, se recomienda usar pyenv. Idempotente: si `python3.12` ya existe en PATH, informa y termina sin hacer nada.

code:
  #!/usr/bin/env bash
  # install_python312
  # -----------------
  # Instala Python 3.12 en Linux detectando la distribución automáticamente.
  # - Ubuntu/Debian/Pop/Mint/Elementary: usa deadsnakes PPA
  # - Fedora/RHEL/CentOS: usa dnf
  # - Arch/Manjaro: usa pacman
  # Instala también python3.12-venv, python3.12-dev y verifica pip.
  #
  # USO:
  #   source install_python312.sh
  #   install_python312

  install_python312() {
      echo "Instalando Python 3.12..."
      echo ""

      # Detectar distribución
      local distro="unknown"
      if [[ -f /etc/os-release ]]; then
          # shellcheck disable=SC1091
          distro="$(. /etc/os-release && echo "${ID:-unknown}")"
          echo "Distribución detectada: ${distro}"
      else
          echo "install_python312: no se pudo detectar la distribución" >&2
      fi
      echo ""

      # Verificar si Python 3.12 ya está instalado
      if command -v python3.12 &>/dev/null; then
          local current_version
          current_version="$(python3.12 --version 2>&1)"
          echo "Python 3.12 ya está instalado: ${current_version}"
          return 0
      fi

      case "$distro" in
          ubuntu|debian|pop|mint|elementary)
              echo "Instalando Python 3.12 usando deadsnakes PPA..."
              echo ""

              echo "Actualizando repositorios..."
              if ! sudo apt update; then
                  echo "install_python312: falló la actualización de repositorios" >&2
                  return 1
              fi

              echo "Verificando software-properties-common..."
              if ! dpkg -l 2>/dev/null | grep -q software-properties-common; then
                  if ! sudo apt install -y software-properties-common; then
                      echo "install_python312: falló la instalación de software-properties-common" >&2
                      return 1
                  fi
              fi

              echo "Añadiendo deadsnakes PPA..."
              if ! sudo add-apt-repository -y ppa:deadsnakes/ppa; then
                  echo "install_python312: falló al añadir deadsnakes PPA" >&2
                  return 1
              fi

              echo "Actualizando lista de paquetes..."
              if ! sudo apt update; then
                  echo "install_python312: falló la actualización después de añadir PPA" >&2
                  return 1
              fi

              echo "Instalando Python 3.12 y herramientas..."
              if ! sudo apt install -y python3.12 python3.12-venv python3.12-dev python3-pip; then
                  echo "install_python312: falló la instalación de Python 3.12" >&2
                  return 1
              fi
              ;;

          fedora|rhel|centos)
              echo "Instalando Python 3.12 usando dnf..."
              if ! sudo dnf install -y python3.12 python3.12-devel; then
                  echo "install_python312: falló la instalación con dnf" >&2
                  return 1
              fi
              ;;

          arch|manjaro)
              echo "Instalando Python 3.12 usando pacman..."
              if ! sudo pacman -S --noconfirm python; then
                  echo "install_python312: falló la instalación con pacman" >&2
                  return 1
              fi
              ;;

          *)
              echo "install_python312: distribución no soportada automáticamente: ${distro}" >&2
              echo "  Opciones manuales:" >&2
              echo "    - Compilar desde fuente: https://www.python.org/downloads/" >&2
              echo "    - Usar pyenv: curl https://pyenv.run | bash" >&2
              return 1
              ;;
      esac

      echo ""

      # Verificar instalación
      if ! command -v python3.12 &>/dev/null; then
          echo "install_python312: Python 3.12 no está disponible después de la instalación" >&2
          echo "  Puede que necesites reiniciar la terminal" >&2
          return 1
      fi

      local installed_version
      installed_version="$(python3.12 --version 2>&1)"
      echo "Python 3.12 instalado correctamente: ${installed_version}"
      echo ""

      # Verificar pip
      echo "Verificando pip para Python 3.12..."
      if ! python3.12 -m pip --version &>/dev/null; then
          echo "pip no disponible, instalando..."
          if ! python3.12 -m ensurepip --upgrade; then
              echo "  Instala pip manualmente: curl -sS https://bootstrap.pypa.io/get-pip.py | python3.12"
          else
              echo "pip instalado para Python 3.12"
          fi
      else
          echo "pip disponible para Python 3.12"
      fi

      echo ""
      echo "Comandos útiles:"
      echo "  python3.12 -m venv .venv   - Crear entorno virtual"
      echo "  source .venv/bin/activate  - Activar entorno"
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_python312 "$@"
  fi

params_schema:
  {"params":[{"name":"(ninguno)","desc":"no acepta argumentos; detecta la distribución automáticamente"}],"output":"progreso a stdout; exit code 1 si la distribución no es soportada o falla la instalación"}


id: install_uv_bash_infra
name: install_uv
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_uv() -> void
tags: ["bash","install","uv","python"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_uv.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 25abaaa6a196151756cbf10f7cf07a156d79bb4ca392d4a2369b539abb4c19d4
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/instaladores/instalar_uv.sh
description:
  Instala uv, el gestor de paquetes Python ultra-rápido escrito en Rust, usando el instalador oficial de astral.sh. Configura PATH en ~/.bashrc y ~/.zshrc. Idempotente: si uv ya está instalado, informa y termina.

example:
  ```bash
  source bash/functions/infra/install_uv.sh

  install_uv

  # Uso posterior
  uv venv
  source .venv/bin/activate
  uv pip install requests pandas
  ```

notes:
  Instala en `$HOME/.cargo/bin`. Requiere `curl`. uv es compatible con pip pero 10-100x más rápido. Después de instalar en una sesión nueva, hay que recargar el shell.

code:
  #!/usr/bin/env bash
  # install_uv
  # ----------
  # Instala uv — gestor de paquetes Python ultra-rápido escrito en Rust.
  # Usa el instalador oficial de astral.sh. Configura PATH en ~/.bashrc y ~/.zshrc.
  #
  # USO:
  #   source install_uv.sh
  #   install_uv

  install_uv() {
      echo "Instalando uv (gestor de paquetes Python)..."
      echo ""

      # Verificar si uv ya está instalado
      if command -v uv &>/dev/null; then
          local current_version
          current_version="$(uv --version 2>/dev/null || echo "desconocida")"
          echo "uv ya está instalado (versión: ${current_version})."
          return 0
      fi

      # Verificar curl
      if ! command -v curl &>/dev/null; then
          echo "install_uv: curl no está instalado (requerido)" >&2
          echo "  Instálalo con: sudo apt install curl" >&2
          return 1
      fi

      echo "Descargando e instalando uv (instalador oficial astral.sh)..."
      if ! curl -LsSf https://astral.sh/uv/install.sh | sh; then
          echo "install_uv: falló la instalación de uv" >&2
          echo "  Verifica tu conexión a internet y permisos" >&2
          return 1
      fi

      echo ""

      # Configurar PATH en ~/.bashrc
      if ! grep -q ".cargo/bin" "$HOME/.bashrc" 2>/dev/null; then
          {
              echo ""
              echo "# uv and cargo binaries"
              echo 'export PATH="$HOME/.cargo/bin:$PATH"'
          } >> "$HOME/.bashrc"
          echo "PATH añadido a ~/.bashrc"
      else
          echo "PATH ya configurado en ~/.bashrc"
      fi

      # Configurar PATH en ~/.zshrc si existe
      if [[ -f "$HOME/.zshrc" ]]; then
          if ! grep -q ".cargo/bin" "$HOME/.zshrc" 2>/dev/null; then
              {
                  echo ""
                  echo "# uv and cargo binaries"
                  echo 'export PATH="$HOME/.cargo/bin:$PATH"'
              } >> "$HOME/.zshrc"
              echo "PATH añadido a ~/.zshrc"
          fi
      fi

      # Cargar PATH en la sesión actual
      export PATH="$HOME/.cargo/bin:$PATH"

      echo ""

      # Verificar instalación
      if ! command -v uv &>/dev/null; then
          echo "uv instalado pero no está en el PATH actual."
          echo "  Ejecuta: source ~/.bashrc  (o abre una nueva terminal)"
      else
          local installed_version
          installed_version="$(uv --version)"
          echo "uv instalado correctamente: ${installed_version}"
      fi

      echo ""
      echo "Comandos útiles de uv:"
      echo "  uv venv                     - Crear entorno virtual"
      echo "  uv pip install <package>    - Instalar paquete"
      echo "  uv pip sync requirements.txt - Sincronizar dependencias"
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_uv "$@"
  fi

params_schema:
  {"params":[{"name":"(ninguno)","desc":"no acepta argumentos"}],"output":"progreso a stdout; exit code 1 si curl no está disponible o falla la instalación"}


id: install_volta_bash_infra
name: install_volta
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_volta() -> void
tags: ["bash","install","volta","node"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_volta.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: a7d130e96a2526037b588dd8db239a2b13fc7e224e1fa4e8252207d93ccbdcc0
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/instaladores/instalar_volta.sh
description:
  Instala Volta, el gestor de versiones de Node.js, usando el instalador oficial de get.volta.sh. Configura VOLTA_HOME y PATH en ~/.bashrc y ~/.zshrc. Idempotente: si Volta ya está instalado, informa y termina.

example:
  ```bash
  source bash/functions/infra/install_volta.sh

  install_volta

  # Uso posterior (tras recargar shell)
  volta install node
  volta install pnpm
  volta list
  ```

notes:
  Volta se instala en `$HOME/.volta`. Requiere `curl`. A diferencia de nvm, Volta gestiona versiones de Node.js a nivel de proyecto via `package.json`. Después de instalar, recargar el shell con `source ~/.bashrc`.

code:
  #!/usr/bin/env bash
  # install_volta
  # -------------
  # Instala Volta — gestor de versiones de Node.js rápido y confiable.
  # Usa el instalador oficial de get.volta.sh. Configura VOLTA_HOME y PATH
  # en ~/.bashrc y ~/.zshrc.
  #
  # USO:
  #   source install_volta.sh
  #   install_volta

  install_volta() {
      echo "Instalando Volta (gestor de versiones Node.js)..."
      echo ""

      # Verificar si Volta ya está instalado
      if command -v volta &>/dev/null; then
          local current_version
          current_version="$(volta --version 2>/dev/null || echo "desconocida")"
          echo "Volta ya está instalado (versión: ${current_version})."
          return 0
      fi

      # Verificar curl
      if ! command -v curl &>/dev/null; then
          echo "install_volta: curl no está instalado (requerido)" >&2
          echo "  Instálalo con: sudo apt install curl" >&2
          return 1
      fi

      echo "Descargando e instalando Volta (instalador oficial)..."
      if ! curl https://get.volta.sh | bash; then
          echo "install_volta: falló la instalación de Volta" >&2
          echo "  Verifica tu conexión a internet" >&2
          return 1
      fi

      echo ""

      # Configurar variables de entorno
      local volta_home="$HOME/.volta"
      export VOLTA_HOME="$volta_home"
      export PATH="$volta_home/bin:$PATH"

      # Configurar en ~/.bashrc
      if ! grep -q "VOLTA_HOME" "$HOME/.bashrc" 2>/dev/null; then
          {
              echo ""
              echo "# Volta configuration"
              echo 'export VOLTA_HOME="$HOME/.volta"'
              echo 'export PATH="$VOLTA_HOME/bin:$PATH"'
          } >> "$HOME/.bashrc"
          echo "Variables añadidas a ~/.bashrc"
      else
          echo "Variables ya configuradas en ~/.bashrc"
      fi

      # Configurar en ~/.zshrc si existe
      if [[ -f "$HOME/.zshrc" ]]; then
          if ! grep -q "VOLTA_HOME" "$HOME/.zshrc" 2>/dev/null; then
              {
                  echo ""
                  echo "# Volta configuration"
                  echo 'export VOLTA_HOME="$HOME/.volta"'
                  echo 'export PATH="$VOLTA_HOME/bin:$PATH"'
              } >> "$HOME/.zshrc"
              echo "Variables añadidas a ~/.zshrc"
          fi
      fi

      echo ""

      # Verificar instalación
      if command -v volta &>/dev/null; then
          local installed_version
          installed_version="$(volta --version)"
          echo "Volta instalado correctamente: ${installed_version}"
      elif [[ -f "$HOME/.volta/bin/volta" ]]; then
          echo "Volta instalado en ${HOME}/.volta/bin pero no está en PATH actual."
          echo "  Ejecuta: source ~/.bashrc  (o abre una nueva terminal)"
      else
          echo "install_volta: Volta no está disponible después de la instalación" >&2
          return 1
      fi

      echo ""
      echo "Próximos pasos:"
      echo "  1. source ~/.bashrc       - Recargar shell"
      echo "  2. volta install node     - Instalar Node.js"
      echo "  3. volta install pnpm     - Instalar pnpm"
      echo ""
      echo "Comandos útiles:"
      echo "  volta install node@20     - Instalar Node.js v20"
      echo "  volta list                - Ver herramientas instaladas"
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_volta "$@"
  fi

params_schema:
  {"params":[{"name":"(ninguno)","desc":"no acepta argumentos"}],"output":"progreso a stdout; exit code 1 si curl no está disponible o falla la instalación"}


id: install_wails_bash_infra
name: install_wails
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_wails() -> void
tags: ["bash","install","wails","desktop"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_wails.sh
created_at: 2026-04-08T21:29:50Z
updated_at: 2026-04-08T21:29:50Z
props: []
emits: []
variant: []
content_hash: cb6ba3f708b63d37905349b1fba09660cbdd3252e11c6d7da444f26329b93454
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/instaladores/instalar_wails.sh
description:
  Instala Wails v2 (framework de apps de escritorio Go). Detecta la distribución Linux e instala las dependencias de sistema (GTK3, WebKit2GTK, build tools) y luego el CLI via 'go install ...@latest'. Requiere Go instalado previamente.

example:
  ```bash
  source bash/functions/infra/install_wails.sh

  install_wails

  # Verificar
  wails doctor

  # Crear proyecto
  wails init -n my-desktop-app -t react
  ```

notes:
  Requiere Go y sudo. Para distribuciones no listadas (opensuse, etc.) instala las dependencias manualmente y luego procede con el CLI. Templates disponibles: vanilla, vue, react, svelte, lit, angular.

code:
  #!/usr/bin/env bash
  # install_wails
  # -------------
  # Instala Wails v2 — framework para aplicaciones de escritorio en Go.
  # Detecta la distribución Linux e instala las dependencias de sistema necesarias
  # (GTK3, WebKit2GTK, build tools) y luego instala el CLI de Wails via go install.
  #
  # USO:
  #   source install_wails.sh
  #   install_wails

  install_wails() {
      echo "Instalando Wails..."
      echo ""

      # Verificar Go
      if ! command -v go &>/dev/null; then
          echo "install_wails: Go no está instalado (requerido)" >&2
          echo "  Instálalo primero con install_go" >&2
          return 1
      fi

      local go_version
      go_version="$(go version)"
      echo "Go detectado: ${go_version}"
      echo ""

      # Detectar distribución
      local distro="unknown"
      if [[ -f /etc/os-release ]]; then
          # shellcheck disable=SC1091
          distro="$(. /etc/os-release && echo "${ID:-unknown}")"
      else
          echo "install_wails: no se pudo detectar la distribución de Linux" >&2
          return 1
      fi

      echo "Instalando dependencias del sistema para ${distro}..."
      case "$distro" in
          ubuntu|debian|linuxmint|pop)
              sudo apt update
              if ! sudo apt install -y libgtk-3-dev libwebkit2gtk-4.0-dev build-essential pkg-config; then
                  echo "install_wails: falló la instalación de dependencias de sistema" >&2
                  return 1
              fi
              ;;
          fedora|rhel|centos)
              if ! sudo dnf install -y gtk3-devel webkit2gtk3-devel gcc-c++ pkgconfig; then
                  echo "install_wails: falló la instalación de dependencias de sistema" >&2
                  return 1
              fi
              ;;
          arch|manjaro)
              if ! sudo pacman -Sy --noconfirm gtk3 webkit2gtk base-devel; then
                  echo "install_wails: falló la instalación de dependencias de sistema" >&2
                  return 1
              fi
              ;;
          opensuse*)
              if ! sudo zypper install -y gtk3-devel webkit2gtk3-devel gcc-c++ pkg-config; then
                  echo "install_wails: falló la instalación de dependencias de sistema" >&2
                  return 1
              fi
              ;;
          *)
              echo "Distribución no reconocida: ${distro}"
              echo "Instala manualmente: gtk3, webkit2gtk, build-essential, pkg-config"
              echo "Continuando con la instalación de Wails CLI..."
              ;;
      esac

      echo ""
      echo "Instalando Wails CLI (go install github.com/wailsapp/wails/v2/cmd/wails@latest)..."
      if ! go install github.com/wailsapp/wails/v2/cmd/wails@latest; then
          echo "install_wails: falló la instalación del CLI de Wails" >&2
          return 1
      fi

      # Asegurar que $GOPATH/bin esté en PATH
      if [[ ":$PATH:" != *":$HOME/go/bin:"* ]]; then
          local shell_config=""
          if [[ -f "$HOME/.bashrc" ]]; then
              shell_config="$HOME/.bashrc"
          elif [[ -f "$HOME/.zshrc" ]]; then
              shell_config="$HOME/.zshrc"
          fi

          if [[ -n "$shell_config" ]]; then
              if ! grep -q 'export PATH=\$PATH:\$HOME/go/bin' "$shell_config" 2>/dev/null; then
                  {
                      echo ""
                      echo "# Go binaries"
                      echo 'export PATH=$PATH:$HOME/go/bin'
                  } >> "$shell_config"
                  echo "PATH de Go añadido a ${shell_config}"
              fi
          fi
          export PATH="$PATH:$HOME/go/bin"
      fi

      echo ""

      # Verificar instalación
      if command -v wails &>/dev/null; then
          local wails_version
          wails_version="$(wails version 2>/dev/null || echo "instalado")"
          echo "Wails instalado correctamente: ${wails_version}"
          echo ""
          echo "Comandos básicos de Wails:"
          echo "  wails init -n myapp -t vanilla  - Crear proyecto"
          echo "  wails dev                        - Modo desarrollo"
          echo "  wails build                      - Build producción"
          echo "  wails doctor                     - Verificar instalación"
      else
          echo "Wails instalado pero no está en PATH."
          echo "  Reinicia tu terminal o ejecuta: source ~/.bashrc"
      fi
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      install_wails "$@"
  fi

params_schema:
  {"params":[{"name":"(ninguno)","desc":"no acepta argumentos; detecta la distribución automáticamente"}],"output":"progreso a stdout; exit code 1 si Go no está disponible, no se detecta la distribución, o falla la instalación"}


id: list_listening_ports_bash_infra
name: list_listening_ports
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: list_listening_ports([mode: string]) -> void
tags: ["bash","ports","network","listening","monitoring"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/list_listening_ports.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 07a3347d47334885513dcb44568d4b5ab5c45b5a902c9ef621f6f6b5e65fe916
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/gestion_linux/puertos_activos.sh
description:
  Lista puertos activos del sistema usando ss (preferido) o netstat como fallback. Modos: all (LISTEN), tcp, udp, established (conexiones activas), stats (resumen + interfaces). Imprime salida tabulada a stdout.

example:
  ```bash
  source bash/functions/infra/list_listening_ports.sh

  # Todos los puertos en escucha
  list_listening_ports

  # Solo TCP
  list_listening_ports tcp

  # Conexiones establecidas
  list_listening_ports established

  # Estadísticas e interfaces
  list_listening_ports stats
  ```

notes:
  Prefiere `ss` (iproute2) sobre `netstat` (net-tools). El modo `established` limita a 30 filas para no saturar el terminal. No incluye monitor en tiempo real (solo snapshot).

code:
  #!/usr/bin/env bash
  # list_listening_ports
  # --------------------
  # Lista puertos activos en el sistema usando ss o netstat.
  # Soporta filtrado por protocolo y estadísticas de red.
  #
  # USO:
  #   source list_listening_ports.sh
  #   list_listening_ports [mode]
  #
  # ARGUMENTOS:
  #   mode   Modo de listado: all|tcp|udp|established|stats (default: all)

  list_listening_ports() {
      local mode="${1:-all}"

      _has_ss() {
          command -v ss &>/dev/null
      }

      _has_netstat() {
          command -v netstat &>/dev/null
      }

      _require_net_tool() {
          if ! _has_ss && ! _has_netstat; then
              echo "list_listening_ports: no se encontró ss ni netstat en el sistema" >&2
              return 1
          fi
      }

      _llp_all() {
          echo "=== Puertos en escucha (LISTEN) ==="
          if _has_ss; then
              ss -tulnp 2>/dev/null | awk 'NR==1 || /LISTEN/ {print}' | column -t
          elif _has_netstat; then
              netstat -tulnp 2>/dev/null | awk 'NR<=2 || /LISTEN/ {print}' | column -t
          fi
          echo ""
      }

      _llp_tcp() {
          echo "=== Puertos TCP ==="
          if _has_ss; then
              ss -tnlp 2>/dev/null | column -t
          elif _has_netstat; then
              netstat -tnlp 2>/dev/null | column -t
          fi
          echo ""
      }

      _llp_udp() {
          echo "=== Puertos UDP ==="
          if _has_ss; then
              ss -unlp 2>/dev/null | column -t
          elif _has_netstat; then
              netstat -unlp 2>/dev/null | column -t
          fi
          echo ""
      }

      _llp_established() {
          echo "=== Conexiones TCP establecidas ==="
          local count=0
          if _has_ss; then
              ss -tnp 2>/dev/null | awk 'NR==1 || /ESTAB/ {print}' | column -t | head -30
              count="$(ss -tnp 2>/dev/null | grep -c ESTAB || echo 0)"
          elif _has_netstat; then
              netstat -tnp 2>/dev/null | awk 'NR<=2 || /ESTABLISHED/ {print}' | column -t | head -30
              count="$(netstat -tnp 2>/dev/null | grep -c ESTABLISHED || echo 0)"
          fi
          echo ""
          echo "Total de conexiones establecidas: ${count}"
          echo ""
      }

      _llp_stats() {
          echo "=== Estadísticas de red ==="
          if _has_ss; then
              ss -s 2>/dev/null
              echo ""
              echo "=== Interfaces de red ==="
              ip -br addr 2>/dev/null || ifconfig -a 2>/dev/null || echo "No se pudo obtener info de interfaces"
          elif _has_netstat; then
              netstat -s 2>/dev/null | head -50
          fi
          echo ""
      }

      _require_net_tool || return 1

      case "$mode" in
          all)         _llp_all ;;
          tcp)         _llp_tcp ;;
          udp)         _llp_udp ;;
          established) _llp_established ;;
          stats)       _llp_stats ;;
          *)
              echo "list_listening_ports: modo desconocido '${mode}'. Usa: all|tcp|udp|established|stats" >&2
              return 1
              ;;
      esac
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      list_listening_ports "$@"
  fi

params_schema:
  {"params":[{"name":"mode","desc":"qué listar: all|tcp|udp|established|stats (default: all)"}],"output":"tabla de puertos/conexiones a stdout; exit code 1 si no hay ss ni netstat, o si el modo es desconocido"}


id: nordvpn_connect_bash_infra
name: nordvpn_connect
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_connect(country?: string, city?: string) -> json
tags: ["vpn","nordvpn","connect","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_connect.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ccdb98fc2ce2c69f41b1dcaaaac86d601b147ace796f93091c8bba9d47dd93e4
description:
  Conecta a NordVPN por pais, ciudad o servidor especifico. Sin argumentos conecta al mejor servidor disponible. Devuelve JSON con resultado.

example:
  ```bash
  source nordvpn_connect.sh

  nordvpn_connect
  # {"ok":true,"server":"us1234.nordvpn.com","country":"auto","city":"auto"}

  nordvpn_connect Spain
  # {"ok":true,"server":"es42.nordvpn.com","country":"Spain","city":"auto"}

  nordvpn_connect Spain Madrid
  # {"ok":true,"server":"es15.nordvpn.com","country":"Spain","city":"Madrid"}
  ```

notes:
  Requiere NordVPN CLI instalado y autenticado (`nordvpn login`). La salida JSON facilita composicion con otros scripts y pipelines. Si ya hay una conexion activa, NordVPN reconecta automaticamente al nuevo destino.

code:
  # nordvpn_connect
  # ---------------
  # Conecta a NordVPN. Acepta pais, ciudad o servidor especifico.
  # Sin argumentos conecta al mejor servidor disponible.
  # Imprime JSON con el resultado de la conexion.
  #
  # USO (sourced):
  #   source nordvpn_connect.sh
  #   nordvpn_connect                    # mejor servidor
  #   nordvpn_connect Spain              # por pais
  #   nordvpn_connect Spain Madrid       # por ciudad
  #   nordvpn_connect Spain '#42'        # servidor especifico

  nordvpn_connect() {
      local country="${1:-}"
      local city="${2:-}"

      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local args=()
      [ -n "$country" ] && args+=("$country")
      [ -n "$city" ] && args+=("$city")

      local output
      output=$(nordvpn connect "${args[@]}" 2>&1)
      local rc=$?

      if [ $rc -eq 0 ] && echo "$output" | grep -qi "connected"; then
          local server
          server=$(echo "$output" | grep -oP '(?<=to )\S+' | head -1)
          echo "{\"ok\":true,\"server\":\"${server}\",\"country\":\"${country:-auto}\",\"city\":\"${city:-auto}\"}"
      else
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"country","desc":"país de destino (opcional; default: auto)"},{"name":"city","desc":"ciudad de destino (opcional; default: auto)"}],"output":"JSON con ok, server, country, city"}


id: nordvpn_disconnect_bash_infra
name: nordvpn_disconnect
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_disconnect() -> json
tags: ["vpn","nordvpn","disconnect","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source nordvpn_disconnect.sh

  nordvpn_disconnect
  # {"ok":true,"status":"disconnected"}
  ```

tests: []
file_path: bash/functions/infra/nordvpn_disconnect.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Idempotente: si no hay conexion activa, retorna ok sin error. Requiere NordVPN CLI instalado.
content_hash: ba29b2977ffedf5c85c1264d693348eb21aeefa38636ff85c815afb7e5acafc8
params_schema: {"output":"JSON con ok y status"}
description:
  Desconecta de NordVPN. Idempotente — si no hay conexion activa retorna ok. Devuelve JSON con resultado.

code:
  # nordvpn_disconnect
  # ------------------
  # Desconecta de NordVPN. Idempotente — si no hay conexion activa, retorna ok.
  # Imprime JSON con el resultado.
  #
  # USO (sourced):
  #   source nordvpn_disconnect.sh
  #   nordvpn_disconnect

  nordvpn_disconnect() {
      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn disconnect 2>&1)
      local rc=$?

      if [ $rc -eq 0 ] || echo "$output" | grep -qi "not connected\|disconnected"; then
          echo '{"ok":true,"status":"disconnected"}'
      else
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi
  }


id: nordvpn_get_ip_bash_infra
name: nordvpn_get_ip
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_get_ip() -> json
tags: ["vpn","nordvpn","ip","infra","network","verification"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_get_ip.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f056919ea70f5d64d7dee9f505ba5657dd47102db33419cb12c8b1c8d9bec4ea
params_schema: {"output":"JSON con ok, ip, vpn_connected, vpn_server, source"}
description:
  Obtiene IP publica actual con fallback entre multiples servicios. Indica si la conexion VPN esta activa y el servidor usado.

example:
  ```bash
  source nordvpn_get_ip.sh

  # Con VPN activa:
  nordvpn_get_ip
  # {"ok":true,"ip":"185.x.x.x","vpn_connected":true,"vpn_server":"es42.nordvpn.com","source":"https://api.ipify.org"}

  # Sin VPN:
  nordvpn_get_ip
  # {"ok":true,"ip":"88.x.x.x","vpn_connected":false,"vpn_server":"","source":"https://api.ipify.org"}
  ```

notes:
  Usa ipify.org como servicio primario con fallback a ifconfig.me e icanhazip.com. Timeout de 5 segundos por servicio. Util para verificar que el tunel VPN esta activo antes de ejecutar operaciones sensibles a la IP.

code:
  # nordvpn_get_ip
  # --------------
  # Obtiene la IP publica actual para verificar que el tunel VPN funciona.
  # Usa multiples servicios como fallback.
  #
  # USO (sourced):
  #   source nordvpn_get_ip.sh
  #   nordvpn_get_ip

  nordvpn_get_ip() {
      local ip=""
      local source=""

      # Intentar multiples servicios
      for svc in "https://api.ipify.org" "https://ifconfig.me" "https://icanhazip.com"; do
          ip=$(curl -s --max-time 5 "$svc" 2>/dev/null)
          if echo "$ip" | grep -qP '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'; then
              source="$svc"
              break
          fi
          ip=""
      done

      if [ -z "$ip" ]; then
          echo '{"ok":false,"error":"no se pudo obtener IP publica"}' >&2
          return 1
      fi

      # Si nordvpn esta disponible, incluir info de conexion
      local connected="false"
      local vpn_server=""
      if command -v nordvpn &>/dev/null; then
          local status_output
          status_output=$(nordvpn status 2>/dev/null)
          if echo "$status_output" | grep -qi "connected"; then
              connected="true"
              vpn_server=$(echo "$status_output" | grep -iP "hostname|server" | head -1 | sed 's/.*: *//')
          fi
      fi

      echo "{\"ok\":true,\"ip\":\"$ip\",\"vpn_connected\":$connected,\"vpn_server\":\"$vpn_server\",\"source\":\"$source\"}"
  }


id: nordvpn_list_cities_bash_infra
name: nordvpn_list_cities
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_list_cities(country: string) -> json
description: Lista ciudades disponibles de un pais en NordVPN como array JSON ordenado.
tags: ["vpn","nordvpn","cities","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_list_cities.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ade8b580824fc744cac0e9636f1614e24f323d9d41390564f47e53a0538bbf9a
example:
  ```bash
  source nordvpn_list_cities.sh

  nordvpn_list_cities Spain
  # {"ok":true,"country":"Spain","count":2,"cities":["Barcelona","Madrid"]}

  nordvpn_list_cities "United_States"
  # {"ok":true,"country":"United_States","count":15,"cities":["Atlanta","Buffalo",...]}
  ```

notes:
  El nombre de pais debe coincidir con lo que devuelve `nordvpn countries`. Usa underscores para paises compuestos (ej: United_States). Las ciudades se devuelven con espacios.

code:
  # nordvpn_list_cities
  # -------------------
  # Lista las ciudades disponibles de un pais en NordVPN como array JSON.
  #
  # USO (sourced):
  #   source nordvpn_list_cities.sh
  #   nordvpn_list_cities Spain

  nordvpn_list_cities() {
      local country="${1:?nordvpn_list_cities: se requiere pais como argumento}"

      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn cities "$country" 2>&1)
      local rc=$?

      if [ $rc -ne 0 ]; then
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi

      echo "$output" | python3 -c '
  import sys, json, re

  country = "'"$country"'"
  text = sys.stdin.read()
  text = re.sub(r"\x1b\[[0-9;]*m", "", text)
  text = re.sub(r"[\t\r]", " ", text)
  cities = [c.strip().replace("_", " ") for c in re.split(r"[,\n]+", text) if c.strip() and c.strip() != "-"]
  cities = [c for c in cities if len(c) > 1]
  cities.sort()
  print(json.dumps({"ok": True, "country": country, "count": len(cities), "cities": cities}))
  '
  }

params_schema:
  {"params":[{"name":"country","desc":"nombre del país en NordVPN (ej: Spain, United_States)"}],"output":"JSON con ok, country, count, cities (array de strings)"}


id: nordvpn_list_countries_bash_infra
name: nordvpn_list_countries
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_list_countries() -> json
description: Lista paises disponibles en NordVPN como array JSON ordenado alfabeticamente.
tags: ["vpn","nordvpn","countries","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_list_countries.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b8e9dae18e9fc5505238584cdeb997cdebe47eb15e130254bc667c1f035525df
params_schema: {"output":"JSON con ok, count, countries (array de strings ordenado alfabéticamente)"}
example:
  ```bash
  source nordvpn_list_countries.sh

  nordvpn_list_countries
  # {"ok":true,"count":60,"countries":["Albania","Argentina","Australia",...,"United States","Vietnam"]}
  ```

notes:
  Parsea la salida de `nordvpn countries` eliminando codigos ANSI y normalizando separadores. Los nombres de paises se devuelven con espacios en vez de underscores.

code:
  # nordvpn_list_countries
  # ----------------------
  # Lista los paises disponibles en NordVPN como array JSON.
  #
  # USO (sourced):
  #   source nordvpn_list_countries.sh
  #   nordvpn_list_countries

  nordvpn_list_countries() {
      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn countries 2>&1)
      local rc=$?

      if [ $rc -ne 0 ]; then
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi

      echo "$output" | python3 -c '
  import sys, json, re

  text = sys.stdin.read()
  text = re.sub(r"\x1b\[[0-9;]*m", "", text)
  text = re.sub(r"[\t\r]", " ", text)
  # Split by comma, whitespace, or newline and clean
  countries = [c.strip().replace("_", " ") for c in re.split(r"[,\n]+", text) if c.strip() and c.strip() != "-"]
  countries = [c for c in countries if len(c) > 1]
  countries.sort()
  print(json.dumps({"ok": True, "count": len(countries), "countries": countries}))
  '
  }


id: nordvpn_set_protocol_bash_infra
name: nordvpn_set_protocol
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_set_protocol(protocol: string) -> json
tags: ["vpn","nordvpn","protocol","nordlynx","wireguard","openvpn","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_set_protocol.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 41343a8927e771c3b6b46241a2b813a900b05f20c821373de8faa403ad5019f0
description:
  Cambia el protocolo de NordVPN entre NordLynx (WireGuard) y OpenVPN. NordLynx recomendado por velocidad.

example:
  ```bash
  source nordvpn_set_protocol.sh

  nordvpn_set_protocol NordLynx
  # {"ok":true,"protocol":"NordLynx"}

  nordvpn_set_protocol OpenVPN
  # {"ok":true,"protocol":"OpenVPN"}
  ```

notes:
  NordLynx es WireGuard wrapeado por NordVPN — mas rapido y moderno. OpenVPN es mas compatible con redes restrictivas. El cambio de protocolo requiere reconectar si hay una conexion activa.

code:
  # nordvpn_set_protocol
  # --------------------
  # Cambia el protocolo de NordVPN (NordLynx o OpenVPN).
  # NordLynx = WireGuard (recomendado por velocidad).
  #
  # USO (sourced):
  #   source nordvpn_set_protocol.sh
  #   nordvpn_set_protocol NordLynx
  #   nordvpn_set_protocol OpenVPN

  nordvpn_set_protocol() {
      local protocol="${1:?nordvpn_set_protocol: se requiere protocolo (NordLynx|OpenVPN)}"

      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      case "$protocol" in
          NordLynx|nordlynx|NORDLYNX) protocol="NordLynx" ;;
          OpenVPN|openvpn|OPENVPN)     protocol="OpenVPN" ;;
          *)
              echo "{\"ok\":false,\"error\":\"protocolo invalido: $protocol (usar NordLynx o OpenVPN)\"}"
              return 1
              ;;
      esac

      local output
      output=$(nordvpn set protocol "$protocol" 2>&1)
      local rc=$?

      if [ $rc -eq 0 ] || echo "$output" | grep -qi "already set\|successfully"; then
          echo "{\"ok\":true,\"protocol\":\"$protocol\"}"
      else
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"protocol","desc":"protocolo a usar: NordLynx (WireGuard) u OpenVPN"}],"output":"JSON con ok y protocol confirmado"}


id: nordvpn_status_bash_infra
name: nordvpn_status
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_status() -> json
tags: ["vpn","nordvpn","status","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_status.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 75dd5ab1ac03ac7fed4abb7dfa5edb55b723e2b702306ba4c178023b3041eb5d
params_schema: {"output":"JSON con estado de VPN: ok, connected, status, hostname, ip, country, city, etc"}
description:
  Obtiene estado actual de NordVPN como JSON estructurado. Incluye servidor, IP, pais, protocolo y estado de conexion.

example:
  ```bash
  source nordvpn_status.sh

  nordvpn_status
  # {"ok":true,"connected":true,"status":"Connected","hostname":"es42.nordvpn.com","ip":"185.x.x.x","country":"Spain","city":"Madrid","current_technology":"NordLynx","current_protocol":"nordlynx","transfer":"1.2 MiB received, 500 KiB sent","uptime":"5 minutes 32 seconds"}

  # Desconectado:
  # {"ok":true,"connected":false,"status":"Disconnected"}
  ```

notes:
  Parsea la salida clave-valor de `nordvpn status` eliminando codigos ANSI. Los campos disponibles dependen del estado de conexion — cuando esta desconectado solo devuelve status y connected.

code:
  # nordvpn_status
  # --------------
  # Obtiene el estado actual de NordVPN como JSON estructurado.
  # Parsea la salida clave-valor de `nordvpn status` a campos JSON.
  #
  # USO (sourced):
  #   source nordvpn_status.sh
  #   nordvpn_status

  nordvpn_status() {
      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn status 2>&1)
      local rc=$?

      if [ $rc -ne 0 ]; then
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi

      # Parsear output clave: valor a JSON con python3
      echo "$output" | python3 -c '
  import sys, json, re

  lines = sys.stdin.read().strip().split("\n")
  data = {"ok": True}
  for line in lines:
      line = re.sub(r"\x1b\[[0-9;]*m", "", line).strip()
      line = line.lstrip("- ")
      if ":" in line:
          key, _, val = line.partition(":")
          key = key.strip().lower().replace(" ", "_")
          val = val.strip()
          if key == "status":
              data["connected"] = val.lower() == "connected"
          data[key] = val
  print(json.dumps(data))
  '
  }


id: notebook_to_pdf_bash_infra
name: notebook_to_pdf
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: notebook_to_pdf(project_dir: string, [pattern: string], [output_dir: string]) -> string
tags: ["jupyter","notebook","pdf","export","nbconvert","playwright"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/notebook_to_pdf.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 25a8d0d0e83b68ff24096b322ad4d3f3d39ac3a5ace80116deae60bb1e9f4965
description:
  Convierte notebooks Jupyter a PDF usando nbconvert webpdf con chromium. Lista los PDFs generados al finalizar.

example:
  ```bash
  source notebook_to_pdf.sh

  # Con defaults (notebooks/*.ipynb -> notebooks/pdf/)
  notebook_to_pdf /home/lucas/analysis/finanzas

  # Con pattern y output_dir custom
  notebook_to_pdf /home/lucas/analysis/finanzas "notebooks/01_*.ipynb" "exports/pdf/"
  ```

notes:
  Requiere nbconvert y playwright con chromium instalados (usa `install_nbconvert` antes). Usa el venv del proyecto directamente (`.venv/bin/jupyter`). El output_dir es relativo a project_dir. Imprime los PDFs generados con sus rutas al finalizar. Falla si no se genera ningun PDF.

code:
  # notebook_to_pdf
  # ----------------
  # Convierte notebooks Jupyter a PDF usando nbconvert webpdf.
  # Requiere nbconvert y playwright con chromium instalados.
  #
  # USO (sourced):
  #   source notebook_to_pdf.sh
  #   notebook_to_pdf /path/to/project
  #   notebook_to_pdf /path/to/project "notebooks/*.ipynb" "notebooks/pdf/"

  notebook_to_pdf() {
      local project_dir="$1"
      local pattern="${2:-notebooks/*.ipynb}"
      local output_dir="${3:-notebooks/pdf/}"

      if [ -z "$project_dir" ]; then
          echo "notebook_to_pdf: se requiere project_dir" >&2
          return 1
      fi

      if [ ! -d "$project_dir/.venv" ]; then
          echo "notebook_to_pdf: no existe .venv en $project_dir" >&2
          return 1
      fi

      # Crear directorio de salida si no existe
      mkdir -p "$project_dir/$output_dir"

      # Convertir notebooks a PDF con nbconvert webpdf
      # nbconvert puede retornar exit != 0 por warnings de validacion JSON
      # que no impiden la generacion del PDF, asi que ignoramos el exit code
      # y verificamos que los PDFs se hayan generado
      local nbconvert_output
      nbconvert_output=$(cd "$project_dir" && \
          .venv/bin/jupyter nbconvert \
              --to webpdf \
              --allow-chromium-download \
              --output-dir="$output_dir" \
              $pattern 2>&1) || true

      echo "$nbconvert_output"

      # Listar PDFs generados
      echo ""
      echo "PDFs generados en ${project_dir}/${output_dir}:"
      local pdf_count=0
      while IFS= read -r -d '' pdf; do
          echo "  $pdf"
          pdf_count=$((pdf_count + 1))
      done < <(find "$project_dir/$output_dir" -name "*.pdf" -print0 2>/dev/null)

      if [ "$pdf_count" -eq 0 ]; then
          echo "  (ninguno encontrado — nbconvert pudo haber fallado)" >&2
          echo "$nbconvert_output" >&2
          return 1
      fi

      echo "  Total: $pdf_count PDFs"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio raíz del proyecto con venv y notebooks"},{"name":"pattern","desc":"glob de notebooks a convertir (default: notebooks/*.ipynb)"},{"name":"output_dir","desc":"directorio destino para PDFs relativo a project_dir (default: notebooks/pdf/)"}],"output":"lista de PDFs generados con sus rutas"}


id: pass_delete_bash_infra
name: pass_delete
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_delete(entry: string) -> void
description: Elimina un secreto del password store (pass).
tags: ["pass","secret","credential","delete"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source pass_delete.sh
  pass_delete agentes/viejo-token
  ```

tests: ["elimina entrada de test","falla con entrada inexistente"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_delete.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Usa `pass rm -f` para eliminar sin prompt de confirmacion.
content_hash: a8befe189a97a3c3f9c5b6284412886f11a77ec52acdaf51c52a181b72dd02c9
code:
  # pass_delete
  # -----------
  # Elimina un secreto del password store.
  # Sale con exit code 1 si la entrada no existe o pass falla.
  #
  # USO (sourced):
  #   source pass_delete.sh
  #   pass_delete agentes/viejo-token

  pass_delete() {
      local entry="$1"

      if [ -z "$entry" ]; then
          echo "pass_delete: se requiere nombre de entrada" >&2
          return 1
      fi

      if ! pass rm -f "$entry" >/dev/null 2>&1; then
          echo "pass_delete: fallo al eliminar '$entry'" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store (ej: agentes/token)"}],"output":"sin salida"}


id: pass_generate_bash_infra
name: pass_generate
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_generate(entry: string, [length: int]) -> string
description: Genera un password aleatorio, lo almacena en el password store e imprime el valor generado.
tags: ["pass","secret","credential","generate","random"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["genera password de longitud especifica","default 24 chars"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_generate.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Usa `pass generate -f -n` (force overwrite, no symbols). Default 24 caracteres alfanumericos.
content_hash: 0b51c948b911254b1cf275df02eed3c1e1a5fb990a7accc8e312e592484c352f
example:
  ```bash
  source pass_generate.sh
  new_pass=$(pass_generate agentes/nuevo-servicio 32)
  echo "password generado: $new_pass"
  ```

code:
  # pass_generate
  # -------------
  # Genera un password aleatorio y lo almacena en el password store.
  # Imprime el password generado a stdout.
  # Sale con exit code 1 si pass falla.
  #
  # USO (sourced):
  #   source pass_generate.sh
  #   pass_generate agentes/nuevo-token 32
  #   pass_generate agentes/api-key         # default 24 chars

  pass_generate() {
      local entry="$1"
      local length="${2:-24}"

      if [ -z "$entry" ]; then
          echo "pass_generate: se requiere nombre de entrada" >&2
          return 1
      fi

      local output
      output=$(pass generate -f -n "$entry" "$length" 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_generate: fallo al generar '$entry': $output" >&2
          return 1
      fi

      # pass generate imprime ANSI escape codes + header + password
      # Extraer ultima linea y limpiar escape codes
      echo "$output" | tail -1 | sed 's/\x1b\[[0-9;]*m//g'
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store"},{"name":"length","desc":"longitud del password (default: 24 caracteres)"}],"output":"password generado en texto plano"}


id: pass_get_bash_infra
name: pass_get
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_get(entry: string) -> string
description: Lee un secreto del password store (pass) y lo imprime a stdout.
tags: ["pass","secret","credential","get"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["lee entrada existente","falla con entrada inexistente"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_get.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7845238d9c7859d3768a00d30f5664eefbbbcd27d8c25954d19f3f419a0d0dd3
example:
  ```bash
  source pass_get.sh
  token=$(pass_get agentes/dataforge-token)
  export GITEA_TOKEN="$token"
  ```

notes:
  Usa `pass show` internamente. Requiere GPG key desbloqueada. No imprime newline final (usa printf %s).

code:
  # pass_get
  # --------
  # Lee un secreto del password store y lo imprime a stdout.
  # Sale con exit code 1 si la entrada no existe o pass falla.
  #
  # USO (sourced):
  #   source pass_get.sh
  #   token=$(pass_get agentes/dataforge-token)

  pass_get() {
      local entry="$1"

      if [ -z "$entry" ]; then
          echo "pass_get: se requiere nombre de entrada" >&2
          return 1
      fi

      local value
      value=$(pass show "$entry" 2>/dev/null)
      if [ $? -ne 0 ]; then
          echo "pass_get: no se pudo leer '$entry'" >&2
          return 1
      fi

      printf '%s' "$value"
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store"}],"output":"valor del secreto en texto plano"}


id: pass_list_bash_infra
name: pass_list
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_list([prefix: string]) -> json
description: Lista entradas del password store como JSON array. Filtra opcionalmente por prefijo.
tags: ["pass","secret","credential","list"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["lista todas las entradas","filtra por prefijo"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_list.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 003a2c5146c97ca46821d37d0ec40037e042c531b420ba0956dbbc3ef2b51572
example:
  ```bash
  source pass_list.sh
  entries=$(pass_list agentes)
  # ["dataforge-token","egutierrez-token","gitea-url"]
  ```

notes:
  Parsea el output tree de `pass ls` y lo convierte a JSON array. Cada entrada es un string con el nombre relativo al prefijo.

code:
  # pass_list
  # ---------
  # Lista entradas del password store como JSON array.
  # Opcionalmente filtra por prefijo.
  # Sale con exit code 1 si pass falla.
  #
  # USO (sourced):
  #   source pass_list.sh
  #   pass_list                  # todas las entradas
  #   pass_list agentes          # solo bajo agentes/

  pass_list() {
      local prefix="${1:-}"

      local raw
      raw=$(pass ls "$prefix" 2>/dev/null)
      if [ $? -ne 0 ]; then
          echo "pass_list: fallo al listar entradas" >&2
          return 1
      fi

      # Parsear output de pass: extraer nombres limpios (sin tree chars)
      local entries
      entries=$(echo "$raw" | sed 's/[│├└──── ]//g' | sed '/^$/d' | grep -v '^Password' | grep -v '^[[:space:]]*$')

      # Construir JSON array
      printf '['
      local first=true
      while IFS= read -r line; do
          line=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
          [ -z "$line" ] && continue
          if [ "$first" = true ]; then
              first=false
          else
              printf ','
          fi
          printf '"%s"' "$line"
      done <<< "$entries"
      printf ']'
  }

params_schema:
  {"params":[{"name":"prefix","desc":"prefijo para filtrar entradas (opcional; ej: agentes)"}],"output":"JSON array de nombres de entradas"}


id: pass_set_bash_infra
name: pass_set
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_set(entry: string, [value: string]) -> void
description: Inserta o sobreescribe un secreto en el password store (pass).
tags: ["pass","secret","credential","set","insert"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source pass_set.sh
  pass_set agentes/nuevo-servicio "token-abc123"
  ```

tests: ["inserta valor y lo lee de vuelta","sobreescribe valor existente"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_set.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 8b8b3f80d17a9f3f4ac33e2b1080c1214e6b719aafe6cda8dba1a6536fbbcecc
notes:
  Usa `pass insert -m -f` para forzar sobreescritura sin prompt interactivo. Si no se pasa valor como argumento, lee de stdin.

code:
  # pass_set
  # --------
  # Inserta o sobreescribe un secreto en el password store.
  # Lee el valor de stdin si no se pasa como segundo argumento.
  # Sale con exit code 1 si pass falla.
  #
  # USO (sourced):
  #   source pass_set.sh
  #   pass_set agentes/nuevo-token "mi-token-secreto"
  #   echo "mi-token" | pass_set agentes/nuevo-token

  pass_set() {
      local entry="$1"
      local value="$2"

      if [ -z "$entry" ]; then
          echo "pass_set: se requiere nombre de entrada" >&2
          return 1
      fi

      if [ -n "$value" ]; then
          printf '%s' "$value" | pass insert -m -f "$entry" >/dev/null 2>&1
      else
          pass insert -m -f "$entry" >/dev/null 2>&1
      fi

      if [ $? -ne 0 ]; then
          echo "pass_set: fallo al escribir '$entry'" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store"},{"name":"value","desc":"valor del secreto (opcional; se lee de stdin si no se proporciona)"}],"output":"sin salida"}


id: pass_sync_bash_infra
name: pass_sync
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_sync() -> json
description: Sincroniza el password store con el repositorio git remoto (pull + push).
tags: ["pass","secret","sync","git"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["sincroniza con remoto"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_sync.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c1a5fdca7d75b68514f4460c1a2eb61137465659facb4db1dcd14969e94bf793
params_schema: {"output":"JSON con resultados de pull y push"}
example:
  ```bash
  source pass_sync.sh
  result=$(pass_sync)
  # {"pull":"Already up to date.","push":"Everything up-to-date"}
  ```

notes:
  Ejecuta `pass git pull` seguido de `pass git push`. Requiere que el password store tenga un remote git configurado. Retorna JSON con la ultima linea de cada operacion.

code:
  # pass_sync
  # ---------
  # Sincroniza el password store con el repositorio git remoto (pull + push).
  # Sale con exit code 1 si la sincronizacion falla.
  #
  # USO (sourced):
  #   source pass_sync.sh
  #   pass_sync

  pass_sync() {
      local pull_out
      pull_out=$(pass git pull 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_sync: fallo en git pull: $pull_out" >&2
          return 1
      fi

      local push_out
      push_out=$(pass git push 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_sync: fallo en git push: $push_out" >&2
          return 1
      fi

      printf '{"pull":"%s","push":"%s"}' \
          "$(echo "$pull_out" | tail -1 | sed 's/"/\\"/g')" \
          "$(echo "$push_out" | tail -1 | sed 's/"/\\"/g')"
  }


id: rsync_deploy_bash_infra
name: rsync_deploy
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: rsync_deploy(local_dir: string, ssh_alias: string, remote_dir: string) -> json
tags: ["rsync","deploy","sync","ssh","remote","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/rsync_deploy.sh
created_at: 2026-04-12T12:48:21Z
updated_at: 2026-04-12T12:48:21Z
props: []
emits: []
variant: []
content_hash: 31262b8381016bf94bc338c93dca76d1359b0f364519aba3b32dd8b2fe109404
description:
  Sincroniza un directorio local a un host remoto via rsync+SSH. Excluye archivos de desarrollo y bases de datos locales. Crea el directorio remoto si no existe.

example:
  ```bash
  source bash/functions/infra/rsync_deploy.sh

  # Deploy de una app al servidor de producción
  result=$(rsync_deploy "apps/dag_engine/" "prod-server" "/opt/apps/dag_engine")
  echo "$result"
  # {"files_transferred": 12, "total_size": "1.23 MB", "ssh_alias": "prod-server", "remote_dir": "/opt/apps/dag_engine"}

  # Deploy con ruta absoluta local
  rsync_deploy "/home/lucas/fn_registry/apps/myapp/" "myserver" "/opt/myapp"
  ```

notes:
  - Usa `rsync -avz --delete`: archivos borrados localmente se borran también en el remoto.
  - Antes del rsync crea el directorio remoto con `ssh mkdir -p` para evitar errores si no existe.
  - Archivos excluidos: `.git`, `operations.db*`, `*.exe`, `node_modules`, `.venv`, `__pycache__`, `build/`, `*.db-shm`, `*.db-wal`.
  - El JSON de salida va a stdout; los mensajes de progreso y errores van a stderr.
  - Exit code 1 si rsync falla o si el directorio local no existe.
  - El `ssh_alias` se resuelve con la configuración de `~/.ssh/config`, incluyendo host, user, identityfile y puerto.

code:
  #!/usr/bin/env bash
  # rsync_deploy — Sincroniza un directorio local a un host remoto via rsync+SSH
  set -euo pipefail

  rsync_deploy() {
      local local_dir="$1"
      local ssh_alias="$2"
      local remote_dir="$3"

      if [[ -z "$local_dir" || -z "$ssh_alias" || -z "$remote_dir" ]]; then
          echo "rsync_deploy: se requieren local_dir, ssh_alias y remote_dir" >&2
          return 1
      fi

      if [[ ! -d "$local_dir" ]]; then
          echo "rsync_deploy: directorio local '$local_dir' no existe" >&2
          return 1
      fi

      # Crear directorio remoto si no existe
      echo "rsync_deploy: verificando directorio remoto '$remote_dir' en '$ssh_alias'..." >&2
      if ! ssh "$ssh_alias" "mkdir -p '$remote_dir'" 2>&1; then
          echo "rsync_deploy: no se pudo crear el directorio remoto '$remote_dir' en '$ssh_alias'" >&2
          return 1
      fi

      # Ejecutar rsync y capturar salida para parsear estadísticas
      local rsync_output
      rsync_output=$(rsync -avz --delete \
          --exclude='.git' \
          --exclude='operations.db*' \
          --exclude='*.exe' \
          --exclude='node_modules' \
          --exclude='.venv' \
          --exclude='__pycache__' \
          --exclude='build/' \
          --exclude='*.db-shm' \
          --exclude='*.db-wal' \
          -e ssh \
          "$local_dir" \
          "${ssh_alias}:${remote_dir}" 2>&1) || {
          echo "rsync_deploy: rsync falló al sincronizar '$local_dir' → '${ssh_alias}:${remote_dir}'" >&2
          echo "$rsync_output" >&2
          return 1
      }

      echo "$rsync_output" >&2

      # Parsear número de archivos transferidos
      local files_transferred
      files_transferred=$(echo "$rsync_output" | grep -oP 'Number of regular files transferred: \K[0-9,]+' | tr -d ',' || echo "0")
      if [[ -z "$files_transferred" ]]; then
          # Intentar formato alternativo de rsync
          files_transferred=$(echo "$rsync_output" | grep -oP 'Number of files transferred: \K[0-9,]+' | tr -d ',' || echo "0")
      fi
      if [[ -z "$files_transferred" ]]; then
          files_transferred="0"
      fi

      # Parsear tamaño total transferido
      local total_size
      total_size=$(echo "$rsync_output" | grep -oP 'Total transferred file size: \K[0-9,.]+ \w+' || echo "0 bytes")
      if [[ -z "$total_size" ]]; then
          total_size="0 bytes"
      fi

      # Emitir JSON a stdout
      printf '{"files_transferred": %s, "total_size": "%s", "ssh_alias": "%s", "remote_dir": "%s"}\n' \
          "$files_transferred" \
          "$total_size" \
          "$ssh_alias" \
          "$remote_dir"
  }

  if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
      rsync_deploy "$@"
  fi

params_schema:
  {"params":[{"name":"local_dir","desc":"ruta al directorio local a sincronizar (ej: apps/dag_engine/)"},{"name":"ssh_alias","desc":"alias SSH del host destino definido en ~/.ssh/config (ej: myserver)"},{"name":"remote_dir","desc":"ruta absoluta del directorio destino en el host remoto (ej: /opt/apps/dag_engine)"}],"output":"JSON con files_transferred (int), total_size (string), ssh_alias (string), remote_dir (string)"}


id: uv_add_packages_bash_infra
name: uv_add_packages
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: uv_add_packages(project_dir: string, ...packages: string) -> void
tags: ["python","uv","pip","packages","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/uv_add_packages.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f807adf1f48d52b92091c7c273f766d781e94ee95bb84908634f583aa688547c
description:
  Instala paquetes Python en un proyecto usando uv add con fallback a pip. Inicializa pyproject.toml si no existe.

example:
  ```bash
  source uv_add_packages.sh
  uv_add_packages /home/lucas/analysis/finanzas jupyter jupyterlab pandas numpy

  # Solo un paquete
  uv_add_packages . polars
  ```

notes:
  Requiere que el venv ya exista (usa `init_uv_venv` antes). Prefiere uv por velocidad y reproducibilidad (lockfile). Si uv no esta disponible, usa pip del venv directamente.

code:
  # uv_add_packages
  # -----------------
  # Instala paquetes Python en un proyecto con uv add.
  # Inicializa pyproject.toml si no existe.
  # Fallback a pip install si uv no esta disponible.
  #
  # USO (sourced):
  #   source uv_add_packages.sh
  #   uv_add_packages /path/to/project jupyter jupyterlab pandas

  uv_add_packages() {
      local project_dir="$1"
      shift
      local packages=("$@")

      if [ ${#packages[@]} -eq 0 ]; then
          echo "uv_add_packages: se requiere al menos un paquete" >&2
          return 1
      fi

      if [ ! -d "$project_dir/.venv" ]; then
          echo "uv_add_packages: no existe .venv en $project_dir — ejecuta init_uv_venv primero" >&2
          return 1
      fi

      if command -v uv &>/dev/null; then
          # Inicializar pyproject.toml si no existe
          if [ ! -f "$project_dir/pyproject.toml" ]; then
              (cd "$project_dir" && uv init 2>/dev/null) || true
          fi
          (cd "$project_dir" && uv add "${packages[@]}" 2>&1)
      else
          "$project_dir/.venv/bin/pip" install "${packages[@]}" 2>&1
      fi
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto con venv existente"},{"name":"packages","desc":"nombres de paquetes Python a instalar (variadic)"}],"output":"sin salida"}


id: write_claude_jupyter_rules_bash_infra
name: write_claude_jupyter_rules
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_claude_jupyter_rules([project_dir: string]) -> string
tags: ["claude","jupyter","rules","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_claude_jupyter_rules.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 66e06d51635c07b708c2b274aeff6961ce58197db2ac3c66fa3816a0bbdb1030
description:
  Genera o actualiza .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter: celdas inmutables, programacion funcional, uso de MCP, acceso al fn_registry.

example:
  ```bash
  source write_claude_jupyter_rules.sh
  path=$(write_claude_jupyter_rules /home/lucas/analysis/finanzas)
  echo "Reglas escritas en: $path"
  ```

notes:
  Idempotente: si CLAUDE.md ya contiene las reglas (detecta "JUPYTER HABILITADO"), no las duplica. Si CLAUDE.md existe sin las reglas, las prepend al contenido existente. Incluye instrucciones de acceso al fn_registry via `FN_REGISTRY_ROOT`.

code:
  # write_claude_jupyter_rules
  # ----------------------------
  # Genera .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter.
  # Si ya existe CLAUDE.md y no tiene las reglas, las prepend.
  #
  # USO (sourced):
  #   source write_claude_jupyter_rules.sh
  #   write_claude_jupyter_rules /path/to/project

  write_claude_jupyter_rules() {
      local project_dir="${1:-.}"
      local claude_dir="${project_dir}/.claude"
      local claude_md="${claude_dir}/CLAUDE.md"

      mkdir -p "$claude_dir"

      # Si ya tiene las reglas, no hacer nada
      if [ -f "$claude_md" ] && grep -q "JUPYTER HABILITADO" "$claude_md" 2>/dev/null; then
          echo "$claude_md"
          return 0
      fi

      local rules
      rules='# JUPYTER HABILITADO EN ESTE ANALISIS

  ## Reglas OBLIGATORIAS para Claude

  ### 1. CODIGO INMUTABLE — NUNCA MODIFICAR CELDAS EXISTENTES
  - **PROHIBIDO** usar NotebookEdit para reemplazar celdas existentes
  - **SIEMPRE** anadir celdas NUEVAS al final del notebook
  - Si hay un error en una celda, crear celda nueva con la correccion
  - El historial de trabajo debe quedar intacto para trazabilidad

  ### 2. PROGRAMACION FUNCIONAL OBLIGATORIA
  - **Funciones puras**: sin efectos secundarios, mismo input -> mismo output
  - **Inmutabilidad**: nunca mutar datos, crear copias transformadas
  - **Composicion**: funciones pequenas que se combinan
  - Preferir: `map`, `filter`, `reduce`, list comprehensions
  - Evitar: loops con mutacion, `global`, modificar argumentos in-place

  ### 3. SIEMPRE usar MCP jupyter para ejecutar codigo Python
  - Las ejecuciones se ven en tiempo real en Jupyter Lab del usuario
  - Compartimos variables y estado del kernel
  - **NUNCA usar bash para ejecutar Python en este analisis**

  ### 4. Verificar Jupyter activo ANTES de ejecutar
  - Si no esta activo: pedir al usuario que ejecute `./run-jupyter-lab.sh`

  ### 5. Gestion de notebooks
  - Notebooks en la carpeta `notebooks/` o subcarpetas
  - Si un notebook tiene >50 celdas, crear uno nuevo
  - Nombrar descriptivamente: `01_exploracion.ipynb`, `02_limpieza.ipynb`

  ### 6. Gestion de Python
  - **SIEMPRE usar `uv`** para gestionar dependencias
  - Anadir paquetes con `uv add nombre_paquete`

  ### 7. Acceso al fn_registry
  - `FN_REGISTRY_ROOT` apunta a la raiz del registry
  - Para importar funciones Python: `sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))`
  - Para consultar registry.db: `sqlite3` o `import sqlite3` con la ruta `$FN_REGISTRY_ROOT/registry.db`

  '

      if [ -f "$claude_md" ]; then
          local existing
          existing=$(cat "$claude_md")
          printf '%s\n%s' "$rules" "$existing" > "$claude_md"
      else
          echo "$rules" > "$claude_md"
      fi

      echo "$claude_md"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del archivo .claude/CLAUDE.md generado o actualizado"}


id: write_jupyter_launcher_bash_infra
name: write_jupyter_launcher
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_jupyter_launcher([project_dir: string]) -> string
tags: ["jupyter","launcher","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_jupyter_launcher.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-12T18:00:53Z
props: []
emits: []
variant: []
content_hash: 6f6270fbeb7e5623bc9dd57cacc2aae00d622a63085464135d5e9b8daea97f22
description:
  Genera un script run-jupyter-lab.sh que lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto y token deshabilitado.

example:
  ```bash
  source write_jupyter_launcher.sh
  path=$(write_jupyter_launcher /home/lucas/analysis/finanzas)
  echo "Launcher creado en: $path"

  # Luego en otra terminal:
  # ./run-jupyter-lab.sh [puerto]
  ```

notes:
  El launcher generado:
  - Autodetecta un puerto libre (8888-8899)
  - Guarda el puerto en `.jupyter-port` para que otros procesos lo lean
  - Activa el venv automaticamente
  - Lanza Jupyter Lab en modo `--collaborative` (requiere jupyter-collaboration)
  - Token y password deshabilitados para acceso local

code:
  # write_jupyter_launcher
  # -----------------------
  # Genera un script run-jupyter-lab.sh en el directorio dado.
  # El script lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto.
  #
  # USO (sourced):
  #   source write_jupyter_launcher.sh
  #   write_jupyter_launcher /path/to/project

  write_jupyter_launcher() {
      local project_dir="${1:-.}"
      local launcher="${project_dir}/run-jupyter-lab.sh"

      cat > "$launcher" << 'LAUNCHER'
  #!/bin/bash
  # Jupyter Lab — modo colaborativo con autodeteccion de puerto
  # Generado por write_jupyter_launcher (fn_registry)

  find_free_port() {
      for port in 8888 8889 8890 8891 8892 8893 8894 8895 8896 8897 8898 8899; do
          if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
             ! lsof -i:"$port" >/dev/null 2>&1; then
              echo $port
              return
          fi
      done
      echo 8888
  }

  PORT=${1:-$(find_free_port)}
  cd "$(dirname "$0")"

  echo $PORT > .jupyter-port

  source .venv/bin/activate 2>/dev/null || true

  # IPython startup: cargar .ipython/ local (FN_REGISTRY_ROOT, helpers, sys.path)
  if [ -d "$(pwd)/.ipython" ]; then
      export IPYTHONDIR="$(pwd)/.ipython"
  fi

  if ! python -c "import jupyter_collaboration" 2>/dev/null; then
      echo "ERROR: jupyter-collaboration no esta instalado"
      echo "Instala con: uv add jupyter-collaboration"
      exit 1
  fi

  echo "════════════════════════════════════════════════"
  echo "  Jupyter Lab + Colaboracion en puerto $PORT"
  echo "════════════════════════════════════════════════"
  echo ""
  echo "  Abre: http://localhost:$PORT"
  echo "  Ctrl+C para detener"
  echo ""

  jupyter lab \
      --port=$PORT \
      --no-browser \
      --ServerApp.token='' \
      --ServerApp.password='' \
      --ServerApp.disable_check_xsrf=True \
      --ServerApp.allow_origin='*' \
      --ServerApp.root_dir="$(pwd)" \
      --collaborative
  LAUNCHER

      chmod +x "$launcher"
      echo "$launcher"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del script run-jupyter-lab.sh generado"}


id: write_jupyter_registry_kernel_bash_infra
name: write_jupyter_registry_kernel
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_jupyter_registry_kernel([project_dir: string]) -> string
tags: ["jupyter","ipython","kernel","registry","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_jupyter_registry_kernel.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-12T18:00:53Z
props: []
emits: []
variant: []
content_hash: 598829d0daf1756d46cfa31bf9473b4d5aead8d273b6140bbd39e85d11ae4649
description:
  Genera un script de startup de IPython que autoconfigura FN_REGISTRY_ROOT, sys.path a python/functions del registry, y helpers fn_query/fn_search/fn_code para consultar registry.db desde notebooks.

example:
  ```bash
  source write_jupyter_registry_kernel.sh
  path=$(write_jupyter_registry_kernel /home/lucas/analysis/finanzas)
  echo "Startup creado en: $path"
  ```

  Luego en cualquier notebook del proyecto:

  ```python
  # Ya disponible automaticamente al abrir el notebook:

  # Buscar funciones
  fn_search("finance")

  # Consultar registry.db directamente
  fn_query("SELECT id, signature FROM functions WHERE domain = ?", ("core",))

  # Ver codigo de una funcion
  print(fn_code("filter_list_py_core"))

  # Importar funciones Python del registry directamente
  from core import filter_list, map_list
  from finance import sma, ema, rsi
  ```

notes:
  Genera `.ipython/profile_default/startup/00_fn_registry.py` que se ejecuta automaticamente al iniciar cualquier kernel IPython en el proyecto. No requiere imports manuales — las funciones `fn_query`, `fn_search` y `fn_code` estan disponibles inmediatamente en cada notebook.

  El `sys.path` se configura para que cada dominio de `python/functions/` sea importable directamente (`from core import filter_list`).

  Detecta `FN_REGISTRY_ROOT` buscando la raiz del repo git o subiendo directorios hasta encontrar `registry.db`.

code:
  # write_jupyter_registry_kernel
  # -------------------------------
  # Genera un script de startup de IPython que autoconfigura el acceso
  # al fn_registry en cada notebook: FN_REGISTRY_ROOT, sys.path a
  # python/functions, y un helper fn_query() para consultar registry.db.
  #
  # USO (sourced):
  #   source write_jupyter_registry_kernel.sh
  #   write_jupyter_registry_kernel /path/to/project

  write_jupyter_registry_kernel() {
      local project_dir="${1:-.}"
      local startup_dir="${project_dir}/.ipython/profile_default/startup"
      local registry_root
      registry_root="$(cd "$project_dir" && cd "$(git -C "$project_dir" rev-parse --show-toplevel 2>/dev/null || echo "../..")" && pwd)"

      # Fallback: si no es git, buscar registry.db subiendo directorios
      if [ ! -f "$registry_root/registry.db" ] && [ -f "$project_dir/../../registry.db" ]; then
          registry_root="$(cd "$project_dir/../.." && pwd)"
      fi

      mkdir -p "$startup_dir"

      cat > "${startup_dir}/00_fn_registry.py" << STARTUP
  """
  fn_registry kernel startup
  Autoconfigura acceso al registry en cada notebook.
  Generado por write_jupyter_registry_kernel (fn_registry).
  """
  import os
  import sys
  import sqlite3
  from pathlib import Path

  # ── FN_REGISTRY_ROOT ────────────────────────────────────────
  # Prioridad: env var > path hardcoded > descubrimiento automatico
  def _discover_registry_root():
      if os.environ.get("FN_REGISTRY_ROOT"):
          return Path(os.environ["FN_REGISTRY_ROOT"]).resolve()
      hardcoded = Path("${registry_root}")
      if (hardcoded / "registry.db").exists():
          return hardcoded
      # Subir desde CWD hasta encontrar registry.db
      p = Path.cwd()
      for _ in range(10):
          if (p / "registry.db").exists():
              return p
          if p.parent == p:
              break
          p = p.parent
      return hardcoded

  FN_REGISTRY_ROOT = _discover_registry_root()
  os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)

  # ── sys.path: importar funciones Python del registry ────────
  _python_functions = FN_REGISTRY_ROOT / "python" / "functions"
  for _domain in sorted(_python_functions.iterdir()) if _python_functions.exists() else []:
      if _domain.is_dir() and not _domain.name.startswith("_"):
          _path = str(_domain)
          if _path not in sys.path:
              sys.path.insert(0, _path)

  # Tambien el directorio padre para imports por dominio: from core import filter_list
  _pf = str(_python_functions)
  if _pf not in sys.path:
      sys.path.insert(0, _pf)

  # ── fn_query: consultar registry.db desde el notebook ───────
  _REGISTRY_DB = FN_REGISTRY_ROOT / "registry.db"

  def fn_query(sql, params=()):
      """Ejecuta una consulta SQL sobre registry.db y retorna las filas.

      Ejemplos:
          fn_query("SELECT id, description FROM functions WHERE domain = ?", ("finance",))
          fn_query("SELECT id FROM functions_fts WHERE functions_fts MATCH ?", ("slice*",))
      """
      if not _REGISTRY_DB.exists():
          raise FileNotFoundError(f"registry.db no encontrado en {_REGISTRY_DB}")
      con = sqlite3.connect(str(_REGISTRY_DB))
      con.row_factory = sqlite3.Row
      try:
          rows = con.execute(sql, params).fetchall()
          return [dict(r) for r in rows]
      finally:
          con.close()

  def fn_search(term):
      """Busca funciones y tipos en el registry por nombre o descripcion.

      Ejemplo:
          fn_search("slice")
          fn_search("finance")
      """
      fts_term = f"name:{term}* OR description:{term}*"
      functions = fn_query(
          "SELECT id, kind, purity, lang, description FROM functions "
          "WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?) "
          "ORDER BY name", (fts_term,)
      )
      types = fn_query(
          "SELECT id, algebraic, lang, description FROM types "
          "WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?) "
          "ORDER BY name", (fts_term,)
      )
      return {"functions": functions, "types": types}

  def fn_code(function_id):
      """Retorna el codigo fuente de una funcion del registry.

      Ejemplo:
          print(fn_code("filter_list_py_core"))
      """
      rows = fn_query("SELECT code FROM functions WHERE id = ?", (function_id,))
      if not rows:
          raise KeyError(f"Funcion no encontrada: {function_id}")
      return rows[0]["code"]

  # ── Mensaje de bienvenida ───────────────────────────────────
  print(f"fn_registry conectado: {FN_REGISTRY_ROOT}")
  print(f"  registry.db: {'OK' if _REGISTRY_DB.exists() else 'NO ENCONTRADO'}")
  print(f"  Python functions: {_pf}")
  print(f"  Helpers: fn_query(), fn_search(), fn_code()")
  STARTUP

      echo "${startup_dir}/00_fn_registry.py"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del archivo startup generado"}


id: write_mcp_jupyter_config_bash_infra
name: write_mcp_jupyter_config
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_mcp_jupyter_config([project_dir: string], [port: int]) -> string
tags: ["mcp","jupyter","config","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_mcp_jupyter_config.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ec2747315cff0aa1f460e7b84e6c6a0d48c67005534f729586341256be0ab91e
description:
  Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server apuntando al venv local y puerto dado. Merge con jq si ya existe.

example:
  ```bash
  source write_mcp_jupyter_config.sh
  path=$(write_mcp_jupyter_config /home/lucas/analysis/finanzas 8890)
  echo "Config MCP en: $path"
  ```

notes:
  El MCP se invoca como modulo Python (`python -m jupyter_mcp_server`) usando el python del venv local, nunca una instalacion global. Si `.mcp.json` ya existe y jq esta disponible, hace merge conservando otros servidores MCP. Sin jq, sobrescribe el archivo.

code:
  # write_mcp_jupyter_config
  # -------------------------
  # Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server.
  # Usa el python del venv local con -m jupyter_mcp_server.server.
  # Configura via env vars (SERVER_URL, TOKEN) — no CLI args.
  # Hace merge si ya existe .mcp.json (requiere jq).
  #
  # USO (sourced):
  #   source write_mcp_jupyter_config.sh
  #   write_mcp_jupyter_config /path/to/project 8888

  write_mcp_jupyter_config() {
      local project_dir="${1:-.}"
      local port="${2:-8888}"
      local mcp_file="${project_dir}/.mcp.json"
      local abs_project
      abs_project="$(cd "$project_dir" && pwd)"

      local python_bin="${abs_project}/.venv/bin/python"
      if [ ! -f "$python_bin" ]; then
          echo "write_mcp_jupyter_config: python no encontrado en ${python_bin}" >&2
          return 1
      fi

      # Verificar que el modulo esta instalado
      if ! "$python_bin" -c "import jupyter_mcp_server" 2>/dev/null; then
          echo "write_mcp_jupyter_config: jupyter_mcp_server no instalado en el venv" >&2
          return 1
      fi

      local new_config
      new_config=$(cat << EOF
  {
    "mcpServers": {
      "jupyter": {
        "command": "${python_bin}",
        "args": ["-m", "jupyter_mcp_server.server"],
        "env": {
          "SERVER_URL": "http://localhost:${port}",
          "TOKEN": ""
        }
      }
    }
  }
  EOF
  )

      if [ -f "$mcp_file" ] && command -v jq &>/dev/null; then
          # Merge conservando otros servidores MCP
          jq -s '.[0] * {mcpServers: ((.[0].mcpServers // {}) * (.[1].mcpServers // {}))}' \
              "$mcp_file" <(echo "$new_config") > "${mcp_file}.tmp"
          mv "${mcp_file}.tmp" "$mcp_file"
      else
          echo "$new_config" > "$mcp_file"
      fi

      echo "$mcp_file"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"},{"name":"port","desc":"puerto Jupyter (default: detectado automáticamente)"}],"output":"ruta del archivo .mcp.json generado o actualizado"}


id: audit_registry_paths_bash_pipelines
name: audit_registry_paths
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: audit_registry_paths([output_file: string]) -> void
tags: ["registry","audit","validation","paths","launcher","pipeline","bash"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/audit_registry_paths.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 6b6fc9847fe3e4a13cb613cb0e725abce527c710b7c2c4e444e43a70c3aee39c
description:
  Audita file_path de todas las functions y types en registry.db, verifica que cada ruta apunte a un archivo existente en disco, y genera un txt con las rutas rotas para que agentes puedan corregirlas.

uses_functions:
  ["assert_command_exists_bash_shell","assert_file_exists_bash_shell","validate_registry_paths_bash_shell","report_execution_json_bash_shell","exit_with_status_bash_shell"]

example:
  ```bash
  # Con default (genera broken_paths.txt en la raiz)
  ./bash/functions/pipelines/audit_registry_paths.sh

  # Con ruta personalizada
  ./bash/functions/pipelines/audit_registry_paths.sh /tmp/broken.txt

  # Desde fn run (pipeline launcher)
  fn run audit_registry_paths
  ```

notes:
  El archivo de salida es consumible por agentes: cada linea tiene el ID de la funcion/tipo y el file_path que necesita correccion. El agente puede leer el .md correspondiente, encontrar el archivo real en disco, y actualizar el campo `file_path`.

documentation:
  ## Flujo

  1. `assert_command_exists` — verifica que `sqlite3` esta disponible
  2. `assert_file_exists` — verifica que `registry.db` existe y reporta su tamano
  3. `validate_registry_paths` (functions) — itera todas las functions, verifica cada file_path
  4. `validate_registry_paths` (types) — itera todos los types, verifica cada file_path
  5. Genera `broken_paths.txt` con formato legible para agentes
  6. `report_execution_json` — imprime JSON de ejecucion a stdout
  7. `exit_with_status` — exit code segun resultado

  ## Formato de salida

  ```
  # Broken file_path entries in registry.db
  # Generated: 2026-04-03T10:00:00Z
  # Total: 11 broken paths
  #
  # Format: id | file_path (in .md) | domain | table
  # ---

  ## Functions (11)

  cdp_click_go_browser | functions/infra/cdp_click.go | browser | functions
  ```

code:
  #!/usr/bin/env bash
  # audit_registry_paths
  # --------------------
  # Audita file_path de functions y types en registry.db.
  # Genera un txt con las rutas rotas para que agentes puedan arreglarlas.
  #
  # Compone: assert_command_exists + assert_file_exists +
  #          validate_registry_paths + report_execution_json + exit_with_status
  #
  # USO:
  #   ./audit_registry_paths.sh [OUTPUT_FILE]
  #
  # ARGUMENTOS (opcionales):
  #   OUTPUT_FILE   Ruta del archivo de salida
  #                 Default: $REGISTRY_ROOT/broken_paths.txt

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/validate_registry_paths.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/report_execution_json.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/exit_with_status.sh"

  OUTPUT_FILE="${1:-$REGISTRY_ROOT/broken_paths.txt}"
  DB_PATH="$REGISTRY_ROOT/registry.db"

  STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  START_MS=$(date +%s%3N)
  STEPS_FILE=$(mktemp)
  trap 'rm -f "$STEPS_FILE"' EXIT

  ok_steps=0
  failed_steps=0

  log_step() {
      local name="$1" action="$2" status="$3" elapsed="$4" output="${5:-}" error="${6:-}"
      printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$action" "$status" "$elapsed" "$output" "$error" >> "$STEPS_FILE"
      if [[ "$status" == "ok" ]]; then ok_steps=$((ok_steps + 1)); else failed_steps=$((failed_steps + 1)); fi
  }

  # Paso 1: verificar sqlite3
  step_start=$(date +%s%3N)
  if assert_command_exists sqlite3; then
      log_step "assert_command_exists" "check sqlite3" "ok" $(( $(date +%s%3N) - step_start ))
  else
      log_step "assert_command_exists" "check sqlite3" "error" $(( $(date +%s%3N) - step_start )) "" "sqlite3 not found"
      ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      DURATION=$(( $(date +%s%3N) - START_MS ))
      set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
      exit 1
  fi

  # Paso 2: verificar registry.db
  step_start=$(date +%s%3N)
  if db_size=$(assert_file_exists "$DB_PATH"); then
      log_step "assert_file_exists" "check registry.db" "ok" $(( $(date +%s%3N) - step_start )) "${db_size} bytes"
  else
      log_step "assert_file_exists" "check registry.db" "error" $(( $(date +%s%3N) - step_start )) "" "registry.db not found"
      ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      DURATION=$(( $(date +%s%3N) - START_MS ))
      set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
      exit 1
  fi

  # Paso 3: validar functions
  step_start=$(date +%s%3N)
  fn_broken=$(validate_registry_paths "$DB_PATH" functions "$REGISTRY_ROOT")
  fn_count=$(printf '%s' "$fn_broken" | grep -c . || true)
  log_step "validate_registry_paths" "check functions" "ok" $(( $(date +%s%3N) - step_start )) "$fn_count broken"

  # Paso 4: validar types
  step_start=$(date +%s%3N)
  ty_broken=$(validate_registry_paths "$DB_PATH" types "$REGISTRY_ROOT")
  ty_count=$(printf '%s' "$ty_broken" | grep -c . || true)
  log_step "validate_registry_paths" "check types" "ok" $(( $(date +%s%3N) - step_start )) "$ty_count broken"

  # Paso 5: generar archivo de salida
  step_start=$(date +%s%3N)
  total_broken=$(( fn_count + ty_count ))

  {
      echo "# Broken file_path entries in registry.db"
      echo "# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
      echo "# Total: $total_broken broken paths"
      echo "#"
      echo "# Format: id | file_path (in .md) | domain | table"
      echo "# ---"
      if [[ $total_broken -eq 0 ]]; then
          echo "# All paths are valid."
      else
          if [[ -n "$fn_broken" ]]; then
              echo ""
              echo "## Functions ($fn_count)"
              printf '%s\n' "$fn_broken" | while IFS=$'\t' read -r id fp domain table; do
                  echo "$id | $fp | $domain | $table"
              done
          fi
          if [[ -n "$ty_broken" ]]; then
              echo ""
              echo "## Types ($ty_count)"
              printf '%s\n' "$ty_broken" | while IFS=$'\t' read -r id fp domain table; do
                  echo "$id | $fp | $domain | $table"
              done
          fi
      fi
  } > "$OUTPUT_FILE"

  log_step "write_output" "generate $OUTPUT_FILE" "ok" $(( $(date +%s%3N) - step_start )) "$total_broken entries"

  # Reporte final
  ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  DURATION=$(( $(date +%s%3N) - START_MS ))
  total_steps=$(( ok_steps + failed_steps ))

  if [[ $total_broken -eq 0 ]]; then
      status="success"
  else
      status="partial"
  fi

  set +e
  report_execution_json "audit_registry_paths" "$status" 0 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"
  set -e

  echo ""
  echo "--- Results ---"
  echo "Broken paths: $total_broken"
  echo "Output: $OUTPUT_FILE"

  exit_with_status "$total_steps" "$ok_steps" "$failed_steps" > /dev/null

params_schema:
  {"params":[{"name":"output_file","desc":"archivo de salida con rutas rotas (default: broken_paths.txt en raíz)"}],"output":"sin salida directa; genera archivo con rutas inválidas"}


id: capacitor_build_apk_bash_pipelines
name: capacitor_build_apk
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: capacitor_build_apk(web_app_dir: string, [app_id: string], [app_name: string]) -> void
tags: ["android","apk","capacitor","mobile","build","pipeline","bash"]
uses_functions: ["install_android_sdk_bash_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/capacitor_build_apk.sh
created_at: 2026-04-06T16:40:45Z
updated_at: 2026-04-06T16:40:45Z
props: []
emits: []
variant: []
content_hash: cc0f228c2ba214cc818c23fb7f266173fd25a868215c855d5ab656f0ed4eae9e
description:
  Pipeline que convierte una web app en un APK de Android usando Capacitor. Valida el entorno (ANDROID_HOME, Java 17+), construye el bundle web si no existe dist/, inicializa Capacitor si no está configurado, añade la plataforma Android, sincroniza y compila el APK con Gradle. El APK final queda en el directorio raíz de la web app.

example:
  ```bash
  # Build con defaults (app-id y app-name desde package.json)
  ./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app

  # Build especificando app-id y app-name
  ./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app \
    --app-id com.miempresa.miapp \
    --app-name "Mi Aplicación"
  ```

notes:
  El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecución inmediatamente.

  El APK generado es un **debug build**, apto para desarrollo y pruebas. Para publicar en Play Store se necesita un release build firmado (`assembleRelease` con un keystore).

  `install_android_sdk_bash_infra` se referencia como dependencia previa: el usuario debe haberlo ejecutado (o haber instalado el SDK manualmente) antes de invocar este pipeline.

  La detección del `app_name` desde `package.json` usa `node -e` inline, lo que requiere que Node.js esté disponible. Si el campo `name` no existe en el JSON, se usa el valor por defecto `app`.

  Para instalar el APK en un dispositivo Android conectado por USB (con depuración USB activada):

  ```bash
  adb install <web_app_dir>/<app_name>.apk
  ```

documentation:
  ## Flujo

  1. **Validación** — verifica que `web_app_dir` existe, tiene `package.json`, que `ANDROID_HOME` está seteado (o sourcea `$HOME/android-sdk/env.sh`) y que Java 17+ está disponible.
  2. **Build web** — si no existe `dist/`, ejecuta `pnpm build` en el directorio de la app.
  3. **Init Capacitor** — si no existe `capacitor.config.ts`, instala `@capacitor/core`, `@capacitor/cli` y `@capacitor/android` via npm y genera el archivo de configuración con el `appId`, `appName` y `webDir: dist`.
  4. **Add Android** — si no existe el directorio `android/`, ejecuta `npx cap add android`.
  5. **Sync** — ejecuta `npx cap sync android` para copiar los assets web al proyecto Android.
  6. **Build APK** — ejecuta `./gradlew assembleDebug` desde `android/`; si falla sale con exit 1.
  7. **Copia APK** — copia `android/app/build/outputs/apk/debug/app-debug.apk` a `<web_app_dir>/<app_name>.apk`.
  8. **Resultado** — imprime la ruta del APK y su tamaño en MB.

  ## Requisitos

  - **Node.js** y **pnpm** disponibles en PATH
  - **Java 17+** disponible en PATH
  - **Android SDK** instalado: `ANDROID_HOME` seteado, o bien `$HOME/android-sdk/env.sh` existente (generado por `install_android_sdk`)
  - **Gradle wrapper** presente en el directorio `android/` (generado por `cap add android`)

code:
  #!/usr/bin/env bash
  # capacitor_build_apk
  # -------------------
  # Pipeline que convierte una web app buildeada en un APK de Android usando Capacitor.
  # Asume que el Android SDK está instalado (via install_android_sdk o manualmente).
  #
  # USO:
  #   ./capacitor_build_apk.sh <web_app_dir> [--app-id com.example.app] [--app-name "My App"]
  #
  # ARGUMENTOS:
  #   web_app_dir         Directorio de la web app (debe contener package.json)
  #   --app-id            ID de la app Android (default: com.fnregistry.app)
  #   --app-name          Nombre visible de la app (default: name de package.json)
  #
  # REQUISITOS:
  #   - Node.js + pnpm instalados en PATH
  #   - Java 17+ instalado en PATH
  #   - Android SDK: ANDROID_HOME seteado o $HOME/android-sdk/env.sh disponible

  set -euo pipefail

  # ---------------------------------------------------------------------------
  # Parseo de argumentos
  # ---------------------------------------------------------------------------

  WEB_APP_DIR=""
  APP_ID="com.fnregistry.app"
  APP_NAME=""

  while [[ $# -gt 0 ]]; do
      case "$1" in
          --app-id)
              APP_ID="$2"
              shift 2
              ;;
          --app-name)
              APP_NAME="$2"
              shift 2
              ;;
          -*)
              echo "[capacitor_build_apk] ERROR: argumento desconocido: $1" >&2
              echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
              exit 1
              ;;
          *)
              WEB_APP_DIR="$1"
              shift
              ;;
      esac
  done

  if [[ -z "$WEB_APP_DIR" ]]; then
      echo "[capacitor_build_apk] ERROR: web_app_dir es obligatorio." >&2
      echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
      exit 1
  fi

  # ---------------------------------------------------------------------------
  # 1. Validación
  # ---------------------------------------------------------------------------

  echo "[capacitor_build_apk] Validando entorno..."

  # Verificar que web_app_dir existe y tiene package.json
  if [[ ! -d "$WEB_APP_DIR" ]]; then
      echo "[capacitor_build_apk] ERROR: directorio no existe: $WEB_APP_DIR" >&2
      exit 1
  fi

  if [[ ! -f "$WEB_APP_DIR/package.json" ]]; then
      echo "[capacitor_build_apk] ERROR: no se encontró package.json en $WEB_APP_DIR" >&2
      exit 1
  fi

  # Resolver app name desde package.json si no se pasó
  if [[ -z "$APP_NAME" ]]; then
      APP_NAME=$(node -e "const p = require('$WEB_APP_DIR/package.json'); process.stdout.write(p.name || 'app');" 2>/dev/null || echo "app")
      echo "[capacitor_build_apk] App name detectado desde package.json: $APP_NAME"
  fi

  # Verificar ANDROID_HOME o sourcea env.sh
  if [[ -z "${ANDROID_HOME:-}" ]]; then
      ANDROID_ENV="$HOME/android-sdk/env.sh"
      if [[ -f "$ANDROID_ENV" ]]; then
          echo "[capacitor_build_apk] ANDROID_HOME no seteado, sourceando $ANDROID_ENV ..."
          # shellcheck source=/dev/null
          source "$ANDROID_ENV"
      else
          echo "[capacitor_build_apk] ERROR: ANDROID_HOME no está seteado y no se encontró $ANDROID_ENV" >&2
          echo "  Instala el SDK con install_android_sdk o setea ANDROID_HOME manualmente." >&2
          exit 1
      fi
  fi

  echo "[capacitor_build_apk] ANDROID_HOME: $ANDROID_HOME"

  # Verificar Java 17+
  if ! command -v java &>/dev/null; then
      echo "[capacitor_build_apk] ERROR: java no está en PATH." >&2
      exit 1
  fi

  JAVA_VERSION=$(java -version 2>&1 | head -1 | grep -oP '(?<=version ")([0-9]+)' | head -1 || echo "0")
  if [[ "$JAVA_VERSION" -lt 17 ]]; then
      echo "[capacitor_build_apk] ERROR: se requiere Java 17+. Versión detectada: $JAVA_VERSION" >&2
      exit 1
  fi

  echo "[capacitor_build_apk] Java $JAVA_VERSION detectado."

  # ---------------------------------------------------------------------------
  # 2. Build web (si no existe dist/)
  # ---------------------------------------------------------------------------

  if [[ ! -d "$WEB_APP_DIR/dist" ]]; then
      echo "[capacitor_build_apk] No se encontró dist/, ejecutando pnpm build..."
      (cd "$WEB_APP_DIR" && pnpm build)
      echo "[capacitor_build_apk] Build web completado."
  else
      echo "[capacitor_build_apk] dist/ ya existe, omitiendo build web."
  fi

  # ---------------------------------------------------------------------------
  # 3. Init Capacitor (si no existe capacitor.config.ts)
  # ---------------------------------------------------------------------------

  if [[ ! -f "$WEB_APP_DIR/capacitor.config.ts" ]]; then
      echo "[capacitor_build_apk] Instalando dependencias de Capacitor..."
      (cd "$WEB_APP_DIR" && npm install @capacitor/core @capacitor/cli @capacitor/android)

      echo "[capacitor_build_apk] Generando capacitor.config.ts..."
      cat > "$WEB_APP_DIR/capacitor.config.ts" <<CAPCONFIG
  import type { CapacitorConfig } from '@capacitor/cli';

  const config: CapacitorConfig = {
    appId: '${APP_ID}',
    appName: '${APP_NAME}',
    webDir: 'dist',
    server: { androidScheme: 'https' }
  };

  export default config;
  CAPCONFIG

      echo "[capacitor_build_apk] capacitor.config.ts generado."
  else
      echo "[capacitor_build_apk] capacitor.config.ts ya existe, omitiendo init."
  fi

  # ---------------------------------------------------------------------------
  # 4. Add Android (si no existe el directorio android/)
  # ---------------------------------------------------------------------------

  if [[ ! -d "$WEB_APP_DIR/android" ]]; then
      echo "[capacitor_build_apk] Añadiendo plataforma Android..."
      (cd "$WEB_APP_DIR" && npx cap add android)
      echo "[capacitor_build_apk] Plataforma Android añadida."
  else
      echo "[capacitor_build_apk] Directorio android/ ya existe, omitiendo cap add."
  fi

  # ---------------------------------------------------------------------------
  # 5. Sync
  # ---------------------------------------------------------------------------

  echo "[capacitor_build_apk] Sincronizando assets web con Android..."
  (cd "$WEB_APP_DIR" && npx cap sync android)
  echo "[capacitor_build_apk] Sync completado."

  # ---------------------------------------------------------------------------
  # 6. Build APK
  # ---------------------------------------------------------------------------

  echo "[capacitor_build_apk] Compilando APK con Gradle..."
  if ! (cd "$WEB_APP_DIR/android" && ./gradlew assembleDebug); then
      echo "[capacitor_build_apk] ERROR: Gradle falló. Revisa los logs anteriores." >&2
      exit 1
  fi

  APK_SOURCE="$WEB_APP_DIR/android/app/build/outputs/apk/debug/app-debug.apk"

  if [[ ! -f "$APK_SOURCE" ]]; then
      echo "[capacitor_build_apk] ERROR: Gradle terminó sin error pero no se encontró el APK en $APK_SOURCE" >&2
      exit 1
  fi

  # ---------------------------------------------------------------------------
  # 7. Copia APK al directorio raíz
  # ---------------------------------------------------------------------------

  APK_DEST="$WEB_APP_DIR/${APP_NAME}.apk"
  cp "$APK_SOURCE" "$APK_DEST"

  # ---------------------------------------------------------------------------
  # 8. Resultado
  # ---------------------------------------------------------------------------

  APK_SIZE_BYTES=$(stat -c%s "$APK_DEST" 2>/dev/null || stat -f%z "$APK_DEST" 2>/dev/null || echo "0")
  APK_SIZE_MB=$(awk "BEGIN {printf \"%.1f\", $APK_SIZE_BYTES/1048576}")

  echo ""
  echo "---------------------------------------------------------------------"
  echo "APK generado: $APK_DEST"
  echo "Tamaño: ${APK_SIZE_MB} MB"
  echo ""
  echo "Para instalar en un dispositivo conectado por USB:"
  echo "  adb install '$APK_DEST'"
  echo "---------------------------------------------------------------------"

params_schema:
  {"params":[{"name":"web_app_dir","desc":"directorio raíz de la web app; debe contener package.json; si no existe dist/ se ejecuta pnpm build automáticamente"},{"name":"app_id","desc":"identificador de la app Android en formato reverse-DNS (default: com.fnregistry.app)"},{"name":"app_name","desc":"nombre visible de la app Android; si se omite, se lee del campo name de package.json"}],"output":"APK de debug en \u003cweb_app_dir\u003e/\u003capp_name\u003e.apk; imprime ruta y tamaño en MB al finalizar"}


id: export_analysis_pdfs_bash_pipelines
name: export_analysis_pdfs
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: export_analysis_pdfs(nombre: string, [pattern: string]) -> void
tags: ["pipeline","jupyter","pdf","export","nbconvert","launcher"]
uses_functions: ["assert_command_exists_bash_shell","install_nbconvert_bash_infra","notebook_to_pdf_bash_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/export_analysis_pdfs.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 547c15c27db0fa782bf3a11e72024e9a2634652f25e17f5790cb3378f3fc39bd
description:
  Exporta todos los notebooks de un analisis Jupyter a PDF. Instala nbconvert y playwright automaticamente si no estan presentes.

example:
  ```bash
  # Exportar todos los notebooks de un analisis
  ./export_analysis_pdfs.sh finanzas

  # Con pattern especifico
  ./export_analysis_pdfs.sh ml "notebooks/01_*.ipynb"

  # Via fn run
  fn run export_analysis_pdfs finanzas
  fn run export_analysis_pdfs ml "notebooks/01_*.ipynb"
  ```

notes:
  El analysis debe existir previamente en `analysis/{nombre}/` con un venv inicializado. Los PDFs se generan en `analysis/{nombre}/notebooks/pdf/` por defecto. El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecucion.

documentation:
  ## Flujo

  1. `assert_command_exists uv` — verifica que uv esta disponible
  2. `install_nbconvert` — instala nbconvert y playwright con chromium (idempotente)
  3. `notebook_to_pdf` — convierte notebooks al patron indicado a PDF en `notebooks/pdf/`

code:
  #!/usr/bin/env bash
  # export_analysis_pdfs
  # ---------------------
  # Pipeline que exporta todos los notebooks de un analisis a PDF.
  # Compone: assert_command_exists + install_nbconvert + notebook_to_pdf
  #
  # USO:
  #   ./export_analysis_pdfs.sh <nombre_analysis> [pattern]
  #
  # EJEMPLOS:
  #   ./export_analysis_pdfs.sh finanzas
  #   ./export_analysis_pdfs.sh ml "notebooks/01_*.ipynb"

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  # Source funciones atomicas
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/install_nbconvert.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/notebook_to_pdf.sh"

  # ── Argumentos ──────────────────────────────────────────────

  NOMBRE="${1:-}"
  if [ -z "$NOMBRE" ]; then
      echo "Uso: $0 <nombre_analysis> [pattern]" >&2
      echo "  Ejemplo: $0 finanzas" >&2
      echo "  Ejemplo: $0 ml 'notebooks/01_*.ipynb'" >&2
      exit 1
  fi
  shift
  PATTERN="${1:-notebooks/*.ipynb}"

  ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}"

  # Verificar que el analysis existe
  if [ ! -d "$ANALYSIS_DIR" ]; then
      echo "Error: analysis '${NOMBRE}' no existe en ${ANALYSIS_DIR}" >&2
      exit 1
  fi

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  EXPORT ANALYSIS PDFs: ${NOMBRE}"
  echo "  Directorio: ${ANALYSIS_DIR}"
  echo "════════════════════════════════════════════════════════════"
  echo ""

  # ── 1. Verificar herramientas ───────────────────────────────

  echo "[1/3] Verificando herramientas..."
  assert_command_exists uv
  echo "  OK"

  # ── 2. Instalar nbconvert + playwright ──────────────────────

  echo "[2/3] Instalando dependencias de exportacion..."
  install_nbconvert "$ANALYSIS_DIR"
  echo "  OK"

  # ── 3. Convertir notebooks a PDF ────────────────────────────

  echo "[3/3] Convirtiendo notebooks a PDF..."
  notebook_to_pdf "$ANALYSIS_DIR" "$PATTERN"

  # ── Resumen ─────────────────────────────────────────────────

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  EXPORT COMPLETADO"
  echo "════════════════════════════════════════════════════════════"

params_schema:
  {"params":[{"name":"nombre","desc":"nombre del análisis en analysis/{nombre}/"},{"name":"pattern","desc":"glob de notebooks a exportar (default: notebooks/*.ipynb)"}],"output":"sin salida directa; genera PDFs en notebooks/pdf/"}


id: gitea_init_app_bash_pipelines
name: gitea_init_app
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: gitea_init_app(directory: string, owner: string, name: string, private: string) -> string
tags: ["gitea","git","pipeline","repo","create","push","launcher","infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/gitea_init_app.sh
created_at: 2026-04-05T23:01:37Z
updated_at: 2026-04-05T23:01:37Z
props: []
emits: []
variant: []
content_hash: 6fad8d2b0ff043b9697e43304b184c7f85d64cdb555bf372d8ccdaaeea4a89df
description:
  Pipeline que crea un repositorio en Gitea, sube el directorio local y añade a egutierrez como colaborador admin. Compone gitea_create_repo → gitea_push_directory → gitea_add_collaborator.

uses_functions:
  ["gitea_create_repo_bash_infra","gitea_push_directory_bash_infra","gitea_add_collaborator_bash_infra"]

example:
  ```bash
  export GITEA_URL="$(pass agentes/gitea-url)"
  export GITEA_TOKEN="$(pass agentes/dataforge-token)"

  # Crear repo con nombre inferido del directorio
  bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg

  # Nombre explícito y repo privado
  bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg my-custom-name true

  # Con flags
  bash bash/functions/pipelines/gitea_init_app.sh \
      --directory /home/lucas/myapp \
      --owner myorg \
      --name my-app \
      --private true
  ```

notes:
  - Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
  - Si el repo ya existe (409), el pipeline continúa con el push y añade el colaborador.
  - El colaborador `egutierrez` es fijo en el pipeline — para variarlo usar las funciones individuales.
  - La URL del repo se imprime a stdout al finalizar.

documentation:
  ## Pasos del pipeline

  1. `gitea_create_repo owner name private` — crea el repo (idempotente si ya existe)
  2. `gitea_push_directory directory owner repo` — inicializa git y hace push del directorio
  3. `gitea_add_collaborator owner repo egutierrez admin` — añade colaborador con permisos admin

code:
  #!/usr/bin/env bash
  # Pipeline: gitea_init_app — Crea repo en Gitea, sube directorio y añade colaborador egutierrez
  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR/../infra/gitea_create_repo.sh"
  source "$SCRIPT_DIR/../infra/gitea_push_directory.sh"
  source "$SCRIPT_DIR/../infra/gitea_add_collaborator.sh"

  main() {
      local directory=""
      local owner=""
      local name=""
      local private="false"

      # Parsear argumentos
      while [[ $# -gt 0 ]]; do
          case "$1" in
              --directory) directory="$2"; shift 2 ;;
              --owner)     owner="$2";     shift 2 ;;
              --name)      name="$2";      shift 2 ;;
              --private)   private="$2";   shift 2 ;;
              *)
                  # Argumentos posicionales: directory owner [name] [private]
                  if [[ -z "$directory" ]]; then
                      directory="$1"
                  elif [[ -z "$owner" ]]; then
                      owner="$1"
                  elif [[ -z "$name" ]]; then
                      name="$1"
                  else
                      private="$1"
                  fi
                  shift
                  ;;
          esac
      done

      if [[ -z "$directory" || -z "$owner" ]]; then
          echo "gitea_init_app: uso: gitea_init_app <directory> <owner> [name] [private]" >&2
          echo "gitea_init_app: o con flags: --directory <dir> --owner <owner> [--name <name>] [--private true]" >&2
          return 1
      fi

      # Inferir nombre del repo desde basename del directorio si no se especificó
      if [[ -z "$name" ]]; then
          name=$(basename "$directory")
          echo "gitea_init_app: nombre inferido del directorio: '$name'" >&2
      fi

      if [[ -z "${GITEA_URL:-}" ]]; then
          echo "gitea_init_app: GITEA_URL no está seteada" >&2
          return 1
      fi
      if [[ -z "${GITEA_TOKEN:-}" ]]; then
          echo "gitea_init_app: GITEA_TOKEN no está seteado" >&2
          return 1
      fi

      echo "gitea_init_app: iniciando pipeline para '$owner/$name'..." >&2
      echo "gitea_init_app: directorio fuente: '$directory'" >&2

      # Paso 1: Crear repo
      echo "gitea_init_app: [1/3] creando repositorio..." >&2
      gitea_create_repo "$owner" "$name" "$private" "" > /dev/null

      # Paso 2: Subir directorio
      echo "gitea_init_app: [2/3] subiendo directorio al repositorio..." >&2
      gitea_push_directory "$directory" "$owner" "$name"

      # Paso 3: Añadir colaborador egutierrez con permisos admin
      echo "gitea_init_app: [3/3] añadiendo colaborador egutierrez..." >&2
      gitea_add_collaborator "$owner" "$name" "egutierrez" "admin"

      echo "gitea_init_app: pipeline completado — ${GITEA_URL}/${owner}/${name}" >&2
      echo "${GITEA_URL}/${owner}/${name}"
  }

  main "$@"

params_schema:
  {"params":[{"name":"directory","desc":"ruta al directorio local a subir como repositorio"},{"name":"owner","desc":"usuario u organización en Gitea que será propietaria del repo"},{"name":"name","desc":"nombre del repositorio (opcional: se infiere del basename del directorio)"},{"name":"private","desc":"si el repo debe ser privado, 'true' o 'false' (default: false)"}],"output":"URL del repositorio creado en Gitea"}


id: init_go_module_bash_pipelines
name: init_go_module
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: init_go_module([module_path: string]) -> void
tags: ["bash","go","module","init","scaffold","launcher"]
uses_functions: ["install_go_bash_infra","assert_command_exists_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/init_go_module.sh
created_at: 2026-04-08T21:30:10Z
updated_at: 2026-04-08T21:30:10Z
props: []
emits: []
variant: []
content_hash: a4ef6ad54b0de42c8a4df397a5fb6a9a8cea4abece1efd9e1dc4bd34e1870740
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/inicializar_repos/go/init_go_module.sh
description:
  Pipeline que inicializa un módulo Go simple en el directorio actual. Crea go.mod (go mod init), main.go con hello world, .gitignore, build.sh (cross-compilation linux/windows/all) y dev.sh para ejecución rápida.

example:
  ```bash
  mkdir mi-modulo && cd mi-modulo

  # Con module_path automático (github.com/user/mi-modulo)
  bash bash/functions/pipelines/init_go_module.sh

  # Con module_path explícito
  bash bash/functions/pipelines/init_go_module.sh github.com/miorg/mi-modulo

  # Ejecutar tras crear
  ./dev.sh
  ./build.sh
  ./build.sh all
  ```

notes:
  Crea un módulo minimalista (main.go simple + build tools). Para proyectos con estructura profesional (cmd/, internal/, pkg/) usar `init_go_project`. No inicializa git — añadido manualmente o via gitea_init_app.

code:
  #!/usr/bin/env bash
  # init_go_module
  # --------------
  # Pipeline que inicializa un módulo Go simple en el directorio actual.
  # Crea: go.mod, main.go, .gitignore, build.sh y dev.sh.
  #
  # USO:
  #   bash init_go_module.sh [module_path]
  #
  # ARGUMENTOS:
  #   module_path   Path del módulo Go (opcional; default: github.com/user/<dirname>)

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

  init_go_module() {
      local module_path="${1:-}"

      local dir_name
      dir_name="$(basename "$(pwd)")"

      if [[ -z "$module_path" ]]; then
          module_path="github.com/user/${dir_name}"
      fi

      echo "=== Inicializar Módulo Go ==="
      echo "  Módulo:    ${module_path}"
      echo "  Directorio: $(pwd)"
      echo ""

      # Verificar Go
      if ! command -v go &>/dev/null; then
          echo "init_go_module: Go no está instalado" >&2
          return 1
      fi
      echo "Go detectado: $(go version)"
      echo ""

      # Inicializar módulo
      echo "Inicializando go module..."
      if ! go mod init "$module_path"; then
          echo "init_go_module: falló go mod init" >&2
          return 1
      fi
      echo "go.mod creado"
      echo ""

      # Crear main.go
      echo "Creando main.go..."
      cat > main.go << 'GOEOF'
  package main

  import (
  	"fmt"
  )

  func main() {
  	fmt.Println("Hola desde Go!")
  	fmt.Println("Modulo inicializado correctamente")
  }
  GOEOF
      echo "main.go creado"
      echo ""

      # Crear .gitignore
      echo "Creando .gitignore..."
      cat > .gitignore << 'IGNEOF'
  # Binarios
  *.exe
  *.exe~
  *.dll
  *.so
  *.dylib
  bin/
  build/

  # Archivos de test
  *.test
  *.out
  coverage.txt
  *.prof

  # IDEs
  .idea/
  .vscode/
  *.swp
  *.swo
  *~

  # OS
  .DS_Store
  Thumbs.db
  IGNEOF
      echo ".gitignore creado"
      echo ""

      # Crear build.sh
      echo "Creando build.sh..."
      cat > build.sh << 'BUILDEOF'
  #!/bin/bash
  set -euo pipefail

  TARGET="${1:-linux}"
  mkdir -p bin

  case "$TARGET" in
      linux)
          GOOS=linux GOARCH=amd64 go build -o bin/app-linux main.go
          chmod +x bin/app-linux
          echo "Compilado: bin/app-linux"
          ;;
      windows)
          GOOS=windows GOARCH=amd64 go build -o bin/app-windows.exe main.go
          echo "Compilado: bin/app-windows.exe"
          ;;
      all)
          GOOS=linux   GOARCH=amd64 go build -o bin/app-linux        main.go && echo "Linux OK"
          GOOS=windows GOARCH=amd64 go build -o bin/app-windows.exe  main.go && echo "Windows OK"
          GOOS=darwin  GOARCH=amd64 go build -o bin/app-macos        main.go && echo "macOS OK"
          ;;
      *)
          echo "Uso: ./build.sh [linux|windows|all]" >&2
          exit 1
          ;;
  esac
  BUILDEOF
      chmod +x build.sh
      echo "build.sh creado"
      echo ""

      # Crear dev.sh
      echo "Creando dev.sh..."
      cat > dev.sh << 'DEVEOF'
  #!/bin/bash
  set -euo pipefail

  go run main.go "$@"
  DEVEOF
      chmod +x dev.sh
      echo "dev.sh creado"
      echo ""

      echo "=== Modulo Go creado exitosamente ==="
      echo ""
      echo "Archivos generados:"
      echo "  main.go     - Codigo del modulo"
      echo "  go.mod      - Modulo Go (${module_path})"
      echo "  .gitignore  - Exclusiones git"
      echo "  build.sh    - Compilar binario"
      echo "  dev.sh      - Ejecutar directamente"
      echo ""
      echo "Proximos pasos:"
      echo "  ./dev.sh           - Ejecutar el modulo"
      echo "  ./build.sh         - Compilar para Linux"
      echo "  ./build.sh windows - Compilar para Windows"
      echo "  ./build.sh all     - Compilar para todo"
  }

  init_go_module "$@"

params_schema:
  {"params":[{"name":"module_path","desc":"path del módulo Go, ej: github.com/user/mymodule (default: github.com/user/\u003cdirname\u003e)"}],"output":"crea archivos en el directorio actual y muestra progreso; exit code 1 si Go no está instalado o go mod init falla"}


id: init_go_project_bash_pipelines
name: init_go_project
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: init_go_project([module_path: string]) -> void
tags: ["bash","go","project","init","scaffold","professional","launcher"]
uses_functions: ["install_go_bash_infra","assert_command_exists_bash_shell","create_project_structure_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/init_go_project.sh
created_at: 2026-04-08T21:30:10Z
updated_at: 2026-04-08T21:30:10Z
props: []
emits: []
variant: []
content_hash: 0788b07e1a527fe615fcbd7756bcfba949ffbba8321c366896fb9bbfb21dae27
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/inicializar_repos/go/init_go_proyect.sh
description:
  Pipeline que inicializa un repositorio Go completo con estructura profesional: cmd/app, internal/config, internal/service (con tests), pkg/version, scripts (run/test/build/build-all/lint), Makefile, .gitignore, README y git init con git add.

example:
  ```bash
  mkdir mi-proyecto && cd mi-proyecto

  # Con module_path automático
  bash bash/functions/pipelines/init_go_project.sh

  # Con module_path explícito
  bash bash/functions/pipelines/init_go_project.sh github.com/miorg/mi-proyecto

  # Ejecutar y testear tras crear
  ./scripts/run.sh
  ./scripts/test.sh
  make build-all
  ```

notes:
  Genera una arquitectura funcional con separación clara: cmd/ (entrypoint), internal/ (lógica privada), pkg/ (librería pública). Incluye un test de ejemplo en internal/service/. Ejecuta go mod tidy y git init + git add al final. Para módulos simples sin estructura, usar `init_go_module`.

code:
  #!/usr/bin/env bash
  # init_go_project
  # ---------------
  # Pipeline que inicializa un repositorio Go completo con estructura profesional:
  # cmd/app, internal/config, internal/service, pkg/version, scripts/, Makefile,
  # .gitignore, README y git init.
  #
  # USO:
  #   bash init_go_project.sh [module_path]
  #
  # ARGUMENTOS:
  #   module_path   Path del módulo Go (opcional; default: github.com/<whoami>/<dirname>)

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

  init_go_project() {
      local module_path="${1:-}"

      local project_name
      project_name="$(basename "$(pwd)")"

      if [[ -z "$module_path" ]]; then
          module_path="github.com/$(whoami)/${project_name}"
      fi

      echo "=== Inicializar Proyecto Go Completo ==="
      echo "  Proyecto: ${project_name}"
      echo "  Módulo:   ${module_path}"
      echo "  Dir:      $(pwd)"
      echo ""

      # Verificar dependencias
      if ! command -v go &>/dev/null; then
          echo "init_go_project: Go no está instalado" >&2
          return 1
      fi
      if ! command -v git &>/dev/null; then
          echo "init_go_project: git no está instalado" >&2
          return 1
      fi

      echo "Go:  $(go version)"
      echo "git: $(git --version)"
      echo ""

      # Crear estructura de carpetas
      echo "Creando estructura de carpetas..."
      mkdir -p cmd/app internal/config internal/service pkg/version scripts bin
      echo "Estructura base creada"
      echo ""

      # go mod init
      echo "Inicializando módulo Go..."
      if ! go mod init "$module_path"; then
          echo "init_go_project: falló go mod init" >&2
          return 1
      fi
      echo "go.mod creado"
      echo ""

      # Crear archivos fuente
      echo "Creando archivos fuente..."

      cat > cmd/app/main.go << GOEOF
  package main

  import (
  	"fmt"

  	"${module_path}/internal/config"
  	"${module_path}/internal/service"
  	"${module_path}/pkg/version"
  )

  func main() {
  	cfg := config.Load()
  	msg := service.BuildStartupMessage(cfg.AppName, version.Current())
  	fmt.Println(msg)
  }
  GOEOF

      cat > internal/config/config.go << 'CFGEOF'
  package config

  import "os"

  type AppConfig struct {
  	AppName string
  }

  func Load() AppConfig {
  	appName := os.Getenv("APP_NAME")
  	if appName == "" {
  		appName = "Go Project"
  	}
  	return AppConfig{AppName: appName}
  }
  CFGEOF

      cat > internal/service/message.go << 'SVCEOF'
  package service

  import "fmt"

  func BuildStartupMessage(appName, appVersion string) string {
  	return fmt.Sprintf("%s iniciado correctamente (version %s)", appName, appVersion)
  }
  SVCEOF

      cat > internal/service/message_test.go << 'TESTEOF'
  package service

  import "testing"

  func TestBuildStartupMessage(t *testing.T) {
  	result := BuildStartupMessage("MyApp", "0.1.0")
  	want := "MyApp iniciado correctamente (version 0.1.0)"
  	if result != want {
  		t.Fatalf("resultado inesperado:\nwant: %s\ngot:  %s", want, result)
  	}
  }
  TESTEOF

      cat > pkg/version/version.go << 'VEREOF'
  package version

  var value = "0.1.0"

  func Current() string {
  	return value
  }
  VEREOF

      echo "Archivos fuente creados"
      echo ""

      # Makefile
      echo "Creando Makefile..."
      cat > Makefile << 'MAKEEOF'
  APP_NAME=app
  CMD_PATH=./cmd/app
  BIN_DIR=./bin

  .PHONY: run build build-all test fmt tidy clean

  run:
  	go run $(CMD_PATH)

  build:
  	mkdir -p $(BIN_DIR)
  	go build -o $(BIN_DIR)/$(APP_NAME) $(CMD_PATH)

  build-all:
  	bash ./scripts/build-all.sh

  test:
  	go test ./...

  fmt:
  	gofmt -w ./cmd ./internal ./pkg

  tidy:
  	go mod tidy

  clean:
  	rm -rf $(BIN_DIR)
  MAKEEOF
      echo "Makefile creado"
      echo ""

      # .gitignore
      cat > .gitignore << 'IGNEOF'
  bin/
  *.exe
  *.exe~
  *.dll
  *.so
  *.dylib
  *.test
  *.out
  coverage.out
  .idea/
  .vscode/
  *.swp
  *.swo
  *~
  .DS_Store
  Thumbs.db
  IGNEOF

      # README
      cat > README.md << READEOF
  # ${project_name}

  Repositorio Go inicializado con estructura completa.

  ## Uso rápido

  \`\`\`bash
  ./scripts/run.sh
  ./scripts/test.sh
  ./scripts/build.sh
  \`\`\`

  ## Make

  \`\`\`bash
  make run
  make test
  make build
  make build-all
  \`\`\`
  READEOF

      # Scripts
      echo "Creando scripts..."

      cat > scripts/run.sh << 'RUNEOF'
  #!/bin/bash
  set -euo pipefail
  go run ./cmd/app
  RUNEOF

      cat > scripts/test.sh << 'TESTSHEOF'
  #!/bin/bash
  set -euo pipefail
  go test ./...
  TESTSHEOF

      cat > scripts/build.sh << 'BUILDSHEOF'
  #!/bin/bash
  set -euo pipefail
  mkdir -p ./bin
  go build -o ./bin/app ./cmd/app
  echo "Binario: ./bin/app"
  BUILDSHEOF

      cat > scripts/build-all.sh << 'BUILDALLEOF'
  #!/bin/bash
  set -euo pipefail
  mkdir -p ./bin
  GOOS=linux   GOARCH=amd64 go build -o ./bin/app-linux-amd64   ./cmd/app
  GOOS=darwin  GOARCH=amd64 go build -o ./bin/app-darwin-amd64  ./cmd/app
  GOOS=windows GOARCH=amd64 go build -o ./bin/app-windows-amd64.exe ./cmd/app
  echo "Builds en ./bin"
  BUILDALLEOF

      cat > scripts/lint.sh << 'LINTEOF'
  #!/bin/bash
  set -euo pipefail
  unformatted="$(gofmt -l ./cmd ./internal ./pkg)"
  if [ -n "$unformatted" ]; then
      echo "Archivos sin formato:" >&2
      echo "$unformatted" >&2
      echo "Ejecuta: make fmt" >&2
      exit 1
  fi
  echo "Formato OK"
  LINTEOF

      chmod +x scripts/*.sh
      echo "Scripts creados"
      echo ""

      # go mod tidy
      echo "Ajustando dependencias (go mod tidy)..."
      go mod tidy
      echo "Dependencias ajustadas"
      echo ""

      # git init
      echo "Inicializando repositorio git..."
      git init >/dev/null 2>&1
      git add .
      echo "Repositorio git inicializado"
      echo ""

      echo "=== Proyecto Go creado exitosamente ==="
      echo ""
      echo "Estructura:"
      echo "  cmd/app/main.go"
      echo "  internal/config/config.go"
      echo "  internal/service/message.go + message_test.go"
      echo "  pkg/version/version.go"
      echo "  scripts/ (run, test, build, build-all, lint)"
      echo "  Makefile, go.mod, .gitignore, README.md"
      echo ""
      echo "Proximos pasos:"
      echo "  1. ./scripts/run.sh"
      echo "  2. ./scripts/test.sh"
      echo "  3. ./scripts/build-all.sh"
  }

  init_go_project "$@"

params_schema:
  {"params":[{"name":"module_path","desc":"path del módulo Go, ej: github.com/org/myproject (default: github.com/\u003cwhoami\u003e/\u003cdirname\u003e)"}],"output":"crea estructura completa en el directorio actual y muestra progreso; exit code 1 si Go o git no están disponibles"}


id: init_jupyter_analysis_bash_pipelines
name: init_jupyter_analysis
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: init_jupyter_analysis(nombre: string, [...paquetes_extra: string]) -> void
tags: ["jupyter","analysis","setup","pipeline","bash","launcher"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/init_jupyter_analysis.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 9adee852b86ced3705792530a50eebe4a7770de3c37002fc94b8a33dfc77a674
description:
  Inicializa un analisis Jupyter completo en analysis/{nombre}/ con venv, paquetes, launcher, MCP y reglas para agentes Claude. Acepta paquetes extra opcionales.

uses_functions:
  ["assert_command_exists_bash_shell","find_free_port_bash_shell","init_uv_venv_bash_infra","uv_add_packages_bash_infra","write_jupyter_launcher_bash_infra","write_mcp_jupyter_config_bash_infra","write_claude_jupyter_rules_bash_infra","write_jupyter_registry_kernel_bash_infra"]

example:
  ```bash
  # Analisis basico
  ./init_jupyter_analysis.sh finanzas

  # Con paquetes extra
  ./init_jupyter_analysis.sh duckdb polars duckdb
  ./init_jupyter_analysis.sh ml scikit-learn torch

  # Via fn run
  fn run init_jupyter_analysis finanzas
  fn run init_jupyter_analysis ml scikit-learn torch
  ```

notes:
  Cada analisis es independiente (propio venv, propio Jupyter, propio MCP). Mismo patron que `apps/` pero para exploraciones no reutilizables.

  El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecucion.

  Paquetes base siempre incluidos: jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib. Los paquetes extra se añaden a estos.

documentation:
  ## Flujo

  1. `assert_command_exists` — verifica que uv o python3 estan disponibles
  2. Crea estructura `analysis/{nombre}/notebooks/` y `analysis/{nombre}/data/`
  3. `init_uv_venv` — crea venv en `analysis/{nombre}/.venv/`
  4. `uv_add_packages` — instala jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib + extras
  5. `write_jupyter_launcher` — genera `run-jupyter-lab.sh` con modo colaborativo
  6. `find_free_port` + `write_mcp_jupyter_config` — detecta puerto libre y genera `.mcp.json`
  7. `write_claude_jupyter_rules` — genera `.claude/CLAUDE.md` con reglas de agente
  8. `write_jupyter_registry_kernel` — genera IPython startup con `fn_query`, `fn_search`, `fn_code` y acceso a `python/functions/`

code:
  #!/usr/bin/env bash
  # init_jupyter_analysis
  # ----------------------
  # Inicializa un analisis Jupyter completo en analysis/{nombre}/.
  # Compone: assert_command_exists + find_free_port + init_uv_venv +
  #          uv_add_packages + write_jupyter_launcher +
  #          write_mcp_jupyter_config + write_claude_jupyter_rules +
  #          write_jupyter_registry_kernel
  #
  # USO:
  #   ./init_jupyter_analysis.sh <nombre> [paquetes_extra...]
  #
  # EJEMPLOS:
  #   ./init_jupyter_analysis.sh finanzas
  #   ./init_jupyter_analysis.sh duckdb polars duckdb
  #   ./init_jupyter_analysis.sh ml scikit-learn torch

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  # Source funciones atomicas
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/find_free_port.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/init_uv_venv.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/uv_add_packages.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_launcher.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_mcp_jupyter_config.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_claude_jupyter_rules.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_registry_kernel.sh"

  # ── Argumentos ──────────────────────────────────────────────

  NOMBRE="${1:-}"
  if [ -z "$NOMBRE" ]; then
      echo "Uso: $0 <nombre> [paquetes_extra...]" >&2
      echo "  Ejemplo: $0 finanzas polars" >&2
      exit 1
  fi
  shift
  EXTRA_PACKAGES=("$@")

  ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}"

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  INIT JUPYTER ANALYSIS: ${NOMBRE}"
  echo "  Directorio: ${ANALYSIS_DIR}"
  echo "════════════════════════════════════════════════════════════"
  echo ""

  # ── 1. Verificar herramientas ───────────────────────────────

  echo "[1/8] Verificando herramientas..."
  assert_command_exists uv || assert_command_exists python3
  echo "  OK"

  # ── 2. Crear estructura de carpetas ─────────────────────────

  echo "[2/8] Creando estructura..."
  mkdir -p "$ANALYSIS_DIR/notebooks" "$ANALYSIS_DIR/data"
  echo "  ${ANALYSIS_DIR}/notebooks/"
  echo "  ${ANALYSIS_DIR}/data/"

  # ── 3. Crear venv ───────────────────────────────────────────

  echo "[3/8] Inicializando venv..."
  venv_path=$(init_uv_venv "$ANALYSIS_DIR")
  echo "  $venv_path"

  # ── 4. Instalar paquetes ────────────────────────────────────

  echo "[4/8] Instalando paquetes..."
  BASE_PACKAGES=(jupyter jupyterlab jupyter-collaboration jupyter-mcp-server pandas numpy matplotlib)
  ALL_PACKAGES=("${BASE_PACKAGES[@]}" "${EXTRA_PACKAGES[@]}")
  uv_add_packages "$ANALYSIS_DIR" "${ALL_PACKAGES[@]}"
  echo "  Instalados: ${ALL_PACKAGES[*]}"

  # ── 5. Generar launcher ─────────────────────────────────────

  echo "[5/8] Generando launcher..."
  launcher=$(write_jupyter_launcher "$ANALYSIS_DIR")
  echo "  $launcher"

  # ── 6. Configurar MCP ───────────────────────────────────────

  echo "[6/8] Configurando MCP..."
  port=$(find_free_port 8888 8899)
  mcp_config=$(write_mcp_jupyter_config "$ANALYSIS_DIR" "$port")
  echo "  $mcp_config (puerto: $port)"

  # ── 7. Reglas para agentes ──────────────────────────────────

  echo "[7/8] Escribiendo reglas Claude..."
  rules=$(write_claude_jupyter_rules "$ANALYSIS_DIR")
  echo "  $rules"

  # ── 8. Kernel startup con acceso al registry ────────────────

  echo "[8/8] Configurando kernel con acceso al registry..."
  kernel_startup=$(write_jupyter_registry_kernel "$ANALYSIS_DIR")
  echo "  $kernel_startup"

  # ── Resumen ─────────────────────────────────────────────────

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  ANALISIS '${NOMBRE}' LISTO"
  echo "════════════════════════════════════════════════════════════"
  echo ""
  echo "  Pasos siguientes:"
  echo ""
  echo "  1. En otra terminal:"
  echo "     cd ${ANALYSIS_DIR} && ./run-jupyter-lab.sh"
  echo ""
  echo "  2. Abrir Claude en el analisis:"
  echo "     cd ${ANALYSIS_DIR} && claude"
  echo ""
  echo "  3. Abrir en navegador: http://localhost:${port}"
  echo ""
  echo "  FN_REGISTRY_ROOT=${REGISTRY_ROOT}"
  echo ""

params_schema:
  {"params":[{"name":"nombre","desc":"nombre del análisis a crear"},{"name":"paquetes_extra","desc":"paquetes Python adicionales a instalar (variadic, opcional)"}],"output":"sin salida directa; estructura completa en analysis/{nombre}/"}


id: setup_metabase_volume_bash_pipelines
name: setup_metabase_volume
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
tags: ["metabase","docker","setup","launcher","pipeline","bash","infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/setup_metabase_volume.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1ef3a6f96c128f50b07f048588f672cf12c2efe339d2bb00438ab7aa0054674f
signature:
  setup_metabase_volume([registry_db_path: string], [container_name: string], [dest_path: string]) -> void

description:
  Copia registry.db al contenedor Docker de Metabase verificando existencia del archivo, disponibilidad de docker, estado del contenedor y coincidencia de tamaños. Todos los argumentos son opcionales con defaults razonables.

uses_functions:
  ["assert_file_exists_bash_shell","assert_command_exists_bash_shell","assert_docker_container_running_bash_infra","docker_cp_file_bash_infra"]

example:
  ```bash
  # Con defaults
  ./functions/pipelines/setup_metabase_volume.sh

  # Con argumentos explícitos
  ./functions/pipelines/setup_metabase_volume.sh \
    /home/lucas/fn_registry/registry.db \
    metabase \
    /registry.db
  ```

notes:
  El pipeline usa `set -euo pipefail` — cualquier fallo en una función individual detiene la ejecución.

  Las funciones individuales se sourcean desde sus rutas en el registry, relativas a `REGISTRY_ROOT` detectado automáticamente desde la ubicación del script.

  Defaults:
  - `REGISTRY_DB_PATH`: `/home/lucas/fn_registry/registry.db`
  - `CONTAINER_NAME`: `metabase`
  - `DEST_PATH`: `/registry.db`

  Nota de persistencia: `docker cp` copia al contenedor en ejecución. Si el contenedor se reinicia, el archivo se pierde. Para persistencia real, montar el directorio como volumen en docker-compose:

  ```yaml
  volumes:
    - /home/lucas/fn_registry:/fn_registry:ro
  ```

  Y usar `--registry-db-path /fn_registry/registry.db`.

documentation:
  ## Flujo

  1. `assert_file_exists` — verifica que `registry.db` existe localmente y obtiene su tamaño
  2. `assert_command_exists` — verifica que `docker` está disponible en PATH
  3. `assert_docker_container_running` — verifica que el contenedor destino está activo; si falla, muestra lista de contenedores activos
  4. `docker_cp_file` — ejecuta `docker cp` y verifica que los tamaños local y remoto coinciden

code:
  #!/usr/bin/env bash
  # setup_metabase_volume
  # ---------------------
  # Copia registry.db al contenedor Docker de Metabase.
  # Compone: assert_file_exists + assert_command_exists +
  #          assert_docker_container_running + docker_cp_file
  #
  # USO:
  #   ./setup_metabase_volume.sh [REGISTRY_DB_PATH] [CONTAINER_NAME] [DEST_PATH]
  #
  # ARGUMENTOS (opcionales, con defaults):
  #   REGISTRY_DB_PATH  Ruta local al registry.db
  #                     Default: /home/lucas/fn_registry/registry.db
  #   CONTAINER_NAME    Nombre del contenedor Docker de Metabase
  #                     Default: metabase
  #   DEST_PATH         Ruta destino dentro del contenedor
  #                     Default: /registry.db

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/assert_docker_container_running.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/docker_cp_file.sh"

  REGISTRY_DB_PATH="${1:-/home/lucas/fn_registry/registry.db}"
  CONTAINER_NAME="${2:-metabase}"
  DEST_PATH="${3:-/registry.db}"

  echo "[setup_metabase_volume] Configuracion:"
  echo "  registry.db local : $REGISTRY_DB_PATH"
  echo "  contenedor Docker  : $CONTAINER_NAME"
  echo "  ruta en contenedor : $DEST_PATH"
  echo ""

  # 1. Verificar archivo local
  local_size=$(assert_file_exists "$REGISTRY_DB_PATH")
  echo "[setup_metabase_volume] Archivo local encontrado: ${local_size} bytes"

  # 2. Verificar que docker esta disponible
  assert_command_exists docker
  echo "[setup_metabase_volume] docker disponible en PATH."

  # 3. Verificar que el contenedor esta corriendo
  if ! assert_docker_container_running "$CONTAINER_NAME"; then
      echo "" >&2
      echo "Contenedores activos:" >&2
      docker ps --format "  {{.Names}}\t{{.Status}}\t{{.Image}}" >&2
      echo "" >&2
      echo "Si el contenedor se llama diferente, pasa el nombre como segundo argumento:" >&2
      echo "  ./setup_metabase_volume.sh $REGISTRY_DB_PATH <nombre_contenedor>" >&2
      exit 1
  fi
  echo "[setup_metabase_volume] Contenedor '$CONTAINER_NAME' encontrado y activo."

  # 4. Copiar archivo y verificar tamaños
  echo "[setup_metabase_volume] Copiando $REGISTRY_DB_PATH -> ${CONTAINER_NAME}:${DEST_PATH} ..."
  result=$(docker_cp_file "$REGISTRY_DB_PATH" "$CONTAINER_NAME" "$DEST_PATH")

  remote_size=$(echo "$result" | grep -o '"remote_size":[0-9]*' | cut -d: -f2)
  echo "[setup_metabase_volume] OK Copia completada y verificada."
  echo "[setup_metabase_volume] Tamanio: ${local_size} bytes (local) = ${remote_size} bytes (remoto)"

  echo ""
  echo "---------------------------------------------------------------------"
  echo "registry.db disponible en el contenedor como: $DEST_PATH"
  echo ""
  echo "Ahora ejecuta main.py con:"
  echo ""
  echo "  METABASE_ADMIN_PASSWORD=<password> \\"
  echo "  REGISTRY_DB_PATH=${DEST_PATH} \\"
  echo "  python apps/metabase_registry/main.py"
  echo ""
  echo "O bien:"
  echo ""
  echo "  python apps/metabase_registry/main.py \\"
  echo "    --admin-password <password> \\"
  echo "    --registry-db-path ${DEST_PATH}"
  echo "---------------------------------------------------------------------"

params_schema:
  {"params":[{"name":"registry_db_path","desc":"ruta a registry.db local (default: /home/lucas/fn_registry/registry.db)"},{"name":"container_name","desc":"nombre del contenedor Metabase (default: metabase)"},{"name":"dest_path","desc":"ruta destino dentro del contenedor (default: /registry.db)"}],"output":"sin salida directa; verifica transferencia con coincidencia de tamaños"}


id: assert_command_exists_bash_shell
name: assert_command_exists
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: assert_command_exists(command_name: string) -> void
tags: ["assert","command","exists","validation","shell","bash","path"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/assert_command_exists.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5c4e73149cf38e36946509c40ea013491b38c991beb820f767fa14edc66fd615
description:
  Verifica que un comando está disponible en el PATH. Sale con exit code 1 si no se encuentra, con mensaje a stderr.

example:
  ```bash
  source functions/shell/assert_command_exists.sh

  assert_command_exists docker
  assert_command_exists jq
  ```

notes:
  Usa `command -v` (POSIX) con redirección `&>/dev/null` para suprimir output. No produce nada a stdout en caso de éxito.

  Output limpio: void en éxito, mensaje a stderr en fallo.

code:
  # assert_command_exists
  # ---------------------
  # Verifica que un comando está disponible en el PATH.
  # No produce output a stdout.
  # Sale con exit code 1 si el comando no se encuentra.
  #
  # USO (sourced):
  #   source assert_command_exists.sh
  #   assert_command_exists docker

  assert_command_exists() {
      local command_name="$1"

      if ! command -v "$command_name" &>/dev/null; then
          echo "assert_command_exists: comando no encontrado en PATH: $command_name" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"command_name","desc":"nombre del comando a verificar en PATH"}],"output":"sin salida en éxito"}


id: assert_file_exists_bash_shell
name: assert_file_exists
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: assert_file_exists(file_path: string) -> string
tags: ["assert","file","exists","validation","shell","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/assert_file_exists.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 4183e99894fa00ecc4c48143268e35993f5a109593be7edea88ad3b9473ac4b3
description:
  Verifica que un archivo existe en el filesystem. Imprime su tamaño en bytes a stdout. Sale con exit code 1 si el archivo no existe.

example:
  ```bash
  source functions/shell/assert_file_exists.sh

  size=$(assert_file_exists /home/lucas/fn_registry/registry.db)
  echo "Tamaño: $size bytes"
  ```

notes:
  La función se sourcea, no se ejecuta directamente. Usa `stat -c%s` para obtener el tamaño en bytes (compatible con GNU coreutils / Linux).

  Output limpio: solo el número de bytes a stdout. Los errores van a stderr.

  No usa `set -e` internamente — el caller controla el flujo con el exit code de retorno.

code:
  # assert_file_exists
  # ------------------
  # Verifica que un archivo existe en el filesystem.
  # Imprime su tamaño en bytes a stdout.
  # Sale con exit code 1 si el archivo no existe.
  #
  # USO (sourced):
  #   source assert_file_exists.sh
  #   size=$(assert_file_exists /ruta/al/archivo)

  assert_file_exists() {
      local file_path="$1"

      if [ ! -f "$file_path" ]; then
          echo "assert_file_exists: archivo no encontrado: $file_path" >&2
          return 1
      fi

      stat -c%s "$file_path"
  }

params_schema:
  {"params":[{"name":"file_path","desc":"ruta del archivo a verificar"}],"output":"tamaño del archivo en bytes"}


id: bash_check_deps_bash_shell
name: bash_check_deps
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
tags: ["bash","check","dependency","command","exists","validation"]
uses_functions: ["bash_log_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_check_deps.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 07bf7bfb25ab32c999571bdc252acdc4064d9562dec1053705b8057029e4a49e
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
signature:
  check_command(cmd: string, [error_code: string], [description: string]) -> void; check_commands(cmd...: string) -> void; check_directory(dir: string, [msg: string]) -> void; check_file(file: string, [msg: string]) -> void

description:
  Verifica existencia de comandos, directorios y archivos con output formateado. Complementa assert_command_exists con mensajes de error detallados y logging.

example:
  ```bash
  source bash/functions/shell/bash_check_deps.sh

  check_command "docker" "DOCKER_NOT_FOUND" "Docker no esta instalado"
  check_commands "git" "curl" "jq"
  check_directory "/var/data" "El directorio de datos no existe"
  check_file "/etc/config.yaml" "Falta el archivo de configuracion"
  ```

notes:
  `check_command` acepta un error_code y descripcion opcionales para mensajes mas descriptivos que `assert_command_exists`. Usa `debug` internamente para loggear cada verificacion.

  `check_commands` verifica multiples comandos en una sola llamada y reporta todos los faltantes antes de retornar 1.

  Sourcea `bash_log.sh` automaticamente, que a su vez sourcea `bash_colors.sh`. No es necesario sourcea dependencias por separado.

code:
  # bash_check_deps
  # ---------------
  # Verifica existencia de comandos, directorios y archivos.
  # Output formateado con colores via bash_log.
  #
  # USO (sourced):
  #   source bash_check_deps.sh
  #   check_command "docker" "DOCKER_NOT_FOUND" "Docker no esta instalado"
  #   check_commands "git" "curl" "jq"
  #   check_directory "/path/to/dir"
  #   check_file "/path/to/file"

  SCRIPT_DIR_BASH_CHECK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_CHECK/bash_log.sh"
  bash_log_init

  check_command() {
      local cmd="$1"
      local error_code="${2:-COMMAND_NOT_FOUND}"
      local description="${3:-El comando '$cmd' no esta disponible}"

      debug "Verificando comando: $cmd"

      if ! command -v "$cmd" &> /dev/null; then
          error "$description ($error_code)"
          return 1
      fi

      return 0
  }

  check_commands() {
      local failed=0
      for cmd in "$@"; do
          if ! command -v "$cmd" &> /dev/null; then
              error "Comando no encontrado: $cmd"
              failed=1
          fi
      done

      if [ $failed -eq 1 ]; then
          error "Faltan multiples dependencias requeridas"
          return 1
      fi

      return 0
  }

  check_directory() {
      local dir="$1"
      local error_msg="${2:-El directorio '$dir' no existe}"

      debug "Verificando directorio: $dir"

      if [ ! -d "$dir" ]; then
          error "$error_msg"
          return 1
      fi

      return 0
  }

  check_file() {
      local file="$1"
      local error_msg="${2:-El archivo '$file' no existe}"

      debug "Verificando archivo: $file"

      if [ ! -f "$file" ]; then
          error "$error_msg"
          return 1
      fi

      return 0
  }

params_schema:
  {"params":[{"name":"cmd","desc":"nombre del comando a verificar en PATH"},{"name":"error_code","desc":"codigo identificador del error; default COMMAND_NOT_FOUND"},{"name":"description","desc":"mensaje de error personalizado; default 'El comando CMD no esta disponible'"},{"name":"dir","desc":"ruta del directorio a verificar"},{"name":"file","desc":"ruta del archivo a verificar"}],"output":"exit code 0 si todas las verificaciones pasan; exit code 1 en caso de fallo con mensaje de error formateado"}


id: bash_colors_bash_shell
name: bash_colors
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: pure
signature: bash_colors() -> void
tags: ["bash","colors","ansi","terminal","symbols","box"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/bash_colors.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 8e8edd6620894fa6901ecb0882b74fadc6aca6a92080c228a5221dcbc450cc20
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
description:
  Exporta variables ANSI de colores, caracteres box drawing y simbolos unicode para uso en scripts de terminal.

example:
  ```bash
  source bash/functions/shell/bash_colors.sh
  bash_colors

  echo -e "${GREEN}Todo bien${NC}"
  echo -e "${RED}${CROSS} Error detectado${NC}"
  echo -e "${BOX_TL}${BOX_H}${BOX_TR}"
  ```

notes:
  Funcion pura: solo define y exporta variables de entorno, sin I/O ni efectos secundarios. Debe llamarse una vez antes de usar cualquier variable de color o simbolo.

  Variables de color disponibles: PURPLE, MAGENTA, GREEN, BLUE, YELLOW, RED, CYAN, ORANGE, GRAY, DIM_GRAY, BOLD, DIM, NC.

  Variables box drawing: BOX_TL, BOX_TR, BOX_BL, BOX_BR, BOX_H, BOX_V, BOX_ML, BOX_MR, BOX_SEP.

  Simbolos unicode: CHECKMARK, CROSS, ARROW, BULLET, WARNING, INFO.

code:
  # bash_colors
  # -----------
  # Variables ANSI de color, caracteres box drawing y simbolos para scripts.
  # Diseñado para ser sourced por otros scripts.
  #
  # USO (sourced):
  #   source bash_colors.sh
  #   bash_colors

  bash_colors() {
      export PURPLE='\033[35m'
      export MAGENTA='\033[35m'
      export GREEN='\033[0;32m'
      export BLUE='\033[0;34m'
      export YELLOW='\033[1;33m'
      export RED='\033[0;31m'
      export CYAN='\033[0;36m'
      export ORANGE='\033[0;33m'
      export GRAY='\033[0;90m'
      export DIM_GRAY='\033[2;37m'
      export BOLD='\033[1m'
      export DIM='\033[2m'
      export NC='\033[0m'

      export BOX_TL="╔"
      export BOX_TR="╗"
      export BOX_BL="╚"
      export BOX_BR="╝"
      export BOX_H="═"
      export BOX_V="║"
      export BOX_ML="╠"
      export BOX_MR="╣"
      export BOX_SEP="─"

      export CHECKMARK="✓"
      export CROSS="✗"
      export ARROW="→"
      export BULLET="•"
      export WARNING="⚠"
      export INFO="ℹ"
  }

params_schema:
  {"output":"exporta variables de entorno con codigos ANSI: colores (RED, GREEN, BLUE, etc.), box drawing (BOX_TL, BOX_H, etc.) y simbolos (CHECKMARK, CROSS, ARROW, etc.)"}


id: bash_confirm_bash_shell
name: bash_confirm
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: bash_confirm(prompt: string, [default: string]) -> exit_code
tags: ["bash","confirm","prompt","interactive","dialog"]
uses_functions: ["bash_colors_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_confirm.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: 14857288f086d38b417c0d8f0861af195b2320dfe788927f36336027f6bf8694
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
description:
  Dialogo interactivo de confirmacion y/n con valor por defecto configurable. Soporta respuestas yes/y/si.

example:
  ```bash
  source bash/functions/shell/bash_confirm.sh

  # Con default no (muestra [y/N])
  if bash_confirm "¿Deseas continuar?"; then
      echo "Continuando..."
  fi

  # Con default yes (muestra [Y/n])
  bash_confirm "¿Eliminar archivos temporales?" "y" && rm -rf /tmp/cache
  ```

notes:
  El prompt muestra el default en mayuscula: `[Y/n]` cuando default=y, `[y/N]` cuando default=n. Si el usuario presiona Enter sin escribir, se usa el valor por defecto.

  Acepta variantes: y, Y, yes, YES, si, SI como afirmativo. Cualquier otra respuesta se trata como negativo.

  Usa colores de `bash_colors` para el prompt en amarillo. Requiere terminal interactiva (lee de stdin con `read -r`).

code:
  # bash_confirm
  # ------------
  # Dialogo de confirmacion y/n con valor por defecto.
  #
  # USO (sourced):
  #   source bash_confirm.sh
  #   bash_confirm "Continuar?" && echo "Si" || echo "No"
  #   bash_confirm "Eliminar?" "y"  # default yes

  SCRIPT_DIR_BASH_CONFIRM="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_CONFIRM/bash_colors.sh"
  bash_colors

  bash_confirm() {
      local prompt="${1:-¿Continuar?}"
      local default="${2:-n}"

      if [ "$default" = "y" ]; then
          prompt="$prompt [Y/n]"
      else
          prompt="$prompt [y/N]"
      fi

      echo -ne "${YELLOW}$prompt ${NC}"
      read -r response

      response=${response:-$default}
      case "$response" in
          [yY][eE][sS]|[yY]|[sS][iI])
              return 0
              ;;
          *)
              return 1
              ;;
      esac
  }

params_schema:
  {"params":[{"name":"prompt","desc":"pregunta a mostrar al usuario; default '¿Continuar?'"},{"name":"default","desc":"valor por defecto cuando el usuario presiona Enter sin escribir nada; 'y' o 'n'; default 'n'"}],"output":"exit code 0 si el usuario confirma (y/yes/si), exit code 1 si niega o acepta el default negativo"}


id: bash_handle_error_bash_shell
name: bash_handle_error
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: handle_error(error_code: string, error_description: string, [solution: string]) -> exit_code
tags: ["bash","error","handler","box","formatted","context"]
uses_functions: ["bash_colors_bash_shell","bash_log_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_handle_error.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: c94f84e7cc80dcbab8bad556a15013d997365a09c89fe2c8be159d0815d4bcbc
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
description:
  Muestra un box de error formateado con contexto del fallo: script, linea, funcion, directorio y usuario. Registra en log.

example:
  ```bash
  source bash/functions/shell/bash_handle_error.sh

  build_project() {
      go build ./... || handle_error "BUILD_FAILED" \
          "La compilacion del proyecto fallo" \
          "Ejecuta 'go mod tidy' y verifica que todas las dependencias esten instaladas"
  }

  connect_db() {
      psql "$DB_URL" -c '\q' 2>/dev/null || handle_error "DB_CONNECTION_ERROR" \
          "No se pudo conectar a la base de datos"
  }
  ```

notes:
  El box usa caracteres unicode de `bash_colors` (BOX_TL, BOX_H, etc.) para el borde en rojo. La informacion de contexto se extrae de `BASH_SOURCE`, `BASH_LINENO` y `FUNCNAME` con offset 2 para apuntar al caller del caller.

  La funcion siempre retorna 1, permitiendo usarla como `cmd || handle_error ...` en pipelines de error.

  Usa `bash_colors` y `bash_log` como dependencias. Sourcea ambas automaticamente al cargarse.

code:
  # bash_handle_error
  # -----------------
  # Muestra un box de error formateado con contexto del fallo.
  # Incluye script, linea, funcion y directorio.
  #
  # USO (sourced):
  #   source bash_handle_error.sh
  #   handle_error "BUILD_FAILED" "La compilacion fallo" "Verifica las dependencias"

  SCRIPT_DIR_BASH_HANDLE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_HANDLE/bash_colors.sh"
  source "$SCRIPT_DIR_BASH_HANDLE/bash_log.sh"
  bash_colors
  bash_log_init

  handle_error() {
      local error_code="$1"
      local error_description="$2"
      local solution="$3"

      echo ""
      echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
      echo -e "${RED}║                    ${CROSS} ERROR DETECTADO                    ║${NC}"
      echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
      echo ""
      echo -e "${RED}${BOLD}Error:${NC} ${error_description}"
      echo -e "${GRAY}Codigo: ${error_code}${NC}"
      echo ""

      if [ -n "$solution" ]; then
          echo -e "${YELLOW}${INFO} Solucion sugerida:${NC}"
          echo -e "   ${solution}"
          echo ""
      fi

      echo -e "${GRAY}${INFO} Informacion adicional:${NC}"
      echo -e "   ${GRAY}${BULLET} Script: ${BASH_SOURCE[2]:-desconocido}${NC}"
      echo -e "   ${GRAY}${BULLET} Linea: ${BASH_LINENO[1]:-desconocido}${NC}"
      echo -e "   ${GRAY}${BULLET} Funcion: ${FUNCNAME[2]:-main}${NC}"
      echo -e "   ${GRAY}${BULLET} Directorio: $(pwd)${NC}"
      echo -e "   ${GRAY}${BULLET} Usuario: $(whoami)${NC}"
      echo ""

      log "ERROR" "Code: $error_code | Description: $error_description | Script: ${BASH_SOURCE[2]} | Line: ${BASH_LINENO[1]}"

      return 1
  }

params_schema:
  {"params":[{"name":"error_code","desc":"codigo identificador del error, ej: BUILD_FAILED, DB_CONNECTION_ERROR"},{"name":"error_description","desc":"descripcion legible del error para mostrar al usuario"},{"name":"solution","desc":"solucion sugerida; opcional; si se provee se muestra en seccion separada"}],"output":"box de error formateado en stdout con contexto (script, linea, funcion, directorio, usuario) + registro en log; retorna exit code 1"}


id: bash_log_bash_shell
name: bash_log
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
tags: ["bash","log","logging","colors","terminal"]
uses_functions: ["bash_colors_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_log.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: acdf449809f9c33380c6914dbe9995bf1a7613bc800a4e5687c238634cf4e939
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
signature:
  bash_log_init() -> void; success(msg: string) -> void; info(msg: string) -> void; warning(msg: string) -> void; error(msg: string) -> void; debug(msg: string) -> void; progress(msg: string) -> void

description:
  Funciones de logging con colores para scripts bash. Incluye niveles success/info/warning/error/debug/progress con escritura a archivo de log.

example:
  ```bash
  source bash/functions/shell/bash_log.sh
  bash_log_init

  success "Servicio iniciado correctamente"
  info "Procesando 42 registros..."
  warning "El puerto 8080 ya esta en uso"
  error "No se pudo conectar a la base de datos"
  debug "Valor de variable: $VAR"
  progress "Descargando imagen Docker..."
  ```

notes:
  Llama a `bash_log_init` una vez al inicio para configurar ERROR_LOG_FILE y DEBUG_MODE. Por defecto el log va a `/tmp/script-errors-YYYYMMDD.log`.

  La variable de entorno `DEBUG_MODE=1` activa los mensajes de debug a stderr y muestra timestamps en todos los logs.

  `error` escribe a stderr; el resto a stdout. Todos los niveles escriben al archivo de log independientemente de DEBUG_MODE.

  Fuente: sourcea `bash_colors.sh` automaticamente al cargarse.

code:
  # bash_log
  # --------
  # Funciones de logging con colores para scripts.
  # Incluye: log, success, info, warning, error, debug, progress.
  #
  # USO (sourced):
  #   source bash_log.sh
  #   bash_log_init
  #   success "Operacion completada"
  #   info "Procesando..."
  #   error "Algo fallo"

  SCRIPT_DIR_BASH_LOG="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_LOG/bash_colors.sh"
  bash_colors

  bash_log_init() {
      export ERROR_LOG_FILE="${ERROR_LOG_FILE:-/tmp/script-errors-$(date +%Y%m%d).log}"
      export DEBUG_MODE="${DEBUG_MODE:-0}"
  }

  log() {
      local level="$1"
      shift
      local message="$@"
      local timestamp
      timestamp=$(date '+%Y-%m-%d %H:%M:%S')
      echo "[$timestamp] [$level] $message" >> "${ERROR_LOG_FILE:-/tmp/script-errors.log}"

      if [ "${DEBUG_MODE:-0}" = "1" ]; then
          echo -e "${GRAY}[$timestamp] [$level] $message${NC}" >&2
      fi
  }

  success() {
      echo -e "${GREEN}${CHECKMARK} $*${NC}"
      log "SUCCESS" "$*"
  }

  info() {
      echo -e "${BLUE}${INFO} $*${NC}"
      log "INFO" "$*"
  }

  warning() {
      echo -e "${YELLOW}${WARNING} $*${NC}"
      log "WARNING" "$*"
  }

  error() {
      echo -e "${RED}${CROSS} Error: $*${NC}" >&2
      log "ERROR" "$*"
  }

  debug() {
      if [ "${DEBUG_MODE:-0}" = "1" ]; then
          echo -e "${GRAY}[DEBUG] $*${NC}" >&2
      fi
      log "DEBUG" "$*"
  }

  progress() {
      echo -e "${CYAN}${ARROW} $*${NC}"
  }

params_schema:
  {"params":[{"name":"msg","desc":"mensaje a mostrar y registrar en el archivo de log"}],"output":"mensaje formateado con colores en stdout/stderr y registro con timestamp en archivo de log (ERROR_LOG_FILE)"}


id: bash_safe_run_bash_shell
name: bash_safe_run
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
tags: ["bash","safe","run","error","trap","handler"]
uses_functions: ["bash_log_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/bash_safe_run.sh
created_at: 2026-04-08T21:23:05Z
updated_at: 2026-04-08T21:23:05Z
props: []
emits: []
variant: []
content_hash: c9efa39908e0dde3bd6695452def89c00a56d1cb84febd0cfd6650207276248b
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/lib/common.sh
signature:
  safe_run(cmd: string, [error_code: string], [error_desc: string]) -> void; setup_error_trap() -> void; error_trap_handler(exit_code: int, line_number: int) -> void

description:
  Ejecuta comandos con manejo de errores integrado. Incluye trap handler que captura fallos con numero de linea y codigo de salida.

example:
  ```bash
  source bash/functions/shell/bash_safe_run.sh
  bash_log_init
  setup_error_trap

  safe_run "go build ./..." "BUILD_FAILED" "La compilacion fallo"
  safe_run "docker compose up -d" "DOCKER_FAILED" "No se pudo iniciar Docker Compose"
  safe_run "npm install" "NPM_FAILED"
  ```

notes:
  `safe_run` usa `eval` internamente para ejecutar el comando, lo que permite pasar comandos con pipes y redirecciones como string. Usar con precaucion en entornos con input no confiable.

  `setup_error_trap` instala un trap `ERR` que llama a `error_trap_handler` automaticamente en cualquier comando fallido del script, mostrando numero de linea y codigo de salida.

  `error_trap_handler` no llama a `exit` — el caller decide si continuar o abortar. Muestra la ruta al log para debugging.

code:
  # bash_safe_run
  # -------------
  # Ejecutar comandos con manejo de errores y trap.
  # Incluye safe_run, setup_error_trap y error_trap_handler.
  #
  # USO (sourced):
  #   source bash_safe_run.sh
  #   setup_error_trap
  #   safe_run "go build ./..." "BUILD_FAILED" "La compilacion fallo"

  SCRIPT_DIR_BASH_SAFE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  source "$SCRIPT_DIR_BASH_SAFE/bash_log.sh"
  bash_log_init

  safe_run() {
      local cmd="$1"
      local error_code="${2:-COMMAND_FAILED}"
      local error_desc="${3:-El comando fallo: $cmd}"

      debug "Ejecutando: $cmd"

      if ! eval "$cmd"; then
          error "$error_desc ($error_code)"
          return 1
      fi

      return 0
  }

  setup_error_trap() {
      trap 'error_trap_handler $? $LINENO' ERR
  }

  error_trap_handler() {
      local exit_code=$1
      local line_number=$2

      if [ "$exit_code" -ne 0 ]; then
          echo ""
          error "El script fallo en la linea $line_number con codigo de salida $exit_code"
          echo -e "${GRAY}Consulta el log: ${ERROR_LOG_FILE:-/tmp/script-errors.log}${NC}"
          echo ""
      fi
  }

params_schema:
  {"params":[{"name":"cmd","desc":"comando a ejecutar via eval"},{"name":"error_code","desc":"codigo identificador del error en caso de fallo; default COMMAND_FAILED"},{"name":"error_desc","desc":"descripcion del error a mostrar; default 'El comando fallo: CMD'"}],"output":"exit code 0 si el comando tuvo exito; exit code 1 con mensaje de error formateado si fallo"}


id: convert_text_case_bash_shell
name: convert_text_case
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: convert_text_case(mode: string, file: string, [output_file: string]) -> void
tags: ["bash","text","convert","case","encoding"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/convert_text_case.sh
created_at: 2026-04-08T21:29:50Z
updated_at: 2026-04-08T21:29:50Z
props: []
emits: []
variant: []
content_hash: 1df7df6d453898ff7a9158dbb44c977a20a990f1ef515158febfb2b1a60d9c0f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/conversores/conversor_texto.sh
description:
  Convierte el contenido de un archivo de texto. Modos: upper (todo mayúsculas), lower (todo minúsculas), lf (normaliza saltos de línea a LF eliminando \r), crlf (normaliza saltos a CRLF). Sin output_file imprime a stdout.

example:
  ```bash
  source bash/functions/shell/convert_text_case.sh

  # Convertir a mayúsculas y mostrar en stdout
  convert_text_case upper mi_archivo.txt

  # Convertir a minúsculas y guardar en archivo
  convert_text_case lower input.txt output_lower.txt

  # Normalizar saltos de línea CRLF a LF
  convert_text_case lf archivo_windows.txt archivo_unix.txt

  # Añadir CRLF (para Windows)
  convert_text_case crlf unix.txt windows.txt
  ```

notes:
  Usa `awk` para las conversiones de case (portable) y `sed` para los saltos de línea. La función no modifica el archivo original si se provee `output_file`. Sin `output_file` imprime directamente a stdout, útil para pipes.

code:
  #!/usr/bin/env bash
  # convert_text_case
  # -----------------
  # Convierte el contenido de texto de un archivo aplicando transformaciones:
  # upper (mayúsculas), lower (minúsculas), lf (normalizar saltos a LF),
  # crlf (normalizar saltos a CRLF).
  #
  # USO:
  #   source convert_text_case.sh
  #   convert_text_case mode file [output_file]
  #
  # ARGUMENTOS:
  #   mode         Transformación a aplicar: upper|lower|lf|crlf (requerido)
  #   file         Archivo de entrada (requerido)
  #   output_file  Archivo de salida (opcional; por defecto imprime a stdout)

  convert_text_case() {
      local mode="${1:-}"
      local input_file="${2:-}"
      local output_file="${3:-}"

      # Validar argumentos requeridos
      if [[ -z "$mode" ]]; then
          echo "convert_text_case: modo requerido (upper|lower|lf|crlf)" >&2
          return 1
      fi

      if [[ -z "$input_file" ]]; then
          echo "convert_text_case: archivo de entrada requerido" >&2
          return 1
      fi

      if [[ ! -f "$input_file" ]]; then
          echo "convert_text_case: archivo no encontrado: ${input_file}" >&2
          return 1
      fi

      # Función auxiliar que aplica la conversión y escribe a stdout o archivo
      _apply_conversion() {
          local src="$1"
          local op="$2"
          case "$op" in
              upper)
                  awk '{ print toupper($0) }' "$src"
                  ;;
              lower)
                  awk '{ print tolower($0) }' "$src"
                  ;;
              lf)
                  sed 's/\r$//' "$src"
                  ;;
              crlf)
                  sed 's/\r$//' "$src" | sed 's/$/\r/'
                  ;;
              *)
                  echo "convert_text_case: modo no soportado: ${op}. Usa: upper|lower|lf|crlf" >&2
                  return 1
                  ;;
          esac
      }

      if [[ -n "$output_file" ]]; then
          _apply_conversion "$input_file" "$mode" > "$output_file"
          echo "convert_text_case: ${input_file} → ${output_file} (modo: ${mode})"
      else
          _apply_conversion "$input_file" "$mode"
      fi
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      convert_text_case "$@"
  fi

params_schema:
  {"params":[{"name":"mode","desc":"transformación a aplicar: upper|lower|lf|crlf (requerido)"},{"name":"file","desc":"ruta al archivo de entrada (requerido)"},{"name":"output_file","desc":"ruta al archivo de salida (opcional; default: stdout)"}],"output":"texto convertido a stdout o escrito en output_file; exit code 1 si el modo o archivo son inválidos"}


id: create_project_structure_bash_shell
name: create_project_structure
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: create_project_structure(project_name: string) -> void
tags: ["bash","project","structure","scaffold","init"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/create_project_structure.sh
created_at: 2026-04-08T21:29:50Z
updated_at: 2026-04-08T21:29:50Z
props: []
emits: []
variant: []
content_hash: 7ecd70311b4e366a9ce1c21832739ab6b1a6cca35aa30576e8510b7f57b64c25
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/inicializar_repos/functional_structure.sh
description:
  Crea la estructura de directorios estándar de un proyecto funcional en el directorio indicado: database (attachments, data, models), dist (desktop/mobile/docker), docker, docs, frontend, logs, notebooks, robots, scripts, src (application/core/middleware con tests y tipos).

example:
  ```bash
  source bash/functions/shell/create_project_structure.sh

  # Crear estructura en un directorio nuevo
  create_project_structure mi-proyecto

  # Usar una ruta
  create_project_structure /home/user/projects/nuevo-proyecto
  ```

notes:
  Crea 68 directorios organizados en la estructura funcional de DevLauncher. La estructura separa claramente application (orquestación), core (lógica pura) y middleware (efectos/I/O) bajo `src/`. No crea archivos, solo directorios. Idempotente: si los directorios ya existen, no falla.

code:
  #!/usr/bin/env bash
  # create_project_structure
  # ------------------------
  # Crea la estructura de directorios estándar de un proyecto funcional
  # (database, dist, docker, docs, frontend, logs, notebooks, robots,
  # scripts, src con application/core/middleware) en el directorio indicado.
  #
  # USO:
  #   source create_project_structure.sh
  #   create_project_structure project_name
  #
  # ARGUMENTOS:
  #   project_name   Nombre del proyecto / ruta destino (requerido)

  create_project_structure() {
      local project_name="${1:-}"

      if [[ -z "$project_name" ]]; then
          echo "create_project_structure: se requiere el nombre del proyecto" >&2
          echo "  Uso: create_project_structure <project_name>" >&2
          return 1
      fi

      local destino="$project_name"

      local directories=(
          "database"
          "database/attachments"
          "database/attachments/audio"
          "database/attachments/docs"
          "database/attachments/images"
          "database/attachments/video"
          "database/data"
          "database/data/01_raw"
          "database/data/02_clean"
          "database/data/03_objects"
          "database/data/04_answers"
          "database/models"
          "dist"
          "dist/desktop"
          "dist/desktop/linux"
          "dist/desktop/mac"
          "dist/desktop/win"
          "dist/docker"
          "dist/mobile"
          "dist/mobile/android"
          "dist/mobile/ios"
          "docker"
          "docs"
          "docs/documentation"
          "docs/pdf"
          "frontend"
          "logs"
          "notebooks"
          "robots"
          "scripts"
          "src"
          "src/application"
          "src/application/event"
          "src/application/jobs"
          "src/application/pipes"
          "src/application/sandbox"
          "src/application/simulation"
          "src/application/tasks"
          "src/application/tests"
          "src/application/tests/benchmark"
          "src/application/tests/fuzzers"
          "src/application/tests/mocks"
          "src/application/tests/mutation"
          "src/core"
          "src/core/base"
          "src/core/functions"
          "src/core/rules"
          "src/core/tests"
          "src/core/tests/benchmark"
          "src/core/tests/fuzzers"
          "src/core/tests/mocks"
          "src/core/tests/mutation"
          "src/core/types"
          "src/core/utils"
          "src/middleware"
          "src/middleware/api"
          "src/middleware/backend"
          "src/middleware/browser"
          "src/middleware/config"
          "src/middleware/connection"
          "src/middleware/controller"
          "src/middleware/i18n"
          "src/middleware/interfaces"
          "src/middleware/queue"
          "src/middleware/servers"
          "src/middleware/tests"
          "src/middleware/tests/benchmark"
          "src/middleware/tests/fuzzers"
          "src/middleware/tests/mocks"
          "src/middleware/tests/mutation"
          "src/middleware/types"
          "src/modules"
      )

      echo "Creando estructura del proyecto: ${destino}"

      for dir in "${directories[@]}"; do
          mkdir -p "${destino%/}/${dir}"
      done

      echo "Estructura creada en: ${destino%/}"
      echo "  ${#directories[@]} directorios creados."
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      create_project_structure "$@"
  fi

params_schema:
  {"params":[{"name":"project_name","desc":"nombre del proyecto o ruta destino donde crear la estructura (requerido)"}],"output":"crea los directorios a stdout con conteo final; exit code 1 si no se proporciona nombre"}


id: exit_with_status_bash_shell
name: exit_with_status
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: pure
signature: exit_with_status(total_steps: int, ok_steps: int, failed_steps: int) -> int
tags: ["execution","status","exit-code","standard"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/exit_with_status.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3c6882702f826c950f90bf52e72130faf89681f0866e65e8653312e779a3075e
description:
  Calcula el exit code estandar (0=success, 1=failure, 2=partial) a partir de contadores de pasos. Si failed_steps=0 imprime 0 y sale con 0. Si ok_steps=0 imprime 1 y sale con 1. Si hay ambos imprime 2 y sale con 2.

example:
  ```bash
  source bash/functions/shell/exit_with_status.sh

  exit_with_status 5 5 0  # stdout: 0, exit code: 0
  exit_with_status 5 0 5  # stdout: 1, exit code: 1
  exit_with_status 5 3 2  # stdout: 2, exit code: 2
  ```

notes:
  Funcion pura: no realiza I/O de sistema, no modifica estado global, no lee variables de entorno. El argumento `total_steps` se recibe para completitud semantica pero la logica solo depende de `ok_steps` y `failed_steps`. El valor se imprime a stdout ademas de usarse como exit code, de modo que el caller puede capturarlo con `$(exit_with_status ...)` o evaluar directamente con `exit_with_status ... ; echo $?`.

code:
  #!/usr/bin/env bash
  # exit_with_status — calcula el exit code estandar a partir de contadores de pasos

  # exit_with_status <total_steps> <ok_steps> <failed_steps>
  #
  # Calcula el exit code estandar:
  #   0 — todos los pasos exitosos (failed_steps == 0)
  #   1 — todos los pasos fallaron (ok_steps == 0)
  #   2 — resultado parcial (hay ok y failed)
  #
  # Imprime el codigo a stdout y sale con ese codigo.
  exit_with_status() {
      local total_steps="$1"
      local ok_steps="$2"
      local failed_steps="$3"

      local code

      if [[ "$failed_steps" -eq 0 ]]; then
          code=0
      elif [[ "$ok_steps" -eq 0 ]]; then
          code=1
      else
          code=2
      fi

      echo "$code"
      return "$code"
  }

params_schema:
  {"params":[{"name":"total_steps","desc":"número total de pasos ejecutados"},{"name":"ok_steps","desc":"número de pasos exitosos"},{"name":"failed_steps","desc":"número de pasos fallidos"}],"output":"exit code: 0 (éxito), 1 (fallo), o 2 (parcial)"}


id: find_free_port_bash_shell
name: find_free_port
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: find_free_port([start_port: int], [end_port: int]) -> int
tags: ["network","port","shell","utility"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/find_free_port.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b89bacb32e1a6293e15b43b58a72c92587d3c5b30e1ac95cda12356948561e82
description:
  Busca el primer puerto TCP libre en un rango dado usando ss y lsof. Retorna el numero de puerto a stdout.

example:
  ```bash
  source find_free_port.sh
  port=$(find_free_port 8888 8899)
  echo "Puerto libre: $port"

  # Con defaults (8888-8899)
  port=$(find_free_port)
  ```

notes:
  Usa `ss -tln` como primer intento y `lsof` como fallback. Ambos deben confirmar que el puerto no esta en uso. Si ningun puerto esta libre en el rango, sale con exit code 1.

code:
  # find_free_port
  # ---------------
  # Busca el primer puerto libre en un rango dado.
  # Imprime el puerto encontrado a stdout.
  # Sale con exit code 1 si no encuentra ninguno.
  #
  # USO (sourced):
  #   source find_free_port.sh
  #   port=$(find_free_port 8888 8899)

  find_free_port() {
      local start="${1:-8888}"
      local end="${2:-8899}"

      for ((port=start; port<=end; port++)); do
          if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
             ! lsof -i:"$port" >/dev/null 2>&1; then
              echo "$port"
              return 0
          fi
      done

      echo "find_free_port: no se encontro puerto libre en rango ${start}-${end}" >&2
      return 1
  }

params_schema:
  {"params":[{"name":"start_port","desc":"puerto inicial del rango (default: 8888)"},{"name":"end_port","desc":"puerto final del rango (default: 8899)"}],"output":"número de puerto TCP libre"}


id: git_clean_branches_bash_shell
name: git_clean_branches
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_clean_branches([base_branch: string], [--remote], [--force]) -> void
tags: ["bash","git","branches","clean","merge"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_clean_branches.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: e15fdf0e7a18d3c6349f8fa942ce387b0f8319fa6390fc9b32e7b6d6e9a4bcc5
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/limpiar_ramas.sh
description:
  Elimina ramas locales ya mergeadas en la rama base (autodetecta main/master). Con --remote también borra las ramas en todos los remotes. Con --force omite la confirmación interactiva.

example:
  ```bash
  source bash/functions/shell/git_clean_branches.sh

  # Limpieza básica con confirmación
  git_clean_branches

  # Sin confirmación, sobre rama base "develop"
  git_clean_branches develop --force

  # Eliminar también en remotes
  git_clean_branches main --remote --force
  ```

notes:
  Ramas protegidas que nunca se eliminan: `main`, `master`, `develop`, `dev`, `staging`, `release`. Siempre hace prune de referencias remotas obsoletas antes de buscar candidatos.

code:
  #!/usr/bin/env bash
  # git_clean_branches
  # ------------------
  # Elimina ramas locales ya mergeadas en la rama base (main/master por defecto).
  # Opcionalmente también elimina las ramas en remotes.
  #
  # USO:
  #   source git_clean_branches.sh
  #   git_clean_branches [base_branch] [--remote] [--force]
  #
  # ARGUMENTOS:
  #   base_branch  Rama base (opcional; autodetecta main/master si se omite)
  #   --remote     Elimina también las ramas en todos los remotes
  #   --force      Omite confirmación interactiva

  git_clean_branches() {
      local base_branch=""
      local delete_remote=false
      local force=false

      # Parsear argumentos
      for arg in "$@"; do
          case "$arg" in
              --remote) delete_remote=true ;;
              --force)  force=true ;;
              *)        [[ -z "$base_branch" ]] && base_branch="$arg" ;;
          esac
      done

      # Validar repo
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_clean_branches: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      # Detectar rama base
      if [[ -z "$base_branch" ]]; then
          if git show-ref --verify --quiet refs/heads/main; then
              base_branch="main"
          elif git show-ref --verify --quiet refs/heads/master; then
              base_branch="master"
          else
              echo "git_clean_branches: no se detectó main/master; pasa la rama base como argumento" >&2
              return 1
          fi
      fi

      echo "git_clean_branches: rama base = ${base_branch}"

      # Hacer prune de referencias remotas obsoletas
      echo "Haciendo prune de referencias remotas..."
      git remote | while IFS= read -r remote; do
          git remote prune "$remote" 2>/dev/null && echo "  Pruned: ${remote}"
      done
      echo ""

      # Buscar ramas locales mergeadas
      local candidates
      candidates="$(git branch --merged "$base_branch" \
          | grep -v -E "^\*|^\s*(main|master|develop|dev|staging|release)$" \
          | sed 's/^[[:space:]]*//' \
          | grep -v "^$" || true)"

      if [[ -z "$candidates" ]]; then
          echo "No hay ramas locales mergeadas para eliminar."
          return 0
      fi

      echo "Ramas candidatas a eliminar:"
      echo "$candidates" | while IFS= read -r b; do
          echo "  - ${b}"
      done
      echo ""

      # Confirmación (omitir si --force)
      if [[ "$force" != true ]]; then
          echo -n "¿Eliminar estas ramas locales? [y/N] "
          read -r answer
          [[ "$answer" != "y" && "$answer" != "Y" ]] && { echo "Operación cancelada."; return 0; }
      fi

      # Eliminar ramas locales
      local deleted=0
      local failed=0
      while IFS= read -r branch; do
          if git branch -d "$branch"; then
              echo "  Eliminada local: ${branch}"
              deleted=$((deleted + 1))
          else
              echo "  No se pudo eliminar: ${branch}" >&2
              failed=$((failed + 1))
          fi
      done <<< "$candidates"

      echo ""
      echo "Resumen: ${deleted} eliminada(s), ${failed} fallida(s)"

      # Eliminar en remotes si se solicitó
      if [[ "$delete_remote" == true ]]; then
          local remotes
          remotes="$(git remote 2>/dev/null || true)"
          if [[ -n "$remotes" ]]; then
              echo ""
              echo "Eliminando ramas en remotes..."
              while IFS= read -r branch; do
                  while IFS= read -r remote; do
                      if git ls-remote --heads "$remote" "$branch" 2>/dev/null | grep -q "$branch"; then
                          if git push "$remote" --delete "$branch"; then
                              echo "  Eliminada remota: ${remote}/${branch}"
                          else
                              echo "  No se pudo eliminar remota: ${remote}/${branch}" >&2
                          fi
                      fi
                  done <<< "$remotes"
              done <<< "$candidates"
          fi
      fi

      echo ""
      echo "Limpieza completada."
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_clean_branches "$@"
  fi

params_schema:
  {"params":[{"name":"base_branch","desc":"rama base para detectar merges (opcional; autodetecta main o master)"},{"name":"--remote","desc":"flag para también eliminar ramas en todos los remotes"},{"name":"--force","desc":"flag para omitir confirmación interactiva"}],"output":"lista de ramas eliminadas a stdout; exit code 1 si no es repo Git o no se detecta la rama base"}


id: git_log_visual_bash_shell
name: git_log_visual
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_log_visual([--count N: int], [--author X: string], [--since D: string], [--all]) -> void
tags: ["bash","git","log","history","graph"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_log_visual.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 9390c00f79081d7797e1931ddc82b3101a80ebf260dec78265ba92aed2ffc70f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/historial_commits.sh
description:
  Muestra el historial de commits Git con grafo visual, colores y formato legible (hash, fecha, autor, mensaje, ramas). Soporta filtros por cantidad, autor, fecha y modo todas-las-ramas. Pagina con less.

example:
  ```bash
  source bash/functions/shell/git_log_visual.sh

  # Últimos 20 commits (default)
  git_log_visual

  # Últimos 50 commits de todas las ramas
  git_log_visual --count 50 --all

  # Commits de un autor esta semana
  git_log_visual --author "lucas" --since "1 week ago"

  # Commits de hoy en todas las ramas
  git_log_visual --since "00:00:00" --all --count 100
  ```

notes:
  Usa `less -R --quit-if-one-screen` para paginar: si el log cabe en pantalla, no abre el paginador. El formato incluye hash corto (amarillo), fecha (cyan), autor (verde), mensaje y refs de ramas (rojo).

code:
  #!/usr/bin/env bash
  # git_log_visual
  # --------------
  # Muestra el historial de commits con grafo visual, colores y formato legible.
  # Soporta filtros por cantidad, autor, fecha y todas las ramas.
  #
  # USO:
  #   source git_log_visual.sh
  #   git_log_visual [--count N] [--author X] [--since D] [--all]
  #
  # ARGUMENTOS:
  #   --count N    Número de commits a mostrar (default: 20)
  #   --author X   Filtrar por nombre o email del autor
  #   --since D    Mostrar commits desde fecha (ej: "1 week ago", "2025-01-01")
  #   --all        Incluir todas las ramas

  git_log_visual() {
      local count=20
      local author=""
      local since=""
      local all_branches=false

      # Parsear argumentos
      while [[ $# -gt 0 ]]; do
          case "$1" in
              --count|-n)
                  count="$2"
                  shift 2
                  ;;
              --author|-a)
                  author="$2"
                  shift 2
                  ;;
              --since|-s)
                  since="$2"
                  shift 2
                  ;;
              --all)
                  all_branches=true
                  shift
                  ;;
              *)
                  shift
                  ;;
          esac
      done

      # Validar repo
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_log_visual: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      local log_format="%C(yellow)%h%C(reset) %C(cyan)%ad%C(reset) %C(green)%an%C(reset) %s%C(red)%d%C(reset)"

      # Construir argumentos del comando
      local args=()
      args+=("--graph")
      args+=("--color=always")
      args+=("--date=short")
      args+=("--format=${log_format}")
      args+=("-n" "${count}")

      [[ -n "$author" ]] && args+=("--author=${author}")
      [[ -n "$since" ]]  && args+=("--since=${since}")
      [[ "$all_branches" == true ]] && args+=("--all")

      # Ejecutar
      git log "${args[@]}" 2>/dev/null | less -R --quit-if-one-screen
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_log_visual "$@"
  fi

params_schema:
  {"params":[{"name":"--count","desc":"número de commits a mostrar (default: 20)"},{"name":"--author","desc":"filtrar por nombre o email del autor (parcial, igual que git --author)"},{"name":"--since","desc":"mostrar solo commits desde esta fecha (ej: '1 week ago', '2025-01-01')"},{"name":"--all","desc":"incluir commits de todas las ramas, no solo la actual"}],"output":"historial de commits paginado con less -R; exit code 1 si no es un repo Git"}


id: git_push_all_remotes_bash_shell
name: git_push_all_remotes
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_push_all_remotes([--message msg: string]) -> void
tags: ["bash","git","push","remote","all"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_push_all_remotes.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 1cbae6159ea94b009645630eb3376359421e0fb42d18331226914e8d0651f29e
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/push_todos_remotes.sh
description:
  Hace commit de todos los cambios pendientes (git add -A + git commit) y pushea la rama actual a todos los remotes configurados. Si no hay cambios solo pushea. Requiere --message si hay cambios sin commitear.

example:
  ```bash
  source bash/functions/shell/git_push_all_remotes.sh

  # Solo push (sin cambios pendientes)
  git_push_all_remotes

  # Commit + push a todos los remotes
  git_push_all_remotes --message "feat: nueva funcionalidad"
  ```

notes:
  Usa `--set-upstream` automáticamente si la rama no existe en el remote. Sale con exit code 1 si hay cambios pero no se pasa `--message`, o si algún push falla.

code:
  #!/usr/bin/env bash
  # git_push_all_remotes
  # --------------------
  # Hace commit de todos los cambios pendientes (si los hay) y pushea
  # a todos los remotes configurados en el repositorio.
  #
  # USO:
  #   source git_push_all_remotes.sh
  #   git_push_all_remotes [--message "mensaje"]
  #
  # ARGUMENTOS:
  #   --message "msg"  Mensaje de commit (si hay cambios). Si se omite y hay
  #                    cambios, sale con error.

  git_push_all_remotes() {
      local commit_msg=""

      # Parsear argumentos
      while [[ $# -gt 0 ]]; do
          case "$1" in
              --message|-m)
                  commit_msg="$2"
                  shift 2
                  ;;
              *)
                  shift
                  ;;
          esac
      done

      # Validar repo
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_push_all_remotes: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      local branch
      branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"

      local remotes
      remotes="$(git remote 2>/dev/null || true)"

      if [[ -z "$remotes" ]]; then
          echo "git_push_all_remotes: no hay remotes configurados en este repositorio" >&2
          return 1
      fi

      echo "Rama actual:  ${branch}"
      echo "Remotes:"
      echo "$remotes" | while IFS= read -r r; do
          local url
          url="$(git remote get-url "$r" 2>/dev/null || echo "?")"
          echo "  ${r} → ${url}"
      done
      echo ""

      # Gestión del commit si hay cambios
      local has_changes=false
      if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
          has_changes=true
      fi

      if [[ "$has_changes" == true ]]; then
          if [[ -z "$commit_msg" ]]; then
              echo "git_push_all_remotes: hay cambios pero no se proporcionó --message" >&2
              echo "  Usa: git_push_all_remotes --message \"tu mensaje\"" >&2
              return 1
          fi

          echo "Cambios detectados:"
          git status --short | while IFS= read -r line; do
              echo "  ${line}"
          done
          echo ""

          echo "Añadiendo todos los cambios..."
          git add -A

          echo "Haciendo commit: \"${commit_msg}\""
          git commit -m "$commit_msg"
          echo ""
      else
          echo "Directorio de trabajo limpio. Se empujarán commits existentes."
          echo ""
      fi

      # Push a cada remote
      local ok_count=0
      local fail_count=0
      local failed_remotes=()
      local remote_count
      remote_count="$(echo "$remotes" | wc -l | tr -d ' ')"

      while IFS= read -r remote; do
          # Verificar si la rama existe en el remote
          local push_flags=""
          if ! git ls-remote --heads "$remote" "$branch" 2>/dev/null | grep -q "$branch"; then
              push_flags="--set-upstream"
              echo "La rama '${branch}' no existe en '${remote}', se creará"
          fi

          echo "Push → ${remote} (${branch})..."
          # shellcheck disable=SC2086
          if git push $push_flags "$remote" "$branch" 2>&1; then
              echo "  Push completado → ${remote}/${branch}"
              ok_count=$((ok_count + 1))
          else
              echo "  Falló el push a ${remote}/${branch}" >&2
              fail_count=$((fail_count + 1))
              failed_remotes+=("$remote")
          fi
          echo ""
      done <<< "$remotes"

      echo "=== Resumen ==="
      echo "Push completado en ${ok_count}/${remote_count} remote(s)"

      if [[ $fail_count -gt 0 ]]; then
          echo "Fallaron ${fail_count} remote(s): ${failed_remotes[*]}" >&2
          return 1
      fi
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_push_all_remotes "$@"
  fi

params_schema:
  {"params":[{"name":"--message","desc":"mensaje de commit a usar si hay cambios pendientes (requerido cuando hay cambios)"}],"output":"progreso del push a stdout por cada remote; exit code 1 si algún push falla"}


id: git_repo_status_bash_shell
name: git_repo_status
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: git_repo_status() -> void
tags: ["bash","git","status","repo","branch"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/git_repo_status.sh
created_at: 2026-04-08T21:27:54Z
updated_at: 2026-04-08T21:27:54Z
props: []
emits: []
variant: []
content_hash: 2ad1031d4d97162d28d9a2870366de255bf7c4a528d8a9bd5c18ddb25a60d0a1
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git
source_license: MIT
source_file: scripts/linux/git_utils/estado_repo.sh
description:
  Muestra el estado completo de un repositorio Git: rama actual, upstream (ahead/behind), cambios pendientes, stash, remotes y últimos 8 commits. Sale con exit code 1 si el directorio actual no es un repo Git.

example:
  ```bash
  # Desde un directorio con repo git
  cd /home/user/my-project
  source bash/functions/shell/git_repo_status.sh
  git_repo_status
  ```

notes:
  No requiere dependencias externas más allá de git. Los colores del log de commits usan `--color=always` de git directamente. No produce output en stdout en caso de error — los mensajes de error van a stderr.

code:
  #!/usr/bin/env bash
  # git_repo_status
  # ---------------
  # Muestra el estado completo de un repositorio Git: rama actual, upstream,
  # cambios pendientes, stash, remotes y últimos commits.
  # Sale con exit code 1 si el directorio actual no es un repositorio Git.
  #
  # USO:
  #   source git_repo_status.sh
  #   git_repo_status
  #
  # O como script directo:
  #   bash git_repo_status.sh

  git_repo_status() {
      if ! git rev-parse --is-inside-work-tree &>/dev/null; then
          echo "git_repo_status: el directorio actual no es un repositorio Git" >&2
          return 1
      fi

      local branch
      branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"

      # Upstream
      local upstream
      upstream="$(git rev-parse --abbrev-ref "${branch}@{upstream}" 2>/dev/null || echo "")"
      local upstream_info
      if [[ -z "$upstream" ]]; then
          upstream_info="sin upstream"
      else
          local ahead behind
          ahead="$(git rev-list "${upstream}..HEAD" --count 2>/dev/null || echo 0)"
          behind="$(git rev-list "HEAD..${upstream}" --count 2>/dev/null || echo 0)"
          upstream_info="↑${ahead} ↓${behind} (${upstream})"
      fi

      echo "=== Rama & Upstream ==="
      echo "  Rama actual:  ${branch}"
      echo "  Upstream:     ${upstream_info}"
      echo ""

      # Cambios
      echo "=== Cambios ==="
      local changes
      changes="$(git status --short 2>/dev/null)"
      if [[ -z "$changes" ]]; then
          echo "  Directorio de trabajo limpio"
      else
          echo "  Cambios pendientes:"
          echo "$changes" | while IFS= read -r line; do
              echo "    ${line}"
          done
      fi
      echo ""

      # Stash
      echo "=== Stash ==="
      local stash_count
      stash_count="$(git stash list 2>/dev/null | wc -l | tr -d ' ')"
      if [[ "$stash_count" -eq 0 ]]; then
          echo "  Sin entradas en stash"
      else
          echo "  ${stash_count} entrada(s) en stash:"
          git stash list | head -5 | while IFS= read -r line; do
              echo "    ${line}"
          done
      fi
      echo ""

      # Remotes
      echo "=== Remotes ==="
      local remotes
      remotes="$(git remote -v 2>/dev/null | grep '(fetch)' || true)"
      if [[ -z "$remotes" ]]; then
          echo "  Sin remotes configurados"
      else
          echo "$remotes" | while IFS= read -r line; do
              local name url
              name="$(echo "$line" | awk '{print $1}')"
              url="$(echo "$line" | awk '{print $2}')"
              echo "  ${name} → ${url}"
          done
      fi
      echo ""

      # Últimos commits
      echo "=== Últimos commits ==="
      git log --oneline --decorate --color=always -8 2>/dev/null || true
      echo ""
  }

  # Ejecutar si se invoca directamente
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      git_repo_status "$@"
  fi

params_schema:
  {"params":[{"name":"(ninguno)","desc":"opera sobre el directorio de trabajo actual (cwd)"}],"output":"imprime el estado completo del repo a stdout; exit code 1 si no es un repo Git"}


id: report_execution_json_bash_shell
name: report_execution_json
kind: function
lang: bash
domain: shell
version: 2.0.0
purity: pure
tags: ["execution","json","report","standard","shell","pure"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/report_execution_json.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d069da827fcca273c1d63cc45ddb2f3fccd4240f176f870a45843381f87fa1be
signature:
  report_execution_json(flow_name: string, status: string, exit_code: int, started_at: string, ended_at: string, duration_ms: int, steps_file: string) -> string

description:
  Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry (docs/execution_standard.md). Recibe los metadatos del flujo y un archivo TSV con resultados de pasos (columnas: name, action, status, elapsed_ms, output, error). Imprime el JSON completo a stdout. Usa jq si esta disponible, con fallback a printf. Funcion pura: sin efectos secundarios ni I/O adicional.

example:
  ```bash
  source bash/functions/shell/report_execution_json.sh

  # Crear archivo de pasos (TSV sin cabecera, 6 columnas)
  cat > /tmp/steps.tsv <<'EOF'
  check_db	command	ok	10	exists
  backup	command	ok	450	done
  push	command	error	2540		remote rejected
  EOF

  report_execution_json \
      "backup_db" "partial" 2 \
      "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 \
      /tmp/steps.tsv
  ```

  Output esperado:

  ```json
  {"name":"backup_db","status":"partial","exit_code":2,"started_at":"2026-04-01T02:00:00Z","ended_at":"2026-04-01T02:00:03Z","duration_ms":3000,"steps_total":3,"steps_ok":2,"steps_failed":1,"steps":[{"name":"check_db","action":"command","status":"ok","elapsed_ms":10,"output":"exists"},{"name":"backup","action":"command","status":"ok","elapsed_ms":450,"output":"done"},{"name":"push","action":"command","status":"error","elapsed_ms":2540,"error":"remote rejected"}]}
  ```

notes:
  Funcion pura: solo lee el archivo de pasos y escribe JSON a stdout, sin efectos secundarios ni I/O adicional mas alla de leer el steps_file.

  Formato TSV: 6 columnas separadas por tabulador real (sin cabecera): name, action, status (ok|error), elapsed_ms, output, error. Los campos output y error pueden estar vacios; se omiten del JSON si no tienen valor, siguiendo el estandar de output estructurado.

  El caller es responsable de calcular status (success|failure|partial) y exit_code (0|1|2) segun las reglas del estandar. Ver exit_with_status_bash_shell para esa logica.

  Compatibilidad dual: con jq construye el JSON de forma robusta (maneja caracteres especiales y saltos de linea en output/error). Sin jq, el fallback con printf escapa backslash, comillas dobles y caracteres de control basicos.

  Puede ejecutarse directamente: `bash report_execution_json.sh <args>`.

code:
  #!/usr/bin/env bash
  # report_execution_json — Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry.
  #
  # USO (sourced):
  #   source report_execution_json.sh
  #   report_execution_json "backup_db" "partial" 2 \
  #       "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
  #
  # USO (ejecutado directamente):
  #   bash report_execution_json.sh "backup_db" "partial" 2 \
  #       "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
  #
  # FORMATO steps_file (TSV sin cabecera, 6 columnas):
  #   name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
  #   Los campos output y error pueden estar vacios.
  #   status valido: ok | error
  #
  # NOTA sobre IFS y tabs: bash trata tab como whitespace en IFS y colapsa
  # campos vacios consecutivos. Se usa 'cut -f N' para parsear cada columna
  # de forma correcta cuando hay campos vacios entre tabs.

  report_execution_json() {
      local flow_name="$1"
      local status="$2"
      local exit_code="$3"
      local started_at="$4"
      local ended_at="$5"
      local duration_ms="$6"
      local steps_file="$7"

      if [[ -z "$flow_name" || -z "$status" || -z "$exit_code" || \
            -z "$started_at" || -z "$ended_at" || -z "$duration_ms" || \
            -z "$steps_file" ]]; then
          echo "report_execution_json: uso: report_execution_json <flow_name> <status> <exit_code> <started_at> <ended_at> <duration_ms> <steps_file>" >&2
          return 1
      fi

      if [[ ! -f "$steps_file" ]]; then
          echo "report_execution_json: archivo de pasos no encontrado: $steps_file" >&2
          return 1
      fi

      if command -v jq >/dev/null 2>&1; then
          _report_execution_json_jq \
              "$flow_name" "$status" "$exit_code" \
              "$started_at" "$ended_at" "$duration_ms" "$steps_file"
      else
          _report_execution_json_printf \
              "$flow_name" "$status" "$exit_code" \
              "$started_at" "$ended_at" "$duration_ms" "$steps_file"
      fi
  }

  # --- Implementacion con jq ---

  _report_execution_json_jq() {
      local flow_name="$1" status="$2" exit_code="$3"
      local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"

      local steps_ok=0 steps_failed=0
      local steps_json="[]"

      while IFS= read -r line; do
          [[ -z "$line" ]] && continue

          local s_name s_action s_status s_elapsed s_output s_error
          s_name=$(printf '%s' "$line" | cut -f1)
          s_action=$(printf '%s' "$line" | cut -f2)
          s_status=$(printf '%s' "$line" | cut -f3)
          s_elapsed=$(printf '%s' "$line" | cut -f4)
          s_output=$(printf '%s' "$line" | cut -f5)
          s_error=$(printf '%s' "$line" | cut -f6)

          [[ -z "$s_name" ]] && continue

          local step_obj
          step_obj=$(jq -n \
              --arg   name     "$s_name" \
              --arg   action   "$s_action" \
              --arg   st       "$s_status" \
              --argjson ms     "${s_elapsed:-0}" \
              --arg   output   "$s_output" \
              --arg   error    "$s_error" \
              '{name: $name, action: $action, status: $st, elapsed_ms: $ms}
               + (if $output != "" then {output: $output} else {} end)
               + (if $error  != "" then {error:  $error}  else {} end)')

          steps_json=$(printf '%s' "$steps_json" | jq --argjson step "$step_obj" '. + [$step]')

          if [[ "$s_status" == "ok" ]]; then
              ((steps_ok++))
          else
              ((steps_failed++))
          fi
      done < "$steps_file"

      local steps_total=$(( steps_ok + steps_failed ))

      jq -n \
          --arg     name         "$flow_name" \
          --arg     st           "$status" \
          --argjson exit_code    "$exit_code" \
          --arg     started_at   "$started_at" \
          --arg     ended_at     "$ended_at" \
          --argjson duration_ms  "$duration_ms" \
          --argjson total        "$steps_total" \
          --argjson ok           "$steps_ok" \
          --argjson failed       "$steps_failed" \
          --argjson steps        "$steps_json" \
          '{
              name:         $name,
              status:       $st,
              exit_code:    $exit_code,
              started_at:   $started_at,
              ended_at:     $ended_at,
              duration_ms:  $duration_ms,
              steps_total:  $total,
              steps_ok:     $ok,
              steps_failed: $failed,
              steps:        $steps
          }'
  }

  # --- Implementacion con printf (fallback sin jq) ---

  _json_escape_rj() {
      local s="$1"
      s="${s//\\/\\\\}"
      s="${s//\"/\\\"}"
      s="${s//$'\n'/\\n}"
      s="${s//$'\r'/\\r}"
      s="${s//$'\t'/\\t}"
      printf '%s' "$s"
  }

  _report_execution_json_printf() {
      local flow_name="$1" status="$2" exit_code="$3"
      local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"

      local steps_ok=0 steps_failed=0
      local steps_parts=()

      while IFS= read -r line; do
          [[ -z "$line" ]] && continue

          local s_name s_action s_status s_elapsed s_output s_error
          s_name=$(printf '%s' "$line" | cut -f1)
          s_action=$(printf '%s' "$line" | cut -f2)
          s_status=$(printf '%s' "$line" | cut -f3)
          s_elapsed=$(printf '%s' "$line" | cut -f4)
          s_output=$(printf '%s' "$line" | cut -f5)
          s_error=$(printf '%s' "$line" | cut -f6)

          [[ -z "$s_name" ]] && continue

          local part
          part=$(printf '{"name":"%s","action":"%s","status":"%s","elapsed_ms":%s' \
              "$(_json_escape_rj "$s_name")" \
              "$(_json_escape_rj "$s_action")" \
              "$(_json_escape_rj "$s_status")" \
              "${s_elapsed:-0}")

          if [[ -n "$s_output" ]]; then
              part+=",\"output\":\"$(_json_escape_rj "$s_output")\""
          fi
          if [[ -n "$s_error" ]]; then
              part+=",\"error\":\"$(_json_escape_rj "$s_error")\""
          fi
          part+="}"

          steps_parts+=("$part")

          if [[ "$s_status" == "ok" ]]; then
              ((steps_ok++))
          else
              ((steps_failed++))
          fi
      done < "$steps_file"

      local steps_total=$(( steps_ok + steps_failed ))

      local steps_array="["
      local first=1
      for part in "${steps_parts[@]}"; do
          if [[ $first -eq 1 ]]; then
              steps_array+="$part"
              first=0
          else
              steps_array+=",$part"
          fi
      done
      steps_array+="]"

      printf '{"name":"%s","status":"%s","exit_code":%s,"started_at":"%s","ended_at":"%s","duration_ms":%s,"steps_total":%s,"steps_ok":%s,"steps_failed":%s,"steps":%s}\n' \
          "$(_json_escape_rj "$flow_name")" \
          "$(_json_escape_rj "$status")" \
          "$exit_code" \
          "$(_json_escape_rj "$started_at")" \
          "$(_json_escape_rj "$ended_at")" \
          "$duration_ms" \
          "$steps_total" \
          "$steps_ok" \
          "$steps_failed" \
          "$steps_array"
  }

  # Permitir ejecucion directa (no solo sourced)
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      report_execution_json "$@"
  fi

params_schema:
  {"params":[{"name":"flow_name","desc":"nombre del flujo ejecutado"},{"name":"status","desc":"estado final: success, failure, o partial"},{"name":"exit_code","desc":"código de salida: 0, 1, o 2"},{"name":"started_at","desc":"timestamp ISO8601 de inicio"},{"name":"ended_at","desc":"timestamp ISO8601 de fin"},{"name":"duration_ms","desc":"duración total en milisegundos"},{"name":"steps_file","desc":"archivo TSV con resultados de cada paso"}],"output":"JSON estructurado con metadatos de ejecución y array de pasos"}


id: run_steps_bash_shell
name: run_steps
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: run_steps(yaml_file: string, [--strict]) -> string
tags: ["execution","yaml","runner","standard","pipeline"]
uses_functions: ["exit_with_status_bash_shell","report_execution_json_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/run_steps.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c633d74c155f5e8accd1bf4d9b22505e47ce005ad9445740721654fd87aa66aa
description:
  Ejecuta pasos de un YAML generico donde cada step tiene action=command. Lee el YAML con yq, ejecuta cada paso secuencialmente con timeout configurable, captura exit code y output, respeta continue_on_error, y al final reporta JSON estandar a stdout via report_execution_json. Sale con exit code 0 (success), 1 (failure) o 2 (partial). Con --strict mapea partial a failure.

example:
  ```yaml
  # /tmp/test_flow.yaml
  name: test_flow
  steps:
    - name: check
      action: command
      command: "echo hello"
    - name: list
      action: command
      command: "ls /tmp"
      continue_on_error: true
    - name: slow_step
      action: command
      command: "sleep 2"
      timeout_ms: 1000
      continue_on_error: true
  ```

  ```bash
  source bash/functions/shell/run_steps.sh

  run_steps /tmp/test_flow.yaml
  # Imprime JSON a stdout:
  # {
  #   "name": "test_flow",
  #   "status": "partial",
  #   "exit_code": 2,
  #   "started_at": "2026-04-01T10:00:00Z",
  #   "ended_at": "2026-04-01T10:00:01Z",
  #   "duration_ms": 1250,
  #   "steps_total": 3,
  #   "steps_ok": 2,
  #   "steps_failed": 1,
  #   "steps": [
  #     {"name": "check", "action": "command", "status": "ok", "elapsed_ms": 10, "output": "hello"},
  #     {"name": "list", "action": "command", "status": "ok", "elapsed_ms": 15},
  #     {"name": "slow_step", "action": "command", "status": "error", "elapsed_ms": 1001, "error": "timeout: comando excedio 1000ms"}
  #   ]
  # }
  # Sale con exit code 2 (partial)

  run_steps /tmp/test_flow.yaml --strict
  # Igual pero status="failure" y exit code=1
  ```

notes:
  Requiere `yq` (v4+, modo expression `-e`) y `jq` (o la implementacion printf de report_execution_json). Solo soporta `action: command` — otros actions se marcan como error del paso. El campo `timeout_ms` por defecto es 30000 (30s); si el comando excede el timeout, `timeout(1)` sale con exit code 124 y se registra el error correspondiente. El output del comando (stdout+stderr combinados) se sanitiza reemplazando tabuladores y saltos de linea por espacios antes de escribir al TSV temporal. Las funciones `exit_with_status` y `report_execution_json` se cargan via `source` desde el mismo directorio que `run_steps.sh`.

code:
  #!/usr/bin/env bash
  # run_steps — ejecuta pasos de un YAML generico (action=command)

  # run_steps <yaml_file> [--strict]
  #
  # Lee un YAML con la estructura:
  #   name: <run_name>
  #   steps:
  #     - name: <step_name>
  #       action: command
  #       command: "<shell_command>"
  #       continue_on_error: true|false   # opcional, default false
  #       timeout_ms: 30000               # opcional, default 30000
  #
  # Para cada paso de action=command:
  #   - Ejecuta el command con timeout (timeout_ms ms)
  #   - Captura exit code, stdout+stderr y elapsed time
  #   - Si falla y continue_on_error=false → aborta
  #   - Acumula resultados en TSV temporal
  #
  # Al final:
  #   - Llama a report_execution_json para generar JSON a stdout
  #   - Llama a exit_with_status para determinar el exit code
  #
  # --strict: mapea partial (2) a failure (1) en status y exit code
  #
  # Requiere: yq, jq (jq opcional si report_execution_json tiene fallback printf)
  run_steps() {
      local yaml_file="$1"
      local strict=0

      if [[ "${2:-}" == "--strict" ]]; then
          strict=1
      fi

      # --- validaciones previas ---
      if [[ -z "$yaml_file" ]]; then
          echo "run_steps: yaml_file requerido" >&2
          return 1
      fi

      if [[ ! -f "$yaml_file" ]]; then
          echo "run_steps: archivo '$yaml_file' no existe" >&2
          return 1
      fi

      if ! command -v yq &>/dev/null; then
          echo "run_steps: yq no encontrado en PATH" >&2
          return 1
      fi

      # --- cargar funciones del estandar ---
      local script_dir
      script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      # shellcheck source=bash/functions/shell/exit_with_status.sh
      source "$script_dir/exit_with_status.sh"
      # shellcheck source=bash/functions/shell/report_execution_json.sh
      source "$script_dir/report_execution_json.sh"

      # --- leer nombre del run ---
      local run_name
      run_name=$(yq e '.name // "unnamed"' "$yaml_file")

      # --- contar pasos ---
      local step_count
      step_count=$(yq e '.steps | length' "$yaml_file")

      if [[ -z "$step_count" || "$step_count" -eq 0 ]]; then
          echo "run_steps: el YAML no tiene steps" >&2
          return 1
      fi

      # --- archivo TSV temporal para acumular resultados ---
      # columnas: name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
      local tsv_file
      tsv_file=$(mktemp /tmp/run_steps_XXXXXX.tsv)
      # shellcheck disable=SC2064
      trap "rm -f '$tsv_file'" EXIT

      local ok_steps=0
      local failed_steps=0
      local started_at
      started_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
      local t_run_start
      t_run_start=$(date +%s%3N)

      # --- iterar sobre cada paso ---
      local i
      for (( i=0; i<step_count; i++ )); do
          local step_name step_action step_command step_continue step_timeout_ms

          step_name=$(yq e ".steps[$i].name // \"step_$i\"" "$yaml_file")
          step_action=$(yq e ".steps[$i].action // \"command\"" "$yaml_file")
          step_command=$(yq e ".steps[$i].command // \"\"" "$yaml_file")
          step_continue=$(yq e ".steps[$i].continue_on_error // false" "$yaml_file")
          step_timeout_ms=$(yq e ".steps[$i].timeout_ms // 30000" "$yaml_file")

          # convertir timeout de ms a segundos enteros (timeout(1) usa segundos)
          local timeout_sec
          timeout_sec=$(( (step_timeout_ms + 999) / 1000 ))

          # solo soportamos action=command
          if [[ "$step_action" != "command" ]]; then
              local err_msg="action '$step_action' no soportada (solo command)"
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "$step_action" "error" "0" "" "$err_msg" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: paso '$step_name' — $err_msg — abortando" >&2
                  break
              fi
              continue
          fi

          if [[ -z "$step_command" ]]; then
              local err_msg="campo 'command' vacio en paso '$step_name'"
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "error" "0" "" "$err_msg" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: $err_msg — abortando" >&2
                  break
              fi
              continue
          fi

          # ejecutar con timeout y capturar output + tiempos
          local t_start t_end elapsed_ms step_output step_exit

          t_start=$(date +%s%3N)
          step_output=$(timeout "$timeout_sec" bash -c "$step_command" 2>&1)
          step_exit=$?
          t_end=$(date +%s%3N)
          elapsed_ms=$(( t_end - t_start ))

          # timeout(1) sale con 124 cuando agota el tiempo
          local step_error=""
          if [[ "$step_exit" -eq 124 ]]; then
              step_error="timeout: comando excedio ${step_timeout_ms}ms"
          fi

          # escapar tabuladores y saltos de linea en el output para el TSV
          local safe_output
          safe_output=$(printf '%s' "$step_output" | tr '\t' ' ' | tr '\n' ' ')

          if [[ "$step_exit" -eq 0 ]]; then
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "ok" "$elapsed_ms" "$safe_output" "" >> "$tsv_file"
              ok_steps=$((ok_steps + 1))
          else
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "error" "$elapsed_ms" "$safe_output" "$step_error" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: paso '$step_name' fallo (exit $step_exit) — abortando" >&2
                  break
              fi
          fi
      done

      local t_run_end
      t_run_end=$(date +%s%3N)
      local total_duration_ms=$(( t_run_end - t_run_start ))
      local ended_at
      ended_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
      local total_steps=$(( ok_steps + failed_steps ))

      # --- calcular status y exit code ---
      local status_code
      status_code=$(exit_with_status "$total_steps" "$ok_steps" "$failed_steps")

      local run_status
      case "$status_code" in
          0) run_status="success" ;;
          1) run_status="failure" ;;
          2)
              if [[ "$strict" -eq 1 ]]; then
                  run_status="failure"
                  status_code=1
              else
                  run_status="partial"
              fi
              ;;
          *) run_status="failure"; status_code=1 ;;
      esac

      # --- generar JSON a stdout ---
      report_execution_json \
          "$run_name" \
          "$run_status" \
          "$status_code" \
          "$started_at" \
          "$ended_at" \
          "$total_duration_ms" \
          "$tsv_file"

      return "$status_code"
  }

params_schema:
  {"params":[{"name":"yaml_file","desc":"archivo YAML con especificación de pasos"},{"name":"--strict","desc":"flag opcional para mapear partial a failure"}],"output":"JSON de ejecución con detalles de cada paso"}


id: validate_registry_paths_bash_shell
name: validate_registry_paths
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: validate_registry_paths(db_path: string, table: string, root_dir: string) -> tsv_stdout
tags: ["registry","validation","paths","audit","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/validate_registry_paths.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: eb3eacaa9aaf99f8e96758d349a0d22db148b7ff821d05a5a0ad4d44cfac23b4
description:
  Consulta registry.db y verifica que cada file_path apunte a un archivo existente en disco. Imprime a stdout las rutas rotas en formato TSV (id, file_path, domain, tabla).

example:
  ```bash
  source validate_registry_paths.sh
  validate_registry_paths /home/lucas/fn_registry/registry.db functions /home/lucas/fn_registry

  # Output (TSV):
  # cdp_click_go_browser	functions/infra/cdp_click.go	browser	functions
  ```

notes:
  Impura porque lee el filesystem y la base de datos. No modifica nada — solo reporta.

  La salida TSV es consumible por otros scripts o pipelines. Si no hay rutas rotas, no imprime nada.

code:
  #!/usr/bin/env bash
  # validate_registry_paths
  # -----------------------
  # Consulta registry.db y verifica que cada file_path apunte a un archivo existente.
  # Recibe la ruta a registry.db y la tabla a validar (functions o types).
  # Imprime a stdout las lineas con rutas rotas en formato TSV:
  #   id<TAB>file_path<TAB>domain<TAB>tabla
  # Exit code 0 siempre (es una consulta, no una asercion).
  #
  # USO (sourced):
  #   source validate_registry_paths.sh
  #   validate_registry_paths /ruta/registry.db functions /ruta/raiz
  #
  # USO (directo):
  #   bash validate_registry_paths.sh /ruta/registry.db functions /ruta/raiz

  validate_registry_paths() {
      local db_path="$1"
      local table="$2"
      local root_dir="$3"

      if [[ -z "$db_path" || -z "$table" || -z "$root_dir" ]]; then
          echo "validate_registry_paths: uso: validate_registry_paths <db_path> <table> <root_dir>" >&2
          return 1
      fi

      if [[ "$table" != "functions" && "$table" != "types" ]]; then
          echo "validate_registry_paths: tabla debe ser 'functions' o 'types'" >&2
          return 1
      fi

      local broken=0
      while IFS='|' read -r id fp domain; do
          if [[ ! -f "$root_dir/$fp" ]]; then
              printf '%s\t%s\t%s\t%s\n' "$id" "$fp" "$domain" "$table"
              ((broken++))
          fi
      done < <(sqlite3 "$db_path" "SELECT id, file_path, domain FROM $table ORDER BY id;")

      return 0
  }

  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      validate_registry_paths "$@"
  fi

params_schema:
  {"params":[{"name":"db_path","desc":"ruta a registry.db"},{"name":"table","desc":"tabla a validar: functions o types"},{"name":"root_dir","desc":"directorio raíz del repositorio"}],"output":"TSV con id, file_path, domain, tabla de rutas inválidas"}


id: dashboard_grid_cpp_core
name: dashboard_grid
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
tags: ["imgui","grid","layout","dashboard","responsive"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/dashboard_grid.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: af6d4a2555dc0b15f4cadb53fe5f83a09ae139b9ceb86c6de3eb5d36ce3fef71
signature:
  void dashboard_grid_begin(int columns = 2, float spacing = 8.0f); void dashboard_grid_next(); void dashboard_grid_end()

description:
  Grid de N columnas para distribuir widgets de dashboard automaticamente con spacing uniforme entre columnas

notes:
  - Llamar `dashboard_grid_next()` entre cada par de widgets, **no** antes del primero ni despues del ultimo.
  - El numero de `dashboard_grid_next()` puede ser mayor que `columns - 1`: el grid hace wrap automatico a la siguiente fila.
  - Combina bien con `dashboard_panel_begin`/`dashboard_panel_end` para crear dashboards con paneles alineados en cuadricula.
  - Si `columns <= 0` se fuerza a 1 para evitar division por cero.

documentation:
  # dashboard_grid

  Divide el ancho disponible en N columnas con spacing uniforme y posiciona cada widget en su celda automaticamente. Soporta grids anidados mediante un stack interno de estado.

  ## Uso

  ```cpp
  dashboard_grid_begin(3, 8.0f);

  // Celda 0
  dashboard_panel_begin("CPU");
  ImGui::Text("%.1f %%", cpu_pct);
  dashboard_panel_end();
  dashboard_grid_next();

  // Celda 1
  dashboard_panel_begin("Memory");
  ImGui::Text("%.0f MB", mem_mb);
  dashboard_panel_end();
  dashboard_grid_next();

  // Celda 2
  dashboard_panel_begin("Disk");
  ImGui::Text("%.0f GB", disk_gb);
  dashboard_panel_end();

  dashboard_grid_end();
  ```

  ## Implementacion

  ### Calculo de ancho

  ```
  col_width = (available_width - spacing * (columns - 1)) / columns
  ```

  `available_width` se obtiene de `ImGui::GetContentRegionAvail().x` en el momento de `dashboard_grid_begin`.

  ### Mecanica de celdas

  Cada celda es un `BeginGroup`/`EndGroup` con `PushItemWidth(col_width)` para que los widgets internos respeten el ancho de columna. Al llamar `dashboard_grid_next`:

  1. Se cierra la celda actual (`PopItemWidth`, `EndGroup`).
  2. Se incrementa el contador interno.
  3. Si `counter % columns != 0` se emite `SameLine(0, spacing)` para continuar en la misma fila.
  4. Si `counter % columns == 0` no se emite `SameLine`: ImGui pasa a la siguiente fila automaticamente.
  5. Se abre la nueva celda (`BeginGroup`, `PushItemWidth`).

  ### Grids anidados

  El estado (columnas, spacing, col_width, counter) se guarda en `g_grid_stack` (un `std::vector` de structs en un namespace anonimo). Cada llamada a `dashboard_grid_begin` hace `push_back` y `dashboard_grid_end` hace `pop_back`, permitiendo anidar grids sin conflicto.

code:
  #include "dashboard_grid.h"
  #include <imgui.h>
  #include <vector>

  // Internal state stack to support nested grids.
  namespace {

  struct GridState {
      int   columns;
      float spacing;
      float col_width;
      int   counter;  // number of dashboard_grid_next() calls so far
  };

  static std::vector<GridState> g_grid_stack;

  } // namespace

  void dashboard_grid_begin(int columns, float spacing) {
      if (columns < 1) columns = 1;

      float available = ImGui::GetContentRegionAvail().x;
      float col_width = (available - spacing * static_cast<float>(columns - 1))
                        / static_cast<float>(columns);
      if (col_width < 1.0f) col_width = 1.0f;

      g_grid_stack.push_back({columns, spacing, col_width, 0});

      ImGui::BeginGroup();
      ImGui::PushItemWidth(col_width);
  }

  void dashboard_grid_next() {
      if (g_grid_stack.empty()) return;

      GridState& s = g_grid_stack.back();

      ImGui::PopItemWidth();
      ImGui::EndGroup();

      s.counter++;

      if (s.counter % s.columns != 0) {
          // Same row: advance horizontally.
          ImGui::SameLine(0.0f, s.spacing);
      }
      // If counter % columns == 0 the next BeginGroup starts a new row automatically.

      ImGui::BeginGroup();
      ImGui::PushItemWidth(s.col_width);
  }

  void dashboard_grid_end() {
      if (g_grid_stack.empty()) return;

      ImGui::PopItemWidth();
      ImGui::EndGroup();

      g_grid_stack.pop_back();
  }

params_schema:
  {"params":[{"name":"columns","desc":"Numero de columnas del grid (minimo 1); el ancho disponible se divide uniformemente entre ellas"},{"name":"spacing","desc":"Espacio horizontal entre columnas en pixels"}],"output":"Layout de grid aplicado al contenido entre dashboard_grid_begin/end; cada celda recibe un ancho uniforme calculado a partir del espacio disponible"}


id: dashboard_panel_cpp_core
name: dashboard_panel
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
tags: ["imgui","panel","container","layout","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/dashboard_panel.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 2471531214723d7570258d0eb1a0c7a19e9e72c7ddb70b670ecbe3de4094559f
signature:
  bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f); void dashboard_panel_end()

description:
  Contenedor estilizado tipo panel para dashboards con titulo, bordes redondeados y tamaño minimo configurable

notes:
  - El titulo actua como ID de ImGui: dos paneles con el mismo titulo en el mismo frame se comportan como uno solo. Usar `##` para diferenciar IDs si se repite el texto: `"Revenue##panel1"`.
  - `AutoResizeY` hace que el panel crezca verticalmente con su contenido; `min_height` establece el piso.
  - El patron begin/end es idomatico en ImGui: `end` debe llamarse siempre para hacer pop de los estilos, aunque `begin` retorne false.

documentation:
  # dashboard_panel

  Panel estilizado para dashboards ImGui. Envuelve un `BeginChild`/`EndChild` con estilos predefinidos: fondo oscuro (`#1F1F26`), bordes redondeados (5 px), borde visible y separador bajo el titulo.

  ## Uso

  ```cpp
  if (dashboard_panel_begin("Revenue", 300.0f, 200.0f)) {
      line_plot("Revenue", xs, ys, N);
  }
  dashboard_panel_end(); // siempre llamar
  ```

  ## Implementacion

  - `PushStyleVar` aplica `ChildRounding = 5.0f` y `ChildBorderSize = 1.0f`
  - `PushStyleColor` establece el fondo del child a `(0.12, 0.12, 0.15, 1.0)`
  - `BeginChild` con `ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY`
  - Titulo con `TextUnformatted` seguido de `Separator`
  - `dashboard_panel_end` hace `EndChild`, `PopStyleColor(1)`, `PopStyleVar(2)`

code:
  #include "dashboard_panel.h"
  #include <imgui.h>

  bool dashboard_panel_begin(const char* title, float min_width, float min_height) {
      ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
      ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
      ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.15f, 1.0f));

      ImVec2 size(min_width > 0.0f ? min_width : 0.0f,
                  min_height > 0.0f ? min_height : 0.0f);

      ImGui::BeginChild(title, size, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY);

      ImGui::TextUnformatted(title);
      ImGui::Separator();

      return true;
  }

  void dashboard_panel_end() {
      ImGui::EndChild();
      ImGui::PopStyleColor(1);
      ImGui::PopStyleVar(2);
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del panel, tambien sirve como ID de ImGui para distinguir multiples paneles"},{"name":"min_width","desc":"Ancho minimo del panel en pixels (0 = sin restriccion)"},{"name":"min_height","desc":"Alto minimo del panel en pixels (0 = sin restriccion)"}],"output":"true si el panel es visible y se debe renderizar contenido; llamar siempre dashboard_panel_end() independientemente del valor de retorno"}


id: docking_layout_cpp_core
name: docking_layout
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: ImGuiID docking_layout(DockPreset preset = DockPreset::Default)
description: Configura un docking space con presets de layout predefinidos para dashboards
tags: ["imgui","docking","layout","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/docking_layout.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 2beea4a5dadc1a44cb60268bf7792617e35516eafb64f6cbf807be84ab53a349
example:
  ```cpp
  void render_fn() {
      ImGuiID dock = docking_layout(DockPreset::SidebarLeft);

      ImGui::Begin("Filters");
      // controles del sidebar
      ImGui::End();

      ImGui::Begin("Main");
      // contenido principal
      ImGui::End();
  }
  ```

notes:
  Requiere que `ImGuiConfigFlags_DockingEnable` este activo en `ImGui::GetIO().ConfigFlags` (habilitado por `app_base.cpp`). El preset se aplica solo en el primer frame (static bool). `imgui_internal.h` es necesario para `DockBuilder*`.

documentation:
  # docking_layout

  Configura un docking space fullscreen sobre el viewport principal con presets de layout predefinidos.

  Usa `DockSpaceOverViewport` para crear el dockspace y, en el primer frame, aplica el preset con `DockBuilderSplitNode`. Llamar una vez al inicio de cada frame, antes de renderizar las ventanas hijas.

  ## Presets disponibles

  | Preset | Layout |
  |---|---|
  | Default | Dockspace completo sin divisiones |
  | TwoColumns | Izquierda 60% / Derecha 40% |
  | ThreeColumns | Tres columnas iguales ~33% |
  | SidebarLeft | Sidebar 25% / Main 75% |
  | SidebarRight | Main 75% / Sidebar 25% |
  | TopBottom | Arriba 60% / Abajo 40% |
  | Dashboard | Grid 2x2 de cuatro paneles |

code:
  #include "core/docking_layout.h"
  #include "imgui_internal.h"

  ImGuiID docking_layout(DockPreset preset) {
      const ImGuiViewport* viewport = ImGui::GetMainViewport();
      ImGuiID dockspace_id = ImGui::DockSpaceOverViewport(0, viewport);

      static bool initialized = false;
      if (initialized) {
          return dockspace_id;
      }
      initialized = true;

      if (preset == DockPreset::Default) {
          return dockspace_id;
      }

      ImGui::DockBuilderRemoveNode(dockspace_id);
      ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
      ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);

      if (preset == DockPreset::TwoColumns) {
          ImGuiID right;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.6f, nullptr, &right);

      } else if (preset == DockPreset::ThreeColumns) {
          ImGuiID center, right;
          ImGuiID left_node;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.33f, &left_node, &center);
          ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.5f, nullptr, &right);

      } else if (preset == DockPreset::SidebarLeft) {
          ImGuiID main;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.25f, nullptr, &main);

      } else if (preset == DockPreset::SidebarRight) {
          ImGuiID sidebar;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.25f, &sidebar, nullptr);

      } else if (preset == DockPreset::TopBottom) {
          ImGuiID bottom;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.6f, nullptr, &bottom);

      } else if (preset == DockPreset::Dashboard) {
          ImGuiID top, bottom;
          ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.5f, &top, &bottom);
          ImGui::DockBuilderSplitNode(top,    ImGuiDir_Left, 0.5f, nullptr, nullptr);
          ImGui::DockBuilderSplitNode(bottom, ImGuiDir_Left, 0.5f, nullptr, nullptr);
      }

      ImGui::DockBuilderFinish(dockspace_id);
      return dockspace_id;
  }

params_schema:
  {"params":[{"name":"preset","desc":"Layout predefinido a aplicar (Default, TwoColumns, ThreeColumns, SidebarLeft, SidebarRight, TopBottom, Dashboard)"}],"output":"ID del dockspace creado, usable con ImGui::SetNextWindowDockID"}


id: fps_overlay_cpp_core
name: fps_overlay
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: void fps_overlay()
tags: ["imgui","fps","overlay","profiling","debug"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/fps_overlay.cpp
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 1a1d25ddac5db7eef21dee59a15e38cd6ace1059e873d19556df7319a5b801d8
params_schema: {"output":"Renderiza el overlay de FPS en el frame ImGui actual"}
description:
  Renderiza un overlay de FPS y frametime en la esquina superior derecha, con soporte opcional de Tracy

documentation:
  # fps_overlay

  Muestra FPS y frametime (ms) en una ventana semi-transparente en la esquina superior derecha.

  Si se compila con `TRACY_ENABLE`, incluye un `ZoneScoped` para profiling con Tracy.

code:
  #include "core/fps_overlay.h"
  #include "imgui.h"

  #ifdef TRACY_ENABLE
  #include "tracy/Tracy.hpp"
  #endif

  void fps_overlay() {
  #ifdef TRACY_ENABLE
      ZoneScoped;
  #endif

      ImGuiIO& io = ImGui::GetIO();
      ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
                             | ImGuiWindowFlags_AlwaysAutoResize
                             | ImGuiWindowFlags_NoSavedSettings
                             | ImGuiWindowFlags_NoFocusOnAppearing
                             | ImGuiWindowFlags_NoNav
                             | ImGuiWindowFlags_NoMove;

      const float pad = 10.0f;
      const ImGuiViewport* viewport = ImGui::GetMainViewport();
      ImVec2 pos(viewport->WorkPos.x + viewport->WorkSize.x - pad,
                 viewport->WorkPos.y + pad);
      ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 0.0f));
      ImGui::SetNextWindowBgAlpha(0.65f);

      if (ImGui::Begin("##fps_overlay", nullptr, flags)) {
          ImGui::Text("%.1f FPS", io.Framerate);
          ImGui::Text("%.3f ms", 1000.0f / io.Framerate);
      }
      ImGui::End();
  }


id: fullscreen_window_cpp_core
name: fullscreen_window
kind: component
lang: cpp
domain: core
version: 0.1.0
purity: pure
signature: bool fullscreen_window_begin(const char* id = "##fullscreen"); void fullscreen_window_end()
tags: ["imgui","layout","fullscreen","window"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/fullscreen_window.cpp
created_at: 2026-04-07T22:24:27Z
updated_at: 2026-04-07T22:24:27Z
props: []
emits: []
framework: imgui
variant: []
content_hash: cf3510f5959117e66eb340deae1d7b929588aa536f25ab649c655615a3c5a289
description:
  Ventana ImGui fullscreen sin decoraciones que ocupa todo el viewport, elimina la necesidad de usar el sistema de ventanas interno

notes:
  - Pura en el sentido de que no hace I/O ni tiene estado propio; solo configura el estado next-frame de ImGui.
  - `WorkPos`/`WorkSize` respetan los menu bars del sistema operativo (en plataformas que los tienen). Para ocupar literalmente toda la pantalla usar `Pos`/`Size` del viewport.
  - Compatible con `dashboard_grid` y `dashboard_panel`: el fullscreen_window actua como contenedor raiz y los paneles/grids se renderizan dentro.
  - El patron begin/end es idiomatico en ImGui: `end` debe llamarse siempre para cerrar la ventana correctamente, aunque `begin` retorne false.

documentation:
  # fullscreen_window

  Wrapper que crea una ventana ImGui que ocupa exactamente el viewport de trabajo (`WorkPos` / `WorkSize`). Elimina todas las decoraciones: title bar, resize grip, move, collapse. Ideal como capa raiz de una aplicacion ImGui donde el contenido propio gestiona el layout.

  ## Uso

  ```cpp
  if (fullscreen_window_begin()) {
      // todo el layout de la app va aqui
      dashboard_grid_begin(3, 8.0f);
      // ...
      dashboard_grid_end();
  }
  fullscreen_window_end(); // siempre llamar
  ```

  Con ID explicito (si se necesitan multiples capas):

  ```cpp
  if (fullscreen_window_begin("##background")) {
      render_background();
  }
  fullscreen_window_end();
  ```

  ## Implementacion

  - `GetMainViewport()` obtiene el viewport principal (compatible con viewports multi-monitor de ImGui)
  - `SetNextWindowPos(vp->WorkPos)` posiciona en el area de trabajo (excluye menu bars del OS)
  - `SetNextWindowSize(vp->WorkSize)` ocupa exactamente el area disponible
  - Flags: `NoTitleBar | NoResize | NoMove | NoCollapse | NoBringToFrontOnFocus | NoNavFocus`
  - `NoBringToFrontOnFocus` y `NoNavFocus` evitan que la ventana fullscreen robe el foco de ventanas superpuestas

code:
  #include "core/fullscreen_window.h"
  #include "imgui.h"

  bool fullscreen_window_begin(const char* id) {
      const ImGuiViewport* vp = ImGui::GetMainViewport();
      ImGui::SetNextWindowPos(vp->WorkPos);
      ImGui::SetNextWindowSize(vp->WorkSize);
      return ImGui::Begin(id, nullptr,
          ImGuiWindowFlags_NoTitleBar      |
          ImGuiWindowFlags_NoResize        |
          ImGuiWindowFlags_NoMove          |
          ImGuiWindowFlags_NoCollapse      |
          ImGuiWindowFlags_NoBringToFrontOnFocus |
          ImGuiWindowFlags_NoNavFocus);
  }

  void fullscreen_window_end() {
      ImGui::End();
  }

params_schema:
  {"params":[{"name":"id","desc":"Identificador ImGui de la ventana, default ##fullscreen (el prefijo ## oculta el texto del titulo)"}],"output":"true si la ventana es visible (siempre true en fullscreen); llamar siempre fullscreen_window_end() independientemente del valor de retorno"}


id: graph_spatial_hash_cpp_core
name: graph_spatial_hash
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: SpatialHash(float cell_size, int table_size)
tags: ["spatial","hash","acceleration","graph","query","hittest"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: cpp/functions/core/graph_spatial_hash.cpp
created_at: 2026-04-07T22:04:57Z
updated_at: 2026-04-07T22:04:57Z
props: []
emits: []
framework: imgui
variant: []
content_hash: a5192801758a6cfb6edc9d9ed505000f8013c39539a6bf4bdff5a879177e81b4
description:
  Spatial hash grid para busqueda O(1) de puntos por posicion, usado para hit-testing de nodos en grafos

notes:
  - No es thread-safe: disenado para uso single-threaded en el hilo de render de ImGui.
  - `build()` debe llamarse cada frame si las posiciones de los nodos cambian.
  - `cell_size` debe ser >= al radio maximo de los nodos para que la vecindad 3x3 capture todos los candidatos posibles.
  - La distancia efectiva al nodo se calcula como `dist(centro) - size`, de modo que el hit-test funciona correctamente para nodos de distintos tamaños.
  - No implementa `operator=` ni copy constructor (deleted) para evitar doble-free de los arrays internos.

documentation:
  # graph_spatial_hash

  Spatial hash grid para busqueda eficiente de nodos en grafos por posicion 2D. Util para hit-testing de nodos en editores de grafos basados en ImGui, donde se necesita saber que nodo esta bajo el cursor en cada frame.

  ## Uso

  ```cpp
  SpatialHash sh(20.0f, 4096);

  // Reconstruir cada frame (o cuando cambian posiciones)
  sh.build(node_xs, node_ys, node_sizes, node_count);

  // Hit-test: nodo mas cercano al cursor
  float dist;
  int hovered = sh.query_nearest(mouse_x, mouse_y, 30.0f, &dist);

  // Seleccion por area: todos los nodos dentro de un radio
  int results[256];
  int count = sh.query_radius(center_x, center_y, radius, results, 256);
  ```

  ## Estructura interna

  - `buckets[table_size]`: cada entrada contiene el indice del primer slot de la cadena, o -1 si el bucket esta vacio.
  - `entries[2 * entry_capacity]`: pares `[node_index, next_in_chain]` almacenados como flat array. Se expande con `realloc` si el numero de nodos supera la capacidad inicial (256).
  - El snapshot de posiciones (`xs`, `ys`, `sizes`) se copia internamente en `build()` para que `query_nearest` y `query_radius` puedan calcular distancias sin recibir los arrays como parametro.

  ## Hash function

  ```
  hash(cx, cy) = (cx * 73856093 ^ cy * 19349663) % table_size
  ```

  Donde `cx = floor(x / cell_size)`, `cy = floor(y / cell_size)`.

code:
  #include "graph_spatial_hash.h"

  #include <cmath>
  #include <cstdlib>
  #include <cstring>

  // ---------------------------------------------------------------------------
  // Helpers
  // ---------------------------------------------------------------------------

  static inline int floor_div(float v, float cell) {
      return static_cast<int>(std::floor(v / cell));
  }

  static inline float sq(float x) { return x * x; }

  // ---------------------------------------------------------------------------
  // Snapshot de posiciones (almacenado internamente en build)
  // La interfaz del header no pasa xs/ys a los metodos de query, por lo que
  // SpatialHash conserva copias internas para calcular distancias.
  // ---------------------------------------------------------------------------

  struct SpatialHashSnap {
      float* xs    = nullptr;
      float* ys    = nullptr;
      float* sizes = nullptr;
      int    count = 0;
  };

  // Almacenamos el snapshot en memoria contigua al final del bloque de entries,
  // para no añadir campos al header publico. Usamos un puntero opaco en un
  // campo reservado. Como el header ya esta fijado, guardamos el snapshot como
  // una variable estatica por instancia — aceptable para uso single-threaded
  // tipico de ImGui. Para multi-instancia se puede usar un map.
  //
  // En la practica, la convencion del registry es que SpatialHash se crea una
  // vez por frame de ImGui y se usa en el mismo hilo de render.

  static SpatialHashSnap g_snap;   // snapshot mas reciente

  // ---------------------------------------------------------------------------
  // SpatialHash
  // ---------------------------------------------------------------------------

  int SpatialHash::cell_hash(int cx, int cy) const {
      unsigned int h = static_cast<unsigned int>(cx) * 73856093u
                     ^ static_cast<unsigned int>(cy) * 19349663u;
      return static_cast<int>(h % static_cast<unsigned int>(table_size));
  }

  SpatialHash::SpatialHash(float cell_size_, int table_size_)
      : cell_size(cell_size_)
      , table_size(table_size_)
      , entry_count(0)
      , entry_capacity(256)
  {
      buckets = static_cast<int*>(std::malloc(
          static_cast<std::size_t>(table_size) * sizeof(int)));

      entries = static_cast<int*>(std::malloc(
          static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));

      std::memset(buckets, -1,
          static_cast<std::size_t>(table_size) * sizeof(int));
  }

  SpatialHash::~SpatialHash() {
      std::free(buckets);
      std::free(entries);

      std::free(g_snap.xs);
      std::free(g_snap.ys);
      std::free(g_snap.sizes);
      g_snap = {};
  }

  void SpatialHash::build(const float* xs, const float* ys, const float* sizes, int count) {
      // --- Limpiar tabla ---
      std::memset(buckets, -1,
          static_cast<std::size_t>(table_size) * sizeof(int));
      entry_count = 0;

      // --- Snapshot de posiciones para queries ---
      if (g_snap.count < count) {
          std::free(g_snap.xs);
          std::free(g_snap.ys);
          std::free(g_snap.sizes);
          g_snap.xs    = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
          g_snap.ys    = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
          g_snap.sizes = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
      }
      std::memcpy(g_snap.xs,    xs,    static_cast<std::size_t>(count) * sizeof(float));
      std::memcpy(g_snap.ys,    ys,    static_cast<std::size_t>(count) * sizeof(float));
      std::memcpy(g_snap.sizes, sizes, static_cast<std::size_t>(count) * sizeof(float));
      g_snap.count = count;

      // --- Insertar nodos en la tabla hash ---
      for (int i = 0; i < count; ++i) {
          int cx     = floor_div(xs[i], cell_size);
          int cy     = floor_div(ys[i], cell_size);
          int bucket = cell_hash(cx, cy);

          // Crecer entries si necesario
          if (entry_count >= entry_capacity) {
              entry_capacity *= 2;
              entries = static_cast<int*>(std::realloc(
                  entries,
                  static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));
          }

          // Insertar al frente de la cadena encadenada
          int slot = entry_count++;
          entries[slot * 2 + 0] = i;               // node_index
          entries[slot * 2 + 1] = buckets[bucket];  // next_in_chain
          buckets[bucket] = slot;
      }
  }

  int SpatialHash::query_nearest(float qx, float qy, float radius, float* out_dist) const {
      int   best_idx = -1;
      float best_d   = radius;  // umbral: solo aceptamos dentro del radio

      int qcx = floor_div(qx, cell_size);
      int qcy = floor_div(qy, cell_size);

      // Escanear vecindad 3x3 de celdas
      for (int dy = -1; dy <= 1; ++dy) {
          for (int dx = -1; dx <= 1; ++dx) {
              int bucket = cell_hash(qcx + dx, qcy + dy);
              int slot   = buckets[bucket];
              while (slot != -1) {
                  int ni      = entries[slot * 2 + 0];
                  float ex    = g_snap.xs[ni] - qx;
                  float ey    = g_snap.ys[ni] - qy;
                  float dist  = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
                  if (dist < best_d) {
                      best_d   = dist;
                      best_idx = ni;
                  }
                  slot = entries[slot * 2 + 1];
              }
          }
      }

      if (out_dist && best_idx != -1)
          *out_dist = best_d;

      return best_idx;
  }

  int SpatialHash::query_radius(float qx, float qy, float radius, int* out, int max_out) const {
      int found = 0;

      int qcx = floor_div(qx, cell_size);
      int qcy = floor_div(qy, cell_size);

      for (int dy = -1; dy <= 1; ++dy) {
          for (int dx = -1; dx <= 1; ++dx) {
              int bucket = cell_hash(qcx + dx, qcy + dy);
              int slot   = buckets[bucket];
              while (slot != -1 && found < max_out) {
                  int ni     = entries[slot * 2 + 0];
                  float ex   = g_snap.xs[ni] - qx;
                  float ey   = g_snap.ys[ni] - qy;
                  float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
                  if (dist <= radius)
                      out[found++] = ni;
                  slot = entries[slot * 2 + 1];
              }
          }
      }

      return found;
  }

params_schema:
  {"params":[{"name":"cell_size","desc":"Tamaño de cada celda del grid espacial (debe ser \u003e= radio maximo de nodos)"},{"name":"table_size","desc":"Numero de buckets en la tabla hash (potencia de 2 recomendada para distribucion uniforme)"}],"output":"Estructura de hash espacial lista para queries de puntos cercanos"}


id: memory_overlay_cpp_core
name: memory_overlay
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: void memory_overlay()
description: Renderiza un overlay de estadisticas de memoria (RSS, peak, vsize) en la esquina inferior derecha
tags: ["imgui","memory","overlay","debug","dashboard","profiling"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
example:
  ```cpp
  // En el loop de render, dentro de un frame ImGui:
  fps_overlay();
  memory_overlay();
  ```

tests: []
file_path: cpp/functions/core/memory_overlay.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 7c325bf54f798be7495f41e26cb8c6110baa191bcf11fc37e8f2df409e90b7d7
params_schema: {"output":"Renderiza el overlay de memoria en el frame ImGui actual"}
notes:
  - La función sigue la misma convención que `fps_overlay`: mismos flags de ventana, mismo alpha (0.65).
  - Posición: esquina inferior derecha (`pivot = {1,1}`), separada 10 px del borde.
  - La lectura de `/proc/self/status` es I/O local de ~0.1 ms, amortizada a 1 Hz — despreciable.
  - Con `TRACY_ENABLE` activo, la función registra una zona `ZoneScoped` para que aparezca en el profiler.
  - Si la plataforma no puede leer las estadísticas, muestra "Memory: N/A" en gris.

documentation:
  # memory_overlay

  Muestra estadísticas de memoria del proceso en una ventana semi-transparente en la esquina inferior derecha. Complementa `fps_overlay` para un dashboard mínimo de rendimiento.

  Las métricas se leen como máximo una vez por segundo y se cachean en una variable estática local, evitando I/O excesivo en el hot path de render.

  ## Métricas mostradas

  | Campo | Linux | Windows |
  |---|---|---|
  | RSS (MB) | `VmRSS` de `/proc/self/status` | `WorkingSetSize` (PSAPI) |
  | Peak RSS (MB) | `VmPeak` de `/proc/self/status` | `PeakWorkingSetSize` (PSAPI) |
  | Virtual Size (MB) | `VmSize` de `/proc/self/status` | N/A (no mostrado) |

  En Windows se requiere enlazar `psapi.lib` (el `.cpp` incluye `#pragma comment(lib, "psapi.lib")`).

code:
  #include "core/memory_overlay.h"
  #include "imgui.h"

  #ifdef TRACY_ENABLE
  #include "tracy/Tracy.hpp"
  #endif

  #ifdef _WIN32
  #define WIN32_LEAN_AND_MEAN
  #include <windows.h>
  #include <psapi.h>
  #pragma comment(lib, "psapi.lib")
  #else
  #include <cstdio>
  #endif

  #include <cstring>

  namespace {

  struct MemStats {
      long rss_kb    = -1; // Resident Set Size
      long peak_kb   = -1; // Peak RSS
      long vsize_kb  = -1; // Virtual memory size
  };

  static MemStats s_cached;
  static double   s_last_sample = -1.0;

  #ifdef _WIN32
  static MemStats sample_memory() {
      MemStats s;
      PROCESS_MEMORY_COUNTERS pmc{};
      if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
          s.rss_kb  = static_cast<long>(pmc.WorkingSetSize       / 1024);
          s.peak_kb = static_cast<long>(pmc.PeakWorkingSetSize   / 1024);
          s.vsize_kb = -1; // not easily available on Windows without VirtualQuery loop
      }
      return s;
  }
  #else
  static MemStats sample_memory() {
      MemStats s;
      FILE* f = std::fopen("/proc/self/status", "r");
      if (!f) return s;

      char line[128];
      while (std::fgets(line, sizeof(line), f)) {
          long val = 0;
          if (std::sscanf(line, "VmRSS: %ld kB",  &val) == 1) s.rss_kb   = val;
          if (std::sscanf(line, "VmPeak: %ld kB", &val) == 1) s.peak_kb  = val;
          if (std::sscanf(line, "VmSize: %ld kB", &val) == 1) s.vsize_kb = val;
      }
      std::fclose(f);
      return s;
  }
  #endif

  } // namespace

  void memory_overlay() {
  #ifdef TRACY_ENABLE
      ZoneScoped;
  #endif

      // Sample at most once per second
      const double now = ImGui::GetTime();
      if (s_last_sample < 0.0 || (now - s_last_sample) >= 1.0) {
          s_cached      = sample_memory();
          s_last_sample = now;
      }

      ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
                             | ImGuiWindowFlags_AlwaysAutoResize
                             | ImGuiWindowFlags_NoSavedSettings
                             | ImGuiWindowFlags_NoFocusOnAppearing
                             | ImGuiWindowFlags_NoNav
                             | ImGuiWindowFlags_NoMove;

      const float pad = 10.0f;
      const ImGuiViewport* vp = ImGui::GetMainViewport();
      ImVec2 pos(vp->WorkPos.x + vp->WorkSize.x - pad,
                 vp->WorkPos.y + vp->WorkSize.y - pad);
      ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 1.0f));
      ImGui::SetNextWindowBgAlpha(0.65f);

      if (ImGui::Begin("##memory_overlay", nullptr, flags)) {
          if (s_cached.rss_kb >= 0) {
              ImGui::Text("RSS:    %5ld MB", s_cached.rss_kb   / 1024);
              ImGui::Text("Peak:   %5ld MB", s_cached.peak_kb  / 1024);
  #ifndef _WIN32
              ImGui::Text("VSize:  %5ld MB", s_cached.vsize_kb / 1024);
  #endif
          } else {
              ImGui::TextDisabled("Memory: N/A");
          }
      }
      ImGui::End();
  }


id: plot_theme_cpp_core
name: plot_theme
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
tags: ["theme","colors","palette","styling","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui","implot"]
tests: []
file_path: cpp/functions/core/plot_theme.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: ccec236a278c13ca850773915133808c920c0a482636fd354e86e2161376e65a
signature:
  void plot_theme_dark() / void plot_theme_light() / void plot_theme_high_contrast() / void plot_theme_apply(const PlotTheme& theme)

description:
  Gestiona temas y paletas de colores para ImPlot e ImGui, con presets dark/light/high-contrast y soporte para temas custom

notes:
  - `plot_theme_apply` llama a `ImPlot::AddColormap` y `ImPlot::SetColormap` con el campo `name` del tema como identificador. Si se llama dos veces con el mismo `name`, ImPlot registra el colormap de nuevo — usar nombres distintos si se crean multiples variantes en runtime.
  - La funcion es "pure" en el sentido de que su unico efecto es escribir en `ImGuiStyle` e `ImPlotStyle` del contexto activo, sin I/O ni estado global propio.
  - Requiere que `ImGui::CreateContext()` e `ImPlot::CreateContext()` hayan sido llamados antes del primer uso.
  - Compatible con C++17. No usa excepciones ni RTTI.

documentation:
  # plot_theme

  Configura el aspecto visual de ventanas ImGui y graficas ImPlot en un solo paso. Tres presets listos para usar y una API de tema custom para casos especificos.

  ## Presets

  ### `plot_theme_dark()`

  Fondo oscuro (`#1A1A1F`) con paleta de 10 colores suaves: steel blue, soft green, warm orange, muted red, lavender, teal, gold, lime, pink, periwinkle. Ideal para dashboards de monitoreo.

  ### `plot_theme_light()`

  Fondo claro (`#F7F7F7`) con colores vibrantes de alto contraste sobre blanco. Recomendado para capturas de pantalla o exportacion a documentos.

  ### `plot_theme_high_contrast()`

  Fondo negro puro con colores neon: cyan, green, orange, magenta, yellow. Disenado para presentaciones en pantallas grandes o condiciones de poca luz.

  ## Uso

  ```cpp
  #include "plot_theme.h"

  // Al inicio del frame, antes de BeginMainMenuBar / Begin
  plot_theme_dark();

  // O con tema custom
  PlotTheme my_theme = plot_theme_preset_dark();
  my_theme.palette[0] = ImVec4(1.0f, 0.0f, 0.5f, 1.0f); // override primer color
  plot_theme_apply(my_theme);
  ```

  ## API

  ```cpp
  // Presets de aplicacion directa
  void plot_theme_dark();
  void plot_theme_light();
  void plot_theme_high_contrast();

  // Tema custom
  void plot_theme_apply(const PlotTheme& theme);

  // Obtener structs para inspeccion o modificacion parcial
  PlotTheme plot_theme_preset_dark();
  PlotTheme plot_theme_preset_light();
  PlotTheme plot_theme_preset_high_contrast();
  ```

  ## Estructura PlotTheme

  ```cpp
  struct PlotTheme {
      const char* name;       // nombre del colormap registrado en ImPlot
      ImVec4 bg;              // fondo del plot (ImPlotCol_PlotBg, ImGuiCol_WindowBg)
      ImVec4 frame_bg;        // fondo del frame (ImPlotCol_FrameBg, ImGuiCol_FrameBg)
      ImVec4 text;            // color del texto (todos los ImGui/ImPlot text cols)
      ImVec4 grid;            // lineas de cuadricula y ticks
      ImVec4 palette[10];     // paleta de colores para series de datos
      int palette_count;      // numero de colores activos en la paleta (1-10)
  };
  ```

code:
  #include "plot_theme.h"

  // ---------------------------------------------------------------------------
  // Preset structs
  // ---------------------------------------------------------------------------

  PlotTheme plot_theme_preset_dark() {
      PlotTheme t;
      t.name          = "dark";
      t.bg            = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
      t.frame_bg      = ImVec4(0.14f, 0.14f, 0.17f, 1.00f);
      t.text          = ImVec4(0.88f, 0.88f, 0.90f, 1.00f);
      t.grid          = ImVec4(0.30f, 0.30f, 0.35f, 0.60f);
      t.palette_count = 10;
      t.palette[0]    = ImVec4(0.33f, 0.62f, 0.91f, 1.00f); // steel blue
      t.palette[1]    = ImVec4(0.35f, 0.78f, 0.54f, 1.00f); // soft green
      t.palette[2]    = ImVec4(0.96f, 0.60f, 0.25f, 1.00f); // warm orange
      t.palette[3]    = ImVec4(0.85f, 0.38f, 0.42f, 1.00f); // muted red
      t.palette[4]    = ImVec4(0.72f, 0.52f, 0.88f, 1.00f); // lavender
      t.palette[5]    = ImVec4(0.38f, 0.80f, 0.82f, 1.00f); // teal
      t.palette[6]    = ImVec4(0.95f, 0.80f, 0.32f, 1.00f); // gold
      t.palette[7]    = ImVec4(0.60f, 0.82f, 0.38f, 1.00f); // lime
      t.palette[8]    = ImVec4(0.90f, 0.55f, 0.75f, 1.00f); // pink
      t.palette[9]    = ImVec4(0.55f, 0.70f, 0.95f, 1.00f); // periwinkle
      return t;
  }

  PlotTheme plot_theme_preset_light() {
      PlotTheme t;
      t.name          = "light";
      t.bg            = ImVec4(0.97f, 0.97f, 0.97f, 1.00f);
      t.frame_bg      = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
      t.text          = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
      t.grid          = ImVec4(0.70f, 0.70f, 0.72f, 0.60f);
      t.palette_count = 10;
      t.palette[0]    = ImVec4(0.12f, 0.47f, 0.71f, 1.00f); // deep blue
      t.palette[1]    = ImVec4(0.17f, 0.63f, 0.17f, 1.00f); // deep green
      t.palette[2]    = ImVec4(0.84f, 0.37f, 0.00f, 1.00f); // burnt orange
      t.palette[3]    = ImVec4(0.84f, 0.15f, 0.16f, 1.00f); // vivid red
      t.palette[4]    = ImVec4(0.58f, 0.40f, 0.74f, 1.00f); // purple
      t.palette[5]    = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // cyan
      t.palette[6]    = ImVec4(0.74f, 0.74f, 0.13f, 1.00f); // olive
      t.palette[7]    = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // sky blue
      t.palette[8]    = ImVec4(0.89f, 0.47f, 0.76f, 1.00f); // rose
      t.palette[9]    = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); // grey
      return t;
  }

  PlotTheme plot_theme_preset_high_contrast() {
      PlotTheme t;
      t.name          = "high_contrast";
      t.bg            = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
      t.frame_bg      = ImVec4(0.05f, 0.05f, 0.05f, 1.00f);
      t.text          = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
      t.grid          = ImVec4(0.30f, 0.30f, 0.30f, 0.80f);
      t.palette_count = 10;
      t.palette[0]    = ImVec4(0.00f, 1.00f, 1.00f, 1.00f); // cyan neon
      t.palette[1]    = ImVec4(0.00f, 1.00f, 0.00f, 1.00f); // green neon
      t.palette[2]    = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); // orange neon
      t.palette[3]    = ImVec4(1.00f, 0.00f, 0.50f, 1.00f); // magenta
      t.palette[4]    = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); // yellow
      t.palette[5]    = ImVec4(0.40f, 0.80f, 1.00f, 1.00f); // sky blue
      t.palette[6]    = ImVec4(1.00f, 0.40f, 0.40f, 1.00f); // salmon
      t.palette[7]    = ImVec4(0.60f, 1.00f, 0.40f, 1.00f); // lime
      t.palette[8]    = ImVec4(1.00f, 0.80f, 1.00f, 1.00f); // lavender bright
      t.palette[9]    = ImVec4(0.80f, 1.00f, 0.80f, 1.00f); // mint bright
      return t;
  }

  // ---------------------------------------------------------------------------
  // Internal helper
  // ---------------------------------------------------------------------------

  static void apply_imgui_colors(const PlotTheme& theme) {
      ImGuiStyle& s = ImGui::GetStyle();
      s.Colors[ImGuiCol_WindowBg]       = theme.bg;
      s.Colors[ImGuiCol_ChildBg]        = theme.bg;
      s.Colors[ImGuiCol_PopupBg]        = theme.frame_bg;
      s.Colors[ImGuiCol_FrameBg]        = theme.frame_bg;
      s.Colors[ImGuiCol_FrameBgHovered] = theme.frame_bg;
      s.Colors[ImGuiCol_FrameBgActive]  = theme.frame_bg;
      s.Colors[ImGuiCol_Text]           = theme.text;
      s.Colors[ImGuiCol_TextDisabled]   = ImVec4(theme.text.x * 0.5f,
                                                 theme.text.y * 0.5f,
                                                 theme.text.z * 0.5f,
                                                 0.80f);
  }

  static void apply_implot_colors(const PlotTheme& theme) {
      ImPlotStyle& ps = ImPlot::GetStyle();
      ps.Colors[ImPlotCol_PlotBg]       = theme.bg;
      ps.Colors[ImPlotCol_PlotBorder]   = theme.frame_bg;
      ps.Colors[ImPlotCol_FrameBg]      = theme.frame_bg;
      ps.Colors[ImPlotCol_LegendBg]     = theme.frame_bg;
      ps.Colors[ImPlotCol_LegendBorder] = theme.grid;
      ps.Colors[ImPlotCol_LegendText]   = theme.text;
      ps.Colors[ImPlotCol_TitleText]    = theme.text;
      ps.Colors[ImPlotCol_InlayText]    = theme.text;
      ps.Colors[ImPlotCol_AxisText]     = theme.text;
      ps.Colors[ImPlotCol_AxisGrid]     = theme.grid;
      ps.Colors[ImPlotCol_AxisTick]     = theme.grid;

      // Register custom colormap from palette
      const int count = theme.palette_count > 10 ? 10 : theme.palette_count;
      ImPlot::AddColormap(theme.name, theme.palette, count, false);
      ImPlot::SetColormap(theme.name);
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  void plot_theme_apply(const PlotTheme& theme) {
      apply_imgui_colors(theme);
      apply_implot_colors(theme);
  }

  void plot_theme_dark() {
      PlotTheme t = plot_theme_preset_dark();
      plot_theme_apply(t);
  }

  void plot_theme_light() {
      PlotTheme t = plot_theme_preset_light();
      plot_theme_apply(t);
  }

  void plot_theme_high_contrast() {
      PlotTheme t = plot_theme_preset_high_contrast();
      plot_theme_apply(t);
  }

params_schema:
  {"params":[{"name":"theme","desc":"Estructura PlotTheme con nombre, colores de fondo/frame/texto/grid y paleta de hasta 10 colores para series de datos"}],"output":"Aplica el tema al contexto ImPlot/ImGui actual modificando ImGuiStyle y ImPlotStyle en memoria"}


id: sidebar_cpp_core
name: sidebar
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: bool sidebar_begin(const char* title, bool* open, float width = 250.0f)
description: Panel lateral colapsable para filtros y controles de dashboard
tags: ["imgui","sidebar","panel","layout","dashboard","controls"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/sidebar.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 6d17e2d385a23082779e6593baa3f6806992f0657326d379bb873260164bdff5
example:
  ```cpp
  static bool filters_open = true;

  void render_fn() {
      if (sidebar_begin("Filters", &filters_open)) {
          ImGui::SliderFloat("Min value", &min_val, 0.0f, 100.0f);
          ImGui::Checkbox("Show inactive", &show_inactive);
      }
      sidebar_end();

      // contenido principal
      ImGui::Begin("Main");
      // ...
      ImGui::End();
  }
  ```

notes:
  El estado de `s_sidebar_was_open` es una variable estatica interna que coordina `sidebar_begin` y `sidebar_end`. Solo un sidebar activo a la vez por frame (patron begin/end clasico de ImGui). Para multiples sidebars simultaneos, instanciar logica propia con estado separado.

documentation:
  # sidebar

  Panel lateral colapsable. Cuando `*open == true` renderiza una ventana ImGui de ancho fijo con boton de cierre. Cuando `*open == false` muestra un boton compacto ">" para reabrir.

  Siempre llamar `sidebar_end()` despues de `sidebar_begin()`, independientemente del valor de retorno.

code:
  #include "core/sidebar.h"
  #include "imgui.h"

  static bool s_sidebar_was_open = false;

  bool sidebar_begin(const char* title, bool* open, float width) {
      if (!*open) {
          s_sidebar_was_open = false;
          if (ImGui::Button(">")) {
              *open = true;
          }
          return false;
      }

      s_sidebar_was_open = true;
      ImGui::SetNextWindowSize(ImVec2(width, 0.0f), ImGuiCond_Always);
      ImGui::Begin(title, open, ImGuiWindowFlags_NoCollapse);
      return true;
  }

  void sidebar_end() {
      if (s_sidebar_was_open) {
          ImGui::End();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del sidebar mostrado en la barra de titulo de la ventana"},{"name":"open","desc":"Puntero al estado abierto/cerrado; se pone a false si el usuario cierra la ventana"},{"name":"width","desc":"Ancho del sidebar en pixels (por defecto 250)"}],"output":"true si el sidebar esta abierto y se debe renderizar contenido entre sidebar_begin y sidebar_end"}


id: tab_container_cpp_core
name: tab_container
kind: component
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: bool tab_container_begin(const char* id)
description: Contenedor de tabs para organizar vistas multiples en un dashboard
tags: ["imgui","tabs","container","layout","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/core/tab_container.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 4ab6bab6a6677ab5d3079bf8537e2fd9ae4ebeefe9b68520b57f5ad269c24003
example:
  ```cpp
  void render_fn() {
      ImGui::Begin("Dashboard");

      if (tab_container_begin("##main_tabs")) {
          if (tab_container_tab("Overview")) {
              render_overview();
              tab_container_tab_end();
          }
          if (tab_container_tab("Details")) {
              render_details();
              tab_container_tab_end();
          }
          if (tab_container_tab("Settings")) {
              render_settings();
              tab_container_tab_end();
          }
      }
      tab_container_end();

      ImGui::End();
  }
  ```

notes:
  Los flags `ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown` son fijos para estandarizar la experiencia de tabs en dashboards. Si se necesitan flags distintos usar `ImGui::BeginTabBar` directamente.

documentation:
  # tab_container

  Wrapper fino sobre `ImGui::BeginTabBar` / `EndTabBar` con flags de dashboard preconfigurados: `Reorderable` (arrastrar tabs) y `FittingPolicyResizeDown` (tabs se achican si no caben).

  API de cuatro funciones siguiendo el patron begin/end de ImGui. Siempre llamar `tab_container_end()` si `tab_container_begin()` retorno true. Siempre llamar `tab_container_tab_end()` dentro del bloque `if (tab_container_tab(...))`.

code:
  #include "core/tab_container.h"
  #include "imgui.h"

  bool tab_container_begin(const char* id) {
      return ImGui::BeginTabBar(
          id,
          ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown
      );
  }

  bool tab_container_tab(const char* label) {
      return ImGui::BeginTabItem(label);
  }

  void tab_container_tab_end() {
      ImGui::EndTabItem();
  }

  void tab_container_end() {
      ImGui::EndTabBar();
  }

params_schema:
  {"params":[{"name":"id","desc":"Identificador unico del tab bar (ej: '##views'). Puede ser invisible con prefijo ##"},{"name":"label","desc":"Etiqueta visible de cada tab individual"}],"output":"tab_container_begin: true si el tab bar esta activo. tab_container_tab: true si el tab esta seleccionado y se debe renderizar su contenido"}


id: time_series_buffer_cpp_core
name: time_series_buffer
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: TimeSeriesBuffer(size_t capacity)
tags: ["buffer","timeseries","streaming","dashboard","data"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: cpp/functions/core/time_series_buffer.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: bf82ceb467c8e2bf492d9b9805f3562c7f7ab08c358c9babe336335522726c3d
description:
  Ring buffer circular para datos de series temporales, optimizado para streaming de metricas en dashboards en tiempo real

notes:
  - No depende de ImGui ni ImPlot directamente — es una estructura de datos pura.
  - No es thread-safe. Para uso multihilo, proteger con mutex externo.
  - Move semantics implementadas; copy queda eliminada (`= delete`) para evitar copias accidentales del heap.
  - `min`, `max` y `average` iteran sobre `count` elementos — O(n). Para buffers grandes en hot paths, considerar mantener un acumulador incremental.

documentation:
  # time_series_buffer

  Ring buffer circular de floats para series temporales en tiempo real. Diseñado para alimentar plots de ImPlot con metricas de streaming (FPS, latencia, uso de CPU, etc.).

  ## Uso basico

  ```cpp
  TimeSeriesBuffer fps_history(512);

  // En el loop principal:
  fps_history.push(ImGui::GetIO().Framerate);

  // Para renderizar con ImPlot:
  float ordered[512];
  size_t n = fps_history.copy_ordered(ordered, 512);
  ImPlot::PlotLines("FPS", ordered, (int)n);
  ```

  ## Semantica del ring buffer

  - `push(v)` escribe en `data[offset % capacity]` y avanza el write head.
  - `count` crece hasta `capacity` y se mantiene ahi — el buffer nunca desborda.
  - `get(0)` retorna el elemento mas antiguo; `get(count-1)` el mas reciente.
  - `latest()` es equivalente a `get(count-1)` pero mas explicito.
  - `copy_ordered` extrae los datos en orden cronologico para pasarlos a ImPlot u otro consumidor que espere un array contiguo.

code:
  #include "time_series_buffer.h"

  #include <algorithm>
  #include <cassert>
  #include <cstring>
  #include <limits>
  #include <numeric>
  #include <utility>

  TimeSeriesBuffer::TimeSeriesBuffer(size_t cap)
      : data(new float[cap]), capacity(cap), count(0), offset(0) {}

  TimeSeriesBuffer::~TimeSeriesBuffer() {
      delete[] data;
  }

  TimeSeriesBuffer::TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept
      : data(other.data), capacity(other.capacity), count(other.count), offset(other.offset) {
      other.data     = nullptr;
      other.capacity = 0;
      other.count    = 0;
      other.offset   = 0;
  }

  TimeSeriesBuffer& TimeSeriesBuffer::operator=(TimeSeriesBuffer&& other) noexcept {
      if (this != &other) {
          delete[] data;
          data           = other.data;
          capacity       = other.capacity;
          count          = other.count;
          offset         = other.offset;
          other.data     = nullptr;
          other.capacity = 0;
          other.count    = 0;
          other.offset   = 0;
      }
      return *this;
  }

  void TimeSeriesBuffer::push(float value) {
      data[offset % capacity] = value;
      offset++;
      if (count < capacity) count++;
  }

  float TimeSeriesBuffer::get(size_t index) const {
      assert(index < count);
      // oldest element is at (offset - count) % capacity
      size_t real = (offset - count + index) % capacity;
      return data[real];
  }

  float TimeSeriesBuffer::latest() const {
      assert(count > 0);
      return data[(offset - 1) % capacity];
  }

  float TimeSeriesBuffer::min() const {
      float m = std::numeric_limits<float>::max();
      for (size_t i = 0; i < count; ++i) m = std::min(m, get(i));
      return m;
  }

  float TimeSeriesBuffer::max() const {
      float m = std::numeric_limits<float>::lowest();
      for (size_t i = 0; i < count; ++i) m = std::max(m, get(i));
      return m;
  }

  float TimeSeriesBuffer::average() const {
      if (count == 0) return 0.0f;
      float sum = 0.0f;
      for (size_t i = 0; i < count; ++i) sum += get(i);
      return sum / static_cast<float>(count);
  }

  size_t TimeSeriesBuffer::size() const {
      return count;
  }

  bool TimeSeriesBuffer::full() const {
      return count == capacity;
  }

  size_t TimeSeriesBuffer::copy_ordered(float* out, size_t out_capacity) const {
      size_t n = std::min(count, out_capacity);
      for (size_t i = 0; i < n; ++i)
          out[i] = get(i);
      return n;
  }

params_schema:
  {"params":[{"name":"capacity","desc":"Numero maximo de muestras que almacena el buffer"}],"output":"Buffer circular listo para push de datos y lectura ordenada"}


id: tracy_zone_cpp_core
name: tracy_zone
kind: function
lang: cpp
domain: core
version: 1.0.0
purity: pure
signature: FN_ZONE(name) / FN_ZONE_COLOR(name, color) / FN_FRAME_MARK / FN_PLOT(name, val)
description: Macros y constantes de conveniencia para Tracy profiling zones, compilables sin Tracy
tags: ["tracy","profiling","debug","performance","raii"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: cpp/functions/core/tracy_zone.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: d97e1d27b60810d0c0ae4455f1e85d22d3ffa3b25cd2927535c48a76ffc12c91
example:
  ```cpp
  #include "core/tracy_zone.h"

  void render_scene() {
      FN_ZONE_COLOR("render_scene", fn_tracy::COLOR_RENDER);
      // ... render calls ...
  }

  void update(float dt) {
      FN_ZONE("update");
      // ... game logic ...
      FN_PLOT("dt_ms", dt * 1000.0f);
  }

  void main_loop() {
      while (running) {
          update(dt);
          render_scene();
          FN_FRAME_MARK;
      }
  }
  ```

notes:
  - Compilar con `-DTRACY_ENABLE` y enlazar `TracyClient.cpp` para activar profiling.
  - Sin `TRACY_ENABLE` el .cpp es prácticamente vacío — coste cero en producción.
  - Los colores son ARGB; si el alpha es 0 Tracy aplica su color automático por zona.
  - `FN_ZONE` expande a `ZoneScopedN(name)` de Tracy, que crea el contexto con `__LINE__` como discriminador — es seguro llamar varias veces en el mismo scope.

documentation:
  # tracy_zone

  Macros de scope para instrumentar secciones de código con Tracy Profiler. Cuando `TRACY_ENABLE` no está definido (ej. builds de producción) todas las macros se expanden a `(void)0`, sin coste alguno.

  ## Macros disponibles

  | Macro | Descripción |
  |---|---|
  | `FN_ZONE("name")` | Zona con nombre, color automático |
  | `FN_ZONE_COLOR("name", color)` | Zona con nombre y color ARGB explícito |
  | `FN_FRAME_MARK` | Marca el límite de frame (eje X de Tracy) |
  | `FN_PLOT("name", val)` | Envía un valor escalar al panel de plots |

  ## Colores predefinidos (`fn_tracy::`)

  ```cpp
  fn_tracy::COLOR_RENDER  // 0x2196F3 azul   — rendering
  fn_tracy::COLOR_UPDATE  // 0x4CAF50 verde  — game update / logic
  fn_tracy::COLOR_IO      // 0xFF9800 naranja — I/O disco/red
  fn_tracy::COLOR_NETWORK // 0xF44336 rojo   — red/HTTP
  fn_tracy::COLOR_COMPUTE // 0x9C27B0 morado — compute / shader prep
  ```

code:
  // tracy_zone.cpp
  // All definitions live in tracy_zone.h (macros + constexpr).
  // This translation unit exists so the header can be included in any build
  // without requiring Tracy — compile with -DTRACY_ENABLE to activate profiling.
  #include "core/tracy_zone.h"

params_schema:
  {"params":[{"name":"name","desc":"Nombre de la zona tal como aparece en la timeline de Tracy"},{"name":"color","desc":"Color ARGB uint32 de la zona en Tracy (opcional, solo FN_ZONE_COLOR)"}],"output":"Zona Tracy activa durante el scope actual; no-op cuando TRACY_ENABLE no está definido"}


id: bar_chart_cpp_viz
name: bar_chart
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
description: Renderiza un grafico de barras verticales usando ImPlot dentro de un frame ImGui
tags: ["implot","chart","visualization","gpu","bar"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot"]
tests: []
file_path: cpp/functions/viz/bar_chart.cpp
created_at: 2026-04-06T21:31:43Z
updated_at: 2026-04-06T21:31:43Z
props: []
emits: []
framework: imgui
variant: []
content_hash: f4b83230686eb4031c0bd4fcc3851aee0475dcc7e8baa1297e759c9356783d48
signature:
  void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width)

documentation:
  # bar_chart

  Wrapper atomico sobre `ImPlot::PlotBars` con configuracion automatica de etiquetas en el eje X.

  Debe llamarse dentro del render callback de `fn::run_app`.

code:
  #include "viz/bar_chart.h"
  #include "implot.h"

  #include <vector>

  void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          std::vector<double> positions(count);
          for (int i = 0; i < count; i++) positions[i] = i;

          ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels);
          ImPlot::PlotBars("##data", values, count, bar_width);
          ImPlot::EndPlot();
      }
  }

  void bar_chart(const char* title, const char* const* labels, const double* values, int count, double bar_width) {
      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          std::vector<double> positions(count);
          for (int i = 0; i < count; i++) positions[i] = i;

          ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels);
          ImPlot::PlotBars("##data", values, count, bar_width);
          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del grafico de barras"},{"name":"labels","desc":"Array de etiquetas para el eje X, una por barra"},{"name":"values","desc":"Array de valores numericos para la altura de cada barra"},{"name":"count","desc":"Numero de barras (longitud de labels y values)"},{"name":"bar_width","desc":"Ancho de cada barra como fraccion del espacio disponible (default 0.67)"}],"output":"Renderiza el grafico de barras en el frame ImGui actual"}


id: candlestick_cpp_viz
name: candlestick
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
tags: ["implot","chart","visualization","gpu","candlestick","ohlc","finance"]
uses_functions: []
uses_types: []
returns: []
imports: ["implot","imgui"]
tests: []
file_path: cpp/functions/viz/candlestick.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: c21c6466b2b06e80e69f72790865b6f012153a8665c1da016f8ac97a6e5897d0
signature:
  void candlestick(const char* title, const double* dates, const double* opens, const double* closes, const double* lows, const double* highs, int count, float width_percent = 0.25f, bool tooltip = true)

description:
  Renderiza un grafico de velas OHLC usando ImPlot custom rendering. Verde para velas alcistas (close >= open), rojo para bajistas.

example:
  ```cpp
  // arrays de datos financieros (timestamps Unix, precios)
  candlestick("BTC/USD", dates, opens, closes, lows, highs, 90);

  // sin tooltip, velas mas anchas
  candlestick("ETH/USD", dates, opens, closes, lows, highs, 30, 0.6f, false);
  ```

notes:
  - El ancho de cada vela se calcula como `(dates[1] - dates[0]) * width_percent * 0.5` en cada lado. Asume spacing uniforme entre velas.
  - Para un solo punto (`count == 1`) el spacing por defecto es 1.0.
  - La deteccion de hover usa un margen de 4px alrededor del area cuerpo+wick para facilitar la interaccion.
  - El eje X usa `ImPlotScale_Time` — si los datos son indices numericos simples (0, 1, 2...) en lugar de timestamps, pasar `ImPlotAxisFlags_NoDecorations` o cambiar `SetupAxisScale`.

documentation:
  # candlestick

  Grafico de velas OHLC completo usando custom rendering de ImPlot. Dibuja body (open-close) y wicks (high-low) por vela usando `ImPlot::GetPlotDrawList()` y `ImPlot::PlotToPixels()` para conversion de coordenadas.

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). El eje X se configura con `ImPlotScale_Time` para timestamps Unix.

  Solo tiene overload `double` porque los datos financieros requieren doble precision.

code:
  #include "viz/candlestick.h"
  #include "imgui.h"
  #include "implot.h"

  void candlestick(const char* title, const double* dates, const double* opens,
                   const double* closes, const double* lows, const double* highs,
                   int count, float width_percent, bool tooltip) {
      if (count <= 0) return;

      // Compute half-width of each candle body in data coordinates.
      // Use spacing between consecutive dates when count > 1, else fallback to 0.5.
      double spacing = (count > 1) ? (dates[1] - dates[0]) : 1.0;
      double half_w  = spacing * (double)width_percent * 0.5;

      ImPlot::SetupAxes("Date", "Price", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
      ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time);

      // Auto-fit X axis to the data range with a small margin.
      ImPlot::SetupAxisLimits(ImAxis_X1,
                              dates[0]         - spacing,
                              dates[count - 1] + spacing,
                              ImGuiCond_Always);

      if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
          ImDrawList* draw = ImPlot::GetPlotDrawList();

          const ImU32 col_bull = IM_COL32(0,  200, 80,  255);  // green — close >= open
          const ImU32 col_bear = IM_COL32(220, 50, 50,  255);  // red   — close < open

          int hovered_idx = -1;

          for (int i = 0; i < count; i++) {
              double x     = dates[i];
              double open  = opens[i];
              double close = closes[i];
              double low   = lows[i];
              double high  = highs[i];

              bool bullish = (close >= open);
              ImU32 col   = bullish ? col_bull : col_bear;

              // Convert data coordinates to screen pixels.
              ImVec2 body_tl = ImPlot::PlotToPixels(x - half_w, bullish ? close : open);
              ImVec2 body_br = ImPlot::PlotToPixels(x + half_w, bullish ? open  : close);
              ImVec2 wick_hi = ImPlot::PlotToPixels(x, high);
              ImVec2 wick_lo = ImPlot::PlotToPixels(x, low);
              float  cx      = (body_tl.x + body_br.x) * 0.5f;

              // Wick (high-low vertical line).
              draw->AddLine(ImVec2(cx, wick_hi.y), ImVec2(cx, wick_lo.y), col, 1.5f);

              // Body rectangle (open-close).
              // Ensure at least 1px height so flat candles are visible.
              if (body_br.y <= body_tl.y + 1.0f) body_br.y = body_tl.y + 1.0f;
              draw->AddRectFilled(body_tl, body_br, col);
              draw->AddRect(body_tl, body_br, col);

              // Track hovered candle for tooltip.
              if (tooltip && ImPlot::IsPlotHovered()) {
                  ImVec2 mouse = ImGui::GetMousePos();
                  if (mouse.x >= body_tl.x - 4 && mouse.x <= body_br.x + 4 &&
                      mouse.y >= wick_hi.y  - 4 && mouse.y <= wick_lo.y  + 4) {
                      hovered_idx = i;
                  }
              }
          }

          // Tooltip for the hovered candle.
          if (tooltip && hovered_idx >= 0) {
              int i = hovered_idx;
              ImGui::BeginTooltip();
              ImGui::Text("O: %.4f", opens[i]);
              ImGui::Text("H: %.4f", highs[i]);
              ImGui::Text("L: %.4f", lows[i]);
              ImGui::Text("C: %.4f", closes[i]);
              ImGui::EndTooltip();
          }

          ImPlot::EndPlot();
      }
  }

params_schema:
  {"params":[{"name":"title","desc":"Titulo del grafico, se muestra como header del plot"},{"name":"dates","desc":"Array de timestamps Unix o indices numericos del eje X, uno por vela"},{"name":"opens","desc":"Array de precios de apertura, uno por vela"},{"name":"closes","desc":"Array de precios de cierre, uno por vela"},{"name":"lows","desc":"Array de precios minimos (punta inferior del wick), uno por vela"},{"name":"highs","desc":"Array de precios maximos (punta superior del wick), uno por vela"},{"name":"count","desc":"Numero de velas (longitud de todos los arrays)"},{"name":"width_percent","desc":"Ancho del body de cada vela como fraccion del espacio entre puntos consecutivos (0.0-1.0, default 0.25)"},{"name":"tooltip","desc":"Si true, muestra tooltip con valores O/H/L/C al hacer hover sobre una vela"}],"output":"Renderiza el grafico de velas OHLC en el frame ImGui actual, sin retornar valor"}


id: gauge_cpp_viz
name: gauge
kind: component
lang: cpp
domain: viz
version: 1.0.0
purity: pure
signature: void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f)
description: Renderiza un indicador circular tipo gauge/velocimetro usando ImGui draw primitives
tags: ["imgui","visualization","gauge","kpi","dashboard"]
uses_functions: []
uses_types: []
returns: []
imports: ["imgui"]
tests: []
file_path: cpp/functions/viz/gauge.cpp
created_at: 2026-04-07T21:42:49Z
updated_at: 2026-04-07T21:42:49Z
props: []
emits: []
framework: imgui
variant: []
content_hash: 585a6c8133bbb20d80773f10639191b4fbb118ac7496940a7745f3e06b01b86a
example:
  ```cpp
  // KPI card con gauge de temperatura
  gauge("CPU Temp", 72.5f, 0.0f, 100.0f, 50.0f);

  // Gauge grande para dashboard principal
  gauge("Velocidad", 3200.0f, 0.0f, 5000.0f, 80.0f);
  ```

notes:
  - El arco de fondo es gris oscuro (IM_COL32(60,60,60,220)), 6px de grosor.
  - La aguja tiene longitud del 75% del radio para evitar solapar el arco.
  - Usa solo `float`; no ofrece overload `double` porque ImGui DrawList trabaja en coordenadas de pantalla (float).
  - El espacio reservado es `diameter x (diameter + line_height)` para incluir la etiqueta.

documentation:
  # gauge

  Indicador circular tipo gauge/velocimetro construido sobre ImGui draw primitives. No requiere ImPlot.

  El arco ocupa 240 grados (de 150deg a 390deg en sentido horario). El color del arco de valor interpolado de verde (minimo) a amarillo (mitad) a rojo (maximo). Una aguja blanca apunta al valor actual.

  Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).

code:
  #include "viz/gauge.h"
  #include "imgui.h"
  #include <cmath>
  #include <cstdio>

  #ifndef M_PI
  #define M_PI 3.14159265358979323846f
  #endif

  void gauge(const char* label, float value, float min_val, float max_val, float radius) {
      ImDrawList* draw_list = ImGui::GetWindowDrawList();
      ImVec2 pos = ImGui::GetCursorScreenPos();

      // Reserve space: diameter + label line
      float diameter = radius * 2.0f;
      ImGui::Dummy(ImVec2(diameter, diameter + ImGui::GetTextLineHeightWithSpacing()));

      ImVec2 center = ImVec2(pos.x + radius, pos.y + radius);

      // Arc spans 240 degrees: from 150deg to 390deg (i.e. 150 to 30 going clockwise)
      // In screen space Y is down, so angles go clockwise.
      // Start angle: 150 degrees = bottom-left, End angle: 390 = 30 degrees = bottom-right
      const float angle_start = (150.0f * (float)M_PI) / 180.0f;
      const float angle_end   = (390.0f * (float)M_PI) / 180.0f;
      const int   num_segments = 64;

      // Background arc (dark gray)
      ImU32 bg_color = IM_COL32(60, 60, 60, 220);
      for (int i = 0; i < num_segments; i++) {
          float a0 = angle_start + (angle_end - angle_start) * ((float)i       / num_segments);
          float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
          draw_list->AddLine(
              ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
              ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
              bg_color, 6.0f);
      }

      // Normalize value to [0, 1]
      float t = 0.0f;
      if (max_val > min_val) {
          t = (value - min_val) / (max_val - min_val);
          if (t < 0.0f) t = 0.0f;
          if (t > 1.0f) t = 1.0f;
      }

      // Color: green (t=0) -> yellow (t=0.5) -> red (t=1)
      float r, g, b;
      if (t < 0.5f) {
          float s = t * 2.0f;
          r = (unsigned char)(s * 255);
          g = 200;
          b = 0;
      } else {
          float s = (t - 0.5f) * 2.0f;
          r = 220;
          g = (unsigned char)((1.0f - s) * 200);
          b = 0;
      }
      ImU32 value_color = IM_COL32((int)r, (int)g, (int)b, 255);

      // Value arc
      float angle_value = angle_start + (angle_end - angle_start) * t;
      int value_segments = (int)(num_segments * t);
      for (int i = 0; i < value_segments; i++) {
          float a0 = angle_start + (angle_end - angle_start) * ((float)i       / num_segments);
          float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
          draw_list->AddLine(
              ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
              ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
              value_color, 6.0f);
      }

      // Needle: line from center to arc at current angle
      float needle_len = radius * 0.75f;
      ImVec2 needle_tip = ImVec2(
          center.x + cosf(angle_value) * needle_len,
          center.y + sinf(angle_value) * needle_len);
      draw_list->AddLine(center, needle_tip, IM_COL32(255, 255, 255, 240), 2.0f);
      draw_list->AddCircleFilled(center, 4.0f, IM_COL32(255, 255, 255, 200));

      // Value text centered
      char val_buf[32];
      snprintf(val_buf, sizeof(val_buf), "%.1f", value);
      ImVec2 val_size = ImGui::CalcTextSize(val_buf);
      draw_list->AddText(
          ImVec2(center.x - val_size.x * 0.5f, center.y + radius * 0.35f),
          IM_COL32(230, 230, 230, 255), val_buf);

      // Label below value
      ImVec2 label_size = ImGui::CalcTextSize(label);
      draw_list->AddText(
          ImVec2(center.x - label_size.x * 0.5f, center.y + radius * 0.35f + val_size.y + 2.0f),
          IM_COL32(180, 180, 180, 200), label);
  }

params_schema:
  {"params":[{"name":"label","desc":"Etiqueta del gauge, se muestra centrada debajo del valor numerico"},{"name":"value","desc":"Valor actual a mostrar en el gauge"},{"name":"min_val","desc":"Valor minimo de la escala (extremo izquierdo del arco)"},{"name":"max_val","desc":"Valor maximo de la escala (extremo derecho del arco)"},{"name":"radius","desc":"Radio del gauge en pixels (default 60.0)"}],"output":"Renderiza el gauge en el frame ImGui actual, reservando espacio con ImGui::Dummy"}


