feat: externalize apps/analysis to Gitea repos, add analysis table
- Migration 007: repo_url on apps table + analysis table with FTS5 - Analysis struct, parser, CRUD, validation, hash computation - Selective purge: remote-only apps/analysis preserved across fn index - CLI: fn app list/clone/pull, fn analysis list/clone/pull - search/show/list now include analysis results - Apps removed from git tracking (content lives in Gitea repos) - .gitkeep for apps/ and analysis/ dirs - Bash functions: jupyter analysis pipeline, shell utilities - Browser domain: CDP functions moved from infra to browser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: init_uv_venv
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "init_uv_venv([project_dir: string]) -> string"
|
||||
description: "Crea un virtualenv Python con uv en el directorio dado si no existe. Fallback a python3 -m venv. Retorna la ruta del venv."
|
||||
tags: [python, venv, uv, setup, 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/init_uv_venv.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source init_uv_venv.sh
|
||||
venv=$(init_uv_venv /home/lucas/analysis/finanzas)
|
||||
echo "Venv creado en: $venv"
|
||||
|
||||
# Idempotente — si ya existe, retorna la ruta sin recrear
|
||||
venv=$(init_uv_venv .)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Idempotente: si el venv ya existe con un python valido, retorna la ruta sin hacer nada. Prefiere uv por velocidad, usa python3 como fallback.
|
||||
@@ -0,0 +1,35 @@
|
||||
# init_uv_venv
|
||||
# -------------
|
||||
# Crea un venv con uv en el directorio especificado si no existe.
|
||||
# Fallback a python -m venv si uv no esta disponible.
|
||||
# Imprime la ruta del venv a stdout.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source init_uv_venv.sh
|
||||
# venv_path=$(init_uv_venv /path/to/project)
|
||||
|
||||
init_uv_venv() {
|
||||
local project_dir="${1:-.}"
|
||||
local venv_path="${project_dir}/.venv"
|
||||
|
||||
if [ -d "$venv_path" ] && [ -f "$venv_path/bin/python" ]; then
|
||||
echo "$venv_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v uv &>/dev/null; then
|
||||
(cd "$project_dir" && uv venv) >/dev/null 2>&1
|
||||
elif command -v python3 &>/dev/null; then
|
||||
python3 -m venv "$venv_path"
|
||||
else
|
||||
echo "init_uv_venv: ni uv ni python3 disponibles" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$venv_path/bin/python" ]; then
|
||||
echo "init_uv_venv: fallo al crear venv en $venv_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$venv_path"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: uv_add_packages
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "uv_add_packages(project_dir: string, ...packages: string) -> void"
|
||||
description: "Instala paquetes Python en un proyecto usando uv add con fallback a pip. Inicializa pyproject.toml si no existe."
|
||||
tags: [python, uv, pip, packages, 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/uv_add_packages.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source uv_add_packages.sh
|
||||
uv_add_packages /home/lucas/analysis/finanzas jupyter jupyterlab pandas numpy
|
||||
|
||||
# Solo un paquete
|
||||
uv_add_packages . polars
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Requiere que el venv ya exista (usa `init_uv_venv` antes). Prefiere uv por velocidad y reproducibilidad (lockfile). Si uv no esta disponible, usa pip del venv directamente.
|
||||
@@ -0,0 +1,35 @@
|
||||
# uv_add_packages
|
||||
# -----------------
|
||||
# Instala paquetes Python en un proyecto con uv add.
|
||||
# Inicializa pyproject.toml si no existe.
|
||||
# Fallback a pip install si uv no esta disponible.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source uv_add_packages.sh
|
||||
# uv_add_packages /path/to/project jupyter jupyterlab pandas
|
||||
|
||||
uv_add_packages() {
|
||||
local project_dir="$1"
|
||||
shift
|
||||
local packages=("$@")
|
||||
|
||||
if [ ${#packages[@]} -eq 0 ]; then
|
||||
echo "uv_add_packages: se requiere al menos un paquete" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$project_dir/.venv" ]; then
|
||||
echo "uv_add_packages: no existe .venv en $project_dir — ejecuta init_uv_venv primero" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v uv &>/dev/null; then
|
||||
# Inicializar pyproject.toml si no existe
|
||||
if [ ! -f "$project_dir/pyproject.toml" ]; then
|
||||
(cd "$project_dir" && uv init 2>/dev/null) || true
|
||||
fi
|
||||
(cd "$project_dir" && uv add "${packages[@]}" 2>&1)
|
||||
else
|
||||
"$project_dir/.venv/bin/pip" install "${packages[@]}" 2>&1
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: write_claude_jupyter_rules
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "write_claude_jupyter_rules([project_dir: string]) -> string"
|
||||
description: "Genera o actualiza .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter: celdas inmutables, programacion funcional, uso de MCP, acceso al fn_registry."
|
||||
tags: [claude, jupyter, rules, setup, 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/write_claude_jupyter_rules.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source write_claude_jupyter_rules.sh
|
||||
path=$(write_claude_jupyter_rules /home/lucas/analysis/finanzas)
|
||||
echo "Reglas escritas en: $path"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Idempotente: si CLAUDE.md ya contiene las reglas (detecta "JUPYTER HABILITADO"), no las duplica. Si CLAUDE.md existe sin las reglas, las prepend al contenido existente. Incluye instrucciones de acceso al fn_registry via `FN_REGISTRY_ROOT`.
|
||||
@@ -0,0 +1,74 @@
|
||||
# write_claude_jupyter_rules
|
||||
# ----------------------------
|
||||
# Genera .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter.
|
||||
# Si ya existe CLAUDE.md y no tiene las reglas, las prepend.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source write_claude_jupyter_rules.sh
|
||||
# write_claude_jupyter_rules /path/to/project
|
||||
|
||||
write_claude_jupyter_rules() {
|
||||
local project_dir="${1:-.}"
|
||||
local claude_dir="${project_dir}/.claude"
|
||||
local claude_md="${claude_dir}/CLAUDE.md"
|
||||
|
||||
mkdir -p "$claude_dir"
|
||||
|
||||
# Si ya tiene las reglas, no hacer nada
|
||||
if [ -f "$claude_md" ] && grep -q "JUPYTER HABILITADO" "$claude_md" 2>/dev/null; then
|
||||
echo "$claude_md"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local rules
|
||||
rules='# JUPYTER HABILITADO EN ESTE ANALISIS
|
||||
|
||||
## Reglas OBLIGATORIAS para Claude
|
||||
|
||||
### 1. CODIGO INMUTABLE — NUNCA MODIFICAR CELDAS EXISTENTES
|
||||
- **PROHIBIDO** usar NotebookEdit para reemplazar celdas existentes
|
||||
- **SIEMPRE** anadir celdas NUEVAS al final del notebook
|
||||
- Si hay un error en una celda, crear celda nueva con la correccion
|
||||
- El historial de trabajo debe quedar intacto para trazabilidad
|
||||
|
||||
### 2. PROGRAMACION FUNCIONAL OBLIGATORIA
|
||||
- **Funciones puras**: sin efectos secundarios, mismo input -> mismo output
|
||||
- **Inmutabilidad**: nunca mutar datos, crear copias transformadas
|
||||
- **Composicion**: funciones pequenas que se combinan
|
||||
- Preferir: `map`, `filter`, `reduce`, list comprehensions
|
||||
- Evitar: loops con mutacion, `global`, modificar argumentos in-place
|
||||
|
||||
### 3. SIEMPRE usar MCP jupyter para ejecutar codigo Python
|
||||
- Las ejecuciones se ven en tiempo real en Jupyter Lab del usuario
|
||||
- Compartimos variables y estado del kernel
|
||||
- **NUNCA usar bash para ejecutar Python en este analisis**
|
||||
|
||||
### 4. Verificar Jupyter activo ANTES de ejecutar
|
||||
- Si no esta activo: pedir al usuario que ejecute `./run-jupyter-lab.sh`
|
||||
|
||||
### 5. Gestion de notebooks
|
||||
- Notebooks en la carpeta `notebooks/` o subcarpetas
|
||||
- Si un notebook tiene >50 celdas, crear uno nuevo
|
||||
- Nombrar descriptivamente: `01_exploracion.ipynb`, `02_limpieza.ipynb`
|
||||
|
||||
### 6. Gestion de Python
|
||||
- **SIEMPRE usar `uv`** para gestionar dependencias
|
||||
- Anadir paquetes con `uv add nombre_paquete`
|
||||
|
||||
### 7. Acceso al fn_registry
|
||||
- `FN_REGISTRY_ROOT` apunta a la raiz del registry
|
||||
- Para importar funciones Python: `sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))`
|
||||
- Para consultar registry.db: `sqlite3` o `import sqlite3` con la ruta `$FN_REGISTRY_ROOT/registry.db`
|
||||
|
||||
'
|
||||
|
||||
if [ -f "$claude_md" ]; then
|
||||
local existing
|
||||
existing=$(cat "$claude_md")
|
||||
printf '%s\n%s' "$rules" "$existing" > "$claude_md"
|
||||
else
|
||||
echo "$rules" > "$claude_md"
|
||||
fi
|
||||
|
||||
echo "$claude_md"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: write_jupyter_launcher
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "write_jupyter_launcher([project_dir: string]) -> string"
|
||||
description: "Genera un script run-jupyter-lab.sh que lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto y token deshabilitado."
|
||||
tags: [jupyter, launcher, setup, 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/write_jupyter_launcher.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source write_jupyter_launcher.sh
|
||||
path=$(write_jupyter_launcher /home/lucas/analysis/finanzas)
|
||||
echo "Launcher creado en: $path"
|
||||
|
||||
# Luego en otra terminal:
|
||||
# ./run-jupyter-lab.sh [puerto]
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
El launcher generado:
|
||||
- Autodetecta un puerto libre (8888-8899)
|
||||
- Guarda el puerto en `.jupyter-port` para que otros procesos lo lean
|
||||
- Activa el venv automaticamente
|
||||
- Lanza Jupyter Lab en modo `--collaborative` (requiere jupyter-collaboration)
|
||||
- Token y password deshabilitados para acceso local
|
||||
@@ -0,0 +1,65 @@
|
||||
# write_jupyter_launcher
|
||||
# -----------------------
|
||||
# Genera un script run-jupyter-lab.sh en el directorio dado.
|
||||
# El script lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source write_jupyter_launcher.sh
|
||||
# write_jupyter_launcher /path/to/project
|
||||
|
||||
write_jupyter_launcher() {
|
||||
local project_dir="${1:-.}"
|
||||
local launcher="${project_dir}/run-jupyter-lab.sh"
|
||||
|
||||
cat > "$launcher" << 'LAUNCHER'
|
||||
#!/bin/bash
|
||||
# Jupyter Lab — modo colaborativo con autodeteccion de puerto
|
||||
# Generado por write_jupyter_launcher (fn_registry)
|
||||
|
||||
find_free_port() {
|
||||
for port in 8888 8889 8890 8891 8892 8893 8894 8895 8896 8897 8898 8899; do
|
||||
if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
|
||||
! lsof -i:"$port" >/dev/null 2>&1; then
|
||||
echo $port
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo 8888
|
||||
}
|
||||
|
||||
PORT=${1:-$(find_free_port)}
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo $PORT > .jupyter-port
|
||||
|
||||
source .venv/bin/activate 2>/dev/null || true
|
||||
|
||||
if ! python -c "import jupyter_collaboration" 2>/dev/null; then
|
||||
echo "ERROR: jupyter-collaboration no esta instalado"
|
||||
echo "Instala con: uv add jupyter-collaboration"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo " Jupyter Lab + Colaboracion en puerto $PORT"
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Abre: http://localhost:$PORT"
|
||||
echo " Ctrl+C para detener"
|
||||
echo ""
|
||||
|
||||
jupyter lab \
|
||||
--port=$PORT \
|
||||
--no-browser \
|
||||
--ServerApp.token='' \
|
||||
--ServerApp.password='' \
|
||||
--ServerApp.disable_check_xsrf=True \
|
||||
--ServerApp.allow_origin='*' \
|
||||
--ServerApp.root_dir="$(pwd)" \
|
||||
--YDocExtension.ystore_class='ypy_websocket.ystore.TempFileYStore' \
|
||||
--collaborative
|
||||
LAUNCHER
|
||||
|
||||
chmod +x "$launcher"
|
||||
echo "$launcher"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: write_jupyter_registry_kernel
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "write_jupyter_registry_kernel([project_dir: string]) -> string"
|
||||
description: "Genera un script de startup de IPython que autoconfigura FN_REGISTRY_ROOT, sys.path a python/functions del registry, y helpers fn_query/fn_search/fn_code para consultar registry.db desde notebooks."
|
||||
tags: [jupyter, ipython, kernel, registry, setup, 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/write_jupyter_registry_kernel.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source write_jupyter_registry_kernel.sh
|
||||
path=$(write_jupyter_registry_kernel /home/lucas/analysis/finanzas)
|
||||
echo "Startup creado en: $path"
|
||||
```
|
||||
|
||||
Luego en cualquier notebook del proyecto:
|
||||
|
||||
```python
|
||||
# Ya disponible automaticamente al abrir el notebook:
|
||||
|
||||
# Buscar funciones
|
||||
fn_search("finance")
|
||||
|
||||
# Consultar registry.db directamente
|
||||
fn_query("SELECT id, signature FROM functions WHERE domain = ?", ("core",))
|
||||
|
||||
# Ver codigo de una funcion
|
||||
print(fn_code("filter_list_py_core"))
|
||||
|
||||
# Importar funciones Python del registry directamente
|
||||
from core import filter_list, map_list
|
||||
from finance import sma, ema, rsi
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Genera `.ipython/profile_default/startup/00_fn_registry.py` que se ejecuta automaticamente al iniciar cualquier kernel IPython en el proyecto. No requiere imports manuales — las funciones `fn_query`, `fn_search` y `fn_code` estan disponibles inmediatamente en cada notebook.
|
||||
|
||||
El `sys.path` se configura para que cada dominio de `python/functions/` sea importable directamente (`from core import filter_list`).
|
||||
|
||||
Detecta `FN_REGISTRY_ROOT` buscando la raiz del repo git o subiendo directorios hasta encontrar `registry.db`.
|
||||
@@ -0,0 +1,111 @@
|
||||
# write_jupyter_registry_kernel
|
||||
# -------------------------------
|
||||
# Genera un script de startup de IPython que autoconfigura el acceso
|
||||
# al fn_registry en cada notebook: FN_REGISTRY_ROOT, sys.path a
|
||||
# python/functions, y un helper fn_query() para consultar registry.db.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source write_jupyter_registry_kernel.sh
|
||||
# write_jupyter_registry_kernel /path/to/project
|
||||
|
||||
write_jupyter_registry_kernel() {
|
||||
local project_dir="${1:-.}"
|
||||
local startup_dir="${project_dir}/.ipython/profile_default/startup"
|
||||
local registry_root
|
||||
registry_root="$(cd "$project_dir" && cd "$(git -C "$project_dir" rev-parse --show-toplevel 2>/dev/null || echo "../..")" && pwd)"
|
||||
|
||||
# Fallback: si no es git, buscar registry.db subiendo directorios
|
||||
if [ ! -f "$registry_root/registry.db" ] && [ -f "$project_dir/../../registry.db" ]; then
|
||||
registry_root="$(cd "$project_dir/../.." && pwd)"
|
||||
fi
|
||||
|
||||
mkdir -p "$startup_dir"
|
||||
|
||||
cat > "${startup_dir}/00_fn_registry.py" << STARTUP
|
||||
"""
|
||||
fn_registry kernel startup
|
||||
Autoconfigura acceso al registry en cada notebook.
|
||||
Generado por write_jupyter_registry_kernel (fn_registry).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
# ── FN_REGISTRY_ROOT ────────────────────────────────────────
|
||||
FN_REGISTRY_ROOT = Path("${registry_root}")
|
||||
os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)
|
||||
|
||||
# ── sys.path: importar funciones Python del registry ────────
|
||||
_python_functions = FN_REGISTRY_ROOT / "python" / "functions"
|
||||
for _domain in sorted(_python_functions.iterdir()) if _python_functions.exists() else []:
|
||||
if _domain.is_dir() and not _domain.name.startswith("_"):
|
||||
_path = str(_domain)
|
||||
if _path not in sys.path:
|
||||
sys.path.insert(0, _path)
|
||||
|
||||
# Tambien el directorio padre para imports por dominio: from core import filter_list
|
||||
_pf = str(_python_functions)
|
||||
if _pf not in sys.path:
|
||||
sys.path.insert(0, _pf)
|
||||
|
||||
# ── fn_query: consultar registry.db desde el notebook ───────
|
||||
_REGISTRY_DB = FN_REGISTRY_ROOT / "registry.db"
|
||||
|
||||
def fn_query(sql, params=()):
|
||||
"""Ejecuta una consulta SQL sobre registry.db y retorna las filas.
|
||||
|
||||
Ejemplos:
|
||||
fn_query("SELECT id, description FROM functions WHERE domain = ?", ("finance",))
|
||||
fn_query("SELECT id FROM functions_fts WHERE functions_fts MATCH ?", ("slice*",))
|
||||
"""
|
||||
if not _REGISTRY_DB.exists():
|
||||
raise FileNotFoundError(f"registry.db no encontrado en {_REGISTRY_DB}")
|
||||
con = sqlite3.connect(str(_REGISTRY_DB))
|
||||
con.row_factory = sqlite3.Row
|
||||
try:
|
||||
rows = con.execute(sql, params).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
def fn_search(term):
|
||||
"""Busca funciones y tipos en el registry por nombre o descripcion.
|
||||
|
||||
Ejemplo:
|
||||
fn_search("slice")
|
||||
fn_search("finance")
|
||||
"""
|
||||
fts_term = f"name:{term}* OR description:{term}*"
|
||||
functions = fn_query(
|
||||
"SELECT id, kind, purity, lang, description FROM functions "
|
||||
"WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?) "
|
||||
"ORDER BY name", (fts_term,)
|
||||
)
|
||||
types = fn_query(
|
||||
"SELECT id, algebraic, lang, description FROM types "
|
||||
"WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?) "
|
||||
"ORDER BY name", (fts_term,)
|
||||
)
|
||||
return {"functions": functions, "types": types}
|
||||
|
||||
def fn_code(function_id):
|
||||
"""Retorna el codigo fuente de una funcion del registry.
|
||||
|
||||
Ejemplo:
|
||||
print(fn_code("filter_list_py_core"))
|
||||
"""
|
||||
rows = fn_query("SELECT code FROM functions WHERE id = ?", (function_id,))
|
||||
if not rows:
|
||||
raise KeyError(f"Funcion no encontrada: {function_id}")
|
||||
return rows[0]["code"]
|
||||
|
||||
# ── Mensaje de bienvenida ───────────────────────────────────
|
||||
print(f"fn_registry conectado: {FN_REGISTRY_ROOT}")
|
||||
print(f" registry.db: {'OK' if _REGISTRY_DB.exists() else 'NO ENCONTRADO'}")
|
||||
print(f" Python functions: {_pf}")
|
||||
print(f" Helpers: fn_query(), fn_search(), fn_code()")
|
||||
STARTUP
|
||||
|
||||
echo "${startup_dir}/00_fn_registry.py"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: write_mcp_jupyter_config
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "write_mcp_jupyter_config([project_dir: string], [port: int]) -> string"
|
||||
description: "Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server apuntando al venv local y puerto dado. Merge con jq si ya existe."
|
||||
tags: [mcp, jupyter, config, setup, 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/write_mcp_jupyter_config.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source write_mcp_jupyter_config.sh
|
||||
path=$(write_mcp_jupyter_config /home/lucas/analysis/finanzas 8890)
|
||||
echo "Config MCP en: $path"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
El MCP se invoca como modulo Python (`python -m jupyter_mcp_server`) usando el python del venv local, nunca una instalacion global. Si `.mcp.json` ya existe y jq esta disponible, hace merge conservando otros servidores MCP. Sin jq, sobrescribe el archivo.
|
||||
@@ -0,0 +1,57 @@
|
||||
# write_mcp_jupyter_config
|
||||
# -------------------------
|
||||
# Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server.
|
||||
# Usa el python del venv local con -m jupyter_mcp_server.
|
||||
# Hace merge si ya existe .mcp.json (requiere jq).
|
||||
#
|
||||
# USO (sourced):
|
||||
# source write_mcp_jupyter_config.sh
|
||||
# write_mcp_jupyter_config /path/to/project 8888
|
||||
|
||||
write_mcp_jupyter_config() {
|
||||
local project_dir="${1:-.}"
|
||||
local port="${2:-8888}"
|
||||
local mcp_file="${project_dir}/.mcp.json"
|
||||
local abs_project
|
||||
abs_project="$(cd "$project_dir" && pwd)"
|
||||
|
||||
local python_bin="${abs_project}/.venv/bin/python"
|
||||
if [ ! -f "$python_bin" ]; then
|
||||
echo "write_mcp_jupyter_config: python no encontrado en ${python_bin}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verificar que el modulo esta instalado
|
||||
if ! "$python_bin" -c "import jupyter_mcp_server" 2>/dev/null; then
|
||||
echo "write_mcp_jupyter_config: jupyter_mcp_server no instalado en el venv" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local new_config
|
||||
new_config=$(cat << EOF
|
||||
{
|
||||
"mcpServers": {
|
||||
"jupyter": {
|
||||
"command": "${python_bin}",
|
||||
"args": [
|
||||
"-m", "jupyter_mcp_server",
|
||||
"--runtime-url", "http://localhost:${port}",
|
||||
"--start-new-runtime", "false"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
if [ -f "$mcp_file" ] && command -v jq &>/dev/null; then
|
||||
# Merge conservando otros servidores MCP
|
||||
jq -s '.[0] * {mcpServers: ((.[0].mcpServers // {}) * (.[1].mcpServers // {}))}' \
|
||||
"$mcp_file" <(echo "$new_config") > "${mcp_file}.tmp"
|
||||
mv "${mcp_file}.tmp" "$mcp_file"
|
||||
else
|
||||
echo "$new_config" > "$mcp_file"
|
||||
fi
|
||||
|
||||
echo "$mcp_file"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: init_jupyter_analysis
|
||||
kind: pipeline
|
||||
lang: bash
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "init_jupyter_analysis(nombre: string, [...paquetes_extra: string]) -> void"
|
||||
description: "Inicializa un analisis Jupyter completo en analysis/{nombre}/ con venv, paquetes, launcher, MCP y reglas para agentes Claude. Acepta paquetes extra opcionales."
|
||||
tags: [jupyter, analysis, setup, pipeline, bash, launcher]
|
||||
uses_functions:
|
||||
- assert_command_exists_bash_shell
|
||||
- find_free_port_bash_shell
|
||||
- init_uv_venv_bash_infra
|
||||
- uv_add_packages_bash_infra
|
||||
- write_jupyter_launcher_bash_infra
|
||||
- write_mcp_jupyter_config_bash_infra
|
||||
- write_claude_jupyter_rules_bash_infra
|
||||
- write_jupyter_registry_kernel_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/init_jupyter_analysis.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Analisis basico
|
||||
./init_jupyter_analysis.sh finanzas
|
||||
|
||||
# Con paquetes extra
|
||||
./init_jupyter_analysis.sh duckdb polars duckdb
|
||||
./init_jupyter_analysis.sh ml scikit-learn torch
|
||||
|
||||
# Via fn run
|
||||
fn run init_jupyter_analysis finanzas
|
||||
fn run init_jupyter_analysis ml scikit-learn torch
|
||||
```
|
||||
|
||||
## Flujo
|
||||
|
||||
1. `assert_command_exists` — verifica que uv o python3 estan disponibles
|
||||
2. Crea estructura `analysis/{nombre}/notebooks/` y `analysis/{nombre}/data/`
|
||||
3. `init_uv_venv` — crea venv en `analysis/{nombre}/.venv/`
|
||||
4. `uv_add_packages` — instala jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib + extras
|
||||
5. `write_jupyter_launcher` — genera `run-jupyter-lab.sh` con modo colaborativo
|
||||
6. `find_free_port` + `write_mcp_jupyter_config` — detecta puerto libre y genera `.mcp.json`
|
||||
7. `write_claude_jupyter_rules` — genera `.claude/CLAUDE.md` con reglas de agente
|
||||
8. `write_jupyter_registry_kernel` — genera IPython startup con `fn_query`, `fn_search`, `fn_code` y acceso a `python/functions/`
|
||||
|
||||
## Notas
|
||||
|
||||
Cada analisis es independiente (propio venv, propio Jupyter, propio MCP). Mismo patron que `apps/` pero para exploraciones no reutilizables.
|
||||
|
||||
El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecucion.
|
||||
|
||||
Paquetes base siempre incluidos: jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib. Los paquetes extra se añaden a estos.
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
# init_jupyter_analysis
|
||||
# ----------------------
|
||||
# Inicializa un analisis Jupyter completo en analysis/{nombre}/.
|
||||
# Compone: assert_command_exists + find_free_port + init_uv_venv +
|
||||
# uv_add_packages + write_jupyter_launcher +
|
||||
# write_mcp_jupyter_config + write_claude_jupyter_rules +
|
||||
# write_jupyter_registry_kernel
|
||||
#
|
||||
# USO:
|
||||
# ./init_jupyter_analysis.sh <nombre> [paquetes_extra...]
|
||||
#
|
||||
# EJEMPLOS:
|
||||
# ./init_jupyter_analysis.sh finanzas
|
||||
# ./init_jupyter_analysis.sh duckdb polars duckdb
|
||||
# ./init_jupyter_analysis.sh ml scikit-learn torch
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
# Source funciones atomicas
|
||||
source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
|
||||
source "$REGISTRY_ROOT/bash/functions/shell/find_free_port.sh"
|
||||
source "$REGISTRY_ROOT/bash/functions/infra/init_uv_venv.sh"
|
||||
source "$REGISTRY_ROOT/bash/functions/infra/uv_add_packages.sh"
|
||||
source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_launcher.sh"
|
||||
source "$REGISTRY_ROOT/bash/functions/infra/write_mcp_jupyter_config.sh"
|
||||
source "$REGISTRY_ROOT/bash/functions/infra/write_claude_jupyter_rules.sh"
|
||||
source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_registry_kernel.sh"
|
||||
|
||||
# ── Argumentos ──────────────────────────────────────────────
|
||||
|
||||
NOMBRE="${1:-}"
|
||||
if [ -z "$NOMBRE" ]; then
|
||||
echo "Uso: $0 <nombre> [paquetes_extra...]" >&2
|
||||
echo " Ejemplo: $0 finanzas polars" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
EXTRA_PACKAGES=("$@")
|
||||
|
||||
ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}"
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " INIT JUPYTER ANALYSIS: ${NOMBRE}"
|
||||
echo " Directorio: ${ANALYSIS_DIR}"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# ── 1. Verificar herramientas ───────────────────────────────
|
||||
|
||||
echo "[1/8] Verificando herramientas..."
|
||||
assert_command_exists uv || assert_command_exists python3
|
||||
echo " OK"
|
||||
|
||||
# ── 2. Crear estructura de carpetas ─────────────────────────
|
||||
|
||||
echo "[2/8] Creando estructura..."
|
||||
mkdir -p "$ANALYSIS_DIR/notebooks" "$ANALYSIS_DIR/data"
|
||||
echo " ${ANALYSIS_DIR}/notebooks/"
|
||||
echo " ${ANALYSIS_DIR}/data/"
|
||||
|
||||
# ── 3. Crear venv ───────────────────────────────────────────
|
||||
|
||||
echo "[3/8] Inicializando venv..."
|
||||
venv_path=$(init_uv_venv "$ANALYSIS_DIR")
|
||||
echo " $venv_path"
|
||||
|
||||
# ── 4. Instalar paquetes ────────────────────────────────────
|
||||
|
||||
echo "[4/8] Instalando paquetes..."
|
||||
BASE_PACKAGES=(jupyter jupyterlab jupyter-collaboration jupyter-mcp-server pandas numpy matplotlib)
|
||||
ALL_PACKAGES=("${BASE_PACKAGES[@]}" "${EXTRA_PACKAGES[@]}")
|
||||
uv_add_packages "$ANALYSIS_DIR" "${ALL_PACKAGES[@]}"
|
||||
echo " Instalados: ${ALL_PACKAGES[*]}"
|
||||
|
||||
# ── 5. Generar launcher ─────────────────────────────────────
|
||||
|
||||
echo "[5/8] Generando launcher..."
|
||||
launcher=$(write_jupyter_launcher "$ANALYSIS_DIR")
|
||||
echo " $launcher"
|
||||
|
||||
# ── 6. Configurar MCP ───────────────────────────────────────
|
||||
|
||||
echo "[6/8] Configurando MCP..."
|
||||
port=$(find_free_port 8888 8899)
|
||||
mcp_config=$(write_mcp_jupyter_config "$ANALYSIS_DIR" "$port")
|
||||
echo " $mcp_config (puerto: $port)"
|
||||
|
||||
# ── 7. Reglas para agentes ──────────────────────────────────
|
||||
|
||||
echo "[7/8] Escribiendo reglas Claude..."
|
||||
rules=$(write_claude_jupyter_rules "$ANALYSIS_DIR")
|
||||
echo " $rules"
|
||||
|
||||
# ── 8. Kernel startup con acceso al registry ────────────────
|
||||
|
||||
echo "[8/8] Configurando kernel con acceso al registry..."
|
||||
kernel_startup=$(write_jupyter_registry_kernel "$ANALYSIS_DIR")
|
||||
echo " $kernel_startup"
|
||||
|
||||
# ── Resumen ─────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " ANALISIS '${NOMBRE}' LISTO"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Pasos siguientes:"
|
||||
echo ""
|
||||
echo " 1. En otra terminal:"
|
||||
echo " cd ${ANALYSIS_DIR} && ./run-jupyter-lab.sh"
|
||||
echo ""
|
||||
echo " 2. Abrir Claude en el analisis:"
|
||||
echo " cd ${ANALYSIS_DIR} && claude"
|
||||
echo ""
|
||||
echo " 3. Abrir en navegador: http://localhost:${port}"
|
||||
echo ""
|
||||
echo " FN_REGISTRY_ROOT=${REGISTRY_ROOT}"
|
||||
echo ""
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: exit_with_status
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: shell
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "exit_with_status(total_steps: int, ok_steps: int, failed_steps: int) -> int"
|
||||
description: "Calcula el exit code estandar (0=success, 1=failure, 2=partial) a partir de contadores de pasos. Si failed_steps=0 imprime 0 y sale con 0. Si ok_steps=0 imprime 1 y sale con 1. Si hay ambos imprime 2 y sale con 2."
|
||||
tags: [execution, status, exit-code, standard]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/shell/exit_with_status.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/shell/exit_with_status.sh
|
||||
|
||||
exit_with_status 5 5 0 # stdout: 0, exit code: 0
|
||||
exit_with_status 5 0 5 # stdout: 1, exit code: 1
|
||||
exit_with_status 5 3 2 # stdout: 2, exit code: 2
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Funcion pura: no realiza I/O de sistema, no modifica estado global, no lee variables de entorno. El argumento `total_steps` se recibe para completitud semantica pero la logica solo depende de `ok_steps` y `failed_steps`. El valor se imprime a stdout ademas de usarse como exit code, de modo que el caller puede capturarlo con `$(exit_with_status ...)` o evaluar directamente con `exit_with_status ... ; echo $?`.
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# exit_with_status — calcula el exit code estandar a partir de contadores de pasos
|
||||
|
||||
# exit_with_status <total_steps> <ok_steps> <failed_steps>
|
||||
#
|
||||
# Calcula el exit code estandar:
|
||||
# 0 — todos los pasos exitosos (failed_steps == 0)
|
||||
# 1 — todos los pasos fallaron (ok_steps == 0)
|
||||
# 2 — resultado parcial (hay ok y failed)
|
||||
#
|
||||
# Imprime el codigo a stdout y sale con ese codigo.
|
||||
exit_with_status() {
|
||||
local total_steps="$1"
|
||||
local ok_steps="$2"
|
||||
local failed_steps="$3"
|
||||
|
||||
local code
|
||||
|
||||
if [[ "$failed_steps" -eq 0 ]]; then
|
||||
code=0
|
||||
elif [[ "$ok_steps" -eq 0 ]]; then
|
||||
code=1
|
||||
else
|
||||
code=2
|
||||
fi
|
||||
|
||||
echo "$code"
|
||||
return "$code"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: find_free_port
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: shell
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "find_free_port([start_port: int], [end_port: int]) -> int"
|
||||
description: "Busca el primer puerto TCP libre en un rango dado usando ss y lsof. Retorna el numero de puerto a stdout."
|
||||
tags: [network, port, shell, utility]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/shell/find_free_port.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source find_free_port.sh
|
||||
port=$(find_free_port 8888 8899)
|
||||
echo "Puerto libre: $port"
|
||||
|
||||
# Con defaults (8888-8899)
|
||||
port=$(find_free_port)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa `ss -tln` como primer intento y `lsof` como fallback. Ambos deben confirmar que el puerto no esta en uso. Si ningun puerto esta libre en el rango, sale con exit code 1.
|
||||
@@ -0,0 +1,25 @@
|
||||
# find_free_port
|
||||
# ---------------
|
||||
# Busca el primer puerto libre en un rango dado.
|
||||
# Imprime el puerto encontrado a stdout.
|
||||
# Sale con exit code 1 si no encuentra ninguno.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source find_free_port.sh
|
||||
# port=$(find_free_port 8888 8899)
|
||||
|
||||
find_free_port() {
|
||||
local start="${1:-8888}"
|
||||
local end="${2:-8899}"
|
||||
|
||||
for ((port=start; port<=end; port++)); do
|
||||
if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
|
||||
! lsof -i:"$port" >/dev/null 2>&1; then
|
||||
echo "$port"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "find_free_port: no se encontro puerto libre en rango ${start}-${end}" >&2
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: report_execution_json
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: shell
|
||||
version: "2.0.0"
|
||||
purity: pure
|
||||
signature: "report_execution_json(flow_name: string, status: string, exit_code: int, started_at: string, ended_at: string, duration_ms: int, steps_file: string) -> string"
|
||||
description: "Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry (docs/execution_standard.md). Recibe los metadatos del flujo y un archivo TSV con resultados de pasos (columnas: name, action, status, elapsed_ms, output, error). Imprime el JSON completo a stdout. Usa jq si esta disponible, con fallback a printf. Funcion pura: sin efectos secundarios ni I/O adicional."
|
||||
tags: [execution, json, report, standard, shell, pure]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/shell/report_execution_json.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/shell/report_execution_json.sh
|
||||
|
||||
# Crear archivo de pasos (TSV sin cabecera, 6 columnas)
|
||||
cat > /tmp/steps.tsv <<'EOF'
|
||||
check_db command ok 10 exists
|
||||
backup command ok 450 done
|
||||
push command error 2540 remote rejected
|
||||
EOF
|
||||
|
||||
report_execution_json \
|
||||
"backup_db" "partial" 2 \
|
||||
"2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 \
|
||||
/tmp/steps.tsv
|
||||
```
|
||||
|
||||
Output esperado:
|
||||
|
||||
```json
|
||||
{"name":"backup_db","status":"partial","exit_code":2,"started_at":"2026-04-01T02:00:00Z","ended_at":"2026-04-01T02:00:03Z","duration_ms":3000,"steps_total":3,"steps_ok":2,"steps_failed":1,"steps":[{"name":"check_db","action":"command","status":"ok","elapsed_ms":10,"output":"exists"},{"name":"backup","action":"command","status":"ok","elapsed_ms":450,"output":"done"},{"name":"push","action":"command","status":"error","elapsed_ms":2540,"error":"remote rejected"}]}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Funcion pura: solo lee el archivo de pasos y escribe JSON a stdout, sin efectos secundarios ni I/O adicional mas alla de leer el steps_file.
|
||||
|
||||
Formato TSV: 6 columnas separadas por tabulador real (sin cabecera): name, action, status (ok|error), elapsed_ms, output, error. Los campos output y error pueden estar vacios; se omiten del JSON si no tienen valor, siguiendo el estandar de output estructurado.
|
||||
|
||||
El caller es responsable de calcular status (success|failure|partial) y exit_code (0|1|2) segun las reglas del estandar. Ver exit_with_status_bash_shell para esa logica.
|
||||
|
||||
Compatibilidad dual: con jq construye el JSON de forma robusta (maneja caracteres especiales y saltos de linea en output/error). Sin jq, el fallback con printf escapa backslash, comillas dobles y caracteres de control basicos.
|
||||
|
||||
Puede ejecutarse directamente: `bash report_execution_json.sh <args>`.
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env bash
|
||||
# report_execution_json — Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry.
|
||||
#
|
||||
# USO (sourced):
|
||||
# source report_execution_json.sh
|
||||
# report_execution_json "backup_db" "partial" 2 \
|
||||
# "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
|
||||
#
|
||||
# USO (ejecutado directamente):
|
||||
# bash report_execution_json.sh "backup_db" "partial" 2 \
|
||||
# "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
|
||||
#
|
||||
# FORMATO steps_file (TSV sin cabecera, 6 columnas):
|
||||
# name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
|
||||
# Los campos output y error pueden estar vacios.
|
||||
# status valido: ok | error
|
||||
#
|
||||
# NOTA sobre IFS y tabs: bash trata tab como whitespace en IFS y colapsa
|
||||
# campos vacios consecutivos. Se usa 'cut -f N' para parsear cada columna
|
||||
# de forma correcta cuando hay campos vacios entre tabs.
|
||||
|
||||
report_execution_json() {
|
||||
local flow_name="$1"
|
||||
local status="$2"
|
||||
local exit_code="$3"
|
||||
local started_at="$4"
|
||||
local ended_at="$5"
|
||||
local duration_ms="$6"
|
||||
local steps_file="$7"
|
||||
|
||||
if [[ -z "$flow_name" || -z "$status" || -z "$exit_code" || \
|
||||
-z "$started_at" || -z "$ended_at" || -z "$duration_ms" || \
|
||||
-z "$steps_file" ]]; then
|
||||
echo "report_execution_json: uso: report_execution_json <flow_name> <status> <exit_code> <started_at> <ended_at> <duration_ms> <steps_file>" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$steps_file" ]]; then
|
||||
echo "report_execution_json: archivo de pasos no encontrado: $steps_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
_report_execution_json_jq \
|
||||
"$flow_name" "$status" "$exit_code" \
|
||||
"$started_at" "$ended_at" "$duration_ms" "$steps_file"
|
||||
else
|
||||
_report_execution_json_printf \
|
||||
"$flow_name" "$status" "$exit_code" \
|
||||
"$started_at" "$ended_at" "$duration_ms" "$steps_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Implementacion con jq ---
|
||||
|
||||
_report_execution_json_jq() {
|
||||
local flow_name="$1" status="$2" exit_code="$3"
|
||||
local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"
|
||||
|
||||
local steps_ok=0 steps_failed=0
|
||||
local steps_json="[]"
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
|
||||
local s_name s_action s_status s_elapsed s_output s_error
|
||||
s_name=$(printf '%s' "$line" | cut -f1)
|
||||
s_action=$(printf '%s' "$line" | cut -f2)
|
||||
s_status=$(printf '%s' "$line" | cut -f3)
|
||||
s_elapsed=$(printf '%s' "$line" | cut -f4)
|
||||
s_output=$(printf '%s' "$line" | cut -f5)
|
||||
s_error=$(printf '%s' "$line" | cut -f6)
|
||||
|
||||
[[ -z "$s_name" ]] && continue
|
||||
|
||||
local step_obj
|
||||
step_obj=$(jq -n \
|
||||
--arg name "$s_name" \
|
||||
--arg action "$s_action" \
|
||||
--arg st "$s_status" \
|
||||
--argjson ms "${s_elapsed:-0}" \
|
||||
--arg output "$s_output" \
|
||||
--arg error "$s_error" \
|
||||
'{name: $name, action: $action, status: $st, elapsed_ms: $ms}
|
||||
+ (if $output != "" then {output: $output} else {} end)
|
||||
+ (if $error != "" then {error: $error} else {} end)')
|
||||
|
||||
steps_json=$(printf '%s' "$steps_json" | jq --argjson step "$step_obj" '. + [$step]')
|
||||
|
||||
if [[ "$s_status" == "ok" ]]; then
|
||||
((steps_ok++))
|
||||
else
|
||||
((steps_failed++))
|
||||
fi
|
||||
done < "$steps_file"
|
||||
|
||||
local steps_total=$(( steps_ok + steps_failed ))
|
||||
|
||||
jq -n \
|
||||
--arg name "$flow_name" \
|
||||
--arg st "$status" \
|
||||
--argjson exit_code "$exit_code" \
|
||||
--arg started_at "$started_at" \
|
||||
--arg ended_at "$ended_at" \
|
||||
--argjson duration_ms "$duration_ms" \
|
||||
--argjson total "$steps_total" \
|
||||
--argjson ok "$steps_ok" \
|
||||
--argjson failed "$steps_failed" \
|
||||
--argjson steps "$steps_json" \
|
||||
'{
|
||||
name: $name,
|
||||
status: $st,
|
||||
exit_code: $exit_code,
|
||||
started_at: $started_at,
|
||||
ended_at: $ended_at,
|
||||
duration_ms: $duration_ms,
|
||||
steps_total: $total,
|
||||
steps_ok: $ok,
|
||||
steps_failed: $failed,
|
||||
steps: $steps
|
||||
}'
|
||||
}
|
||||
|
||||
# --- Implementacion con printf (fallback sin jq) ---
|
||||
|
||||
_json_escape_rj() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
_report_execution_json_printf() {
|
||||
local flow_name="$1" status="$2" exit_code="$3"
|
||||
local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"
|
||||
|
||||
local steps_ok=0 steps_failed=0
|
||||
local steps_parts=()
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
|
||||
local s_name s_action s_status s_elapsed s_output s_error
|
||||
s_name=$(printf '%s' "$line" | cut -f1)
|
||||
s_action=$(printf '%s' "$line" | cut -f2)
|
||||
s_status=$(printf '%s' "$line" | cut -f3)
|
||||
s_elapsed=$(printf '%s' "$line" | cut -f4)
|
||||
s_output=$(printf '%s' "$line" | cut -f5)
|
||||
s_error=$(printf '%s' "$line" | cut -f6)
|
||||
|
||||
[[ -z "$s_name" ]] && continue
|
||||
|
||||
local part
|
||||
part=$(printf '{"name":"%s","action":"%s","status":"%s","elapsed_ms":%s' \
|
||||
"$(_json_escape_rj "$s_name")" \
|
||||
"$(_json_escape_rj "$s_action")" \
|
||||
"$(_json_escape_rj "$s_status")" \
|
||||
"${s_elapsed:-0}")
|
||||
|
||||
if [[ -n "$s_output" ]]; then
|
||||
part+=",\"output\":\"$(_json_escape_rj "$s_output")\""
|
||||
fi
|
||||
if [[ -n "$s_error" ]]; then
|
||||
part+=",\"error\":\"$(_json_escape_rj "$s_error")\""
|
||||
fi
|
||||
part+="}"
|
||||
|
||||
steps_parts+=("$part")
|
||||
|
||||
if [[ "$s_status" == "ok" ]]; then
|
||||
((steps_ok++))
|
||||
else
|
||||
((steps_failed++))
|
||||
fi
|
||||
done < "$steps_file"
|
||||
|
||||
local steps_total=$(( steps_ok + steps_failed ))
|
||||
|
||||
local steps_array="["
|
||||
local first=1
|
||||
for part in "${steps_parts[@]}"; do
|
||||
if [[ $first -eq 1 ]]; then
|
||||
steps_array+="$part"
|
||||
first=0
|
||||
else
|
||||
steps_array+=",$part"
|
||||
fi
|
||||
done
|
||||
steps_array+="]"
|
||||
|
||||
printf '{"name":"%s","status":"%s","exit_code":%s,"started_at":"%s","ended_at":"%s","duration_ms":%s,"steps_total":%s,"steps_ok":%s,"steps_failed":%s,"steps":%s}\n' \
|
||||
"$(_json_escape_rj "$flow_name")" \
|
||||
"$(_json_escape_rj "$status")" \
|
||||
"$exit_code" \
|
||||
"$(_json_escape_rj "$started_at")" \
|
||||
"$(_json_escape_rj "$ended_at")" \
|
||||
"$duration_ms" \
|
||||
"$steps_total" \
|
||||
"$steps_ok" \
|
||||
"$steps_failed" \
|
||||
"$steps_array"
|
||||
}
|
||||
|
||||
# Permitir ejecucion directa (no solo sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
report_execution_json "$@"
|
||||
fi
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: run_steps
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: shell
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "run_steps(yaml_file: string, [--strict]) -> string"
|
||||
description: "Ejecuta pasos de un YAML generico donde cada step tiene action=command. Lee el YAML con yq, ejecuta cada paso secuencialmente con timeout configurable, captura exit code y output, respeta continue_on_error, y al final reporta JSON estandar a stdout via report_execution_json. Sale con exit code 0 (success), 1 (failure) o 2 (partial). Con --strict mapea partial a failure."
|
||||
tags: [execution, yaml, runner, standard, pipeline]
|
||||
uses_functions: [exit_with_status_bash_shell, report_execution_json_bash_shell]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/shell/run_steps.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```yaml
|
||||
# /tmp/test_flow.yaml
|
||||
name: test_flow
|
||||
steps:
|
||||
- name: check
|
||||
action: command
|
||||
command: "echo hello"
|
||||
- name: list
|
||||
action: command
|
||||
command: "ls /tmp"
|
||||
continue_on_error: true
|
||||
- name: slow_step
|
||||
action: command
|
||||
command: "sleep 2"
|
||||
timeout_ms: 1000
|
||||
continue_on_error: true
|
||||
```
|
||||
|
||||
```bash
|
||||
source bash/functions/shell/run_steps.sh
|
||||
|
||||
run_steps /tmp/test_flow.yaml
|
||||
# Imprime JSON a stdout:
|
||||
# {
|
||||
# "name": "test_flow",
|
||||
# "status": "partial",
|
||||
# "exit_code": 2,
|
||||
# "started_at": "2026-04-01T10:00:00Z",
|
||||
# "ended_at": "2026-04-01T10:00:01Z",
|
||||
# "duration_ms": 1250,
|
||||
# "steps_total": 3,
|
||||
# "steps_ok": 2,
|
||||
# "steps_failed": 1,
|
||||
# "steps": [
|
||||
# {"name": "check", "action": "command", "status": "ok", "elapsed_ms": 10, "output": "hello"},
|
||||
# {"name": "list", "action": "command", "status": "ok", "elapsed_ms": 15},
|
||||
# {"name": "slow_step", "action": "command", "status": "error", "elapsed_ms": 1001, "error": "timeout: comando excedio 1000ms"}
|
||||
# ]
|
||||
# }
|
||||
# Sale con exit code 2 (partial)
|
||||
|
||||
run_steps /tmp/test_flow.yaml --strict
|
||||
# Igual pero status="failure" y exit code=1
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Requiere `yq` (v4+, modo expression `-e`) y `jq` (o la implementacion printf de report_execution_json). Solo soporta `action: command` — otros actions se marcan como error del paso. El campo `timeout_ms` por defecto es 30000 (30s); si el comando excede el timeout, `timeout(1)` sale con exit code 124 y se registra el error correspondiente. El output del comando (stdout+stderr combinados) se sanitiza reemplazando tabuladores y saltos de linea por espacios antes de escribir al TSV temporal. Las funciones `exit_with_status` y `report_execution_json` se cargan via `source` desde el mismo directorio que `run_steps.sh`.
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
# run_steps — ejecuta pasos de un YAML generico (action=command)
|
||||
|
||||
# run_steps <yaml_file> [--strict]
|
||||
#
|
||||
# Lee un YAML con la estructura:
|
||||
# name: <run_name>
|
||||
# steps:
|
||||
# - name: <step_name>
|
||||
# action: command
|
||||
# command: "<shell_command>"
|
||||
# continue_on_error: true|false # opcional, default false
|
||||
# timeout_ms: 30000 # opcional, default 30000
|
||||
#
|
||||
# Para cada paso de action=command:
|
||||
# - Ejecuta el command con timeout (timeout_ms ms)
|
||||
# - Captura exit code, stdout+stderr y elapsed time
|
||||
# - Si falla y continue_on_error=false → aborta
|
||||
# - Acumula resultados en TSV temporal
|
||||
#
|
||||
# Al final:
|
||||
# - Llama a report_execution_json para generar JSON a stdout
|
||||
# - Llama a exit_with_status para determinar el exit code
|
||||
#
|
||||
# --strict: mapea partial (2) a failure (1) en status y exit code
|
||||
#
|
||||
# Requiere: yq, jq (jq opcional si report_execution_json tiene fallback printf)
|
||||
run_steps() {
|
||||
local yaml_file="$1"
|
||||
local strict=0
|
||||
|
||||
if [[ "${2:-}" == "--strict" ]]; then
|
||||
strict=1
|
||||
fi
|
||||
|
||||
# --- validaciones previas ---
|
||||
if [[ -z "$yaml_file" ]]; then
|
||||
echo "run_steps: yaml_file requerido" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$yaml_file" ]]; then
|
||||
echo "run_steps: archivo '$yaml_file' no existe" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v yq &>/dev/null; then
|
||||
echo "run_steps: yq no encontrado en PATH" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- cargar funciones del estandar ---
|
||||
local script_dir
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=bash/functions/shell/exit_with_status.sh
|
||||
source "$script_dir/exit_with_status.sh"
|
||||
# shellcheck source=bash/functions/shell/report_execution_json.sh
|
||||
source "$script_dir/report_execution_json.sh"
|
||||
|
||||
# --- leer nombre del run ---
|
||||
local run_name
|
||||
run_name=$(yq e '.name // "unnamed"' "$yaml_file")
|
||||
|
||||
# --- contar pasos ---
|
||||
local step_count
|
||||
step_count=$(yq e '.steps | length' "$yaml_file")
|
||||
|
||||
if [[ -z "$step_count" || "$step_count" -eq 0 ]]; then
|
||||
echo "run_steps: el YAML no tiene steps" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- archivo TSV temporal para acumular resultados ---
|
||||
# columnas: name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
|
||||
local tsv_file
|
||||
tsv_file=$(mktemp /tmp/run_steps_XXXXXX.tsv)
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -f '$tsv_file'" EXIT
|
||||
|
||||
local ok_steps=0
|
||||
local failed_steps=0
|
||||
local started_at
|
||||
started_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
local t_run_start
|
||||
t_run_start=$(date +%s%3N)
|
||||
|
||||
# --- iterar sobre cada paso ---
|
||||
local i
|
||||
for (( i=0; i<step_count; i++ )); do
|
||||
local step_name step_action step_command step_continue step_timeout_ms
|
||||
|
||||
step_name=$(yq e ".steps[$i].name // \"step_$i\"" "$yaml_file")
|
||||
step_action=$(yq e ".steps[$i].action // \"command\"" "$yaml_file")
|
||||
step_command=$(yq e ".steps[$i].command // \"\"" "$yaml_file")
|
||||
step_continue=$(yq e ".steps[$i].continue_on_error // false" "$yaml_file")
|
||||
step_timeout_ms=$(yq e ".steps[$i].timeout_ms // 30000" "$yaml_file")
|
||||
|
||||
# convertir timeout de ms a segundos enteros (timeout(1) usa segundos)
|
||||
local timeout_sec
|
||||
timeout_sec=$(( (step_timeout_ms + 999) / 1000 ))
|
||||
|
||||
# solo soportamos action=command
|
||||
if [[ "$step_action" != "command" ]]; then
|
||||
local err_msg="action '$step_action' no soportada (solo command)"
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$step_name" "$step_action" "error" "0" "" "$err_msg" >> "$tsv_file"
|
||||
failed_steps=$((failed_steps + 1))
|
||||
|
||||
if [[ "$step_continue" != "true" ]]; then
|
||||
echo "run_steps: paso '$step_name' — $err_msg — abortando" >&2
|
||||
break
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -z "$step_command" ]]; then
|
||||
local err_msg="campo 'command' vacio en paso '$step_name'"
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$step_name" "command" "error" "0" "" "$err_msg" >> "$tsv_file"
|
||||
failed_steps=$((failed_steps + 1))
|
||||
|
||||
if [[ "$step_continue" != "true" ]]; then
|
||||
echo "run_steps: $err_msg — abortando" >&2
|
||||
break
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
# ejecutar con timeout y capturar output + tiempos
|
||||
local t_start t_end elapsed_ms step_output step_exit
|
||||
|
||||
t_start=$(date +%s%3N)
|
||||
step_output=$(timeout "$timeout_sec" bash -c "$step_command" 2>&1)
|
||||
step_exit=$?
|
||||
t_end=$(date +%s%3N)
|
||||
elapsed_ms=$(( t_end - t_start ))
|
||||
|
||||
# timeout(1) sale con 124 cuando agota el tiempo
|
||||
local step_error=""
|
||||
if [[ "$step_exit" -eq 124 ]]; then
|
||||
step_error="timeout: comando excedio ${step_timeout_ms}ms"
|
||||
fi
|
||||
|
||||
# escapar tabuladores y saltos de linea en el output para el TSV
|
||||
local safe_output
|
||||
safe_output=$(printf '%s' "$step_output" | tr '\t' ' ' | tr '\n' ' ')
|
||||
|
||||
if [[ "$step_exit" -eq 0 ]]; then
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$step_name" "command" "ok" "$elapsed_ms" "$safe_output" "" >> "$tsv_file"
|
||||
ok_steps=$((ok_steps + 1))
|
||||
else
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$step_name" "command" "error" "$elapsed_ms" "$safe_output" "$step_error" >> "$tsv_file"
|
||||
failed_steps=$((failed_steps + 1))
|
||||
|
||||
if [[ "$step_continue" != "true" ]]; then
|
||||
echo "run_steps: paso '$step_name' fallo (exit $step_exit) — abortando" >&2
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
local t_run_end
|
||||
t_run_end=$(date +%s%3N)
|
||||
local total_duration_ms=$(( t_run_end - t_run_start ))
|
||||
local ended_at
|
||||
ended_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
local total_steps=$(( ok_steps + failed_steps ))
|
||||
|
||||
# --- calcular status y exit code ---
|
||||
local status_code
|
||||
status_code=$(exit_with_status "$total_steps" "$ok_steps" "$failed_steps")
|
||||
|
||||
local run_status
|
||||
case "$status_code" in
|
||||
0) run_status="success" ;;
|
||||
1) run_status="failure" ;;
|
||||
2)
|
||||
if [[ "$strict" -eq 1 ]]; then
|
||||
run_status="failure"
|
||||
status_code=1
|
||||
else
|
||||
run_status="partial"
|
||||
fi
|
||||
;;
|
||||
*) run_status="failure"; status_code=1 ;;
|
||||
esac
|
||||
|
||||
# --- generar JSON a stdout ---
|
||||
report_execution_json \
|
||||
"$run_name" \
|
||||
"$run_status" \
|
||||
"$status_code" \
|
||||
"$started_at" \
|
||||
"$ended_at" \
|
||||
"$total_duration_ms" \
|
||||
"$tsv_file"
|
||||
|
||||
return "$status_code"
|
||||
}
|
||||
Reference in New Issue
Block a user