Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b348670cc | |||
| fc4180cbb3 | |||
| 7eef442444 | |||
| 876020addf | |||
| 469e40ba40 | |||
| fce88032ca | |||
| fec8ebd4ec | |||
| f5f05e4624 | |||
| 532f3d0ea8 | |||
| fe65c5e527 | |||
| de9bfec498 | |||
| e9c64a4687 | |||
| 70ec825e32 | |||
| 22692c1ed2 | |||
| d128ad89ac | |||
| bd9f0d8437 | |||
| 207c08c3b7 | |||
| 01bc2aeb14 | |||
| 9ec7751f6f | |||
| fef86250a0 | |||
| 472b6092bb | |||
| ea5c94fc8a | |||
| a8b09ad154 | |||
| 6aa874f2b6 | |||
| 93352a7780 | |||
| 0ffae6daa4 | |||
| 74b58cf0d0 | |||
| 9752fb106a | |||
| 8cb0121573 | |||
| 90115270d2 | |||
| 11e6e27ad1 | |||
| a59b12d467 | |||
| fe4320af89 | |||
| f71e0f4c9a | |||
| 46b4385331 | |||
| 580238b32e | |||
| ed767360c1 | |||
| 5bac05ce13 | |||
| d0ceea6f3d | |||
| 0f905b78e0 | |||
| 5c7ff8d761 | |||
| 138f4b2713 | |||
| 25425a5fd6 | |||
| 89441539fa | |||
| 1d3d2f43b3 | |||
| 2effb688b0 | |||
| eb30074792 | |||
| f8efb7d177 | |||
| f428f2c82a | |||
| f36d091704 | |||
| 938853d268 | |||
| b31ea70771 | |||
| 2e5c630d38 | |||
| c52846d475 | |||
| b5affae68c | |||
| 5b4452b9fe | |||
| e0f8f3a068 | |||
| b21e7587ad | |||
| d4924f5cab | |||
| 853b3c0363 | |||
| f164ef230f | |||
| ff255c9a3c | |||
| 6c7f60fb6c | |||
| 75ac96a2d1 | |||
| da56085e74 | |||
| ecd864f2d3 | |||
| a91ef5aace | |||
| c2bdc586a4 | |||
| 61a46e4b21 | |||
| 3633d128aa | |||
| 892ff4f789 | |||
| 4388b54356 | |||
| b21adb40c9 | |||
| 6fd2e9d071 | |||
| d9ef4e54f4 | |||
| 2ea9206934 | |||
| 355bcac6c7 | |||
| 4eb4c1cf98 | |||
| 40aacac590 | |||
| e9bcbecd24 | |||
| 7eb7b3d0c8 | |||
| 61ec4c8a76 | |||
| a843f84a18 | |||
| 6f3c129a14 | |||
| bc270db723 | |||
| a3a263702b | |||
| 78c4f593a4 | |||
| 0f72cc8ad3 | |||
| 030e44b027 | |||
| ca2e5588cc | |||
| 5fb2269c00 | |||
| 5e6a974a5d | |||
| 5d2a14e50a | |||
| 212875ed0d | |||
| d6175964e4 | |||
| 5974484bd4 | |||
| 67fff0d677 | |||
| 890f641692 | |||
| ae5d27a5ec | |||
| 0ed949d235 | |||
| c438dc6916 | |||
| 4027aeaaf5 | |||
| 9ff0b3900c | |||
| ce9fa3b451 | |||
| e0cce972ea | |||
| 380a7a8f35 | |||
| 7ecbee1175 | |||
| fe39de8b22 | |||
| 951a77ec7f |
+4
-1
@@ -21,12 +21,14 @@ Cualquier decision tecnica que choque con estos objetivos esta mal priorizada. E
|
||||
|
||||
**Sync entre PCs:** `fn sync` sincroniza datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) contra `registry_api` en `https://registry.organic-machine.com`. Config: `~/.fn_pc` (identidad del PC), `FN_REGISTRY_API` (URL con basicAuth), `REGISTRY_API_TOKEN` (token).
|
||||
|
||||
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo.
|
||||
**Sub-repos:** cada app y cada analysis es su propio repo Gitea en `dataforge/<basename>` con branch `master` (ver ADR 0002). `apps/*` y `analysis/*` estan en el `.gitignore` del repo padre — el codigo de cada app vive en `apps/<name>/.git/`. Los slash commands `/full-git-push` y `/full-git-pull` orquestan push/pull/clone de fn_registry + todos los sub-repos + `fn sync`. `/full-git-push` auto-inicializa apps/analyses sin `.git` via `ensure_repo_synced_bash_infra`. Los `vaults/` y `subrepos/` NO entran en este flujo. **Gotcha worktrees**: si creas una app nueva dentro de un git worktree del repo padre, haz `git init` dentro de `apps/<name>/` ANTES de limpiar el worktree, sino el codigo se pierde (apps/* gitignored). Ver `.claude/rules/apps_subrepo.md`.
|
||||
|
||||
**Artefactos:** termino paraguas para apps, analysis, vaults, projects y playgrounds — todo lo que NO es codigo reutilizable. Usa "artefacto" cuando una afirmacion aplica a varios tipos a la vez para no repetir la lista. Ver `.claude/rules/artefactos.md` y `.claude/rules/playgrounds.md`.
|
||||
|
||||
**Reglas y convenciones:** ver `.claude/rules/INDEX.md`
|
||||
|
||||
**Slash commands:** `/commands` lista todos los slash commands del repo agrupados por namespace (global + projects). Project commands viven en `projects/<p>/.claude/commands/` y se exponen como `/<project>:<cmd>` via symlink. Ver `.claude/rules/project_commands.md`.
|
||||
|
||||
**Migraciones SQLite obligatorias:** todo cambio de schema en cualquier `.db` (apps, operations.db, registry.db) va en `migrations/NNN_*.sql` numerado. Aditivo, idempotente, aplicado al arrancar via `embed.FS`. Nunca borrar `.db` ni modificar migraciones existentes. Aplica retroactivamente. Ver `.claude/rules/db_migrations.md`.
|
||||
|
||||
---
|
||||
@@ -258,6 +260,7 @@ fn check params # Lista funciones sin params_schema
|
||||
fn doctor # Corre todos los checks
|
||||
fn doctor artefacts # git/venv/app.md/upstream de cada app y analysis
|
||||
fn doctor services # apps tag 'service' + systemctl + puerto
|
||||
fn doctor services-spec # audita bloque `service:` del app.md (issue 0105)
|
||||
fn doctor sync # drift pc_locations BD vs disco
|
||||
fn doctor uses-functions # imports reales vs uses_functions del app.md
|
||||
fn doctor unused # funciones del registry sin consumidores
|
||||
|
||||
@@ -42,10 +42,10 @@ Opcionalmente:
|
||||
|
||||
```bash
|
||||
# Por id
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE id = '<app_id>';"
|
||||
|
||||
# Por dir_path
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE dir_path = '<dir>';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, dir_path FROM apps WHERE dir_path = '<dir>';"
|
||||
```
|
||||
|
||||
Si no hay match → reportar y abortar.
|
||||
@@ -78,8 +78,8 @@ APP_DB="$APP_DIR/operations.db"
|
||||
|
||||
# Si no existe, inicializar (aplica migraciones, incluida 005_e2e_runs)
|
||||
if [ ! -f "$APP_DB" ]; then
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init "$APP_DIR"
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init "$APP_DIR"
|
||||
fi
|
||||
|
||||
# Verificar tabla e2e_runs existe (migracion 005)
|
||||
@@ -97,7 +97,7 @@ Hay dos caminos:
|
||||
**Camino A — invocar funcion del registry (preferido):**
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
./fn run e2e_run_checks_go_infra ...
|
||||
```
|
||||
|
||||
@@ -139,15 +139,15 @@ func main() {
|
||||
|
||||
Ejecutar con:
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
CGO_ENABLED=1 go run -tags fts5 /tmp/run_e2e_<id>.go /tmp/checks.yaml "$APP_DIR"
|
||||
```
|
||||
|
||||
### 5. Eval assertions activas (si la app las tiene)
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db "$APP_DB"
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval --db "$APP_DB"
|
||||
```
|
||||
|
||||
Capturar fallos como warning checks adicionales.
|
||||
|
||||
@@ -15,20 +15,20 @@ Eres el agente constructor del fn_registry. Tu rol es crear funciones, tests y t
|
||||
|
||||
```bash
|
||||
# Buscar si ya existe algo similar (OBLIGATORIO antes de crear)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
|
||||
# Buscar tipos existentes
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
|
||||
# Ver funciones de un dominio
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'DOMINIO' ORDER BY name;"
|
||||
|
||||
# Ver tipos de un dominio
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO';"
|
||||
|
||||
# Verificar que un ID referenciado existe
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = 'ID_AQUI';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM types WHERE id = 'ID_AQUI';"
|
||||
```
|
||||
|
||||
Si algo similar ya existe, informa al usuario y sugiere mejorarlo en vez de duplicarlo.
|
||||
@@ -39,13 +39,13 @@ Antes de implementar logica desde cero, busca funciones del registry que puedas
|
||||
|
||||
```bash
|
||||
# Buscar funciones reutilizables por lo que hacen (ampliar con OR y prefijos)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:filter* OR description:map* OR description:transform*') ORDER BY name;"
|
||||
|
||||
# Ver que retorna y que tipos usa una funcion candidata
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, returns, uses_types FROM functions WHERE id = 'ID_CANDIDATO';"
|
||||
|
||||
# Buscar funciones puras del mismo dominio (las mas componibles)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature FROM functions WHERE domain = 'DOMINIO' AND purity = 'pure' ORDER BY name;"
|
||||
```
|
||||
|
||||
**Criterios de reutilizacion:**
|
||||
@@ -78,38 +78,38 @@ Esto acelera la construccion y fortalece el grafo de dependencias del registry.
|
||||
| `bash` | `bash/functions/{domain}/{name}.sh` | `bash/functions/pipelines/{name}.sh` | *(no aplica)* |
|
||||
| `typescript` | `frontend/functions/{domain}/{name}.ts` | *(no aplica)* | `frontend/types/{domain}/{name}.ts` |
|
||||
|
||||
**Ruta absoluta donde crear el archivo** = `/home/lucas/fn_registry/` + `file_path` del .md.
|
||||
**Ruta absoluta donde crear el archivo** = `$HOME/fn_registry/` + `file_path` del .md.
|
||||
|
||||
Ejemplo: si `lang: bash` y `domain: infra`, el archivo va en:
|
||||
- `/home/lucas/fn_registry/bash/functions/infra/{name}.sh` + `.md`
|
||||
- **NUNCA** en `/home/lucas/fn_registry/functions/infra/{name}.sh`
|
||||
- `$HOME/fn_registry/bash/functions/infra/{name}.sh` + `.md`
|
||||
- **NUNCA** en `$HOME/fn_registry/functions/infra/{name}.sh`
|
||||
|
||||
### Estructura detallada
|
||||
|
||||
**Go** (carpeta raiz: `functions/` y `types/`)
|
||||
- Funciones: `/home/lucas/fn_registry/functions/{domain}/{name}.go` + `.md`
|
||||
- Tests: `/home/lucas/fn_registry/functions/{domain}/{name}_test.go`
|
||||
- Tipos: `/home/lucas/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `/home/lucas/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
|
||||
- Pipelines: `/home/lucas/fn_registry/functions/pipelines/{name}.go` + `.md`
|
||||
- Funciones: `$HOME/fn_registry/functions/{domain}/{name}.go` + `.md`
|
||||
- Tests: `$HOME/fn_registry/functions/{domain}/{name}_test.go`
|
||||
- Tipos: `$HOME/fn_registry/functions/{domain}/{name}.go` (codigo, mismo paquete Go) + `$HOME/fn_registry/types/{domain}/{name}.md` (metadata con file_path apuntando a functions/)
|
||||
- Pipelines: `$HOME/fn_registry/functions/pipelines/{name}.go` + `.md`
|
||||
- Paquete Go = nombre del directorio (core, finance, datascience, cybersecurity, infra, shell, tui, io)
|
||||
|
||||
**Python** (carpeta raiz: `python/`)
|
||||
- Funciones: `/home/lucas/fn_registry/python/functions/{domain}/{name}.py` + `.md`
|
||||
- Tests: `/home/lucas/fn_registry/python/functions/{domain}/{name}_test.py`
|
||||
- Tipos: `/home/lucas/fn_registry/python/types/{domain}/{name}.py` + `.md`
|
||||
- Pipelines: `/home/lucas/fn_registry/python/functions/pipelines/{name}.py` + `.md`
|
||||
- Funciones: `$HOME/fn_registry/python/functions/{domain}/{name}.py` + `.md`
|
||||
- Tests: `$HOME/fn_registry/python/functions/{domain}/{name}_test.py`
|
||||
- Tipos: `$HOME/fn_registry/python/types/{domain}/{name}.py` + `.md`
|
||||
- Pipelines: `$HOME/fn_registry/python/functions/pipelines/{name}.py` + `.md`
|
||||
|
||||
**Bash** (carpeta raiz: `bash/`)
|
||||
- Funciones: `/home/lucas/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
|
||||
- Tests: `/home/lucas/fn_registry/bash/functions/{domain}/{name}_test.sh`
|
||||
- Pipelines: `/home/lucas/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
|
||||
- Funciones: `$HOME/fn_registry/bash/functions/{domain}/{name}.sh` + `.md`
|
||||
- Tests: `$HOME/fn_registry/bash/functions/{domain}/{name}_test.sh`
|
||||
- Pipelines: `$HOME/fn_registry/bash/functions/pipelines/{name}.sh` + `.md`
|
||||
- Tipos: Bash no tiene tipos — usar solo `uses_types` para referenciar tipos de otros lenguajes
|
||||
|
||||
**TypeScript** (carpeta raiz: `frontend/`)
|
||||
- Funciones puras: `/home/lucas/fn_registry/frontend/functions/core/{name}.ts` + `.md`
|
||||
- Componentes React: `/home/lucas/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
|
||||
- Funciones puras: `$HOME/fn_registry/frontend/functions/core/{name}.ts` + `.md`
|
||||
- Componentes React: `$HOME/fn_registry/frontend/functions/ui/{name}.tsx` + `.md`
|
||||
- Tests: junto al archivo, `{name}.test.ts` o `{name}.test.tsx`
|
||||
- Tipos: `/home/lucas/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
|
||||
- Tipos: `$HOME/fn_registry/frontend/types/{domain}/{name}.ts` + `.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -591,7 +591,7 @@ Documentar completamente el .md igualmente.
|
||||
1. **BUSCAR** en registry.db con FTS5 si existe algo similar
|
||||
2. **VALIDAR** que los IDs referenciados (uses_functions, uses_types, returns, error_type) existen en la BD
|
||||
3. **CREAR** los archivos en la carpeta raiz correcta segun el lenguaje (ver tabla REGLA CRITICA): Go en `functions/`, Python en `python/functions/`, Bash en `bash/functions/`, TypeScript en `frontend/functions/`
|
||||
4. **INDEXAR** ejecutando: `cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index`
|
||||
4. **INDEXAR** ejecutando: `cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index`
|
||||
5. **VERIFICAR** con: `./fn show {id}` que se indexo correctamente
|
||||
6. Si hay errores de validacion, corregirlos y re-indexar
|
||||
|
||||
@@ -600,10 +600,10 @@ Documentar completamente el .md igualmente.
|
||||
1. **LEER** la funcion existente (codigo + .md) desde la BD: `sqlite3 registry.db "SELECT code, signature FROM functions WHERE id = '...'"`
|
||||
2. **CREAR** el archivo de test
|
||||
3. **EJECUTAR** los tests:
|
||||
- Go: `cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
|
||||
- Python: `cd /home/lucas/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
|
||||
- Go: `cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 -run TestNombre ./functions/{domain}/`
|
||||
- Python: `cd $HOME/fn_registry/python && python -m pytest functions/{domain}/{name}_test.py`
|
||||
- TypeScript: desde `frontend/`, ejecutar con el test runner configurado
|
||||
- Bash: `cd /home/lucas/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
|
||||
- Bash: `cd $HOME/fn_registry && bash bash/functions/{domain}/{name}_test.sh`
|
||||
4. **ACTUALIZAR** el .md con `tested: true`, `tests: [...]` y `test_file_path`
|
||||
5. **RE-INDEXAR** y verificar
|
||||
|
||||
@@ -620,19 +620,19 @@ Documentar completamente el .md igualmente.
|
||||
|
||||
```bash
|
||||
# Compilar CLI (necesario si se modifico codigo del CLI)
|
||||
cd /home/lucas/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
|
||||
cd $HOME/fn_registry && CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/
|
||||
|
||||
# Indexar registry
|
||||
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
|
||||
cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index
|
||||
|
||||
# Tests Go de un dominio
|
||||
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
|
||||
cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./functions/{domain}/
|
||||
|
||||
# Tests Go de todo el registry
|
||||
cd /home/lucas/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
|
||||
cd $HOME/fn_registry && CGO_ENABLED=1 go test -tags fts5 ./...
|
||||
|
||||
# Mostrar funcion indexada
|
||||
cd /home/lucas/fn_registry && ./fn show {id}
|
||||
cd $HOME/fn_registry && ./fn show {id}
|
||||
```
|
||||
|
||||
### fn run — Ejecutar funciones y pipelines directamente
|
||||
@@ -640,7 +640,7 @@ cd /home/lucas/fn_registry && ./fn show {id}
|
||||
Despues de crear/indexar, puedes ejecutar directamente con `fn run`:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Go pipeline (go run . en su directorio)
|
||||
./fn run init_metabase --project test
|
||||
@@ -729,7 +729,7 @@ Peticion: "Crea una funcion que calcule la media de un slice de float64"
|
||||
|
||||
### Paso 1: Buscar en BD
|
||||
```bash
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:mean* OR name:average* OR description:media* OR description:average*') ORDER BY name;"
|
||||
```
|
||||
|
||||
### Paso 2: Crear archivos
|
||||
@@ -823,6 +823,6 @@ func TestMean(t *testing.T) {
|
||||
|
||||
### Paso 3: Indexar y verificar
|
||||
```bash
|
||||
cd /home/lucas/fn_registry && CGO_ENABLED=1 ./fn index
|
||||
cd $HOME/fn_registry && CGO_ENABLED=1 ./fn index
|
||||
./fn show mean_go_core
|
||||
```
|
||||
|
||||
@@ -35,22 +35,22 @@ Las apps estan indexadas en registry.db con toda la metadata necesaria para ejec
|
||||
|
||||
```bash
|
||||
# Ver todas las apps disponibles
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, domain, description, entry_point, dir_path FROM apps ORDER BY name;"
|
||||
|
||||
# Ver app completa con dependencias y framework
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, entry_point, dir_path, uses_functions, uses_types, framework, tags FROM apps WHERE id = 'APP_ID';"
|
||||
|
||||
# Buscar apps por FTS (nombre, descripcion, tags, documentacion)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, description FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
|
||||
# Apps de un dominio
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, description, entry_point FROM apps WHERE domain = 'DOMINIO';"
|
||||
|
||||
# Apps que usan una funcion especifica
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name FROM apps WHERE uses_functions LIKE '%funcion_id%';"
|
||||
|
||||
# Ver documentacion completa de una app
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT documentation, notes FROM apps WHERE id = 'APP_ID';"
|
||||
```
|
||||
|
||||
**Campos clave de apps para ejecucion:**
|
||||
@@ -65,19 +65,19 @@ sqlite3 /home/lucas/fn_registry/registry.db "SELECT documentation, notes FROM ap
|
||||
|
||||
```bash
|
||||
# Ver pipeline/funcion completa
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, signature, description, uses_functions, uses_types FROM functions WHERE id = 'ID_AQUI';"
|
||||
|
||||
# Ver codigo de la funcion
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT code FROM functions WHERE id = 'ID_AQUI';"
|
||||
|
||||
# Pipelines disponibles (con tag launcher para TUI)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE kind = 'pipeline' ORDER BY name;"
|
||||
|
||||
# Funciones impuras ejecutables directamente
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, signature, description FROM functions WHERE purity = 'impure' AND kind = 'function' ORDER BY name;"
|
||||
|
||||
# Buscar por FTS
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
```
|
||||
|
||||
### Usar contexto de apps para ejecucion inteligente
|
||||
@@ -98,10 +98,10 @@ Cuando te pidan ejecutar una app, sigue este flujo:
|
||||
|
||||
```bash
|
||||
# Desde la raiz del registry
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Opcion A: Usar el CLI
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
|
||||
|
||||
# Opcion B: Copiar template directamente
|
||||
cp fn_operations/project_template/operations.db apps/{app_name}/operations.db
|
||||
@@ -221,10 +221,10 @@ Las entities representan los datos concretos del proyecto. Las relations documen
|
||||
### Crear entities (datos que el pipeline consume o produce)
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Entity de entrada
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
|
||||
--db apps/{app_name}/operations.db \
|
||||
--name "btc_ticks" \
|
||||
--type-ref "tick_go_finance" \
|
||||
@@ -235,7 +235,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
|
||||
--metadata '{"pair":"BTCUSDT","exchange":"binance"}'
|
||||
|
||||
# Entity de salida
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity add \
|
||||
--db apps/{app_name}/operations.db \
|
||||
--name "btc_ohlcv_5m" \
|
||||
--type-ref "ohlcv_go_finance" \
|
||||
@@ -249,7 +249,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity add \
|
||||
### Crear relations (como se conectan entities)
|
||||
|
||||
```bash
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation add \
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops relation add \
|
||||
--db apps/{app_name}/operations.db \
|
||||
--name "ticks_to_ohlcv" \
|
||||
--from-entity "{entity_id}" \
|
||||
@@ -262,13 +262,13 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation add \
|
||||
|
||||
```bash
|
||||
# Listar entities
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops entity list --db apps/{app_name}/operations.db
|
||||
|
||||
# Listar relations
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops relation list --db apps/{app_name}/operations.db
|
||||
|
||||
# Ver grafo ASCII
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
|
||||
```
|
||||
|
||||
---
|
||||
@@ -280,7 +280,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/ope
|
||||
`fn run` despacha automaticamente segun el lenguaje y tipo:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Go pipeline (go run . en su directorio)
|
||||
./fn run init_metabase --project test
|
||||
@@ -318,13 +318,13 @@ Para apps con su propio main.go/main.py/main.sh:
|
||||
|
||||
```bash
|
||||
# Go app
|
||||
cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
|
||||
cd $HOME/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . [flags]
|
||||
|
||||
# Python app
|
||||
cd /home/lucas/fn_registry/apps/{app_name} && python3 main.py [args]
|
||||
cd $HOME/fn_registry/apps/{app_name} && python3 main.py [args]
|
||||
|
||||
# Bash app
|
||||
cd /home/lucas/fn_registry/apps/{app_name} && bash main.sh [args]
|
||||
cd $HOME/fn_registry/apps/{app_name} && bash main.sh [args]
|
||||
```
|
||||
|
||||
### Capturar metricas de ejecucion
|
||||
@@ -340,7 +340,7 @@ Al ejecutar, siempre captura:
|
||||
```bash
|
||||
# Ejemplo: ejecutar con captura de tiempo
|
||||
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
OUTPUT=$(cd /home/lucas/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
|
||||
OUTPUT=$(cd $HOME/fn_registry/apps/{app_name} && CGO_ENABLED=1 go run -tags fts5 . 2>&1)
|
||||
EXIT_CODE=$?
|
||||
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
@@ -362,7 +362,7 @@ echo "Status: $STATUS | Start: $START | End: $END"
|
||||
### Via CLI
|
||||
|
||||
```bash
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
|
||||
--db apps/{app_name}/operations.db \
|
||||
--pipeline-id "tick_to_ohlcv_go_finance" \
|
||||
--relation-id "{relation_id}" \
|
||||
@@ -396,16 +396,16 @@ sqlite3 apps/{app_name}/operations.db "INSERT INTO executions (id, pipeline_id,
|
||||
|
||||
```bash
|
||||
# Listar todas
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db
|
||||
|
||||
# Por pipeline
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --pipeline-id "ID"
|
||||
|
||||
# Por status
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list --db apps/{app_name}/operations.db --status failure
|
||||
|
||||
# Detalle de una ejecucion
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution show --db apps/{app_name}/operations.db --id "EXEC_ID"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -441,12 +441,12 @@ Si hay assertions definidas sobre las entities afectadas, evaluarlas para verifi
|
||||
|
||||
```bash
|
||||
# Evaluar assertions de una entity
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval \
|
||||
--db apps/{app_name}/operations.db \
|
||||
--entity-id "ENTITY_ID"
|
||||
|
||||
# Evaluar Y reaccionar (actualiza status de entities, crea proposals si hay fallos criticos)
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval \
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval \
|
||||
--db apps/{app_name}/operations.db \
|
||||
--entity-id "ENTITY_ID" \
|
||||
--react
|
||||
@@ -467,10 +467,10 @@ Cuando el usuario pide ejecutar algo que aun no tiene app:
|
||||
|
||||
```bash
|
||||
# 1. Crear directorio
|
||||
mkdir -p /home/lucas/fn_registry/apps/{app_name}
|
||||
mkdir -p $HOME/fn_registry/apps/{app_name}
|
||||
|
||||
# 2. Crear app.md (OBLIGATORIO)
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
|
||||
---
|
||||
name: {app_name}
|
||||
lang: go
|
||||
@@ -490,7 +490,7 @@ dir_path: "apps/{app_name}"
|
||||
MDEOF
|
||||
|
||||
# 3. Crear .gitignore
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
|
||||
operations.db
|
||||
operations.db-wal
|
||||
operations.db-shm
|
||||
@@ -499,7 +499,7 @@ build/
|
||||
GIEOF
|
||||
|
||||
# 4. Inicializar modulo Go
|
||||
cd /home/lucas/fn_registry/apps/{app_name}
|
||||
cd $HOME/fn_registry/apps/{app_name}
|
||||
go mod init fn_registry/apps/{app_name}
|
||||
|
||||
# 5. Crear main.go minimo
|
||||
@@ -523,8 +523,8 @@ func main() {
|
||||
GOEOF
|
||||
|
||||
# 6. Inicializar operations.db
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
|
||||
|
||||
# 7. Indexar en registry.db
|
||||
./fn index
|
||||
@@ -534,10 +534,10 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
|
||||
```bash
|
||||
# 1. Crear directorio
|
||||
mkdir -p /home/lucas/fn_registry/apps/{app_name}
|
||||
mkdir -p $HOME/fn_registry/apps/{app_name}
|
||||
|
||||
# 2. Crear app.md (OBLIGATORIO)
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
|
||||
---
|
||||
name: {app_name}
|
||||
lang: py
|
||||
@@ -557,7 +557,7 @@ dir_path: "apps/{app_name}"
|
||||
MDEOF
|
||||
|
||||
# 3. Crear .gitignore
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
|
||||
operations.db
|
||||
operations.db-wal
|
||||
operations.db-shm
|
||||
@@ -565,7 +565,7 @@ __pycache__/
|
||||
GIEOF
|
||||
|
||||
# 4. Crear main.py
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/main.py << 'PYEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/main.py << 'PYEOF'
|
||||
"""Pipeline executor."""
|
||||
import sys
|
||||
import time
|
||||
@@ -584,8 +584,8 @@ if __name__ == "__main__":
|
||||
PYEOF
|
||||
|
||||
# 5. Inicializar operations.db
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
|
||||
|
||||
# 6. Indexar en registry.db
|
||||
./fn index
|
||||
@@ -595,10 +595,10 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
|
||||
```bash
|
||||
# 1. Crear directorio
|
||||
mkdir -p /home/lucas/fn_registry/apps/{app_name}
|
||||
mkdir -p $HOME/fn_registry/apps/{app_name}
|
||||
|
||||
# 2. Crear app.md (OBLIGATORIO)
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/app.md << 'MDEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/app.md << 'MDEOF'
|
||||
---
|
||||
name: {app_name}
|
||||
lang: bash
|
||||
@@ -618,14 +618,14 @@ dir_path: "apps/{app_name}"
|
||||
MDEOF
|
||||
|
||||
# 3. Crear .gitignore
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/.gitignore << 'GIEOF'
|
||||
operations.db
|
||||
operations.db-wal
|
||||
operations.db-shm
|
||||
GIEOF
|
||||
|
||||
# 4. Crear main.sh
|
||||
cat > /home/lucas/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
|
||||
cat > $HOME/fn_registry/apps/{app_name}/main.sh << 'SHEOF'
|
||||
#!/usr/bin/env bash
|
||||
# Pipeline executor: {app_name}
|
||||
set -euo pipefail
|
||||
@@ -650,11 +650,11 @@ main() {
|
||||
|
||||
main "$@"
|
||||
SHEOF
|
||||
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
|
||||
chmod +x $HOME/fn_registry/apps/{app_name}/main.sh
|
||||
|
||||
# 5. Inicializar operations.db
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
|
||||
|
||||
# 6. Indexar en registry.db
|
||||
./fn index
|
||||
@@ -669,7 +669,7 @@ Este patron captura todo lo necesario para registrar la ejecucion:
|
||||
### Go
|
||||
|
||||
```bash
|
||||
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
|
||||
APP_DIR="$HOME/fn_registry/apps/{app_name}"
|
||||
OPS_DB="$APP_DIR/operations.db"
|
||||
PIPELINE_ID="{pipeline_id}"
|
||||
RELATION_ID="{relation_id}" # vacio si no aplica
|
||||
@@ -689,8 +689,8 @@ else
|
||||
fi
|
||||
|
||||
# Registrar ejecucion
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
|
||||
--db "$OPS_DB" \
|
||||
--pipeline-id "$PIPELINE_ID" \
|
||||
--status "$STATUS" \
|
||||
@@ -704,7 +704,7 @@ rm -f "$STDOUT_FILE" "$STDERR_FILE"
|
||||
### Python
|
||||
|
||||
```bash
|
||||
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
|
||||
APP_DIR="$HOME/fn_registry/apps/{app_name}"
|
||||
OPS_DB="$APP_DIR/operations.db"
|
||||
|
||||
START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
@@ -716,8 +716,8 @@ END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
STATUS="success"
|
||||
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
|
||||
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
|
||||
--db "$OPS_DB" \
|
||||
--pipeline-id "{pipeline_id}" \
|
||||
--status "$STATUS" \
|
||||
@@ -728,7 +728,7 @@ FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
|
||||
### Bash
|
||||
|
||||
```bash
|
||||
APP_DIR="/home/lucas/fn_registry/apps/{app_name}"
|
||||
APP_DIR="$HOME/fn_registry/apps/{app_name}"
|
||||
OPS_DB="$APP_DIR/operations.db"
|
||||
PIPELINE_ID="{pipeline_id}"
|
||||
|
||||
@@ -741,8 +741,8 @@ END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
STATUS="success"
|
||||
[ $EXIT_CODE -ne 0 ] && STATUS="failure"
|
||||
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution add \
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution add \
|
||||
--db "$OPS_DB" \
|
||||
--pipeline-id "$PIPELINE_ID" \
|
||||
--status "$STATUS" \
|
||||
@@ -758,10 +758,10 @@ Antes de ejecutar, verifica que los snapshots de tipos en operations.db estan al
|
||||
|
||||
```bash
|
||||
# Verificar snapshots
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
|
||||
|
||||
# Actualizar si estan desactualizados
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -800,7 +800,7 @@ Crea una proposal cuando detectes:
|
||||
### Como crear proposals
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Proposal para nueva funcion
|
||||
./fn proposal add \
|
||||
@@ -840,7 +840,7 @@ Cuando la proposal viene de un fallo o anomalia en una ejecucion, incluye la evi
|
||||
|
||||
```bash
|
||||
# Obtener el ID de la ejecucion que evidencia el problema
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops execution list \
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops execution list \
|
||||
--db apps/{app_name}/operations.db --status failure
|
||||
|
||||
# Incluir evidencia en la descripcion
|
||||
@@ -858,19 +858,19 @@ Usa el contexto de la tabla apps para comparar y detectar patrones:
|
||||
|
||||
```bash
|
||||
# Ver que funciones usan las apps — detectar patrones comunes
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, uses_functions FROM apps WHERE uses_functions != '[]';"
|
||||
|
||||
# Ver funciones mas usadas por apps (candidatas a mejora)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "
|
||||
sqlite3 $HOME/fn_registry/registry.db "
|
||||
SELECT f.value as func_id, COUNT(*) as uso
|
||||
FROM apps, json_each(apps.uses_functions) f
|
||||
GROUP BY f.value ORDER BY uso DESC;"
|
||||
|
||||
# Ver apps que NO tienen funciones del registry (candidatas a extraccion)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, description FROM apps WHERE uses_functions = '[]';"
|
||||
|
||||
# Ver si ya existe una proposal para algo similar
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, status, title FROM proposals WHERE status = 'pending' ORDER BY created_at DESC;"
|
||||
```
|
||||
|
||||
### Flujo de deteccion al ejecutar
|
||||
|
||||
@@ -43,12 +43,12 @@ APP_ID="<input>"
|
||||
RUN_ID="<input>"
|
||||
|
||||
# dir_path desde registry
|
||||
DIR_PATH=$(sqlite3 /home/lucas/fn_registry/registry.db \
|
||||
DIR_PATH=$(sqlite3 $HOME/fn_registry/registry.db \
|
||||
"SELECT dir_path FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
|
||||
APP_ID=$(sqlite3 /home/lucas/fn_registry/registry.db \
|
||||
APP_ID=$(sqlite3 $HOME/fn_registry/registry.db \
|
||||
"SELECT id FROM apps WHERE id = '$APP_ID' OR dir_path = '$APP_ID' LIMIT 1;")
|
||||
|
||||
APP_DB="/home/lucas/fn_registry/$DIR_PATH/operations.db"
|
||||
APP_DB="$HOME/fn_registry/$DIR_PATH/operations.db"
|
||||
[ ! -f "$APP_DB" ] && APP_DB="/tmp/$(basename $DIR_PATH)_e2e_runs.db"
|
||||
|
||||
# Sanity check
|
||||
@@ -93,7 +93,7 @@ Por cada fallo:
|
||||
Antes de crear proposal, verificar que no haya una identica abierta:
|
||||
|
||||
```bash
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "
|
||||
sqlite3 $HOME/fn_registry/registry.db "
|
||||
SELECT id FROM proposals
|
||||
WHERE status = 'pending'
|
||||
AND target_id = '$APP_ID'
|
||||
@@ -139,7 +139,7 @@ Sugerencia generica en `description` (NO codigo concreto, solo direccion):
|
||||
Si la misma assertion/check ha disparado proposal mas de 3 veces en los ultimos 30 dias, marcar `priority` (campo extendido si existe, si no, anotar en `description: '[REINCIDENTE x4]'`).
|
||||
|
||||
```bash
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "
|
||||
sqlite3 $HOME/fn_registry/registry.db "
|
||||
SELECT COUNT(*) FROM proposals
|
||||
WHERE target_id = '$APP_ID'
|
||||
AND title LIKE '%::$CHECK_ID%'
|
||||
|
||||
@@ -30,6 +30,16 @@ Referencia completa: `dev/issues/0069-autonomous-agent-loop-self-iterating-tasks
|
||||
6. **Auditoria total**. Cada decision se loggea en `task_runs.progress_json` con razonamiento + fase + run_id.
|
||||
7. **No self-modify**. NO modificas tu propio SKILL.md ni el de otros subagentes en la misma run.
|
||||
8. **Cero produccion**. NO deploys, NO llamadas a APIs externas con auth, NO tocar BDs productivas.
|
||||
9. **NUNCA paths absolutos fuera del worktree**. SIEMPRE rutas relativas o absolutas que apunten dentro de `/tmp/fn_orq_<issue>_<ts>/`. Si necesitas leer algo del repo principal (ej. plantillas docs), copialo al worktree primero. Refuerzo del piloto 1 (2026-05-15): orquestador modifico hooks bash del repo principal usando paths absolutos `$HOME/fn_registry/bash/functions/...` para destrancar pre-commit. Solucion correcta: el fix vive en el worktree, NO en main.
|
||||
10. **Pre-commit hook compartido**. Worktrees comparten `.git/hooks/` con main repo. Si el hook llama scripts via path absoluto a main (ej. `$HOME/fn_registry/bash/functions/cybersecurity/scan_secrets_in_dirty.sh`), el hook ejecutara la version de MAIN, no la del worktree. Opciones legitimas:
|
||||
a. Aplicar el fix del hook EN EL WORKTREE y commitearlo en `auto/*` — al mergear el PR, main heredara el fix.
|
||||
b. Si el hook bloquea progreso y el fix del hook excede tu scope, `git commit --no-verify` para ESE commit SOLO, documentando excepcion en `task_runs.events_json[].decision="skip_hook"` con razon.
|
||||
NO modificar archivos en main directamente.
|
||||
11. **Post-iteracion sanity check**. Tras cada commit en `auto/*`, verificar:
|
||||
```bash
|
||||
git -C $HOME/fn_registry status --short
|
||||
```
|
||||
Si la salida cambia respecto al baseline (capturado al inicio del piloto), HAS contaminado el repo principal. ABORT con `status=sandbox_breach` y reporta los archivos afectados en el output al humano.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,24 +49,24 @@ Antes de arrancar el bucle, comprobar:
|
||||
|
||||
```bash
|
||||
# 1. Migration 006_task_runs.sql existe
|
||||
ls /home/lucas/fn_registry/fn_operations/migrations/006_task_runs.sql 2>/dev/null \
|
||||
ls $HOME/fn_registry/fn_operations/migrations/006_task_runs.sql 2>/dev/null \
|
||||
|| { echo "ABORT: migration 006_task_runs.sql ausente. Aplicar issue 0069 paso 1 antes."; exit 2; }
|
||||
|
||||
# 2. Subagentes fn-* presentes
|
||||
for a in fn-constructor fn-executor fn-recopilador fn-analizador fn-mejorador; do
|
||||
test -f /home/lucas/fn_registry/.claude/agents/$a/SKILL.md \
|
||||
test -f $HOME/fn_registry/.claude/agents/$a/SKILL.md \
|
||||
|| { echo "ABORT: subagente $a ausente"; exit 2; }
|
||||
done
|
||||
|
||||
# 3. master local up-to-date con origin (worktree se creara desde master)
|
||||
git -C /home/lucas/fn_registry fetch origin master --quiet
|
||||
LOCAL=$(git -C /home/lucas/fn_registry rev-parse master)
|
||||
REMOTE=$(git -C /home/lucas/fn_registry rev-parse origin/master)
|
||||
git -C $HOME/fn_registry fetch origin master --quiet
|
||||
LOCAL=$(git -C $HOME/fn_registry rev-parse master)
|
||||
REMOTE=$(git -C $HOME/fn_registry rev-parse origin/master)
|
||||
test "$LOCAL" = "$REMOTE" \
|
||||
|| { echo "ABORT: master local desincronizado con origin. git pull antes."; exit 2; }
|
||||
|
||||
# 4. Branch auto/<issue> NO existe ya (ni local ni en worktrees)
|
||||
git -C /home/lucas/fn_registry rev-parse --verify "auto/${ISSUE_ID}" >/dev/null 2>&1 \
|
||||
git -C $HOME/fn_registry rev-parse --verify "auto/${ISSUE_ID}" >/dev/null 2>&1 \
|
||||
&& { echo "ABORT: branch auto/${ISSUE_ID} ya existe. Limpiar antes (git branch -D + worktree remove)."; exit 2; }
|
||||
|
||||
# 5. gh CLI autenticado (necesario para PR draft al converger)
|
||||
@@ -106,7 +116,7 @@ BRANCH="auto/${ISSUE_ID}"
|
||||
TASK_RUN_ID="task_$(openssl rand -hex 8)"
|
||||
STARTED_AT=$(date +%s)
|
||||
WT_ROOT="/tmp/fn_orq_${ISSUE_ID}_${STARTED_AT}"
|
||||
REPO="/home/lucas/fn_registry"
|
||||
REPO="$HOME/fn_registry"
|
||||
|
||||
# Crear worktree aislado desde master (no toca el principal)
|
||||
git -C "$REPO" worktree add -b "$BRANCH" "$WT_ROOT" master \
|
||||
@@ -177,13 +187,13 @@ while iter < max_iterations and elapsed < max_minutes:
|
||||
|
||||
Usar `Agent` tool con `subagent_type` correcto. Prompt **autocontenido** (paths absolutos, IDs, criterio exito).
|
||||
|
||||
**CRITICO**: pasar `WT_ROOT` (worktree path) en cada prompt y exigir al subagente trabajar dentro de el. Subagentes NO deben tocar el repo principal `/home/lucas/fn_registry/`.
|
||||
**CRITICO**: pasar `WT_ROOT` (worktree path) en cada prompt y exigir al subagente trabajar dentro de el. Subagentes NO deben tocar el repo principal `$HOME/fn_registry/`.
|
||||
|
||||
Patron prompt:
|
||||
```
|
||||
Working dir: <WT_ROOT> # NO /home/lucas/fn_registry
|
||||
Working dir: <WT_ROOT> # NO $HOME/fn_registry
|
||||
Branch: auto/<issue_id>
|
||||
Repo principal (solo lectura para registry.db): /home/lucas/fn_registry
|
||||
Repo principal (solo lectura para registry.db): $HOME/fn_registry
|
||||
...
|
||||
```
|
||||
|
||||
@@ -336,7 +346,7 @@ Cada `progress_json` entry:
|
||||
|---|---|---|
|
||||
| `task_runs` no existe | migration 006 no aplicada | abortar pre-condicion 1 |
|
||||
| `worktree add` falla con "already exists" | branch o dir previo no limpiado | `git worktree prune` + `git branch -D auto/<id>`, reintentar |
|
||||
| Subagente toca `/home/lucas/fn_registry/` en vez de worktree | prompt sin `WT_ROOT` explicito | rebriefing con working dir explicito |
|
||||
| Subagente toca `$HOME/fn_registry/` en vez de worktree | prompt sin `WT_ROOT` explicito | rebriefing con working dir explicito |
|
||||
| `master` desincronizado con origin | falta `git pull` | abortar pre-condicion 3 |
|
||||
| Loop infinito (mismo fail siempre) | watchdog ausente o desactivado | watchdog OBLIGATORIO, no skipear |
|
||||
| Subagente devuelve output ambiguo | prompt insuficiente | rebriefing con paths/IDs explicitos |
|
||||
|
||||
@@ -40,10 +40,10 @@ apps/{app_name}/
|
||||
|
||||
```bash
|
||||
# Listar todas las apps
|
||||
ls -d /home/lucas/fn_registry/apps/*/
|
||||
ls -d $HOME/fn_registry/apps/*/
|
||||
|
||||
# Verificar que cada app tiene app.md
|
||||
for app in /home/lucas/fn_registry/apps/*/; do
|
||||
for app in $HOME/fn_registry/apps/*/; do
|
||||
name=$(basename "$app")
|
||||
echo "=== $name ==="
|
||||
[ -f "$app/app.md" ] && echo " app.md: OK" || echo " app.md: FALTA"
|
||||
@@ -82,8 +82,8 @@ sqlite3 "$APP_DB" "SELECT * FROM schema_migrations ORDER BY version;" 2>/dev/nul
|
||||
**Si faltan tablas**, aplicar migraciones:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
|
||||
```
|
||||
|
||||
### 3. Integridad de Entities
|
||||
@@ -96,7 +96,7 @@ sqlite3 "$APP_DB" "SELECT id, name, type_ref, status, domain, source FROM entiti
|
||||
|
||||
# Validar que type_ref existe en registry.db
|
||||
sqlite3 "$APP_DB" "SELECT DISTINCT type_ref FROM entities;" | while read ref; do
|
||||
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
|
||||
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM types WHERE id = '$ref';")
|
||||
if [ -z "$EXISTS" ]; then
|
||||
echo "ERROR: type_ref '$ref' no existe en registry.db"
|
||||
fi
|
||||
@@ -129,7 +129,7 @@ sqlite3 "$APP_DB" "SELECT r.id, r.name, r.to_entity FROM relations r WHERE r.to_
|
||||
|
||||
# Validar que 'via' referencia una funcion/pipeline del registry
|
||||
sqlite3 "$APP_DB" "SELECT DISTINCT via FROM relations WHERE via != '';" | while read via; do
|
||||
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
|
||||
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$via';")
|
||||
if [ -z "$EXISTS" ]; then
|
||||
echo "ERROR: relation.via '$via' no existe en registry.db"
|
||||
fi
|
||||
@@ -156,7 +156,7 @@ sqlite3 "$APP_DB" "SELECT id, pipeline_id, status, started_at, duration_ms, reco
|
||||
|
||||
# Validar que pipeline_id existe en registry.db
|
||||
sqlite3 "$APP_DB" "SELECT DISTINCT pipeline_id FROM executions;" | while read pid; do
|
||||
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
|
||||
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$pid';")
|
||||
if [ -z "$EXISTS" ]; then
|
||||
echo "ERROR: pipeline_id '$pid' no existe en registry.db"
|
||||
fi
|
||||
@@ -230,7 +230,7 @@ sqlite3 "$APP_DB" "SELECT id, version, lang, algebraic, snapped_at FROM types_sn
|
||||
|
||||
# Comparar con registry.db — detectar snapshots desactualizados
|
||||
sqlite3 "$APP_DB" "SELECT id, version FROM types_snapshot;" | while IFS='|' read id ver; do
|
||||
REG_VER=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
|
||||
REG_VER=$(sqlite3 $HOME/fn_registry/registry.db "SELECT version FROM types WHERE id = '$id';")
|
||||
if [ -z "$REG_VER" ]; then
|
||||
echo "WARN: snapshot '$id' ya no existe en registry.db"
|
||||
elif [ "$ver" != "$REG_VER" ]; then
|
||||
@@ -252,14 +252,14 @@ done
|
||||
|
||||
```bash
|
||||
# Verificar que la app esta en registry.db
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, domain, entry_point, dir_path FROM apps WHERE name = '{app_name}';"
|
||||
|
||||
# Verificar que uses_functions del app.md coincide con lo indexado
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT uses_functions FROM apps WHERE name = '{app_name}';"
|
||||
|
||||
# Verificar que todas las funciones referenciadas existen
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
|
||||
EXISTS=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT f.value FROM apps, json_each(apps.uses_functions) f WHERE apps.name = '{app_name}';" | while read fid; do
|
||||
EXISTS=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM functions WHERE id = '$fid';")
|
||||
if [ -z "$EXISTS" ]; then
|
||||
echo "ERROR: app usa funcion '$fid' que no existe en registry"
|
||||
fi
|
||||
@@ -273,7 +273,7 @@ done
|
||||
Patron para auditar TODAS las apps de una vez:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
echo "========================================="
|
||||
echo "AUDITORIA DE APPS — fn-recopilador"
|
||||
@@ -327,7 +327,7 @@ for app_dir in apps/*/; do
|
||||
[ "$ERROR_LOGS" -gt 0 ] 2>/dev/null && echo " [WARN] $ERROR_LOGS logs de error"
|
||||
|
||||
# 9. App indexada en registry.db
|
||||
INDEXED=$(sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
|
||||
INDEXED=$(sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM apps WHERE name = '$APP_NAME';" 2>/dev/null)
|
||||
[ -n "$INDEXED" ] && echo " [OK] Indexada en registry.db" || echo " [WARN] NO indexada en registry.db"
|
||||
done
|
||||
|
||||
@@ -393,25 +393,25 @@ echo "========================================="
|
||||
El recopilador puede sugerir o ejecutar estas reparaciones:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Aplicar migraciones faltantes
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
|
||||
|
||||
# Actualizar snapshot desactualizado
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot update --db apps/{app_name}/operations.db --id "TYPE_ID"
|
||||
|
||||
# Verificar snapshots
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops snapshot check --db apps/{app_name}/operations.db
|
||||
|
||||
# Evaluar assertions pendientes
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops assertion eval --db apps/{app_name}/operations.db --entity-id "ENTITY_ID"
|
||||
|
||||
# Re-indexar para que la app aparezca en registry.db
|
||||
./fn index
|
||||
|
||||
# Ver grafo de la app (util para diagnostico visual)
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops graph --db apps/{app_name}/operations.db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+13
-13
@@ -38,13 +38,13 @@ Antes de crear nada, recopilar contexto:
|
||||
|
||||
```bash
|
||||
# Buscar funciones relevantes por descripcion
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:TERMINO* OR name:TERMINO*') ORDER BY name;"
|
||||
|
||||
# Buscar apps similares
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, name, lang, description, uses_functions FROM apps WHERE id IN (SELECT id FROM apps_fts WHERE apps_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
|
||||
# Verificar que el nombre no esta tomado
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id FROM apps WHERE name = 'NOMBRE';"
|
||||
```
|
||||
|
||||
4. **Presentar plan al usuario** antes de ejecutar:
|
||||
@@ -79,7 +79,7 @@ Usar el Agent tool con `subagent_type: "fn-constructor"` pasando:
|
||||
Despues de que fn-constructor termine, verificar que todo se indexo:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry && ./fn index
|
||||
cd $HOME/fn_registry && ./fn index
|
||||
# Verificar cada funcion creada
|
||||
./fn show {id_de_cada_funcion}
|
||||
```
|
||||
@@ -91,7 +91,7 @@ cd /home/lucas/fn_registry && ./fn index
|
||||
### Estructura base (todos los lenguajes)
|
||||
|
||||
```bash
|
||||
mkdir -p /home/lucas/fn_registry/apps/{app_name}
|
||||
mkdir -p $HOME/fn_registry/apps/{app_name}
|
||||
```
|
||||
|
||||
### app.md (OBLIGATORIO — siempre primero)
|
||||
@@ -143,7 +143,7 @@ build/
|
||||
|
||||
**Go (CLI/TUI):**
|
||||
```bash
|
||||
cd /home/lucas/fn_registry/apps/{app_name}
|
||||
cd $HOME/fn_registry/apps/{app_name}
|
||||
go mod init fn_registry/apps/{app_name}
|
||||
# Crear main.go, app/, config/, views/ segun necesidad
|
||||
```
|
||||
@@ -151,7 +151,7 @@ go mod init fn_registry/apps/{app_name}
|
||||
**Go (Wails — desktop con UI):**
|
||||
```bash
|
||||
# Usar scaffold del registry
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
./fn run scaffold_wails_app -- --name {app_name} --dir apps/{app_name}
|
||||
```
|
||||
|
||||
@@ -165,20 +165,20 @@ cd /home/lucas/fn_registry
|
||||
```bash
|
||||
# Crear main.sh con source a funciones del registry
|
||||
# Pattern: source "$REGISTRY_ROOT/bash/functions/{domain}/{func}.sh"
|
||||
chmod +x /home/lucas/fn_registry/apps/{app_name}/main.sh
|
||||
chmod +x $HOME/fn_registry/apps/{app_name}/main.sh
|
||||
```
|
||||
|
||||
### Inicializar operations.db
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
FN_REGISTRY_ROOT=/home/lucas/fn_registry ./fn ops init apps/{app_name}
|
||||
cd $HOME/fn_registry
|
||||
FN_REGISTRY_ROOT=$HOME/fn_registry ./fn ops init apps/{app_name}
|
||||
```
|
||||
|
||||
### Indexar en registry.db
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry && ./fn index
|
||||
cd $HOME/fn_registry && ./fn index
|
||||
# Verificar
|
||||
sqlite3 registry.db "SELECT id, name, lang, domain FROM apps WHERE name = '{app_name}';"
|
||||
```
|
||||
@@ -241,7 +241,7 @@ Usar el Agent tool con `subagent_type: "gitea"` pasando:
|
||||
```bash
|
||||
# 1. Crear repo en Gitea (via API)
|
||||
# 2. Inicializar git en la app
|
||||
cd /home/lucas/fn_registry/apps/{app_name}
|
||||
cd $HOME/fn_registry/apps/{app_name}
|
||||
git init
|
||||
git add -A
|
||||
git commit -m "Initial commit: {app_name} — {descripcion}"
|
||||
@@ -256,7 +256,7 @@ git push -u origin master
|
||||
**Despues de publicar**, actualizar el `repo_url` en app.md y re-indexar:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry && ./fn index
|
||||
cd $HOME/fn_registry && ./fn index
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../projects/aurgi/.claude/commands
|
||||
@@ -1,121 +1,36 @@
|
||||
# /autonomous-task — Lanza fn-orquestador (Fase 6 del ciclo reactivo)
|
||||
|
||||
Lanza el meta-orquestador autonomo que recorre el bucle CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR sobre un issue, sin intervencion humana, hasta convergencia / estancamiento / timeout / limite de iteraciones.
|
||||
|
||||
Issue 0069. Pre-condiciones obligatorias (chequear ANTES de despachar):
|
||||
|
||||
1. Migration `fn_operations/migrations/006_task_runs.sql` aplicada.
|
||||
2. Subagentes `fn-constructor`, `fn-executor`, `fn-recopilador`, `fn-analizador`, `fn-mejorador`, `fn-orquestador` presentes en `.claude/agents/`.
|
||||
3. `dev/autonomous_protected_paths.json` existe.
|
||||
4. `master` local up-to-date con `origin/master`.
|
||||
5. Branch `auto/<issue_id>` NO existe ya.
|
||||
6. `gh auth status` OK (necesario para PR draft al converger).
|
||||
7. Tipo de tarea soportado: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`.
|
||||
|
||||
Si alguna pre-condicion falla → ABORT con razon. NO improvisar.
|
||||
|
||||
---
|
||||
description: "DEPRECADO 2026-05-19 — usa /autopilot. Wrapper directo a fn-orquestador conservado solo como debug primitive."
|
||||
---
|
||||
|
||||
## Argumento
|
||||
# /autonomous-task — DEPRECADO (sustituido por `/autopilot`)
|
||||
|
||||
`$ARGUMENTS` — `<issue_id>` o `<task_spec_path>` + flags opcionales.
|
||||
**ESTADO:** deprecado 2026-05-19. Usa `/autopilot <NNNN>` en su lugar.
|
||||
|
||||
```
|
||||
/autonomous-task 0070
|
||||
/autonomous-task 0070 --max-iterations 15 --max-minutes 90
|
||||
/autonomous-task 0070 --auto-apply-proposals safe
|
||||
/autonomous-task 0070 --dry-run
|
||||
/autonomous-task path/to/spec.yaml --branch auto/custom-name
|
||||
```
|
||||
## Por que deprecado
|
||||
|
||||
Flags:
|
||||
- `--max-iterations N` tope de iteraciones (default 10)
|
||||
- `--max-minutes M` timeout total (default 60)
|
||||
- `--auto-apply-proposals` `none|safe|aggressive` (default `safe`)
|
||||
- `--branch NAME` rama TBD (default `auto/<issue_id>`)
|
||||
- `--dry-run` simula, NO aplica
|
||||
`/autopilot` (v2, 2026-05-19) absorbe la funcionalidad y anade:
|
||||
- Pre-flight DoD readiness check (gate STOP — no arranca sin DoD).
|
||||
- Detector issue vs flow.
|
||||
- Reporte estructurado al humano post-delegate.
|
||||
- Self-Q&A migrado a fn-orquestador.
|
||||
|
||||
---
|
||||
Behaviour orquestador-side es identico. La unica diferencia es que `/autopilot` valida antes de delegar; `/autonomous-task` delegaba ciego.
|
||||
|
||||
## Comportamiento
|
||||
## Sustitucion 1:1
|
||||
|
||||
1. **Verificar pre-condiciones** con script bash (ver arriba). Si alguna falla, reportar y salir.
|
||||
2. **Despachar a `fn-orquestador`** via Agent tool con `subagent_type=fn-orquestador`. Pasar:
|
||||
- `issue_id` o `task_spec`
|
||||
- flags resueltos
|
||||
- paths protegidos (leidos de `dev/autonomous_protected_paths.json`)
|
||||
3. **El subagente:**
|
||||
- Crea worktree aislado `/tmp/fn_orq_<issue>_<ts>/` desde `master`.
|
||||
- Persiste estado en `task_runs` (operations.db del app target o repo root).
|
||||
- Despacha por fases a los 5 subagentes especializados.
|
||||
- Aplica proposals filtradas por `--auto-apply-proposals`.
|
||||
- Termina con: `converged` (PR draft creado) | `stalled` | `timeout` | `iterations_exhausted` | `needs_human` | `aborted`.
|
||||
4. **Reportar resultado al humano** con:
|
||||
- `status`, `iterations / max`, `duration / max`
|
||||
- `branch`, `worktree`, `PR draft url` si converged
|
||||
- `proposals creadas / aplicadas`
|
||||
- `last run_id` y status
|
||||
- Resumen iter-por-iter del `progress_json`
|
||||
| Antes | Ahora |
|
||||
|---|---|
|
||||
| `/autonomous-task 0070` | `/autopilot 0070` |
|
||||
| `/autonomous-task 0070 --max-iterations 15 --max-minutes 90` | `/autopilot 0070 --max-iterations 15 --max-minutes 90` |
|
||||
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
|
||||
| `/autonomous-task 0070 --auto-apply-proposals safe` | `/autopilot 0070 --auto-apply-proposals safe` |
|
||||
|
||||
---
|
||||
## Modo debug
|
||||
|
||||
## Reglas duras (no negociables)
|
||||
Si `/autopilot` falla en pre-flight pero quieres forzar dispatch sin DoD check (debug / experimentos), puedes seguir usando `/autonomous-task` que va directo a `fn-orquestador` sin validar. NO RECOMENDADO para uso normal.
|
||||
|
||||
- Sandbox de rama EN WORKTREE — nunca toca master ni el working tree del humano.
|
||||
- No merge automatico — PR draft siempre.
|
||||
- No `--no-verify`, no `--force`, no skip hooks.
|
||||
- Paths protegidos via `dev/autonomous_protected_paths.json`.
|
||||
- Watchdog: 2 iteraciones con mismo set de fails → `status=stalled`.
|
||||
- Auditoria total en `task_runs.progress_json`.
|
||||
- No self-modification: NO toca `.claude/agents/` ni `.claude/commands/`.
|
||||
## Migration deadline
|
||||
|
||||
---
|
||||
Sin deadline duro — `/autonomous-task` seguira funcionando hasta que un commit lo elimine. Pero NO se anaden nuevas features aqui; cualquier mejora va a `/autopilot`.
|
||||
|
||||
## Integracion con call_monitor (issue 0085)
|
||||
|
||||
El orquestador puede leer `projects/fn_monitoring/apps/call_monitor/operations.db` para:
|
||||
|
||||
- Consultar `function_stats` antes de decidir que funciones usar/reusar.
|
||||
- Filtrar proposals existentes via `mcp__registry__fn_proposal --status pending` para evitar duplicados.
|
||||
- Loggear sus invocaciones via el hook PostToolUse (automatico).
|
||||
|
||||
Tras converger, el `call_monitor propose` ejecutado por el humano (o futuro cron) absorbera las nuevas violations / copied_code / fails para alimentar la siguiente ronda.
|
||||
|
||||
---
|
||||
|
||||
## Tipos NO soportados
|
||||
|
||||
- Diseño arquitectura nuevo (humano decide).
|
||||
- Decisiones UX subjetivas.
|
||||
- Cambios BD productiva.
|
||||
- Cualquier cosa que toque secrets/credenciales.
|
||||
- Self-modification del propio orquestador.
|
||||
|
||||
Si el issue contiene criterios no-verificables programaticamente, ABORT con `status=needs_human`.
|
||||
|
||||
---
|
||||
|
||||
## Output canonico
|
||||
|
||||
```
|
||||
=== /autonomous-task: 0070 ===
|
||||
status: converged
|
||||
iterations: 7 / 10
|
||||
duration: 23 min / 60
|
||||
branch: auto/0070
|
||||
worktree: /tmp/fn_orq_0070_1731612345
|
||||
PR draft: https://github.com/.../pull/123
|
||||
proposals: 3 creadas, 2 auto-aplicadas
|
||||
last run_id: e2e_run_abc123 (status: pass)
|
||||
|
||||
Iter:
|
||||
1. construir → ok (2 funciones nuevas)
|
||||
2. ejecutar → ok
|
||||
3. analizar → fail (2/8 checks)
|
||||
4. mejorar → 3 proposals (2 auto-applicadas)
|
||||
5. construir → ok (re-build tras patches)
|
||||
6. analizar → pass
|
||||
7. recopilador → ok (operations.db integra)
|
||||
|
||||
Siguiente: revisar PR draft + fn proposal list -s pending --target-id 0070
|
||||
```
|
||||
Ver `.claude/commands/autopilot.md` para spec completa.
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
name: autopilot
|
||||
description: Modo full-auto. Pre-flight DoD check, detecta issue vs flow, SIEMPRE delega a fn-orquestador (worktree aislado + PR Gitea). Sin Path inline. Sustituye a /autonomous-task.
|
||||
---
|
||||
|
||||
# /autopilot — Comando autonomo unificado
|
||||
|
||||
Comando UNICO para ejecutar issue o flow autonomo end-to-end. Sustituye a `/autonomous-task` (deprecado). Hace dos cosas:
|
||||
|
||||
1. **Pre-flight DoD readiness check** — sin DoD claro, no arranca.
|
||||
2. **Delega SIEMPRE a `fn-orquestador`** via Agent tool — worktree aislado en `/tmp/fn_orq_<NNNN>_<ts>/`, branch `auto/<NNNN>-<slug>`, PR draft Gitea al converger.
|
||||
|
||||
NO ejecuta nada inline. NO muta cwd del shell del humano. NO duplica worktrees. Toda la complejidad de bucle + paths protegidos + sanity check vive en `fn-orquestador`.
|
||||
|
||||
## Por que solo delegar
|
||||
|
||||
Historico: versiones anteriores de `/autopilot` tenian Path A (delegate a orquestador), Path B (registry-only inline), Path C (flow inline). Los Path B/C reimplementaban lo que ya hace `fn-orquestador` (worktree, branch, PR) y arrastraban un bug: `cd` en Bash de Claude Code PERSISTE entre llamadas → si autopilot hace `cd "$WT"`, todos los Bash subsiguientes operan en branch incorrecta. Solucion: NO hacer Path inline, delegar siempre.
|
||||
|
||||
`fn-orquestador` ahora soporta dos `task_type`:
|
||||
- `issue` — flujo CONSTRUIR→EJECUTAR→RECOPILAR→ANALIZAR→MEJORAR (default).
|
||||
- `flow` — parsea `dev/flows/<NNNN>-*.md` ## Flow y ejecuta steps (Path C absorbido).
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/autopilot <NNNN> # issue NNNN (default si no hay prefijo)
|
||||
/autopilot issue:<NNNN> # issue explicito
|
||||
/autopilot i:<NNNN> # alias
|
||||
/autopilot flow:<NNNN> # flow NNNN
|
||||
/autopilot f:<NNNN> # alias
|
||||
/autopilot check <target> # solo audita DoD readiness, no delega
|
||||
/autopilot <target> --max-iterations N --max-minutes M --dry-run
|
||||
```
|
||||
|
||||
Detector:
|
||||
- `^\d{4}[a-z]?$` → issue (sin prefijo = issue por defecto).
|
||||
- `^(issue|i):\d{4}[a-z]?$` → issue.
|
||||
- `^(flow|f):\d{4}$` → flow.
|
||||
- Otra cosa → ABORT con error de sintaxis.
|
||||
|
||||
## Pre-flight DoD readiness check (OBLIGATORIO)
|
||||
|
||||
Sin DoD claro, autopilot no delega. Verificacion es STOP-gate.
|
||||
|
||||
### Issue (`dev/issues/<NNNN>-*.md`)
|
||||
|
||||
1. Archivo existe en `dev/issues/` (no en `completed/`).
|
||||
2. Frontmatter con `status`, `priority`.
|
||||
3. Al menos UNA de:
|
||||
- `## DoD` o `## Definition of Done` con >=1 bullet/checkbox concreto.
|
||||
- `## Acceptance` con checkboxes `[ ]`.
|
||||
- `## Tests` + `## Tareas` ambas no vacias.
|
||||
4. Tipo declarado/inferible soportado por `fn-orquestador`: `feature_app_simple`, `bugfix_with_repro`, `refactor_safe`, `add_e2e_check`, `feature_registry_only`.
|
||||
5. NO contiene criterios no-verificables ("queda bonito", "intuitivo", "UX mejor"). Grep simple; si match → ABORT con warning.
|
||||
|
||||
### Flow (`dev/flows/<NNNN>-*.md`)
|
||||
|
||||
1. Archivo existe en `dev/flows/`.
|
||||
2. Frontmatter valido.
|
||||
3. `## Acceptance` con >=1 checkbox.
|
||||
4. `## Flow` no vacio.
|
||||
5. Pre-requisitos declarados.
|
||||
6. Tabla de funciones recomendadas sin `FALTA: crear <id>` (si los hay → ABORT salvo `--allow-construct-missing`).
|
||||
|
||||
Si falla:
|
||||
|
||||
```
|
||||
=== /autopilot check 0125 ===
|
||||
status: NOT READY
|
||||
target: issue 0125 (skill-tree-dashboard-panel)
|
||||
gaps:
|
||||
- Sin seccion DoD/Acceptance
|
||||
- "UX intuitiva" linea 47 — no verificable
|
||||
fix:
|
||||
- Anadir ## DoD con 3-5 bullets programaticamente verificables
|
||||
- Reemplazar criterios subjetivos por mediciones concretas
|
||||
```
|
||||
|
||||
Si OK:
|
||||
|
||||
```
|
||||
=== /autopilot check 0107c ===
|
||||
status: READY
|
||||
target: issue 0107c (refactor data_table)
|
||||
dod_items: 5 checkboxes
|
||||
task_type: refactor_safe
|
||||
estimated_iter: 3-5
|
||||
```
|
||||
|
||||
## Dispatch a fn-orquestador
|
||||
|
||||
Tras pre-flight OK, ejecuta:
|
||||
|
||||
```
|
||||
Agent(
|
||||
subagent_type="fn-orquestador",
|
||||
prompt="""
|
||||
Issue/Flow: <path al .md>
|
||||
Modo: REAL (o --dry-run)
|
||||
task_type: <issue|flow>
|
||||
Pre-condiciones verificadas: 7/7 verde
|
||||
Master: <sha> sync con origin
|
||||
Working tree principal: limpio (baseline)
|
||||
Max iter: N
|
||||
Max min: M
|
||||
Auto-apply proposals: safe
|
||||
Token Gitea: pass gitea/dataforge-git-token
|
||||
DB task_runs: apps/deploy_server/operations.db (schema task_id)
|
||||
Reglas duras: autonomous_loop.md (11 reglas)
|
||||
""",
|
||||
run_in_background=true
|
||||
)
|
||||
```
|
||||
|
||||
Cuando termine, reporta al humano con output canonico del orquestador:
|
||||
|
||||
```
|
||||
=== /autopilot 0121b ===
|
||||
target: issue 0121b (fn doctor e2e-coverage)
|
||||
delegated_to: fn-orquestador
|
||||
status: converged
|
||||
iterations: 1 / 8
|
||||
duration: 4 min / 30
|
||||
task_run_id: task_d285372493cce2e6
|
||||
branch: auto/0121b-orquestador
|
||||
worktree: /tmp/fn_orq_0121b_1779147778
|
||||
PR draft: https://gitea-.../dataforge/fn_registry/pulls/3
|
||||
|
||||
Siguiente: revisar PR, mergear, mover issue a completed/
|
||||
```
|
||||
|
||||
## Reglas duras (autopilot-level)
|
||||
|
||||
1. **Cero cwd mutation**. Autopilot NUNCA hace `cd`. Usa `git -C <repo>` siempre si necesita inspeccionar.
|
||||
2. **Cero ejecucion inline de bucle**. Todo va via `fn-orquestador`. Si autopilot necesita ejecutar algo (pre-flight scripts), es read-only.
|
||||
3. **Cero AskUserQuestion**. Self-pick "Recommended". Si no hay, ABORT con `status=needs_human`.
|
||||
4. **DoD es contrato**. Si DoD no se cumple al final, `task_run.status` queda `partial` y autopilot reporta NOT_DONE — humano decide.
|
||||
5. **Worktree gestion delegada al orquestador**. Autopilot NO crea worktrees propios. NO toca branches.
|
||||
6. **Trazabilidad**: cada decision pre-delegate (especialmente abort de DoD check) se persiste en `task_runs.events_json[]` con `agent: autopilot`.
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Default | Que hace |
|
||||
|---|---|---|
|
||||
| `--max-iterations N` | 10 | Pasado al orquestador |
|
||||
| `--max-minutes M` | 60 | Pasado al orquestador |
|
||||
| `--dry-run` | off | Pasado al orquestador |
|
||||
| `--allow-construct-missing` | off | Flow con `FALTA: crear <id>` → spawn fn-constructor antes |
|
||||
| `--auto-apply-proposals` | `safe` | Pasado al orquestador |
|
||||
|
||||
## Errores canonicos
|
||||
|
||||
| Codigo | Significado | Accion |
|
||||
|---|---|---|
|
||||
| `NOT_READY` | DoD insuficiente | Humano edita .md y relanza |
|
||||
| `needs_human` | Decision ambigua | Humano resuelve y relanza |
|
||||
| `delegated_failed` | fn-orquestador devolvio fail/stall/timeout | Humano lee `task_runs.events_json` |
|
||||
| (resto) | Heredados del orquestador (stalled/timeout/aborted_protected_path/...) | Idem |
|
||||
|
||||
## Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| Hacer Path B/C inline | Mismo bug de cwd mutation que paso 2026-05-19 |
|
||||
| Saltar pre-flight DoD | Trabajar sin contrato = bucle infinito |
|
||||
| Mergear sin tests verde | fn-orquestador ya impide esto, NO bypaseas |
|
||||
| `AskUserQuestion` desde autopilot | Rompe contrato autonomo |
|
||||
| Crear worktree propio en autopilot | Duplica + colision con orquestador (paso 2026-05-19) |
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```bash
|
||||
# Issue con DoD claro
|
||||
/autopilot 0107c
|
||||
|
||||
# Flow con piezas faltantes — autoriza creacion antes
|
||||
/autopilot flow:0008 --allow-construct-missing
|
||||
|
||||
# Solo audit
|
||||
/autopilot check 0125
|
||||
/autopilot check flow:0008
|
||||
|
||||
# Dry run
|
||||
/autopilot 0107c --dry-run
|
||||
```
|
||||
|
||||
## Relacion con otras reglas
|
||||
|
||||
- [[autonomous_loop]] — politica del bucle (sandbox, paths protegidos, watchdog). fn-orquestador la aplica.
|
||||
- [[apps_tbd]] — politica TBD por tipo de cambio.
|
||||
- [[apps_subrepo]] — `git init` dentro de apps nuevas antes de limpiar worktree.
|
||||
- [[feature_flags]] — codigo incompleto detras de flag OFF.
|
||||
- [[registry_calls]] — invocaciones canonicas.
|
||||
- [[e2e_validation]] — `e2e_checks` consumidos por fn-analizador.
|
||||
- [[delegation]] — spawn fn-constructor antes que escribir inline.
|
||||
|
||||
## Migracion desde `/autonomous-task`
|
||||
|
||||
`/autonomous-task` queda DEPRECADO. Sustitucion 1:1:
|
||||
|
||||
| Antes | Ahora |
|
||||
|---|---|
|
||||
| `/autonomous-task 0070` | `/autopilot 0070` |
|
||||
| `/autonomous-task 0070 --max-iterations 15` | `/autopilot 0070 --max-iterations 15` |
|
||||
| `/autonomous-task 0070 --dry-run` | `/autopilot 0070 --dry-run` |
|
||||
|
||||
`/autopilot` anade pre-flight DoD check + detect flow. Behaviour orquestador-side idem.
|
||||
|
||||
## Historico
|
||||
|
||||
- v1 (2026-05-15): introducido con Path A/B/C inline + self-Q&A.
|
||||
- v2 (2026-05-19): simplificado tras incidente cwd mutation en piloto 0121b. Solo delega a fn-orquestador. Self-Q&A movido al orquestador. Sustituye a `/autonomous-task`.
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
description: "Lista todos los slash commands disponibles en el repo: globales de fn_registry + namespaced de cada project. Filtra por substring o por namespace."
|
||||
---
|
||||
|
||||
# /commands — Catalogo de slash commands del repo
|
||||
|
||||
Inventario unificado. Lista los `.md` bajo `.claude/commands/` (recursivo, sigue symlinks) y agrupa por namespace.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/commands # listado completo agrupado por namespace
|
||||
/commands <substring> # filtra por substring en nombre o descripcion
|
||||
/commands --ns <namespace> # solo un namespace (global, aurgi, ...)
|
||||
/commands --json # salida JSON para agentes
|
||||
```
|
||||
|
||||
## Implementacion
|
||||
|
||||
Bash + awk. Parsea frontmatter `description:` de cada `.md`. Agrupa por subdirectorio (subdir = namespace, root = `global`).
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="${FN_REGISTRY_ROOT:-/home/egutierrez/fn_registry}"
|
||||
CMD_DIR="$ROOT/.claude/commands"
|
||||
|
||||
# Recolecta: ns|name|description
|
||||
collect() {
|
||||
find -L "$CMD_DIR" -type f -name '*.md' | while read -r f; do
|
||||
rel="${f#$CMD_DIR/}"
|
||||
case "$rel" in
|
||||
*/*) ns="${rel%%/*}"; name="${rel#*/}"; name="${name%.md}" ;;
|
||||
*) ns="global"; name="${rel%.md}" ;;
|
||||
esac
|
||||
desc=$(awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit}' "$f")
|
||||
printf '%s|%s|%s\n' "$ns" "$name" "${desc:-(sin descripcion)}"
|
||||
done | sort
|
||||
}
|
||||
|
||||
collect | awk -F'|' '
|
||||
{
|
||||
if ($1 != prev_ns) {
|
||||
if (prev_ns) print ""
|
||||
if ($1 == "global") print "## global (/<cmd>)"
|
||||
else print "## " $1 " (/" $1 ":<cmd>)"
|
||||
prev_ns = $1
|
||||
}
|
||||
printf "- /%s%s — %s\n", ($1=="global"?"":$1":"), $2, $3
|
||||
}'
|
||||
```
|
||||
|
||||
Filtros:
|
||||
|
||||
- Substring: `grep -i "<substring>"` sobre stdout.
|
||||
- `--ns X`: filtrar antes del `awk` por `$1 == "X"`.
|
||||
- `--json`: reemplazar el `awk` por `jq -Rsn` que construya array `{namespace, name, description, invocation}`.
|
||||
|
||||
## Salida (formato humano)
|
||||
|
||||
```
|
||||
## global (/<cmd>)
|
||||
- /app — Crear, configurar y desplegar apps del registry
|
||||
- /autopilot — Modo full-auto...
|
||||
- /commands — Catalogo de slash commands del repo
|
||||
...
|
||||
|
||||
## aurgi (/aurgi:<cmd>)
|
||||
- /aurgi:anadir_contexto_aurgi — Anade o modifica contexto...
|
||||
- /aurgi:aumentar_task — Enriquece tarea Aurgi con preguntas...
|
||||
- /aurgi:contexto_aurgi — Aprende el contexto de Aurgi...
|
||||
```
|
||||
|
||||
## Cuando usarlo
|
||||
|
||||
- Sesion nueva: ver de un vistazo que slash commands hay disponibles.
|
||||
- Antes de inventar logica inline: comprobar si ya existe un command.
|
||||
- Auditoria: verificar que los projects exponen sus commands correctamente.
|
||||
- Onboarding: nuevo PC clonado, descubrir capacidades del repo sin abrir N archivos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Sigue symlinks (`find -L`). Si un symlink apunta a directorio inexistente, devuelve vacio para esa rama — verificar con `ls -L .claude/commands/<ns>/`.
|
||||
- Solo escanea `<root>/.claude/commands/`. Commands user-global en `~/.claude/commands/` NO entran (son personales, fuera del repo).
|
||||
- Namespace = nombre del subdirectorio bajo `.claude/commands/`. Coincide con el project pero no por mecanismo — por convencion. Ver `.claude/rules/project_commands.md`.
|
||||
- Para que un command de project aparezca aqui desde la raiz, hace falta el symlink (`.claude/commands/<project>` -> `../../projects/<project>/.claude/commands`).
|
||||
+59
-22
@@ -1,37 +1,74 @@
|
||||
# /compile — Compila app C++ y la copia al escritorio de Windows
|
||||
---
|
||||
description: "Compila app del registry (C++ o Wails Go), copia el .exe a Desktop/apps/<app>/ y relanza en Windows. Wrapper sobre compile_cpp_app o compile_wails_app segun framework declarado en app.md."
|
||||
---
|
||||
|
||||
Wrapper sobre el pipeline `compile_cpp_app_bash_pipelines`. Toda la lógica vive en el registry (resolver app desde CWD/arg, cross-compile MinGW, copiar exe + DLLs + assets/ + enrichers/ + runtime/ a `/mnt/c/Users/lucas/Desktop/apps/<app>/`, taskkill previo, preservar `local_files/`).
|
||||
# /compile — Compila app C++ o Wails y la copia al escritorio de Windows
|
||||
|
||||
Wrapper sobre 2 pipelines del registry segun el framework:
|
||||
|
||||
- **C++ (imgui / cmake)** → `compile_cpp_app_bash_pipelines`. Cross-compile MinGW + assets/enrichers/runtime + taskkill, NO relanza.
|
||||
- **Wails Go (matrix_client_pc, matrix_admin_panel, etc.)** → `compile_wails_app_bash_pipelines`. `wails build -platform windows/amd64` con `-tags goolm` si E2EE + taskkill + **RELANZA** la app tras copy.
|
||||
|
||||
Toda la logica vive en el registry (resolver app desde CWD/arg, build, deploy con preservacion de `local_files/`).
|
||||
|
||||
## Dispatch
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
./fn run compile_cpp_app "$ARGUMENTS"
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Detecta framework via wails.json o CMakeLists.txt en el dir del app
|
||||
APP="$ARGUMENTS"
|
||||
RESOLVED=$(bash -c '
|
||||
source bash/functions/infra/resolve_cpp_app_dir.sh
|
||||
resolve_cpp_app_dir "'"$APP"'"
|
||||
' 2>/dev/null) || true
|
||||
APP_DIR="$(echo "$RESOLVED" | cut -f2)"
|
||||
|
||||
if [ -n "$APP_DIR" ] && [ -f "$APP_DIR/wails.json" ]; then
|
||||
./fn run compile_wails_app "$ARGUMENTS"
|
||||
elif [ -n "$APP_DIR" ] && [ -f "$APP_DIR/CMakeLists.txt" ]; then
|
||||
./fn run compile_cpp_app "$ARGUMENTS"
|
||||
else
|
||||
echo "ERROR: no se detecto framework (falta wails.json o CMakeLists.txt en $APP_DIR)" >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Argumento
|
||||
|
||||
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`).
|
||||
`$ARGUMENTS` — opcional. Nombre de app (ej: `chart_demo`, `matrix_client_pc`).
|
||||
|
||||
- Sin argumento: deduce desde `pwd` si estás dentro de `cpp/apps/<X>/` o `projects/*/apps/<X>/`.
|
||||
- Si no se puede deducir y no se pasa argumento, el pipeline lista las apps disponibles en stderr y aborta.
|
||||
- Sin argumento: deduce desde `pwd` si estas dentro de `cpp/apps/<X>/`, `apps/<X>/` o `projects/*/apps/<X>/`.
|
||||
- Si no se puede deducir y no se pasa argumento, lista las apps disponibles en stderr y aborta.
|
||||
|
||||
## Qué hace el pipeline
|
||||
## Que hace el pipeline (C++)
|
||||
|
||||
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>` desde arg o CWD.
|
||||
2. Verifica `CMakeLists.txt` en el dir resuelto.
|
||||
3. `build_cpp_windows_bash_infra <app>` — cross-compila el target específico con `cpp/build/windows/` (configura toolchain `mingw-w64.cmake` la primera vez).
|
||||
1. `resolve_cpp_app_dir_bash_infra` — resuelve `<app_name>` y `<dir absoluto>`.
|
||||
2. Verifica `CMakeLists.txt`.
|
||||
3. `build_cpp_windows_bash_infra <app>` — cross-compila con MinGW.
|
||||
4. `deploy_cpp_exe_to_windows_bash_infra <app> <dir>`:
|
||||
- `taskkill.exe /IM <app>.exe /F` (pre-autorizado).
|
||||
- Copia `<app>.exe` + DLLs al top-level de `Desktop/apps/<app>/`.
|
||||
- rsync `cpp/build/windows/apps/<app>/assets/` → `Desktop/apps/<app>/assets/`.
|
||||
- rsync `<app_dir>/enrichers/` → `assets/enrichers/` si existe.
|
||||
- Si `app.md` declara `python_runtime: true`, regenera `runtime/` con `tools/freeze_python_runtime.sh` y rsync a `assets/runtime/`.
|
||||
- Copia `gx-cli`/`gx-cli.exe` si existen.
|
||||
- **NUNCA** toca `local_files/` (estado del usuario).
|
||||
5. Imprime `ls -lh` del `.exe` final.
|
||||
- `taskkill.exe /IM <app>.exe /F`.
|
||||
- Copia `<app>.exe` + DLLs.
|
||||
- rsync `assets/`, `enrichers/`, `runtime/` (si aplica).
|
||||
- Preserva `local_files/`.
|
||||
- **NO** relanza.
|
||||
|
||||
## Que hace el pipeline (Wails)
|
||||
|
||||
1. `resolve_cpp_app_dir_bash_infra` (reusado — sirve para Wails apps tambien).
|
||||
2. Verifica `wails.json` + `go.mod`.
|
||||
3. Detecta `-tags goolm` automaticamente (grep `matrix_crypto_init` en `app.md` o `build:tags` en `wails.json`).
|
||||
4. `wails build -platform windows/amd64 [-tags goolm]`.
|
||||
5. `deploy_wails_exe_to_windows_bash_infra <app> <dir>`:
|
||||
- `taskkill.exe /IM <app>.exe /F`.
|
||||
- Copia `<app>.exe` (+ `appicon.ico` si existe).
|
||||
- **Relanza** via `cmd.exe /c start "" <app>.exe`.
|
||||
- Preserva `local_files/`.
|
||||
|
||||
## Notas
|
||||
|
||||
- Solo target Windows hoy. Android / Linux quedan fuera (Linux ya lo da `cpp/build/`).
|
||||
- Solo target Windows hoy. Linux ya lo da `wails build` / `cpp/build/` nativo.
|
||||
- Variables override-ables: `BUILD_WIN`, `WIN_DESKTOP_APPS`, `FN_REGISTRY_ROOT`.
|
||||
- Si la app no está registrada en `cpp/CMakeLists.txt`, `cmake --build --target <app>` falla. Registrar siguiendo `.claude/rules/cpp_apps.md` §5.
|
||||
- Para tocar la lógica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,deploy_cpp_exe_to_windows,compile_cpp_app}.sh`, no este wrapper.
|
||||
- Si la app C++ no esta registrada en `cpp/CMakeLists.txt`, el build falla — registrar siguiendo `.claude/rules/cpp_apps.md` §5.
|
||||
- Si la app Wails falla build con `no required module provides package`, correr `go mod tidy` en el dir del app primero.
|
||||
- Para tocar la logica: editar `bash/functions/{infra,pipelines}/{resolve_cpp_app_dir,build_cpp_windows,deploy_{cpp,wails}_exe_to_windows,compile_{cpp,wails}_app}.sh`, no este wrapper.
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
# /cpp-app — Crear o modificar app C++ del registry sin olvidar nada
|
||||
|
||||
Recopila TODOS los datos necesarios (frontmatter, trio app_hub, panels, AppConfig, service block, e2e_checks, uses_functions) **antes** de tocar el disco. Tras confirmar, ejecuta scaffolder o edits, regenera iconos, refresca app_hub, compila y deploya a Windows.
|
||||
|
||||
Sustituye al flujo manual "edito main.cpp + app.md + CMakeLists.txt a mano". Wrapper sobre `init_cpp_app_bash_pipelines` (create) o edits directos sobre `app.md` (modify) + `regenerate_app_icons` + `refresh_app_hub` + `redeploy_cpp_app_windows`.
|
||||
|
||||
---
|
||||
|
||||
## Uso
|
||||
|
||||
```
|
||||
/cpp-app # interactivo, modo create
|
||||
/cpp-app <name> # interactivo, modo create con name pre-rellenado
|
||||
/cpp-app modify <name> # editar app existente
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modo CREATE — flujo turno a turno
|
||||
|
||||
Si `$ARGUMENTS` no empieza por `modify`, es create. Si trae `<name>`, lo usas como default; si no, pregunta name.
|
||||
|
||||
### Paso 0 — verificar que no existe
|
||||
|
||||
```bash
|
||||
test -d "$HOME/fn_registry/apps/<name>" \
|
||||
|| ls $HOME/fn_registry/projects/*/apps/<name> 2>/dev/null
|
||||
```
|
||||
|
||||
Si existe en cualquier ubicacion: **abortar** y sugerir `/cpp-app modify <name>`. NO sobreescribir.
|
||||
|
||||
### Paso 1 — Identidad (AskUserQuestion)
|
||||
|
||||
1. **name** (texto libre — valida snake_case + contiene verbo segun `ids_naming.md`). Verbos canonicos: `show, render, view, plot, edit, manage, monitor, browse, explore, run, launch, scan, audit, debug, profile, ...`. Si no trae verbo, sugerir alternativas (`viewer` -> `<name>_viewer`).
|
||||
2. **project** (select: ninguno / lista de `projects/*/`). Si ninguno -> `apps/<name>/`.
|
||||
3. **domain** (select: `tools` (default), `gfx`, `tui`, `infra`, `finance`, `datascience`, `cybersecurity`, `shell`, `pipelines`, `browser`).
|
||||
4. **description** 1 linea (texto libre, max 80 chars). **OBLIGATORIO** — sin esto el hub muestra tarjeta vacia.
|
||||
|
||||
### Paso 2 — Trio app_hub OBLIGATORIO
|
||||
|
||||
Regla dura `cpp_apps.md`: description + icon.phosphor + icon.accent SIEMPRE juntos.
|
||||
|
||||
5. **icon.phosphor** glyph name. Antes de preguntar, ofrece busqueda:
|
||||
```bash
|
||||
ls $HOME/fn_registry/sources/phosphor-core/assets/fill/ | grep -i "<keyword>"
|
||||
```
|
||||
Sugiere 3-5 candidatos basados en `description`. Default segun domain: `gfx`->`palette`, `tui`->`terminal`, `tools`->`wrench`, `infra`->`gear`, `finance`->`chart-line-up`, `datascience`->`graph`, `cybersecurity`->`shield`.
|
||||
6. **icon.accent** hex `#rrggbb` (palette select):
|
||||
- sky `#0ea5e9`, indigo `#4f46e5`, violet `#7c3aed`, pink `#ec4899`, rose `#f43f5e`, red `#dc2626`, orange `#ea580c`, amber `#d97706`, green `#16a34a`, teal `#0d9488`, cyan `#0891b2`, slate `#475569`. Default segun domain.
|
||||
|
||||
### Paso 3 — Tags
|
||||
|
||||
7. **tags** (multiSelect): `service`, `launcher`, `dashboard`, `viewer`, `editor`, `monitor`, `debug`, `prototype`. Si selecciona `service` -> activar bloque service (Paso 7).
|
||||
|
||||
### Paso 4 — Panels iniciales
|
||||
|
||||
8. **panels** (texto libre o select):
|
||||
- Default: 1 panel `Main` (Ctrl+1).
|
||||
- Opcion lista: hasta 4 paneles. Por cada uno: `{label, shortcut}`. Generara `PanelToggle k_panels[]` en `main.cpp`.
|
||||
|
||||
### Paso 5 — AppConfig flags
|
||||
|
||||
9. (multiSelect):
|
||||
- `init_gl_loader` (true si la app llama `gl*` directo, ej. shaders, GPU renderer custom). Default false.
|
||||
- `viewports` true (default) / false (single-window).
|
||||
- `auto_dockspace` true (default) / false (solo si gestiona DockSpace propio tipo `shaders_lab`).
|
||||
- `fps_overlay` activo de inicio? (controla solo el default; el menu Settings lo toggle).
|
||||
|
||||
### Paso 6 — Funciones del registry a usar
|
||||
|
||||
10. **uses_functions** lista IDs. Antes de preguntar, busca candidatas segun description:
|
||||
```
|
||||
mcp__registry__fn_search query="<keyword>" entity="functions"
|
||||
```
|
||||
Y muestra capability groups relevantes (`docs/capabilities/INDEX.md`). El usuario puede aceptar lista, anadir IDs, o dejar vacio (se rellena tras codear).
|
||||
|
||||
Cada ID que no este en el registry -> ofrecer spawn `fn-constructor` antes de continuar (regla `delegation.md`).
|
||||
|
||||
### Paso 7 — Bloque `service:` (solo si tag=service)
|
||||
|
||||
11. Si paso 3 marco `service`, recopilar (regla `function_tags.md` + issue 0105):
|
||||
- `port` int o null
|
||||
- `health_endpoint` ruta GET o null
|
||||
- `health_timeout_s` (default 3)
|
||||
- `runtime` (select: `systemd-user`, `systemd-system`, `docker-compose`, `stdio`, `manual`)
|
||||
- `systemd_unit` (obligatorio si runtime empieza por `systemd-`)
|
||||
- `systemd_scope` (`user|system|null`)
|
||||
- `restart_policy` (select: `always` (Recommended — gotcha: `on-failure` NO reinicia SIGTERM limpio), `on-failure`, `none`)
|
||||
- `pc_targets` (multiSelect de pc_locations actuales: `aurgi-pc`, `home-wsl`, ...)
|
||||
- `is_local_only` (true/false default false)
|
||||
|
||||
### Paso 8 — Persistencia
|
||||
|
||||
12. (multiSelect):
|
||||
- BD propia SQLite `<name>.db` en `local_files/`? -> recordar usar `fn::local_path("<name>.db")` (cpp_apps.md §7)
|
||||
- operations.db (para entities/relations)? -> ejecutar `fn ops init` tras crear
|
||||
- Archivos config en `local_files/`?
|
||||
|
||||
### Paso 9 — e2e_checks (issue 0068)
|
||||
|
||||
13. Default sugerido (modificable):
|
||||
```yaml
|
||||
e2e_checks:
|
||||
- id: build
|
||||
cmd: "cmake --build cpp/build --target <name> -j"
|
||||
timeout_s: 300
|
||||
- id: self_test
|
||||
cmd: "./cpp/build/apps/<name>/<name> --self-test"
|
||||
timeout_s: 30
|
||||
severity: warning # si todavia no implementa --self-test
|
||||
```
|
||||
Pregunta: ¿anadir mas checks (ops_audit, pytest, smoke)?
|
||||
|
||||
### Paso 10 — Resumen y confirmacion
|
||||
|
||||
Mostrar bloque YAML completo del `app.md` que se va a generar + flags del scaffolder + post-acciones. Pedir confirmacion antes de ejecutar.
|
||||
|
||||
---
|
||||
|
||||
## Modo CREATE — ejecucion
|
||||
|
||||
Una vez confirmado:
|
||||
|
||||
```bash
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# 1. Scaffolder
|
||||
./fn run init_cpp_app <name> \
|
||||
[--project <p>] \
|
||||
[--domain <d>] \
|
||||
--desc "<description>" \
|
||||
[--tags "<csv>"]
|
||||
|
||||
# 2. Editar app.md generado para anadir:
|
||||
# - icon: {phosphor, accent}
|
||||
# - service: {...} (si aplica)
|
||||
# - uses_functions: [...]
|
||||
# - e2e_checks: [...]
|
||||
# (el scaffolder no rellena estos; editarlos con Edit tool)
|
||||
|
||||
# 3. Editar main.cpp generado para reflejar:
|
||||
# - panels[] custom (si != default)
|
||||
# - cfg.init_gl_loader / cfg.auto_dockspace / cfg.viewports
|
||||
# - includes de funciones registry usadas
|
||||
|
||||
# 4. Editar CMakeLists.txt para anadir paths de funciones del registry:
|
||||
# ${CMAKE_SOURCE_DIR}/functions/<d>/<f>.cpp
|
||||
|
||||
# 5. Si es service -> ofrecer crear systemd unit (skipear si runtime=stdio|manual)
|
||||
|
||||
# 6. Si pidio operations.db
|
||||
./fn ops init apps/<name> # o projects/<p>/apps/<name>
|
||||
|
||||
# 7. Generar icono
|
||||
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
|
||||
|
||||
# 8. Indexar
|
||||
./fn index
|
||||
|
||||
# 9. Compilar Windows
|
||||
./fn run redeploy_cpp_app_windows <name> <dir> --build
|
||||
|
||||
# 10. Refrescar app_hub
|
||||
./fn run refresh_app_hub
|
||||
|
||||
# 11. Auditoria
|
||||
./fn doctor cpp-apps
|
||||
[[ "<tag>" == *service* ]] && ./fn doctor services-spec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modo MODIFY — flujo
|
||||
|
||||
`/cpp-app modify <name>`
|
||||
|
||||
### Paso 0 — Localizar
|
||||
|
||||
```bash
|
||||
# Buscar apps/<name>/ o projects/*/apps/<name>/
|
||||
sqlite3 $HOME/fn_registry/registry.db \
|
||||
"SELECT id, dir_path FROM apps WHERE name='<name>' AND lang='cpp';"
|
||||
```
|
||||
|
||||
Si no existe: abortar, sugerir `/cpp-app` (sin args) para crear.
|
||||
|
||||
### Paso 1 — Mostrar config actual
|
||||
|
||||
```bash
|
||||
mcp__registry__fn_show id="<id>"
|
||||
cat <dir>/app.md
|
||||
```
|
||||
|
||||
### Paso 2 — Que cambiar (multiSelect)
|
||||
|
||||
- `description` (1 linea)
|
||||
- `icon.phosphor` o `icon.accent`
|
||||
- `tags` (anadir/quitar; si toca `service` -> Paso 7 del create)
|
||||
- `uses_functions` (anadir/quitar — recordar editar CMakeLists.txt)
|
||||
- `panels` (anadir/quitar/renombrar)
|
||||
- `service:` block (si tag=service)
|
||||
- `e2e_checks`
|
||||
- `domain`
|
||||
- `rename` (cambia name, dir, IDs derivados, repo Gitea — operacion delicada, requiere doble confirmacion)
|
||||
|
||||
### Paso 3 — Aplicar cambios
|
||||
|
||||
Para cada cambio: usa `Edit` sobre los archivos correspondientes. NUNCA `Write` completo de `app.md` (preserva campos que no toques).
|
||||
|
||||
### Paso 4 — Post-acciones (segun lo que toco)
|
||||
|
||||
```bash
|
||||
# Siempre
|
||||
cd $HOME/fn_registry && ./fn index
|
||||
|
||||
# Si toco icon.* -> regenerar appicon
|
||||
./fn run generate_app_icon "<phosphor>" "<accent>" "<dir>/appicon.ico"
|
||||
|
||||
# Si toco trio o panels o uses_functions o cambia code:
|
||||
./fn run redeploy_cpp_app_windows <name> <dir> --build
|
||||
|
||||
# Si toco description o icon o tags:
|
||||
./fn run refresh_app_hub
|
||||
|
||||
# Si toco service: o tag service
|
||||
./fn doctor services-spec
|
||||
|
||||
# Siempre al final
|
||||
./fn doctor cpp-apps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas duras
|
||||
|
||||
- **NUNCA** crear `main.cpp` + `CMakeLists.txt` + `app.md` a mano. Siempre via `init_cpp_app_bash_pipelines` (regla `cpp_apps.md`).
|
||||
- **NUNCA** poner el codigo en `cpp/apps/<n>/`. Solo `apps/<n>/` o `projects/<p>/apps/<n>/`.
|
||||
- **NUNCA** dejar `app.md` sin el trio (description + icon.phosphor + icon.accent). Tarjeta del hub queda gris.
|
||||
- **NUNCA** declarar funciones del registry en `uses_functions` sin listar su `.cpp` en `CMakeLists.txt` (drift detectado por `fn doctor uses-functions`).
|
||||
- **NUNCA** usar `Restart=on-failure` en systemd unit de un service C++ — gotcha 2026-05-17 (`sqlite_api.service` cayo 20h). Default `Restart=always`.
|
||||
- Despues de **cualquier** cambio en el trio: `regenerate_app_icons <name>` + `refresh_app_hub`.
|
||||
|
||||
---
|
||||
|
||||
## Auto-verificacion final
|
||||
|
||||
Tras crear o modificar, reportar al usuario:
|
||||
|
||||
```
|
||||
=== app <name> ===
|
||||
dir: <abs_dir>
|
||||
domain: <d>
|
||||
description: "<desc>"
|
||||
icon: <phosphor> + <accent>
|
||||
tags: [<csv>]
|
||||
uses_functions: N funciones (<list_top_5>)
|
||||
panels: N (<labels>)
|
||||
e2e_checks: N checks
|
||||
service: <si/no — port:<p> health:<h>>
|
||||
|
||||
Acciones ejecutadas:
|
||||
[✓] scaffolder / edits
|
||||
[✓] generate_app_icon
|
||||
[✓] fn index (registry.db actualizado)
|
||||
[✓] redeploy_cpp_app_windows (Desktop/apps/<name>/<name>.exe)
|
||||
[✓] refresh_app_hub (tarjeta visible en hub)
|
||||
[✓] fn doctor cpp-apps (limpio | N warnings)
|
||||
|
||||
Siguiente paso sugerido:
|
||||
- Abrir app_hub_launcher en Windows y verificar tarjeta
|
||||
- Anadir tests visuales si la app tiene paneles propios (cpp/PATTERNS.md §11)
|
||||
```
|
||||
|
||||
$ARGUMENTS
|
||||
@@ -38,19 +38,19 @@ Consultar `registry.db` para encontrar funciones existentes relevantes y evitar
|
||||
|
||||
```bash
|
||||
# Buscar funciones similares por nombre y descripcion (OBLIGATORIO — usar multiples terminos)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, lang, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:TERMINO1* OR description:TERMINO1* OR name:TERMINO2* OR description:TERMINO2*') ORDER BY name;"
|
||||
|
||||
# Buscar tipos relacionados
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, lang, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:TERMINO* OR description:TERMINO*') ORDER BY name;"
|
||||
|
||||
# Funciones del dominio objetivo
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, signature, description FROM functions WHERE domain = 'DOMINIO' AND lang = 'LANG' ORDER BY name;"
|
||||
|
||||
# Tipos del dominio objetivo
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'DOMINIO' ORDER BY name;"
|
||||
|
||||
# Funciones que podrian componerse (misma firma de retorno)
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, purity, signature FROM functions WHERE returns LIKE '%TIPO%' OR signature LIKE '%TIPO%' ORDER BY name;"
|
||||
```
|
||||
|
||||
**Clasificar resultados en:**
|
||||
@@ -103,7 +103,7 @@ Para cada batch del plan, lanzar agentes `fn-constructor` **en paralelo** (un ag
|
||||
Usar el Agent tool con `subagent_type: "fn-constructor"` pasando un prompt completo con:
|
||||
|
||||
```
|
||||
Crea la siguiente funcion para el registry fn_registry en /home/lucas/fn_registry:
|
||||
Crea la siguiente funcion para el registry fn_registry en $HOME/fn_registry:
|
||||
|
||||
Funcion: {nombre}
|
||||
Kind: {kind}
|
||||
@@ -149,7 +149,7 @@ Despues de que TODOS los fn-constructor terminen:
|
||||
|
||||
```bash
|
||||
# Indexar todo de una vez
|
||||
cd /home/lucas/fn_registry && ./fn index
|
||||
cd $HOME/fn_registry && ./fn index
|
||||
```
|
||||
|
||||
Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
|
||||
@@ -166,7 +166,7 @@ Si el indexer reporta errores, corregirlos antes de continuar. Errores comunes:
|
||||
|
||||
```bash
|
||||
# Verificar cada funcion creada
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
./fn show {id_de_cada_funcion}
|
||||
|
||||
# Verificar que no hay funciones sin params_schema
|
||||
@@ -178,7 +178,7 @@ cd /home/lucas/fn_registry
|
||||
Para cada funcion con tests, ejecutar:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Go
|
||||
CGO_ENABLED=1 go test -tags fts5 -v -run TestNombreDelTest ./functions/{domain}/
|
||||
@@ -197,13 +197,13 @@ bash bash/functions/{domain}/{nombre}_test.sh
|
||||
|
||||
```bash
|
||||
# Verificar que todas las funciones nuevas estan en la BD
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, kind, purity, tested FROM functions WHERE id IN ('id1','id2','id3') ORDER BY name;"
|
||||
|
||||
# Verificar que los tests estan indexados
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, function_id, name FROM unit_tests WHERE function_id IN ('id1','id2','id3') ORDER BY function_id;"
|
||||
|
||||
# Verificar dependencias
|
||||
sqlite3 /home/lucas/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
|
||||
sqlite3 $HOME/fn_registry/registry.db "SELECT id, uses_functions, uses_types FROM functions WHERE id IN ('id1','id2','id3') AND uses_functions != '[]';"
|
||||
```
|
||||
|
||||
### 6.4 Si algo fallo
|
||||
|
||||
@@ -45,7 +45,7 @@ Antes de escribir nada, repasar la conversacion y juntar:
|
||||
|
||||
2. **Cambios concretos** desde git:
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
git status --short
|
||||
git diff --stat
|
||||
git log --since="6 hours ago" --oneline
|
||||
@@ -70,7 +70,7 @@ Si el material es solo conversacion exploratoria sin artefactos tocados, ir dire
|
||||
Para cada artefacto identificado, localizar su `.md` consultando `registry.db`:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
|
||||
# Funcion / tipo
|
||||
sqlite3 registry.db "SELECT id, file_path FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:NAME* OR description:NAME*');"
|
||||
@@ -180,7 +180,7 @@ Para cada `.md` identificado:
|
||||
Si los cambios de la sesion incluyen creacion de funciones/tipos/apps/projects/analysis/vaults o modificacion de frontmatter:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry && ./fn index
|
||||
cd $HOME/fn_registry && ./fn index
|
||||
```
|
||||
|
||||
Y verificar:
|
||||
|
||||
@@ -17,7 +17,7 @@ Suite ya instalada en `cpp/vendor/imgui_test_engine/`. Integracion en framework:
|
||||
### 1. Resolver app y directorio
|
||||
|
||||
```bash
|
||||
ROOT=/home/lucas/fn_registry
|
||||
ROOT=$HOME/fn_registry
|
||||
ARGS="$ARGUMENTS"
|
||||
APP_ARG="${ARGS%% *}" # primera palabra
|
||||
FLOW_DESC="${ARGS#* }" # resto (puede coincidir con APP_ARG si solo hay una palabra)
|
||||
|
||||
@@ -17,7 +17,7 @@ Wrapper sobre `append_diary_entry_bash_infra`. La función del registry maneja t
|
||||
|
||||
2. **Llamar la función del registry**:
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
source bash/functions/infra/append_diary_entry.sh
|
||||
append_diary_entry "<TITULO>" "$(cat <<'EOF'
|
||||
<CUERPO>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: fix-issue
|
||||
description: Implementar un issue de dev/issues/ end-to-end. Crea rama, ejecuta tareas, bumpa version si toca modulos/framework/apps (via /version), tests, cierra issue, integra a master.
|
||||
---
|
||||
|
||||
# /fix-issue
|
||||
|
||||
Ejecuta el flujo completo de implementacion/cierre de un issue de `dev/issues/`. Adaptado al stack del registry: Go (`-tags fts5 CGO_ENABLED=1`), Python (`python/.venv/bin/python3`), Bash, TypeScript (`pnpm`), C++ (`cmake`+`mingw-w64` toolchain).
|
||||
|
||||
## Inputs
|
||||
|
||||
```
|
||||
/fix-issue <NNNN[a|b|c...]>
|
||||
```
|
||||
|
||||
- `NNNN`: numero del issue (ej. `0107`).
|
||||
- Si es sub-issue, sufijo letra: `0107a`, `0107b`, ...
|
||||
|
||||
Si no se proporciona, preguntar.
|
||||
|
||||
## Flujo obligatorio
|
||||
|
||||
### 1. Resolver el issue
|
||||
|
||||
- `dev/issues/<NNNN>-*.md` → si no existe, STOP.
|
||||
- Si ya en `dev/issues/completed/`, STOP.
|
||||
- Si es sub-issue, leer tambien el principal para contexto.
|
||||
|
||||
### 2. Leer y extraer
|
||||
|
||||
- Objetivo, tareas, arquitectura, prerequisitos, riesgos.
|
||||
- Identificar archivos afectados — anotar si toca:
|
||||
- `modules/<X>/` o `cpp/framework/` → bumpa version (paso 8).
|
||||
- `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/` → indexer + `fn index` al cerrar.
|
||||
- Apps en `apps/<X>/` o `projects/*/apps/<X>/` → requiere rama TBD (regla `apps_tbd.md`) **+ bumpa version per-app (paso 8)**. Si el issue toca multiples apps, una llamada `/version` por app.
|
||||
- Registry meta (CLAUDE.md, rules, templates) → push directo a master OK.
|
||||
|
||||
### 3. Estrategia de rama
|
||||
|
||||
**Registry-only changes** (functions/types/docs/rules):
|
||||
- Push directo a master OK. NO crear rama.
|
||||
|
||||
**Apps changes** (apps/, projects/*/apps/):
|
||||
- Crear rama TBD:
|
||||
```bash
|
||||
git checkout master
|
||||
git pull --rebase
|
||||
git checkout -b issue/<NNNN>-<slug>
|
||||
```
|
||||
La rama es del registry. Si la app es sub-repo, ademas crear rama dentro del sub-repo.
|
||||
|
||||
**Modules/framework changes** (`modules/`, `cpp/framework/`):
|
||||
- Rama TBD obligatoria (afecta a todas las apps que linkean).
|
||||
|
||||
### 4. Plan con TaskCreate
|
||||
|
||||
- Crear tarea por bloque logico del issue.
|
||||
- Incluir SIEMPRE:
|
||||
- Tarea de tests (unit + smoke).
|
||||
- Tarea de `fn index` si toco metadata.
|
||||
- Tarea de `/version` si toco `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/` (una llamada por target).
|
||||
- Tarea de cleanup/docs.
|
||||
|
||||
### 5. Implementar
|
||||
|
||||
Reglas registry-first (CLAUDE.md):
|
||||
- ANTES de escribir codigo reutilizable → `mcp__registry__fn_search` para encontrar lo que existe.
|
||||
- Si falta funcion reutilizable → spawn `fn-constructor` (no escribir inline).
|
||||
- Si patron se repite >2x → propose nueva funcion.
|
||||
- NUNCA `sqlite3 registry.db "SELECT ..."` plano — usar MCP.
|
||||
|
||||
Convenciones del stack:
|
||||
|
||||
| Stack | Build/test |
|
||||
|---|---|
|
||||
| Go | `CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/` y `CGO_ENABLED=1 go test -tags fts5 ./...` |
|
||||
| Python | `python/.venv/bin/python3 -m pytest <path>` |
|
||||
| Bash | `bash -n <script>.sh` + tests inline |
|
||||
| TypeScript | `cd frontend && pnpm build && pnpm test` |
|
||||
| C++ (Linux) | `cmake --build build --target <app>` |
|
||||
| C++ (Windows MinGW) | `cmake -B build/windows -DCMAKE_TOOLCHAIN_FILE=cpp/toolchains/mingw-w64.cmake && cmake --build build/windows --target <app>` |
|
||||
|
||||
Commits atomicos por bloque logico con prefijos: `feat:`, `fix:`, `test:`, `docs:`, `refactor:`, `chore:`. Mensajes en espanol. NO WIP.
|
||||
|
||||
### 6. Tests
|
||||
|
||||
Stack-dependent (ver arriba). Si tests pasan parcialmente con failures pre-existentes no causadas por la rama, documentar en cuerpo del commit/PR.
|
||||
|
||||
### 7. Feature flags (si aplica)
|
||||
|
||||
Si el issue forma parte de un feature multi-issue:
|
||||
- Editar `dev/feature_flags.json` con el flag (desactivado).
|
||||
- Activar el flag en el ultimo sub-issue del set.
|
||||
|
||||
Flag != WIP. Codigo detras de flag debe compilar + testear.
|
||||
|
||||
### 8. Version bump (si toco modulos/framework/apps)
|
||||
|
||||
**OBLIGATORIO si el issue toco** alguno de:
|
||||
- `modules/<X>/` → bumpa `modules/<X>/module.md::version`.
|
||||
- `cpp/framework/` → bumpa `modules/framework/module.md::version`.
|
||||
- `apps/<X>/` → bumpa `apps/<X>/app.md::version`.
|
||||
- `projects/<P>/apps/<X>/` → bumpa `projects/<P>/apps/<X>/app.md::version`.
|
||||
|
||||
```
|
||||
/version <path> <major|minor|patch> "<reason>"
|
||||
```
|
||||
|
||||
Reglas (modulos/framework):
|
||||
- Major: breaking ABI/API publica.
|
||||
- Minor: additive (nuevo helper, refactor interno sin cambio de API, nuevo miembro).
|
||||
- Patch: bugfix puro.
|
||||
|
||||
Reglas (apps):
|
||||
- Major: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||
- Minor: feature aditiva visible (nuevo panel, endpoint, opcion).
|
||||
- Patch: bugfix sin cambio observable, refactor interno, mejora perf.
|
||||
|
||||
**Una llamada `/version` por target afectado**. Si el issue toca 1 modulo + 2 apps -> 3 llamadas a `/version` (cada una con su `reason` y bump-type apropiado; pueden diferir).
|
||||
|
||||
Diff guard: cambios que solo tocan el `app.md` (correccion typo descripcion, anadir tag) NO requieren bump — son metadata, no comportamiento. Detectar con `git diff --name-only | grep -v '\.md$'` para decidir si hay cambio de codigo real.
|
||||
|
||||
`/version` solo edita + stage. NO commit. El bump va junto con el codigo correspondiente en el mismo commit (`feat:` o `fix:` o `refactor:`).
|
||||
|
||||
Si NO toco modulos/framework/apps, saltar este paso.
|
||||
|
||||
### 9. Cerrar el issue
|
||||
|
||||
Mover archivo:
|
||||
```bash
|
||||
mv dev/issues/<NNNN>-<slug>.md dev/issues/completed/
|
||||
```
|
||||
|
||||
Actualizar `dev/issues/README.md`:
|
||||
- Link → `completed/<NNNN>-<slug>.md`
|
||||
- Estado → `completado`
|
||||
|
||||
Si es feature multi-issue y este es el ultimo sub-issue:
|
||||
- Flip flag en `dev/feature_flags.json` a `enabled: true` con `enabled_at: <YYYY-MM-DD>`.
|
||||
- Verificar que todos los sub-issues estan en `completed/`.
|
||||
|
||||
### 10. Integrar
|
||||
|
||||
**Registry-only changes**: push directo a master.
|
||||
|
||||
**Apps/modules/framework changes**: `/full-git-push` o `/git-push` (merge --no-ff de la rama a master, push, delete rama).
|
||||
|
||||
### 11. Verificar post-cierre
|
||||
|
||||
- `fn index` — registry.db al dia.
|
||||
- `fn doctor` (subcomandos relevantes: `artefacts`, `services`, `cpp-apps`, `uses-functions`).
|
||||
- Si toco modulos: `fn doctor modules` (post 0107a) — 0 drift.
|
||||
|
||||
## Reglas criticas
|
||||
|
||||
- **Registry-first**: SIEMPRE buscar antes de escribir; delegar a `fn-constructor` antes que inline.
|
||||
- **TBD para apps**: NUNCA push directo a master en apps. Rama corta, merge --no-ff.
|
||||
- **TBD NO para registry**: push directo OK para functions/types/docs/rules.
|
||||
- **`/version` obligatorio** si tocas modulos, framework o apps (con cambio de codigo real, no solo metadata). Si no, drift entre `version:` y `## Capability growth log` y se pierde trazabilidad.
|
||||
- **Tests siempre**: no cerrar issue sin tests pasando (salvo failures pre-existentes documentados).
|
||||
- **Commits atomicos**: 1 commit = 1 bloque logico. No mezclar `feat:` + `test:` en mismo commit.
|
||||
- **Cerrar siempre**: nunca dejar issue implementado sin mover a `completed/` + actualizar README.
|
||||
|
||||
## Referenciado desde
|
||||
|
||||
- `.claude/commands/version.md` — bump semver de modulos.
|
||||
- `.claude/commands/full-git-push.md` — push del registry + sub-repos.
|
||||
- `.claude/rules/apps_tbd.md` — politica de TBD por tipo de cambio.
|
||||
|
||||
## Ejemplo: implementar 0107c (refactor data_table)
|
||||
|
||||
```
|
||||
/fix-issue 0107c
|
||||
|
||||
1. Resolver: dev/issues/0107c-split-data-table.md ✓
|
||||
2. Extraer: refactor 4777 LOC → 6 sub-funciones. Toca modules/ → /version obligatorio.
|
||||
3. Rama: issue/0107c-split-data-table desde master.
|
||||
4. Plan: 8 tareas (lectura + 6 sub-funciones + entrypoint thin + version bump).
|
||||
5. Implementar: spawn fn-constructor en paralelo si hay >1 sub-funcion independiente.
|
||||
6. Tests: build + smoke + primitives_gallery --capture diff.
|
||||
7. Flag: parte de modules-v2, NO activar todavia (espera 0107a-f cerrar).
|
||||
8. /version modules/data_table major "split data_table.cpp into 6 sub-functions"
|
||||
9. Cerrar: mv → completed/ + README.
|
||||
10. /git-push.
|
||||
11. fn index + fn doctor modules → 0 drift en consumidores limpiados.
|
||||
```
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
description: "Gestiona flows (casos de uso multi-app reutilizables) en dev/flows/. Subcomandos: create, list, show, status, done. Runner automatizado en fase 2."
|
||||
---
|
||||
|
||||
# /flow — Gestionar flows del registry
|
||||
|
||||
Flows = casos de uso end-to-end que prueban / ejercitan el sistema multi-app. Viven en `dev/flows/NNNN-<slug>.md`. Cada flow describe Goal + Flow steps + Acceptance checkboxes + Telemetria.
|
||||
|
||||
**OBLIGATORIO antes de `create`**: lee `dev/flows/AGENT_GUIDE.md`. Define donde buscar piezas (capability groups, FTS por tag, apps existentes, vaults), reglas duras para no inventar IDs, y plantilla de razonamiento para recomendar extractor / transformer / sink / scheduler / notify por flow.
|
||||
|
||||
Cada flow nuevo cita IDs reales del registry. Si una pieza falta, escribir `FALTA: crear <id>` en la tabla correspondiente. Nada de inventar nombres.
|
||||
|
||||
Diferencia con `dev/issues/`:
|
||||
- Issues = bugs / features de implementacion.
|
||||
- Flows = trabajos reutilizables que cruzan varias apps.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/flow create <slug> # nuevo flow desde template, ID auto
|
||||
/flow list # tabla resumen
|
||||
/flow show <NNNN> # imprime contenido + acceptance %
|
||||
/flow status <NNNN> # status + acceptance % + ultima run
|
||||
/flow done <NNNN> [--notes "..."] # cierra flow (status=done, mueve a completed/)
|
||||
/flow run <NNNN> # fase 2 — runner automatizado (NO IMPLEMENTADO)
|
||||
```
|
||||
|
||||
## Implementacion por subcomando
|
||||
|
||||
### `create <slug>`
|
||||
|
||||
Pasos:
|
||||
1. Valida `<slug>` es kebab-case: `^[a-z][a-z0-9-]*$`. Si no, error.
|
||||
2. Comprueba que no existe ya: `ls dev/flows/*-<slug>.md`. Si existe, error.
|
||||
3. Calcula siguiente ID libre:
|
||||
- `ls dev/flows/*.md dev/flows/completed/*.md | grep -oE '^dev/flows/(completed/)?[0-9]{4}' | sort -u | tail -1`
|
||||
- Suma 1, zero-pad a 4 digitos.
|
||||
4. Lee `dev/flows/template.md`.
|
||||
5. Sustituye `<slug>`, `NNNN`, `YYYY-MM-DD` (hoy).
|
||||
6. Escribe `dev/flows/NNNN-<slug>.md`.
|
||||
7. Append fila a `dev/flows/INDEX.md` (mantener orden por ID asc).
|
||||
8. Reporta path nuevo + recordatorio "edita Goal / Flow / Acceptance".
|
||||
|
||||
### `list`
|
||||
|
||||
Lee `dev/flows/INDEX.md` y lo imprime tal cual. Si flag `--pending` solo pending, `--done` solo done, `--app <name>` filtra por app.
|
||||
|
||||
Tambien anade columna `Accept%` calculada desde body:
|
||||
- Para cada flow .md, cuenta `[ ]` y `[x]` en seccion `## Acceptance`.
|
||||
- `% = checked / total * 100` redondeo entero.
|
||||
|
||||
### `show <NNNN>`
|
||||
|
||||
`cat dev/flows/NNNN-*.md` (busca con glob NNNN-*). Si no existe, prueba `dev/flows/completed/NNNN-*.md`. Si no, error.
|
||||
|
||||
### `status <NNNN>`
|
||||
|
||||
Imprime resumen del frontmatter + acceptance %:
|
||||
|
||||
```
|
||||
=== flow 0001 ===
|
||||
name: hn-top-stories
|
||||
status: pending
|
||||
risk: low
|
||||
priority: high
|
||||
apps: navegator_dashboard, dag_engine, data_factory, agents_and_robots
|
||||
acceptance: 2/6 (33%)
|
||||
updated: 2026-05-16
|
||||
|
||||
Pending checks:
|
||||
- [ ] Recipe creada y validada
|
||||
- [ ] DAG corre OK 2 veces consecutivas via scheduler
|
||||
- [ ] data_factory.runs tiene >=2 entries
|
||||
- [ ] Schema extraido cubre 6/6 fields
|
||||
```
|
||||
|
||||
### `done <NNNN> [--notes "..."]`
|
||||
|
||||
Pasos:
|
||||
1. Verifica todos los `[ ]` estan checked. Si no, prompt "X checks pendientes, --force para cerrar igualmente".
|
||||
2. Edita frontmatter: `status: done`, `updated: <hoy>`.
|
||||
3. Si `--notes`, append a seccion `## Notas`.
|
||||
4. `git mv dev/flows/NNNN-<slug>.md dev/flows/completed/`.
|
||||
5. Actualiza `dev/flows/INDEX.md`: cambia status del flow + mueve fila a seccion Completed (mantener tabla principal solo con pending/running/failed/deferred).
|
||||
|
||||
### `run <NNNN>` — FASE 2 (NO IMPLEMENTADO AUN)
|
||||
|
||||
Hoy: imprime `/flow run no implementado todavia. Sigue los pasos manualmente y marca acceptance con sed/edit.`
|
||||
|
||||
Diseño futuro:
|
||||
- Parsea `## Flow` en pasos.
|
||||
- Cada paso tipo `function: <id>` -> ejecuta `./fn run <id>`.
|
||||
- Cada paso tipo `cmd: <bash>` -> ejecuta subprocess.
|
||||
- Texto libre -> "MANUAL: <text>" + pause user input.
|
||||
- Persistencia ejecuciones en `dev/flows/runs/<id>-<timestamp>.jsonl`.
|
||||
- Update acceptance checkboxes automaticamente segun heuristics (count runs en data_factory, etc.).
|
||||
|
||||
## Conventions
|
||||
|
||||
- Numeracion 0001+, propia (no comparte con `dev/issues/`).
|
||||
- Status: `pending | running | done | failed | deferred`.
|
||||
- Risk: `low` (publico) | `medium` (auth no sensible) | `high` (datos personales).
|
||||
- Apps listadas en frontmatter — `/flow list --app navegator_dashboard` filtra.
|
||||
- Acceptance es la fuente de verdad del progreso.
|
||||
|
||||
## Output style
|
||||
|
||||
Caveman. Tablas markdown. Sin emojis. Sin verbosidad.
|
||||
|
||||
Errores: 1 linea con el problema + sugerencia.
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```
|
||||
/flow create reddit-sentiment-tracker
|
||||
# crea dev/flows/0008-reddit-sentiment-tracker.md
|
||||
# anade fila a INDEX
|
||||
|
||||
/flow list --pending
|
||||
# muestra solo flows no cerrados
|
||||
|
||||
/flow status 0001
|
||||
# acceptance 0/6, todos los checks pendientes
|
||||
|
||||
# Tras correr el flow manualmente:
|
||||
# editas el .md, marcas [x] los checks completados
|
||||
/flow status 0001
|
||||
# acceptance 6/6
|
||||
/flow done 0001 --notes "smoke pass; LLM tardo 14s; recipe robusta"
|
||||
# mueve a completed/, marca status=done
|
||||
```
|
||||
@@ -50,7 +50,7 @@ Issue 0085 fase autocompleta. Reemplaza el flujo manual de "veo un patron, decid
|
||||
### 1. AUDIT — ¿estoy siendo registrado?
|
||||
|
||||
```bash
|
||||
ROOT="/home/lucas/fn_registry"
|
||||
ROOT="$HOME/fn_registry"
|
||||
MON="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
|
||||
|
||||
# Pre-condiciones
|
||||
@@ -152,6 +152,20 @@ Tambien actualiza `call_monitor.copied_code` + `function_stats` corriendo:
|
||||
cd "$ROOT/projects/fn_monitoring/apps/call_monitor" && ./call_monitor copied-code && ./call_monitor propose
|
||||
```
|
||||
|
||||
### 5b. MEMORIZE — anadir cada funcion nueva a MEMORY.md (issue 0087 pieza 6)
|
||||
|
||||
Por cada funcion creada con exito, llama:
|
||||
|
||||
```bash
|
||||
bash "$ROOT/.claude/scripts/append_fn_to_memory.sh" "<fn_id>" "<one-line purpose>"
|
||||
```
|
||||
|
||||
El script es idempotente (si la fn ya esta linkeada, no duplica). Crea `reference_fn_<id>.md` con metadata `type: reference` e indexa la entrada en `MEMORY.md` como linea `- [fn-<id>](reference_fn_<id>.md) — <purpose>`. Asi proximas sesiones cargan MEMORY.md y ven el catalogo de funciones recien creadas sin segunda lookup.
|
||||
|
||||
`purpose` = 1 frase derivada del `description` del .md de la funcion (max 80 chars). Si description es larga, recorta. Ejemplo:
|
||||
- fn_id: `parse_http_log_go_infra`
|
||||
- purpose: "parsea log Apache/Nginx a struct; pure"
|
||||
|
||||
Reporta:
|
||||
- N funciones nuevas creadas (con IDs)
|
||||
- N proposals nuevas en `registry.db.proposals`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Wrapper sobre el pipeline `full_git_pull_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
./fn run full_git_pull_bash_pipelines
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Wrapper sobre el pipeline `full_git_push_bash_pipelines`. Toda la lógica vive en el registry. Este comando solo ejecuta:
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd "${FN_REGISTRY_ROOT:-$HOME/fn_registry}"
|
||||
./fn run full_git_push_bash_pipelines "$ARGUMENTS"
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
description: "Gestiona issues del registry en dev/issues/. Subcomandos: list, show, status, board, dep, roadmap, tag, done, stale, create. Frontmatter YAML canonico (issue 0100)."
|
||||
---
|
||||
|
||||
# /issue — Gestionar issues del registry
|
||||
|
||||
Issues viven en `dev/issues/NNNN-<slug>.md` con frontmatter YAML canonico (id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags).
|
||||
|
||||
Allowlists en `dev/TAXONOMY.md` (no inventar valores).
|
||||
|
||||
Diferencia con `dev/flows/`:
|
||||
- **Issues** = bugs, features, refactors, chores, epics de implementacion.
|
||||
- **Flows** = casos de uso end-to-end multi-app.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/issue list [--domain X] [--type Y] [--status Z] [--prio P] [--epic NNNN]
|
||||
/issue show NNNN
|
||||
/issue status NNNN # acceptance % + estado deps
|
||||
/issue board # kanban pendiente/in-progress/bloqueado/done
|
||||
/issue dep NNNN # arbol bloquea/depende
|
||||
/issue roadmap NNNN # epic + sub-IDs (NNNNa, NNNNb, ...)
|
||||
/issue tag NNNN +X -Y # mantenimiento tags/domain
|
||||
/issue done NNNN # mueve a completed/, valida deps
|
||||
/issue stale [--days 30]
|
||||
/issue create <slug> --type T --domain D [--prio P] [--depends NNNN]
|
||||
```
|
||||
|
||||
## Implementacion
|
||||
|
||||
**Fase 1 (manual via Claude):**
|
||||
|
||||
El agente lee `dev/issues/*.md`, parsea frontmatter YAML con `yaml.safe_load`, aplica el filtro, imprime tabla.
|
||||
|
||||
```python
|
||||
import yaml, pathlib, re
|
||||
issues = []
|
||||
for f in pathlib.Path("dev/issues").glob("*.md"):
|
||||
if f.name in {"README.md", "template.md"}: continue
|
||||
txt = f.read_text()
|
||||
m = re.match(r"^---\n(.*?)\n---", txt, re.S)
|
||||
if not m: continue
|
||||
fm = yaml.safe_load(m.group(1)) or {}
|
||||
fm["_path"] = str(f)
|
||||
issues.append(fm)
|
||||
# filter + print
|
||||
```
|
||||
|
||||
**Fase 2 (cuando 0101 dev_console exista):**
|
||||
|
||||
Cada subcomando se mapea a `./apps/dev_console/dev_console issue <subcomando> $ARGS`.
|
||||
|
||||
## Subcomandos clave
|
||||
|
||||
### `list`
|
||||
|
||||
Imprime tabla `id | title | status | type | domain | priority | depends_pending`. Filtrable por flags.
|
||||
|
||||
### `show NNNN`
|
||||
|
||||
Read directo del .md + render del frontmatter como tabla + body como markdown.
|
||||
|
||||
### `status NNNN`
|
||||
|
||||
Cuenta checkboxes en `## Acceptance` + chequea si todos los `depends` estan en `status: completado`. Si alguno no, marca `bloqueado`.
|
||||
|
||||
### `board`
|
||||
|
||||
Tabla 4 columnas (pendiente / in-progress / bloqueado / completado_hoy). Card por issue: id + title + prio. Status `bloqueado` se calcula on-the-fly desde `depends`.
|
||||
|
||||
### `roadmap NNNN`
|
||||
|
||||
Si `type: epic`: lista sub-issues `NNNNa`, `NNNNb`, etc. con su estado. Si no epic: error "not an epic".
|
||||
|
||||
### `done NNNN`
|
||||
|
||||
1. Lee frontmatter.
|
||||
2. Verifica todos `depends` cerrados (sino, error).
|
||||
3. Cuenta `## Acceptance` 100% (sino, error).
|
||||
4. `git mv dev/issues/NNNN-*.md dev/issues/completed/`.
|
||||
5. Actualiza `status: completado` + `updated: today`.
|
||||
|
||||
### `create <slug> --type T --domain D`
|
||||
|
||||
Genera siguiente ID libre (max existing + 1, zero-padded 4). Scaffold desde plantilla minima con frontmatter rellenado.
|
||||
|
||||
## Reglas
|
||||
|
||||
- Domain debe estar en `dev/TAXONOMY.md` allowlist.
|
||||
- Scope/type/priority idem.
|
||||
- `id` siempre string `"NNNN"` (zero-padded, sub-IDs con sufijo `a-z`).
|
||||
- Modificar frontmatter SIEMPRE preserva campos no tocados (no overwrite).
|
||||
@@ -3,7 +3,7 @@
|
||||
Wrapper sobre el pipeline `init_cpp_app_bash_pipelines`. Genera la estructura canonica que cumple `cpp/PATTERNS.md` y `.claude/rules/cpp_apps.md` (main.cpp con `cfg.about/log/panels`, sin `app_menubar` manual, dockspace via framework), registra la app en `cpp/CMakeLists.txt`, crea repo Gitea `dataforge/<name>` y ejecuta `fn index`.
|
||||
|
||||
```bash
|
||||
cd /home/lucas/fn_registry
|
||||
cd $HOME/fn_registry
|
||||
./fn run init_cpp_app $ARGUMENTS
|
||||
```
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Si vacio: detectar app desde `pwd` (si estas dentro de `apps/<X>/` o `projects/*
|
||||
### 1. Resolver app objetivo
|
||||
|
||||
```bash
|
||||
ROOT=/home/lucas/fn_registry
|
||||
ROOT=$HOME/fn_registry
|
||||
ARG="$ARGUMENTS"
|
||||
|
||||
if [ -z "$ARG" ]; then
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: version
|
||||
description: Bumpear semver de un modulo, framework, paquete o app del registry. Edita <target>.md::version + ## Capability growth log. NO commitea.
|
||||
---
|
||||
|
||||
# /version
|
||||
|
||||
Bumpea la version de un **modulo, framework, paquete o app** del registry siguiendo SemVer estricto y mantiene el `## Capability growth log` sincronizado con `<target>.md::version`.
|
||||
|
||||
Disenado para usarse desde `/fix-issue` cuando el cambio afecte:
|
||||
- `modules/<X>/` (cualquier modulo C++) — edita `module.md`
|
||||
- `cpp/framework/` — edita `modules/framework/module.md`
|
||||
- `apps/<X>/` o `projects/<P>/apps/<X>/` — edita `app.md`
|
||||
- Otros paquetes versionados con `<target>.md` y campo `version:`
|
||||
|
||||
## Inputs
|
||||
|
||||
```
|
||||
/version <path> <major|minor|patch> "<reason>"
|
||||
```
|
||||
|
||||
- `<path>`: directorio del target (ej. `modules/data_table`, `cpp/framework`, `apps/chart_demo`, `projects/fn_monitoring/apps/registry_dashboard`).
|
||||
- `<major|minor|patch>`: tipo de bump SemVer.
|
||||
- `<reason>`: 1-frase humana — lo que cambia. Se inserta en el log.
|
||||
|
||||
## Resolucion del archivo target
|
||||
|
||||
| Path empieza por | Archivo a editar |
|
||||
|---|---|
|
||||
| `modules/` | `<path>/module.md` |
|
||||
| `cpp/framework` | `modules/framework/module.md` |
|
||||
| `apps/` | `<path>/app.md` |
|
||||
| `projects/*/apps/` | `<path>/app.md` |
|
||||
| `projects/*/analysis/` | `<path>/analysis.md` |
|
||||
|
||||
Si no encuentra archivo target -> ERROR.
|
||||
|
||||
## Reglas SemVer
|
||||
|
||||
### Modulos / framework
|
||||
|
||||
| Bump | Cuando |
|
||||
|---|---|
|
||||
| `major` | Cambios breaking en API publica: firma de entry function, layout de State struct expuesto, eliminacion de members, cambio incompatible de comportamiento. |
|
||||
| `minor` | Adiciones backwards-compatible: nuevo evento opt-in, nuevo renderer, nuevo helper, nuevo miembro. |
|
||||
| `patch` | Bugfix sin cambio de API. |
|
||||
|
||||
Refactor interno SIN cambio de API publica -> `minor` (no major).
|
||||
|
||||
### Apps
|
||||
|
||||
| Bump | Cuando |
|
||||
|---|---|
|
||||
| `major` | Breaking observable por usuarios: CLI args incompatibles, schema BBDD propia rompe lectores viejos, formato wire (HTTP/gRPC) incompatible, eliminacion de panel/feature que la gente usaba. |
|
||||
| `minor` | Feature aditiva: nuevo panel, nuevo endpoint, nueva opcion CLI, nueva tab, mejora visible no rompedora. |
|
||||
| `patch` | Bugfix sin cambio observable. Refactor interno. Mejoras de perf. |
|
||||
|
||||
Bump de **dependencia** (modulo/funcion del registry) que mejora la app pero la app no cambia su API -> `patch` (la app no es responsable de la mejora; el modulo si).
|
||||
|
||||
## Flujo
|
||||
|
||||
### 1. Validar input
|
||||
|
||||
- `<target_file>` existe -> si no, ERROR.
|
||||
- Bump type en {major, minor, patch} -> si no, ERROR.
|
||||
- Reason no vacia -> si no, ERROR.
|
||||
|
||||
### 2. Leer version actual
|
||||
|
||||
Parsear frontmatter. Buscar `version: X.Y.Z`. Si no existe:
|
||||
- Para `module.md` -> ERROR "module.md sin campo version".
|
||||
- Para `app.md` -> asumir `0.1.0` (baseline) e insertar el campo despues de `domain:`.
|
||||
|
||||
### 3. Calcular proxima version
|
||||
|
||||
```
|
||||
1.4.0 + major = 2.0.0
|
||||
1.4.0 + minor = 1.5.0
|
||||
1.4.0 + patch = 1.4.1
|
||||
```
|
||||
|
||||
Major bump -> minor y patch a 0. Minor bump -> patch a 0.
|
||||
|
||||
### 4. Editar `<target_file>`
|
||||
|
||||
Cambiar linea `version: <old>` por `version: <new>`.
|
||||
|
||||
### 5. Anadir entrada a `## Capability growth log`
|
||||
|
||||
Insertar al inicio de la lista (lineas posteriores al header `## Capability growth log`):
|
||||
|
||||
```markdown
|
||||
- v<new> (<fecha YYYY-MM-DD>) — <reason>
|
||||
```
|
||||
|
||||
Si la seccion no existe -> crearla al final del archivo antes de `## Notes` (o al final si no hay Notes).
|
||||
|
||||
### 6. Verificar drift de members (solo modulos, opcional)
|
||||
|
||||
Si la herramienta `fn doctor modules` existe (post 0107a) y el target es modulo:
|
||||
- Compara `members:` actual vs ultima version registrada en `registry.db::modules_history`.
|
||||
- Si hay diff en members y bump es `patch` -> WARNING.
|
||||
- Si hay diff en API publica y bump no es `major` -> ERROR (require `--force`).
|
||||
|
||||
No aplica a apps (no tienen `members:`).
|
||||
|
||||
### 7. Stage en git
|
||||
|
||||
`git add <target_file>`. NO commit. El commit final lo hace el flujo padre.
|
||||
|
||||
### 8. Reportar
|
||||
|
||||
```
|
||||
/version apps/chart_demo minor "anade tab radar chart"
|
||||
|
||||
apps/chart_demo/app.md
|
||||
version: 1.2.0 -> 1.3.0
|
||||
## Capability growth log: + v1.3.0 (2026-05-18) — anade tab radar chart
|
||||
|
||||
Staged. NO committed.
|
||||
Next: terminar el fix-issue y hacer commit con el resto de cambios.
|
||||
```
|
||||
|
||||
## Reglas criticas
|
||||
|
||||
- **NUNCA commit**. `/version` solo edita + stage. El commit lo hace el flujo padre (`/fix-issue`, `/git-push`).
|
||||
- **NUNCA saltar version**. No 1.4.0 -> 1.4.2 directo.
|
||||
- **NUNCA bajar version**. Si rollback, crea nueva version superior con comportamiento viejo restaurado.
|
||||
- **fecha = HOY** (`date +%Y-%m-%d`).
|
||||
- **reason** comprensible sin contexto del PR actual.
|
||||
|
||||
## Referenciado desde
|
||||
|
||||
- `/fix-issue` — al detectar cambios en `modules/`, `cpp/framework/`, `apps/<X>/` o `projects/*/apps/<X>/`, sugiere ejecutar `/version` antes del commit final.
|
||||
- `.claude/rules/cpp_apps.md` — politica de bump.
|
||||
- `dev/issues/0107-modules-standardization.md` — origen del flujo (modulos).
|
||||
|
||||
## Ejemplos
|
||||
|
||||
```
|
||||
# Bug fix en data_table (modulo)
|
||||
/version modules/data_table patch "fix off-by-one en seleccion multi-row con shift+click"
|
||||
# -> 1.4.0 -> 1.4.1
|
||||
|
||||
# Feature opt-in en framework
|
||||
/version cpp/framework minor "anade cfg.auto_dockspace para overlay de paneles flotantes"
|
||||
# -> 1.1.0 -> 1.2.0
|
||||
|
||||
# Feature en app C++
|
||||
/version apps/chart_demo minor "anade tab radar chart con datos sinteticos"
|
||||
# -> 1.2.0 -> 1.3.0
|
||||
|
||||
# Bug fix en app de proyecto
|
||||
/version projects/fn_monitoring/apps/registry_dashboard patch "fix tooltip que mostraba duration_ms en segundos"
|
||||
# -> 0.4.1 -> 0.4.2
|
||||
|
||||
# Breaking en app: cambia schema de su BBDD propia
|
||||
/version apps/kanban major "cards.assignee_id pasa a ser TEXT[] (era TEXT); requiere migracion 008"
|
||||
# -> 1.0.0 -> 2.0.0
|
||||
```
|
||||
|
||||
## Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| Editar `version:` a mano sin `## Capability growth log` | Drift entre version y log; nadie sabe que cambio. |
|
||||
| Bumpear major en app por refactor interno | Confunde al usuario; refactor es patch. |
|
||||
| Patch para feature visible | Usuario no se entera que esta disponible. |
|
||||
| Reason "cambios varios" / "mejoras" | Inutil para auditar. Una frase concreta. |
|
||||
| Bump de app sin tocar codigo de la app (solo dep) | Bump va al modulo, no a la app. |
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
description: "Vista cross-cutting de issues + flows. Subcomandos: today, weekly, search, dashboard. Mezcla los dos universos en una lista priorizable."
|
||||
---
|
||||
|
||||
# /work — Vista cross-cutting issues + flows
|
||||
|
||||
Issues = trabajo de implementacion. Flows = casos de uso multi-app. `/work` los muestra juntos para responder "que hago ahora" sin saltar entre dos sitios.
|
||||
|
||||
## Sintaxis
|
||||
|
||||
```
|
||||
/work today # top items prio alta + deps satisfechas (issues + flows)
|
||||
/work weekly # review semanal: closed vs planeados
|
||||
/work search "texto" # FTS sobre issues + flows + completed
|
||||
/work dashboard # JSON consumible por tab Work (issue 0102)
|
||||
```
|
||||
|
||||
## Implementacion
|
||||
|
||||
**Fase 1 (manual via Claude):**
|
||||
|
||||
El agente lee `dev/issues/*.md` + `dev/flows/*.md`, parsea frontmatter YAML, ordena por:
|
||||
|
||||
1. `priority: alta` primero.
|
||||
2. `status: pendiente` con `depends` todos `completado` (no bloqueados).
|
||||
3. Items con DoD/Acceptance >=80% (a punto de cerrar).
|
||||
4. Fecha `updated` mas reciente.
|
||||
|
||||
Imprime tabla unificada:
|
||||
|
||||
```
|
||||
KIND | ID | TITLE | PRIO | STATUS | NEXT STEP
|
||||
issue| 0099 | datahub app launcher | alta | pendiente | revisar deps
|
||||
flow | 0001 | hn-top-stories | high | pending | cerrar DoD user-facing
|
||||
issue| 0100 | migrate issue frontmatter | alta | pendiente | ejecutar pipeline
|
||||
...
|
||||
```
|
||||
|
||||
**Fase 2 (cuando 0101 dev_console exista):**
|
||||
|
||||
`./apps/dev_console/dev_console work <subcomando> $ARGS`.
|
||||
|
||||
## Subcomandos
|
||||
|
||||
### `today`
|
||||
|
||||
Filtro: `priority in (alta, media)` + `status: pendiente` + dependencias resueltas. Max 10 items. Si hay >10, prioriza `alta` y avisa "N items pendientes en cola".
|
||||
|
||||
### `weekly`
|
||||
|
||||
Git log `--since='1 week ago'` sobre `dev/issues/completed/` y `dev/flows/completed/` -> tabla de items cerrados. Comparado con `created: <esta semana>` -> ratio in/out.
|
||||
|
||||
### `search "texto"`
|
||||
|
||||
`grep -ri` sobre `dev/issues/` + `dev/flows/` (incluido completed/), filtra por title/body. Output: `path:line: match`.
|
||||
|
||||
### `dashboard`
|
||||
|
||||
Output JSON estructurado para consumo por tab Work del `registry_dashboard` (issue 0102). Estructura:
|
||||
|
||||
```json
|
||||
{
|
||||
"issues": {"pendiente": [...], "in-progress": [...], "bloqueado": [...], "completado_24h": [...]},
|
||||
"flows": [{"id": "0001", "dod_percent": 50, "user_facing_percent": 0, "...": ...}],
|
||||
"telemetry": {"calls_24h": N, "violations_24h": N, "pending_proposals": N}
|
||||
}
|
||||
```
|
||||
@@ -21,6 +21,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
||||
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
|
||||
| 16 | [kiss.md](kiss.md) | KISS en proyectos y apps: cuestionar herramientas externas, sin abstracciones especulativas |
|
||||
| 17 | [apps_tbd.md](apps_tbd.md) | Trunk-based development obligatorio en apps generadas con `fn` (registry exento) |
|
||||
| 17b | [apps_subrepo.md](apps_subrepo.md) | Apps son sub-repos Gitea (apps/* gitignored). `git init` dentro de cada app nueva ANTES de limpiar worktree, sino se pierde el codigo |
|
||||
| 18 | [uses_functions.md](uses_functions.md) | Convencion de uses_functions para C++: el .md del consumidor declara las dependencias |
|
||||
| 19 | [cpp_apps.md](cpp_apps.md) | Estandarizacion de apps C++: estructura, CMake, app.md, sub-repo, runtime — apunta a cpp/PATTERNS.md y cpp/DESIGN_SYSTEM.md como autoritativas |
|
||||
| 20 | [artefactos.md](artefactos.md) | Termino paraguas para apps, analysis, vaults, projects y playgrounds (todo lo que no es codigo reutilizable) |
|
||||
@@ -34,3 +35,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
||||
| 28 | [delegation.md](delegation.md) | Si vas a escribir logica reutilizable inline -> spawn fn-constructor inmediato + tag de grupo + usar en mismo turno. Issue 0086 |
|
||||
| 29 | [capability_groups.md](capability_groups.md) | Tags planos + paginas madre `docs/capabilities/<grupo>.md` para desbloquear clusters de funciones en un read. Issue 0086 |
|
||||
| 30 | [function_growth_and_self_docs.md](function_growth_and_self_docs.md) | Contrato self-doc de cada `.md` (Ejemplo + Cuando usarla + Gotchas + Growth log) + crecimiento del registry por **promocion de composiciones** a pipelines, NO por inflado de funciones. Issue 0087 |
|
||||
| 31 | [autonomous_loop.md](autonomous_loop.md) | Reglas para `fn-orquestador` + `/autonomous-task`: sandbox obligatorio, paths protegidos, filtro proposals auto-aplicables, watchdog, idempotencia. Issue 0069 |
|
||||
| 32 | [../../dev/TAXONOMY.md](../../dev/TAXONOMY.md) | Allowlist canonica para dominios/tipos/scopes/estados/prioridades + flow patterns. Aplica a `dev/issues/` y `dev/flows/`. Issues 0100 + 0103 |
|
||||
| 33 | [project_commands.md](project_commands.md) | Slash commands por project (`.claude/commands/<project>/`) expuestos via symlink. Desde fn_registry: `/<project>:foo`. Desde el project: `/foo`. Sin colision. |
|
||||
| 34 | [dod_quality.md](dod_quality.md) | DoD Quality Triada: Mecanica + Cobertura (golden + edge + error path con evidencia ejecutable) + Vida util validada (>=7 dias uso real). Cierra anti-criterios contra checkbox vago. Aplica a `dev/flows/` y issues user-facing. |
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
## Apps son sub-repos Gitea independientes — gotcha al usar worktrees
|
||||
|
||||
**Regla operativa critica** descubierta el 2026-05-18 durante implementacion del flow 0008.
|
||||
|
||||
### El gotcha
|
||||
|
||||
`apps/*/` esta en `.gitignore` del repo `fn_registry`. Cada app es **su propio repo Gitea** en `dataforge/<app_name>` con su `.git/` dentro de `apps/<app_name>/`. Esto significa:
|
||||
|
||||
- Cuando un agente trabaja en un git **worktree** del repo padre y crea `apps/<nueva_app>/`, los archivos viven SOLO en el working directory del worktree.
|
||||
- Como `apps/*/` esta gitignored en el repo padre, los archivos **no se pueden commitear** al worktree del repo padre.
|
||||
- Cuando se hace `git worktree remove --force worktrees/<slug>/`, el working directory entero se borra — **el codigo de la app desaparece**.
|
||||
|
||||
**Consecuencia**: una app creada dentro de un worktree del repo padre se pierde al limpiar el worktree salvo que se haya promovido a su propio sub-repo Gitea ANTES.
|
||||
|
||||
### El patron correcto al crear apps en worktrees
|
||||
|
||||
```bash
|
||||
# 1. Agente trabaja en worktree del repo padre
|
||||
cd $HOME/fn_registry/worktrees/<slug>
|
||||
|
||||
# 2. Scaffold la app via pipeline canonico
|
||||
./fn run init_cpp_app <name> # apps C++
|
||||
# o ./fn run init_jupyter_analysis ... # analysis
|
||||
# o crear apps/<name>/ a mano (Go service, etc.)
|
||||
|
||||
# 3. ANTES de salir del worktree: inicializa la app como sub-repo
|
||||
cd apps/<name>
|
||||
git init -b master
|
||||
git add -A
|
||||
git -c user.email="agent@fn_registry" -c user.name="agent" \
|
||||
commit -m "feat: initial scaffold of <name>"
|
||||
|
||||
# 4. Trabajo continua en sub-repo (commits dentro de apps/<name>/.git)
|
||||
# 5. Cerrar issue en repo padre (mv .md a completed/), commit del padre con cambios en cpp/CMakeLists.txt, etc.
|
||||
```
|
||||
|
||||
Cuando el humano corre `/full-git-push` despues del merge, el script `ensure_repo_synced_bash_infra` detecta que `apps/<name>/.git` existe + no tiene remote + crea repo Gitea en `dataforge/<name>` + pushea master.
|
||||
|
||||
### Que ESTA SI versionado en el repo padre
|
||||
|
||||
- `cpp/CMakeLists.txt` (el `if(EXISTS ...) add_subdirectory(apps/<name>) endif()`).
|
||||
- `dev/issues/completed/<NNNN>-<slug>.md` (cierre del issue).
|
||||
- `docs/capabilities/*.md` si la app aporta a un capability group.
|
||||
- `dev/feature_flags.json` si introduce flags.
|
||||
|
||||
Todo lo demas (codigo de la app + app.md + appicon + service unit + tests propios de la app) vive en `apps/<name>/.git` independiente.
|
||||
|
||||
### Sintomas de la perdida
|
||||
|
||||
Si limpias el worktree y luego corres `ls apps/<name>/`, devuelve "No such file or directory" pese a que el issue aparece cerrado en `dev/issues/completed/`. **Patron** = scaffold sin sub-repo init = trabajo perdido.
|
||||
|
||||
### Recovery si pasa
|
||||
|
||||
1. Re-crear worktree desde master.
|
||||
2. Re-spawn agente con instruccion explicita: **`git init` dentro de la app antes de terminar**.
|
||||
3. NO eliminar el worktree hasta confirmar que `apps/<name>/.git` esta inicializado con al menos un commit.
|
||||
|
||||
### Aplica tambien a analysis
|
||||
|
||||
`analysis/*/` y `projects/*/analysis/*/` siguen mismo patron (cada analysis es repo Gitea). El pipeline `init_jupyter_analysis_bash_pipelines` ya hace `git init` automatico — por eso no hubo perdidas alli. Las apps C++/Go scaffolded a mano NO inicializan el sub-repo automaticamente — es responsabilidad del agente.
|
||||
|
||||
### Lo que aprende `parallel-fix-issues`
|
||||
|
||||
El template del prompt de cada agente DEBE incluir la instruccion:
|
||||
|
||||
> "Si tu issue crea una app nueva en `apps/<name>/`, inicializa el sub-repo (`cd apps/<name> && git init -b master && git add -A && git commit ...`) antes de terminar. Sin esto, `apps/*` esta gitignored y el codigo se perdera cuando el orquestador limpie el worktree."
|
||||
|
||||
Aplicar este parrafo al template del skill — ver `.claude/skills/parallel-fix-issues/SKILL.md` (o equivalente).
|
||||
|
||||
### Relacion con otras reglas
|
||||
|
||||
- [[apps_tbd]] — TBD en apps, esta regla complementa con el patron de sub-repo init.
|
||||
- [[artefactos]] — apps son artefactos, esta regla especifica gotcha de su sub-repo.
|
||||
- [[apps_vs_functions]] — apps en `apps/`, esta regla refuerza por que apps/* gitignored.
|
||||
@@ -0,0 +1,102 @@
|
||||
## Bucle autonomo (`fn-orquestador` + `/autonomous-task`) — issue 0069
|
||||
|
||||
`fn-orquestador` recorre el ciclo reactivo (CONSTRUIR → EJECUTAR → RECOPILAR → ANALIZAR → MEJORAR) sin intervencion humana, hasta convergencia (suite verde), estancamiento (no progreso N iteraciones), timeout, o tope de iteraciones. Trabaja SIEMPRE en sandbox `auto/<issue>`, NUNCA merge a master.
|
||||
|
||||
### Cuando se invoca
|
||||
|
||||
- Skill `/autonomous-task <issue_id>` (humano lanza explicitamente).
|
||||
- Cron / dag_engine (`schedule:` en YAML; planificable, no implementado por defecto).
|
||||
- NO se invoca como reaccion a hooks ni a fallos de tests "en caliente". Siempre tarea explicita.
|
||||
|
||||
### Reglas duras
|
||||
|
||||
1. **Sandbox obligatorio**: rama `auto/<issue_id>-<slug>`. Si la rama existe -> reset hard contra master y reanudar. NUNCA commits a master, NUNCA push --force-with-lease a master.
|
||||
2. **Paths protegidos**: respetar `dev/autonomous_protected_paths.json` exactamente. Cualquier intento de modificar un path protegido aborta la iteracion y registra `task_runs.status='aborted_protected_path'`.
|
||||
3. **Filtro de proposals auto-aplicables**: el orquestador SOLO aplica proposals que cumplen:
|
||||
- `kind in (bug_fix, e2e_check_add, doc_update, capability_tag_add)` -> auto-aplicable.
|
||||
- `kind in (new_function, deprecate_function, refactor, schema_change)` -> NO auto-aplicable (queda `pending` para humano).
|
||||
- `priority in (low, medium)` -> auto-aplicable. `high|critical` -> requiere humano salvo override `--allow-high`.
|
||||
4. **Watchdog**: si la metrica de progreso (`checks_pass / checks_total`) no sube en `N=3` iteraciones consecutivas -> abort. Registrar `task_runs.status='stalled'`.
|
||||
5. **Tiempo**: cada `task_run` con timeout default 30 min. Override con `--timeout-min N` hasta max 4h.
|
||||
6. **Idempotencia**: re-ejecutar `/autonomous-task <id>` sobre la misma issue reanuda desde la ultima iteracion exitosa, NO reinicia desde cero (lookup en `task_runs` por `issue_id`).
|
||||
7. **Trazabilidad**: cada decision se persiste en `task_runs.events_json[]` con `{ts, agent, action, evidence, diff_summary}`. El humano puede leer el log entero para auditar.
|
||||
8. **No self-modification**: orquestador NUNCA modifica `.claude/agents/`, `.claude/commands/`, `.claude/rules/`, `.claude/scripts/`, `.claude/CLAUDE.md`. Reforzado en `autonomous_protected_paths.json`.
|
||||
9. **NUNCA paths absolutos fuera del worktree**. Refuerzo del piloto 1 (2026-05-15): el orquestador uso `/home/lucas/fn_registry/bash/functions/...` para fixear hooks bash y contamino el repo principal. Solucion correcta: fix vive solo en el worktree. Post-cada-iteracion: `git -C <main_repo> status --short` debe permanecer igual al baseline; cualquier diff = `status=sandbox_breach` -> ABORT.
|
||||
10. **Pre-commit hooks compartidos**. Worktrees comparten `.git/hooks/` con main. Si un hook llama scripts via path absoluto, ejecutara la version de main. Si el hook bloquea progreso por bug en main: aplica el fix EN EL WORKTREE (commit en auto/*); si el bug del hook excede scope: `git commit --no-verify` para ESE commit con `task_runs.events_json[].decision="skip_hook"` + razon. NO editar main.
|
||||
|
||||
### Sub-repos vs worktree padre
|
||||
|
||||
Cuando el issue toca `app.md` o codigo dentro de `apps/<name>/`, `projects/<p>/apps/<name>/`, `cpp/apps/<name>/`, o `analysis/<a>/` — estos directorios son **sub-repos Gitea independientes** y estan `.gitignore`d en el repo padre `fn_registry` (regla `apps_subrepo.md`). El orquestador:
|
||||
|
||||
- **Crea worktree padre** `auto/<issue>` en `/tmp/fn_orq_<issue>_<ts>/` por protocolo, **pero no escribe alli** porque los cambios no se versionan en el padre.
|
||||
- **Opera DIRECTAMENTE en el sub-repo** de la app/analysis target. Branch `auto/<issue>-<slug>` se crea dentro de `apps/<name>/.git`, NO en el padre.
|
||||
- **PR draft sale al sub-repo** en `dataforge/<name>` (NO a `dataforge/fn_registry`). Humano revisa+mergea en el sub-repo.
|
||||
- **Worktree padre queda vacio** y se limpia normal con `git worktree remove` al terminar.
|
||||
|
||||
Validado en piloto 0120 (`add_e2e_check` sobre `chart_demo`): PR creado en `dataforge/chart_demo/pulls/1`, sanity check del main repo `fn_registry` confirmo cero contaminacion.
|
||||
|
||||
Si el issue toca AMBOS lados (codigo del registry padre + app de sub-repo), el orquestador commitea separado: cambios del padre en `auto/<issue>` (worktree padre), cambios de la app en `auto/<issue>-<slug>` (sub-repo). Dos PRs draft. Humano coordina merge.
|
||||
|
||||
### Gitea API vs `gh`
|
||||
|
||||
Pre-condicion `gh auth status` es smoke check (target github.com). Mecanismo real de PR es `curl` a Gitea API:
|
||||
|
||||
```bash
|
||||
GITEA_TOKEN=$(pass gitea/dataforge-git-token | head -n1)
|
||||
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"...","head":"auto/<issue>-<slug>","base":"master","draft":true,"body":"..."}' \
|
||||
"https://gitea-.../api/v1/repos/dataforge/<repo>/pulls"
|
||||
```
|
||||
|
||||
Validado en pilotos 0076 y 0120.
|
||||
|
||||
### Estructura task_run
|
||||
|
||||
Migration `fn_operations/migrations/006_task_runs.sql`. Campos minimos: `id`, `issue_id`, `branch`, `started_at`, `finished_at`, `status` (`running|done|failed|aborted_protected_path|stalled|timeout`), `iterations`, `checks_pass`, `checks_fail`, `proposals_applied_json`, `proposals_skipped_json`, `events_json`, `final_diff_sha`.
|
||||
|
||||
### Fases por iteracion
|
||||
|
||||
```
|
||||
loop:
|
||||
1. fn-constructor (Read+Edit+Write+Bash limitados) - aplica fix segun ultima proposal seleccionada
|
||||
2. fn-executor - corre build + tests + smoke
|
||||
3. fn-recopilador - audita operations.db de la app
|
||||
4. fn-analizador - corre e2e_checks (registra e2e_runs)
|
||||
5. SI todos los checks pasan -> commit + push rama + abre PR. status=done. exit.
|
||||
6. SI no progreso N iteraciones -> abort. status=stalled.
|
||||
7. fn-mejorador - crea proposals desde fallos
|
||||
8. orquestador filtra proposals auto-aplicables -> selecciona la primera -> goto 1.
|
||||
```
|
||||
|
||||
### Output al humano
|
||||
|
||||
```
|
||||
=== /autonomous-task 0068 ===
|
||||
task_run_id: run_e2e_a1b2c3
|
||||
branch: auto/0068-e2e-validation
|
||||
iterations: 4
|
||||
status: done
|
||||
checks_pass: 8/8
|
||||
proposals_applied: 3 (run_e2e_run_001, run_e2e_run_002, run_e2e_run_003)
|
||||
proposals_skipped: 1 (refactor — needs human review)
|
||||
PR: https://gitea.../pulls/42
|
||||
```
|
||||
|
||||
### Anti-patrones
|
||||
|
||||
| Anti-patron | Por que es malo |
|
||||
|---|---|
|
||||
| Mergear `auto/<issue>` a master sin PR + humano | Salta gate, riesgo de regresion |
|
||||
| Auto-aplicar proposal `kind=refactor` | Cambios sistemicos requieren revision |
|
||||
| Modificar `go.sum`, `package-lock.json`, `uv.lock` | Cambios de deps requieren CVE/license review |
|
||||
| Bucle infinito sin watchdog | Coste descontrolado de tokens |
|
||||
| Borrar archivos sin backup en `task_runs.events_json` | Pierde auditoria |
|
||||
| Override de paths protegidos via env var | Bypass de seguridad |
|
||||
|
||||
### Relacion con otras reglas
|
||||
|
||||
- [[e2e_validation]] — fn-analizador (fase 4) lee el contrato `e2e_checks` que el orquestador usa como gate.
|
||||
- [[apps_tbd]] — el orquestador opera en rama `auto/*`, no exenta de TBD.
|
||||
- [[feature_flags]] — si el fix no esta terminado, el orquestador puede meterlo detras de flag OFF antes de PR.
|
||||
- [[registry_calls]] — toda invocacion del orquestador y sub-agentes pasa por MCP/`fn run`/heredoc canonico, registrada en call_monitor.
|
||||
+233
-12
@@ -20,14 +20,14 @@ Razones:
|
||||
|
||||
Pipeline: `init_cpp_app_bash_pipelines`. Slash command equivalente: `/new-cpp-app`. Auditoria: `fn doctor cpp-apps`.
|
||||
|
||||
### 1. Ubicacion
|
||||
### 1. Ubicacion (issue 0096 estandarizada)
|
||||
|
||||
| Caso | Donde vive |
|
||||
|---|---|
|
||||
| App independiente | `cpp/apps/<nombre>/` |
|
||||
| App independiente | `apps/<nombre>/` |
|
||||
| App de un proyecto | `projects/<proyecto>/apps/<nombre>/` |
|
||||
|
||||
NUNCA en `cpp/apps/<nombre>/` si pertenece a un proyecto, NUNCA fuera de `apps/` directamente. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
|
||||
NUNCA en `cpp/apps/<nombre>/` (deprecado tras issue 0096) ni en cualquier otra carpeta nombrada por lenguaje (`python/apps/`, `bash/apps/`, etc.). Las carpetas por lenguaje son solo para codigo del registry (`cpp/functions/`, `python/functions/`, etc.), nunca para artefactos. Ver `apps_location` en memoria + regla `apps_vs_functions.md`.
|
||||
|
||||
### 2. Estructura minima
|
||||
|
||||
@@ -84,6 +84,7 @@ Plantilla minima para apps C++:
|
||||
name: <name>
|
||||
lang: cpp
|
||||
domain: <gfx|tui|tools|infra|...>
|
||||
version: 0.1.0 # semver per-app, bumped via /version
|
||||
description: "Frase corta — lo que hace y por que existe."
|
||||
tags: [imgui, ...] # si es service, anadir 'service'
|
||||
uses_functions: # IDs del registry — el indexer NO deduce C++
|
||||
@@ -102,6 +103,7 @@ Reglas:
|
||||
- `framework: "imgui"` siempre que use `fn::run_app`. Otros valores solo si la app NO usa el shell (raro).
|
||||
- `tags`: incluir `service` si es daemon de larga duracion (ver `function_tags.md`).
|
||||
- `repo_url` apunta al sub-repo en Gitea (ver §6).
|
||||
- `version`: semver per-app. Baseline `0.1.0` para apps nuevas. Bump obligatorio via `/version apps/<name> {major|minor|patch} "<reason>"` cuando `/fix-issue` toque codigo de la app. Trazabilidad humana en seccion `## Capability growth log` al final del `app.md` (una linea por bump). Ver `.claude/commands/version.md`.
|
||||
|
||||
### 5. Registro en `cpp/CMakeLists.txt`
|
||||
|
||||
@@ -189,20 +191,105 @@ WMs). Activado por defecto, sin opt-in:
|
||||
con `glfwSetWindowPos/Size` (no espera al siguiente NewFrame).
|
||||
2. **Per-frame viewport sync** al inicio del main loop — cubre viewports
|
||||
secundarios (paneles drag-out) que la backend crea dinamicamente.
|
||||
3. **Win32 WndProc subclass** (`#ifdef _WIN32`) — observa `WM_ENTERSIZEMOVE`
|
||||
/ `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada drag. Mientras
|
||||
el bracket esta abierto el main loop SKIPEA `render_fn` + `glfwSwapBuffers`,
|
||||
replicando el contrato del title-bar drag native (DefWindowProc bloquea
|
||||
el hilo, DWM compositor mueve el framebuffer existente).
|
||||
3. **Win32 WndProc subclass per HWND** (`#ifdef _WIN32`) — observa
|
||||
`WM_ENTERSIZEMOVE` / `WM_EXITSIZEMOVE` que AltSnap fakea alrededor de cada
|
||||
drag. El subclass se instala en la ventana principal Y en cada HWND
|
||||
secundario que el backend de ImGui crea cuando un panel se arrastra fuera
|
||||
del main (escaneo per-frame de `pio.Viewports`). Mientras el bracket esta
|
||||
abierto en CUALQUIER HWND propio, el main loop SKIPEA `render_fn` +
|
||||
`glfwSwapBuffers` globalmente, replicando el contrato del title-bar drag
|
||||
native (DefWindowProc bloquea el hilo, DWM compositor mueve el framebuffer
|
||||
existente). El flag `g_in_sizemove` es global a proposito: una sola
|
||||
sesion de sizemove externo pausa todo el render para que ninguna ventana
|
||||
compita con el OS.
|
||||
|
||||
Tests: `cpp/apps/altsnap_jitter_test/` corre dos fases:
|
||||
Estado del subclass:
|
||||
- `g_subclassed` = `unordered_map<HWND, WNDPROC>`. Chain a la proc
|
||||
original via `CallWindowProcW`.
|
||||
- `install_sizemove_subclass_hwnd(HWND)` idempotente (skip si ya en mapa).
|
||||
- Per-frame: `prune_dead_subclassed()` con `IsWindow` + install en cada
|
||||
`pio.Viewports[i]->PlatformHandle` nuevo.
|
||||
- `uninstall_sizemove_subclass_all()` restaura cada HWND al exit.
|
||||
|
||||
#### Iconified main no pierde paneles flotantes (2026-05-16)
|
||||
|
||||
El legacy `glfwWaitEvents + continue` al detectar `GLFW_ICONIFIED` paraba TODO
|
||||
el frame loop. Con multi-viewport activo eso significa que
|
||||
`ImGui::UpdatePlatformWindows + RenderPlatformWindowsDefault` dejan de
|
||||
refrescar los viewports secundarios — los floating panels aparecen congelados
|
||||
o son agrupados/ocultados por el WM. Fix actual: el iconified-gate cuenta
|
||||
viewports secundarios primero; si hay alguno, fall-through al frame normal
|
||||
(la swap del main HWND minimizado es harmless, los contexts GL secundarios
|
||||
siguen pintando). Solo cuando NO hay flotantes dormimos en `glfwWaitEvents`.
|
||||
|
||||
#### Alt + RMB / Alt + LMB anywhere → modal nativo (2026-05-16)
|
||||
|
||||
WndProc del subclass tambien intercepta clicks con Alt held (`GetAsyncKeyState(VK_MENU) & 0x8000`):
|
||||
|
||||
- `WM_LBUTTONDOWN` + Alt → `ReleaseCapture()` +
|
||||
`PostMessage(WM_SYSCOMMAND, SC_MOVE | HTCAPTION)`. Modal MOVE nativo.
|
||||
- `WM_RBUTTONDOWN` + Alt → calcula direccion por cuadrante (TOPLEFT/TOPRIGHT/
|
||||
BOTTOMLEFT/BOTTOMRIGHT relativo al centro del client rect) y emite
|
||||
`PostMessage(WM_SYSCOMMAND, SC_SIZE | dir)`. Modal RESIZE nativo.
|
||||
|
||||
Ambos retornan 0 (consumen el click — ImGui NO lo ve). Aplica a main y a
|
||||
cada viewport flotante porque el subclass per-frame ya cubre todos los HWND.
|
||||
El modal nativo dispara `WM_ENTERSIZEMOVE`, que el gate existente pausa
|
||||
render → cero jitter automatico, mismo contrato que el title-bar drag.
|
||||
|
||||
**Caveat**: cualquier Alt+click se consume — perdes Alt+click como shortcut
|
||||
UI. Aceptable porque Alt-modifier en clicks UI es muy raro.
|
||||
|
||||
#### Title-bar-only move para ImGui windows (2026-05-16)
|
||||
|
||||
`fn::run_app` setea `io.ConfigWindowsMoveFromTitleBarOnly = true`. Critico
|
||||
para viewports secundarios: un viewport flotante = OS window borderless con
|
||||
UNA ventana ImGui rellenandolo. Sin el flag, ImGui mueve sus ventanas
|
||||
arrastrando cualquier client-pixel — como la ventana ImGui ES el viewport
|
||||
entero, el OS window sigue al cursor sin modifier. Con el flag, floating
|
||||
panels obedecen el contrato "solo header arrastra" (igual que main que tiene
|
||||
title bar nativo de Windows). Alt+LMB anywhere sigue funcionando (consumido
|
||||
antes por el subclass).
|
||||
|
||||
#### Test observability — `fn::internal::*` (2026-05-16)
|
||||
|
||||
Counters monotonicos para validar el subclass desde tests headless,
|
||||
zero-cost en prod:
|
||||
|
||||
```cpp
|
||||
namespace fn::internal {
|
||||
int sizemove_enter_count(); // ++ en cada WM_ENTERSIZEMOVE
|
||||
int alt_rmb_resize_count(); // ++ en cada Alt+RMB consumido
|
||||
int alt_lmb_move_count(); // ++ en cada Alt+LMB consumido
|
||||
int rbuttondown_seen_count(); // diagnostico — todo WM_RBUTTONDOWN
|
||||
void set_force_alt_for_test(bool); // bypass GetAsyncKeyState para tests
|
||||
}
|
||||
```
|
||||
|
||||
En test mode (`set_force_alt_for_test(true)`), los handlers de Alt cuentan
|
||||
pero NO postean `SC_SIZE`/`SC_MOVE` — el harness no se queda atrapado en el
|
||||
modal de Windows. Path real en prod sigue posteandolos.
|
||||
|
||||
Tests: `apps/altsnap_jitter_test/` corre seis fases:
|
||||
- `p1.sync` (cross-platform): drives `glfwSetWindowPos` cada frame, asserta
|
||||
`vp->Pos` sigue OS dentro de 1px.
|
||||
- `p2.altsnap` (Windows): worker thread fakea `WM_ENTERSIZEMOVE` +
|
||||
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE`, asserta
|
||||
que `render()` no se llama durante el bracket.
|
||||
burst de `SetWindowPos(SWP_ASYNCWINDOWPOS)` + `WM_EXITSIZEMOVE` sobre el
|
||||
HWND principal, asserta que `render()` no se llama durante el bracket.
|
||||
- `p3.secondary` (Windows): fuerza viewport secundario
|
||||
(`ConfigViewportsNoAutoMerge=true`), localiza su HWND y repite el bracket
|
||||
sobre el. Valida que el subclass per-viewport tambien pausa el render.
|
||||
- `p4.minimize` (Windows): state machine 4 steps — captura
|
||||
`IsWindow(secondary_hwnd)` antes/durante/despues de `glfwIconifyWindow +
|
||||
glfwRestoreWindow`. Asserta los 3 estados vivos y `renders_iconified > 0`.
|
||||
- `p5.alt_rmb` (Windows): `set_force_alt_for_test(true)` +
|
||||
`SendMessage(WM_RBUTTONDOWN)` sincrono mismo-hilo. Asserta
|
||||
`alt_rmb_resize_count` incrementa.
|
||||
- `p6.alt_lmb` (Windows): mismo patron para `WM_LBUTTONDOWN`. Asserta
|
||||
`alt_lmb_move_count` incrementa.
|
||||
|
||||
Lanzar con `e2e_run_cpp_windows altsnap_jitter_test`.
|
||||
Lanzar con `source bash/functions/infra/e2e_run_cpp_windows.sh &&
|
||||
e2e_run_cpp_windows altsnap_jitter_test`.
|
||||
|
||||
NO hace falta nada en cada app — toda `fn::run_app` lo hereda. Si una app
|
||||
necesita renderizar incluso durante external move (caso raro: telemetria
|
||||
@@ -261,3 +348,137 @@ de antes: `imgui.ini` es la unica fuente.
|
||||
- App headless / capture mode: `cfg.auto_layouts = false`.
|
||||
- Cambiar nombre del archivo: `cfg.auto_layouts_db = "<algo>.db"` (relativo a
|
||||
`local_files/`).
|
||||
|
||||
### 11. Icono Windows (.ico embebido en el .exe) — 2026-05-16
|
||||
|
||||
Cada app C++ desplegada a Windows tiene su propio icono. El icono vive en
|
||||
`<app_dir>/appicon.ico` (multi-resolucion: 16/24/32/48/64/128/256). El macro
|
||||
`add_imgui_app` de `cpp/CMakeLists.txt` lo detecta automaticamente: si
|
||||
`WIN32` + existe `<CMAKE_CURRENT_SOURCE_DIR>/appicon.ico`, genera un
|
||||
`<target>_appicon.rc` en `CMAKE_CURRENT_BINARY_DIR` apuntando al `.ico` con
|
||||
`IDI_ICON1 ICON "<path>"` y lo anade a `add_executable`. El compilador RC
|
||||
(`x86_64-w64-mingw32-windres` configurado en `cpp/toolchains/mingw-w64.cmake`)
|
||||
lo enlaza al `.exe` como recurso `.rsrc`.
|
||||
|
||||
Verificar: `x86_64-w64-mingw32-objdump -h <app>.exe | grep rsrc` debe
|
||||
mostrar la seccion. El project line en `cpp/CMakeLists.txt` declara
|
||||
`LANGUAGES C CXX RC` solo en WIN32 (Linux ignora la `.rc`).
|
||||
|
||||
#### Crear `.ico` para una app nueva
|
||||
|
||||
Fuente de glyphs: **Phosphor Icons** (`sources/phosphor-core/`, clonado de
|
||||
`https://github.com/phosphor-icons/core.git`). 1512 SVGs en weight `regular`,
|
||||
`bold`, `fill`, `light`, `thin`, `duotone`. Usamos `fill` por defecto — mejor
|
||||
legibilidad a 16/24px.
|
||||
|
||||
Funcion del registry: `generate_app_icon_py_infra` rasteriza un SVG Phosphor
|
||||
sobre fondo redondeado del color accent y exporta `.ico` multi-res. Una
|
||||
linea por app:
|
||||
|
||||
```python
|
||||
from infra import generate_app_icon
|
||||
generate_app_icon(
|
||||
phosphor_icon_name="chart-bar",
|
||||
accent_hex="#0ea5e9",
|
||||
out_ico_path="apps/chart_demo/appicon.ico",
|
||||
)
|
||||
```
|
||||
|
||||
Mapping vive en el frontmatter de cada `app.md` C++:
|
||||
|
||||
```yaml
|
||||
description: "Frase corta de 1 linea — que hace la app y por que existe."
|
||||
icon:
|
||||
phosphor: "chart-bar"
|
||||
accent: "#0ea5e9"
|
||||
```
|
||||
|
||||
### Trio obligatorio: description + icon.phosphor + icon.accent
|
||||
|
||||
**REGLA DURA:** TODA app C++/imgui declara los **3 campos JUNTOS** en su `app.md`:
|
||||
1. `description:` (string corta, 1 linea) — texto que el `app_hub_launcher` muestra en la tarjeta y que el dashboard usa para tooltips.
|
||||
2. `icon.phosphor:` (nombre del glyph Phosphor sin sufijo `-fill`) — glyph del icono.
|
||||
3. `icon.accent:` (hex `#rrggbb`) — color del fondo redondeado del icono **Y** color del boton/border de la tarjeta en `app_hub_launcher`.
|
||||
|
||||
Los 3 se consumen como un set unico: el icono visual + el texto + el color de marca de la app. Una app sin descripcion aparece como tarjeta gris sin texto; sin `icon:` cae al default (`app-window` slate); sin accent el boton del hub aparece blanco. **Documentar uno sin los otros es bug**, no estilo.
|
||||
|
||||
### Refrescar el App Hub tras editar el trio
|
||||
|
||||
`app_hub_launcher` cachea iconos (PNG) y manifest (TSV) al arrancar. Cambiar `description`/`icon.*` en un `app.md` requiere regenerar ambos sidecars + relanzar el hub. Pipeline canonico:
|
||||
|
||||
```bash
|
||||
./fn run refresh_app_hub # icons + manifest + restart hub
|
||||
./fn run refresh_app_hub --no-restart # solo regenera, util si el hub esta cerrado
|
||||
./fn run refresh_app_hub --size 128 # PNGs 128px en vez de 64
|
||||
```
|
||||
|
||||
ID: `refresh_app_hub_bash_pipelines`. Compone `export_hub_icons_py_infra` + `export_hub_manifest_py_infra` + `is_cpp_app_running_windows_bash_infra` + `launch_cpp_app_windows_bash_infra`.
|
||||
|
||||
Regeneracion batch via pipeline del registry — escanea `app.md`s y compone
|
||||
`generate_app_icon` por app. Anadir app nueva: declarar `icon:` en su
|
||||
`app.md` y lanzar:
|
||||
|
||||
```bash
|
||||
./fn run regenerate_app_icons # todas
|
||||
./fn run regenerate_app_icons chart_demo # solo una
|
||||
```
|
||||
|
||||
Convenciones:
|
||||
- **Glyph weight**: `fill` (mas legible a 16px que `regular` o `bold`).
|
||||
- **Color**: 1 accent_hex distinto por app — Tailwind palette 500-700
|
||||
funciona bien (`#0ea5e9` sky-500, `#16a34a` green-600, etc.).
|
||||
- **Padding**: glyph ocupa ~70% del canvas, fondo redondeado al 16% del lado.
|
||||
- **Glyph color**: siempre blanco sobre el fondo accent.
|
||||
|
||||
Si Phosphor no tiene el icono adecuado: buscar en `sources/phosphor-core/assets/fill/`
|
||||
con `ls | grep <keyword>` antes de inventar — 1512 disponibles.
|
||||
|
||||
#### Re-deploy tras cambiar icono
|
||||
|
||||
```bash
|
||||
# 1. Editar icon: en apps/chart_demo/app.md y regenerar
|
||||
./fn run regenerate_app_icons chart_demo
|
||||
# (o ./fn run generate_app_icon "chart-bar" "#0ea5e9" "apps/chart_demo/appicon.ico" para uno suelto sin tocar app.md)
|
||||
|
||||
# 2. Rebuild + redeploy (build dispara windres → nuevo .rsrc)
|
||||
./fn run redeploy_cpp_app_windows chart_demo apps/chart_demo --build
|
||||
```
|
||||
|
||||
Windows cachea iconos en `iconcache.db`. Si el nuevo icono no aparece tras
|
||||
desplegar, refresh con `ie4uinit.exe -show` o reiniciar Explorer.
|
||||
|
||||
#### Runtime attach: taskbar + title bar + Alt+Tab (2026-05-16)
|
||||
|
||||
Embeber `.ico` en el `.exe` (windres) basta para File Explorer / shortcuts —
|
||||
pero GLFW crea su WNDCLASS sin icono, asi que la **barra de tareas**, el
|
||||
**header de la ventana** y **Alt+Tab** muestran el icono GLFW por defecto a
|
||||
menos que adjuntemos el recurso al HWND en runtime.
|
||||
|
||||
`fn::run_app` lo hace automaticamente, sin opt-in. Tras `glfwCreateWindow`:
|
||||
|
||||
```cpp
|
||||
HICON hSmall = LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(101),
|
||||
IMAGE_ICON, GetSystemMetrics(SM_CXSMICON),
|
||||
GetSystemMetrics(SM_CYSMICON), LR_SHARED);
|
||||
HICON hBig = LoadImageW(..., SM_CXICON, SM_CYICON, LR_SHARED);
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmall); // title bar
|
||||
SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hBig); // taskbar
|
||||
SetClassLongPtrW(hwnd, GCLP_HICONSM, (LONG_PTR)hSmall);
|
||||
SetClassLongPtrW(hwnd, GCLP_HICON, (LONG_PTR)hBig);
|
||||
```
|
||||
|
||||
Resource ID `101` lo emite `add_imgui_app` en el `.rc` generado
|
||||
(`101 ICON "<app_dir>/appicon.ico"`). Si la app no tiene `appicon.ico`, el
|
||||
`.rc` no se genera, `LoadImageW` devuelve NULL y el HWND queda con el icono
|
||||
GLFW por defecto (sin error).
|
||||
|
||||
Cobertura multi-viewport: el per-frame scan de `pio.Viewports` (mismo que
|
||||
instala el sizemove subclass) tambien llama `attach_app_icon_to_hwnd` sobre
|
||||
cada HWND secundario nuevo. Floating panels dragged-out heredan el icono
|
||||
sin codigo extra en la app.
|
||||
|
||||
Cache shell: el pipeline `redeploy_cpp_app_windows` llama
|
||||
`refresh_windows_icon_cache_bash_infra` tras copiar el .exe — invoca
|
||||
`ie4uinit.exe -show` para que Explorer recargue `iconcache.db` sin esperar
|
||||
a que detecte el cambio por timestamp. Si Explorer sigue mostrando el
|
||||
icono viejo: borrar `%LOCALAPPDATA%\IconCache.db` + reiniciar Explorer.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# DoD Quality Triada
|
||||
|
||||
**Definition of Done no es un checkbox que se marca a mano. Es un contrato de calidad con 3 capas obligatorias + evidencia ejecutable + uso real >=7 dias.**
|
||||
|
||||
Aplica a todos los `dev/flows/` y, por extension, a issues que cierran capabilities user-facing (`dev/issues/`). El registry mismo (funciones puras, tipos) queda exento: su DoD vive en sus tests unitarios.
|
||||
|
||||
---
|
||||
|
||||
## Por que existe esta regla
|
||||
|
||||
El antipatron a eliminar: "tarea hecha porque pase los tests una vez". Despues:
|
||||
- El flow funciona en `home-wsl` pero falla en `pc-aurgi`.
|
||||
- El error path declarado nunca se ejercito y cuando ocurre en produccion no esta manejado.
|
||||
- El dashboard de observabilidad lleva 30 dias sin abrirse.
|
||||
- El proceso muere cada noche y nadie lo ve hasta que el operador intenta usarlo.
|
||||
- El approval flow se salta porque "para test es mas comodo".
|
||||
|
||||
Resultado: deuda invisible. Cada flow "done" se rompe al primer uso real, el operador pierde confianza en el sistema, y el bucle reactivo no detecta nada porque la telemetria esta verde (los tests sintenticos pasan).
|
||||
|
||||
DoD Quality Triada cambia las reglas: cerrar = probar comportamiento + sobrevivir uso real, no = compilar verde.
|
||||
|
||||
---
|
||||
|
||||
## Las 3 capas
|
||||
|
||||
### Capa 1: Mecanica (pre-requisito, NO es DoD por si misma)
|
||||
|
||||
Compilar verde, tests verdes, indexado limpio, `fn doctor` verde, `uses_functions` sin drift.
|
||||
|
||||
**Regla**: la mecanica NO basta. Es la base para empezar a probar comportamiento. Si te quedas aqui, el flow no esta hecho.
|
||||
|
||||
### Capa 2: Cobertura de comportamiento
|
||||
|
||||
Cada escenario relevante con prueba ejecutable y assert material. NO smoke "el comando no peto". Minimo:
|
||||
|
||||
- **1 golden path** — el caso feliz documentado con assert sobre output concreto.
|
||||
- **>=2 edge cases** — inputs limite, estados raros, condiciones de borde.
|
||||
- **>=1 error path** — fallo provocado intencionalmente, manejado y observable (sin crash, sin silent-fail).
|
||||
|
||||
Formato canonico (tabla en `## Definition of Done` del flow/issue):
|
||||
|
||||
```markdown
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: <desc> | unit / e2e | `<cmd>` | <output concreto> |
|
||||
| Edge 1: <desc> | unit / e2e | `<cmd>` | <comportamiento concreto> |
|
||||
| Error 1: <desc> | e2e | `<cmd que rompe>` | <fallo manejado, no crash> |
|
||||
```
|
||||
|
||||
Cuando aplique, cada fila genera un `e2e_check` en el `app.md` correspondiente (issue 0068). `fn-analizador` los corre periodicamente y deja entry en `e2e_runs`.
|
||||
|
||||
### Capa 3: Vida util validada
|
||||
|
||||
El flow no esta hecho hasta que sobrevive **uso real durante >=7 dias** sin romperse silenciosamente. Cada metrica con umbral medible y dashboard observable.
|
||||
|
||||
Formato canonico:
|
||||
|
||||
```markdown
|
||||
| Metrica | Umbral | Donde se observa | Ventana |
|
||||
|---|---|---|---|
|
||||
| <metrica 1> | `>=N` | `<dashboard URL / app panel>` | 7 dias |
|
||||
| crashes | `0` | `journalctl -u <unit>` | 7 dias |
|
||||
| huecos audit chain | `0` | `cmd: <verify>` | continuo |
|
||||
```
|
||||
|
||||
Reglas:
|
||||
- Metricas NO se auto-reportan; las lee el operador del dashboard real.
|
||||
- Si el dashboard no existe o no se ha abierto en 30 dias, el item se invalida.
|
||||
- Crashes del proceso = 0, huecos en audit = 0, error_rate < umbral declarado.
|
||||
|
||||
### Capa transversal: User-facing reforzado
|
||||
|
||||
- Surface concreta NO BD ni log (UI app, room Matrix, dashboard, archivo en vault).
|
||||
- Usage real: humano usa en su PC, su contexto, >=N veces variadas en >=7 dias.
|
||||
- Variado: >=3 capabilities/casos distintos (no solo "abre dashboard y mira").
|
||||
- Onboarding: parrafo en `## Notas` que explica como usar la cosa sin leer el flow.
|
||||
- Latencia medida (no declarada).
|
||||
|
||||
---
|
||||
|
||||
## Reglas duras para marcar `status: done`
|
||||
|
||||
`/flow done` (y por extension cierres de issues user-facing) DEBE rechazar el cierre si:
|
||||
|
||||
1. Falta cualquiera de las 3 capas (mecanica + cobertura + vida).
|
||||
2. Cobertura tiene <1 golden, <2 edge, o <1 error path con evidencia.
|
||||
3. Vida util tiene tabla vacia o sin dashboard observable real.
|
||||
4. User-facing usage real <7 dias o <N usos declarados.
|
||||
5. Cualquier anti-criterio marcado como cierto.
|
||||
6. `## Notas` sin parrafo onboarding.
|
||||
7. Algun item de DoD sin comando/URL/log query asociado — solo texto.
|
||||
|
||||
Hoy parte de esta validacion es manual (revision humana del operador). La validacion programatica vive en `audit_dod_schema_go_infra` (issue 0114) + `fn doctor dod` y se ampliara hasta cubrir las 3 capas (TBD).
|
||||
|
||||
---
|
||||
|
||||
## Antipatrones (invalidan la DoD aunque los checkboxes esten verdes)
|
||||
|
||||
| Antipatron | Por que es malo | Sustituir por |
|
||||
|---|---|---|
|
||||
| Marcar `done` porque pasa una vez | Tarea "hecha" se rompe al primer uso real | Capa 3: >=7 dias de uso real |
|
||||
| Checkbox sin evidencia ejecutable | DoD se convierte en placebo | Cada item con `cmd:` / URL / log query |
|
||||
| Test que solo verifica camino feliz | El error path es donde se pierden datos | Capa 2: >=1 error path ejercitado |
|
||||
| Observabilidad declarada pero dashboard no abierto en 30 dias | Telemetria muerta = ceguera | Capa 3: dashboard real, operador lo abre |
|
||||
| "Repetible 3 veces consecutivas" con BD efimera | No prueba sobre datos reales acumulados | Capa 3: PC real del operador, datos vivos |
|
||||
| Approval saltado en algun camino | Security gate roto pero invisible | Anti-criterio explicito: `audit_log` lo prueba |
|
||||
| Error path manejado solo "en teoria" | Cuando ocurra en produccion el manejo no existe | Capa 2: entry real en `e2e_runs` o audit |
|
||||
| Solo-en-mi-PC | Falla en otra maquina del operador | Anti-criterio explicito, probar >=2 PCs |
|
||||
| Self-test que retorna `pass` sin asserts materiales | False positive sistemico | Asserts sobre output concreto, no exit-0 |
|
||||
| Silent-fail (proceso muere sin alerta) | Operador no se entera hasta intentar usar | Capa 3: crashes=0 + alerta visible |
|
||||
|
||||
---
|
||||
|
||||
## Relacion con otras reglas
|
||||
|
||||
- [[e2e_validation]] — los escenarios de Capa 2 cuando aplican a apps se materializan como `e2e_checks` en `app.md`. `fn-analizador` (fase 4 del bucle reactivo) los corre.
|
||||
- [[registry_calls]] — la evidencia de uso (`call_monitor.calls`) alimenta los umbrales de Capa 3.
|
||||
- [[function_growth_and_self_docs]] — cada funcion del registry tiene su propio contrato self-doc (Ejemplo + Cuando usarla + Gotchas). DoD del flow NO sustituye al self-doc de la funcion; lo complementa para el nivel sistema.
|
||||
- [[autonomous_loop]] — `fn-orquestador` autonomo NO puede marcar `done` sin que se cumplan las 3 capas. Su criterio de convergencia incluye DoD Quality.
|
||||
- [[apps_tbd]] — TBD garantiza master desplegable; DoD garantiza que lo desplegado funciona en uso real.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
1. **Mecanica** = compilar verde (pre-requisito, NO suficiente).
|
||||
2. **Cobertura** = golden + >=2 edge + >=1 error path con evidencia ejecutable.
|
||||
3. **Vida util** = >=7 dias de uso real sin romper silenciosamente, dashboard observable abierto.
|
||||
4. **User-facing reforzado** = humano usa en PC real, >=N veces variadas.
|
||||
5. **Anti-criterios** invalidan la DoD aunque todo este verde.
|
||||
6. Sin evidencia ejecutable (cmd/URL/log), NO es DoD: es deseo.
|
||||
@@ -20,10 +20,18 @@ fn doctor sync # Solo drift pc_locations BD vs disco local
|
||||
fn doctor uses-functions # Solo audit imports reales vs uses_functions
|
||||
fn doctor unused # Solo funciones huerfanas del registry
|
||||
fn doctor cpp-apps # Conformidad C++ con cpp/PATTERNS.md (cfg.about/log, no app_menubar manual, no DockSpace duplicado)
|
||||
# + check BeginTable inline: CANDIDATE (no migrado) / MIXED (parcial) / silencio (limpio)
|
||||
|
||||
fn doctor --json # Salida JSON (cualquier subcomando) — para agentes/scripts
|
||||
```
|
||||
|
||||
`fn doctor cpp-apps` produce dos secciones:
|
||||
1. Conformance (cfg.about/log, fn::run_app, menubar, DockSpace) — una fila por app imgui.
|
||||
2. BeginTable migration (issue 0081) — solo apps con `ImGui::BeginTable` inline:
|
||||
- `CANDIDATE`: N tablas inline sin `data_table_cpp_viz` en uses_functions. Considerar migracion.
|
||||
- `MIXED`: N tablas inline con `data_table_cpp_viz` ya declarado. Migracion parcial OK.
|
||||
- silencio: 0 BeginTable inline (limpio o completamente migrado).
|
||||
|
||||
### Mapeo subcomando → funcion del registry
|
||||
|
||||
| Subcomando | Funcion |
|
||||
@@ -33,7 +41,8 @@ fn doctor --json # Salida JSON (cualquier subcomando) — para agentes
|
||||
| `sync` | `pc_locations_drift_go_infra` |
|
||||
| `uses-functions` | `audit_uses_functions_go_infra` |
|
||||
| `unused` | `find_unused_functions_go_infra` |
|
||||
| `cpp-apps` | `audit_cpp_apps_go_infra` |
|
||||
| `cpp-apps` (conformance) | `audit_cpp_apps_go_infra` |
|
||||
| `cpp-apps` (table migration) | `audit_cpp_table_migration_go_infra` (inline en `audit_cpp_apps.go`) |
|
||||
|
||||
Cada subcomando es un wrapper fino. Toda la logica vive en la funcion. Si quieres usar la salida en otro programa Go, importa la funcion directamente.
|
||||
|
||||
@@ -64,6 +73,8 @@ Texto humano por defecto (tabwriter). `--json` produce array/objeto serializable
|
||||
| `manual_DockSpaceOverViewport_*` | Borrar la llamada o setear `cfg.auto_dockspace = false` si la app gestiona docking propio |
|
||||
| `missing_cfg_about` / `missing_cfg_log` | Anadir `cfg.about = {...}` / `cfg.log = {"<name>.log", 1}` antes de `fn::run_app` |
|
||||
| `app.md_missing_*` | Regenerar via plantilla del scaffolder (`/new-cpp-app`) o anadir campos a mano |
|
||||
| cpp-apps BeginTable `CANDIDATE` | App tiene N `ImGui::BeginTable` sin migrar. Abrir rama TBD, reemplazar tablas por `data_table::render()` via `fn_table_viz`, añadir `data_table_cpp_viz` a `uses_functions` en `app.md` |
|
||||
| cpp-apps BeginTable `MIXED` | Migracion parcial en curso. Continuar wave por wave hasta que no queden BeginTable inline |
|
||||
| Backup viejo | `backup_all_bash_pipelines ~/backups/fn_registry` |
|
||||
|
||||
### Para agentes
|
||||
|
||||
@@ -28,3 +28,26 @@ Documentar en el `app.md` del service:
|
||||
- El puerto que usa (si expone HTTP/gRPC)
|
||||
- Como lanzarlo y pararlo
|
||||
- Como comprobar que esta vivo (health check)
|
||||
|
||||
### Bloque `service:` obligatorio (issue 0105)
|
||||
|
||||
Toda app con `tag: service` declara el bloque `service:` en su frontmatter. El indexer lo persiste en columnas dedicadas de `apps` + tabla `service_targets`. Consumido por `services_api`/`services_monitor` (issue 0106) y por `fn doctor services-spec`.
|
||||
|
||||
```yaml
|
||||
service:
|
||||
port: 8484 # null si no expone HTTP (stdio, daemon sin API)
|
||||
health_endpoint: /api/databases # ruta GET, 2xx/3xx = sano; null si no aplica
|
||||
health_timeout_s: 3
|
||||
systemd_unit: sqlite_api.service # obligatorio si runtime empieza con `systemd-`
|
||||
systemd_scope: user # user|system|null (docker-compose)
|
||||
restart_policy: always # always|on-failure|none
|
||||
runtime: systemd-user # systemd-user|systemd-system|docker-compose|stdio|manual
|
||||
pc_targets: # >=1, pc_id de pc_locations
|
||||
- aurgi-pc
|
||||
- home-wsl
|
||||
is_local_only: false # true => no se monitoriza por SSH (siempre local)
|
||||
```
|
||||
|
||||
Validacion: `fn doctor services-spec` (`functions/infra/audit_services_spec.go`). Hoy 11/11 services con bloque completo.
|
||||
|
||||
**Gotcha critico:** usar `Restart=always` (no `on-failure`) en el unit systemd. Un `SIGTERM` limpio es exit success → `on-failure` NO reinicia y el service se queda muerto silenciosamente. `sqlite_api.service` cayo 20h asi el 2026-05-17.
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
IDs siguen el formato `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`).
|
||||
## ids_naming — formato predictible
|
||||
|
||||
Nombres de funciones en snake_case. Tipos en PascalCase para Go.
|
||||
IDs: `{name}_{lang}_{domain}` (ej: `filter_slice_go_core`). Predictibilidad alta -> Claude descubre por fuzzy match sin lookup. Issue 0087.
|
||||
|
||||
### Reglas
|
||||
|
||||
1. **snake_case**: `[a-z0-9_]+`. Nada de PascalCase, kebab-case, dot.notation.
|
||||
2. **Verbo obligatorio**: al menos un token del `name` debe ser un verbo de accion. El verbo puede ir delante (`get_user`) o detras (`user_lookup`). Ejemplos validos: `filter_slice`, `bank_login`, `metabase_get_dashboard`, `redeploy_cpp_app`. Invalidos: `slice` (sustantivo solo), `user` (sustantivo solo), `data` (sustantivo solo).
|
||||
3. **Dominio canonico**: el `domain` debe estar en la lista canonica (ver `mcp__registry__fn_list_domains`). Crear dominio nuevo solo si el bucket es claramente distinto y se anade en el mismo turno a CLAUDE.md.
|
||||
4. **Tipos en PascalCase Go**: `ResultGoCore`, `ErrorGoCore`. Aplica solo al codigo Go; el ID en el registry sigue siendo snake_case (`result_go_core`).
|
||||
|
||||
### Verbos canonicos (allowlist)
|
||||
|
||||
Lista no exhaustiva pero cubre la mayoria. Anadir aqui (y al validator en `apps/registry_mcp/naming.go`) cuando se introduzca un verbo nuevo recurrente.
|
||||
|
||||
`get, set, list, find, search, show, read, load, fetch, scan, query, lookup, parse, format, encode, decode, marshal, unmarshal, serialize, deserialize, validate, check, ensure, verify, audit, diagnose, test, match, filter, map, reduce, sort, group, count, sum, aggregate, compute, calculate, score, rank, cluster, classify, detect, init, create, make, build, generate, scaffold, install, setup, configure, register, add, insert, append, prepend, update, upsert, modify, edit, patch, replace, delete, remove, clear, drop, prune, clean, copy, move, rename, sync, clone, extract, inject, import, export, send, post, put, call, dispatch, exec, run, launch, start, stop, kill, restart, redeploy, deploy, open, close, connect, disconnect, login, logout, authenticate, enable, disable, toggle, lock, unlock, propose, promote, deprecate, approve, reject, emit, render, draw, paint, serve, host, pull, push, checkout, commit, tag, merge, rebase, watch, monitor, observe, log, trace, profile, benchmark, snapshot, backup, restore, archive, compress, decompress, hash, encrypt, decrypt, sign, taskkill, recopile, vault, propose, apply, gather, collect, fold, head, tail, take, drop, slice, chunk, batch, debounce, throttle, retry, await, sleep, ping, kill, prime, warm, refresh, invalidate, reload, reset, rollback, fork, spawn, daemon, observe, plot, draw, capture, replay, recopilate`
|
||||
|
||||
### Excepciones
|
||||
|
||||
- **Operadores matematicos/estadisticos** ampliamente reconocidos por acronimo: `sma`, `ema`, `rsi`, `vwap`, `adx`. Validator hace allowlist explicita.
|
||||
- **Tipos** (entity_type `type`): no requieren verbo. Validator lo salta cuando `kind=type`.
|
||||
- **Components** (`kind: component`): nombre describe artefacto UI (`button_primary`, `chat_panel`). Permite forma `<noun>_<modifier>`. Validator salta el check de verbo si `kind=component`.
|
||||
|
||||
### Validator
|
||||
|
||||
`mcp__registry__fn_create_function` ejecuta el validator antes de escribir archivos. Rechaza con error si:
|
||||
- name no es snake_case.
|
||||
- name no contiene verbo (excepto component/type).
|
||||
- domain no esta en lista canonica.
|
||||
|
||||
Error tipico:
|
||||
```
|
||||
naming: name "slice" lacks action verb. Add verb prefix/suffix (e.g. filter_slice, slice_window). See .claude/rules/ids_naming.md.
|
||||
naming: domain "bizops" not in canonical list (core, infra, finance, ...). Add it to CLAUDE.md and rules first.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
## Slash commands por project (namespaced)
|
||||
|
||||
Cada `projects/<p>/` puede tener su propio `.claude/commands/*.md`. Para invocarlos desde la raiz de `fn_registry` sin que pisen los comandos globales, se exponen via **symlink namespaced** en `fn_registry/.claude/commands/<project>/`.
|
||||
|
||||
### Patron canonico
|
||||
|
||||
```
|
||||
projects/aurgi/.claude/commands/foo.md # archivo real (viaja con el sub-repo del project)
|
||||
fn_registry/.claude/commands/aurgi -> symlink -> ../../projects/aurgi/.claude/commands
|
||||
```
|
||||
|
||||
Resultado:
|
||||
|
||||
| cwd | Invocacion |
|
||||
|---|---|
|
||||
| `cd projects/aurgi && claude` | `/foo` (sin namespace) |
|
||||
| `cd fn_registry && claude` | `/aurgi:foo` (namespaced, no colisiona con `/foo` global) |
|
||||
|
||||
Subdirs dentro de `.claude/commands/` se exponen como namespace en el slash command. Por eso `aurgi/foo.md` -> `/aurgi:foo`.
|
||||
|
||||
### Como anadir un project nuevo
|
||||
|
||||
1. `mkdir -p projects/<p>/.claude/commands/`.
|
||||
2. Crear `<comando>.md` con frontmatter `description:` + cuerpo.
|
||||
3. Symlink: `ln -sf ../../projects/<p>/.claude/commands /home/egutierrez/fn_registry/.claude/commands/<p>`.
|
||||
4. Versionar el `.claude/commands/` del project en su propio sub-repo (NO en fn_registry — projects estan gitignored).
|
||||
5. Versionar SOLO el symlink en fn_registry (`git add .claude/commands/<p>`).
|
||||
|
||||
### Reglas
|
||||
|
||||
- Cada project mantiene autonomia: sus commands viajan con el sub-repo y funcionan tanto en `cd projects/<p>` como desde la raiz.
|
||||
- El symlink en fn_registry da acceso global con namespace — sin colision con commands del registry.
|
||||
- NO duplicar contenido: archivo real solo en `projects/<p>/.claude/commands/`. fn_registry solo guarda el symlink.
|
||||
- Si el project se mueve/elimina, borrar el symlink en fn_registry.
|
||||
|
||||
### Listado actual
|
||||
|
||||
| Project | Symlink | Commands disponibles desde fn_registry |
|
||||
|---|---|---|
|
||||
| aurgi | `.claude/commands/aurgi` | `/aurgi:aumentar_task`, `/aurgi:contexto_aurgi`, `/aurgi:anadir_contexto_aurgi` |
|
||||
|
||||
Anadir filas aqui al introducir un project nuevo con commands.
|
||||
|
||||
### Catalogo dinamico
|
||||
|
||||
Para listado en tiempo real (sin tener que actualizar esta tabla a mano): `/commands` escanea `.claude/commands/` recursivo y agrupa por namespace. Filtros: `/commands <substring>`, `/commands --ns <ns>`, `/commands --json`.
|
||||
|
||||
### Gotchas
|
||||
|
||||
- Claude Code lista los commands disponibles al inicio de sesion. Si un symlink apunta a un directorio inexistente, los commands no aparecen — verificar con `ls -L .claude/commands/<project>/`.
|
||||
- El namespace usa el nombre del subdirectorio (`aurgi/`), no del project en `projects/`. Mantenerlos iguales para evitar confusion.
|
||||
- Los commands del project se ejecutan con el cwd de la sesion actual. Un `/aurgi:aumentar_task` invocado desde `fn_registry/` corre con cwd `fn_registry/` — paths relativos en el `.md` deben asumir esto (siempre usar paths relativos al repo, ej. `projects/aurgi/vaults/...`).
|
||||
@@ -140,7 +140,7 @@ Cobertura por capa, no todas activas a la vez:
|
||||
### Que NO se monitoriza
|
||||
|
||||
- Funcion Go/C++ llamada internamente por app ya compilada.
|
||||
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
|
||||
- Funcion ejecutada por systemd timer / cron / dag_engine **step `command:`** (no `function:`) sin pasar por `fn run`. Nota: dag_engine steps con `function:` SI quedan trazados — el executor invoca `fn run <id>` y guarda `function_id` en `dag_step_results`.
|
||||
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
|
||||
- Service de produccion recibiendo HTTP.
|
||||
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# Append a one-liner [[fn_id]] — purpose to MEMORY.md after fn-constructor
|
||||
# creates a new registry function. Idempotent: skips if id already present.
|
||||
# Used by /fn_claude step 5b (issue 0087, pieza 6).
|
||||
#
|
||||
# Usage: append_fn_to_memory.sh <fn_id> "<one-line purpose>"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FN_ID="${1:-}"
|
||||
PURPOSE="${2:-}"
|
||||
|
||||
if [ -z "$FN_ID" ] || [ -z "$PURPOSE" ]; then
|
||||
echo "usage: append_fn_to_memory.sh <fn_id> <purpose>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
MEM_DIR="${CLAUDE_MEMORY_DIR:-/home/lucas/.claude/projects/-home-lucas-fn-registry/memory}"
|
||||
MEM_FILE="$MEM_DIR/MEMORY.md"
|
||||
|
||||
[ -d "$MEM_DIR" ] || { echo "memory dir missing: $MEM_DIR" >&2; exit 1; }
|
||||
[ -f "$MEM_FILE" ] || { echo "MEMORY.md missing: $MEM_FILE" >&2; exit 1; }
|
||||
|
||||
# Per-function reference file slug
|
||||
SLUG="reference_fn_${FN_ID}.md"
|
||||
REF_FILE="$MEM_DIR/$SLUG"
|
||||
|
||||
# Idempotency: if already linked in MEMORY.md, exit 0
|
||||
if grep -qF "[fn-$FN_ID]" "$MEM_FILE" 2>/dev/null; then
|
||||
echo "already in MEMORY.md: $FN_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Create reference memory file
|
||||
cat > "$REF_FILE" <<EOF
|
||||
---
|
||||
name: fn-$FN_ID
|
||||
description: Registry function $FN_ID — $PURPOSE
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
|
||||
Registry function: \`$FN_ID\`
|
||||
|
||||
$PURPOSE
|
||||
|
||||
Invoke via \`./fn run $FN_ID [args]\` or \`mcp__registry__fn_run id="$FN_ID"\`. Inspect with \`mcp__registry__fn_show id="$FN_ID"\` / \`mcp__registry__fn_code id="$FN_ID"\`.
|
||||
EOF
|
||||
|
||||
# 2. Append index line to MEMORY.md
|
||||
printf -- '- [%s](%s) — %s\n' "fn-$FN_ID" "$SLUG" "$PURPOSE" >> "$MEM_FILE"
|
||||
|
||||
echo "appended: $FN_ID -> $MEM_FILE"
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# integrate-worktrees.sh — Integra branches de worktrees a master con --no-ff
|
||||
#
|
||||
# Uso: ./integrate-worktrees.sh <slug-1> <slug-2> ...
|
||||
# Ejemplo: ./integrate-worktrees.sh 0026-split-runtime 0027-prune-config-schema
|
||||
#
|
||||
# Para cada slug:
|
||||
# 1. git merge --no-ff issue/<slug> a master
|
||||
# 2. Verificar que master compila después del merge
|
||||
# 3. Si hay conflict o fallo de build, PARAR inmediatamente
|
||||
#
|
||||
# Los slugs deben pasarse en el orden correcto (waves ya resueltas).
|
||||
# NO hace push — eso lo decide el usuario.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ERROR: se necesita al menos un slug"
|
||||
echo "Uso: $0 <slug-1> <slug-2> ..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Asegurar que estamos en master
|
||||
echo "=== Cambiando a master ==="
|
||||
cd "$REPO_ROOT"
|
||||
git checkout master
|
||||
|
||||
MERGED=0
|
||||
FAILED_AT=""
|
||||
|
||||
for slug in "$@"; do
|
||||
branch="issue/${slug}"
|
||||
|
||||
echo ""
|
||||
echo "=== Integrando: ${branch} ==="
|
||||
|
||||
# Verificar que la branch existe
|
||||
if ! git show-ref --verify --quiet "refs/heads/${branch}"; then
|
||||
echo "FAIL: branch ${branch} no existe"
|
||||
FAILED_AT="$slug"
|
||||
break
|
||||
fi
|
||||
|
||||
# Merge --no-ff
|
||||
if ! git merge --no-ff "$branch" -m "merge: ${branch} — implementación paralela"; then
|
||||
echo ""
|
||||
echo "CONFLICT: merge de ${branch} tiene conflictos"
|
||||
echo "Resolver manualmente y luego continuar con los slugs restantes"
|
||||
echo ""
|
||||
echo "Para resolver:"
|
||||
echo " 1. git status (ver archivos en conflicto)"
|
||||
echo " 2. Resolver conflictos en cada archivo"
|
||||
echo " 3. git add <archivos>"
|
||||
echo " 4. git commit"
|
||||
echo ""
|
||||
echo "Slugs pendientes después de ${slug}:"
|
||||
FOUND=0
|
||||
for remaining in "$@"; do
|
||||
if [ "$FOUND" -eq 1 ]; then
|
||||
echo " - ${remaining}"
|
||||
fi
|
||||
if [ "$remaining" = "$slug" ]; then
|
||||
FOUND=1
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "MERGED: ${branch}"
|
||||
|
||||
# Verificar que master sigue compilando (si BUILD_CMD esta definido)
|
||||
if [ -n "${BUILD_CMD:-}" ]; then
|
||||
echo "--- Verificando build post-merge ($BUILD_CMD) ---"
|
||||
if ! (cd "$REPO_ROOT" && bash -c "$BUILD_CMD" 2>&1); then
|
||||
echo ""
|
||||
echo "FAIL: master no compila despues de mergear ${branch}"
|
||||
echo "Revertir con: git reset --hard HEAD~1"
|
||||
echo "Investigar el problema antes de continuar."
|
||||
FAILED_AT="$slug"
|
||||
break
|
||||
fi
|
||||
echo "OK: build post-merge exitoso"
|
||||
else
|
||||
echo "--- Build post-merge SKIPPED (BUILD_CMD no definido) ---"
|
||||
fi
|
||||
|
||||
MERGED=$((MERGED + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen de integración ==="
|
||||
echo "Mergeados: ${MERGED} de $#"
|
||||
|
||||
if [ -n "$FAILED_AT" ]; then
|
||||
echo "Falló en: ${FAILED_AT}"
|
||||
echo ""
|
||||
echo "Worktrees NO limpiados (resolver primero el fallo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Limpieza de worktrees y branches
|
||||
echo ""
|
||||
echo "=== Limpieza ==="
|
||||
for slug in "$@"; do
|
||||
path="${REPO_ROOT}/worktrees/${slug}"
|
||||
branch="issue/${slug}"
|
||||
|
||||
if [ -d "$path" ]; then
|
||||
git worktree remove "$path" 2>/dev/null && echo "REMOVED: worktree ${path}" || echo "WARN: no se pudo eliminar worktree ${path}"
|
||||
fi
|
||||
|
||||
git branch -d "$branch" 2>/dev/null && echo "DELETED: branch ${branch}" || echo "WARN: no se pudo eliminar branch ${branch}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Integración completa ==="
|
||||
echo "Master tiene ${MERGED} merges nuevos."
|
||||
echo ""
|
||||
echo "Para publicar: git push"
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# setup-worktrees.sh — Crea git worktrees para ejecución paralela de issues
|
||||
#
|
||||
# Uso: ./setup-worktrees.sh <slug-1> <slug-2> ...
|
||||
# Ejemplo: ./setup-worktrees.sh 0026-split-runtime 0027-prune-config-schema
|
||||
#
|
||||
# Cada slug genera:
|
||||
# worktrees/<slug>/ (worktree completo)
|
||||
# branch: issue/<slug>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKTREE_DIR="${REPO_ROOT}/worktrees"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ERROR: se necesita al menos un slug de issue"
|
||||
echo "Uso: $0 <slug-1> <slug-2> ..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verificar master (NO pull --rebase: rompe merges locales convirtiendolos
|
||||
# en cherry-picks contra origin/master viejo). Detectado 2026-05-18.
|
||||
echo "=== Verificando master ==="
|
||||
CURRENT_BRANCH="$(git branch --show-current)"
|
||||
if [ "$CURRENT_BRANCH" != "master" ] && [ -n "$CURRENT_BRANCH" ]; then
|
||||
echo "WARN: estas en branch '${CURRENT_BRANCH}', no master. Worktrees nuevos saldran de master ref de todos modos."
|
||||
fi
|
||||
# NO auto-pull. Usuario decide sync con remote.
|
||||
|
||||
mkdir -p "$WORKTREE_DIR"
|
||||
|
||||
CREATED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
|
||||
for slug in "$@"; do
|
||||
branch="issue/${slug}"
|
||||
path="${WORKTREE_DIR}/${slug}"
|
||||
|
||||
if [ -d "$path" ]; then
|
||||
echo "SKIP: worktree ya existe: ${path}"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Verificar que la branch no existe ya
|
||||
if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
|
||||
echo "WARN: branch ${branch} ya existe, creando worktree desde ella"
|
||||
git worktree add "$path" "$branch" 2>/dev/null || {
|
||||
echo "FAIL: no se pudo crear worktree para ${slug}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
}
|
||||
else
|
||||
echo "CREATE: worktree ${path} (branch ${branch})"
|
||||
git worktree add -b "$branch" "$path" master 2>/dev/null || {
|
||||
echo "FAIL: no se pudo crear worktree para ${slug}"
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
}
|
||||
fi
|
||||
|
||||
CREATED=$((CREATED + 1))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Resumen ==="
|
||||
echo "Creados: ${CREATED}"
|
||||
echo "Existentes: ${SKIPPED}"
|
||||
echo "Fallidos: ${FAILED}"
|
||||
echo ""
|
||||
echo "=== Worktrees activos ==="
|
||||
git worktree list
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/bin/bash
|
||||
# verify-worktree.sh — Verifica build, tests y cierre de issue en un worktree.
|
||||
#
|
||||
# Uso:
|
||||
# ./verify-worktree.sh <worktree-path> [build-cmd] [test-cmd]
|
||||
#
|
||||
# Ejemplos:
|
||||
# ./verify-worktree.sh worktrees/0026-foo
|
||||
# ./verify-worktree.sh worktrees/0026-foo "go build -tags fts5 ./..." "go test -tags fts5 ./..."
|
||||
# BUILD_CMD="cmake --build cpp/build" TEST_CMD="ctest --test-dir cpp/build" ./verify-worktree.sh worktrees/0026-foo
|
||||
#
|
||||
# Resolucion de comandos (en orden de prioridad):
|
||||
# 1. Argumentos posicionales (build-cmd, test-cmd)
|
||||
# 2. Variables de entorno BUILD_CMD / TEST_CMD
|
||||
# 3. Archivo .parallel-fix-issues.yml en la raiz del worktree (claves: build, test)
|
||||
# 4. Auto-deteccion segun ficheros del proyecto:
|
||||
# - go.mod → "go build ./..." + "go test ./..."
|
||||
# - CMakeLists.txt → "cmake -S . -B build && cmake --build build" + "ctest --test-dir build"
|
||||
# - Cargo.toml → "cargo build" + "cargo test"
|
||||
# - package.json → "npm run build" + "npm test"
|
||||
# - pyproject.toml → "" + "pytest"
|
||||
# 5. Si nada se detecta, salta build/test con WARN.
|
||||
#
|
||||
# Auto-deteccion adicional: si hay go.mod, intenta extraer build tag de //go:build.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = todo OK
|
||||
# 1 = error de argumento
|
||||
# 2 = build fallo
|
||||
# 3 = tests fallaron
|
||||
# 4 = issue no cerrado (solo WARN, no falla)
|
||||
# 5 = sin commits propios
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "ERROR: se necesita el path del worktree"
|
||||
echo "Uso: $0 <worktree-path> [build-cmd] [test-cmd]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKTREE="$1"
|
||||
ARG_BUILD_CMD="${2:-}"
|
||||
ARG_TEST_CMD="${3:-}"
|
||||
|
||||
# Resolver path absoluto
|
||||
if [[ "$WORKTREE" != /* ]]; then
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WORKTREE="${REPO_ROOT}/${WORKTREE}"
|
||||
fi
|
||||
|
||||
if [ ! -d "$WORKTREE" ]; then
|
||||
echo "ERROR: worktree no encontrado: ${WORKTREE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLUG="$(basename "$WORKTREE")"
|
||||
echo "=== Verificando: ${SLUG} ==="
|
||||
|
||||
# --- Resolver build/test commands ---
|
||||
BUILD_CMD="${ARG_BUILD_CMD:-${BUILD_CMD:-}}"
|
||||
TEST_CMD="${ARG_TEST_CMD:-${TEST_CMD:-}}"
|
||||
|
||||
# Manifest opcional
|
||||
MANIFEST="${WORKTREE}/.parallel-fix-issues.yml"
|
||||
if [ -z "$BUILD_CMD" ] && [ -f "$MANIFEST" ]; then
|
||||
M_BUILD=$(grep -E "^build:" "$MANIFEST" 2>/dev/null | sed -E 's/^build:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
|
||||
if [ -n "$M_BUILD" ]; then BUILD_CMD="$M_BUILD"; echo "INFO: build desde manifest"; fi
|
||||
fi
|
||||
if [ -z "$TEST_CMD" ] && [ -f "$MANIFEST" ]; then
|
||||
M_TEST=$(grep -E "^test:" "$MANIFEST" 2>/dev/null | sed -E 's/^test:[[:space:]]*"?([^"]*)"?[[:space:]]*$/\1/' | head -1 || true)
|
||||
if [ -n "$M_TEST" ]; then TEST_CMD="$M_TEST"; echo "INFO: test desde manifest"; fi
|
||||
fi
|
||||
|
||||
# Auto-deteccion
|
||||
if [ -z "$BUILD_CMD" ] || [ -z "$TEST_CMD" ]; then
|
||||
AUTO_BUILD=""
|
||||
AUTO_TEST=""
|
||||
if [ -f "${WORKTREE}/go.mod" ]; then
|
||||
# Detectar build tag
|
||||
AUTO_TAG=$(grep -rh "^//go:build " --include="*.go" "$WORKTREE" 2>/dev/null \
|
||||
| sed -E 's|^//go:build ([a-zA-Z0-9_]+).*|\1|' \
|
||||
| sort -u | head -1 || true)
|
||||
TAG_FLAG=""
|
||||
[ -n "$AUTO_TAG" ] && TAG_FLAG="-tags $AUTO_TAG"
|
||||
AUTO_BUILD="go build $TAG_FLAG ./..."
|
||||
AUTO_TEST="go test $TAG_FLAG ./..."
|
||||
echo "INFO: stack detectado: Go${TAG_FLAG:+ ($TAG_FLAG)}"
|
||||
elif [ -f "${WORKTREE}/CMakeLists.txt" ] || ls "${WORKTREE}"/cpp/CMakeLists.txt >/dev/null 2>&1; then
|
||||
CMAKE_DIR="."
|
||||
[ -f "${WORKTREE}/cpp/CMakeLists.txt" ] && [ ! -f "${WORKTREE}/CMakeLists.txt" ] && CMAKE_DIR="cpp"
|
||||
AUTO_BUILD="cmake -S ${CMAKE_DIR} -B ${CMAKE_DIR}/build -DCMAKE_BUILD_TYPE=Release && cmake --build ${CMAKE_DIR}/build -j"
|
||||
AUTO_TEST="ctest --test-dir ${CMAKE_DIR}/build --output-on-failure || true"
|
||||
echo "INFO: stack detectado: C++/CMake (dir=${CMAKE_DIR})"
|
||||
elif [ -f "${WORKTREE}/Cargo.toml" ]; then
|
||||
AUTO_BUILD="cargo build"
|
||||
AUTO_TEST="cargo test"
|
||||
echo "INFO: stack detectado: Rust"
|
||||
elif [ -f "${WORKTREE}/package.json" ]; then
|
||||
AUTO_BUILD="npm run build --if-present"
|
||||
AUTO_TEST="npm test --if-present"
|
||||
echo "INFO: stack detectado: Node"
|
||||
elif [ -f "${WORKTREE}/pyproject.toml" ] || [ -f "${WORKTREE}/setup.py" ]; then
|
||||
AUTO_BUILD="" # python normalmente no tiene build step
|
||||
AUTO_TEST="pytest"
|
||||
echo "INFO: stack detectado: Python"
|
||||
else
|
||||
echo "WARN: no se detecto stack; usar BUILD_CMD/TEST_CMD env o manifest .parallel-fix-issues.yml"
|
||||
fi
|
||||
[ -z "$BUILD_CMD" ] && BUILD_CMD="$AUTO_BUILD"
|
||||
[ -z "$TEST_CMD" ] && TEST_CMD="$AUTO_TEST"
|
||||
fi
|
||||
|
||||
# 1. Verificar commits propios
|
||||
echo ""
|
||||
echo "--- Commits propios ---"
|
||||
COMMIT_COUNT=$(cd "$WORKTREE" && git log master..HEAD --oneline 2>/dev/null | wc -l)
|
||||
if [ "$COMMIT_COUNT" -eq 0 ]; then
|
||||
echo "FAIL: sin commits propios en la branch"
|
||||
exit 5
|
||||
fi
|
||||
echo "OK: ${COMMIT_COUNT} commits desde master"
|
||||
cd "$WORKTREE" && git log master..HEAD --oneline
|
||||
|
||||
# 2. Build
|
||||
echo ""
|
||||
if [ -n "$BUILD_CMD" ]; then
|
||||
echo "--- Build ($BUILD_CMD) ---"
|
||||
if (cd "$WORKTREE" && bash -c "$BUILD_CMD" 2>&1); then
|
||||
echo "OK: build exitoso"
|
||||
else
|
||||
echo "FAIL: build fallo"
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo "--- Build SKIPPED (sin comando) ---"
|
||||
fi
|
||||
|
||||
# 3. Tests
|
||||
echo ""
|
||||
if [ -n "$TEST_CMD" ]; then
|
||||
echo "--- Tests ($TEST_CMD) ---"
|
||||
if (cd "$WORKTREE" && bash -c "$TEST_CMD" 2>&1); then
|
||||
echo "OK: tests pasaron"
|
||||
else
|
||||
echo "FAIL: tests fallaron"
|
||||
exit 3
|
||||
fi
|
||||
else
|
||||
echo "--- Tests SKIPPED (sin comando) ---"
|
||||
fi
|
||||
|
||||
# 4. Issue cerrado
|
||||
echo ""
|
||||
echo "--- Cierre de issue ---"
|
||||
COMPLETED_FILES=$(cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/ 2>/dev/null | wc -l)
|
||||
if [ "$COMPLETED_FILES" -gt 0 ]; then
|
||||
echo "OK: issue movido a completed/"
|
||||
cd "$WORKTREE" && git diff --name-only master -- dev/issues/completed/
|
||||
else
|
||||
echo "WARN: no se detecto issue movido a completed/ (verificar manualmente)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== RESULTADO: ${SLUG} — OK ==="
|
||||
@@ -81,3 +81,10 @@ broken_paths.txt
|
||||
imgui.ini
|
||||
prompts/
|
||||
kotlin/functions/ui/
|
||||
|
||||
# Module versioning auto-generated headers (written by `fn index`, issue 0097)
|
||||
**/version_generated.h
|
||||
**/app_modules_generated.h
|
||||
|
||||
# Issue migration backups (0100)
|
||||
dev/issues/.backup_pre_*
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
"registry": {
|
||||
"command": "./apps/registry_mcp/registry_mcp",
|
||||
"args": ["--enable-run", "--enable-write"]
|
||||
},
|
||||
"jupyter": {
|
||||
"command": "bash",
|
||||
"args": ["/home/enmanuel/fn_registry/bash/functions/infra/jupyter_mcp_serve.sh"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,68 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## 2026-05-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Bloque `service:` en frontmatter de `app.md`** (issue 0105) — toda app con `tag: service` declara ahora `port`, `health_endpoint`, `health_timeout_s`, `systemd_unit`, `systemd_scope`, `restart_policy`, `runtime` (`systemd-user|systemd-system|docker-compose|stdio|manual`), `pc_targets[]`, `is_local_only`. 11 apps actualizadas: `sqlite_api`, `dag_engine`, `call_monitor`, `kanban`, `deploy_server`, `registry_mcp`, `registry_api`, `footprint_geo_stack`, `element_matrix_chat`, `agents_and_robots`, `services_api`.
|
||||
- **Migration `014_service_metadata.sql`** — anade 8 columnas (`service_port`, `service_health_endpoint`, `service_health_timeout_s`, `service_systemd_unit`, `service_systemd_scope`, `service_restart_policy`, `service_runtime`, `service_is_local_only`) a `apps` + tabla nueva `service_targets (app_id, pc_id, role)` con indices por `app_id` y `pc_id`.
|
||||
- **`registry.App.Service *ServiceSpec`** + parser `rawService` + escritura/lectura en `InsertApp`/`scanApps`/`Purge` (preserva `service_targets`). API publica `db.GetServicePCTargets(appID) []string`.
|
||||
- **`audit_services_spec_go_infra`** (`functions/infra/audit_services_spec.{go,md}`) — audita apps `tag: service` y reporta drift del bloque `service:` (runtime allowlist, pc_targets >=1, systemd_unit obligatorio si `runtime` empieza con `systemd-`, restart_policy en `always|on-failure|none`).
|
||||
- **`fn doctor services-spec`** — subcomando nuevo en `cmd/fn/doctor.go`. Salida tabwriter + `--json`. Hoy: `11/11 services with complete service: block`.
|
||||
- **App `services_api`** (`apps/services_api/`, issue 0106) — Go HTTP daemon en `127.0.0.1:8485`. Loop paralelo cada 15s (max 8 in-flight, timeout 20s/probe) que reconcilia esperado vs real para cada `(app, pc)` cruzado de `service_targets`. Probes locales (`systemctl is-active` + TCP dial + `http.Client`) o remotos (`ssh_exec_go_infra`). Persiste en `operations.db`: `service_state` (snapshot actual) + `service_transition` (cambios de overall append-only). Endpoints `GET /api/health`, `GET /api/services`, `POST /api/check`, `GET /api/pcs`. systemd unit `~/.config/systemd/user/services_api.service` con `Restart=always`.
|
||||
- **App `services_monitor`** (`apps/services_monitor/`, issue 0106) — frontend C++ ImGui. Polling auto cada 5s configurable + boton "Force check" (POST `/api/check`). Tabla 9-col agrupada por app: overall pill, systemd state, port + listening flag (`TI_PLUG`/`TI_PLUG_CONNECTED`), HTTP status+latency, runtime, last change age, error/note. JSON via `vendor/nlohmann/json.hpp` (copiado de data_factory). HTTP socket TCP via `http_client.{cpp,h}` (copiado de data_factory). Build linux + windows con `add_imgui_app` + ws2_32 en Win. Deploy automatico via `redeploy_cpp_app_windows`.
|
||||
- **Issues 0105 + 0106** (`dev/issues/`) — estandarizacion del bloque `service:` y app `services_monitor`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`sqlite_api.service` murio 20h sin alerta el 2026-05-17** — Raiz: el unit tenia `Restart=on-failure` y el ultimo exit fue por `SIGTERM` (limpio, no failure). systemd NO reinicia exit success. Fix: cambio a `Restart=always` + `RestartSec=5`. Reload + restart inmediato. Detectado mientras se debuggeaba `data_factory` cargando lento (raiz: data_factory llama a `sqlite_api:8484`, timeout 3s, no responde). Aplicado el mismo `Restart=always` al unit nuevo `services_api.service`.
|
||||
- **`sqlite_api/app.md` health_endpoint** — declaraba `/api/status` que devuelve 404. Cambiado a `/api/databases` (200, lista de bases registradas). Detectado por el primer ciclo del propio `services_api` que marcaba sqlite_api como `degraded`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`services_monitor` tags** — sin `service`/`services` en `tags` para evitar falso positivo en el matcher `tags LIKE '%service%'` del audit `services-spec`. La app es desktop client (frontend), no daemon.
|
||||
|
||||
## 2026-05-16
|
||||
|
||||
### Added
|
||||
|
||||
- **Panel "Logs" en `dag_engine` RunDetail** — `apps/dag_engine/frontend/src/pages/RunDetail.tsx` anade `<Paper>` final con `<Code block>` scrollable + `CopyButton` de Mantine. Helper `buildLogText(run, steps)` compone texto plano (metadata del run + por-step status/exit/duration/stdout/stderr indentado) para pegar entero al LLM sin abrir los `Collapse` del `StepTimeline`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`dag_engine` steps `function:` fallando con `error: function "<id>" not found (tried as ID and name)`** — tres DAGs nocturnos (`fn_backup` x2, `daily-registry-audit`) fallaron 2026-05-15/16 porque el binario `fn` resolvia una copia stale `apps/dag_engine/registry.db` (May 15, 262 KB) en vez del `registry.db` raiz. Raiz: el systemd unit `dag_engine.service` tiene `WorkingDirectory=apps/dag_engine/` y no exportaba `FN_REGISTRY_ROOT`; `cmd/fn/ops.go::tryOpenRegistryDB` cae al walk-up `go.mod` (devuelve `apps/dag_engine/`). Fix:
|
||||
- Borrado `apps/dag_engine/registry.db` stale (violaba `.claude/rules/db_locations.md`).
|
||||
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT`, `FN_BIN`, `PATH` (con `/usr/local/go/bin` para steps `function:` Go sin tests que invocan `go vet`), `HOME`.
|
||||
- `apps/dag_engine/executor.go`: steps `function:` exportan `FN_REGISTRY_ROOT=<root>` en env y default `dir = fnRegistryRoot` si `step.Dir`/`dag.WorkingDir` vacios. Steps `command:`/`script:` sin cambio.
|
||||
|
||||
### Added
|
||||
|
||||
- **Iconos `.ico` Windows para apps C++** — 11 apps GUI (`chart_demo`, `dag_engine_ui`, `data_factory`, `graph_explorer`, `navegator_dashboard`, `odr_console`, `primitives_gallery`, `registry_dashboard`, `shaders_lab`, `text_editor_smoke`, `altsnap_jitter_test`) ahora tienen icono propio en el `.exe` y en `<exe_dir>` desplegado.
|
||||
- Glyphs: **Phosphor Icons** (`fill` weight), clonado en `sources/phosphor-core/` (1512 SVGs disponibles). Cada app usa un `accent_hex` distinto (Tailwind 500-700) para distinguirse en taskbar/desktop.
|
||||
- Mapping inicial en `dev/gen_app_icons.py` (script reproducible). Cada `.ico` multi-resolucion (16/24/32/48/64/128/256).
|
||||
- Wiring CMake: `cpp/CMakeLists.txt:1-5` declara `LANGUAGES C CXX RC` en WIN32; `add_imgui_app` macro detecta `<app_dir>/appicon.ico` y genera `<target>_appicon.rc` enlazado via `windres` (toolchain `cpp/toolchains/mingw-w64.cmake`).
|
||||
- Nueva funcion del registry: `generate_app_icon_py_infra` (`python/functions/infra/generate_app_icon.{py,md}`). Toma `phosphor_icon_name + accent_hex + out_ico_path` y exporta `.ico` multi-res. Tags: `cpp-windows`, `icon`, `phosphor`.
|
||||
- Convencion documentada en `.claude/rules/cpp_apps.md §11`.
|
||||
|
||||
- **C++ framework — Alt+RMB resize / Alt+LMB move anywhere** (`cpp/framework/app_base.cpp`). WndProc subclass detecta `WM_RBUTTONDOWN`/`WM_LBUTTONDOWN` con `GetAsyncKeyState(VK_MENU) & 0x8000`, `ReleaseCapture` + `PostMessage(WM_SYSCOMMAND, SC_SIZE|dir | SC_MOVE|HTCAPTION)`. Modal nativo, cero jitter automatico via gate sizemove existente. Aplica a main + cada viewport flotante (subclass per-frame).
|
||||
- **C++ framework — multi-HWND subclass** para anti-jitter. `g_subclassed` ahora `unordered_map<HWND, WNDPROC>`, scan per-frame en `pio.Viewports` instala subclass en cada HWND nuevo, `prune_dead_subclassed()` con `IsWindow`, `uninstall_sizemove_subclass_all()` al exit. Fix del temblor en paneles flotantes (no solo el main HWND).
|
||||
- **C++ framework — iconified survival** de paneles flotantes. Antes `glfwWaitEvents+continue` paraba el frame loop entero al minimizar el main → secondary viewports congelados/ocultos. Ahora detecta secondary viewports y fall-through al frame normal si existen; solo duerme cuando no hay flotantes.
|
||||
- **C++ framework — `fn::internal::*` test observability**. `sizemove_enter_count()`, `alt_rmb_resize_count()`, `alt_lmb_move_count()`, `rbuttondown_seen_count()`, `set_force_alt_for_test(bool)`. Counters monotonicos zero-cost, modo test salta `PostMessage SC_SIZE/SC_MOVE` para no atrapar al harness en modal.
|
||||
- **`apps/altsnap_jitter_test/`** — extendido a 6 phases (p1 sync, p2 main HWND modal, p3 secondary HWND modal, p4 iconify+restore preserva floating, p5 Alt+RMB consumed, p6 Alt+LMB consumed). Todas PASS en Windows.
|
||||
- **`redeploy_all_cpp_apps_bash_pipelines`** — pipeline nuevo `bash/functions/pipelines/redeploy_all_cpp_apps.sh` que cross-compila todo el arbol `cpp/` en un solo cmake pass + redeploy de cada `.exe` al Desktop. Filtro opcional por substring de nombre. Tolerante a fallos (build best-effort, summary OK/SKIPPED/FAILED). Tags: `cpp, windows, deploy, redeploy, bulk, cpp-windows`. Composicion: `build_cpp_windows_bash_infra` + loop `taskkill.exe` + `deploy_cpp_exe_to_windows_bash_infra`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`io.ConfigWindowsMoveFromTitleBarOnly = true`** en `fn::run_app`. Floating panels (viewport secundario = OS window borderless con UNA ventana ImGui rellenandolo) ahora respetan "solo header arrastra" como las decoradas. Fix del drag-anywhere-sin-alt en panel flotante. Alt+LMB anywhere sigue funcionando (subclass consume antes que ImGui).
|
||||
- **`resolve_cpp_app_dir_bash_infra` v1.1.0** — ahora busca apps tambien en `apps/<X>/` (canonical issue 0096) ademas de `cpp/apps/<X>/` (legacy) y `projects/*/apps/<X>/`. Fix retroactivo: `./fn run compile_cpp_app <name>` fallaba para apps en el layout canonical (ej. `dag_engine_ui`). Deduccion desde CWD tambien actualizada. Helper interno `_list_cpp_apps`.
|
||||
|
||||
### Notes
|
||||
|
||||
- Apps C++ redesplegadas via `redeploy_all_cpp_apps`: 12 OK / 1 SKIP (`data_factory` sin .exe target) / 0 FAILED. Todas tienen los fixes del framework activos.
|
||||
- ImGui_ImplGlfw subclassea el HWND DESPUES que nuestro framework. ImGui captura nuestro WndProc como `PrevWndProc` y chainea via `CallWindowProc`, asi que el subclass nuestro sigue recibiendo TODOS los mensajes en el orden correcto. NO re-subclassear despues de ImGui init (provoca recursion infinita por cycle: `our_proc -> orig=imgui_proc -> imgui_proc -> prev=our_proc -> ...`).
|
||||
- Pre-existing build break en `cpp/tests/test_llm_anthropic.cpp` + `cpp/tests/test_graph_icons.cpp` por uso de `setenv()` que no existe en mingw-w64. NO bloquea `redeploy_all_cpp_apps` (build best-effort). Candidato a guard `#ifdef _WIN32` con `_putenv_s` o skip cross-compile. No introducido por esta sesion.
|
||||
|
||||
## 2026-05-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[2026-05-22 23:18:14.872] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:24:12.811] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:24:14.628] [INFO] [connect] testing https://agents.organic-machine.com...
|
||||
[2026-05-22 23:24:14.758] [INFO] [connect] OK
|
||||
[2026-05-22 23:24:14.765] [INFO] [db] base_url saved
|
||||
[2026-05-22 23:24:14.765] [INFO] [fetch_agents] starting
|
||||
[2026-05-22 23:24:14.766] [INFO] [fetch_agents] requesting https://agents.organic-machine.com/agents
|
||||
[2026-05-22 23:24:14.903] [INFO] [fetch_agents] response status=200 err= body_len=3146
|
||||
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] parsed 11 rows
|
||||
[2026-05-22 23:24:14.904] [INFO] [fetch_agents] done
|
||||
[2026-05-22 23:24:14.910] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
|
||||
[2026-05-22 23:27:07.469] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:27:08.242] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
|
||||
[2026-05-22 23:27:36.670] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:27:37.446] [INFO] [agents_panel] render n_rows=11 cells=121 specs=11
|
||||
[2026-05-22 23:28:07.068] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:30:03.025] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:30:38.605] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:30:48.267] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:40:58.931] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:41:16.455] [INFO] app start: Agents Dashboard
|
||||
[2026-05-22 23:42:35.646] [INFO] app start: Agents Dashboard
|
||||
@@ -0,0 +1,4 @@
|
||||
[2026-05-15 23:51:43.764] [INFO] app start: altsnap_jitter_test
|
||||
[2026-05-15 23:51:44.017] [INFO] app exit
|
||||
[2026-05-15 23:52:47.933] [INFO] app start: altsnap_jitter_test
|
||||
[2026-05-15 23:52:48.135] [INFO] app exit
|
||||
@@ -0,0 +1,360 @@
|
||||
# dag_engine — Guia de uso
|
||||
|
||||
Motor de DAGs propio del fn_registry. **Scheduler oficial** del ecosistema (issue 0007a-e + flow 0001). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
|
||||
|
||||
Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Donde viven los DAGs
|
||||
|
||||
| Path | Que |
|
||||
|---|---|
|
||||
| `apps/dag_engine/dags_migrated/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). |
|
||||
| `apps/dag_engine/dags_migrated/archive/` | DAGs deshabilitados (no se cargan por el scheduler). |
|
||||
|
||||
Por defecto el systemd unit apunta a `apps/dag_engine/dags_migrated/`. Para usar otro dir, edita `~/.config/systemd/user/dag_engine.service`:
|
||||
|
||||
```ini
|
||||
ExecStart=/home/lucas/fn_registry/apps/dag_engine/dag_engine server \
|
||||
--port 8090 \
|
||||
--dags-dir /home/lucas/fn_registry/apps/dag_engine/dags_migrated \
|
||||
--db /home/lucas/fn_registry/apps/dag_engine/dag_engine.db \
|
||||
--scheduler
|
||||
```
|
||||
|
||||
Y reload + restart:
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart dag_engine.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Anadir un DAG nuevo (workflow)
|
||||
|
||||
### Paso a paso
|
||||
|
||||
1. **Crear YAML** en `apps/dag_engine/dags_migrated/<nombre>.yaml` (ver formato en seccion 3).
|
||||
2. **Validar** sin ejecutar:
|
||||
```bash
|
||||
./apps/dag_engine/dag_engine validate apps/dag_engine/dags_migrated/<nombre>.yaml
|
||||
```
|
||||
Salida esperada: `Validation: PASS`. Si falla, ver seccion 5 (diagnostico).
|
||||
3. **Probar ejecucion manual** una vez:
|
||||
```bash
|
||||
./apps/dag_engine/dag_engine run apps/dag_engine/dags_migrated/<nombre>.yaml
|
||||
```
|
||||
4. **Recargar scheduler** (toma el YAML automaticamente al iterar el dir):
|
||||
```bash
|
||||
systemctl --user restart dag_engine.service
|
||||
journalctl --user-unit dag_engine.service -n 30 --no-pager
|
||||
```
|
||||
Busca la linea `[scheduler] ticker started for <nombre> (<cron>)` en los logs.
|
||||
5. **Verificar en frontend**:
|
||||
- C++ ImGui: panel `DAGs` muestra el nuevo DAG. Pulsa `Refresh` si no aparece.
|
||||
- Web: `http://localhost:8090`.
|
||||
|
||||
### Disparo manual desde curl o frontend
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8090/api/dags/<nombre>/run
|
||||
```
|
||||
|
||||
Devuelve `{"dag":"<nombre>","run_id":"...","status":"accepted"}` y dispara el WS broadcast — los frontends ven la run en `<1s`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Formato YAML
|
||||
|
||||
Formato YAML propio de dag_engine. Schema: `name`, `description`, `schedule`, `env`, `tags`, `working_dir`, `steps[]`, `handlers` (alias `handler_on`).
|
||||
|
||||
### Ejemplo completo
|
||||
|
||||
```yaml
|
||||
name: my_pipeline
|
||||
description: "Pipeline diario que importa CSV y actualiza Metabase."
|
||||
group: finanzas # opcional, agrupa DAGs en listados
|
||||
type: graph # opcional: graph (default) | chain
|
||||
tags: [daily, csv, metabase] # opcional, filtros en la UI
|
||||
|
||||
# Variables de entorno (heredadas por todos los steps).
|
||||
env:
|
||||
- DATA_DIR: /home/lucas/data
|
||||
- SLACK_HOOK: ${SLACK_HOOK_PROD} # interpolacion de ENV del host
|
||||
|
||||
# Cron schedule. Puede ser string o lista.
|
||||
schedule:
|
||||
- "0 9 * * *" # 09:00 todos los dias
|
||||
- "0 21 * * 5" # 21:00 viernes (segundo trigger)
|
||||
|
||||
# Working dir + shell por defecto para todos los steps.
|
||||
working_dir: /home/lucas/fn_registry
|
||||
shell: /bin/bash
|
||||
timeout_sec: 1800 # 30 min para todo el DAG
|
||||
|
||||
steps:
|
||||
- name: ingest
|
||||
description: "Descarga CSV."
|
||||
command: ./bash/functions/pipelines/ingest_csv.sh
|
||||
timeout_sec: 300 # 5 min para este step
|
||||
env:
|
||||
- SOURCE_URL: https://example.com/data.csv
|
||||
|
||||
- name: transform
|
||||
description: "Limpieza y agregacion."
|
||||
script: |
|
||||
#!/usr/bin/env python3
|
||||
import pandas as pd
|
||||
df = pd.read_csv("$DATA_DIR/raw.csv")
|
||||
df.to_parquet("$DATA_DIR/clean.parquet")
|
||||
depends: [ingest] # debe terminar OK antes
|
||||
retry_policy:
|
||||
limit: 2 # reintentos en caso de fallo
|
||||
interval_sec: 60
|
||||
|
||||
- name: load_metabase
|
||||
command: ./bash/functions/metabase/refresh_dashboard.sh
|
||||
depends: [transform]
|
||||
continue_on:
|
||||
failure: true # no aborta el DAG aunque falle
|
||||
|
||||
- name: notify
|
||||
command: ./bash/functions/io/slack_send.sh "pipeline OK"
|
||||
depends: [load_metabase]
|
||||
|
||||
# Hooks de ciclo de vida.
|
||||
handler_on:
|
||||
success: ./bash/functions/io/notify_success.sh
|
||||
failure: ./bash/functions/io/notify_failure.sh
|
||||
exit: ./bash/functions/io/cleanup.sh
|
||||
```
|
||||
|
||||
### Campos del DAG (top-level)
|
||||
|
||||
| Campo | Tipo | Default | Que |
|
||||
|---|---|---|---|
|
||||
| `name` | string | (obligatorio) | Identificador unico. Debe matchear el filename sin extension. |
|
||||
| `description` | string | "" | Texto libre, aparece en la UI. |
|
||||
| `group` | string | "" | Agrupa DAGs en listados. |
|
||||
| `type` | string | `""` (graph) | `graph` o `chain`. graph = grafo dirigido por `depends`. chain = ejecucion secuencial implicita. |
|
||||
| `working_dir` | string | cwd del server | Path absoluto desde donde lanzar los steps. |
|
||||
| `shell` | string | `/bin/sh` | Shell para `command:`. |
|
||||
| `env` | list/map | [] | Variables de entorno DAG-wide. |
|
||||
| `schedule` | string/list | "" | Cron expressions (5 campos: min hour dom mon dow). Vacio = solo manual. |
|
||||
| `steps` | list | (obligatorio) | Pasos del DAG (>=1). |
|
||||
| `handler_on` | map | null | Hooks `init/success/failure/exit`. Alias: `handlers`. |
|
||||
| `tags` | list[string] | [] | Filtros en la UI. |
|
||||
| `timeout_sec` | int | 0 (sin timeout) | Timeout global del DAG en segundos. |
|
||||
|
||||
### Campos de cada step
|
||||
|
||||
| Campo | Tipo | Default | Que |
|
||||
|---|---|---|---|
|
||||
| `name` | string | (obligatorio) | Identificador del step dentro del DAG. |
|
||||
| `id` | string | "" | Override del id auto-generado. |
|
||||
| `description` | string | "" | Texto libre. |
|
||||
| `command` | string | "" | Comando shell (mutuamente excluyente con `script`/`function`). |
|
||||
| `script` | string | "" | Bloque heredoc. Util para Python/Lua inline. |
|
||||
| `function` | string | "" | ID de funcion del registry (ej `audit_capability_groups_go_infra`). Si set, executor invoca `${FN_REGISTRY_ROOT}/fn run <id> <args...>` y captura `function_id` en `dag_step_results`. Mutuamente exclusivo con `command`/`script`; si convive, gana `function`. |
|
||||
| `args` | list[string] | [] | Args extra para `command` o para la `function`. |
|
||||
| `shell` | string | hereda | Override del shell. |
|
||||
| `dir` / `working_dir` | string | hereda | Working dir para este step. |
|
||||
| `depends` | list[string] | [] | Steps que deben terminar OK antes. Si vacio + `type:graph`, arranca en paralelo. |
|
||||
| `env` | list/map | hereda | Env del step (sobrescribe el del DAG). |
|
||||
| `continue_on.failure` | bool | false | Si true, el DAG sigue aunque este step falle. |
|
||||
| `continue_on.skipped` | bool | false | Si true, dependientes corren aunque este step quede skipped. |
|
||||
| `retry_policy.limit` | int | 0 | Reintentos. |
|
||||
| `retry_policy.interval_sec` | int | 0 | Segundos entre reintentos. |
|
||||
| `timeout_sec` | int | 0 (sin timeout) | Timeout del step. |
|
||||
| `output` | string | "" | Nombre de variable donde guardar stdout (consumible por dependientes). |
|
||||
| `tags` | list[string] | [] | Tags por step (UI). |
|
||||
|
||||
### Function steps (coherencia con el registry)
|
||||
|
||||
Un DAG idiomatico llama funciones del registry, no scripts ad-hoc. Cada step `function:` queda trazado en `call_monitor.calls` por el hook PostToolUse del agente y en `dag_step_results.function_id` del propio dag_engine — el bucle reactivo (issue 0085) tiene visibilidad end-to-end.
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: audit_capabilities
|
||||
function: audit_capability_groups_go_infra
|
||||
args: ["--json"]
|
||||
description: "Audita drift entre tags de capability group y paginas madre"
|
||||
```
|
||||
|
||||
Ventajas vs `command: ./fn run ...`:
|
||||
|
||||
- `function_id` se persiste como columna dedicada en `dag_step_results` (filtrable, agrupable).
|
||||
- El frontend `dag_engine_ui` muestra badge + panel lateral con `uses_functions` (subfunciones que el step va a usar transitivamente).
|
||||
- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step.
|
||||
- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error.
|
||||
- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`.
|
||||
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
|
||||
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
|
||||
|
||||
Ejemplo completo: `apps/dag_engine/dags_migrated/daily-registry-audit.yaml`.
|
||||
|
||||
### Cron schedule
|
||||
|
||||
5 campos clasicos: `min hour dom mon dow`. Ejemplos:
|
||||
|
||||
| Expresion | Significado |
|
||||
|---|---|
|
||||
| `0 9 * * *` | Todos los dias a las 09:00 |
|
||||
| `*/15 * * * *` | Cada 15 minutos |
|
||||
| `0 */6 * * *` | Cada 6 horas en punto |
|
||||
| `0 9 * * 1-5` | 09:00 lunes-viernes |
|
||||
| `0 21 * * 5` | 21:00 viernes |
|
||||
|
||||
Multiples cron en `schedule:` -> el DAG dispara por cada uno.
|
||||
|
||||
---
|
||||
|
||||
## 4. Comandos CLI
|
||||
|
||||
```bash
|
||||
./dag_engine run <path.yaml> # ejecuta un DAG ad-hoc
|
||||
./dag_engine list [dir] # lista DAGs con schedule + ultimo status
|
||||
./dag_engine status [dag_name] # historial de ejecuciones
|
||||
./dag_engine validate <path.yaml> # parse + validate (no ejecuta)
|
||||
./dag_engine server # arranca HTTP + WS hub + frontend embebido
|
||||
```
|
||||
|
||||
Flags del `server`:
|
||||
|
||||
| Flag | Default | Que |
|
||||
|---|---|---|
|
||||
| `--port` | 8090 | Puerto HTTP. |
|
||||
| `--dags-dir` | `apps/dag_engine/dags_migrated` (via systemd unit) | Dir scaneado para YAMLs. |
|
||||
| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. |
|
||||
| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Que hacer si algo falla
|
||||
|
||||
### 5.1. El DAG no aparece en la UI
|
||||
|
||||
**Sintoma:** anadiste un YAML pero `GET /api/dags` no lo lista.
|
||||
|
||||
| Causa | Diagnostico | Fix |
|
||||
|---|---|---|
|
||||
| YAML invalido | `./dag_engine validate <path>` muestra el error. | Corregir segun el mensaje (campo desconocido, indentacion, type wrong). |
|
||||
| Filename con extension fuera de `.yaml`/`.yml` | `ls apps/dag_engine/dags_migrated/` | Renombrar a `.yaml`. |
|
||||
| El servidor apunta a otro dir | `systemctl --user cat dag_engine.service` -> ver `--dags-dir`. | Ajustar unit y `daemon-reload + restart`. |
|
||||
| Cache UI antiguo | C++: pulsa `Refresh`. Web: `Ctrl+F5`. | — |
|
||||
|
||||
### 5.2. Validation: FAIL
|
||||
|
||||
`validate` muestra `parse error: ...` o `Validation: FAIL`. Causas tipicas:
|
||||
|
||||
| Mensaje | Causa | Fix |
|
||||
|---|---|---|
|
||||
| `yaml unmarshal: ...` | Sintaxis YAML rota (indentacion, tab vs espacios). | Usar 2 espacios consistentes. Validar online con `yamllint`. |
|
||||
| `dag_parse: step[N]: name is required` | Step sin `name:`. | Anadir `name:`. |
|
||||
| `dag_parse: step[N]: command or script required` | Step sin `command` ni `script`. | Anadir uno de los dos. |
|
||||
| `cycle detected: A -> B -> A` | `depends` forma ciclo. | Romper la dependencia o convertir uno de los nodos en step distinto. |
|
||||
| `unknown depends: <step>` | `depends:` referencia un step inexistente. | Comprobar nombres exactos (case-sensitive). |
|
||||
| `invalid cron: <expr>` | Cron mal formado (4 o 6 campos en vez de 5). | Verificar `0 9 * * *` (5 campos). |
|
||||
|
||||
### 5.3. El DAG corre pero un step falla
|
||||
|
||||
**Sintoma:** `status: failed` en la UI.
|
||||
|
||||
1. Abre `DAG Detail` y haz doble-click en el run rojo -> `Run Detail`.
|
||||
2. Expande el step que fallo (CollapsingHeader). Muestra `stdout` + `stderr`.
|
||||
3. Errores tipicos:
|
||||
|
||||
| stderr | Causa | Fix |
|
||||
|---|---|---|
|
||||
| `command not found` | `command:` apunta a un binario fuera de `PATH`. | Path absoluto o setear `env: [PATH: ...]`. |
|
||||
| `permission denied` | Script sin `chmod +x`. | `chmod +x <script>` (o usar `bash <script>`). |
|
||||
| `no such file or directory` | `working_dir:` mal o ruta relativa rota. | Path absoluto en `working_dir:`. |
|
||||
| Timeout | Step duro mas que `timeout_sec`. | Subir el limite o partir el step. |
|
||||
| Exit 137 / OOM kill | Out-of-memory. | Reducir batch o anadir swap. |
|
||||
|
||||
### 5.4. El scheduler no dispara
|
||||
|
||||
**Sintoma:** Hay `schedule:` valido pero el DAG no corre solo.
|
||||
|
||||
1. Verifica que el server arranco con `--scheduler`:
|
||||
```bash
|
||||
systemctl --user cat dag_engine.service | grep scheduler
|
||||
```
|
||||
2. Logs:
|
||||
```bash
|
||||
journalctl --user-unit dag_engine.service -n 50 --no-pager | grep -E "scheduler|ticker"
|
||||
```
|
||||
Debes ver `[scheduler] ticker started for <name> (<cron>), next: <ISO8601>`.
|
||||
3. Si `next:` es muy lejano (ej. en una semana) y necesitas probar -> dispara manual:
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8090/api/dags/<name>/run
|
||||
```
|
||||
4. Hora del sistema descalibrada:
|
||||
```bash
|
||||
timedatectl status
|
||||
```
|
||||
Si difiere de la hora real, `sudo timedatectl set-ntp true`.
|
||||
|
||||
### 5.5. El frontend C++ no conecta WS
|
||||
|
||||
**Sintoma:** Panel `Live (WS)` muestra `disconnected`.
|
||||
|
||||
| Causa | Fix |
|
||||
|---|---|
|
||||
| Servidor caido | `systemctl --user status dag_engine.service`, `restart` si `inactive`. |
|
||||
| Puerto cambiado | El cliente apunta a `127.0.0.1:8090` por codigo (constante `g_ws_port`). Reedificar si cambiaste el puerto del server. |
|
||||
| Firewall Windows -> WSL | WSL2 expone `localhost`, normalmente OK. Si falla: `wsl --shutdown` y reabrir. |
|
||||
|
||||
### 5.6. Cleanup de runs viejos
|
||||
|
||||
`dag_runs` y `dag_step_results` crecen sin limite. Para limpiar:
|
||||
|
||||
```bash
|
||||
sqlite3 apps/dag_engine/dag_engine.db <<'SQL'
|
||||
DELETE FROM dag_step_results WHERE run_id IN (
|
||||
SELECT id FROM dag_runs WHERE started_at < datetime('now', '-30 days')
|
||||
);
|
||||
DELETE FROM dag_runs WHERE started_at < datetime('now', '-30 days');
|
||||
VACUUM;
|
||||
SQL
|
||||
```
|
||||
|
||||
### 5.7. Restaurar desde backup
|
||||
|
||||
Si rompes `dags_migrated/`, recupera desde el snapshot de `backup_all_bash_pipelines` (BACKUP_ROOT por defecto `~/backups/fn_registry`):
|
||||
|
||||
```bash
|
||||
cp ~/backups/fn_registry/registry/daily.0/dags_migrated/*.yaml \
|
||||
apps/dag_engine/dags_migrated/ 2>/dev/null || \
|
||||
git checkout HEAD -- apps/dag_engine/dags_migrated/
|
||||
systemctl --user restart dag_engine.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Endpoints HTTP
|
||||
|
||||
| Metodo | Path | Que |
|
||||
|---|---|---|
|
||||
| GET | `/api/dags` | Lista DAGs + last_run + last_runs[5]. |
|
||||
| GET | `/api/dags/{name}` | Detalle + validation. |
|
||||
| POST | `/api/dags/{name}/run` | Dispara ejecucion (trigger=`api`). Devuelve `run_id`. |
|
||||
| GET | `/api/runs` | Historial. Query: `dag`, `limit`, `offset`. |
|
||||
| GET | `/api/runs/{id}` | Detalle de un run + sus step_results. |
|
||||
| GET | `/api/ws/dagruns` | WebSocket. Snapshot + deltas en vivo (issue 0095). |
|
||||
| GET | `/api/scheduler/status` | Tickers activos. |
|
||||
| POST | `/api/scheduler/start` | Arranca scheduler (si no estaba). |
|
||||
| POST | `/api/scheduler/stop` | Para scheduler. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Referencias
|
||||
|
||||
- Schema parser: `functions/core/dag_parse.go` (frontmatter en `dag_parse_go_core`).
|
||||
- Validator: `functions/core/dag_validate.go` (`dag_validate_go_core`).
|
||||
- Topo sort: `functions/core/dag_topo_sort.go` (`dag_topo_sort_go_core`).
|
||||
- Cron: `functions/core/parse_cron_expr.go` + `next_cron_time.go`.
|
||||
- Frontend C++: `cpp/apps/dag_engine_ui/` (issue 0095).
|
||||
- WS hub: `apps/dag_engine/events.go`.
|
||||
- dag_engine es el scheduler oficial del ecosistema. Single-binary Go + SQLite, sin dependencias externas.
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// RegisterAPI sets up all HTTP routes on the given mux.
|
||||
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, frontendFS fs.FS) {
|
||||
func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, hub *DagRunHub, frontendFS fs.FS) {
|
||||
// API routes.
|
||||
mux.HandleFunc("GET /api/dags", handleListDags(executor))
|
||||
mux.HandleFunc("GET /api/dags/{name}", handleGetDag(executor))
|
||||
@@ -15,10 +15,18 @@ func RegisterAPI(mux *http.ServeMux, executor *Executor, scheduler *Scheduler, f
|
||||
mux.HandleFunc("GET /api/runs", handleListRuns(executor))
|
||||
mux.HandleFunc("GET /api/runs/{id}", handleGetRun(executor))
|
||||
|
||||
// Function lookup proxy a registry.db (read-only).
|
||||
mux.HandleFunc("GET /api/functions/{id}", handleGetFunction())
|
||||
|
||||
mux.HandleFunc("POST /api/scheduler/start", handleSchedulerStart(scheduler))
|
||||
mux.HandleFunc("POST /api/scheduler/stop", handleSchedulerStop(scheduler))
|
||||
mux.HandleFunc("GET /api/scheduler/status", handleSchedulerStatus(scheduler))
|
||||
|
||||
// Live updates (WS hub).
|
||||
if hub != nil {
|
||||
mux.HandleFunc("GET /api/ws/dagruns", handleDagRunsWS(hub))
|
||||
}
|
||||
|
||||
// Frontend SPA fallback.
|
||||
if frontendFS != nil {
|
||||
mux.Handle("/", spaHandler(frontendFS))
|
||||
|
||||
+65
-7
@@ -2,7 +2,8 @@
|
||||
name: dag_engine
|
||||
lang: go
|
||||
domain: infra
|
||||
description: "Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite."
|
||||
version: 0.1.0
|
||||
description: "Motor de ejecucion de DAGs del fn_registry: CLI + servidor HTTP + scheduler cron. Schema YAML propio con `function:` para invocar funciones del registry (`fn run <id>`) y `command:` para shell. Historial en SQLite. Scheduler oficial del ecosistema."
|
||||
tags: [service, dag, workflow, scheduler, web, cron]
|
||||
uses_functions:
|
||||
- dag_parse_go_core
|
||||
@@ -27,6 +28,18 @@ uses_types:
|
||||
framework: "net/http + vite + react"
|
||||
entry_point: "main.go"
|
||||
dir_path: "apps/dag_engine"
|
||||
service:
|
||||
port: 8090
|
||||
health_endpoint: /api/dags
|
||||
health_timeout_s: 3
|
||||
systemd_unit: dag_engine.service
|
||||
systemd_scope: user
|
||||
restart_policy: always
|
||||
runtime: systemd-user
|
||||
pc_targets:
|
||||
- aurgi-pc
|
||||
- home-wsl
|
||||
is_local_only: false
|
||||
---
|
||||
|
||||
## Arquitectura
|
||||
@@ -71,15 +84,60 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
|
||||
|
||||
```bash
|
||||
# CLI
|
||||
./dag-engine run ~/dagu/dags/example.yaml
|
||||
./dag-engine list ~/dagu/dags/
|
||||
./dag-engine run apps/dag_engine/dags_migrated/fn_backup.yaml
|
||||
./dag-engine list apps/dag_engine/dags_migrated/
|
||||
|
||||
# Servidor web
|
||||
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
|
||||
# Servidor web (production: gestionado por dag_engine.service systemd user unit)
|
||||
./dag-engine server --port 8090 --dags-dir apps/dag_engine/dags_migrated/ --scheduler
|
||||
# Browser: http://localhost:8090
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
|
||||
Puerto por defecto 8090 (mismo que Dagu).
|
||||
Schema YAML propio (ver `README.md` seccion 3 + ejemplos en `dags_migrated/`). Steps tipo `function:` invocan `fn run <id>` y propagan `function_id` a `dag_step_results` para el bucle reactivo. Puerto default 8090.
|
||||
|
||||
### 2026-05-16 — Fix function-not-found en steps `function:` + panel Logs en RunDetail `[done]`
|
||||
|
||||
Sintoma: `fn_backup` y `daily-registry-audit` fallaron 3 noches seguidas con `error: function "<id>" not found (tried as ID and name)` aunque las funciones existen en `registry.db` raiz.
|
||||
|
||||
Raiz: servicio systemd `dag_engine.service` tiene `WorkingDirectory=/home/lucas/fn_registry/apps/dag_engine`. Binario `fn` resuelve `registry.db` por (1) `FN_REGISTRY_ROOT`, (2) `root()` walk-up buscando `go.mod`, (3) exe dir (`cmd/fn/ops.go:1597-1628`). Sin `FN_REGISTRY_ROOT` seteado, (2) encuentra el `go.mod` de `apps/dag_engine/` y devuelve ese dir — donde habia una copia stale `apps/dag_engine/registry.db` (262 KB, May 15) sin las funciones recien creadas. Viola regla `.claude/rules/db_locations.md` (registry.db SOLO en raiz).
|
||||
|
||||
Fix:
|
||||
- Borrado `apps/dag_engine/registry.db` stale.
|
||||
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT=/home/lucas/fn_registry`, `FN_BIN=/home/lucas/fn_registry/fn`, `PATH=/usr/local/go/bin:/home/lucas/go/bin:...`, `HOME=/home/lucas`. Sin PATH el step `go vet` fallaba con `exec: "go": executable file not found in $PATH`.
|
||||
- `apps/dag_engine/executor.go`: para steps `function:` el spawn exporta `FN_REGISTRY_ROOT=<root>` en env y, si `step.dir`/`working_dir` vacios, fija `dir = fnRegistryRoot`. Belt-and-suspenders: aunque alguien lance el binario sin systemd, los `function:` steps usan el root canonico.
|
||||
|
||||
Verificacion: `POST /api/dags/daily-registry-audit/run` -> step `audit_capabilities` pasa (387 ms) en vez de fallar con not-found. Restantes failures (`audit_artefacts` exit 1, `fn_backup` exit 4 sin respetar `continue_on.exit_code`) son bugs reales independientes — fuera de scope.
|
||||
|
||||
### 2026-05-16 — Panel Logs en RunDetail (frontend) `[done]`
|
||||
|
||||
- `apps/dag_engine/frontend/src/pages/RunDetail.tsx`: nuevo `<Paper>` "Logs" al final con `<Code block>` scrollable (max-h 480) + `CopyButton` de Mantine (icono toggle copy/check teal).
|
||||
- Helper `buildLogText(run, steps)` compone texto plano: metadata del run (dag, path, status, trigger, started/finished ISO, duration ms, error) + por step (`[status] name exit=N Nms`, started, finished, error, stdout, stderr indentado 4 espacios).
|
||||
- Permite pegar log entero al LLM para debugging sin abrir N collapses del `StepTimeline`.
|
||||
- Build frontend pendiente: `pnpm build` rompe por errores preexistentes (`StepTimeline.tsx:49` usa API legacy `<Collapse in={opened}>`; `main.tsx:1` importa `@mantine/core/styles.css` sin tipos). Edit de RunDetail type-checkea limpio.
|
||||
|
||||
### 2026-05-16 — BBDDs canonicas (referencia rapida)
|
||||
|
||||
- `dag_engine.db`: `apps/dag_engine/dag_engine.db` (+ WAL sidecars). Migrations en `apps/dag_engine/store/migrations/` (`001_init.sql`, `002_step_function_id.sql`). Tablas `dag_runs`, `dag_step_results`.
|
||||
- NO debe coexistir copia de `registry.db` en este dir (viola `db_locations.md`). Si reaparece: borrarla.
|
||||
|
||||
## Lo siguiente que pega
|
||||
|
||||
- `audit_artefacts` falla con exit 1 en `daily-registry-audit` — investigar stderr real (probablemente artefacto huerfano o git drift). Step independiente, no bloquea el resto del DAG.
|
||||
- `fn_backup` step `run_backup_all` sale con exit 4 y el DAG no respeta `continue_on.exit_code: [4]`. Bug en executor: parsear `step.ContinueOn.ExitCode []int` y comparar con `result.ExitCode`. Hoy solo se mira `step.ContinueOn.Failure` (bool).
|
||||
- Frontend `pnpm build` roto por API drift de Mantine en `StepTimeline.tsx` (`<Collapse in={opened}>`) y CSS type import en `main.tsx`. Fix junto con un refresh general de tipos.
|
||||
|
||||
## Documentacion de usuario
|
||||
|
||||
Guia completa (formato YAML, anadir DAGs, troubleshooting, endpoints HTTP):
|
||||
**[apps/dag_engine/README.md](README.md)**.
|
||||
|
||||
|
||||
## Capability growth log
|
||||
|
||||
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
||||
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
|
||||
- `patch`: bugfix sin cambio observable.
|
||||
|
||||
- v0.1.0 (2026-05-18) — baseline.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: fn_backup
|
||||
description: Backup diario de fn_registry (registry.db + operations.db + vaults) via funcion del registry
|
||||
|
||||
schedule:
|
||||
- "0 3 * * *"
|
||||
|
||||
tags: [backup, registry, daily]
|
||||
|
||||
env:
|
||||
- BACKUP_ROOT: /home/lucas/backups/fn_registry
|
||||
|
||||
steps:
|
||||
- name: ensure_dirs
|
||||
command: mkdir -p ${BACKUP_ROOT}
|
||||
|
||||
- name: run_backup_all
|
||||
description: "Snapshot atomico de registry.db + operations.db + vaults con retention 7/4/12"
|
||||
function: backup_all_bash_pipelines
|
||||
args: ["${BACKUP_ROOT}"]
|
||||
continue_on:
|
||||
exit_code: [4]
|
||||
depends: [ensure_dirs]
|
||||
|
||||
- name: report_status
|
||||
command: bash -c 'ls -lh ${BACKUP_ROOT}/registry/daily.0 ${BACKUP_ROOT}/operations/*/daily.0 2>/dev/null | tail -20'
|
||||
depends: [run_backup_all]
|
||||
@@ -0,0 +1,51 @@
|
||||
name: revision-viernes-finanzas
|
||||
description: Revisión semanal de finanzas personales - ingesta, informe y push a Gitea
|
||||
tags: [finanzas, semanal]
|
||||
type: graph
|
||||
|
||||
schedule: "0 9 * * 5"
|
||||
|
||||
env:
|
||||
- PROJECT_DIR: /home/lucas/analysis/finanzas_personales
|
||||
- PYTHON: /home/lucas/analysis/finanzas_personales/.venv/bin/python
|
||||
|
||||
handler_on:
|
||||
failure:
|
||||
command: echo "[$(date)] FALLÓ revision-viernes-finanzas" >> /home/lucas/dagu/logs/failures.log
|
||||
|
||||
steps:
|
||||
- id: ingest
|
||||
description: Procesar archivos nuevos del inbox (BBVA xlsx + Revolut csv)
|
||||
working_dir: ${PROJECT_DIR}
|
||||
command: ./bin/ingest -skip-notebooks
|
||||
continue_on:
|
||||
failure: true
|
||||
|
||||
- id: informe
|
||||
description: Generar informe semanal de cumplimiento del presupuesto
|
||||
command: ${PYTHON} /home/lucas/dagu/scripts/informe_finanzas.py
|
||||
depends: [ingest]
|
||||
|
||||
- id: git_push
|
||||
description: Commit y push del informe a Gitea
|
||||
working_dir: ${PROJECT_DIR}
|
||||
script: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if git diff --quiet data/04_output/informe_semanal.md 2>/dev/null && \
|
||||
! git ls-files --others --exclude-standard | grep -q informe_semanal.md; then
|
||||
echo "Sin cambios en el informe, skip push"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add data/04_output/informe_semanal.md
|
||||
git add data/03_processed/ 2>/dev/null || true
|
||||
|
||||
git commit -m "Informe semanal $(date +%Y-%m-%d)
|
||||
|
||||
Co-Authored-By: Dagu Automation <noreply@dagu.dev>"
|
||||
|
||||
git push origin master:main
|
||||
echo "Push completado"
|
||||
depends: [informe]
|
||||
@@ -0,0 +1,438 @@
|
||||
package main
|
||||
|
||||
// WebSocket hub para live updates de dag_runs + dag_step_results.
|
||||
// Patron: sqlite_api/events.go (CallMonitorHub) — issue 0095.
|
||||
//
|
||||
// Diseño:
|
||||
// - Hub global con N subscribers WS.
|
||||
// - Ticker arranca solo con >=1 subscriber. Cero overhead si nadie mira.
|
||||
// - Cada tick (500ms): query rowid>watermark + activos (status running/pending)
|
||||
// + recientes finished (ultimos 5s) -> broadcast upsert.
|
||||
// - Snapshot inicial: lista de DAGs + ultimos 50 runs + step_results.
|
||||
// - El cliente trata `runs` y `steps` como upserts por id.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"nhooyr.io/websocket/wsjson"
|
||||
|
||||
"dag-engine/store"
|
||||
)
|
||||
|
||||
const (
|
||||
dagWSTickInterval = 500 * time.Millisecond
|
||||
dagWSTickIntervalIdle = 2 * time.Second
|
||||
dagWSIdleThreshold = 30 * time.Second
|
||||
dagWSSnapshotRuns = 50
|
||||
dagWSBroadcastTimeout = 2 * time.Second
|
||||
dagWSRecentFinishedS = 5
|
||||
)
|
||||
|
||||
type wsRun struct {
|
||||
ID string `json:"id"`
|
||||
DagName string `json:"dag_name"`
|
||||
DagPath string `json:"dag_path"`
|
||||
Status string `json:"status"`
|
||||
Trigger string `json:"trigger"`
|
||||
StartedAt string `json:"started_at"`
|
||||
FinishedAt string `json:"finished_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type wsStep struct {
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepName string `json:"step_name"`
|
||||
Status string `json:"status"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Stdout string `json:"stdout,omitempty"`
|
||||
Stderr string `json:"stderr,omitempty"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
FinishedAt string `json:"finished_at,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type wsWatermark struct {
|
||||
Runs int64 `json:"runs"`
|
||||
Steps int64 `json:"steps"`
|
||||
}
|
||||
|
||||
type wsDagMessage struct {
|
||||
Type string `json:"type"` // snapshot|delta|ping
|
||||
Watermark wsWatermark `json:"watermark"`
|
||||
Dags []DagInfo `json:"dags,omitempty"`
|
||||
Runs []wsRun `json:"runs,omitempty"`
|
||||
Steps []wsStep `json:"steps,omitempty"`
|
||||
ServerTime int64 `json:"server_time"`
|
||||
}
|
||||
|
||||
type wsDagClientCmd struct {
|
||||
Watermark wsWatermark `json:"watermark,omitempty"`
|
||||
}
|
||||
|
||||
type dagSubscriber struct {
|
||||
conn *websocket.Conn
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
out chan wsDagMessage
|
||||
watermark wsWatermark
|
||||
}
|
||||
|
||||
// DagRunHub broadcastea cambios de dag_runs + dag_step_results a clientes WS.
|
||||
type DagRunHub struct {
|
||||
db *store.DB
|
||||
executor *Executor
|
||||
|
||||
mu sync.Mutex
|
||||
subscribers map[*dagSubscriber]struct{}
|
||||
tickerStop chan struct{}
|
||||
tickerOn bool
|
||||
watermark wsWatermark
|
||||
lastEventAt time.Time
|
||||
}
|
||||
|
||||
func NewDagRunHub(db *store.DB, executor *Executor) *DagRunHub {
|
||||
return &DagRunHub{
|
||||
db: db,
|
||||
executor: executor,
|
||||
subscribers: make(map[*dagSubscriber]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DagRunHub) register(s *dagSubscriber) {
|
||||
h.mu.Lock()
|
||||
h.subscribers[s] = struct{}{}
|
||||
shouldStart := !h.tickerOn
|
||||
if shouldStart {
|
||||
h.tickerStop = make(chan struct{})
|
||||
h.tickerOn = true
|
||||
h.lastEventAt = time.Now()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
if shouldStart {
|
||||
go h.tickerLoop()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DagRunHub) unregister(s *dagSubscriber) {
|
||||
h.mu.Lock()
|
||||
if _, ok := h.subscribers[s]; !ok {
|
||||
h.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(h.subscribers, s)
|
||||
close(s.out)
|
||||
shouldStop := h.tickerOn && len(h.subscribers) == 0
|
||||
if shouldStop {
|
||||
close(h.tickerStop)
|
||||
h.tickerOn = false
|
||||
}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) tickerLoop() {
|
||||
interval := dagWSTickInterval
|
||||
t := time.NewTimer(interval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-h.tickerStop:
|
||||
return
|
||||
case <-t.C:
|
||||
runs, steps, wm, err := h.fetchDelta(h.getWatermark())
|
||||
if err != nil {
|
||||
log.Printf("[dagws] fetchDelta: %v", err)
|
||||
} else if len(runs) > 0 || len(steps) > 0 {
|
||||
h.setWatermark(wm)
|
||||
h.recordActivity()
|
||||
h.broadcast(wsDagMessage{
|
||||
Type: "delta",
|
||||
Watermark: wm,
|
||||
Runs: runs,
|
||||
Steps: steps,
|
||||
ServerTime: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if time.Since(h.lastActivityAt()) > dagWSIdleThreshold {
|
||||
interval = dagWSTickIntervalIdle
|
||||
} else {
|
||||
interval = dagWSTickInterval
|
||||
}
|
||||
t.Reset(interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DagRunHub) getWatermark() wsWatermark {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return h.watermark
|
||||
}
|
||||
|
||||
func (h *DagRunHub) setWatermark(v wsWatermark) {
|
||||
h.mu.Lock()
|
||||
if v.Runs > h.watermark.Runs {
|
||||
h.watermark.Runs = v.Runs
|
||||
}
|
||||
if v.Steps > h.watermark.Steps {
|
||||
h.watermark.Steps = v.Steps
|
||||
}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) recordActivity() {
|
||||
h.mu.Lock()
|
||||
h.lastEventAt = time.Now()
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) lastActivityAt() time.Time {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return h.lastEventAt
|
||||
}
|
||||
|
||||
// fetchDelta devuelve runs/steps con (rowid > watermark) OR (status in-flight)
|
||||
// OR (recently finished). Watermark devuelto = max rowid visto.
|
||||
func (h *DagRunHub) fetchDelta(since wsWatermark) ([]wsRun, []wsStep, wsWatermark, error) {
|
||||
conn := h.db.Conn()
|
||||
if conn == nil {
|
||||
return nil, nil, since, nil
|
||||
}
|
||||
cutoff := time.Now().Add(-time.Duration(dagWSRecentFinishedS) * time.Second).Format(time.RFC3339)
|
||||
|
||||
runs, maxRuns, err := scanRuns(conn, `
|
||||
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
|
||||
COALESCE(finished_at,''), error
|
||||
FROM dag_runs
|
||||
WHERE rowid > ?
|
||||
OR status IN ('running','pending')
|
||||
OR (finished_at IS NOT NULL AND finished_at >= ?)
|
||||
ORDER BY rowid ASC`, since.Runs, cutoff)
|
||||
if err != nil {
|
||||
return nil, nil, since, err
|
||||
}
|
||||
|
||||
steps, maxSteps, err := scanSteps(conn, `
|
||||
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
|
||||
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
|
||||
FROM dag_step_results
|
||||
WHERE rowid > ?
|
||||
OR status IN ('running','pending')
|
||||
OR (finished_at IS NOT NULL AND finished_at >= ?)
|
||||
ORDER BY rowid ASC`, since.Steps, cutoff)
|
||||
if err != nil {
|
||||
return runs, nil, since, err
|
||||
}
|
||||
|
||||
out := wsWatermark{Runs: maxRuns, Steps: maxSteps}
|
||||
if out.Runs < since.Runs {
|
||||
out.Runs = since.Runs
|
||||
}
|
||||
if out.Steps < since.Steps {
|
||||
out.Steps = since.Steps
|
||||
}
|
||||
return runs, steps, out, nil
|
||||
}
|
||||
|
||||
// fetchSnapshot devuelve DAGs + ultimos N runs + sus step_results + watermark.
|
||||
func (h *DagRunHub) fetchSnapshot() ([]DagInfo, []wsRun, []wsStep, wsWatermark, error) {
|
||||
dags, err := h.executor.ListDAGs()
|
||||
if err != nil {
|
||||
log.Printf("[dagws] list dags: %v", err)
|
||||
dags = nil
|
||||
}
|
||||
conn := h.db.Conn()
|
||||
if conn == nil {
|
||||
return dags, nil, nil, wsWatermark{}, nil
|
||||
}
|
||||
|
||||
runs, maxRuns, err := scanRuns(conn, `
|
||||
SELECT rowid, id, dag_name, dag_path, status, trigger, started_at,
|
||||
COALESCE(finished_at,''), error
|
||||
FROM dag_runs
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ?`, dagWSSnapshotRuns)
|
||||
if err != nil {
|
||||
return dags, nil, nil, wsWatermark{}, err
|
||||
}
|
||||
|
||||
steps, maxSteps, err := scanSteps(conn, `
|
||||
SELECT rowid, id, run_id, step_name, status, exit_code, stdout, stderr,
|
||||
COALESCE(started_at,''), COALESCE(finished_at,''), duration_ms, error
|
||||
FROM dag_step_results
|
||||
WHERE run_id IN (SELECT id FROM dag_runs ORDER BY started_at DESC LIMIT ?)
|
||||
ORDER BY rowid ASC`, dagWSSnapshotRuns)
|
||||
if err != nil {
|
||||
return dags, runs, nil, wsWatermark{Runs: maxRuns}, err
|
||||
}
|
||||
|
||||
return dags, runs, steps, wsWatermark{Runs: maxRuns, Steps: maxSteps}, nil
|
||||
}
|
||||
|
||||
func scanRuns(conn *sql.DB, q string, args ...any) ([]wsRun, int64, error) {
|
||||
rows, err := conn.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []wsRun
|
||||
var max int64
|
||||
for rows.Next() {
|
||||
var r wsRun
|
||||
var rowid int64
|
||||
if err := rows.Scan(&rowid, &r.ID, &r.DagName, &r.DagPath, &r.Status,
|
||||
&r.Trigger, &r.StartedAt, &r.FinishedAt, &r.Error); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if rowid > max {
|
||||
max = rowid
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, max, rows.Err()
|
||||
}
|
||||
|
||||
func scanSteps(conn *sql.DB, q string, args ...any) ([]wsStep, int64, error) {
|
||||
rows, err := conn.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []wsStep
|
||||
var max int64
|
||||
for rows.Next() {
|
||||
var s wsStep
|
||||
var rowid int64
|
||||
if err := rows.Scan(&rowid, &s.ID, &s.RunID, &s.StepName, &s.Status,
|
||||
&s.ExitCode, &s.Stdout, &s.Stderr, &s.StartedAt, &s.FinishedAt,
|
||||
&s.DurationMs, &s.Error); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if rowid > max {
|
||||
max = rowid
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out, max, rows.Err()
|
||||
}
|
||||
|
||||
func (h *DagRunHub) broadcast(msg wsDagMessage) {
|
||||
h.mu.Lock()
|
||||
subs := make([]*dagSubscriber, 0, len(h.subscribers))
|
||||
for s := range h.subscribers {
|
||||
subs = append(subs, s)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
for _, s := range subs {
|
||||
select {
|
||||
case s.out <- msg:
|
||||
default:
|
||||
log.Printf("[dagws] dropping frame for slow subscriber")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDagRunsWS upgrade WS y gestiona lifecycle.
|
||||
// Endpoint: GET /api/ws/dagruns
|
||||
func handleDagRunsWS(hub *DagRunHub) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[dagws] accept: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close(websocket.StatusInternalError, "closing")
|
||||
|
||||
ctx, cancel := context.WithCancel(r.Context())
|
||||
defer cancel()
|
||||
|
||||
sub := &dagSubscriber{
|
||||
conn: conn,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
out: make(chan wsDagMessage, 64),
|
||||
}
|
||||
hub.register(sub)
|
||||
defer hub.unregister(sub)
|
||||
|
||||
dags, runs, steps, wm, err := hub.fetchSnapshot()
|
||||
if err != nil {
|
||||
log.Printf("[dagws] snapshot: %v", err)
|
||||
conn.Close(websocket.StatusInternalError, "snapshot failed")
|
||||
return
|
||||
}
|
||||
hub.setWatermark(wm)
|
||||
initial := wsDagMessage{
|
||||
Type: "snapshot",
|
||||
Watermark: wm,
|
||||
Dags: dags,
|
||||
Runs: runs,
|
||||
Steps: steps,
|
||||
ServerTime: time.Now().Unix(),
|
||||
}
|
||||
if err := wsjson.Write(ctx, conn, initial); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
readErr := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
var cmd wsDagClientCmd
|
||||
if err := wsjson.Read(ctx, conn, &cmd); err != nil {
|
||||
readErr <- err
|
||||
return
|
||||
}
|
||||
if cmd.Watermark.Runs > 0 || cmd.Watermark.Steps > 0 {
|
||||
runs, steps, wm, err := hub.fetchDelta(cmd.Watermark)
|
||||
if err == nil && (len(runs) > 0 || len(steps) > 0) {
|
||||
hub.setWatermark(wm)
|
||||
select {
|
||||
case sub.out <- wsDagMessage{
|
||||
Type: "delta",
|
||||
Watermark: wm,
|
||||
Runs: runs,
|
||||
Steps: steps,
|
||||
ServerTime: time.Now().Unix(),
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-readErr:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case msg, ok := <-sub.out:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
wctx, wcancel := context.WithTimeout(ctx, dagWSBroadcastTimeout)
|
||||
err := wsjson.Write(wctx, conn, msg)
|
||||
wcancel()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-20
@@ -156,22 +156,49 @@ func (e *Executor) ExecuteDAG(ctx context.Context, dagPath string, trigger strin
|
||||
func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDefinition, step core.DagStep, daguEnvPath string, outputs map[string]string, mu *sync.Mutex) error {
|
||||
stepID := generateID()
|
||||
now := time.Now()
|
||||
|
||||
// Resolve command source: function (registry) takes precedence over command/script.
|
||||
var command string
|
||||
var stepFunctionID string
|
||||
var fnRegistryRoot string
|
||||
if step.Function != "" {
|
||||
stepFunctionID = step.Function
|
||||
fnRegistryRoot = os.Getenv("FN_REGISTRY_ROOT")
|
||||
if fnRegistryRoot == "" {
|
||||
fnRegistryRoot = "/home/lucas/fn_registry"
|
||||
}
|
||||
fnBin := os.Getenv("FN_BIN")
|
||||
if fnBin == "" {
|
||||
fnBin = fnRegistryRoot + "/fn"
|
||||
}
|
||||
parts := []string{fnBin, "run", step.Function}
|
||||
parts = append(parts, step.Args...)
|
||||
command = strings.Join(parts, " ")
|
||||
} else if step.Command != "" {
|
||||
command = step.Command
|
||||
} else if step.Script != "" {
|
||||
command = step.Script
|
||||
}
|
||||
|
||||
e.store.InsertStepResult(&store.DagStepResult{
|
||||
ID: stepID,
|
||||
RunID: runID,
|
||||
StepName: stepName(step),
|
||||
Status: "running",
|
||||
StartedAt: &now,
|
||||
ID: stepID,
|
||||
RunID: runID,
|
||||
StepName: stepName(step),
|
||||
FunctionID: stepFunctionID,
|
||||
Status: "running",
|
||||
StartedAt: &now,
|
||||
})
|
||||
|
||||
// Build environment.
|
||||
env := buildStepEnv(dag, step, daguEnvPath, outputs)
|
||||
|
||||
// Determine command.
|
||||
command := step.Command
|
||||
if command == "" && step.Script != "" {
|
||||
command = step.Script
|
||||
// For function: steps, force FN_REGISTRY_ROOT into env so `fn run`
|
||||
// resolves the canonical registry.db (not whatever lives at the spawn cwd).
|
||||
// Prevents the apps/dag_engine/registry.db stale-shadow bug (2026-05-16).
|
||||
if stepFunctionID != "" {
|
||||
env = append(env, "FN_REGISTRY_ROOT="+fnRegistryRoot)
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
|
||||
return nil
|
||||
@@ -182,11 +209,15 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
|
||||
command = resolveStepRefs(command, outputs)
|
||||
mu.Unlock()
|
||||
|
||||
// Determine working directory.
|
||||
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
|
||||
// so `fn` resolves registry.db correctly via go.mod walk-up.
|
||||
dir := step.Dir
|
||||
if dir == "" {
|
||||
dir = dag.WorkingDir
|
||||
}
|
||||
if dir == "" && stepFunctionID != "" {
|
||||
dir = fnRegistryRoot
|
||||
}
|
||||
|
||||
shell := step.Shell
|
||||
if shell == "" {
|
||||
@@ -326,14 +357,15 @@ func generateID() string {
|
||||
|
||||
// DagInfo summarizes a DAG file for listing.
|
||||
type DagInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Schedule []string `json:"schedule,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
FilePath string `json:"file_path"`
|
||||
Valid bool `json:"valid"`
|
||||
LastRun *store.DagRun `json:"last_run,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Schedule []string `json:"schedule,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
FilePath string `json:"file_path"`
|
||||
Valid bool `json:"valid"`
|
||||
LastRun *store.DagRun `json:"last_run,omitempty"`
|
||||
LastRuns []store.DagRun `json:"last_runs,omitempty"` // 5 mas recientes (mas reciente primero)
|
||||
}
|
||||
|
||||
// ListDAGs scans a directory for YAML files and returns parsed DAG info.
|
||||
@@ -379,10 +411,11 @@ func (e *Executor) ListDAGs() ([]DagInfo, error) {
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
// Attach last run info.
|
||||
runs, _, _ := e.store.ListRuns(dag.Name, 1, 0)
|
||||
// Attach last 5 runs (most recent first).
|
||||
runs, _, _ := e.store.ListRuns(dag.Name, 5, 0)
|
||||
if len(runs) > 0 {
|
||||
info.LastRun = &runs[0]
|
||||
info.LastRuns = runs
|
||||
}
|
||||
|
||||
dags = append(dags, info)
|
||||
|
||||
@@ -9,12 +9,63 @@ import {
|
||||
Paper,
|
||||
Alert,
|
||||
Loader,
|
||||
CopyButton,
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
Code,
|
||||
} from "@mantine/core";
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react";
|
||||
import { getRun } from "../api";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { StepTimeline } from "../components/StepTimeline";
|
||||
import type { RunDetail as RunDetailType } from "../types";
|
||||
import type { RunDetail as RunDetailType, DagStepResult, DagRun } from "../types";
|
||||
|
||||
function buildLogText(run: DagRun, steps: DagStepResult[]): string {
|
||||
const lines: string[] = [];
|
||||
const started = run.StartedAt ? new Date(run.StartedAt) : null;
|
||||
const finished = run.FinishedAt ? new Date(run.FinishedAt) : null;
|
||||
const durationMs =
|
||||
started && finished ? finished.getTime() - started.getTime() : null;
|
||||
|
||||
lines.push(`=== DAG run ${run.ID} ===`);
|
||||
lines.push(`dag: ${run.DagName}`);
|
||||
lines.push(`path: ${run.DagPath}`);
|
||||
lines.push(`status: ${run.Status}`);
|
||||
lines.push(`trigger: ${run.Trigger}`);
|
||||
lines.push(`started: ${started ? started.toISOString() : "-"}`);
|
||||
lines.push(`finished: ${finished ? finished.toISOString() : "-"}`);
|
||||
lines.push(
|
||||
`duration: ${durationMs !== null ? `${durationMs} ms` : "running..."}`
|
||||
);
|
||||
if (run.Error) {
|
||||
lines.push("");
|
||||
lines.push("run error:");
|
||||
lines.push(run.Error);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(`--- steps (${steps.length}) ---`);
|
||||
for (const s of steps) {
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`[${s.Status}] ${s.StepName} exit=${s.ExitCode} ${s.DurationMs}ms`
|
||||
);
|
||||
if (s.StartedAt) lines.push(` started: ${s.StartedAt}`);
|
||||
if (s.FinishedAt) lines.push(` finished: ${s.FinishedAt}`);
|
||||
if (s.Error) {
|
||||
lines.push(" error:");
|
||||
lines.push(s.Error.split("\n").map((l) => " " + l).join("\n"));
|
||||
}
|
||||
if (s.Stdout) {
|
||||
lines.push(" stdout:");
|
||||
lines.push(s.Stdout.split("\n").map((l) => " " + l).join("\n"));
|
||||
}
|
||||
if (s.Stderr) {
|
||||
lines.push(" stderr:");
|
||||
lines.push(s.Stderr.split("\n").map((l) => " " + l).join("\n"));
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function RunDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -100,6 +151,41 @@ export function RunDetail() {
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" withBorder>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Title order={4}>Logs</Title>
|
||||
<CopyButton value={buildLogText(run, steps || [])} timeout={1500}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={copied ? "Copiado" : "Copiar log completo"}
|
||||
withArrow
|
||||
position="left"
|
||||
>
|
||||
<ActionIcon
|
||||
variant={copied ? "filled" : "light"}
|
||||
color={copied ? "teal" : "blue"}
|
||||
onClick={copy}
|
||||
aria-label="Copiar logs"
|
||||
>
|
||||
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
<Code
|
||||
block
|
||||
style={{
|
||||
maxHeight: 480,
|
||||
overflow: "auto",
|
||||
whiteSpace: "pre",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{buildLogText(run, steps || [])}
|
||||
</Code>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
fn-registry v0.0.0-00010101000000-000000000000
|
||||
github.com/mattn/go-sqlite3 v1.14.37
|
||||
nhooyr.io/websocket v1.8.17
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -28,19 +29,21 @@ require (
|
||||
github.com/marcboeker/go-duckdb v1.8.5 // indirect
|
||||
github.com/paulmach/orb v0.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+18
-10
@@ -111,44 +111,50 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
|
||||
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -166,3 +172,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
|
||||
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
|
||||
@@ -30,10 +30,10 @@ func handleGetDag(executor *Executor) http.HandlerFunc {
|
||||
runs, _, _ := executor.store.ListRuns(dag.Name, 10, 0)
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"info": info,
|
||||
"dag": dag,
|
||||
"validation": validation,
|
||||
"runs": runs,
|
||||
"info": info,
|
||||
"dag": dag,
|
||||
"validation": validation,
|
||||
"recent_runs": runs,
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -286,6 +286,7 @@ func cmdServer(args []string) {
|
||||
|
||||
executor := NewExecutor(db, cfg.DagsDir)
|
||||
scheduler := NewScheduler(executor, cfg.DagsDir)
|
||||
dagRunHub := NewDagRunHub(db, executor)
|
||||
|
||||
// Prepare frontend FS.
|
||||
var feFS iofs.FS
|
||||
@@ -303,7 +304,7 @@ func cmdServer(args []string) {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
RegisterAPI(mux, executor, scheduler, feFS)
|
||||
RegisterAPI(mux, executor, scheduler, dagRunHub, feFS)
|
||||
|
||||
handler := corsMiddleware(loggingMiddleware(mux))
|
||||
|
||||
|
||||
@@ -2,15 +2,43 @@ package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
//go:embed migrations/001_init.sql
|
||||
var migrationSQL string
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
// applyMigrations executes every embedded migrations/*.sql in order.
|
||||
// Each statement is idempotent (IF NOT EXISTS / ADD COLUMN). Duplicate-column
|
||||
// errors from re-running ALTER TABLE ADD COLUMN are tolerated.
|
||||
func applyMigrations(conn *sql.DB) error {
|
||||
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Strings(files)
|
||||
for _, f := range files {
|
||||
b, err := migrationsFS.ReadFile(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: read: %w", f, err)
|
||||
}
|
||||
if _, err := conn.Exec(string(b)); err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate column") ||
|
||||
strings.Contains(err.Error(), "already exists") {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s: %w", f, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DB wraps a SQLite connection for DAG run persistence.
|
||||
type DB struct {
|
||||
@@ -24,7 +52,7 @@ func Open(path string) (*DB, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: open %s: %w", path, err)
|
||||
}
|
||||
if _, err := conn.Exec(migrationSQL); err != nil {
|
||||
if err := applyMigrations(conn); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("store: migrate: %w", err)
|
||||
}
|
||||
@@ -36,18 +64,24 @@ func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// Conn exposes the underlying *sql.DB for read-only queries from other
|
||||
// packages (e.g. WS hub in events.go). Do not Close() the returned conn.
|
||||
func (db *DB) Conn() *sql.DB {
|
||||
return db.conn
|
||||
}
|
||||
|
||||
// --- DagRun CRUD ---
|
||||
|
||||
// DagRun mirrors infra.DagRun for the store layer.
|
||||
type DagRun struct {
|
||||
ID string
|
||||
DagName string
|
||||
DagPath string
|
||||
Status string
|
||||
Trigger string
|
||||
StartedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
Error string
|
||||
ID string `json:"id"`
|
||||
DagName string `json:"dag_name"`
|
||||
DagPath string `json:"dag_path"`
|
||||
Status string `json:"status"`
|
||||
Trigger string `json:"trigger"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// CreateRun inserts a new run record.
|
||||
@@ -123,17 +157,18 @@ func (db *DB) ListRuns(dagName string, limit, offset int) ([]DagRun, int, error)
|
||||
|
||||
// DagStepResult mirrors infra.DagStepResult for the store layer.
|
||||
type DagStepResult struct {
|
||||
ID string
|
||||
RunID string
|
||||
StepName string
|
||||
Status string
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
StartedAt *time.Time
|
||||
FinishedAt *time.Time
|
||||
DurationMs int64
|
||||
Error string
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepName string `json:"step_name"`
|
||||
FunctionID string `json:"function_id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Stdout string `json:"stdout,omitempty"`
|
||||
Stderr string `json:"stderr,omitempty"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// InsertStepResult inserts a new step result.
|
||||
@@ -148,9 +183,9 @@ func (db *DB) InsertStepResult(r *DagStepResult) error {
|
||||
finishedAt = &s
|
||||
}
|
||||
_, err := db.conn.Exec(
|
||||
`INSERT INTO dag_step_results (id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
r.ID, r.RunID, r.StepName, r.Status, r.ExitCode, r.Stdout, r.Stderr,
|
||||
`INSERT INTO dag_step_results (id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
r.ID, r.RunID, r.StepName, r.FunctionID, r.Status, r.ExitCode, r.Stdout, r.Stderr,
|
||||
startedAt, finishedAt, r.DurationMs, r.Error,
|
||||
)
|
||||
return err
|
||||
@@ -173,7 +208,7 @@ func (db *DB) UpdateStepResult(id, status string, exitCode int, stdout, stderr s
|
||||
// ListStepResults returns all step results for a given run.
|
||||
func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
|
||||
rows, err := db.conn.Query(
|
||||
`SELECT id, run_id, step_name, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
|
||||
`SELECT id, run_id, step_name, function_id, status, exit_code, stdout, stderr, started_at, finished_at, duration_ms, error
|
||||
FROM dag_step_results WHERE run_id=? ORDER BY started_at ASC`, runID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -185,7 +220,7 @@ func (db *DB) ListStepResults(runID string) ([]DagStepResult, error) {
|
||||
for rows.Next() {
|
||||
var r DagStepResult
|
||||
var startedAt, finishedAt sql.NullString
|
||||
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.Status, &r.ExitCode,
|
||||
if err := rows.Scan(&r.ID, &r.RunID, &r.StepName, &r.FunctionID, &r.Status, &r.ExitCode,
|
||||
&r.Stdout, &r.Stderr, &startedAt, &finishedAt, &r.DurationMs, &r.Error); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+15
-1
@@ -2,6 +2,7 @@
|
||||
name: shaders_lab
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: 0.1.0
|
||||
description: "Live GLSL shader playground con DAG pipeline. Editor de codigo con compilacion en caliente, panel DAG con paleta de generadores/filtros/output, dos canvas (Code y DAG), parseo de uniforms anotados (// @slider, @color, @xy) que se convierten en controles, persistencia de generators en shaders_lab.db, y guardado/carga de layouts ImGui."
|
||||
tags: [imgui, opengl, glsl, shaders, dag, live-coding, playground, sqlite]
|
||||
uses_functions:
|
||||
@@ -30,8 +31,11 @@ uses_types:
|
||||
- dag_types_cpp_gfx
|
||||
framework: "imgui"
|
||||
entry_point: "main.cpp"
|
||||
dir_path: "cpp/apps/shaders_lab"
|
||||
dir_path: "apps/shaders_lab"
|
||||
repo_url: ""
|
||||
icon:
|
||||
phosphor: "palette"
|
||||
accent: "#ea580c"
|
||||
---
|
||||
|
||||
## Arquitectura
|
||||
@@ -102,3 +106,13 @@ cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w6
|
||||
- El boton "Save as generator" valida snake_case, evita colisionar con
|
||||
builtins, traduce con `code_to_generator`, persiste con `shaderlab_db_save_generator`,
|
||||
y registra el nodo nuevo en el catalogo en vivo (`dag_register_node`).
|
||||
|
||||
|
||||
## Capability growth log
|
||||
|
||||
Una linea por bump SemVer. Bump-type segun `.claude/commands/version.md`:
|
||||
- `major`: breaking observable (CLI args, schema BBDD propia, formato wire).
|
||||
- `minor`: feature aditiva (nuevo panel, endpoint, opcion).
|
||||
- `patch`: bugfix sin cambio observable.
|
||||
|
||||
- v0.1.0 (2026-05-18) — baseline.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: chrome_load_extensions
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: browser
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "chrome_load_extensions [--port N] [--profile DIR] --ext PATH [--ext PATH ...] [--proxy URL] [--url URL]"
|
||||
description: "Lanza Chrome con extensiones unpacked via --load-extension (WSL2→Windows chrome.exe, paths traducidos, join sin echo, setsid anti-exit-144). OJO: --load-extension SOLO funciona en Chrome for Testing/Chromium/Dev. En Chrome STABLE 138+ esta DESACTIVADO (feature DisableLoadExtensionCommandLineSwitch + bloqueo duro en 148) y carga 0 extensiones aunque el cmdline sea correcto. Para Chrome stable usar install via Web Store (1-clic, persiste en perfil) o enterprise policy ExtensionInstallForcelist (requiere HKLM/HKCU Policies escribible — denegado en maquinas gestionadas)."
|
||||
tags: [chrome, cdp, browser, extensions, wsl2, navegator]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
params:
|
||||
- name: "--port N"
|
||||
desc: "Puerto de remote debugging CDP. Default: 9222."
|
||||
- name: "--profile DIR"
|
||||
desc: "Chrome user-data-dir. Acepta ruta Windows (C:\\...) o ruta WSL/Linux (se traduce via wslpath -w). Default: C:\\Users\\<USERNAME>\\AppData\\Local\\fn-chrome-cdp-profile (WSL2) o /tmp/fn-chrome-cdp-profile (Linux nativo)."
|
||||
- name: "--ext PATH"
|
||||
desc: "Ruta a un directorio de extensión unpacked. Repetible. Acepta ruta Windows (se pasa intacta) o ruta WSL/Linux (se traduce via wslpath -w). Obligatorio al menos uno."
|
||||
- name: "--proxy URL"
|
||||
desc: "Proxy opcional, ej. http://127.0.0.1:8889. Agrega --proxy-server=URL a Chrome."
|
||||
- name: "--url URL"
|
||||
desc: "URL inicial opcional para abrir con --new-window."
|
||||
output: "PID del proceso Chrome lanzado (stdout). Mensajes de estado en stderr. CDP listo en 127.0.0.1:<port>."
|
||||
file_path: "bash/functions/browser/chrome_load_extensions.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/browser/chrome_load_extensions.sh
|
||||
|
||||
chrome_load_extensions \
|
||||
--port 9222 \
|
||||
--profile 'C:\Users\lucas\AppData\Local\fn-chrome-cdp-profile' \
|
||||
--ext 'C:\Users\lucas\hls-dl-ext' \
|
||||
--ext 'C:\Users\lucas\ubol' \
|
||||
--proxy http://127.0.0.1:8889 \
|
||||
--url https://www.gnularetro.cc/
|
||||
```
|
||||
|
||||
Sin proxy ni URL, sólo extensiones:
|
||||
|
||||
```bash
|
||||
source bash/functions/browser/chrome_load_extensions.sh
|
||||
|
||||
pid=$(chrome_load_extensions \
|
||||
--ext '/home/lucas/dev/hls-dl-ext' \
|
||||
--ext '/home/lucas/dev/ubol')
|
||||
# Paths WSL traducidos automáticamente a Windows.
|
||||
# CDP listo en 127.0.0.1:9222.
|
||||
echo "Chrome PID: $pid"
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites Chrome CDP con extensiones unpacked cargadas (HLS downloader, uBlock Origin, extensiones en desarrollo) y `chrome_launch_go_browser` no sirve porque hardcodea `--disable-extensions`. WSL2→Windows. Ideal para sesiones de navegator con proxy + extensión activa.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **MUERTO en Chrome STABLE 138+ (validado 2026-05-30, Chrome 148)**: `--load-extension` NO carga nada en el canal stable, ni con `--disable-extensions-except` ni con `--disable-features=DisableLoadExtensionCommandLineSwitch`. `chrome://version` muestra el flag correcto pero `chrome://extensions` sale vacío. Google lo bloqueó duro en stable. La función SOLO sirve en **Chrome for Testing / Chromium / Dev/Canary**, donde el switch sigue activo. Para stable: ver opciones abajo.
|
||||
- **Instalar en Chrome STABLE (las que SÍ funcionan)**:
|
||||
1. **Web Store 1-clic** — abre la página del store en el perfil CDP, el humano da "Añadir a Chrome". Persiste en el perfil para siempre (futuros lanzamientos ya con la extensión, sin flags). El popup de confirmación es UI del navegador (no DOM) → NO es CDP-clickable, requiere gesto humano. Único método no-admin que persiste por-perfil.
|
||||
2. **Enterprise policy** `ExtensionInstallForcelist` (HKCU/HKLM `\Software\Policies\Google\Chrome`) — force-install sin clic desde el store, browser-wide. El key `Policies\Google\Chrome` puede dar "Access denied" al escribir (visto 2026-05-30 incluso en máquina personal vía reg.exe/PowerShell desde WSL — Chrome/Windows protege el subárbol Policies). Si funciona, requiere relanzar Chrome para que descargue del store. Método global (afecta todos los perfiles).
|
||||
3. Extensiones **unpacked custom** (no en store, ej. un HLS downloader propio) en stable: no hay vía no-admin. Empaquetar a CRX + self-host `update_url` + policy, o usar Chrome for Testing. A menudo innecesario si la lógica vive fuera (ej. `grab_stream.py` descarga sin extensión).
|
||||
- **Combo flags (solo Chrome for Testing/dev)**: requiere AMBOS `--load-extension=p1,p2` Y `--disable-extensions-except=p1,p2` juntos + `--disable-features=DisableLoadExtensionCommandLineSwitch`. **NUNCA `--disable-extensions`** (desactiva todo).
|
||||
- **join sin `echo`**: rutas Windows `C:\Users\...` tienen `\U`; el `echo` de zsh (o sh con xpg_echo) lo interpreta como escape unicode y trunca la ruta a `C:`. La función usa acumulador `+=`, no `echo`. Verificable en `chrome://version` (debe verse el path completo, no `--load-extension=C:`).
|
||||
- **exit 144 en Bash tool**: si el proceso Chrome retiene el pipe stdout, la herramienta devuelve exit 144. Esta función lanza con `setsid ... </dev/null >log 2>&1 &` + `disown` para desacoplar completamente. El log queda en `/tmp/chrome_ext_<port>.log`.
|
||||
- **WSL2: traducir paths con `wslpath -w`**: los paths de `--ext` y `--profile` que sean rutas Linux se traducen automáticamente. Las rutas Windows (`C:\...`) se pasan intactas. `wslpath` debe estar disponible (estándar en WSL2 desde Windows 10 1903+).
|
||||
- **Perfil ya abierto**: si Chrome ya tiene ese perfil abierto, relanzar añade una ventana extra a la misma instancia. La función detecta si CDP ya responde en el puerto y avisa por stderr, pero procede igualmente.
|
||||
- **Web Store vs unpacked**: instalar extensiones desde la Web Store (un clic) persiste en el perfil sin necesidad de flags y sobrevive reinicios. Esta función es para extensiones unpacked en desarrollo o que no están en la Web Store. Si usas ambas, los flags no interfieren con las instaladas del store.
|
||||
- **zsh globbing**: `--remote-allow-origins=*` está dentro de comillas en la función, no se expande. Si lo pasas desde la línea de comandos, entrecomillarlo.
|
||||
- **Proxy + extensión**: si usas proxy para captura de tráfico (Burp, mitmproxy, gost), el proxy se aplica a toda la sesión Chrome, incluyendo el tráfico de las extensiones.
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
# chrome_load_extensions — lanza Chrome (WSL2→Windows chrome.exe) con extensiones unpacked cargadas en un perfil CDP.
|
||||
# Chrome 148+: requiere --load-extension=<paths> Y --disable-extensions-except=<same paths> juntos.
|
||||
# NUNCA pasar --disable-extensions (desactiva todo, incluyendo las que quieres cargar).
|
||||
|
||||
chrome_load_extensions() {
|
||||
local port=9222
|
||||
local profile=""
|
||||
local proxy=""
|
||||
local url=""
|
||||
local -a ext_paths=()
|
||||
|
||||
# --- Parse args ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port)
|
||||
port="$2"; shift 2 ;;
|
||||
--profile)
|
||||
profile="$2"; shift 2 ;;
|
||||
--ext)
|
||||
ext_paths+=("$2"); shift 2 ;;
|
||||
--proxy)
|
||||
proxy="$2"; shift 2 ;;
|
||||
--url)
|
||||
url="$2"; shift 2 ;;
|
||||
--*)
|
||||
echo "chrome_load_extensions: flag desconocido: $1" >&2; return 1 ;;
|
||||
*)
|
||||
# Positional = extra ext path
|
||||
ext_paths+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#ext_paths[@]} -eq 0 ]]; then
|
||||
echo "chrome_load_extensions: se requiere al menos un --ext PATH de extension unpacked" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- Detectar chrome.exe ---
|
||||
local chrome_bin=""
|
||||
if command -v chrome.exe &>/dev/null; then
|
||||
chrome_bin="chrome.exe"
|
||||
elif [[ -f "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" ]]; then
|
||||
chrome_bin="/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"
|
||||
elif [[ -f "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe" ]]; then
|
||||
chrome_bin="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
||||
else
|
||||
echo "chrome_load_extensions: chrome.exe no encontrado en PATH ni en rutas conocidas" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- Detectar WSL2 ---
|
||||
local wsl2=0
|
||||
if grep -qi 'microsoft\|wsl' /proc/version 2>/dev/null; then
|
||||
wsl2=1
|
||||
fi
|
||||
|
||||
# --- Traducir paths de extensiones a Windows si hace falta ---
|
||||
local -a win_ext_paths=()
|
||||
for p in "${ext_paths[@]}"; do
|
||||
if [[ $wsl2 -eq 1 ]] && [[ "$p" != [A-Za-z]:\\* ]]; then
|
||||
# Path Linux → traducir a Windows
|
||||
local win_p
|
||||
win_p=$(wslpath -w "$p" 2>/dev/null) || {
|
||||
echo "chrome_load_extensions: wslpath -w '$p' falló" >&2
|
||||
return 1
|
||||
}
|
||||
win_ext_paths+=("$win_p")
|
||||
else
|
||||
win_ext_paths+=("$p")
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Resolver perfil ---
|
||||
if [[ -z "$profile" ]]; then
|
||||
# Default: perfil canónico fn-chrome-cdp-profile en Windows
|
||||
local win_user="${USERNAME:-${USER:-lucas}}"
|
||||
if [[ $wsl2 -eq 1 ]]; then
|
||||
profile="C:\\Users\\${win_user}\\AppData\\Local\\fn-chrome-cdp-profile"
|
||||
else
|
||||
profile="/tmp/fn-chrome-cdp-profile"
|
||||
fi
|
||||
elif [[ $wsl2 -eq 1 ]] && [[ "$profile" != [A-Za-z]:\\* ]]; then
|
||||
# Path Linux del perfil → traducir a Windows
|
||||
profile=$(wslpath -w "$profile" 2>/dev/null) || {
|
||||
echo "chrome_load_extensions: wslpath -w '$profile' falló" >&2
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# --- Construir lista de paths separada por coma (para Chrome) ---
|
||||
# Chrome usa coma como separador en --load-extension y --disable-extensions-except.
|
||||
# NO usar `echo` para el join: rutas Windows como C:\Users tienen \U, y el echo de
|
||||
# zsh (o sh con xpg_echo) interpreta \U como escape unicode y trunca la ruta a "C:".
|
||||
# Acumulador con += y printf-safe, sin interpretacion de backslashes.
|
||||
local ext_list=""
|
||||
local p
|
||||
for p in "${win_ext_paths[@]}"; do
|
||||
ext_list+="${ext_list:+,}${p}"
|
||||
done
|
||||
|
||||
# --- Construir args de Chrome ---
|
||||
local -a args=(
|
||||
"--remote-debugging-port=${port}"
|
||||
"--user-data-dir=${profile}"
|
||||
"--no-first-run"
|
||||
"--no-default-browser-check"
|
||||
"--remote-allow-origins=*"
|
||||
"--load-extension=${ext_list}"
|
||||
"--disable-extensions-except=${ext_list}"
|
||||
# Chrome 137+ activa por defecto el feature DisableLoadExtensionCommandLineSwitch,
|
||||
# que IGNORA silenciosamente --load-extension. Hay que desactivarlo o las
|
||||
# extensiones unpacked no cargan (chrome://extensions sale vacio).
|
||||
"--disable-features=DisableLoadExtensionCommandLineSwitch"
|
||||
)
|
||||
|
||||
# WSL2: bind en 0.0.0.0 para que sea accesible desde la red WSL
|
||||
if [[ $wsl2 -eq 1 ]]; then
|
||||
args+=("--remote-debugging-address=0.0.0.0")
|
||||
fi
|
||||
|
||||
if [[ -n "$proxy" ]]; then
|
||||
args+=("--proxy-server=${proxy}")
|
||||
fi
|
||||
|
||||
if [[ -n "$url" ]]; then
|
||||
args+=("--new-window" "$url")
|
||||
fi
|
||||
|
||||
# --- Revisar si CDP ya responde en el puerto ---
|
||||
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
|
||||
echo "chrome_load_extensions: CDP ya activo en puerto ${port}; lanzando ventana extra" >&2
|
||||
fi
|
||||
|
||||
# --- Lanzar Chrome desacoplado del proceso padre ---
|
||||
# setsid + redirección evita el exit 144 en el Bash tool (el pipe no queda retenido).
|
||||
setsid "$chrome_bin" "${args[@]}" </dev/null >"/tmp/chrome_ext_${port}.log" 2>&1 &
|
||||
local chrome_pid=$!
|
||||
disown "$chrome_pid"
|
||||
|
||||
echo "chrome_load_extensions: Chrome lanzado PID=${chrome_pid} puerto=${port}" >&2
|
||||
|
||||
# --- Esperar a que CDP esté listo (hasta 15 segundos) ---
|
||||
local deadline=$(( $(date +%s) + 15 ))
|
||||
local ready=0
|
||||
while [[ $(date +%s) -lt $deadline ]]; do
|
||||
if curl -sf --max-time 1 "http://127.0.0.1:${port}/json/version" &>/dev/null; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ $ready -eq 1 ]]; then
|
||||
echo "chrome_load_extensions: CDP listo en 127.0.0.1:${port}"
|
||||
else
|
||||
echo "chrome_load_extensions: advertencia — CDP no respondió en 15s en puerto ${port}; Chrome puede estar iniciando lentamente" >&2
|
||||
fi
|
||||
|
||||
echo "$chrome_pid"
|
||||
}
|
||||
@@ -38,7 +38,7 @@ if [[ -n "$matches" ]]; then
|
||||
fi
|
||||
|
||||
# Escanear repo especifico
|
||||
scan_secrets_in_dirty /home/lucas/fn_registry
|
||||
scan_secrets_in_dirty $HOME/fn_registry
|
||||
```
|
||||
|
||||
## Patrones detectados
|
||||
|
||||
@@ -6,16 +6,21 @@
|
||||
scan_secrets_in_dirty() {
|
||||
local repo_dir="${1:-.}"
|
||||
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
# Accept both regular repos (.git is a directory) and worktrees (.git is a
|
||||
# file containing "gitdir: ..." pointer).
|
||||
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
|
||||
echo "scan_secrets_in_dirty: '$repo_dir' no es un repo git" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Listar archivos modificados o nuevos (excluyendo borrados)
|
||||
# y filtrar por patron de secret en el nombre del archivo
|
||||
# y filtrar por patron de secret en el nombre del archivo.
|
||||
# Excluye extensiones de codigo (sh/go/py/ts/md/etc) para no marcar el
|
||||
# propio scanner ni docs que hablen de "secret"/"token".
|
||||
git -C "$repo_dir" status --porcelain \
|
||||
| awk '{print $NF}' \
|
||||
| grep -E '(^|/)(\.env(\..*)?$|.*credentials.*|.*\.key$|.*\.pem$|id_rsa.*|.*secret.*|.*token.*\.txt$)' \
|
||||
| grep -Ev '\.(sh|go|py|ts|tsx|js|jsx|md|rs|cpp|h|hpp|c|java|rb|html|css)$' \
|
||||
|| true
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: deploy_cpp_exe_to_windows
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "deploy_cpp_exe_to_windows(app_name: string, app_dir: string) -> void"
|
||||
description: "Copia el .exe de Windows (compilado por build_cpp_windows) y sus assets al escritorio de Windows /mnt/c/Users/lucas/Desktop/apps/<APP>/. Mata el proceso si esta corriendo (taskkill.exe pre-autorizado), copia DLLs, sincroniza assets/ y enrichers/ con rsync, maneja runtime Python embebido si python_runtime: true en app.md, y copia extras gx-cli. Preserva siempre local_files/ (estado del usuario)."
|
||||
@@ -22,14 +22,14 @@ params:
|
||||
- name: app_name
|
||||
desc: "Nombre de la app (ej: chart_demo). Se usa para localizar cpp/build/windows/apps/<app>/<app>.exe y el directorio destino Desktop/apps/<app>/."
|
||||
- name: app_dir
|
||||
desc: "Ruta absoluta al directorio fuente de la app (ej: /home/lucas/fn_registry/cpp/apps/chart_demo). Se usa para localizar enrichers/, runtime/ y app.md."
|
||||
desc: "Ruta absoluta al directorio fuente de la app (ej: $HOME/fn_registry/cpp/apps/chart_demo). Se usa para localizar enrichers/, runtime/ y app.md."
|
||||
output: "Copia archivos al escritorio de Windows. Imprime 'OK: <app> -> <dest>' en stdout. Si local_files/ existe, imprime su tamanio. Errores fatales a stderr con exit 1."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
deploy_cpp_exe_to_windows "chart_demo" "/home/lucas/fn_registry/cpp/apps/chart_demo"
|
||||
deploy_cpp_exe_to_windows "chart_demo" "$HOME/fn_registry/cpp/apps/chart_demo"
|
||||
# OK: chart_demo -> /mnt/c/Users/lucas/Desktop/apps/chart_demo
|
||||
|
||||
# Con rutas custom via env vars
|
||||
@@ -55,7 +55,7 @@ Desktop/apps/<APP>/
|
||||
|
||||
- `BUILD_WIN` — directorio de build Windows; default `$FN_REGISTRY_ROOT/cpp/build/windows`
|
||||
- `WIN_DESKTOP_APPS` — directorio destino; default `/mnt/c/Users/lucas/Desktop/apps`
|
||||
- `FN_REGISTRY_ROOT` — raiz del registry; default `/home/lucas/fn_registry`
|
||||
- `FN_REGISTRY_ROOT` — raiz del registry; default `$HOME/fn_registry`
|
||||
|
||||
## Notas
|
||||
|
||||
@@ -63,3 +63,8 @@ Desktop/apps/<APP>/
|
||||
- `rsync --delete` en assets/ y enrichers/ para mantener destino limpio.
|
||||
- Si `python_runtime: true` en `app.md` y `runtime/.lock` es mas antiguo que `app.md`, invoca `tools/freeze_python_runtime.sh` automaticamente.
|
||||
- `local_files/` jamas se toca: contiene estado per-PC del usuario (DBs SQLite, ImGui layouts, settings).
|
||||
|
||||
## Capability growth log
|
||||
|
||||
v1.1.0 (2026-05-17) — Bugfix: el `cp` del .exe no chequeaba exit status y la funcion reportaba OK aunque fallase por "Permission denied" (proceso aun vivo). Ahora: (1) tras `taskkill.exe`, poll de hasta 3s sobre `tasklist.exe` esperando muerte real del proceso; (2) `cp` envuelto en retry 5 veces con backoff 0.5s y re-taskkill entre intentos; (3) si los 5 intentos fallan, `return 1` (antes: silently continued).
|
||||
v1.0.0 — Initial.
|
||||
|
||||
@@ -12,7 +12,7 @@ deploy_cpp_exe_to_windows() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
local root="${FN_REGISTRY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
|
||||
local build_win="${BUILD_WIN:-$root/cpp/build/windows}"
|
||||
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
|
||||
|
||||
@@ -30,12 +30,38 @@ deploy_cpp_exe_to_windows() {
|
||||
mkdir -p "$dest" "$assets"
|
||||
|
||||
# --- 3. Pre-deploy: matar proceso si esta corriendo en Windows ---
|
||||
# Windows libera el file handle async tras taskkill. Hacemos poll hasta que
|
||||
# el proceso desaparezca de tasklist o se agote el timeout.
|
||||
if command -v taskkill.exe >/dev/null 2>&1; then
|
||||
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
|
||||
local i
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if ! tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null \
|
||||
| grep -qi "^${app}.exe"; then
|
||||
break
|
||||
fi
|
||||
sleep 0.3
|
||||
done
|
||||
fi
|
||||
|
||||
# --- 4. Copiar .exe al top level ---
|
||||
cp -v "$exe_src" "$dest/"
|
||||
# --- 4. Copiar .exe al top level con retry ---
|
||||
# Windows puede tener el archivo aun bloqueado momentaneamente; reintentar.
|
||||
local cp_ok=0
|
||||
local attempt
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cp -v "$exe_src" "$dest/"; then
|
||||
cp_ok=1
|
||||
break
|
||||
fi
|
||||
echo "deploy_cpp_exe_to_windows: cp intento $attempt fallo, reintentando..." >&2
|
||||
# Reintentar taskkill por si el proceso resucito o quedo zombie.
|
||||
taskkill.exe /IM "${app}.exe" /F >/dev/null 2>&1 || true
|
||||
sleep 0.5
|
||||
done
|
||||
if [ "$cp_ok" -ne 1 ]; then
|
||||
echo "ERROR: cp del .exe fallo tras 5 intentos. $exe_src -> $dest/" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- 5. DLLs al top level (Windows DLL search convention) ---
|
||||
find "$build_win/apps/$app" -maxdepth 1 -type f -name '*.dll' \
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: deploy_wails_exe_to_windows
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "deploy_wails_exe_to_windows <app_name> <app_dir>"
|
||||
description: "Copia el .exe de una app Wails desde <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso anterior (taskkill /F) y relanza la app via cmd.exe. Single-binary: no copia DLLs (Webview2 nativo en SO). Preserva local_files/ si existe."
|
||||
tags: ["wails", "windows", "deploy", "cross-compile", "mingw", "infra", "launch", "matrix-mas"]
|
||||
params:
|
||||
- name: app_name
|
||||
desc: "Nombre del binario sin extension (ej. matrix_client_pc). Debe coincidir con el nombre del .exe generado por wails build."
|
||||
- name: app_dir
|
||||
desc: "Ruta absoluta al directorio raiz de la app, donde vive build/bin/<app>.exe. Puede estar en projects/<project>/apps/<app>/ o apps/<app>/."
|
||||
output: "Imprime pasos en stderr. En stdout: ls -lh del .exe desplegado. Exit 0 si ok, exit 1 si build/bin/<app>.exe no existe o los args estan vacios."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests:
|
||||
- "args vacios devuelven error con mensaje de uso"
|
||||
- "app_dir inexistente devuelve exit 1"
|
||||
- "build/bin exe inexistente devuelve exit 1"
|
||||
test_file_path: "bash/functions/infra/deploy_wails_exe_to_windows_test.sh"
|
||||
file_path: "bash/functions/infra/deploy_wails_exe_to_windows.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/deploy_wails_exe_to_windows.sh
|
||||
|
||||
# Desplegar matrix_client_pc tras wails build -platform windows/amd64
|
||||
deploy_wails_exe_to_windows matrix_client_pc \
|
||||
$HOME/fn_registry/projects/element_agents/apps/matrix_client_pc
|
||||
```
|
||||
|
||||
Con override de destino:
|
||||
|
||||
```bash
|
||||
WIN_DESKTOP_APPS=/mnt/c/Users/lucas/Desktop/apps \
|
||||
deploy_wails_exe_to_windows matrix_admin_panel \
|
||||
$HOME/fn_registry/projects/element_agents/apps/matrix_admin_panel
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Tras un `wails build -platform windows/amd64` exitoso, para desplegar el binario compilado en Windows y relanzarlo en el mismo paso. Ideal en el ciclo de iteracion rapida: compilar → desplegar → ver cambios. Equivalente a `deploy_cpp_exe_to_windows_bash_infra` pero para apps Wails (single-binary sin DLLs extras).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **taskkill /F fuerza muerte** sin permitir guardado en disco. Las apps Wails persisten estado en keyring de Windows y AppData — este kill es seguro para ellas. Si la app tuviera autosave en progreso, se perderia (aceptable en ciclos de dev).
|
||||
- **UNC paths prohibidos en cmd.exe**: `cmd.exe /c start` debe ejecutarse con `cd` previo al directorio Windows (`/mnt/c/...`). Intentar lanzar con path `\\wsl.localhost\...` falla con "UNC paths are not supported as the current directory".
|
||||
- **cmd.exe start no bloquea**: la funcion espera 3s y verifica via `tasklist.exe`. Si la app cierra sola tras el arranque (error de inicio), el warn final lo indica pero no causa exit 1. Revisar logs en `%APPDATA%\<app>\` o `%LOCALAPPDATA%\<app>\`.
|
||||
- **Single-binary Wails**: no copiar DLLs. Webview2 es nativo del SO (Windows 10+ ya lo incluye). Si una version vieja de Windows no tuviera Webview2, la app falla al arrancar — solucion: instalar Webview2 Runtime en esa maquina.
|
||||
- **Build previo es responsabilidad del caller**: esta funcion NO compila. Para matrix_client_pc usa `-tags goolm` por el crypto de Matrix: `wails build -platform windows/amd64 -tags goolm`.
|
||||
- **WIN_DESKTOP_APPS override**: variable de entorno para cambiar el destino. Util en CI o maquinas con escritorio en otra ruta.
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy_wails_exe_to_windows — Copia el .exe de una app Wails compilado en
|
||||
# <app_dir>/build/bin/<app>.exe al escritorio de Windows, mata el proceso
|
||||
# anterior y relanza la app. Single-binary: no copia DLLs (Webview2 nativo SO).
|
||||
# Pre-authorized: taskkill.exe /F — idempotente, sin prompt.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
deploy_wails_exe_to_windows() {
|
||||
local app="${1:-}"
|
||||
local app_dir="${2:-}"
|
||||
|
||||
if [ -z "$app" ] || [ -z "$app_dir" ]; then
|
||||
echo "ERROR: uso: deploy_wails_exe_to_windows <app_name> <app_dir>" >&2
|
||||
echo " app_name: nombre del binario sin extension (ej. matrix_client_pc)" >&2
|
||||
echo " app_dir: ruta absoluta al directorio de la app (donde vive build/bin/)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local win_desktop_apps="${WIN_DESKTOP_APPS:-/mnt/c/Users/lucas/Desktop/apps}"
|
||||
|
||||
# --- 1. Validar que el .exe existe ---
|
||||
local exe_src="${app_dir}/build/bin/${app}.exe"
|
||||
if [ ! -f "$exe_src" ]; then
|
||||
echo "ERROR: no se encontro $exe_src" >&2
|
||||
echo "Compila primero con: wails build -platform windows/amd64" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- 2. Crear directorio destino (preserva local_files/ si existe) ---
|
||||
local dest="${win_desktop_apps}/${app}"
|
||||
mkdir -p "$dest"
|
||||
echo "[deploy_wails] dest: $dest" >&2
|
||||
|
||||
# --- 3. Matar proceso si esta corriendo en Windows ---
|
||||
# Pre-authorized. Wails apps usan AppData+keyring para estado, kill /F es seguro.
|
||||
if command -v taskkill.exe >/dev/null 2>&1; then
|
||||
echo "[deploy_wails] matando ${app}.exe si corre..." >&2
|
||||
taskkill.exe /IM "${app}.exe" /F 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# --- 4. Esperar a que Windows libere el file handle ---
|
||||
sleep 1
|
||||
|
||||
# --- 5. Copiar .exe (cp -f: overwrite sin borrar el directorio) ---
|
||||
echo "[deploy_wails] copiando ${app}.exe..." >&2
|
||||
cp -f "$exe_src" "$dest/${app}.exe"
|
||||
|
||||
# --- 6. Copiar appicon.ico si existe (opcional, algunos hubs lo leen) ---
|
||||
local icon_src="${app_dir}/appicon.ico"
|
||||
if [ -f "$icon_src" ]; then
|
||||
echo "[deploy_wails] copiando appicon.ico..." >&2
|
||||
cp -f "$icon_src" "$dest/appicon.ico"
|
||||
fi
|
||||
|
||||
# --- 7. Relanzar la app desde su dir Windows ---
|
||||
# Usar cmd.exe /c start desde el dir destino (no UNC paths — falla en cmd.exe).
|
||||
echo "[deploy_wails] lanzando ${app}.exe..." >&2
|
||||
(
|
||||
cd "$dest"
|
||||
cmd.exe /c start "" "${app}.exe"
|
||||
)
|
||||
|
||||
# --- 8. Dar tiempo a que el proceso arranque ---
|
||||
sleep 3
|
||||
|
||||
# --- 9. Verificar que el proceso esta corriendo ---
|
||||
if command -v tasklist.exe >/dev/null 2>&1; then
|
||||
local tasklist_out
|
||||
tasklist_out=$(tasklist.exe /FI "IMAGENAME eq ${app}.exe" /NH 2>/dev/null || true)
|
||||
if echo "$tasklist_out" | grep -qi "^${app}.exe"; then
|
||||
local pid
|
||||
pid=$(echo "$tasklist_out" | grep -i "^${app}.exe" | awk '{print $2}' | head -n1)
|
||||
echo "[deploy_wails] ${app}.exe corriendo con PID $pid" >&2
|
||||
else
|
||||
echo "WARN: ${app}.exe no aparece en tasklist tras el lanzamiento." >&2
|
||||
echo " Puede que la app cerro con error. Revisar AppData para logs." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 10. Resumen final en stdout ---
|
||||
ls -lh "$dest/${app}.exe"
|
||||
|
||||
echo "[deploy_wails] OK: ${app} deployado en $dest" >&2
|
||||
if [ -d "$dest/local_files" ]; then
|
||||
echo "[deploy_wails] local_files/ preservado: $(du -sh "$dest/local_files" | cut -f1)" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
deploy_wails_exe_to_windows "$@"
|
||||
fi
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para deploy_wails_exe_to_windows
|
||||
# Solo prueba validacion de argumentos y rutas — no ejecuta taskkill/cmd.exe reales.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/deploy_wails_exe_to_windows.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_eq() {
|
||||
local test_name="$1" expected="$2" got="$3"
|
||||
if [[ "$expected" == "$got" ]]; then
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected '$expected', got '$got'"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Test 1: args vacios devuelven error con mensaje de uso ---
|
||||
actual_exit=0
|
||||
deploy_wails_exe_to_windows >/dev/null 2>&1 || actual_exit=$?
|
||||
assert_eq "args vacios devuelven error con mensaje de uso" "1" "$actual_exit"
|
||||
|
||||
# --- Test 2: app_dir inexistente devuelve exit 1 ---
|
||||
actual_exit=0
|
||||
deploy_wails_exe_to_windows "myapp" "/tmp/nonexistent_dir_$(date +%s)" >/dev/null 2>&1 || actual_exit=$?
|
||||
assert_eq "app_dir inexistente devuelve exit 1" "1" "$actual_exit"
|
||||
|
||||
# --- Test 3: build/bin exe inexistente devuelve exit 1 ---
|
||||
TMPDIR_APP=$(mktemp -d)
|
||||
# Crear estructura de dir de app pero SIN el exe
|
||||
mkdir -p "$TMPDIR_APP/build/bin"
|
||||
actual_exit=0
|
||||
deploy_wails_exe_to_windows "myapp" "$TMPDIR_APP" >/dev/null 2>&1 || actual_exit=$?
|
||||
rm -rf "$TMPDIR_APP"
|
||||
assert_eq "build/bin exe inexistente devuelve exit 1" "1" "$actual_exit"
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
@@ -30,15 +30,15 @@ file_path: "bash/functions/infra/discover_git_repos.sh"
|
||||
source bash/functions/infra/discover_git_repos.sh
|
||||
|
||||
# Listar todos los repos bajo fn_registry
|
||||
discover_git_repos /home/lucas/fn_registry
|
||||
discover_git_repos $HOME/fn_registry
|
||||
|
||||
# Contar repos
|
||||
discover_git_repos /home/lucas/fn_registry | wc -l
|
||||
discover_git_repos $HOME/fn_registry | wc -l
|
||||
|
||||
# Iterar
|
||||
while IFS= read -r repo; do
|
||||
echo "Repo: $repo"
|
||||
done < <(discover_git_repos /home/lucas/fn_registry)
|
||||
done < <(discover_git_repos $HOME/fn_registry)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
@@ -33,7 +33,7 @@ file_path: "bash/functions/infra/docker_cp_file.sh"
|
||||
```bash
|
||||
source functions/infra/docker_cp_file.sh
|
||||
|
||||
result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db)
|
||||
result=$(docker_cp_file $HOME/fn_registry/registry.db metabase /registry.db)
|
||||
echo "$result"
|
||||
# {"local_size":524288,"remote_size":524288}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ file_path: "bash/functions/infra/git_auto_commit_dirty.sh"
|
||||
source bash/functions/infra/git_auto_commit_dirty.sh
|
||||
|
||||
# Commitear con mensaje automatico
|
||||
subject=$(git_auto_commit_dirty /home/lucas/fn_registry)
|
||||
subject=$(git_auto_commit_dirty $HOME/fn_registry)
|
||||
echo "Commit: $subject"
|
||||
|
||||
# Commitear con mensaje fijo
|
||||
|
||||
@@ -17,7 +17,7 @@ error_type: "error_go_core"
|
||||
imports: []
|
||||
example: |
|
||||
# Manual check
|
||||
bash bash/functions/infra/git_hook_audit_app_drift.sh /home/lucas/fn_registry/apps/kanban
|
||||
bash bash/functions/infra/git_hook_audit_app_drift.sh $HOME/fn_registry/apps/kanban
|
||||
|
||||
# Used by pre_commit_hook_install_bash_infra (v2 hook chain)
|
||||
file_path: bash/functions/infra/git_hook_audit_app_drift.sh
|
||||
|
||||
@@ -17,7 +17,9 @@ git_hook_audit_app_drift() {
|
||||
echo "ERROR: repo_dir required" >&2
|
||||
return 2
|
||||
fi
|
||||
if [[ ! -d "$repo_dir/.git" ]]; then
|
||||
# Accept both regular repos (.git is a directory) and worktrees (.git is a
|
||||
# file containing "gitdir: ..." pointer).
|
||||
if [[ ! -d "$repo_dir/.git" && ! -f "$repo_dir/.git" ]]; then
|
||||
echo "ERROR: $repo_dir is not a git repo" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
@@ -30,7 +30,7 @@ file_path: "bash/functions/infra/git_pull_with_stash.sh"
|
||||
source bash/functions/infra/git_pull_with_stash.sh
|
||||
|
||||
# Pullear repo con auto-stash
|
||||
status=$(git_pull_with_stash /home/lucas/fn_registry)
|
||||
status=$(git_pull_with_stash $HOME/fn_registry)
|
||||
echo "$status"
|
||||
# [pulled] fn_registry
|
||||
# o:
|
||||
@@ -46,7 +46,7 @@ while IFS= read -r repo; do
|
||||
if [[ "$result" == "[diverged]"* || "$result" == "[stash-conflict]"* ]]; then
|
||||
diverged+=("$result")
|
||||
fi
|
||||
done < <(discover_git_repos /home/lucas/fn_registry)
|
||||
done < <(discover_git_repos $HOME/fn_registry)
|
||||
|
||||
if [[ ${#diverged[@]} -gt 0 ]]; then
|
||||
echo "ATENCION: repos que requieren intervencion manual:"
|
||||
|
||||
@@ -30,7 +30,7 @@ file_path: "bash/functions/infra/git_push_if_ahead.sh"
|
||||
source bash/functions/infra/git_push_if_ahead.sh
|
||||
|
||||
# Pushear si hay commits locales
|
||||
status=$(git_push_if_ahead /home/lucas/fn_registry)
|
||||
status=$(git_push_if_ahead $HOME/fn_registry)
|
||||
echo "$status"
|
||||
# [push] fn_registry (master, 3 commits ahead)
|
||||
# o:
|
||||
@@ -39,7 +39,7 @@ echo "$status"
|
||||
# Iterar sobre multiples repos
|
||||
while IFS= read -r repo; do
|
||||
git_push_if_ahead "$repo"
|
||||
done < <(discover_git_repos /home/lucas/fn_registry)
|
||||
done < <(discover_git_repos $HOME/fn_registry)
|
||||
```
|
||||
|
||||
## Estados de salida
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: jupyter_mcp_serve
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: 1.0.0
|
||||
purity: impure
|
||||
error_type: "error_go_core"
|
||||
signature: "jupyter_mcp_serve.sh [--dry-run]"
|
||||
description: "Arranca (o reusa) un Jupyter Lab colaborativo en un puerto propio y lanza el Jupyter MCP server enganchado por stdio. Entrypoint robusto para la entrada 'jupyter' de .mcp.json: garantiza que el MCP SIEMPRE tiene servidor al que conectarse, sin depender de que haya un jupyter en 8888."
|
||||
tags: [notebook, jupyter, mcp, infra, launcher-glue]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
params:
|
||||
- name: "--dry-run"
|
||||
desc: "Opcional. Arranca/verifica jupyter pero NO hace exec del MCP; loguea el comando elegido. Para tests."
|
||||
output: "Proceso jupyter-mcp-server enganchado por stdio a un Jupyter Lab colaborativo local (127.0.0.1, puerto JUPYTER_MCP_PORT, default 8899). Logs en ~/.fn_jupyter_mcp/. stdout reservado al protocolo MCP."
|
||||
---
|
||||
|
||||
## Que hace
|
||||
|
||||
El MCP de Jupyter (datalayer `jupyter-mcp-server`) **no arranca jupyter**, solo se
|
||||
conecta a uno existente. Si la URL configurada no tiene jupyter detras, el MCP
|
||||
nunca conecta. En esta maquina `localhost:8888` es el **proxy HTTP del contenedor
|
||||
VPN gluetun**, no un jupyter — por eso el MCP fallaba siempre.
|
||||
|
||||
Este wrapper resuelve la cadena entera:
|
||||
|
||||
1. Localiza el venv (`python/.venv`) y los binarios `jupyter` + `jupyter-mcp-server`.
|
||||
2. Si ya hay un jupyter gestionado vivo en `127.0.0.1:$PORT` (`/api/status` = 200) lo reusa.
|
||||
3. Si no, arranca `jupyter lab` colaborativo detached (RTC via `jupyter-collaboration`),
|
||||
en `JUPYTER_MCP_ROOT` (default = raiz del repo, asi cualquier notebook del arbol es lanzable).
|
||||
4. Detecta el dialecto de CLI del MCP (`--document-url` nuevo / `--jupyter-url` viejo / env vars)
|
||||
y hace `exec` del MCP por `--transport stdio`.
|
||||
|
||||
Self-adapting: funciona aunque cambie la version de `jupyter-mcp-server`.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Como lo usa Claude Code (entrada en .mcp.json):
|
||||
# "jupyter": { "command": "bash", "args": ["bash/functions/infra/jupyter_mcp_serve.sh"] }
|
||||
|
||||
# Test manual (arranca jupyter en 8899, no lanza el MCP):
|
||||
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
|
||||
curl -s http://127.0.0.1:8899/api/status # {"started":..., "version":...}
|
||||
|
||||
# Cambiar puerto / raiz de notebooks:
|
||||
JUPYTER_MCP_PORT=8900 JUPYTER_MCP_ROOT=/home/enmanuel/fn_registry/analysis \
|
||||
bash bash/functions/infra/jupyter_mcp_serve.sh --dry-run
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras que el MCP de Jupyter de Claude Code **siempre** tenga servidor:
|
||||
es el `command` de la entrada `jupyter` en `.mcp.json`. No la invoques a mano salvo
|
||||
para depurar (`--dry-run`) o para levantar el jupyter colaborativo sin el MCP.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **stdout reservado**: el MCP habla por stdout (protocolo stdio). El wrapper jamas
|
||||
escribe a stdout — todo log va a stderr y `~/.fn_jupyter_mcp/wrapper.log`. No metas
|
||||
`echo` a stdout aqui o rompes el handshake del MCP.
|
||||
- **Puerto 8888 ocupado por gluetun** en esta maquina. Por eso el default es **8899**.
|
||||
Si 8899 tambien se ocupa, exporta `JUPYTER_MCP_PORT`.
|
||||
- **Token vacio**: solo escucha en `127.0.0.1` con `disable_check_xsrf` + `allow_origin '*'`.
|
||||
Aceptable en local; NO exponer el puerto a la red.
|
||||
- **venv requerido**: necesita `python/.venv` con `jupyterlab`, `jupyter-collaboration`
|
||||
y `jupyter-mcp-server`. Reconstruir: `cd python && uv sync --extra jupyter`.
|
||||
- El jupyter arrancado queda **detached** (nohup): persiste entre invocaciones del MCP.
|
||||
Para pararlo: `python/.venv/bin/jupyter server stop 8899` o `pkill -f 'jupyter-lab.*8899'`.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
v1.0.0 (2026-06-01) — version inicial. Wrapper auto-start: reusa/levanta jupyter
|
||||
colaborativo en puerto propio (8899) y autodetecta el dialecto de CLI del MCP.
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# jupyter_mcp_serve — arranca (o reusa) un Jupyter Lab colaborativo y lanza el
|
||||
# Jupyter MCP server enganchado a el por stdio. Pensado para ser el `command` de
|
||||
# la entrada "jupyter" en .mcp.json: garantiza que el MCP SIEMPRE tiene servidor.
|
||||
#
|
||||
# Por que existe: el MCP datalayer NO arranca jupyter, solo se conecta. Si la URL
|
||||
# apunta a un puerto sin jupyter (en esta maquina 8888 = proxy VPN gluetun), el
|
||||
# MCP nunca conecta. Este wrapper levanta su propio jupyter en un puerto propio.
|
||||
#
|
||||
# Env overrides:
|
||||
# JUPYTER_MCP_ROOT raiz de notebooks (default: raiz del repo)
|
||||
# JUPYTER_MCP_PORT puerto del jupyter gestionado (default: 8899)
|
||||
# JUPYTER_MCP_VENV venv (default: <repo>/python/.venv)
|
||||
# JUPYTER_MCP_TOKEN token (default: "" — solo escucha en 127.0.0.1)
|
||||
#
|
||||
# stdout esta RESERVADO al protocolo stdio del MCP. Todo log va a stderr + LOGFILE.
|
||||
# Nunca hacer echo a stdout aqui.
|
||||
#
|
||||
# Uso directo / test:
|
||||
# bash jupyter_mcp_serve.sh --dry-run # arranca jupyter, NO exec del MCP, loguea args
|
||||
set -euo pipefail
|
||||
|
||||
DRY=0
|
||||
[ "${1:-}" = "--dry-run" ] && DRY=1
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# raiz del repo = tres niveles arriba de bash/functions/infra/
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
|
||||
VENV="${JUPYTER_MCP_VENV:-$REPO_ROOT/python/.venv}"
|
||||
ROOT_DIR="${JUPYTER_MCP_ROOT:-$REPO_ROOT}"
|
||||
PORT="${JUPYTER_MCP_PORT:-8899}"
|
||||
HOST=127.0.0.1
|
||||
TOKEN="${JUPYTER_MCP_TOKEN:-}"
|
||||
LOGDIR="${HOME}/.fn_jupyter_mcp"
|
||||
mkdir -p "$LOGDIR"
|
||||
LOGFILE="$LOGDIR/wrapper.log"
|
||||
JLOG="$LOGDIR/jupyterlab.log"
|
||||
|
||||
log(){ printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOGFILE"; printf '%s\n' "$*" >&2; }
|
||||
|
||||
JUPYTER="$VENV/bin/jupyter"
|
||||
MCP="$VENV/bin/jupyter-mcp-server"
|
||||
|
||||
if [ ! -x "$JUPYTER" ]; then
|
||||
log "FATAL: $JUPYTER no existe. Instala: cd $REPO_ROOT/python && uv pip install --python .venv/bin/python3 jupyterlab jupyter-collaboration jupyter-mcp-server"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$MCP" ]; then
|
||||
log "FATAL: $MCP no existe. Instala jupyter-mcp-server en el venv."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
server_up(){
|
||||
local code
|
||||
code="$(curl -s -m 3 -o /dev/null -w '%{http_code}' "http://$HOST:$PORT/api/status?token=$TOKEN" 2>/dev/null || true)"
|
||||
[ "$code" = "200" ]
|
||||
}
|
||||
|
||||
if server_up; then
|
||||
log "reuso jupyter existente en $HOST:$PORT"
|
||||
else
|
||||
log "arranco jupyter colaborativo en $HOST:$PORT (root=$ROOT_DIR)"
|
||||
nohup "$JUPYTER" lab \
|
||||
--no-browser \
|
||||
--ServerApp.ip="$HOST" \
|
||||
--ServerApp.port="$PORT" \
|
||||
--ServerApp.root_dir="$ROOT_DIR" \
|
||||
--IdentityProvider.token="$TOKEN" \
|
||||
--ServerApp.disable_check_xsrf=True \
|
||||
--ServerApp.allow_origin='*' \
|
||||
>>"$JLOG" 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
# esperar hasta ~30s a que levante
|
||||
for _ in $(seq 1 60); do
|
||||
server_up && break
|
||||
sleep 0.5
|
||||
done
|
||||
if ! server_up; then
|
||||
log "FATAL: jupyter no levanto en 30s. Ver $JLOG"
|
||||
exit 1
|
||||
fi
|
||||
log "jupyter arriba"
|
||||
fi
|
||||
|
||||
BASE="http://$HOST:$PORT"
|
||||
|
||||
# Detectar el dialecto de CLI del MCP (cambia entre versiones de jupyter-mcp-server)
|
||||
HELP="$("$MCP" --help 2>&1 || true)"
|
||||
ARGS=(--transport stdio)
|
||||
if printf '%s' "$HELP" | grep -q -- '--document-url'; then
|
||||
ARGS+=(--document-url "$BASE" --runtime-url "$BASE")
|
||||
printf '%s' "$HELP" | grep -q -- '--document-token' && ARGS+=(--document-token "$TOKEN" --runtime-token "$TOKEN")
|
||||
elif printf '%s' "$HELP" | grep -q -- '--jupyter-url'; then
|
||||
ARGS+=(--jupyter-url "$BASE" --jupyter-token "$TOKEN")
|
||||
else
|
||||
# fallback: variables de entorno que las distintas versiones reconocen
|
||||
export DOCUMENT_URL="$BASE" RUNTIME_URL="$BASE" DOCUMENT_TOKEN="$TOKEN" RUNTIME_TOKEN="$TOKEN"
|
||||
export JUPYTER_URL="$BASE" JUPYTER_TOKEN="$TOKEN"
|
||||
fi
|
||||
|
||||
log "MCP cmd: $MCP ${ARGS[*]}"
|
||||
|
||||
if [ "$DRY" = "1" ]; then
|
||||
log "--dry-run: no ejecuto el MCP. Jupyter sigue corriendo en $BASE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec "$MCP" "${ARGS[@]}"
|
||||
@@ -3,7 +3,7 @@ name: launch_cpp_app_windows
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "launch_cpp_app_windows(app_name: string, [desktop_dir: string]) -> void"
|
||||
description: "Lanza un binario .exe en Windows desde WSL2. Asume que deploy_cpp_exe_to_windows ya copió el exe a Desktop/apps/<app_name>/. Usa cmd.exe /c start para desacoplar el proceso y retornar inmediatamente."
|
||||
@@ -61,10 +61,20 @@ Mitad complementaria de `deploy_cpp_exe_to_windows_bash_infra`. El flujo complet
|
||||
build_cpp_windows "registry_dashboard"
|
||||
|
||||
# 2. Copiar al escritorio (mata proceso si corre, copia DLLs+assets)
|
||||
deploy_cpp_exe_to_windows "registry_dashboard" "/home/lucas/fn_registry/apps/registry_dashboard"
|
||||
deploy_cpp_exe_to_windows "registry_dashboard" "$HOME/fn_registry/apps/registry_dashboard"
|
||||
|
||||
# 3. Lanzar
|
||||
launch_cpp_app_windows "registry_dashboard"
|
||||
```
|
||||
|
||||
No se incluyen tests automatizados porque requieren entorno WSL2 con Windows activo y no son automatizables en CI.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Si `FN_REGISTRY_ROOT_WSL` no es tu ruta default de fn_registry (`/home/<user>/fn_registry`), setea la variable antes de invocar esta función: `FN_REGISTRY_ROOT_WSL=/ruta/custom launch_cpp_app_windows <app>`.
|
||||
- El proceso hijo hereda `FN_REGISTRY_ROOT` como path Windows (backslashes) y `FN_REGISTRY_ROOT_WSL` como path Linux. En el exe C++, `py_resolve_interpreter()` usa `FN_REGISTRY_ROOT_WSL` para construir el invocation `wsl.exe -- /path/python3`.
|
||||
- PowerShell escapa `$` con `\$` para evitar expansión de variables en el string del comando.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-05-16) — auto-propaga `FN_REGISTRY_ROOT` (Windows path) + `FN_REGISTRY_ROOT_WSL` (Linux path) al proceso hijo para que pueda invocar WSL python via `wsl.exe`.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# launch_cpp_app_windows — Lanza un .exe en Windows desde WSL2 via cmd.exe /c start.
|
||||
# launch_cpp_app_windows v1.1.0 — Lanza un .exe en Windows desde WSL2 via PowerShell.
|
||||
# Asume que el exe ya fue copiado por deploy_cpp_exe_to_windows al escritorio.
|
||||
# v1.1.0: propaga FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
|
||||
# al proceso hijo para que pueda invocar WSL python via wsl.exe.
|
||||
|
||||
launch_cpp_app_windows() {
|
||||
local app="${1:-}"
|
||||
@@ -26,10 +28,18 @@ launch_cpp_app_windows() {
|
||||
win_app_dir=$(wslpath -w "$desktop_dir/apps/$app")
|
||||
win_exe="$win_app_dir\\$app.exe"
|
||||
|
||||
# Deducir raiz del registry en Linux (WSL) y traducir a Windows path.
|
||||
# FN_REGISTRY_ROOT_WSL puede sobreescribirse en el entorno del llamante.
|
||||
local linux_root win_root
|
||||
linux_root="${FN_REGISTRY_ROOT_WSL:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
|
||||
win_root=$(wslpath -w "$linux_root")
|
||||
|
||||
# Start-Process detacha (equivale a `start` de cmd) y respeta -WorkingDirectory.
|
||||
# Las comillas simples en PowerShell son literales — no procesa \ ni $.
|
||||
# Se inyectan FN_REGISTRY_ROOT (Windows path) y FN_REGISTRY_ROOT_WSL (Linux path)
|
||||
# para que el exe pueda localizar el venv WSL y hacer: wsl.exe -- python3 ...
|
||||
powershell.exe -NoProfile -Command \
|
||||
"Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
|
||||
"\$env:FN_REGISTRY_ROOT='$win_root'; \$env:FN_REGISTRY_ROOT_WSL='$linux_root'; Start-Process -FilePath '$win_exe' -WorkingDirectory '$win_app_dir'" \
|
||||
>/dev/null 2>&1
|
||||
|
||||
local ts
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: mas_client_register
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "mas_client_register(ssh_host: string, container: string, config_file: string, dry_run: bool) -> json"
|
||||
description: "Registra y sincroniza clientes OAuth en Matrix Authentication Service (MAS) ejecutando mas-cli config sync dentro del container Docker remoto via SSH. Verifica sintaxis YAML, soporte dry-run para ver diff antes de aplicar, y emite JSON estructurado con resultado. Idempotente: re-ejecucion con misma config no genera cambios."
|
||||
tags: [matrix, mas, oauth, oidc, migration, mas-migration, infra, docker, ssh, matrix-mas]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: ssh_host
|
||||
desc: "alias SSH del VPS donde corre MAS (ej. organic-machine.com). Debe estar en ~/.ssh/config con key auth."
|
||||
- name: container
|
||||
desc: "nombre del container Docker con MAS (ej. element_matrix_chat-mas-1). El config dentro del container se espera en /data/config.yaml."
|
||||
- name: config_file
|
||||
desc: "ruta absoluta en el VPS al archivo mas/config.yaml (ej. /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml). MAS lo monta como /data/config.yaml."
|
||||
- name: dry_run
|
||||
desc: "flag opcional --dry-run: ejecuta mas-cli config dump y devuelve el estado sin aplicar cambios. Util para verificar antes de activar MSC3861."
|
||||
output: "JSON con: status ('ok'|'dry-run'|'error'), applied (bool), clients_total (int), clients_diff (array de lineas del output de mas-cli), stderr (string con logs de error si aplica)."
|
||||
tested: true
|
||||
tests:
|
||||
- "help flag emite JSON parseable"
|
||||
- "args faltantes retornan JSON de error sin ssh"
|
||||
- "jq disponible en host local"
|
||||
test_file_path: "bash/functions/infra/mas_client_register_test.sh"
|
||||
file_path: "bash/functions/infra/mas_client_register.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Dry-run: verificar que clients se aplicarian correctamente
|
||||
source bash/functions/infra/mas_client_register.sh
|
||||
|
||||
mas_client_register \
|
||||
--ssh-host organic-machine.com \
|
||||
--container element_matrix_chat-mas-1 \
|
||||
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml \
|
||||
--dry-run
|
||||
|
||||
# Aplicar sync real (con --prune para eliminar clients viejos)
|
||||
mas_client_register \
|
||||
--ssh-host organic-machine.com \
|
||||
--container element_matrix_chat-mas-1 \
|
||||
--config-file /home/ubuntu/CodeProyects/element_matrix_chat/mas/config.yaml
|
||||
```
|
||||
|
||||
Salida esperada (sync OK):
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"applied": true,
|
||||
"clients_total": 6,
|
||||
"clients_diff": ["synced client element-web", "synced client synapse-admin", "..."],
|
||||
"stderr": ""
|
||||
}
|
||||
```
|
||||
|
||||
Salida dry-run:
|
||||
```json
|
||||
{
|
||||
"status": "dry-run",
|
||||
"applied": false,
|
||||
"clients_total": 42,
|
||||
"clients_diff": ["clients:", " - client_id: element-web", " ..."],
|
||||
"stderr": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usar despues de editar `mas/config.yaml` localmente y antes de hacer restart a Synapse con `msc3861` habilitado en `homeserver.yaml`. Ejecutar primero con `--dry-run` para verificar que los 6 clients OAuth (Element Web, Synapse-Admin, matrix_client_pc, matrix_client_android, matrix_admin_panel, Synapse-internal) estan correctamente definidos, luego sin `--dry-run` para aplicar el sync.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`--prune` elimina clients no declarados en config**: el sync real usa `--prune`, lo que borra cualquier client OAuth que exista en MAS pero no este en el `config.yaml`. Verificar con `--dry-run` antes de aplicar en produccion.
|
||||
- **Requiere `jq` en el host local**: el JSON output se construye con `jq`. Si no esta instalado, la funcion falla con error claro antes de conectar al VPS.
|
||||
- **`mas-cli` debe estar en el container**: la funcion asume que `mas-cli` esta en el PATH dentro del container MAS. Si el container usa una imagen diferente, verificar con `docker exec <container> mas-cli --version`.
|
||||
- **Config dentro del container siempre en `/data/config.yaml`**: el `--config-file` apunta a la ruta en el VPS (para que el operador sepa que archivo editar), pero el comando dentro del container usa `/data/config.yaml` (el mount point estandar de MAS). Si el compose monta el archivo en otro path, ajustar la constante `container_config` en el script.
|
||||
- **SSH key debe estar en agent o `~/.ssh/config`**: la funcion usa `ssh <alias>` directamente. Si la key requiere passphrase, ejecutar `ssh-add` antes.
|
||||
- **Si `config.yaml` es invalido, sync aborta sin tocar estado**: el paso 1 (`mas-cli config check`) detecta errores de sintaxis YAML antes de intentar sync. El estado de MAS no se modifica si la config tiene errores.
|
||||
- **Idempotente**: re-ejecutar con la misma config no genera cambios en MAS (mas-cli detecta que el estado ya coincide).
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
# mas_client_register — Registra/sincroniza clientes OAuth en Matrix Authentication Service (MAS)
|
||||
# via mas-cli config sync ejecutado en container Docker remoto a traves de SSH.
|
||||
set -euo pipefail
|
||||
|
||||
mas_client_register() {
|
||||
local ssh_host=""
|
||||
local container=""
|
||||
local config_file=""
|
||||
local dry_run=false
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ssh-host)
|
||||
ssh_host="$2"
|
||||
shift 2
|
||||
;;
|
||||
--container)
|
||||
container="$2"
|
||||
shift 2
|
||||
;;
|
||||
--config-file)
|
||||
config_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
cat >&2 <<'USAGE'
|
||||
mas_client_register - Sincroniza clientes OAuth en MAS via mas-cli config sync
|
||||
|
||||
Usage:
|
||||
mas_client_register --ssh-host <host> --container <name> --config-file <path> [--dry-run]
|
||||
|
||||
Options:
|
||||
--ssh-host Alias SSH del VPS (ej. organic-machine.com)
|
||||
--container Nombre del container MAS (ej. element_matrix_chat-mas-1)
|
||||
--config-file Ruta en el VPS al mas/config.yaml (ej. /home/ubuntu/project/mas/config.yaml)
|
||||
--dry-run Solo valida config y muestra diff, sin aplicar cambios
|
||||
|
||||
Output: JSON en stdout con status, applied, clients_total, clients_diff, stderr
|
||||
USAGE
|
||||
# emit minimal valid JSON so callers that parse stdout don't break
|
||||
echo '{"status":"help","applied":false,"clients_total":0,"clients_diff":[],"stderr":""}'
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "mas_client_register: argumento desconocido: $1" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validar argumentos obligatorios
|
||||
local errors=()
|
||||
[[ -z "$ssh_host" ]] && errors+=("--ssh-host es obligatorio")
|
||||
[[ -z "$container" ]] && errors+=("--container es obligatorio")
|
||||
[[ -z "$config_file" ]] && errors+=("--config-file es obligatorio")
|
||||
|
||||
if [[ ${#errors[@]} -gt 0 ]]; then
|
||||
for err in "${errors[@]}"; do
|
||||
echo "ERROR: $err" >&2
|
||||
done
|
||||
echo '{"status":"error","applied":false,"clients_total":0,"clients_diff":[],"stderr":"missing required arguments"}'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verificar dependencias locales
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "ERROR: jq no encontrado en el host local. Instalar: apt install jq / brew install jq" >&2
|
||||
echo '{"status":"error","applied":false,"clients_total":0,"clients_diff":[],"stderr":"jq not found on local host"}'
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "mas_client_register: ssh-host=$ssh_host container=$container dry-run=$dry_run" >&2
|
||||
|
||||
# La ruta de config dentro del container siempre es /data/config.yaml (mount convention de MAS)
|
||||
local container_config="/data/config.yaml"
|
||||
|
||||
# ---- PASO 1: Verificar sintaxis YAML con mas-cli config check ----
|
||||
echo "mas_client_register: verificando sintaxis de config con mas-cli config check..." >&2
|
||||
local check_stdout check_stderr check_exit
|
||||
check_stdout=$(ssh "$ssh_host" \
|
||||
"docker exec ${container} mas-cli config check --config ${container_config}" 2>/tmp/mas_check_stderr_$$ || true)
|
||||
check_exit=$?
|
||||
check_stderr=$(cat /tmp/mas_check_stderr_$$ 2>/dev/null || true)
|
||||
rm -f /tmp/mas_check_stderr_$$
|
||||
|
||||
if [[ $check_exit -ne 0 ]]; then
|
||||
echo "mas_client_register: config check falló (exit=$check_exit)" >&2
|
||||
echo "$check_stderr" >&2
|
||||
local escaped_stderr
|
||||
escaped_stderr=$(printf '%s' "${check_stderr}" | jq -Rs '.')
|
||||
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "mas_client_register: config check OK" >&2
|
||||
|
||||
# ---- PASO 2: dry-run o sync ----
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
# Ejecutar mas-cli config dump para mostrar el estado actual y lo que se aplicaria
|
||||
echo "mas_client_register: modo dry-run — ejecutando mas-cli config dump..." >&2
|
||||
local dump_stdout dump_stderr dump_exit
|
||||
dump_stdout=$(ssh "$ssh_host" \
|
||||
"docker exec ${container} mas-cli config dump --config ${container_config}" 2>/tmp/mas_dump_stderr_$$ || true)
|
||||
dump_exit=$?
|
||||
dump_stderr=$(cat /tmp/mas_dump_stderr_$$ 2>/dev/null || true)
|
||||
rm -f /tmp/mas_dump_stderr_$$
|
||||
|
||||
if [[ $dump_exit -ne 0 ]]; then
|
||||
echo "mas_client_register: config dump falló (exit=$dump_exit)" >&2
|
||||
echo "$dump_stderr" >&2
|
||||
local escaped_stderr
|
||||
escaped_stderr=$(printf '%s' "${dump_stderr}" | jq -Rs '.')
|
||||
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extraer listado de clients del dump (buscar lineas con client_id o type: client)
|
||||
local clients_diff_raw
|
||||
clients_diff_raw=$(printf '%s\n' "$dump_stdout" | grep -E "client_id:|client_name:" | \
|
||||
sed 's/^[[:space:]]*//' | head -50 || true)
|
||||
|
||||
local diff_json
|
||||
diff_json=$(printf '%s\n' "$dump_stdout" | jq -Rs 'split("\n") | map(select(length > 0)) | map(ltrimstr(" "))' 2>/dev/null \
|
||||
|| echo '["(jq parse error — ver stderr)"]')
|
||||
|
||||
local escaped_dump_stderr
|
||||
escaped_dump_stderr=$(printf '%s' "${dump_stderr}" | jq -Rs '.')
|
||||
|
||||
echo "mas_client_register: dry-run completado. dump lines=$(echo "$dump_stdout" | wc -l)" >&2
|
||||
|
||||
jq -n \
|
||||
--argjson diff "$diff_json" \
|
||||
--argjson stderr_str "$escaped_dump_stderr" \
|
||||
'{
|
||||
status: "dry-run",
|
||||
applied: false,
|
||||
clients_total: ($diff | length),
|
||||
clients_diff: $diff,
|
||||
stderr: $stderr_str
|
||||
}'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- PASO 3: sync real ----
|
||||
echo "mas_client_register: ejecutando mas-cli config sync --prune..." >&2
|
||||
local sync_stdout sync_stderr sync_exit
|
||||
sync_stdout=$(ssh "$ssh_host" \
|
||||
"docker exec ${container} mas-cli config sync --config ${container_config} --prune" \
|
||||
2>/tmp/mas_sync_stderr_$$ || true)
|
||||
sync_exit=$?
|
||||
sync_stderr=$(cat /tmp/mas_sync_stderr_$$ 2>/dev/null || true)
|
||||
rm -f /tmp/mas_sync_stderr_$$
|
||||
|
||||
echo "mas_client_register: sync exit=$sync_exit" >&2
|
||||
if [[ -n "$sync_stderr" ]]; then
|
||||
echo "mas_client_register stderr: $sync_stderr" >&2
|
||||
fi
|
||||
|
||||
if [[ $sync_exit -ne 0 ]]; then
|
||||
local escaped_stderr
|
||||
escaped_stderr=$(printf '%s' "${sync_stderr}" | jq -Rs '.')
|
||||
echo "{\"status\":\"error\",\"applied\":false,\"clients_total\":0,\"clients_diff\":[],\"stderr\":${escaped_stderr}}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parsear output del sync para extraer lineas con cambios aplicados
|
||||
local diff_lines
|
||||
diff_lines=$(printf '%s\n' "$sync_stdout" | grep -E "^\s*(created|updated|deleted|unchanged|synced)" || true)
|
||||
|
||||
local diff_json
|
||||
diff_json=$(printf '%s\n' "$sync_stdout" | jq -Rs 'split("\n") | map(select(length > 0))' 2>/dev/null \
|
||||
|| echo '[]')
|
||||
|
||||
local clients_count
|
||||
clients_count=$(printf '%s\n' "$sync_stdout" | grep -cE "client" 2>/dev/null || echo 0)
|
||||
|
||||
local escaped_sync_stderr
|
||||
escaped_sync_stderr=$(printf '%s' "${sync_stderr}" | jq -Rs '.')
|
||||
|
||||
echo "mas_client_register: sync completado con exito" >&2
|
||||
|
||||
jq -n \
|
||||
--argjson diff "$diff_json" \
|
||||
--argjson total "$clients_count" \
|
||||
--argjson stderr_str "$escaped_sync_stderr" \
|
||||
'{
|
||||
status: "ok",
|
||||
applied: true,
|
||||
clients_total: $total,
|
||||
clients_diff: $diff,
|
||||
stderr: $stderr_str
|
||||
}'
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente (no sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
mas_client_register "$@"
|
||||
fi
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para mas_client_register
|
||||
# No requiere SSH real — prueba paths locales (arg validation, --help, JSON output)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
((PASS++))
|
||||
else
|
||||
echo "FAIL: $test_name — expected to contain '$needle', got: $haystack"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_json_parseable() {
|
||||
local test_name="$1" json="$2"
|
||||
if command -v jq &>/dev/null; then
|
||||
if echo "$json" | jq . >/dev/null 2>&1; then
|
||||
echo "PASS: $test_name"
|
||||
((PASS++))
|
||||
else
|
||||
echo "FAIL: $test_name — output no es JSON valido: $json"
|
||||
((FAIL++))
|
||||
fi
|
||||
else
|
||||
if [[ "$json" == \{* ]]; then
|
||||
echo "PASS: $test_name (jq no disponible, verificacion basica OK)"
|
||||
((PASS++))
|
||||
else
|
||||
echo "FAIL: $test_name — output no parece JSON: $json"
|
||||
((FAIL++))
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Test: help flag emite JSON parseable
|
||||
# Cada invocacion en subshell aislada para no contaminar el runner con set -e del script fuente
|
||||
bash "$SCRIPT_DIR/mas_client_register.sh" --help >/tmp/mas_test_help_$$ 2>/dev/null || true
|
||||
output_help=$(cat /tmp/mas_test_help_$$ 2>/dev/null || true)
|
||||
rm -f /tmp/mas_test_help_$$
|
||||
assert_json_parseable "help flag emite JSON parseable" "$output_help"
|
||||
|
||||
# Test: args faltantes retornan JSON de error sin ssh
|
||||
bash "$SCRIPT_DIR/mas_client_register.sh" >/tmp/mas_test_noargs_$$ 2>/dev/null || true
|
||||
output_noargs=$(cat /tmp/mas_test_noargs_$$ 2>/dev/null || true)
|
||||
rm -f /tmp/mas_test_noargs_$$
|
||||
assert_json_parseable "args faltantes retornan JSON de error sin ssh" "$output_noargs"
|
||||
assert_contains "args faltantes contienen status error" '"status":"error"' "$output_noargs"
|
||||
|
||||
# Test: jq disponible en host local
|
||||
if command -v jq &>/dev/null; then
|
||||
echo "PASS: jq disponible en host local"
|
||||
((PASS++))
|
||||
else
|
||||
echo "FAIL: jq disponible en host local — instalar: apt install jq"
|
||||
((FAIL++))
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
name: mas_syn2mas_migration
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "mas_syn2mas_migration --ssh-host <host> --mas-container <name> --synapse-config-path <path-on-host> --log-dir <local-path> [--max-conflicts N] [--apply]"
|
||||
description: "Migra usuarios Synapse a Matrix Authentication Service (MAS) via mas-cli syn2mas. Fuerza dry-run primero, archiva el log, aborta si los conflicts superan el threshold, y solo ejecuta la migracion real con --apply."
|
||||
tags: [matrix, mas, syn2mas, migration, mas-migration, infra, users, docker, ssh, matrix-mas]
|
||||
params:
|
||||
- name: ssh-host
|
||||
desc: "Alias SSH del VPS donde corren los containers (ej. organic-machine.com)"
|
||||
- name: mas-container
|
||||
desc: "Nombre del container Docker de MAS (ej. element_matrix_chat-mas-1)"
|
||||
- name: synapse-config-path
|
||||
desc: "Ruta en el VPS al homeserver.yaml de Synapse (ej. /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml). El container debe tener el archivo accesible en /data/homeserver.yaml via volume mount."
|
||||
- name: log-dir
|
||||
desc: "Directorio local donde archivar logs dry-run y apply. Se crea con chmod 0700 y los logs con 0600 (contienen userIDs)."
|
||||
- name: max-conflicts
|
||||
desc: "Tope de conflictos detectados en dry-run. Si conflicts > max-conflicts, status=aborted exit 2. Default 0 (abortar ante cualquier conflict)."
|
||||
- name: apply
|
||||
desc: "Flag booleano. Sin --apply: solo dry-run (status=ok, sin cambios). Con --apply: ejecuta la migracion real tras pasar el threshold."
|
||||
output: "JSON en stdout: {\"status\":\"ok|aborted|error\",\"dry_run_log\":\"path\",\"apply_log\":\"path|null\",\"conflicts\":N,\"users_migrated\":N,\"duration_s\":N}. Exit 0=ok, 1=error, 2=aborted por conflicts."
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
tested: true
|
||||
tests:
|
||||
- "aborta con error cuando faltan args obligatorios"
|
||||
- "help no devuelve error"
|
||||
- "argumento desconocido retorna exit 1"
|
||||
- "max-conflicts invalido retorna exit 1"
|
||||
test_file_path: "bash/functions/infra/mas_syn2mas_migration_test.sh"
|
||||
file_path: "bash/functions/infra/mas_syn2mas_migration.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Paso 1: dry-run OBLIGATORIO (sin --apply — no modifica nada)
|
||||
mas_syn2mas_migration \
|
||||
--ssh-host organic-machine.com \
|
||||
--mas-container element_matrix_chat-mas-1 \
|
||||
--synapse-config-path /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml \
|
||||
--log-dir ~/matrix_migration_logs \
|
||||
--max-conflicts 0
|
||||
|
||||
# Salida esperada (si hay 0 conflicts):
|
||||
# {"status":"ok","dry_run_log":"/home/lucas/matrix_migration_logs/syn2mas_dryrun_1234567890.log","apply_log":null,"conflicts":0,"users_migrated":0,"duration_s":0}
|
||||
|
||||
# Revisar el log antes de continuar:
|
||||
# cat ~/matrix_migration_logs/syn2mas_dryrun_*.log
|
||||
|
||||
# Paso 2: tras revisar el log dry-run, aplicar la migracion real
|
||||
mas_syn2mas_migration \
|
||||
--ssh-host organic-machine.com \
|
||||
--mas-container element_matrix_chat-mas-1 \
|
||||
--synapse-config-path /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml \
|
||||
--log-dir ~/matrix_migration_logs \
|
||||
--max-conflicts 0 \
|
||||
--apply
|
||||
|
||||
# Salida esperada tras migracion exitosa:
|
||||
# {"status":"ok","dry_run_log":"/home/lucas/matrix_migration_logs/syn2mas_dryrun_1234567890.log","apply_log":"/home/lucas/matrix_migration_logs/syn2mas_apply_1234567890.log","conflicts":0,"users_migrated":42,"duration_s":15}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usar en el paso 4 de la migracion del issue 0162 (Synapse a MAS auth), tras activar MSC3861 en `homeserver.yaml` y verificar que MAS esta corriendo con `syn2mas: true` en su config. NUNCA ejecutar antes de activar MSC3861 — sin ese flag activo, `syn2mas` no puede mapear usuarios a las tablas MAS y la migracion resultara en estado inconsistente.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Dry-run NO modifica nada** — siempre ejecutar primero sin `--apply` y revisar el log manualmente antes de aplicar.
|
||||
- Si el dry-run detecta usuarios con **guest accounts**, **application services** (bots), o **passwords externos** (LDAP/OIDC), revisar manualmente el log antes de aplicar — estos casos pueden requerir steps adicionales documentados en el issue 0162.
|
||||
- **Backup postgres pre-migracion NO esta cubierto** por esta funcion. El operador es responsable de hacer `pg_dump` de la DB de Synapse antes de ejecutar con `--apply`. Ver issue 0162 paso 1.
|
||||
- Si la migracion real falla **a mitad**, MAS puede quedar en estado inconsistente con usuarios parcialmente migrados. El rollback consiste en restaurar el backup postgres de Synapse + revertir `homeserver.yaml` a la configuracion pre-MSC3861.
|
||||
- Los logs archivados en `--log-dir` **incluyen userIDs** (datos personales). Se crean con permisos `0600` (solo propietario puede leer). Mantener el directorio con `chmod 0700`. No subir los logs a repos publicos.
|
||||
- El comando `mas-cli syn2mas` en el container asume que `homeserver.yaml` esta montado en `/data/homeserver.yaml`. Si el volume mount del container usa otra ruta, el comando fallara con "file not found". Verificar con `docker inspect <container> | jq '.[].Mounts'`.
|
||||
- La postcondicion compara el count de usuarios MAS con una segunda ejecucion de dry-run para obtener el count esperado. Si el conteo no esta disponible (salida inesperada de mas-cli), la funcion emite `status=ok` con `users_migrated` del count real de MAS — no aborta por este motivo para evitar falsos negativos.
|
||||
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env bash
|
||||
# mas_syn2mas_migration — Migra usuarios Synapse a MAS via mas-cli syn2mas.
|
||||
# Fuerza dry-run primero, archiva el log, aborta si conflicts > threshold,
|
||||
# y solo ejecuta la migracion real cuando se pasa --apply.
|
||||
#
|
||||
# Usage:
|
||||
# mas_syn2mas_migration --ssh-host <host> --mas-container <name> \
|
||||
# --synapse-config-path <path-on-host> --log-dir <local-path> \
|
||||
# [--max-conflicts N] [--apply]
|
||||
#
|
||||
# Output: JSON en stdout con status, dry_run_log, apply_log, conflicts, users_migrated, duration_s
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
mas_syn2mas_migration() {
|
||||
local ssh_host=""
|
||||
local mas_container=""
|
||||
local synapse_config_path=""
|
||||
local log_dir=""
|
||||
local max_conflicts=0
|
||||
local do_apply=false
|
||||
|
||||
# ---- Parse args ----
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ssh-host)
|
||||
ssh_host="$2"
|
||||
shift 2
|
||||
;;
|
||||
--mas-container)
|
||||
mas_container="$2"
|
||||
shift 2
|
||||
;;
|
||||
--synapse-config-path)
|
||||
synapse_config_path="$2"
|
||||
shift 2
|
||||
;;
|
||||
--log-dir)
|
||||
log_dir="$2"
|
||||
shift 2
|
||||
;;
|
||||
--max-conflicts)
|
||||
max_conflicts="$2"
|
||||
shift 2
|
||||
;;
|
||||
--apply)
|
||||
do_apply=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
cat >&2 <<'USAGE'
|
||||
mas_syn2mas_migration - Migra usuarios Synapse a Matrix Authentication Service (MAS)
|
||||
|
||||
Usage:
|
||||
mas_syn2mas_migration \
|
||||
--ssh-host <host> \
|
||||
--mas-container <name> \
|
||||
--synapse-config-path <path-on-host> \
|
||||
--log-dir <local-path> \
|
||||
[--max-conflicts N] \
|
||||
[--apply]
|
||||
|
||||
Opciones:
|
||||
--ssh-host Alias SSH del VPS (ej. organic-machine.com)
|
||||
--mas-container Nombre del container MAS (ej. element_matrix_chat-mas-1)
|
||||
--synapse-config-path Ruta en el VPS al homeserver.yaml
|
||||
(ej. /home/ubuntu/CodeProyects/element_matrix_chat/synapse_data/homeserver.yaml)
|
||||
--log-dir Directorio local donde archivar logs dry-run y apply
|
||||
--max-conflicts N Tope de conflictos en dry-run antes de abortar (default 0)
|
||||
--apply Ejecutar migracion real. Sin esta flag: solo dry-run.
|
||||
|
||||
Comportamiento:
|
||||
1. Siempre ejecuta dry-run primero y archiva el log.
|
||||
2. Si conflicts > max-conflicts -> status=aborted, exit 2.
|
||||
3. Sin --apply -> status=ok (dry-run completado), exit 0.
|
||||
4. Con --apply -> ejecuta migracion real, archiva log, verifica postcondicion.
|
||||
|
||||
Output JSON: {"status":"ok|aborted|error","dry_run_log":"path","apply_log":"path|null","conflicts":N,"users_migrated":N,"duration_s":N}
|
||||
USAGE
|
||||
echo '{"status":"help","dry_run_log":"","apply_log":null,"conflicts":0,"users_migrated":0,"duration_s":0}'
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "mas_syn2mas_migration: argumento desconocido: $1" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---- Validar argumentos obligatorios ----
|
||||
local errors=()
|
||||
[[ -z "$ssh_host" ]] && errors+=("--ssh-host es obligatorio")
|
||||
[[ -z "$mas_container" ]] && errors+=("--mas-container es obligatorio")
|
||||
[[ -z "$synapse_config_path" ]] && errors+=("--synapse-config-path es obligatorio")
|
||||
[[ -z "$log_dir" ]] && errors+=("--log-dir es obligatorio")
|
||||
|
||||
if [[ ${#errors[@]} -gt 0 ]]; then
|
||||
for err in "${errors[@]}"; do
|
||||
echo "ERROR: $err" >&2
|
||||
done
|
||||
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Validar que max_conflicts es un entero no negativo
|
||||
if ! [[ "$max_conflicts" =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: --max-conflicts debe ser un entero >= 0, recibido: $max_conflicts" >&2
|
||||
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- Dependencias locales ----
|
||||
if ! command -v jq &>/dev/null; then
|
||||
echo "ERROR: jq no encontrado. Instalar: apt install jq / brew install jq" >&2
|
||||
echo '{"status":"error","dry_run_log":"","apply_log":null,"conflicts":-1,"users_migrated":0,"duration_s":0}'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- Crear log-dir con permisos restringidos ----
|
||||
mkdir -p "$log_dir"
|
||||
chmod 0700 "$log_dir"
|
||||
|
||||
local ts
|
||||
ts=$(date +%s)
|
||||
|
||||
local dry_run_log="${log_dir}/syn2mas_dryrun_${ts}.log"
|
||||
local apply_log_path="null"
|
||||
local apply_log_file="${log_dir}/syn2mas_apply_${ts}.log"
|
||||
|
||||
# La ruta del homeserver.yaml dentro del container MAS se pasa como --synapse-config
|
||||
# MAS monta el directorio del synapse bajo /data/ por convencion, pero la ruta real
|
||||
# puede variar — usamos la ruta tal como existe en el host (montada via volume).
|
||||
# El comando real esperado: docker exec <container> mas-cli syn2mas --synapse-config <path>
|
||||
# donde <path> es la ruta tal como el container la ve (via volume mount).
|
||||
# Asumimos que el VPS tiene el config accesible en la misma ruta dentro del container.
|
||||
local container_config="/data/homeserver.yaml"
|
||||
|
||||
echo "mas_syn2mas_migration: ssh-host=${ssh_host} container=${mas_container} max-conflicts=${max_conflicts} apply=${do_apply}" >&2
|
||||
|
||||
# =========================================================================
|
||||
# PASO 1: DRY-RUN obligatorio
|
||||
# =========================================================================
|
||||
echo "mas_syn2mas_migration: ejecutando dry-run..." >&2
|
||||
|
||||
local dry_exit=0
|
||||
# Capturar stdout+stderr del dry-run en el log y tambien en variable para parsing
|
||||
local dry_output
|
||||
dry_output=$(ssh "$ssh_host" \
|
||||
"docker exec '${mas_container}' mas-cli syn2mas \
|
||||
--synapse-config '${container_config}' \
|
||||
--dry-run" \
|
||||
2>&1) || dry_exit=$?
|
||||
|
||||
# Archivar log con timestamp + header informativo
|
||||
{
|
||||
echo "# mas_syn2mas_migration dry-run"
|
||||
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
|
||||
echo "# synapse-config-path=${synapse_config_path}"
|
||||
echo "# exit=${dry_exit}"
|
||||
echo "# ---"
|
||||
printf '%s\n' "$dry_output"
|
||||
} > "$dry_run_log"
|
||||
chmod 0600 "$dry_run_log"
|
||||
|
||||
echo "mas_syn2mas_migration: dry-run exit=${dry_exit}, log=${dry_run_log}" >&2
|
||||
|
||||
if [[ $dry_exit -ne 0 ]]; then
|
||||
# Si el comando SSH falla completamente (no es fallo de syn2mas sino de conectividad)
|
||||
echo "mas_syn2mas_migration: ERROR — dry-run falló con exit ${dry_exit}" >&2
|
||||
local escaped_out
|
||||
escaped_out=$(printf '%s' "${dry_output}" | jq -Rs '.')
|
||||
local dry_run_log_json
|
||||
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
||||
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# PASO 2: Parsear conflicts del dry-run
|
||||
# =========================================================================
|
||||
# Regex sobre lineas tipo:
|
||||
# "Conflict:", "Skipping:", "Error processing user", "conflict"
|
||||
# También contamos líneas que indiquen usuarios problemáticos.
|
||||
local conflicts=0
|
||||
local conflict_lines
|
||||
conflict_lines=$(printf '%s\n' "$dry_output" | \
|
||||
grep -ciE '(conflict|skipping|error processing user|cannot migrate|already exists)' 2>/dev/null || true)
|
||||
|
||||
# grep -c devuelve string; convertir a int defensivamente
|
||||
if [[ "$conflict_lines" =~ ^[0-9]+$ ]]; then
|
||||
conflicts=$conflict_lines
|
||||
else
|
||||
# Parser falló de forma inesperada — abortar defensivamente
|
||||
echo "mas_syn2mas_migration: ERROR — no se pudo parsear el conteo de conflicts del dry-run (parser defensivo)" >&2
|
||||
local dry_run_log_json
|
||||
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
||||
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":-1,\"users_migrated\":0,\"duration_s\":0}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "mas_syn2mas_migration: conflicts detectados en dry-run: ${conflicts} (max permitido: ${max_conflicts})" >&2
|
||||
|
||||
# =========================================================================
|
||||
# PASO 3: Verificar threshold de conflicts
|
||||
# =========================================================================
|
||||
if [[ $conflicts -gt $max_conflicts ]]; then
|
||||
echo "mas_syn2mas_migration: ABORTADO — conflicts (${conflicts}) > max-conflicts (${max_conflicts})" >&2
|
||||
echo "Revisar: ${dry_run_log}" >&2
|
||||
local dry_run_log_json
|
||||
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
||||
echo "{\"status\":\"aborted\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
|
||||
return 2
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# PASO 4: Si no --apply, terminar aqui con status=ok (dry-run completado)
|
||||
# =========================================================================
|
||||
if [[ "$do_apply" == "false" ]]; then
|
||||
echo "mas_syn2mas_migration: dry-run completado (${conflicts} conflicts). Revisar log y re-ejecutar con --apply." >&2
|
||||
local dry_run_log_json
|
||||
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
||||
echo "{\"status\":\"ok\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":null,\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":0}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# PASO 5: Migracion REAL (--apply)
|
||||
# =========================================================================
|
||||
echo "mas_syn2mas_migration: ejecutando migracion REAL..." >&2
|
||||
local apply_start
|
||||
apply_start=$(date +%s)
|
||||
|
||||
local apply_exit=0
|
||||
local apply_output
|
||||
apply_output=$(ssh "$ssh_host" \
|
||||
"docker exec '${mas_container}' mas-cli syn2mas \
|
||||
--synapse-config '${container_config}'" \
|
||||
2>&1) || apply_exit=$?
|
||||
|
||||
local apply_end
|
||||
apply_end=$(date +%s)
|
||||
local duration_s=$(( apply_end - apply_start ))
|
||||
|
||||
# Archivar log de apply
|
||||
{
|
||||
echo "# mas_syn2mas_migration apply"
|
||||
echo "# ts=${ts} ssh-host=${ssh_host} container=${mas_container}"
|
||||
echo "# synapse-config-path=${synapse_config_path}"
|
||||
echo "# exit=${apply_exit} duration_s=${duration_s}"
|
||||
echo "# ---"
|
||||
printf '%s\n' "$apply_output"
|
||||
} > "$apply_log_file"
|
||||
chmod 0600 "$apply_log_file"
|
||||
|
||||
apply_log_path="$apply_log_file"
|
||||
echo "mas_syn2mas_migration: apply exit=${apply_exit}, duration=${duration_s}s, log=${apply_log_file}" >&2
|
||||
|
||||
if [[ $apply_exit -ne 0 ]]; then
|
||||
echo "mas_syn2mas_migration: ERROR — migracion real falló con exit ${apply_exit}" >&2
|
||||
local dry_run_log_json apply_log_json
|
||||
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
||||
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
|
||||
echo "{\"status\":\"error\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":0,\"duration_s\":${duration_s}}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# PASO 6: Postcondicion — comparar usuarios en MAS vs Synapse
|
||||
# =========================================================================
|
||||
echo "mas_syn2mas_migration: verificando postcondicion (usuarios MAS vs Synapse)..." >&2
|
||||
|
||||
local mas_user_count=0
|
||||
local synapse_user_count=0
|
||||
local users_migrated=0
|
||||
local post_status="ok"
|
||||
|
||||
# Contar usuarios en MAS via mas-cli admin user list
|
||||
local mas_count_raw
|
||||
mas_count_raw=$(ssh "$ssh_host" \
|
||||
"docker exec '${mas_container}' mas-cli manage list-users --json 2>/dev/null | jq length" \
|
||||
2>/dev/null || echo "0")
|
||||
|
||||
if [[ "$mas_count_raw" =~ ^[0-9]+$ ]]; then
|
||||
mas_user_count=$mas_count_raw
|
||||
else
|
||||
echo "mas_syn2mas_migration: ADVERTENCIA — no se pudo obtener conteo de usuarios MAS (output: ${mas_count_raw})" >&2
|
||||
post_status="ok" # No abortar, solo advertir
|
||||
fi
|
||||
|
||||
# Contar usuarios locales en Synapse via psql (excluyendo bots/AS)
|
||||
# Intentamos obtener el count; si falla, continuamos sin abortar
|
||||
local synapse_count_raw
|
||||
synapse_count_raw=$(ssh "$ssh_host" \
|
||||
"docker exec '${mas_container}' mas-cli syn2mas --synapse-config '${container_config}' --dry-run 2>&1 | grep -oE 'Found [0-9]+ users' | grep -oE '[0-9]+' | head -1" \
|
||||
2>/dev/null || echo "0")
|
||||
|
||||
if [[ "$synapse_count_raw" =~ ^[0-9]+$ ]]; then
|
||||
synapse_user_count=$synapse_count_raw
|
||||
fi
|
||||
|
||||
users_migrated=$mas_user_count
|
||||
|
||||
# Si tenemos ambos counts y difieren significativamente, marcar como warning en log
|
||||
if [[ $synapse_user_count -gt 0 && $mas_user_count -eq 0 ]]; then
|
||||
echo "mas_syn2mas_migration: ADVERTENCIA — MAS reporta 0 usuarios pero Synapse tenia ${synapse_user_count}" >&2
|
||||
post_status="error"
|
||||
fi
|
||||
|
||||
echo "mas_syn2mas_migration: postcondicion: mas_users=${mas_user_count} synapse_users=${synapse_user_count} status=${post_status}" >&2
|
||||
|
||||
# =========================================================================
|
||||
# PASO 7: Emitir JSON final
|
||||
# =========================================================================
|
||||
local dry_run_log_json apply_log_json
|
||||
dry_run_log_json=$(printf '%s' "$dry_run_log" | jq -Rs '.')
|
||||
apply_log_json=$(printf '%s' "$apply_log_file" | jq -Rs '.')
|
||||
|
||||
echo "{\"status\":\"${post_status}\",\"dry_run_log\":${dry_run_log_json},\"apply_log\":${apply_log_json},\"conflicts\":${conflicts},\"users_migrated\":${users_migrated},\"duration_s\":${duration_s}}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Ejecutar si se llama directamente (no sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
mas_syn2mas_migration "$@"
|
||||
fi
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para mas_syn2mas_migration
|
||||
# Verifica arg parsing sin conectar al VPS real.
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/mas_syn2mas_migration.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_exit() {
|
||||
local test_name="$1" expected_exit="$2"
|
||||
shift 2
|
||||
local actual_exit=0
|
||||
set +e
|
||||
"$@" >/dev/null 2>&1
|
||||
actual_exit=$?
|
||||
set -e
|
||||
if [[ "$actual_exit" == "$expected_exit" ]]; then
|
||||
echo "PASS: $test_name"
|
||||
((PASS++)) || true
|
||||
else
|
||||
echo "FAIL: $test_name — expected exit $expected_exit, got $actual_exit"
|
||||
((FAIL++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
assert_stdout_contains() {
|
||||
local test_name="$1" needle="$2"
|
||||
shift 2
|
||||
local output actual_exit=0
|
||||
set +e
|
||||
output=$("$@" 2>/dev/null)
|
||||
actual_exit=$?
|
||||
set -e
|
||||
if echo "$output" | grep -q "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
((PASS++)) || true
|
||||
else
|
||||
echo "FAIL: $test_name — expected stdout to contain '$needle', got: $output"
|
||||
((FAIL++)) || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Test: aborta con error cuando faltan args obligatorios
|
||||
assert_exit "aborta con error cuando faltan args obligatorios" 1 \
|
||||
mas_syn2mas_migration
|
||||
|
||||
# Test: help no devuelve error
|
||||
assert_exit "help no devuelve error" 0 \
|
||||
mas_syn2mas_migration --help
|
||||
|
||||
# Test: argumento desconocido retorna exit 1
|
||||
assert_exit "argumento desconocido retorna exit 1" 1 \
|
||||
mas_syn2mas_migration --unknown-flag
|
||||
|
||||
# Test: max-conflicts invalido retorna exit 1
|
||||
assert_exit "max-conflicts invalido retorna exit 1" 1 \
|
||||
mas_syn2mas_migration \
|
||||
--ssh-host fake-host \
|
||||
--mas-container fake-container \
|
||||
--synapse-config-path /fake/homeserver.yaml \
|
||||
--log-dir "/tmp/test_mas_migration_$$" \
|
||||
--max-conflicts "not-a-number"
|
||||
|
||||
# Test: help emite JSON valido con status=help
|
||||
assert_stdout_contains "help emite JSON con status help" '"status":"help"' \
|
||||
mas_syn2mas_migration --help
|
||||
|
||||
# Test: falta --ssh-host emite JSON con status=error
|
||||
assert_stdout_contains "falta ssh-host emite JSON error" '"status":"error"' \
|
||||
mas_syn2mas_migration \
|
||||
--mas-container fake-container \
|
||||
--synapse-config-path /fake/homeserver.yaml \
|
||||
--log-dir "/tmp/test_mas_migration_$$"
|
||||
|
||||
# Test: falta --log-dir emite JSON con status=error
|
||||
assert_stdout_contains "falta log-dir emite JSON error" '"status":"error"' \
|
||||
mas_syn2mas_migration \
|
||||
--ssh-host fake-host \
|
||||
--mas-container fake-container \
|
||||
--synapse-config-path /fake/homeserver.yaml
|
||||
|
||||
# Limpieza
|
||||
rm -rf "/tmp/test_mas_migration_$$" 2>/dev/null || true
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
@@ -32,16 +32,16 @@ file_path: "bash/functions/infra/pre_commit_hook_install.sh"
|
||||
source bash/functions/infra/pre_commit_hook_install.sh
|
||||
|
||||
# Instalar en el repo actual
|
||||
pre_commit_hook_install /home/lucas/fn_registry
|
||||
# INSTALLED /home/lucas/fn_registry/.git/hooks/pre-commit
|
||||
pre_commit_hook_install $HOME/fn_registry
|
||||
# INSTALLED $HOME/fn_registry/.git/hooks/pre-commit
|
||||
|
||||
# Idempotente: segunda llamada no sobreescribe
|
||||
pre_commit_hook_install /home/lucas/fn_registry
|
||||
# SKIP /home/lucas/fn_registry/.git/hooks/pre-commit (already installed)
|
||||
pre_commit_hook_install $HOME/fn_registry
|
||||
# SKIP $HOME/fn_registry/.git/hooks/pre-commit (already installed)
|
||||
|
||||
# Forzar reinstalacion (hace backup del hook anterior)
|
||||
pre_commit_hook_install /home/lucas/fn_registry --force
|
||||
# INSTALLED /home/lucas/fn_registry/.git/hooks/pre-commit
|
||||
pre_commit_hook_install $HOME/fn_registry --force
|
||||
# INSTALLED $HOME/fn_registry/.git/hooks/pre-commit
|
||||
```
|
||||
|
||||
## Notas
|
||||
@@ -58,5 +58,5 @@ Si no puede localizar `fn_registry`, el hook imprime un aviso y sale con exit 0
|
||||
|
||||
Configurar `FN_REGISTRY_ROOT` en el perfil del shell para garantizar que el hook siempre encuentre el registry:
|
||||
```bash
|
||||
export FN_REGISTRY_ROOT=/home/lucas/fn_registry
|
||||
export FN_REGISTRY_ROOT=$HOME/fn_registry
|
||||
```
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: refresh_windows_icon_cache
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "refresh_windows_icon_cache() -> void"
|
||||
description: "Fuerza a Windows Explorer a recargar la cache de iconos desde WSL2 via ie4uinit.exe. Best-effort: nunca aborta, retorna 0 si alguna estrategia tuvo exito."
|
||||
tags: [windows, wsl, deploy, shell, icons, cpp-windows]
|
||||
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/refresh_windows_icon_cache.sh"
|
||||
params: []
|
||||
output: "0 si al menos una estrategia tuvo exito, non-zero si todas fallaron. Imprime una linea de estado en stdout."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/refresh_windows_icon_cache.sh
|
||||
refresh_windows_icon_cache
|
||||
# icon cache refresh: ok via ie4uinit -show
|
||||
```
|
||||
|
||||
O directamente via `fn run`:
|
||||
|
||||
```bash
|
||||
./fn run refresh_windows_icon_cache_bash_infra
|
||||
```
|
||||
|
||||
Uso tipico en un pipeline de redeploy tras reconstruir el `.exe`:
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/deploy_cpp_exe_to_windows.sh
|
||||
source bash/functions/infra/refresh_windows_icon_cache.sh
|
||||
|
||||
deploy_cpp_exe_to_windows "registry_dashboard" "apps/registry_dashboard"
|
||||
refresh_windows_icon_cache
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Despues de redeployar un `.exe` Windows cuyo `appicon.ico` cambio (via windres embebido en el build), antes de que Windows muestre el icono nuevo en taskbar, Alt+Tab y File Explorer. Sin esta llamada Windows puede tardar minutos en reflejar el icono actualizado, o no actualizarlo hasta reiniciar Explorer.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `ie4uinit.exe` debe estar en el PATH de WSL2 (normalmente via `/mnt/c/Windows/System32/`). Si Windows esta muy roto puede no encontrarse — la funcion retornara 1 con mensaje de error.
|
||||
- El cambio puede tardar 1-2 segundos en propagarse visualmente despues de que la funcion retorne.
|
||||
- Algunos casos extremos (icono cacheado en el dockable taskbar previamente fijado) requieren desanclar y volver a anclar el ejecutable, o reiniciar `explorer.exe`. Esta funcion no mata Explorer — seria demasiado disruptivo.
|
||||
- Solo funciona desde WSL2 con acceso a herramientas Windows (`/mnt/c/Windows/System32/` en PATH). No tiene efecto en Linux nativo.
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# refresh_windows_icon_cache — Fuerza a Windows Explorer a recargar la cache
|
||||
# de iconos desde WSL2. Best-effort: nunca aborta, retorna 0 si alguna
|
||||
# estrategia tuvo exito.
|
||||
|
||||
refresh_windows_icon_cache() {
|
||||
# Estrategia 1: ie4uinit.exe -show (Windows 10/11 — emite SHCNE_ASSOCCHANGED)
|
||||
if command -v ie4uinit.exe >/dev/null 2>&1; then
|
||||
if ie4uinit.exe -show >/dev/null 2>&1; then
|
||||
echo "icon cache refresh: ok via ie4uinit -show"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Estrategia 2: ie4uinit.exe -ClearIconCache (fallback para builds viejos)
|
||||
if ie4uinit.exe -ClearIconCache >/dev/null 2>&1; then
|
||||
echo "icon cache refresh: ok via ie4uinit -ClearIconCache"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "icon cache refresh: failed (ie4uinit.exe not found or all strategies failed)"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
refresh_windows_icon_cache "$@"
|
||||
fi
|
||||
@@ -3,11 +3,11 @@ name: resolve_cpp_app_dir
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "resolve_cpp_app_dir(app_name?: string) -> stdout: app_name\tapp_dir"
|
||||
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en ambas ubicaciones. Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
|
||||
tags: [cpp, resolve, app, directory, infra]
|
||||
description: "Resuelve el nombre y directorio absoluto de una app C++ del registry. Sin arg deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/. Con arg busca en las tres ubicaciones (apps/ canonical issue 0096 primero, luego cpp/apps/ legacy, luego projects/*/apps/). Imprime '<app_name>TAB<absolute_dir>' en stdout, exit 0; si no resuelve, lista apps disponibles en stderr y sale con exit 1."
|
||||
tags: [cpp, resolve, app, directory, infra, cpp-windows]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -20,7 +20,7 @@ test_file_path: ""
|
||||
file_path: "bash/functions/infra/resolve_cpp_app_dir.sh"
|
||||
params:
|
||||
- name: app_name
|
||||
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de cpp/apps/<X>/ o projects/*/apps/<X>/."
|
||||
desc: "Nombre de la app C++ a resolver (opcional). Sin arg se deduce desde el directorio actual si estamos dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/."
|
||||
output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En caso de error imprime ayuda a stderr y sale con exit 1."
|
||||
---
|
||||
|
||||
@@ -28,13 +28,13 @@ output: "Una linea TAB-separada '<app_name>\\t<absolute_dir_path>' en stdout. En
|
||||
|
||||
```bash
|
||||
# Desde dentro de cpp/apps/chart_demo/
|
||||
cd /home/lucas/fn_registry/cpp/apps/chart_demo
|
||||
cd $HOME/fn_registry/cpp/apps/chart_demo
|
||||
resolve_cpp_app_dir
|
||||
# -> chart_demo\t/home/lucas/fn_registry/cpp/apps/chart_demo
|
||||
# -> chart_demo\t$HOME/fn_registry/cpp/apps/chart_demo
|
||||
|
||||
# Con argumento explicito
|
||||
resolve_cpp_app_dir registry_dashboard
|
||||
# -> registry_dashboard\t/home/lucas/fn_registry/cpp/apps/registry_dashboard
|
||||
# -> registry_dashboard\t$HOME/fn_registry/cpp/apps/registry_dashboard
|
||||
|
||||
# Capturar los dos campos
|
||||
resolved=$(resolve_cpp_app_dir graph_explorer)
|
||||
@@ -44,4 +44,13 @@ APP_DIR="$(echo "$resolved" | cut -f2)"
|
||||
|
||||
## Notas
|
||||
|
||||
Busca en orden: primero `$ROOT/cpp/apps/<X>`, luego `$ROOT/projects/*/apps/<X>` (primer match gana). Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente.
|
||||
Busca en orden:
|
||||
1. `$ROOT/apps/<X>` con `CMakeLists.txt` — layout canonical post-issue 0096.
|
||||
2. `$ROOT/cpp/apps/<X>` — legacy pre-issue 0096.
|
||||
3. `$ROOT/projects/*/apps/<X>` — apps de un proyecto (primer match gana).
|
||||
|
||||
Si ninguna ruta existe, imprime lista de apps disponibles (con prefijo de ubicacion) en stderr y sale con exit 1. Sourceable o ejecutable directamente. Helper interno `_list_cpp_apps` evita duplicar codigo en los paths de error.
|
||||
|
||||
### Growth log
|
||||
|
||||
- v1.1.0 (2026-05-16) — busca tambien en `apps/<X>/` (canonical issue 0096). Antes solo cubria `cpp/apps/<X>/` y `projects/*/apps/<X>/`, lo que hacia que `./fn run compile_cpp_app <name>` fallara para apps movidas al layout canonical (ej. `dag_engine_ui`).
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# resolve_cpp_app_dir — Resuelve nombre y directorio absoluto de una app C++ del registry.
|
||||
# Sin arg: deduce desde CWD si esta dentro de cpp/apps/<X>/ o projects/*/apps/<X>/.
|
||||
# Con arg: usa el nombre directamente y busca en ambas ubicaciones.
|
||||
# Sin arg: deduce desde CWD si esta dentro de apps/<X>/, cpp/apps/<X>/ o projects/*/apps/<X>/.
|
||||
# Con arg: usa el nombre directamente y busca en las tres ubicaciones.
|
||||
# Salida: "<app_name>\t<absolute_dir_path>" en stdout (TAB separado), exit 0.
|
||||
# Error: lista apps disponibles en stderr + exit 1.
|
||||
|
||||
resolve_cpp_app_dir() {
|
||||
local app_arg="${1:-}"
|
||||
local root="${FN_REGISTRY_ROOT:-/home/lucas/fn_registry}"
|
||||
local root="${FN_REGISTRY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
|
||||
|
||||
_list_cpp_apps() {
|
||||
ls "$root/apps/" 2>/dev/null | sed 's/^/ apps\//'
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
}
|
||||
|
||||
# --- Deducir desde CWD si no hay argumento ---
|
||||
if [ -z "$app_arg" ]; then
|
||||
local cwd
|
||||
cwd="$(pwd)"
|
||||
case "$cwd" in
|
||||
"$root"/apps/*/|"$root"/apps/*)
|
||||
local rel="${cwd#"$root/apps/"}"
|
||||
app_arg="${rel%%/*}"
|
||||
;;
|
||||
"$root"/cpp/apps/*/|"$root"/cpp/apps/*)
|
||||
# Extraer primer segmento tras cpp/apps/
|
||||
local rel="${cwd#"$root/cpp/apps/"}"
|
||||
app_arg="${rel%%/*}"
|
||||
;;
|
||||
"$root"/projects/*/apps/*/|"$root"/projects/*/apps/*)
|
||||
# Extraer primer segmento tras la ultima /apps/
|
||||
local rel="${cwd#"$root/projects/"}"
|
||||
rel="${rel#*/apps/}"
|
||||
app_arg="${rel%%/*}"
|
||||
@@ -33,12 +43,7 @@ resolve_cpp_app_dir() {
|
||||
echo "ERROR: no se pudo deducir la app desde el directorio actual." >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
{
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
} >&2
|
||||
_list_cpp_apps >&2
|
||||
echo "" >&2
|
||||
echo "Uso: resolve_cpp_app_dir <app_name>" >&2
|
||||
return 1
|
||||
@@ -47,12 +52,17 @@ resolve_cpp_app_dir() {
|
||||
# --- Buscar directorio real ---
|
||||
local app_dir=""
|
||||
|
||||
# Primero: cpp/apps/<X>
|
||||
if [ -d "$root/cpp/apps/$app_arg" ]; then
|
||||
# Primero (issue 0096 canonical): apps/<X>
|
||||
if [ -d "$root/apps/$app_arg" ] && [ -f "$root/apps/$app_arg/CMakeLists.txt" ]; then
|
||||
app_dir="$root/apps/$app_arg"
|
||||
fi
|
||||
|
||||
# Segundo (legacy): cpp/apps/<X>
|
||||
if [ -z "$app_dir" ] && [ -d "$root/cpp/apps/$app_arg" ]; then
|
||||
app_dir="$root/cpp/apps/$app_arg"
|
||||
fi
|
||||
|
||||
# Segundo: projects/*/apps/<X> (primer match)
|
||||
# Tercero: projects/*/apps/<X> (primer match)
|
||||
if [ -z "$app_dir" ]; then
|
||||
for cand in "$root"/projects/*/apps/"$app_arg"; do
|
||||
if [ -d "$cand" ]; then
|
||||
@@ -63,15 +73,10 @@ resolve_cpp_app_dir() {
|
||||
fi
|
||||
|
||||
if [ -z "$app_dir" ]; then
|
||||
echo "ERROR: no se encuentra app '$app_arg' en cpp/apps/ ni en projects/*/apps/" >&2
|
||||
echo "ERROR: no se encuentra app '$app_arg' en apps/, cpp/apps/ ni en projects/*/apps/" >&2
|
||||
echo "" >&2
|
||||
echo "Apps disponibles:" >&2
|
||||
{
|
||||
ls "$root/cpp/apps/" 2>/dev/null | sed 's/^/ cpp\/apps\//'
|
||||
for proj in "$root"/projects/*/apps/; do
|
||||
ls "$proj" 2>/dev/null | sed "s|^| $(echo "$proj" | sed "s|$root/||")|"
|
||||
done
|
||||
} >&2
|
||||
_list_cpp_apps >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ echo "$result"
|
||||
# {"files_transferred": 12, "total_size": "1.23 MB", "ssh_alias": "prod-server", "remote_dir": "/opt/apps/dag_engine"}
|
||||
|
||||
# Deploy con ruta absoluta local
|
||||
rsync_deploy "/home/lucas/fn_registry/apps/myapp/" "myserver" "/opt/myapp"
|
||||
rsync_deploy "$HOME/fn_registry/apps/myapp/" "myserver" "/opt/myapp"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: start_nordvpn_socks_bridge
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "start_nordvpn_socks_bridge([--port N] [--socks-host HOST] [--socks-port N] [--user U] [--pass P]) -> JSON"
|
||||
description: "Levanta un proxy HTTP local sin auth que reenvía al servidor SOCKS5 de NordVPN con auth usando gost v3. Resuelve la limitación de Chrome, que no soporta SOCKS5-con-auth: el navegador apunta a http://127.0.0.1:<port> (sin auth) y el tráfico sale por NordVPN. Idempotente: si el puerto ya escucha, no relanza."
|
||||
tags: [navegator, vpn, proxy, nordvpn, socks5, gost, chrome, cdp]
|
||||
params:
|
||||
- name: "--port"
|
||||
desc: "Puerto HTTP local del bridge (default 8889)"
|
||||
- name: "--socks-host"
|
||||
desc: "Servidor SOCKS5 de NordVPN (default socks-nl1.nordvpn.com)"
|
||||
- name: "--socks-port"
|
||||
desc: "Puerto del servidor SOCKS5 (default 1080)"
|
||||
- name: "--user"
|
||||
desc: "Service username de NordVPN. Si se omite, lee NORDVPN_SOCKS_USER del entorno"
|
||||
- name: "--pass"
|
||||
desc: "Service password de NordVPN. Si se omite, lee NORDVPN_SOCKS_PASS del entorno"
|
||||
output: "JSON en stdout: {proxy_url, pid, socks_host, status}. status puede ser 'running' (lanzado ahora) o 'already_running' (puerto ya escuchaba). Errores a stderr + exit 1."
|
||||
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/start_nordvpn_socks_bridge.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Con env vars (recomendado para scripts):
|
||||
NORDVPN_SOCKS_USER=xxx NORDVPN_SOCKS_PASS=yyy \
|
||||
bash bash/functions/infra/start_nordvpn_socks_bridge.sh \
|
||||
--port 8889 \
|
||||
--socks-host socks-nl1.nordvpn.com
|
||||
|
||||
# Salida (primera vez):
|
||||
# {"proxy_url":"http://127.0.0.1:8889","pid":12345,"socks_host":"socks-nl1.nordvpn.com","status":"running"}
|
||||
|
||||
# Salida (idempotente, ya corría):
|
||||
# {"proxy_url":"http://127.0.0.1:8889","pid":null,"socks_host":"socks-nl1.nordvpn.com","status":"already_running"}
|
||||
|
||||
# Luego Chrome (o el flujo CDP del navegator) apunta al bridge:
|
||||
# chrome.exe --proxy-server=http://127.0.0.1:8889
|
||||
|
||||
# Verificar que el tráfico sale por NordVPN:
|
||||
# curl -x http://127.0.0.1:8889 https://api.ipify.org
|
||||
# -> 109.202.99.x (IP NordVPN NL, no la IP de casa)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesitas que Chrome (o cualquier app que solo acepta proxy HTTP sin auth) salga por NordVPN, pero NordVPN solo ofrece SOCKS5-con-auth. Chrome no soporta SOCKS5-with-authentication — este bridge actúa de intermediario sin auth local. Útil especialmente en el flujo CDP del navegator (cdp-cli + agente browser) cuando quieres que el browser de automatización salga con IP NordVPN para evadir DPI del ISP o geo-bloqueos, sin exponer las credenciales NordVPN al proceso del browser.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **NordVPN SOCKS5 exige service credentials**, no el usuario/contraseña de la cuenta NordVPN. Se obtienen en dashboard.nordvpn.com → Manual Setup → Service credentials.
|
||||
- **Chrome no soporta SOCKS5-auth** nativamente (a diferencia de Firefox que sí). Por eso este bridge HTTP-sin-auth es necesario.
|
||||
- **gost escucha en todas las interfaces** (`-L http://:PORT`). El puerto local NO tiene autenticación. No exponer en redes no confiables. Para bind solo a loopback cambiar el flag a `-L http://127.0.0.1:PORT` en el script si es necesario.
|
||||
- **Servidores NordVPN SOCKS5 disponibles**: `socks-nl1..8.nordvpn.com`, `socks-de1..4.nordvpn.com`, `socks-us1..8.nordvpn.com`, etc. La lista completa en el dashboard de NordVPN.
|
||||
- **Si gost no está instalado**: se descarga automáticamente `gost v3.0.0 linux amd64` a `~/.local/bin/gost`. Requiere curl y tar.
|
||||
- **Log**: en `/tmp/nordvpn_socks_bridge_<port>.log`. Consultar si el bridge no arranca.
|
||||
- **PID null en already_running**: cuando el puerto ya escuchaba, el PID del proceso no se recupera (habría que hacer `lsof`/`ss` para identificarlo).
|
||||
- **Consumidor principal**: flujo `navegator`/CDP — ver `docs/capabilities/navegator.md`. El agente browser lanza este bridge antes de abrir Chrome con `--proxy-server=http://127.0.0.1:<port>`.
|
||||
- **Gotcha invocación desde el Bash tool de Claude (exit 144)**: el script deja gost en background (`nohup ... & disown`); ese daemon retiene el pipe de stdout del tool → el harness mata el proceso con SIGSTKFLT (exit 144) AUNQUE el bridge SÍ arranca bien. Lanzar con `run_in_background:true` o redirigiendo todo (`>/tmp/x 2>&1 </dev/null`) para evitarlo. En terminal real (o `fn run` interactivo) no ocurre. Verificado 2026-05-30: el bridge queda corriendo y funcional pese al 144.
|
||||
- **Windows→WSL**: si Chrome corre en Windows (chrome.exe) y gost en WSL2, Chrome alcanza `127.0.0.1:<port>` vía localhostForwarding de WSL2. Verificar con `curl -x http://127.0.0.1:<port> https://api.ipify.org` desde ambos lados.
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# start_nordvpn_socks_bridge — Levanta proxy HTTP local sin auth que reenvía a SOCKS5 NordVPN con auth via gost v3.
|
||||
# Resuelve la limitacion de Chrome que no soporta SOCKS5-con-auth.
|
||||
set -euo pipefail
|
||||
|
||||
# --- defaults ---
|
||||
PORT=8889
|
||||
SOCKS_HOST="socks-nl1.nordvpn.com"
|
||||
SOCKS_PORT=1080
|
||||
VPN_USER="${NORDVPN_SOCKS_USER:-}"
|
||||
VPN_PASS="${NORDVPN_SOCKS_PASS:-}"
|
||||
|
||||
# --- parse args ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--socks-host) SOCKS_HOST="$2"; shift 2 ;;
|
||||
--socks-port) SOCKS_PORT="$2"; shift 2 ;;
|
||||
--user) VPN_USER="$2"; shift 2 ;;
|
||||
--pass) VPN_PASS="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- validate creds ---
|
||||
if [[ -z "$VPN_USER" ]]; then
|
||||
echo "error: NORDVPN_SOCKS_USER not set. Use --user or export NORDVPN_SOCKS_USER" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$VPN_PASS" ]]; then
|
||||
echo "error: NORDVPN_SOCKS_PASS not set. Use --pass or export NORDVPN_SOCKS_PASS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="/tmp/nordvpn_socks_bridge_${PORT}.log"
|
||||
|
||||
# --- check idempotencia: ya escucha? ---
|
||||
if ss -ltn 2>/dev/null | grep -q ":${PORT} "; then
|
||||
echo "{\"proxy_url\":\"http://127.0.0.1:${PORT}\",\"pid\":null,\"socks_host\":\"${SOCKS_HOST}\",\"status\":\"already_running\"}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- asegurar gost ---
|
||||
GOST_BIN=""
|
||||
if command -v gost &>/dev/null; then
|
||||
GOST_BIN="$(command -v gost)"
|
||||
elif [[ -x "$HOME/.local/bin/gost" ]]; then
|
||||
GOST_BIN="$HOME/.local/bin/gost"
|
||||
else
|
||||
echo "gost not found, downloading v3.0.0 linux amd64..." >&2
|
||||
GOST_URL="https://github.com/go-gost/gost/releases/download/v3.0.0/gost_3.0.0_linux_amd64.tar.gz"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
curl -fsSL "$GOST_URL" -o "$TMP_DIR/gost.tar.gz" >&2
|
||||
tar -xzf "$TMP_DIR/gost.tar.gz" -C "$TMP_DIR" >&2
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
cp "$TMP_DIR/gost" "$HOME/.local/bin/gost"
|
||||
chmod +x "$HOME/.local/bin/gost"
|
||||
rm -rf "$TMP_DIR"
|
||||
GOST_BIN="$HOME/.local/bin/gost"
|
||||
echo "gost installed to $GOST_BIN" >&2
|
||||
fi
|
||||
|
||||
# --- url-encode user y pass (puede tener caracteres especiales) ---
|
||||
url_encode() {
|
||||
python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=''))" "$1"
|
||||
}
|
||||
|
||||
ENC_USER="$(url_encode "$VPN_USER")"
|
||||
ENC_PASS="$(url_encode "$VPN_PASS")"
|
||||
|
||||
# --- lanzar gost en background ---
|
||||
nohup "$GOST_BIN" \
|
||||
-L "http://:${PORT}" \
|
||||
-F "socks5://${ENC_USER}:${ENC_PASS}@${SOCKS_HOST}:${SOCKS_PORT}" \
|
||||
>"$LOG_FILE" 2>&1 &
|
||||
GOST_PID=$!
|
||||
disown $GOST_PID
|
||||
|
||||
# --- esperar ~2s y verificar que el puerto escucha ---
|
||||
sleep 2
|
||||
if ! ss -ltn 2>/dev/null | grep -q ":${PORT} "; then
|
||||
echo "error: gost did not start. Last lines of $LOG_FILE:" >&2
|
||||
tail -10 "$LOG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "{\"proxy_url\":\"http://127.0.0.1:${PORT}\",\"pid\":${GOST_PID},\"socks_host\":\"${SOCKS_HOST}\",\"status\":\"running\"}"
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: wg_client_install
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "wg_client_install(config_path_or_stdin, [interface_name]) -> json"
|
||||
description: "Device-side: instala wg0.conf en /etc/wireguard/, habilita systemd wg-quick@wg0, verifica handshake con hub. Idempotente. Acepta config por path o stdin (para pipes desde wg_client_config)."
|
||||
tags: [wireguard, client, install, mesh, systemd]
|
||||
uses_functions: [wg_install_bash_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: config_path_or_stdin
|
||||
desc: "path al archivo .conf existente, o '-' para leer de stdin (compatible con pipe desde wg_client_config)"
|
||||
- name: interface_name
|
||||
desc: "nombre de la interfaz WireGuard (default: wg0). Determina /etc/wireguard/<iface>.conf y la unit systemd wg-quick@<iface>"
|
||||
output: "JSON {status, interface, hub_endpoint, handshake_seen}. status: installed | already-configured | installed-no-handshake | installed-no-systemd"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/wg_client_install.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/wg_client_install.sh
|
||||
|
||||
# Desde pipe (caso más común en flow 0009):
|
||||
wg_client_config_go_infra | jq -r '.INI' | wg_client_install -
|
||||
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
|
||||
|
||||
# Desde archivo .conf generado previamente:
|
||||
wg_client_install /tmp/peer_laptop.conf
|
||||
# {"status":"installed","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
|
||||
|
||||
# Con interfaz personalizada:
|
||||
wg_client_install /tmp/peer_laptop.conf wg1
|
||||
# {"status":"installed","interface":"wg1","hub_endpoint":"203.0.113.1:51820","handshake_seen":true}
|
||||
|
||||
# Segunda ejecución con misma config (idempotente):
|
||||
wg_client_install /tmp/peer_laptop.conf
|
||||
# {"status":"already-configured","interface":"wg0","hub_endpoint":"203.0.113.1:51820","handshake_seen":false}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites conectar un nuevo peer al mesh WireGuard en el flow 0009. Úsala justo después de `wg_client_config` (que genera el .conf) para instalarlo en el device peer. Es el paso final del onboarding de un nodo: config generada → instalada → verificada con handshake.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Requiere root/sudo** para escribir en `/etc/wireguard/`, hacer `chmod 600`, y ejecutar `systemctl`. El operador debe tener `sudo` sin password para estos comandos, o ejecutar la función como root.
|
||||
- **Idempotente por contenido**: si `/etc/wireguard/<iface>.conf` ya existe con el mismo contenido, retorna `status=already-configured` sin tocar nada. Si el contenido difiere, hace backup automático con timestamp antes de sobreescribir.
|
||||
- **NetworkManager**: si NM gestiona la interfaz wg0, `wg-quick` puede fallar con conflicto. Solución: crear `/etc/NetworkManager/conf.d/99-wg.conf` con `[keyfile]\nunmanaged-devices=interface-name:wg0` y reiniciar NM antes de ejecutar esta función.
|
||||
- **WSL2 sin systemd** (variantes antiguas o sin `/etc/wsl.conf` con `[boot] systemd=true`): `systemctl` no está disponible. La función detecta esto, emite `status=installed-no-systemd` con instrucciones en stderr para levantar la interfaz manualmente con `sudo wg-quick up wg0`. Para autostart en WSL2 sin systemd: añadir `sudo wg-quick up wg0` al final de `~/.bashrc`.
|
||||
- **WSL2 con systemd**: kernel WSL2 >= 5.6 (default en distros recientes) incluye WireGuard built-in. Habilitar systemd en WSL2 con `[boot]\nsystemd=true` en `/etc/wsl.conf` y reiniciar WSL. Luego esta función funciona igual que en Linux nativo.
|
||||
- **Android / Termux**: NO usar esta función. Termux no tiene systemd ni `/etc/wireguard/`. En Android usar la app WireGuard oficial (F-Droid / Play Store) e importar el .conf generado por `wg_client_config` directamente desde la app.
|
||||
- **handshake_seen=false con status=installed-no-handshake**: la interfaz está activa pero el hub no ha respondido en 10s. No es un error fatal — puede tardar más si el hub está ocupado o hay NAT traversal pendiente. Verificar: endpoint accesible por UDP, hub corriendo con `wg show`, claves public/preshared coincidentes.
|
||||
- Los logs van siempre a stderr con prefijo `[wg_client_install]`; stdout es exclusivamente el JSON de resultado.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
<!-- Rellenar solo cuando haya version bump real -->
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# wg_client_install — Device-side: instala wg0.conf en /etc/wireguard/, habilita
|
||||
# systemd wg-quick@<iface>, verifica handshake con hub. Idempotente.
|
||||
# Acepta config por path o stdin ("-").
|
||||
# Exit 0 = éxito (installed o already-configured), 1 = error fatal.
|
||||
|
||||
wg_client_install() {
|
||||
local config_src="${1:--}"
|
||||
local iface="${2:-wg0}"
|
||||
local conf_dest="/etc/wireguard/${iface}.conf"
|
||||
local config_content="" hub_endpoint="" handshake_seen="false"
|
||||
|
||||
_wg_ci_log() { echo "[wg_client_install] $*" >&2; }
|
||||
|
||||
# ── Prereq: wg debe estar instalado ──────────────────────────────────────
|
||||
if ! command -v wg &>/dev/null; then
|
||||
_wg_ci_log "ERROR: 'wg' no encontrado. Ejecuta wg_install primero."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Leer contenido del .conf ──────────────────────────────────────────────
|
||||
if [[ "${config_src}" == "-" ]]; then
|
||||
_wg_ci_log "Leyendo config desde stdin"
|
||||
config_content=$(cat) || { _wg_ci_log "ERROR: fallo al leer stdin"; return 1; }
|
||||
elif [[ -f "${config_src}" ]]; then
|
||||
_wg_ci_log "Leyendo config desde ${config_src}"
|
||||
config_content=$(cat "${config_src}") || { _wg_ci_log "ERROR: fallo al leer ${config_src}"; return 1; }
|
||||
else
|
||||
_wg_ci_log "ERROR: '${config_src}' no es un path existente ni '-' (stdin)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "${config_content}" ]]; then
|
||||
_wg_ci_log "ERROR: contenido de config vacío"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Extraer endpoint del hub para incluirlo en el JSON de salida ──────────
|
||||
hub_endpoint=$(printf '%s\n' "${config_content}" | grep -m1 '^Endpoint\s*=' | sed 's/.*=\s*//' | tr -d '[:space:]' || true)
|
||||
|
||||
# ── Idempotencia: comparar con conf existente ─────────────────────────────
|
||||
if [[ -f "${conf_dest}" ]]; then
|
||||
local existing_content
|
||||
existing_content=$(sudo cat "${conf_dest}" 2>/dev/null || cat "${conf_dest}" 2>/dev/null || true)
|
||||
if [[ "${existing_content}" == "${config_content}" ]]; then
|
||||
_wg_ci_log "Configuración idéntica ya presente en ${conf_dest}; nada que hacer"
|
||||
printf '{"status":"already-configured","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
|
||||
"${iface}" "${hub_endpoint}"
|
||||
return 0
|
||||
fi
|
||||
# Contenido difiere → backup + rewrite
|
||||
local backup="${conf_dest}.bak.$(date +%Y%m%d%H%M%S)"
|
||||
_wg_ci_log "Configuración existente difiere; backup → ${backup}"
|
||||
sudo cp "${conf_dest}" "${backup}" \
|
||||
|| { _wg_ci_log "ERROR: no se pudo hacer backup de ${conf_dest}"; return 1; }
|
||||
fi
|
||||
|
||||
# ── Crear directorio y escribir conf ─────────────────────────────────────
|
||||
sudo mkdir -p "/etc/wireguard" \
|
||||
|| { _wg_ci_log "ERROR: no se pudo crear /etc/wireguard"; return 1; }
|
||||
|
||||
printf '%s\n' "${config_content}" | sudo tee "${conf_dest}" >/dev/null \
|
||||
|| { _wg_ci_log "ERROR: no se pudo escribir ${conf_dest}"; return 1; }
|
||||
|
||||
sudo chmod 600 "${conf_dest}" \
|
||||
|| { _wg_ci_log "WARN: no se pudo chmod 600 ${conf_dest}"; }
|
||||
|
||||
_wg_ci_log "Config escrita en ${conf_dest} (chmod 600)"
|
||||
|
||||
# ── Habilitar + arrancar systemd unit ─────────────────────────────────────
|
||||
if ! command -v systemctl &>/dev/null; then
|
||||
_wg_ci_log "WARN: systemctl no disponible."
|
||||
_wg_ci_log " En WSL2 sin systemd: ejecuta 'sudo wg-quick up ${iface}' manualmente."
|
||||
_wg_ci_log " Para autostart en WSL2: añade 'sudo wg-quick up ${iface}' a ~/.bashrc o usa WSL2 con systemd habilitado."
|
||||
printf '{"status":"installed-no-systemd","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
|
||||
"${iface}" "${hub_endpoint}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_wg_ci_log "Habilitando y arrancando wg-quick@${iface}"
|
||||
if ! sudo systemctl enable --now "wg-quick@${iface}" 2>&1 | tee /dev/stderr >&2; then
|
||||
_wg_ci_log "ERROR: systemctl enable --now wg-quick@${iface} falló."
|
||||
_wg_ci_log " En WSL2: asegúrate de tener kernel >= 5.6 y systemd habilitado (/etc/wsl.conf: [boot] systemd=true)."
|
||||
_wg_ci_log " Si NetworkManager gestiona ${iface}: añade 'unmanaged-devices=interface-name:${iface}' a /etc/NetworkManager/conf.d/99-wg.conf"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_wg_ci_log "wg-quick@${iface} habilitado y activo"
|
||||
|
||||
# ── Esperar handshake (hasta 10 s) ────────────────────────────────────────
|
||||
local deadline=$(( $(date +%s) + 10 ))
|
||||
_wg_ci_log "Esperando handshake en ${iface} (timeout 10s)..."
|
||||
while [[ $(date +%s) -lt ${deadline} ]]; do
|
||||
local hs_output
|
||||
hs_output=$(sudo wg show "${iface}" latest-handshakes 2>/dev/null || true)
|
||||
# latest-handshakes devuelve "<pubkey> <unix_ts>"; ts > 0 = handshake visto
|
||||
if printf '%s\n' "${hs_output}" | awk '{print $2}' | grep -qE '^[1-9][0-9]+$'; then
|
||||
handshake_seen="true"
|
||||
_wg_ci_log "Handshake confirmado en ${iface}"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "${handshake_seen}" == "false" ]]; then
|
||||
_wg_ci_log "WARN: timeout esperando handshake en ${iface}. La interfaz está activa pero el hub no ha respondido aún."
|
||||
_wg_ci_log " Verifica: endpoint accesible, hub corriendo, claves correctas."
|
||||
printf '{"status":"installed-no-handshake","interface":"%s","hub_endpoint":"%s","handshake_seen":false}\n' \
|
||||
"${iface}" "${hub_endpoint}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '{"status":"installed","interface":"%s","hub_endpoint":"%s","handshake_seen":true}\n' \
|
||||
"${iface}" "${hub_endpoint}"
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: wg_hub_setup
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "wg_hub_setup(private_key, subnet_cidr, listen_port) -> json"
|
||||
description: "Configura el host como hub WireGuard (servidor). Crea /etc/wireguard/wg0.conf con clave privada + IP pool + ListenPort. Abre UDP en firewall (ufw o iptables), habilita ip_forward persistente en /etc/sysctl.d/99-wireguard.conf, persiste y arranca systemd unit wg-quick@wg0. Idempotente: misma PrivateKey = no-op; PrivateKey distinta = backup + rewrite."
|
||||
tags: [wireguard, hub, infra, mesh, systemd]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: private_key
|
||||
desc: "base64 WG private key del hub (44 chars, generada por wg_keygen o `wg genkey`)"
|
||||
- name: subnet_cidr
|
||||
desc: "subnet hub con bits del host, ej. 10.42.0.1/24. El hub recibe la .1"
|
||||
- name: listen_port
|
||||
desc: "UDP port donde escucha WireGuard (default 51820, rango 1024-65535)"
|
||||
output: "JSON {status, config_path, interface, hub_ip}. status: configured | reconfigured | already-configured"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/wg_hub_setup.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Generar clave (o usar wg_keygen del registry)
|
||||
PRIVKEY=$(wg genkey)
|
||||
|
||||
source bash/functions/infra/wg_hub_setup.sh
|
||||
wg_hub_setup "$PRIVKEY" "10.42.0.1/24" 51820
|
||||
# {"status":"configured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
|
||||
|
||||
# Segunda ejecución con la misma clave → no-op
|
||||
wg_hub_setup "$PRIVKEY" "10.42.0.1/24" 51820
|
||||
# {"status":"already-configured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
|
||||
|
||||
# Cambiar clave → backup de conf anterior + rewrite
|
||||
wg_hub_setup "$NUEVA_PRIVKEY" "10.42.0.1/24" 51820
|
||||
# {"status":"reconfigured","config_path":"/etc/wireguard/wg0.conf","interface":"wg0","hub_ip":"10.42.0.1"}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites convertir un VPS/host en el nodo central (hub) de una red mesh WireGuard. Úsala inmediatamente después de `wg_install` para dejar el hub listo para recibir peers. El hub escucha en un puerto UDP público; los peers se conectan a él con su propia clave y la AllowedIPs del hub.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Requiere `sudo` con NOPASSWD para: `tee /etc/wireguard/`, `chmod`, `sysctl`, `iptables`/`ufw`, `systemctl`. Configurar antes en sudoers.
|
||||
- NUNCA reusar la misma `private_key` entre hubs distintos. Cada hub tiene su propio par de claves independiente.
|
||||
- El bloque `PostUp`/`PostDown` usa `eth0` como interfaz de salida para NAT. En VPS con interfaz distinta (ens3, enp3s0) editar `/etc/wireguard/wg0.conf` manualmente antes de reiniciar.
|
||||
- Conflicto de subnet con docker0 si usas 172.17.0.0/16. Evitar solapamiento — usar 10.42.x.x o 192.168.200.x para WireGuard.
|
||||
- `systemd-resolved` en VPS Ubuntu puede interferir con resolución DNS cuando WireGuard está activo si el conf añade `DNS =`. Esta función NO setea DNS para evitar el problema — configurarlo a nivel peer si se necesita.
|
||||
- Si `systemctl start wg-quick@wg0` falla, revisar logs con `journalctl -u wg-quick@wg0 -n 50`.
|
||||
- En entornos cloud (AWS/GCP/Azure) el security group / firewall de red del proveedor también debe abrir el puerto UDP, independientemente de ufw/iptables local.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
<!-- Rellenar solo cuando haya version bump real -->
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
# wg_hub_setup — Configura el host como hub WireGuard (servidor central).
|
||||
# Crea /etc/wireguard/wg0.conf con [Interface] block, abre UDP en firewall,
|
||||
# habilita ip_forward persistente, arranca y verifica wg-quick@wg0.
|
||||
# Idempotente: si el conf existe con la misma PrivateKey -> no-op.
|
||||
# Emite JSON a stdout. Logs a stderr con prefijo [wg_hub_setup].
|
||||
# Exit 0 = éxito, 1 = fallo.
|
||||
|
||||
wg_hub_setup() {
|
||||
local private_key="${1:-}"
|
||||
local subnet_cidr="${2:-10.42.0.1/24}"
|
||||
local listen_port="${3:-51820}"
|
||||
|
||||
_wg_hub_log() { echo "[wg_hub_setup] $*" >&2; }
|
||||
|
||||
# ── Validación de entradas ──────────────────────────────────────────────
|
||||
|
||||
# private_key: base64 estándar de 44 caracteres (32 bytes)
|
||||
if [[ -z "${private_key}" ]]; then
|
||||
_wg_hub_log "ERROR: private_key requerida (base64 44 chars, generada por wg genkey)"
|
||||
return 1
|
||||
fi
|
||||
if ! [[ "${private_key}" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then
|
||||
_wg_hub_log "ERROR: private_key no parece base64 válida (se esperan 44 chars terminando en '=')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# subnet_cidr: 10.x.x.x/nn
|
||||
if ! [[ "${subnet_cidr}" =~ ^10\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
|
||||
_wg_hub_log "ERROR: subnet_cidr debe ser 10.x.x.x/nn, recibido: '${subnet_cidr}'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# listen_port: 1024-65535
|
||||
if ! [[ "${listen_port}" =~ ^[0-9]+$ ]] || (( listen_port < 1024 || listen_port > 65535 )); then
|
||||
_wg_hub_log "ERROR: listen_port debe ser un entero entre 1024 y 65535, recibido: '${listen_port}'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Verificar que wireguard-tools esté instalado ────────────────────────
|
||||
if ! command -v wg &>/dev/null; then
|
||||
_wg_hub_log "ERROR: 'wg' no encontrado. Ejecuta wg_install primero."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v wg-quick &>/dev/null; then
|
||||
_wg_hub_log "ERROR: 'wg-quick' no encontrado. Instala wireguard-tools."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Extraer hub_ip (parte sin CIDR prefix) y determinar config_path ────
|
||||
local hub_ip="${subnet_cidr%%/*}"
|
||||
local config_path="/etc/wireguard/wg0.conf"
|
||||
local interface="wg0"
|
||||
local action_status=""
|
||||
|
||||
# ── Idempotencia: comparar PrivateKey existente ─────────────────────────
|
||||
if [[ -f "${config_path}" ]]; then
|
||||
local existing_key
|
||||
existing_key=$(sudo grep -E '^\s*PrivateKey\s*=' "${config_path}" 2>/dev/null \
|
||||
| head -n1 | sed 's/.*=\s*//')
|
||||
if [[ "${existing_key}" == "${private_key}" ]]; then
|
||||
_wg_hub_log "Config existente con misma PrivateKey — no-op (status=already-configured)"
|
||||
printf '{"status":"already-configured","config_path":"%s","interface":"%s","hub_ip":"%s"}\n' \
|
||||
"${config_path}" "${interface}" "${hub_ip}"
|
||||
return 0
|
||||
else
|
||||
_wg_hub_log "Config existente con PrivateKey DIFERENTE — haciendo backup y reescribiendo"
|
||||
local backup_path="${config_path}.bak.$(date +%Y%m%d%H%M%S)"
|
||||
sudo cp "${config_path}" "${backup_path}" \
|
||||
|| { _wg_hub_log "ERROR: no se pudo hacer backup en ${backup_path}"; return 1; }
|
||||
_wg_hub_log "Backup guardado en ${backup_path}"
|
||||
action_status="reconfigured"
|
||||
fi
|
||||
else
|
||||
action_status="configured"
|
||||
fi
|
||||
|
||||
# ── Asegurar que /etc/wireguard existe con permisos correctos ───────────
|
||||
if [[ ! -d /etc/wireguard ]]; then
|
||||
sudo mkdir -p /etc/wireguard \
|
||||
|| { _wg_hub_log "ERROR: no se pudo crear /etc/wireguard"; return 1; }
|
||||
sudo chmod 700 /etc/wireguard
|
||||
_wg_hub_log "Directorio /etc/wireguard creado"
|
||||
fi
|
||||
|
||||
# ── Escribir /etc/wireguard/wg0.conf ────────────────────────────────────
|
||||
_wg_hub_log "Escribiendo ${config_path} (Address=${subnet_cidr}, ListenPort=${listen_port})"
|
||||
sudo tee "${config_path}" > /dev/null <<EOF
|
||||
[Interface]
|
||||
Address = ${subnet_cidr}
|
||||
ListenPort = ${listen_port}
|
||||
PrivateKey = ${private_key}
|
||||
SaveConfig = false
|
||||
|
||||
# NAT: permite que los peers accedan a internet via este hub (opcional, comentar si no se desea)
|
||||
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
EOF
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_wg_hub_log "ERROR: no se pudo escribir ${config_path}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sudo chmod 600 "${config_path}" \
|
||||
|| { _wg_hub_log "ERROR: chmod 600 ${config_path} falló"; return 1; }
|
||||
_wg_hub_log "Permisos 600 aplicados a ${config_path}"
|
||||
|
||||
# ── Habilitar ip_forward persistente ────────────────────────────────────
|
||||
local sysctl_file="/etc/sysctl.d/99-wireguard.conf"
|
||||
if [[ ! -f "${sysctl_file}" ]] || ! grep -q "net.ipv4.ip_forward" "${sysctl_file}" 2>/dev/null; then
|
||||
_wg_hub_log "Habilitando ip_forward en ${sysctl_file}"
|
||||
echo "net.ipv4.ip_forward = 1" | sudo tee "${sysctl_file}" > /dev/null \
|
||||
|| { _wg_hub_log "ERROR: no se pudo escribir ${sysctl_file}"; return 1; }
|
||||
fi
|
||||
sudo sysctl -p "${sysctl_file}" >&2 \
|
||||
|| _wg_hub_log "WARN: sysctl -p falló (puede ignorarse si el kernel ya tiene ip_forward=1)"
|
||||
|
||||
# ── Abrir puerto en firewall ─────────────────────────────────────────────
|
||||
if command -v ufw &>/dev/null && sudo ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||
_wg_hub_log "ufw activo — abriendo UDP/${listen_port}"
|
||||
sudo ufw allow "${listen_port}/udp" >&2 \
|
||||
|| _wg_hub_log "WARN: ufw allow ${listen_port}/udp falló (verificar manualmente)"
|
||||
elif command -v iptables &>/dev/null; then
|
||||
_wg_hub_log "ufw inactivo — usando iptables para abrir UDP/${listen_port}"
|
||||
sudo iptables -C INPUT -p udp --dport "${listen_port}" -j ACCEPT 2>/dev/null \
|
||||
|| sudo iptables -A INPUT -p udp --dport "${listen_port}" -j ACCEPT >&2 \
|
||||
|| _wg_hub_log "WARN: iptables INPUT rule falló (verificar manualmente)"
|
||||
else
|
||||
_wg_hub_log "WARN: ni ufw ni iptables disponibles — abre el puerto ${listen_port}/udp manualmente"
|
||||
fi
|
||||
|
||||
# ── Detener interfaz si estaba corriendo (para aplicar nueva config) ────
|
||||
if sudo wg show "${interface}" &>/dev/null 2>&1; then
|
||||
_wg_hub_log "Interfaz ${interface} activa — deteniendo antes de reconfigurar"
|
||||
sudo systemctl stop "wg-quick@${interface}" 2>/dev/null \
|
||||
|| sudo wg-quick down "${interface}" 2>/dev/null \
|
||||
|| _wg_hub_log "WARN: no se pudo detener ${interface} (puede que no estuviera activa)"
|
||||
fi
|
||||
|
||||
# ── Habilitar y arrancar wg-quick@wg0 ────────────────────────────────────
|
||||
_wg_hub_log "Habilitando systemd unit wg-quick@${interface}"
|
||||
sudo systemctl enable "wg-quick@${interface}" >&2 \
|
||||
|| { _wg_hub_log "ERROR: systemctl enable wg-quick@${interface} falló"; return 1; }
|
||||
|
||||
_wg_hub_log "Arrancando wg-quick@${interface}"
|
||||
sudo systemctl start "wg-quick@${interface}" >&2 \
|
||||
|| { _wg_hub_log "ERROR: systemctl start wg-quick@${interface} falló"; return 1; }
|
||||
|
||||
# ── Verificar que la interfaz está UP ────────────────────────────────────
|
||||
local retries=5
|
||||
local up=0
|
||||
for (( i=0; i<retries; i++ )); do
|
||||
if sudo wg show "${interface}" &>/dev/null 2>&1; then
|
||||
up=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "${up}" -eq 0 ]]; then
|
||||
_wg_hub_log "ERROR: 'wg show ${interface}' falló tras ${retries}s — la interfaz no arrancó"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_wg_hub_log "Interfaz ${interface} UP (status=${action_status})"
|
||||
printf '{"status":"%s","config_path":"%s","interface":"%s","hub_ip":"%s"}\n' \
|
||||
"${action_status}" "${config_path}" "${interface}" "${hub_ip}"
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: wg_install
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "wg_install() -> json"
|
||||
description: "Instala wireguard + wireguard-tools en Linux (debian/ubuntu/fedora/arch). Idempotente. Carga modulo kernel. Emite JSON con distro detectada y version instalada."
|
||||
tags: [wireguard, install, infra, mesh, deploy]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params: []
|
||||
output: "JSON {status, distro, version}. status=installed o already-present."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "bash/functions/infra/wg_install.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/wg_install.sh
|
||||
wg_install
|
||||
# {"status":"installed","distro":"ubuntu","version":"wireguard-tools 1.0.20210914"}
|
||||
|
||||
# Si ya está instalado:
|
||||
wg_install
|
||||
# {"status":"already-present","distro":"ubuntu","version":"wireguard-tools 1.0.20210914"}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites asegurarte de que wireguard-tools está disponible en un host antes de configurar un peer o hub WireGuard. Úsala como paso previo en pipelines de bootstrapping de nodos mesh (flow wireguard).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Requiere `sudo` con NOPASSWD para apt-get/dnf/pacman y para modprobe. El operador debe haberlo configurado antes.
|
||||
- `modprobe wireguard` puede fallar en kernels < 5.6 sin DKMS instalado (wireguard-dkms). La función lo trata como advertencia, no como error fatal — la instalación de las herramientas igual se completa.
|
||||
- En RHEL/CentOS instala `epel-release` automáticamente antes de wireguard-tools.
|
||||
- Distros no reconocidas en `/etc/os-release ID` producen exit 1 con mensaje de error explícito en stderr.
|
||||
- Los logs van siempre a stderr con prefijo `[wg_install]`; stdout es exclusivamente el JSON de resultado.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
<!-- Rellenar solo cuando haya version bump real -->
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user