101 Commits

Author SHA1 Message Date
egutierrez 5aef738bc8 merge: quick/bigquery-functions — Funciones BigQuery Python, tipo BQClient, comando meta_bigq 2026-04-07 18:45:38 +02:00
egutierrez 126a20ce07 chore: update registry.db with BigQuery functions index
Reindexado con las nuevas funciones BigQuery y tipo BQClient.
2026-04-07 18:45:15 +02:00
egutierrez f3e62e8303 feat: add meta_bigq command for Metabase + BigQuery operations
Comando Claude con referencia completa de funciones Metabase y BigQuery,
flujos tipicos y ejemplos de uso combinado.
2026-04-07 18:45:11 +02:00
egutierrez 5965997c9e chore: add google-cloud-bigquery dependencies
Dependencias del SDK oficial de BigQuery para las funciones Python del registry.
2026-04-07 18:45:06 +02:00
egutierrez 690e68a542 feat: add BigQuery Python functions and BQClient type
Funciones CRUD completas para BigQuery: auth, datasets, tables, queries,
jobs, routines, load/export. Tipo BQClient como wrapper del SDK oficial.
2026-04-07 18:45:02 +02:00
egutierrez c311623a76 merge: quick/mantine-cpp-new-functions — Mantine v9, C++, OSINT refactor, nuevas funciones 2026-04-06 23:47:41 +02:00
egutierrez b1016ec845 chore: add Kotlin directory structure, update registry.db and gitignore
Añade estructura inicial kotlin/functions/, actualiza registry.db con todos
los cambios indexados, y ajusta .gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:27 +02:00
egutierrez cbc4c5eafa feat: add Python core and infra functions — PWA, geocoding, POI matching
Nuevas funciones Python: build_guide_prompt, generate_pwa_manifest,
generate_service_worker, match_pois_to_interests (core), nominatim_reverse_geocode,
ollama_chat, overpass_nearby_pois (infra). Incluye tests unitarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:19 +02:00
egutierrez 89e443ab18 feat: add bash infra functions — Gitea, Android SDK, Mantine, Capacitor
Nuevas funciones bash: gestión Gitea (create_repo, list_repos, add_collaborator,
push_directory), install_android_sdk, install_mantine, frontend_doctor.
Pipelines: capacitor_build_apk y gitea_init_app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:10 +02:00
egutierrez f4932ce64c refactor: reorganize OSINT types — genéricos a core, específicos en cybersecurity
Mueve tipos genéricos (Person, Organization, Location, Email, Phone, Document,
Domain, Event, SocialMedia) de cybersecurity a core. Mantiene en cybersecurity
solo los específicos de seguridad (CryptoWallet, IPAddress, Malware, Vulnerability).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:47:01 +02:00
egutierrez 2d108c295a refactor: migrate frontend from shadcn/Tailwind to Mantine v9
Reescribe todos los componentes UI para usar Mantine v9 en lugar de shadcn/Tailwind.
Elimina cn(), CVA, components.json, theme_provider custom y globals.css con Tailwind.
Añade 25+ componentes nuevos (AppShell, AuthForm, DatePickerInput, Dropzone, etc.)
y MantineProvider como wrapper estándar del sistema de temas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:46:44 +02:00
egutierrez 73a4c3a148 feat: add C++ support with ImGui/ImPlot framework and vendor submodules
Añade soporte C++ al registry: vendor submodules (glfw, imgui, implot, tracy),
sistema de build con CMake y toolchains cross-platform, runner C++ en fn CLI,
parser de tests Google Test, y funciones bash para build Linux/Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:46:36 +02:00
egutierrez 356dbcdadd feat: include registry.db in repo and ignore broken_paths.txt
Share the SQLite registry database so apps/analysis repos can consume
it without needing the full function tree to rebuild.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 00:55:19 +02:00
egutierrez 1233efb31d merge: quick/docs-params-schema — documentación de params_schema 2026-04-06 00:35:49 +02:00
egutierrez 513c2fb4a7 docs: documenta params_schema en CLAUDE.md y templates
Actualiza schema rápido, ejemplo FTS5, sección de añadir funciones y los tres
templates (function, pipeline, component) con los campos params/output obligatorios.
2026-04-06 00:35:40 +02:00
egutierrez 9b5c430f7f merge: quick/params-schema-composability — params_schema para composabilidad de funciones 2026-04-05 18:45:27 +02:00
egutierrez 5f4f1f7508 docs: params/output semántico en 506 funciones para composabilidad
Añade campos params y output al frontmatter YAML de las 506 funciones del registry.
Cada parámetro tiene descripción semántica (qué representa, unidades, rango típico)
y cada función describe qué produce su output. Permite a agentes razonar sobre
cadenas de composición (ej: prices → log_return → sharpe_ratio) sin leer código.
2026-04-05 18:45:16 +02:00
egutierrez 9b4bb3aabc feat: fn check params y fn show muestra params_schema
Nuevo subcomando 'fn check params' lista funciones sin params_schema documentado.
'fn show' ahora muestra el campo Params con el JSON semántico de inputs/outputs.
2026-04-05 18:45:05 +02:00
egutierrez 34ecadf5a4 feat: add params_schema column for function composability
Nueva columna params_schema en functions con migración 009. Almacena JSON
con descripción semántica de inputs/outputs para que agentes razonen sobre
composabilidad de funciones. Incluye: campo en modelo Go, parsing de params/output
del frontmatter YAML, serialización a JSON, FTS5 rebuild con nueva columna,
hash de contenido actualizado, y warning en indexer cuando faltan params.
2026-04-05 18:45:01 +02:00
egutierrez b55f120a00 merge: quick/unit-tests-e2e-tables — tablas unit_tests y e2e_tests, parser de tests, docs 2026-04-05 18:19:48 +02:00
egutierrez 89730911c2 chore: añade directorio dev/ con issues y funciones implementadas
Tracking de issues completados (jupyter tools) y funciones implementadas (specs de diseño ya resueltas).
2026-04-05 18:19:36 +02:00
egutierrez 3a3a8fd9a9 docs: convenciones de testing y schema unit_tests/e2e_tests
Nuevo docs/testing.md con convenciones de test por lenguaje (Go, Python, Bash con 3 opciones), tablas unit_tests y e2e_tests, consultas FTS5 de ejemplo. Actualiza functions.md y CLAUDE.md con referencia a unit_tests.
2026-04-05 18:19:26 +02:00
egutierrez 29b1c4cd8b feat: fn index extrae unit_tests automáticamente
El indexer lee test_file_path de funciones testeadas, parsea los test cases y los inserta en unit_tests. El output de fn index ahora muestra el conteo de unit_tests extraídos.
2026-04-05 18:19:21 +02:00
egutierrez 131f860a94 feat: parser automático de test files Go/Python/Bash
Extrae test cases individuales con su código desde archivos _test. Go detecta func TestXxx, Python detecta def test_xxx, Bash soporta tres convenciones: test_xxx(){}, secciones === nombre ===, y comentarios # Test:.
2026-04-05 18:19:17 +02:00
egutierrez 9660a1c432 feat: modelos y CRUD para unit_tests y e2e_tests
UnitTest en registry con Insert, GetByFunction, Search FTS5, Purge. E2ETest en fn_operations con Insert, Get, List, UpdateResult, Delete. Ambos con scan helpers y serialización JSON.
2026-04-05 18:19:10 +02:00
egutierrez 256e038cbe feat: tablas unit_tests y e2e_tests
Migración 008 en registry.db para unit_tests con FTS5 (tests individuales extraídos de archivos de test). Migración 004 en operations.db para e2e_tests con FTS5 (tests de integración entre funciones dentro de apps).
2026-04-05 18:19:05 +02:00
egutierrez b406b29074 merge: quick/bulk-functions-sources-notebook — funciones Go/Python/Bash, tipos, notebook, sources 2026-04-05 17:13:13 +02:00
egutierrez 834e910bcf fix: pivot_test comparaciones de tipo — sum retorna float64, no int
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:13:03 +02:00
egutierrez 7605a5760a chore: dependencias Python, sources manifest, reglas de extracción y comando extract-source
Actualiza pyproject.toml con nuevas dependencias (pdfplumber, python-docx, ebooklib, openpyxl, etc.).
Actualiza sources.yaml con funciones extraídas de repos externos.
Mejora reglas de extracción en sources.md.
Añade comando Claude extract-source para workflow de extracción.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:12:05 +02:00
egutierrez 9f4ac6de32 feat: funciones Bash — install_nbconvert, notebook_to_pdf, export_analysis_pdfs
Infra: install_nbconvert (instala nbconvert+deps), notebook_to_pdf (convierte .ipynb a PDF).
Pipeline: export_analysis_pdfs (exporta todos los notebooks de analysis/ a PDF).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:57 +02:00
egutierrez a9f2c60e3d feat: mejoras notebook functions — discover multi-servidor, write batch ops
jupyter_discover: soporte multi-servidor, detección de modo colaborativo mejorada.
jupyter_write: operaciones batch (insert, edit, delete), manejo robusto de Y.js.
jupyter_exec: mejoras en ejecución directa al kernel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:50 +02:00
egutierrez 9fd0ca9cac feat: funciones Python infra y tipos Python (core, datascience, infra)
Infra: cache_to_file, cache_to_sqlite, http_download_file, http_get_json,
http_post_json, read_file_with_encoding, safe_extract_zip, scan_directory,
setup_logger, normalize_zip_filenames.
Tipos: 30+ tipos core (agent_action, context, task, message, parse_result...),
6 tipos datascience (entity_candidate, extraction_result...), 2 tipos infra.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:43 +02:00
egutierrez 63a9cb5273 feat: funciones Python datascience, finance, cybersecurity y pipelines
Datascience: aggregate_by_group, deduplicate_entities/relations, detect_drift,
diff_entities/relations, extract_entities/relations_llm, hotness_score, melt,
merge_graphs, pivot, build_entity/relation_schema_prompt.
Finance: avellaneda_stoikov_quotes, generate_gbm_prices, generate_taker_order,
hawkes_intensity + módulo finance.py.
Cybersecurity: envelope_encrypt/decrypt + módulo cybersecurity.py.
Pipelines: extraction_pipeline, monte_carlo_market, run_market_sim.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:32 +02:00
egutierrez 25a392df48 feat: funciones Python core — parsers, formatters, retry, serialización, LLM utils y más
178 archivos: módulo core.py actualizado + ~80 funciones nuevas con tests.
Incluye: parse_llm_json, extract_text_from_file, retry_with_backoff, circuit_breaker,
from_csv/to_csv, from_jsonl/to_jsonl, html_to_markdown, pdf_to_markdown, docx/epub/excel
converters, cache_decorator, react_loop, task_manager, template rendering, entre otros.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:21 +02:00
egutierrez 9c0d24d3ef feat: funciones Go — core (cron, join_by_key, validate_struct), datascience (pivot, diff_entities), infra (http, cache, cron_ticker)
Nuevas funciones Go con tests en tres dominios:
- core: parse_cron_expr, next_cron_time, join_by_key, validate_struct_fields + tipo CronSchedule
- datascience: pivot (tabla dinámica), diff_entities (comparación de entidades)
- infra: http_get_json, http_post_json, http_download_file, cache_to_sqlite, cron_ticker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:12 +02:00
egutierrez bee3b0d946 merge: quick/native-select-search-graph — native select, SearchBar, GraphContainer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:49 +02:00
egutierrez af039f6023 feat: componente GraphContainer con sigma.js y graphology
Visualizacion interactiva de grafos con WebGL via sigma.js, estructura de
datos graphology, y layout ForceAtlas2 adaptativo. Soporta grafos dirigidos
multi-edge, leyenda de tipos de nodo, y eventos click/double-click.
Nuevas deps: graphology, sigma, recharts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:34 +02:00
egutierrez f168795bda feat: componente SearchBar con debounce y clear
Input de busqueda con icono, debounce configurable y boton de limpiar.
Exportado desde index.ts del barrel de UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:29 +02:00
egutierrez bbd2cbff3e refactor: migrar Select y SimpleSelect a native HTML select
Select reescrito de @base-ui/react primitives a <select> nativo con wrapper
para mantener la misma API visual (ChevronDown, estilos tema). SimpleSelect
actualizado para usar <select>/<optgroup> directamente sin intermediarios.
Checkbox corregido: import CheckboxIndicator separado reemplazado por
CheckboxPrimitive.Indicator para consistencia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:02:24 +02:00
egutierrez 056ce6679c merge: quick/frontmatter-fixes-new-components-osint — frontmatter fixes, UI components, OSINT types, indexer warnings 2026-04-03 03:24:20 +02:00
egutierrez eb9476503f chore: regla frontend_theming y comandos claude
Nueva regla para usar componentes @fn_library y sistema de temas CSS variables en todos los frontends. Añade directorio de comandos claude.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:52 +02:00
egutierrez e7a00e221e feat: funciones bash audit_registry_paths y validate_registry_paths
Pipeline para auditar file_path del registry contra disco y función shell para validar paths individuales.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:47 +02:00
egutierrez f61a4c4b18 feat: tipos OSINT para dominio cybersecurity
13 tipos product (person, email, domain, IP, phone, social_media, organization, location, vulnerability, malware, crypto_wallet, document, event) para modelar entidades de inteligencia de fuentes abiertas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:43 +02:00
egutierrez 40d6db312d feat: funciones core frontend — generate_theme_css, get_computed_color, get_theme_tokens
Utilidades TypeScript puras para generación de CSS desde tema, resolución de colores computados y extracción de tokens del sistema de temas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:38 +02:00
egutierrez c5bb64160f feat: nuevos componentes UI — accordion, avatar, breadcrumb, checkbox, command, dropdown, pagination, popover, radio, sheet, select, switch, textarea, toast
Componentes React accesibles basados en Radix UI con soporte de temas via CSS variables. Incluye barrel export en index.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:32 +02:00
egutierrez e89b78cc45 feat: warnings en indexer para file_path inexistentes en disco
Valida post-insert que file_path y test_file_path de funciones y tipos apunten a archivos reales. Reporta warnings sin bloquear el indexado.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:25 +02:00
egutierrez e33b306225 fix: corregir lang y file_path en frontmatter de funciones existentes
Normaliza lang: typescript → ts en funciones frontend y corrige file_path de functions/infra/ → functions/browser/ en funciones CDP. Actualiza referencias cn_typescript_core → cn_ts_core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:20 +02:00
egutierrez 9c859e96d8 merge: quick/ssh-pass-stubs-embedding-datascience — SSH, pass, stubs, embedding, datascience, jupyter fix 2026-04-02 22:04:29 +02:00
egutierrez 10d17f9362 chore: gitignore .local
Ignora directorio .local para archivos locales de desarrollo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:04:00 +02:00
egutierrez 974f704214 fix: jupyter_exec usa run_in_executor para execute_cell
Evita bloquear el event loop asyncio ejecutando execute_cell (operación
síncrona con websocket) en un thread executor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:59 +02:00
egutierrez 0fa16a033c feat: módulo embedding — encode, model CRUD, stores sqlvec y usearch
Funciones Python para embeddings: carga/guardado de modelos, encoding de
texto, y almacenamiento/búsqueda vectorial con sqlite-vec y usearch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:57 +02:00
egutierrez f851988d6f feat: funciones datascience — ops_to_rdf_triples, ops_to_sigma_json, render_sigma_html
Conversión de operations.db a triples RDF y formato sigma.js, más
renderizado HTML standalone con dark theme y ForceAtlas2 layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:51 +02:00
egutierrez e9a8cbf20f feat: build tags y stubs para clickhouse y duckdb
Añade build tags noclickhouse/noduckdb a las implementaciones reales y
crea stubs que devuelven error para compilar sin las dependencias CGO.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:49 +02:00
egutierrez 846012c087 feat: funciones pass para gestión de secretos — get, set, list, delete, generate, sync
Wrappers Bash sobre pass (password-store) para CRUD de secretos, generación
de contraseñas y sincronización con git. Incluye script de test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:44 +02:00
egutierrez 6d0d63cb23 feat: funciones SSH para infra — conn, check, exec, download, upload, tunnel
Conjunto completo de funciones SSH para operaciones remotas: conexión,
verificación de host, ejecución de comandos, transferencia de archivos
(upload/download) y gestión de túneles. Incluye tipo SSHConn y tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:03:41 +02:00
egutierrez b220f8c0be merge: quick/frontend-ui-components-sqlite-open — componentes UI nuevos y mejorados, sqlite_open basePath 2026-04-02 15:32:52 +02:00
egutierrez 4c52b41b7b feat: sqlite_open basePath — resuelve paths relativos desde directorio de config
Nuevo parámetro basePath en SQLiteOpen para resolver paths relativos
contra un directorio base (ej: filepath.Dir del archivo YAML de config)
en lugar del cwd del proceso. basePath vacío mantiene comportamiento anterior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:32:40 +02:00
egutierrez aea2131dcb feat: mejoras componentes UI — card variants, kpi_card slots, sparkline colors, bar_chart horizontal radius
- card: variantes default/borderless/ghost con ring condicional
- kpi_card: props unit, action, chart y delta con label/suffix personalizable
- sparkline: prop colors para colores por barra en variant bar
- bar_chart: radius condicional según orientación horizontal/vertical

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:32:35 +02:00
egutierrez 1aaeec5090 feat: componentes data_table y pie_chart — tabla con sorting/pagination y gráfico circular Recharts
Nuevos componentes React/TS en frontend/functions/ui/:
- data_table: tabla de datos con columnas tipadas, sorting, paginación y formato personalizable
- pie_chart: gráfico circular Recharts con tooltips, leyenda y paleta configurable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:32:28 +02:00
egutierrez 7e3599e3ac merge: quick/nordvpn-db-wails-frontend-notebook — NordVPN, DB multi-engine, Wails, frontend React/TS, Jupyter notebook, lorenz_step 2026-04-01 20:56:18 +02:00
egutierrez 29c8046d4e chore: actualizar deps Go, sources.yaml y funciones infra modificadas
Nuevas dependencias para ClickHouse, DuckDB, Postgres drivers.
Actualizar sources.yaml con funciones extraídas.
Ajustes menores en write_jupyter_launcher, write_mcp_jupyter_config y docker_run_container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:55 +02:00
egutierrez 125ef74358 docs: regla notebook_collaboration y actualización INDEX
Nueva regla para colaboración en notebooks Jupyter via funciones del registry.
Documenta el flujo discover → read → write → exec y las reglas de uso.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:48 +02:00
egutierrez 960f310bcf feat: lorenz_step datascience — paso del atractor de Lorenz
Función pura Go que calcula un paso del sistema de ecuaciones de Lorenz.
Útil para simulaciones de sistemas dinámicos y visualizaciones caóticas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:44 +02:00
egutierrez 268a76602a feat: funciones Jupyter notebook Python — discover, read, write, exec, kernel
Funciones Python para interactuar con Jupyter Lab programáticamente:
descubrir instancias, leer/escribir celdas, ejecutar código y gestionar kernels.
Reemplazan MCP jupyter con API REST + WebSocket directa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:39 +02:00
egutierrez dc78d8fea3 feat: funciones frontend React/TS — componentes UI, hooks Wails, charts y tipos
Componentes React reutilizables: card, dialog, tabs, select, alert, badge, button, input, label,
skeleton, tooltip, progress_bar, page_header, form_field, settings_page, crud_page, analytics_page,
dashboard_layout. Charts: area, bar, line, sparkline, kpi_card, chart_container.
Hooks Wails: use_wails_query, use_wails_mutation, use_wails_stream, use_wails_event, use_animated_canvas.
Funciones core: cn, format_compact, chart_colors, get_series_color, wails_cache, theme_config_to_colors.
Tipos: chart_series, wails_ipc, theme_config, component_variants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:34 +02:00
egutierrez e02a950ee0 feat: funciones Wails — scaffold, CRUD bindings, build, eventos y streaming
Funciones Go para crear apps Wails: scaffold estructura, bind CRUD genérico,
build multiplataforma, emit eventos y stream de datos al frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:24 +02:00
egutierrez a75170cbc6 feat: abstracción DB multi-engine — CRUD genérico y openers para SQLite, Postgres, ClickHouse, DuckDB
Funciones Go con interfaz unificada para operaciones DB: open, close, create_table, exec, query, insert_row, insert_batch.
Openers específicos por engine. Tipo DBConfig para configuración común.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:17 +02:00
egutierrez c33e907fef feat: funciones NordVPN bash y Go — CLI, contenedor Docker y parser de estado
Funciones bash para instalar, conectar, desconectar, estado, IP, ciudades, países y protocolo.
Funciones Go para gestionar contenedor NordVPN (run/start/stop) y parsear estado.
Incluye tipo NordVPNStatus y tests para el parser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:55:08 +02:00
egutierrez d7f2c00d7b feat: externalize apps/analysis to Gitea repos, add analysis table
- Migration 007: repo_url on apps table + analysis table with FTS5
- Analysis struct, parser, CRUD, validation, hash computation
- Selective purge: remote-only apps/analysis preserved across fn index
- CLI: fn app list/clone/pull, fn analysis list/clone/pull
- search/show/list now include analysis results
- Apps removed from git tracking (content lives in Gitea repos)
- .gitkeep for apps/ and analysis/ dirs
- Bash functions: jupyter analysis pipeline, shell utilities
- Browser domain: CDP functions moved from infra to browser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 04:23:51 +02:00
egutierrez 8f24157096 merge: quick/content-hash-sources-infra-functions — content hash, sources, funciones infra/core/PowerShell y app navegador 2026-03-30 14:25:18 +02:00
egutierrez 3b88857999 chore: schema rápido en CLAUDE.md, sync Metabase en CLI, fix main.py
Agrega documentación de schema rápido en CLAUDE.md, regla sources en INDEX.
CLI fn index sincroniza registry.db a directorio Metabase si existe.
fn show muestra campos source_*. Fix import en metabase_registry/main.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:54 +02:00
egutierrez 4d6ea9a910 feat: funciones PowerShell infra — firewall y portproxy
Funciones PowerShell para gestión de red en Windows: win_firewall_add_rule,
win_firewall_remove_rule, win_portproxy_add y win_portproxy_remove.
Útiles para configurar acceso de red en entornos WSL2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:45 +02:00
egutierrez bb38eedfd1 feat: app script_navegador y dashboard Metabase
App Go para ejecutar scripts de navegación automatizada usando las
funciones CDP del registry. Incluye script de creación de dashboard
en Metabase para monitoreo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:39 +02:00
egutierrez 9d3bfd2cd2 feat: cdp_wait_load y mejoras en CDP connect/launch
Nueva función cdp_wait_load para esperar carga completa de página.
CdpConnect ahora soporta host remoto via CdpConnectHost (útil para
WSL2 donde Chrome Windows escucha en IP distinta). Mejoras en
chrome_launch para configuración más flexible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:21 +02:00
egutierrez 90693fb32f feat: funciones infra — Docker, deploy, build y health check
Funciones impuras para gestión de contenedores: docker_build_image,
docker_compose_up/down, docker_volume_create/list/remove,
generate_dockerfile, write_dockerfile, go_build_binary, health_check_http,
deploy_app y stop_app. Todas con tests unitarios donde aplica.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:12 +02:00
egutierrez b5a6711c64 feat: funciones core — detect_cycle, generate_id, rewrite_rule
Tres funciones puras para el dominio core: detección de ciclos en grafos
dirigidos (DFS), generación de IDs determinísticos, y reescritura de
reglas con pattern matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:24:00 +02:00
egutierrez c72ae15429 feat: source attribution para funciones externas
Sistema de extracción de funciones desde repos externos. Agrega campos
source_repo, source_license y source_file en functions y types (migración 006).
Incluye manifest sources/sources.yaml, regla sources.md, parser con campos
de atribución, y template actualizado con los nuevos campos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:23:53 +02:00
egutierrez e3bb9c3b38 feat: content hash y timestamps inteligentes en registry
Agrega content_hash a functions, types y apps para detectar cambios reales
entre reindexaciones. Los timestamps created_at se preservan si el contenido
no cambió, y updated_at solo se actualiza cuando hay cambios efectivos.
Incluye migración 005, hash.go con SHA256 determinístico, y ajustes en
store/indexer/models para el nuevo flujo de timestamps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:23:45 +02:00
egutierrez 48caec5665 merge: quick/chrome-cdp-and-ops-logs — funciones Chrome CDP y logs en operations 2026-03-29 17:31:16 +02:00
egutierrez 169cb0853b feat: modelo Log y CRUD en fn_operations
Tipo Log con niveles debug/info/warn/error, source, entity_id y execution_id
opcionales. Migración 003_logs.sql y funciones InsertLog, GetLog, ListLogs
con filtros combinables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:31:03 +02:00
egutierrez add09c2faa feat: funciones Chrome CDP para automatización de navegador
10 funciones Go en infra/ para controlar Chrome via Chrome DevTools Protocol:
chrome_launch, cdp_connect, cdp_navigate, cdp_evaluate, cdp_screenshot,
cdp_click, cdp_type_text, cdp_wait_element, cdp_get_html, cdp_close.
WebSocket RFC 6455 implementado sin dependencias externas.
Incluye tests de integración con Chrome real.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:30:56 +02:00
egutierrez f748256c1d fix: findOpsDB falla con error en vez de crear operations.db en la raíz
Antes, si no encontraba operations.db subiendo directorios, hacía
fallback silencioso a ./operations.db — lo que creaba la BD en la raíz
violando la regla de db_locations. Ahora retorna error explícito
indicando que se debe ejecutar fn ops init en el directorio correcto.

También elimina operations.db espuria de la raíz (2 executions de
metabase_registry creadas por el fallback).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:29:47 +02:00
egutierrez 4b2240fbce merge: quick/metabase-ops-pipelines — pipelines operativos Metabase y fix permisos SQLite 2026-03-29 00:54:38 +01:00
egutierrez c2528c6ea4 docs: documentación completa de metabase_registry
Arquitectura de mounts Docker, tabla de databases, permisos SQLite
(nunca chown, siempre chmod), flujo para app nueva paso a paso,
y referencia a los 3 pipelines relacionados.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:54:28 +01:00
egutierrez dd324b7785 feat: pipelines Metabase — add ops db, create ops dashboard, fix permissions
Tres pipelines Python para gestionar operations.db en Metabase:
- metabase_add_ops_db: registra la operations.db de una app como database SQLite
- metabase_create_ops_dashboard: genera dashboard operativo con 14 cards (KPIs,
  distribución, executions, assertions) para cualquier app
- metabase_fix_permissions: arregla SQLITE_READONLY_DIRECTORY haciendo chmod
  777/666 sin chown (que se propaga al host via bind mount)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:54:24 +01:00
egutierrez 9095fe8c65 feat: dashboard apps y mejora layout del dashboard Overview
Dashboard fn-registry Apps con 10 cards: KPIs por lenguaje, dominio,
framework, dependencias y catálogo completo. Cards del Overview
ampliadas a grid de 24 columnas con tamaños más legibles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 00:54:18 +01:00
egutierrez d6240022a4 merge: quick/apps-table-metabase-dashboard — tabla apps, dashboard Metabase, funciones Python 2026-03-29 00:14:21 +01:00
egutierrez 405be396c8 feat: dashboard Metabase del registry + regla apps vs functions
Script Python que crea un dashboard en Metabase con 15 cards: KPIs
escalares, distribucion por lenguaje/dominio/kind/pureza, ranking de
funciones mas usadas y complejas, cobertura de tests y tabla cruzada.
Agrega regla apps_vs_functions que establece que codigo reutilizable va
en functions/ y codigo especifico/hardcodeado va en apps/.
2026-03-29 00:14:07 +01:00
egutierrez 2c15a0b5e9 feat: tabla apps en registry — modelo, parser, indexer y CLI
Agrega soporte completo para indexar aplicaciones del directorio apps/.
Cada app tiene un descriptor app.md con frontmatter YAML que el indexer
recoge automaticamente. Incluye migracion 004, modelo App, ParseAppMD,
ValidateApp, store CRUD con FTS5, y soporte en fn list/search/show.
Crea descriptores app.md para docker_tui, pipeline_launcher y metabase_registry.
2026-03-29 00:13:57 +01:00
egutierrez eaed99e52c feat: funciones Python para core, cybersecurity, datascience y finance
Agrega funciones Python reutilizables organizadas por dominio:
- core: composicion funcional (pipe, compose, map, filter, reduce, etc.)
- cybersecurity: analisis de amenazas y puertos
- datascience: estadisticas y deteccion de outliers
- finance: indicadores tecnicos y analisis financiero
2026-03-29 00:13:50 +01:00
egutierrez ac71d4b079 feat: pyrunner mejorado para fn run Python
Refactoriza la ejecucion de funciones Python en fn run. Extrae la logica
a pyrunner.go con soporte para importar dependencias del registry y
ejecutar con el venv del proyecto. Agrega WalCheckpoint en db.go para
que lectores externos vean datos actualizados tras fn index.
2026-03-29 00:13:46 +01:00
egutierrez f11f60d121 chore: gitignore completo — __pycache__, .env, venv, node_modules
Añadidos patrones recursivos para Python (__pycache__, .pyc, .venv),
Node (node_modules), y secrets (.env, .env.*) en cualquier subdirectorio.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:25:55 +01:00
egutierrez e0573302af merge: quick/fn-run-types-metabase — fn run multi-lenguaje, tipos Go unificados, metabase setup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:45 +01:00
egutierrez 54be36dd63 docs: CLAUDE.md actualizado con fn run, tipos Go en functions/, bash functions
Documentación de fn run para todos los lenguajes, nueva ubicación de tipos Go,
sección de uso por agentes. Añadidas funciones Bash del registry (shell, infra,
core, pipelines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:30 +01:00
egutierrez 72c572e1ea feat: metabase_setup Python, fix list_databases, volumen Docker en init_metabase
Nueva función metabase_setup para setup inicial via API. Fix list_databases
que no extraía data del response wrapper. Pipeline init_metabase soporta
--mb-volumes para montar SQLite como volumen con fix de permisos automático.
Añadido .env a gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:20 +01:00
egutierrez 2bae07d1f5 feat: fn run — ejecución multi-lenguaje de funciones y pipelines desde CLI
Nuevo comando que despacha automáticamente según lenguaje: Go pipelines con
go run, Go functions con go test/vet, Python con venv y -m para imports
relativos, Bash directo, TypeScript con tsx del frontend. Resolución por
nombre con desambiguación. Añadido GetFunctionsByName al store y tsx al frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:12 +01:00
egutierrez 9abaefeb00 fix: stubs shell y tui usan tipos nativos en firmas en vez de Result/Option de core
Result[T] y Option[T] de core no son accesibles desde otros paquetes sin import.
Cambiado a (T, error) y (T, bool) siguiendo la regla de tipos nativos en firmas.
Añadidas dependencias bubbletea/bubbles/lipgloss al go.mod raíz para que tui compile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:06 +01:00
egutierrez 05444f74d3 refactor: mover .go de tipos Go a functions/{domain}/ para compilación unificada
Los archivos .go de tipos ahora viven junto a las funciones en functions/{domain}/
(mismo paquete Go), resolviendo errores de compilación por tipos no encontrados
(Option, Pair, Result, etc.). Los .md de metadata permanecen en types/{domain}/
con file_path actualizado a functions/. Se elimina types.go duplicado de infra.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 23:23:00 +01:00
egutierrez 528a16cd5a merge: quick/frontend-setup-metabase-docs — frontend setup, metabase Go/Py, documentación en registry 2026-03-28 20:33:20 +01:00
egutierrez d549aa0314 chore: gitignore para node_modules, dist y __pycache__
Añade .gitignore en frontend/ y python/ para excluir artefactos
generados. Elimina node_modules/, dist/ y __pycache__/ del tracking.
2026-03-28 20:33:04 +01:00
egutierrez 3798e2d959 feat: setup frontend con pnpm, vite, react, tailwind y shadcn
Inicializa directorio frontend/ con stack React moderno:
pnpm + vite 8 + react 19 + tailwind v4 + shadcn v4 (base-nova).
Estructura functions/core (TS puro) y functions/ui (componentes React).
El indexer descubre frontend/functions/ y frontend/types/ automáticamente.
Elimina functions/components/ (legacy) y actualiza referencias en
CLAUDE.md y template de componentes.
2026-03-28 20:32:40 +01:00
1383 changed files with 79450 additions and 2768 deletions
+178 -11
View File
@@ -10,13 +10,26 @@ Registry personal de codigo reutilizable con busqueda FTS. Diseñado para compos
--- ---
## Explorar el registry (USAR SIEMPRE) ## Explorar el registry (OBLIGATORIO)
Antes de escribir codigo, SIEMPRE consulta registry.db para evitar duplicados y descubrir funciones reutilizables. **SIEMPRE** consulta registry.db antes de escribir codigo, crear funciones, o responder sobre el registry. No uses grep/glob sobre archivos .go/.md — la BD es la fuente de verdad.
**La BD contiene el codigo y la documentacion completa** de cada funcion y tipo en los campos `code`, `documentation` y `notes`. Estos campos tambien estan indexados en FTS5, asi que puedes buscar dentro del codigo y la documentacion directamente. Para leer el codigo de una funcion: `SELECT code FROM functions WHERE id = '...'`. Para leer su documentacion: `SELECT documentation FROM functions WHERE id = '...'`.
**Busquedas FTS5 obligatorias:** Usa SIEMPRE la tabla FTS5 para buscar tanto por `name` como por `description`. Esto encuentra coincidencias parciales y similares que una busqueda exacta perderia. Usa operadores FTS5: `OR` para ampliar, `*` para prefijos, `NEAR` para proximidad.
```bash ```bash
# FTS5 # Busqueda FTS5 por nombre Y descripcion (USAR SIEMPRE ESTE PATRON)
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'slice') ORDER BY name;" sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slice OR description:slice') ORDER BY name;"
# FTS5 con prefijo (encuentra slice, slicing, sliced...)
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:slic* OR description:slic*') ORDER BY name;"
# FTS5 en tipos
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH 'name:result OR description:result') ORDER BY name;"
# FTS5 por semantica de params (composabilidad)
sqlite3 registry.db "SELECT id, json_extract(params_schema, '$.output') FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'params_schema:retornos');"
# Por dominio # Por dominio
sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;" sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain = 'finance' ORDER BY name;"
@@ -24,7 +37,7 @@ sqlite3 registry.db "SELECT id, purity, signature FROM functions WHERE domain =
# Puras de un dominio # Puras de un dominio
sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;" sqlite3 registry.db "SELECT id, signature FROM functions WHERE domain = 'core' AND purity = 'pure' ORDER BY name;"
# Tipos # Tipos por dominio
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';" sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE domain = 'cybersecurity';"
# Dependencias # Dependencias
@@ -37,19 +50,46 @@ sqlite3 registry.db "SELECT id, kind, status, title FROM proposals WHERE status
sqlite3 registry.db ".schema" sqlite3 registry.db ".schema"
``` ```
**Regla:** Si necesitas saber si algo existe o hay algo similar, haz la consulta FTS5 sobre la BD. No asumas que no existe sin consultar primero.
### Schema rapido
**functions** — columnas: `id, name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, example, tested, tests, test_file_path, file_path, created_at, updated_at, props, emits, has_state, framework, variant, notes, documentation, code, content_hash, source_repo, source_license, source_file, params_schema`
- `params_schema`: JSON con semántica de inputs/outputs. Formato: `{"params":[{"name":"x","desc":"..."}],"output":"..."}`. Buscable via FTS5.
- Enums: `kind`(function|pipeline|component) `purity`(pure|impure) `lang`(go|py|bash|ps)
- Dominios: core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser
**types** — columnas: `id, name, lang, domain, version, algebraic, definition, description, tags, uses_types, file_path, created_at, updated_at, examples, notes, documentation, code, content_hash, source_repo, source_license, source_file`
- Enums: `algebraic`(product|sum)
**unit_tests** — columnas: `id, function_id, name, code, file_path, lang, created_at, updated_at`
- Extraidos automaticamente por `fn index` desde los archivos de test
- FK: `function_id``functions.id`
**FTS5 (columnas buscables):**
- `functions_fts`: id, name, description, tags, signature, domain, example, notes, documentation, code, params_schema
- `types_fts`: id, name, description, tags, domain, examples, notes, documentation, code
- `unit_tests_fts`: id, name, code, function_id, lang
--- ---
## Estructura ## Estructura
``` ```
fn-registry/ fn-registry/
functions/{domain}/ # .go + .md por funcion (core, finance, datascience, cybersecurity) functions/{domain}/ # .go + .md por funcion Y tipo Go (core, finance, datascience, cybersecurity)
functions/pipelines/ # Composiciones, siempre impuras functions/pipelines/ # Composiciones, siempre impuras
functions/components/ # React (.tsx) types/{domain}/ # Solo .md de tipos (los .go viven en functions/{domain}/)
types/{domain}/ # .go + .md por tipo python/functions/ # .py + .md por funcion Python
python/types/ # .py + .md por tipo Python
bash/functions/ # .sh + .md por funcion Bash (core, infra, io, shell)
frontend/ # pnpm + vite + react + mantine
frontend/functions/ # .tsx/.ts + .md (core para TS puro, ui para componentes React)
frontend/types/ # .ts + .md por tipo
registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones
fn_operations/ # Paquete Go: operations database (libreria) fn_operations/ # Paquete Go: operations database (libreria)
apps/ # Apps ejecutables (TUIs, CLIs) — modulos Go independientes, cada una con su operations.db apps/ # Apps ejecutables (TUIs, CLIs, scripts) — codigo NO reutilizable, cada una con su operations.db
analysis/ # Exploraciones Jupyter independientes — cada una con su venv, MCP y kernel conectado al registry
cmd/fn/ # CLI principal cmd/fn/ # CLI principal
docs/ # Specs de diseño docs/ # Specs de diseño
docs/templates/ # Plantillas de frontmatter docs/templates/ # Plantillas de frontmatter
@@ -76,6 +116,24 @@ fn search -k function -p pure -d core "slice"
fn list [-d domain] [-k kind] fn list [-d domain] [-k kind]
fn show <id> fn show <id>
fn add -k function # Template fn add -k function # Template
fn check params # Lista funciones sin params_schema
# Ejecutar funciones y pipelines (fn run)
fn run <id_or_name> [args...] # Ejecuta por ID o nombre
fn run init_metabase --project test # Go pipeline (go run .)
fn run setup_metabase_volume # Bash pipeline (bash <file>)
fn run metabase_setup_py_infra # Python (python/.venv/bin/python3 <file>)
fn run my_component_ts_core # TypeScript (frontend/node_modules/.bin/tsx <file>)
fn run filter_slice_go_core # Go function con tests (go test -v)
fn run docker_pull_image_go_infra # Go function sin tests (go vet)
# Despacho por lenguaje:
# go (con main.go en dir) → go run .
# go (con tests) → go test -v -count=1 -tags fts5 ./pkg/
# go (sin tests) → go vet -tags fts5 ./pkg/
# py → python/.venv/bin/python3 <file>
# bash → bash <file>
# ts → frontend/node_modules/.bin/tsx <file>
# Si el nombre es ambiguo, muestra los IDs para desambiguar.
# Proposals # Proposals
fn proposal add --kind new_function --title "..." --created-by agent [--target-id <id>] fn proposal add --kind new_function --title "..." --created-by agent [--target-id <id>]
@@ -96,16 +154,41 @@ fn ops assertion result add|list
`FN_REGISTRY_ROOT` env var permite que `fn ops` acceda a registry.db desde cualquier directorio. `FN_REGISTRY_ROOT` env var permite que `fn ops` acceda a registry.db desde cualquier directorio.
### Uso de fn run por agentes
`fn run` permite ejecutar directamente funciones y pipelines del registry desde la terminal. Usar para:
- Lanzar pipelines con sus argumentos: `./fn run init_metabase --project fn_registry`
- Correr tests de funciones Go: `./fn run filter_slice_go_core`
- Ejecutar scripts Python/Bash del registry sin montar paths manualmente
- Verificar que funciones Go compilan correctamente (go vet)
Entornos usados automaticamente:
- Python: `python/.venv/bin/python3` (venv del proyecto)
- TypeScript: `frontend/node_modules/.bin/tsx` (node del proyecto)
- Go: `go run .` / `go test` / `go vet` con `CGO_ENABLED=1 -tags fts5`
- Bash: `bash` del sistema
--- ---
## Añadir funciones ## Añadir funciones
1. Consulta la BD para verificar que no existe algo similar 1. Consulta la BD para verificar que no existe algo similar
2. Crea dos archivos: `functions/{domain}/{name}.go` + `functions/{domain}/{name}.md` 2. Crea dos archivos segun el lenguaje:
- Go: `functions/{domain}/{name}.go` + `.md`
- Python: `python/functions/{domain}/{name}.py` + `.md`
- Bash: `bash/functions/{domain}/{name}.sh` + `.md`
- TypeScript: `frontend/functions/{domain}/{name}.ts` + `.md`
3. Ejecuta `./fn index` y verifica con `./fn show {id}` 3. Ejecuta `./fn index` y verifica con `./fn show {id}`
Frontmatter del .md — ver template completo en `docs/templates/` o con `fn add -k function`. Frontmatter del .md — ver template completo en `docs/templates/` o con `fn add -k function`.
Campos `params` y `output` (obligatorios en frontmatter):
- `params`: lista de `{name, desc}` con descripción semántica de cada parámetro (qué representa, unidades, rango)
- `output`: descripción semántica de lo que retorna la función
- Para componentes: solo `output` (ya tienen `props`)
- Se indexan como JSON en `params_schema` y son buscables via FTS5
- `fn check params` lista funciones sin documentar
Reglas de integridad (el indexer las valida): Reglas de integridad (el indexer las valida):
- Pipeline → siempre impuro + uses_functions no vacio - Pipeline → siempre impuro + uses_functions no vacio
- Pure → returns_optional: false + error_type: "" - Pure → returns_optional: false + error_type: ""
@@ -118,7 +201,91 @@ Reglas de integridad (el indexer las valida):
## Añadir tipos ## Añadir tipos
Dos archivos: `types/{domain}/{name}.go` + `types/{domain}/{name}.md`. Ver template en `docs/templates/`. Dos archivos en directorios separados:
- **Codigo Go:** `functions/{domain}/{name}.go` (junto a las funciones, mismo paquete Go)
- **Metadata .md:** `types/{domain}/{name}.md` con `file_path` apuntando a `functions/{domain}/{name}.go`
Los `.go` de tipos viven en `functions/{domain}/` para que Go los compile en el mismo paquete que las funciones que los usan. Los `.md` se mantienen en `types/{domain}/` para que el indexer los identifique como tipos.
Ver template en `docs/templates/`.
---
## Analysis (exploraciones Jupyter)
Carpeta `analysis/` para exploraciones de datos con Jupyter + agentes Claude. Mismo patron que `apps/` — cada analisis es independiente con su propio venv, MCP y kernel.
**NO es codigo reutilizable** — son investigaciones ad-hoc. Si algo de un analisis resulta util, se extrae como funcion al registry.
### Estructura
```
analysis/
{tema}/ # Cada analisis es autonomo
.venv/ # Deps propias (gitignored)
.mcp.json # MCP jupyter apuntando a SU venv (gitignored)
.claude/CLAUDE.md # Reglas para agentes en este analisis
.ipython/profile_default/startup/ # Kernel startup con acceso al registry
00_fn_registry.py # Autocarga FN_REGISTRY_ROOT, helpers, sys.path
notebooks/ # Notebooks de exploracion
data/ # Datos locales (gitignored)
run-jupyter-lab.sh # Launcher Jupyter colaborativo
pyproject.toml # Deps gestionadas con uv
```
### Crear un analisis nuevo
```bash
# Basico
fn run init_jupyter_analysis finanzas
# Con paquetes extra
fn run init_jupyter_analysis ml scikit-learn torch
fn run init_jupyter_analysis duckdb polars duckdb
```
El pipeline `init_jupyter_analysis_bash_pipelines` compone 8 funciones atomicas del registry.
### Usar un analisis
```bash
# Terminal 1: lanzar Jupyter
cd analysis/{tema} && ./run-jupyter-lab.sh
# Terminal 2: abrir Claude con MCP jupyter
cd analysis/{tema} && claude
# Navegador: http://localhost:8888
```
### Acceso al registry desde notebooks
El kernel startup (`00_fn_registry.py`) se ejecuta automaticamente al abrir cualquier notebook y provee:
```python
# Helpers disponibles sin importar nada:
fn_search("slice") # Busca funciones y tipos por nombre/descripcion
fn_query("SELECT ...") # SQL directo sobre registry.db
fn_code("filter_list_py_core") # Codigo fuente de una funcion
# Importar funciones Python del registry directamente:
from core import filter_list, map_list, reduce_list
from finance import sma, ema, rsi
from metabase import MetabaseClient
# Variable de entorno disponible:
import os
os.environ["FN_REGISTRY_ROOT"] # Raiz del registry
```
### Reglas para agentes en analysis
Cada analisis tiene su `.claude/CLAUDE.md` con reglas especificas:
- Celdas inmutables: nunca modificar celdas existentes, solo anadir nuevas
- Programacion funcional obligatoria: funciones puras, sin mutacion
- Usar MCP jupyter para ejecutar codigo, nunca bash
- Notebooks en `notebooks/`, maximo 50 celdas por notebook
- Dependencias con `uv add`, nunca pip directo
--- ---
+215
View File
@@ -0,0 +1,215 @@
# /extract-source — Extraer funciones de un repo en sources/
Eres un agente extractor de funciones. Tu trabajo es analizar un repositorio clonado en `sources/` y extraer funciones reutilizables al registry siguiendo las reglas de `.claude/rules/sources.md`.
---
## Argumento
`$ARGUMENTS` — nombre del directorio en `sources/` (ej: `MiroFish`, `OpenViking`). Si no se proporciona, listar los directorios disponibles en `sources/` y pedir al usuario que elija.
---
## PASO 0: Validar el source
```bash
ls sources/$ARGUMENTS/
```
Si no existe, abortar. Verificar que tenga licencia compatible (MIT, Apache 2.0, BSD, ISC, MPL-2.0, Unlicense). Si es AGPL, GPL, o no tiene licencia, **advertir al usuario** y pedir confirmacion antes de continuar.
Identificar:
- **Licencia**: leer LICENSE/LICENSE.md/COPYING
- **Lenguaje principal**: detectar por archivos (*.go, *.py, *.rs, *.ts, *.js, Cargo.toml, go.mod, pyproject.toml, package.json)
- **URL del repo**: buscar en README, .git/config, o package.json
---
## PASO 1: Revisar el manifest
Leer `sources/sources.yaml` para ver si este repo ya tiene extracciones previas. Si las tiene, listarlas al usuario y preguntar si quiere continuar extrayendo mas o si quiere re-evaluar las existentes.
---
## PASO 2: Explorar el repositorio
Analizar la estructura del repo para identificar **todas las funciones candidatas** — puras e impuras. El objetivo es maximizar la extraccion de codigo util.
### Que buscar (por categoria)
**A. Funciones puras** (algoritmos, transformaciones, calculos, validaciones):
- Parsers, encoders/decoders, formatters
- Algoritmos matematicos, estadisticos, financieros
- Transformaciones de datos, filtros, mappers
- Validaciones, sanitizaciones
**B. Funciones impuras** (I/O, red, estado externo):
- Clientes HTTP/API (REST, GraphQL, WebSocket)
- Operaciones de filesystem (leer, escribir, monitorear archivos)
- Interacciones con bases de datos (queries, migraciones)
- Operaciones Docker, cloud, infraestructura
- Scraping, crawling, recoleccion de datos
- Notificaciones, envio de mensajes
**C. Pipelines** (composiciones multi-paso):
- Flujos ETL (extract-transform-load)
- Workflows de setup/deploy/provision
- Secuencias de procesamiento de datos
- Orquestaciones que componen varias funciones
**D. Tipos reutilizables** (structs, enums, interfaces):
- Modelos de dominio genericos
- Tipos de configuracion
- Interfaces/protocolos bien definidos
### Estrategia de exploracion segun lenguaje
- **Go**: `pkg/`, `internal/`, `utils/`, `lib/`, `cmd/` — funciones exportadas, handlers, clients
- **Python**: `src/`, `lib/`, `utils/`, `core/`, `api/` — funciones, clases client, decoradores
- **Rust**: `crates/`, `src/lib.rs` — funciones pub, traits implementados
- **TypeScript/JS**: `src/`, `lib/`, `utils/`, `services/` — funciones, hooks, componentes
- **Bash**: `scripts/`, `bin/`, `tools/` — funciones con firma clara
### Que ignorar
- main(), CLI entry points (pero extraer las funciones que invocan)
- Tests (pero notar cuales funciones estan bien testeadas — marcar `tested: true`)
- Funciones que dependen de tipos internos complejos **no adaptables**
- Codigo con dependencias externas pesadas que no esten en fn_registry
- Config loaders hardcodeados a un proyecto especifico
---
## PASO 3: Consultar el registry para evitar duplicados
Antes de proponer cualquier funcion, buscar en registry.db con FTS5:
```bash
# Por cada candidata, buscar similares
sqlite3 registry.db "SELECT id, kind, purity, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:NOMBRE* OR description:DESCRIPCION') ORDER BY name;"
```
Si ya existe algo similar, descartarla o anotar que es una mejora/variante.
---
## PASO 4: Presentar candidatas al usuario
Agrupar las candidatas por categoria y mostrar en tablas separadas:
### Funciones puras
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | Descripcion |
|---|---|---|---|---|---|
### Funciones impuras
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | I/O tipo | Descripcion |
|---|---|---|---|---|---|---|
(I/O tipo: HTTP, filesystem, DB, Docker, network, etc.)
### Pipelines (composiciones)
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | Funciones que compone | Descripcion |
|---|---|---|---|---|---|---|
### Tipos
| # | Nombre propuesto | Origen (archivo) | Lang destino | Dominio | Algebraic | Descripcion |
|---|---|---|---|---|---|---|
Para cada candidata indicar:
- Por que cumple el filtro de calidad
- Si requiere adaptacion (renombrar tipos, quitar dependencias, traducir lenguaje)
- Si es traduccion de otro lenguaje (ej: Rust → Go)
- Para impuras: cual es el `error_type` apropiado
**Esperar confirmacion del usuario** antes de extraer. El usuario puede:
- Aprobar todas (`all`)
- Seleccionar por numero (`1,3,5-8`)
- Seleccionar por categoria (`todas las puras`, `solo pipelines`)
- Pedir explorar mas areas del repo
- Descartar y terminar
---
## PASO 5: Extraer funciones aprobadas
Para cada funcion aprobada:
### 5a. Determinar destino y clasificacion
| Naturaleza | Destino | kind | purity |
|---|---|---|---|
| Algoritmo/logica pura | Go/Python `functions/{domain}/` | function | pure |
| Funcion con I/O (HTTP, DB, fs) | Go/Python `functions/{domain}/` | function | impure |
| Script/utilidad sistema | Bash `bash/functions/{domain}/` | function | impure |
| UI/componente | TypeScript `frontend/functions/{domain}/` | component | — |
| Composicion multi-paso | `functions/pipelines/` o `python/functions/pipelines/` | pipeline | impure |
| C/Rust/otro lenguaje | Traducir a Go o Python manteniendo semantica | segun caso | segun caso |
### 5b. Crear archivos
1. **Codigo** — copiar y adaptar:
- Renombrar a snake_case
- Usar tipos nativos en firma (no tipos internos del repo)
- Quitar dependencias externas, usar stdlib
- Ajustar al paquete Go destino (nombre = nombre del directorio)
- Si es traduccion, mantener la semantica y documentar el origen
2. **Metadata .md** — crear frontmatter completo:
- `source_repo`: URL del repo original
- `source_license`: licencia del repo
- `source_file`: path relativo del archivo original dentro del repo
- Todos los campos obligatorios segun el tipo (function/pipeline/component)
- Reglas de pureza:
- `pure``returns_optional: false` + `error_type: ""`
- `impure``error_type: "error_go_core"` (o equivalente Python)
- `pipeline``purity: impure` + `uses_functions` con las funciones que compone
### 5c. Verificar integridad
```bash
# Indexar
./fn index
# Verificar cada funcion extraida
./fn show {id}
```
Si el indexer reporta errores, corregir antes de continuar.
---
## PASO 6: Actualizar manifest
Anadir las funciones extraidas a `sources/sources.yaml` bajo el repo correspondiente:
```yaml
- repo: https://github.com/user/project
license: MIT
cloned_dir: nombre_directorio
extracted:
- id: funcion_go_core
source_file: pkg/utils.go
date: YYYY-MM-DD # fecha de hoy
```
Si el repo no existe en el manifest, crear la entrada completa.
---
## PASO 7: Resumen
Mostrar al usuario:
- Funciones extraidas exitosamente (con IDs)
- Funciones descartadas y por que
- Warnings del indexer si hubo
- Sugerencia de areas del repo que podrian explorarse en el futuro
---
## Reglas criticas
- **NUNCA extraer sin aprobacion del usuario** — siempre presentar candidatas primero
- **NUNCA ignorar el filtro de calidad** — si no cumple todos los criterios, no se extrae
- **SIEMPRE consultar registry.db** antes de proponer — evitar duplicados
- **SIEMPRE atribuir** — source_repo, source_license, source_file en el .md
- **SIEMPRE actualizar sources.yaml** — es el manifest versionado
- **Licencias no permisivas** (GPL, AGPL) requieren advertencia explicita al usuario
- **Traduccion de lenguaje** es valida — documentar el origen claramente
+486
View File
@@ -0,0 +1,486 @@
# /frontend — Skill para proyectos frontend
Eres un arquitecto frontend experto. Esta skill se activa cuando el usuario pide crear un proyecto frontend, una app con UI, un componente nuevo, o una feature frontend. Tu trabajo es garantizar que TODO el frontend se construya usando el sistema de funciones reutilizables del registry y las mejores practicas actuales.
## Stack
- **pnpm** — gestor de paquetes
- **React 19** — UI library
- **Vite 8** — build tool
- **Mantine v9** — component library + styling (props, no CSS manual)
- **Phosphor Icons** — `@phosphor-icons/react`
- **Recharts** — charts (via `@mantine/charts`)
**NO usar:** Tailwind, shadcn, CVA, clsx, cn(), lucide-react, styled-components, emotion, CSS-in-JS runtime.
---
## PASO 1: Consultar el registry (OBLIGATORIO)
Antes de escribir una sola linea de codigo, consulta registry.db para saber que componentes, funciones y tipos frontend ya existen:
```bash
# Componentes y funciones frontend disponibles
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
# Tipos frontend disponibles
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
# Busqueda FTS5 si buscas algo especifico
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:chart* OR description:chart*') ORDER BY name;"
```
Tambien lista los archivos reales en disco ya que no todos estan indexados aun:
```bash
ls frontend/functions/ui/ # Componentes React
ls frontend/functions/core/ # Utilidades TS puras
ls frontend/types/ # Tipos
```
**REGLA:** Si un componente ya existe en `frontend/functions/ui/` (alias `@fn_library`), USALO. Nunca recrear lo que ya existe.
---
## PASO 2: Determinar el tipo de trabajo
### A) App nueva en `apps/`
Ir a → Seccion SCAFFOLD APP
### B) Componente nuevo para el registry
Ir a → Seccion CREAR COMPONENTE
### C) Feature en app existente
Ir a → Seccion CREAR FEATURE
---
## SCAFFOLD APP
Crear la estructura completa de una app frontend nueva en `apps/{nombre}/frontend/`.
### Estructura obligatoria
```
apps/{nombre}/
frontend/
package.json
vite.config.ts
tsconfig.json
postcss.config.cjs
index.html
src/
main.tsx # Entry point con MantineProvider
App.tsx # Root con Router
app.css # Minimal (font-smoothing solo)
features/ # Feature-based co-location
{feature}/
components/ # Componentes del feature
hooks/ # Hooks del feature
types.ts # Tipos del feature
index.ts # Barrel export publico
components/ # Componentes compartidos de esta app (no reutilizables)
hooks/ # Hooks compartidos
lib/ # Utilidades, API client
types/ # Tipos globales de la app
```
### package.json base
```json
{
"name": "{nombre}",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "tsc -b && vite build",
"preview": "vite preview --host"
},
"dependencies": {
"@mantine/core": "^9.0.0",
"@mantine/hooks": "^9.0.0",
"@mantine/notifications": "^9.0.0",
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "~5.9.3",
"vite": "^8.0.0"
}
}
```
Agregar dependencias extras segun necesidad:
- **Charts**: `@mantine/charts`, `recharts`
- **Tablas**: `@tanstack/react-table`
- **Forms**: `react-hook-form`, `@hookform/resolvers`, `zod`
- **Dates**: `@mantine/dates`, `dayjs`
- **Router**: `react-router` o `@tanstack/react-router`
- **State**: `zustand` (client state), `@tanstack/react-query` (server state)
- **Wails**: los hooks de Wails ya estan en `@fn_library` (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent, WailsProvider)
### vite.config.ts base
```ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@fn_library': resolve(__dirname, '../../../frontend/functions/ui'),
},
dedupe: ['react', 'react-dom'],
},
css: {
postcss: resolve(__dirname, './postcss.config.cjs'),
},
build: {
target: 'es2022',
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
},
},
},
},
})
```
### postcss.config.cjs base
```js
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
```
### app.css base
```css
/* Minimal — Mantine handles all theming via MantineProvider */
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
### main.tsx base
```tsx
import '@mantine/core/styles.css'
import '@mantine/notifications/styles.css'
import './app.css'
import React from 'react'
import ReactDOM from 'react-dom/client'
import { MantineProvider, createTheme } from '@mantine/core'
import { Notifications } from '@mantine/notifications'
import App from './App'
const theme = createTheme({
primaryColor: 'blue',
defaultRadius: 'md',
// Customize colors, fonts, etc. here
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MantineProvider theme={theme} defaultColorScheme="dark">
<Notifications />
<App />
</MantineProvider>
</React.StrictMode>,
)
```
### Despues del scaffold
```bash
cd apps/{nombre}/frontend && pnpm install
```
---
## CREAR COMPONENTE
Para componentes nuevos que van al registry en `frontend/functions/`.
### Reglas de implementacion
1. **Mantine first**: wrappear componentes de Mantine. Solo crear desde cero si Mantine no tiene equivalente.
2. **Styling via props**: usar props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.) y el style system. NUNCA clases CSS manuales ni Tailwind.
3. **CSS variables de Mantine**: si necesitas styles inline, usar `var(--mantine-color-*)`, `var(--mantine-spacing-*)`, etc.
4. **Iconos**: usar `@phosphor-icons/react`, no lucide-react ni @tabler/icons-react.
5. **Props tipadas**: usar `React.ComponentPropsWithoutRef<"element">` para HTML props spreading.
6. **Accesibilidad**:
- Elementos semanticos: `<button>` para acciones, `<a>` para navegacion
- NUNCA `<div onClick>` para elementos interactivos
- `aria-label` en botones de solo icono
- `aria-invalid` + `aria-describedby` en inputs con error
- Focus management en modales/popovers
7. **Discriminated unions** cuando las props cambian segun variante:
```tsx
type Props = { size?: 'sm' | 'md' | 'lg'; children: React.ReactNode } & (
| { variant: 'link'; href: string; onClick?: never }
| { variant: 'button'; onClick: () => void; href?: never }
)
```
### Patron de archivo .tsx
```tsx
import { Select, type SelectProps } from '@mantine/core'
// Re-export con defaults o logica adicional si necesario
interface MySelectProps extends Omit<SelectProps, 'xxx'> {
customProp?: string
}
function MySelect({ customProp, ...props }: MySelectProps) {
return <Select {...props} />
}
export { MySelect }
export type { MySelectProps }
```
### Patron de archivo .md
**IMPORTANTE:** El campo `lang` debe ser `ts` (no `typescript`). El indexer solo reconoce `ts`. Los IDs siguen el formato `{name}_ts_{domain}`.
```yaml
---
name: component_name
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "ComponentName(props: ComponentProps): JSX.Element"
description: "Descripcion concisa de que hace el componente"
tags: [component, ui, ...]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@mantine/core"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/component_name.tsx"
props:
- name: variant
type: "'default' | 'secondary'"
required: false
description: "Estilo visual"
emits: []
has_state: false
framework: react
variant: [default]
---
## Ejemplo
...codigo de ejemplo...
## Notas
...notas relevantes...
```
### Despues de crear
```bash
./fn index && ./fn show {id}
```
---
## CREAR FEATURE
Para features dentro de una app existente. Co-location obligatoria.
### Estructura
```
src/features/{feature_name}/
components/
FeatureMain.tsx # Componente principal
FeatureDetail.tsx # Sub-componentes
hooks/
useFeatureData.ts # Hooks del feature
types.ts # Tipos locales
index.ts # Barrel export
```
### Barrel export (index.ts)
```ts
// Solo exportar la API publica del feature
export { FeatureMain } from './components/FeatureMain'
export { useFeatureData } from './hooks/useFeatureData'
export type { FeatureItem, FeatureConfig } from './types'
```
### Patrones de estado obligatorios
**Server state** (datos de API/backend):
```tsx
// Con @tanstack/react-query
const queryKeys = {
all: ['feature'] as const,
list: (filters: Filters) => [...queryKeys.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.all, 'detail', id] as const,
}
function useFeatureList(filters: Filters) {
return useQuery({
queryKey: queryKeys.list(filters),
queryFn: () => fetchFeatureList(filters),
})
}
```
**Client state** (UI state compartido):
```tsx
// Con Zustand
import { create } from 'zustand'
interface FeatureStore {
selectedId: string | null
setSelected: (id: string | null) => void
}
const useFeatureStore = create<FeatureStore>((set) => ({
selectedId: null,
setSelected: (id) => set({ selectedId: id }),
}))
```
**Wails** (apps de escritorio):
```tsx
// Usar hooks del registry
import { useWailsQuery, useWailsMutation } from '@fn_library'
function useFeatureData() {
return useWailsQuery('GetFeatureData', [], { staleTime: 60_000 })
}
```
### Code splitting por ruta
```tsx
import { lazy, Suspense } from 'react'
import { Skeleton } from '@mantine/core'
const FeaturePage = lazy(() => import('./features/feature/components/FeaturePage'))
function AppRoutes() {
return (
<Routes>
<Route path="/feature" element={
<Suspense fallback={<Skeleton height="100vh" />}>
<FeaturePage />
</Suspense>
} />
</Routes>
)
}
```
---
## CHECKLIST DE VALIDACION (ejecutar siempre al final)
Antes de dar por terminado cualquier trabajo frontend, verificar:
### Colores y estilos
- [ ] CERO colores hardcodeados en componentes (no hex, no rgb inline)
- [ ] Styling via props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.)
- [ ] Si se necesitan styles inline, usar CSS variables de Mantine (`var(--mantine-color-*)`)
- [ ] NO clases CSS manuales, NO Tailwind, NO cn(), NO CVA
### Componentes del registry
- [ ] Verificado que no se esta recreando algo que ya existe en `@fn_library` (`frontend/functions/ui/`)
- [ ] Componentes de `@fn_library` usados donde aplica: Card, Select, SimpleSelect, KPICard, Sparkline, DashboardLayout, DataTable, charts, hooks Wails
- [ ] Componentes de Mantine usados directamente donde `@fn_library` no tiene wrapper: Button, TextInput, Table, Alert, Badge, Skeleton, Tabs, Tooltip, Group, Stack, Grid, Box, Paper, AppShell, Container
### Iconos
- [ ] Usando `@phosphor-icons/react` para iconos
- [ ] NO lucide-react, NO @tabler/icons-react
### TypeScript
- [ ] Props interfaces con `React.ComponentPropsWithoutRef` para HTML spreading
- [ ] Discriminated unions donde las props varian segun tipo/variante
- [ ] `as const` para arrays literales y config objects
- [ ] No `any` — usar `unknown` + type guards si es necesario
### Accesibilidad
- [ ] Elementos semanticos (button, a — no div onClick)
- [ ] `aria-label` en botones de solo icono
- [ ] `aria-invalid` + `aria-describedby` en inputs con validacion
- [ ] Focus trap en modales y popovers
- [ ] `prefers-reduced-motion` respetado (ya en app.css base)
### Performance
- [ ] Lazy loading en rutas (`React.lazy` + `Suspense`)
- [ ] `manualChunks` en vite.config para vendor splitting
- [ ] Sin barrel exports profundos que maten tree-shaking
- [ ] Listas largas virtualizadas si >100 items
### Estructura
- [ ] Features co-located: componente + hook + tipos + barrel en el mismo directorio
- [ ] Un `index.ts` por feature con API publica explicita
- [ ] Componentes reutilizables de la app en `src/components/`
- [ ] Tipos compartidos en `src/types/`
---
## ANTI-PATRONES (nunca hacer)
1. **`<div onClick={...}>`** → usar `<button>` o componente Mantine
2. **`style={{ color: '#3b82f6' }}`** → usar prop `c="blue"` o `var(--mantine-color-blue-6)`
3. **`import Button from './MyButton'`** cuando existe en Mantine → usar `import { Button } from '@mantine/core'`
4. **Estado global para todo** → segmentar: server state (React Query), client state (Zustand), form state (React Hook Form), URL state (search params)
5. **`index.ts` en la raiz de `src/`** que re-exporta todo → mata tree-shaking
6. **`// @ts-ignore`** → arreglar el tipo
7. **CSS-in-JS runtime** (styled-components, emotion) → usar props de Mantine
8. **Tailwind, CVA, cn(), clsx** → usar props de Mantine y su style system
9. **Crear utilidades que ya existen**: `getSeriesColor()`, `ChartContainer`, `DashboardLayout`, `DataTable` ya estan en `@fn_library`
10. **Colores de chart hardcodeados** → usar `@mantine/charts` color system o `getSeriesColor()`
$ARGUMENTS
+457
View File
@@ -0,0 +1,457 @@
# /meta_bigq — Operar Metabase y BigQuery desde el registry
Eres un agente de datos. Tienes acceso a funciones Python del fn_registry para controlar **Metabase** (dashboards, cards, queries, usuarios) y **Google BigQuery** (datasets, tablas, queries, jobs, routines). Usa estas funciones directamente — no inventes llamadas HTTP manuales.
---
## Como ejecutar funciones
```bash
PYTHON="python/.venv/bin/python3"
# Ejecutar codigo inline
$PYTHON -c "
import sys; sys.path.insert(0, 'python/functions')
from metabase import metabase_auth, metabase_list_dashboards
client = metabase_auth('http://localhost:3000', 'admin@fnregistry.local', 'FnRegistry2024!')
print(metabase_list_dashboards(client))
"
# O con fn run para pipelines
./fn run init_metabase --project fn_registry
./fn run setup_metabase_volume
./fn run metabase_create_ops_dashboard docker_tui
```
Variables de entorno tipicas:
- `METABASE_URL` (default: `http://localhost:3000`)
- `METABASE_ADMIN_EMAIL` (default: `admin@fnregistry.local`)
- `METABASE_ADMIN_PASSWORD` (default: `FnRegistry2024!`)
- BigQuery usa ADC (`gcloud auth application-default login`) o `GOOGLE_APPLICATION_CREDENTIALS`
---
## METABASE — Referencia rapida
### Auth
```python
from metabase import metabase_auth, MetabaseClient
# Login con email/password
client = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
# O directo con API key
client = MetabaseClient("http://localhost:3000", "mb_api_key_xxxxx")
# Context manager
with metabase_auth(...) as client:
pass # se cierra solo
```
### Cards (preguntas)
```python
from metabase import (
metabase_list_cards, # (client, filter="", model_id=0) -> list[dict]
metabase_get_card, # (client, card_id) -> dict
metabase_create_card, # (client, name, dataset_query, display="table", collection_id=0, description="") -> dict
metabase_update_card, # (client, card_id, **fields) -> dict # fields: name, description, display, dataset_query, archived...
metabase_delete_card, # (client, card_id) -> None # IRREVERSIBLE, preferir archived=True
metabase_execute_card, # (client, card_id, parameters=None) -> dict # ejecuta query de card guardada
metabase_execute_query, # (client, database_id, sql, max_results=0) -> dict # query ad-hoc
)
# Crear card con SQL nativo
card = metabase_create_card(client, "Ventas por mes", {
"database": 1, "type": "native",
"native": {"query": "SELECT date_trunc('month', created_at) as mes, SUM(total) FROM orders GROUP BY 1"},
}, display="line")
# Actualizar query de una card
metabase_update_card(client, card["id"], dataset_query={
"database": 1, "type": "native",
"native": {"query": "SELECT ... nueva query ..."},
})
# Archivar (soft-delete)
metabase_update_card(client, 42, archived=True)
# Query ad-hoc sin guardar
result = metabase_execute_query(client, 1, "SELECT COUNT(*) FROM users")
# result["data"]["rows"] = [[42]]
```
**Filtros de list_cards:** `all`, `mine`, `fav`, `archived`, `recent`, `popular`, `database`, `table`
### Dashboards
```python
from metabase import (
metabase_list_dashboards, # (client, filter="") -> list[dict]
metabase_get_dashboard, # (client, dashboard_id) -> dict # incluye dashcards
metabase_create_dashboard, # (client, name, description="", collection_id=0) -> dict
metabase_update_dashboard, # (client, dashboard_id, **fields) -> dict
metabase_delete_dashboard, # (client, dashboard_id) -> None # IRREVERSIBLE
)
# Crear dashboard + agregar cards
dash = metabase_create_dashboard(client, "KPIs Operativos", description="Metricas diarias")
# Posicionar cards en el dashboard (dashcards es el estado COMPLETO)
metabase_update_dashboard(client, dash["id"], dashcards=[
{"id": -1, "card_id": card1["id"], "row": 0, "col": 0, "size_x": 6, "size_y": 4},
{"id": -2, "card_id": card2["id"], "row": 0, "col": 6, "size_x": 6, "size_y": 4},
{"id": -3, "card_id": card3["id"], "row": 4, "col": 0, "size_x": 12, "size_y": 6},
])
# id negativo = card nueva, id positivo = card existente, omitida = eliminada
```
**Filtros de list_dashboards:** `all`, `mine`, `archived`
### Databases
```python
from metabase import (
metabase_list_databases, # (client, include_tables=False) -> list
metabase_add_database, # (client, name, engine, details) -> dict
metabase_get_database, # (client, database_id) -> dict
)
# Agregar SQLite
metabase_add_database(client, "Operations DB", "sqlite", {"db": "/data/operations.db"})
# Agregar PostgreSQL
metabase_add_database(client, "DW", "postgres", {
"host": "localhost", "port": 5432, "dbname": "warehouse",
"user": "reader", "password": "secret",
})
```
### Usuarios
```python
from metabase import (
metabase_list_users, # (client, status="", query="", limit=0, offset=0) -> dict
metabase_get_user, # (client, user_id) -> dict
metabase_create_user, # (client, first_name, last_name, email, password="", group_ids=None) -> dict
metabase_update_user, # (client, user_id, **fields) -> dict
metabase_deactivate_user, # (client, user_id) -> None # soft-delete
)
```
### Setup y pipelines
```python
from metabase import metabase_setup
# Setup inicial de instancia nueva (obtiene setup-token automaticamente)
metabase_setup("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
```
```bash
# Pipelines ejecutables con fn run
./fn run init_metabase --project fn_registry # Docker: Postgres + Metabase
./fn run setup_metabase_volume # Copiar registry.db al contenedor
./fn run metabase_add_ops_db docker_tui # Registrar operations.db como database
./fn run metabase_create_ops_dashboard docker_tui # Dashboard operativo completo
./fn run metabase_fix_permissions # Arreglar permisos SQLite en Docker
```
---
## BIGQUERY — Referencia rapida
### Auth
```python
from bigquery import bq_auth, BQClient
# ADC (gcloud auth application-default login)
client = bq_auth()
# Proyecto explicito
client = bq_auth("my-project-id")
# Service account JSON
client = bq_auth(credentials_path="/path/to/sa.json")
# Context manager
with bq_auth("my-project") as client:
pass
```
### Datasets
```python
from bigquery import (
bq_create_dataset, # (client, dataset_id, location="US", description="", labels=None, default_table_expiration_ms=0) -> dict
bq_get_dataset, # (client, dataset_id) -> dict
bq_list_datasets, # (client) -> list[dict]
bq_update_dataset, # (client, dataset_id, description=None, labels=None, default_table_expiration_ms=None) -> dict
bq_delete_dataset, # (client, dataset_id, delete_contents=False) -> None
)
bq_create_dataset(client, "analytics", location="EU", description="Data warehouse")
bq_delete_dataset(client, "temp", delete_contents=True) # borra tablas incluidas
```
### Tables
```python
from bigquery import (
bq_create_table, # (client, dataset_id, table_id, schema, partitioning=None, clustering=None, description="", labels=None) -> dict
bq_get_table, # (client, dataset_id, table_id) -> dict # schema, num_rows, num_bytes, partitioning...
bq_list_tables, # (client, dataset_id) -> list[dict]
bq_update_table, # (client, dataset_id, table_id, schema=None, description=None, labels=None) -> dict
bq_delete_table, # (client, dataset_id, table_id) -> None
bq_preview_rows, # (client, dataset_id, table_id, max_results=10) -> dict # SIN COSTE de query
)
# Crear tabla con particionamiento
bq_create_table(client, "analytics", "events",
schema=[
{"name": "event_id", "type": "STRING", "mode": "REQUIRED"},
{"name": "user_id", "type": "STRING"},
{"name": "event_type", "type": "STRING"},
{"name": "created_at", "type": "TIMESTAMP"},
{"name": "payload", "type": "JSON"},
],
partitioning={"type": "DAY", "field": "created_at"},
clustering=["event_type", "user_id"],
)
# Preview sin coste (usa Storage Read API, no ejecuta query)
preview = bq_preview_rows(client, "analytics", "events", max_results=5)
# {"columns": [...], "rows": [[...], ...], "total_rows": 1234567}
# Schema: solo se pueden AGREGAR columnas, nunca eliminar
bq_update_table(client, "analytics", "events", schema=[
*existing_schema,
{"name": "new_col", "type": "STRING"},
])
```
**Tipos de schema:** `STRING`, `INT64`, `FLOAT64`, `BOOL`, `TIMESTAMP`, `DATE`, `DATETIME`, `BYTES`, `NUMERIC`, `JSON`, `RECORD`/`STRUCT`, `GEOGRAPHY`
**Modos:** `NULLABLE` (default), `REQUIRED`, `REPEATED`
### Queries y datos
```python
from bigquery import (
bq_query, # (client, sql, params=None, dry_run=False) -> dict
bq_insert_rows, # (client, dataset_id, table_id, rows) -> dict
bq_load_from_gcs, # (client, uri, dataset_id, table_id, source_format="CSV", write_disposition="WRITE_APPEND", autodetect=True, skip_leading_rows=0) -> dict
bq_load_from_file, # (client, file_path, dataset_id, table_id, ...) -> dict # mismos params que gcs
bq_export_to_gcs, # (client, dataset_id, table_id, destination_uri, destination_format="CSV", compression="NONE") -> dict
bq_copy_table, # (client, source_dataset, source_table, dest_dataset, dest_table, write_disposition="WRITE_EMPTY") -> dict
)
# Query simple
result = bq_query(client, "SELECT COUNT(*) as total FROM analytics.events")
# {"columns": ["total"], "rows": [[1234567]], "total_rows": 1, "bytes_processed": 0, "cache_hit": True}
# Query parametrizada (usa @nombre en SQL)
result = bq_query(client, "SELECT * FROM analytics.events WHERE event_type = @tipo LIMIT @n", params=[
{"name": "tipo", "type": "STRING", "value": "purchase"},
{"name": "n", "type": "INT64", "value": 100},
])
# Estimar coste ANTES de ejecutar (no procesa datos)
estimate = bq_query(client, "SELECT * FROM analytics.events", dry_run=True)
# {"total_bytes_processed": 5368709120, "total_bytes_billed": 5368709120}
gb = estimate["total_bytes_processed"] / (1024**3)
print(f"Esta query procesara {gb:.2f} GB (~${gb * 6.25:.2f} USD)")
# Streaming insert
bq_insert_rows(client, "analytics", "events", [
{"event_id": "e1", "user_id": "u1", "event_type": "click", "created_at": "2026-04-07T10:00:00Z"},
{"event_id": "e2", "user_id": "u2", "event_type": "purchase", "created_at": "2026-04-07T10:01:00Z"},
])
# {"inserted": 2, "errors": []}
# Cargar CSV desde GCS
bq_load_from_gcs(client, "gs://bucket/data/*.csv", "analytics", "events",
source_format="CSV", write_disposition="WRITE_TRUNCATE", skip_leading_rows=1)
# Cargar archivo local
bq_load_from_file(client, "/tmp/data.parquet", "analytics", "events",
source_format="PARQUET", write_disposition="WRITE_APPEND")
# Exportar a GCS
bq_export_to_gcs(client, "analytics", "events", "gs://bucket/export/events-*.csv",
destination_format="CSV", compression="GZIP")
# Copiar tabla
bq_copy_table(client, "analytics", "events", "analytics_backup", "events_20260407")
```
**write_disposition:** `WRITE_TRUNCATE` (reemplazar), `WRITE_APPEND` (agregar), `WRITE_EMPTY` (solo si vacia)
**source_format:** `CSV`, `NEWLINE_DELIMITED_JSON`, `AVRO`, `PARQUET`, `ORC`
### Jobs
```python
from bigquery import (
bq_list_jobs, # (client, state_filter="", max_results=50, all_users=False) -> list[dict]
bq_get_job, # (client, job_id) -> dict # state, bytes_processed, errors
bq_cancel_job, # (client, job_id) -> dict
)
# Ver jobs corriendo
running = bq_list_jobs(client, state_filter="running")
for j in running:
print(j["job_id"], j["job_type"], j["bytes_processed"])
# Cancelar un job pesado
bq_cancel_job(client, "job_abc123")
```
**state_filter:** `running`, `pending`, `done`
### Routines (UDFs / Procedures)
```python
from bigquery import (
bq_create_routine, # (client, dataset_id, routine_id, body, routine_type="SCALAR_FUNCTION", language="SQL", arguments=None, return_type="", description="") -> dict
bq_list_routines, # (client, dataset_id) -> list[dict]
bq_delete_routine, # (client, dataset_id, routine_id) -> None
)
# UDF SQL
bq_create_routine(client, "analytics", "double_value",
body="x * 2",
arguments=[{"name": "x", "data_type": "INT64"}],
return_type="INT64",
)
# Stored procedure
bq_create_routine(client, "analytics", "refresh_summary",
body="BEGIN INSERT INTO summary SELECT ... FROM events; END;",
routine_type="PROCEDURE",
)
# UDF JavaScript
bq_create_routine(client, "analytics", "parse_ua",
body="return uaParser.parse(ua).browser.name;",
language="JAVASCRIPT",
arguments=[{"name": "ua", "data_type": "STRING"}],
return_type="STRING",
)
```
---
## Flujos tipicos
### 1. Explorar BigQuery y visualizar en Metabase
```python
import sys; sys.path.insert(0, "python/functions")
from bigquery import bq_auth, bq_query
from metabase import metabase_auth, metabase_create_card, metabase_create_dashboard, metabase_update_dashboard
# 1. Explorar datos en BQ
bq = bq_auth("my-project")
result = bq_query(bq, "SELECT event_type, COUNT(*) as cnt FROM analytics.events GROUP BY 1 ORDER BY 2 DESC LIMIT 10")
print(result["columns"], result["rows"])
# 2. Registrar BQ como database en Metabase (si no esta)
# Metabase soporta BigQuery como engine nativo
# 3. Crear cards en Metabase apuntando a BQ
mb = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
card = metabase_create_card(mb, "Eventos por tipo", {
"database": 2, # ID de la database BQ en Metabase
"type": "native",
"native": {"query": "SELECT event_type, COUNT(*) as cnt FROM analytics.events GROUP BY 1 ORDER BY 2 DESC"},
}, display="bar")
# 4. Crear dashboard
dash = metabase_create_dashboard(mb, "Analytics Overview")
metabase_update_dashboard(mb, dash["id"], dashcards=[
{"id": -1, "card_id": card["id"], "row": 0, "col": 0, "size_x": 12, "size_y": 6},
])
```
### 2. ETL: archivo local -> BigQuery -> Metabase dashboard
```python
from bigquery import bq_auth, bq_load_from_file, bq_query, bq_preview_rows
from metabase import metabase_auth, metabase_execute_query
bq = bq_auth("my-project")
# Cargar datos
bq_load_from_file(bq, "/tmp/sales.csv", "warehouse", "sales",
source_format="CSV", write_disposition="WRITE_TRUNCATE", skip_leading_rows=1)
# Verificar
preview = bq_preview_rows(bq, "warehouse", "sales", max_results=3)
print(preview["total_rows"], "filas cargadas")
# Consultar via Metabase (si BQ esta registrado como database)
mb = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
result = metabase_execute_query(mb, 2, "SELECT region, SUM(amount) FROM sales GROUP BY 1")
```
### 3. Montar infraestructura desde cero
```bash
# 1. Levantar Metabase + Postgres
./fn run init_metabase --project fn_registry
# 2. Copiar registry.db al contenedor
./fn run setup_metabase_volume
# 3. Setup inicial
python/.venv/bin/python3 -c "
import sys; sys.path.insert(0, 'python/functions')
from metabase import metabase_setup
metabase_setup('http://localhost:3000', 'admin@fnregistry.local', 'FnRegistry2024!')
"
# 4. Registrar operations.db de una app
./fn run metabase_add_ops_db docker_tui
# 5. Dashboard operativo automatico
./fn run metabase_create_ops_dashboard docker_tui
```
### 4. Auditar costes de BigQuery
```python
from bigquery import bq_auth, bq_list_jobs, bq_query
bq = bq_auth("my-project")
# Jobs recientes completados
jobs = bq_list_jobs(bq, state_filter="done", max_results=20, all_users=True)
total_bytes = sum(j.get("bytes_processed") or 0 for j in jobs)
print(f"Ultimos 20 jobs: {total_bytes / (1024**3):.2f} GB procesados")
# Dry-run antes de queries caras
estimate = bq_query(bq, "SELECT * FROM analytics.events WHERE created_at > '2026-01-01'", dry_run=True)
gb = estimate["total_bytes_processed"] / (1024**3)
cost = gb * 6.25 # $6.25/TB on-demand
print(f"Coste estimado: ${cost:.2f} USD ({gb:.1f} GB)")
```
---
## Buscar mas funciones
Si necesitas algo que no esta aqui, busca en el registry:
```bash
# FTS5 por nombre o descripcion
./fn search "lo que buscas"
# Ver detalles de una funcion
./fn show <id>
# Inline desde Python
sqlite3 registry.db "SELECT id, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:export*') ORDER BY name;"
```
$ARGUMENTS
+4
View File
@@ -13,3 +13,7 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 07 | [proposals.md](proposals.md) | Quien crea proposals y cuando | | 07 | [proposals.md](proposals.md) | Quien crea proposals y cuando |
| 08 | [tag_launcher.md](tag_launcher.md) | Tag launcher para Pipeline Launcher TUI | | 08 | [tag_launcher.md](tag_launcher.md) | Tag launcher para Pipeline Launcher TUI |
| 09 | [go_packages.md](go_packages.md) | Nombre de paquete Go = nombre del directorio | | 09 | [go_packages.md](go_packages.md) | Nombre de paquete Go = nombre del directorio |
| 10 | [apps_vs_functions.md](apps_vs_functions.md) | Codigo reutilizable en functions/, no reutilizable en apps/ |
| 11 | [sources.md](sources.md) | Extraccion de funciones desde repos externos |
| 12 | [notebook_collaboration.md](notebook_collaboration.md) | Colaboración en notebooks Jupyter via funciones del registry |
| 13 | [frontend_theming.md](frontend_theming.md) | Componentes propios y sistema de temas en frontends |
+9
View File
@@ -0,0 +1,9 @@
Solo codigo reutilizable y componible va en `functions/`, `python/functions/`, `bash/functions/`, `frontend/functions/`.
Scripts especificos, dashboards hardcodeados, CLIs de un solo uso, y cualquier codigo que no sea una primitiva componible va en `apps/`. Cada app en `apps/` es independiente: puede importar funciones del registry pero nunca al reves.
Criterios para decidir:
- **functions/**: firma generica, sin credenciales ni config hardcodeada, util en multiples contextos
- **apps/**: orquesta funciones del registry para un caso concreto, tiene config/credenciales, layout fijo
Las apps Python importan funciones del registry con: `sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))` y luego `from <paquete> import ...` (sin prefijo `functions.`).
+11
View File
@@ -0,0 +1,11 @@
En todos los frontends se usan los componentes de `@fn_library` (alias a `frontend/functions/ui/`) antes que elementos HTML nativos o librerias externas.
El sistema de UI es Mantine v9. Todos los componentes de @fn_library wrappean componentes de Mantine.
**Theming:** Cada app define su tema con `createTheme()` de `@mantine/core` y lo pasa a `MantineProvider` (o `FnMantineProvider` de @fn_library). No se usan CSS variables custom — Mantine genera las suyas automaticamente (`--mantine-color-*`).
**Styling:** No se usa Tailwind, CVA, cn(), ni clases CSS manuales. Los componentes se estilizan con props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, etc.) y el style system de Mantine.
**Iconos:** Se usa `@tabler/icons-react` (el set nativo de Mantine), no lucide-react.
**Layout:** Se usan los componentes de layout de Mantine: `Group`, `Stack`, `Grid`, `Flex`, `SimpleGrid`, `AppShell`, `Container`, `Box`, `Paper`.
+55
View File
@@ -0,0 +1,55 @@
## Colaboración en notebooks Jupyter
### Requisito previo
El usuario debe tener Jupyter Lab corriendo en modo colaborativo (`--collaborative`) y el notebook abierto en el browser. Sin esto, los cambios no se ven en tiempo real.
El launcher estándar (`run-jupyter-lab.sh` generado por `init_jupyter_analysis`) ya incluye `--collaborative`.
### Funciones del registry (dominio `notebook`)
| Función | ID | Para qué |
|---|---|---|
| `jupyter_discover` | `jupyter_discover_py_notebook` | Descubrir instancias Jupyter activas, kernels, sesiones, modo colaborativo |
| `jupyter_read` | `jupyter_read_py_notebook` | Leer celdas (todas o una), metadata del notebook |
| `jupyter_exec` | `jupyter_exec_py_notebook` | Ejecutar: append+execute, execute celda existente, o directo al kernel |
| `jupyter_write` | `jupyter_write_py_notebook` | Escribir: append code/markdown, insert, edit, delete celdas |
| `jupyter_kernel` | `jupyter_kernel_py_notebook` | CRUD de kernels: list, start, restart, interrupt, shutdown, sessions |
### Invocación desde cualquier sesión de Claude
```bash
PYTHON="python/.venv/bin/python3"
# 1. Descubrir qué Jupyter está corriendo
$PYTHON python/functions/notebook/jupyter_discover.py --json
# 2. Leer notebook
$PYTHON python/functions/notebook/jupyter_read.py notebooks/01.ipynb --json
# 3. Añadir celda y ejecutar (el usuario la ve en tiempo real)
$PYTHON python/functions/notebook/jupyter_exec.py append notebooks/01.ipynb "df.describe()"
# 4. Ejecutar celda existente
$PYTHON python/functions/notebook/jupyter_exec.py cell notebooks/01.ipynb 3
# 5. Ejecutar en kernel sin tocar notebook
$PYTHON python/functions/notebook/jupyter_exec.py kernel "print(df.shape)"
# 6. Añadir markdown
$PYTHON python/functions/notebook/jupyter_write.py append-markdown notebooks/01.ipynb "## Resumen"
# 7. Gestionar kernels
$PYTHON python/functions/notebook/jupyter_kernel.py list
$PYTHON python/functions/notebook/jupyter_kernel.py sessions
$PYTHON python/functions/notebook/jupyter_kernel.py shutdown <kernel_id>
```
### Reglas de uso
- **SIEMPRE** ejecutar `jupyter_discover` primero para confirmar que Jupyter está activo y el notebook abierto.
- Las funciones resuelven automáticamente el `kernel_id` de la sesión del notebook y el `username` colaborativo via `/api/sessions` y `/api/me`.
- Después de escribir/ejecutar, las funciones mantienen la conexión WebSocket 2 segundos para que Y.js propague los cambios al browser.
- **NO usar MCP jupyter** — estas funciones reemplazan al MCP y funcionan desde cualquier directorio sin registrar nada.
- El token por defecto es vacío (sin auth). Si el server tiene token, pasarlo con `--token`.
- Los paths de notebooks son relativos a la raíz del servidor Jupyter (normalmente `analysis/{tema}/`).
+60
View File
@@ -0,0 +1,60 @@
## Extraccion de funciones desde repos externos (`sources/`)
### Workflow
1. Clonar repo en `sources/<nombre>` (gitignored, solo el manifest `sources/sources.yaml` se versiona)
2. El agente analiza el repo y propone funciones candidatas
3. Las funciones se **copian y adaptan** al formato del registry (.go/.py/.sh/.ts + .md con frontmatter)
4. `fn index` las registra. El manifest se actualiza con las funciones extraidas.
### Filtro de calidad (obligatorio antes de extraer)
Una funcion externa solo se extrae si cumple TODOS estos criterios:
- **Firma generica**: no depende de tipos internos del repo origen ni de config hardcodeada
- **Sin estado global**: no usa variables globales, singletons, ni init() con side effects
- **Dependencias minimas**: solo stdlib o dependencias ya presentes en fn_registry
- **Sin credenciales**: no contiene secrets, API keys, ni paths absolutos
- **Testeable**: la logica debe poder validarse con tests unitarios
- **No duplicada**: consultar registry.db con FTS5 antes de extraer para evitar duplicados
- **Licencia compatible**: el repo debe tener licencia permisiva (MIT, Apache 2.0, BSD, etc.)
### Clasificacion de pureza al extraer
Extraer tanto funciones puras como impuras. La clasificacion correcta es obligatoria:
- **Pure**: sin I/O, sin estado mutable, determinista. Extraer como `purity: pure`.
- **Impure**: hace I/O (red, disco, DB, HTTP), usa concurrencia, o depende de estado externo. Extraer como `purity: impure` con `error_type` apropiado.
- **Pipeline**: compone multiples funciones para un flujo completo. Extraer como `kind: pipeline`, siempre impuro.
No descartar funciones utiles solo por ser impuras. Una funcion que hace HTTP requests, lee archivos, o interactua con bases de datos es valiosa si su firma es generica y reutilizable.
### Adaptacion al extraer
- Renombrar a snake_case siguiendo la convencion del registry
- Adaptar firma para usar tipos nativos (no tipos internos del repo)
- Crear .md con frontmatter completo incluyendo `source_repo`, `source_license`, `source_file`
- Actualizar `sources/sources.yaml` con la extraccion
### Campos de atribucion en frontmatter
```yaml
source_repo: "https://github.com/user/project"
source_license: "MIT"
source_file: "pkg/original_file.go"
```
Estos campos se indexan en registry.db y permiten consultar:
```sql
SELECT id, source_repo, source_license FROM functions WHERE source_repo != '';
```
### Lenguajes soportados para extraccion
Cualquier lenguaje puede analizarse como fuente. El destino depende de la naturaleza de la funcion:
- Algoritmos/logica pura → Go (functions/{domain}/) o Python (python/functions/{domain}/)
- Funciones impuras (I/O, HTTP, DB) → Go o Python segun el dominio
- Scripts/utilidades sistema → Bash (bash/functions/{domain}/)
- UI/frontend → TypeScript (frontend/functions/{domain}/)
- Flujos multi-paso → Pipeline en el lenguaje mas natural
- C/Rust/otros → Traducir a Go o Python, manteniendo la semantica original
+29 -2
View File
@@ -1,5 +1,4 @@
# SQLite index (regenerable con fn index) — SOLO en raiz # SQLite index — journal/wal temporales
registry.db
registry.db-journal registry.db-journal
registry.db-wal registry.db-wal
@@ -24,6 +23,34 @@ registry.db-wal
*.swo *.swo
*~ *~
# Secrets
**/.env
**/.env.*
# Python
**/__pycache__/
**/*.pyc
**/*.pyo
python/.venv/
# Externalized apps and analysis (each is its own Gitea repo)
apps/*/
analysis/*/
# Node / pnpm
**/node_modules/
# Sources — repos externos clonados (solo se versiona el manifest)
sources/*/
# C++ build artifacts
cpp/build/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Archivos locales
.local
broken_paths.txt
+13
View File
@@ -0,0 +1,13 @@
[submodule "cpp/vendor/imgui"]
path = cpp/vendor/imgui
url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "cpp/vendor/implot"]
path = cpp/vendor/implot
url = https://github.com/epezent/implot.git
[submodule "cpp/vendor/tracy"]
path = cpp/vendor/tracy
url = https://github.com/wolfpld/tracy.git
[submodule "/home/lucas/fn_registry/cpp/vendor/glfw"]
path = /home/lucas/fn_registry/cpp/vendor/glfw
url = https://github.com/glfw/glfw.git
View File
-5
View File
@@ -1,5 +0,0 @@
build/
*.exe
*.dll
*.so
*.dylib
-19
View File
@@ -1,19 +0,0 @@
.PHONY: run build clean install tidy help
run: ## Ejecuta la TUI
go run .
build: ## Compila el binario
go build -trimpath -ldflags='-s -w' -o build/docker-tui .
clean: ## Limpia artefactos
rm -rf build/
install: build ## Instala en ~/.local/bin
cp build/docker-tui ~/.local/bin/docker-tui
tidy: ## go mod tidy
go mod tidy
help: ## Muestra esta ayuda
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
-175
View File
@@ -1,175 +0,0 @@
package app
import (
"docker-tui/views"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lucasdataproyects/devfactory/tui"
)
type View int
const (
ViewContainers View = iota
ViewImages
ViewVolumes
ViewNetworks
ViewCompose
)
var tabNames = []string{"Containers", "Images", "Volumes", "Networks", "Compose"}
type Model struct {
tui.BaseModel
activeTab int
containers views.ContainersModel
images views.ImagesModel
volumes views.VolumesModel
networks views.NetworksModel
compose views.ComposeModel
ready bool
}
func New() Model {
styles := tui.DefaultStyles()
return Model{
BaseModel: tui.NewBaseModel().WithStyles(styles),
containers: views.NewContainersModel(styles),
images: views.NewImagesModel(styles),
volumes: views.NewVolumesModel(styles),
networks: views.NewNetworksModel(styles),
compose: views.NewComposeModel(styles),
}
}
func (m Model) Init() tea.Cmd {
return m.containers.Init()
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case views.KeyQuit:
return m, tea.Quit
case "q", "0", "esc":
updated, atBase := m.handleBack()
if atBase {
return updated, tea.Quit
}
return updated, nil
case views.KeyTab:
m.activeTab = (m.activeTab + 1) % len(tabNames)
return m, m.initActiveView()
case "shift+tab":
m.activeTab = (m.activeTab - 1 + len(tabNames)) % len(tabNames)
return m, m.initActiveView()
}
case tea.WindowSizeMsg:
m.HandleWindowSize(msg)
m.ready = true
}
var cmd tea.Cmd
switch View(m.activeTab) {
case ViewContainers:
m.containers, cmd = m.containers.Update(msg)
case ViewImages:
m.images, cmd = m.images.Update(msg)
case ViewVolumes:
m.volumes, cmd = m.volumes.Update(msg)
case ViewNetworks:
m.networks, cmd = m.networks.Update(msg)
case ViewCompose:
m.compose, cmd = m.compose.Update(msg)
}
return m, cmd
}
func (m Model) View() string {
if !m.ready {
return "Loading..."
}
// Tab bar
tabs := m.renderTabs()
// Active view content
var content string
switch View(m.activeTab) {
case ViewContainers:
content = m.containers.View()
case ViewImages:
content = m.images.View()
case ViewVolumes:
content = m.volumes.View()
case ViewNetworks:
content = m.networks.View()
case ViewCompose:
content = m.compose.View()
}
// Status bar
status := m.Styles.StatusBar.Render(" Tab: switch view │ Ctrl+C: quit │ Enter: action │ r: refresh")
return lipgloss.JoinVertical(lipgloss.Left,
tabs,
"",
content,
"",
status,
)
}
func (m Model) renderTabs() string {
var tabs []string
for i, name := range tabNames {
if i == m.activeTab {
tabs = append(tabs, m.Styles.Selected.Render(" "+name+" "))
} else {
tabs = append(tabs, m.Styles.Muted.Render(" "+name+" "))
}
}
row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
return m.Styles.Header.Render("Docker TUI") + " " + row
}
// handleBack asks the active view to go back one level.
// Returns the updated model and true if the view was already at base level (app should quit).
func (m Model) handleBack() (Model, bool) {
switch View(m.activeTab) {
case ViewContainers:
atBase := m.containers.HandleBack()
return m, atBase
case ViewImages:
atBase := m.images.HandleBack()
return m, atBase
case ViewVolumes:
atBase := m.volumes.HandleBack()
return m, atBase
case ViewNetworks:
atBase := m.networks.HandleBack()
return m, atBase
case ViewCompose:
atBase := m.compose.HandleBack()
return m, atBase
}
return m, true
}
func (m Model) initActiveView() tea.Cmd {
switch View(m.activeTab) {
case ViewContainers:
return m.containers.Init()
case ViewImages:
return m.images.Init()
case ViewVolumes:
return m.volumes.Init()
case ViewNetworks:
return m.networks.Init()
case ViewCompose:
return m.compose.Init()
}
return nil
}
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
echo "==> Tidying modules..."
go mod tidy
echo "==> Building docker-tui..."
mkdir -p build
go build -trimpath -ldflags='-s -w' -o build/docker-tui .
echo "==> Done: build/docker-tui ($(du -h build/docker-tui | cut -f1))"
echo " Run with: ./build/docker-tui"
-15
View File
@@ -1,15 +0,0 @@
package config
// Config holds Docker TUI configuration.
type Config struct {
ComposeFile string
RefreshInterval int // seconds, 0 = manual
}
// Default returns sensible defaults.
func Default() Config {
return Config{
ComposeFile: "docker-compose.yml",
RefreshInterval: 0,
}
}
-43
View File
@@ -1,43 +0,0 @@
module docker-tui
go 1.22.2
require (
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/lucasdataproyects/devfactory v0.0.0
)
require (
github.com/apache/arrow/go/v14 v14.0.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbles v0.18.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/marcboeker/go-duckdb v1.6.5 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
)
replace github.com/lucasdataproyects/devfactory => /home/lucas/.local_agentes/backend
-84
View File
@@ -1,84 +0,0 @@
github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO5IONw=
github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/marcboeker/go-duckdb v1.6.5 h1:XCfR1JVZxsemcSPxRQKK0R0ESfgRMHTEqh3Y+dv40SI=
github.com/marcboeker/go-duckdb v1.6.5/go.mod h1:WtWeqqhZoTke/Nbd7V9lnBx7I2/A/q0SAq/urGzPCMs=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o=
gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-6
View File
@@ -1,6 +0,0 @@
go 1.22.2
use (
.
/home/lucas/.local_agentes/backend
)
-40
View File
@@ -1,40 +0,0 @@
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apache/thrift v0.17.0/go.mod h1:OLxhMRJxomX+1I/KUw03qoV3mMz16BwaKI+d4fPBx7Q=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/substrait-io/substrait-go v0.4.2/go.mod h1:qhpnLmrcvAnlZsUyPXZRqldiHapPTXC3t7xFgDi3aQg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/libc v1.22.4/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.21.2/go.mod h1:cxbLkB5WS32DnQqeH4h4o1B0eMr8W/y8/RGuxQ3JsC0=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-20
View File
@@ -1,20 +0,0 @@
package main
import (
"fmt"
"os"
"docker-tui/app"
"github.com/lucasdataproyects/devfactory/tui"
)
func main() {
model := app.New()
result := tui.RunFullscreen(model)
if result.IsErr() {
fmt.Fprintf(os.Stderr, "error: %v\n", result.Error())
os.Exit(1)
}
}
-201
View File
@@ -1,201 +0,0 @@
package views
import (
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/lucasdataproyects/devfactory/tui"
)
type composeState int
const (
composeLoading composeState = iota
composeList
composeAction
composeLogs
)
type composeLoadedMsg []ComposeService
type composeActionMsg struct{ output string; err error }
type composeLogsMsg struct{ output string; err error }
type ComposeModel struct {
state composeState
list tui.ListModel
spinner tui.SpinnerModel
styles tui.Styles
services []ComposeService
output string
scrollOff int
err error
}
func NewComposeModel(styles tui.Styles) ComposeModel {
return ComposeModel{
state: composeLoading,
list: tui.NewList(nil),
spinner: tui.NewSpinner("Loading compose services..."),
styles: styles,
}
}
func (m ComposeModel) Init() tea.Cmd {
return tea.Batch(m.spinner.Init(), loadCompose)
}
func loadCompose() tea.Msg {
services, err := ComposePS()
if err != nil {
return composeLoadedMsg(nil)
}
return composeLoadedMsg(services)
}
func (m ComposeModel) Update(msg tea.Msg) (ComposeModel, tea.Cmd) {
switch msg := msg.(type) {
case composeLoadedMsg:
m.services = []ComposeService(msg)
items := make([]tui.ListItem, 0, len(m.services)+2)
// Add action items at the top
items = append(items,
tui.ListItem{Title: "▶ Compose Up", Description: "docker compose up -d", Value: "up"},
tui.ListItem{Title: "■ Compose Down", Description: "docker compose down", Value: "down"},
)
for _, s := range m.services {
stateIcon := "●"
if s.State == "running" {
stateIcon = "▶"
}
items = append(items, tui.ListItem{
Title: fmt.Sprintf("%s %s", stateIcon, s.Name),
Description: fmt.Sprintf("Service: %s — %s", s.Service, s.Status),
Value: s,
})
}
m.list.SetItems(items)
m.state = composeList
return m, nil
case composeActionMsg:
m.output = msg.output
if msg.err != nil {
m.output = fmt.Sprintf("Error: %v", msg.err)
}
m.state = composeList
return m, loadCompose
case composeLogsMsg:
m.output = msg.output
if msg.err != nil {
m.output = fmt.Sprintf("Error: %v", msg.err)
}
m.state = composeLogs
m.scrollOff = 0
return m, nil
case tea.KeyMsg:
switch m.state {
case composeList:
switch msg.String() {
case "r":
m.state = composeLoading
return m, tea.Batch(m.spinner.Init(), loadCompose)
case "l":
m.state = composeAction
return m, func() tea.Msg {
output, err := ComposeLogs(100)
return composeLogsMsg{output: output, err: err}
}
case "enter":
if item := m.list.SelectedItem(); item != nil {
switch v := item.Value.(type) {
case string:
m.state = composeAction
if v == "up" {
return m, func() tea.Msg {
output, err := ComposeUp()
return composeActionMsg{output: output, err: err}
}
}
return m, func() tea.Msg {
output, err := ComposeDown()
return composeActionMsg{output: output, err: err}
}
}
}
}
case composeLogs:
switch msg.String() {
case "j", "down":
m.scrollOff++
case "k", "up":
if m.scrollOff > 0 {
m.scrollOff--
}
}
return m, nil
}
}
var cmd tea.Cmd
switch m.state {
case composeLoading, composeAction:
var model tea.Model
model, cmd = m.spinner.Update(msg)
m.spinner = model.(tui.SpinnerModel)
case composeList:
var model tea.Model
model, cmd = m.list.Update(msg)
m.list = model.(tui.ListModel)
}
return m, cmd
}
// HandleBack retrocede un nivel. Retorna true si ya estaba en estado base.
func (m *ComposeModel) HandleBack() bool {
switch m.state {
case composeLogs:
m.state = composeList
return false
default:
return true
}
}
func (m ComposeModel) View() string {
switch m.state {
case composeLoading, composeAction:
return m.spinner.View()
case composeList:
if len(m.services) == 0 {
help := m.styles.Muted.Render(" No compose services. Use Enter on 'Compose Up' or press 'r' to refresh.")
return m.list.View() + "\n" + help
}
help := m.styles.Muted.Render(" Enter: up/down │ l: logs │ r: refresh")
return m.list.View() + "\n" + help
case composeLogs:
return m.renderLogs()
}
return ""
}
func (m ComposeModel) renderLogs() string {
lines := strings.Split(m.output, "\n")
if len(lines) == 0 {
lines = []string{"(empty)"}
}
maxLines := 20
if m.scrollOff >= len(lines) {
m.scrollOff = max(0, len(lines)-1)
}
end := min(m.scrollOff+maxLines, len(lines))
visible := lines[m.scrollOff:end]
header := m.styles.Header.Render("Compose Logs")
content := strings.Join(visible, "\n")
help := m.styles.Muted.Render(" j/k: scroll │ Esc: back")
return header + "\n" + content + "\n" + help
}
-251
View File
@@ -1,251 +0,0 @@
package views
import (
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lucasdataproyects/devfactory/tui"
)
type containersState int
const (
containersLoading containersState = iota
containersList
containersAction
containersLogs
)
type containersLoadedMsg []DockerContainer
type containersActionMsg struct{ output string; err error }
type containersLogsMsg struct{ output string; err error }
type ContainersModel struct {
state containersState
list tui.FilteredListModel
spinner tui.SpinnerModel
styles tui.Styles
containers []DockerContainer
output string
scrollOff int
err error
}
func NewContainersModel(styles tui.Styles) ContainersModel {
return ContainersModel{
state: containersLoading,
list: tui.NewFilteredList(nil, "Filter containers..."),
spinner: tui.NewSpinner("Loading containers..."),
styles: styles,
}
}
func (m ContainersModel) Init() tea.Cmd {
return tea.Batch(
m.spinner.Init(),
loadContainers,
)
}
func loadContainers() tea.Msg {
containers, err := ListContainers()
if err != nil {
return containersLoadedMsg(nil)
}
return containersLoadedMsg(containers)
}
func (m ContainersModel) Update(msg tea.Msg) (ContainersModel, tea.Cmd) {
switch msg := msg.(type) {
case containersLoadedMsg:
m.containers = []DockerContainer(msg)
items := make([]tui.ListItem, len(m.containers))
for i, c := range m.containers {
stateIcon := "●"
if c.State == "running" {
stateIcon = "▶"
} else if c.State == "exited" {
stateIcon = "■"
}
items[i] = tui.ListItem{
Title: fmt.Sprintf("%s %s", stateIcon, c.Names),
Description: fmt.Sprintf("%s — %s", c.Image, c.Status),
Value: c,
}
}
m.list.SetItems(items)
m.state = containersList
return m, nil
case containersActionMsg:
m.output = msg.output
if msg.err != nil {
m.output = fmt.Sprintf("Error: %v", msg.err)
}
m.state = containersList
// Refresh after action
return m, loadContainers
case containersLogsMsg:
m.output = msg.output
if msg.err != nil {
m.output = fmt.Sprintf("Error: %v", msg.err)
}
m.state = containersLogs
m.scrollOff = 0
return m, nil
case tea.KeyMsg:
switch m.state {
case containersList:
switch msg.String() {
case "r":
m.state = containersLoading
return m, tea.Batch(m.spinner.Init(), loadContainers)
case "enter":
if item := m.list.SelectedItem(); item != nil {
c := item.Value.(DockerContainer)
if c.State == "running" {
return m, stopContainerCmd(c.ID)
}
return m, startContainerCmd(c.ID)
}
case "l":
if item := m.list.SelectedItem(); item != nil {
c := item.Value.(DockerContainer)
m.state = containersAction
return m, logsContainerCmd(c.ID)
}
case "x":
if item := m.list.SelectedItem(); item != nil {
c := item.Value.(DockerContainer)
return m, restartContainerCmd(c.ID)
}
}
case containersLogs:
switch msg.String() {
case "j", "down":
m.scrollOff++
case "k", "up":
if m.scrollOff > 0 {
m.scrollOff--
}
}
return m, nil
}
}
// Delegate to sub-components
var cmd tea.Cmd
switch m.state {
case containersLoading:
var spinnerModel tea.Model
spinnerModel, cmd = m.spinner.Update(msg)
m.spinner = spinnerModel.(tui.SpinnerModel)
case containersList:
var listModel tea.Model
listModel, cmd = m.list.Update(msg)
m.list = listModel.(tui.FilteredListModel)
}
return m, cmd
}
// HandleBack retrocede un nivel. Retorna true si ya estaba en estado base (el caller debe salir).
func (m *ContainersModel) HandleBack() bool {
switch m.state {
case containersLogs:
m.state = containersList
return false
default:
return true
}
}
func (m ContainersModel) View() string {
switch m.state {
case containersLoading:
return m.spinner.View()
case containersList:
if len(m.containers) == 0 {
return m.styles.Muted.Render("No containers found. Press 'r' to refresh.")
}
help := m.styles.Muted.Render(" Enter: start/stop │ l: logs │ x: restart │ r: refresh │ /: filter")
return m.list.View() + "\n" + help
case containersAction:
return m.spinner.View()
case containersLogs:
return m.renderOutput()
}
return ""
}
func (m ContainersModel) renderOutput() string {
lines := splitLines(m.output)
maxLines := 20
if m.scrollOff >= len(lines) {
m.scrollOff = max(0, len(lines)-1)
}
end := min(m.scrollOff+maxLines, len(lines))
visible := lines[m.scrollOff:end]
header := m.styles.Header.Render("Container Logs")
content := lipgloss.JoinVertical(lipgloss.Left, visible...)
help := m.styles.Muted.Render(" j/k: scroll │ Esc: back")
return header + "\n" + content + "\n" + help
}
func startContainerCmd(id string) tea.Cmd {
return func() tea.Msg {
err := StartContainer(id)
return containersActionMsg{output: "Started " + id, err: err}
}
}
func stopContainerCmd(id string) tea.Cmd {
return func() tea.Msg {
err := StopContainer(id)
return containersActionMsg{output: "Stopped " + id, err: err}
}
}
func restartContainerCmd(id string) tea.Cmd {
return func() tea.Msg {
err := RestartContainer(id)
return containersActionMsg{output: "Restarted " + id, err: err}
}
}
func logsContainerCmd(id string) tea.Cmd {
return func() tea.Msg {
output, err := ContainerLogs(id, 100)
return containersLogsMsg{output: output, err: err}
}
}
func splitLines(s string) []string {
if s == "" {
return []string{"(empty)"}
}
lines := strings.Split(s, "\n")
if len(lines) == 0 {
return []string{"(empty)"}
}
return lines
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
-192
View File
@@ -1,192 +0,0 @@
package views
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/lucasdataproyects/devfactory/shell"
)
const dockerTimeout = 15 * time.Second
// --- Containers ---
func ListContainers() ([]DockerContainer, error) {
result := shell.RunWithTimeout("docker", dockerTimeout, "ps", "-a", "--format", "{{json .}}")
stdout, err := result.Both()
if err != nil {
return nil, err
}
return parseJSONLines[DockerContainer](stdout.Stdout)
}
func StartContainer(id string) error {
_, err := shell.RunWithTimeout("docker", dockerTimeout, "start", id).Both()
return err
}
func StopContainer(id string) error {
_, err := shell.RunWithTimeout("docker", dockerTimeout, "stop", id).Both()
return err
}
func RestartContainer(id string) error {
_, err := shell.RunWithTimeout("docker", dockerTimeout, "restart", id).Both()
return err
}
func ContainerLogs(id string, lines int) (string, error) {
result := shell.RunWithTimeout("docker", dockerTimeout, "logs", "--tail", itoa(lines), id)
out, err := result.Both()
if err != nil {
return "", err
}
// docker logs writes to both stdout and stderr
output := out.Stdout
if out.Stderr != "" {
if output != "" {
output += "\n"
}
output += out.Stderr
}
return output, nil
}
// --- Images ---
func ListImages() ([]DockerImage, error) {
result := shell.RunWithTimeout("docker", dockerTimeout, "image", "ls", "--format", "{{json .}}")
stdout, err := result.Both()
if err != nil {
return nil, err
}
return parseJSONLines[DockerImage](stdout.Stdout)
}
func PullImage(name string) (string, error) {
result := shell.RunWithTimeout("docker", 120*time.Second, "pull", name)
out, err := result.Both()
if err != nil {
return "", err
}
return out.Stdout, nil
}
func RemoveImage(id string) error {
_, err := shell.RunWithTimeout("docker", dockerTimeout, "rmi", id).Both()
return err
}
// --- Volumes ---
func ListVolumes() ([]DockerVolume, error) {
result := shell.RunWithTimeout("docker", dockerTimeout, "volume", "ls", "--format", "{{json .}}")
stdout, err := result.Both()
if err != nil {
return nil, err
}
return parseJSONLines[DockerVolume](stdout.Stdout)
}
func CreateVolume(name string) error {
args := []string{"volume", "create"}
if name != "" {
args = append(args, name)
}
_, err := shell.RunWithTimeout("docker", dockerTimeout, args...).Both()
return err
}
func RemoveVolume(name string) error {
_, err := shell.RunWithTimeout("docker", dockerTimeout, "volume", "rm", name).Both()
return err
}
// --- Networks ---
func ListNetworks() ([]DockerNetwork, error) {
result := shell.RunWithTimeout("docker", dockerTimeout, "network", "ls", "--format", "{{json .}}")
stdout, err := result.Both()
if err != nil {
return nil, err
}
return parseJSONLines[DockerNetwork](stdout.Stdout)
}
func CreateNetwork(name string) error {
_, err := shell.RunWithTimeout("docker", dockerTimeout, "network", "create", name).Both()
return err
}
func RemoveNetwork(name string) error {
_, err := shell.RunWithTimeout("docker", dockerTimeout, "network", "rm", name).Both()
return err
}
// --- Compose ---
func ComposePS() ([]ComposeService, error) {
result := shell.RunWithTimeout("docker", dockerTimeout, "compose", "ps", "--format", "json")
stdout, err := result.Both()
if err != nil {
return nil, err
}
// docker compose ps --format json returns a JSON array
var services []ComposeService
if err := json.Unmarshal([]byte(stdout.Stdout), &services); err != nil {
// Try line-by-line as fallback
return parseJSONLines[ComposeService](stdout.Stdout)
}
return services, nil
}
func ComposeUp() (string, error) {
result := shell.RunWithTimeout("docker", 120*time.Second, "compose", "up", "-d")
out, err := result.Both()
if err != nil {
return "", err
}
return out.Stdout + out.Stderr, nil
}
func ComposeDown() (string, error) {
result := shell.RunWithTimeout("docker", 60*time.Second, "compose", "down")
out, err := result.Both()
if err != nil {
return "", err
}
return out.Stdout + out.Stderr, nil
}
func ComposeLogs(lines int) (string, error) {
result := shell.RunWithTimeout("docker", dockerTimeout, "compose", "logs", "--tail", itoa(lines))
out, err := result.Both()
if err != nil {
return "", err
}
return out.Stdout + out.Stderr, nil
}
// --- Helpers ---
func parseJSONLines[T any](s string) ([]T, error) {
var result []T
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var item T
if err := json.Unmarshal([]byte(line), &item); err != nil {
continue
}
result = append(result, item)
}
return result, nil
}
func itoa(n int) string {
return fmt.Sprintf("%d", n)
}
-132
View File
@@ -1,132 +0,0 @@
package views
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lucasdataproyects/devfactory/tui"
)
type imagesState int
const (
imagesLoading imagesState = iota
imagesList
imagesAction
)
type imagesLoadedMsg []DockerImage
type imagesActionMsg struct{ output string; err error }
type ImagesModel struct {
state imagesState
list tui.FilteredListModel
spinner tui.SpinnerModel
styles tui.Styles
images []DockerImage
err error
}
func NewImagesModel(styles tui.Styles) ImagesModel {
return ImagesModel{
state: imagesLoading,
list: tui.NewFilteredList(nil, "Filter images..."),
spinner: tui.NewSpinner("Loading images..."),
styles: styles,
}
}
func (m ImagesModel) Init() tea.Cmd {
return tea.Batch(m.spinner.Init(), loadImages)
}
func loadImages() tea.Msg {
images, err := ListImages()
if err != nil {
return imagesLoadedMsg(nil)
}
return imagesLoadedMsg(images)
}
func (m ImagesModel) Update(msg tea.Msg) (ImagesModel, tea.Cmd) {
switch msg := msg.(type) {
case imagesLoadedMsg:
m.images = []DockerImage(msg)
items := make([]tui.ListItem, len(m.images))
for i, img := range m.images {
tag := img.Tag
if tag == "" {
tag = "latest"
}
items[i] = tui.ListItem{
Title: fmt.Sprintf("%s:%s", img.Repository, tag),
Description: fmt.Sprintf("Size: %s — %s", img.Size, img.ID[:12]),
Value: img,
}
}
m.list.SetItems(items)
m.state = imagesList
return m, nil
case imagesActionMsg:
if msg.err != nil {
m.err = msg.err
}
m.state = imagesList
return m, loadImages
case tea.KeyMsg:
if m.state == imagesList {
switch msg.String() {
case "r":
m.state = imagesLoading
return m, tea.Batch(m.spinner.Init(), loadImages)
case "d", "delete":
if item := m.list.SelectedItem(); item != nil {
img := item.Value.(DockerImage)
m.state = imagesAction
return m, func() tea.Msg {
err := RemoveImage(img.ID)
return imagesActionMsg{output: "Removed", err: err}
}
}
}
}
}
var cmd tea.Cmd
switch m.state {
case imagesLoading, imagesAction:
var model tea.Model
model, cmd = m.spinner.Update(msg)
m.spinner = model.(tui.SpinnerModel)
case imagesList:
var model tea.Model
model, cmd = m.list.Update(msg)
m.list = model.(tui.FilteredListModel)
}
return m, cmd
}
// HandleBack retrocede un nivel. Retorna true si ya estaba en estado base.
func (m *ImagesModel) HandleBack() bool {
return true
}
func (m ImagesModel) View() string {
switch m.state {
case imagesLoading, imagesAction:
return m.spinner.View()
case imagesList:
if len(m.images) == 0 {
return m.styles.Muted.Render("No images found. Press 'r' to refresh.")
}
help := m.styles.Muted.Render(" d: remove │ r: refresh │ /: filter")
view := m.list.View() + "\n" + help
if m.err != nil {
view += "\n" + m.styles.Error.Render(fmt.Sprintf(" Error: %v", m.err))
}
return view
}
return ""
}
-14
View File
@@ -1,14 +0,0 @@
package views
// Navigation key constants.
const (
KeyQuit = "ctrl+c"
KeyEsc = "esc"
KeyBack = "0"
KeyTab = "tab"
)
// IsBack returns true if the key should trigger back navigation.
func IsBack(key string) bool {
return key == KeyEsc || key == KeyBack
}
-128
View File
@@ -1,128 +0,0 @@
package views
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lucasdataproyects/devfactory/tui"
)
type networksState int
const (
networksLoading networksState = iota
networksList
networksAction
)
type networksLoadedMsg []DockerNetwork
type networksActionMsg struct{ output string; err error }
type NetworksModel struct {
state networksState
list tui.ListModel
spinner tui.SpinnerModel
styles tui.Styles
networks []DockerNetwork
err error
}
func NewNetworksModel(styles tui.Styles) NetworksModel {
return NetworksModel{
state: networksLoading,
list: tui.NewList(nil),
spinner: tui.NewSpinner("Loading networks..."),
styles: styles,
}
}
func (m NetworksModel) Init() tea.Cmd {
return tea.Batch(m.spinner.Init(), loadNetworks)
}
func loadNetworks() tea.Msg {
networks, err := ListNetworks()
if err != nil {
return networksLoadedMsg(nil)
}
return networksLoadedMsg(networks)
}
func (m NetworksModel) Update(msg tea.Msg) (NetworksModel, tea.Cmd) {
switch msg := msg.(type) {
case networksLoadedMsg:
m.networks = []DockerNetwork(msg)
items := make([]tui.ListItem, len(m.networks))
for i, n := range m.networks {
items[i] = tui.ListItem{
Title: n.Name,
Description: fmt.Sprintf("Driver: %s — Scope: %s", n.Driver, n.Scope),
Value: n,
}
}
m.list.SetItems(items)
m.state = networksList
return m, nil
case networksActionMsg:
if msg.err != nil {
m.err = msg.err
}
m.state = networksList
return m, loadNetworks
case tea.KeyMsg:
if m.state == networksList {
switch msg.String() {
case "r":
m.state = networksLoading
return m, tea.Batch(m.spinner.Init(), loadNetworks)
case "d", "delete":
if item := m.list.SelectedItem(); item != nil {
net := item.Value.(DockerNetwork)
m.state = networksAction
return m, func() tea.Msg {
err := RemoveNetwork(net.Name)
return networksActionMsg{output: "Removed", err: err}
}
}
}
}
}
var cmd tea.Cmd
switch m.state {
case networksLoading, networksAction:
var model tea.Model
model, cmd = m.spinner.Update(msg)
m.spinner = model.(tui.SpinnerModel)
case networksList:
var model tea.Model
model, cmd = m.list.Update(msg)
m.list = model.(tui.ListModel)
}
return m, cmd
}
// HandleBack retrocede un nivel. Retorna true si ya estaba en estado base.
func (m *NetworksModel) HandleBack() bool {
return true
}
func (m NetworksModel) View() string {
switch m.state {
case networksLoading, networksAction:
return m.spinner.View()
case networksList:
if len(m.networks) == 0 {
return m.styles.Muted.Render("No networks found. Press 'r' to refresh.")
}
help := m.styles.Muted.Render(" d: remove │ r: refresh")
view := m.list.View() + "\n" + help
if m.err != nil {
view += "\n" + m.styles.Error.Render(fmt.Sprintf(" Error: %v", m.err))
}
return view
}
return ""
}
-45
View File
@@ -1,45 +0,0 @@
package views
// DockerContainer represents a container from docker ps --format json.
type DockerContainer struct {
ID string `json:"ID"`
Names string `json:"Names"`
Image string `json:"Image"`
Status string `json:"Status"`
State string `json:"State"`
Ports string `json:"Ports"`
}
// DockerImage represents an image from docker image ls --format json.
type DockerImage struct {
ID string `json:"ID"`
Repository string `json:"Repository"`
Tag string `json:"Tag"`
Size string `json:"Size"`
CreatedAt string `json:"CreatedAt"`
}
// DockerVolume represents a volume from docker volume ls --format json.
type DockerVolume struct {
Name string `json:"Name"`
Driver string `json:"Driver"`
Mountpoint string `json:"Mountpoint"`
}
// DockerNetwork represents a network from docker network ls --format json.
type DockerNetwork struct {
ID string `json:"ID"`
Name string `json:"Name"`
Driver string `json:"Driver"`
Scope string `json:"Scope"`
}
// ComposeService represents a compose service from docker compose ps --format json.
type ComposeService struct {
ID string `json:"ID"`
Name string `json:"Name"`
Service string `json:"Service"`
State string `json:"State"`
Status string `json:"Status"`
Ports string `json:"Ports"`
}
-128
View File
@@ -1,128 +0,0 @@
package views
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lucasdataproyects/devfactory/tui"
)
type volumesState int
const (
volumesLoading volumesState = iota
volumesList
volumesAction
)
type volumesLoadedMsg []DockerVolume
type volumesActionMsg struct{ output string; err error }
type VolumesModel struct {
state volumesState
list tui.ListModel
spinner tui.SpinnerModel
styles tui.Styles
volumes []DockerVolume
err error
}
func NewVolumesModel(styles tui.Styles) VolumesModel {
return VolumesModel{
state: volumesLoading,
list: tui.NewList(nil),
spinner: tui.NewSpinner("Loading volumes..."),
styles: styles,
}
}
func (m VolumesModel) Init() tea.Cmd {
return tea.Batch(m.spinner.Init(), loadVolumes)
}
func loadVolumes() tea.Msg {
volumes, err := ListVolumes()
if err != nil {
return volumesLoadedMsg(nil)
}
return volumesLoadedMsg(volumes)
}
func (m VolumesModel) Update(msg tea.Msg) (VolumesModel, tea.Cmd) {
switch msg := msg.(type) {
case volumesLoadedMsg:
m.volumes = []DockerVolume(msg)
items := make([]tui.ListItem, len(m.volumes))
for i, v := range m.volumes {
items[i] = tui.ListItem{
Title: v.Name,
Description: fmt.Sprintf("Driver: %s", v.Driver),
Value: v,
}
}
m.list.SetItems(items)
m.state = volumesList
return m, nil
case volumesActionMsg:
if msg.err != nil {
m.err = msg.err
}
m.state = volumesList
return m, loadVolumes
case tea.KeyMsg:
if m.state == volumesList {
switch msg.String() {
case "r":
m.state = volumesLoading
return m, tea.Batch(m.spinner.Init(), loadVolumes)
case "d", "delete":
if item := m.list.SelectedItem(); item != nil {
vol := item.Value.(DockerVolume)
m.state = volumesAction
return m, func() tea.Msg {
err := RemoveVolume(vol.Name)
return volumesActionMsg{output: "Removed", err: err}
}
}
}
}
}
var cmd tea.Cmd
switch m.state {
case volumesLoading, volumesAction:
var model tea.Model
model, cmd = m.spinner.Update(msg)
m.spinner = model.(tui.SpinnerModel)
case volumesList:
var model tea.Model
model, cmd = m.list.Update(msg)
m.list = model.(tui.ListModel)
}
return m, cmd
}
// HandleBack retrocede un nivel. Retorna true si ya estaba en estado base.
func (m *VolumesModel) HandleBack() bool {
return true
}
func (m VolumesModel) View() string {
switch m.state {
case volumesLoading, volumesAction:
return m.spinner.View()
case volumesList:
if len(m.volumes) == 0 {
return m.styles.Muted.Render("No volumes found. Press 'r' to refresh.")
}
help := m.styles.Muted.Render(" d: remove │ r: refresh")
view := m.list.View() + "\n" + help
if m.err != nil {
view += "\n" + m.styles.Error.Render(fmt.Sprintf(" Error: %v", m.err))
}
return view
}
return ""
}
-181
View File
@@ -1,181 +0,0 @@
package app
import (
"fmt"
ops "fn-registry/fn_operations"
"fn-registry/registry"
"pipeline-launcher/config"
"pipeline-launcher/views"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lucasdataproyects/devfactory/tui"
)
// View identifies which tab is active.
type View int
const (
ViewPipelines View = iota
ViewHistory
)
var tabNames = []string{"Pipelines", "History"}
// Model is the top-level TUI model with two tabs.
type Model struct {
tui.BaseModel
activeTab int
pipelines views.PipelinesModel
history views.HistoryModel
ready bool
registryDB *registry.DB
opsDB *ops.DB
}
// New creates the Model, opening both databases.
func New(cfg config.Config) (Model, error) {
regDB, err := registry.Open(cfg.RegistryDB)
if err != nil {
return Model{}, fmt.Errorf("opening registry: %w", err)
}
opsDB, err := ops.Open(cfg.OperationsDB)
if err != nil {
regDB.Close()
return Model{}, fmt.Errorf("opening operations: %w", err)
}
// Build pipeline name map for history view
fns, _ := regDB.SearchFunctions("", registry.KindPipeline, "", "", "")
names := make(map[string]string, len(fns))
for _, f := range fns {
names[f.ID] = f.Name
}
styles := tui.DarkStyles()
return Model{
BaseModel: tui.NewBaseModel().WithStyles(styles),
pipelines: views.NewPipelinesModel(styles, regDB, opsDB, cfg.RegistryRoot),
history: views.NewHistoryModel(styles, opsDB, names),
registryDB: regDB,
opsDB: opsDB,
}, nil
}
// Close closes both database connections.
func (m Model) Close() {
if m.registryDB != nil {
m.registryDB.Close()
}
if m.opsDB != nil {
m.opsDB.Close()
}
}
func (m Model) Init() tea.Cmd {
return m.pipelines.Init()
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case views.KeyQuit:
return m, tea.Quit
case "q":
updated, atBase := m.handleBack()
if atBase {
return updated, tea.Quit
}
return updated, nil
case views.KeyEsc, views.KeyBack:
updated, atBase := m.handleBack()
if atBase {
return updated, nil
}
return updated, nil
case views.KeyTab:
m.activeTab = (m.activeTab + 1) % len(tabNames)
return m, m.initActiveView()
case "shift+tab":
m.activeTab = (m.activeTab - 1 + len(tabNames)) % len(tabNames)
return m, m.initActiveView()
}
case tea.WindowSizeMsg:
m.HandleWindowSize(msg)
m.ready = true
}
var cmd tea.Cmd
switch View(m.activeTab) {
case ViewPipelines:
m.pipelines, cmd = m.pipelines.Update(msg)
case ViewHistory:
m.history, cmd = m.history.Update(msg)
}
return m, cmd
}
func (m Model) View() string {
if !m.ready {
return "Loading..."
}
tabs := m.renderTabs()
var content string
switch View(m.activeTab) {
case ViewPipelines:
content = m.pipelines.View()
case ViewHistory:
content = m.history.View()
}
status := m.Styles.StatusBar.Render(" Tab: switch view │ Ctrl+C: quit │ Enter: action │ r: refresh")
return lipgloss.JoinVertical(lipgloss.Left,
tabs,
"",
content,
"",
status,
)
}
func (m Model) renderTabs() string {
var tabs []string
for i, name := range tabNames {
if i == m.activeTab {
tabs = append(tabs, m.Styles.Selected.Render(" "+name+" "))
} else {
tabs = append(tabs, m.Styles.Muted.Render(" "+name+" "))
}
}
row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
return m.Styles.Header.Render("Pipeline Launcher") + " " + row
}
func (m Model) handleBack() (Model, bool) {
switch View(m.activeTab) {
case ViewPipelines:
atBase := m.pipelines.HandleBack()
return m, atBase
case ViewHistory:
atBase := m.history.HandleBack()
return m, atBase
}
return m, true
}
func (m Model) initActiveView() tea.Cmd {
switch View(m.activeTab) {
case ViewPipelines:
return m.pipelines.Init()
case ViewHistory:
return m.history.Init()
}
return nil
}
-27
View File
@@ -1,27 +0,0 @@
package config
import (
"os"
"path/filepath"
)
// Config holds paths to databases.
type Config struct {
RegistryDB string // Path to registry.db
OperationsDB string // Path to operations.db
RegistryRoot string // Root directory of the registry (for resolving file paths)
}
// Default returns a Config resolved from environment or sensible defaults.
func Default() Config {
root := os.Getenv("FN_REGISTRY_ROOT")
if root == "" {
root = "."
}
return Config{
RegistryDB: filepath.Join(root, "registry.db"),
OperationsDB: filepath.Join(root, "apps", "pipeline_launcher", "operations.db"),
RegistryRoot: root,
}
}
-38
View File
@@ -1,38 +0,0 @@
module pipeline-launcher
go 1.22.2
require (
fn-registry v0.0.0
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/lucasdataproyects/devfactory v0.0.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.37 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
fn-registry => /home/lucas/fn_registry
github.com/lucasdataproyects/devfactory => /home/lucas/.local_agentes/backend
)
-51
View File
@@ -1,51 +0,0 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-29
View File
@@ -1,29 +0,0 @@
package main
import (
"fmt"
"os"
"pipeline-launcher/app"
"pipeline-launcher/config"
"github.com/lucasdataproyects/devfactory/tui"
)
func main() {
cfg := config.Default()
model, err := app.New(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer model.Close()
result := tui.RunFullscreen(model)
if result.IsErr() {
fmt.Fprintf(os.Stderr, "error: %v\n", result.Error())
os.Exit(1)
}
}
-219
View File
@@ -1,219 +0,0 @@
package views
import (
"encoding/json"
"fmt"
"strings"
ops "fn-registry/fn_operations"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lucasdataproyects/devfactory/tui"
)
type historyState int
const (
historyLoading historyState = iota
historyList
historyDetail
)
type historyLoadedMsg []ops.Execution
// HistoryModel shows execution history.
type HistoryModel struct {
state historyState
list tui.FilteredListModel
spinner tui.SpinnerModel
styles tui.Styles
executions []ops.Execution
detail string
scrollOff int
opsDB *ops.DB
pipelineNames map[string]string
}
// NewHistoryModel creates a new history view.
func NewHistoryModel(styles tui.Styles, opsDB *ops.DB, names map[string]string) HistoryModel {
return HistoryModel{
state: historyLoading,
list: tui.NewFilteredList(nil, "Filter executions..."),
spinner: tui.NewSpinner("Loading history..."),
styles: styles,
opsDB: opsDB,
pipelineNames: names,
}
}
func (m HistoryModel) Init() tea.Cmd {
return tea.Batch(
m.spinner.Init(),
m.loadHistory(),
)
}
func (m HistoryModel) loadHistory() tea.Cmd {
return func() tea.Msg {
execs, err := m.opsDB.ListExecutions("", "", "")
if err != nil {
return historyLoadedMsg(nil)
}
return historyLoadedMsg(execs)
}
}
func (m HistoryModel) Update(msg tea.Msg) (HistoryModel, tea.Cmd) {
switch msg := msg.(type) {
case historyLoadedMsg:
m.executions = []ops.Execution(msg)
items := make([]tui.ListItem, len(m.executions))
for i, e := range m.executions {
icon := "●"
switch e.Status {
case ops.ExecSuccess:
icon = "✓"
case ops.ExecFailure:
icon = "✗"
case ops.ExecPartial:
icon = "~"
}
name := e.PipelineID
if n, ok := m.pipelineNames[e.PipelineID]; ok {
name = n
}
dur := ""
if e.DurationMs != nil {
dur = fmt.Sprintf("%dms", *e.DurationMs)
}
items[i] = tui.ListItem{
Title: fmt.Sprintf("%s %s", icon, name),
Description: fmt.Sprintf("%s — %s — %s", string(e.Status), dur, e.StartedAt.Format("2006-01-02 15:04:05")),
Value: e,
}
}
m.list.SetItems(items)
m.state = historyList
return m, nil
case tea.KeyMsg:
switch m.state {
case historyList:
switch msg.String() {
case "r":
m.state = historyLoading
m.spinner = tui.NewSpinner("Loading history...")
return m, tea.Batch(m.spinner.Init(), m.loadHistory())
case "enter":
// Delegate enter to list first so it selects the cursor item
updated, _ := m.list.Update(msg)
m.list = updated.(tui.FilteredListModel)
if item := m.list.SelectedItem(); item != nil {
e := item.Value.(ops.Execution)
m.detail = formatExecution(e)
m.state = historyDetail
m.scrollOff = 0
}
return m, nil
}
case historyDetail:
switch msg.String() {
case "j", "down":
m.scrollOff++
case "k", "up":
if m.scrollOff > 0 {
m.scrollOff--
}
}
return m, nil
}
}
// Delegate to sub-components
var cmd tea.Cmd
switch m.state {
case historyLoading:
var spinnerModel tea.Model
spinnerModel, cmd = m.spinner.Update(msg)
m.spinner = spinnerModel.(tui.SpinnerModel)
case historyList:
var listModel tea.Model
listModel, cmd = m.list.Update(msg)
m.list = listModel.(tui.FilteredListModel)
}
return m, cmd
}
// HandleBack retrocede un nivel. Retorna true si ya en estado base.
func (m *HistoryModel) HandleBack() bool {
switch m.state {
case historyDetail:
m.state = historyList
return false
default:
return true
}
}
func (m HistoryModel) View() string {
switch m.state {
case historyLoading:
return m.spinner.View()
case historyList:
if len(m.executions) == 0 {
return m.styles.Muted.Render("No executions found. Launch a pipeline first.")
}
help := m.styles.Muted.Render(" Enter: details │ r: refresh │ /: filter")
return m.list.View() + "\n" + help
case historyDetail:
return m.renderDetail()
}
return ""
}
func (m HistoryModel) renderDetail() string {
lines := splitLines(m.detail)
maxLines := 20
if m.scrollOff >= len(lines) {
m.scrollOff = max(0, len(lines)-1)
}
end := min(m.scrollOff+maxLines, len(lines))
visible := lines[m.scrollOff:end]
header := m.styles.Header.Render("Execution Detail")
content := lipgloss.JoinVertical(lipgloss.Left, visible...)
help := m.styles.Muted.Render(" j/k: scroll │ Esc: back")
return header + "\n" + content + "\n" + help
}
func formatExecution(e ops.Execution) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("ID: %s\n", e.ID))
sb.WriteString(fmt.Sprintf("Pipeline: %s\n", e.PipelineID))
sb.WriteString(fmt.Sprintf("Status: %s\n", e.Status))
sb.WriteString(fmt.Sprintf("Started: %s\n", e.StartedAt.Format("2006-01-02 15:04:05")))
if e.EndedAt != nil {
sb.WriteString(fmt.Sprintf("Ended: %s\n", e.EndedAt.Format("2006-01-02 15:04:05")))
}
if e.DurationMs != nil {
sb.WriteString(fmt.Sprintf("Duration: %dms\n", *e.DurationMs))
}
if e.RecordsIn != nil {
sb.WriteString(fmt.Sprintf("Records In: %d\n", *e.RecordsIn))
}
if e.RecordsOut != nil {
sb.WriteString(fmt.Sprintf("Records Out: %d\n", *e.RecordsOut))
}
if e.Error != "" {
sb.WriteString(fmt.Sprintf("\n--- Error ---\n%s\n", e.Error))
}
if len(e.Metrics) > 0 {
sb.WriteString("\n--- Metrics ---\n")
b, _ := json.MarshalIndent(e.Metrics, "", " ")
sb.WriteString(string(b))
sb.WriteString("\n")
}
return sb.String()
}
-14
View File
@@ -1,14 +0,0 @@
package views
// Navigation key constants.
const (
KeyQuit = "ctrl+c"
KeyEsc = "esc"
KeyBack = "0"
KeyTab = "tab"
)
// IsBack returns true if the key should trigger back navigation.
func IsBack(key string) bool {
return key == KeyEsc || key == KeyBack
}
-398
View File
@@ -1,398 +0,0 @@
package views
import (
"fmt"
"strings"
ops "fn-registry/fn_operations"
"fn-registry/registry"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lucasdataproyects/devfactory/tui"
)
type pipelinesState int
const (
pipelinesLoading pipelinesState = iota
pipelinesList
pipelinesArgs
pipelinesRunning
pipelinesOutput
)
type pipelinesLoadedMsg []registry.Function
type pipelineFinishedMsg RunResult
type pipelineFlagsMsg []PipelineFlag
// PipelinesModel lists and launches pipelines.
type PipelinesModel struct {
state pipelinesState
list tui.FilteredListModel
spinner tui.SpinnerModel
styles tui.Styles
pipelines []registry.Function
selectedFn *registry.Function
flags []PipelineFlag
inputs []textinput.Model
focusIdx int
output string
lastResult *RunResult
scrollOff int
err error
registryDB *registry.DB
opsDB *ops.DB
registryRoot string
}
// NewPipelinesModel creates a new pipelines view.
func NewPipelinesModel(styles tui.Styles, regDB *registry.DB, opsDB *ops.DB, root string) PipelinesModel {
return PipelinesModel{
state: pipelinesLoading,
list: tui.NewFilteredList(nil, "Filter pipelines..."),
spinner: tui.NewSpinner("Loading pipelines..."),
styles: styles,
registryDB: regDB,
opsDB: opsDB,
registryRoot: root,
}
}
func (m PipelinesModel) Init() tea.Cmd {
return tea.Batch(
m.spinner.Init(),
m.loadPipelines(),
)
}
func (m PipelinesModel) loadPipelines() tea.Cmd {
return func() tea.Msg {
fns, err := m.registryDB.SearchFunctions("", registry.KindPipeline, "", "", "")
if err != nil {
return pipelinesLoadedMsg(nil)
}
// Only show pipelines tagged with "launcher"
var launchable []registry.Function
for _, f := range fns {
for _, t := range f.Tags {
if t == "launcher" {
launchable = append(launchable, f)
break
}
}
}
return pipelinesLoadedMsg(launchable)
}
}
// buildInputs creates a textinput for each flag, pre-filled with defaults.
func (m *PipelinesModel) buildInputs() tea.Cmd {
m.inputs = make([]textinput.Model, len(m.flags))
for i, f := range m.flags {
ti := textinput.New()
ti.CharLimit = 256
ti.Width = 40
if f.Default != "" {
ti.SetValue(f.Default)
}
if f.Required {
ti.Placeholder = "(requerido)"
}
m.inputs[i] = ti
}
m.focusIdx = 0
if len(m.inputs) > 0 {
m.inputs[0].Focus()
return textinput.Blink
}
return nil
}
func (m *PipelinesModel) focusInput(idx int) tea.Cmd {
if idx < 0 || idx >= len(m.inputs) {
return nil
}
for i := range m.inputs {
m.inputs[i].Blur()
}
m.focusIdx = idx
m.inputs[idx].Focus()
return textinput.Blink
}
// collectArgs builds CLI args from the form inputs.
func (m PipelinesModel) collectArgs() []string {
var args []string
for i, f := range m.flags {
val := strings.TrimSpace(m.inputs[i].Value())
if val != "" {
args = append(args, "--"+f.Name, val)
}
}
return args
}
func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) {
switch msg := msg.(type) {
case pipelinesLoadedMsg:
m.pipelines = []registry.Function(msg)
items := make([]tui.ListItem, len(m.pipelines))
for i, p := range m.pipelines {
items[i] = tui.ListItem{
Title: p.Name,
Description: fmt.Sprintf("%s — %s", p.Domain, truncate(p.Description, 60)),
Value: p,
}
}
m.list.SetItems(items)
m.state = pipelinesList
return m, nil
case pipelineFlagsMsg:
m.flags = []PipelineFlag(msg)
cmd := m.buildInputs()
return m, cmd
case pipelineFinishedMsg:
result := RunResult(msg)
m.lastResult = &result
var sb strings.Builder
if result.Status == ops.ExecSuccess {
sb.WriteString("[OK] ")
} else {
sb.WriteString("[FAIL] ")
}
fmt.Fprintf(&sb, "Pipeline: %s\n", result.PipelineID)
fmt.Fprintf(&sb, "Execution: %s\n", result.ExecID)
fmt.Fprintf(&sb, "Duration: %dms\n", result.DurationMs)
sb.WriteString("\n--- stdout ---\n")
if result.Stdout != "" {
sb.WriteString(result.Stdout)
} else {
sb.WriteString("(empty)")
}
if result.Stderr != "" {
sb.WriteString("\n--- stderr ---\n")
sb.WriteString(result.Stderr)
}
if result.Err != nil {
fmt.Fprintf(&sb, "\n--- error ---\n%v", result.Err)
}
m.output = sb.String()
m.state = pipelinesOutput
m.scrollOff = 0
return m, nil
case tea.KeyMsg:
switch m.state {
case pipelinesList:
switch msg.String() {
case "r":
m.state = pipelinesLoading
m.spinner = tui.NewSpinner("Loading pipelines...")
return m, tea.Batch(m.spinner.Init(), m.loadPipelines())
case "enter":
updated, _ := m.list.Update(msg)
m.list = updated.(tui.FilteredListModel)
if item := m.list.SelectedItem(); item != nil {
fn := item.Value.(registry.Function)
m.selectedFn = &fn
m.flags = nil
m.inputs = nil
m.state = pipelinesArgs
root := m.registryRoot
fnCopy := fn
return m, func() tea.Msg {
return pipelineFlagsMsg(GetPipelineFlags(&fnCopy, root))
}
}
return m, nil
}
case pipelinesArgs:
switch msg.String() {
case "tab", "down":
cmd := m.focusInput((m.focusIdx + 1) % max(len(m.inputs), 1))
return m, cmd
case "shift+tab", "up":
idx := m.focusIdx - 1
if idx < 0 {
idx = max(len(m.inputs)-1, 0)
}
cmd := m.focusInput(idx)
return m, cmd
case "ctrl+enter", "ctrl+s":
args := m.collectArgs()
m.state = pipelinesRunning
m.spinner = tui.NewSpinner(fmt.Sprintf("Running %s...", m.selectedFn.Name))
return m, tea.Batch(m.spinner.Init(), m.runPipelineCmd(m.selectedFn, args))
case "esc":
m.state = pipelinesList
return m, nil
}
case pipelinesOutput:
switch msg.String() {
case "j", "down":
m.scrollOff++
case "k", "up":
if m.scrollOff > 0 {
m.scrollOff--
}
}
return m, nil
}
}
// Delegate to sub-components
var cmd tea.Cmd
switch m.state {
case pipelinesLoading, pipelinesRunning:
var spinnerModel tea.Model
spinnerModel, cmd = m.spinner.Update(msg)
m.spinner = spinnerModel.(tui.SpinnerModel)
case pipelinesList:
var listModel tea.Model
listModel, cmd = m.list.Update(msg)
m.list = listModel.(tui.FilteredListModel)
case pipelinesArgs:
if m.focusIdx >= 0 && m.focusIdx < len(m.inputs) {
m.inputs[m.focusIdx], cmd = m.inputs[m.focusIdx].Update(msg)
}
}
return m, cmd
}
func (m PipelinesModel) runPipelineCmd(fn *registry.Function, args []string) tea.Cmd {
regRoot := m.registryRoot
opsDB := m.opsDB
fnCopy := *fn
return func() tea.Msg {
result := RunPipeline(&fnCopy, regRoot, opsDB, args)
return pipelineFinishedMsg(result)
}
}
// HandleBack retrocede un nivel. Retorna true si ya en estado base.
func (m *PipelinesModel) HandleBack() bool {
switch m.state {
case pipelinesArgs:
m.state = pipelinesList
return false
case pipelinesOutput:
m.state = pipelinesList
return false
default:
return true
}
}
func (m PipelinesModel) View() string {
switch m.state {
case pipelinesLoading:
return m.spinner.View()
case pipelinesList:
if len(m.pipelines) == 0 {
return m.styles.Muted.Render("No pipelines found. Press 'r' to refresh.")
}
help := m.styles.Muted.Render(" Enter: launch │ r: refresh │ /: filter")
return m.list.View() + "\n" + help
case pipelinesArgs:
return m.renderArgsForm()
case pipelinesRunning:
return m.spinner.View()
case pipelinesOutput:
return m.renderOutput()
}
return ""
}
func (m PipelinesModel) renderArgsForm() string {
header := m.styles.Header.Render(m.selectedFn.Name)
var parts []string
parts = append(parts, header, "")
if len(m.flags) == 0 {
parts = append(parts, m.styles.Muted.Render(" Loading flags..."))
} else if len(m.inputs) == 0 {
parts = append(parts, m.styles.Muted.Render(" No flags available. Ctrl+S to run."))
} else {
for i, f := range m.flags {
marker := " "
if f.Required {
marker = m.styles.Error.Render("* ")
}
name := fmt.Sprintf("--%-16s", f.Name)
cursor := " "
if i == m.focusIdx {
cursor = m.styles.Info.Render("> ")
}
label := fmt.Sprintf("%s%s%s", cursor, marker, m.styles.Label.Render(name))
input := m.inputs[i].View()
desc := f.Desc
if f.Default != "" {
desc += m.styles.Muted.Render(fmt.Sprintf(" (default: %s)", f.Default))
}
parts = append(parts, label+input)
parts = append(parts, " "+m.styles.Muted.Render(desc))
}
}
parts = append(parts, "")
parts = append(parts, m.styles.Muted.Render(" ↑/↓: navigate │ Ctrl+S: run │ Esc: cancel"))
return lipgloss.JoinVertical(lipgloss.Left, parts...)
}
func (m PipelinesModel) renderOutput() string {
lines := splitLines(m.output)
maxLines := 20
if m.scrollOff >= len(lines) {
m.scrollOff = max(0, len(lines)-1)
}
end := min(m.scrollOff+maxLines, len(lines))
visible := lines[m.scrollOff:end]
header := m.styles.Header.Render("Pipeline Output")
content := lipgloss.JoinVertical(lipgloss.Left, visible...)
help := m.styles.Muted.Render(" j/k: scroll │ Esc: back")
return header + "\n" + content + "\n" + help
}
func splitLines(s string) []string {
if s == "" {
return []string{"(empty)"}
}
lines := strings.Split(s, "\n")
if len(lines) == 0 {
return []string{"(empty)"}
}
return lines
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n-3] + "..."
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
-146
View File
@@ -1,146 +0,0 @@
package views
import (
"bytes"
"fmt"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
ops "fn-registry/fn_operations"
"fn-registry/registry"
)
// PipelineFlag describes a CLI flag parsed from -help output.
type PipelineFlag struct {
Name string // e.g. "project"
Type string // e.g. "string"
Desc string // description text
Default string // default value, empty if none
Required bool // true if no default
}
var flagLineRe = regexp.MustCompile(`^\s+-(\S+)\s+(\S+)$`)
var defaultRe = regexp.MustCompile(`\(default "(.*)"\)`)
// GetPipelineFlags runs `go run . -help` and parses the flag output.
func GetPipelineFlags(fn *registry.Function, registryRoot string) []PipelineFlag {
absPath := filepath.Join(registryRoot, fn.FilePath)
dir := filepath.Dir(absPath)
cmd := exec.Command("go", "run", ".", "-help")
cmd.Dir = dir
var stderr bytes.Buffer
cmd.Stderr = &stderr
cmd.Run() // -help exits with code 2, ignore error
return parseFlags(stderr.String())
}
func parseFlags(output string) []PipelineFlag {
var flags []PipelineFlag
lines := strings.Split(output, "\n")
for i := 0; i < len(lines); i++ {
m := flagLineRe.FindStringSubmatch(lines[i])
if m == nil {
continue
}
f := PipelineFlag{Name: m[1], Type: m[2]}
// Next line is the description
if i+1 < len(lines) {
desc := strings.TrimSpace(lines[i+1])
if dm := defaultRe.FindStringSubmatch(desc); dm != nil {
f.Default = dm[1]
f.Desc = strings.TrimSpace(defaultRe.ReplaceAllString(desc, ""))
} else {
f.Desc = desc
}
i++
}
f.Required = f.Default == "" && !strings.Contains(strings.ToLower(f.Desc), "opcional")
flags = append(flags, f)
}
return flags
}
// RunResult holds the outcome of a pipeline execution.
type RunResult struct {
Stdout string
Stderr string
ExecID string
PipelineID string
Status ops.ExecutionStatus
DurationMs int64
Err error
}
// RunPipeline executes a pipeline as a subprocess and records the execution.
func RunPipeline(fn *registry.Function, registryRoot string, opsDB *ops.DB, args []string) RunResult {
absPath := filepath.Join(registryRoot, fn.FilePath)
dir := filepath.Dir(absPath)
startedAt := time.Now().UTC()
cmdArgs := append([]string{"run", "."}, args...)
cmd := exec.Command("go", cmdArgs...)
cmd.Dir = dir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
endedAt := time.Now().UTC()
status := ops.ExecSuccess
var execErr string
if err != nil {
status = ops.ExecFailure
execErr = err.Error()
if stderr.Len() > 0 {
execErr = stderr.String()
}
}
execID := fmt.Sprintf("exec_%d", time.Now().UnixNano())
durationMs := endedAt.Sub(startedAt).Milliseconds()
execution := &ops.Execution{
ID: execID,
PipelineID: fn.ID,
Status: status,
StartedAt: startedAt,
EndedAt: &endedAt,
DurationMs: &durationMs,
Error: execErr,
CreatedAt: time.Now().UTC(),
}
insertErr := ops.InsertExecutionSafe(opsDB, execution)
if insertErr != nil {
return RunResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExecID: execID,
PipelineID: fn.ID,
Status: status,
DurationMs: durationMs,
Err: fmt.Errorf("pipeline ran but failed to record: %w", insertErr),
}
}
return RunResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExecID: execID,
PipelineID: fn.ID,
Status: status,
DurationMs: durationMs,
Err: err,
}
}
@@ -0,0 +1,42 @@
---
name: assert_docker_container_running
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "assert_docker_container_running(container_name: string) -> void"
description: "Verifica que un contenedor Docker está corriendo. Sale con exit code 1 si no está activo, con mensaje a stderr."
tags: [assert, docker, container, running, validation, infra, bash]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: container_name
desc: "nombre del contenedor Docker a verificar"
output: "sin salida; exit code 0 si existe y está corriendo, 1 si no"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/assert_docker_container_running.sh"
---
## Ejemplo
```bash
source functions/infra/assert_docker_container_running.sh
assert_docker_container_running metabase
echo "Contenedor activo, continuando..."
```
## Notas
Usa `docker ps --format '{{.Names}}'` con grep anclado (`^name$`) para evitar matches parciales (ej: "metabase" no matchea "metabase-test").
Output limpio: void en éxito. El mensaje de error en stderr no incluye lista de contenedores activos — eso es responsabilidad del pipeline/caller.
Requiere que `docker` esté en PATH. Combinar con `assert_command_exists` antes de llamar.
@@ -0,0 +1,19 @@
# assert_docker_container_running
# --------------------------------
# Verifica que un contenedor Docker está corriendo.
# No produce output a stdout en caso de éxito.
# Sale con exit code 1 si el contenedor no está corriendo,
# con mensaje descriptivo a stderr.
#
# USO (sourced):
# source assert_docker_container_running.sh
# assert_docker_container_running metabase
assert_docker_container_running() {
local container_name="$1"
if ! docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then
echo "assert_docker_container_running: el contenedor '$container_name' no está corriendo" >&2
return 1
fi
}
+36
View File
@@ -0,0 +1,36 @@
---
name: build_cpp_linux
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "build_cpp_linux(target?: string) -> void"
description: "Compila las funciones y apps C++ del registry para Linux nativo usando cmake"
tags: [cpp, build, cmake, linux, imgui]
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/build_cpp_linux.sh"
params:
- name: target
desc: "Nombre del target cmake a compilar (opcional, sin argumento compila todo)"
output: "Compila los binarios en cpp/build/linux/"
---
# build_cpp_linux
Configura y compila el proyecto C++ (ImGui/ImPlot) para Linux nativo.
Usa cmake con compilacion paralela (`-j$(nproc)`). Si no se ha configurado antes, ejecuta `cmake -B` automaticamente.
```bash
fn run build_cpp_linux # Compilar todo
fn run build_cpp_linux chart_demo # Compilar solo chart_demo
```
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
CPP_ROOT="$REGISTRY_ROOT/cpp"
BUILD_DIR="$CPP_ROOT/build/linux"
TARGET="${1:-}"
# Configure if needed
if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
echo "[build_cpp_linux] Configuring cmake..."
cmake -B "$BUILD_DIR" -S "$CPP_ROOT"
fi
# Build
if [ -n "$TARGET" ]; then
echo "[build_cpp_linux] Building target: $TARGET"
cmake --build "$BUILD_DIR" --target "$TARGET" -- -j"$(nproc)"
else
echo "[build_cpp_linux] Building all targets..."
cmake --build "$BUILD_DIR" -- -j"$(nproc)"
fi
echo "[build_cpp_linux] Done. Binaries in $BUILD_DIR"
+38
View File
@@ -0,0 +1,38 @@
---
name: build_cpp_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "build_cpp_windows(target?: string) -> void"
description: "Cross-compila las funciones y apps C++ del registry para Windows usando mingw-w64"
tags: [cpp, build, cmake, windows, cross-compile, mingw, imgui]
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/build_cpp_windows.sh"
params:
- name: target
desc: "Nombre del target cmake a compilar (opcional, sin argumento compila todo)"
output: "Produce binarios .exe de Windows en cpp/build/windows/"
---
# build_cpp_windows
Cross-compila el proyecto C++ para Windows desde Linux usando el toolchain mingw-w64.
Los .exe resultantes incluyen runtime linkado estaticamente (self-contained).
```bash
fn run build_cpp_windows # Compilar todo
fn run build_cpp_windows chart_demo # Compilar solo chart_demo
```
Requiere `mingw-w64`: `sudo apt install mingw-w64`
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
CPP_ROOT="$REGISTRY_ROOT/cpp"
BUILD_DIR="$CPP_ROOT/build/windows"
TOOLCHAIN="$CPP_ROOT/toolchains/mingw-w64.cmake"
TARGET="${1:-}"
# Check mingw is available
if ! command -v x86_64-w64-mingw32-g++ &>/dev/null; then
echo "[build_cpp_windows] Error: mingw-w64 not found. Install with: sudo apt install mingw-w64"
exit 1
fi
# Configure if needed
if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
echo "[build_cpp_windows] Configuring cmake with mingw-w64 toolchain..."
cmake -B "$BUILD_DIR" -S "$CPP_ROOT" -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN"
fi
# Build
if [ -n "$TARGET" ]; then
echo "[build_cpp_windows] Cross-compiling target: $TARGET"
cmake --build "$BUILD_DIR" --target "$TARGET" -- -j"$(nproc)"
else
echo "[build_cpp_windows] Cross-compiling all targets..."
cmake --build "$BUILD_DIR" -- -j"$(nproc)"
fi
echo "[build_cpp_windows] Done. Windows binaries in $BUILD_DIR"
if [ -n "$TARGET" ]; then
file "$BUILD_DIR"/**/"$TARGET".exe 2>/dev/null || file "$BUILD_DIR/$TARGET".exe 2>/dev/null || true
fi
+49
View File
@@ -0,0 +1,49 @@
---
name: docker_cp_file
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "docker_cp_file(local_path: string, container_name: string, dest_path: string) -> string"
description: "Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide. Imprime JSON con local_size y remote_size a stdout. Sale con exit code 1 si docker cp falla o los tamaños difieren."
tags: [docker, cp, copy, file, container, transfer, infra, bash]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: local_path
desc: "ruta del archivo local a copiar"
- name: container_name
desc: "nombre del contenedor Docker destino"
- name: dest_path
desc: "ruta destino dentro del contenedor"
output: "JSON con local_size y remote_size en bytes"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/docker_cp_file.sh"
---
## Ejemplo
```bash
source functions/infra/docker_cp_file.sh
result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db)
echo "$result"
# {"local_size":524288,"remote_size":524288}
local_size=$(echo "$result" | grep -o '"local_size":[0-9]*' | cut -d: -f2)
```
## Notas
La verificación de tamaño usa `docker exec stat -c%s` sobre el contenedor destino. Si `stat` no está disponible en el contenedor, `remote_size` será -1 y la función fallará.
Output a stdout: JSON minificado con campos `local_size` y `remote_size` (enteros, bytes).
Usa `printf` en lugar de `echo` para garantizar que no haya newline extra en el JSON.
+33
View File
@@ -0,0 +1,33 @@
# docker_cp_file
# --------------
# Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide.
# Imprime JSON con local_size y remote_size a stdout si la copia es exitosa.
# Sale con exit code 1 si docker cp falla o si los tamaños no coinciden.
#
# USO (sourced):
# source docker_cp_file.sh
# result=$(docker_cp_file /ruta/local.db metabase /dest/path.db)
docker_cp_file() {
local local_path="$1"
local container_name="$2"
local dest_path="$3"
if ! docker cp "$local_path" "${container_name}:${dest_path}" 2>/dev/null; then
echo "docker_cp_file: fallo al copiar '$local_path' a '${container_name}:${dest_path}'" >&2
return 1
fi
local local_size
local_size=$(stat -c%s "$local_path")
local remote_size
remote_size=$(docker exec "$container_name" stat -c%s "$dest_path" 2>/dev/null || echo "-1")
if [ "$local_size" != "$remote_size" ]; then
echo "docker_cp_file: tamaños no coinciden (local=${local_size}, remoto=${remote_size})" >&2
return 1
fi
printf '{"local_size":%s,"remote_size":%s}' "$local_size" "$remote_size"
}
+54
View File
@@ -0,0 +1,54 @@
---
name: frontend_doctor
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "frontend_doctor(project_dir: string) -> diagnostics_stdout"
description: "Diagnostica la salud de un proyecto frontend Mantine. Verifica Node, React, Mantine, PostCSS, TypeScript, vite.config y detecta residuos de shadcn/@base-ui. Imprime tabla de checks con exit code 0/1."
tags: [frontend, mantine, doctor, diagnostics, health, validation]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio del proyecto frontend con package.json"
output: "tabla de checks con ✓/✗ por cada validación y resumen final"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/frontend_doctor.sh"
---
## Ejemplo
```bash
# Diagnosticar un proyecto
bash frontend_doctor.sh ./apps/rapid_dashboards/frontend
# Output:
# === Frontend Doctor: ./apps/rapid_dashboards/frontend ===
#
# ✓ Node >= 18 22.12.0
# ✓ Package manager detected pnpm
# ✓ node_modules present
# ✓ @mantine/core 7.17.0
# ✓ @mantine/hooks
# ✓ @mantine/charts
# ✓ React >= 18 19.2.4
# ✓ postcss.config present
# ✓ TypeScript >= 5 6.0.2
# ✓ vite.config present
# ✓ No shadcn residual
# ✓ No @base-ui residual
#
# Resultado: todo OK
```
## Notas
Checks informativos, no modifica nada. Util para validar que un proyecto esta correctamente configurado despues de instalar Mantine o migrar desde shadcn. Exit code 0 si todo OK, 1 si hay problemas.
+150
View File
@@ -0,0 +1,150 @@
# frontend_doctor
# ----------------
# Diagnostica la salud de un proyecto frontend Mantine.
# Verifica dependencias, configuracion y versiones.
# Imprime tabla de checks y retorna exit code 0 (ok) o 1 (fallos).
#
# USO (sourced):
# source frontend_doctor.sh
# frontend_doctor /path/to/frontend
#
# USO (directo):
# bash frontend_doctor.sh /path/to/frontend
frontend_doctor() {
local project_dir="$1"
local failures=0
if [ -z "$project_dir" ]; then
echo "frontend_doctor: se requiere project_dir" >&2
return 1
fi
if [ ! -f "$project_dir/package.json" ]; then
echo "frontend_doctor: no existe package.json en $project_dir" >&2
return 1
fi
echo "=== Frontend Doctor: $project_dir ==="
echo ""
# Helper: check y reportar
_check() {
local label="$1"
local ok="$2"
local detail="$3"
if [ "$ok" = "1" ]; then
printf " ✓ %-35s %s\n" "$label" "$detail"
else
printf " ✗ %-35s %s\n" "$label" "$detail"
((failures++))
fi
}
# 1. Node >= 18
local node_ver=""
local node_ok=0
if command -v node &>/dev/null; then
node_ver=$(node -v 2>/dev/null | sed 's/v//')
local node_major=$(echo "$node_ver" | cut -d. -f1)
[ "$node_major" -ge 18 ] 2>/dev/null && node_ok=1
fi
_check "Node >= 18" "$node_ok" "${node_ver:-not found}"
# 2. Package manager
local pm_ok=0
local pm_name="none"
if [ -f "$project_dir/pnpm-lock.yaml" ]; then
pm_name="pnpm"; pm_ok=1
elif [ -f "$project_dir/yarn.lock" ]; then
pm_name="yarn"; pm_ok=1
elif [ -f "$project_dir/package-lock.json" ]; then
pm_name="npm"; pm_ok=1
fi
_check "Package manager detected" "$pm_ok" "$pm_name"
# 3. node_modules existe
local nm_ok=0
[ -d "$project_dir/node_modules" ] && nm_ok=1
_check "node_modules present" "$nm_ok" ""
# 4. @mantine/core instalado
local mantine_ok=0
local mantine_ver=""
if [ -f "$project_dir/node_modules/@mantine/core/package.json" ]; then
mantine_ver=$(node -e "console.log(require('$project_dir/node_modules/@mantine/core/package.json').version)" 2>/dev/null)
mantine_ok=1
fi
_check "@mantine/core" "$mantine_ok" "${mantine_ver:-not installed}"
# 5. @mantine/hooks
local hooks_ok=0
[ -d "$project_dir/node_modules/@mantine/hooks" ] && hooks_ok=1
_check "@mantine/hooks" "$hooks_ok" ""
# 6. @mantine/charts
local charts_ok=0
[ -d "$project_dir/node_modules/@mantine/charts" ] && charts_ok=1
_check "@mantine/charts" "$charts_ok" ""
# 7. React >= 18
local react_ok=0
local react_ver=""
if [ -f "$project_dir/node_modules/react/package.json" ]; then
react_ver=$(node -e "console.log(require('$project_dir/node_modules/react/package.json').version)" 2>/dev/null)
local react_major=$(echo "$react_ver" | cut -d. -f1)
[ "$react_major" -ge 18 ] 2>/dev/null && react_ok=1
fi
_check "React >= 18" "$react_ok" "${react_ver:-not found}"
# 8. postcss.config presente
local postcss_ok=0
if [ -f "$project_dir/postcss.config.cjs" ] || [ -f "$project_dir/postcss.config.js" ] || [ -f "$project_dir/postcss.config.mjs" ]; then
postcss_ok=1
fi
_check "postcss.config present" "$postcss_ok" ""
# 9. TypeScript >= 5
local ts_ok=0
local ts_ver=""
if [ -f "$project_dir/node_modules/typescript/package.json" ]; then
ts_ver=$(node -e "console.log(require('$project_dir/node_modules/typescript/package.json').version)" 2>/dev/null)
local ts_major=$(echo "$ts_ver" | cut -d. -f1)
[ "$ts_major" -ge 5 ] 2>/dev/null && ts_ok=1
fi
_check "TypeScript >= 5" "$ts_ok" "${ts_ver:-not found}"
# 10. vite.config presente
local vite_ok=0
if [ -f "$project_dir/vite.config.ts" ] || [ -f "$project_dir/vite.config.js" ]; then
vite_ok=1
fi
_check "vite.config present" "$vite_ok" ""
# 11. Shadcn residual (warning)
local shadcn_clean=1
if [ -f "$project_dir/components.json" ]; then
shadcn_clean=0
fi
_check "No shadcn residual" "$shadcn_clean" "$([ "$shadcn_clean" = "0" ] && echo 'components.json found')"
# 12. @base-ui residual (warning)
local baseui_clean=1
if [ -d "$project_dir/node_modules/@base-ui" ]; then
baseui_clean=0
fi
_check "No @base-ui residual" "$baseui_clean" "$([ "$baseui_clean" = "0" ] && echo '@base-ui still installed')"
echo ""
if [ "$failures" -eq 0 ]; then
echo " Resultado: todo OK"
return 0
else
echo " Resultado: $failures problema(s) encontrado(s)"
return 1
fi
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
frontend_doctor "$@"
fi
@@ -0,0 +1,53 @@
---
name: gitea_add_collaborator
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_add_collaborator(owner: string, repo: string, username: string, permission: string) -> void"
description: "Añade un colaborador a un repositorio Gitea con el nivel de permisos indicado. Silencioso si el colaborador ya existe (422)."
tags: [gitea, git, collaborator, permission, repo, api, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: owner
desc: "usuario u organización propietaria del repositorio"
- name: repo
desc: "nombre del repositorio"
- name: username
desc: "nombre de usuario del colaborador a añadir"
- name: permission
desc: "nivel de permisos: 'read', 'write' o 'admin' (default: admin)"
output: "vacío — efectos observables a través de la API de Gitea"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_add_collaborator.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_add_collaborator.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Añadir colaborador con permiso admin (default)
gitea_add_collaborator "myorg" "my-app" "egutierrez"
# Añadir colaborador con permiso de solo lectura
gitea_add_collaborator "myorg" "my-app" "reviewer" "read"
```
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- Un 422 de la API indica que el usuario ya es colaborador — se trata como éxito silencioso.
- La función no produce salida a stdout; los mensajes informativos van a stderr.
- Nivel `admin` da acceso completo al repo incluyendo settings.
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# gitea_add_collaborator — Añade un colaborador a un repositorio Gitea
gitea_add_collaborator() {
local owner="$1"
local repo="$2"
local username="$3"
local permission="${4:-admin}"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_add_collaborator: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_add_collaborator: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$owner" || -z "$repo" || -z "$username" ]]; then
echo "gitea_add_collaborator: se requieren owner, repo y username" >&2
return 1
fi
local payload
payload=$(printf '{"permission":"%s"}' "$permission")
echo "gitea_add_collaborator: añadiendo '$username' a '$owner/$repo' con permiso '$permission'..." >&2
local response http_code
response=$(curl -s -w "\n%{http_code}" \
-X PUT \
-H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$payload" \
"${GITEA_URL}/api/v1/repos/${owner}/${repo}/collaborators/${username}")
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | head -n -1)
if [[ "$http_code" == "204" || "$http_code" == "200" ]]; then
echo "gitea_add_collaborator: '$username' añadido a '$owner/$repo'" >&2
return 0
fi
if [[ "$http_code" == "422" ]]; then
echo "gitea_add_collaborator: '$username' ya es colaborador de '$owner/$repo' (silencioso)" >&2
return 0
fi
echo "gitea_add_collaborator: error (HTTP ${http_code}): ${body}" >&2
return 1
}
+56
View File
@@ -0,0 +1,56 @@
---
name: gitea_create_repo
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_create_repo(owner: string, name: string, private: string, description: string) -> string"
description: "Crea un repositorio en Gitea para un owner. Intenta crearlo en org primero; si el owner no es una org (404/422), lo crea en el usuario autenticado. No falla fatalmente si el repo ya existe (409)."
tags: [gitea, git, repo, create, infra, api]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: owner
desc: "usuario u organización propietaria del repo"
- name: name
desc: "nombre del repositorio a crear"
- name: private
desc: "si el repo es privado, 'true' o 'false' (default: false)"
- name: description
desc: "descripción del repositorio (opcional)"
output: "JSON del repositorio creado según la API de Gitea"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_create_repo.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_create_repo.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Crear repo público en org o usuario
repo_json=$(gitea_create_repo "myorg" "my-app")
# Crear repo privado con descripción
repo_json=$(gitea_create_repo "myorg" "my-app" "true" "Mi aplicación principal")
# Extraer la URL del clon
clone_url=$(echo "$repo_json" | jq -r '.clone_url')
```
## Notas
- Requiere variables de entorno `GITEA_URL` y `GITEA_TOKEN` seteadas antes de invocar.
- El fallback org → usuario ocurre con HTTP 404 o 422 en el endpoint de orgs.
- Un 409 se reporta a stderr pero la función retorna 0 — el repo ya existe es una condición aceptable para idempotencia.
- Los mensajes informativos van a stderr; el JSON de respuesta va a stdout.
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# gitea_create_repo — Crea un repositorio en Gitea para un owner (org o usuario)
gitea_create_repo() {
local owner="$1"
local name="$2"
local private="${3:-false}"
local description="${4:-}"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_create_repo: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_create_repo: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$owner" || -z "$name" ]]; then
echo "gitea_create_repo: se requieren owner y name" >&2
return 1
fi
local payload
payload=$(printf '{"name":"%s","private":%s,"description":"%s","auto_init":false}' \
"$name" "$private" "$description")
echo "gitea_create_repo: intentando crear '$owner/$name' en org..." >&2
local response http_code
response=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$payload" \
"${GITEA_URL}/api/v1/orgs/${owner}/repos")
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
echo "gitea_create_repo: repo '$owner/$name' creado en org" >&2
echo "$body"
return 0
fi
if [[ "$http_code" == "409" ]]; then
echo "gitea_create_repo: repo '$owner/$name' ya existe (409)" >&2
echo "$body"
return 0
fi
if [[ "$http_code" == "404" || "$http_code" == "422" ]]; then
echo "gitea_create_repo: org no encontrada (${http_code}), intentando en usuario..." >&2
response=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$payload" \
"${GITEA_URL}/api/v1/user/repos")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
echo "gitea_create_repo: repo '$owner/$name' creado en usuario" >&2
echo "$body"
return 0
fi
if [[ "$http_code" == "409" ]]; then
echo "gitea_create_repo: repo '$owner/$name' ya existe (409)" >&2
echo "$body"
return 0
fi
echo "gitea_create_repo: error al crear en usuario (HTTP ${http_code}): ${body}" >&2
return 1
fi
echo "gitea_create_repo: error inesperado (HTTP ${http_code}): ${body}" >&2
return 1
}
+51
View File
@@ -0,0 +1,51 @@
---
name: gitea_list_repos
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_list_repos(owner: string) -> string"
description: "Lista repositorios de un owner en Gitea. Intenta listar como org primero; si falla, lista como usuario. Imprime una línea por repo en formato name<TAB>html_url<TAB>description."
tags: [gitea, git, repo, list, org, user, api, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: owner
desc: "nombre del usuario u organización cuyos repos se listan"
output: "una línea por repositorio con columnas separadas por tabulador: name, html_url, description"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_list_repos.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_list_repos.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Listar todos los repos de una org
gitea_list_repos "myorg"
# my-app https://git.example.com/myorg/my-app Mi aplicación principal
# infra https://git.example.com/myorg/infra
# Iterar sobre los repos
while IFS=$'\t' read -r name url desc; do
echo "Repo: $name$url"
done < <(gitea_list_repos "myorg")
```
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- Usa `jq` si está disponible; fallback con grep/sed en caso contrario.
- El límite es 50 repos por página. Para owners con más de 50 repos habría que implementar paginación.
- Los mensajes informativos van a stderr; los datos tabulados van a stdout.
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# gitea_list_repos — Lista repositorios de un owner (org o usuario) en Gitea
gitea_list_repos() {
local owner="$1"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_list_repos: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_list_repos: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$owner" ]]; then
echo "gitea_list_repos: se requiere owner" >&2
return 1
fi
echo "gitea_list_repos: listando repos de '$owner' (intentando org)..." >&2
local response http_code body
response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/orgs/${owner}/repos?limit=50")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
if [[ "$http_code" != "200" ]]; then
echo "gitea_list_repos: org no encontrada (${http_code}), intentando usuario..." >&2
response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/users/${owner}/repos?limit=50")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
if [[ "$http_code" != "200" ]]; then
echo "gitea_list_repos: error listando repos de usuario (HTTP ${http_code}): ${body}" >&2
return 1
fi
fi
# Formatear salida como name\thtml_url\tdescription
if command -v jq &>/dev/null; then
echo "$body" | jq -r '.[] | [.name, .html_url, (.description // "")] | @tsv'
else
# Fallback sin jq: extraer campos básicos con grep/sed
echo "$body" | grep -o '"name":"[^"]*"\|"html_url":"[^"]*"\|"description":"[^"]*"' \
| paste - - - | sed 's/"name":"//;s/"html_url":"//;s/"description":"//;s/"//g'
fi
}
@@ -0,0 +1,55 @@
---
name: gitea_push_directory
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_push_directory(directory: string, owner: string, repo: string, branch: string) -> void"
description: "Inicializa git en un directorio local y lo sube a un repositorio Gitea existente. Si el directorio ya tiene .git, actualiza el remote y pushea cambios pendientes. Protege registry.db añadiéndolo al .gitignore antes del commit."
tags: [gitea, git, push, directory, sync, infra, repo]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: directory
desc: "ruta absoluta o relativa al directorio local a subir"
- name: owner
desc: "usuario u organización propietaria del repositorio Gitea destino"
- name: repo
desc: "nombre del repositorio Gitea destino (debe existir previamente)"
- name: branch
desc: "rama en la que hacer push (default: main)"
output: "vacío — efectos observables en el repositorio Gitea destino"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_push_directory.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_push_directory.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Subir directorio a repo existente
gitea_push_directory "/home/lucas/myproject" "myorg" "my-app"
# Subir a rama específica
gitea_push_directory "/home/lucas/myproject" "myorg" "my-app" "develop"
```
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- El token se embebe en la URL del remote para autenticación (nunca se imprime a stdout/stderr, se enmascara con ***).
- Si `registry.db` existe en el directorio, se añade automáticamente al `.gitignore` local.
- Si el `.git` ya existe con un remote diferente, se redirige al repo indicado sin perder el historial local.
- Usa `--force-with-lease` para el primer push y fallback a push normal (para repos vacíos recién creados).
- El commit se firma con `agent@fn-registry` si no hay configuración git en el entorno.
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# gitea_push_directory — Inicializa git en un directorio y lo sube a un repo Gitea existente
gitea_push_directory() {
local directory="$1"
local owner="$2"
local repo="$3"
local branch="${4:-main}"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_push_directory: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_push_directory: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$directory" || -z "$owner" || -z "$repo" ]]; then
echo "gitea_push_directory: se requieren directory, owner y repo" >&2
return 1
fi
if [[ ! -d "$directory" ]]; then
echo "gitea_push_directory: directorio '$directory' no existe" >&2
return 1
fi
# Construir URL con credenciales embebidas para autenticación
local gitea_host
gitea_host=$(echo "$GITEA_URL" | sed 's|https\?://||')
local remote_url="https://${GITEA_TOKEN}@${gitea_host}/${owner}/${repo}.git"
local display_url="https://***@${gitea_host}/${owner}/${repo}.git"
echo "gitea_push_directory: procesando '$directory' → '$owner/$repo' (rama: $branch)..." >&2
# Añadir registry.db al .gitignore local si existe en el directorio
if [[ -f "$directory/registry.db" ]]; then
echo "gitea_push_directory: añadiendo registry.db al .gitignore..." >&2
if [[ ! -f "$directory/.gitignore" ]] || ! grep -qxF "registry.db" "$directory/.gitignore"; then
echo "registry.db" >> "$directory/.gitignore"
fi
fi
# Gestionar estado del repositorio git
if [[ -d "$directory/.git" ]]; then
local existing_remote
existing_remote=$(git -C "$directory" remote get-url origin 2>/dev/null || echo "")
if [[ -z "$existing_remote" ]]; then
echo "gitea_push_directory: añadiendo remote origin..." >&2
git -C "$directory" remote add origin "$remote_url"
else
# Comparar remote sin token para detectar si apunta al mismo repo
local clean_existing
clean_existing=$(echo "$existing_remote" | sed 's|https://[^@]*@||;s|https://||')
local clean_target="${gitea_host}/${owner}/${repo}.git"
if [[ "$clean_existing" != "$clean_target" ]]; then
echo "gitea_push_directory: remote apunta a otro destino ('$clean_existing'), actualizando..." >&2
git -C "$directory" remote set-url origin "$remote_url"
else
echo "gitea_push_directory: remote ya apunta al destino correcto, actualizando token..." >&2
git -C "$directory" remote set-url origin "$remote_url"
fi
fi
else
echo "gitea_push_directory: inicializando nuevo repositorio git..." >&2
git -C "$directory" init
git -C "$directory" remote add origin "$remote_url"
fi
# Configurar rama por defecto
git -C "$directory" checkout -B "$branch" 2>/dev/null || true
# Añadir y commitear cambios si los hay
git -C "$directory" add -A
local status
status=$(git -C "$directory" status --porcelain)
if [[ -n "$status" ]]; then
echo "gitea_push_directory: commiteando cambios..." >&2
git -C "$directory" -c user.email="agent@fn-registry" -c user.name="fn-registry agent" \
commit -m "chore: sync from fn-registry agent"
else
echo "gitea_push_directory: sin cambios pendientes, solo haciendo push..." >&2
fi
echo "gitea_push_directory: haciendo push a $display_url..." >&2
git -C "$directory" push --set-upstream origin "$branch" --force-with-lease 2>&1 \
| sed "s|${GITEA_TOKEN}|***|g" >&2 \
|| git -C "$directory" push --set-upstream origin "$branch" 2>&1 \
| sed "s|${GITEA_TOKEN}|***|g" >&2
echo "gitea_push_directory: push completado a '$owner/$repo' rama '$branch'" >&2
}
+40
View File
@@ -0,0 +1,40 @@
---
name: init_uv_venv
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "init_uv_venv([project_dir: string]) -> string"
description: "Crea un virtualenv Python con uv en el directorio dado si no existe. Fallback a python3 -m venv. Retorna la ruta del venv."
tags: [python, venv, uv, setup, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio del proyecto donde crear el venv (default: directorio actual)"
output: "ruta absoluta del venv creado o existente"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/init_uv_venv.sh"
---
## Ejemplo
```bash
source init_uv_venv.sh
venv=$(init_uv_venv /home/lucas/analysis/finanzas)
echo "Venv creado en: $venv"
# Idempotente — si ya existe, retorna la ruta sin recrear
venv=$(init_uv_venv .)
```
## Notas
Idempotente: si el venv ya existe con un python valido, retorna la ruta sin hacer nada. Prefiere uv por velocidad, usa python3 como fallback.
+35
View File
@@ -0,0 +1,35 @@
# init_uv_venv
# -------------
# Crea un venv con uv en el directorio especificado si no existe.
# Fallback a python -m venv si uv no esta disponible.
# Imprime la ruta del venv a stdout.
#
# USO (sourced):
# source init_uv_venv.sh
# venv_path=$(init_uv_venv /path/to/project)
init_uv_venv() {
local project_dir="${1:-.}"
local venv_path="${project_dir}/.venv"
if [ -d "$venv_path" ] && [ -f "$venv_path/bin/python" ]; then
echo "$venv_path"
return 0
fi
if command -v uv &>/dev/null; then
(cd "$project_dir" && uv venv) >/dev/null 2>&1
elif command -v python3 &>/dev/null; then
python3 -m venv "$venv_path"
else
echo "init_uv_venv: ni uv ni python3 disponibles" >&2
return 1
fi
if [ ! -f "$venv_path/bin/python" ]; then
echo "init_uv_venv: fallo al crear venv en $venv_path" >&2
return 1
fi
echo "$venv_path"
}
@@ -0,0 +1,63 @@
---
name: install_android_sdk
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_android_sdk() -> void"
description: "Descarga e instala Android SDK command-line tools y JDK 17 localmente (sin root/sudo) en $ANDROID_SDK_DIR (default: $HOME/android-sdk). Idempotente: detecta instalacion existente y sale sin hacer nada. Genera env.sh con JAVA_HOME, ANDROID_HOME y PATH listos para hacer source."
tags: [android, sdk, jdk, java, install, infra, mobile]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "sin salida estructurada; imprime progreso y resumen final con rutas de instalacion"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_android_sdk.sh"
---
## Ejemplo
```bash
# Instalacion en directorio por defecto ($HOME/android-sdk)
source install_android_sdk.sh
# Instalacion en directorio personalizado
ANDROID_SDK_DIR=/opt/android source install_android_sdk.sh
# Si ya esta instalado:
# Android SDK ya instalado en: /home/user/android-sdk
# Instalacion completa imprime:
# Descargando JDK 17...
# JDK 17 instalado: /home/user/android-sdk/jdk-17/jdk-17.0.x+y
# Descargando Android cmdline-tools...
# cmdline-tools instalados
# Aceptando licencias de Android SDK...
# Instalando platform-tools, platforms;android-34, build-tools;34.0.0...
#
# Android SDK instalado en: /home/user/android-sdk
# JDK 17: /home/user/android-sdk/jdk-17/jdk-17.0.x+y
# Para activar: source /home/user/android-sdk/env.sh
# Activar entorno en sesion actual
source ~/android-sdk/env.sh
```
## Notas
Requiere `curl` y `unzip` (disponibles en la mayoria de distros Linux). No requiere root ni sudo.
El JDK se descarga desde Adoptium (Eclipse Temurin) via su API oficial. La URL de cmdline-tools apunta a la version 11076708 (2024). Si Google actualiza la version, cambiar la URL con el nuevo numero de build.
La reorganizacion del zip es necesaria porque Google distribuye cmdline-tools con estructura `cmdline-tools/bin/...` pero sdkmanager espera estar en `cmdline-tools/latest/bin/sdkmanager` para que Android Studio y otras herramientas lo detecten correctamente.
El archivo `env.sh` generado en `$ANDROID_SDK_DIR/env.sh` contiene las variables de entorno necesarias (`JAVA_HOME`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `PATH`) y puede hacerse source desde `.bashrc`, `.zshrc` o desde scripts de CI.
Paquetes instalados: `platform-tools` (adb, fastboot), `platforms;android-34` (API 34), `build-tools;34.0.0`.
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# install_android_sdk — Descarga e instala Android SDK command-line tools y JDK 17
# localmente (sin root/sudo) en $ANDROID_SDK_DIR (default: $HOME/android-sdk).
set -euo pipefail
install_android_sdk() {
local sdk_dir="${ANDROID_SDK_DIR:-$HOME/android-sdk}"
local tmp_dir
tmp_dir="$(mktemp -d)"
# Limpia temporales al salir
trap 'rm -rf "$tmp_dir"' EXIT
# 1. Verifica si ya está instalado
if [[ -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
if JAVA_HOME="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1)" \
"$sdk_dir/cmdline-tools/latest/bin/sdkmanager" --version &>/dev/null; then
echo "Android SDK ya instalado en: $sdk_dir"
return 0
fi
fi
mkdir -p "$sdk_dir"
# 2. Descarga JDK 17 si no existe
local jdk_dir
jdk_dir="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1 || true)"
if [[ -z "$jdk_dir" ]]; then
echo "Descargando JDK 17..."
local jdk_tar="$tmp_dir/jdk17.tar.gz"
local jdk_url="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jdk/hotspot/normal/eclipse"
if ! curl -fL --progress-bar -o "$jdk_tar" "$jdk_url"; then
echo "ERROR: fallo al descargar JDK 17 desde $jdk_url" >&2
return 1
fi
mkdir -p "$sdk_dir/jdk-17"
echo "Extrayendo JDK 17..."
if ! tar -xzf "$jdk_tar" -C "$sdk_dir/jdk-17"; then
echo "ERROR: fallo al extraer JDK 17" >&2
return 1
fi
jdk_dir="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1 || true)"
if [[ -z "$jdk_dir" ]]; then
echo "ERROR: no se encontro directorio jdk-17* tras la extraccion" >&2
return 1
fi
if ! JAVA_HOME="$jdk_dir" "$jdk_dir/bin/java" -version &>/dev/null; then
echo "ERROR: java -version fallo tras instalar JDK" >&2
return 1
fi
echo "JDK 17 instalado: $jdk_dir"
else
echo "JDK 17 ya presente: $jdk_dir"
fi
export JAVA_HOME="$jdk_dir"
# 3. Descarga Android cmdline-tools si no existen
if [[ ! -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
echo "Descargando Android cmdline-tools..."
local tools_zip="$tmp_dir/cmdline-tools.zip"
local tools_url="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
if ! curl -fL --progress-bar -o "$tools_zip" "$tools_url"; then
echo "ERROR: fallo al descargar Android cmdline-tools desde $tools_url" >&2
return 1
fi
local tools_tmp="$tmp_dir/cmdline-tools-extracted"
mkdir -p "$tools_tmp"
echo "Extrayendo cmdline-tools..."
if ! unzip -q "$tools_zip" -d "$tools_tmp"; then
echo "ERROR: fallo al extraer cmdline-tools" >&2
return 1
fi
# La estructura del zip es cmdline-tools/bin/..., reorganizar a cmdline-tools/latest/
mkdir -p "$sdk_dir/cmdline-tools"
if [[ -d "$tools_tmp/cmdline-tools" ]]; then
mv "$tools_tmp/cmdline-tools" "$sdk_dir/cmdline-tools/latest"
else
echo "ERROR: estructura inesperada en el zip de cmdline-tools" >&2
return 1
fi
if [[ ! -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
echo "ERROR: sdkmanager no encontrado tras extraer cmdline-tools" >&2
return 1
fi
echo "cmdline-tools instalados"
else
echo "cmdline-tools ya presentes"
fi
local sdkmanager="$sdk_dir/cmdline-tools/latest/bin/sdkmanager"
export ANDROID_HOME="$sdk_dir"
export ANDROID_SDK_ROOT="$sdk_dir"
export PATH="$JAVA_HOME/bin:$sdk_dir/cmdline-tools/latest/bin:$sdk_dir/platform-tools:$PATH"
# 4. Acepta licencias e instala paquetes necesarios
echo "Aceptando licencias de Android SDK..."
if ! yes | "$sdkmanager" --licenses; then
echo "ERROR: fallo al aceptar licencias de Android SDK" >&2
return 1
fi
echo "Instalando platform-tools, platforms;android-34, build-tools;34.0.0..."
if ! "$sdkmanager" "platform-tools" "platforms;android-34" "build-tools;34.0.0"; then
echo "ERROR: fallo al instalar paquetes de Android SDK" >&2
return 1
fi
# 5. Genera archivo de entorno
local env_file="$sdk_dir/env.sh"
cat > "$env_file" <<EOF
export JAVA_HOME="$JAVA_HOME"
export ANDROID_HOME="$sdk_dir"
export ANDROID_SDK_ROOT="$sdk_dir"
export PATH="\$JAVA_HOME/bin:$sdk_dir/cmdline-tools/latest/bin:$sdk_dir/platform-tools:\$PATH"
EOF
# 6. Resumen final
echo ""
echo "Android SDK instalado en: $sdk_dir"
echo "JDK 17: $JAVA_HOME"
echo "Para activar: source $sdk_dir/env.sh"
}
install_android_sdk
+37
View File
@@ -0,0 +1,37 @@
---
name: install_cpp_deps
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_cpp_deps() -> void"
description: "Verifica e instala las dependencias de sistema necesarias para compilar C++ con ImGui (cmake, g++, glfw, mesa)"
tags: [cpp, dependencies, setup, cmake, imgui]
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/install_cpp_deps.sh"
params: []
output: "Instala paquetes faltantes via apt o confirma que todo esta instalado"
---
# install_cpp_deps
Verifica las dependencias necesarias para el build C++:
- `cmake` — sistema de build
- `g++` / `build-essential` — compilador
- `libglfw3-dev` — windowing (GLFW)
- `libgl1-mesa-dev` — OpenGL headers
Tambien reporta si `mingw-w64` esta disponible para cross-compile a Windows.
```bash
fn run install_cpp_deps
```
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[install_cpp_deps] Checking C++ build dependencies..."
MISSING=()
if ! command -v cmake &>/dev/null; then
MISSING+=(cmake)
else
echo " cmake: $(cmake --version | head -1)"
fi
if ! command -v g++ &>/dev/null; then
MISSING+=(g++ build-essential)
else
echo " g++: $(g++ --version | head -1)"
fi
if ! dpkg -s libglfw3-dev &>/dev/null 2>&1; then
MISSING+=(libglfw3-dev)
else
echo " libglfw3-dev: installed"
fi
if ! dpkg -s libgl1-mesa-dev &>/dev/null 2>&1; then
MISSING+=(libgl1-mesa-dev)
else
echo " libgl1-mesa-dev: installed"
fi
# Optional: mingw for cross-compile
if command -v x86_64-w64-mingw32-g++ &>/dev/null; then
echo " mingw-w64: $(x86_64-w64-mingw32-g++ --version | head -1)"
else
echo " mingw-w64: not installed (optional, for Windows cross-compile)"
fi
if [ ${#MISSING[@]} -eq 0 ]; then
echo "[install_cpp_deps] All dependencies satisfied."
exit 0
fi
echo ""
echo "[install_cpp_deps] Missing packages: ${MISSING[*]}"
echo "[install_cpp_deps] Installing..."
sudo apt-get update -qq
sudo apt-get install -y -qq "${MISSING[@]}"
echo "[install_cpp_deps] Done."
+40
View File
@@ -0,0 +1,40 @@
---
name: install_mantine
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_mantine(project_dir: string) -> void"
description: "Instala Mantine UI con todas sus dependencias (@mantine/core, hooks, charts, notifications, form) y PostCSS en un proyecto frontend. Detecta package manager por lockfile. Genera postcss.config.cjs si no existe. Idempotente."
tags: [mantine, frontend, install, react, ui, postcss]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio del proyecto frontend con package.json"
output: "sin salida; muestra progreso de instalación"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_mantine.sh"
---
## Ejemplo
```bash
# Instalar Mantine en un proyecto con pnpm
source install_mantine.sh
install_mantine ./apps/rapid_dashboards/frontend
# Uso directo
bash install_mantine.sh ./frontend
```
## Notas
Detecta el package manager por lockfile: pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm. Instala las dependencias core de Mantine v7+ y el stack PostCSS necesario. Si postcss.config.cjs ya existe no lo sobreescribe.
+97
View File
@@ -0,0 +1,97 @@
# install_mantine
# ---------------
# Instala dependencias de Mantine UI en un proyecto frontend.
# Detecta package manager por lockfile (pnpm > yarn > npm).
# Genera postcss.config.cjs si no existe.
# Idempotente: no reinstala si ya estan presentes.
#
# USO (sourced):
# source install_mantine.sh
# install_mantine /path/to/frontend
#
# USO (directo):
# bash install_mantine.sh /path/to/frontend
install_mantine() {
local project_dir="$1"
if [ -z "$project_dir" ]; then
echo "install_mantine: se requiere project_dir" >&2
return 1
fi
if [ ! -f "$project_dir/package.json" ]; then
echo "install_mantine: no existe package.json en $project_dir" >&2
return 1
fi
# Detectar package manager
local pm="npm"
local add_cmd="install"
local add_dev_flag="--save-dev"
if [ -f "$project_dir/pnpm-lock.yaml" ] || [ -f "$project_dir/pnpm-workspace.yaml" ]; then
pm="pnpm"
add_cmd="add"
add_dev_flag="-D"
elif [ -f "$project_dir/yarn.lock" ]; then
pm="yarn"
add_cmd="add"
add_dev_flag="--dev"
elif [ -f "$project_dir/package-lock.json" ]; then
pm="npm"
add_cmd="install"
add_dev_flag="--save-dev"
fi
echo "Detectado package manager: $pm"
# Dependencias runtime
local runtime_deps="@mantine/core @mantine/hooks @mantine/charts @mantine/notifications @mantine/form"
echo "Instalando dependencias Mantine..."
(cd "$project_dir" && $pm $add_cmd $runtime_deps 2>&1)
if [ $? -ne 0 ]; then
echo "install_mantine: fallo instalando dependencias runtime" >&2
return 1
fi
# Dependencias PostCSS (dev)
local dev_deps="postcss postcss-preset-mantine postcss-simple-vars"
echo "Instalando dependencias PostCSS..."
(cd "$project_dir" && $pm $add_cmd $add_dev_flag $dev_deps 2>&1)
if [ $? -ne 0 ]; then
echo "install_mantine: fallo instalando dependencias PostCSS" >&2
return 1
fi
# Generar postcss.config.cjs si no existe
if [ ! -f "$project_dir/postcss.config.cjs" ]; then
echo "Generando postcss.config.cjs..."
cat > "$project_dir/postcss.config.cjs" << 'POSTCSS'
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
POSTCSS
echo "postcss.config.cjs creado"
else
echo "postcss.config.cjs ya existe, no se sobreescribe"
fi
echo "Mantine instalado correctamente en $project_dir"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
install_mantine "$@"
fi
+36
View File
@@ -0,0 +1,36 @@
---
name: install_nbconvert
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_nbconvert(project_dir: string) -> void"
description: "Instala nbconvert y playwright con chromium en un proyecto uv existente. Idempotente: uv add no reinstala si los paquetes ya estan presentes."
tags: [jupyter, nbconvert, pdf, export, playwright, python, uv]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio del proyecto con venv existente"
output: "sin salida"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_nbconvert.sh"
---
## Ejemplo
```bash
source install_nbconvert.sh
install_nbconvert /home/lucas/analysis/finanzas
```
## Notas
Requiere que el venv ya exista (usa `init_uv_venv` antes). La instalacion de chromium via `uv run playwright install chromium` puede tardar la primera vez. La salida de playwright se suprime si tiene exito — solo se muestra si hay un error.
+32
View File
@@ -0,0 +1,32 @@
# install_nbconvert
# ------------------
# Instala nbconvert y playwright con chromium en un proyecto uv existente.
# Idempotente: uv add no reinstala si los paquetes ya estan presentes.
#
# USO (sourced):
# source install_nbconvert.sh
# install_nbconvert /path/to/project
install_nbconvert() {
local project_dir="$1"
if [ -z "$project_dir" ]; then
echo "install_nbconvert: se requiere project_dir" >&2
return 1
fi
if [ ! -d "$project_dir/.venv" ]; then
echo "install_nbconvert: no existe .venv en $project_dir — ejecuta init_uv_venv primero" >&2
return 1
fi
# Instalar nbconvert y playwright via uv add
(cd "$project_dir" && uv add nbconvert playwright 2>&1)
# Instalar chromium — capturar output, solo mostrar si hay error
local playwright_output
if ! playwright_output=$(cd "$project_dir" && uv run playwright install chromium 2>&1); then
echo "$playwright_output" >&2
return 1
fi
}
+39
View File
@@ -0,0 +1,39 @@
---
name: install_nordvpn
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_nordvpn() -> void"
description: "Instala NordVPN CLI en Ubuntu/Debian (incluido WSL2). Configura repositorio oficial, instala paquete y habilita servicio nordvpnd. Idempotente."
tags: [vpn, nordvpn, install, infra, wsl2]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "sin salida; muestra estado de instalación"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_nordvpn.sh"
---
## Ejemplo
```bash
source install_nordvpn.sh
install_nordvpn
# nordvpn ya instalado: NordVPN Version 3.x.x
# — o —
# Instalando NordVPN CLI...
# NordVPN instalado: NordVPN Version 3.x.x
# NOTA: ejecuta 'nordvpn login' para autenticarte
```
## Notas
Usa el script de instalacion oficial de NordVPN. En WSL2 sin systemd, levanta nordvpnd manualmente. Agrega el usuario al grupo nordvpn para evitar sudo en comandos posteriores. Despues de instalar, se requiere `nordvpn login` para autenticarse.
+41
View File
@@ -0,0 +1,41 @@
# install_nordvpn
# ---------------
# Instala NordVPN CLI en Ubuntu/Debian (incluido WSL2).
# Configura el repositorio oficial, instala el paquete y habilita el servicio.
# Si ya esta instalado, no hace nada.
#
# USO (sourced):
# source install_nordvpn.sh
# install_nordvpn
install_nordvpn() {
if command -v nordvpn &>/dev/null; then
echo "nordvpn ya instalado: $(nordvpn version 2>/dev/null)"
return 0
fi
echo "Instalando NordVPN CLI..."
# Descargar e instalar via script oficial
sh <(curl -sSf https://downloads.nordcdn.com/apps/linux/install.sh) 2>&1
if ! command -v nordvpn &>/dev/null; then
echo "install_nordvpn: fallo la instalacion" >&2
return 1
fi
# Agregar usuario al grupo nordvpn para evitar sudo
sudo usermod -aG nordvpn "$USER" 2>/dev/null || true
# Habilitar servicio (systemd o manual para WSL2)
if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
sudo systemctl enable --now nordvpnd 2>/dev/null || true
else
# WSL2 sin systemd — levantar daemon manualmente
sudo nordvpnd &>/dev/null &
sleep 2
fi
echo "NordVPN instalado: $(nordvpn version 2>/dev/null)"
echo "NOTA: ejecuta 'nordvpn login' para autenticarte"
}
+46
View File
@@ -0,0 +1,46 @@
---
name: nordvpn_connect
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "nordvpn_connect(country?: string, city?: string) -> json"
description: "Conecta a NordVPN por pais, ciudad o servidor especifico. Sin argumentos conecta al mejor servidor disponible. Devuelve JSON con resultado."
tags: [vpn, nordvpn, connect, infra, network]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: country
desc: "país de destino (opcional; default: auto)"
- name: city
desc: "ciudad de destino (opcional; default: auto)"
output: "JSON con ok, server, country, city"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/nordvpn_connect.sh"
---
## Ejemplo
```bash
source nordvpn_connect.sh
nordvpn_connect
# {"ok":true,"server":"us1234.nordvpn.com","country":"auto","city":"auto"}
nordvpn_connect Spain
# {"ok":true,"server":"es42.nordvpn.com","country":"Spain","city":"auto"}
nordvpn_connect Spain Madrid
# {"ok":true,"server":"es15.nordvpn.com","country":"Spain","city":"Madrid"}
```
## Notas
Requiere NordVPN CLI instalado y autenticado (`nordvpn login`). La salida JSON facilita composicion con otros scripts y pipelines. Si ya hay una conexion activa, NordVPN reconecta automaticamente al nuevo destino.
+39
View File
@@ -0,0 +1,39 @@
# nordvpn_connect
# ---------------
# Conecta a NordVPN. Acepta pais, ciudad o servidor especifico.
# Sin argumentos conecta al mejor servidor disponible.
# Imprime JSON con el resultado de la conexion.
#
# USO (sourced):
# source nordvpn_connect.sh
# nordvpn_connect # mejor servidor
# nordvpn_connect Spain # por pais
# nordvpn_connect Spain Madrid # por ciudad
# nordvpn_connect Spain '#42' # servidor especifico
nordvpn_connect() {
local country="${1:-}"
local city="${2:-}"
if ! command -v nordvpn &>/dev/null; then
echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
return 1
fi
local args=()
[ -n "$country" ] && args+=("$country")
[ -n "$city" ] && args+=("$city")
local output
output=$(nordvpn connect "${args[@]}" 2>&1)
local rc=$?
if [ $rc -eq 0 ] && echo "$output" | grep -qi "connected"; then
local server
server=$(echo "$output" | grep -oP '(?<=to )\S+' | head -1)
echo "{\"ok\":true,\"server\":\"${server}\",\"country\":\"${country:-auto}\",\"city\":\"${city:-auto}\"}"
else
echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
return 1
fi
}
@@ -0,0 +1,36 @@
---
name: nordvpn_disconnect
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "nordvpn_disconnect() -> json"
description: "Desconecta de NordVPN. Idempotente — si no hay conexion activa retorna ok. Devuelve JSON con resultado."
tags: [vpn, nordvpn, disconnect, infra, network]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "JSON con ok y status"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/nordvpn_disconnect.sh"
---
## Ejemplo
```bash
source nordvpn_disconnect.sh
nordvpn_disconnect
# {"ok":true,"status":"disconnected"}
```
## Notas
Idempotente: si no hay conexion activa, retorna ok sin error. Requiere NordVPN CLI instalado.
@@ -0,0 +1,26 @@
# nordvpn_disconnect
# ------------------
# Desconecta de NordVPN. Idempotente — si no hay conexion activa, retorna ok.
# Imprime JSON con el resultado.
#
# USO (sourced):
# source nordvpn_disconnect.sh
# nordvpn_disconnect
nordvpn_disconnect() {
if ! command -v nordvpn &>/dev/null; then
echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
return 1
fi
local output
output=$(nordvpn disconnect 2>&1)
local rc=$?
if [ $rc -eq 0 ] || echo "$output" | grep -qi "not connected\|disconnected"; then
echo '{"ok":true,"status":"disconnected"}'
else
echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
return 1
fi
}
+41
View File
@@ -0,0 +1,41 @@
---
name: nordvpn_get_ip
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "nordvpn_get_ip() -> json"
description: "Obtiene IP publica actual con fallback entre multiples servicios. Indica si la conexion VPN esta activa y el servidor usado."
tags: [vpn, nordvpn, ip, infra, network, verification]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "JSON con ok, ip, vpn_connected, vpn_server, source"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/nordvpn_get_ip.sh"
---
## Ejemplo
```bash
source nordvpn_get_ip.sh
# Con VPN activa:
nordvpn_get_ip
# {"ok":true,"ip":"185.x.x.x","vpn_connected":true,"vpn_server":"es42.nordvpn.com","source":"https://api.ipify.org"}
# Sin VPN:
nordvpn_get_ip
# {"ok":true,"ip":"88.x.x.x","vpn_connected":false,"vpn_server":"","source":"https://api.ipify.org"}
```
## Notas
Usa ipify.org como servicio primario con fallback a ifconfig.me e icanhazip.com. Timeout de 5 segundos por servicio. Util para verificar que el tunel VPN esta activo antes de ejecutar operaciones sensibles a la IP.
+42
View File
@@ -0,0 +1,42 @@
# nordvpn_get_ip
# --------------
# Obtiene la IP publica actual para verificar que el tunel VPN funciona.
# Usa multiples servicios como fallback.
#
# USO (sourced):
# source nordvpn_get_ip.sh
# nordvpn_get_ip
nordvpn_get_ip() {
local ip=""
local source=""
# Intentar multiples servicios
for svc in "https://api.ipify.org" "https://ifconfig.me" "https://icanhazip.com"; do
ip=$(curl -s --max-time 5 "$svc" 2>/dev/null)
if echo "$ip" | grep -qP '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'; then
source="$svc"
break
fi
ip=""
done
if [ -z "$ip" ]; then
echo '{"ok":false,"error":"no se pudo obtener IP publica"}' >&2
return 1
fi
# Si nordvpn esta disponible, incluir info de conexion
local connected="false"
local vpn_server=""
if command -v nordvpn &>/dev/null; then
local status_output
status_output=$(nordvpn status 2>/dev/null)
if echo "$status_output" | grep -qi "connected"; then
connected="true"
vpn_server=$(echo "$status_output" | grep -iP "hostname|server" | head -1 | sed 's/.*: *//')
fi
fi
echo "{\"ok\":true,\"ip\":\"$ip\",\"vpn_connected\":$connected,\"vpn_server\":\"$vpn_server\",\"source\":\"$source\"}"
}
@@ -0,0 +1,41 @@
---
name: nordvpn_list_cities
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "nordvpn_list_cities(country: string) -> json"
description: "Lista ciudades disponibles de un pais en NordVPN como array JSON ordenado."
tags: [vpn, nordvpn, cities, infra, network]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: country
desc: "nombre del país en NordVPN (ej: Spain, United_States)"
output: "JSON con ok, country, count, cities (array de strings)"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/nordvpn_list_cities.sh"
---
## Ejemplo
```bash
source nordvpn_list_cities.sh
nordvpn_list_cities Spain
# {"ok":true,"country":"Spain","count":2,"cities":["Barcelona","Madrid"]}
nordvpn_list_cities "United_States"
# {"ok":true,"country":"United_States","count":15,"cities":["Atlanta","Buffalo",...]}
```
## Notas
El nombre de pais debe coincidir con lo que devuelve `nordvpn countries`. Usa underscores para paises compuestos (ej: United_States). Las ciudades se devuelven con espacios.
@@ -0,0 +1,38 @@
# nordvpn_list_cities
# -------------------
# Lista las ciudades disponibles de un pais en NordVPN como array JSON.
#
# USO (sourced):
# source nordvpn_list_cities.sh
# nordvpn_list_cities Spain
nordvpn_list_cities() {
local country="${1:?nordvpn_list_cities: se requiere pais como argumento}"
if ! command -v nordvpn &>/dev/null; then
echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
return 1
fi
local output
output=$(nordvpn cities "$country" 2>&1)
local rc=$?
if [ $rc -ne 0 ]; then
echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
return 1
fi
echo "$output" | python3 -c '
import sys, json, re
country = "'"$country"'"
text = sys.stdin.read()
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
text = re.sub(r"[\t\r]", " ", text)
cities = [c.strip().replace("_", " ") for c in re.split(r"[,\n]+", text) if c.strip() and c.strip() != "-"]
cities = [c for c in cities if len(c) > 1]
cities.sort()
print(json.dumps({"ok": True, "country": country, "count": len(cities), "cities": cities}))
'
}
@@ -0,0 +1,36 @@
---
name: nordvpn_list_countries
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "nordvpn_list_countries() -> json"
description: "Lista paises disponibles en NordVPN como array JSON ordenado alfabeticamente."
tags: [vpn, nordvpn, countries, infra, network]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "JSON con ok, count, countries (array de strings ordenado alfabéticamente)"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/nordvpn_list_countries.sh"
---
## Ejemplo
```bash
source nordvpn_list_countries.sh
nordvpn_list_countries
# {"ok":true,"count":60,"countries":["Albania","Argentina","Australia",...,"United States","Vietnam"]}
```
## Notas
Parsea la salida de `nordvpn countries` eliminando codigos ANSI y normalizando separadores. Los nombres de paises se devuelven con espacios en vez de underscores.
@@ -0,0 +1,36 @@
# nordvpn_list_countries
# ----------------------
# Lista los paises disponibles en NordVPN como array JSON.
#
# USO (sourced):
# source nordvpn_list_countries.sh
# nordvpn_list_countries
nordvpn_list_countries() {
if ! command -v nordvpn &>/dev/null; then
echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
return 1
fi
local output
output=$(nordvpn countries 2>&1)
local rc=$?
if [ $rc -ne 0 ]; then
echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
return 1
fi
echo "$output" | python3 -c '
import sys, json, re
text = sys.stdin.read()
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
text = re.sub(r"[\t\r]", " ", text)
# Split by comma, whitespace, or newline and clean
countries = [c.strip().replace("_", " ") for c in re.split(r"[,\n]+", text) if c.strip() and c.strip() != "-"]
countries = [c for c in countries if len(c) > 1]
countries.sort()
print(json.dumps({"ok": True, "count": len(countries), "countries": countries}))
'
}
@@ -0,0 +1,41 @@
---
name: nordvpn_set_protocol
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "nordvpn_set_protocol(protocol: string) -> json"
description: "Cambia el protocolo de NordVPN entre NordLynx (WireGuard) y OpenVPN. NordLynx recomendado por velocidad."
tags: [vpn, nordvpn, protocol, nordlynx, wireguard, openvpn, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: protocol
desc: "protocolo a usar: NordLynx (WireGuard) u OpenVPN"
output: "JSON con ok y protocol confirmado"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/nordvpn_set_protocol.sh"
---
## Ejemplo
```bash
source nordvpn_set_protocol.sh
nordvpn_set_protocol NordLynx
# {"ok":true,"protocol":"NordLynx"}
nordvpn_set_protocol OpenVPN
# {"ok":true,"protocol":"OpenVPN"}
```
## Notas
NordLynx es WireGuard wrapeado por NordVPN — mas rapido y moderno. OpenVPN es mas compatible con redes restrictivas. El cambio de protocolo requiere reconectar si hay una conexion activa.
@@ -0,0 +1,38 @@
# nordvpn_set_protocol
# --------------------
# Cambia el protocolo de NordVPN (NordLynx o OpenVPN).
# NordLynx = WireGuard (recomendado por velocidad).
#
# USO (sourced):
# source nordvpn_set_protocol.sh
# nordvpn_set_protocol NordLynx
# nordvpn_set_protocol OpenVPN
nordvpn_set_protocol() {
local protocol="${1:?nordvpn_set_protocol: se requiere protocolo (NordLynx|OpenVPN)}"
if ! command -v nordvpn &>/dev/null; then
echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
return 1
fi
case "$protocol" in
NordLynx|nordlynx|NORDLYNX) protocol="NordLynx" ;;
OpenVPN|openvpn|OPENVPN) protocol="OpenVPN" ;;
*)
echo "{\"ok\":false,\"error\":\"protocolo invalido: $protocol (usar NordLynx o OpenVPN)\"}"
return 1
;;
esac
local output
output=$(nordvpn set protocol "$protocol" 2>&1)
local rc=$?
if [ $rc -eq 0 ] || echo "$output" | grep -qi "already set\|successfully"; then
echo "{\"ok\":true,\"protocol\":\"$protocol\"}"
else
echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
return 1
fi
}
+39
View File
@@ -0,0 +1,39 @@
---
name: nordvpn_status
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "nordvpn_status() -> json"
description: "Obtiene estado actual de NordVPN como JSON estructurado. Incluye servidor, IP, pais, protocolo y estado de conexion."
tags: [vpn, nordvpn, status, infra, network]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "JSON con estado de VPN: ok, connected, status, hostname, ip, country, city, etc"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/nordvpn_status.sh"
---
## Ejemplo
```bash
source nordvpn_status.sh
nordvpn_status
# {"ok":true,"connected":true,"status":"Connected","hostname":"es42.nordvpn.com","ip":"185.x.x.x","country":"Spain","city":"Madrid","current_technology":"NordLynx","current_protocol":"nordlynx","transfer":"1.2 MiB received, 500 KiB sent","uptime":"5 minutes 32 seconds"}
# Desconectado:
# {"ok":true,"connected":false,"status":"Disconnected"}
```
## Notas
Parsea la salida clave-valor de `nordvpn status` eliminando codigos ANSI. Los campos disponibles dependen del estado de conexion — cuando esta desconectado solo devuelve status y connected.
+43
View File
@@ -0,0 +1,43 @@
# nordvpn_status
# --------------
# Obtiene el estado actual de NordVPN como JSON estructurado.
# Parsea la salida clave-valor de `nordvpn status` a campos JSON.
#
# USO (sourced):
# source nordvpn_status.sh
# nordvpn_status
nordvpn_status() {
if ! command -v nordvpn &>/dev/null; then
echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
return 1
fi
local output
output=$(nordvpn status 2>&1)
local rc=$?
if [ $rc -ne 0 ]; then
echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
return 1
fi
# Parsear output clave: valor a JSON con python3
echo "$output" | python3 -c '
import sys, json, re
lines = sys.stdin.read().strip().split("\n")
data = {"ok": True}
for line in lines:
line = re.sub(r"\x1b\[[0-9;]*m", "", line).strip()
line = line.lstrip("- ")
if ":" in line:
key, _, val = line.partition(":")
key = key.strip().lower().replace(" ", "_")
val = val.strip()
if key == "status":
data["connected"] = val.lower() == "connected"
data[key] = val
print(json.dumps(data))
'
}
+45
View File
@@ -0,0 +1,45 @@
---
name: notebook_to_pdf
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "notebook_to_pdf(project_dir: string, [pattern: string], [output_dir: string]) -> string"
description: "Convierte notebooks Jupyter a PDF usando nbconvert webpdf con chromium. Lista los PDFs generados al finalizar."
tags: [jupyter, notebook, pdf, export, nbconvert, playwright]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio raíz del proyecto con venv y notebooks"
- name: pattern
desc: "glob de notebooks a convertir (default: notebooks/*.ipynb)"
- name: output_dir
desc: "directorio destino para PDFs relativo a project_dir (default: notebooks/pdf/)"
output: "lista de PDFs generados con sus rutas"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/notebook_to_pdf.sh"
---
## Ejemplo
```bash
source notebook_to_pdf.sh
# Con defaults (notebooks/*.ipynb -> notebooks/pdf/)
notebook_to_pdf /home/lucas/analysis/finanzas
# Con pattern y output_dir custom
notebook_to_pdf /home/lucas/analysis/finanzas "notebooks/01_*.ipynb" "exports/pdf/"
```
## Notas
Requiere nbconvert y playwright con chromium instalados (usa `install_nbconvert` antes). Usa el venv del proyecto directamente (`.venv/bin/jupyter`). El output_dir es relativo a project_dir. Imprime los PDFs generados con sus rutas al finalizar. Falla si no se genera ningun PDF.
+59
View File
@@ -0,0 +1,59 @@
# notebook_to_pdf
# ----------------
# Convierte notebooks Jupyter a PDF usando nbconvert webpdf.
# Requiere nbconvert y playwright con chromium instalados.
#
# USO (sourced):
# source notebook_to_pdf.sh
# notebook_to_pdf /path/to/project
# notebook_to_pdf /path/to/project "notebooks/*.ipynb" "notebooks/pdf/"
notebook_to_pdf() {
local project_dir="$1"
local pattern="${2:-notebooks/*.ipynb}"
local output_dir="${3:-notebooks/pdf/}"
if [ -z "$project_dir" ]; then
echo "notebook_to_pdf: se requiere project_dir" >&2
return 1
fi
if [ ! -d "$project_dir/.venv" ]; then
echo "notebook_to_pdf: no existe .venv en $project_dir" >&2
return 1
fi
# Crear directorio de salida si no existe
mkdir -p "$project_dir/$output_dir"
# Convertir notebooks a PDF con nbconvert webpdf
# nbconvert puede retornar exit != 0 por warnings de validacion JSON
# que no impiden la generacion del PDF, asi que ignoramos el exit code
# y verificamos que los PDFs se hayan generado
local nbconvert_output
nbconvert_output=$(cd "$project_dir" && \
.venv/bin/jupyter nbconvert \
--to webpdf \
--allow-chromium-download \
--output-dir="$output_dir" \
$pattern 2>&1) || true
echo "$nbconvert_output"
# Listar PDFs generados
echo ""
echo "PDFs generados en ${project_dir}/${output_dir}:"
local pdf_count=0
while IFS= read -r -d '' pdf; do
echo " $pdf"
pdf_count=$((pdf_count + 1))
done < <(find "$project_dir/$output_dir" -name "*.pdf" -print0 2>/dev/null)
if [ "$pdf_count" -eq 0 ]; then
echo " (ninguno encontrado — nbconvert pudo haber fallado)" >&2
echo "$nbconvert_output" >&2
return 1
fi
echo " Total: $pdf_count PDFs"
}
+36
View File
@@ -0,0 +1,36 @@
---
name: pass_delete
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "pass_delete(entry: string) -> void"
description: "Elimina un secreto del password store (pass)."
tags: [pass, secret, credential, delete]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: entry
desc: "ruta de entrada en el password store (ej: agentes/token)"
output: "sin salida"
tested: true
tests: ["elimina entrada de test", "falla con entrada inexistente"]
test_file_path: "bash/functions/infra/pass_test.sh"
file_path: "bash/functions/infra/pass_delete.sh"
---
## Ejemplo
```bash
source pass_delete.sh
pass_delete agentes/viejo-token
```
## Notas
Usa `pass rm -f` para eliminar sin prompt de confirmacion.
+22
View File
@@ -0,0 +1,22 @@
# pass_delete
# -----------
# Elimina un secreto del password store.
# Sale con exit code 1 si la entrada no existe o pass falla.
#
# USO (sourced):
# source pass_delete.sh
# pass_delete agentes/viejo-token
pass_delete() {
local entry="$1"
if [ -z "$entry" ]; then
echo "pass_delete: se requiere nombre de entrada" >&2
return 1
fi
if ! pass rm -f "$entry" >/dev/null 2>&1; then
echo "pass_delete: fallo al eliminar '$entry'" >&2
return 1
fi
}
+39
View File
@@ -0,0 +1,39 @@
---
name: pass_generate
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "pass_generate(entry: string, [length: int]) -> string"
description: "Genera un password aleatorio, lo almacena en el password store e imprime el valor generado."
tags: [pass, secret, credential, generate, random]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: entry
desc: "ruta de entrada en el password store"
- name: length
desc: "longitud del password (default: 24 caracteres)"
output: "password generado en texto plano"
tested: true
tests: ["genera password de longitud especifica", "default 24 chars"]
test_file_path: "bash/functions/infra/pass_test.sh"
file_path: "bash/functions/infra/pass_generate.sh"
---
## Ejemplo
```bash
source pass_generate.sh
new_pass=$(pass_generate agentes/nuevo-servicio 32)
echo "password generado: $new_pass"
```
## Notas
Usa `pass generate -f -n` (force overwrite, no symbols). Default 24 caracteres alfanumericos.
+31
View File
@@ -0,0 +1,31 @@
# pass_generate
# -------------
# Genera un password aleatorio y lo almacena en el password store.
# Imprime el password generado a stdout.
# Sale con exit code 1 si pass falla.
#
# USO (sourced):
# source pass_generate.sh
# pass_generate agentes/nuevo-token 32
# pass_generate agentes/api-key # default 24 chars
pass_generate() {
local entry="$1"
local length="${2:-24}"
if [ -z "$entry" ]; then
echo "pass_generate: se requiere nombre de entrada" >&2
return 1
fi
local output
output=$(pass generate -f -n "$entry" "$length" 2>&1)
if [ $? -ne 0 ]; then
echo "pass_generate: fallo al generar '$entry': $output" >&2
return 1
fi
# pass generate imprime ANSI escape codes + header + password
# Extraer ultima linea y limpiar escape codes
echo "$output" | tail -1 | sed 's/\x1b\[[0-9;]*m//g'
}
+37
View File
@@ -0,0 +1,37 @@
---
name: pass_get
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "pass_get(entry: string) -> string"
description: "Lee un secreto del password store (pass) y lo imprime a stdout."
tags: [pass, secret, credential, get]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: entry
desc: "ruta de entrada en el password store"
output: "valor del secreto en texto plano"
tested: true
tests: ["lee entrada existente", "falla con entrada inexistente"]
test_file_path: "bash/functions/infra/pass_test.sh"
file_path: "bash/functions/infra/pass_get.sh"
---
## Ejemplo
```bash
source pass_get.sh
token=$(pass_get agentes/dataforge-token)
export GITEA_TOKEN="$token"
```
## Notas
Usa `pass show` internamente. Requiere GPG key desbloqueada. No imprime newline final (usa printf %s).
+26
View File
@@ -0,0 +1,26 @@
# pass_get
# --------
# Lee un secreto del password store y lo imprime a stdout.
# Sale con exit code 1 si la entrada no existe o pass falla.
#
# USO (sourced):
# source pass_get.sh
# token=$(pass_get agentes/dataforge-token)
pass_get() {
local entry="$1"
if [ -z "$entry" ]; then
echo "pass_get: se requiere nombre de entrada" >&2
return 1
fi
local value
value=$(pass show "$entry" 2>/dev/null)
if [ $? -ne 0 ]; then
echo "pass_get: no se pudo leer '$entry'" >&2
return 1
fi
printf '%s' "$value"
}
+37
View File
@@ -0,0 +1,37 @@
---
name: pass_list
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "pass_list([prefix: string]) -> json"
description: "Lista entradas del password store como JSON array. Filtra opcionalmente por prefijo."
tags: [pass, secret, credential, list]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: prefix
desc: "prefijo para filtrar entradas (opcional; ej: agentes)"
output: "JSON array de nombres de entradas"
tested: true
tests: ["lista todas las entradas", "filtra por prefijo"]
test_file_path: "bash/functions/infra/pass_test.sh"
file_path: "bash/functions/infra/pass_list.sh"
---
## Ejemplo
```bash
source pass_list.sh
entries=$(pass_list agentes)
# ["dataforge-token","egutierrez-token","gitea-url"]
```
## Notas
Parsea el output tree de `pass ls` y lo convierte a JSON array. Cada entrada es un string con el nombre relativo al prefijo.
+40
View File
@@ -0,0 +1,40 @@
# pass_list
# ---------
# Lista entradas del password store como JSON array.
# Opcionalmente filtra por prefijo.
# Sale con exit code 1 si pass falla.
#
# USO (sourced):
# source pass_list.sh
# pass_list # todas las entradas
# pass_list agentes # solo bajo agentes/
pass_list() {
local prefix="${1:-}"
local raw
raw=$(pass ls "$prefix" 2>/dev/null)
if [ $? -ne 0 ]; then
echo "pass_list: fallo al listar entradas" >&2
return 1
fi
# Parsear output de pass: extraer nombres limpios (sin tree chars)
local entries
entries=$(echo "$raw" | sed 's/[│├└──── ]//g' | sed '/^$/d' | grep -v '^Password' | grep -v '^[[:space:]]*$')
# Construir JSON array
printf '['
local first=true
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
[ -z "$line" ] && continue
if [ "$first" = true ]; then
first=false
else
printf ','
fi
printf '"%s"' "$line"
done <<< "$entries"
printf ']'
}
+38
View File
@@ -0,0 +1,38 @@
---
name: pass_set
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "pass_set(entry: string, [value: string]) -> void"
description: "Inserta o sobreescribe un secreto en el password store (pass)."
tags: [pass, secret, credential, set, insert]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: entry
desc: "ruta de entrada en el password store"
- name: value
desc: "valor del secreto (opcional; se lee de stdin si no se proporciona)"
output: "sin salida"
tested: true
tests: ["inserta valor y lo lee de vuelta", "sobreescribe valor existente"]
test_file_path: "bash/functions/infra/pass_test.sh"
file_path: "bash/functions/infra/pass_set.sh"
---
## Ejemplo
```bash
source pass_set.sh
pass_set agentes/nuevo-servicio "token-abc123"
```
## Notas
Usa `pass insert -m -f` para forzar sobreescritura sin prompt interactivo. Si no se pasa valor como argumento, lee de stdin.
+31
View File
@@ -0,0 +1,31 @@
# pass_set
# --------
# Inserta o sobreescribe un secreto en el password store.
# Lee el valor de stdin si no se pasa como segundo argumento.
# Sale con exit code 1 si pass falla.
#
# USO (sourced):
# source pass_set.sh
# pass_set agentes/nuevo-token "mi-token-secreto"
# echo "mi-token" | pass_set agentes/nuevo-token
pass_set() {
local entry="$1"
local value="$2"
if [ -z "$entry" ]; then
echo "pass_set: se requiere nombre de entrada" >&2
return 1
fi
if [ -n "$value" ]; then
printf '%s' "$value" | pass insert -m -f "$entry" >/dev/null 2>&1
else
pass insert -m -f "$entry" >/dev/null 2>&1
fi
if [ $? -ne 0 ]; then
echo "pass_set: fallo al escribir '$entry'" >&2
return 1
fi
}
+35
View File
@@ -0,0 +1,35 @@
---
name: pass_sync
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "pass_sync() -> json"
description: "Sincroniza el password store con el repositorio git remoto (pull + push)."
tags: [pass, secret, sync, git]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "JSON con resultados de pull y push"
tested: true
tests: ["sincroniza con remoto"]
test_file_path: "bash/functions/infra/pass_test.sh"
file_path: "bash/functions/infra/pass_sync.sh"
---
## Ejemplo
```bash
source pass_sync.sh
result=$(pass_sync)
# {"pull":"Already up to date.","push":"Everything up-to-date"}
```
## Notas
Ejecuta `pass git pull` seguido de `pass git push`. Requiere que el password store tenga un remote git configurado. Retorna JSON con la ultima linea de cada operacion.
+28
View File
@@ -0,0 +1,28 @@
# pass_sync
# ---------
# Sincroniza el password store con el repositorio git remoto (pull + push).
# Sale con exit code 1 si la sincronizacion falla.
#
# USO (sourced):
# source pass_sync.sh
# pass_sync
pass_sync() {
local pull_out
pull_out=$(pass git pull 2>&1)
if [ $? -ne 0 ]; then
echo "pass_sync: fallo en git pull: $pull_out" >&2
return 1
fi
local push_out
push_out=$(pass git push 2>&1)
if [ $? -ne 0 ]; then
echo "pass_sync: fallo en git push: $push_out" >&2
return 1
fi
printf '{"pull":"%s","push":"%s"}' \
"$(echo "$pull_out" | tail -1 | sed 's/"/\\"/g')" \
"$(echo "$push_out" | tail -1 | sed 's/"/\\"/g')"
}
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
# pass_test.sh — Tests para funciones pass del registry
# Usa la entrada test/fn_registry_test como sandbox (se limpia al final)
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/pass_get.sh"
source "$SCRIPT_DIR/pass_set.sh"
source "$SCRIPT_DIR/pass_list.sh"
source "$SCRIPT_DIR/pass_delete.sh"
source "$SCRIPT_DIR/pass_generate.sh"
source "$SCRIPT_DIR/pass_sync.sh"
TEST_ENTRY="test/fn_registry_test"
PASS=0
FAIL=0
pass_cleanup() {
pass rm -f "$TEST_ENTRY" >/dev/null 2>&1 || true
}
assert_eq() {
local test_name="$1" got="$2" want="$3"
if [ "$got" = "$want" ]; then
echo " PASS: $test_name"
((PASS++))
else
echo " FAIL: $test_name (got='$got', want='$want')"
((FAIL++))
fi
}
assert_contains() {
local test_name="$1" got="$2" want="$3"
if echo "$got" | grep -q "$want"; then
echo " PASS: $test_name"
((PASS++))
else
echo " FAIL: $test_name (got='$got', want contener '$want')"
((FAIL++))
fi
}
assert_nonzero() {
local test_name="$1" got="$2"
if [ -n "$got" ]; then
echo " PASS: $test_name"
((PASS++))
else
echo " FAIL: $test_name (got vacio)"
((FAIL++))
fi
}
assert_fail() {
local test_name="$1"
shift
set +e
"$@" 2>/dev/null
local rc=$?
set -e
if [ "$rc" -eq 0 ]; then
echo " FAIL: $test_name (esperaba fallo pero exitoso)"
((FAIL++))
else
echo " PASS: $test_name"
((PASS++))
fi
}
# Pre-check
if ! command -v pass &>/dev/null; then
echo "SKIP: pass no disponible"
exit 0
fi
trap pass_cleanup EXIT
echo "=== pass_get ==="
echo " test: lee entrada existente (agentes/gitea-url)"
got=$(pass_get agentes/gitea-url)
assert_nonzero "lee entrada existente" "$got"
echo " test: falla con entrada inexistente"
assert_fail "falla con entrada inexistente" pass_get "no/existe/xyz"
echo ""
echo "=== pass_set ==="
echo " test: inserta valor y lo lee de vuelta"
pass_set "$TEST_ENTRY" "test-value-12345"
got=$(pass_get "$TEST_ENTRY")
assert_eq "inserta y lee" "$got" "test-value-12345"
echo " test: sobreescribe valor existente"
pass_set "$TEST_ENTRY" "overwritten-value"
got=$(pass_get "$TEST_ENTRY")
assert_eq "sobreescribe" "$got" "overwritten-value"
# Limpiar para siguiente test
pass_cleanup
echo ""
echo "=== pass_list ==="
echo " test: lista todas las entradas"
got=$(pass_list)
assert_contains "lista todas" "$got" "dataforge-token"
echo " test: filtra por prefijo agentes"
got=$(pass_list agentes)
assert_contains "filtra agentes" "$got" "gitea-url"
echo ""
echo "=== pass_generate ==="
echo " test: genera password de 16 chars"
generated=$(pass_generate "$TEST_ENTRY" 16)
assert_eq "longitud 16" "${#generated}" "16"
echo " test: valor almacenado coincide"
stored=$(pass_get "$TEST_ENTRY")
assert_eq "stored = generated" "$stored" "$generated"
pass_cleanup
echo " test: default 24 chars"
generated=$(pass_generate "$TEST_ENTRY")
assert_eq "longitud default 24" "${#generated}" "24"
pass_cleanup
echo ""
echo "=== pass_delete ==="
echo " test: elimina entrada de test"
pass_set "$TEST_ENTRY" "to-delete"
pass_delete "$TEST_ENTRY"
assert_fail "despues de delete no se puede leer" pass_get "$TEST_ENTRY"
echo " test: falla con entrada inexistente"
assert_fail "delete inexistente" pass_delete "no/existe/xyz_delete_test"
echo ""
echo "=== pass_sync ==="
echo " test: sincroniza con remoto"
got=$(pass_sync)
assert_contains "sync retorna json" "$got" "pull"
echo ""
echo "================================"
echo "Resultados: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
+41
View File
@@ -0,0 +1,41 @@
---
name: uv_add_packages
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "uv_add_packages(project_dir: string, ...packages: string) -> void"
description: "Instala paquetes Python en un proyecto usando uv add con fallback a pip. Inicializa pyproject.toml si no existe."
tags: [python, uv, pip, packages, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio del proyecto con venv existente"
- name: packages
desc: "nombres de paquetes Python a instalar (variadic)"
output: "sin salida"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/uv_add_packages.sh"
---
## Ejemplo
```bash
source uv_add_packages.sh
uv_add_packages /home/lucas/analysis/finanzas jupyter jupyterlab pandas numpy
# Solo un paquete
uv_add_packages . polars
```
## Notas
Requiere que el venv ya exista (usa `init_uv_venv` antes). Prefiere uv por velocidad y reproducibilidad (lockfile). Si uv no esta disponible, usa pip del venv directamente.

Some files were not shown because too many files have changed in this diff Show More