feat(infra): auto-commit con 88 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 00:16:46 +02:00
parent 6bc97df5c0
commit eb8dbf66a1
126 changed files with 10933 additions and 287 deletions
@@ -0,0 +1,58 @@
---
name: ensure_project_gitignore
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "ensure_project_gitignore(project_dir: string) -> void"
description: "Garantiza de forma idempotente que el .gitignore de un directorio de project contiene las lineas canonicas que excluyen del repo del project el contenido de sus sub-repos hijos (apps y analyses son repos Gitea independientes) y sus vaults (datos fuera de git). Evita el doble-tracking al hacer push del project."
tags: [git, gitignore, projects, infra]
params:
- name: project_dir
desc: "Ruta al directorio del project (p. ej. projects/aurgi). Debe existir; si no, error a stderr y return 1. El .gitignore se escribe/actualiza en <project_dir>/.gitignore."
output: "Sin salida en stdout. A stderr informa de la accion realizada: 'created' si creo el .gitignore, 'updated: anadidas N lineas' si anadio lineas faltantes, u 'ok: ya completo' si nada cambiaba. Codigo de salida 0 en exito, 1 si project_dir falta o no existe."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/ensure_project_gitignore.sh"
---
## Ejemplo
```bash
source bash/functions/infra/ensure_project_gitignore.sh
# Asegura que projects/aurgi/.gitignore excluye el contenido de sus hijos.
ensure_project_gitignore projects/aurgi
# stderr: ensure_project_gitignore: created projects/aurgi/.gitignore
# (o: updated: anadidas 2 lineas / ok: ya completo)
```
Las lineas canonicas que la funcion garantiza son:
```
apps/*/
analysis/*/
vaults/*
!vaults/.gitkeep
!vaults/vault.yaml
```
## Cuando usarla
Llamala justo despues de crear un project nuevo (`mkdir -p projects/<nombre>/{apps,analysis,vaults}`) y antes de inicializar su repo Gitea con `ensure_repo_synced`, para que el repo del project nunca trackee el contenido de sus sub-repos hijos. Tambien al adoptar un project existente que aun no tiene estas exclusiones, o como paso de saneamiento cuando `git status` del project muestra contenido de `apps/`/`analysis/` que deberia estar ignorado.
## Gotchas
- La funcion modifica el filesystem (escribe en `<project_dir>/.gitignore`): es impura. No commitea ni hace push — solo deja el `.gitignore` correcto.
- La comparacion para no duplicar es linea-exacta (`grep -Fxq`). Una linea equivalente pero con espacios extra, comentario adjunto o glob distinto (p. ej. `apps/*` sin la barra final) NO se considera presente y la canonica se anade igualmente; podrian quedar ambas formas. Mantener el `.gitignore` con las lineas canonicas tal cual evita ruido.
- Si el `.gitignore` existente no termina en salto de linea, la funcion anade uno antes de apendar para no pegar la primera linea nueva al final de la ultima existente.
- Solo gestiona las exclusiones de sub-repos hijos y vaults del nivel-project; no toca otras reglas que el `.gitignore` ya contenga ni las reordena.
- Si una linea canonica ya existia con su forma exacta, no se vuelve a anadir (idempotente): re-ejecutar es seguro.
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# ensure_project_gitignore — Garantiza de forma idempotente que el .gitignore de
# un directorio de project (projects/<nombre>/) contiene las lineas canonicas que
# excluyen del repo del project el contenido de sus sub-repos hijos (apps y
# analyses son repos Gitea independientes) y sus vaults (datos fuera de git).
#
# Esto evita que al hacer push del project se trackee por error el contenido de
# los hijos (doble-tracking). Ver .claude/rules/apps_subrepo.md y
# .claude/rules/projects.md.
#
# Uso:
# ensure_project_gitignore <project_dir>
#
# Salida:
# stdout vacio. A stderr informa de la accion realizada (created / updated / ok).
ensure_project_gitignore() {
local project_dir="$1"
if [[ -z "$project_dir" ]]; then
echo "ensure_project_gitignore: se requiere project_dir" >&2
return 1
fi
if [[ ! -d "$project_dir" ]]; then
echo "ensure_project_gitignore: directorio '$project_dir' no existe" >&2
return 1
fi
local gitignore="$project_dir/.gitignore"
# Lineas canonicas que deben estar presentes (orden de referencia).
local -a canonical=(
"apps/*/"
"analysis/*/"
"vaults/*"
"!vaults/.gitkeep"
"!vaults/vault.yaml"
)
# Caso 1: el .gitignore no existe — crearlo con el contenido canonico.
if [[ ! -f "$gitignore" ]]; then
printf '%s\n' "${canonical[@]}" > "$gitignore"
echo "ensure_project_gitignore: created $gitignore" >&2
return 0
fi
# Caso 2: existe — anadir solo las lineas que falten (comparacion linea-exacta),
# preservando el contenido y el orden existentes.
# Si el archivo no termina en newline, anadir uno antes de apendar para no
# pegar la primera linea nueva al final de la ultima existente.
if [[ -s "$gitignore" && -n "$(tail -c 1 "$gitignore")" ]]; then
printf '\n' >> "$gitignore"
fi
local line added=0
for line in "${canonical[@]}"; do
# grep -F -x: match literal de linea completa, sin interpretar metacaracteres.
if ! grep -Fxq -- "$line" "$gitignore"; then
printf '%s\n' "$line" >> "$gitignore"
added=$((added + 1))
fi
done
if [[ $added -gt 0 ]]; then
echo "ensure_project_gitignore: updated: anadidas $added lineas a $gitignore" >&2
else
echo "ensure_project_gitignore: ok: ya completo $gitignore" >&2
fi
return 0
}
# Si se invoca como script (no source), ejecutar la funcion.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
ensure_project_gitignore "$@"
fi
+10 -3
View File
@@ -3,14 +3,15 @@ name: full_git_pull
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "full_git_pull() -> stdout: tabla resumen"
description: "Pull automatico de fn_registry + todos los sub-repos locales + submodules + fn sync. Descubre repos locales, stashea dirty trees antes de pullear, hace pull --ff-only, actualiza submodulos del repo principal, pulla ~/.password-store, regenera registry.db con fn index y ejecuta fn sync."
description: "Pull automatico de fn_registry + todos los sub-repos locales + submodules + fn sync. Descubre repos locales, stashea dirty trees antes de pullear, hace pull --ff-only, actualiza submodulos del repo principal, pulla ~/.password-store, regenera registry.db con fn index, ejecuta fn sync y reclona los sub-repos hijos faltantes de cada project (apps/analysis) via clone_project_subrepos."
tags: [git, pull, sync, registry, pipeline, pendiente-usar]
uses_functions:
- discover_git_repos_bash_infra
- git_pull_with_stash_bash_infra
- clone_project_subrepos_bash_pipelines
- pass_get_bash_infra
uses_types: []
returns: []
@@ -51,4 +52,10 @@ bash bash/functions/pipelines/full_git_pull.sh
## Notas
Solo hace pull fast-forward — nunca rebase ni merge automatico. Los repos con divergencia o conflicto de stash se listan al final del resumen para intervencion manual, pero el pipeline no aborta por ellos. No clona repos faltantes: cada PC tiene el subset que le interesa (clonar manualmente si se necesita uno nuevo). Modo completamente no-interactivo.
Solo hace pull fast-forward — nunca rebase ni merge automatico. Los repos con divergencia o conflicto de stash se listan al final del resumen para intervencion manual, pero el pipeline no aborta por ellos. Modo completamente no-interactivo.
Desde v1.1.0 SI reclona los sub-repos hijos faltantes de cada project: tras `fn sync` (que trae a `registry.db` las filas de apps/analysis de todos los PCs), itera los projects y llama `clone_project_subrepos` para traer al disco los hijos que falten, re-indexando si clono alguno. `registry.db` actua como manifest de sub-repos, asi que clonar el project paraguas + `/full-git-pull` reconstruye su arbol entero sin adivinar nombres. Los repos sueltos (sin project) siguen sin auto-clonarse: cada PC tiene el subset que le interesa.
## Capability growth log
- v1.1.0 (2026-06-10) — anade el paso 6: reclonado de sub-repos hijos de cada project via `clone_project_subrepos` tras `fn sync`, con re-index si clona alguno. Permite reconstruir el arbol completo de un project en un PC nuevo (issue 0171).
+39
View File
@@ -149,6 +149,42 @@ full_git_pull() {
fi
fi
# --- Paso 6: Reclonar sub-repos hijos de cada project (issue 0171) ---
# Tras fn sync, registry.db contiene las filas apps/analysis de TODOS los PCs.
# clone_project_subrepos clona en este disco los hijos que falten (skip si ya
# existen). Asi, clonar el project paraguas y correr /full-git-pull reconstruye
# su arbol entero sin adivinar nombres de sub-repos: registry.db ES el manifest.
echo "" >&2
echo "[6/6] Reclonando sub-repos de projects..." >&2
local reclone_summary=" [skip] sin projects o registry.db"
if [[ -f "$registry_root/registry.db" ]] && command -v sqlite3 >/dev/null 2>&1; then
export FN_REGISTRY_ROOT="$registry_root"
export GITEA_URL="${GITEA_URL:-$(pass_get agentes/gitea-url | head -n1 2>/dev/null || true)}"
local clone_script="$SCRIPT_DIR/clone_project_subrepos.sh"
local any_cloned=0
if [[ -f "$clone_script" ]]; then
while IFS= read -r proj_id; do
[[ -z "$proj_id" ]] && continue
local clone_out
clone_out=$(bash "$clone_script" "$proj_id" 2>&1 || true)
if echo "$clone_out" | grep -q '\[cloned\]'; then
any_cloned=1
echo " $proj_id: nuevos sub-repos clonados" >&2
fi
done < <(sqlite3 "$registry_root/registry.db" "SELECT id FROM projects;" 2>/dev/null)
if [[ "$any_cloned" -eq 1 ]]; then
echo " re-index tras clonado..." >&2
[[ -x "$fn_bin" ]] && CGO_ENABLED=1 "$fn_bin" index >/dev/null 2>&1 || true
reclone_summary=" OK: nuevos sub-repos clonados + re-index"
else
reclone_summary=" OK: nada que clonar (todo presente)"
fi
else
reclone_summary=" [skip] clone_project_subrepos.sh no encontrado"
fi
fi
echo " $reclone_summary" >&2
# --- Resumen ---
echo ""
echo "===== RESUMEN full_git_pull ====="
@@ -171,6 +207,9 @@ full_git_pull() {
echo ""
echo "fn sync:"
echo "$sync_summary"
echo ""
echo "Reclonado sub-repos de projects:"
echo "$reclone_summary"
if [[ ${#diverged[@]} -gt 0 || ${#conflicts[@]} -gt 0 ]]; then
echo ""
+7 -2
View File
@@ -3,10 +3,10 @@ name: full_git_push
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "full_git_push(commit_message?: string) -> stdout: tabla resumen"
description: "Push automatico de fn_registry + todos los sub-repos + fn sync. Descubre repos, escanea secrets (aborta si detecta), auto-inicializa apps/analyses sin .git via ensure_repo_synced, auto-commitea dirty trees, pushea solo repos adelantados, pushea ~/.password-store sin commitear, y ejecuta fn sync."
description: "Push automatico de fn_registry + todos los sub-repos + fn sync. Descubre repos, escanea secrets (aborta si detecta), auto-inicializa apps/analyses Y projects paraguas sin .git via ensure_repo_synced (asegurando el .gitignore canonico del project antes), auto-commitea dirty trees, pushea solo repos adelantados, pushea ~/.password-store sin commitear, y ejecuta fn sync."
tags: [git, push, sync, registry, pipeline, pendiente-usar]
uses_functions:
- discover_git_repos_bash_infra
@@ -14,6 +14,7 @@ uses_functions:
- git_auto_commit_dirty_bash_infra
- git_push_if_ahead_bash_infra
- ensure_repo_synced_bash_infra
- ensure_project_gitignore_bash_infra
- pass_get_bash_infra
uses_types: []
returns: []
@@ -62,3 +63,7 @@ bash bash/functions/pipelines/full_git_push.sh "feat: nueva funcion"
## Notas
El unico motivo para abortar antes de commitear es la deteccion de secrets. Cualquier otro error (push rechazado por non-fast-forward, fn sync no disponible) se reporta en el resumen y el pipeline continua con el resto de repos. Modo completamente no-interactivo.
## Capability growth log
- v1.1.0 (2026-06-10) — auto-inicializa tambien los projects paraguas (`projects/<p>/`) sin repo Gitea, no solo apps/analyses. Antes de pushear cada project asegura su `.gitignore` canonico via `ensure_project_gitignore` para no trackear el contenido de los sub-repos hijos. Cierra el agujero por el que projects como aurgi/obsidian/osint vivian solo en disco y se perdian al borrar el PC (issue 0171).
+27
View File
@@ -13,6 +13,7 @@ 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 "$INFRA_DIR/ensure_project_gitignore.sh"
source "$CYBERSEC_DIR/scan_secrets_in_dirty.sh"
full_git_push() {
@@ -65,6 +66,32 @@ full_git_push() {
ensure_repo_synced "$d" dataforge "$(basename "$d")" master "chore: initial sync" || \
echo " [warn] fallo inicializando $d" >&2
done < <(sqlite3 "$registry_root/registry.db" "SELECT dir_path FROM apps WHERE dir_path != '' UNION SELECT dir_path FROM analysis WHERE dir_path != '';" 2>/dev/null)
# Paso 1c: Auto-inicializar los PROJECTS paraguas sin .git (issue 0171).
# El directorio projects/<p>/ versiona SOLO las docs de nivel-project
# (project.md, vault.yaml, CONVENTIONS.md, tools/...). Sus hijos apps/* y
# analysis/* son sub-repos Gitea independientes, excluidos por el .gitignore
# canonico que ensure_project_gitignore garantiza ANTES del push para no
# trackear su contenido (doble-tracking). Sin esto, un project sin repo
# (aurgi, obsidian, osint) vivia solo en disco y se perdia al borrar el PC.
if [[ -f "$registry_root/registry.db" ]] && command -v sqlite3 >/dev/null 2>&1; then
while IFS= read -r proj_dir; do
[[ -z "$proj_dir" ]] && continue
local pd="$registry_root/$proj_dir"
[[ -d "$pd" ]] || continue
# Garantizar el .gitignore canonico ANTES de cualquier git add -A.
ensure_project_gitignore "$pd" || \
echo " [warn] no se pudo asegurar .gitignore de $pd" >&2
if [[ -d "$pd/.git" ]]; then
git -C "$pd" remote get-url origin >/dev/null 2>&1 && continue
echo " fix-remote: $pd (.git sin origin)" >&2
else
echo " auto-init project: $pd" >&2
fi
ensure_repo_synced "$pd" dataforge "$(basename "$pd")" master "chore: initial sync project" || \
echo " [warn] fallo inicializando project $pd" >&2
done < <(sqlite3 "$registry_root/registry.db" "SELECT CASE WHEN dir_path != '' THEN dir_path ELSE 'projects/'||id END FROM projects;" 2>/dev/null)
fi
else
echo " [warn] registry.db o sqlite3 no disponibles — omitiendo auto-init BD-driven" >&2
fi