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:
@@ -1,190 +1,29 @@
|
||||
# /full-git-push — Push automático de fn_registry + todos los sub-repos + fn sync
|
||||
# /full-git-push — Push automático de fn_registry + sub-repos + fn sync
|
||||
|
||||
Pushea el repo principal `fn_registry` y todos los sub-repos git anidados (apps y analyses, cada uno como repo independiente bajo `dataforge/<name>` en Gitea), y luego ejecuta `fn sync` para empujar la metadata no regenerable (proposals, apps, projects, analysis, vaults, pc_locations) al `registry_api`.
|
||||
|
||||
**Estandar:** todo `apps/<name>`, `analysis/<name>`, `projects/*/apps/<name>` y `projects/*/analysis/<name>` debe tener su propio repo Gitea bajo `dataforge/<basename>`. Los `subrepos/` de la raiz NO entran (mirrors upstream). Los `vaults/` tampoco.
|
||||
|
||||
**Modo automático (preferencia del usuario):** este comando NO pregunta. Si hay dirty trees, commitea automáticamente con un mensaje generado a partir de los cambios. Prioridad: hacer commits frecuentes y pushear rápido. **Único límite:** no commitear archivos que parezcan secrets (`.env`, `*credentials*`, `*.key`, `*.pem`, `id_rsa*`) — si se detectan, abortar y avisar.
|
||||
|
||||
## Argumento
|
||||
|
||||
`$ARGUMENTS` — opcional. Si se pasa texto, se usa como mensaje de commit. Sin argumento, se genera uno automáticamente con el patrón:
|
||||
|
||||
```
|
||||
chore: auto-commit (<N> archivos modificados, <N> nuevos, <N> borrados)
|
||||
|
||||
- <ruta1>
|
||||
- <ruta2>
|
||||
...
|
||||
```
|
||||
|
||||
Si los cambios tienen un patrón claro (todos en un mismo dominio/dir), usar ese patrón en el subject:
|
||||
- todo bajo `python/functions/<dom>/` → `feat(<dom>): auto-commit con N cambios`
|
||||
- todo bajo `dev/issues/` → `chore(issues): auto-commit`
|
||||
- mezclado → `chore: auto-commit`
|
||||
|
||||
## Pasos
|
||||
|
||||
### 1. Descubrir repos git + apps/analyses sin git
|
||||
Wrapper sobre el pipeline `full_git_push_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
|
||||
REPOS=$(find . -name ".git" -type d \
|
||||
-not -path "./.git" -not -path "./.git/*" \
|
||||
-not -path "*/node_modules/*" -not -path "*/.venv/*" \
|
||||
-not -path "*/cpp/vendor/*" -not -path "*/cpp/build/*" \
|
||||
-not -path "*/sources/*" -not -path "*/temp/*" -not -path "*/subrepos/*" 2>/dev/null \
|
||||
| sed 's|/.git$||')
|
||||
REPOS=". $REPOS"
|
||||
|
||||
# Apps/analyses sin .git — auto-inicializar como dataforge/<basename>
|
||||
MISSING=()
|
||||
for d in apps/*/ analysis/*/ projects/*/apps/*/ projects/*/analysis/*/; do
|
||||
d="${d%/}"
|
||||
[[ -d "$d/.git" ]] || MISSING+=("$d")
|
||||
done
|
||||
./fn run full_git_push_bash_pipelines "$ARGUMENTS"
|
||||
```
|
||||
|
||||
### 1b. Auto-inicializar repos faltantes (sin pedir confirmación)
|
||||
## Argumento
|
||||
|
||||
Para cada `$d` en `MISSING`:
|
||||
`$ARGUMENTS` — opcional. Mensaje de commit fijo para todos los repos dirty. Sin argumento, el pipeline genera un mensaje automático por repo según los paths cambiados (ver `bash/functions/infra/git_auto_commit_dirty.sh`).
|
||||
|
||||
```bash
|
||||
export GITEA_URL=$(pass agentes/gitea-url | head -n1)
|
||||
export GITEA_TOKEN=$(pass gitea/dataforge-git-token | head -n1)
|
||||
export FN_REGISTRY_INFRA_DIR=/home/lucas/fn_registry/bash/functions/infra
|
||||
## Qué hace el pipeline
|
||||
|
||||
bash -c "
|
||||
source $FN_REGISTRY_INFRA_DIR/ensure_repo_synced.sh
|
||||
ensure_repo_synced '$d' dataforge \"\$(basename '$d')\" master 'chore: initial sync'
|
||||
"
|
||||
```
|
||||
|
||||
Si `$d/.gitignore` no existe antes de inicializar, escribir uno apropiado (ver `.claude/rules/apps_vs_functions.md`). Solo abortar la inicialización de ese repo concreto si falla; seguir con el resto.
|
||||
|
||||
### 2. Detectar secrets antes de commitear
|
||||
|
||||
Para cada repo dirty, listar archivos modificados/nuevos y comprobar nombres sospechosos:
|
||||
|
||||
```bash
|
||||
for r in $REPOS; do
|
||||
( cd "$r" \
|
||||
&& git status --porcelain | awk '{print $2}' \
|
||||
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
|
||||
| head -5
|
||||
)
|
||||
done
|
||||
```
|
||||
|
||||
Si la lista de coincidencias **no está vacía**, abortar el push completo, listar los archivos sospechosos y pedir al usuario que los gestione (añadir a `.gitignore`, mover, o decidir explícitamente que entren).
|
||||
|
||||
### 3. Auto-commitear dirty trees
|
||||
|
||||
Para cada repo con cambios sin commitear:
|
||||
|
||||
```bash
|
||||
for r in $REPOS; do
|
||||
( cd "$r"
|
||||
[ -z "$(git status --porcelain)" ] && exit 0 # limpio, nada que hacer
|
||||
git add -A
|
||||
if [ -n "$ARGUMENTS" ]; then
|
||||
MSG="$ARGUMENTS"
|
||||
else
|
||||
MSG="$(generate_auto_message)" # patrón descrito en sección 'Argumento'
|
||||
fi
|
||||
git commit -m "$MSG" \
|
||||
-m "Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" 2>&1 | tail -3
|
||||
)
|
||||
done
|
||||
```
|
||||
|
||||
`generate_auto_message` debe inspeccionar `git diff --cached --stat` y producir un subject como `feat(notebook): N cambios` cuando todos los paths comparten prefijo, o `chore: auto-commit` si están dispersos.
|
||||
|
||||
### 4. Push solo de repos con cambios locales (NO usar `git push` ciego)
|
||||
|
||||
**Filtrar primero, pushear despues.** Sobre 30+ repos, hacer `git push` en todos (incluso "Everything up-to-date") cuesta 30-90s en handshakes SSH. Usar refs locales (sin red) para decidir si hay algo que pushear:
|
||||
|
||||
```bash
|
||||
for r in $REPOS; do
|
||||
( cd "$r" \
|
||||
&& BRANCH=$(git rev-parse --abbrev-ref HEAD) \
|
||||
&& UPSTREAM_OK=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) \
|
||||
&& if [ -z "$UPSTREAM_OK" ]; then
|
||||
echo "[push -u] $r ($BRANCH)"
|
||||
git push -u origin "$BRANCH" 2>&1 | tail -3
|
||||
else
|
||||
AHEAD=$(git rev-list --count @{u}..HEAD 2>/dev/null)
|
||||
if [ "${AHEAD:-0}" -gt 0 ]; then
|
||||
echo "[push] $r ($BRANCH, $AHEAD commits ahead)"
|
||||
git push origin "$BRANCH" 2>&1 | tail -3
|
||||
else
|
||||
echo "[skip] $r (up-to-date local refs)"
|
||||
fi
|
||||
fi
|
||||
)
|
||||
done
|
||||
```
|
||||
|
||||
Reglas:
|
||||
- Sin upstream → `git push -u` (siempre).
|
||||
- Con upstream y `rev-list @{u}..HEAD` > 0 → push.
|
||||
- Con upstream y 0 ahead → skip (ni siquiera intentar).
|
||||
|
||||
`rev-list @{u}..HEAD` solo lee refs locales, no toca la red. Esto es seguro porque cualquier commit local hecho en este PC ya esta a partir del paso 3 (auto-commit). Si en otro PC se hizo push y aqui no se ha hecho pull, sigue siendo correcto: no tenemos nada local que pushear.
|
||||
|
||||
Si `push` rechaza por non-fast-forward (rama behind), no abortar el resto. Reportar ese repo concreto y sugerir `/full-git-pull` antes; seguir con los demás repos.
|
||||
|
||||
### 5. Push del repo de pass (`~/.password-store`)
|
||||
|
||||
El password store es su propio repo Git en Gitea (`dataforge/pass-secrets`). `pass insert/edit/rm` ya commitea automaticamente, por lo que aqui SOLO hay que pushear los commits locales.
|
||||
|
||||
```bash
|
||||
PASS_DIR="$HOME/.password-store"
|
||||
if [ -d "$PASS_DIR/.git" ]; then
|
||||
( cd "$PASS_DIR" \
|
||||
&& DIRTY=$(git status --porcelain | wc -l) \
|
||||
&& if [ "$DIRTY" -gt 0 ]; then
|
||||
echo "[warn] $PASS_DIR tiene cambios sin commitear; pass deberia commitear solo. Saltando push."
|
||||
else
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD) \
|
||||
&& AHEAD=$(git rev-list --count @{u}..HEAD 2>/dev/null) \
|
||||
&& if [ "${AHEAD:-0}" -gt 0 ]; then
|
||||
echo "[push] pass-secrets ($BRANCH, $AHEAD commits ahead)"
|
||||
git push origin "$BRANCH" 2>&1 | tail -3
|
||||
else
|
||||
echo "[skip] pass-secrets (up-to-date)"
|
||||
fi
|
||||
fi
|
||||
)
|
||||
fi
|
||||
```
|
||||
|
||||
Reglas:
|
||||
- NO hacer `git add` ni `git commit` en `~/.password-store` (pass lo gestiona). Si hay dirty tree, avisar y saltar.
|
||||
- NO escanear `*.gpg` por patrones de secrets — son secrets cifrados, su contenido es opaco.
|
||||
- El push usa el remote ya configurado (`dataforge/pass-secrets` en Gitea).
|
||||
|
||||
### 6. fn sync
|
||||
|
||||
```bash
|
||||
USER=$(pass registry/basicauth-user | head -1)
|
||||
PASSWD=$(pass registry/basicauth-pass | head -1)
|
||||
TOKEN=$(pass registry/api-token | head -1)
|
||||
export FN_REGISTRY_API="https://${USER}:${PASSWD}@registry.organic-machine.com"
|
||||
export REGISTRY_API_TOKEN="$TOKEN"
|
||||
./fn sync
|
||||
```
|
||||
|
||||
Si `pass` falla con "decryption failed" → gpg-agent bloqueado. Pedir al usuario que ejecute `pass show unlock` en su terminal real (Bash tool no tiene TTY) y reintentar. Esa entrada es un dummy que solo muestra `Desbloqueada!` para cachear el passphrase sin exponer ninguna API key.
|
||||
|
||||
### 7. Resumen
|
||||
|
||||
Tabla concisa: por repo, commits creados (cuántos y subject), commits pusheados, o "ya estaba al día". Estado de `pass-secrets` (pushed / skipped / dirty). Y resultado de `fn sync` (sent / received / imported).
|
||||
1. `discover_git_repos_bash_infra` — lista repos bajo `fn_registry` (excluye `node_modules`, `.venv`, `cpp/vendor`, `cpp/build`, `sources`, `temp`, `subrepos`).
|
||||
2. Auto-inicializa apps/analyses sin `.git` con `ensure_repo_synced_bash_infra` (Gitea `dataforge/<basename>`).
|
||||
3. `scan_secrets_in_dirty_bash_cybersecurity` — aborta si detecta nombres sospechosos (`.env*`, `*credentials*`, `*.key`, `*.pem`, `id_rsa*`, `*secret*`, `*token*.txt`).
|
||||
4. `git_auto_commit_dirty_bash_infra` — commitea cada repo dirty.
|
||||
5. `git_push_if_ahead_bash_infra` — push solo si `rev-list @{u}..HEAD > 0` (sin red previa).
|
||||
6. Push de `~/.password-store` (sin commitear, pass autocommitea).
|
||||
7. `./fn sync` con credenciales cargadas desde `pass`.
|
||||
|
||||
## Notas
|
||||
|
||||
- **Modo no-interactivo por diseño.** El usuario prefiere commits frecuentes y push rápido. No se pregunta si commitear ni se pide mensaje (salvo que se pase via `$ARGUMENTS`).
|
||||
- **Secrets son la única razón para abortar antes de commitear.** Cualquier patrón sospechoso (`.env`, credenciales, claves) detiene el flujo y se reporta al usuario.
|
||||
- Submodules `cpp/vendor/` (mirrors upstream) se ignoran.
|
||||
- Si un sub-repo va `behind` el remote, su push se omite con un mensaje (no se aborta el resto). El usuario corre `/full-git-pull` cuando le convenga.
|
||||
- **Modo no-interactivo por diseño.** Auto-commitea sin preguntar.
|
||||
- **Único motivo de aborto antes de commitear:** secret detectado por nombre.
|
||||
- Si un sub-repo va `behind` el remote, su push se omite (no aborta el resto). Correr `/full-git-pull` y reintentar.
|
||||
- Para tocar la lógica: editar las funciones del registry, no este wrapper.
|
||||
|
||||
Reference in New Issue
Block a user