xonecode 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -7
- package/THIRD_PARTY_NOTICES.md +128 -0
- package/apps/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/apps/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/apps/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/apps/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/apps/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/apps/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/apps/web/dist/assets/c-BIGW1oBm.js +1 -0
- package/apps/web/dist/assets/cpp-DddhVWhK.js +1 -0
- package/apps/web/dist/assets/csharp-DSvCPggb.js +1 -0
- package/apps/web/dist/assets/css-CLj8gQPS.js +1 -0
- package/apps/web/dist/assets/go-C27-OAKa.js +1 -0
- package/apps/web/dist/assets/html-CfGypltT.js +1 -0
- package/apps/web/dist/assets/index-BB3SamhR.css +1 -0
- package/apps/web/dist/assets/index-CFYAoHdw.js +477 -0
- package/apps/web/dist/assets/ini-BEwlwnbL.js +1 -0
- package/apps/web/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/apps/web/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/apps/web/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/apps/web/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/apps/web/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/apps/web/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/apps/web/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/apps/web/dist/assets/java-CylS5w8V.js +1 -0
- package/apps/web/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/apps/web/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/apps/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/apps/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/apps/web/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/apps/web/dist/assets/kotlin-BdnUsdx6.js +1 -0
- package/apps/web/dist/assets/less-B1dDrJ26.js +1 -0
- package/apps/web/dist/assets/lua-BaeVxFsk.js +1 -0
- package/apps/web/dist/assets/markdown-Cvjx9yec.js +1 -0
- package/apps/web/dist/assets/mdx-Cmh6b_Ma.js +1 -0
- package/apps/web/dist/assets/php-D_2--9WK.js +1 -0
- package/apps/web/dist/assets/python-B6aJPvgy.js +1 -0
- package/apps/web/dist/assets/ruby-DSHSDbk1.js +1 -0
- package/apps/web/dist/assets/rust-B1yitclQ.js +1 -0
- package/apps/web/dist/assets/scss-D5BDwBP9.js +1 -0
- package/apps/web/dist/assets/sql-CRqJ_cUM.js +1 -0
- package/apps/web/dist/assets/swift-C2oV4EkX.js +1 -0
- package/apps/web/dist/assets/toml-vGWfd6FD.js +1 -0
- package/apps/web/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/apps/web/dist/assets/yaml-Buea-lGh.js +1 -0
- package/apps/web/dist/iconos/xonecode.png +0 -0
- package/apps/web/dist/index.html +20 -0
- package/dist/agent/agentesEnDisco.js +539 -0
- package/dist/agent/agentesEnDisco.js.map +1 -0
- package/dist/agent/arbolDeProyecto.js +245 -0
- package/dist/agent/arbolDeProyecto.js.map +1 -0
- package/dist/agent/artefactosEnDisco.js +139 -0
- package/dist/agent/artefactosEnDisco.js.map +1 -0
- package/dist/agent/aumentador.js +175 -0
- package/dist/agent/aumentador.js.map +1 -0
- package/dist/agent/authEnDisco.js +76 -1
- package/dist/agent/authEnDisco.js.map +1 -1
- package/dist/agent/busquedaRegex.js +92 -0
- package/dist/agent/busquedaRegex.js.map +1 -0
- package/dist/agent/catalogoModelos.js +345 -0
- package/dist/agent/catalogoModelos.js.map +1 -0
- package/dist/agent/checkpointer.js +127 -0
- package/dist/agent/checkpointer.js.map +1 -0
- package/dist/agent/cloudstudioClient.js +194 -0
- package/dist/agent/cloudstudioClient.js.map +1 -0
- package/dist/agent/cloudstudioMcp.js +722 -0
- package/dist/agent/cloudstudioMcp.js.map +1 -0
- package/dist/agent/configEnDisco.js +309 -11
- package/dist/agent/configEnDisco.js.map +1 -1
- package/dist/agent/consumoExterno.js +99 -0
- package/dist/agent/consumoExterno.js.map +1 -0
- package/dist/agent/descarga.js +149 -0
- package/dist/agent/descarga.js.map +1 -0
- package/dist/agent/diagnosticoDeTools.js +51 -0
- package/dist/agent/diagnosticoDeTools.js.map +1 -0
- package/dist/agent/dispositivosEnMaquina.js +596 -0
- package/dist/agent/dispositivosEnMaquina.js.map +1 -0
- package/dist/agent/escrituraDeCodex.js +134 -0
- package/dist/agent/escrituraDeCodex.js.map +1 -0
- package/dist/agent/escrituraDeOpencode.js +80 -0
- package/dist/agent/escrituraDeOpencode.js.map +1 -0
- package/dist/agent/escrituraExterna.js +742 -0
- package/dist/agent/escrituraExterna.js.map +1 -0
- package/dist/agent/gemini.js +81 -0
- package/dist/agent/gemini.js.map +1 -0
- package/dist/agent/git.js +83 -0
- package/dist/agent/git.js.map +1 -0
- package/dist/agent/gitSync.js +441 -0
- package/dist/agent/gitSync.js.map +1 -0
- package/dist/agent/hotswap.js +448 -0
- package/dist/agent/hotswap.js.map +1 -0
- package/dist/agent/instalacionEnMaquina.js +252 -0
- package/dist/agent/instalacionEnMaquina.js.map +1 -0
- package/dist/agent/instantanea.js +7 -10
- package/dist/agent/instantanea.js.map +1 -1
- package/dist/agent/interrupts.js +11 -2
- package/dist/agent/interrupts.js.map +1 -1
- package/dist/agent/juezDeTarea.js +345 -0
- package/dist/agent/juezDeTarea.js.map +1 -0
- package/dist/agent/lanzamientoEnMaquina.js +377 -0
- package/dist/agent/lanzamientoEnMaquina.js.map +1 -0
- package/dist/agent/manifiesto.js +59 -0
- package/dist/agent/manifiesto.js.map +1 -0
- package/dist/agent/memoriaDeProyecto.js +60 -0
- package/dist/agent/memoriaDeProyecto.js.map +1 -0
- package/dist/agent/modelos.js +105 -17
- package/dist/agent/modelos.js.map +1 -1
- package/dist/agent/modelosDeMotor.js +204 -0
- package/dist/agent/modelosDeMotor.js.map +1 -0
- package/dist/agent/paqueteDelProyecto.js +181 -0
- package/dist/agent/paqueteDelProyecto.js.map +1 -0
- package/dist/agent/perfiles.js +92 -31
- package/dist/agent/perfiles.js.map +1 -1
- package/dist/agent/persona.js +40 -0
- package/dist/agent/persona.js.map +1 -0
- package/dist/agent/procesosEnMaquina.js +204 -0
- package/dist/agent/procesosEnMaquina.js.map +1 -0
- package/dist/agent/proyecto.js +311 -13
- package/dist/agent/proyecto.js.map +1 -1
- package/dist/agent/puente.js +63 -6
- package/dist/agent/puente.js.map +1 -1
- package/dist/agent/resumenDeContexto.js +30 -0
- package/dist/agent/resumenDeContexto.js.map +1 -0
- package/dist/agent/resumenDeTool.js +64 -18
- package/dist/agent/resumenDeTool.js.map +1 -1
- package/dist/agent/sesionGit.js +481 -0
- package/dist/agent/sesionGit.js.map +1 -0
- package/dist/agent/settingsEnDisco.js +189 -0
- package/dist/agent/settingsEnDisco.js.map +1 -0
- package/dist/agent/subagenteCodex.js +368 -0
- package/dist/agent/subagenteCodex.js.map +1 -0
- package/dist/agent/subagenteExterno.js +423 -0
- package/dist/agent/subagenteExterno.js.map +1 -0
- package/dist/agent/subagenteOpencode.js +389 -0
- package/dist/agent/subagenteOpencode.js.map +1 -0
- package/dist/agent/subida.js +202 -0
- package/dist/agent/subida.js.map +1 -0
- package/dist/agent/tareasEnDisco.js +466 -0
- package/dist/agent/tareasEnDisco.js.map +1 -0
- package/dist/agent/turnoReal.js +663 -50
- package/dist/agent/turnoReal.js.map +1 -1
- package/dist/agent/versionEnDisco.js +48 -0
- package/dist/agent/versionEnDisco.js.map +1 -0
- package/dist/agent/xoneAgent.js +290 -49
- package/dist/agent/xoneAgent.js.map +1 -1
- package/dist/agent/zip.js +52 -0
- package/dist/agent/zip.js.map +1 -0
- package/dist/cli/acuseDeModelo.js +30 -0
- package/dist/cli/acuseDeModelo.js.map +1 -0
- package/dist/cli/aprobar.js +55 -2
- package/dist/cli/aprobar.js.map +1 -1
- package/dist/cli/config.js +9 -13
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/consola.js +750 -19
- package/dist/cli/consola.js.map +1 -1
- package/dist/cli/main.js +740 -81
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/markdown.js +188 -2
- package/dist/cli/markdown.js.map +1 -1
- package/dist/cli/run.js +32 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stdio.js +46 -19
- package/dist/cli/stdio.js.map +1 -1
- package/dist/cli/tema.js +96 -8
- package/dist/cli/tema.js.map +1 -1
- package/dist/cli/tokens.js +29 -0
- package/dist/cli/tokens.js.map +1 -0
- package/dist/cli/tui/app.js +171 -0
- package/dist/cli/tui/app.js.map +1 -0
- package/dist/cli/tui/aprobarTui.js +80 -0
- package/dist/cli/tui/aprobarTui.js.map +1 -0
- package/dist/cli/tui/barra.js +37 -0
- package/dist/cli/tui/barra.js.map +1 -0
- package/dist/cli/tui/correrTui.js +478 -0
- package/dist/cli/tui/correrTui.js.map +1 -0
- package/dist/cli/tui/entrada.js +166 -0
- package/dist/cli/tui/entrada.js.map +1 -0
- package/dist/cli/tui/filas.js +24 -0
- package/dist/cli/tui/filas.js.map +1 -0
- package/dist/cli/tui/logo.js +49 -0
- package/dist/cli/tui/logo.js.map +1 -0
- package/dist/cli/tui/pielTui.js +14 -0
- package/dist/cli/tui/pielTui.js.map +1 -0
- package/dist/cli/tui/raton.js +126 -0
- package/dist/cli/tui/raton.js.map +1 -0
- package/dist/cli/tui/sidebar.js +86 -0
- package/dist/cli/tui/sidebar.js.map +1 -0
- package/dist/cli/tui/store.js +143 -0
- package/dist/cli/tui/store.js.map +1 -0
- package/dist/cli/tui/tarjeta.js +19 -0
- package/dist/cli/tui/tarjeta.js.map +1 -0
- package/dist/cli/tui/temaInk.js +114 -0
- package/dist/cli/tui/temaInk.js.map +1 -0
- package/dist/cli/tui/transcript.js +254 -0
- package/dist/cli/tui/transcript.js.map +1 -0
- package/dist/cli/wizardInicial.js +240 -0
- package/dist/cli/wizardInicial.js.map +1 -0
- package/dist/core/actos.js +113 -0
- package/dist/core/actos.js.map +1 -0
- package/dist/core/adjuntos.js +97 -0
- package/dist/core/adjuntos.js.map +1 -0
- package/dist/core/agentes.js +227 -0
- package/dist/core/agentes.js.map +1 -0
- package/dist/core/artefactos.js +149 -0
- package/dist/core/artefactos.js.map +1 -0
- package/dist/core/cloudstudio.js +7 -0
- package/dist/core/cloudstudio.js.map +1 -0
- package/dist/core/config.js +212 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/contextos.js +53 -1
- package/dist/core/contextos.js.map +1 -1
- package/dist/core/descargas.js +106 -0
- package/dist/core/descargas.js.map +1 -0
- package/dist/core/descriptoresDeApp.js +279 -0
- package/dist/core/descriptoresDeApp.js.map +1 -0
- package/dist/core/dispositivos.js +466 -0
- package/dist/core/dispositivos.js.map +1 -0
- package/dist/core/entrega.js +141 -0
- package/dist/core/entrega.js.map +1 -0
- package/dist/core/entrelazar.js +90 -0
- package/dist/core/entrelazar.js.map +1 -0
- package/dist/core/esqueleto.js +10 -0
- package/dist/core/esqueleto.js.map +1 -1
- package/dist/core/modelos.js +235 -3
- package/dist/core/modelos.js.map +1 -1
- package/dist/core/notify.js +27 -9
- package/dist/core/notify.js.map +1 -1
- package/dist/core/planDeSubida.js +127 -0
- package/dist/core/planDeSubida.js.map +1 -0
- package/dist/core/ports.js +189 -1
- package/dist/core/ports.js.map +1 -1
- package/dist/core/puedeLanzarse.js +239 -0
- package/dist/core/puedeLanzarse.js.map +1 -0
- package/dist/core/rutaVirtual.js +42 -0
- package/dist/core/rutaVirtual.js.map +1 -0
- package/dist/core/settings.js +246 -0
- package/dist/core/settings.js.map +1 -0
- package/dist/core/tareas.js +230 -0
- package/dist/core/tareas.js.map +1 -0
- package/dist/core/textos.js +36 -0
- package/dist/core/textos.js.map +1 -0
- package/dist/core/turno.js +70 -10
- package/dist/core/turno.js.map +1 -1
- package/dist/core/version.js +31 -0
- package/dist/core/version.js.map +1 -0
- package/dist/vendor/tokenTracking.js +22 -1
- package/dist/vendor/tokenTracking.js.map +1 -1
- package/dist/web/servidor/arranque.js +3964 -0
- package/dist/web/servidor/arranque.js.map +1 -0
- package/dist/web/servidor/consolaDeTarea.js +161 -0
- package/dist/web/servidor/consolaDeTarea.js.map +1 -0
- package/dist/web/servidor/consolaWeb.js +389 -0
- package/dist/web/servidor/consolaWeb.js.map +1 -0
- package/dist/web/servidor/corredorDeTareas.js +1119 -0
- package/dist/web/servidor/corredorDeTareas.js.map +1 -0
- package/dist/web/servidor/pielWeb.js +269 -0
- package/dist/web/servidor/pielWeb.js.map +1 -0
- package/dist/web/servidor/servidor.js +257 -0
- package/dist/web/servidor/servidor.js.map +1 -0
- package/dist/web/servidor/sesiones.js +440 -0
- package/dist/web/servidor/sesiones.js.map +1 -0
- package/dist/web/servidor/transporte.js +169 -0
- package/dist/web/servidor/transporte.js.map +1 -0
- package/dist/web/servidor/vestibulo.js +1276 -0
- package/dist/web/servidor/vestibulo.js.map +1 -0
- package/package.json +30 -8
- package/skills/archify/LICENSE +22 -0
- package/skills/archify/SKILL.md +109 -0
- package/skills/archify/THIRD_PARTY_NOTICES.md +56 -0
- package/skills/archify/assets/template.html +14787 -0
- package/skills/archify/bin/archify.mjs +1990 -0
- package/skills/archify/bin/open-artifact.mjs +86 -0
- package/skills/archify/bin/preview.mjs +648 -0
- package/skills/archify/bin/visual-check.mjs +829 -0
- package/skills/archify/brand-marks/README.md +31 -0
- package/skills/archify/brand-marks/catalog.json +131 -0
- package/skills/archify/delta/architecture-delta.mjs +1214 -0
- package/skills/archify/examples/agent-run.lifecycle.json +71 -0
- package/skills/archify/examples/agent-tool-call.workflow.json +94 -0
- package/skills/archify/examples/async-job-roundtrip.sequence.json +61 -0
- package/skills/archify/examples/brand-aware-delivery.architecture.json +47 -0
- package/skills/archify/examples/cache-miss-request.sequence.json +81 -0
- package/skills/archify/examples/checkout-platform.base.architecture.json +31 -0
- package/skills/archify/examples/checkout-platform.head.architecture.json +31 -0
- package/skills/archify/examples/dataflow-product-analytics.html +14898 -0
- package/skills/archify/examples/deployment-release.lifecycle.json +49 -0
- package/skills/archify/examples/event-stream.dataflow.json +57 -0
- package/skills/archify/examples/incident-response.workflow.json +64 -0
- package/skills/archify/examples/lifecycle-agent-run.html +14847 -0
- package/skills/archify/examples/product-analytics.dataflow.json +76 -0
- package/skills/archify/examples/production-deployment.architecture.json +71 -0
- package/skills/archify/examples/release-delivery.workflow.json +62 -0
- package/skills/archify/examples/sequence-cache-miss-request.html +14913 -0
- package/skills/archify/examples/web-app-rendered.html +14862 -0
- package/skills/archify/examples/web-app.architecture.json +46 -0
- package/skills/archify/examples/workflow-agent-tool-call-rendered.html +14904 -0
- package/skills/archify/migrations/workflow-v2.mjs +279 -0
- package/skills/archify/package.json +17 -0
- package/skills/archify/recipes/scenarios.mjs +391 -0
- package/skills/archify/references/authoring-contract.md +198 -0
- package/skills/archify/references/brand-marks.md +69 -0
- package/skills/archify/references/delivery-contract.md +116 -0
- package/skills/archify/references/viewer-runtime.md +45 -0
- package/skills/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/archify/renderers/architecture/render-architecture.mjs +1078 -0
- package/skills/archify/renderers/dataflow/README.md +104 -0
- package/skills/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- package/skills/archify/renderers/lifecycle/README.md +115 -0
- package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- package/skills/archify/renderers/sequence/README.md +114 -0
- package/skills/archify/renderers/sequence/render-sequence.mjs +453 -0
- package/skills/archify/renderers/shared/brand-marks.mjs +563 -0
- package/skills/archify/renderers/shared/cli.mjs +218 -0
- package/skills/archify/renderers/shared/desktop-readability.mjs +26 -0
- package/skills/archify/renderers/shared/diagnostics.mjs +127 -0
- package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- package/skills/archify/renderers/shared/generated-validators.mjs +13 -0
- package/skills/archify/renderers/shared/geometry.mjs +1423 -0
- package/skills/archify/renderers/shared/i18n.mjs +594 -0
- package/skills/archify/renderers/shared/layout-report.mjs +40 -0
- package/skills/archify/renderers/shared/legend.mjs +217 -0
- package/skills/archify/renderers/shared/output-path.mjs +321 -0
- package/skills/archify/renderers/shared/repository-evidence.mjs +235 -0
- package/skills/archify/renderers/shared/text-fit.mjs +49 -0
- package/skills/archify/renderers/shared/utils.mjs +232 -0
- package/skills/archify/renderers/shared/validator.mjs +86 -0
- package/skills/archify/renderers/workflow/README.md +223 -0
- package/skills/archify/renderers/workflow/render-workflow.mjs +35 -0
- package/skills/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/skills/archify/schemas/README.md +211 -0
- package/skills/archify/schemas/architecture.schema.json +176 -0
- package/skills/archify/schemas/common.schema.json +115 -0
- package/skills/archify/schemas/dataflow.schema.json +243 -0
- package/skills/archify/schemas/lifecycle.schema.json +266 -0
- package/skills/archify/schemas/sequence.schema.json +223 -0
- package/skills/archify/schemas/workflow.schema.json +428 -0
- package/skills/archify/scripts/check-render-output.mjs +835 -0
- package/skills/archify/scripts/check-update.mjs +1667 -0
- package/skills/archify/scripts/render-examples.mjs +26 -0
- package/skills/archify/scripts/update-contract.mjs +182 -0
- package/skills/archify/skill-release.json +10 -0
- package/skills/artifacts-builder/SKILL.md +120 -0
- package/skills/artifacts-builder/reference/diagramas.md +123 -0
- package/skills/artifacts-builder/reference/estilo.md +123 -0
- package/skills/artifacts-builder/reference/exportar-pdf.md +56 -0
- package/skills/artifacts-builder/reference/graficas.md +22 -0
- package/skills/artifacts-builder/scripts/bundle_artifact.py +109 -0
- package/skills/artifacts-builder/scripts/validate_artifact.py +345 -0
- package/skills/xone-hotswap/SKILL.md +185 -0
- package/skills/xone-hotswap/references/comandos.md +706 -0
- package/skills/xone-hotswap/references/conexion-y-despliegue.md +226 -0
|
@@ -0,0 +1,1276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* El vestíbulo: lo que hay ANTES de que exista ninguna raíz.
|
|
3
|
+
*
|
|
4
|
+
* `correrConsola` es un lazo sobre UNA `raiz`, y la jerarquía entorno → proyecto → sesión
|
|
5
|
+
* necesita un sitio donde vivir mientras no hay proyecto abierto. De ahí las dos clases de
|
|
6
|
+
* consola: el vestíbulo (sin raíz) y la consola de proyecto (con la suya).
|
|
7
|
+
*
|
|
8
|
+
* Los pasos son los mismos del alta de terminal, y cada uno solo aparece si falta lo que
|
|
9
|
+
* decide. El de cuenta se detecta como siempre: si el papel `trabajo` resuelve por
|
|
10
|
+
* `omision`, nadie eligió nunca. **No hay marca de «primer arranque»**: sería una segunda
|
|
11
|
+
* fuente de verdad sobre algo que el sistema ya sabe.
|
|
12
|
+
*
|
|
13
|
+
* Este fichero NO toca disco ni red por su cuenta. Todo lo que escribe o conecta entra por
|
|
14
|
+
* la opción correspondiente, y las que escriben (`guardarCredencial`, `guardarEntorno`,
|
|
15
|
+
* `guardarConfigDeProyecto`, `descargar`) son OBLIGATORIAS: un valor por omisión que
|
|
16
|
+
* escribiera en el `~/.xonecode` de verdad convertiría cualquier test en una escritura en
|
|
17
|
+
* la casa del usuario, y uno que no escribiera sería el no-op silencioso que este repo
|
|
18
|
+
* evita en todas partes.
|
|
19
|
+
*
|
|
20
|
+
* La única LECTURA de disco con valor por omisión es `esProyecto` («¿hay copia local
|
|
21
|
+
* aquí?»), y sigue la misma regla que `sesiones ?? SESIONES_EN_DISCO`: entra por opción
|
|
22
|
+
* para que un test pueda contestarla sin carpetas, y su omisión es la de verdad porque
|
|
23
|
+
* mentir en esa pregunta abre un proyecto que no existe.
|
|
24
|
+
*/
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { homedir } from "node:os";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
import { consumoDeLosActos, sumarConsumo } from "../../core/actos.js";
|
|
30
|
+
import { consumoDeLaSesion, consumoPersistible, esDoble } from "../../core/ports.js";
|
|
31
|
+
import { rutaDeWorkspace } from "../../core/settings.js";
|
|
32
|
+
import { URL_CLOUDSTUDIO_POR_OMISION, SCOPES_CLOUDSTUDIO_AGENTE, AVISO_DE_URL_DE_MCP, adoptarLegadoSiProcede, conectarCloudStudio, rutaAuthPorDefecto, sesionCloudStudio, urlDeMcpAceptable, } from "../../agent/cloudstudioMcp.js";
|
|
33
|
+
import { clienteCloudStudio } from "../../agent/cloudstudioClient.js";
|
|
34
|
+
import { guardarCloudStudioDeProyecto, guardarEntornoDeProyecto, guardarModeloGlobal as guardarModeloGlobalEnDisco, guardarModoDeProyecto, guardarProyectoCloudStudioDeProyecto, guardarRamaDeProyecto, } from "../../agent/configEnDisco.js";
|
|
35
|
+
import { correrConsola, ejecutarTurnoGuionizado, } from "../../cli/consola.js";
|
|
36
|
+
import { asistenteDeModelo } from "../../cli/wizardInicial.js";
|
|
37
|
+
import { crearConsolaWeb } from "./consolaWeb.js";
|
|
38
|
+
import { anotarActo, borrarSesion, crearSesion, elegirDispositivo, listarSesiones, reabrirSesion, renombrarSesion, } from "./sesiones.js";
|
|
39
|
+
/**
|
|
40
|
+
* Los dos CloudStudio oficiales, pre-rellenados.
|
|
41
|
+
*
|
|
42
|
+
* La URL de WebStudio no se repite aquí: se importa de `agent/cloudstudioMcp.ts`, que es
|
|
43
|
+
* donde vive desde que `adoptarLegadoSiProcede` la necesitó (`cli/consola.ts` la reexporta
|
|
44
|
+
* por compatibilidad). Dos literales de la misma dirección es cómo divergen el día que una
|
|
45
|
+
* se corrige y la otra no — el mismo motivo por el que `segmentoSeguro` no se copió.
|
|
46
|
+
*/
|
|
47
|
+
export const ENTORNOS_OFICIALES = [
|
|
48
|
+
{ id: "webstudio", nombre: "XOne WebStudio", url: URL_CLOUDSTUDIO_POR_OMISION },
|
|
49
|
+
{ id: "manager", nombre: "XOne Manager", url: "https://mcp.xonemanager.com/mcp" },
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* El tercero de la lista, y no un botón aparte de la interfaz: un on-premise es un entorno
|
|
53
|
+
* como los otros dos, y presentarlo al mismo nivel es lo que evita que la web tenga dos
|
|
54
|
+
* caminos distintos para registrar lo mismo.
|
|
55
|
+
*/
|
|
56
|
+
export const ENTORNO_OTRO = { id: "otro", nombre: "Otro (on-premise)", url: "" };
|
|
57
|
+
/** La base del workspace por omisión: `~/.xonecode`. La disposición de dentro la fija
|
|
58
|
+
* `rutaDeWorkspace` y no es configurable. */
|
|
59
|
+
export function baseDeWorkspacePorOmision() {
|
|
60
|
+
return join(homedir(), ".xonecode");
|
|
61
|
+
}
|
|
62
|
+
const SESIONES_EN_DISCO = {
|
|
63
|
+
crear: crearSesion,
|
|
64
|
+
listar: listarSesiones,
|
|
65
|
+
anotar: anotarActo,
|
|
66
|
+
reabrir: reabrirSesion,
|
|
67
|
+
borrar: borrarSesion,
|
|
68
|
+
renombrar: renombrarSesion,
|
|
69
|
+
elegirDispositivo,
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* ¿Hay una copia local de proyecto en esa raíz? Es el MISMO criterio con el que el alta
|
|
73
|
+
* decide `proyectos[].local` (`arranque.ts#hayCopiaLocal`, que tira de aquí): existe su
|
|
74
|
+
* `.xonecode/config.json`. Dos copias del predicado es cómo divergen el día que una se
|
|
75
|
+
* afine — el mismo motivo por el que la regla de URL de MCP vive en un solo sitio.
|
|
76
|
+
*
|
|
77
|
+
* Se lo traga todo y devuelve `false`: una raíz que no se puede ni mirar (permisos, un
|
|
78
|
+
* enlace roto) no es un proyecto sobre el que abrir nada, y esta puerta falla CERRADO.
|
|
79
|
+
*/
|
|
80
|
+
export function esProyectoEnDisco(raiz) {
|
|
81
|
+
// Sin `try`: `existsSync` no lanza nunca —traga cualquier error del sistema y devuelve
|
|
82
|
+
// false—, así que envolverlo sugeriría un peligro que no existe.
|
|
83
|
+
return existsSync(join(raiz, ".xonecode", "config.json"));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Lo que se contesta a quien intenta abrir —o borrar— la conversación de una tarea que sigue
|
|
87
|
+
* corriendo. Vive en una constante porque lo leen las dos guardas y su test, y porque es lo
|
|
88
|
+
* ÚNICO que el usuario ve de esta regla.
|
|
89
|
+
*
|
|
90
|
+
* **Dice qué SÍ se puede hacer**, y no es cortesía: la vista en vivo existe justo al lado, así
|
|
91
|
+
* que un «no puedes» a secas es lo que hace que alguien insista o dé el botón por roto. Y no
|
|
92
|
+
* lleva ninguna ruta de la máquina: sale por el cable, que puede ir por un túnel.
|
|
93
|
+
*/
|
|
94
|
+
export function motivoDeProyectoTrabajando() {
|
|
95
|
+
return ("ese proyecto tiene una sesión con un turno en marcha: dos conversaciones sobre la misma " +
|
|
96
|
+
"copia de trabajo se pisarían los ficheros. Vuelve a ella para verla, o párala, y abre la " +
|
|
97
|
+
"otra cuando termine.");
|
|
98
|
+
}
|
|
99
|
+
export const MOTIVO_SESION_DE_TAREA_EN_CURSO = "esa conversación es la de una tarea en curso: el agente está escribiendo en ella ahora " +
|
|
100
|
+
"mismo. Pulsa «Ver lo que hace» en su tarjeta para mirarla, y ábrela cuando termine.";
|
|
101
|
+
/**
|
|
102
|
+
* El `config.json` del proyecto, escrito con los escritores que ya existen.
|
|
103
|
+
*
|
|
104
|
+
* El orden importa y `modo` va el ÚLTIMO a propósito: si algo revienta a mitad, lo que
|
|
105
|
+
* queda en disco es un `cloudstudio` sin `modo`, que es exactamente el caso que
|
|
106
|
+
* `crearSincronizador` ya sabe contar («este proyecto no es cloud…»). Al revés dejaría un
|
|
107
|
+
* proyecto declarado cloud sin saber contra qué sincronizar.
|
|
108
|
+
*
|
|
109
|
+
* `entorno` se escribe ADEMÁS de `cloudstudio.url`, nunca en su lugar: conservar la URL es
|
|
110
|
+
* lo que hace literalmente cierto que la sincronización no se toca — `crearSincronizador`
|
|
111
|
+
* y todo lo que cuelga de él la leen igual que hoy.
|
|
112
|
+
*/
|
|
113
|
+
export function escribirProyectoEnDisco(raiz, datos) {
|
|
114
|
+
guardarCloudStudioDeProyecto(raiz, datos.url, datos.scopes);
|
|
115
|
+
guardarProyectoCloudStudioDeProyecto(raiz, datos.proyecto);
|
|
116
|
+
guardarRamaDeProyecto(raiz, datos.rama);
|
|
117
|
+
guardarEntornoDeProyecto(raiz, datos.entorno);
|
|
118
|
+
return { ruta: guardarModoDeProyecto(raiz, "cloud").ruta };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Los dos cables de CloudStudio del vestíbulo, con el id del entorno y la vuelta a la web
|
|
122
|
+
* ya enhebrados.
|
|
123
|
+
*
|
|
124
|
+
* Las dos cosas son deudas que se pagan aquí: sin `entornoId`, un proyecto ya migrado a
|
|
125
|
+
* `webstudio` reautenticaría en un segundo hueco `legado`; y sin `redirigirA`, la página
|
|
126
|
+
* del callback termina diciendo «vuelve a la terminal», que en un navegador es falso.
|
|
127
|
+
*/
|
|
128
|
+
export function conexionDeVestibulo(urlDeLaWeb) {
|
|
129
|
+
const comunes = (entorno) => ({
|
|
130
|
+
scopes: entorno.scopes ?? SCOPES_CLOUDSTUDIO_AGENTE,
|
|
131
|
+
entornoId: entorno.id,
|
|
132
|
+
...(urlDeLaWeb === undefined ? {} : { redirigirA: urlDeLaWeb }),
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
proyectosDeEntorno: async (entorno) => {
|
|
136
|
+
const conexion = await conectarCloudStudio(entorno.url, comunes(entorno));
|
|
137
|
+
return {
|
|
138
|
+
proyectos: conexion.proyectos,
|
|
139
|
+
...(conexion.servidor === undefined ? {} : { servidor: { nombre: conexion.servidor.nombre } }),
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
ramasDeProyecto: async (entorno, proyecto) => {
|
|
143
|
+
const sesion = await sesionCloudStudio(entorno.url, comunes(entorno));
|
|
144
|
+
try {
|
|
145
|
+
const puerto = clienteCloudStudio(sesion.invocar, proyecto);
|
|
146
|
+
await puerto.abrir(proyecto);
|
|
147
|
+
return await puerto.ramas();
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
await sesion.cerrar();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* La regla de URL de entorno es la MISMA que la de quien conecta de verdad
|
|
157
|
+
* (`agent/cloudstudioMcp.ts#urlDeMcpAceptable`), importada y no copiada: cuando eran dos
|
|
158
|
+
* criterios, el wizard aceptaba un loopback que este fichero rechazaba, con dos mensajes
|
|
159
|
+
* claros que se contradecían. Ver allí por qué se resolvió por el lado permisivo.
|
|
160
|
+
*/
|
|
161
|
+
const urlDeEntornoValida = urlDeMcpAceptable;
|
|
162
|
+
/**
|
|
163
|
+
* El host de una URL, o la URL entera si no parsea. No debería hacer falta ese segundo caso
|
|
164
|
+
* —`urlDeEntornoValida` corre antes y ya rechaza lo que no parsea—, pero si algún día
|
|
165
|
+
* alguien invierte ese orden, mejor un id feo que una excepción.
|
|
166
|
+
*/
|
|
167
|
+
function hostDeUrl(url) {
|
|
168
|
+
try {
|
|
169
|
+
return new URL(url).host;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return url;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Misma URL, dicha de dos formas (`https://x/mcp` y `https://x/mcp/`), es el mismo sitio. */
|
|
176
|
+
function mismaUrl(a, b) {
|
|
177
|
+
const normal = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
|
|
178
|
+
return normal(a) === normal(b);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* La identidad del entorno, DEDUCIDA de la URL cuando no la trae puesta.
|
|
182
|
+
*
|
|
183
|
+
* El formulario del navegador pide solo la URL: un nombre tecleado a mano es un dato que
|
|
184
|
+
* el usuario se inventa y que después hay que creerse en la barra lateral y en la ruta del
|
|
185
|
+
* workspace. Las dos reglas:
|
|
186
|
+
*
|
|
187
|
+
* - Si la URL es la de un entorno OFICIAL, se usa su identidad (aunque se haya escrito a
|
|
188
|
+
* mano en el hueco de «otro»): dos entradas para el mismo servidor son dos carpetas de
|
|
189
|
+
* workspace y dos huecos de OAuth para la misma cuenta.
|
|
190
|
+
* - Si no, el id y el nombre salen del HOST. El id pasa por `segmentoSeguro`
|
|
191
|
+
* (`core/settings.ts`) porque acaba siendo una carpeta (`rutaDeWorkspace`), así que se
|
|
192
|
+
* quita todo lo que no sea letra, cifra, punto o guion — los dos puntos del puerto
|
|
193
|
+
* incluidos, que en Windows parten la ruta. El nombre conserva el host tal cual, que es
|
|
194
|
+
* para leerlo.
|
|
195
|
+
*
|
|
196
|
+
* Lo que NO se deduce aquí es nada que venga del servidor: eso exige haber hecho OAuth, y
|
|
197
|
+
* el registro pasa antes. Un nombre inventado a partir de la URL es verdad comprobable;
|
|
198
|
+
* uno «bonito» sacado de la nada no lo sería.
|
|
199
|
+
*/
|
|
200
|
+
export function identidadDeEntorno(entorno) {
|
|
201
|
+
const oficial = ENTORNOS_OFICIALES.find((o) => mismaUrl(o.url, entorno.url));
|
|
202
|
+
if (oficial !== undefined)
|
|
203
|
+
return { ...entorno, id: oficial.id, nombre: oficial.nombre };
|
|
204
|
+
// Un id y un nombre puestos por quien llama se respetan: el paso de entorno del wizard
|
|
205
|
+
// manda «otro» (o vacío) justo para pedir esta deducción, pero otra piel puede traerlos.
|
|
206
|
+
if (entorno.id !== "" && entorno.id !== ENTORNO_OTRO.id && entorno.nombre !== "")
|
|
207
|
+
return entorno;
|
|
208
|
+
const host = hostDeUrl(entorno.url);
|
|
209
|
+
const id = host.replace(/[^a-zA-Z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
210
|
+
return {
|
|
211
|
+
...entorno,
|
|
212
|
+
id: id === "" ? "on-premise" : id,
|
|
213
|
+
nombre: entorno.nombre === "" ? host : entorno.nombre,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function crearVestibulo(opciones) {
|
|
217
|
+
const sesiones = opciones.sesiones ?? SESIONES_EN_DISCO;
|
|
218
|
+
const crearConsola = opciones.crearConsola ?? crearConsolaWeb;
|
|
219
|
+
const correr = opciones.correr ?? correrConsola;
|
|
220
|
+
const base = opciones.baseDeWorkspace ?? baseDeWorkspacePorOmision();
|
|
221
|
+
const adoptarLegado = opciones.adoptarLegado ??
|
|
222
|
+
// `rutaAuthPorDefecto` y no un `join` propio: el mismo literal en dos ficheros es lo
|
|
223
|
+
// que esta cabecera condena, y `cloudstudioMcp.ts` la exporta justo para esto.
|
|
224
|
+
((entorno) => adoptarLegadoSiProcede(rutaAuthPorDefecto(), entorno));
|
|
225
|
+
const guardarModeloGlobal = opciones.guardarModeloGlobal ?? guardarModeloGlobalEnDisco;
|
|
226
|
+
const esProyecto = opciones.esProyecto ?? esProyectoEnDisco;
|
|
227
|
+
const consolaDelVestibulo = crearConsola({
|
|
228
|
+
catalogoModelos: opciones.catalogoModelos,
|
|
229
|
+
guardarModeloGlobal,
|
|
230
|
+
...(opciones.msDeEspera === undefined ? {} : { msDeEspera: opciones.msDeEspera }),
|
|
231
|
+
});
|
|
232
|
+
const informar = opciones.informar ?? ((texto) => consolaDelVestibulo.consola.escribir(`${texto}\n`));
|
|
233
|
+
/** Los registrados al arrancar MÁS los que este vestíbulo acaba de dar de alta: el paso 3
|
|
234
|
+
* llega justo después del 2 y `settings.json` no se vuelve a leer entre medias. */
|
|
235
|
+
const registrados = [...(opciones.entornos ?? [])];
|
|
236
|
+
/** Lo ya escrito, para que `cancelar()` pueda decirlo en vez de fingir que no pasó nada. */
|
|
237
|
+
let credencialEscrita;
|
|
238
|
+
let proyectoEscrito;
|
|
239
|
+
/**
|
|
240
|
+
* Las consolas de PERSONA vivas, por raíz. Antes era una sola variable, y cambiar de
|
|
241
|
+
* sesión mataba el turno que estuviera corriendo.
|
|
242
|
+
*
|
|
243
|
+
* **La clave es la raíz, y eso es la mitad del diseño**: nunca hay dos consolas sobre la
|
|
244
|
+
* misma copia de trabajo, así que dos conversaciones no pueden escribir el mismo fichero
|
|
245
|
+
* ni pisarse el hilo del checkpointer. Lo que se gana es lo otro: dos proyectos distintos
|
|
246
|
+
* pueden estar trabajando a la vez, y mirar uno no interrumpe al otro.
|
|
247
|
+
*
|
|
248
|
+
* No crece sin freno: una consola de segundo plano se cierra ella sola en cuanto su turno
|
|
249
|
+
* acaba (`cerrarSiSobra`). El hilo no se pierde —el checkpointer lo reanuda al reabrir la
|
|
250
|
+
* sesión—, así que cerrarla no cuesta nada y dejarla viva costaría un `correrConsola` por
|
|
251
|
+
* proyecto visitado.
|
|
252
|
+
*/
|
|
253
|
+
const abiertas = new Map();
|
|
254
|
+
/** La raíz de la consola que el cable está pintando. `undefined` = el vestíbulo. */
|
|
255
|
+
let enFoco;
|
|
256
|
+
const consolaEnFoco = () => enFoco === undefined ? undefined : abiertas.get(enFoco);
|
|
257
|
+
/** Quien quiera enterarse de que el modelo en vigor cambió. UNA sola: el cable es uno. */
|
|
258
|
+
let escuchaDeEstado;
|
|
259
|
+
/** Y de que hay (o deja de haber) un turno corriendo. */
|
|
260
|
+
let escuchaDeTurno;
|
|
261
|
+
/** Y de que la sesión en foco ha consumido más tokens. */
|
|
262
|
+
let escuchaDeConsumo;
|
|
263
|
+
/**
|
|
264
|
+
* Avisa de que algo cambió en las consolas de persona: un turno empieza o acaba —en
|
|
265
|
+
* CUALQUIERA de ellas— o una de segundo plano se cierra.
|
|
266
|
+
*
|
|
267
|
+
* El booleano sigue siendo el de siempre: «¿hay turno en vuelo en la que se está
|
|
268
|
+
* mirando?». Lo que se ensanchó es el DISPARADOR, y hacía falta: sin eso, un turno de
|
|
269
|
+
* segundo plano que arranca o termina no repintaría la barra, que es donde se ve qué
|
|
270
|
+
* sesión está trabajando.
|
|
271
|
+
*
|
|
272
|
+
* `deLaDeAhora` existe por una trampa de orden: el primer flanco de un turno puede
|
|
273
|
+
* ocurrir ANTES de que la consola esté en el mapa —`construirConsolaDeProyecto` arranca
|
|
274
|
+
* su lazo dentro, y un `correr` que ejecute un turno en el acto (los tests, y cualquier
|
|
275
|
+
* ejecutor que no espere una línea) lo dispara desde ahí—. Preguntándole al mapa se
|
|
276
|
+
* habría contestado «no hay turno» justo en el flanco que lo anuncia. Quien llama sí sabe
|
|
277
|
+
* de qué consola habla, así que lo dice.
|
|
278
|
+
*/
|
|
279
|
+
const avisarDeLasAbiertas = (deLaDeAhora) => {
|
|
280
|
+
const enfocada = deLaDeAhora !== undefined && deLaDeAhora.raiz === enFoco
|
|
281
|
+
? deLaDeAhora.turnoEnVuelo
|
|
282
|
+
: consolaEnFoco()?.turnoEnVuelo === true;
|
|
283
|
+
escuchaDeTurno?.(enfocada);
|
|
284
|
+
};
|
|
285
|
+
const entornoPorId = (id) => {
|
|
286
|
+
const encontrado = registrados.find((e) => e.id === id);
|
|
287
|
+
if (encontrado === undefined) {
|
|
288
|
+
throw new Error(`el entorno «${id}» no está registrado en settings.json`);
|
|
289
|
+
}
|
|
290
|
+
return encontrado;
|
|
291
|
+
};
|
|
292
|
+
/** Escribe la credencial y la RECUERDA, sin decir nada: quien llama decide si lo dice,
|
|
293
|
+
* porque `asistenteDeModelo` ya lo dice por su cuenta y decirlo dos veces es ruido. */
|
|
294
|
+
const registrarCredencial = (proveedor, clave) => {
|
|
295
|
+
const guardada = opciones.guardarCredencial(proveedor, clave);
|
|
296
|
+
credencialEscrita = { proveedor, ruta: guardada.ruta };
|
|
297
|
+
return guardada;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* El nombre BUENO del entorno, el que dice el propio servidor, cuando por fin se ha
|
|
301
|
+
* hablado con él.
|
|
302
|
+
*
|
|
303
|
+
* Del alta solo sale la URL, así que hasta aquí el nombre era el host — verdad
|
|
304
|
+
* comprobable, pero fea («mcp.casa.local»). El `serverInfo` del initialize llega en la
|
|
305
|
+
* primera conexión de verdad, que es `proyectosDe`, y solo entonces se puede sustituir.
|
|
306
|
+
*
|
|
307
|
+
* Dos guardas, y las dos importan:
|
|
308
|
+
* - Solo se pisa un nombre DEDUCIDO (el que sigue siendo igual al host). Un nombre que
|
|
309
|
+
* puso una persona —o el de un entorno oficial, «XOne WebStudio»— no lo cambia un
|
|
310
|
+
* servidor remoto por su cuenta.
|
|
311
|
+
* - El id NO se toca nunca. Es un segmento de ruta (`rutaDeWorkspace`) y ya hay una copia
|
|
312
|
+
* local colgando de él: cambiarlo aquí sería mudar la carpeta del proyecto de sitio
|
|
313
|
+
* porque el servidor decidió llamarse de otra forma.
|
|
314
|
+
*/
|
|
315
|
+
const renombrarConElServidor = (entorno, nombreDelServidor) => {
|
|
316
|
+
if (nombreDelServidor === undefined || nombreDelServidor === "")
|
|
317
|
+
return;
|
|
318
|
+
if (entorno.nombre !== hostDeUrl(entorno.url))
|
|
319
|
+
return;
|
|
320
|
+
if (nombreDelServidor === entorno.nombre)
|
|
321
|
+
return;
|
|
322
|
+
const renombrado = { ...entorno, nombre: nombreDelServidor };
|
|
323
|
+
opciones.guardarEntorno(renombrado);
|
|
324
|
+
const donde = registrados.findIndex((e) => e.id === entorno.id);
|
|
325
|
+
if (donde >= 0)
|
|
326
|
+
registrados.splice(donde, 1, renombrado);
|
|
327
|
+
informar(`entorno «${entorno.id}»: el servidor dice llamarse «${nombreDelServidor}»`);
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Cierra la consola de ESA raíz, si hay alguna.
|
|
331
|
+
*
|
|
332
|
+
* Se ESPERA a que el lazo retorne. Dos `correrConsola` vivos sobre la misma copia de
|
|
333
|
+
* trabajo comparten el ejecutor real y se pisarían el hilo — que es exactamente lo que la
|
|
334
|
+
* clave por raíz del mapa impide, y esto es la otra mitad: se saca del mapa DESPUÉS de
|
|
335
|
+
* que su lazo haya terminado, así que entre medias nadie la puede reclamar como libre.
|
|
336
|
+
*/
|
|
337
|
+
const cerrarConsolaDeProyecto = async (raiz) => {
|
|
338
|
+
const consola = abiertas.get(raiz);
|
|
339
|
+
if (consola === undefined)
|
|
340
|
+
return;
|
|
341
|
+
await consola.cerrar();
|
|
342
|
+
abiertas.delete(raiz);
|
|
343
|
+
if (enFoco === raiz)
|
|
344
|
+
enFoco = undefined;
|
|
345
|
+
};
|
|
346
|
+
/** Todas: lo que hacen `cancelar()` y `cerrar()`. Un fallo al cerrar una no puede dejar a
|
|
347
|
+
* las demás vivas — es la misma regla que `cerrarLasDeTareas`. */
|
|
348
|
+
const cerrarLasDeProyecto = async () => {
|
|
349
|
+
for (const raiz of [...abiertas.keys()]) {
|
|
350
|
+
await cerrarConsolaDeProyecto(raiz).catch(() => abiertas.delete(raiz));
|
|
351
|
+
}
|
|
352
|
+
enFoco = undefined;
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Cierra las consolas de segundo plano que NO están trabajando, menos la de esa raíz.
|
|
356
|
+
*
|
|
357
|
+
* Es la otra mitad del acotado, y va ANTES de construir la nueva: lo que se queda vivo en
|
|
358
|
+
* segundo plano es solo lo que tiene un turno en marcha. Sin esto, visitar diez proyectos
|
|
359
|
+
* dejaría diez `correrConsola` corriendo para siempre — y de paso se conserva el orden
|
|
360
|
+
* que ya estaba probado: el lazo anterior TERMINA antes de que arranque el siguiente, que
|
|
361
|
+
* es lo normal porque lo normal es cambiar de sesión sin nada en vuelo.
|
|
362
|
+
*/
|
|
363
|
+
const cerrarLasOciosasSalvo = async (raizQueSigue) => {
|
|
364
|
+
for (const [raiz, consola] of [...abiertas]) {
|
|
365
|
+
if (raiz === raizQueSigue)
|
|
366
|
+
continue;
|
|
367
|
+
if (consola.turnoEnVuelo && !consola.cerrada)
|
|
368
|
+
continue;
|
|
369
|
+
await cerrarConsolaDeProyecto(raiz).catch(() => abiertas.delete(raiz));
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Una consola de segundo plano que acaba de terminar su turno ya no hace falta: se cierra.
|
|
374
|
+
*
|
|
375
|
+
* Es lo que hace que «varias consolas vivas» sea acotado en vez de una fuga: sin esto,
|
|
376
|
+
* cada proyecto que alguien visitara dejaría un `correrConsola` corriendo para siempre.
|
|
377
|
+
* Y no se pierde nada al cerrarla — el hilo del agente lo reanuda el checkpointer cuando
|
|
378
|
+
* esa sesión se vuelva a abrir; lo único que se paga es el arranque.
|
|
379
|
+
*
|
|
380
|
+
* Va POR LA COLA y no en el acto: quien llama es el `finally` del propio turno, y
|
|
381
|
+
* `cerrar()` aguarda `terminada`, o sea el lazo que todavía está dentro de ese turno.
|
|
382
|
+
* Encolarlo lo deja para cuando la pasada haya devuelto. Las tres condiciones se vuelven a
|
|
383
|
+
* mirar ahí dentro, porque entre medias el foco puede haber vuelto a este proyecto o
|
|
384
|
+
* puede haber empezado otro turno.
|
|
385
|
+
*/
|
|
386
|
+
const cerrarSiSobra = (raiz) => {
|
|
387
|
+
void enCola(async () => {
|
|
388
|
+
if (enFoco === raiz)
|
|
389
|
+
return;
|
|
390
|
+
const consola = abiertas.get(raiz);
|
|
391
|
+
if (consola === undefined || consola.turnoEnVuelo)
|
|
392
|
+
return;
|
|
393
|
+
if (!consola.cerrada)
|
|
394
|
+
await cerrarConsolaDeProyecto(raiz);
|
|
395
|
+
else
|
|
396
|
+
abiertas.delete(raiz);
|
|
397
|
+
// Se vuelve a avisar: esta raíz acaba de quedar LIBRE, y de eso cuelgan la barra y la
|
|
398
|
+
// cola de tareas («gana la persona» deja de aplicar aquí). Avisar solo en el flanco
|
|
399
|
+
// del turno habría contado la raíz como ocupada, porque este cierre viene después.
|
|
400
|
+
avisarDeLasAbiertas();
|
|
401
|
+
}).catch(() => undefined);
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* La cola de las operaciones que tocan `abiertas`.
|
|
405
|
+
*
|
|
406
|
+
* «Una consola por raíz» no se sostiene sin serializar: dos `POST` que lleguen a la vez se
|
|
407
|
+
* entrelazan en el `await` del cierre, los dos ven la raíz libre, los dos arrancan un
|
|
408
|
+
* `correrConsola` y el primero se queda vivo y sin nadie que lo pueda cerrar nunca. La
|
|
409
|
+
* cola se traga los rechazos —si no, un fallo
|
|
410
|
+
* dejaría la cadena rota para todo lo que venga detrás— y el rechazo de verdad se
|
|
411
|
+
* devuelve sin tragar a quien llamó.
|
|
412
|
+
*/
|
|
413
|
+
let cola = Promise.resolve();
|
|
414
|
+
const enCola = (operacion) => {
|
|
415
|
+
const resultado = cola.then(operacion);
|
|
416
|
+
cola = resultado.catch(() => undefined);
|
|
417
|
+
return resultado;
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Construye una consola de proyecto — y NADA de la contabilidad del cable.
|
|
421
|
+
*
|
|
422
|
+
* Se extrajo de `abrirDeVerdad` cuando apareció la segunda puerta (`abrirParaTarea`), y
|
|
423
|
+
* la línea del corte es exactamente esa: aquí dentro está TODO lo que decide con qué
|
|
424
|
+
* barreras corre el agente —el ejecutor real por `crearEjecutor`, de donde cuelga el
|
|
425
|
+
* backend con las vistas aplanadas retiradas y la guarda de artefactos; las dependencias
|
|
426
|
+
* de la raíz; la foto del antes; el volcado— y fuera se queda solo quién está enganchado
|
|
427
|
+
* al cable (`abierto`, y el cierre de lo anterior).
|
|
428
|
+
*
|
|
429
|
+
* Que sea UNA función no es comodidad: si las dos puertas divergieran, una tarea correría
|
|
430
|
+
* con menos barreras que una persona, y eso no se ve — se descubre cuando el agente ya
|
|
431
|
+
* escribió. Lo único que `alCable` apaga son los dos AVISOS al navegador; ni una regla.
|
|
432
|
+
*/
|
|
433
|
+
const construirConsolaDeProyecto = async ({ raiz, sesion, adjuntos, tarea, enElCable, alFlancoDeTurno, }) => {
|
|
434
|
+
const reabierta = sesion === undefined ? undefined : sesiones.reabrir(raiz, sesion);
|
|
435
|
+
/**
|
|
436
|
+
* Lo que costó esta conversación ANTES de este proceso, leído de los `fin` que el
|
|
437
|
+
* `.jsonl` ya tenía.
|
|
438
|
+
*
|
|
439
|
+
* **Se lee UNA vez, aquí, y no se vuelve a mirar.** El fichero CRECE por debajo mientras
|
|
440
|
+
* este proceso vive —`volcar()` anota cada turno—, así que releerlo a mitad de sesión
|
|
441
|
+
* contaría dos veces los turnos que ya están en el tracker vivo. Esa es la regla que se
|
|
442
|
+
* puede romper en silencio dentro de un año, y por eso la base solo se fija en este
|
|
443
|
+
* punto: a partir de aquí, lo que crezca lo aporta la sesión viva.
|
|
444
|
+
*
|
|
445
|
+
* `undefined` cuando ningún `fin` lo trae, que es una sesión escrita antes de que esto
|
|
446
|
+
* existiera. No se rellena con ceros: un cero que nadie ha medido es la cifra inventada
|
|
447
|
+
* de siempre, y aquí se distinguen «anterior» y «gastó cero».
|
|
448
|
+
*/
|
|
449
|
+
const consumoHistorico = reabierta === undefined ? undefined : consumoDeLosActos(reabierta.actos);
|
|
450
|
+
/**
|
|
451
|
+
* La sesión real, que nace DENTRO del primer turno —`crearEjecutorReal` avisa después de
|
|
452
|
+
* `inspeccionar` y de `abrirSesionReal`—. Se declara aquí arriba, antes de construir la
|
|
453
|
+
* consola, porque quien pregunta por el consumo se construye ANTES que ella.
|
|
454
|
+
*/
|
|
455
|
+
let sesionReal;
|
|
456
|
+
/**
|
|
457
|
+
* Lo consumido por la sesión VIVA, sin la base histórica.
|
|
458
|
+
*
|
|
459
|
+
* **Es la fuente de la piel, y no la de la pantalla.** Son dos lecturas distintas a
|
|
460
|
+
* propósito: la piel resta este valor contra el que estampó en el `fin` anterior para
|
|
461
|
+
* sacar el gasto del turno, y si aquí entrara la base histórica el PRIMER `fin` de una
|
|
462
|
+
* sesión reabierta estamparía la base entera como si se hubiera gastado otra vez — y
|
|
463
|
+
* reabrir tres veces triplicaría el total. Lo que se pinta es la suma, y esa se compone
|
|
464
|
+
* abajo.
|
|
465
|
+
*/
|
|
466
|
+
const consumoVivo = () => {
|
|
467
|
+
const vivo = sesionReal?.consumo?.();
|
|
468
|
+
return vivo === undefined ? undefined : consumoPersistible(vivo);
|
|
469
|
+
};
|
|
470
|
+
const consolaWeb = crearConsola({
|
|
471
|
+
catalogoModelos: opciones.catalogoModelos,
|
|
472
|
+
guardarModeloGlobal,
|
|
473
|
+
...(opciones.msDeEspera === undefined ? {} : { msDeEspera: opciones.msDeEspera }),
|
|
474
|
+
consumoAcumulado: consumoVivo,
|
|
475
|
+
});
|
|
476
|
+
// `Partial<Consola>` sobre el objeto recién creado: lo que depende de la raíz (`/sync`,
|
|
477
|
+
// los escritores del proyecto) no lo puede saber `consolaWeb`, que no conoce ninguna.
|
|
478
|
+
Object.assign(consolaWeb.consola, opciones.dependenciasDeProyecto?.(raiz) ?? {});
|
|
479
|
+
// La foto del ANTES se toma AQUÍ, al abrir, no en el primer volcado: para entonces el
|
|
480
|
+
// agente ya habría escrito y el «antes» incluiría su trabajo. Se nombra más abajo,
|
|
481
|
+
// cuando el id existe. Se lanza sin esperar —abrir no puede quedarse esperando a git— y
|
|
482
|
+
// el rechazo se traga: sin marca, la vista lo dice.
|
|
483
|
+
const foto = opciones.marcarSesion?.(raiz).catch(() => undefined);
|
|
484
|
+
// La misma regla que la foto de arriba, en el mismo instante y por el mismo motivo:
|
|
485
|
+
// medido después, el «ya había trabajo de alguien» incluiría el de esta sesión. Se
|
|
486
|
+
// lanza sin esperar y el rechazo se traga: es un dato accesorio y abrir un proyecto no
|
|
487
|
+
// puede depender de que git conteste.
|
|
488
|
+
const trabajoAlAbrir = Promise.resolve(opciones.sinCommitear?.(raiz)).catch(() => undefined);
|
|
489
|
+
/**
|
|
490
|
+
* La ref ya APUNTADA, para poder esperarla al cerrar.
|
|
491
|
+
*
|
|
492
|
+
* `volcar()` la lanza y no la aguarda —es síncrono y corre en el `finally` de cada
|
|
493
|
+
* turno—, y eso está bien para quien mira: la pestaña Revisión pregunta cuando alguien
|
|
494
|
+
* la abre, mucho después. Lo que no vale es para quien MIDE: el corredor de tareas
|
|
495
|
+
* comprueba que lo escrito se pueda revisar justo después de cerrar la consola, y
|
|
496
|
+
* medido con git de verdad la ref todavía no estaba —`cambiosDeSesion` devolvía
|
|
497
|
+
* `sin-marca`— así que TODA tarea se habría aparcado diciendo que nadie puede revisarla
|
|
498
|
+
* en un proyecto donde sí se puede. Guardarla aquí y esperarla en `cerrar()` es lo que
|
|
499
|
+
* lo cierra sin que abrir un proyecto tenga que aguardar a git.
|
|
500
|
+
*/
|
|
501
|
+
let marcado;
|
|
502
|
+
/**
|
|
503
|
+
* El id de la sesión, decidido al ABRIR y no en el primer volcado.
|
|
504
|
+
*
|
|
505
|
+
* Es el `thread_id` del grafo, y sin esa igualdad no hay nada que reanudar: al reabrir
|
|
506
|
+
* hay que preguntarle al checkpointer por la misma cadena con la que se escribió. Antes
|
|
507
|
+
* nacía en `volcar()` y el hilo era otro uuid, así que reabrir una sesión abría un hilo
|
|
508
|
+
* que no existía en ninguna parte.
|
|
509
|
+
*
|
|
510
|
+
* Lo que sigue siendo PEREZOSO es la entrada del índice (`anotada`): un uuid en memoria
|
|
511
|
+
* no ensucia nada, pero una entrada escrita al abrir dejaría una sesión vacía en la
|
|
512
|
+
* barra cada vez que alguien mira un proyecto y se va sin decir nada. Y un hilo sin
|
|
513
|
+
* turnos tampoco deja checkpoint: LangGraph solo escribe cuando el grafo corre.
|
|
514
|
+
*/
|
|
515
|
+
const idSesion = sesion ?? randomUUID();
|
|
516
|
+
/** ¿Tiene ya entrada en el índice? Reabrir implica que sí. */
|
|
517
|
+
let anotada = sesion !== undefined;
|
|
518
|
+
/**
|
|
519
|
+
* Reabrir ya no implica releer: si el checkpointer guarda ese hilo, la conversación
|
|
520
|
+
* CONTINÚA de verdad y el aviso no sale. Solo se pregunta al reabrir —una sesión nueva
|
|
521
|
+
* no puede tener memoria— y una respuesta negativa deja el aviso puesto, que es el lado
|
|
522
|
+
* conservador: enseñarlo de más es preferible a prometer una memoria que no está.
|
|
523
|
+
*/
|
|
524
|
+
let historica = reabierta?.historica ?? false;
|
|
525
|
+
if (historica && (await opciones.hayMemoriaDeHilo?.(raiz, sesion)) === true)
|
|
526
|
+
historica = false;
|
|
527
|
+
let dispositivo = reabierta?.dispositivo;
|
|
528
|
+
let cerrada = false;
|
|
529
|
+
/** Ver `ConsolaDeProyecto.turnoEnVuelo`: lo pone y lo quita el envoltorio de abajo, que
|
|
530
|
+
* es el único sitio que ve los dos flancos. */
|
|
531
|
+
let turnoEnVuelo = false;
|
|
532
|
+
let volcados = 0;
|
|
533
|
+
/**
|
|
534
|
+
* Marcado desde que empieza el cierre. Hace falta porque `crearEjecutorReal` avisa de
|
|
535
|
+
* la sesión DESPUÉS de `inspeccionar` y `abrirSesionReal` —segundos en el primer
|
|
536
|
+
* turno—, así que un cierre en esa ventana encontraba `sesionReal` sin definir, no
|
|
537
|
+
* abortaba nada, y el turno seguía corriendo entero sobre un proyecto que el usuario
|
|
538
|
+
* ya había dejado. La sesión que llega tarde se cierra en cuanto se anuncia.
|
|
539
|
+
*/
|
|
540
|
+
let cerrando = false;
|
|
541
|
+
const base = opciones.crearEjecutor?.((s) => {
|
|
542
|
+
sesionReal = s;
|
|
543
|
+
if (cerrando)
|
|
544
|
+
s.cerrar();
|
|
545
|
+
/**
|
|
546
|
+
* La cuenta de tokens se escucha AQUÍ y no al construir la consola porque la sesión
|
|
547
|
+
* se anuncia tarde: `crearEjecutorReal` avisa después de `inspeccionar` y
|
|
548
|
+
* `abrirSesionReal`, o sea segundos dentro del primer turno. Suscribirse antes sería
|
|
549
|
+
* suscribirse a algo que no existe.
|
|
550
|
+
*
|
|
551
|
+
* Solo se avisa si esta consola es la que el cable está pintando: los tokens de un
|
|
552
|
+
* turno de segundo plano no pueden cambiar el contador que alguien está mirando en
|
|
553
|
+
* otra sesión — la misma regla que hace que sus actos no se cuelen en ese chat.
|
|
554
|
+
*/
|
|
555
|
+
s.alCambiarConsumo?.(() => {
|
|
556
|
+
if (enFoco === raiz)
|
|
557
|
+
escuchaDeConsumo?.();
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
// Solo si la hay: `undefined` es lo que reciben las aperturas de persona, y de ahí
|
|
561
|
+
// sale que su agente no tenga `/adjuntos/` montada.
|
|
562
|
+
adjuntos === undefined ? undefined : { adjuntos });
|
|
563
|
+
// El ejecutor que de verdad va a correr los turnos de ESTA consola de proyecto — se
|
|
564
|
+
// fija UNA vez, aquí, para que `volcar` y `ejecutarTurno` miren siempre el mismo valor.
|
|
565
|
+
const ejecutorEfectivo = base ?? ejecutarTurnoGuionizado;
|
|
566
|
+
/**
|
|
567
|
+
* Vuelca al `.jsonl` los actos nuevos.
|
|
568
|
+
*
|
|
569
|
+
* Se llama en las FRONTERAS de turno y al cerrar, nunca a mitad: durante un turno el
|
|
570
|
+
* último acto de la piel todavía muta (el cierre de una racha de tools SUSTITUYE a su
|
|
571
|
+
* apertura dentro del mismo acto, `core/actos.ts#conLineaDeTool`), y un `.jsonl` que
|
|
572
|
+
* solo sabe anexar guardaría las dos líneas. El coste, medido y asumido: un cierre
|
|
573
|
+
* abrupto a mitad de turno pierde ese turno, no la sesión.
|
|
574
|
+
*
|
|
575
|
+
* La sesión se crea PEREZOSAMENTE, en el primer volcado: un proyecto que se abre y se
|
|
576
|
+
* cierra sin decir nada no deja una sesión vacía en el índice.
|
|
577
|
+
*
|
|
578
|
+
* Un ejecutor de PEGA (`--guion`, sin `crearEjecutor`) no vuelca NADA: el `.jsonl` de
|
|
579
|
+
* una sesión es historia del proyecto, y un transcript de pega indistinguible de uno
|
|
580
|
+
* real la ensuciaría — mañana alguien la reabre, lee la respuesta guionizada y no
|
|
581
|
+
* puede saber si fue el agente de pega o un fallo del de verdad. La decisión la toma
|
|
582
|
+
* la marca `ES_DOBLE` sobre `ejecutarTurnoGuionizado` (`cli/consola.ts`), nunca un
|
|
583
|
+
* booleano de este cierre ni la bandera `--guion` del argv: la misma razón por la que
|
|
584
|
+
* el resto del repo marca los dobles con un Symbol y no con un campo que alguien puede
|
|
585
|
+
* olvidar o poner mal. La guarda vive AQUÍ, en el único sitio que escribe a disco, y no
|
|
586
|
+
* repartida en cada llamador — `ejecutarTurno` de abajo y el `cerrar()` de más abajo
|
|
587
|
+
* comparten la misma `volcar`, así que ninguno de los dos puede colarse sin ella.
|
|
588
|
+
*/
|
|
589
|
+
const volcar = () => {
|
|
590
|
+
if (esDoble(ejecutorEfectivo))
|
|
591
|
+
return;
|
|
592
|
+
const todos = consolaWeb.actos();
|
|
593
|
+
if (todos.length <= volcados)
|
|
594
|
+
return;
|
|
595
|
+
if (!anotada) {
|
|
596
|
+
sesiones.crear(raiz, idSesion, tarea);
|
|
597
|
+
anotada = true;
|
|
598
|
+
// Ahora sí hay una sesión en el índice a la que apuntar la foto de la apertura.
|
|
599
|
+
// Se guarda la promesa en vez de tirarla: `cerrar()` la espera. Ver `marcado`.
|
|
600
|
+
marcado = foto?.then((apuntar) => apuntar?.(idSesion));
|
|
601
|
+
}
|
|
602
|
+
for (const acto of todos.slice(volcados))
|
|
603
|
+
sesiones.anotar(raiz, idSesion, acto);
|
|
604
|
+
volcados = todos.length;
|
|
605
|
+
// El dispositivo elegido ANTES de que la sesión estuviera en el índice se anota
|
|
606
|
+
// ahora, que es la primera vez que hay una entrada a la que apuntarlo. Sin esto,
|
|
607
|
+
// elegir dispositivo y hablar después perdía la elección al reabrir.
|
|
608
|
+
if (dispositivo !== undefined)
|
|
609
|
+
sesiones.elegirDispositivo?.(raiz, idSesion, dispositivo);
|
|
610
|
+
};
|
|
611
|
+
/**
|
|
612
|
+
* El mensaje del commit del turno.
|
|
613
|
+
*
|
|
614
|
+
* Lleva el TÍTULO de la sesión, que es el asidero con el que una persona la reconoce en
|
|
615
|
+
* la barra, y el id de la tarea cuando la escribió una: es lo que permite leer el
|
|
616
|
+
* historial y saber qué salió de algo que corría solo.
|
|
617
|
+
*
|
|
618
|
+
* Lo que NO dice es «lo escribió el agente». El commit barre todo lo que haya cambiado,
|
|
619
|
+
* incluido lo que tocara una persona a mano en esa carpeta durante el turno — la misma
|
|
620
|
+
* honestidad que separa `Tarea.autorizadas` de «aplicados».
|
|
621
|
+
*/
|
|
622
|
+
const mensajeDeCommit = () => {
|
|
623
|
+
const titulo = sesiones.listar(raiz).find((e) => e.id === idSesion)?.titulo;
|
|
624
|
+
return `xonecode: ${titulo === undefined || titulo === "" ? "sesión sin título" : titulo}${tarea === undefined ? "" : ` [tarea ${tarea}]`}`;
|
|
625
|
+
};
|
|
626
|
+
const ejecutarTurno = async (peticion, estado, consola) => {
|
|
627
|
+
/**
|
|
628
|
+
* La raíz del estado tiene que ser la de ESTA consola, y aquí se comprueba.
|
|
629
|
+
*
|
|
630
|
+
* Desde que `ejecutarTurno` se expone (`ConsolaDeProyecto.ejecutarTurno`, para las
|
|
631
|
+
* tareas), el estado lo puede construir quien llama — y de `estado.raiz` sale el
|
|
632
|
+
* backend del agente (`crearEjecutorReal` → `abrirSesionReal` → `backendDeAgente`).
|
|
633
|
+
* Una raíz equivocada no da error: hace que el agente lea y ESCRIBA en otro proyecto,
|
|
634
|
+
* con la aprobación de ese otro y sin un solo síntoma. Falla cerrado, como todo lo
|
|
635
|
+
* que decide sobre escrituras.
|
|
636
|
+
*
|
|
637
|
+
* Por la puerta de las personas esto no puede saltar nunca: el estado lo lleva
|
|
638
|
+
* `correrConsola` y ningún comando cambia `raiz` (medido en `cli/consola.ts`: solo
|
|
639
|
+
* `fuentes`, `seleccionesDeCatalogo` y `hilo`). Por eso la guarda va en el envoltorio
|
|
640
|
+
* COMPARTIDO y no solo en la puerta nueva — una guarda que solo vigila una puerta es
|
|
641
|
+
* la divergencia que esto viene a evitar.
|
|
642
|
+
*/
|
|
643
|
+
if (estado.raiz !== raiz) {
|
|
644
|
+
throw new Error("ese turno trae la raíz de otro proyecto: esta consola solo corre turnos sobre la suya");
|
|
645
|
+
}
|
|
646
|
+
// El primer turno NUEVO es lo que deja de ser histórica una sesión reabierta.
|
|
647
|
+
// `sesiones.ts` no puede hacerlo —`reabrirSesion` es una lectura pura sin estado
|
|
648
|
+
// entre llamadas—, así que lo hace quien es dueño de la sesión viva, que es esto.
|
|
649
|
+
historica = false;
|
|
650
|
+
// Este envoltorio es el ÚNICO sitio que sabe cuándo empieza y cuándo acaba un turno:
|
|
651
|
+
// `correrConsola` solo lo espera y la piel solo ve eventos. De aquí sale lo que apaga
|
|
652
|
+
// el compositor, saca el botón de parar y enciende el borde vivo.
|
|
653
|
+
turnoEnVuelo = true;
|
|
654
|
+
consolaWeb.turno(true);
|
|
655
|
+
alFlancoDeTurno?.(true);
|
|
656
|
+
try {
|
|
657
|
+
// Se DEVUELVE lo que el turno informe. Sin este `return`, el canal que
|
|
658
|
+
// `crearEjecutorReal` acaba de abrir moría en este envoltorio: el corredor de
|
|
659
|
+
// tareas no tendría con qué medir si lo hecho es entregable, y ninguna piel se
|
|
660
|
+
// enteraría — que es exactamente cómo el `terminada` falso pasó desapercibido.
|
|
661
|
+
return await ejecutorEfectivo(peticion, estado, consola);
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
// En el `finally`: un turno que revienta o que se cancela también TERMINA, y dejar
|
|
665
|
+
// el compositor apagado para siempre sería peor que no haberlo apagado nunca.
|
|
666
|
+
turnoEnVuelo = false;
|
|
667
|
+
consolaWeb.turno(false);
|
|
668
|
+
alFlancoDeTurno?.(false);
|
|
669
|
+
volcar();
|
|
670
|
+
// Y el commit, DESPUÉS de volcar: así la entrada del índice ya existe y su título
|
|
671
|
+
// —que es lo que da nombre al commit— es el de verdad.
|
|
672
|
+
//
|
|
673
|
+
// Se ESPERA, al contrario que la ref de la foto: medido sobre una copia del
|
|
674
|
+
// proyecto real del usuario (165 MB, 41 colecciones), `add -A` + `commit` tardan
|
|
675
|
+
// 74 ms, y quien llama acaba de esperar un turno entero. Y hace falta esperarlo: la
|
|
676
|
+
// puerta de entrega del corredor corre en cuanto el turno devuelve y le pregunta a
|
|
677
|
+
// git, así que un commit suelto correría contra ella.
|
|
678
|
+
//
|
|
679
|
+
// Envuelto entero: esto va en el `finally` del turno, y una excepción aquí se
|
|
680
|
+
// llevaría por delante el cierre —el compositor apagado para siempre—. Un fallo se
|
|
681
|
+
// DICE, que es lo contrario de dejar el árbol sucio en silencio hasta que
|
|
682
|
+
// `/sync subir` se niegue.
|
|
683
|
+
try {
|
|
684
|
+
// El id de sesión va APARTE del mensaje: de él sale el sello con el que después
|
|
685
|
+
// se atribuye el cambio (`agent/sesionGit.ts`), y el título que va en el asunto no
|
|
686
|
+
// identifica nada — dos sesiones se pueden llamar igual, y renombrar una lo cambia.
|
|
687
|
+
const aviso = await opciones.commitearTurno?.(raiz, mensajeDeCommit(), idSesion);
|
|
688
|
+
if (aviso !== undefined)
|
|
689
|
+
informar(aviso);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
informar(`no se pudo commitear el turno: ${error.message}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
// El estado con el que arranca el lazo, guardado ANTES de arrancarlo: es lo que
|
|
697
|
+
// `estadoDeSesion` devuelve mientras nadie haya cambiado nada, y sin él quien pinte el
|
|
698
|
+
// modelo en vigor no tendría qué enseñar hasta el primer `/modelo` — que es justo lo
|
|
699
|
+
// que se quiere evitar (enseñar «no se sabe» cuando sí se sabe).
|
|
700
|
+
let estadoDeSesion = {
|
|
701
|
+
// El hilo ES el id de la sesión: es lo que permite que reabrirla continúe la
|
|
702
|
+
// conversación en vez de releerla. `/nuevo` sigue pudiendo cambiarlo — abre otro hilo
|
|
703
|
+
// dentro de la misma sesión, y eso es exactamente lo que dice que hace.
|
|
704
|
+
hilo: idSesion,
|
|
705
|
+
raiz,
|
|
706
|
+
// Se LEE aquí, una vez por consola y no una por proceso: es lo que hace que una
|
|
707
|
+
// sesión nueva vea el `config.json` de ahora. Ver `OpcionesDelVestibulo.fuentes`.
|
|
708
|
+
fuentes: opciones.fuentes?.() ?? {},
|
|
709
|
+
};
|
|
710
|
+
// `/modelo` y `/modelos` cambian el modelo EN CALIENTE y no tocan disco, así que esta
|
|
711
|
+
// es la única forma de enterarse. Ver `Consola.alEstado`.
|
|
712
|
+
consolaWeb.consola.alEstado = (nuevo) => {
|
|
713
|
+
estadoDeSesion = nuevo;
|
|
714
|
+
// Al cable solo por la puerta de las personas: la escucha reemite el estado de
|
|
715
|
+
// modelos a los navegadores conectados, y el modelo de una tarea no es el que está
|
|
716
|
+
// en vigor para quien mira. El estado LOCAL se actualiza igual — `/modelo` dentro de
|
|
717
|
+
// la tarea tiene que surtir efecto en la tarea.
|
|
718
|
+
if (enElCable())
|
|
719
|
+
escuchaDeEstado?.(nuevo);
|
|
720
|
+
};
|
|
721
|
+
const terminada = correr(consolaWeb.consola, estadoDeSesion, ejecutarTurno).finally(() => {
|
|
722
|
+
cerrada = true;
|
|
723
|
+
});
|
|
724
|
+
// Un lazo que revienta sin que nadie espere `terminada` sería un rechazo sin manejar
|
|
725
|
+
// que tumba el proceso. Se cuenta y se sigue: la consola del vestíbulo sigue viva.
|
|
726
|
+
terminada.catch((error) => {
|
|
727
|
+
informar(`la consola del proyecto terminó con un error: ${error instanceof Error ? error.message : String(error)}`);
|
|
728
|
+
});
|
|
729
|
+
const consolaDeProyecto = {
|
|
730
|
+
raiz,
|
|
731
|
+
/**
|
|
732
|
+
* Lo que lleva consumido su sesión, contando lo de ANTES de este proceso.
|
|
733
|
+
*
|
|
734
|
+
* **Se PREGUNTA a la sesión, no se cachea**: el acumulador vive en el cierre de
|
|
735
|
+
* `abrirSesionReal` y es la única fuente; una copia aquí se quedaría vieja entre
|
|
736
|
+
* avisos. Y se le SUMA la base histórica leída al abrir, que es lo que hace que
|
|
737
|
+
* reabrir una conversación no ponga el contador a cero: sin esto, una sesión volvía
|
|
738
|
+
* sin ninguna cifra hasta su primer turno nuevo.
|
|
739
|
+
*
|
|
740
|
+
* `sesionReal` llega TARDE —`crearEjecutorReal` la anuncia dentro del primer turno—,
|
|
741
|
+
* así que antes de eso solo hay base, y si tampoco la hay se contesta `undefined`, que
|
|
742
|
+
* es «no consta» y hace que no se pinte nada.
|
|
743
|
+
*/
|
|
744
|
+
consumo: () => {
|
|
745
|
+
const vivo = consumoVivo();
|
|
746
|
+
if (vivo === undefined) {
|
|
747
|
+
return consumoHistorico === undefined ? undefined : consumoDeLaSesion(consumoHistorico);
|
|
748
|
+
}
|
|
749
|
+
if (consumoHistorico === undefined)
|
|
750
|
+
return consumoDeLaSesion(vivo);
|
|
751
|
+
return consumoDeLaSesion(sumarConsumo(consumoHistorico, vivo));
|
|
752
|
+
},
|
|
753
|
+
get estadoDeSesion() {
|
|
754
|
+
return estadoDeSesion;
|
|
755
|
+
},
|
|
756
|
+
// Ver `ConsolaDeProyecto.idDeHilo`: este es el id crudo, sin esperar al índice.
|
|
757
|
+
idDeHilo: idSesion,
|
|
758
|
+
get sesion() {
|
|
759
|
+
// Solo cuando está en el ÍNDICE. El id existe desde que se abre —es el hilo—, pero
|
|
760
|
+
// hacia fuera «hay sesión» significa «hay una entrada que la barra puede marcar»:
|
|
761
|
+
// devolver un uuid que no está en ninguna lista haría que la barra buscara una fila
|
|
762
|
+
// que no existe, y que Revisión dijera que la sesión ya empezó sin haber empezado.
|
|
763
|
+
return anotada ? idSesion : undefined;
|
|
764
|
+
},
|
|
765
|
+
get historica() {
|
|
766
|
+
return historica;
|
|
767
|
+
},
|
|
768
|
+
trabajoAlAbrir,
|
|
769
|
+
get dispositivo() {
|
|
770
|
+
return dispositivo;
|
|
771
|
+
},
|
|
772
|
+
elegirDispositivo: (elegido) => {
|
|
773
|
+
dispositivo = elegido;
|
|
774
|
+
// Si todavía no está en el índice, se queda en memoria: `volcar()` lo anotará en
|
|
775
|
+
// cuanto cree la entrada. Escribir aquí una entrada nueva la enseñaría en la barra
|
|
776
|
+
// como una sesión vacía que nadie ha empezado.
|
|
777
|
+
if (anotada)
|
|
778
|
+
sesiones.elegirDispositivo?.(raiz, idSesion, elegido);
|
|
779
|
+
},
|
|
780
|
+
get cerrada() {
|
|
781
|
+
return cerrada;
|
|
782
|
+
},
|
|
783
|
+
get turnoEnVuelo() {
|
|
784
|
+
return turnoEnVuelo;
|
|
785
|
+
},
|
|
786
|
+
consola: consolaWeb,
|
|
787
|
+
cancelarTurno: () => {
|
|
788
|
+
if (sesionReal?.cancelar === undefined)
|
|
789
|
+
return false;
|
|
790
|
+
sesionReal.cancelar();
|
|
791
|
+
return true;
|
|
792
|
+
},
|
|
793
|
+
recibir: (mensaje) => {
|
|
794
|
+
consolaWeb.recibir(mensaje);
|
|
795
|
+
/**
|
|
796
|
+
* Una PROSA del usuario da de alta la sesión en el índice AHORA, no al acabar el
|
|
797
|
+
* turno.
|
|
798
|
+
*
|
|
799
|
+
* `volcar()` era lo único que creaba la entrada, y corre en la frontera del turno:
|
|
800
|
+
* así que la conversación no aparecía en la barra hasta que el asistente terminaba
|
|
801
|
+
* de contestar —minutos— y hasta entonces no había fila que marcar, ni que enseñar
|
|
802
|
+
* trabajando, ni que volver a abrir. Lo dijo el usuario mirando la pantalla.
|
|
803
|
+
*
|
|
804
|
+
* No contradice la pereza que la entrada tenía: esa existía para que «alguien mira
|
|
805
|
+
* un proyecto y se va sin decir nada» no dejara una sesión vacía en la barra, y
|
|
806
|
+
* mandar un mensaje es exactamente decir algo. Lo que sigue siendo perezoso es lo
|
|
807
|
+
* mismo de antes: abrir no escribe nada.
|
|
808
|
+
*
|
|
809
|
+
* **Solo con el turno parado**, y eso es la otra mitad. `volcar` no puede correr a
|
|
810
|
+
* mitad de turno: el último acto de la piel todavía muta —el cierre de una racha de
|
|
811
|
+
* tools SUSTITUYE a su apertura dentro del mismo acto— y el `.jsonl` solo sabe
|
|
812
|
+
* anexar, así que guardaría las dos líneas. Con el turno en vuelo no hace falta:
|
|
813
|
+
* la entrada ya la creó la prosa que lo arrancó. Y el acto de usuario en sí no muta
|
|
814
|
+
* nunca, que es lo que hace seguro volcarlo suelto.
|
|
815
|
+
*/
|
|
816
|
+
if (mensaje.clase === "prosa" && !turnoEnVuelo)
|
|
817
|
+
volcar();
|
|
818
|
+
},
|
|
819
|
+
// El transcript del cliente lleva PRIMERO lo releído y después lo de esta ejecución.
|
|
820
|
+
// Lo releído no vuelve a pasar por `volcar`: ya está en el `.jsonl`.
|
|
821
|
+
conectar: (enviar) => [...(reabierta?.actos ?? []), ...consolaWeb.conectar(enviar)],
|
|
822
|
+
desconectar: (enviar) => consolaWeb.desconectar(enviar),
|
|
823
|
+
soltar: (enviar) => consolaWeb.soltar(enviar),
|
|
824
|
+
mensajesDeAprobacion: () => consolaWeb.mensajesDeAprobacion(),
|
|
825
|
+
actos: () => [...(reabierta?.actos ?? []), ...consolaWeb.actos()],
|
|
826
|
+
cerrar: async () => {
|
|
827
|
+
// El orden es el que evita que cerrar cuelgue: `consolaWeb.cerrar()` pone el EOF,
|
|
828
|
+
// pero `correrConsola` no lo mira hasta que el turno en vuelo devuelve, y un turno
|
|
829
|
+
// en vuelo puede tardar minutos. Abortar la sesión real primero es lo que lo
|
|
830
|
+
// desbloquea — y es para lo que existe `SesionReal.cerrar()`.
|
|
831
|
+
cerrando = true;
|
|
832
|
+
sesionReal?.cerrar();
|
|
833
|
+
consolaWeb.cerrar();
|
|
834
|
+
await terminada.catch(() => 0);
|
|
835
|
+
volcar();
|
|
836
|
+
},
|
|
837
|
+
/**
|
|
838
|
+
* Espera a que la ref de la sesión esté ESCRITA, para quien vaya a mirarla.
|
|
839
|
+
*
|
|
840
|
+
* Estuvo un rato dentro de `cerrar()` y era una regresión para quien está sentado
|
|
841
|
+
* delante: cerrar un proyecto pasaba a aguardar un `git add -A` + `write-tree` sobre
|
|
842
|
+
* el árbol entero, o sea una pausa visible en un proyecto grande — y por un camino
|
|
843
|
+
* que no la necesita. Quien la necesita es el CORREDOR de tareas, que mide la
|
|
844
|
+
* condición de «revisable» justo después de cerrar; así que la espera la hace él y no
|
|
845
|
+
* se le cobra a nadie más. Ver `marcado`.
|
|
846
|
+
*/
|
|
847
|
+
esperarMarca: async () => void (await marcado?.catch(() => undefined)),
|
|
848
|
+
// El MISMO objeto que se le acaba de pasar a `correr`, no un segundo envoltorio: dos
|
|
849
|
+
// versiones del turno divergen en la primera corrección que solo toque a una.
|
|
850
|
+
ejecutarTurno,
|
|
851
|
+
terminada,
|
|
852
|
+
};
|
|
853
|
+
return consolaDeProyecto;
|
|
854
|
+
};
|
|
855
|
+
const abrirDeVerdad = async (apertura) => {
|
|
856
|
+
/**
|
|
857
|
+
* ANTES de cerrar nada: si esa conversación es la de una tarea en curso, no se abre.
|
|
858
|
+
*
|
|
859
|
+
* Es la MISMA guarda que `borrarSesion`, con el mismo motivo de fondo —el hilo lo está
|
|
860
|
+
* escribiendo alguien— y sobre la misma fuente (`deTareas`, que es quien sabe qué
|
|
861
|
+
* consolas están vivas). Lo que pasaba sin ella: el `thread_id` del checkpointer ES el id
|
|
862
|
+
* de la sesión, así que abrirla aquí ponía DOS consolas escribiendo el mismo hilo, con el
|
|
863
|
+
* agente a mitad de turno en una de ellas. El botón existía desde siempre (el título de
|
|
864
|
+
* la tarjeta del kanban), y con «Ver lo que hace» a su lado un botón correcto pegado a uno
|
|
865
|
+
* peligroso enseña que los dos son igual de seguros.
|
|
866
|
+
*
|
|
867
|
+
* **El orden es la mitad del arreglo**: abajo se cierra la consola que ocupe esa raíz,
|
|
868
|
+
* así que una guarda puesta después habría cerrado la sesión de la persona para después
|
|
869
|
+
* negarse a abrir la otra — dos daños en vez de ninguno.
|
|
870
|
+
*
|
|
871
|
+
* Se compara con `sesion` Y con `idDeHilo` porque son dos momentos de la misma
|
|
872
|
+
* conversación: `sesion` no existe hasta que se vuelca el primer acto, y `idDeHilo` desde
|
|
873
|
+
* el primer instante. El que viaja en la tarjeta es el primero; el que cubre la ventana
|
|
874
|
+
* anterior al volcado es el segundo.
|
|
875
|
+
*/
|
|
876
|
+
if (apertura.sesion !== undefined && esDeUnaTareaEnCurso(apertura.raiz, apertura.sesion)) {
|
|
877
|
+
throw new Error(MOTIVO_SESION_DE_TAREA_EN_CURSO);
|
|
878
|
+
}
|
|
879
|
+
// Poda: una consola cuyo lazo ya terminó por su cuenta (un `/salir`, un fallo del lazo)
|
|
880
|
+
// no puede seguir ocupando su raíz. Es la misma poda perezosa que `deTareas`, y aquí es
|
|
881
|
+
// lo que impide que un proyecto quede inabrible para siempre.
|
|
882
|
+
for (const [raiz, vieja] of [...abiertas])
|
|
883
|
+
if (vieja.cerrada)
|
|
884
|
+
abiertas.delete(raiz);
|
|
885
|
+
const yaAbierta = abiertas.get(apertura.raiz);
|
|
886
|
+
if (yaAbierta !== undefined) {
|
|
887
|
+
/**
|
|
888
|
+
* La MISMA sesión de un proyecto que ya está abierto: solo se muda el foco.
|
|
889
|
+
*
|
|
890
|
+
* Es el caso corriente de volver a ella desde la barra, y cerrarla para reabrirla
|
|
891
|
+
* sería tirar el turno que quizá está corriendo — justo lo que este cambio existe para
|
|
892
|
+
* evitar. Se compara con `sesion` Y con `idDeHilo` por lo mismo que la guarda de las
|
|
893
|
+
* tareas: son dos momentos de la misma conversación, y el segundo cubre la ventana
|
|
894
|
+
* anterior al primer volcado.
|
|
895
|
+
*/
|
|
896
|
+
const laMisma = apertura.sesion !== undefined &&
|
|
897
|
+
(yaAbierta.sesion === apertura.sesion || yaAbierta.idDeHilo === apertura.sesion);
|
|
898
|
+
/**
|
|
899
|
+
* Abrir el PROYECTO —sin nombrar sesión— que está trabajando es VOLVER a lo que está
|
|
900
|
+
* pasando, no pedir una conversación nueva.
|
|
901
|
+
*
|
|
902
|
+
* Y es la única forma de llegar: el id de una sesión nace al volcar su primer acto, o
|
|
903
|
+
* sea al final del turno, así que la conversación que acabas de arrancar no tiene fila
|
|
904
|
+
* en la barra todavía. Medido en la pantalla del usuario: arrancó una sesión, se fue,
|
|
905
|
+
* pulsó el nombre del proyecto para ver qué hacía y se llevó el rechazo cinco veces —
|
|
906
|
+
* el botón muerto de siempre, y sin ninguna otra puerta detrás.
|
|
907
|
+
*
|
|
908
|
+
* La distinción con «nueva sesión» no la hace esto, la hace el cliente: el «+» se
|
|
909
|
+
* apaga mientras el proyecto trabaja, porque una segunda conversación sobre la misma
|
|
910
|
+
* copia de trabajo sigue siendo imposible. Lo que queda aquí es que el gesto de
|
|
911
|
+
* «llévame a este proyecto» lleve a alguna parte.
|
|
912
|
+
*/
|
|
913
|
+
const volverALoQueTrabaja = apertura.sesion === undefined && yaAbierta.turnoEnVuelo;
|
|
914
|
+
if (laMisma || volverALoQueTrabaja) {
|
|
915
|
+
enFoco = apertura.raiz;
|
|
916
|
+
await cerrarLasOciosasSalvo(apertura.raiz);
|
|
917
|
+
return yaAbierta;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* OTRA sesión del MISMO proyecto, con la que hay trabajando: se DECLINA.
|
|
921
|
+
*
|
|
922
|
+
* Una consola por raíz no es una limitación de este mapa: es la copia de trabajo. Dos
|
|
923
|
+
* conversaciones escribiendo los mismos ficheros no tienen aislamiento de ninguna
|
|
924
|
+
* clase —es el mismo motivo por el que una tarea no arranca donde hay una persona— y
|
|
925
|
+
* cerrar la que trabaja para abrir la otra sería la interrupción que se acaba de
|
|
926
|
+
* quitar. Si está OCIOSA sí se cierra y se abre la nueva, que es lo de siempre.
|
|
927
|
+
*/
|
|
928
|
+
if (yaAbierta.turnoEnVuelo)
|
|
929
|
+
throw new Error(motivoDeProyectoTrabajando());
|
|
930
|
+
await cerrarConsolaDeProyecto(apertura.raiz);
|
|
931
|
+
}
|
|
932
|
+
const raiz = apertura.raiz;
|
|
933
|
+
await cerrarLasOciosasSalvo(raiz);
|
|
934
|
+
/**
|
|
935
|
+
* El foco se muda ANTES de construir, y no después.
|
|
936
|
+
*
|
|
937
|
+
* No es orden por gusto: el lazo arranca DENTRO de `construirConsolaDeProyecto`, así que
|
|
938
|
+
* su primer flanco de turno puede salir de ahí —lo hace con cualquier `correr` que no
|
|
939
|
+
* espere una línea—, y con el foco puesto después ese flanco habría hablado de la
|
|
940
|
+
* consola anterior. Que quede apuntando a una raíz sin consola si la construcción
|
|
941
|
+
* revienta es inocuo: `consolaEnFoco()` pregunta al mapa, y ahí no habrá nada.
|
|
942
|
+
*/
|
|
943
|
+
enFoco = raiz;
|
|
944
|
+
const consolaDeProyecto = await construirConsolaDeProyecto({
|
|
945
|
+
...apertura,
|
|
946
|
+
enElCable: () => enFoco === raiz,
|
|
947
|
+
alFlancoDeTurno: (activo) => {
|
|
948
|
+
// La barra se repinta en los DOS flancos y esté o no en foco: de aquí sale que una
|
|
949
|
+
// sesión de segundo plano está trabajando.
|
|
950
|
+
avisarDeLasAbiertas({ raiz, turnoEnVuelo: activo });
|
|
951
|
+
// Y al terminar, si ya no la mira nadie, se cierra. Ver `cerrarSiSobra`.
|
|
952
|
+
if (!activo)
|
|
953
|
+
cerrarSiSobra(raiz);
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
abiertas.set(raiz, consolaDeProyecto);
|
|
957
|
+
return consolaDeProyecto;
|
|
958
|
+
};
|
|
959
|
+
/**
|
|
960
|
+
* Las consolas abiertas para tareas. No son `abierto` —nadie las mira— pero cada una
|
|
961
|
+
* tiene su `correrConsola` vivo, así que `cerrar()` tiene que llevárselas: un lazo que
|
|
962
|
+
* nadie cierra deja el proceso sin terminar, y «cierra el propio vestíbulo» sería falso.
|
|
963
|
+
*
|
|
964
|
+
* El objeto se guarda TAL CUAL, sin envolver su `cerrar`: `ConsolaDeProyecto` expone
|
|
965
|
+
* captadores (`sesion`, `historica`, `cerrada`) y una copia con `...` los congelaría en
|
|
966
|
+
* el valor que tuvieran al abrir — `sesion` se quedaría en `undefined` para siempre. Las
|
|
967
|
+
* ya cerradas se PODAN al abrir la siguiente, que es cuando puede empezar a crecer.
|
|
968
|
+
*/
|
|
969
|
+
const deTareas = new Set();
|
|
970
|
+
/**
|
|
971
|
+
* ¿Esa conversación la está escribiendo una tarea de fondo AHORA?
|
|
972
|
+
*
|
|
973
|
+
* La fuente es `deTareas` y no el índice de tareas a propósito: lo que hace daño es que
|
|
974
|
+
* haya dos consolas de ESTE proceso sobre el mismo hilo, y eso solo lo sabe este conjunto.
|
|
975
|
+
* Una tarea `en-proceso` en el OTRO proceso no aparece aquí, y esa limitación es la misma
|
|
976
|
+
* que ya declara `borrarSesion` — el límite lo pone el sistema operativo, no esta función.
|
|
977
|
+
*/
|
|
978
|
+
const esDeUnaTareaEnCurso = (raiz, sesion) => [...deTareas].some((consola) => consola.raiz === raiz && !consola.cerrada && (consola.sesion === sesion || consola.idDeHilo === sesion));
|
|
979
|
+
const abrirParaTarea = async (raiz, sesion, adjuntos, tarea) => {
|
|
980
|
+
// ANTES de construir nada: sin esto, una raíz equivocada dejaba un `correrConsola`
|
|
981
|
+
// vivo, una foto de git lanzada y un id de hilo gastado. Y el motivo no lleva la ruta
|
|
982
|
+
// —puede ser la del home— porque de aquí el error sube al registro de la tarea.
|
|
983
|
+
if (!esProyecto(raiz)) {
|
|
984
|
+
throw new Error("esa raíz no es un proyecto de xonecode: falta su .xonecode/config.json");
|
|
985
|
+
}
|
|
986
|
+
for (const vieja of deTareas)
|
|
987
|
+
if (vieja.cerrada)
|
|
988
|
+
deTareas.delete(vieja);
|
|
989
|
+
// `sesion` reenviada tal cual: es lo que hace que `construirConsolaDeProyecto` REABRA
|
|
990
|
+
// ese hilo —`idSesion = sesion ?? randomUUID()`— en vez de abrir uno nuevo. `undefined`
|
|
991
|
+
// en la primera ejecución de la tarea sigue dando una sesión nueva, como siempre.
|
|
992
|
+
// `adjuntos` reenviada igual que `sesion`, y por el mismo motivo: quien la conoce es el
|
|
993
|
+
// corredor, que es el único que sabe de qué tarea es esta apertura.
|
|
994
|
+
// `tarea` viaja por lo mismo que las otras dos: quien sabe de qué tarea es esta
|
|
995
|
+
// apertura es el corredor. Es lo que marca su fila en el índice como sesión de tarea.
|
|
996
|
+
const consolaDeProyecto = await construirConsolaDeProyecto({
|
|
997
|
+
raiz,
|
|
998
|
+
sesion,
|
|
999
|
+
adjuntos,
|
|
1000
|
+
tarea,
|
|
1001
|
+
// Nunca. Una consola de tarea no está en el cable ni puede llegar a estarlo: el foco
|
|
1002
|
+
// es de las de persona, y su raíz podría coincidir con una de ellas.
|
|
1003
|
+
enElCable: () => false,
|
|
1004
|
+
});
|
|
1005
|
+
deTareas.add(consolaDeProyecto);
|
|
1006
|
+
return consolaDeProyecto;
|
|
1007
|
+
};
|
|
1008
|
+
/** Cierra las consolas de tarea que queden vivas. Ver `deTareas`. */
|
|
1009
|
+
const cerrarLasDeTareas = async () => {
|
|
1010
|
+
for (const consola of [...deTareas]) {
|
|
1011
|
+
deTareas.delete(consola);
|
|
1012
|
+
// Una que ya cerró el corredor no se vuelve a cerrar, y un fallo al cerrar una no
|
|
1013
|
+
// puede dejar a las demás vivas: el cierre es lo último que corre.
|
|
1014
|
+
if (!consola.cerrada)
|
|
1015
|
+
await consola.cerrar().catch(() => undefined);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
consola: consolaDelVestibulo,
|
|
1020
|
+
nombre: opciones.nombre,
|
|
1021
|
+
async pasosPendientes() {
|
|
1022
|
+
const pasos = [];
|
|
1023
|
+
// La MISMA condición que usa `asistenteDeModelo`: si `trabajo` no resolvió por
|
|
1024
|
+
// `omision`, alguien eligió (proveedor, modelo, clave si el proveedor la pide,
|
|
1025
|
+
// global o bandera) y no se pregunta.
|
|
1026
|
+
if (opciones.origenDeTrabajo === "omision")
|
|
1027
|
+
pasos.push("cuenta");
|
|
1028
|
+
if (registrados.length === 0)
|
|
1029
|
+
pasos.push("entorno");
|
|
1030
|
+
// El proyecto YA NO es un paso del alta — cambio de rumbo del usuario: con cuenta
|
|
1031
|
+
// y entorno resueltos se entra directo al dashboard, y el proyecto se elige en la
|
|
1032
|
+
// barra lateral (entorno → proyectos → sesiones), no aquí. `abierto` sigue
|
|
1033
|
+
// existiendo como estado (`proyectoAbierto()`, más abajo) pero no bloquea nada.
|
|
1034
|
+
return pasos;
|
|
1035
|
+
},
|
|
1036
|
+
alCambiarEstadoDeSesion(escucha) {
|
|
1037
|
+
escuchaDeEstado = escucha;
|
|
1038
|
+
},
|
|
1039
|
+
alCambiarTurno(escucha) {
|
|
1040
|
+
escuchaDeTurno = escucha;
|
|
1041
|
+
},
|
|
1042
|
+
alCambiarConsumo(escucha) {
|
|
1043
|
+
escuchaDeConsumo = escucha;
|
|
1044
|
+
},
|
|
1045
|
+
// La del FOCO, igual que `proyectoAbierto()`: es la sesión que se está mirando.
|
|
1046
|
+
consumoDeSesion: () => consolaEnFoco()?.consumo?.(),
|
|
1047
|
+
opcionesDeEntorno: () => [...ENTORNOS_OFICIALES, ENTORNO_OTRO],
|
|
1048
|
+
entornosRegistrados: () => [...registrados],
|
|
1049
|
+
async pasoDeCuenta() {
|
|
1050
|
+
// Sigue siendo `asistenteDeModelo` con la consola web detrás y nada más. Lo único que
|
|
1051
|
+
// esta piel añade es `exigirEleccion`: aquí el paso de cuenta es la PUERTA del
|
|
1052
|
+
// dashboard, así que cancelar vuelve a preguntar en vez de dejar entrar con el modelo
|
|
1053
|
+
// por omisión. El asistente decide cuándo parar mirando `eof()` — sin cliente, no hay
|
|
1054
|
+
// a quién insistirle.
|
|
1055
|
+
return asistenteDeModelo(consolaDelVestibulo.consola, {
|
|
1056
|
+
origenDeTrabajo: opciones.origenDeTrabajo,
|
|
1057
|
+
hayCredencial: opciones.hayCredencial ?? (() => false),
|
|
1058
|
+
guardarCredencial: registrarCredencial,
|
|
1059
|
+
// La clave se prueba contra el catálogo ANTES de escribirse; esto es lo que la
|
|
1060
|
+
// pone en el proceso mientras tanto. Entra por opción como todo lo que toca el
|
|
1061
|
+
// sistema: un test no puede escribir en el `process.env` del que corre los tests
|
|
1062
|
+
// sin decirlo.
|
|
1063
|
+
...(opciones.aplicarCredencial === undefined ? {} : { aplicarCredencial: opciones.aplicarCredencial }),
|
|
1064
|
+
exigirEleccion: true,
|
|
1065
|
+
});
|
|
1066
|
+
},
|
|
1067
|
+
async guardarCredencialDe(proveedor, clave) {
|
|
1068
|
+
const guardada = registrarCredencial(proveedor, clave);
|
|
1069
|
+
// Se dice AQUÍ, en el momento: si el usuario cancela el paso siguiente, un
|
|
1070
|
+
// «cancelado» a secas daría a entender que no se tocó nada, y la clave ya está en
|
|
1071
|
+
// disco. Es la misma frase que escribe `/provider` y el asistente de terminal.
|
|
1072
|
+
informar(`credencial de ${proveedor} guardada en ${guardada.ruta}`);
|
|
1073
|
+
return guardada;
|
|
1074
|
+
},
|
|
1075
|
+
async registrarEntorno(entorno) {
|
|
1076
|
+
if (!urlDeEntornoValida(entorno.url)) {
|
|
1077
|
+
throw new Error(AVISO_DE_URL_DE_MCP);
|
|
1078
|
+
}
|
|
1079
|
+
// Lo ÚNICO que el formulario pide es la URL; la identidad se deduce de ella.
|
|
1080
|
+
const identificado = identidadDeEntorno(entorno);
|
|
1081
|
+
const guardado = opciones.guardarEntorno(identificado);
|
|
1082
|
+
// Justo después de registrar, y no antes: `adoptarLegadoSiProcede` solo actúa si la
|
|
1083
|
+
// URL es la oficial por omisión, que es la única que el fichero plano pudo usar.
|
|
1084
|
+
adoptarLegado(identificado);
|
|
1085
|
+
// SUSTITUYE por id, como hace `settingsEnDisco.guardarEntorno` en el fichero. Con un
|
|
1086
|
+
// `push` a secas, registrar dos veces el mismo entorno —cosa que la web hace cada vez
|
|
1087
|
+
// que alguien lo elige— dejaba dos entradas para un id, y `entornoPorId` resolvía a la
|
|
1088
|
+
// primera: la vieja. Registrar es idempotente aquí igual que en disco.
|
|
1089
|
+
const yaEstaba = registrados.findIndex((e) => e.id === identificado.id);
|
|
1090
|
+
if (yaEstaba >= 0)
|
|
1091
|
+
registrados.splice(yaEstaba, 1, identificado);
|
|
1092
|
+
else
|
|
1093
|
+
registrados.push(identificado);
|
|
1094
|
+
informar(`entorno «${identificado.id}» registrado en ${guardado.ruta}`);
|
|
1095
|
+
// El entorno YA identificado vuelve con la ruta: quien registró un «otro» no sabe con
|
|
1096
|
+
// qué id quedó, y `arranque.ts` necesita ese id exacto para pedirle los proyectos —
|
|
1097
|
+
// con el «otro» de la lista, `entornoPorId` no encontraría nada.
|
|
1098
|
+
return { ...guardado, entorno: identificado };
|
|
1099
|
+
},
|
|
1100
|
+
async guardarProyectosVisibles(entorno, proyectos) {
|
|
1101
|
+
const registrado = entornoPorId(entorno);
|
|
1102
|
+
const conProyectos = { ...registrado, proyectos: [...proyectos] };
|
|
1103
|
+
const guardado = opciones.guardarEntorno(conProyectos);
|
|
1104
|
+
const donde = registrados.findIndex((e) => e.id === registrado.id);
|
|
1105
|
+
if (donde >= 0)
|
|
1106
|
+
registrados.splice(donde, 1, conProyectos);
|
|
1107
|
+
informar(`proyectos visibles de «${registrado.id}»: ${proyectos.length === 0 ? "ninguno" : proyectos.join(", ")}`);
|
|
1108
|
+
return guardado;
|
|
1109
|
+
},
|
|
1110
|
+
async proyectosDe(entorno) {
|
|
1111
|
+
const registrado = entornoPorId(entorno);
|
|
1112
|
+
if (opciones.proyectosDeEntorno === undefined) {
|
|
1113
|
+
throw new Error("esta ejecución no tiene conexión con CloudStudio");
|
|
1114
|
+
}
|
|
1115
|
+
const { proyectos, servidor } = await opciones.proyectosDeEntorno(registrado);
|
|
1116
|
+
renombrarConElServidor(registrado, servidor?.nombre);
|
|
1117
|
+
return proyectos;
|
|
1118
|
+
},
|
|
1119
|
+
async ramasDe(entorno, proyecto) {
|
|
1120
|
+
// El NOMBRE, siempre: es lo único que el servidor sabe abrir.
|
|
1121
|
+
const nombre = typeof proyecto === "string" ? proyecto : proyecto.nombre;
|
|
1122
|
+
const registrado = entornoPorId(entorno);
|
|
1123
|
+
if (opciones.ramasDeProyecto === undefined) {
|
|
1124
|
+
throw new Error("esta ejecución no tiene conexión con CloudStudio");
|
|
1125
|
+
}
|
|
1126
|
+
return opciones.ramasDeProyecto(registrado, nombre);
|
|
1127
|
+
},
|
|
1128
|
+
raizDeProyecto(entorno, proyecto) {
|
|
1129
|
+
return rutaDeWorkspace(base, entornoPorId(entorno).id, proyecto);
|
|
1130
|
+
},
|
|
1131
|
+
sesionesDe(raiz) {
|
|
1132
|
+
// Una carpeta que no existe no es un fallo: es un proyecto que nunca se abrió. El
|
|
1133
|
+
// puerto se lo traga y devuelve vacío, que es la verdad.
|
|
1134
|
+
try {
|
|
1135
|
+
// Campo a campo y no la entrada entera: del índice cuelgan además `creada` y el
|
|
1136
|
+
// `dispositivo`, que la barra no pinta. Los tres que sí salen son los que distinguen
|
|
1137
|
+
// una fila de otra: CUÁNDO se tocó, de QUIÉN es y CUÁNTO costó.
|
|
1138
|
+
return sesiones.listar(raiz).map((s) => ({
|
|
1139
|
+
id: s.id,
|
|
1140
|
+
titulo: s.titulo,
|
|
1141
|
+
...(s.ultimoTurno === undefined ? {} : { ultimoTurno: s.ultimoTurno }),
|
|
1142
|
+
...(s.tarea === undefined ? {} : { tarea: s.tarea }),
|
|
1143
|
+
...(s.consumo === undefined ? {} : { consumo: s.consumo }),
|
|
1144
|
+
}));
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
return [];
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
async borrarSesion(raiz, id) {
|
|
1151
|
+
// Antes de tocar NADA: si esa conversación es la de una tarea en curso, no se borra.
|
|
1152
|
+
// `olvidarMarcaDeSesion` y `olvidarMemoriaDeHilo` no son condicionales, así que
|
|
1153
|
+
// borrar aquí se habría llevado la ref de git y el checkpoint de un hilo que el
|
|
1154
|
+
// agente sigue escribiendo — y su siguiente `volcar()` habría devuelto la fila a la
|
|
1155
|
+
// barra, dejando al usuario con un botón que parece no funcionar. Ver `deTareas`.
|
|
1156
|
+
// El MISMO predicado que la guarda de `abrirDeVerdad`: dos copias de «¿la está
|
|
1157
|
+
// escribiendo una tarea?» acabarían discrepando, y la que se quedara vieja sería la
|
|
1158
|
+
// que deja pasar el daño.
|
|
1159
|
+
if (esDeUnaTareaEnCurso(raiz, id)) {
|
|
1160
|
+
return {
|
|
1161
|
+
borrada: false,
|
|
1162
|
+
cerroLaAbierta: false,
|
|
1163
|
+
motivo: "esa conversación es la de una tarea en curso: se podrá borrar cuando termine",
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
// Cerrar ANTES de borrar: ver el comentario del contrato. Y solo si es ESA sesión —
|
|
1167
|
+
// cerrar el proyecto entero porque se borró una conversación vieja de la lista sería
|
|
1168
|
+
// llevarse por delante el trabajo en curso.
|
|
1169
|
+
// La consola de ESA sesión, si hay alguna abierta: puede no ser la que está en foco
|
|
1170
|
+
// desde que cambiar de sesión no mata la anterior. `cerroLaAbierta` sigue queriendo
|
|
1171
|
+
// decir «la que estabas mirando», que es lo que el cliente usa para volver al
|
|
1172
|
+
// escritorio: cerrar una de segundo plano no le cambia la vista a nadie.
|
|
1173
|
+
const suya = abiertas.get(raiz);
|
|
1174
|
+
const laDeEsaSesion = suya !== undefined && suya.sesion === id;
|
|
1175
|
+
const cerroLaAbierta = laDeEsaSesion && enFoco === raiz;
|
|
1176
|
+
if (laDeEsaSesion)
|
|
1177
|
+
await cerrarConsolaDeProyecto(raiz);
|
|
1178
|
+
const borrada = sesiones.borrar?.(raiz, id) ?? false;
|
|
1179
|
+
// La ref de git de la sesión se va con ella: apunta a un árbol que solo esa vista
|
|
1180
|
+
// usaba, y dejarla mantendría ese árbol vivo para siempre (`agent/sesionGit.ts`).
|
|
1181
|
+
// No es condicional al `borrada`: una entrada de índice ya perdida no es motivo para
|
|
1182
|
+
// dejar la ref colgada.
|
|
1183
|
+
await opciones.olvidarMarcaDeSesion?.(raiz, id);
|
|
1184
|
+
// Y su memoria: el `thread_id` es el id de la sesión, así que el checkpoint queda
|
|
1185
|
+
// huérfano y con el contenido de todo lo que se escribió en ella. Tampoco condicional
|
|
1186
|
+
// al `borrada`, por lo mismo que la ref.
|
|
1187
|
+
await opciones.olvidarMemoriaDeHilo?.(raiz, id);
|
|
1188
|
+
return { borrada, cerroLaAbierta };
|
|
1189
|
+
},
|
|
1190
|
+
renombrarSesion(raiz, id, titulo) {
|
|
1191
|
+
return sesiones.renombrar?.(raiz, id, titulo) ?? false;
|
|
1192
|
+
},
|
|
1193
|
+
async completarProyecto({ entorno, proyecto, rama }) {
|
|
1194
|
+
const registrado = entornoPorId(entorno);
|
|
1195
|
+
// El listado remoto trae `{id, nombre}`; un nombre suelto vale porque en CloudStudio
|
|
1196
|
+
// el proyecto se abre POR NOMBRE (`studio_open_project`) y el id solo identifica.
|
|
1197
|
+
//
|
|
1198
|
+
// Se DESESTRUCTURA, y no es estilo: lo que entre aquí acaba en el `config.json` del
|
|
1199
|
+
// proyecto (`guardarProyectoCloudStudioDeProyecto`, abajo), y quien llama le pasa una
|
|
1200
|
+
// fila del listado remoto — que hoy trae además `compartido` y mañana podría traer
|
|
1201
|
+
// otra cosa. TypeScript no avisa: la comprobación de propiedades de más solo salta
|
|
1202
|
+
// con un literal, no con un objeto que llega por variable. Copiar los dos campos que
|
|
1203
|
+
// SON la identidad es lo que impide que el disco se llene de datos del servidor.
|
|
1204
|
+
const identidad = typeof proyecto === "string"
|
|
1205
|
+
? { id: proyecto, nombre: proyecto }
|
|
1206
|
+
: { id: proyecto.id, nombre: proyecto.nombre };
|
|
1207
|
+
const raiz = rutaDeWorkspace(base, registrado.id, identidad.nombre);
|
|
1208
|
+
const datos = {
|
|
1209
|
+
entorno: registrado.id,
|
|
1210
|
+
url: registrado.url,
|
|
1211
|
+
scopes: registrado.scopes ?? SCOPES_CLOUDSTUDIO_AGENTE,
|
|
1212
|
+
proyecto: identidad,
|
|
1213
|
+
rama,
|
|
1214
|
+
};
|
|
1215
|
+
// El alta se escribe ENTERA antes de bajar, y no después. No es un descuido: la
|
|
1216
|
+
// frase que se dice cuando la descarga falla es «reintenta con /sync bajar», y
|
|
1217
|
+
// `/sync` lee del disco el proyecto y la rama — sin el alta escrita, ese consejo
|
|
1218
|
+
// sería mentira. «No a medias» significa completa o nada, no «nada».
|
|
1219
|
+
const { ruta } = opciones.guardarConfigDeProyecto(raiz, datos);
|
|
1220
|
+
proyectoEscrito = { raiz, ruta };
|
|
1221
|
+
informar(`proyecto dado de alta en ${ruta}`);
|
|
1222
|
+
try {
|
|
1223
|
+
await opciones.descargar({ ...datos, raiz });
|
|
1224
|
+
}
|
|
1225
|
+
catch (error) {
|
|
1226
|
+
const detalle = error instanceof Error ? error.message : String(error);
|
|
1227
|
+
informar(`no se pudo descargar el proyecto: ${detalle}`);
|
|
1228
|
+
informar(`el alta quedó completa en ${ruta}; reintenta la descarga con «/sync bajar»`);
|
|
1229
|
+
// Se propaga: quien llama decide si vuelve al paso de proyecto o abre igualmente
|
|
1230
|
+
// la copia vacía. Tragárselo aquí dejaría creer que el proyecto está bajado.
|
|
1231
|
+
throw error;
|
|
1232
|
+
}
|
|
1233
|
+
return { raiz, ruta };
|
|
1234
|
+
},
|
|
1235
|
+
abrirProyecto: (apertura) => enCola(() => abrirDeVerdad(apertura)),
|
|
1236
|
+
/**
|
|
1237
|
+
* Abrir un proyecto para una TAREA: la misma construcción, sin registrarlo como el
|
|
1238
|
+
* proyecto abierto ni mudar el sumidero del cable.
|
|
1239
|
+
*
|
|
1240
|
+
* Existe porque el vestíbulo sirve UN proyecto a la vez (`proyectoAbierto()` devuelve
|
|
1241
|
+
* uno) y el cable se muda a esa consola (`arranque.ts#adjuntar`): si el corredor
|
|
1242
|
+
* reusara `abrirProyecto`, cada tarea que arrancase cerraría la consola de quien esté
|
|
1243
|
+
* trabajando y le movería la vista.
|
|
1244
|
+
*
|
|
1245
|
+
* Va por la misma COLA que la otra puerta aunque no toque `abierto`: la cola es lo que
|
|
1246
|
+
* sostiene «una consola de proyecto a la vez», y colarse por delante de un
|
|
1247
|
+
* un cierre a medias sería empezar a construir mientras el lazo anterior
|
|
1248
|
+
* todavía vive. El precio, dicho: una tarea que llega mientras se cierra un turno
|
|
1249
|
+
* humano de minutos espera a que acabe. Para trabajo de fondo es el lado correcto.
|
|
1250
|
+
*/
|
|
1251
|
+
abrirParaTarea: (raiz, sesion, adjuntos, tarea) => enCola(() => abrirParaTarea(raiz, sesion, adjuntos, tarea)),
|
|
1252
|
+
proyectoAbierto: () => consolaEnFoco(),
|
|
1253
|
+
// La del foco INCLUIDA, y las cerradas fuera: una consola cuyo lazo ya terminó no ocupa
|
|
1254
|
+
// su raíz, y de esta lista cuelga la guarda de «gana la persona».
|
|
1255
|
+
proyectosAbiertos: () => [...abiertas.values()].filter((c) => !c.cerrada),
|
|
1256
|
+
cancelar: () => enCola(async () => {
|
|
1257
|
+
await cerrarLasDeProyecto();
|
|
1258
|
+
informar("alta cancelada");
|
|
1259
|
+
// Negar lo que ya está en disco sería mentir. Cancelar no BORRA nada: dice qué quedó.
|
|
1260
|
+
if (credencialEscrita !== undefined) {
|
|
1261
|
+
informar(`la credencial de ${credencialEscrita.proveedor} sigue escrita en ${credencialEscrita.ruta}`);
|
|
1262
|
+
}
|
|
1263
|
+
if (proyectoEscrito !== undefined) {
|
|
1264
|
+
informar(`el alta del proyecto sigue escrita en ${proyectoEscrito.ruta}`);
|
|
1265
|
+
}
|
|
1266
|
+
}),
|
|
1267
|
+
cerrar: () => enCola(async () => {
|
|
1268
|
+
await cerrarLasDeProyecto();
|
|
1269
|
+
// Y las de las tareas: cada una tiene su `correrConsola` vivo, y dejarlo corriendo
|
|
1270
|
+
// deja el proceso sin terminar. Ver `deTareas`.
|
|
1271
|
+
await cerrarLasDeTareas();
|
|
1272
|
+
consolaDelVestibulo.cerrar();
|
|
1273
|
+
}),
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
//# sourceMappingURL=vestibulo.js.map
|