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

313 lines
10 KiB
Bash

#!/usr/bin/env bash
# tbd_branch_create — crea rama TBD desde master/main actualizado
#
# Uso:
# tbd_branch_create issue <NNNN> <slug>
# tbd_branch_create quick <slug>
#
# Opciones especiales:
# --test ejecutar suite de tests internos y salir
set -euo pipefail
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
_tbd_detect_base() {
# Retorna 'master' o 'main' — el primero que exista localmente
if git show-ref --verify --quiet refs/heads/master 2>/dev/null; then
echo "master"
elif git show-ref --verify --quiet refs/heads/main 2>/dev/null; then
echo "main"
else
echo ""
fi
}
_tbd_require_git_repo() {
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "ERROR: el directorio actual no es un repositorio git." >&2
return 1
fi
}
_tbd_pull_rebase() {
# Hace pull --rebase solo si hay upstream configurado; si no, es no-op.
local has_upstream
has_upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo "")
if [[ -n "$has_upstream" ]]; then
git pull --rebase
else
echo "(sin remote upstream — saltando pull)"
fi
}
# ---------------------------------------------------------------------------
# funcion principal
# ---------------------------------------------------------------------------
tbd_branch_create() {
local mode="${1:-}"
if [[ -z "$mode" ]] || [[ "$mode" != "issue" && "$mode" != "quick" ]]; then
echo "Uso: tbd_branch_create issue <NNNN> <slug>" >&2
echo " tbd_branch_create quick <slug>" >&2
return 1
fi
# Verificar repo git
_tbd_require_git_repo
# Detectar rama base
local base
base=$(_tbd_detect_base)
if [[ -z "$base" ]]; then
echo "ERROR: no se encontro rama 'master' ni 'main' en el repo local." >&2
return 1
fi
# Construir nombre de rama segun modo
local branch_name
if [[ "$mode" == "issue" ]]; then
local num="${2:-}"
local slug="${3:-}"
if [[ -z "$num" || -z "$slug" ]]; then
echo "Uso: tbd_branch_create issue <NNNN> <slug>" >&2
return 1
fi
if [[ ! "$num" =~ ^[0-9]{4}$ ]]; then
echo "ERROR: <NNNN> debe ser exactamente 4 digitos numericos (ej: 0042)." >&2
return 1
fi
if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
echo "ERROR: slug debe ser kebab-case ASCII (ej: fix-typo, add-auth)." >&2
return 1
fi
branch_name="issue/${num}-${slug}"
else
# quick
local slug="${2:-}"
if [[ -z "$slug" ]]; then
echo "Uso: tbd_branch_create quick <slug>" >&2
return 1
fi
if [[ ! "$slug" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
echo "ERROR: slug debe ser kebab-case ASCII (ej: fix-typo, update-readme)." >&2
return 1
fi
branch_name="quick/${slug}"
fi
# Verificar si la rama ya existe
if git show-ref --verify --quiet "refs/heads/${branch_name}" 2>/dev/null; then
echo "ERROR: la rama '${branch_name}' ya existe localmente." >&2
return 1
fi
# Asegurarse de estar en la rama base
local current
current=$(git branch --show-current)
if [[ "$current" != "$base" ]]; then
echo "Cambiando a ${base}..."
git checkout "$base"
fi
# Verificar working tree limpio
local dirty
dirty=$(git status --porcelain)
if [[ -n "$dirty" ]]; then
echo "ERROR: working tree dirty. Commit o stash los cambios antes de crear la rama." >&2
return 1
fi
# Actualizar base desde remote si hay upstream
echo "Actualizando ${base}..."
_tbd_pull_rebase
# Crear la rama
git checkout -b "$branch_name"
echo "Rama '${branch_name}' creada desde ${base} actualizada."
return 0
}
# ---------------------------------------------------------------------------
# modo --test
# ---------------------------------------------------------------------------
_tbd_branch_create_tests() {
local PASS=0
local FAIL=0
_assert_eq() {
local name="$1" expected="$2" got="$3"
if [[ "$expected" == "$got" ]]; then
echo "PASS: $name"
PASS=$((PASS + 1))
else
echo "FAIL: $name — expected '${expected}', got '${got}'"
FAIL=$((FAIL + 1))
fi
}
_assert_exit() {
local name="$1" expected_exit="$2"
shift 2
local got_exit=0
"$@" > /dev/null 2>&1 || got_exit=$?
if [[ "$expected_exit" == "$got_exit" ]]; then
echo "PASS: $name (exit ${got_exit})"
PASS=$((PASS + 1))
else
echo "FAIL: $name — expected exit ${expected_exit}, got ${got_exit}"
FAIL=$((FAIL + 1))
fi
}
# ---------------------------------------------------------------------------
# Setup: bare remote + repo clonado (master)
# ---------------------------------------------------------------------------
local tmproot
tmproot=$(mktemp -d)
local bare="$tmproot/remote.git"
local work="$tmproot/work"
git -c init.defaultBranch=master init --bare -q "$bare"
git clone -q "$bare" "$work"
(
cd "$work"
git config user.email "test@test.com"
git config user.name "Test"
echo "init" > README.md
git add README.md
git commit -q -m "chore: init"
git push -q -u origin master
)
# ---------------------------------------------------------------------------
# Test: rama issue valida
# ---------------------------------------------------------------------------
(
cd "$work"
tbd_branch_create issue 0042 add-auth
) > /dev/null 2>&1
local result
result=$(cd "$work" && git branch --show-current)
_assert_eq "issue branch created" "issue/0042-add-auth" "$result"
# Volver a master
(cd "$work" && git checkout -q master)
# ---------------------------------------------------------------------------
# Test: rama quick valida
# ---------------------------------------------------------------------------
(
cd "$work"
tbd_branch_create quick fix-typo
) > /dev/null 2>&1
result=$(cd "$work" && git branch --show-current)
_assert_eq "quick branch created" "quick/fix-typo" "$result"
(cd "$work" && git checkout -q master)
# ---------------------------------------------------------------------------
# Test: numero de issue invalido (3 digitos)
# ---------------------------------------------------------------------------
_assert_exit "issue number must be 4 digits" 1 bash -c "
cd '$work'
source '${BASH_SOURCE[0]}'
tbd_branch_create issue 042 fix
"
# ---------------------------------------------------------------------------
# Test: slug invalido (mayusculas)
# ---------------------------------------------------------------------------
_assert_exit "slug must be kebab-case" 1 bash -c "
cd '$work'
source '${BASH_SOURCE[0]}'
tbd_branch_create issue 0001 Fix-Typo
"
# ---------------------------------------------------------------------------
# Test: modo invalido
# ---------------------------------------------------------------------------
_assert_exit "invalid mode exits 1" 1 bash -c "
cd '$work'
source '${BASH_SOURCE[0]}'
tbd_branch_create hotfix my-slug
"
# ---------------------------------------------------------------------------
# Test: sin argumentos
# ---------------------------------------------------------------------------
_assert_exit "no args exits 1" 1 bash -c "
cd '$work'
source '${BASH_SOURCE[0]}'
tbd_branch_create
"
# ---------------------------------------------------------------------------
# Test: working tree dirty → error
# ---------------------------------------------------------------------------
echo "dirty" > "$work/dirty.txt"
_assert_exit "dirty tree exits 1" 1 bash -c "
cd '$work'
source '${BASH_SOURCE[0]}'
tbd_branch_create quick clean-slug
"
rm -f "$work/dirty.txt"
# ---------------------------------------------------------------------------
# Test: rama ya existe → error
# ---------------------------------------------------------------------------
(cd "$work" && git checkout -q -b issue/0099-existing && git checkout -q master)
_assert_exit "existing branch exits 1" 1 bash -c "
cd '$work'
source '${BASH_SOURCE[0]}'
tbd_branch_create issue 0099 existing
"
# ---------------------------------------------------------------------------
# Test: funciona con 'main' como rama base
# ---------------------------------------------------------------------------
local bare2="$tmproot/remote2.git"
local work2="$tmproot/work2"
git -c init.defaultBranch=main init --bare -q "$bare2"
git clone -q "$bare2" "$work2"
(
cd "$work2"
git config user.email "test@test.com"
git config user.name "Test"
echo "init" > README.md
git add README.md
git commit -q -m "chore: init"
git push -q -u origin main
tbd_branch_create quick use-main
) > /dev/null 2>&1
result=$(cd "$work2" && git branch --show-current)
_assert_eq "works with main as base" "quick/use-main" "$result"
# ---------------------------------------------------------------------------
# Cleanup y resultado
# ---------------------------------------------------------------------------
rm -rf "$tmproot"
echo "---"
echo "Results: ${PASS} passed, ${FAIL} failed"
[[ $FAIL -eq 0 ]] || exit 1
}
# ---------------------------------------------------------------------------
# entry point
# ---------------------------------------------------------------------------
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
if [[ "${1:-}" == "--test" ]]; then
_tbd_branch_create_tests
else
tbd_branch_create "$@"
fi
fi