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,56 @@
|
||||
---
|
||||
name: scan_secrets_in_dirty
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "scan_secrets_in_dirty(repo_dir: string) -> stdout: matched paths"
|
||||
description: "Para un repo git, lista archivos modificados/nuevos cuyo nombre matchee patron de secret. Patrones: .env, credentials, .key, .pem, id_rsa, secret, token*.txt. Stdout vacio si no hay matches. Exit 0 siempre que el repo exista."
|
||||
tags: [git, secrets, security, scan, credentials, cybersecurity]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: repo_dir
|
||||
desc: "path al repo git a escanear; default '.'"
|
||||
output: "paths sospechosos por stdout (uno por linea), vacio si todo limpio; exit 1 solo si repo_dir no es un repo git"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/cybersecurity/scan_secrets_in_dirty.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/cybersecurity/scan_secrets_in_dirty.sh
|
||||
|
||||
# Escanear repo actual
|
||||
matches=$(scan_secrets_in_dirty .)
|
||||
if [[ -n "$matches" ]]; then
|
||||
echo "ABORTAR: archivos sospechosos detectados:"
|
||||
echo "$matches"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Escanear repo especifico
|
||||
scan_secrets_in_dirty /home/lucas/fn_registry
|
||||
```
|
||||
|
||||
## Patrones detectados
|
||||
|
||||
- `.env`, `.env.local`, `.env.production`, etc.
|
||||
- `*credentials*`
|
||||
- `*.key`
|
||||
- `*.pem`
|
||||
- `id_rsa*`
|
||||
- `*secret*`
|
||||
- `*token*.txt`
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `git status --porcelain` para listar solo archivos del working tree (modificados, nuevos, staged). No escanea el contenido del archivo, solo el nombre. Las claves GPG cifradas (`.gpg`) no se detectan intencionalmente — son opacas. Exit 0 siempre que el directorio sea un repo git valido, incluso si no hay matches.
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# scan_secrets_in_dirty — Para un repo git, lista archivos modificados/nuevos
|
||||
# cuyo nombre matchee patron de secret. Stdout vacio si no hay matches.
|
||||
# Exit 0 siempre que el repo exista (el caller decide si abortar).
|
||||
|
||||
scan_secrets_in_dirty() {
|
||||
local repo_dir="${1:-.}"
|
||||
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Listar archivos modificados o nuevos (excluyendo borrados)
|
||||
# y filtrar por patron de secret en el nombre del archivo
|
||||
git -C "$repo_dir" status --porcelain \
|
||||
| awk '{print $NF}' \
|
||||
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
|
||||
|| true
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
scan_secrets_in_dirty "$@"
|
||||
fi
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: append_diary_entry
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "append_diary_entry(titulo: string, cuerpo: string) -> string"
|
||||
description: "Añade una entrada al diario del dia en ${DIARY_DIR:-docs/diary}/YYYY-MM-DD.md. Crea el archivo con cabecera si no existe. Nunca reescribe contenido previo. Si cuerpo es vacio escribe solo el header de la seccion."
|
||||
tags: [diary, markdown, append, idempotent, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: titulo
|
||||
desc: "Titulo corto de la entrada (obligatorio). Se usa como cabecera H2 junto a la hora actual. Exit 2 si viene vacio."
|
||||
- name: cuerpo
|
||||
desc: "Cuerpo markdown de la entrada (opcional). Si se omite o es vacio, se escribe solo la linea de cabecera H2 sin parrafo adicional — util para marcar un momento sin detalle."
|
||||
output: "Path relativo al archivo de diario escrito (ej: docs/diary/2026-05-07.md). Varia segun DIARY_DIR y la fecha del sistema."
|
||||
tested: true
|
||||
tests:
|
||||
- "crear archivo nuevo con titulo y cuerpo"
|
||||
- "append mismo dia conserva primera entrada"
|
||||
- "entrada sin cuerpo escribe header"
|
||||
test_file_path: "bash/functions/infra/append_diary_entry.sh"
|
||||
file_path: "bash/functions/infra/append_diary_entry.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/append_diary_entry.sh
|
||||
|
||||
# Entrada con titulo y cuerpo
|
||||
path=$(append_diary_entry "cerrado issue #42" "- Fix en http_client.cpp\n- Tests pasando")
|
||||
echo "Escrito en: $path"
|
||||
|
||||
# Entrada sin cuerpo (marcar momento)
|
||||
append_diary_entry "inicio sesion tarde"
|
||||
|
||||
# Usar directorio personalizado
|
||||
DIARY_DIR=/home/lucas/personal/diary append_diary_entry "nota personal" "Reunion a las 18h."
|
||||
```
|
||||
|
||||
## Formato del bloque escrito
|
||||
|
||||
```markdown
|
||||
# 2026-05-07
|
||||
|
||||
## 14:32 — cerrado issue #42
|
||||
|
||||
- Fix en http_client.cpp
|
||||
- Tests pasando
|
||||
|
||||
## 17:05 — inicio sesion tarde
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Respeta la variable de entorno `DIARY_DIR` para reusar fuera del registry.
|
||||
- `mkdir -p` garantiza que el directorio se crea si no existe.
|
||||
- El bloque siempre empieza con una linea en blanco para separar entradas anteriores.
|
||||
- Los tests viven en el mismo `.sh` bajo el guard `[ "$1" = "--test" ]`. Ejecutar con `bash bash/functions/infra/append_diary_entry.sh --test`.
|
||||
- `error_type: error_go_core` sigue la convencion del registry para funciones bash impuras aunque bash use exit codes nativos, no el tipo Go.
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# append_diary_entry — Añade una entrada al diario del dia en docs/diary/YYYY-MM-DD.md
|
||||
|
||||
append_diary_entry() {
|
||||
local titulo="$1"
|
||||
local cuerpo="${2:-}"
|
||||
|
||||
if [[ -z "$titulo" ]]; then
|
||||
echo "append_diary_entry: titulo requerido" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
local date time diary_dir diary_file
|
||||
date=$(date +%Y-%m-%d)
|
||||
time=$(date +%H:%M)
|
||||
diary_dir="${DIARY_DIR:-docs/diary}"
|
||||
diary_file="${diary_dir}/${date}.md"
|
||||
|
||||
# Crear directorio y cabecera del dia si el archivo no existe
|
||||
if [[ ! -f "$diary_file" ]]; then
|
||||
mkdir -p "$diary_dir"
|
||||
printf '# %s\n' "$date" > "$diary_file"
|
||||
fi
|
||||
|
||||
# Componer bloque de entrada
|
||||
if [[ -n "$cuerpo" ]]; then
|
||||
printf '\n## %s — %s\n\n%s\n' "$time" "$titulo" "$cuerpo" >> "$diary_file"
|
||||
else
|
||||
printf '\n## %s — %s\n' "$time" "$titulo" >> "$diary_file"
|
||||
fi
|
||||
|
||||
echo "$diary_file"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Self-test: bash bash/functions/infra/append_diary_entry.sh --test
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ] && [ "${1:-}" = "--test" ]; then
|
||||
set -euo pipefail
|
||||
|
||||
declare -i PASS=0 FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1" pattern="$2" file="$3"
|
||||
if grep -qF "$pattern" "$file"; then
|
||||
echo "PASS: $test_name"
|
||||
PASS+=1
|
||||
else
|
||||
echo "FAIL: $test_name — patron '$pattern' no encontrado en $file"
|
||||
FAIL+=1
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup: directorio temporal
|
||||
tmpdir=$(mktemp -d)
|
||||
export DIARY_DIR="$tmpdir/diary"
|
||||
|
||||
# Test 1: crear archivo nuevo con titulo y cuerpo
|
||||
diary_file=$(append_diary_entry "primer bloque" "Hecho algo importante hoy.")
|
||||
assert_contains "crear archivo nuevo con titulo y cuerpo" "primer bloque" "$diary_file"
|
||||
assert_contains "crear archivo nuevo con titulo y cuerpo" "# $(date +%Y-%m-%d)" "$diary_file"
|
||||
assert_contains "crear archivo nuevo con titulo y cuerpo" "Hecho algo importante hoy." "$diary_file"
|
||||
|
||||
# Test 2: append en el mismo dia sin reescribir contenido previo
|
||||
append_diary_entry "segundo bloque" "Otro parrafo de trabajo." > /dev/null
|
||||
assert_contains "append mismo dia conserva primera entrada" "primer bloque" "$diary_file"
|
||||
assert_contains "append mismo dia anade segunda entrada" "segundo bloque" "$diary_file"
|
||||
|
||||
# Test 3: entrada sin cuerpo (solo header)
|
||||
append_diary_entry "solo titulo sin cuerpo" "" > /dev/null
|
||||
assert_contains "entrada sin cuerpo escribe header" "solo titulo sin cuerpo" "$diary_file"
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$tmpdir"
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
fi
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: backup_sqlite_db
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "backup_sqlite_db <source> <dest>"
|
||||
description: "Snapshot atomico de una BD SQLite usando VACUUM INTO. Mas seguro que cp: no corrompe si hay escrituras concurrentes. Crea el directorio destino si no existe. Si dest ya existe, lo sobrescribe."
|
||||
tags: [backup, sqlite, vacuum, snapshot, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: source
|
||||
desc: "Ruta absoluta o relativa a la BD SQLite fuente. Debe existir y ser un archivo SQLite valido."
|
||||
- name: dest
|
||||
desc: "Ruta absoluta o relativa del snapshot destino. Se crea (o sobreescribe) con VACUUM INTO."
|
||||
output: "Imprime 'OK <bytes> bytes -> <dest>' a stdout en caso de exito. Los errores van a stderr."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/backup_sqlite_db.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/backup_sqlite_db.sh
|
||||
|
||||
# Backup de registry.db en directorio backups/
|
||||
backup_sqlite_db registry.db backups/registry_$(date +%Y%m%d_%H%M%S).db
|
||||
# OK 2457600 bytes -> backups/registry_20260507_103045.db
|
||||
|
||||
# Backup con rutas absolutas
|
||||
backup_sqlite_db /opt/apps/myapp/myapp.db /mnt/backups/myapp.db
|
||||
```
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Codigo | Significado |
|
||||
|--------|-------------|
|
||||
| 0 | Exito |
|
||||
| 1 | Source no existe |
|
||||
| 2 | Source no es SQLite valido |
|
||||
| 3 | Fallo en VACUUM INTO o creacion de directorio |
|
||||
| 4 | Dest tiene tamaño 0 tras el backup |
|
||||
| 5 | sqlite3 CLI no encontrado en PATH |
|
||||
|
||||
## Notas
|
||||
|
||||
`VACUUM INTO` (disponible desde SQLite 3.27.0) ejecuta un vacuum completo y escribe la BD resultado en un nuevo archivo. Es atomico: si falla a mitad, el destino no queda en estado inconsistente. Esto lo hace superior a un `cp` simple cuando la BD puede estar recibiendo escrituras concurrentes (WAL mode, journal). El archivo resultante es siempre una BD limpia y compactada.
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# backup_sqlite_db — Snapshot atomico de una BD SQLite usando VACUUM INTO.
|
||||
# Mas seguro que cp: no corrompe si hay escrituras concurrentes.
|
||||
|
||||
backup_sqlite_db() {
|
||||
local source="$1"
|
||||
local dest="$2"
|
||||
|
||||
# Verificar dependencia sqlite3
|
||||
if ! command -v sqlite3 &>/dev/null; then
|
||||
echo "backup_sqlite_db: sqlite3 no encontrado en PATH" >&2
|
||||
return 5
|
||||
fi
|
||||
|
||||
# Verificar que source existe
|
||||
if [[ ! -f "$source" ]]; then
|
||||
echo "backup_sqlite_db: source no existe: $source" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verificar que source es SQLite valido (header magico)
|
||||
local header
|
||||
header=$(head -c 16 "$source" 2>/dev/null | strings | head -n1)
|
||||
if [[ "$header" != "SQLite format 3" ]]; then
|
||||
echo "backup_sqlite_db: source no es una BD SQLite valida: $source" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Crear directorio destino si no existe
|
||||
local dest_dir
|
||||
dest_dir=$(dirname "$dest")
|
||||
if [[ ! -d "$dest_dir" ]]; then
|
||||
mkdir -p "$dest_dir" || {
|
||||
echo "backup_sqlite_db: no se pudo crear directorio: $dest_dir" >&2
|
||||
return 3
|
||||
}
|
||||
fi
|
||||
|
||||
# Si el destino existe, borrarlo para que VACUUM INTO no falle
|
||||
if [[ -f "$dest" ]]; then
|
||||
rm -f "$dest"
|
||||
fi
|
||||
|
||||
# Ejecutar VACUUM INTO (escape de comillas simples en el path)
|
||||
local escaped_dest="${dest//\'/\'\'}"
|
||||
if ! sqlite3 "$source" "VACUUM INTO '${escaped_dest}';" 2>/dev/null; then
|
||||
echo "backup_sqlite_db: fallo en VACUUM INTO: source=$source dest=$dest" >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
# Verificar que dest existe y tiene tamaño > 0
|
||||
if [[ ! -f "$dest" ]]; then
|
||||
echo "backup_sqlite_db: dest no fue creado: $dest" >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
local bytes
|
||||
bytes=$(wc -c < "$dest" 2>/dev/null)
|
||||
if [[ -z "$bytes" || "$bytes" -eq 0 ]]; then
|
||||
echo "backup_sqlite_db: dest tiene tamaño 0: $dest" >&2
|
||||
return 4
|
||||
fi
|
||||
|
||||
echo "OK ${bytes} bytes -> ${dest}"
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: deploy_cpp_exe_to_windows
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "deploy_cpp_exe_to_windows(app_name: string, app_dir: string) -> void"
|
||||
description: "Copia el .exe de Windows (compilado por build_cpp_windows) y sus assets al escritorio de Windows /mnt/c/Users/lucas/Desktop/apps/<APP>/. Mata el proceso si esta corriendo (taskkill.exe pre-autorizado), copia DLLs, sincroniza assets/ y enrichers/ con rsync, maneja runtime Python embebido si python_runtime: true en app.md, y copia extras gx-cli. Preserva siempre local_files/ (estado del usuario)."
|
||||
tags: [cpp, deploy, windows, exe, dll, assets, rsync]
|
||||
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/deploy_cpp_exe_to_windows.sh"
|
||||
params:
|
||||
- name: app_name
|
||||
desc: "Nombre de la app (ej: chart_demo). Se usa para localizar cpp/build/windows/apps/<app>/<app>.exe y el directorio destino Desktop/apps/<app>/."
|
||||
- name: app_dir
|
||||
desc: "Ruta absoluta al directorio fuente de la app (ej: /home/lucas/fn_registry/cpp/apps/chart_demo). Se usa para localizar enrichers/, runtime/ y app.md."
|
||||
output: "Copia archivos al escritorio de Windows. Imprime 'OK: <app> -> <dest>' en stdout. Si local_files/ existe, imprime su tamanio. Errores fatales a stderr con exit 1."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
deploy_cpp_exe_to_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_demo"
|
||||
# OK: chart_demo -> /mnt/c/Users/lucas/Desktop/apps/chart_demo
|
||||
|
||||
# Con rutas custom via env vars
|
||||
BUILD_WIN=/custom/build deploy_cpp_exe_to_windows "myapp" "/path/to/myapp"
|
||||
```
|
||||
|
||||
## Layout destino
|
||||
|
||||
```
|
||||
Desktop/apps/<APP>/
|
||||
├── <APP>.exe # binario (top level, convencion DLL Windows)
|
||||
├── *.dll # DLLs nativas junto al exe
|
||||
├── assets/
|
||||
│ ├── *.ttf # fuentes de add_imgui_app
|
||||
│ ├── enrichers/ # si <app_dir>/enrichers/ existe
|
||||
│ ├── runtime/ # Python embed si python_runtime: true en app.md
|
||||
│ ├── gx-cli # si existe en app_dir/
|
||||
│ └── gx-cli.exe # si existe en app_dir/
|
||||
└── local_files/ # NUNCA tocado (estado del usuario: DBs, settings, layouts)
|
||||
```
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
- `BUILD_WIN` — directorio de build Windows; default `$FN_REGISTRY_ROOT/cpp/build/windows`
|
||||
- `WIN_DESKTOP_APPS` — directorio destino; default `/mnt/c/Users/lucas/Desktop/apps`
|
||||
- `FN_REGISTRY_ROOT` — raiz del registry; default `/home/lucas/fn_registry`
|
||||
|
||||
## Notas
|
||||
|
||||
- `taskkill.exe /IM <app>.exe /F` pre-autorizado por el usuario (sin pedir confirmacion).
|
||||
- `rsync --delete` en assets/ y enrichers/ para mantener destino limpio.
|
||||
- Si `python_runtime: true` en `app.md` y `runtime/.lock` es mas antiguo que `app.md`, invoca `tools/freeze_python_runtime.sh` automaticamente.
|
||||
- `local_files/` jamas se toca: contiene estado per-PC del usuario (DBs SQLite, ImGui layouts, settings).
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy_cpp_exe_to_windows — Copia el .exe compilado (y DLLs, assets, enrichers, runtime)
|
||||
# desde cpp/build/windows/ al escritorio de Windows /mnt/c/Users/lucas/Desktop/apps/<APP>/.
|
||||
# Preserva local_files/ (estado del usuario). Pre-authorized: taskkill.exe /F para matar proceso.
|
||||
|
||||
deploy_cpp_exe_to_windows() {
|
||||
local app="${1:-}"
|
||||
local app_dir="${2:-}"
|
||||
|
||||
if [ -z "$app" ] || [ -z "$app_dir" ]; then
|
||||
echo "ERROR: uso: deploy_cpp_exe_to_windows <app_name> <app_dir>" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
|
||||
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
|
||||
|
||||
# --- 1. Verificar que el .exe existe ---
|
||||
local exe_src="$build_win/apps/$app/$app.exe"
|
||||
if [ ! -f "$exe_src" ]; then
|
||||
echo "ERROR: no se ha generado $exe_src" >&2
|
||||
echo "Compila primero con: build_cpp_windows $app" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- 2. Crear directorios destino ---
|
||||
local dest="$win_desktop_apps/$app"
|
||||
local assets="$dest/assets"
|
||||
mkdir -p "$dest" "$assets"
|
||||
|
||||
# --- 3. Pre-deploy: matar proceso si esta corriendo en Windows ---
|
||||
if command -v taskkill.exe >/dev/null 2>&1; then
|
||||
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# --- 4. Copiar .exe al top level ---
|
||||
cp -v "$exe_src" "$dest/"
|
||||
|
||||
# --- 5. DLLs al top level (Windows DLL search convention) ---
|
||||
find "$build_win/apps/$app" -maxdepth 1 -type f -name '*.dll' \
|
||||
-exec cp -v {} "$dest/" \;
|
||||
|
||||
# --- 6. assets/ del build (TTFs, etc.) -> dest/assets/ ---
|
||||
if [ -d "$build_win/apps/$app/assets" ]; then
|
||||
rsync -a --delete "$build_win/apps/$app/assets/" "$assets/"
|
||||
fi
|
||||
|
||||
# --- 7. enrichers/ del app_dir -> assets/enrichers/ ---
|
||||
if [ -d "$app_dir/enrichers" ]; then
|
||||
rsync -a --delete \
|
||||
--exclude '__pycache__' \
|
||||
--exclude '*.pyc' \
|
||||
"$app_dir/enrichers/" "$assets/enrichers/"
|
||||
fi
|
||||
|
||||
# --- 8. runtime/ Python embebido -> assets/runtime/ ---
|
||||
if grep -q '^python_runtime:[[:space:]]*true' "$app_dir/app.md" 2>/dev/null; then
|
||||
if [ ! -d "$app_dir/runtime/python" ] || \
|
||||
[ "$app_dir/app.md" -nt "$app_dir/runtime/.lock" ]; then
|
||||
echo "[freeze] regenerando runtime Python (Windows) para $app"
|
||||
"$app_dir/tools/freeze_python_runtime.sh" "$app_dir" windows
|
||||
fi
|
||||
rsync -a --delete \
|
||||
--exclude '__pycache__' \
|
||||
--exclude '*.pyc' \
|
||||
"$app_dir/runtime/" "$assets/runtime/"
|
||||
fi
|
||||
|
||||
# --- 9. Extras: gx-cli, gx-cli.exe -> assets/ ---
|
||||
for extra in gx-cli gx-cli.exe; do
|
||||
if [ -f "$app_dir/$extra" ]; then
|
||||
cp -v "$app_dir/$extra" "$assets/"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 10. NO tocar local_files/ (estado del usuario) ---
|
||||
echo ""
|
||||
echo "OK: $app -> $dest"
|
||||
if [ -d "$dest/local_files" ]; then
|
||||
echo " local_files/ preservado: $(du -sh "$dest/local_files" | cut -f1)"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
deploy_cpp_exe_to_windows "$@"
|
||||
fi
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: discover_git_repos
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "discover_git_repos(root_dir: string) -> stdout: newline-separated paths"
|
||||
description: "Encuentra todos los repos git dentro de root_dir. Devuelve paths absolutos (sin /.git) en stdout, uno por linea, ordenados. Incluye el propio root_dir si tiene .git. Excluye node_modules, .venv, cpp/vendor, cpp/build, sources, temp, subrepos e interior de .git."
|
||||
tags: [git, repo, discover, find, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: root_dir
|
||||
desc: "directorio raiz a escanear (absoluto o relativo); default '.'"
|
||||
output: "paths absolutos newline-separated por stdout, uno por linea, ordenados alfabeticamente; el propio root_dir aparece primero si tiene .git"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/discover_git_repos.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/discover_git_repos.sh
|
||||
|
||||
# Listar todos los repos bajo fn_registry
|
||||
discover_git_repos /home/lucas/fn_registry
|
||||
|
||||
# Contar repos
|
||||
discover_git_repos /home/lucas/fn_registry | wc -l
|
||||
|
||||
# Iterar
|
||||
while IFS= read -r repo; do
|
||||
echo "Repo: $repo"
|
||||
done < <(discover_git_repos /home/lucas/fn_registry)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `find` con `-mindepth 2` para subdirectorios (el root se comprueba por separado). Los resultados se emiten ordenados alfabeticamente. La funcion es impura porque lee el sistema de archivos. Sale con exit code 1 si `root_dir` no existe.
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# discover_git_repos — Encuentra todos los repos git dentro de root_dir.
|
||||
# Devuelve paths relativos (sin /.git) en stdout, uno por linea, ordenados.
|
||||
# Incluye el propio root_dir si tiene .git. Excluye node_modules, .venv,
|
||||
# cpp/vendor, cpp/build, sources, temp, subrepos e interior de .git.
|
||||
|
||||
discover_git_repos() {
|
||||
local root_dir="${1:-.}"
|
||||
|
||||
if [[ ! -d "$root_dir" ]]; then
|
||||
echo "discover_git_repos: directorio '$root_dir' no existe" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Normalizar: convertir a path absoluto para calculos consistentes
|
||||
local abs_root
|
||||
abs_root="$(cd "$root_dir" && pwd)"
|
||||
|
||||
# Incluir el propio root si tiene .git
|
||||
local results=()
|
||||
if [[ -d "$abs_root/.git" ]]; then
|
||||
results+=("$abs_root")
|
||||
fi
|
||||
|
||||
# Buscar .git/ en subdirectorios, con exclusiones
|
||||
while IFS= read -r git_dir; do
|
||||
local repo_dir="${git_dir%/.git}"
|
||||
results+=("$repo_dir")
|
||||
done < <(find "$abs_root" -mindepth 2 -name ".git" -type d \
|
||||
-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 | sort)
|
||||
|
||||
# Imprimir resultados ordenados (uno por linea)
|
||||
printf '%s\n' "${results[@]}" | sort
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
discover_git_repos "$@"
|
||||
fi
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: git_auto_commit_dirty
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "git_auto_commit_dirty(repo_dir: string, message?: string) -> stdout: commit subject or empty"
|
||||
description: "Si el repo tiene cambios sin commitear, hace git add -A y git commit. Genera mensaje automatico si no se pasa: detecta patron de dominio (python/functions/<dom>/, dev/issues/, functions/<dom>/, bash/functions/<dom>/) o usa chore: auto-commit con lista de paths. Anade Co-Authored-By Claude. Salta si el repo es ~/.password-store."
|
||||
tags: [git, commit, auto, infra, dirty]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: repo_dir
|
||||
desc: "path al repo git donde commitear; default '.'"
|
||||
- name: message
|
||||
desc: "mensaje de commit fijo (opcional); si se omite, se genera automaticamente segun los archivos cambiados"
|
||||
output: "subject del commit creado por stdout (primera linea del mensaje); vacio si no habia cambios o si el repo es password-store"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/git_auto_commit_dirty.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/git_auto_commit_dirty.sh
|
||||
|
||||
# Commitear con mensaje automatico
|
||||
subject=$(git_auto_commit_dirty /home/lucas/fn_registry)
|
||||
echo "Commit: $subject"
|
||||
|
||||
# Commitear con mensaje fijo
|
||||
git_auto_commit_dirty /home/lucas/myapp "feat: add new feature"
|
||||
|
||||
# Revisar si se creo commit (subject no vacio = commit creado)
|
||||
if [[ -n "$subject" ]]; then
|
||||
echo "Commit creado: $subject"
|
||||
else
|
||||
echo "Sin cambios"
|
||||
fi
|
||||
```
|
||||
|
||||
## Logica de mensaje automatico
|
||||
|
||||
| Patron | Mensaje generado |
|
||||
|---|---|
|
||||
| Todos los paths bajo `python/functions/<dom>/` | `feat(<dom>): auto-commit con N cambios` |
|
||||
| Todos los paths bajo `dev/issues/` | `chore(issues): auto-commit` |
|
||||
| Todos los paths bajo `functions/<dom>/` | `feat(<dom>): auto-commit con N cambios` |
|
||||
| Todos los paths bajo `bash/functions/<dom>/` | `feat(<dom>): auto-commit con N cambios` |
|
||||
| Paths dispersos | `chore: auto-commit (N archivos)` + lista |
|
||||
|
||||
## Notas
|
||||
|
||||
El Co-Authored-By se anade siempre como segundo `-m` del commit. El repo `~/.password-store` (o `$PASSWORD_STORE_DIR`) se salta silenciosamente — `pass` gestiona sus propios commits. Exit 1 solo si el repo no existe o si git commit falla.
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# git_auto_commit_dirty — Si el repo tiene cambios sin commitear, hace git add -A
|
||||
# y git commit con mensaje generado o el que se pase como argumento.
|
||||
# Stdout: subject del commit creado, vacio si no habia cambios.
|
||||
# Salta sin commitear si el repo es ~/.password-store (pass gestiona sus propios commits).
|
||||
|
||||
git_auto_commit_dirty() {
|
||||
local repo_dir="${1:-.}"
|
||||
local message="${2:-}"
|
||||
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
echo "git_auto_commit_dirty: '$repo_dir' no es un repo git" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Normalizar path
|
||||
local abs_repo
|
||||
abs_repo="$(cd "$repo_dir" && pwd)"
|
||||
|
||||
# Saltear ~/.password-store — pass gestiona sus propios commits
|
||||
local pass_dir
|
||||
pass_dir="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
|
||||
pass_dir="$(cd "$pass_dir" 2>/dev/null && pwd)" || true
|
||||
if [[ -n "$pass_dir" && "$abs_repo" == "$pass_dir"* ]]; then
|
||||
# No commitear — pass se autocommitea
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Comprobar si hay cambios
|
||||
local status
|
||||
status=$(git -C "$abs_repo" status --porcelain)
|
||||
if [[ -z "$status" ]]; then
|
||||
# Nada que commitear
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Generar mensaje automatico si no se proporciono
|
||||
if [[ -z "$message" ]]; then
|
||||
message="$(_git_auto_commit_dirty_generate_message "$abs_repo" "$status")"
|
||||
fi
|
||||
|
||||
# Commitear
|
||||
git -C "$abs_repo" add -A
|
||||
local commit_output
|
||||
commit_output=$(git -C "$abs_repo" commit \
|
||||
-m "$message" \
|
||||
-m "Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>" \
|
||||
2>&1)
|
||||
local exit_code=$?
|
||||
if [[ $exit_code -ne 0 ]]; then
|
||||
echo "git_auto_commit_dirty: fallo al commitear en '$abs_repo'" >&2
|
||||
echo "$commit_output" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Imprimir subject del commit
|
||||
echo "$message" | head -1
|
||||
}
|
||||
|
||||
# Funcion auxiliar: genera mensaje de commit automatico
|
||||
_git_auto_commit_dirty_generate_message() {
|
||||
local repo_dir="$1"
|
||||
local status="$2"
|
||||
|
||||
# Contar tipos de cambios
|
||||
local n_modified n_added n_deleted
|
||||
n_modified=$(echo "$status" | grep -c '^ M\| M\|MM\|AM\|CM\|RM' 2>/dev/null || true)
|
||||
n_added=$(echo "$status" | grep -c '^??\|^A ' 2>/dev/null || true)
|
||||
n_deleted=$(echo "$status" | grep -c '^ D\|^D ' 2>/dev/null || true)
|
||||
|
||||
# Extraer paths (sin el prefijo de status)
|
||||
local paths
|
||||
paths=$(echo "$status" | awk '{print $NF}')
|
||||
|
||||
# Detectar patron comun: todos bajo python/functions/<dom>/
|
||||
local py_dom
|
||||
py_dom=$(echo "$paths" | grep -oE '^python/functions/[^/]+' | sort -u)
|
||||
if [[ $(echo "$py_dom" | wc -l) -eq 1 && -n "$py_dom" ]]; then
|
||||
local dom
|
||||
dom=$(echo "$py_dom" | sed 's|python/functions/||')
|
||||
local n_total
|
||||
n_total=$(echo "$paths" | wc -l)
|
||||
echo "feat($dom): auto-commit con $n_total cambios"
|
||||
return
|
||||
fi
|
||||
|
||||
# Detectar patron: todos bajo dev/issues/
|
||||
if echo "$paths" | grep -q '^dev/issues/' && ! echo "$paths" | grep -qv '^dev/issues/'; then
|
||||
echo "chore(issues): auto-commit"
|
||||
return
|
||||
fi
|
||||
|
||||
# Detectar patron: todos bajo functions/<dom>/ (Go)
|
||||
local go_dom
|
||||
go_dom=$(echo "$paths" | grep -oE '^functions/[^/]+' | sort -u)
|
||||
if [[ $(echo "$go_dom" | wc -l) -eq 1 && -n "$go_dom" ]]; then
|
||||
local dom
|
||||
dom=$(echo "$go_dom" | sed 's|functions/||')
|
||||
local n_total
|
||||
n_total=$(echo "$paths" | wc -l)
|
||||
echo "feat($dom): auto-commit con $n_total cambios"
|
||||
return
|
||||
fi
|
||||
|
||||
# Detectar patron: todos bajo bash/functions/<dom>/
|
||||
local bash_dom
|
||||
bash_dom=$(echo "$paths" | grep -oE '^bash/functions/[^/]+' | sort -u)
|
||||
if [[ $(echo "$bash_dom" | wc -l) -eq 1 && -n "$bash_dom" ]]; then
|
||||
local dom
|
||||
dom=$(echo "$bash_dom" | sed 's|bash/functions/||')
|
||||
local n_total
|
||||
n_total=$(echo "$paths" | wc -l)
|
||||
echo "feat($dom): auto-commit con $n_total cambios"
|
||||
return
|
||||
fi
|
||||
|
||||
# Caso general: cambios dispersos
|
||||
local n_total
|
||||
n_total=$(echo "$paths" | wc -l)
|
||||
local subject="chore: auto-commit ($n_total archivos)"
|
||||
|
||||
# Listar hasta 10 paths en el body (se anade como segundo -m)
|
||||
local body
|
||||
body=$(echo "$paths" | head -10 | awk '{print "- "$0}')
|
||||
if [[ $(echo "$paths" | wc -l) -gt 10 ]]; then
|
||||
body="$body"$'\n'"- ..."
|
||||
fi
|
||||
|
||||
echo "$subject"$'\n\n'"$body"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
git_auto_commit_dirty "$@"
|
||||
fi
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: git_pull_with_stash
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "git_pull_with_stash(repo_dir: string) -> stdout: status"
|
||||
description: "Si el repo tiene cambios pendientes, los stashea antes de pullear. Hace fetch origin + pull --ff-only. Si hay divergencia reporta [diverged] y restaura el stash. Si stash pop da conflicto reporta [stash-conflict] sin tocarlo. Exit 0 siempre para que el caller pueda continuar con otros repos."
|
||||
tags: [git, pull, stash, infra, sync]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: repo_dir
|
||||
desc: "path al repo git donde pullear; default '.'"
|
||||
output: "linea de estado por stdout: '[pulled] repo', '[up-to-date] repo', '[diverged] repo' o '[stash-conflict] repo'"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/git_pull_with_stash.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/git_pull_with_stash.sh
|
||||
|
||||
# Pullear repo con auto-stash
|
||||
status=$(git_pull_with_stash /home/lucas/fn_registry)
|
||||
echo "$status"
|
||||
# [pulled] fn_registry
|
||||
# o:
|
||||
# [up-to-date] fn_registry
|
||||
# o:
|
||||
# [diverged] fn_registry (pull fallo por divergencia)
|
||||
|
||||
# Iterar y coleccionar divergencias
|
||||
diverged=()
|
||||
while IFS= read -r repo; do
|
||||
result=$(git_pull_with_stash "$repo")
|
||||
echo "$result"
|
||||
if [[ "$result" == "[diverged]"* || "$result" == "[stash-conflict]"* ]]; then
|
||||
diverged+=("$result")
|
||||
fi
|
||||
done < <(discover_git_repos /home/lucas/fn_registry)
|
||||
|
||||
if [[ ${#diverged[@]} -gt 0 ]]; then
|
||||
echo "ATENCION: repos que requieren intervencion manual:"
|
||||
printf ' %s\n' "${diverged[@]}"
|
||||
fi
|
||||
```
|
||||
|
||||
## Estados de salida
|
||||
|
||||
| Linea stdout | Significado |
|
||||
|---|---|
|
||||
| `[pulled] repo` | Se trajo commits nuevos correctamente |
|
||||
| `[up-to-date] repo` | Ya estaba al dia (o sin remote) |
|
||||
| `[diverged] repo` | Pull --ff-only fallo — requiere rebase/merge manual |
|
||||
| `[stash-conflict] repo` | Pull ok pero stash pop tuvo conflictos — requiere resolucion manual |
|
||||
|
||||
## Notas
|
||||
|
||||
Solo hace pull fast-forward — nunca rebase ni merge automatico. El stash incluye untracked (`--include-untracked`) para no perder archivos nuevos no trackeados. Exit 1 solo si `repo_dir` no es un repo git. Todos los demas casos (divergencia, conflictos, sin remote) retornan exit 0 con linea descriptiva.
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# git_pull_with_stash — Si el repo tiene cambios, los stashea antes de pullear.
|
||||
# Hace fetch + pull --ff-only. Si hay divergencia, restaura el stash y reporta.
|
||||
# Exit 0 siempre (el caller decide como manejar los errores).
|
||||
|
||||
git_pull_with_stash() {
|
||||
local repo_dir="${1:-.}"
|
||||
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
echo "git_pull_with_stash: '$repo_dir' no es un repo git" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local abs_repo
|
||||
abs_repo="$(cd "$repo_dir" && pwd)"
|
||||
|
||||
local repo_name
|
||||
repo_name="$(basename "$abs_repo")"
|
||||
|
||||
# Comprobar si hay cambios pendientes
|
||||
local dirty
|
||||
dirty=$(git -C "$abs_repo" status --porcelain | wc -l)
|
||||
local stashed=0
|
||||
|
||||
if [[ "$dirty" -gt 0 ]]; then
|
||||
git -C "$abs_repo" stash push \
|
||||
--include-untracked \
|
||||
-m "auto-stash before pull" \
|
||||
>/dev/null 2>&1 || true
|
||||
stashed=1
|
||||
fi
|
||||
|
||||
# Fetch origin
|
||||
git -C "$abs_repo" fetch origin >/dev/null 2>&1 || {
|
||||
# Sin remote configurado o sin red — considerar up-to-date
|
||||
if [[ "$stashed" -eq 1 ]]; then
|
||||
git -C "$abs_repo" stash pop >/dev/null 2>&1 || true
|
||||
fi
|
||||
echo "[up-to-date] $repo_name (no remote)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Pull --ff-only
|
||||
local pull_out
|
||||
pull_out=$(git -C "$abs_repo" pull --ff-only 2>&1)
|
||||
local pull_exit=$?
|
||||
|
||||
if [[ $pull_exit -ne 0 ]]; then
|
||||
# Divergencia — restaurar stash y reportar
|
||||
if [[ "$stashed" -eq 1 ]]; then
|
||||
git -C "$abs_repo" stash pop >/dev/null 2>&1 || true
|
||||
fi
|
||||
echo "[diverged] $repo_name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Pull exitoso — restaurar stash si habia
|
||||
if [[ "$stashed" -eq 1 ]]; then
|
||||
local pop_out
|
||||
pop_out=$(git -C "$abs_repo" stash pop 2>&1)
|
||||
local pop_exit=$?
|
||||
if [[ $pop_exit -ne 0 ]]; then
|
||||
echo "[stash-conflict] $repo_name"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determinar si se trajo algo nuevo
|
||||
if echo "$pull_out" | grep -q "Already up to date"; then
|
||||
echo "[up-to-date] $repo_name"
|
||||
else
|
||||
local commits
|
||||
commits=$(echo "$pull_out" | grep -c 'Fast-forward\|commit\|Merge' || true)
|
||||
echo "[pulled] $repo_name"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
git_pull_with_stash "$@"
|
||||
fi
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: git_push_if_ahead
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "git_push_if_ahead(repo_dir: string) -> stdout: status line"
|
||||
description: "Decide si pushear un repo git usando solo refs locales (sin tocar la red para decidir). Sin upstream hace push -u; con upstream y ahead > 0 pushea; con 0 ahead salta. Si el push falla no aborta — emite [error] y exit 0."
|
||||
tags: [git, push, infra, ahead, upstream]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: repo_dir
|
||||
desc: "path al repo git a evaluar; default '.'"
|
||||
output: "linea de estado por stdout: '[push -u] repo (branch)', '[push] repo (branch, N commits ahead)', '[skip] repo (up-to-date)' o '[error] repo: razon'"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/git_push_if_ahead.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/git_push_if_ahead.sh
|
||||
|
||||
# Pushear si hay commits locales
|
||||
status=$(git_push_if_ahead /home/lucas/fn_registry)
|
||||
echo "$status"
|
||||
# [push] fn_registry (master, 3 commits ahead)
|
||||
# o:
|
||||
# [skip] fn_registry (up-to-date)
|
||||
|
||||
# Iterar sobre multiples repos
|
||||
while IFS= read -r repo; do
|
||||
git_push_if_ahead "$repo"
|
||||
done < <(discover_git_repos /home/lucas/fn_registry)
|
||||
```
|
||||
|
||||
## Estados de salida
|
||||
|
||||
| Linea stdout | Significado |
|
||||
|---|---|
|
||||
| `[push -u] repo (branch)` | Sin upstream — se hizo push -u origin |
|
||||
| `[push] repo (branch, N ahead)` | Tenia commits locales — se pusheo |
|
||||
| `[skip] repo (up-to-date)` | 0 ahead localmente — no se toco la red |
|
||||
| `[error] repo: razon` | Push rechazo (non-fast-forward, etc.) — se reporta pero exit 0 |
|
||||
|
||||
## Notas
|
||||
|
||||
`rev-list --count @{u}..HEAD` solo lee refs locales, no hace fetch. Esto es correcto para un push-only workflow: si en otro PC se hizo push, aqui no tenemos nada local que pushear de todas formas. El error `[error]` tipicamente indica que la rama remote esta adelante — el caller debe sugerir `/full-git-pull`.
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# git_push_if_ahead — Decide si pushear un repo git sin tocar la red para decidir.
|
||||
# Usa refs locales (rev-list) para determinar si hay commits por enviar.
|
||||
# Stdout: linea de estado con [push|push -u|skip|error].
|
||||
|
||||
git_push_if_ahead() {
|
||||
local repo_dir="${1:-.}"
|
||||
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
echo "git_push_if_ahead: '$repo_dir' no es un repo git" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local abs_repo
|
||||
abs_repo="$(cd "$repo_dir" && pwd)"
|
||||
|
||||
# Nombre corto para display (basename del path)
|
||||
local repo_name
|
||||
repo_name="$(basename "$abs_repo")"
|
||||
|
||||
# Rama actual
|
||||
local branch
|
||||
branch=$(git -C "$abs_repo" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
|
||||
echo "[error] $repo_name: no se puede determinar la rama actual"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Comprobar si existe upstream configurado
|
||||
local upstream
|
||||
upstream=$(git -C "$abs_repo" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$upstream" ]]; then
|
||||
# Sin upstream: push -u para establecer tracking
|
||||
echo "[push -u] $repo_name ($branch)" >&2
|
||||
local push_out
|
||||
push_out=$(git -C "$abs_repo" push -u origin "$branch" 2>&1) || {
|
||||
echo "[error] $repo_name: $push_out"
|
||||
return 0
|
||||
}
|
||||
echo "$push_out" | tail -3 >&2
|
||||
echo "[push -u] $repo_name ($branch)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Contar commits locales que no estan en upstream (sin red)
|
||||
local ahead
|
||||
ahead=$(git -C "$abs_repo" rev-list --count '@{u}..HEAD' 2>/dev/null || echo "0")
|
||||
|
||||
if [[ "${ahead:-0}" -gt 0 ]]; then
|
||||
echo "[push] $repo_name ($branch, $ahead commits ahead)" >&2
|
||||
local push_out
|
||||
push_out=$(git -C "$abs_repo" push origin "$branch" 2>&1) || {
|
||||
echo "[error] $repo_name: $(echo "$push_out" | tail -1)"
|
||||
return 0
|
||||
}
|
||||
echo "$push_out" | tail -3 >&2
|
||||
echo "[push] $repo_name ($branch, $ahead commits ahead)"
|
||||
else
|
||||
echo "[skip] $repo_name (up-to-date)"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
git_push_if_ahead "$@"
|
||||
fi
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: port_kill
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "port_kill(port: int, signal: string) -> void"
|
||||
description: "Mata los procesos que escuchan en un puerto TCP dado. Idempotente: si no hay proceso en el puerto retorna exit 0. Hace un segundo intento con SIGKILL si el primer intento con signal no libera el puerto."
|
||||
tags: ["port", "kill", "process", "tcp"]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: port
|
||||
desc: "Numero de puerto TCP (1-65535) cuyos procesos en estado LISTEN seran terminados."
|
||||
- name: signal
|
||||
desc: "Signal opcional para kill. Default TERM. Acepta KILL, INT, TERM, HUP o numerico (9, 15, ...). Si el proceso sobrevive, se reintenta automaticamente con KILL."
|
||||
output: "Imprime a stdout una linea por cada PID matado (KILLED pid=X signal=Y port=Z) o NO_PROCESS si el puerto ya estaba libre. Errores a stderr. Exit codes: 0=OK, 2=puerto invalido, 3=puerto sigue ocupado tras SIGKILL, 4=permiso denegado, 5=ni lsof ni fuser disponibles."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/port_kill.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/port_kill.sh
|
||||
|
||||
# Matar proceso en puerto 8080 con SIGTERM (default)
|
||||
port_kill 8080
|
||||
|
||||
# Matar proceso en puerto 3000 con SIGKILL directo
|
||||
port_kill 3000 KILL
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Idempotente: si ningun proceso escucha en el puerto, imprime `NO_PROCESS port=<port>` y retorna 0 sin error.
|
||||
|
||||
Preferencia de herramienta: usa `lsof -ti tcp:<port> -sTCP:LISTEN` si esta disponible; fallback a `fuser -n tcp <port>`. Si ninguno esta instalado, retorna exit 5.
|
||||
|
||||
Logica de reintento: tras enviar la signal inicial espera 2 segundos. Si el puerto sigue ocupado y la signal no era KILL/9, realiza un segundo intento con SIGKILL. Si tras ese segundo intento el puerto sigue bloqueado, retorna exit 3.
|
||||
|
||||
Permisos: si los PIDs pertenecen a root y el invocador no tiene privilegios, `kill` fallara y se reporta `PERMISSION_DENIED pid=<pid>` a stderr con exit 4. Ejecutar con sudo si es necesario.
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# port_kill — Mata los procesos que escuchan en un puerto TCP dado.
|
||||
|
||||
port_kill() {
|
||||
local port="${1:-}"
|
||||
local signal="${2:-TERM}"
|
||||
|
||||
# Validar puerto
|
||||
if [[ -z "$port" ]] || ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
|
||||
echo "ERROR: puerto invalido: '$port' (debe ser 1-65535)" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Verificar herramienta disponible
|
||||
local tool=""
|
||||
if command -v lsof &>/dev/null; then
|
||||
tool="lsof"
|
||||
elif command -v fuser &>/dev/null; then
|
||||
tool="fuser"
|
||||
else
|
||||
echo "ERROR: se requiere lsof o fuser (ninguno disponible)" >&2
|
||||
return 5
|
||||
fi
|
||||
|
||||
# Obtener PIDs
|
||||
local pids=()
|
||||
if [[ "$tool" == "lsof" ]]; then
|
||||
mapfile -t pids < <(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null)
|
||||
else
|
||||
mapfile -t pids < <(fuser -n tcp "${port}" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$')
|
||||
fi
|
||||
|
||||
if (( ${#pids[@]} == 0 )); then
|
||||
echo "NO_PROCESS port=${port}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Matar cada PID
|
||||
local permission_denied=0
|
||||
for pid in "${pids[@]}"; do
|
||||
[[ -z "$pid" ]] && continue
|
||||
if kill "-${signal}" "$pid" 2>/dev/null; then
|
||||
echo "KILLED pid=${pid} signal=${signal} port=${port}"
|
||||
else
|
||||
echo "PERMISSION_DENIED pid=${pid}" >&2
|
||||
permission_denied=1
|
||||
fi
|
||||
done
|
||||
|
||||
if (( permission_denied )); then
|
||||
return 4
|
||||
fi
|
||||
|
||||
# Esperar 2s y verificar
|
||||
sleep 2
|
||||
local remaining=()
|
||||
if [[ "$tool" == "lsof" ]]; then
|
||||
mapfile -t remaining < <(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null)
|
||||
else
|
||||
mapfile -t remaining < <(fuser -n tcp "${port}" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$')
|
||||
fi
|
||||
|
||||
if (( ${#remaining[@]} == 0 )); then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Segundo intento con KILL si signal != KILL
|
||||
if [[ "$signal" != "KILL" && "$signal" != "9" ]]; then
|
||||
for pid in "${remaining[@]}"; do
|
||||
[[ -z "$pid" ]] && continue
|
||||
kill -KILL "$pid" 2>/dev/null && echo "KILLED pid=${pid} signal=KILL port=${port}"
|
||||
done
|
||||
sleep 1
|
||||
local still=()
|
||||
if [[ "$tool" == "lsof" ]]; then
|
||||
mapfile -t still < <(lsof -ti "tcp:${port}" -sTCP:LISTEN 2>/dev/null)
|
||||
else
|
||||
mapfile -t still < <(fuser -n tcp "${port}" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$')
|
||||
fi
|
||||
if (( ${#still[@]} > 0 )); then
|
||||
echo "ERROR: puerto ${port} sigue ocupado tras SIGKILL" >&2
|
||||
return 3
|
||||
fi
|
||||
else
|
||||
echo "ERROR: puerto ${port} sigue ocupado tras SIGKILL" >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: pre_commit_hook_install
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "pre_commit_hook_install(repo_dir: string, [--force]) -> void"
|
||||
description: "Instala un hook pre-commit en .git/hooks/pre-commit de un repo dado. El hook invoca scan_secrets_in_dirty para abortar el commit si detecta secrets en archivos staged. Idempotente: si el hook ya esta instalado (marca fn_registry-pre-commit-v1) no lo sobreescribe a menos que se pase --force."
|
||||
tags: ["git", "hook", "precommit", "secrets"]
|
||||
uses_functions: ["scan_secrets_in_dirty_bash_cybersecurity"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: repo_dir
|
||||
desc: "Ruta al directorio raiz del repo Git (debe contener .git/hooks/)."
|
||||
- name: --force
|
||||
desc: "Flag opcional. Si se pasa, sobreescribe el hook aunque ya exista (hace backup con timestamp)."
|
||||
output: "Imprime INSTALLED <path> o SKIP <path> (already installed). Retorna exit code 1 si repo_dir no es un repo git valido, 2 si el hook existe y no es nuestro sin --force."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/pre_commit_hook_install.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/pre_commit_hook_install.sh
|
||||
|
||||
# Instalar en el repo actual
|
||||
pre_commit_hook_install /home/lucas/fn_registry
|
||||
# INSTALLED /home/lucas/fn_registry/.git/hooks/pre-commit
|
||||
|
||||
# Idempotente: segunda llamada no sobreescribe
|
||||
pre_commit_hook_install /home/lucas/fn_registry
|
||||
# SKIP /home/lucas/fn_registry/.git/hooks/pre-commit (already installed)
|
||||
|
||||
# Forzar reinstalacion (hace backup del hook anterior)
|
||||
pre_commit_hook_install /home/lucas/fn_registry --force
|
||||
# INSTALLED /home/lucas/fn_registry/.git/hooks/pre-commit
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Idempotente por diseno: la marca `# fn_registry-pre-commit-v1` en el hook generado sirve como identificador. Si el hook existe y tiene la marca, la funcion no sobreescribe a menos que se pase `--force`.
|
||||
|
||||
Si el hook existe pero NO tiene la marca (es un hook ajeno), la funcion falla con exit 2 y un mensaje de error claro. Con `--force`, se hace backup a `pre-commit.bak.<timestamp>` antes de reescribir.
|
||||
|
||||
El hook generado localiza `fn_registry` en dos pasos:
|
||||
1. La variable de entorno `FN_REGISTRY_ROOT` si esta definida.
|
||||
2. Si el repo donde se hace commit tiene `registry.db` en la raiz, asume que el propio repo es `fn_registry`.
|
||||
|
||||
Si no puede localizar `fn_registry`, el hook imprime un aviso y sale con exit 0 (no bloquea el commit). Esto permite instalar el hook en repos externos al registry sin romper su flujo.
|
||||
|
||||
Configurar `FN_REGISTRY_ROOT` en el perfil del shell para garantizar que el hook siempre encuentre el registry:
|
||||
```bash
|
||||
export FN_REGISTRY_ROOT=/home/lucas/fn_registry
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# pre_commit_hook_install — instala hook pre-commit que invoca scan_secrets_in_dirty
|
||||
|
||||
pre_commit_hook_install() {
|
||||
local repo_dir="$1"
|
||||
local force=0
|
||||
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--force) force=1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
local hooks_dir="$repo_dir/.git/hooks"
|
||||
local hook_path="$hooks_dir/pre-commit"
|
||||
local marker="# fn_registry-pre-commit-v1"
|
||||
|
||||
if [[ ! -d "$hooks_dir" ]]; then
|
||||
echo "[pre_commit_hook_install] ERROR: '$repo_dir' no es un repo git valido (falta .git/hooks)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -f "$hook_path" ]]; then
|
||||
if grep -qF "$marker" "$hook_path"; then
|
||||
if [[ $force -eq 0 ]]; then
|
||||
echo "SKIP $hook_path (already installed)"
|
||||
return 0
|
||||
else
|
||||
local backup="$hook_path.bak.$(date +%s)"
|
||||
cp "$hook_path" "$backup"
|
||||
echo "[pre_commit_hook_install] Backup: $backup" >&2
|
||||
fi
|
||||
else
|
||||
if [[ $force -eq 0 ]]; then
|
||||
echo "[pre_commit_hook_install] ERROR: '$hook_path' existe y no es nuestro. Usa --force para sobreescribir." >&2
|
||||
return 2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
cat > "$hook_path" <<'HOOK'
|
||||
#!/usr/bin/env bash
|
||||
# fn_registry-pre-commit-v1
|
||||
set -e
|
||||
|
||||
# Localizar fn_registry root: env var FN_REGISTRY_ROOT o asumir mismo repo si tiene registry.db en raiz
|
||||
REGISTRY_ROOT="${FN_REGISTRY_ROOT:-}"
|
||||
if [ -z "$REGISTRY_ROOT" ]; then
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
if [ -f "$REPO_ROOT/registry.db" ]; then
|
||||
REGISTRY_ROOT="$REPO_ROOT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$REGISTRY_ROOT" ] || [ ! -f "$REGISTRY_ROOT/bash/functions/cybersecurity/scan_secrets_in_dirty.sh" ]; then
|
||||
echo "[pre-commit] fn_registry no localizable; saltando scan de secrets" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ejecutar scan en repo actual (cwd)
|
||||
bash "$REGISTRY_ROOT/bash/functions/cybersecurity/scan_secrets_in_dirty.sh" "$(git rev-parse --show-toplevel)"
|
||||
HOOK
|
||||
|
||||
chmod +x "$hook_path"
|
||||
echo "INSTALLED $hook_path"
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: resolve_cpp_app_dir
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "resolve_cpp_app_dir(app_name?: string) -> stdout: app_name\tapp_dir"
|
||||
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en ambas ubicaciones. Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
|
||||
tags: [cpp, resolve, app, directory, infra]
|
||||
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/resolve_cpp_app_dir.sh"
|
||||
params:
|
||||
- name: app_name
|
||||
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de cpp/apps/<X>/ o projects/*/apps/<X>/."
|
||||
output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En caso de error imprime ayuda a stderr y sale con exit 1."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Desde dentro de cpp/apps/chart_demo/
|
||||
cd /home/lucas/fn_registry/cpp/apps/chart_demo
|
||||
resolve_cpp_app_dir
|
||||
# -> chart_demo\t/home/lucas/fn_registry/cpp/apps/chart_demo
|
||||
|
||||
# Con argumento explicito
|
||||
resolve_cpp_app_dir registry_dashboard
|
||||
# -> registry_dashboard\t/home/lucas/fn_registry/cpp/apps/registry_dashboard
|
||||
|
||||
# Capturar los dos campos
|
||||
resolved=$(resolve_cpp_app_dir graph_explorer)
|
||||
APP="$(echo "$resolved" | cut -f1)"
|
||||
APP_DIR="$(echo "$resolved" | cut -f2)"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Busca en orden: primero `$ROOT/cpp/apps/<X>`, luego `$ROOT/projects/*/apps/<X>` (primer match gana). Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente.
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# resolve_cpp_app_dir — Resuelve nombre y directorio absoluto de una app C++ del registry.
|
||||
# Sin arg: deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/.
|
||||
# Con arg: usa el nombre directamente y busca en ambas ubicaciones.
|
||||
# Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0.
|
||||
# Error: lista apps disponibles en stderr + exit 1.
|
||||
|
||||
resolve_cpp_app_dir() {
|
||||
local app_arg="${1:-}"
|
||||
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
|
||||
# --- Deducir desde CWD si no hay argumento ---
|
||||
if [ -z "$app_arg" ]; then
|
||||
local cwd
|
||||
cwd="$(pwd)"
|
||||
case "$cwd" in
|
||||
"$root"/cpp/apps/*/|"$root"/cpp/apps/*)
|
||||
# Extraer primer segmento tras cpp/apps/
|
||||
local rel="${cwd#"$root/cpp/apps/"}"
|
||||
app_arg="${rel%%/*}"
|
||||
;;
|
||||
"$root"/projects/*/apps/*/|"$root"/projects/*/apps/*)
|
||||
# Extraer primer segmento tras la ultima /apps/
|
||||
local rel="${cwd#"$root/projects/"}"
|
||||
rel="${rel#*/apps/}"
|
||||
app_arg="${rel%%/*}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- Aun vacio: listar y abortar ---
|
||||
if [ -z "$app_arg" ]; then
|
||||
echo "ERROR: no se pudo deducir la app desde el directorio actual." >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
{
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
} >&2
|
||||
echo "" >&2
|
||||
echo "Uso: resolve_cpp_app_dir <app_name>" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- Buscar directorio real ---
|
||||
local app_dir=""
|
||||
|
||||
# Primero: cpp/apps/<X>
|
||||
if [ -d "$root/cpp/apps/$app_arg" ]; then
|
||||
app_dir="$root/cpp/apps/$app_arg"
|
||||
fi
|
||||
|
||||
# Segundo: projects/*/apps/<X> (primer match)
|
||||
if [ -z "$app_dir" ]; then
|
||||
for cand in "$root"/projects/*/apps/"$app_arg"; do
|
||||
if [ -d "$cand" ]; then
|
||||
app_dir="$cand"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "$app_dir" ]; then
|
||||
echo "ERROR: no se encuentra app '$app_arg' en cpp/apps/ ni en projects/*/apps/" >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
{
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
} >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\t%s\n' "$app_arg" "$app_dir"
|
||||
}
|
||||
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
resolve_cpp_app_dir "$@"
|
||||
fi
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: rotate_backups
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "rotate_backups(dir: string, new_file: string, daily: int, weekly: int, monthly: int) -> string"
|
||||
description: "Aplica retention policy estilo rsnapshot (daily/weekly/monthly) sobre un directorio de backups. Mueve el backup recien creado a daily.0, desplaza los anteriores y promueve a weekly/monthly al fin de periodo."
|
||||
tags: ["backup", "rotate", "retention"]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: dir
|
||||
desc: "Directorio donde viven los backups. Se crea si no existe."
|
||||
- name: new_file
|
||||
desc: "Ruta al backup recien creado. Se mueve dentro de dir como daily.0."
|
||||
- name: daily
|
||||
desc: "Numero de slots diarios a conservar. Por defecto 7."
|
||||
- name: weekly
|
||||
desc: "Numero de slots semanales a conservar (promovidos cada domingo). Por defecto 4."
|
||||
- name: monthly
|
||||
desc: "Numero de slots mensuales a conservar (promovidos el dia 1 de cada mes). Por defecto 12."
|
||||
output: "Linea ROTATED daily=<N> weekly=<N> monthly=<N> dir=<dir> en stdout con el conteo de slots ocupados tras la rotacion. Exit code != 0 en error (1: new_file no existe, 2: dir no se puede crear, 3: argumento numerico invalido)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/rotate_backups.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/rotate_backups.sh
|
||||
|
||||
rotate_backups ~/backups/registry /tmp/registry-snap.db 7 4 12
|
||||
# ROTATED daily=1 weekly=0 monthly=0 dir=/root/backups/registry
|
||||
|
||||
# Con key=value
|
||||
rotate_backups ~/backups/pg /tmp/pg-dump.sql daily=7 weekly=4 monthly=12
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Estilo rsnapshot: los slots se numeran desde 0 (mas reciente). El algoritmo aplica rotaciones en orden inverso para no sobreescribir:
|
||||
|
||||
1. **Monthly** (dia 1): copia `weekly.<weekly-1>` → `monthly.0` antes de desplazar, borra el mas viejo.
|
||||
2. **Weekly** (domingo): copia `daily.<daily-1>` → `weekly.0` antes de desplazar, borra el mas viejo.
|
||||
3. **Daily** (siempre): borra `daily.<daily-1>`, desplaza daily.i → daily.i+1, mueve new_file → daily.0.
|
||||
|
||||
La promocion weekly y monthly se ejecuta ANTES de la rotacion daily para que `daily.<daily-1>` y `weekly.<weekly-1>` aun existan cuando se necesitan. Sin dependencias externas — solo `mv`, `rm`, `cp`, `mkdir`, `date`.
|
||||
|
||||
Los slots pueden ser archivos o directorios (caller decide el formato del backup).
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# rotate_backups — retention policy estilo rsnapshot (daily/weekly/monthly)
|
||||
|
||||
rotate_backups() {
|
||||
local dir="$1"
|
||||
local new_file="$2"
|
||||
local daily=7
|
||||
local weekly=4
|
||||
local monthly=12
|
||||
|
||||
# Parsear args posicionales o key=value
|
||||
local i=3
|
||||
for arg in "${@:3}"; do
|
||||
case "$arg" in
|
||||
daily=*) daily="${arg#daily=}" ;;
|
||||
weekly=*) weekly="${arg#weekly=}" ;;
|
||||
monthly=*) monthly="${arg#monthly=}" ;;
|
||||
*)
|
||||
case $i in
|
||||
3) daily="$arg" ;;
|
||||
4) weekly="$arg" ;;
|
||||
5) monthly="$arg" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
((i++))
|
||||
done
|
||||
|
||||
# Validar numericos
|
||||
if ! [[ "$daily" =~ ^[0-9]+$ ]] || ! [[ "$weekly" =~ ^[0-9]+$ ]] || ! [[ "$monthly" =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: daily, weekly y monthly deben ser enteros positivos" >&2
|
||||
return 3
|
||||
fi
|
||||
|
||||
# Validar new_file
|
||||
if [[ ! -e "$new_file" ]]; then
|
||||
echo "ERROR: new_file no existe: $new_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Crear directorio si no existe
|
||||
if ! mkdir -p "$dir" 2>/dev/null; then
|
||||
echo "ERROR: no se pudo crear el directorio: $dir" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
local dow
|
||||
dow=$(date +%w) # 0=domingo
|
||||
local dom
|
||||
dom=$(date +%d) # 01-31
|
||||
|
||||
local rotated_daily=0
|
||||
local rotated_weekly=0
|
||||
local rotated_monthly=0
|
||||
|
||||
# --- Monthly: si dia 1, rotar desde weekly.<weekly-1> ANTES de borrar ---
|
||||
if [[ "$dom" == "01" ]] && (( monthly > 0 )); then
|
||||
local weekly_src="$dir/weekly.$((weekly - 1))"
|
||||
if [[ -e "$weekly_src" ]]; then
|
||||
# Borrar monthly mas viejo
|
||||
local oldest_monthly="$dir/monthly.$((monthly - 1))"
|
||||
[[ -e "$oldest_monthly" ]] && rm -rf "$oldest_monthly"
|
||||
# Desplazar monthly.<i> → monthly.<i+1>
|
||||
for (( j = monthly - 2; j >= 0; j-- )); do
|
||||
[[ -e "$dir/monthly.$j" ]] && mv "$dir/monthly.$j" "$dir/monthly.$((j+1))"
|
||||
done
|
||||
cp -a "$weekly_src" "$dir/monthly.0"
|
||||
rotated_monthly=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Weekly: si domingo, rotar desde daily.<daily-1> ANTES de borrarlo ---
|
||||
if [[ "$dow" == "0" ]] && (( weekly > 0 )); then
|
||||
local daily_src="$dir/daily.$((daily - 1))"
|
||||
if [[ -e "$daily_src" ]]; then
|
||||
# Borrar weekly mas viejo
|
||||
local oldest_weekly="$dir/weekly.$((weekly - 1))"
|
||||
[[ -e "$oldest_weekly" ]] && rm -rf "$oldest_weekly"
|
||||
# Desplazar weekly.<i> → weekly.<i+1>
|
||||
for (( j = weekly - 2; j >= 0; j-- )); do
|
||||
[[ -e "$dir/weekly.$j" ]] && mv "$dir/weekly.$j" "$dir/weekly.$((j+1))"
|
||||
done
|
||||
cp -a "$daily_src" "$dir/weekly.0"
|
||||
rotated_weekly=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Daily: borrar el mas viejo, desplazar, mover new_file a daily.0 ---
|
||||
local oldest_daily="$dir/daily.$((daily - 1))"
|
||||
[[ -e "$oldest_daily" ]] && rm -rf "$oldest_daily"
|
||||
|
||||
for (( j = daily - 2; j >= 0; j-- )); do
|
||||
[[ -e "$dir/daily.$j" ]] && mv "$dir/daily.$j" "$dir/daily.$((j+1))"
|
||||
done
|
||||
|
||||
mv "$new_file" "$dir/daily.0"
|
||||
rotated_daily=1
|
||||
|
||||
# Contar slots ocupados para el reporte
|
||||
local cnt_daily=0 cnt_weekly=0 cnt_monthly=0
|
||||
for (( j = 0; j < daily; j++ )); do [[ -e "$dir/daily.$j" ]] && ((cnt_daily++)); done
|
||||
for (( j = 0; j < weekly; j++ )); do [[ -e "$dir/weekly.$j" ]] && ((cnt_weekly++)); done
|
||||
for (( j = 0; j < monthly; j++ )); do [[ -e "$dir/monthly.$j" ]] && ((cnt_monthly++)); done
|
||||
|
||||
echo "ROTATED daily=$cnt_daily weekly=$cnt_weekly monthly=$cnt_monthly dir=$dir"
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: tail_journal
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "tail_journal(unit: string, lines: int=100, follow: bool=false, since: string=\"\", priority: string=\"info\") -> void"
|
||||
description: "Wrapper sobre journalctl con formato consistente. Tail logs de una unidad systemd con coloreado, filtro por prioridad y seguimiento opcional."
|
||||
tags: ["journal", "systemd", "logs"]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: unit
|
||||
desc: "Nombre de la unidad systemd (ej. registry-api.service, caddy). Acepta sin extension .service."
|
||||
- name: lines
|
||||
desc: "Numero de lineas iniciales a mostrar. Default 100."
|
||||
- name: follow
|
||||
desc: "Si true o -f, activa seguimiento continuo (journalctl -f). Default false."
|
||||
- name: since
|
||||
desc: "Filtro temporal: '1 hour ago', 'yesterday', '2026-05-07'. Default vacio (sin filtro)."
|
||||
- name: priority
|
||||
desc: "Prioridad minima de logs: emerg|alert|crit|err|warning|notice|info|debug. Default info."
|
||||
output: "Lineas de journalctl en formato short-iso, una por linea. Si follow=true, flujo continuo."
|
||||
example: "tail_journal registry-api 200 true"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/tail_journal.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Ver ultimas 100 lineas de un servicio
|
||||
tail_journal caddy
|
||||
|
||||
# Seguimiento continuo con 50 lineas iniciales
|
||||
tail_journal registry-api.service 50 true
|
||||
|
||||
# Solo errores de la ultima hora
|
||||
tail_journal my-app 200 false "1 hour ago" err
|
||||
|
||||
# Con pipe (funciona sin bufferizado)
|
||||
tail_journal registry-api 100 true "" warning | grep "ERROR"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Detecta automaticamente si la unit pertenece al usuario (systemctl --user) o al sistema (sudo journalctl). Si la unit no es de usuario, se llama con sudo — el usuario debe tener NOPASSWD para journalctl o sudo configurado.
|
||||
|
||||
Si follow=true, usa `stdbuf -oL` para deshabilitar el bufferizado de stdout, lo que permite piping en tiempo real (ej. `tail_journal ... | grep pattern`).
|
||||
|
||||
Exit codes: 1 unit no existe o no especificada, 2 prioridad invalida, 5 journalctl no disponible.
|
||||
```
|
||||
---
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# tail_journal — wrapper sobre journalctl con formato consistente
|
||||
|
||||
tail_journal() {
|
||||
local unit="${1:-}"
|
||||
local lines="${2:-100}"
|
||||
local follow="${3:-false}"
|
||||
local since="${4:-}"
|
||||
local priority="${5:-info}"
|
||||
|
||||
if [[ -z "$unit" ]]; then
|
||||
echo "tail_journal: unit requerida" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Normalizar nombre de unit (añadir .service si no tiene extension)
|
||||
local unit_full="$unit"
|
||||
if [[ "$unit" != *.* ]]; then
|
||||
unit_full="${unit}.service"
|
||||
fi
|
||||
|
||||
# Validar prioridad
|
||||
case "$priority" in
|
||||
emerg|alert|crit|err|warning|notice|info|debug) ;;
|
||||
*)
|
||||
echo "tail_journal: prioridad invalida '$priority'. Valores: emerg alert crit err warning notice info debug" >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verificar que journalctl esta disponible
|
||||
if ! command -v journalctl &>/dev/null; then
|
||||
echo "tail_journal: journalctl no disponible" >&2
|
||||
return 5
|
||||
fi
|
||||
|
||||
# Detectar si la unit es de usuario o de sistema
|
||||
local user_flag=""
|
||||
if systemctl --user list-units --all 2>/dev/null | grep -q "$unit_full"; then
|
||||
user_flag="--user"
|
||||
fi
|
||||
|
||||
# Construir comando base
|
||||
local -a cmd
|
||||
if [[ -z "$user_flag" ]]; then
|
||||
cmd=(sudo journalctl)
|
||||
else
|
||||
cmd=(journalctl --user)
|
||||
fi
|
||||
|
||||
cmd+=(-u "$unit_full" -n "$lines" -p "$priority" --output=short-iso)
|
||||
|
||||
if [[ -n "$since" ]]; then
|
||||
cmd+=(--since "$since")
|
||||
fi
|
||||
|
||||
local follow_flag=false
|
||||
if [[ "$follow" == "true" || "$follow" == "-f" ]]; then
|
||||
follow_flag=true
|
||||
cmd+=(-f)
|
||||
fi
|
||||
|
||||
# Verificar que la unit existe (solo si no hay user_flag y no es sudo)
|
||||
if [[ -n "$user_flag" ]]; then
|
||||
if ! systemctl --user list-units --all 2>/dev/null | grep -q "$unit_full"; then
|
||||
echo "tail_journal: unit '$unit_full' no encontrada" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ejecutar sin bufferizar si follow=true
|
||||
if [[ "$follow_flag" == "true" ]]; then
|
||||
stdbuf -oL "${cmd[@]}"
|
||||
else
|
||||
"${cmd[@]}"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: tbd_branch_create
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "tbd_branch_create(mode: string, ...args: string) -> void"
|
||||
description: "Crea una rama TBD (trunk-based development) desde master/main actualizado. Soporta modos 'issue <NNNN> <slug>' y 'quick <slug>'. Autodetecta la rama base (master/main), verifica working tree limpio, hace pull --rebase y crea la rama. Valida formato de numero de issue (4 digitos) y slug (kebab-case ASCII)."
|
||||
tags: [git, tbd, branch, trunk-based-development, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: mode
|
||||
desc: "Modo de creacion: 'issue' para ramas issue/<NNNN>-<slug> o 'quick' para ramas quick/<slug>."
|
||||
- name: args
|
||||
desc: "Para mode=issue: NNNN (4 digitos) + slug (kebab-case). Para mode=quick: solo slug (kebab-case)."
|
||||
output: "Crea la rama y cambia a ella. Imprime confirmacion a stdout. Exit 1 en caso de error (dirty tree, formato invalido, repo inexistente)."
|
||||
tested: true
|
||||
tests:
|
||||
- "issue branch created"
|
||||
- "quick branch created"
|
||||
- "issue number must be 4 digits"
|
||||
- "slug must be kebab-case"
|
||||
- "invalid mode exits 1"
|
||||
- "no args exits 1"
|
||||
- "dirty tree exits 1"
|
||||
- "existing branch exits 1"
|
||||
- "works with main as base"
|
||||
test_file_path: "bash/functions/infra/tbd_branch_create.sh"
|
||||
file_path: "bash/functions/infra/tbd_branch_create.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Crear rama para un issue
|
||||
tbd_branch_create issue 0042 add-auth
|
||||
|
||||
# Crear rama para cambio rapido
|
||||
tbd_branch_create quick fix-typo-readme
|
||||
|
||||
# Ejecutar suite de tests
|
||||
bash bash/functions/infra/tbd_branch_create.sh --test
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
La funcion autodetecta la rama base probando primero `master` y luego `main` con `git show-ref --verify`. Si el directorio actual no es un repo git, sale con error. Si el working tree tiene cambios no commiteados, sale con error antes de hacer pull. Los tests internos se ejecutan con `--test` y crean repos temporales aislados con `mktemp -d`.
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: tbd_branch_finish
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "tbd_branch_finish([merge_title: string]) -> void"
|
||||
description: "Integra una rama TBD (issue/* o quick/*) a master/main con merge --no-ff, publica el merge al remote y elimina la rama local. Autodetecta la rama base (master/main), verifica working tree limpio y construye el titulo del merge commit. NO ejecuta tests — esa responsabilidad es del caller. Exit 2 si hay conflicto de merge (deja al usuario resolver)."
|
||||
tags: [git, tbd, merge, trunk-based-development, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: merge_title
|
||||
desc: "Titulo opcional del merge commit. Si se provee, el commit sera 'merge: <rama> — <merge_title>'. Si no, sera 'merge: <rama>'."
|
||||
output: "Mergea la rama a base, hace push y elimina la rama local. Imprime confirmacion a stdout. Exit 1 en error (dirty tree, push fallido, no en rama TBD). Exit 2 si hay conflicto de merge (merge iniciado pero no resuelto — el usuario debe resolver manualmente)."
|
||||
tested: true
|
||||
tests:
|
||||
- "finish lands on base branch"
|
||||
- "issue branch deleted locally"
|
||||
- "finish exits 0"
|
||||
- "merge commit pushed to remote"
|
||||
- "quick branch finish lands on master"
|
||||
- "merge title included in commit"
|
||||
- "dirty tree exits 1"
|
||||
- "on master exits 1"
|
||||
- "non-TBD branch exits 1"
|
||||
- "works with main as base"
|
||||
test_file_path: "bash/functions/infra/tbd_branch_finish.sh"
|
||||
file_path: "bash/functions/infra/tbd_branch_finish.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Desde una rama issue/0042-add-auth, sin titulo adicional
|
||||
tbd_branch_finish
|
||||
|
||||
# Con titulo descriptivo en el merge commit
|
||||
tbd_branch_finish "implementar autenticacion OAuth"
|
||||
|
||||
# Ejecutar suite de tests
|
||||
bash bash/functions/infra/tbd_branch_finish.sh --test
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
La funcion NO ejecuta tests del proyecto — esa responsabilidad es del caller (slash command, agente, CI) porque depende del stack (go test, pytest, ctest, etc.). Si el push falla, la rama local NO se elimina para no perder trabajo. Si hay conflicto de merge, sale con exit 2 dejando el repo en estado de merge-en-progreso para que el usuario resuelva con `git add` + `git commit`. Los tests internos usan un remote bare simulado con `git init --bare` para validar el push completo.
|
||||
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env bash
|
||||
# tbd_branch_finish — integra rama TBD a master/main y publica
|
||||
#
|
||||
# Uso:
|
||||
# tbd_branch_finish [<merge_title>]
|
||||
#
|
||||
# Opciones especiales:
|
||||
# --test ejecutar suite de tests internos y salir
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_tbd_finish_detect_base() {
|
||||
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_finish_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
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# funcion principal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
tbd_branch_finish() {
|
||||
local merge_title="${1:-}"
|
||||
|
||||
# Verificar repo git
|
||||
_tbd_finish_require_git_repo
|
||||
|
||||
# Detectar rama actual
|
||||
local branch
|
||||
branch=$(git branch --show-current)
|
||||
|
||||
# Validar que es una rama TBD (issue/* o quick/*)
|
||||
if [[ "$branch" != issue/* && "$branch" != quick/* ]]; then
|
||||
if [[ "$branch" == "master" || "$branch" == "main" ]]; then
|
||||
echo "ERROR: ya estas en ${branch}, nada que mergear." >&2
|
||||
return 1
|
||||
else
|
||||
echo "ERROR: la rama actual '${branch}' no es una rama TBD (issue/* o quick/*)." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verificar working tree limpio
|
||||
local dirty
|
||||
dirty=$(git status --porcelain)
|
||||
if [[ -n "$dirty" ]]; then
|
||||
echo "ERROR: hay cambios sin commitear. Haz commit antes de mergear." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Detectar rama base
|
||||
local base
|
||||
base=$(_tbd_finish_detect_base)
|
||||
if [[ -z "$base" ]]; then
|
||||
echo "ERROR: no se encontro rama 'master' ni 'main' en el repo local." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Componer titulo del merge commit
|
||||
local title
|
||||
if [[ -n "$merge_title" ]]; then
|
||||
title="merge: ${branch} — ${merge_title}"
|
||||
else
|
||||
title="merge: ${branch}"
|
||||
fi
|
||||
|
||||
# Cambiar a base y actualizar
|
||||
echo "Cambiando a ${base}..."
|
||||
git checkout "$base"
|
||||
|
||||
echo "Actualizando ${base}..."
|
||||
git pull --rebase
|
||||
|
||||
# Merge --no-ff
|
||||
echo "Mergeando ${branch} en ${base}..."
|
||||
local merge_exit=0
|
||||
git merge --no-ff "$branch" -m "$title" || merge_exit=$?
|
||||
|
||||
if [[ $merge_exit -ne 0 ]]; then
|
||||
echo "merge conflict: resolver manualmente y continuar (git add + git commit)." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Push
|
||||
echo "Publicando ${base}..."
|
||||
local push_exit=0
|
||||
git push || push_exit=$?
|
||||
if [[ $push_exit -ne 0 ]]; then
|
||||
echo "ERROR: git push fallo (exit ${push_exit}). La rama '${branch}' NO fue eliminada." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Eliminar rama local
|
||||
git branch -d "$branch"
|
||||
|
||||
echo "Rama '${branch}' integrada a ${base} y publicada. Rama local eliminada."
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# modo --test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_tbd_branch_finish_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 de trabajo
|
||||
# ---------------------------------------------------------------------------
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
trap "rm -rf '$tmpdir'" EXIT
|
||||
|
||||
local bare="$tmpdir/remote.git"
|
||||
local work="$tmpdir/work"
|
||||
|
||||
# Crear remote bare
|
||||
git -c init.defaultBranch=master init --bare -q "$bare"
|
||||
|
||||
# Clonar para tener origin
|
||||
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: merge issue branch sin titulo
|
||||
# ---------------------------------------------------------------------------
|
||||
(
|
||||
cd "$work"
|
||||
git checkout -q -b issue/0001-add-feature
|
||||
echo "feature" > feature.txt
|
||||
git add feature.txt
|
||||
git commit -q -m "feat: add feature"
|
||||
)
|
||||
|
||||
local finish_exit=0
|
||||
(
|
||||
cd "$work"
|
||||
tbd_branch_finish
|
||||
) > /dev/null 2>&1 || finish_exit=$?
|
||||
|
||||
local current
|
||||
current=$(cd "$work" && git branch --show-current)
|
||||
_assert_eq "finish lands on base branch" "master" "$current"
|
||||
|
||||
local branch_gone=0
|
||||
(cd "$work" && git show-ref --verify --quiet refs/heads/issue/0001-add-feature) && branch_gone=1 || branch_gone=0
|
||||
_assert_eq "issue branch deleted locally" "0" "$branch_gone"
|
||||
|
||||
_assert_eq "finish exits 0" "0" "$finish_exit"
|
||||
|
||||
# Verificar merge commit en remote
|
||||
local remote_log
|
||||
remote_log=$(cd "$work" && git log --oneline -2 origin/master)
|
||||
local has_merge=0
|
||||
[[ "$remote_log" == *"merge: issue/0001-add-feature"* ]] && has_merge=1 || has_merge=0
|
||||
_assert_eq "merge commit pushed to remote" "1" "$has_merge"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: merge quick branch con titulo
|
||||
# ---------------------------------------------------------------------------
|
||||
(
|
||||
cd "$work"
|
||||
git checkout -q -b quick/fix-typo
|
||||
echo "fix" > fix.txt
|
||||
git add fix.txt
|
||||
git commit -q -m "fix: typo"
|
||||
)
|
||||
|
||||
(
|
||||
cd "$work"
|
||||
tbd_branch_finish "corregir typo en README"
|
||||
) > /dev/null 2>&1
|
||||
|
||||
current=$(cd "$work" && git branch --show-current)
|
||||
_assert_eq "quick branch finish lands on master" "master" "$current"
|
||||
|
||||
remote_log=$(cd "$work" && git log --oneline -2 origin/master)
|
||||
local has_title=0
|
||||
[[ "$remote_log" == *"merge: quick/fix-typo — corregir typo en README"* ]] && has_title=1 || has_title=0
|
||||
_assert_eq "merge title included in commit" "1" "$has_title"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: dirty tree → exit 1
|
||||
# ---------------------------------------------------------------------------
|
||||
(
|
||||
cd "$work"
|
||||
git checkout -q -b quick/dirty-test
|
||||
)
|
||||
echo "dirty" > "$work/dirty.txt"
|
||||
|
||||
_assert_exit "dirty tree exits 1" 1 bash -c "
|
||||
cd '$work'
|
||||
source '${BASH_SOURCE[0]}'
|
||||
tbd_branch_finish
|
||||
"
|
||||
rm -f "$work/dirty.txt"
|
||||
(cd "$work" && git checkout -q master)
|
||||
(cd "$work" && git branch -D quick/dirty-test 2>/dev/null || true)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: ya en master → exit 1
|
||||
# ---------------------------------------------------------------------------
|
||||
_assert_exit "on master exits 1" 1 bash -c "
|
||||
cd '$work'
|
||||
source '${BASH_SOURCE[0]}'
|
||||
tbd_branch_finish
|
||||
"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: rama no TBD → exit 1
|
||||
# ---------------------------------------------------------------------------
|
||||
(cd "$work" && git checkout -q -b feature/non-tbd 2>/dev/null)
|
||||
_assert_exit "non-TBD branch exits 1" 1 bash -c "
|
||||
cd '$work'
|
||||
source '${BASH_SOURCE[0]}'
|
||||
tbd_branch_finish
|
||||
"
|
||||
(cd "$work" && git checkout -q master && git branch -D feature/non-tbd 2>/dev/null || true)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: funciona con 'main' como rama base
|
||||
# ---------------------------------------------------------------------------
|
||||
local tmpdir2
|
||||
tmpdir2=$(mktemp -d)
|
||||
local bare2="$tmpdir2/remote.git"
|
||||
local work2="$tmpdir2/work"
|
||||
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
|
||||
git checkout -q -b quick/use-main
|
||||
echo "x" > x.txt
|
||||
git add x.txt
|
||||
git commit -q -m "feat: x"
|
||||
tbd_branch_finish
|
||||
) > /dev/null 2>&1
|
||||
current=$(cd "$work2" && git branch --show-current)
|
||||
_assert_eq "works with main as base" "main" "$current"
|
||||
rm -rf "$tmpdir2"
|
||||
|
||||
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_finish_tests
|
||||
else
|
||||
tbd_branch_finish "$@"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: wait_for_http
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "wait_for_http <url> [timeout_seconds] [interval_seconds]"
|
||||
description: "Hace polling a una URL HTTP/HTTPS hasta recibir respuesta 2xx o agotar el timeout. Util en deploys, post-restart de servicios y smoke tests."
|
||||
tags: [http, wait, poll, health, deploy, smoke-test]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: url
|
||||
desc: "URL completa del endpoint a sondear (debe empezar por http:// o https://)"
|
||||
- name: timeout_seconds
|
||||
desc: "Tiempo maximo de espera en segundos. Default: 30"
|
||||
- name: interval_seconds
|
||||
desc: "Intervalo entre intentos en segundos. Default: 1"
|
||||
output: "stdout: 'OK <url> (<elapsed>s)' al primer 2xx. stderr: linea de progreso por intento y mensaje TIMEOUT si se agota. Exit 0=ok, 1=timeout, 2=URL invalida, 5=curl no instalado."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/wait_for_http.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/wait_for_http.sh
|
||||
|
||||
# Esperar hasta 60 segundos con sondeo cada 2 segundos
|
||||
wait_for_http https://api.example.com/health 60 2
|
||||
|
||||
# Uso tipico en un pipeline de deploy
|
||||
wait_for_http http://localhost:8080/health || { echo "servicio no arranco"; exit 1; }
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Acepta cualquier codigo 2xx (200, 201, 204...) como OK. Los codigos 3xx se tratan como "no listo" — el servicio debe responder directamente con 2xx, no redirigir.
|
||||
|
||||
curl se invoca con `--max-time 5` para no bloquear el loop si la conexion tarda. Los errores de curl (DNS, conexion rechazada) se tratan como "no listo" y el loop continua.
|
||||
|
||||
Salida de progreso va a stderr para no contaminar pipelines que capturen stdout.
|
||||
---
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# wait_for_http — polling HTTP hasta 200/2xx o timeout
|
||||
|
||||
wait_for_http() {
|
||||
local url="${1:-}"
|
||||
local timeout="${2:-30}"
|
||||
local interval="${3:-1}"
|
||||
|
||||
# Validar dependencia
|
||||
if ! command -v curl &>/dev/null; then
|
||||
echo "wait_for_http: curl no encontrado" >&2
|
||||
return 5
|
||||
fi
|
||||
|
||||
# Validar URL
|
||||
if [[ -z "$url" || ( "$url" != http://* && "$url" != https://* ) ]]; then
|
||||
echo "wait_for_http: URL invalida — debe empezar por http:// o https://" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
local start
|
||||
start=$(date +%s)
|
||||
local last_code="none"
|
||||
|
||||
while true; do
|
||||
local now
|
||||
now=$(date +%s)
|
||||
local elapsed=$(( now - start ))
|
||||
|
||||
if (( elapsed >= timeout )); then
|
||||
echo "TIMEOUT $url after ${timeout}s, last code: $last_code" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local code
|
||||
code=$(curl -fsS -o /dev/null -w "%{http_code}" --max-time 5 "$url" 2>/dev/null || true)
|
||||
last_code="${code:-000}"
|
||||
|
||||
echo ". waiting $url code=$last_code elapsed=${elapsed}s" >&2
|
||||
|
||||
# 2xx => OK
|
||||
if [[ "$last_code" =~ ^2[0-9]{2}$ ]]; then
|
||||
local finish
|
||||
finish=$(date +%s)
|
||||
local total=$(( finish - start ))
|
||||
echo "OK $url (${total}s)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep "$interval"
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: wait_for_port
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "wait_for_port(host: string, port: int, timeout_seconds: int, interval_seconds: int) -> int"
|
||||
description: "Hace polling TCP a host:puerto hasta que acepte conexiones o agote el timeout. Util para esperar a que un servicio (DB, API, container) este listo antes de ejecutar pasos siguientes."
|
||||
tags: [tcp, wait, poll, port]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: host
|
||||
desc: "Hostname, IP o DNS del servicio a esperar (ej: localhost, 192.168.1.10, db.internal)"
|
||||
- name: port
|
||||
desc: "Puerto TCP a sondear (1-65535)"
|
||||
- name: timeout_seconds
|
||||
desc: "Tiempo maximo de espera en segundos antes de abortar. Default 30"
|
||||
- name: interval_seconds
|
||||
desc: "Intervalo en segundos entre intentos de conexion. Default 1"
|
||||
output: "Exit 0 e imprime 'OK host:port (Ns)' en stdout cuando el puerto acepta conexiones. Exit 1 con mensaje TIMEOUT en stderr si se agota el tiempo. Exit 2 si el puerto es invalido. Exit 3 si el host esta vacio."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/wait_for_port.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/wait_for_port.sh
|
||||
|
||||
# Esperar PostgreSQL con timeout de 60s
|
||||
wait_for_port localhost 5432 60
|
||||
|
||||
# Esperar Redis con intervalo de 2s
|
||||
wait_for_port 192.168.1.10 6379 30 2
|
||||
|
||||
# Uso en pipeline de deploy
|
||||
wait_for_port localhost 8080 45 && curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Detecta al inicio si `nc` esta disponible. Si no, usa el builtin `/dev/tcp` de bash (disponible en bash >= 3.0, no requiere herramientas externas).
|
||||
|
||||
Metodos en orden de prioridad:
|
||||
1. `nc -z -w 2 <host> <port>` — netcat con timeout de 2s por intento
|
||||
2. `exec 3<>/dev/tcp/<host>/<port>` — bash builtin TCP, no requiere nc
|
||||
|
||||
Cada intento fallido imprime `. waiting host:port elapsed=Ns` en stderr para visibilidad en logs de CI/CD.
|
||||
|
||||
Complementa `wait_for_http_bash_infra` (HTTP/HTTPS): esta funcion opera a nivel TCP puro, util cuando el servicio no expone HTTP o cuando se quiere verificar la capa de red antes de la aplicacion.
|
||||
|
||||
Codigos de salida:
|
||||
- 0: puerto disponible
|
||||
- 1: timeout agotado
|
||||
- 2: puerto invalido (no numerico o fuera de 1-65535)
|
||||
- 3: host vacio
|
||||
---
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# wait_for_port — polling TCP a host:puerto hasta conexion o timeout
|
||||
|
||||
wait_for_port() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local timeout="${3:-30}"
|
||||
local interval="${4:-1}"
|
||||
|
||||
# Validaciones
|
||||
if [[ -z "$host" ]]; then
|
||||
echo "wait_for_port: host vacio" >&2
|
||||
return 3
|
||||
fi
|
||||
if ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
|
||||
echo "wait_for_port: puerto invalido '$port'" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Detectar metodo disponible una sola vez
|
||||
local method
|
||||
if command -v nc &>/dev/null; then
|
||||
method="nc"
|
||||
else
|
||||
method="devtcp"
|
||||
fi
|
||||
|
||||
local _try_connect
|
||||
if [[ "$method" == "nc" ]]; then
|
||||
_try_connect() { nc -z -w 2 "$host" "$port" &>/dev/null; }
|
||||
else
|
||||
_try_connect() {
|
||||
(exec 3<>/dev/tcp/"$host"/"$port") &>/dev/null
|
||||
}
|
||||
fi
|
||||
|
||||
local start elapsed
|
||||
start=$(date +%s)
|
||||
|
||||
while true; do
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
if (( elapsed >= timeout )); then
|
||||
echo "TIMEOUT ${host}:${port} after ${timeout}s" >&2
|
||||
return 1
|
||||
fi
|
||||
echo ". waiting ${host}:${port} elapsed=${elapsed}s" >&2
|
||||
if _try_connect; then
|
||||
echo "OK ${host}:${port} (${elapsed}s)"
|
||||
return 0
|
||||
fi
|
||||
sleep "$interval"
|
||||
done
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: backup_all
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "backup_all(backup_root: string) -> void"
|
||||
description: "Backup completo del estado del registry: snapshot atomico de registry.db, snapshot de cada operations.db de cada app, y rsync de todos los vaults declarados en vault.yaml. Aplica retention 7/4/12 (daily/weekly/monthly) con rotate_backups. Idempotente, llamable a diario desde cron o systemd-timer."
|
||||
tags: ["backup", "launcher", "pipeline", "retention"]
|
||||
uses_functions:
|
||||
- backup_sqlite_db_bash_infra
|
||||
- rotate_backups_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: backup_root
|
||||
desc: "Directorio raiz donde se guardan todos los backups (ej. ~/backups/fn_registry). Se crea si no existe."
|
||||
output: "Linea de resumen a stdout: ISO_timestamp, bytes de registry.db, conteo de ops backupeadas, conteo de vaults sincronizados, errores parciales y segundos transcurridos. Misma linea se hace append en backup_root/backup_log.txt."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/backup_all.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Backup manual a ~/backups/fn_registry
|
||||
export FN_REGISTRY_ROOT=/home/lucas/fn_registry
|
||||
backup_all ~/backups/fn_registry
|
||||
|
||||
# Salida esperada:
|
||||
# 2026-05-07T10:30:00+02:00 registry=4194304B ops=3 vaults=2 partial_errors=0 elapsed=12s
|
||||
|
||||
# Entrada en crontab (diario a las 02:00)
|
||||
# 0 2 * * * FN_REGISTRY_ROOT=/home/lucas/fn_registry bash /home/lucas/fn_registry/bash/functions/pipelines/backup_all.sh ~/backups/fn_registry
|
||||
```
|
||||
|
||||
## Estructura de backup_root/
|
||||
|
||||
```
|
||||
registry/
|
||||
daily.0 daily.1 ... daily.6
|
||||
weekly.0 ... weekly.3
|
||||
monthly.0 ... monthly.11
|
||||
operations/
|
||||
<app_name>/
|
||||
daily.0 ... (misma retencion)
|
||||
vaults/
|
||||
<vault_name>/
|
||||
daily.0/ (directorio rsync con hard-links)
|
||||
daily.1/ ...
|
||||
backup_log.txt
|
||||
```
|
||||
|
||||
## Codigos de salida
|
||||
|
||||
| Codigo | Significado |
|
||||
|--------|-------------|
|
||||
| 0 | Exito completo |
|
||||
| 1 | FN_REGISTRY_ROOT no localizable |
|
||||
| 2 | backup_root no se puede crear/escribir |
|
||||
| 3 | Fallo critico en backup de registry.db |
|
||||
| 4 | Errores parciales en ops o vaults (no critico, continua) |
|
||||
| 5 | Herramientas del sistema faltantes (sqlite3, rsync, find) |
|
||||
|
||||
## Notas
|
||||
|
||||
Idempotente: llamar multiples veces el mismo dia solo rota si hay cambios de fecha. Los vaults deducan bloques iguales con rsync --link-dest, por lo que el primer run ocupa el espacio real y los sucesivos solo almacenan los deltas. Operations.db de apps sin dicho archivo se ignoran silenciosamente. Requiere FN_REGISTRY_ROOT seteado o ejecutar desde la raiz del registry.
|
||||
@@ -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 "$@"
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: compile_cpp_app
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "compile_cpp_app(app_name?: string) -> void"
|
||||
description: "Pipeline que resuelve la app C++ desde el nombre o CWD, la cross-compila para Windows con mingw-w64, y despliega el .exe al escritorio de Windows. Composicion de resolve_cpp_app_dir + build_cpp_windows + deploy_cpp_exe_to_windows."
|
||||
tags: [cpp, compile, windows, mingw, cross-compile, deploy, pipeline]
|
||||
uses_functions:
|
||||
- resolve_cpp_app_dir_bash_infra
|
||||
- build_cpp_windows_bash_infra
|
||||
- deploy_cpp_exe_to_windows_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/compile_cpp_app.sh"
|
||||
params:
|
||||
- name: app_name
|
||||
desc: "Nombre de la app a compilar (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de cpp/apps/<X>/ o projects/*/apps/<X>/."
|
||||
output: "Compila el .exe y lo despliega al escritorio de Windows. Imprime progreso por steps a stderr y resumen final con ls -lh del .exe resultante."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Desde dentro del directorio de la app (sin arg)
|
||||
cd /home/lucas/fn_registry/cpp/apps/chart_demo
|
||||
fn run compile_cpp_app
|
||||
|
||||
# Con nombre explicito desde cualquier directorio
|
||||
fn run compile_cpp_app registry_dashboard
|
||||
|
||||
# Directo
|
||||
bash bash/functions/pipelines/compile_cpp_app.sh graph_explorer
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
1. `resolve_cpp_app_dir` — deduce nombre y directorio absoluto de la app (desde CWD o arg)
|
||||
2. Verifica que existe `CMakeLists.txt` en el directorio de la app
|
||||
3. `build_cpp_windows` — cross-compila con mingw-w64 solo el target de la app
|
||||
4. `deploy_cpp_exe_to_windows` — copia exe, DLLs, assets, enrichers y runtime al escritorio de Windows
|
||||
5. Imprime `ls -lh` del exe final en Desktop/apps/<APP>/
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
- `FN_REGISTRY_ROOT` — raiz del registry; default `/home/lucas/fn_registry`
|
||||
- `BUILD_WIN` — directorio de build Windows; default `$FN_REGISTRY_ROOT/cpp/build/windows`
|
||||
- `WIN_DESKTOP_APPS` — directorio destino; default `/mnt/c/Users/lucas/Desktop/apps`
|
||||
|
||||
## Notas
|
||||
|
||||
Reemplaza la logica del slash command `/compile`. No lleva tag `launcher` porque no es un pipeline TUI-lanzable (tarda minutos en compilar).
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pipeline: compile_cpp_app — Resuelve la app, la cross-compila para Windows y despliega al escritorio.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
INFRA_DIR="$SCRIPT_DIR/../infra"
|
||||
|
||||
source "$INFRA_DIR/resolve_cpp_app_dir.sh"
|
||||
source "$INFRA_DIR/build_cpp_windows.sh"
|
||||
source "$INFRA_DIR/deploy_cpp_exe_to_windows.sh"
|
||||
|
||||
compile_cpp_app() {
|
||||
local app_arg="${1:-}"
|
||||
|
||||
# --- Paso 1: Resolver nombre y directorio de la app ---
|
||||
echo "[1/3] Resolviendo app..." >&2
|
||||
local resolved
|
||||
resolved=$(resolve_cpp_app_dir "$app_arg")
|
||||
local APP APP_DIR
|
||||
APP="$(echo "$resolved" | cut -f1)"
|
||||
APP_DIR="$(echo "$resolved" | cut -f2)"
|
||||
echo " App: $APP" >&2
|
||||
echo " Dir: $APP_DIR" >&2
|
||||
|
||||
# --- Verificar que tiene CMakeLists.txt ---
|
||||
if [ ! -f "$APP_DIR/CMakeLists.txt" ]; then
|
||||
echo "ERROR: $APP_DIR/CMakeLists.txt no encontrado." >&2
|
||||
echo "La app '$APP' no esta registrada con CMake. Ver cpp_apps.md §5." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- Paso 2: Cross-compilar para Windows ---
|
||||
echo "" >&2
|
||||
echo "[2/3] Compilando '$APP' para Windows (mingw-w64)..." >&2
|
||||
build_cpp_windows "$APP"
|
||||
|
||||
# --- Paso 3: Desplegar al escritorio de Windows ---
|
||||
echo "" >&2
|
||||
echo "[3/3] Desplegando '$APP' al escritorio de Windows..." >&2
|
||||
deploy_cpp_exe_to_windows "$APP" "$APP_DIR"
|
||||
|
||||
# --- Resumen final ---
|
||||
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
|
||||
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
|
||||
local final_exe="$win_desktop_apps/$APP/$APP.exe"
|
||||
|
||||
echo "" >&2
|
||||
if [ -f "$final_exe" ]; then
|
||||
echo "===== compile_cpp_app: OK =====" >&2
|
||||
ls -lh "$final_exe" >&2
|
||||
else
|
||||
echo "WARN: no se encuentra $final_exe" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
compile_cpp_app "$@"
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: full_git_pull
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.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."
|
||||
tags: [git, pull, sync, registry, pipeline]
|
||||
uses_functions:
|
||||
- discover_git_repos_bash_infra
|
||||
- git_pull_with_stash_bash_infra
|
||||
- pass_get_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params: []
|
||||
output: "tabla resumen por stdout: pull status de cada repo, estado de pass-secrets, submodulos actualizados, resultado de fn index, resultado de fn sync; lista de repos con divergencia o conflicto de stash al final"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/full_git_pull.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Pull completo
|
||||
fn run full_git_pull
|
||||
|
||||
# Directo
|
||||
bash bash/functions/pipelines/full_git_pull.sh
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
1. `discover_git_repos` — lista repos git locales bajo `$FN_REGISTRY_ROOT`
|
||||
2. `git_pull_with_stash` — para cada repo: stash si dirty, fetch, pull --ff-only, pop stash
|
||||
3. `git submodule update --init --recursive` — actualiza submodulos del repo principal
|
||||
4. `git_pull_with_stash` sobre `~/.password-store` (si existe)
|
||||
5. `CGO_ENABLED=1 ./fn index` — regenera registry.db
|
||||
6. `fn sync` — sincroniza proposals, apps, projects, analysis, vaults, pc_locations
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
- `FN_REGISTRY_ROOT` — raiz del registry; default `/home/lucas/fn_registry`
|
||||
- `FN_REGISTRY_API`, `REGISTRY_API_TOKEN` — se cargan de `pass registry/*`
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pipeline: full_git_pull — Pull automatico de fn_registry + sub-repos + submodules + fn sync
|
||||
# Descubre repos locales, stashea dirty trees, hace pull --ff-only, actualiza submodules,
|
||||
# regenera registry.db y ejecuta fn sync.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
INFRA_DIR="$SCRIPT_DIR/../infra"
|
||||
|
||||
source "$INFRA_DIR/discover_git_repos.sh"
|
||||
source "$INFRA_DIR/git_pull_with_stash.sh"
|
||||
source "$INFRA_DIR/pass_get.sh"
|
||||
|
||||
full_git_pull() {
|
||||
# Resolver raiz del registry
|
||||
local registry_root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
cd "$registry_root"
|
||||
|
||||
echo "=== full_git_pull: inicio ===" >&2
|
||||
echo "Registry root: $registry_root" >&2
|
||||
|
||||
# --- Paso 1: Descubrir repos ---
|
||||
echo "" >&2
|
||||
echo "[1/5] Descubriendo repos git..." >&2
|
||||
local repos
|
||||
repos=$(discover_git_repos "$registry_root")
|
||||
local n_repos
|
||||
n_repos=$(echo "$repos" | grep -c . || true)
|
||||
echo " Encontrados: $n_repos repos" >&2
|
||||
|
||||
# --- Paso 2: Pull de cada repo ---
|
||||
echo "" >&2
|
||||
echo "[2/5] Pullando repos..." >&2
|
||||
local pull_summary=""
|
||||
local diverged=()
|
||||
local conflicts=()
|
||||
|
||||
while IFS= read -r repo; do
|
||||
[[ -z "$repo" ]] && continue
|
||||
local result
|
||||
result=$(git_pull_with_stash "$repo" 2>/dev/null || true)
|
||||
if [[ -n "$result" ]]; then
|
||||
echo " $result" >&2
|
||||
pull_summary="$pull_summary"$'\n'" $result"
|
||||
if [[ "$result" == "[diverged]"* ]]; then
|
||||
diverged+=("$repo")
|
||||
elif [[ "$result" == "[stash-conflict]"* ]]; then
|
||||
conflicts+=("$repo")
|
||||
fi
|
||||
fi
|
||||
done <<< "$repos"
|
||||
|
||||
# --- Paso 3: Submodules del repo principal ---
|
||||
echo "" >&2
|
||||
echo "[3/5] Actualizando submodulos del repo principal..." >&2
|
||||
local submodule_summary=" [skip] sin submodulos"
|
||||
if [[ -f "$registry_root/.gitmodules" ]]; then
|
||||
local sub_out
|
||||
sub_out=$(git -C "$registry_root" submodule update --init --recursive 2>&1 | tail -10 || true)
|
||||
echo "$sub_out" >&2
|
||||
submodule_summary=" OK: $sub_out"
|
||||
fi
|
||||
|
||||
# --- Paso 3b: Pull de ~/.password-store ---
|
||||
echo "" >&2
|
||||
echo "[3b] Pullando ~/.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_result
|
||||
pass_result=$(git_pull_with_stash "$pass_dir" 2>/dev/null || true)
|
||||
echo " $pass_result" >&2
|
||||
pass_summary=" $pass_result"
|
||||
if [[ "$pass_result" == "[diverged]"* ]]; then
|
||||
diverged+=("$pass_dir")
|
||||
elif [[ "$pass_result" == "[stash-conflict]"* ]]; then
|
||||
conflicts+=("$pass_dir")
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Paso 4: Regenerar registry.db ---
|
||||
echo "" >&2
|
||||
echo "[4/5] Regenerando registry.db..." >&2
|
||||
local index_summary=" [skip] fn no encontrado"
|
||||
local fn_bin="$registry_root/fn"
|
||||
if [[ -x "$fn_bin" ]]; then
|
||||
local index_out
|
||||
index_out=$(CGO_ENABLED=1 "$fn_bin" index 2>&1 | tail -3 || true)
|
||||
echo "$index_out" >&2
|
||||
index_summary=" OK: $index_out"
|
||||
else
|
||||
echo " [warn] $fn_bin no encontrado — intentando build..." >&2
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
CGO_ENABLED=1 go build -tags fts5 -o "$fn_bin" "$registry_root/cmd/fn/" 2>&1 >&2 || true
|
||||
if [[ -x "$fn_bin" ]]; then
|
||||
local index_out
|
||||
index_out=$(CGO_ENABLED=1 "$fn_bin" index 2>&1 | tail -3 || true)
|
||||
echo "$index_out" >&2
|
||||
index_summary=" OK (post-build): $index_out"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Paso 5: fn sync ---
|
||||
echo "" >&2
|
||||
echo "[5/5] Ejecutando fn sync..." >&2
|
||||
local sync_summary=" [skip] fn sync: credenciales no disponibles"
|
||||
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
|
||||
fi
|
||||
|
||||
# --- Resumen ---
|
||||
echo ""
|
||||
echo "===== RESUMEN full_git_pull ====="
|
||||
echo ""
|
||||
echo "Pull status por repo:"
|
||||
if [[ -n "$pull_summary" ]]; then
|
||||
echo "$pull_summary"
|
||||
else
|
||||
echo " (ninguno)"
|
||||
fi
|
||||
echo ""
|
||||
echo "pass-secrets:"
|
||||
echo "$pass_summary"
|
||||
echo ""
|
||||
echo "Submodulos:"
|
||||
echo "$submodule_summary"
|
||||
echo ""
|
||||
echo "fn index:"
|
||||
echo "$index_summary"
|
||||
echo ""
|
||||
echo "fn sync:"
|
||||
echo "$sync_summary"
|
||||
|
||||
if [[ ${#diverged[@]} -gt 0 || ${#conflicts[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "ATENCION — Repos que requieren intervencion manual:"
|
||||
for r in "${diverged[@]+"${diverged[@]}"}"; do
|
||||
echo " [diverged] $r → git rebase o git merge manual"
|
||||
done
|
||||
for r in "${conflicts[@]+"${conflicts[@]}"}"; do
|
||||
echo " [stash-conflict] $r → resolver conflicto y git stash drop"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================="
|
||||
}
|
||||
|
||||
full_git_pull "$@"
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: full_git_push
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.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."
|
||||
tags: [git, push, sync, registry, pipeline]
|
||||
uses_functions:
|
||||
- discover_git_repos_bash_infra
|
||||
- scan_secrets_in_dirty_bash_cybersecurity
|
||||
- git_auto_commit_dirty_bash_infra
|
||||
- git_push_if_ahead_bash_infra
|
||||
- ensure_repo_synced_bash_infra
|
||||
- pass_get_bash_infra
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: commit_message
|
||||
desc: "mensaje de commit fijo para todos los repos (opcional); si se omite, cada repo recibe un mensaje generado automaticamente segun sus cambios"
|
||||
output: "tabla resumen por stdout: commits creados por repo, push status de cada repo, estado de pass-secrets, resultado de fn sync"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/pipelines/full_git_push.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Push con mensaje automatico
|
||||
fn run full_git_push
|
||||
|
||||
# Push con mensaje fijo para todos los repos
|
||||
fn run full_git_push "chore: sync desde home-wsl"
|
||||
|
||||
# Directo
|
||||
bash bash/functions/pipelines/full_git_push.sh "feat: nueva funcion"
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
1. `discover_git_repos` — lista todos los repos bajo `$FN_REGISTRY_ROOT`
|
||||
2. Auto-init — para cada app/analysis sin `.git`, llama `ensure_repo_synced` (requiere `GITEA_URL`/`GITEA_TOKEN` via `pass_get`)
|
||||
3. `scan_secrets_in_dirty` — escanea cada repo; si hay matches **aborta todo** y lista los archivos
|
||||
4. `git_auto_commit_dirty` — commitea dirty trees con mensaje fijo o generado
|
||||
5. `git_push_if_ahead` — pushea solo repos con commits locales (sin tocar la red para los up-to-date)
|
||||
6. Push de `~/.password-store` — solo push (sin commit; pass se autocommitea)
|
||||
7. `fn sync` — sincroniza proposals, apps, projects, analysis, vaults, pc_locations con registry_api
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
- `FN_REGISTRY_ROOT` — raiz del registry; default `/home/lucas/fn_registry`
|
||||
- `GITEA_URL`, `GITEA_TOKEN` — se cargan de `pass agentes/gitea-url` y `pass gitea/dataforge-git-token`
|
||||
- `FN_REGISTRY_API`, `REGISTRY_API_TOKEN` — se cargan de `pass registry/*`
|
||||
|
||||
## 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.
|
||||
@@ -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