merge: quick/new-bash-go-functions — Nuevas funciones Bash y Go multi-dominio

Batch de funciones nuevas para el registry:
- 12 funciones Bash shell (utilidades de scripting y git)
- 10 funciones Bash infra (instaladores y diagnostico)
- 12 funciones Bash cybersecurity (auditoria y hardening)
- 2 pipelines Bash (inicializacion de proyectos Go)
- 5 funciones Go core (strings y versiones)
- 7 funciones Go infra (gestion SSH config) + tipo SshConfigEntry
- 1 funcion Go shell (extract_script_description)
- 7 funciones Go tui (renderizado y terminal helpers)
This commit is contained in:
2026-04-12 13:55:34 +02:00
116 changed files with 7664 additions and 0 deletions
@@ -0,0 +1,52 @@
---
name: analyze_dns
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "analyze_dns(domain: string, mode: string) -> void"
description: "Análisis DNS completo de un dominio: registros A/AAAA/MX/NS/TXT/CNAME/SOA, consulta whois y verificación contra listas negras DNSBL (spamhaus, spamcop, sorbs, barracuda)."
tags: [bash, cybersecurity, dns, network, whois, dnsbl, reconnaissance]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: domain
desc: "dominio a analizar, ej: example.com"
- name: mode
desc: "modo de análisis: records (solo registros DNS), whois (solo whois), dnsbl (solo listas negras) o all (todo, por defecto)"
output: "imprime registros DNS, información whois y estado DNSBL a stdout con colores ANSI"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/analyze_dns.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/redes/analisis_dns.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/analyze_dns.sh
# Análisis completo
analyze_dns example.com
# Solo registros DNS
analyze_dns example.com records
# Solo whois
analyze_dns example.com whois
# Solo DNSBL
analyze_dns example.com dnsbl
```
## Notas
Requiere `dig` (paquete dnsutils). `whois` es opcional — si no está instalado y el modo es `all`, se omite el paso whois con aviso. Las listas negras DNSBL se consultan via DNS inverso (técnica estándar sin HTTP). El modo `dnsbl` resuelve primero la IP del dominio y luego construye la consulta invertida para cada blacklist.
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# analyze_dns
# -----------
# Análisis DNS completo de un dominio: registros A/AAAA/MX/NS/TXT/CNAME/SOA,
# consulta whois y verificación contra listas negras DNSBL.
#
# USO (directo):
# analyze_dns example.com [records|whois|dnsbl|all]
#
# Depende de: dig, whois (opcional), curl (para DNSBL)
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 ──────────────────────────────────────────────────────────
_dns_is_valid_domain() {
local domain="$1"
[[ -n "$domain" && "$domain" =~ ^[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$ ]]
}
_dns_build_dnsbl_query() {
local ip="$1"
local bl="$2"
local reversed
reversed="$(echo "$ip" | awk -F. '{print $4"."$3"."$2"."$1}')"
echo "${reversed}.${bl}"
}
# ─── Funciones de efecto ──────────────────────────────────────────────────────
_dns_query_record() {
local domain="$1"
local type="$2"
local result
result="$(dig +short "$type" "$domain" 2>/dev/null || true)"
if [[ -z "$result" ]]; then
echo " (sin registros)"
else
echo "$result" | while IFS= read -r line; do
echo " * $line"
done
fi
}
_dns_show_all_records() {
local domain="$1"
echo ""
for type in A AAAA MX NS TXT CNAME SOA; do
echo -e "${CYAN}── ${type} ──────────────────${NC}"
_dns_query_record "$domain" "$type"
echo ""
done
}
_dns_show_whois() {
local domain="$1"
echo ""
info "Consultando whois de ${domain}..."
echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
whois "$domain" 2>/dev/null \
| grep -iE "(registrar|registrant|creation|expiry|expire|updated|name server|status)" \
| head -20 \
| while IFS= read -r line; do echo -e " ${DIM_GRAY}${line}${NC}"; done
echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
}
_dns_check_dnsbl() {
local domain="$1"
local ip
ip="$(dig +short A "$domain" 2>/dev/null | head -1 || true)"
if [[ -z "$ip" ]]; then
warning "No se pudo resolver la IP de $domain para comprobar DNSBL"
return
fi
info "IP a comprobar: $ip"
echo ""
local blacklists=(
"zen.spamhaus.org"
"bl.spamcop.net"
"dnsbl.sorbs.net"
"b.barracudacentral.org"
)
local found=0
for bl in "${blacklists[@]}"; do
local query
query="$(_dns_build_dnsbl_query "$ip" "$bl")"
local result
result="$(dig +short A "$query" 2>/dev/null || true)"
if [[ -n "$result" ]]; then
echo -e " ${RED}LISTADO${NC} ${bl} ($result)"
found=$((found + 1))
else
echo -e " ${GREEN}limpio${NC} ${bl}"
fi
done
echo ""
if [[ $found -eq 0 ]]; then
success "La IP no aparece en ninguna lista negra comprobada"
else
warning "La IP aparece en ${found} lista(s) negra(s)"
fi
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
analyze_dns() {
local domain="$1"
local mode="${2:-all}"
if [[ -z "$domain" ]]; then
error "analyze_dns: se requiere un dominio como primer argumento" >&2
return 1
fi
if ! _dns_is_valid_domain "$domain"; then
error "analyze_dns: dominio no válido: '$domain'" >&2
return 1
fi
if ! command -v dig &>/dev/null; then
error "analyze_dns: 'dig' no está instalado (sudo apt install dnsutils)" >&2
return 1
fi
info "Analizando: ${domain}"
case "$mode" in
records)
_dns_show_all_records "$domain"
;;
whois)
if ! command -v whois &>/dev/null; then
error "analyze_dns: 'whois' no está instalado (sudo apt install whois)" >&2
return 1
fi
_dns_show_whois "$domain"
;;
dnsbl)
_dns_check_dnsbl "$domain"
;;
all)
_dns_show_all_records "$domain"
if command -v whois &>/dev/null; then
_dns_show_whois "$domain"
else
warning "whois no disponible, omitiendo"
fi
echo ""
_dns_check_dnsbl "$domain"
;;
*)
error "analyze_dns: modo no válido '$mode'. Use: records|whois|dnsbl|all" >&2
return 1
;;
esac
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
analyze_dns "$@"
fi
@@ -0,0 +1,47 @@
---
name: audit_http_headers
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "audit_http_headers(url: string) -> void"
description: "Audita las cabeceras HTTP de seguridad de una URL: verifica la presencia de HSTS (con validación de max-age mínimo de 6 meses), Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy y cabeceras CORS. También detecta cabeceras que exponen información del servidor."
tags: [bash, cybersecurity, web, http, headers, security, hsts, csp, hardening]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: url
desc: "URL del sitio web a auditar; si no tiene esquema se añade https:// automáticamente"
output: "imprime el estado de cada cabecera de seguridad (ok/falta/advertencia), el valor de las presentes y cabeceras que exponen información del servidor"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/audit_http_headers.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/web/cabeceras_http.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/audit_http_headers.sh
# Con URL completa
audit_http_headers https://example.com
# Sin esquema (añade https:// automáticamente)
audit_http_headers example.com
# Seguir redirecciones
audit_http_headers http://example.com
```
## Notas
Usa `curl -sI --location` para seguir redirecciones y obtener solo cabeceras. El check de HSTS valida que `max-age` sea >= 15.768.000 segundos (6 meses), valor mínimo recomendado por OWASP. Las cabeceras Server, X-Powered-By, X-AspNet-Version y X-Generator se marcan como advertencia por revelar información del stack tecnológico. Timeout de 15 segundos para evitar cuelgues.
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# audit_http_headers
# ------------------
# Audita las cabeceras HTTP de seguridad de una URL: HSTS, CSP, X-Frame-Options,
# X-Content-Type-Options, Referrer-Policy, Permissions-Policy y otras.
# También muestra cabeceras que exponen información del servidor.
#
# USO (directo):
# audit_http_headers <url>
#
# Depende de: curl
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 ──────────────────────────────────────────────────────────
_hdr_normalize_url() {
local url="$1"
if [[ ! "$url" =~ ^https?:// ]]; then
echo "https://${url}"
else
echo "$url"
fi
}
_hdr_header_present() {
local headers="$1"
local name="$2"
echo "$headers" | grep -qi "^${name}:"
}
_hdr_extract_value() {
local headers="$1"
local name="$2"
echo "$headers" | grep -i "^${name}:" | cut -d: -f2- | xargs
}
_hdr_hsts_is_strong() {
local value="$1"
local max_age
max_age="$(echo "$value" | grep -oE 'max-age=[0-9]+' | cut -d= -f2 || echo 0)"
[[ "$max_age" -ge 15768000 ]] # 6 meses en segundos
}
# ─── Funciones de efecto ──────────────────────────────────────────────────────
_hdr_fetch() {
local url="$1"
curl -sI --max-time 15 --location "$url" 2>/dev/null | tr -d '\r'
}
_hdr_check_header() {
local headers="$1"
local name="$2"
local description="$3"
if _hdr_header_present "$headers" "$name"; then
local value
value="$(_hdr_extract_value "$headers" "$name")"
echo -e " ${GREEN}[ok]${NC} ${name}"
echo -e " ${GRAY}${value}${NC}"
else
echo -e " ${RED}[x] ${NC} ${name} -- ${description}"
fi
}
_hdr_check_hsts() {
local headers="$1"
local name="Strict-Transport-Security"
if _hdr_header_present "$headers" "$name"; then
local value
value="$(_hdr_extract_value "$headers" "$name")"
if _hdr_hsts_is_strong "$value"; then
echo -e " ${GREEN}[ok]${NC} ${name}"
else
echo -e " ${YELLOW}[!] ${NC} ${name} -- max-age demasiado corto (<6 meses)"
fi
echo -e " ${GRAY}${value}${NC}"
else
echo -e " ${RED}[x] ${NC} ${name} -- HSTS no configurado"
fi
}
_hdr_show_server_info() {
local headers="$1"
echo ""
echo -e "${CYAN}── Información del servidor ──────────────────────${NC}"
for h in Server X-Powered-By X-AspNet-Version X-Generator; do
if _hdr_header_present "$headers" "$h"; then
local val
val="$(_hdr_extract_value "$headers" "$h")"
echo -e " ${YELLOW}[!]${NC} ${h}: ${val} (información expuesta)"
fi
done
local status
status="$(echo "$headers" | head -1)"
echo -e " ${CYAN}Status:${NC} ${status}"
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
audit_http_headers() {
local raw_url="$1"
if [[ -z "$raw_url" ]]; then
error "audit_http_headers: se requiere una URL como argumento" >&2
return 1
fi
if ! command -v curl &>/dev/null; then
error "audit_http_headers: 'curl' no está instalado (sudo apt install curl)" >&2
return 1
fi
local url
url="$(_hdr_normalize_url "$raw_url")"
info "Consultando cabeceras de: ${url}"
local headers
headers="$(_hdr_fetch "$url")"
if [[ -z "$headers" ]]; then
error "audit_http_headers: no se pudieron obtener las cabeceras. ¿El sitio está disponible?" >&2
return 1
fi
echo ""
echo -e "${PURPLE}════════ Cabeceras de Seguridad ════════════════${NC}"
echo ""
_hdr_check_hsts "$headers"
_hdr_check_header "$headers" "Content-Security-Policy" "Previene XSS e inyección de contenido"
_hdr_check_header "$headers" "X-Frame-Options" "Previene clickjacking"
_hdr_check_header "$headers" "X-Content-Type-Options" "Previene MIME sniffing"
_hdr_check_header "$headers" "Referrer-Policy" "Controla información del referrer"
_hdr_check_header "$headers" "Permissions-Policy" "Controla acceso a APIs del navegador"
_hdr_check_header "$headers" "Cross-Origin-Opener-Policy" "Aísla el contexto de navegación"
_hdr_check_header "$headers" "Cross-Origin-Resource-Policy" "Controla compartición de recursos"
_hdr_show_server_info "$headers"
echo ""
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
audit_http_headers "$@"
fi
@@ -0,0 +1,44 @@
---
name: audit_ssh_config
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "audit_ssh_config(config_path: string) -> void"
description: "Audita la configuración de sshd_config evaluando parámetros de seguridad críticos (PermitRootLogin, PasswordAuthentication, Port, MaxAuthTries, X11Forwarding, AllowUsers). También revisa intentos de login fallidos en los logs y lista las claves autorizadas del usuario actual."
tags: [bash, cybersecurity, ssh, audit, security, hardening, linux]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: config_path
desc: "ruta al archivo sshd_config a auditar (por defecto: /etc/ssh/sshd_config)"
output: "imprime checks con nivel ok/warn/bad para cada parámetro, últimos 10 intentos de login fallidos y lista de claves autorizadas"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/audit_ssh_config.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/sistema/auditar_ssh.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/audit_ssh_config.sh
# Auditar la configuración por defecto
audit_ssh_config
# Auditar un archivo alternativo
audit_ssh_config /etc/ssh/sshd_config.d/custom.conf
```
## Notas
Los logs de intentos fallidos se buscan primero en `journalctl` (systemd) y si no está disponible en `/var/log/auth.log`. Leer `/etc/ssh/sshd_config` puede requerir permisos de root en algunos sistemas. Los criterios de evaluación siguen las recomendaciones de CIS Benchmark para SSH: PermitRootLogin=no, PasswordAuthentication=no, MaxAuthTries<=3.
@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# audit_ssh_config
# ----------------
# Audita la configuración de sshd_config evaluando parámetros de seguridad,
# revisa intentos de login fallidos y lista las claves autorizadas del usuario.
#
# USO (directo):
# audit_ssh_config [/ruta/a/sshd_config]
#
# Depende de: grep, ssh (opcional para validación), journalctl o /var/log/auth.log
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 ──────────────────────────────────────────────────────────
_ssh_get_value() {
local config="$1"
local key="$2"
grep -iE "^[[:space:]]*${key}[[:space:]]" "$config" 2>/dev/null \
| tail -1 | awk '{print $2}' | xargs
}
_ssh_eval_permit_root() {
local val="${1:-yes}"
case "${val,,}" in
no|prohibit-password) echo "ok" ;;
without-password) echo "warn" ;;
*) echo "bad" ;;
esac
}
_ssh_eval_password_auth() {
local val="${1:-yes}"
[[ "${val,,}" == "no" ]] && echo "ok" || echo "bad"
}
_ssh_eval_max_auth_tries() {
local val="${1:-6}"
[[ "$val" -le 3 ]] && echo "ok" || echo "warn"
}
_ssh_eval_x11_forwarding() {
local val="${1:-no}"
[[ "${val,,}" == "no" ]] && echo "ok" || echo "warn"
}
# ─── Funciones de presentación ────────────────────────────────────────────────
_ssh_print_check() {
local level="$1"
local label="$2"
local value="$3"
local note="$4"
case "$level" in
ok) echo -e " ${GREEN}[ok]${NC} ${label}: ${value}" ;;
warn) echo -e " ${YELLOW}[!] ${NC} ${label}: ${value} -- ${note}" ;;
bad) echo -e " ${RED}[x] ${NC} ${label}: ${value} -- ${note}" ;;
esac
}
_ssh_show_config_checks() {
local config="$1"
echo -e "${PURPLE}════════ Configuración sshd_config ════════════${NC}"
echo ""
local permit_root
permit_root="$(_ssh_get_value "$config" "PermitRootLogin")"
permit_root="${permit_root:-yes (por defecto)}"
_ssh_print_check "$(_ssh_eval_permit_root "$permit_root")" \
"PermitRootLogin" "$permit_root" "debería ser 'no' o 'prohibit-password'"
local pass_auth
pass_auth="$(_ssh_get_value "$config" "PasswordAuthentication")"
pass_auth="${pass_auth:-yes (por defecto)}"
_ssh_print_check "$(_ssh_eval_password_auth "$pass_auth")" \
"PasswordAuthentication" "$pass_auth" "debería ser 'no' (usar claves)"
local port
port="$(_ssh_get_value "$config" "Port")"
port="${port:-22 (por defecto)}"
if [[ "$port" == "22"* ]]; then
_ssh_print_check "warn" "Port" "$port" "considera cambiar el puerto 22"
else
_ssh_print_check "ok" "Port" "$port" ""
fi
local max_tries
max_tries="$(_ssh_get_value "$config" "MaxAuthTries")"
max_tries="${max_tries:-6 (por defecto)}"
_ssh_print_check "$(_ssh_eval_max_auth_tries "${max_tries%% *}")" \
"MaxAuthTries" "$max_tries" "recomendado <= 3"
local x11
x11="$(_ssh_get_value "$config" "X11Forwarding")"
x11="${x11:-no (por defecto)}"
_ssh_print_check "$(_ssh_eval_x11_forwarding "$x11")" \
"X11Forwarding" "$x11" "deshabilitar si no se usa"
local allow_users allow_groups
allow_users="$(_ssh_get_value "$config" "AllowUsers")"
allow_groups="$(_ssh_get_value "$config" "AllowGroups")"
if [[ -z "$allow_users" && -z "$allow_groups" ]]; then
_ssh_print_check "warn" "AllowUsers/AllowGroups" "(no definidos)" "considera restringir acceso por usuario o grupo"
else
[[ -n "$allow_users" ]] && _ssh_print_check "ok" "AllowUsers" "$allow_users" ""
[[ -n "$allow_groups" ]] && _ssh_print_check "ok" "AllowGroups" "$allow_groups" ""
fi
echo ""
}
_ssh_show_failed_logins() {
echo -e "${PURPLE}════════ Últimos intentos de login fallidos ════${NC}"
echo ""
if command -v journalctl &>/dev/null; then
journalctl -u ssh -u sshd --no-pager -q 2>/dev/null \
| grep -i "failed\|invalid\|error" | tail -10 \
| while IFS= read -r line; do echo -e " ${DIM_GRAY}${line}${NC}"; done || true
elif [[ -f /var/log/auth.log ]]; then
grep -i "failed\|invalid" /var/log/auth.log 2>/dev/null | tail -10 \
| while IFS= read -r line; do echo -e " ${DIM_GRAY}${line}${NC}"; done || true
else
info "No se encontró fuente de logs de autenticación"
fi
echo ""
}
_ssh_show_authorized_keys() {
echo -e "${PURPLE}════════ Claves autorizadas (~/.ssh) ═══════════${NC}"
echo ""
local auth_keys="$HOME/.ssh/authorized_keys"
if [[ -f "$auth_keys" ]]; then
local count
count="$(wc -l < "$auth_keys")"
info "${count} clave(s) en authorized_keys:"
while IFS= read -r line; do
[[ -z "$line" || "$line" == "#"* ]] && continue
local key_type key_comment
key_type="$(echo "$line" | awk '{print $1}')"
key_comment="$(echo "$line" | awk '{print $NF}')"
echo -e " ${GREEN}*${NC} ${key_type} -- ${key_comment}"
done < "$auth_keys"
else
info "No existe $auth_keys"
fi
echo ""
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
audit_ssh_config() {
local config_path="${1:-/etc/ssh/sshd_config}"
if [[ ! -f "$config_path" ]]; then
warning "audit_ssh_config: no se encontró $config_path -- ¿está instalado sshd?"
else
_ssh_show_config_checks "$config_path"
fi
_ssh_show_failed_logins
_ssh_show_authorized_keys
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
audit_ssh_config "$@"
fi
@@ -0,0 +1,38 @@
---
name: check_firewall
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "check_firewall() -> void"
description: "Detecta el firewall activo del sistema (ufw, firewalld o iptables) y muestra su estado, reglas activas y puertos en escucha para cruzar con las reglas. Si no se detecta ningún firewall, emite una advertencia de exposición."
tags: [bash, cybersecurity, firewall, ufw, iptables, network, hardening, linux]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "imprime el firewall detectado, su estado (activo/inactivo), reglas vigentes y lista de puertos en escucha"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/check_firewall.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/sistema/firewall_status.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/check_firewall.sh
check_firewall
```
## Notas
La detección sigue el orden: ufw > firewalld > iptables > none. Para ufw muestra `ufw status verbose`; para firewalld muestra zona por defecto, servicios y puertos permitidos; para iptables muestra las cadenas INPUT/OUTPUT/FORWARD. Leer reglas de iptables requiere privilegios de root. El cruce de puertos en escucha (via `ss -tlnp`) ayuda a identificar servicios sin regla de firewall correspondiente.
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# check_firewall
# --------------
# Detecta el firewall activo del sistema (ufw, firewalld o iptables) y muestra
# su estado y reglas. También lista los puertos en escucha para cruzar con reglas.
#
# USO (directo):
# check_firewall
#
# Depende de: ufw, firewall-cmd o iptables (el que esté disponible), ss
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 ──────────────────────────────────────────────────────────
_fw_detect() {
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status:"; then
echo "ufw"
elif command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q "running"; then
echo "firewalld"
elif command -v iptables &>/dev/null; then
echo "iptables"
else
echo "none"
fi
}
_fw_ufw_is_active() {
ufw status 2>/dev/null | grep -q "Status: active"
}
_fw_firewalld_is_running() {
firewall-cmd --state 2>/dev/null | grep -q "running"
}
_fw_iptables_has_rules() {
local count
count="$(iptables -L INPUT --line-numbers 2>/dev/null | grep -c "^[0-9]" || echo 0)"
[[ "$count" -gt 0 ]]
}
# ─── Funciones de efecto ──────────────────────────────────────────────────────
_fw_show_ufw() {
echo -e "${PURPLE}════════ UFW ════════════════════════════════════${NC}"
echo ""
if _fw_ufw_is_active; then
success "UFW está activo"
else
echo -e " ${RED}[x]${NC} UFW está INACTIVO"
fi
echo ""
info "Reglas activas:"
ufw status verbose 2>/dev/null | while IFS= read -r line; do
echo -e " ${DIM_GRAY}${line}${NC}"
done
echo ""
}
_fw_show_firewalld() {
echo -e "${PURPLE}════════ FirewallD ══════════════════════════════${NC}"
echo ""
if _fw_firewalld_is_running; then
success "firewalld está activo"
else
echo -e " ${RED}[x]${NC} firewalld está INACTIVO"
fi
echo ""
local zone
zone="$(firewall-cmd --get-default-zone 2>/dev/null || echo "desconocida")"
info "Zona por defecto: ${zone}"
echo ""
info "Servicios permitidos en zona ${zone}:"
firewall-cmd --zone="$zone" --list-services 2>/dev/null \
| tr ' ' '\n' | while IFS= read -r svc; do
echo -e " ${GREEN}*${NC} ${svc}"
done
echo ""
info "Puertos permitidos:"
firewall-cmd --zone="$zone" --list-ports 2>/dev/null \
| tr ' ' '\n' | while IFS= read -r port; do
[[ -n "$port" ]] && echo -e " ${YELLOW}*${NC} ${port}"
done || true
echo ""
}
_fw_show_iptables() {
echo -e "${PURPLE}════════ iptables ═══════════════════════════════${NC}"
echo ""
for chain in INPUT OUTPUT FORWARD; do
echo -e "${CYAN}── ${chain} ──${NC}"
iptables -L "$chain" --line-numbers -n 2>/dev/null \
| while IFS= read -r line; do echo -e " ${DIM_GRAY}${line}${NC}"; done
echo ""
done
if ! _fw_iptables_has_rules; then
echo -e " ${YELLOW}[!]${NC} No hay reglas INPUT definidas -- el sistema puede estar sin filtrar tráfico"
fi
}
_fw_show_none() {
echo ""
echo -e " ${RED}[x]${NC} No se detectó ningún firewall activo (ufw, firewalld, iptables)"
echo -e " ${YELLOW}[!]${NC} El sistema puede estar completamente expuesto"
echo ""
info "Para instalar y activar ufw: sudo apt install ufw && sudo ufw enable"
}
_fw_show_listening_crosscheck() {
echo ""
echo -e "${PURPLE}════════ Puertos en escucha (para cruzar con reglas) ════${NC}"
echo ""
ss -tlnp 2>/dev/null | tail -n +2 | while IFS= read -r line; do
echo -e " ${DIM_GRAY}${line}${NC}"
done
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
check_firewall() {
local fw
fw="$(_fw_detect)"
info "Firewall detectado: ${fw}"
echo ""
case "$fw" in
ufw) _fw_show_ufw ;;
firewalld) _fw_show_firewalld ;;
iptables) _fw_show_iptables ;;
none) _fw_show_none ;;
esac
_fw_show_listening_crosscheck
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
check_firewall "$@"
fi
@@ -0,0 +1,38 @@
---
name: detect_suspicious_users
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "detect_suspicious_users() -> void"
description: "Revisa el sistema en busca de indicadores de compromiso en cuentas de usuario: UIDs 0 extras (además de root), usuarios con shell de login válida, homes en rutas inusuales, miembros de grupos privilegiados (sudo, docker, wheel, adm, etc.) y sesiones activas."
tags: [bash, cybersecurity, users, audit, linux, privilege-escalation, hardening]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "imprime secciones con UIDs 0, usuarios con shell, homes inusuales, grupos privilegiados, últimos logins y sesiones activas"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/detect_suspicious_users.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/sistema/usuarios_sospechosos.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/detect_suspicious_users.sh
detect_suspicious_users
```
## Notas
Los usuarios de sistema (UID <= 999) se excluyen del check de shell válida para evitar falsos positivos. Los grupos privilegiados monitorizados son: sudo, wheel, docker, adm, lxd, libvirt, kvm, disk, shadow. Homes inusuales son aquellos fuera de /home, /root, /var, /srv, /nonexistent y /tmp. `lastlog` puede no estar disponible en todas las distribuciones.
@@ -0,0 +1,162 @@
#!/usr/bin/env bash
# detect_suspicious_users
# -----------------------
# Revisa el sistema en busca de usuarios potencialmente sospechosos:
# UIDs 0 extras, shells válidas, homes en rutas inusuales, grupos privilegiados
# y sesiones activas.
#
# USO (directo):
# detect_suspicious_users
#
# Depende de: /etc/passwd, getent, w, lastlog
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 ───────────────────────────────────────────────────────────────
_SUSPICIOUS_VALID_SHELLS=("/bin/bash" "/bin/sh" "/bin/zsh" "/bin/fish" "/usr/bin/bash" "/usr/bin/zsh" "/usr/bin/fish")
_SUSPICIOUS_PRIVILEGED_GROUPS=("sudo" "wheel" "docker" "adm" "lxd" "libvirt" "kvm" "disk" "shadow")
_SUSPICIOUS_SYSTEM_USERS_MAX_UID=999
# ─── Funciones puras ──────────────────────────────────────────────────────────
_sus_is_valid_shell() {
local shell="$1"
for s in "${_SUSPICIOUS_VALID_SHELLS[@]}"; do
[[ "$shell" == "$s" ]] && return 0
done
return 1
}
_sus_is_system_user() {
local uid="$1"
[[ "$uid" -le $_SUSPICIOUS_SYSTEM_USERS_MAX_UID ]]
}
_sus_is_unusual_home() {
local home="$1"
[[ ! "$home" =~ ^(/home|/root|/var|/srv|/nonexistent|/tmp) ]]
}
# ─── Funciones de efecto ──────────────────────────────────────────────────────
_sus_show_uid0_users() {
echo -e "${PURPLE}════════ Usuarios con UID 0 (root) ═════════════${NC}"
echo ""
local found=0
while IFS=: read -r username _ uid _; do
if [[ "$uid" -eq 0 ]]; then
if [[ "$username" == "root" ]]; then
echo -e " ${GREEN}[ok]${NC} root (esperado)"
else
echo -e " ${RED}[x]${NC} ${username} tiene UID 0 -- SOSPECHOSO"
fi
found=$((found + 1))
fi
done < /etc/passwd
if [[ $found -eq 1 ]]; then
echo ""
success "Solo root tiene UID 0"
fi
echo ""
}
_sus_show_users_with_shell() {
echo -e "${PURPLE}════════ Usuarios con shell de login válida ═════${NC}"
echo ""
echo -e " ${GRAY}(excluye usuarios de sistema con UID <= ${_SUSPICIOUS_SYSTEM_USERS_MAX_UID})${NC}"
echo ""
local found=0
while IFS=: read -r username _ uid _ _ home shell; do
if ! _sus_is_system_user "$uid" && _sus_is_valid_shell "$shell"; then
echo -e " ${CYAN}*${NC} ${username} (UID ${uid}) -- shell: ${shell} -- home: ${home}"
found=$((found + 1))
fi
done < /etc/passwd
[[ $found -eq 0 ]] && info "No se encontraron usuarios normales con shell válida"
echo ""
}
_sus_show_unusual_homes() {
echo -e "${PURPLE}════════ Usuarios con home inusual ══════════════${NC}"
echo ""
local found=0
while IFS=: read -r username _ uid _ _ home shell; do
if _sus_is_valid_shell "$shell" && _sus_is_unusual_home "$home"; then
echo -e " ${YELLOW}[!]${NC} ${username} -- home: ${home}"
found=$((found + 1))
fi
done < /etc/passwd
[[ $found -eq 0 ]] && success "No se detectaron homes en rutas inusuales"
echo ""
}
_sus_show_privileged_groups() {
echo -e "${PURPLE}════════ Grupos privilegiados y sus miembros ════${NC}"
echo ""
for group in "${_SUSPICIOUS_PRIVILEGED_GROUPS[@]}"; do
local members
members="$(getent group "$group" 2>/dev/null | cut -d: -f4 || true)"
if [[ -n "$members" ]]; then
echo -e " ${YELLOW}*${NC} ${group}: ${members}"
fi
done
echo ""
}
_sus_show_active_sessions() {
echo -e "${PURPLE}════════ Sesiones activas ═══════════════════════${NC}"
echo ""
w 2>/dev/null | while IFS= read -r line; do
echo -e " ${DIM_GRAY}${line}${NC}"
done
echo ""
}
_sus_show_last_logins() {
echo -e "${PURPLE}════════ Últimos logins por usuario ═════════════${NC}"
echo ""
if command -v lastlog &>/dev/null; then
lastlog 2>/dev/null | awk 'NR==1 || $NF != "logged" {
if (NR==1 || $2 != "**Never") printf " %-16s %-10s %s\n", $1, $2, $NF
}' | grep -v "^$" | while IFS= read -r line; do
echo -e " ${DIM_GRAY}${line}${NC}"
done
else
warning "lastlog no disponible"
fi
echo ""
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
detect_suspicious_users() {
_sus_show_uid0_users
_sus_show_users_with_shell
_sus_show_unusual_homes
_sus_show_privileged_groups
_sus_show_last_logins
_sus_show_active_sessions
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
detect_suspicious_users "$@"
fi
@@ -0,0 +1,50 @@
---
name: encrypt_file
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "encrypt_file(mode: string, file: string) -> void"
description: "Cifra o descifra un archivo usando AES-256-CBC con PBKDF2 (310.000 iteraciones) via openssl. La contraseña se lee de la variable de entorno ENCRYPT_PASSWORD o se solicita interactivamente. El archivo cifrado se guarda con extensión .enc; al descifrar se recupera el nombre original."
tags: [bash, cybersecurity, encryption, aes256, openssl, crypto, pbkdf2]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: mode
desc: "operación a realizar: encrypt (cifrar) o decrypt (descifrar)"
- name: file
desc: "ruta al archivo a cifrar o descifrar"
output: "genera el archivo cifrado (input.enc) o descifrado (input sin .enc, o input.dec) e imprime progreso a stdout"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/encrypt_file.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/utilidades/cifrar_archivo.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/encrypt_file.sh
# Cifrar (solicita contraseña interactivamente)
encrypt_file encrypt documento.pdf
# Descifrar
encrypt_file decrypt documento.pdf.enc
# Con contraseña via variable de entorno (no interactivo)
ENCRYPT_PASSWORD="mi-secreto-seguro" encrypt_file encrypt datos.tar.gz
ENCRYPT_PASSWORD="mi-secreto-seguro" encrypt_file decrypt datos.tar.gz.enc
```
## Notas
Usa `openssl enc -aes-256-cbc -pbkdf2 -iter 310000` — compatible con OpenSSL 1.1.1+. Las 310.000 iteraciones de PBKDF2 siguen las recomendaciones NIST para derivación de claves en 2024. La contraseña se limpia de memoria al terminar. Si el archivo de salida ya existe, la función falla silenciosamente (no sobrescribe por seguridad cuando se usa con ENCRYPT_PASSWORD).
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# encrypt_file
# ------------
# Cifra o descifra un archivo usando AES-256-CBC con PBKDF2 (310.000 iteraciones).
# La contraseña se lee de la variable de entorno ENCRYPT_PASSWORD o se solicita
# interactivamente por stdin.
#
# USO (directo):
# encrypt_file encrypt <archivo>
# encrypt_file decrypt <archivo.enc>
# ENCRYPT_PASSWORD=secreto encrypt_file encrypt archivo.txt
#
# Depende de: openssl
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 ──────────────────────────────────────────────────────────
_enc_output_path_encrypt() {
local input="$1"
echo "${input}.enc"
}
_enc_output_path_decrypt() {
local input="$1"
if [[ "$input" == *.enc ]]; then
echo "${input%.enc}"
else
echo "${input}.dec"
fi
}
_enc_human_size() {
local file="$1"
du -sh "$file" 2>/dev/null | cut -f1
}
# ─── Funciones de efecto ──────────────────────────────────────────name──────────
_enc_ask_password() {
local pass
read -rsp "Contraseña: " pass
echo "" >&2
echo "$pass"
}
_enc_ask_password_confirm() {
local pass1 pass2
read -rsp "Contraseña: " pass1
echo "" >&2
read -rsp "Confirmar contraseña: " pass2
echo "" >&2
if [[ "$pass1" != "$pass2" ]]; then
error "Las contraseñas no coinciden" >&2
return 1
fi
if [[ ${#pass1} -lt 8 ]]; then
warning "La contraseña es muy corta (mínimo 8 caracteres recomendado)"
fi
echo "$pass1"
}
_enc_do_encrypt() {
local input="$1"
local output="$2"
local password="$3"
openssl enc -aes-256-cbc -pbkdf2 -iter 310000 \
-in "$input" -out "$output" \
-pass "pass:${password}" 2>/dev/null
}
_enc_do_decrypt() {
local input="$1"
local output="$2"
local password="$3"
openssl enc -d -aes-256-cbc -pbkdf2 -iter 310000 \
-in "$input" -out "$output" \
-pass "pass:${password}" 2>/dev/null
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
encrypt_file() {
local mode="$1"
local file="$2"
if [[ -z "$mode" || -z "$file" ]]; then
error "encrypt_file: uso: encrypt_file <encrypt|decrypt> <archivo>" >&2
return 1
fi
if ! command -v openssl &>/dev/null; then
error "encrypt_file: 'openssl' no está instalado (sudo apt install openssl)" >&2
return 1
fi
if [[ ! -f "$file" ]]; then
error "encrypt_file: archivo no encontrado: $file" >&2
return 1
fi
local password
if [[ -n "${ENCRYPT_PASSWORD:-}" ]]; then
password="$ENCRYPT_PASSWORD"
else
case "$mode" in
encrypt) password="$(_enc_ask_password_confirm)" || return 1 ;;
decrypt) password="$(_enc_ask_password)" ;;
*)
error "encrypt_file: modo no válido '$mode'. Use: encrypt|decrypt" >&2
return 1
;;
esac
fi
case "$mode" in
encrypt)
local output
output="$(_enc_output_path_encrypt "$file")"
info "Archivo: ${file} ($(_enc_human_size "$file"))"
info "Salida: ${output}"
info "Cifrando con AES-256-CBC + PBKDF2..."
if _enc_do_encrypt "$file" "$output" "$password"; then
success "Archivo cifrado: ${output} ($(_enc_human_size "$output"))"
else
error "encrypt_file: el cifrado falló" >&2
rm -f "$output"
return 1
fi
;;
decrypt)
local output
output="$(_enc_output_path_decrypt "$file")"
info "Archivo: ${file} ($(_enc_human_size "$file"))"
info "Salida: ${output}"
info "Descifrando..."
if _enc_do_decrypt "$file" "$output" "$password"; then
success "Archivo descifrado: ${output} ($(_enc_human_size "$output"))"
else
error "encrypt_file: el descifrado falló -- ¿contraseña incorrecta?" >&2
rm -f "$output"
return 1
fi
;;
esac
# Limpiar contraseña de memoria
password=""
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
encrypt_file "$@"
fi
@@ -0,0 +1,46 @@
---
name: enumerate_subdomains
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "enumerate_subdomains(domain: string, output_file: string) -> void"
description: "Enumera subdominios de un dominio objetivo usando un diccionario integrado de ~100 subdominios comunes (www, mail, api, dev, admin, vpn, etc.). Detecta tanto registros A (IP directa) como CNAME. Muestra progreso cada 20 subdominios y opcionalmente guarda los resultados en un archivo."
tags: [bash, cybersecurity, dns, subdomain, enumeration, reconnaissance, osint]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: domain
desc: "dominio objetivo a enumerar, ej: example.com"
- name: output_file
desc: "ruta al archivo donde guardar los resultados (opcional; si se omite, solo imprime a stdout)"
output: "imprime subdominios encontrados con su IP o CNAME, progreso cada 20 entradas y resumen final con total encontrados"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/enumerate_subdomains.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/web/subdominios.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/enumerate_subdomains.sh
# Solo imprimir resultados
enumerate_subdomains example.com
# Guardar resultados en archivo
enumerate_subdomains example.com /tmp/subdominios.txt
```
## Notas
Requiere `dig` (paquete dnsutils). El diccionario integrado cubre subdominios comunes en entornos corporativos y de desarrollo: www, api, dev, admin, vpn, git, jenkins, staging, prod, db, mail, smtp, ns1/ns2, grafana, kibana, docker, k8s, auth, sso, etc. (~100 entradas). La enumeración es puramente pasiva via DNS — no realiza ningún tipo de conexión al servidor web. Los subdominios con CNAME sin resolución A se marcan en amarillo.
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# enumerate_subdomains
# --------------------
# Enumera subdominios de un dominio objetivo usando un diccionario integrado de
# ~100 subdominios comunes. Detecta tanto registros A (IP directa) como CNAME.
# Opcionalmente guarda el resultado en un archivo.
#
# USO (directo):
# enumerate_subdomains <dominio> [archivo_salida]
# enumerate_subdomains example.com
# enumerate_subdomains example.com /tmp/resultado.txt
#
# Depende de: dig (dnsutils)
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
# ─── Diccionario integrado ─────────────────────────────────────────────────────
_SUBDOMAIN_WORDLIST=(
www mail ftp api dev admin vpn ssh git gitlab github jenkins ci cd
staging prod test demo beta alpha app web portal intranet extranet
remote desktop files cdn static assets media img images upload
db database mysql postgres redis mongo smtp pop imap webmail mx
ns1 ns2 dns autodiscover autoconfig crm erp shop store payment
backup old legacy v1 v2 v3 internal corp office support helpdesk
wiki docs doc status monitor grafana kibana elastic search
registry docker k8s kubernetes auth sso login oauth api2 mobile
)
# ─── Funciones puras ──────────────────────────────────────────────────────────
_sub_is_valid_domain() {
[[ -n "$1" && "$1" =~ ^[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$ ]]
}
_sub_build_fqdn() {
local sub="$1"
local domain="$2"
echo "${sub}.${domain}"
}
# ─── Funciones de efecto ──────────────────────────────────────────────────────
_sub_resolve_a() {
local fqdn="$1"
dig +short A "$fqdn" 2>/dev/null | grep -E '^[0-9]+\.' | head -1 || true
}
_sub_resolve_cname() {
local fqdn="$1"
dig +short CNAME "$fqdn" 2>/dev/null | head -1 || true
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
enumerate_subdomains() {
local domain="$1"
local output_file="${2:-}"
if [[ -z "$domain" ]]; then
error "enumerate_subdomains: se requiere un dominio como argumento" >&2
return 1
fi
if ! _sub_is_valid_domain "$domain"; then
error "enumerate_subdomains: dominio no válido: '$domain'" >&2
return 1
fi
if ! command -v dig &>/dev/null; then
error "enumerate_subdomains: 'dig' no está instalado (sudo apt install dnsutils)" >&2
return 1
fi
local total="${#_SUBDOMAIN_WORDLIST[@]}"
local found=0
local checked=0
info "Probando ${total} subdominios en ${domain}..."
echo ""
if [[ -n "$output_file" ]]; then
echo "# Subdominios encontrados en ${domain} -- $(date)" > "$output_file"
fi
for sub in "${_SUBDOMAIN_WORDLIST[@]}"; do
local fqdn
fqdn="$(_sub_build_fqdn "$sub" "$domain")"
checked=$((checked + 1))
local ip
ip="$(_sub_resolve_a "$fqdn")"
if [[ -n "$ip" ]]; then
echo -e " ${GREEN}[ok]${NC} ${fqdn} -> ${CYAN}${ip}${NC}"
[[ -n "$output_file" ]] && echo "${fqdn} -> ${ip}" >> "$output_file"
found=$((found + 1))
else
local cname
cname="$(_sub_resolve_cname "$fqdn")"
if [[ -n "$cname" ]]; then
echo -e " ${YELLOW}[cn]${NC} ${fqdn} -> CNAME: ${cname}"
[[ -n "$output_file" ]] && echo "${fqdn} -> CNAME: ${cname}" >> "$output_file"
found=$((found + 1))
fi
fi
# Progreso cada 20 subdominios
if (( checked % 20 == 0 )); then
echo -e " ${DIM_GRAY}[${checked}/${total} probados, ${found} encontrados]${NC}"
fi
done
echo ""
echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
if [[ $found -eq 0 ]]; then
info "No se encontraron subdominios en el diccionario"
else
success "Total encontrados: ${found} de ${total} probados"
[[ -n "$output_file" ]] && info "Resultado guardado en: ${output_file}"
fi
echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
enumerate_subdomains "$@"
fi
@@ -0,0 +1,54 @@
---
name: generate_password
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "generate_password(mode: string, length: int, count: int) -> void"
description: "Genera contraseñas seguras en cuatro modos: full (alfanumérico + símbolos, excluye caracteres ambiguos), alpha (solo alfanumérico), passphrase (palabras aleatorias unidas con guión) y pin (numérico). Calcula y muestra la entropía en bits para cada modo."
tags: [bash, cybersecurity, password, generator, entropy, security, urandom]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: mode
desc: "modo de generación: full (alfanumérico+símbolos, por defecto), alpha (solo letras y números), passphrase (palabras), pin (numérico)"
- name: length
desc: "longitud en caracteres para full/alpha/pin, o número de palabras para passphrase (por defecto: 16)"
- name: count
desc: "número de contraseñas a generar (por defecto: 1)"
output: "imprime las contraseñas generadas a stdout (una por línea) con información de entropía"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/generate_password.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/utilidades/generar_password.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/generate_password.sh
# Contraseña completa de 20 caracteres
generate_password full 20
# 5 contraseñas alfanuméricas de 16 caracteres
generate_password alpha 16 5
# Passphrase de 6 palabras
generate_password passphrase 6
# PIN de 8 dígitos
generate_password pin 8
```
## Notas
Usa `/dev/urandom` como fuente de aleatoriedad criptográficamente segura. El modo `full` excluye caracteres ambiguos (0, O, l, I, 1) para mejorar legibilidad. El modo `passphrase` requiere un diccionario del sistema (`/usr/share/dict/words` o similar). La entropía se calcula como log2(charset^length) en bits. Las contraseñas nunca se escriben a disco.
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# generate_password
# -----------------
# Genera contraseñas seguras en varios modos: completo (alfanumérico + símbolos),
# solo alfanumérico, passphrase de palabras o PIN numérico.
# Calcula la entropía en bits para cada contraseña generada.
#
# USO (directo):
# generate_password [full|alpha|passphrase|pin] [longitud] [cantidad]
#
# Depende de: /dev/urandom, python3 (para entropía), shuf (para passphrases)
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 ───────────────────────────────────────────────────────────────
_GENPW_DEFAULT_LENGTH=16
_GENPW_DEFAULT_COUNT=1
_GENPW_WORDLIST_PATHS=("/usr/share/dict/words" "/usr/dict/words" "/usr/share/dict/american-english")
# ─── Funciones puras ──────────────────────────────────────────────────────────
_genpw_find_wordlist() {
for path in "${_GENPW_WORDLIST_PATHS[@]}"; do
[[ -f "$path" ]] && echo "$path" && return
done
echo ""
}
_genpw_calc_entropy() {
local charset_size="$1"
local length="$2"
python3 -c "import math; print(f'{math.log2(${charset_size}**${length}):.1f}')" 2>/dev/null || echo "?"
}
# ─── Funciones de generación ──────────────────────────────────────────────────
_genpw_gen_full() {
local length="$1"
# Alfanumérico + símbolos (excluye ambiguos: 0OlI1)
tr -dc 'A-HJ-NP-Za-km-z2-9!@#$%^&*()_+-=[]{}|;:,.<>?' \
< /dev/urandom | head -c "$length"
echo
}
_genpw_gen_alpha() {
local length="$1"
tr -dc 'A-HJ-NP-Za-km-z2-9' \
< /dev/urandom | head -c "$length"
echo
}
_genpw_gen_passphrase() {
local words="$1"
local wordlist
wordlist="$(_genpw_find_wordlist)"
if [[ -z "$wordlist" ]]; then
error "generate_password: no se encontró diccionario (sudo apt install wamerican)" >&2
return 1
fi
local phrase=""
for ((i=0; i<words; i++)); do
local word
word="$(shuf -n1 "$wordlist" | tr -dc 'a-z' | head -c 20)"
[[ ${#word} -lt 3 ]] && { i=$((i-1)); continue; }
phrase="${phrase}${word}-"
done
echo "${phrase%-}"
}
_genpw_gen_pin() {
local length="$1"
tr -dc '0-9' < /dev/urandom | head -c "$length"
echo
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
generate_password() {
local mode="${1:-full}"
local length="${2:-$_GENPW_DEFAULT_LENGTH}"
local count="${3:-$_GENPW_DEFAULT_COUNT}"
# Validar que length y count son numéricos
if ! [[ "$length" =~ ^[0-9]+$ ]] || ! [[ "$count" =~ ^[0-9]+$ ]]; then
error "generate_password: longitud y cantidad deben ser números enteros positivos" >&2
return 1
fi
local charset_size entropy
case "$mode" in
full)
charset_size=78
entropy="$(_genpw_calc_entropy $charset_size "$length")"
info "Contraseñas alfanuméricas + símbolos (longitud: ${length}, entropía: ~${entropy} bits)"
echo ""
for ((i=1; i<=count; i++)); do
_genpw_gen_full "$length"
done
;;
alpha)
charset_size=56
entropy="$(_genpw_calc_entropy $charset_size "$length")"
info "Contraseñas alfanuméricas (longitud: ${length}, entropía: ~${entropy} bits)"
echo ""
for ((i=1; i<=count; i++)); do
_genpw_gen_alpha "$length"
done
;;
passphrase)
info "Passphrases (${length} palabras)"
echo ""
for ((i=1; i<=count; i++)); do
_genpw_gen_passphrase "$length" || return 1
done
;;
pin)
charset_size=10
entropy="$(_genpw_calc_entropy $charset_size "$length")"
info "PINs numéricos (longitud: ${length}, entropía: ~${entropy} bits)"
echo ""
for ((i=1; i<=count; i++)); do
_genpw_gen_pin "$length"
done
;;
*)
error "generate_password: modo no válido '$mode'. Use: full|alpha|passphrase|pin" >&2
return 1
;;
esac
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
generate_password "$@"
fi
@@ -0,0 +1,44 @@
---
name: geolocate_ip
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "geolocate_ip(target: string) -> void"
description: "Geolocaliza una dirección IP o dominio usando la API pública de ip-api.com. Muestra país, región, ciudad, coordenadas, ISP, ASN y detecta VPN, Proxy o infraestructura de hosting."
tags: [bash, cybersecurity, network, geoip, ip, osint, reconnaissance]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: target
desc: "dirección IP (IPv4) o nombre de dominio a geolocalizar"
output: "imprime información de geolocalización a stdout: país, ciudad, ISP, ASN, coordenadas y flags de VPN/Proxy/Hosting"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/geolocate_ip.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/redes/geoip.sh"
---
## Ejemplo
```bash
source bash/functions/cybersecurity/geolocate_ip.sh
# Geolocalizar una IP
geolocate_ip 8.8.8.8
# Geolocalizar un dominio (resuelve a IP primero)
geolocate_ip example.com
```
## Notas
Requiere `curl`. Si se pasa un dominio en lugar de una IP, se resuelve a IP usando `dig` antes de consultar la API. La API de ip-api.com es gratuita para uso no comercial con límite de 45 req/min. Los campos `proxy=true` y `hosting=true` indican posible uso de VPN, proxy Tor o datacenter.
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# geolocate_ip
# ------------
# Geolocaliza una IP o dominio usando la API pública de ip-api.com.
# Muestra país, ciudad, ISP, ASN y detecta VPN/Proxy/Hosting.
#
# USO (directo):
# geolocate_ip <ip_o_dominio>
#
# Depende de: curl, dig (para resolver dominios)
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 ───────────────────────────────────────────────────────────────
_GEOIP_API="http://ip-api.com/json"
_GEOIP_FIELDS="status,message,country,countryCode,regionName,city,zip,lat,lon,isp,org,as,proxy,hosting,query"
# ─── Funciones puras ──────────────────────────────────────────────────────────
_geo_is_ip() {
[[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]
}
_geo_build_api_url() {
local target="$1"
echo "${_GEOIP_API}/${target}?fields=${_GEOIP_FIELDS}"
}
_geo_extract_field() {
local json="$1"
local key="$2"
echo "$json" | grep -o "\"${key}\":[^,}]*" | cut -d: -f2- | tr -d '"' | xargs
}
# ─── Funciones de efecto ──────────────────────────────────────────────────────
_geo_resolve_domain() {
local domain="$1"
dig +short A "$domain" 2>/dev/null | head -1 || true
}
_geo_fetch() {
local target="$1"
local url
url="$(_geo_build_api_url "$target")"
curl -s --max-time 10 "$url" 2>/dev/null
}
_geo_display_result() {
local json="$1"
local status
status="$(_geo_extract_field "$json" "status")"
if [[ "$status" != "success" ]]; then
local msg
msg="$(_geo_extract_field "$json" "message")"
error "La API devolvió error: ${msg:-respuesta inesperada}" >&2
return 1
fi
local ip_queried country country_code region city zip lat lon isp org asn proxy hosting
ip_queried="$(_geo_extract_field "$json" "query")"
country="$(_geo_extract_field "$json" "country")"
country_code="$(_geo_extract_field "$json" "countryCode")"
region="$(_geo_extract_field "$json" "regionName")"
city="$(_geo_extract_field "$json" "city")"
zip="$(_geo_extract_field "$json" "zip")"
lat="$(_geo_extract_field "$json" "lat")"
lon="$(_geo_extract_field "$json" "lon")"
isp="$(_geo_extract_field "$json" "isp")"
org="$(_geo_extract_field "$json" "org")"
asn="$(_geo_extract_field "$json" "as")"
proxy="$(_geo_extract_field "$json" "proxy")"
hosting="$(_geo_extract_field "$json" "hosting")"
echo ""
echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
echo -e " ${CYAN}IP consultada:${NC} ${ip_queried}"
echo -e " ${CYAN}País:${NC} ${country} (${country_code})"
echo -e " ${CYAN}Región:${NC} ${region}"
echo -e " ${CYAN}Ciudad:${NC} ${city} ${zip}"
echo -e " ${CYAN}Coordenadas:${NC} ${lat}, ${lon}"
echo -e " ${CYAN}ISP:${NC} ${isp}"
echo -e " ${CYAN}Organización:${NC} ${org}"
echo -e " ${CYAN}ASN:${NC} ${asn}"
echo ""
if [[ "$proxy" == "true" ]]; then
echo -e " ${RED}[!] VPN / Proxy detectado${NC}"
fi
if [[ "$hosting" == "true" ]]; then
echo -e " ${YELLOW}[i] Hosting / datacenter detectado${NC}"
fi
if [[ "$proxy" != "true" && "$hosting" != "true" ]]; then
echo -e " ${GREEN}[ok] Sin indicios de VPN, Proxy o Tor${NC}"
fi
echo -e "${PURPLE}════════════════════════════════════════════════════════════${NC}"
}
# ─── Punto de entrada ─────────────────────────────────────────────────────────
geolocate_ip() {
local target="$1"
if [[ -z "$target" ]]; then
error "geolocate_ip: se requiere una IP o dominio como argumento" >&2
return 1
fi
if ! command -v curl &>/dev/null; then
error "geolocate_ip: 'curl' no está instalado (sudo apt install curl)" >&2
return 1
fi
local query_target="$target"
if ! _geo_is_ip "$target"; then
info "Resolviendo dominio a IP..."
local resolved
resolved="$(_geo_resolve_domain "$target")"
if [[ -z "$resolved" ]]; then
error "geolocate_ip: no se pudo resolver '$target'" >&2
return 1
fi
info "Resuelto: ${target} -> ${resolved}"
query_target="$resolved"
fi
info "Consultando geolocalización de ${query_target}..."
local json
json="$(_geo_fetch "$query_target")"
if [[ -z "$json" ]]; then
error "geolocate_ip: no se obtuvo respuesta de la API" >&2
return 1
fi
_geo_display_result "$json"
}
# Ejecutar si se llama directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
geolocate_ip "$@"
fi
@@ -0,0 +1,47 @@
---
name: inspect_ssl_cert
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "inspect_ssl_cert(host: string) -> void"
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."
tags: [bash, cybersecurity, ssl, tls, certificate, web, openssl, security]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/inspect_ssl_cert.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/web/ssl_cert_info.sh"
---
## Ejemplo
```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
```
## Notas
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.
@@ -0,0 +1,169 @@
#!/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
@@ -0,0 +1,47 @@
---
name: list_active_connections
kind: function
lang: bash
domain: cybersecurity
version: "1.0.0"
purity: impure
signature: "list_active_connections(mode: string) -> void"
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)."
tags: [bash, cybersecurity, network, connections, monitoring, ss, ports]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/list_active_connections.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/redes/conexiones_activas.sh"
---
## Ejemplo
```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
```
## Notas
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).
@@ -0,0 +1,109 @@
#!/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
@@ -0,0 +1,51 @@
---
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"
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."
tags: [bash, cybersecurity, hash, integrity, checksum, md5, sha256, sha512]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/cybersecurity/verify_file_hash.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/ciberseguridad/utilidades/verificar_hash.sh"
---
## Ejemplo
```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
```
## Notas
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`.
@@ -0,0 +1,100 @@
#!/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
@@ -0,0 +1,52 @@
---
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"
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%."
tags: [bash, disk, space, analysis, filesystem]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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 >90%; exit code 1 si modo desconocido"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/analyze_disk_space.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/gestion_linux/espacio_disponible.sh"
---
## Ejemplo
```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
```
## Notas
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.
@@ -0,0 +1,93 @@
#!/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
+46
View File
@@ -0,0 +1,46 @@
---
name: detect_wsl
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "detect_wsl([--check]) -> void"
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."
tags: [bash, wsl, windows, detect, integration]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/detect_wsl.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/gestion_linux/wsl_host.sh"
---
## Ejemplo
```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
```
## Notas
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).
+97
View File
@@ -0,0 +1,97 @@
#!/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
+49
View File
@@ -0,0 +1,49 @@
---
name: install_go
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_go([version: string], [--force]) -> void"
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."
tags: [bash, install, go, golang]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_go.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/instaladores/instalar_go.sh"
---
## Ejemplo
```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
```
## Notas
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.
+115
View File
@@ -0,0 +1,115 @@
#!/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
+45
View File
@@ -0,0 +1,45 @@
---
name: install_nodejs
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_nodejs([version: string]) -> void"
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."
tags: [bash, install, nodejs, nvm]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_nodejs.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/instaladores/instalar_nodejs.sh"
---
## Ejemplo
```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
```
## Notas
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.
+75
View File
@@ -0,0 +1,75 @@
#!/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
+40
View File
@@ -0,0 +1,40 @@
---
name: install_pnpm
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_pnpm() -> void"
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."
tags: [bash, install, pnpm, node]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_pnpm.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/instaladores/instalar_pnpm.sh"
---
## Ejemplo
```bash
source bash/functions/infra/install_pnpm.sh
install_pnpm
```
## Notas
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.
+64
View File
@@ -0,0 +1,64 @@
#!/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
+40
View File
@@ -0,0 +1,40 @@
---
name: install_python312
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_python312() -> void"
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."
tags: [bash, install, python, python312]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_python312.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/instaladores/instalar_python312.sh"
---
## Ejemplo
```bash
source bash/functions/infra/install_python312.sh
install_python312
```
## Notas
Requiere `sudo`. Para distribuciones no soportadas, se recomienda usar pyenv. Idempotente: si `python3.12` ya existe en PATH, informa y termina sin hacer nada.
+136
View File
@@ -0,0 +1,136 @@
#!/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
+45
View File
@@ -0,0 +1,45 @@
---
name: install_uv
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_uv() -> void"
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."
tags: [bash, install, uv, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "(ninguno)"
desc: "no acepta argumentos"
output: "progreso a stdout; exit code 1 si curl no está disponible o falla la instalación"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_uv.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/instaladores/instalar_uv.sh"
---
## Ejemplo
```bash
source bash/functions/infra/install_uv.sh
install_uv
# Uso posterior
uv venv
source .venv/bin/activate
uv pip install requests pandas
```
## Notas
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.
+88
View File
@@ -0,0 +1,88 @@
#!/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
+45
View File
@@ -0,0 +1,45 @@
---
name: install_volta
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_volta() -> void"
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."
tags: [bash, install, volta, node]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: "(ninguno)"
desc: "no acepta argumentos"
output: "progreso a stdout; exit code 1 si curl no está disponible o falla la instalación"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_volta.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/instaladores/instalar_volta.sh"
---
## Ejemplo
```bash
source bash/functions/infra/install_volta.sh
install_volta
# Uso posterior (tras recargar shell)
volta install node
volta install pnpm
volta list
```
## Notas
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`.
+100
View File
@@ -0,0 +1,100 @@
#!/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
+46
View File
@@ -0,0 +1,46 @@
---
name: install_wails
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_wails() -> void"
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."
tags: [bash, install, wails, desktop]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_wails.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/instaladores/instalar_wails.sh"
---
## Ejemplo
```bash
source bash/functions/infra/install_wails.sh
install_wails
# Verificar
wails doctor
# Crear proyecto
wails init -n my-desktop-app -t react
```
## Notas
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.
+123
View File
@@ -0,0 +1,123 @@
#!/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
@@ -0,0 +1,50 @@
---
name: list_listening_ports
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "list_listening_ports([mode: string]) -> void"
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."
tags: [bash, ports, network, listening, monitoring]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/list_listening_ports.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/gestion_linux/puertos_activos.sh"
---
## Ejemplo
```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
```
## Notas
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).
@@ -0,0 +1,108 @@
#!/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
@@ -0,0 +1,49 @@
---
name: init_go_module
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "init_go_module([module_path: string]) -> void"
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."
tags: [bash, go, module, init, scaffold, launcher]
uses_functions: [install_go_bash_infra, assert_command_exists_bash_shell]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: module_path
desc: "path del módulo Go, ej: github.com/user/mymodule (default: github.com/user/<dirname>)"
output: "crea archivos en el directorio actual y muestra progreso; exit code 1 si Go no está instalado o go mod init falla"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/init_go_module.sh"
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"
---
## Ejemplo
```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
```
## Notas
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.
+160
View File
@@ -0,0 +1,160 @@
#!/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 "$@"
@@ -0,0 +1,49 @@
---
name: init_go_project
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "init_go_project([module_path: string]) -> void"
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."
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: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: module_path
desc: "path del módulo Go, ej: github.com/org/myproject (default: github.com/<whoami>/<dirname>)"
output: "crea estructura completa en el directorio actual y muestra progreso; exit code 1 si Go o git no están disponibles"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/init_go_project.sh"
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"
---
## Ejemplo
```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
```
## Notas
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`.
+296
View File
@@ -0,0 +1,296 @@
#!/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 "$@"
+55
View File
@@ -0,0 +1,55 @@
---
name: bash_check_deps
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
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."
tags: [bash, check, dependency, command, exists, validation]
uses_functions: [bash_log_bash_shell]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/bash_check_deps.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/lib/common.sh"
---
## Ejemplo
```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"
```
## Notas
`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.
+75
View File
@@ -0,0 +1,75 @@
# 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
}
+47
View File
@@ -0,0 +1,47 @@
---
name: bash_colors
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: pure
signature: "bash_colors() -> void"
description: "Exporta variables ANSI de colores, caracteres box drawing y simbolos unicode para uso en scripts de terminal."
tags: [bash, colors, ansi, terminal, symbols, box]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params: []
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.)"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/bash_colors.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/lib/common.sh"
---
## Ejemplo
```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}"
```
## Notas
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.
+41
View File
@@ -0,0 +1,41 @@
# 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=""
}
+52
View File
@@ -0,0 +1,52 @@
---
name: bash_confirm
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "bash_confirm(prompt: string, [default: string]) -> exit_code"
description: "Dialogo interactivo de confirmacion y/n con valor por defecto configurable. Soporta respuestas yes/y/si."
tags: [bash, confirm, prompt, interactive, dialog]
uses_functions: [bash_colors_bash_shell]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/bash_confirm.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/lib/common.sh"
---
## Ejemplo
```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
```
## Notas
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`).
+36
View File
@@ -0,0 +1,36 @@
# 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
}
+57
View File
@@ -0,0 +1,57 @@
---
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"
description: "Muestra un box de error formateado con contexto del fallo: script, linea, funcion, directorio y usuario. Registra en log."
tags: [bash, error, handler, box, formatted, context]
uses_functions: [bash_colors_bash_shell, bash_log_bash_shell]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/bash_handle_error.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/lib/common.sh"
---
## Ejemplo
```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"
}
```
## Notas
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.
+47
View File
@@ -0,0 +1,47 @@
# 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
}
+52
View File
@@ -0,0 +1,52 @@
---
name: bash_log
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
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."
tags: [bash, log, logging, colors, terminal]
uses_functions: [bash_colors_bash_shell]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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)"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/bash_log.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/lib/common.sh"
---
## Ejemplo
```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..."
```
## Notas
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.
+64
View File
@@ -0,0 +1,64 @@
# 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}"
}
+52
View File
@@ -0,0 +1,52 @@
---
name: bash_safe_run
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
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."
tags: [bash, safe, run, error, trap, handler]
uses_functions: [bash_log_bash_shell]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/bash_safe_run.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/lib/common.sh"
---
## Ejemplo
```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"
```
## Notas
`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.
+44
View File
@@ -0,0 +1,44 @@
# 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
}
+54
View File
@@ -0,0 +1,54 @@
---
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"
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."
tags: [bash, text, convert, case, encoding]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/convert_text_case.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/conversores/conversor_texto.sh"
---
## Ejemplo
```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
```
## Notas
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.
+73
View File
@@ -0,0 +1,73 @@
#!/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
@@ -0,0 +1,44 @@
---
name: create_project_structure
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "create_project_structure(project_name: string) -> void"
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)."
tags: [bash, project, structure, scaffold, init]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/create_project_structure.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/inicializar_repos/functional_structure.sh"
---
## Ejemplo
```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
```
## Notas
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.
@@ -0,0 +1,114 @@
#!/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
@@ -0,0 +1,51 @@
---
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"
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."
tags: [bash, git, branches, clean, merge]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/git_clean_branches.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/git_utils/limpiar_ramas.sh"
---
## Ejemplo
```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
```
## Notas
Ramas protegidas que nunca se eliminan: `main`, `master`, `develop`, `dev`, `staging`, `release`. Siempre hace prune de referencias remotas obsoletas antes de buscar candidatos.
+126
View File
@@ -0,0 +1,126 @@
#!/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
+56
View File
@@ -0,0 +1,56 @@
---
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"
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."
tags: [bash, git, log, history, graph]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/git_log_visual.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/git_utils/historial_commits.sh"
---
## Ejemplo
```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
```
## Notas
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).
+75
View File
@@ -0,0 +1,75 @@
#!/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
@@ -0,0 +1,44 @@
---
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"
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."
tags: [bash, git, push, remote, all]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/git_push_all_remotes.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/git_utils/push_todos_remotes.sh"
---
## Ejemplo
```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"
```
## Notas
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.
@@ -0,0 +1,127 @@
#!/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
+41
View File
@@ -0,0 +1,41 @@
---
name: git_repo_status
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "git_repo_status() -> void"
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."
tags: [bash, git, status, repo, branch]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/git_repo_status.sh"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "scripts/linux/git_utils/estado_repo.sh"
---
## Ejemplo
```bash
# Desde un directorio con repo git
cd /home/user/my-project
source bash/functions/shell/git_repo_status.sh
git_repo_status
```
## Notas
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.
+95
View File
@@ -0,0 +1,95 @@
#!/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
+36
View File
@@ -0,0 +1,36 @@
package core
import (
"strconv"
"strings"
)
// CompareVersions compares two semver strings (e.g. "v1.4.0", "1.2.3").
// Returns -1 if a < b, 0 if equal, 1 if a > b.
func CompareVersions(a, b string) int {
pa := parseVersionParts(a)
pb := parseVersionParts(b)
for i := 0; i < 3; i++ {
if pa[i] < pb[i] {
return -1
}
if pa[i] > pb[i] {
return 1
}
}
return 0
}
func parseVersionParts(v string) [3]int {
v = strings.TrimPrefix(v, "v")
parts := strings.SplitN(v, ".", 3)
var nums [3]int
for i, p := range parts {
if i >= 3 {
break
}
n, _ := strconv.Atoi(p)
nums[i] = n
}
return nums
}
+42
View File
@@ -0,0 +1,42 @@
---
name: compare_versions
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func CompareVersions(a, b string) int"
description: "Compara dos strings de version semantica (semver). Soporta formato con o sin prefijo 'v'. Retorna -1, 0 o 1."
tags: [core, version, semver, compare, parse]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [strconv, strings]
params:
- name: a
desc: "primera version (ej: 'v1.4.0' o '1.4.0')"
- name: b
desc: "segunda version"
output: "-1 si a < b, 0 si iguales, 1 si a > b"
tested: false
tests: []
test_file_path: ""
file_path: "functions/core/compare_versions.go"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "installer/core/version.go"
---
## Ejemplo
```go
core.CompareVersions("v1.4.0", "v1.3.9") // 1
core.CompareVersions("1.0.0", "1.0.0") // 0
core.CompareVersions("v0.9.0", "v1.0.0") // -1
```
## Notas
Compara major.minor.patch como enteros. Ignora pre-release tags y metadata (solo los 3 numeros). Partes faltantes se tratan como 0.
+21
View File
@@ -0,0 +1,21 @@
package core
import "strings"
// LongestCommonPrefix returns the longest string that is a prefix of every item.
// Returns "" if the slice is empty or no common prefix exists.
func LongestCommonPrefix(items []string) string {
if len(items) == 0 {
return ""
}
prefix := items[0]
for _, item := range items[1:] {
for !strings.HasPrefix(item, prefix) {
if prefix == "" {
return ""
}
prefix = prefix[:len(prefix)-1]
}
}
return prefix
}
+42
View File
@@ -0,0 +1,42 @@
---
name: longest_common_prefix
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func LongestCommonPrefix(items []string) string"
description: "Retorna el prefijo comun mas largo compartido por todos los strings del slice. Util para autocompletado de rutas y comandos."
tags: [core, string, prefix, autocomplete, common]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [strings]
params:
- name: items
desc: "slice de strings para buscar prefijo comun"
output: "prefijo comun mas largo, o vacio si no hay coincidencia"
tested: false
tests: []
test_file_path: ""
file_path: "functions/core/longest_common_prefix.go"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "launcher/core/commands.go"
---
## Ejemplo
```go
prefix := core.LongestCommonPrefix([]string{"/home/user/docs", "/home/user/downloads", "/home/user/desktop"})
// prefix == "/home/user/d"
empty := core.LongestCommonPrefix([]string{"abc", "xyz"})
// empty == ""
```
## Notas
Compara caracter por caracter acortando el prefijo progresivamente. Complejidad O(n*m) donde n es el numero de strings y m la longitud del prefijo.
+15
View File
@@ -0,0 +1,15 @@
package core
import "strings"
// ParseVersion extracts the version tag from the first word of the first line.
// For example, "v1.4.0 - Description" returns "v1.4.0".
// Returns "" if the input is empty.
func ParseVersion(content string) string {
line := strings.SplitN(strings.TrimSpace(content), "\n", 2)[0]
word := strings.Fields(line)
if len(word) == 0 {
return ""
}
return word[0]
}
+42
View File
@@ -0,0 +1,42 @@
---
name: parse_version
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func ParseVersion(content string) string"
description: "Extrae el tag de version del primer campo de la primera linea de texto. Util para parsear archivos VERSION.txt o salida de comandos --version."
tags: [core, version, parse, text, extract]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [strings]
params:
- name: content
desc: "texto con version en la primera palabra de la primera linea"
output: "string con el tag de version (ej: 'v1.4.0'), o vacio"
tested: false
tests: []
test_file_path: ""
file_path: "functions/core/parse_version.go"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "installer/core/version.go"
---
## Ejemplo
```go
v := core.ParseVersion("v0.4.9 - Instalador Julia Windows (2026-02-28)")
// v == "v0.4.9"
v2 := core.ParseVersion("1.0.0\nChangelog...")
// v2 == "1.0.0"
```
## Notas
Toma solo el primer campo (separado por whitespace) de la primera linea. No valida formato semver — simplemente extrae el primer token.
+13
View File
@@ -0,0 +1,13 @@
package core
import "path/filepath"
// RelOrFull returns target relative to base when possible,
// otherwise returns target unchanged.
func RelOrFull(base, target string) string {
rel, err := filepath.Rel(base, target)
if err != nil {
return target
}
return rel
}
+44
View File
@@ -0,0 +1,44 @@
---
name: rel_or_full
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func RelOrFull(base, target string) string"
description: "Retorna el path de target relativo a base. Si filepath.Rel falla, retorna target sin cambios. Util para mostrar paths cortos en UIs."
tags: [core, path, relative, filepath, display]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [path/filepath]
params:
- name: base
desc: "directorio base para calcular la ruta relativa"
- name: target
desc: "path absoluto o relativo a convertir"
output: "path relativo a base, o target original si la conversion falla"
tested: false
tests: []
test_file_path: ""
file_path: "functions/core/rel_or_full.go"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "launcher/core/script_query.go"
---
## Ejemplo
```go
rel := core.RelOrFull("/home/user/project", "/home/user/project/src/main.go")
// rel == "src/main.go"
full := core.RelOrFull("/home/user", "/other/path")
// full == "../../other/path"
```
## Notas
Wrapper seguro sobre `filepath.Rel` que nunca retorna error. Si los paths estan en volúmenes distintos (Windows), retorna el target original.
+20
View File
@@ -0,0 +1,20 @@
package core
import "strings"
// SplitCommandAndArg splits "cmd arg" into its two parts.
// Returns (cmd, arg, true) if a space separator is found,
// or ("", "", false) if the input has no space or is empty.
func SplitCommandAndArg(input string) (cmd, arg string, ok bool) {
trimmed := strings.TrimSpace(input)
idx := strings.Index(trimmed, " ")
if idx == -1 {
return "", "", false
}
cmd = strings.TrimSpace(trimmed[:idx])
arg = strings.TrimSpace(trimmed[idx+1:])
if cmd == "" {
return "", "", false
}
return cmd, arg, true
}
+42
View File
@@ -0,0 +1,42 @@
---
name: split_command_and_arg
kind: function
lang: go
domain: core
version: "1.0.0"
purity: pure
signature: "func SplitCommandAndArg(input string) (cmd, arg string, ok bool)"
description: "Separa un string 'comando argumento' en sus dos partes por el primer espacio. Retorna ok=false si no hay espacio o el input esta vacio."
tags: [core, string, split, command, parse, cli]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [strings]
params:
- name: input
desc: "string con formato 'comando argumento'"
output: "tupla (cmd, arg, ok) donde ok indica si se encontro separador"
tested: false
tests: []
test_file_path: ""
file_path: "functions/core/split_command_and_arg.go"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "launcher/core/commands.go"
---
## Ejemplo
```go
cmd, arg, ok := core.SplitCommandAndArg("cd /home/user")
// cmd == "cd", arg == "/home/user", ok == true
_, _, ok2 := core.SplitCommandAndArg("exit")
// ok2 == false
```
## Notas
Solo separa por el primer espacio. El argumento puede contener espacios adicionales. Trims whitespace en ambos extremos del input y de cada parte.
+16
View File
@@ -0,0 +1,16 @@
package infra
import "fmt"
// SSHConfigAddEntry añade un nuevo entry a la lista. Retorna error si el alias
// ya existe. No muta el slice original.
func SSHConfigAddEntry(entries []SSHConfigEntry, entry SSHConfigEntry) ([]SSHConfigEntry, error) {
for _, e := range entries {
if e.Alias == entry.Alias {
return nil, fmt.Errorf("alias %q already exists", entry.Alias)
}
}
result := make([]SSHConfigEntry, len(entries), len(entries)+1)
copy(result, entries)
return append(result, entry), nil
}
+38
View File
@@ -0,0 +1,38 @@
---
name: ssh_config_add_entry
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigAddEntry(entries []SSHConfigEntry, entry SSHConfigEntry) ([]SSHConfigEntry, error)"
description: "Añade un nuevo SSHConfigEntry a la lista. Error si el alias ya existe."
tags: [ssh, config, add, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [fmt]
params:
- name: entries
desc: "lista actual de SSHConfigEntry"
- name: entry
desc: "nuevo entry a añadir con alias unico"
output: "nueva lista con el entry añadido al final"
tested: true
tests: ["añade entry a lista vacia", "añade entry a lista existente", "error si alias duplicado"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_add_entry.go"
---
## Ejemplo
```go
newEntry := SSHConfigEntry{Alias: "staging", HostName: "10.0.0.2", User: "deploy"}
updated, err := SSHConfigAddEntry(entries, newEntry)
```
## Notas
No muta el slice original — crea una copia nueva. La validacion de unicidad es por alias exacto.
+21
View File
@@ -0,0 +1,21 @@
package infra
// SSHConfigEntry representa un bloque Host en ~/.ssh/config.
type SSHConfigEntry struct {
Alias string // Nombre del host (lo que va despues de "Host")
HostName string // IP o hostname real del servidor
User string // Usuario remoto
Port int // Puerto SSH (0 = no especificado, usa default 22)
IdentityFile string // Ruta a clave privada
Options map[string]string // Opciones SSH adicionales (ForwardAgent, ProxyJump, etc.)
}
// ToSSHConn convierte un SSHConfigEntry a SSHConn para usar con las funciones SSH del registry.
func (e SSHConfigEntry) ToSSHConn() SSHConn {
return SSHConn{
Host: e.HostName,
Port: e.Port,
User: e.User,
KeyPath: e.IdentityFile,
}
}
+12
View File
@@ -0,0 +1,12 @@
package infra
// SSHConfigFind busca un entry por alias en la lista. Retorna el entry y true
// si lo encuentra, o un SSHConfigEntry vacio y false si no existe.
func SSHConfigFind(entries []SSHConfigEntry, alias string) (SSHConfigEntry, bool) {
for _, e := range entries {
if e.Alias == alias {
return e, true
}
}
return SSHConfigEntry{}, false
}
+41
View File
@@ -0,0 +1,41 @@
---
name: ssh_config_find
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigFind(entries []SSHConfigEntry, alias string) (SSHConfigEntry, bool)"
description: "Busca un entry por alias en la lista de SSHConfigEntry."
tags: [ssh, config, search, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: entries
desc: "lista de SSHConfigEntry donde buscar"
- name: alias
desc: "nombre del host a buscar (case-sensitive)"
output: "el SSHConfigEntry encontrado y true, o entry vacio y false si no existe"
tested: true
tests: ["encuentra entry existente", "retorna false para alias inexistente"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_find.go"
---
## Ejemplo
```go
entry, ok := SSHConfigFind(entries, "myserver")
if ok {
conn := entry.ToSSHConn()
SSHCheck(conn)
}
```
## Notas
Busqueda lineal por coincidencia exacta del alias. Para pocos entries (tipico en un SSH config personal) es suficiente.
+84
View File
@@ -0,0 +1,84 @@
package infra
import (
"bufio"
"strconv"
"strings"
)
// SSHConfigParse parsea el contenido de un archivo ~/.ssh/config y retorna
// una lista de SSHConfigEntry. Ignora comentarios y lineas vacias.
// Los bloques Host con wildcards (* o ?) se ignoran.
func SSHConfigParse(content string) []SSHConfigEntry {
var entries []SSHConfigEntry
var current *SSHConfigEntry
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value := splitDirective(line)
if key == "" {
continue
}
if strings.EqualFold(key, "Host") {
if current != nil && current.Alias != "" {
entries = append(entries, *current)
}
if strings.ContainsAny(value, "*?") {
current = nil
continue
}
current = &SSHConfigEntry{
Alias: value,
Options: make(map[string]string),
}
continue
}
if current == nil {
continue
}
switch strings.ToLower(key) {
case "hostname":
current.HostName = value
case "user":
current.User = value
case "port":
if p, err := strconv.Atoi(value); err == nil {
current.Port = p
}
case "identityfile":
current.IdentityFile = value
default:
current.Options[key] = value
}
}
if current != nil && current.Alias != "" {
entries = append(entries, *current)
}
return entries
}
// splitDirective separa una linea "Key Value" o "Key=Value" en clave y valor.
func splitDirective(line string) (string, string) {
// Intentar separar por =
if idx := strings.IndexByte(line, '='); idx >= 0 {
return strings.TrimSpace(line[:idx]), strings.TrimSpace(line[idx+1:])
}
// Separar por primer espacio/tab
fields := strings.SplitN(line, " ", 2)
if len(fields) < 2 {
fields = strings.SplitN(line, "\t", 2)
}
if len(fields) < 2 {
return "", ""
}
return strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1])
}
+43
View File
@@ -0,0 +1,43 @@
---
name: ssh_config_parse
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigParse(content string) []SSHConfigEntry"
description: "Parsea el contenido de un archivo ~/.ssh/config y retorna una lista de SSHConfigEntry."
tags: [ssh, config, parser, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [bufio, strconv, strings]
params:
- name: content
desc: "texto completo del archivo ~/.ssh/config"
output: "lista de SSHConfigEntry, uno por cada bloque Host (sin wildcards)"
tested: true
tests: ["parsea config con multiples hosts", "ignora comentarios y lineas vacias", "ignora hosts con wildcards", "parsea opciones extra en Options map"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_parse.go"
---
## Ejemplo
```go
content := `Host myserver
HostName 192.168.1.100
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519`
entries := SSHConfigParse(content)
// entries[0].Alias == "myserver"
// entries[0].HostName == "192.168.1.100"
```
## Notas
Ignora bloques `Host *` y `Host 192.168.?.*` (wildcards). Soporta directivas con separador espacio o `=`. Las directivas no reconocidas se almacenan en el map `Options`.
+279
View File
@@ -0,0 +1,279 @@
package infra
import (
"strings"
"testing"
)
func TestSSHConfigParse_MultipleHosts(t *testing.T) {
content := `
Host prod
HostName 10.0.0.1
User admin
Port 22
IdentityFile ~/.ssh/id_prod
Host staging
HostName 10.0.0.2
User deploy
Port 2222
`
entries := SSHConfigParse(content)
if len(entries) != 2 {
t.Fatalf("expected 2 entries, got %d", len(entries))
}
if entries[0].Alias != "prod" {
t.Errorf("expected alias prod, got %s", entries[0].Alias)
}
if entries[0].HostName != "10.0.0.1" {
t.Errorf("expected hostname 10.0.0.1, got %s", entries[0].HostName)
}
if entries[0].User != "admin" {
t.Errorf("expected user admin, got %s", entries[0].User)
}
if entries[0].Port != 22 {
t.Errorf("expected port 22, got %d", entries[0].Port)
}
if entries[0].IdentityFile != "~/.ssh/id_prod" {
t.Errorf("expected identity file ~/.ssh/id_prod, got %s", entries[0].IdentityFile)
}
if entries[1].Alias != "staging" {
t.Errorf("expected alias staging, got %s", entries[1].Alias)
}
if entries[1].Port != 2222 {
t.Errorf("expected port 2222, got %d", entries[1].Port)
}
}
func TestSSHConfigParse_IgnoresCommentsAndBlanks(t *testing.T) {
content := `
# This is a comment
Host myserver
HostName 192.168.1.1
# inline comment
User root
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].User != "root" {
t.Errorf("expected user root, got %s", entries[0].User)
}
}
func TestSSHConfigParse_IgnoresWildcards(t *testing.T) {
content := `
Host *
ServerAliveInterval 60
Host prod
HostName 10.0.0.1
Host 192.168.*
User local
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry (wildcards ignored), got %d", len(entries))
}
if entries[0].Alias != "prod" {
t.Errorf("expected alias prod, got %s", entries[0].Alias)
}
}
func TestSSHConfigParse_ExtraOptions(t *testing.T) {
content := `Host jump
HostName bastion.example.com
User ops
ForwardAgent yes
ProxyJump none
`
entries := SSHConfigParse(content)
if len(entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(entries))
}
if entries[0].Options["ForwardAgent"] != "yes" {
t.Errorf("expected ForwardAgent=yes, got %s", entries[0].Options["ForwardAgent"])
}
if entries[0].Options["ProxyJump"] != "none" {
t.Errorf("expected ProxyJump=none, got %s", entries[0].Options["ProxyJump"])
}
}
func TestSSHConfigRender_FullEntry(t *testing.T) {
entries := []SSHConfigEntry{{
Alias: "prod",
HostName: "10.0.0.1",
User: "admin",
Port: 2222,
IdentityFile: "~/.ssh/id_prod",
}}
result := SSHConfigRender(entries)
if !strings.Contains(result, "Host prod") {
t.Error("missing Host directive")
}
if !strings.Contains(result, "HostName 10.0.0.1") {
t.Error("missing HostName")
}
if !strings.Contains(result, "User admin") {
t.Error("missing User")
}
if !strings.Contains(result, "Port 2222") {
t.Error("missing Port")
}
if !strings.Contains(result, "IdentityFile ~/.ssh/id_prod") {
t.Error("missing IdentityFile")
}
}
func TestSSHConfigRender_MinimalEntry(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "local"}}
result := SSHConfigRender(entries)
if result != "Host local\n" {
t.Errorf("expected minimal render, got %q", result)
}
}
func TestSSHConfigRender_MultipleEntries(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "a", HostName: "1.1.1.1"},
{Alias: "b", HostName: "2.2.2.2"},
}
result := SSHConfigRender(entries)
parts := strings.Split(result, "\n\n")
if len(parts) < 2 {
t.Error("expected blocks separated by blank line")
}
}
func TestSSHConfigRender_OptionsSorted(t *testing.T) {
entries := []SSHConfigEntry{{
Alias: "test",
HostName: "1.1.1.1",
Options: map[string]string{"ProxyJump": "bastion", "ForwardAgent": "yes"},
}}
result := SSHConfigRender(entries)
fwIdx := strings.Index(result, "ForwardAgent")
pjIdx := strings.Index(result, "ProxyJump")
if fwIdx < 0 || pjIdx < 0 {
t.Fatal("missing options in render")
}
if fwIdx > pjIdx {
t.Error("options should be sorted alphabetically")
}
}
func TestSSHConfigFind_Exists(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "prod", HostName: "10.0.0.1"},
{Alias: "staging", HostName: "10.0.0.2"},
}
entry, ok := SSHConfigFind(entries, "staging")
if !ok {
t.Fatal("expected to find staging")
}
if entry.HostName != "10.0.0.2" {
t.Errorf("expected hostname 10.0.0.2, got %s", entry.HostName)
}
}
func TestSSHConfigFind_NotExists(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "prod"}}
_, ok := SSHConfigFind(entries, "nope")
if ok {
t.Error("expected not found")
}
}
func TestSSHConfigAddEntry_ToEmpty(t *testing.T) {
entry := SSHConfigEntry{Alias: "new", HostName: "1.1.1.1"}
result, err := SSHConfigAddEntry(nil, entry)
if err != nil {
t.Fatal(err)
}
if len(result) != 1 || result[0].Alias != "new" {
t.Errorf("unexpected result: %+v", result)
}
}
func TestSSHConfigAddEntry_ToExisting(t *testing.T) {
existing := []SSHConfigEntry{{Alias: "old"}}
entry := SSHConfigEntry{Alias: "new"}
result, err := SSHConfigAddEntry(existing, entry)
if err != nil {
t.Fatal(err)
}
if len(result) != 2 {
t.Fatalf("expected 2 entries, got %d", len(result))
}
// Original no mutado
if len(existing) != 1 {
t.Error("original slice was mutated")
}
}
func TestSSHConfigAddEntry_DuplicateAlias(t *testing.T) {
existing := []SSHConfigEntry{{Alias: "prod"}}
entry := SSHConfigEntry{Alias: "prod"}
_, err := SSHConfigAddEntry(existing, entry)
if err == nil {
t.Error("expected error for duplicate alias")
}
}
func TestSSHConfigRemoveEntry_Exists(t *testing.T) {
entries := []SSHConfigEntry{
{Alias: "a"},
{Alias: "b"},
{Alias: "c"},
}
result, err := SSHConfigRemoveEntry(entries, "b")
if err != nil {
t.Fatal(err)
}
if len(result) != 2 {
t.Fatalf("expected 2 entries, got %d", len(result))
}
if result[0].Alias != "a" || result[1].Alias != "c" {
t.Errorf("unexpected order: %+v", result)
}
// Original no mutado
if len(entries) != 3 {
t.Error("original slice was mutated")
}
}
func TestSSHConfigRemoveEntry_NotExists(t *testing.T) {
entries := []SSHConfigEntry{{Alias: "prod"}}
_, err := SSHConfigRemoveEntry(entries, "nope")
if err == nil {
t.Error("expected error for missing alias")
}
}
func TestSSHConfigParseRender_Roundtrip(t *testing.T) {
original := []SSHConfigEntry{
{Alias: "prod", HostName: "10.0.0.1", User: "admin", Port: 22, IdentityFile: "~/.ssh/id_prod"},
{Alias: "staging", HostName: "10.0.0.2", User: "deploy", Port: 2222},
}
text := SSHConfigRender(original)
parsed := SSHConfigParse(text)
if len(parsed) != len(original) {
t.Fatalf("roundtrip: expected %d entries, got %d", len(original), len(parsed))
}
for i := range original {
if parsed[i].Alias != original[i].Alias {
t.Errorf("roundtrip[%d]: alias mismatch %s != %s", i, parsed[i].Alias, original[i].Alias)
}
if parsed[i].HostName != original[i].HostName {
t.Errorf("roundtrip[%d]: hostname mismatch", i)
}
if parsed[i].User != original[i].User {
t.Errorf("roundtrip[%d]: user mismatch", i)
}
if parsed[i].Port != original[i].Port {
t.Errorf("roundtrip[%d]: port mismatch", i)
}
}
}
+21
View File
@@ -0,0 +1,21 @@
package infra
import (
"fmt"
"os"
"path/filepath"
)
// SSHConfigRead lee y parsea el archivo ~/.ssh/config. Si el archivo no existe,
// retorna una lista vacia sin error (config todavia no creado).
func SSHConfigRead() ([]SSHConfigEntry, error) {
path := filepath.Join(os.Getenv("HOME"), ".ssh", "config")
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("ssh config read: %w", err)
}
return SSHConfigParse(string(data)), nil
}
+39
View File
@@ -0,0 +1,39 @@
---
name: ssh_config_read
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func SSHConfigRead() ([]SSHConfigEntry, error)"
description: "Lee y parsea ~/.ssh/config. Retorna lista vacia si el archivo no existe."
tags: [ssh, config, read, remote]
uses_functions: [ssh_config_parse_go_infra]
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [fmt, os, path/filepath]
params: []
output: "lista de SSHConfigEntry parseados del archivo ~/.ssh/config"
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/ssh_config_read.go"
---
## Ejemplo
```go
entries, err := SSHConfigRead()
if err != nil {
log.Fatal(err)
}
for _, e := range entries {
fmt.Printf("%s -> %s@%s\n", e.Alias, e.User, e.HostName)
}
```
## Notas
Si `~/.ssh/config` no existe, retorna lista vacia y nil (no es error — el usuario simplemente no tiene config aun). Usa `SSHConfigParse` internamente.
@@ -0,0 +1,22 @@
package infra
import "fmt"
// SSHConfigRemoveEntry elimina el entry con el alias dado. Retorna error si
// el alias no existe. No muta el slice original.
func SSHConfigRemoveEntry(entries []SSHConfigEntry, alias string) ([]SSHConfigEntry, error) {
idx := -1
for i, e := range entries {
if e.Alias == alias {
idx = i
break
}
}
if idx < 0 {
return nil, fmt.Errorf("alias %q not found", alias)
}
result := make([]SSHConfigEntry, 0, len(entries)-1)
result = append(result, entries[:idx]...)
result = append(result, entries[idx+1:]...)
return result, nil
}
@@ -0,0 +1,37 @@
---
name: ssh_config_remove_entry
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigRemoveEntry(entries []SSHConfigEntry, alias string) ([]SSHConfigEntry, error)"
description: "Elimina un entry por alias de la lista. Error si el alias no existe."
tags: [ssh, config, remove, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [fmt]
params:
- name: entries
desc: "lista actual de SSHConfigEntry"
- name: alias
desc: "alias del host a eliminar"
output: "nueva lista sin el entry eliminado"
tested: true
tests: ["elimina entry existente", "error si alias no existe"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_remove_entry.go"
---
## Ejemplo
```go
updated, err := SSHConfigRemoveEntry(entries, "old-server")
```
## Notas
No muta el slice original — crea una copia nueva sin el entry eliminado.
+42
View File
@@ -0,0 +1,42 @@
package infra
import (
"fmt"
"sort"
"strings"
)
// SSHConfigRender convierte una lista de SSHConfigEntry al formato texto
// de ~/.ssh/config. Cada bloque se separa con una linea en blanco.
func SSHConfigRender(entries []SSHConfigEntry) string {
var sb strings.Builder
for i, e := range entries {
if i > 0 {
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("Host %s\n", e.Alias))
if e.HostName != "" {
sb.WriteString(fmt.Sprintf(" HostName %s\n", e.HostName))
}
if e.User != "" {
sb.WriteString(fmt.Sprintf(" User %s\n", e.User))
}
if e.Port != 0 {
sb.WriteString(fmt.Sprintf(" Port %d\n", e.Port))
}
if e.IdentityFile != "" {
sb.WriteString(fmt.Sprintf(" IdentityFile %s\n", e.IdentityFile))
}
if len(e.Options) > 0 {
keys := make([]string, 0, len(e.Options))
for k := range e.Options {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
sb.WriteString(fmt.Sprintf(" %s %s\n", k, e.Options[k]))
}
}
}
return sb.String()
}
+39
View File
@@ -0,0 +1,39 @@
---
name: ssh_config_render
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SSHConfigRender(entries []SSHConfigEntry) string"
description: "Convierte una lista de SSHConfigEntry al formato texto de ~/.ssh/config."
tags: [ssh, config, render, remote]
uses_functions: []
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: ""
imports: [fmt, sort, strings]
params:
- name: entries
desc: "lista de SSHConfigEntry a renderizar"
output: "texto en formato ~/.ssh/config con bloques Host separados por linea en blanco"
tested: true
tests: ["renderiza entry completo", "renderiza entry minimo solo con alias", "separa bloques con linea en blanco", "ordena opciones extra alfabeticamente"]
test_file_path: "functions/infra/ssh_config_parse_test.go"
file_path: "functions/infra/ssh_config_render.go"
---
## Ejemplo
```go
entries := []SSHConfigEntry{{
Alias: "prod", HostName: "10.0.0.1", User: "admin", Port: 22,
}}
text := SSHConfigRender(entries)
// "Host prod\n HostName 10.0.0.1\n User admin\n Port 22\n"
```
## Notas
Campos vacios o Port=0 se omiten. Las opciones extra se renderizan en orden alfabetico para determinismo.
+33
View File
@@ -0,0 +1,33 @@
package infra
import (
"fmt"
"os"
"path/filepath"
)
// SSHConfigWrite escribe la lista de entries al archivo ~/.ssh/config.
// Crea un backup (.config.bak) antes de sobrescribir si el archivo ya existe.
// Crea el directorio ~/.ssh si no existe.
func SSHConfigWrite(entries []SSHConfigEntry) error {
sshDir := filepath.Join(os.Getenv("HOME"), ".ssh")
configPath := filepath.Join(sshDir, "config")
backupPath := filepath.Join(sshDir, "config.bak")
if err := os.MkdirAll(sshDir, 0700); err != nil {
return fmt.Errorf("ssh config write: create dir: %w", err)
}
// Backup si existe
if data, err := os.ReadFile(configPath); err == nil {
if err := os.WriteFile(backupPath, data, 0600); err != nil {
return fmt.Errorf("ssh config write: backup: %w", err)
}
}
content := SSHConfigRender(entries)
if err := os.WriteFile(configPath, []byte(content), 0600); err != nil {
return fmt.Errorf("ssh config write: %w", err)
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
---
name: ssh_config_write
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func SSHConfigWrite(entries []SSHConfigEntry) error"
description: "Escribe entries a ~/.ssh/config con backup automatico del archivo previo."
tags: [ssh, config, write, remote]
uses_functions: [ssh_config_render_go_infra]
uses_types: [ssh_config_entry_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [fmt, os, path/filepath]
params:
- name: entries
desc: "lista de SSHConfigEntry a escribir en ~/.ssh/config"
output: "nil si la escritura fue exitosa"
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/ssh_config_write.go"
---
## Ejemplo
```go
entries, _ := SSHConfigRead()
entries, _ = SSHConfigAddEntry(entries, SSHConfigEntry{
Alias: "prod", HostName: "10.0.0.1", User: "deploy",
})
if err := SSHConfigWrite(entries); err != nil {
log.Fatal(err)
}
```
## Notas
Crea `~/.ssh/` con permisos 0700 si no existe. Hace backup a `~/.ssh/config.bak` antes de sobrescribir. El archivo resultante tiene permisos 0600 (solo lectura/escritura para el owner).
@@ -0,0 +1,48 @@
package shell
import (
"bufio"
"os"
"path/filepath"
"strings"
)
// ExtractScriptDescription parses a script file and returns the first
// meaningful comment line as a description. Skips shebangs and empty lines.
// Falls back to the filename (without extension, underscores replaced by spaces).
func ExtractScriptDescription(scriptPath string) string {
file, err := os.Open(scriptPath)
if err != nil {
return fallbackDescription(scriptPath)
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() && lineCount < 5 {
line := strings.TrimSpace(scanner.Text())
lineCount++
if strings.HasPrefix(line, "#!") || line == "" {
continue
}
if strings.HasPrefix(line, "#") {
desc := strings.TrimSpace(strings.TrimPrefix(line, "#"))
desc = strings.TrimSpace(strings.TrimPrefix(desc, "Script:"))
desc = strings.TrimSpace(strings.TrimPrefix(desc, "Script para"))
desc = strings.TrimSpace(strings.TrimPrefix(desc, "Descripción:"))
desc = strings.TrimSpace(strings.TrimPrefix(desc, "Description:"))
if desc != "" {
return desc
}
}
}
return fallbackDescription(scriptPath)
}
func fallbackDescription(scriptPath string) string {
name := filepath.Base(scriptPath)
name = strings.TrimSuffix(name, filepath.Ext(name))
return strings.ReplaceAll(name, "_", " ")
}
@@ -0,0 +1,46 @@
---
name: extract_script_description
kind: function
lang: go
domain: shell
version: "1.0.0"
purity: impure
signature: "func ExtractScriptDescription(scriptPath string) string"
description: "Parsea un archivo de script y extrae la primera linea de comentario util como descripcion. Salta shebangs y lineas vacias. Si no encuentra descripcion, usa el nombre del archivo."
tags: [shell, script, description, parse, comment, metadata]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [bufio, os, path/filepath, strings]
params:
- name: scriptPath
desc: "ruta al archivo de script (.sh, .ps1, .bat)"
output: "descripcion extraida del primer comentario, o nombre del archivo humanizado"
tested: false
tests: []
test_file_path: ""
file_path: "functions/shell/extract_script_description.go"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/DevLauncher.git"
source_license: "MIT"
source_file: "launcher/middleware/reader.go"
---
## Ejemplo
```go
// Dado un script con:
// #!/bin/bash
// # Analisis completo de DNS para un dominio
desc := shell.ExtractScriptDescription("/path/to/analisis_dns.sh")
// desc == "Analisis completo de DNS para un dominio"
// Sin comentario util
desc2 := shell.ExtractScriptDescription("/path/to/instalar_go.sh")
// desc2 == "instalar go" (fallback del nombre)
```
## Notas
Busca en las primeras 5 lineas. Remueve prefijos comunes como "Script:", "Descripción:", "Description:" del comentario. Si el archivo no existe o no tiene comentarios utiles, genera una descripcion a partir del nombre del archivo reemplazando underscores por espacios.

Some files were not shown because too many files have changed in this diff Show More