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,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pipeline: full_git_push — Push automatico de fn_registry + todos los sub-repos + fn sync
|
||||
# Descubre repos, escanea secrets, auto-commitea dirty trees, pushea solo los ahead,
|
||||
# pushea ~/.password-store, y ejecuta fn sync para sincronizar metadata no regenerable.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
INFRA_DIR="$SCRIPT_DIR/../infra"
|
||||
CYBERSEC_DIR="$SCRIPT_DIR/../cybersecurity"
|
||||
|
||||
source "$INFRA_DIR/discover_git_repos.sh"
|
||||
source "$INFRA_DIR/git_auto_commit_dirty.sh"
|
||||
source "$INFRA_DIR/git_push_if_ahead.sh"
|
||||
source "$INFRA_DIR/pass_get.sh"
|
||||
source "$INFRA_DIR/ensure_repo_synced.sh"
|
||||
source "$CYBERSEC_DIR/scan_secrets_in_dirty.sh"
|
||||
|
||||
full_git_push() {
|
||||
local commit_message="${1:-}"
|
||||
|
||||
# Resolver raiz del registry
|
||||
local registry_root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
cd "$registry_root"
|
||||
|
||||
echo "=== full_git_push: inicio ===" >&2
|
||||
echo "Registry root: $registry_root" >&2
|
||||
|
||||
# --- Paso 1: Descubrir repos ---
|
||||
echo "" >&2
|
||||
echo "[1/6] Descubriendo repos git..." >&2
|
||||
local repos
|
||||
repos=$(discover_git_repos "$registry_root")
|
||||
|
||||
# --- Paso 1b: Auto-inicializar apps/analyses sin .git ---
|
||||
echo "" >&2
|
||||
echo "[1b] Verificando apps/analyses sin git..." >&2
|
||||
|
||||
local gitea_url gitea_token
|
||||
gitea_url=$(pass_get agentes/gitea-url | head -n1 2>/dev/null || true)
|
||||
gitea_token=$(pass_get gitea/dataforge-git-token | head -n1 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$gitea_url" && -n "$gitea_token" ]]; then
|
||||
export GITEA_URL="$gitea_url"
|
||||
export GITEA_TOKEN="$gitea_token"
|
||||
export FN_REGISTRY_INFRA_DIR="$INFRA_DIR"
|
||||
|
||||
local missing_dirs=()
|
||||
for pattern in "apps/*/" "analysis/*/" "projects/*/apps/*/" "projects/*/analysis/*/"; do
|
||||
while IFS= read -r d; do
|
||||
d="${d%/}"
|
||||
if [[ -d "$d" && ! -d "$d/.git" ]]; then
|
||||
missing_dirs+=("$d")
|
||||
fi
|
||||
done < <(find "$registry_root" -maxdepth 4 -type d -name "$(basename "$pattern")" 2>/dev/null | grep -E "$pattern" || true)
|
||||
done
|
||||
|
||||
# Forma mas directa: iterar directorios conocidos
|
||||
for pattern in apps analysis; do
|
||||
if [[ -d "$registry_root/$pattern" ]]; then
|
||||
for d in "$registry_root/$pattern"/*/; do
|
||||
d="${d%/}"
|
||||
[[ -d "$d" ]] || continue
|
||||
[[ -d "$d/.git" ]] && continue
|
||||
echo " auto-init: $d" >&2
|
||||
ensure_repo_synced "$d" dataforge "$(basename "$d")" master "chore: initial sync" || \
|
||||
echo " [warn] fallo inicializando $d" >&2
|
||||
done
|
||||
fi
|
||||
done
|
||||
for proj in "$registry_root"/projects/*/; do
|
||||
for subdir in apps analysis; do
|
||||
[[ -d "$proj$subdir" ]] || continue
|
||||
for d in "$proj$subdir"/*/; do
|
||||
d="${d%/}"
|
||||
[[ -d "$d" ]] || continue
|
||||
[[ -d "$d/.git" ]] && continue
|
||||
echo " auto-init: $d" >&2
|
||||
ensure_repo_synced "$d" dataforge "$(basename "$d")" master "chore: initial sync" || \
|
||||
echo " [warn] fallo inicializando $d" >&2
|
||||
done
|
||||
done
|
||||
done
|
||||
else
|
||||
echo " [skip] GITEA_URL/GITEA_TOKEN no disponibles — omitiendo auto-init" >&2
|
||||
fi
|
||||
|
||||
# Redescubrir repos tras posibles inicializaciones
|
||||
repos=$(discover_git_repos "$registry_root")
|
||||
|
||||
# --- Paso 2: Escanear secrets ---
|
||||
echo "" >&2
|
||||
echo "[2/6] Escaneando secrets en dirty trees..." >&2
|
||||
local secret_matches=""
|
||||
while IFS= read -r repo; do
|
||||
[[ -z "$repo" ]] && continue
|
||||
local matches
|
||||
matches=$(scan_secrets_in_dirty "$repo" 2>/dev/null || true)
|
||||
if [[ -n "$matches" ]]; then
|
||||
secret_matches="$secret_matches"$'\n'"--- $repo ---"$'\n'"$matches"
|
||||
fi
|
||||
done <<< "$repos"
|
||||
|
||||
if [[ -n "$secret_matches" ]]; then
|
||||
echo "" >&2
|
||||
echo "ABORTANDO: archivos sospechosos detectados antes de commitear:" >&2
|
||||
echo "$secret_matches" >&2
|
||||
echo "" >&2
|
||||
echo "Gestiona esos archivos (.gitignore, mover, o decidir si entran) y reintenta." >&2
|
||||
return 1
|
||||
fi
|
||||
echo " OK: sin archivos sospechosos" >&2
|
||||
|
||||
# --- Paso 3: Auto-commitear dirty trees ---
|
||||
echo "" >&2
|
||||
echo "[3/6] Auto-commiteando dirty trees..." >&2
|
||||
local commits_summary=""
|
||||
while IFS= read -r repo; do
|
||||
[[ -z "$repo" ]] && continue
|
||||
local subject
|
||||
subject=$(git_auto_commit_dirty "$repo" "$commit_message" 2>/dev/null || true)
|
||||
if [[ -n "$subject" ]]; then
|
||||
local repo_name
|
||||
repo_name="$(basename "$repo")"
|
||||
echo " commit: $repo_name — $subject" >&2
|
||||
commits_summary="$commits_summary"$'\n'" $repo_name: $subject"
|
||||
fi
|
||||
done <<< "$repos"
|
||||
|
||||
# --- Paso 4: Push de repos con commits locales ---
|
||||
echo "" >&2
|
||||
echo "[4/6] Pusheando repos adelantados..." >&2
|
||||
local push_summary=""
|
||||
while IFS= read -r repo; do
|
||||
[[ -z "$repo" ]] && continue
|
||||
local status_line
|
||||
status_line=$(git_push_if_ahead "$repo" 2>/dev/null || true)
|
||||
if [[ -n "$status_line" ]]; then
|
||||
echo " $status_line" >&2
|
||||
push_summary="$push_summary"$'\n'" $status_line"
|
||||
fi
|
||||
done <<< "$repos"
|
||||
|
||||
# --- Paso 5: Push de ~/.password-store (sin commitear) ---
|
||||
echo "" >&2
|
||||
echo "[5/6] Verificando ~/.password-store..." >&2
|
||||
local pass_dir="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
|
||||
local pass_summary=" [skip] password-store: no encontrado"
|
||||
if [[ -d "$pass_dir/.git" ]]; then
|
||||
local pass_dirty
|
||||
pass_dirty=$(git -C "$pass_dir" status --porcelain | wc -l)
|
||||
if [[ "$pass_dirty" -gt 0 ]]; then
|
||||
echo " [warn] ~/.password-store tiene cambios sin commitear; pass debe commitear solo. Saltando push." >&2
|
||||
pass_summary=" [warn] password-store: dirty (pass no commiteo)"
|
||||
else
|
||||
local pass_status
|
||||
pass_status=$(git_push_if_ahead "$pass_dir" 2>/dev/null || true)
|
||||
echo " $pass_status" >&2
|
||||
pass_summary=" $pass_status"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Paso 6: fn sync ---
|
||||
echo "" >&2
|
||||
echo "[6/6] Ejecutando fn sync..." >&2
|
||||
local sync_summary=" [skip] fn sync: credenciales no disponibles"
|
||||
local fn_bin="$registry_root/fn"
|
||||
if [[ -x "$fn_bin" ]]; then
|
||||
local api_user api_pass api_token
|
||||
api_user=$(pass_get registry/basicauth-user | head -n1 2>/dev/null || true)
|
||||
api_pass=$(pass_get registry/basicauth-pass | head -n1 2>/dev/null || true)
|
||||
api_token=$(pass_get registry/api-token | head -n1 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$api_user" && -n "$api_pass" && -n "$api_token" ]]; then
|
||||
export FN_REGISTRY_API="https://${api_user}:${api_pass}@registry.organic-machine.com"
|
||||
export REGISTRY_API_TOKEN="$api_token"
|
||||
local sync_out
|
||||
sync_out=$("$fn_bin" sync 2>&1) && {
|
||||
sync_summary=" OK: $sync_out"
|
||||
} || {
|
||||
sync_summary=" [error] fn sync: $sync_out"
|
||||
}
|
||||
echo " $sync_summary" >&2
|
||||
else
|
||||
echo " [warn] Credenciales registry no disponibles — omitiendo fn sync" >&2
|
||||
fi
|
||||
else
|
||||
echo " [warn] $fn_bin no encontrado — omitiendo fn sync" >&2
|
||||
fi
|
||||
|
||||
# --- Resumen ---
|
||||
echo ""
|
||||
echo "===== RESUMEN full_git_push ====="
|
||||
echo ""
|
||||
echo "Commits creados:"
|
||||
if [[ -n "$commits_summary" ]]; then
|
||||
echo "$commits_summary"
|
||||
else
|
||||
echo " (ninguno)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Push status:"
|
||||
if [[ -n "$push_summary" ]]; then
|
||||
echo "$push_summary"
|
||||
else
|
||||
echo " (ninguno)"
|
||||
fi
|
||||
echo ""
|
||||
echo "pass-secrets:"
|
||||
echo "$pass_summary"
|
||||
echo ""
|
||||
echo "fn sync:"
|
||||
echo "$sync_summary"
|
||||
echo ""
|
||||
echo "================================="
|
||||
}
|
||||
|
||||
full_git_push "$@"
|
||||
Reference in New Issue
Block a user