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>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# backup_all — Backup completo del estado del registry: registry.db, operations.db de cada app, y vaults declarados.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../infra/backup_sqlite_db.sh"
|
||||
source "$SCRIPT_DIR/../infra/rotate_backups.sh"
|
||||
|
||||
backup_all() {
|
||||
local backup_root="${1:?Arg 1 requerido: backup_root}"
|
||||
local start_ts
|
||||
start_ts=$(date +%s)
|
||||
|
||||
# --- 1. Localizar FN_REGISTRY_ROOT ---
|
||||
local registry_root
|
||||
if [[ -n "${FN_REGISTRY_ROOT:-}" && -f "$FN_REGISTRY_ROOT/registry.db" ]]; then
|
||||
registry_root="$FN_REGISTRY_ROOT"
|
||||
elif [[ -f "$(pwd)/registry.db" ]]; then
|
||||
registry_root="$(pwd)"
|
||||
else
|
||||
echo "ERROR: No se puede localizar registry.db. Setea FN_REGISTRY_ROOT o ejecuta desde la raiz del registry." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- 2. Verificar herramientas del sistema ---
|
||||
local missing_tools=()
|
||||
for tool in sqlite3 rsync find; do
|
||||
command -v "$tool" &>/dev/null || missing_tools+=("$tool")
|
||||
done
|
||||
if [[ ${#missing_tools[@]} -gt 0 ]]; then
|
||||
echo "ERROR: Herramientas faltantes: ${missing_tools[*]}" >&2
|
||||
return 5
|
||||
fi
|
||||
|
||||
# --- 3. Crear backup_root ---
|
||||
if ! mkdir -p "$backup_root/registry" "$backup_root/operations" "$backup_root/vaults"; then
|
||||
echo "ERROR: No se puede crear/escribir en $backup_root" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
local log_file="$backup_root/backup_log.txt"
|
||||
local iso_ts
|
||||
iso_ts=$(date --iso-8601=seconds)
|
||||
local ops_count=0
|
||||
local vaults_count=0
|
||||
local registry_bytes=0
|
||||
local partial_errors=0
|
||||
|
||||
# --- 4. Backup registry.db ---
|
||||
local snap_registry="/tmp/registry-snap-$$.db"
|
||||
if ! backup_sqlite_db "$registry_root/registry.db" "$snap_registry"; then
|
||||
echo "ERROR critico: Fallo snapshot de registry.db" >&2
|
||||
rm -f "$snap_registry"
|
||||
return 3
|
||||
fi
|
||||
registry_bytes=$(stat -c%s "$snap_registry" 2>/dev/null || echo 0)
|
||||
rotate_backups "$backup_root/registry" "$snap_registry" 7 4 12
|
||||
rm -f "$snap_registry"
|
||||
|
||||
# --- 5. Backup operations.db de cada app ---
|
||||
while IFS= read -r ops_db; do
|
||||
local app_dir
|
||||
app_dir="$(dirname "$ops_db")"
|
||||
local app_name
|
||||
app_name="$(basename "$app_dir")"
|
||||
local snap_ops="/tmp/ops-snap-$$-${app_name}.db"
|
||||
if backup_sqlite_db "$ops_db" "$snap_ops"; then
|
||||
rotate_backups "$backup_root/operations/$app_name" "$snap_ops" 7 4 12 || ((partial_errors++))
|
||||
rm -f "$snap_ops"
|
||||
((ops_count++))
|
||||
else
|
||||
echo "WARN: Fallo snapshot de $ops_db — skipped" >&2
|
||||
rm -f "$snap_ops"
|
||||
((partial_errors++))
|
||||
fi
|
||||
done < <(find "$registry_root/apps" "$registry_root/projects" -name "operations.db" -maxdepth 4 2>/dev/null || true)
|
||||
|
||||
# --- 6. Backup vaults via rsync + link-dest ---
|
||||
local vault_yaml
|
||||
while IFS= read -r vault_yaml; do
|
||||
if [[ ! -f "$vault_yaml" ]]; then continue; fi
|
||||
# Parsear entradas de vault.yaml: buscar pares name/path
|
||||
local current_name=""
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*name:[[:space:]]*(.+)$ ]]; then
|
||||
current_name="${BASH_REMATCH[1]}"
|
||||
elif [[ "$line" =~ ^[[:space:]]*path:[[:space:]]*(.+)$ && -n "$current_name" ]]; then
|
||||
local vault_path="${BASH_REMATCH[1]}"
|
||||
# Expandir ~ si fuera necesario
|
||||
vault_path="${vault_path/#\~/$HOME}"
|
||||
if [[ ! -d "$vault_path" ]]; then
|
||||
echo "WARN: Vault '$current_name' path '$vault_path' no existe — skipped" >&2
|
||||
((partial_errors++))
|
||||
current_name=""
|
||||
continue
|
||||
fi
|
||||
local vault_dest="$backup_root/vaults/$current_name"
|
||||
mkdir -p "$vault_dest"
|
||||
local link_dest="$vault_dest/daily.1"
|
||||
local tmp_dest="$vault_dest/daily.0.tmp"
|
||||
rm -rf "$tmp_dest"
|
||||
if [[ -d "$link_dest" ]]; then
|
||||
rsync -a --link-dest="$link_dest" "$vault_path/" "$tmp_dest/"
|
||||
else
|
||||
rsync -a "$vault_path/" "$tmp_dest/"
|
||||
fi
|
||||
# Rotacion manual de directorios (7 daily, 4 weekly, 12 monthly)
|
||||
_rotate_vault_dirs "$vault_dest" 7 4 12
|
||||
mv "$tmp_dest" "$vault_dest/daily.0"
|
||||
((vaults_count++))
|
||||
current_name=""
|
||||
fi
|
||||
done < "$vault_yaml"
|
||||
done < <(find "$registry_root/projects" -name "vault.yaml" -maxdepth 4 2>/dev/null || true)
|
||||
|
||||
# --- 7. Log y stdout ---
|
||||
local end_ts elapsed
|
||||
end_ts=$(date +%s)
|
||||
elapsed=$(( end_ts - start_ts ))
|
||||
local summary="${iso_ts} registry=${registry_bytes}B ops=${ops_count} vaults=${vaults_count} partial_errors=${partial_errors} elapsed=${elapsed}s"
|
||||
echo "$summary" >> "$log_file"
|
||||
echo "$summary"
|
||||
|
||||
if [[ $partial_errors -gt 0 ]]; then
|
||||
echo "WARN: $partial_errors errores parciales (no criticos). Ver $log_file" >&2
|
||||
return 4
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Rotacion manual de directorios vault (mismo algoritmo que rotate_backups pero sobre dirs)
|
||||
_rotate_vault_dirs() {
|
||||
local dir="$1"
|
||||
local daily="${2:-7}"
|
||||
local weekly="${3:-4}"
|
||||
local monthly="${4:-12}"
|
||||
|
||||
# Promover a monthly (del weekly.0 al ultimo monthly)
|
||||
local week_day
|
||||
week_day=$(date +%u) # 1=lunes..7=domingo
|
||||
local month_day
|
||||
month_day=$(date +%d)
|
||||
|
||||
if [[ "$month_day" == "01" && -d "$dir/weekly.0" ]]; then
|
||||
for ((i=monthly-1; i>=1; i--)); do
|
||||
[[ -d "$dir/monthly.$((i-1))" ]] && mv "$dir/monthly.$((i-1))" "$dir/monthly.$i"
|
||||
done
|
||||
[[ -d "$dir/weekly.0" ]] && cp -al "$dir/weekly.0" "$dir/monthly.0"
|
||||
fi
|
||||
|
||||
if [[ "$week_day" == "7" && -d "$dir/daily.0" ]]; then
|
||||
for ((i=weekly-1; i>=1; i--)); do
|
||||
[[ -d "$dir/weekly.$((i-1))" ]] && mv "$dir/weekly.$((i-1))" "$dir/weekly.$i"
|
||||
done
|
||||
[[ -d "$dir/daily.0" ]] && cp -al "$dir/daily.0" "$dir/weekly.0"
|
||||
fi
|
||||
|
||||
# Rotar daily
|
||||
[[ -d "$dir/daily.$((daily-1))" ]] && rm -rf "$dir/daily.$((daily-1))"
|
||||
for ((i=daily-1; i>=1; i--)); do
|
||||
[[ -d "$dir/daily.$((i-1))" ]] && mv "$dir/daily.$((i-1))" "$dir/daily.$i"
|
||||
done
|
||||
}
|
||||
|
||||
backup_all "$@"
|
||||
Reference in New Issue
Block a user