merge: quick/fn-run-types-metabase — fn run multi-lenguaje, tipos Go unificados, metabase setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 23:23:45 +01:00
96 changed files with 1934 additions and 113 deletions
+48 -4
View File
@@ -55,9 +55,12 @@ sqlite3 registry.db ".schema"
```
fn-registry/
functions/{domain}/ # .go + .md por funcion (core, finance, datascience, cybersecurity)
functions/{domain}/ # .go + .md por funcion Y tipo Go (core, finance, datascience, cybersecurity)
functions/pipelines/ # Composiciones, siempre impuras
types/{domain}/ # .go + .md por tipo
types/{domain}/ # Solo .md de tipos (los .go viven en functions/{domain}/)
python/functions/ # .py + .md por funcion Python
python/types/ # .py + .md por tipo Python
bash/functions/ # .sh + .md por funcion Bash (core, infra, io, shell)
frontend/ # pnpm + vite + react + tailwind + shadcn
frontend/functions/ # .tsx/.ts + .md (core para TS puro, ui para componentes React)
frontend/types/ # .ts + .md por tipo
@@ -91,6 +94,23 @@ fn list [-d domain] [-k kind]
fn show <id>
fn add -k function # Template
# Ejecutar funciones y pipelines (fn run)
fn run <id_or_name> [args...] # Ejecuta por ID o nombre
fn run init_metabase --project test # Go pipeline (go run .)
fn run setup_metabase_volume # Bash pipeline (bash <file>)
fn run metabase_setup_py_infra # Python (python/.venv/bin/python3 <file>)
fn run my_component_ts_core # TypeScript (frontend/node_modules/.bin/tsx <file>)
fn run filter_slice_go_core # Go function con tests (go test -v)
fn run docker_pull_image_go_infra # Go function sin tests (go vet)
# Despacho por lenguaje:
# go (con main.go en dir) → go run .
# go (con tests) → go test -v -count=1 -tags fts5 ./pkg/
# go (sin tests) → go vet -tags fts5 ./pkg/
# py → python/.venv/bin/python3 <file>
# bash → bash <file>
# ts → frontend/node_modules/.bin/tsx <file>
# Si el nombre es ambiguo, muestra los IDs para desambiguar.
# Proposals
fn proposal add --kind new_function --title "..." --created-by agent [--target-id <id>]
fn proposal list [-k kind] [-s status]
@@ -110,12 +130,30 @@ fn ops assertion result add|list
`FN_REGISTRY_ROOT` env var permite que `fn ops` acceda a registry.db desde cualquier directorio.
### Uso de fn run por agentes
`fn run` permite ejecutar directamente funciones y pipelines del registry desde la terminal. Usar para:
- Lanzar pipelines con sus argumentos: `./fn run init_metabase --project fn_registry`
- Correr tests de funciones Go: `./fn run filter_slice_go_core`
- Ejecutar scripts Python/Bash del registry sin montar paths manualmente
- Verificar que funciones Go compilan correctamente (go vet)
Entornos usados automaticamente:
- Python: `python/.venv/bin/python3` (venv del proyecto)
- TypeScript: `frontend/node_modules/.bin/tsx` (node del proyecto)
- Go: `go run .` / `go test` / `go vet` con `CGO_ENABLED=1 -tags fts5`
- Bash: `bash` del sistema
---
## Añadir funciones
1. Consulta la BD para verificar que no existe algo similar
2. Crea dos archivos: `functions/{domain}/{name}.go` + `functions/{domain}/{name}.md`
2. Crea dos archivos segun el lenguaje:
- Go: `functions/{domain}/{name}.go` + `.md`
- Python: `python/functions/{domain}/{name}.py` + `.md`
- Bash: `bash/functions/{domain}/{name}.sh` + `.md`
- TypeScript: `frontend/functions/{domain}/{name}.ts` + `.md`
3. Ejecuta `./fn index` y verifica con `./fn show {id}`
Frontmatter del .md — ver template completo en `docs/templates/` o con `fn add -k function`.
@@ -132,7 +170,13 @@ Reglas de integridad (el indexer las valida):
## Añadir tipos
Dos archivos: `types/{domain}/{name}.go` + `types/{domain}/{name}.md`. Ver template en `docs/templates/`.
Dos archivos en directorios separados:
- **Codigo Go:** `functions/{domain}/{name}.go` (junto a las funciones, mismo paquete Go)
- **Metadata .md:** `types/{domain}/{name}.md` con `file_path` apuntando a `functions/{domain}/{name}.go`
Los `.go` de tipos viven en `functions/{domain}/` para que Go los compile en el mismo paquete que las funciones que los usan. Los `.md` se mantienen en `types/{domain}/` para que el indexer los identifique como tipos.
Ver template en `docs/templates/`.
---
+3
View File
@@ -24,6 +24,9 @@ registry.db-wal
*.swo
*~
# Secrets
.env
# OS
.DS_Store
Thumbs.db
+411
View File
@@ -0,0 +1,411 @@
"""
apps/metabase_registry/main.py
==============================
Setup completo de fn-registry en Metabase: datasource, cards y dashboard.
USO
---
Via variables de entorno:
METABASE_URL=http://localhost:3000 \
METABASE_ADMIN_EMAIL=admin@example.com \
METABASE_ADMIN_PASSWORD=secret \
REGISTRY_DB_PATH=/registry.db \
python main.py
Via argumentos CLI:
python main.py \
--url http://localhost:3000 \
--admin-email admin@example.com \
--admin-password secret \
--registry-db-path /registry.db
Para crear un usuario nuevo (opcional):
python main.py ... \
--new-user-email dev@example.com \
--new-user-first-name Dev \
--new-user-last-name User \
--new-user-password devpass
NOTA SOBRE registry.db EN DOCKER
----------------------------------
Metabase corre en Docker y necesita acceder a registry.db. El path que
se configura en Metabase (--registry-db-path) debe ser la ruta DENTRO
del contenedor. Usa setup_volume.sh para copiar el archivo al contenedor
antes de ejecutar este script.
./setup_volume.sh /home/lucas/fn_registry/registry.db
Tras copiar, el archivo queda en /registry.db dentro del contenedor,
que es el valor por defecto de --registry-db-path.
DEPENDENCIAS
------------
Instalar con: pip install -r requirements.txt
O con uv: uv pip install -r requirements.txt
Las funciones metabase_add_database y metabase_list_databases deben
existir en python/functions/metabase/databases.py (creadas por el
fn-constructor).
"""
import argparse
import os
import sys
import json
# Agregar el directorio de funciones Python al path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))
from metabase import (
MetabaseClient,
metabase_create_user,
metabase_create_card,
metabase_create_dashboard,
metabase_update_dashboard,
)
from metabase.client import metabase_auth
from metabase.databases import metabase_add_database, metabase_list_databases
# ---------------------------------------------------------------------------
# Configuracion de cards de ejemplo
# ---------------------------------------------------------------------------
CARDS_CONFIG = [
{
"name": "Funciones por dominio",
"description": "Cuenta de funciones agrupadas por dominio del registry",
"sql": "SELECT domain, count(*) AS total FROM functions GROUP BY domain ORDER BY total DESC",
"display": "bar",
},
{
"name": "Funciones puras vs impuras",
"description": "Distribucion de pureza funcional en el registry",
"sql": "SELECT purity, count(*) AS total FROM functions GROUP BY purity ORDER BY total DESC",
"display": "pie",
},
{
"name": "Funciones por kind",
"description": "Distribucion por tipo: function, pipeline, component",
"sql": "SELECT kind, count(*) AS total FROM functions GROUP BY kind ORDER BY total DESC",
"display": "bar",
},
{
"name": "Buscar funciones",
"description": "Lista completa de funciones con id, dominio, pureza y descripcion",
"sql": (
"SELECT id, domain, kind, purity, signature, description "
"FROM functions ORDER BY domain, name"
),
"display": "table",
},
{
"name": "Tipos por dominio",
"description": "Cuenta de tipos registrados por dominio",
"sql": "SELECT domain, count(*) AS total FROM types GROUP BY domain ORDER BY total DESC",
"display": "bar",
},
{
"name": "Proposals pendientes",
"description": "Proposals del registry que estan pendientes de revision",
"sql": (
"SELECT id, kind, title, created_by, created_at "
"FROM proposals WHERE status = 'pending' ORDER BY created_at DESC"
),
"display": "table",
},
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def log(msg: str) -> None:
print(f"[metabase_registry] {msg}", flush=True)
def log_ok(msg: str) -> None:
print(f"[metabase_registry] OK {msg}", flush=True)
def log_err(msg: str) -> None:
print(f"[metabase_registry] ERR {msg}", file=sys.stderr, flush=True)
def find_existing_database(client: MetabaseClient, name: str) -> dict | None:
"""Busca una database por nombre en Metabase. Retorna None si no existe."""
try:
databases = metabase_list_databases(client)
for db in databases:
if db.get("name") == name:
return db
except Exception as e:
log(f"No se pudo listar databases: {e}")
return None
def build_dataset_query(database_id: int, sql: str) -> dict:
"""Construye el dataset_query para una card SQL nativa."""
return {
"type": "native",
"database": database_id,
"native": {
"query": sql,
"template-tags": {},
},
}
def create_cards(client: MetabaseClient, database_id: int) -> list[dict]:
"""Crea todas las cards de ejemplo. Retorna lista de cards creadas."""
created = []
for cfg in CARDS_CONFIG:
log(f"Creando card: {cfg['name']!r} ...")
try:
card = metabase_create_card(
client=client,
name=cfg["name"],
dataset_query=build_dataset_query(database_id, cfg["sql"]),
display=cfg["display"],
description=cfg["description"],
)
log_ok(f"Card creada: id={card['id']} nombre={card['name']!r}")
created.append(card)
except Exception as e:
log_err(f"No se pudo crear card {cfg['name']!r}: {e}")
return created
def create_overview_dashboard(client: MetabaseClient, cards: list[dict]) -> dict | None:
"""Crea el dashboard 'fn-registry Overview' con todas las cards."""
log("Creando dashboard 'fn-registry Overview' ...")
try:
dashboard = metabase_create_dashboard(
client=client,
name="fn-registry Overview",
description="Vista general del function registry: funciones, tipos, proposals y metricas.",
)
log_ok(f"Dashboard creado: id={dashboard['id']}")
except Exception as e:
log_err(f"No se pudo crear dashboard: {e}")
return None
if not cards:
log("Sin cards para agregar al dashboard.")
return dashboard
# Posicionar cards en una grilla de 3 columnas, 24 unidades de ancho total.
# Cada card ocupa 8 columnas x 6 filas.
cols = 3
card_w = 8
card_h = 6
dashcards = []
for idx, card in enumerate(cards):
col = (idx % cols) * card_w
row = (idx // cols) * card_h
dashcards.append({
"id": -(idx + 1), # ID negativo = nueva dashcard
"card_id": card["id"],
"size_x": card_w,
"size_y": card_h,
"col": col,
"row": row,
"parameter_mappings": [],
"visualization_settings": {},
})
try:
metabase_update_dashboard(
client=client,
dashboard_id=dashboard["id"],
dashcards=dashcards,
)
log_ok(f"Dashboard actualizado con {len(dashcards)} cards.")
except Exception as e:
log_err(f"No se pudo poblar el dashboard con cards: {e}")
return dashboard
# ---------------------------------------------------------------------------
# Flujo principal
# ---------------------------------------------------------------------------
def run(args: argparse.Namespace) -> int:
"""Ejecuta el setup completo. Retorna 0 en exito, 1 en fallo critico."""
# 1. Autenticar como admin
log(f"Autenticando en {args.url} como {args.admin_email} ...")
try:
client = metabase_auth(args.url, args.admin_email, args.admin_password)
log_ok("Autenticacion exitosa.")
except Exception as e:
log_err(f"Fallo de autenticacion: {e}")
return 1
with client:
# 2. Crear usuario nuevo (opcional)
if args.new_user_email:
if not args.new_user_first_name or not args.new_user_last_name:
log_err("Para crear usuario se requieren --new-user-first-name y --new-user-last-name.")
return 1
log(f"Creando usuario {args.new_user_email} ...")
try:
user = metabase_create_user(
client=client,
first_name=args.new_user_first_name,
last_name=args.new_user_last_name,
email=args.new_user_email,
password=args.new_user_password or "",
)
log_ok(f"Usuario creado: id={user['id']} email={user['email']}")
except Exception as e:
log_err(f"No se pudo crear usuario: {e}")
# No es critico, continuar
# 3. Agregar registry.db como datasource SQLite
db_name = "fn-registry"
log(f"Verificando si ya existe la database {db_name!r} en Metabase ...")
existing_db = find_existing_database(client, db_name)
if existing_db:
db_id = existing_db["id"]
log_ok(f"Database ya existe: id={db_id} nombre={db_name!r}. Se reutiliza.")
else:
log(f"Registrando registry.db ({args.registry_db_path}) como datasource SQLite ...")
try:
new_db = metabase_add_database(
client=client,
name=db_name,
engine="sqlite",
details={"db": args.registry_db_path},
)
db_id = new_db["id"]
log_ok(f"Database registrada: id={db_id} path={args.registry_db_path}")
except Exception as e:
log_err(f"No se pudo registrar la database: {e}")
log_err(
"Verifica que registry.db este accesible desde el contenedor Docker. "
"Usa setup_volume.sh para copiar el archivo."
)
return 1
# 4. Crear cards de ejemplo
log(f"Creando {len(CARDS_CONFIG)} cards con database_id={db_id} ...")
cards = create_cards(client, db_id)
log(f"Cards creadas: {len(cards)}/{len(CARDS_CONFIG)}")
# 5. Crear dashboard
dashboard = create_overview_dashboard(client, cards)
if dashboard:
log_ok(
f"Dashboard disponible en: {args.url}/dashboard/{dashboard['id']}"
)
else:
log_err("El dashboard no pudo crearse.")
summary = {
"url": args.url,
"database_id": db_id,
"cards_created": len(cards),
"dashboard_id": dashboard["id"] if dashboard else None,
"dashboard_url": f"{args.url}/dashboard/{dashboard['id']}" if dashboard else None,
}
print("\n--- Resumen ---")
print(json.dumps(summary, indent=2))
return 0
# ---------------------------------------------------------------------------
# CLI / env vars
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="metabase_registry",
description="Setup de fn-registry como datasource en Metabase.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
# Conexion Metabase
p.add_argument(
"--url",
default=os.environ.get("METABASE_URL", "http://localhost:3000"),
help="URL base de Metabase (env: METABASE_URL). Default: http://localhost:3000",
)
p.add_argument(
"--admin-email",
default=os.environ.get("METABASE_ADMIN_EMAIL", "admin@example.com"),
dest="admin_email",
help="Email del admin (env: METABASE_ADMIN_EMAIL).",
)
p.add_argument(
"--admin-password",
default=os.environ.get("METABASE_ADMIN_PASSWORD", ""),
dest="admin_password",
help="Password del admin (env: METABASE_ADMIN_PASSWORD).",
)
# Registry DB path (ruta dentro del contenedor Docker)
p.add_argument(
"--registry-db-path",
default=os.environ.get("REGISTRY_DB_PATH", "/registry.db"),
dest="registry_db_path",
help=(
"Ruta al registry.db DENTRO del contenedor Docker "
"(env: REGISTRY_DB_PATH). Default: /registry.db"
),
)
# Nuevo usuario (todos opcionales)
p.add_argument(
"--new-user-email",
default=os.environ.get("NEW_USER_EMAIL", ""),
dest="new_user_email",
help="Email del nuevo usuario a crear (env: NEW_USER_EMAIL). Opcional.",
)
p.add_argument(
"--new-user-first-name",
default=os.environ.get("NEW_USER_FIRST_NAME", ""),
dest="new_user_first_name",
help="Nombre del nuevo usuario (env: NEW_USER_FIRST_NAME).",
)
p.add_argument(
"--new-user-last-name",
default=os.environ.get("NEW_USER_LAST_NAME", ""),
dest="new_user_last_name",
help="Apellido del nuevo usuario (env: NEW_USER_LAST_NAME).",
)
p.add_argument(
"--new-user-password",
default=os.environ.get("NEW_USER_PASSWORD", ""),
dest="new_user_password",
help="Password del nuevo usuario (env: NEW_USER_PASSWORD). Opcional.",
)
return p
def main() -> None:
parser = build_parser()
args = parser.parse_args()
if not args.admin_password:
parser.error(
"Se requiere la password del admin. "
"Usa --admin-password o la variable de entorno METABASE_ADMIN_PASSWORD."
)
sys.exit(run(args))
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
# Dependencias de apps/metabase_registry
#
# Instalar con pip:
# pip install -r requirements.txt
#
# O con uv (recomendado, desde la raiz del repo):
# cd /home/lucas/fn_registry/python && uv pip install -r ../apps/metabase_registry/requirements.txt
#
# O directamente desde el directorio python (que ya tiene httpx):
# cd /home/lucas/fn_registry/python && uv run python ../apps/metabase_registry/main.py
# HTTP client — mismo que usa el paquete python/functions/metabase
httpx>=0.27.0
@@ -0,0 +1,38 @@
---
name: assert_docker_container_running
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "assert_docker_container_running(container_name: string) -> void"
description: "Verifica que un contenedor Docker está corriendo. Sale con exit code 1 si no está activo, con mensaje a stderr."
tags: [assert, docker, container, running, validation, infra, bash]
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/assert_docker_container_running.sh"
---
## Ejemplo
```bash
source functions/infra/assert_docker_container_running.sh
assert_docker_container_running metabase
echo "Contenedor activo, continuando..."
```
## Notas
Usa `docker ps --format '{{.Names}}'` con grep anclado (`^name$`) para evitar matches parciales (ej: "metabase" no matchea "metabase-test").
Output limpio: void en éxito. El mensaje de error en stderr no incluye lista de contenedores activos — eso es responsabilidad del pipeline/caller.
Requiere que `docker` esté en PATH. Combinar con `assert_command_exists` antes de llamar.
@@ -0,0 +1,19 @@
# assert_docker_container_running
# --------------------------------
# Verifica que un contenedor Docker está corriendo.
# No produce output a stdout en caso de éxito.
# Sale con exit code 1 si el contenedor no está corriendo,
# con mensaje descriptivo a stderr.
#
# USO (sourced):
# source assert_docker_container_running.sh
# assert_docker_container_running metabase
assert_docker_container_running() {
local container_name="$1"
if ! docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then
echo "assert_docker_container_running: el contenedor '$container_name' no está corriendo" >&2
return 1
fi
}
+41
View File
@@ -0,0 +1,41 @@
---
name: docker_cp_file
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "docker_cp_file(local_path: string, container_name: string, dest_path: string) -> string"
description: "Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide. Imprime JSON con local_size y remote_size a stdout. Sale con exit code 1 si docker cp falla o los tamaños difieren."
tags: [docker, cp, copy, file, container, transfer, infra, bash]
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/docker_cp_file.sh"
---
## Ejemplo
```bash
source functions/infra/docker_cp_file.sh
result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db)
echo "$result"
# {"local_size":524288,"remote_size":524288}
local_size=$(echo "$result" | grep -o '"local_size":[0-9]*' | cut -d: -f2)
```
## Notas
La verificación de tamaño usa `docker exec stat -c%s` sobre el contenedor destino. Si `stat` no está disponible en el contenedor, `remote_size` será -1 y la función fallará.
Output a stdout: JSON minificado con campos `local_size` y `remote_size` (enteros, bytes).
Usa `printf` en lugar de `echo` para garantizar que no haya newline extra en el JSON.
+33
View File
@@ -0,0 +1,33 @@
# docker_cp_file
# --------------
# Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide.
# Imprime JSON con local_size y remote_size a stdout si la copia es exitosa.
# Sale con exit code 1 si docker cp falla o si los tamaños no coinciden.
#
# USO (sourced):
# source docker_cp_file.sh
# result=$(docker_cp_file /ruta/local.db metabase /dest/path.db)
docker_cp_file() {
local local_path="$1"
local container_name="$2"
local dest_path="$3"
if ! docker cp "$local_path" "${container_name}:${dest_path}" 2>/dev/null; then
echo "docker_cp_file: fallo al copiar '$local_path' a '${container_name}:${dest_path}'" >&2
return 1
fi
local local_size
local_size=$(stat -c%s "$local_path")
local remote_size
remote_size=$(docker exec "$container_name" stat -c%s "$dest_path" 2>/dev/null || echo "-1")
if [ "$local_size" != "$remote_size" ]; then
echo "docker_cp_file: tamaños no coinciden (local=${local_size}, remoto=${remote_size})" >&2
return 1
fi
printf '{"local_size":%s,"remote_size":%s}' "$local_size" "$remote_size"
}
@@ -0,0 +1,65 @@
---
name: setup_metabase_volume
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "setup_metabase_volume([registry_db_path: string], [container_name: string], [dest_path: string]) -> void"
description: "Copia registry.db al contenedor Docker de Metabase verificando existencia del archivo, disponibilidad de docker, estado del contenedor y coincidencia de tamaños. Todos los argumentos son opcionales con defaults razonables."
tags: [metabase, docker, setup, launcher, pipeline, bash, infra]
uses_functions:
- assert_file_exists_bash_shell
- assert_command_exists_bash_shell
- assert_docker_container_running_bash_infra
- docker_cp_file_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/setup_metabase_volume.sh"
---
## Ejemplo
```bash
# Con defaults
./functions/pipelines/setup_metabase_volume.sh
# Con argumentos explícitos
./functions/pipelines/setup_metabase_volume.sh \
/home/lucas/fn_registry/registry.db \
metabase \
/registry.db
```
## Flujo
1. `assert_file_exists` — verifica que `registry.db` existe localmente y obtiene su tamaño
2. `assert_command_exists` — verifica que `docker` está disponible en PATH
3. `assert_docker_container_running` — verifica que el contenedor destino está activo; si falla, muestra lista de contenedores activos
4. `docker_cp_file` — ejecuta `docker cp` y verifica que los tamaños local y remoto coinciden
## Notas
El pipeline usa `set -euo pipefail` — cualquier fallo en una función individual detiene la ejecución.
Las funciones individuales se sourcean desde sus rutas en el registry, relativas a `REGISTRY_ROOT` detectado automáticamente desde la ubicación del script.
Defaults:
- `REGISTRY_DB_PATH`: `/home/lucas/fn_registry/registry.db`
- `CONTAINER_NAME`: `metabase`
- `DEST_PATH`: `/registry.db`
Nota de persistencia: `docker cp` copia al contenedor en ejecución. Si el contenedor se reinicia, el archivo se pierde. Para persistencia real, montar el directorio como volumen en docker-compose:
```yaml
volumes:
- /home/lucas/fn_registry:/fn_registry:ro
```
Y usar `--registry-db-path /fn_registry/registry.db`.
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# setup_metabase_volume
# ---------------------
# Copia registry.db al contenedor Docker de Metabase.
# Compone: assert_file_exists + assert_command_exists +
# assert_docker_container_running + docker_cp_file
#
# USO:
# ./setup_metabase_volume.sh [REGISTRY_DB_PATH] [CONTAINER_NAME] [DEST_PATH]
#
# ARGUMENTOS (opcionales, con defaults):
# REGISTRY_DB_PATH Ruta local al registry.db
# Default: /home/lucas/fn_registry/registry.db
# CONTAINER_NAME Nombre del contenedor Docker de Metabase
# Default: metabase
# DEST_PATH Ruta destino dentro del contenedor
# Default: /registry.db
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
source "$REGISTRY_ROOT/bash/functions/infra/assert_docker_container_running.sh"
source "$REGISTRY_ROOT/bash/functions/infra/docker_cp_file.sh"
REGISTRY_DB_PATH="${1:-/home/lucas/fn_registry/registry.db}"
CONTAINER_NAME="${2:-metabase}"
DEST_PATH="${3:-/registry.db}"
echo "[setup_metabase_volume] Configuracion:"
echo " registry.db local : $REGISTRY_DB_PATH"
echo " contenedor Docker : $CONTAINER_NAME"
echo " ruta en contenedor : $DEST_PATH"
echo ""
# 1. Verificar archivo local
local_size=$(assert_file_exists "$REGISTRY_DB_PATH")
echo "[setup_metabase_volume] Archivo local encontrado: ${local_size} bytes"
# 2. Verificar que docker esta disponible
assert_command_exists docker
echo "[setup_metabase_volume] docker disponible en PATH."
# 3. Verificar que el contenedor esta corriendo
if ! assert_docker_container_running "$CONTAINER_NAME"; then
echo "" >&2
echo "Contenedores activos:" >&2
docker ps --format " {{.Names}}\t{{.Status}}\t{{.Image}}" >&2
echo "" >&2
echo "Si el contenedor se llama diferente, pasa el nombre como segundo argumento:" >&2
echo " ./setup_metabase_volume.sh $REGISTRY_DB_PATH <nombre_contenedor>" >&2
exit 1
fi
echo "[setup_metabase_volume] Contenedor '$CONTAINER_NAME' encontrado y activo."
# 4. Copiar archivo y verificar tamaños
echo "[setup_metabase_volume] Copiando $REGISTRY_DB_PATH -> ${CONTAINER_NAME}:${DEST_PATH} ..."
result=$(docker_cp_file "$REGISTRY_DB_PATH" "$CONTAINER_NAME" "$DEST_PATH")
remote_size=$(echo "$result" | grep -o '"remote_size":[0-9]*' | cut -d: -f2)
echo "[setup_metabase_volume] OK Copia completada y verificada."
echo "[setup_metabase_volume] Tamanio: ${local_size} bytes (local) = ${remote_size} bytes (remoto)"
echo ""
echo "---------------------------------------------------------------------"
echo "registry.db disponible en el contenedor como: $DEST_PATH"
echo ""
echo "Ahora ejecuta main.py con:"
echo ""
echo " METABASE_ADMIN_PASSWORD=<password> \\"
echo " REGISTRY_DB_PATH=${DEST_PATH} \\"
echo " python apps/metabase_registry/main.py"
echo ""
echo "O bien:"
echo ""
echo " python apps/metabase_registry/main.py \\"
echo " --admin-password <password> \\"
echo " --registry-db-path ${DEST_PATH}"
echo "---------------------------------------------------------------------"
@@ -0,0 +1,36 @@
---
name: assert_command_exists
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "assert_command_exists(command_name: string) -> void"
description: "Verifica que un comando está disponible en el PATH. Sale con exit code 1 si no se encuentra, con mensaje a stderr."
tags: [assert, command, exists, validation, shell, bash, path]
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/assert_command_exists.sh"
---
## Ejemplo
```bash
source functions/shell/assert_command_exists.sh
assert_command_exists docker
assert_command_exists jq
```
## Notas
Usa `command -v` (POSIX) con redirección `&>/dev/null` para suprimir output. No produce nada a stdout en caso de éxito.
Output limpio: void en éxito, mensaje a stderr en fallo.
@@ -0,0 +1,18 @@
# assert_command_exists
# ---------------------
# Verifica que un comando está disponible en el PATH.
# No produce output a stdout.
# Sale con exit code 1 si el comando no se encuentra.
#
# USO (sourced):
# source assert_command_exists.sh
# assert_command_exists docker
assert_command_exists() {
local command_name="$1"
if ! command -v "$command_name" &>/dev/null; then
echo "assert_command_exists: comando no encontrado en PATH: $command_name" >&2
return 1
fi
}
@@ -0,0 +1,38 @@
---
name: assert_file_exists
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "assert_file_exists(file_path: string) -> string"
description: "Verifica que un archivo existe en el filesystem. Imprime su tamaño en bytes a stdout. Sale con exit code 1 si el archivo no existe."
tags: [assert, file, exists, validation, shell, bash]
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/assert_file_exists.sh"
---
## Ejemplo
```bash
source functions/shell/assert_file_exists.sh
size=$(assert_file_exists /home/lucas/fn_registry/registry.db)
echo "Tamaño: $size bytes"
```
## Notas
La función se sourcea, no se ejecuta directamente. Usa `stat -c%s` para obtener el tamaño en bytes (compatible con GNU coreutils / Linux).
Output limpio: solo el número de bytes a stdout. Los errores van a stderr.
No usa `set -e` internamente — el caller controla el flujo con el exit code de retorno.
@@ -0,0 +1,20 @@
# assert_file_exists
# ------------------
# Verifica que un archivo existe en el filesystem.
# Imprime su tamaño en bytes a stdout.
# Sale con exit code 1 si el archivo no existe.
#
# USO (sourced):
# source assert_file_exists.sh
# size=$(assert_file_exists /ruta/al/archivo)
assert_file_exists() {
local file_path="$1"
if [ ! -f "$file_path" ]; then
echo "assert_file_exists: archivo no encontrado: $file_path" >&2
return 1
fi
stat -c%s "$file_path"
}
+3
View File
@@ -33,6 +33,8 @@ func main() {
cmdOps(os.Args[2:])
case "proposal":
cmdProposal(os.Args[2:])
case "run":
cmdRun(os.Args[2:])
case "help", "-h", "--help":
printUsage()
default:
@@ -51,6 +53,7 @@ Usage:
fn list [-k kind] [-d domain] [-l lang]
fn show <id> Muestra entrada completa
fn add [-k kind] Abre $EDITOR con template
fn run <id_or_name> [args...] Ejecuta funcion/pipeline (go/py/bash)
fn ops <subcommand> Gestiona operations.db (fn ops help)
fn proposal <add|list|show|update> Gestiona proposals`)
}
+208
View File
@@ -0,0 +1,208 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"fn-registry/registry"
)
func cmdRun(args []string) {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "usage: fn run <id_or_name> [args...]")
os.Exit(1)
}
idOrName := args[0]
passArgs := args[1:]
registryRoot := root()
dbPath := filepath.Join(registryRoot, dbName)
db, err := registry.Open(dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error: cannot open registry: %v\n", err)
os.Exit(1)
}
defer db.Close()
fn, err := resolveFunction(db, idOrName)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if fn.FilePath == "" {
fmt.Fprintf(os.Stderr, "error: %s has no file_path in registry\n", fn.ID)
os.Exit(1)
}
absPath := filepath.Join(registryRoot, fn.FilePath)
if _, err := os.Stat(absPath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "error: file not found: %s\n", absPath)
os.Exit(1)
}
cmd, err := buildCommand(fn, registryRoot, absPath, passArgs)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
fmt.Fprintf(os.Stderr, "[fn run] %s (%s/%s) %s\n", fn.ID, fn.Lang, fn.Kind, strings.Join(passArgs, " "))
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func resolveFunction(db *registry.DB, idOrName string) (*registry.Function, error) {
fn, err := db.GetFunction(idOrName)
if err == nil {
return fn, nil
}
fns, err := db.GetFunctionsByName(idOrName)
if err != nil {
return nil, fmt.Errorf("lookup failed: %w", err)
}
if len(fns) == 0 {
return nil, fmt.Errorf("function %q not found (tried as ID and name)", idOrName)
}
if len(fns) == 1 {
return &fns[0], nil
}
var b strings.Builder
fmt.Fprintf(&b, "ambiguous name %q — found %d matches:\n", idOrName, len(fns))
for _, f := range fns {
fmt.Fprintf(&b, " %s (%s/%s)\n", f.ID, f.Lang, f.Kind)
}
fmt.Fprintf(&b, "use the full ID to disambiguate")
return nil, fmt.Errorf("%s", b.String())
}
func buildCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) {
switch fn.Lang {
case "go":
return buildGoCommand(fn, registryRoot, absPath, args)
case "py":
return buildPyCommand(registryRoot, absPath, args)
case "bash":
return buildBashCommand(absPath, args)
case "ts":
return buildTsCommand(registryRoot, absPath, args)
default:
return nil, fmt.Errorf("unsupported lang %q for execution", fn.Lang)
}
}
func buildGoCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) {
dir := filepath.Dir(absPath)
env := append(os.Environ(), "CGO_ENABLED=1")
// If directory has main.go → go run . (pipelines and standalone executables)
mainGo := filepath.Join(dir, "main.go")
if _, err := os.Stat(mainGo); err == nil {
cmdArgs := append([]string{"run", "."}, args...)
cmd := exec.Command("go", cmdArgs...)
cmd.Dir = dir
cmd.Env = env
return cmd, nil
}
// Library code: if it has tests → go test
if fn.Tested && fn.TestFilePath != "" {
testAbs := filepath.Join(registryRoot, fn.TestFilePath)
if _, err := os.Stat(testAbs); err == nil {
relPkg, _ := filepath.Rel(registryRoot, dir)
pkgPath := "./" + filepath.ToSlash(relPkg)
cmdArgs := append([]string{"test", "-v", "-count=1", "-tags", "fts5", pkgPath}, args...)
cmd := exec.Command("go", cmdArgs...)
cmd.Dir = registryRoot
cmd.Env = env
return cmd, nil
}
}
// No tests: go vet (compilation check)
relPkg, _ := filepath.Rel(registryRoot, dir)
pkgPath := "./" + filepath.ToSlash(relPkg)
cmdArgs := []string{"vet", "-tags", "fts5", pkgPath}
cmd := exec.Command("go", cmdArgs...)
cmd.Dir = registryRoot
cmd.Env = env
fmt.Fprintf(os.Stderr, "[fn run] %s is library code without tests — running go vet\n", fn.ID)
return cmd, nil
}
func buildPyCommand(registryRoot, absPath string, args []string) (*exec.Cmd, error) {
venvPython := filepath.Join(registryRoot, "python", ".venv", "bin", "python3")
pythonBin := "python3"
if _, err := os.Stat(venvPython); err == nil {
pythonBin = venvPython
}
dir := filepath.Dir(absPath)
// If the file is inside a package (has __init__.py), use python -m
// so relative imports work. PYTHONPATH points to python/functions/ or
// the equivalent parent that contains the domain packages.
initPy := filepath.Join(dir, "__init__.py")
if _, err := os.Stat(initPy); err == nil {
// The pythonPath is the well-known python/functions/ directory
// which contains domain packages (metabase/, etc.)
pythonPath := filepath.Join(registryRoot, "python", "functions")
if _, err := os.Stat(pythonPath); os.IsNotExist(err) {
// Fallback: walk up from dir to find the parent of the top package
pythonPath = filepath.Dir(dir)
}
// Build module path: metabase/databases.py → metabase.databases
relToRoot, _ := filepath.Rel(pythonPath, absPath)
modPath := strings.TrimSuffix(relToRoot, ".py")
modPath = strings.ReplaceAll(filepath.ToSlash(modPath), "/", ".")
cmdArgs := append([]string{"-m", modPath}, args...)
cmd := exec.Command(pythonBin, cmdArgs...)
cmd.Dir = pythonPath
cmd.Env = append(os.Environ(), "PYTHONPATH="+pythonPath)
return cmd, nil
}
// Standalone script (no __init__.py)
cmdArgs := append([]string{absPath}, args...)
cmd := exec.Command(pythonBin, cmdArgs...)
cmd.Dir = dir
return cmd, nil
}
func buildBashCommand(absPath string, args []string) (*exec.Cmd, error) {
cmdArgs := append([]string{absPath}, args...)
cmd := exec.Command("bash", cmdArgs...)
cmd.Dir = filepath.Dir(absPath)
return cmd, nil
}
func buildTsCommand(registryRoot, absPath string, args []string) (*exec.Cmd, error) {
tsxBin := filepath.Join(registryRoot, "frontend", "node_modules", ".bin", "tsx")
if _, err := os.Stat(tsxBin); os.IsNotExist(err) {
return nil, fmt.Errorf("tsx not found — run: cd frontend && pnpm add -D tsx")
}
cmdArgs := append([]string{absPath}, args...)
cmd := exec.Command(tsxBin, cmdArgs...)
cmd.Dir = filepath.Dir(absPath)
return cmd, nil
}
+1
View File
@@ -31,6 +31,7 @@
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"typescript": "^6.0.2",
"vite": "^8.0.3"
}
+305 -8
View File
@@ -41,7 +41,7 @@ importers:
devDependencies:
'@tailwindcss/vite':
specifier: ^4.2.2
version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))
version: 4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))
'@types/react':
specifier: ^19.2.14
version: 19.2.14
@@ -50,16 +50,19 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: ^6.0.1
version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))
version: 6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))
tailwindcss:
specifier: ^4.2.2
version: 4.2.2
tsx:
specifier: ^4.21.0
version: 4.21.0
typescript:
specifier: ^6.0.2
version: 6.0.2
vite:
specifier: ^8.0.3
version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)
version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)
packages:
@@ -236,6 +239,162 @@ packages:
'@emnapi/wasi-threads@1.2.0':
resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
'@esbuild/aix-ppc64@0.27.4':
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.4':
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.4':
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.4':
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.4':
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.4':
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.4':
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.4':
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.4':
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.4':
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.4':
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.4':
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.4':
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.4':
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.4':
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.4':
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.4':
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.4':
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.4':
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.4':
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.4':
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.4':
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.4':
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.4':
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.4':
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.4':
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
@@ -872,6 +1031,11 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
esbuild@0.27.4:
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -1011,6 +1175,9 @@ packages:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
get-tsconfig@4.13.7:
resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -1539,6 +1706,9 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
@@ -1729,6 +1899,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
hasBin: true
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
@@ -2131,6 +2306,84 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.27.4':
optional: true
'@esbuild/android-arm64@0.27.4':
optional: true
'@esbuild/android-arm@0.27.4':
optional: true
'@esbuild/android-x64@0.27.4':
optional: true
'@esbuild/darwin-arm64@0.27.4':
optional: true
'@esbuild/darwin-x64@0.27.4':
optional: true
'@esbuild/freebsd-arm64@0.27.4':
optional: true
'@esbuild/freebsd-x64@0.27.4':
optional: true
'@esbuild/linux-arm64@0.27.4':
optional: true
'@esbuild/linux-arm@0.27.4':
optional: true
'@esbuild/linux-ia32@0.27.4':
optional: true
'@esbuild/linux-loong64@0.27.4':
optional: true
'@esbuild/linux-mips64el@0.27.4':
optional: true
'@esbuild/linux-ppc64@0.27.4':
optional: true
'@esbuild/linux-riscv64@0.27.4':
optional: true
'@esbuild/linux-s390x@0.27.4':
optional: true
'@esbuild/linux-x64@0.27.4':
optional: true
'@esbuild/netbsd-arm64@0.27.4':
optional: true
'@esbuild/netbsd-x64@0.27.4':
optional: true
'@esbuild/openbsd-arm64@0.27.4':
optional: true
'@esbuild/openbsd-x64@0.27.4':
optional: true
'@esbuild/openharmony-arm64@0.27.4':
optional: true
'@esbuild/sunos-x64@0.27.4':
optional: true
'@esbuild/win32-arm64@0.27.4':
optional: true
'@esbuild/win32-ia32@0.27.4':
optional: true
'@esbuild/win32-x64@0.27.4':
optional: true
'@floating-ui/core@1.7.5':
dependencies:
'@floating-ui/utils': 0.2.11
@@ -2383,12 +2636,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2
'@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))':
'@tailwindcss/vite@4.2.2(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))':
dependencies:
'@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2
tailwindcss: 4.2.2
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)
'@ts-morph/common@0.27.0':
dependencies:
@@ -2413,10 +2666,10 @@ snapshots:
'@types/validate-npm-package-name@4.0.2': {}
'@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1))':
'@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1)
vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)
accepts@2.0.0:
dependencies:
@@ -2643,6 +2896,35 @@ snapshots:
dependencies:
es-errors: 1.3.0
esbuild@0.27.4:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.4
'@esbuild/android-arm': 0.27.4
'@esbuild/android-arm64': 0.27.4
'@esbuild/android-x64': 0.27.4
'@esbuild/darwin-arm64': 0.27.4
'@esbuild/darwin-x64': 0.27.4
'@esbuild/freebsd-arm64': 0.27.4
'@esbuild/freebsd-x64': 0.27.4
'@esbuild/linux-arm': 0.27.4
'@esbuild/linux-arm64': 0.27.4
'@esbuild/linux-ia32': 0.27.4
'@esbuild/linux-loong64': 0.27.4
'@esbuild/linux-mips64el': 0.27.4
'@esbuild/linux-ppc64': 0.27.4
'@esbuild/linux-riscv64': 0.27.4
'@esbuild/linux-s390x': 0.27.4
'@esbuild/linux-x64': 0.27.4
'@esbuild/netbsd-arm64': 0.27.4
'@esbuild/netbsd-x64': 0.27.4
'@esbuild/openbsd-arm64': 0.27.4
'@esbuild/openbsd-x64': 0.27.4
'@esbuild/openharmony-arm64': 0.27.4
'@esbuild/sunos-x64': 0.27.4
'@esbuild/win32-arm64': 0.27.4
'@esbuild/win32-ia32': 0.27.4
'@esbuild/win32-x64': 0.27.4
escalade@3.2.0: {}
escape-html@1.0.3: {}
@@ -2820,6 +3102,10 @@ snapshots:
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
get-tsconfig@4.13.7:
dependencies:
resolve-pkg-maps: 1.0.0
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -3248,6 +3534,8 @@ snapshots:
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
@@ -3501,6 +3789,13 @@ snapshots:
tslib@2.8.1: {}
tsx@4.21.0:
dependencies:
esbuild: 0.27.4
get-tsconfig: 4.13.7
optionalDependencies:
fsevents: 2.3.3
tw-animate-css@1.4.0: {}
type-fest@5.5.0:
@@ -3539,7 +3834,7 @@ snapshots:
vary@1.1.2: {}
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(jiti@2.6.1):
vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -3547,8 +3842,10 @@ snapshots:
rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
tinyglobby: 0.2.15
optionalDependencies:
esbuild: 0.27.4
fsevents: 2.3.3
jiti: 2.6.1
tsx: 4.21.0
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
-28
View File
@@ -1,28 +0,0 @@
package infra
// ContainerInfo representa la información básica de un contenedor Docker.
type ContainerInfo struct {
ID string
Name string
Image string
Status string
State string
Ports string
Created string
Labels map[string]string
}
// ImageInfo representa la información básica de una imagen Docker local.
type ImageInfo struct {
ID string
Repository string
Tag string
Size string
Created string
}
// MetabaseClient holds the connection details for a Metabase instance API.
type MetabaseClient struct {
BaseURL string // e.g. "http://localhost:3000"
Token string // session token or API key
}
+9 -4
View File
@@ -30,13 +30,17 @@ file_path: "functions/pipelines/init_metabase/main.go"
## Ejemplo
```bash
# Básico
go run functions/pipelines/init_metabase/main.go \
--project analytics \
--metabase-port 3000 \
--pg-port 5432 \
--pg-user metabase \
--pg-password metabase \
--pg-database metabase
--pg-password metabase
# Con volume para registry.db (detecta cambios en vivo)
go run functions/pipelines/init_metabase/main.go \
--project fn_registry \
--mb-volumes "/home/lucas/fn_registry/registry.db:/data/registry.db"
```
Salida JSON:
@@ -60,7 +64,8 @@ El pipeline orquesta 5 pasos secuenciales:
2. **Pull** — descarga `postgres:16` y `metabase/metabase:latest`
3. **Postgres** — inicia con volume persistente (named volume por defecto o bind mount con `--pg-volume`)
4. **Health check** — retry exponencial (hasta ~34 min) con `pg_isready` dentro del contenedor
5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable
5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable. Con `--mb-volumes` monta volumes adicionales (ej: registry.db para SQLite)
6. **Permisos** — ajusta ownership de directorios montados para el usuario `metabase` (UID 2000) dentro del contenedor
Reutiliza conceptualmente `docker_create_network`, `docker_pull_image`, `docker_run_container`, `docker_inspect_container` y `retry_with_backoff`, reimplementadas inline por ser un ejecutable independiente.
+33 -4
View File
@@ -28,7 +28,8 @@ func main() {
pgUser := flag.String("pg-user", "metabase", "Usuario Postgres")
pgPass := flag.String("pg-password", "metabase", "Password Postgres")
pgDB := flag.String("pg-database", "metabase", "Base de datos Postgres")
pgVolume := flag.String("pg-volume", "", "Path host para persistencia (default: docker named volume)")
pgVolume := flag.String("pg-volume", "", "Path host para persistencia Postgres (default: docker named volume)")
mbVolumes := flag.String("mb-volumes", "", "Volumes adicionales para Metabase, separados por coma (ej: /host/path:/container/path,/otro:/dest)")
flag.Parse()
if *project == "" {
@@ -37,7 +38,12 @@ func main() {
os.Exit(1)
}
result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume)
var extraVolumes []string
if *mbVolumes != "" {
extraVolumes = strings.Split(*mbVolumes, ",")
}
result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume, extraVolumes)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
@@ -48,7 +54,7 @@ func main() {
enc.Encode(result)
}
func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string) (*MetabaseResult, error) {
func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string, mbExtraVolumes []string) (*MetabaseResult, error) {
networkName := project + "-net"
pgName := project + "-postgres"
mbName := project + "-metabase"
@@ -118,13 +124,36 @@ func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string
"-e", "MB_DB_USER=" + pgUser,
"-e", "MB_DB_PASS=" + pgPass,
"-e", "MB_DB_HOST=" + pgName,
"metabase/metabase:latest",
}
for _, v := range mbExtraVolumes {
mbArgs = append(mbArgs, "-v", strings.TrimSpace(v))
}
mbArgs = append(mbArgs, "metabase/metabase:latest")
mbID, err := dockerCmd(mbArgs...)
if err != nil {
return nil, fmt.Errorf("starting metabase: %w", err)
}
// 6. Fix permisos para volumes SQLite — Metabase corre como UID 2000
for _, v := range mbExtraVolumes {
parts := strings.SplitN(strings.TrimSpace(v), ":", 2)
if len(parts) < 2 {
continue
}
destPath := parts[1]
// Si es un archivo, fix el directorio padre; si es directorio, fix directo
dir := destPath
if strings.Contains(destPath, ".") {
// Probablemente un archivo, usar dirname
idx := strings.LastIndex(destPath, "/")
if idx > 0 {
dir = destPath[:idx]
}
}
fmt.Fprintf(os.Stderr, " Fijando permisos de %s para usuario metabase...\n", dir)
dockerCmd("exec", "-u", "root", mbName, "chown", "metabase:metabase", dir)
}
mbURL := fmt.Sprintf("http://localhost:%s", mbPort)
fmt.Fprintf(os.Stderr, "\nStack listo. Metabase disponible en %s\n", mbURL)
+4 -2
View File
@@ -1,9 +1,11 @@
package shell
import "fmt"
// Implementation: github.com/lucasdataproyects/devfactory/shell
// Run ejecuta un comando del sistema con timeout de 30 segundos y devuelve el resultado.
func Run(name string, args ...string) Result[CmdResult] {
func Run(name string, args ...string) (CmdResult, error) {
// stub — implementation in devfactory/shell
return Result[CmdResult]{}
return CmdResult{}, fmt.Errorf("not implemented")
}
+6 -3
View File
@@ -1,11 +1,14 @@
package shell
import "time"
import (
"fmt"
"time"
)
// Implementation: github.com/lucasdataproyects/devfactory/shell
// RunWithTimeout ejecuta un comando del sistema con timeout configurable.
func RunWithTimeout(name string, timeout time.Duration, args ...string) Result[CmdResult] {
func RunWithTimeout(name string, timeout time.Duration, args ...string) (CmdResult, error) {
// stub — implementation in devfactory/shell
return Result[CmdResult]{}
return CmdResult{}, fmt.Errorf("not implemented")
}
+4 -2
View File
@@ -1,9 +1,11 @@
package shell
import "fmt"
// Implementation: github.com/lucasdataproyects/devfactory/shell
// RunPipe encadena multiples comandos con pipe y devuelve el resultado final.
func RunPipe(commands ...string) Result[CmdResult] {
func RunPipe(commands ...string) (CmdResult, error) {
// stub — implementation in devfactory/shell
return Result[CmdResult]{}
return CmdResult{}, fmt.Errorf("not implemented")
}
+4 -2
View File
@@ -1,9 +1,11 @@
package shell
import "fmt"
// Implementation: github.com/lucasdataproyects/devfactory/shell
// RunShell ejecuta un comando shell interpretado por /bin/sh.
func RunShell(command string) Result[CmdResult] {
func RunShell(command string) (CmdResult, error) {
// stub — implementation in devfactory/shell
return Result[CmdResult]{}
return CmdResult{}, fmt.Errorf("not implemented")
}
+6 -3
View File
@@ -1,11 +1,14 @@
package shell
import "time"
import (
"fmt"
"time"
)
// Implementation: github.com/lucasdataproyects/devfactory/shell
// RunShellTimeout ejecuta un comando shell con timeout configurable.
func RunShellTimeout(command string, timeout time.Duration) Result[CmdResult] {
func RunShellTimeout(command string, timeout time.Duration) (CmdResult, error) {
// stub — implementation in devfactory/shell
return Result[CmdResult]{}
return CmdResult{}, fmt.Errorf("not implemented")
}
+3 -3
View File
@@ -2,8 +2,8 @@ package shell
// Implementation: github.com/lucasdataproyects/devfactory/shell
// Which busca la ruta de un ejecutable en el PATH del sistema. Devuelve None si no existe.
func Which(name string) Option[string] {
// Which busca la ruta de un ejecutable en el PATH del sistema. Devuelve ("", false) si no existe.
func Which(name string) (string, bool) {
// stub — implementation in devfactory/shell
return Option[string]{}
return "", false
}
+4 -4
View File
@@ -1,9 +1,9 @@
package tui
// Implementation: github.com/lucasdataproyects/devfactory/tui
import "fmt"
// Confirm muestra un dialogo de confirmacion Si/No en terminal y devuelve la eleccion del usuario.
func Confirm(prompt string) Result[bool] {
// Confirm muestra un prompt de confirmacion si/no en la terminal.
func Confirm(prompt string) (bool, error) {
// stub — implementation in devfactory/tui
return Result[bool]{Value: false}
return false, fmt.Errorf("not implemented")
}
+4 -6
View File
@@ -1,12 +1,10 @@
package tui
// Implementation: github.com/lucasdataproyects/devfactory/tui
import tea "github.com/charmbracelet/bubbletea"
import "github.com/charmbracelet/bubbletea"
// RunFullscreen ejecuta un modelo Bubble Tea en modo fullscreen.
func RunFullscreen[T tea.Model](model T) Result[T] {
// RunFullscreen ejecuta un modelo Bubbletea en modo fullscreen.
func RunFullscreen[T tea.Model](model T) (T, error) {
// stub — implementation in devfactory/tui
var zero T
return Result[T]{Value: zero}
return zero, nil
}
+4 -6
View File
@@ -1,12 +1,10 @@
package tui
// Implementation: github.com/lucasdataproyects/devfactory/tui
import tea "github.com/charmbracelet/bubbletea"
import "github.com/charmbracelet/bubbletea"
// RunModel ejecuta un modelo Bubble Tea y devuelve el modelo final o error.
func RunModel[T tea.Model](model T) Result[T] {
// RunModel ejecuta un modelo Bubbletea y devuelve el modelo final.
func RunModel[T tea.Model](model T) (T, error) {
// stub — implementation in devfactory/tui
var zero T
return Result[T]{Value: zero}
return zero, nil
}
+4 -6
View File
@@ -1,12 +1,10 @@
package tui
// Implementation: github.com/lucasdataproyects/devfactory/tui
import tea "github.com/charmbracelet/bubbletea"
import "github.com/charmbracelet/bubbletea"
// RunWithMouseSupport ejecuta un modelo Bubble Tea con soporte de raton habilitado.
func RunWithMouseSupport[T tea.Model](model T) Result[T] {
// RunWithMouseSupport ejecuta un modelo Bubbletea con soporte de mouse.
func RunWithMouseSupport[T tea.Model](model T) (T, error) {
// stub — implementation in devfactory/tui
var zero T
return Result[T]{Value: zero}
return zero, nil
}
+28 -1
View File
@@ -1,8 +1,35 @@
module fn-registry
go 1.22.2
go 1.24.2
require (
github.com/mattn/go-sqlite3 v1.14.37
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v1.0.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.3.8 // indirect
)
+50
View File
@@ -1,5 +1,55 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+4
View File
@@ -2,10 +2,14 @@ from .client import MetabaseClient
from .users import metabase_list_users, metabase_get_user, metabase_create_user, metabase_update_user, metabase_deactivate_user
from .cards import metabase_list_cards, metabase_get_card, metabase_create_card, metabase_update_card, metabase_delete_card, metabase_execute_card, metabase_execute_query
from .dashboards import metabase_list_dashboards, metabase_get_dashboard, metabase_create_dashboard, metabase_update_dashboard, metabase_delete_dashboard
from .databases import metabase_list_databases, metabase_add_database, metabase_get_database
from .setup import metabase_setup
__all__ = [
"MetabaseClient",
"metabase_list_users", "metabase_get_user", "metabase_create_user", "metabase_update_user", "metabase_deactivate_user",
"metabase_list_cards", "metabase_get_card", "metabase_create_card", "metabase_update_card", "metabase_delete_card", "metabase_execute_card", "metabase_execute_query",
"metabase_list_dashboards", "metabase_get_dashboard", "metabase_create_dashboard", "metabase_update_dashboard", "metabase_delete_dashboard",
"metabase_list_databases", "metabase_add_database", "metabase_get_database",
"metabase_setup",
]
+105
View File
@@ -0,0 +1,105 @@
"""CRUD de databases de Metabase."""
from .client import MetabaseClient
def metabase_list_databases(
client: MetabaseClient,
include_tables: bool = False,
) -> list:
"""Lista las databases configuradas en Metabase.
Endpoint: GET /api/database.
Args:
client: Cliente autenticado.
include_tables: Si True, incluye las tablas de cada database en la respuesta.
Returns:
Lista de dicts, cada uno con campos de la database:
- id: ID numerico de la database
- name: Nombre dado en Metabase
- engine: Motor de BD (sqlite, postgres, mysql, etc.)
- details: Dict con parametros de conexion
- is_full_sync: Si sincroniza el schema automaticamente
- tables: Lista de tablas (solo si include_tables=True)
Example:
>>> dbs = metabase_list_databases(client)
>>> for db in dbs:
... print(db["id"], db["name"], db["engine"])
>>> dbs = metabase_list_databases(client, include_tables=True)
>>> for db in dbs:
... print(db["name"], [t["name"] for t in db.get("tables", [])])
"""
params = {}
if include_tables:
params["include"] = "tables"
result = client.request("GET", "/api/database", params=params)
if isinstance(result, dict) and "data" in result:
return result["data"]
return result
def metabase_add_database(
client: MetabaseClient,
name: str,
engine: str,
details: dict,
) -> dict:
"""Agrega una nueva database a Metabase.
Endpoint: POST /api/database. Requiere permisos de superusuario.
Args:
client: Cliente autenticado con permisos admin.
name: Nombre descriptivo para la database en Metabase.
engine: Motor de base de datos. Ejemplos: "sqlite", "postgres",
"mysql", "h2", "mongo", "bigquery".
details: Dict con parametros de conexion especificos del engine.
Para SQLite: {"db": "/ruta/al/archivo.db"}
Para Postgres: {"host": "...", "port": 5432, "dbname": "...",
"user": "...", "password": "..."}
Returns:
Dict con la database creada, incluyendo el campo "id" asignado
por Metabase y todos los campos de configuracion.
Raises:
httpx.HTTPStatusError: 400 si los datos de conexion son invalidos.
Example:
>>> db = metabase_add_database(client, "Mi SQLite", "sqlite", {"db": "/data/ops.db"})
>>> print(db["id"], db["name"])
"""
body = {
"name": name,
"engine": engine,
"details": details,
}
return client.request("POST", "/api/database", json=body)
def metabase_get_database(client: MetabaseClient, database_id: int) -> dict:
"""Obtiene los detalles de una database de Metabase por su ID.
Endpoint: GET /api/database/:id.
Args:
client: Cliente autenticado.
database_id: ID numerico de la database.
Returns:
Dict con campos de la database: id, name, engine, details,
is_full_sync, is_on_demand, auto_run_queries, created_at,
updated_at, features, etc.
Raises:
httpx.HTTPStatusError: 404 si la database no existe.
Example:
>>> db = metabase_get_database(client, 2)
>>> print(db["name"], db["engine"])
"""
return client.request("GET", f"/api/database/{database_id}")
@@ -0,0 +1,44 @@
---
name: metabase_add_database
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def metabase_add_database(client: MetabaseClient, name: str, engine: str, details: dict) -> dict"
description: "Agrega una nueva database a Metabase via POST /api/database. Soporta cualquier engine (sqlite, postgres, mysql, etc.)."
tags: [metabase, database, add, create, api, python]
uses_functions: []
uses_types: [MetabaseClient_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/databases.py"
---
## Ejemplo
```python
# SQLite
db = metabase_add_database(client, "Ops DB", "sqlite", {"db": "/data/operations.db"})
print(db["id"])
# Postgres
db = metabase_add_database(client, "Prod PG", "postgres", {
"host": "localhost",
"port": 5432,
"dbname": "myapp",
"user": "reader",
"password": "secret",
})
```
## Notas
Requiere permisos de superusuario. El campo `details` depende del engine:
para SQLite solo necesita `{"db": "/ruta/archivo.db"}`.
Retorna la database creada con su `id` asignado por Metabase.
@@ -0,0 +1,33 @@
---
name: metabase_get_database
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def metabase_get_database(client: MetabaseClient, database_id: int) -> dict"
description: "Obtiene los detalles de una database de Metabase por su ID. Endpoint: GET /api/database/:id."
tags: [metabase, database, get, api, python]
uses_functions: []
uses_types: [MetabaseClient_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/databases.py"
---
## Ejemplo
```python
db = metabase_get_database(client, 2)
print(db["name"], db["engine"])
```
## Notas
Error 404 si la database no existe. Retorna campos completos incluyendo
id, name, engine, details, is_full_sync, auto_run_queries, created_at, features.
@@ -0,0 +1,39 @@
---
name: metabase_list_databases
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def metabase_list_databases(client: MetabaseClient, include_tables: bool = False) -> list"
description: "Lista las databases configuradas en Metabase. Endpoint: GET /api/database. Soporta incluir tablas con include_tables=True."
tags: [metabase, database, list, api, python]
uses_functions: []
uses_types: [MetabaseClient_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/databases.py"
---
## Ejemplo
```python
dbs = metabase_list_databases(client)
for db in dbs:
print(db["id"], db["name"], db["engine"])
# Con tablas incluidas
dbs = metabase_list_databases(client, include_tables=True)
for db in dbs:
print(db["name"], [t["name"] for t in db.get("tables", [])])
```
## Notas
Si include_tables=True agrega el query param `?include=tables` al request.
Retorna lista directa (no paginada) de todas las databases accesibles.
@@ -0,0 +1,44 @@
---
name: metabase_setup
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "metabase_setup(base_url: str, admin_email: str, admin_password: str, admin_first_name: str, admin_last_name: str, site_name: str, site_locale: str) -> dict"
description: "Ejecuta el setup inicial de una instancia Metabase nueva via POST /api/setup. Obtiene el setup-token automaticamente y crea el usuario admin con preferencias del sitio."
tags: [metabase, setup, api, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/metabase/setup.py"
---
## Ejemplo
```python
from metabase.setup import metabase_setup
result = metabase_setup(
base_url="http://localhost:3000",
admin_email="admin@fnregistry.local",
admin_password="FnRegistry2024!",
admin_first_name="Lucas",
admin_last_name="Admin",
site_name="fn-registry",
site_locale="es",
)
print(result["id"]) # session token
```
## Notas
Solo funciona en instancias sin configurar (setup-token disponible). Si Metabase ya tiene un usuario, lanza RuntimeError.
El setup-token se obtiene automaticamente de GET /api/session/properties. Una vez usado, Metabase invalida el token.
+83
View File
@@ -0,0 +1,83 @@
"""Setup inicial de Metabase via API."""
import httpx
def metabase_setup(
base_url: str,
admin_email: str,
admin_password: str,
admin_first_name: str = "Admin",
admin_last_name: str = "User",
site_name: str = "Metabase",
site_locale: str = "en",
) -> dict:
"""Ejecuta el setup inicial de una instancia Metabase nueva.
Usa el setup-token de una instancia sin configurar para crear el
usuario admin y configurar preferencias del sitio. Solo funciona
una vez — si Metabase ya tiene un usuario, retorna error.
Endpoint: POST /api/setup (requiere setup-token valido).
Args:
base_url: URL base de la instancia (ej: "http://localhost:3000").
admin_email: Email del usuario admin a crear.
admin_password: Password del admin (min 8 chars con complejidad).
admin_first_name: Nombre del admin.
admin_last_name: Apellido del admin.
site_name: Nombre del sitio Metabase.
site_locale: Locale del sitio (ej: "es", "en").
Returns:
Dict con el session id del admin recien creado.
Raises:
httpx.HTTPStatusError: 400 si el setup-token es invalido o
Metabase ya fue configurado.
RuntimeError: Si no se puede obtener el setup-token.
Example:
>>> result = metabase_setup(
... "http://localhost:3000",
... "admin@example.com",
... "SecurePass123!",
... site_name="fn-registry",
... site_locale="es",
... )
>>> print(result["id"]) # session token
"""
url = base_url.rstrip("/")
# Obtener setup-token
resp = httpx.get(f"{url}/api/session/properties")
resp.raise_for_status()
props = resp.json()
setup_token = props.get("setup-token")
if not setup_token:
raise RuntimeError(
"No setup-token disponible. Metabase ya fue configurado "
"o has-user-setup es True."
)
# Ejecutar setup
resp = httpx.post(
f"{url}/api/setup",
json={
"token": setup_token,
"user": {
"first_name": admin_first_name,
"last_name": admin_last_name,
"email": admin_email,
"password": admin_password,
},
"prefs": {
"site_name": site_name,
"site_locale": site_locale,
"allow_tracking": False,
},
},
timeout=30.0,
)
resp.raise_for_status()
return resp.json()
+10
View File
@@ -239,6 +239,16 @@ func (db *DB) GetType(id string) (*Type, error) {
return &ts[0], nil
}
// GetFunctionsByName returns all functions matching a given name (across langs/domains).
func (db *DB) GetFunctionsByName(name string) ([]Function, error) {
rows, err := db.conn.Query("SELECT * FROM functions WHERE name = ? ORDER BY lang, domain", name)
if err != nil {
return nil, err
}
defer rows.Close()
return scanFunctions(rows)
}
// DeleteFunction removes a function by ID.
func (db *DB) DeleteFunction(id string) error {
_, err := db.conn.Exec("DELETE FROM functions WHERE id = ?", id)
+1 -1
View File
@@ -11,5 +11,5 @@ definition: |
description: "Tipo de error base del registry. Referenciado como error_type por funciones impuras."
tags: [error, base, impure]
uses_types: []
file_path: "types/core/error.go"
file_path: "functions/core/error.go"
---
+1 -1
View File
@@ -11,7 +11,7 @@ definition: |
description: "Tipo suma generico que representa un valor opcional: Some(T) o None. Alternativa a punteros nil para modelar ausencia de valor de forma explicita."
tags: [option, sum, nullable, functional, generic]
uses_types: []
file_path: "types/core/option.go"
file_path: "functions/core/option.go"
---
## Notas
+1 -1
View File
@@ -12,5 +12,5 @@ definition: |
description: "Tipo producto generico que agrupa dos valores de tipos potencialmente distintos. Util para ZipSlices y operaciones que devuelven dos resultados."
tags: [pair, tuple, product, generic]
uses_types: []
file_path: "types/core/pair.go"
file_path: "functions/core/pair.go"
---
+1 -1
View File
@@ -12,7 +12,7 @@ definition: |
description: "Tipo suma generico que representa exito (Ok) o fallo (Err). Permite componer operaciones que pueden fallar sin recurrir a multiples returns (T, error)."
tags: [result, sum, error-handling, functional, generic]
uses_types: []
file_path: "types/core/result.go"
file_path: "functions/core/result.go"
---
## Notas
+1 -1
View File
@@ -13,5 +13,5 @@ definition: |
description: "Rango de red CIDR parseado con network, broadcast y numero de hosts."
tags: [cybersecurity, network, cidr, ip]
uses_types: []
file_path: "types/cybersecurity/cidr_block.go"
file_path: "functions/cybersecurity/cidr_block.go"
---
+1 -1
View File
@@ -12,5 +12,5 @@ definition: |
description: "Tipo suma para resultados de escaneo TCP: Open (con banner), Closed o Filtered."
tags: [cybersecurity, port, scan, network]
uses_types: []
file_path: "types/cybersecurity/port_result.go"
file_path: "functions/cybersecurity/port_result.go"
---
+1 -1
View File
@@ -12,5 +12,5 @@ definition: |
description: "Tipo suma para resultados de deteccion de amenazas: Clean, Suspicious o Malicious."
tags: [cybersecurity, threat, detection, sqli]
uses_types: []
file_path: "types/cybersecurity/threat_result.go"
file_path: "functions/cybersecurity/threat_result.go"
---
+1 -1
View File
@@ -11,5 +11,5 @@ definition: |
description: "Tipo suma que clasifica un dato como Normal o Outlier con su z-score. Usado por DetectOutliers."
tags: [datascience, outlier, anomaly, statistics]
uses_types: []
file_path: "types/datascience/outlier_result.go"
file_path: "functions/datascience/outlier_result.go"
---
+1 -1
View File
@@ -13,5 +13,5 @@ definition: |
description: "Resultado de Bollinger Bands con bandas superior, media e inferior."
tags: [finance, bollinger, indicator, bands]
uses_types: []
file_path: "types/finance/bollinger_result.go"
file_path: "functions/finance/bollinger_result.go"
---
+1 -1
View File
@@ -13,5 +13,5 @@ definition: |
description: "Resultado de maximo drawdown con el valor de caida y los indices de inicio y fin."
tags: [finance, drawdown, risk, metric]
uses_types: []
file_path: "types/finance/drawdown_result.go"
file_path: "functions/finance/drawdown_result.go"
---
+1 -1
View File
@@ -15,5 +15,5 @@ definition: |
description: "Vela de mercado con precios de apertura, maximo, minimo, cierre y volumen."
tags: [finance, market, candle, ohlcv]
uses_types: []
file_path: "types/finance/ohlcv.go"
file_path: "functions/finance/ohlcv.go"
---
+1 -1
View File
@@ -14,5 +14,5 @@ definition: |
description: "Evento de trade individual en un mercado. Contiene simbolo, precio, volumen y timestamp."
tags: [finance, market, tick, trade]
uses_types: []
file_path: "types/finance/tick.go"
file_path: "functions/finance/tick.go"
---
+1 -1
View File
@@ -18,7 +18,7 @@ definition: |
description: "Información básica de un contenedor Docker: ID, nombre, imagen, estado, puertos, labels."
tags: [docker, container, infra]
uses_types: []
file_path: "types/infra/container_info.go"
file_path: "functions/infra/container_info.go"
---
## Ejemplo
+1 -1
View File
@@ -15,7 +15,7 @@ definition: |
description: "Información básica de una imagen Docker local: ID, repositorio, tag, tamaño, fecha."
tags: [docker, image, infra]
uses_types: []
file_path: "types/infra/image_info.go"
file_path: "functions/infra/image_info.go"
---
## Ejemplo
+1 -1
View File
@@ -12,7 +12,7 @@ definition: |
description: "Cliente para la API REST de Metabase. Contiene la URL base de la instancia y el token de autenticacion (session token o API key)."
tags: [metabase, api, client, infra]
uses_types: []
file_path: "types/infra/metabase_client.go"
file_path: "functions/infra/metabase_client.go"
---
## Notas
+1 -1
View File
@@ -13,7 +13,7 @@ definition: |
description: "Resultado de la ejecucion de un comando del sistema con stdout, stderr y codigo de salida."
tags: [shell, command, process, result]
uses_types: []
file_path: "types/shell/cmd_result.go"
file_path: "functions/shell/cmd_result.go"
---
## Notas
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Modelo base que provee dimensiones de terminal, estilos y manejo de errores comunes a todas las vistas TUI."
tags: [tui, base, model, component]
uses_types: [styles_go_tui]
file_path: "types/tui/base_model.go"
file_path: "functions/tui/base_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Dialogo de confirmacion Si/No interactivo. Embeds BaseModel. Implementa tea.Model."
tags: [tui, confirm, dialog, component, interactive]
uses_types: [base_model_go_tui]
file_path: "types/tui/confirm_model.go"
file_path: "functions/tui/confirm_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Lista con filtrado por texto en tiempo real. Embeds ListModel y añade busqueda interactiva."
tags: [tui, list, filter, component, interactive]
uses_types: [list_model_go_tui, list_item_go_tui]
file_path: "types/tui/filtered_list_model.go"
file_path: "functions/tui/filtered_list_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Item individual de una lista TUI con titulo, descripcion y valor arbitrario."
tags: [tui, list, component]
uses_types: []
file_path: "types/tui/list_item.go"
file_path: "functions/tui/list_item.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Componente lista seleccionable con cursor, scroll y seleccion simple o multiple. Implementa tea.Model."
tags: [tui, list, component, interactive]
uses_types: [list_item_go_tui, styles_go_tui]
file_path: "types/tui/list_model.go"
file_path: "functions/tui/list_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Gestor de multiples barras de progreso simultaneas. Implementa tea.Model."
tags: [tui, progress, multi, component]
uses_types: [progress_model_go_tui, styles_go_tui]
file_path: "types/tui/multi_progress_model.go"
file_path: "functions/tui/multi_progress_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Barra de progreso con porcentaje, ETA y tiempo transcurrido. Implementa tea.Model."
tags: [tui, progress, component, interactive]
uses_types: [styles_go_tui]
file_path: "types/tui/progress_model.go"
file_path: "functions/tui/progress_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Indicador de carga animado con mensaje personalizable. Implementa tea.Model."
tags: [tui, spinner, loading, component]
uses_types: []
file_path: "types/tui/spinner_model.go"
file_path: "functions/tui/spinner_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Spinner que se auto-detiene tras un timeout configurable. Embeds SpinnerModel."
tags: [tui, spinner, timeout, component]
uses_types: [spinner_model_go_tui]
file_path: "types/tui/spinner_with_timeout_model.go"
file_path: "functions/tui/spinner_with_timeout_model.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Coleccion completa de estilos lipgloss pre-configurados para tipografia, estados, componentes y layout."
tags: [tui, styles, lipgloss, theme]
uses_types: [theme_go_tui]
file_path: "types/tui/styles.go"
file_path: "functions/tui/styles.go"
---
+1 -1
View File
@@ -9,5 +9,5 @@ definition: |
description: "Paleta de colores para terminal con 9 colores semanticos. Base del sistema de estilos."
tags: [tui, theme, styles, colors]
uses_types: []
file_path: "types/tui/theme.go"
file_path: "functions/tui/theme.go"
---