Files
egutierrez 2a3d780347 feat(doctor): add fn doctor CLI + 14 functions for system management
Adds `fn doctor` read-only diagnostic command with subcommands artefacts,
services, sync, uses-functions, unused, and --json flag for agents.
Each subcommand wraps a registry function in functions/infra/.

New functions:
- artefact_doctor, services_status, pc_locations_drift,
  audit_uses_functions, find_unused_functions (Go diagnostics)
- backup_sqlite_db, rotate_backups, wait_for_http, wait_for_port,
  port_kill, tail_journal, pre_commit_hook_install (bash utilities)
- notify_telegram (Go HTTP)
- backup_all pipeline (tag launcher)

Plus prior session leftovers (scan_secrets_in_dirty, append_diary_entry,
git utilities, http_session_cookie_middleware, compile/full-git pipelines).

Fixes pc_locations_drift filepath.Join bug with absolute dir_path.
Documents fn doctor in CLAUDE.md, .claude/rules/fn_doctor.md (rule 23),
docs/architecture.md, CHANGELOG.md (2026-05-07), and diary entry.

First fn doctor uses-functions run found drift in 7/12 apps (deuda
para sincronizar app.md con imports reales).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:42:10 +02:00

91 lines
2.6 KiB
Bash

#!/usr/bin/env bash
# port_kill — Mata los procesos que escuchan en un puerto TCP dado.
port_kill() {
local port="${1:-}"
local signal="${2:-TERM}"
# Validar puerto
if [[ -z "$port" ]] || ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
echo "ERROR: puerto invalido: '$port' (debe ser 1-65535)" >&2
return 2
fi
# Verificar herramienta disponible
local tool=""
if command -v lsof &>/dev/null; then
tool="lsof"
elif command -v fuser &>/dev/null; then
tool="fuser"
else
echo "ERROR: se requiere lsof o fuser (ninguno disponible)" >&2
return 5
fi
# Obtener PIDs
local pids=()
if [[ "$tool" == "lsof" ]]; then
mapfile -t pids < <(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null)
else
mapfile -t pids < <(fuser -n tcp "${port}" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$')
fi
if (( ${#pids[@]} == 0 )); then
echo "NO_PROCESS port=${port}"
return 0
fi
# Matar cada PID
local permission_denied=0
for pid in "${pids[@]}"; do
[[ -z "$pid" ]] && continue
if kill "-${signal}" "$pid" 2>/dev/null; then
echo "KILLED pid=${pid} signal=${signal} port=${port}"
else
echo "PERMISSION_DENIED pid=${pid}" >&2
permission_denied=1
fi
done
if (( permission_denied )); then
return 4
fi
# Esperar 2s y verificar
sleep 2
local remaining=()
if [[ "$tool" == "lsof" ]]; then
mapfile -t remaining < <(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null)
else
mapfile -t remaining < <(fuser -n tcp "${port}" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$')
fi
if (( ${#remaining[@]} == 0 )); then
return 0
fi
# Segundo intento con KILL si signal != KILL
if [[ "$signal" != "KILL" && "$signal" != "9" ]]; then
for pid in "${remaining[@]}"; do
[[ -z "$pid" ]] && continue
kill -KILL "$pid" 2>/dev/null && echo "KILLED pid=${pid} signal=KILL port=${port}"
done
sleep 1
local still=()
if [[ "$tool" == "lsof" ]]; then
mapfile -t still < <(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null)
else
mapfile -t still < <(fuser -n tcp "${port}" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$')
fi
if (( ${#still[@]} > 0 )); then
echo "ERROR: puerto ${port} sigue ocupado tras SIGKILL" >&2
return 3
fi
else
echo "ERROR: puerto ${port} sigue ocupado tras SIGKILL" >&2
return 3
fi
return 0
}