xonecode 0.5.1 → 0.8.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/apps/web/dist/assets/{index-BB3SamhR.css → index-Boak51UP.css} +1 -1
- package/apps/web/dist/assets/index-Cg28x6nG.js +477 -0
- package/apps/web/dist/assets/{php-D_2--9WK.js → php-Cu6F_QCJ.js} +1 -1
- package/apps/web/dist/assets/{ruby-DSHSDbk1.js → ruby-BiqQPoC6.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/dist/agent/{cloudstudioClient.js → cloudstudio/cloudstudioClient.js} +10 -0
- package/dist/agent/cloudstudio/cloudstudioClient.js.map +1 -0
- package/dist/agent/{cloudstudioMcp.js → cloudstudio/cloudstudioMcp.js} +5 -5
- package/dist/agent/cloudstudio/cloudstudioMcp.js.map +1 -0
- package/dist/agent/{descarga.js → cloudstudio/descarga.js} +17 -7
- package/dist/agent/cloudstudio/descarga.js.map +1 -0
- package/dist/agent/cloudstudio/manifiesto.js.map +1 -0
- package/dist/agent/cloudstudio/ramaActiva.js +11 -0
- package/dist/agent/cloudstudio/ramaActiva.js.map +1 -0
- package/dist/agent/{subida.js → cloudstudio/subida.js} +16 -7
- package/dist/agent/cloudstudio/subida.js.map +1 -0
- package/dist/agent/{zip.js → cloudstudio/zip.js} +1 -1
- package/dist/agent/cloudstudio/zip.js.map +1 -0
- package/dist/agent/{authEnDisco.js → config/authEnDisco.js} +3 -3
- package/dist/agent/config/authEnDisco.js.map +1 -0
- package/dist/agent/{catalogoModelos.js → config/catalogoModelos.js} +47 -1
- package/dist/agent/config/catalogoModelos.js.map +1 -0
- package/dist/agent/{configEnDisco.js → config/configEnDisco.js} +6 -6
- package/dist/agent/config/configEnDisco.js.map +1 -0
- package/dist/agent/{crearProyecto.js → config/crearProyecto.js} +2 -2
- package/dist/agent/config/crearProyecto.js.map +1 -0
- package/dist/agent/config/entorno.js.map +1 -0
- package/dist/agent/config/gemini.js.map +1 -0
- package/dist/agent/config/modelos.js +235 -0
- package/dist/agent/config/modelos.js.map +1 -0
- package/dist/agent/{modelosDeMotor.js → config/modelosDeMotor.js} +1 -1
- package/dist/agent/config/modelosDeMotor.js.map +1 -0
- package/dist/agent/config/mudanzaEnDisco.js +121 -0
- package/dist/agent/config/mudanzaEnDisco.js.map +1 -0
- package/dist/agent/{persona.js → config/persona.js} +1 -1
- package/dist/agent/config/persona.js.map +1 -0
- package/dist/agent/config/selectorEnMaquina.js +45 -0
- package/dist/agent/config/selectorEnMaquina.js.map +1 -0
- package/dist/agent/{settingsEnDisco.js → config/settingsEnDisco.js} +37 -2
- package/dist/agent/config/settingsEnDisco.js.map +1 -0
- package/dist/agent/{versionEnDisco.js → config/versionEnDisco.js} +8 -4
- package/dist/agent/config/versionEnDisco.js.map +1 -0
- package/dist/agent/dispositivos/arranqueDeEmulador.js +146 -0
- package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -0
- package/dist/agent/{dispositivosEnMaquina.js → dispositivos/dispositivosEnMaquina.js} +93 -6
- package/dist/agent/dispositivos/dispositivosEnMaquina.js.map +1 -0
- package/dist/agent/dispositivos/hotswap.js.map +1 -0
- package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -0
- package/dist/agent/dispositivos/juezVisual.js +152 -0
- package/dist/agent/dispositivos/juezVisual.js.map +1 -0
- package/dist/agent/{lanzamientoEnMaquina.js → dispositivos/lanzamientoEnMaquina.js} +1 -1
- package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -0
- package/dist/agent/{paqueteDelProyecto.js → dispositivos/paqueteDelProyecto.js} +5 -5
- package/dist/agent/dispositivos/paqueteDelProyecto.js.map +1 -0
- package/dist/agent/dispositivos/procesosEnMaquina.js.map +1 -0
- package/dist/agent/dispositivos/sensorDeArranque.js +154 -0
- package/dist/agent/dispositivos/sensorDeArranque.js.map +1 -0
- package/dist/agent/exportarPdf.js +140 -0
- package/dist/agent/exportarPdf.js.map +1 -0
- package/dist/agent/{arbolDeProyecto.js → grafo/arbolDeProyecto.js} +2 -2
- package/dist/agent/grafo/arbolDeProyecto.js.map +1 -0
- package/dist/agent/{artefactosEnDisco.js → grafo/artefactosEnDisco.js} +2 -2
- package/dist/agent/grafo/artefactosEnDisco.js.map +1 -0
- package/dist/agent/{busquedaRegex.js → grafo/busquedaRegex.js} +41 -1
- package/dist/agent/grafo/busquedaRegex.js.map +1 -0
- package/dist/agent/grafo/criticaVisual.js +115 -0
- package/dist/agent/grafo/criticaVisual.js.map +1 -0
- package/dist/agent/grafo/excluirTools.js +82 -0
- package/dist/agent/grafo/excluirTools.js.map +1 -0
- package/dist/agent/{memoriaDeProyecto.js → grafo/memoriaDeProyecto.js} +1 -1
- package/dist/agent/grafo/memoriaDeProyecto.js.map +1 -0
- package/dist/agent/grafo/navegacionXone.js +231 -0
- package/dist/agent/grafo/navegacionXone.js.map +1 -0
- package/dist/agent/{perfiles.js → grafo/perfiles.js} +96 -10
- package/dist/agent/grafo/perfiles.js.map +1 -0
- package/dist/agent/{proyecto.js → grafo/proyecto.js} +274 -24
- package/dist/agent/grafo/proyecto.js.map +1 -0
- package/dist/agent/grafo/skills.js +425 -0
- package/dist/agent/grafo/skills.js.map +1 -0
- package/dist/agent/grafo/xoneAgent.js +566 -0
- package/dist/agent/grafo/xoneAgent.js.map +1 -0
- package/dist/agent/navegacion/estilosEnDisco.js +110 -0
- package/dist/agent/navegacion/estilosEnDisco.js.map +1 -0
- package/dist/agent/navegacion/hechosEnDisco.js +26 -0
- package/dist/agent/navegacion/hechosEnDisco.js.map +1 -0
- package/dist/agent/navegacion/indiceEnDisco.js +36 -0
- package/dist/agent/navegacion/indiceEnDisco.js.map +1 -0
- package/dist/agent/navegacion/modeloDeProyecto.js +228 -0
- package/dist/agent/navegacion/modeloDeProyecto.js.map +1 -0
- package/dist/agent/raizDelPaquete.js +56 -0
- package/dist/agent/raizDelPaquete.js.map +1 -0
- package/dist/agent/{checkpointer.js → sesiones/checkpointer.js} +1 -1
- package/dist/agent/sesiones/checkpointer.js.map +1 -0
- package/dist/agent/sesiones/git.js.map +1 -0
- package/dist/agent/{gitSync.js → sesiones/gitSync.js} +3 -3
- package/dist/agent/sesiones/gitSync.js.map +1 -0
- package/dist/agent/{sesionGit.js → sesiones/sesionGit.js} +2 -2
- package/dist/agent/sesiones/sesionGit.js.map +1 -0
- package/dist/agent/subagentes/agentesEnDisco.js +818 -0
- package/dist/agent/subagentes/agentesEnDisco.js.map +1 -0
- package/dist/agent/subagentes/consumoExterno.js.map +1 -0
- package/dist/agent/subagentes/escrituraDeCodex.js.map +1 -0
- package/dist/agent/{escrituraDeOpencode.js → subagentes/escrituraDeOpencode.js} +1 -1
- package/dist/agent/subagentes/escrituraDeOpencode.js.map +1 -0
- package/dist/agent/{escrituraExterna.js → subagentes/escrituraExterna.js} +59 -6
- package/dist/agent/subagentes/escrituraExterna.js.map +1 -0
- package/dist/agent/subagentes/pluginDeSkills.js +99 -0
- package/dist/agent/subagentes/pluginDeSkills.js.map +1 -0
- package/dist/agent/subagentes/subagenteCodex.js.map +1 -0
- package/dist/agent/{subagenteExterno.js → subagentes/subagenteExterno.js} +48 -0
- package/dist/agent/subagentes/subagenteExterno.js.map +1 -0
- package/dist/agent/{subagenteOpencode.js → subagentes/subagenteOpencode.js} +53 -1
- package/dist/agent/subagentes/subagenteOpencode.js.map +1 -0
- package/dist/agent/{aumentador.js → tareas/aumentador.js} +2 -2
- package/dist/agent/tareas/aumentador.js.map +1 -0
- package/dist/agent/{juezDeTarea.js → tareas/juezDeTarea.js} +6 -6
- package/dist/agent/tareas/juezDeTarea.js.map +1 -0
- package/dist/agent/{tareasEnDisco.js → tareas/tareasEnDisco.js} +53 -4
- package/dist/agent/tareas/tareasEnDisco.js.map +1 -0
- package/dist/agent/{diagnosticoDeTools.js → turno/diagnosticoDeTools.js} +5 -1
- package/dist/agent/turno/diagnosticoDeTools.js.map +1 -0
- package/dist/agent/{guionizado.js → turno/guionizado.js} +1 -1
- package/dist/agent/turno/guionizado.js.map +1 -0
- package/dist/agent/turno/informeDeTraza.js +245 -0
- package/dist/agent/turno/informeDeTraza.js.map +1 -0
- package/dist/agent/turno/inspectorDePrompt.js +205 -0
- package/dist/agent/turno/inspectorDePrompt.js.map +1 -0
- package/dist/agent/{instantanea.js → turno/instantanea.js} +7 -2
- package/dist/agent/turno/instantanea.js.map +1 -0
- package/dist/agent/{interrupts.js → turno/interrupts.js} +2 -2
- package/dist/agent/turno/interrupts.js.map +1 -0
- package/dist/agent/turno/mensajes.js.map +1 -0
- package/dist/agent/turno/normalizar.js.map +1 -0
- package/dist/agent/{puente.js → turno/puente.js} +71 -4
- package/dist/agent/turno/puente.js.map +1 -0
- package/dist/agent/turno/resumenDeContexto.js +260 -0
- package/dist/agent/turno/resumenDeContexto.js.map +1 -0
- package/dist/agent/{resumenDeTool.js → turno/resumenDeTool.js} +33 -0
- package/dist/agent/turno/resumenDeTool.js.map +1 -0
- package/dist/agent/turno/rubrica.js +91 -0
- package/dist/agent/turno/rubrica.js.map +1 -0
- package/dist/agent/turno/textoDeTool.js.map +1 -0
- package/dist/agent/{turnoReal.js → turno/turnoReal.js} +210 -25
- package/dist/agent/turno/turnoReal.js.map +1 -0
- package/dist/agent/turno/verificador.js.map +1 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/consola.js +72 -9
- package/dist/cli/consola.js.map +1 -1
- package/dist/cli/describe.js +2 -2
- package/dist/cli/describe.js.map +1 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/fuentesDeDisco.js +29 -0
- package/dist/cli/fuentesDeDisco.js.map +1 -0
- package/dist/cli/main.js +87 -31
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/run.js +40 -10
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/traza.js +48 -0
- package/dist/cli/traza.js.map +1 -0
- package/dist/cli/tui/correrTui.js +4 -4
- package/dist/cli/tui/correrTui.js.map +1 -1
- package/dist/cli/verify.js +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/core/actos.js +1 -1
- package/dist/core/adjuntos.js +2 -2
- package/dist/core/agentes.js +247 -7
- package/dist/core/agentes.js.map +1 -1
- package/dist/core/artefactos.js +4 -4
- package/dist/core/config.js +1 -1
- package/dist/core/contextos.js +12 -1
- package/dist/core/contextos.js.map +1 -1
- package/dist/core/descargas.js +1 -1
- package/dist/core/dispositivos.js +53 -3
- package/dist/core/dispositivos.js.map +1 -1
- package/dist/core/esfuerzo.js +141 -0
- package/dist/core/esfuerzo.js.map +1 -0
- package/dist/core/esqueleto.js +1 -1
- package/dist/core/estilos.js +78 -0
- package/dist/core/estilos.js.map +1 -0
- package/dist/core/exportacion.js +140 -0
- package/dist/core/exportacion.js.map +1 -0
- package/dist/core/hechosDelProyecto.js +87 -0
- package/dist/core/hechosDelProyecto.js.map +1 -0
- package/dist/core/logDeDispositivo.js +143 -0
- package/dist/core/logDeDispositivo.js.map +1 -0
- package/dist/core/modelos.js +48 -3
- package/dist/core/modelos.js.map +1 -1
- package/dist/core/mudanzaDeWorkspace.js +93 -0
- package/dist/core/mudanzaDeWorkspace.js.map +1 -0
- package/dist/core/navegacion.js +134 -0
- package/dist/core/navegacion.js.map +1 -0
- package/dist/core/notify.js +5 -0
- package/dist/core/notify.js.map +1 -1
- package/dist/core/planDeSubida.js +1 -1
- package/dist/core/planes.js +70 -0
- package/dist/core/planes.js.map +1 -0
- package/dist/core/ports.js +4 -0
- package/dist/core/ports.js.map +1 -1
- package/dist/core/rubrica.js +90 -0
- package/dist/core/rubrica.js.map +1 -0
- package/dist/core/selectorDeCarpeta.js +87 -0
- package/dist/core/selectorDeCarpeta.js.map +1 -0
- package/dist/core/settings.js +56 -7
- package/dist/core/settings.js.map +1 -1
- package/dist/core/shellDeAgente.js +144 -0
- package/dist/core/shellDeAgente.js.map +1 -0
- package/dist/core/skills.js +158 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/topeAgotado.js +69 -0
- package/dist/core/topeAgotado.js.map +1 -0
- package/dist/core/zipDeSkill.js +107 -0
- package/dist/core/zipDeSkill.js.map +1 -0
- package/dist/web/servidor/arranque.js +853 -54
- package/dist/web/servidor/arranque.js.map +1 -1
- package/dist/web/servidor/corredorDeTareas.js +3 -3
- package/dist/web/servidor/corredorDeTareas.js.map +1 -1
- package/dist/web/servidor/sesiones.js +34 -6
- package/dist/web/servidor/sesiones.js.map +1 -1
- package/dist/web/servidor/transporte.js +1 -1
- package/dist/web/servidor/transporte.js.map +1 -1
- package/dist/web/servidor/vestibulo.js +62 -13
- package/dist/web/servidor/vestibulo.js.map +1 -1
- package/package.json +7 -4
- package/skills/archify/SKILL.md +25 -2
- package/skills/artifacts-builder/SKILL.md +23 -0
- package/skills/xone-hotswap/SKILL.md +96 -0
- package/skills/xone-hotswap/scripts/xone-arrancar-android +202 -0
- package/skills/xone-hotswap/scripts/xone-arrancar-ios +85 -0
- package/skills/xone-hotswap/scripts/xone-captura-android +93 -0
- package/skills/xone-hotswap/scripts/xone-desplegar-android +439 -0
- package/skills/xone-hotswap/scripts/xone-hotswap +201 -0
- package/skills/xone-hotswap/scripts/xone-log-android +102 -0
- package/skills/xone-hotswap/scripts/xone-reiniciar-android +132 -0
- package/skills/xone-plan-builder/SKILL.md +22 -4
- package/skills/xone-spec-builder/SKILL.md +37 -7
- package/apps/web/dist/assets/index-CFYAoHdw.js +0 -477
- package/dist/agent/agentesEnDisco.js +0 -539
- package/dist/agent/agentesEnDisco.js.map +0 -1
- package/dist/agent/arbolDeProyecto.js.map +0 -1
- package/dist/agent/artefactosEnDisco.js.map +0 -1
- package/dist/agent/aumentador.js.map +0 -1
- package/dist/agent/authEnDisco.js.map +0 -1
- package/dist/agent/busquedaRegex.js.map +0 -1
- package/dist/agent/catalogoModelos.js.map +0 -1
- package/dist/agent/checkpointer.js.map +0 -1
- package/dist/agent/cloudstudioClient.js.map +0 -1
- package/dist/agent/cloudstudioMcp.js.map +0 -1
- package/dist/agent/configEnDisco.js.map +0 -1
- package/dist/agent/consumoExterno.js.map +0 -1
- package/dist/agent/crearProyecto.js.map +0 -1
- package/dist/agent/descarga.js.map +0 -1
- package/dist/agent/diagnosticoDeTools.js.map +0 -1
- package/dist/agent/dispositivosEnMaquina.js.map +0 -1
- package/dist/agent/entorno.js.map +0 -1
- package/dist/agent/escrituraDeCodex.js.map +0 -1
- package/dist/agent/escrituraDeOpencode.js.map +0 -1
- package/dist/agent/escrituraExterna.js.map +0 -1
- package/dist/agent/gemini.js.map +0 -1
- package/dist/agent/git.js.map +0 -1
- package/dist/agent/gitSync.js.map +0 -1
- package/dist/agent/guionizado.js.map +0 -1
- package/dist/agent/hotswap.js.map +0 -1
- package/dist/agent/instalacionEnMaquina.js.map +0 -1
- package/dist/agent/instantanea.js.map +0 -1
- package/dist/agent/interrupts.js.map +0 -1
- package/dist/agent/juezDeTarea.js.map +0 -1
- package/dist/agent/lanzamientoEnMaquina.js.map +0 -1
- package/dist/agent/manifiesto.js.map +0 -1
- package/dist/agent/memoriaDeProyecto.js.map +0 -1
- package/dist/agent/mensajes.js.map +0 -1
- package/dist/agent/modelos.js +0 -130
- package/dist/agent/modelos.js.map +0 -1
- package/dist/agent/modelosDeMotor.js.map +0 -1
- package/dist/agent/normalizar.js.map +0 -1
- package/dist/agent/paqueteDelProyecto.js.map +0 -1
- package/dist/agent/perfiles.js.map +0 -1
- package/dist/agent/persona.js.map +0 -1
- package/dist/agent/procesosEnMaquina.js.map +0 -1
- package/dist/agent/proyecto.js.map +0 -1
- package/dist/agent/puente.js.map +0 -1
- package/dist/agent/resumenDeContexto.js +0 -30
- package/dist/agent/resumenDeContexto.js.map +0 -1
- package/dist/agent/resumenDeTool.js.map +0 -1
- package/dist/agent/sesionGit.js.map +0 -1
- package/dist/agent/settingsEnDisco.js.map +0 -1
- package/dist/agent/skills.js +0 -65
- package/dist/agent/skills.js.map +0 -1
- package/dist/agent/subagenteCodex.js.map +0 -1
- package/dist/agent/subagenteExterno.js.map +0 -1
- package/dist/agent/subagenteOpencode.js.map +0 -1
- package/dist/agent/subida.js.map +0 -1
- package/dist/agent/tareasEnDisco.js.map +0 -1
- package/dist/agent/textoDeTool.js.map +0 -1
- package/dist/agent/turnoReal.js.map +0 -1
- package/dist/agent/verificador.js.map +0 -1
- package/dist/agent/versionEnDisco.js.map +0 -1
- package/dist/agent/xoneAgent.js +0 -347
- package/dist/agent/xoneAgent.js.map +0 -1
- package/dist/agent/zip.js.map +0 -1
- /package/dist/agent/{manifiesto.js → cloudstudio/manifiesto.js} +0 -0
- /package/dist/agent/{entorno.js → config/entorno.js} +0 -0
- /package/dist/agent/{gemini.js → config/gemini.js} +0 -0
- /package/dist/agent/{hotswap.js → dispositivos/hotswap.js} +0 -0
- /package/dist/agent/{instalacionEnMaquina.js → dispositivos/instalacionEnMaquina.js} +0 -0
- /package/dist/agent/{procesosEnMaquina.js → dispositivos/procesosEnMaquina.js} +0 -0
- /package/dist/agent/{git.js → sesiones/git.js} +0 -0
- /package/dist/agent/{consumoExterno.js → subagentes/consumoExterno.js} +0 -0
- /package/dist/agent/{escrituraDeCodex.js → subagentes/escrituraDeCodex.js} +0 -0
- /package/dist/agent/{subagenteCodex.js → subagentes/subagenteCodex.js} +0 -0
- /package/dist/agent/{mensajes.js → turno/mensajes.js} +0 -0
- /package/dist/agent/{normalizar.js → turno/normalizar.js} +0 -0
- /package/dist/agent/{textoDeTool.js → turno/textoDeTool.js} +0 -0
- /package/dist/agent/{verificador.js → turno/verificador.js} +0 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Poner ESTE proyecto en un Android y dejar la app arrancada. La plantilla completa.
|
|
4
|
+
*
|
|
5
|
+
* POR QUÉ EXISTE. «Lanza la app» no es un comando del protocolo hotswap: es una cadena de
|
|
6
|
+
* cinco pasos, y cuatro de ellos no viajan por el canal (el túnel, el ZIP, la subida HTTP y
|
|
7
|
+
* el reinicio). El canal solo hace el quinto. Esta plantilla es esa cadena, en el orden en el
|
|
8
|
+
* que se midió que funciona.
|
|
9
|
+
*
|
|
10
|
+
* USO (desde la raíz del proyecto, que es el `cwd` de tus comandos):
|
|
11
|
+
* xone-desplegar-android
|
|
12
|
+
* xone-desplegar-android --captura
|
|
13
|
+
* xone-desplegar-android --app MiApp --serie emulator-5554
|
|
14
|
+
*
|
|
15
|
+
* EL ORDEN, Y POR QUÉ ES ESE:
|
|
16
|
+
* 1. **El túnel primero.** Sin `adb forward`, `127.0.0.1:8443` no lleva a ninguna parte: el
|
|
17
|
+
* servidor escucha en el localhost del APARATO. No se quita al terminar — hay que
|
|
18
|
+
* reaplicarlo tras cada reconexión, así que quitarlo dejaría el siguiente intento hablando
|
|
19
|
+
* con nadie.
|
|
20
|
+
* 2. **El ZIP**, sin `.xonecode` ni `.git`. No es higiene: el `checkpoint.sqlite` de un
|
|
21
|
+
* proyecto real pesaba 122 MB él solo, contra 6,7 MB el proyecto entero.
|
|
22
|
+
* 3. **La subida.** Subir NO aplica: el proceso tiene cargado en memoria lo de antes.
|
|
23
|
+
* 4. **El reinicio**, que es lo único que aplica lo subido. Y es `SetupActivity`, **nunca**
|
|
24
|
+
* `.mainEntry`: medido, `.mainEntry` no levanta el servidor hotswap.
|
|
25
|
+
* 5. **El lanzamiento**, y LUEGO la comprobación: `{"result":true}` de `launchApplication`
|
|
26
|
+
* significa «aceptado», no «arrancó». Lo que dice si está viva es el árbol de controles,
|
|
27
|
+
* que tarda unos segundos en contestar algo.
|
|
28
|
+
*
|
|
29
|
+
* LA TRAMPA QUE CUESTA UNA SESIÓN: **sin `bd/gestion.db` la app no arranca** y el verificador
|
|
30
|
+
* estático dice VERDE igual. El `app.xml` declara `<connection connstring="bd/gestion.db"/>` y
|
|
31
|
+
* esa base se genera FUERA (XOne Studio o `xone-db-tools create-db`). Si la app muere con
|
|
32
|
+
* «Error opening database», mira eso antes que nada.
|
|
33
|
+
*
|
|
34
|
+
* LO QUE NECESITA: `adb` en el PATH, y nada más. El ZIP se construye aquí dentro con `zlib`
|
|
35
|
+
* —ni binario externo ni dependencias—, así que esto no depende de que haya un `zip` en el
|
|
36
|
+
* sistema, que en Windows no lo hay.
|
|
37
|
+
*/
|
|
38
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
39
|
+
// Y se calla SU aviso, no todos: node grita en cada arranque que la verificación está
|
|
40
|
+
// desactivada, y ese grito acaba en la salida del comando —o sea en el contexto del agente—
|
|
41
|
+
// invitándole a «arreglar» algo que es deliberado y está acotado a este proceso hijo.
|
|
42
|
+
process.removeAllListeners("warning");
|
|
43
|
+
process.on("warning", (aviso) => {
|
|
44
|
+
if (!String(aviso.message).includes("NODE_TLS_REJECT_UNAUTHORIZED")) console.warn(aviso);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
import { execFileSync } from "node:child_process";
|
|
48
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
49
|
+
import { basename, join, relative, sep } from "node:path";
|
|
50
|
+
import { deflateRawSync } from "node:zlib";
|
|
51
|
+
|
|
52
|
+
const PAQUETE = "com.xone.android.framework";
|
|
53
|
+
const ACTIVIDAD = `${PAQUETE}/com.xone.android.hotswap.activities.SetupActivity`;
|
|
54
|
+
const PUERTO = 8443;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Un ZIP en memoria, sin `zip` del sistema y sin dependencias.
|
|
58
|
+
*
|
|
59
|
+
* Se escribió a mano por dos razones, y ninguna es el gusto por reinventar: **en Windows no
|
|
60
|
+
* hay `zip`**, así que llamar al binario dejaba esta plantilla sirviendo en dos sistemas de
|
|
61
|
+
* tres; y una skill instalada en la casa del usuario no tiene `node_modules` donde resolver
|
|
62
|
+
* una librería. Con `zlib`, que viene con node, no hace falta ninguna de las dos cosas.
|
|
63
|
+
*
|
|
64
|
+
* Es el formato mínimo y suficiente: entradas DEFLATE con cabecera local, directorio central y
|
|
65
|
+
* su cola. Sin ZIP64 —un proyecto XOne no llega a 4 GB ni a 65.535 ficheros— y sin atributos de
|
|
66
|
+
* Unix, que el framework no mira.
|
|
67
|
+
*/
|
|
68
|
+
const TABLA_CRC = (() => {
|
|
69
|
+
const tabla = new Uint32Array(256);
|
|
70
|
+
for (let i = 0; i < 256; i++) {
|
|
71
|
+
let c = i;
|
|
72
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
73
|
+
tabla[i] = c >>> 0;
|
|
74
|
+
}
|
|
75
|
+
return tabla;
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
function crc32(buf) {
|
|
79
|
+
let c = 0xffffffff;
|
|
80
|
+
for (let i = 0; i < buf.length; i++) c = TABLA_CRC[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
81
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Los ficheros del proyecto, en rutas relativas con `/`, saltándose lo que no sube. */
|
|
85
|
+
function ficherosDe(raiz, fuera, dentro = raiz, salida = []) {
|
|
86
|
+
for (const entrada of readdirSync(dentro, { withFileTypes: true })) {
|
|
87
|
+
const completa = join(dentro, entrada.name);
|
|
88
|
+
const rel = relative(raiz, completa).split(sep).join("/");
|
|
89
|
+
if (fuera.some((f) => rel === f || rel.startsWith(`${f}/`))) continue;
|
|
90
|
+
// No se sigue un enlace: un symlink a una carpeta de fuera metería el disco entero.
|
|
91
|
+
if (entrada.isSymbolicLink()) continue;
|
|
92
|
+
if (entrada.isDirectory()) ficherosDe(raiz, fuera, completa, salida);
|
|
93
|
+
else if (entrada.isFile()) salida.push(rel);
|
|
94
|
+
}
|
|
95
|
+
return salida;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function crearZip(raiz, fuera) {
|
|
99
|
+
const locales = [];
|
|
100
|
+
const central = [];
|
|
101
|
+
let desplazamiento = 0;
|
|
102
|
+
|
|
103
|
+
for (const rel of ficherosDe(raiz, fuera)) {
|
|
104
|
+
const datos = readFileSync(join(raiz, rel));
|
|
105
|
+
const comprimido = deflateRawSync(datos);
|
|
106
|
+
const nombre = Buffer.from(rel, "utf8");
|
|
107
|
+
const suma = crc32(datos);
|
|
108
|
+
|
|
109
|
+
const local = Buffer.alloc(30);
|
|
110
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
111
|
+
local.writeUInt16LE(20, 4); // versión mínima
|
|
112
|
+
local.writeUInt16LE(0x0800, 6); // nombres en UTF-8
|
|
113
|
+
local.writeUInt16LE(8, 8); // deflate
|
|
114
|
+
local.writeUInt32LE(suma, 14);
|
|
115
|
+
local.writeUInt32LE(comprimido.length, 18);
|
|
116
|
+
local.writeUInt32LE(datos.length, 22);
|
|
117
|
+
local.writeUInt16LE(nombre.length, 26);
|
|
118
|
+
locales.push(local, nombre, comprimido);
|
|
119
|
+
|
|
120
|
+
const entrada = Buffer.alloc(46);
|
|
121
|
+
entrada.writeUInt32LE(0x02014b50, 0);
|
|
122
|
+
entrada.writeUInt16LE(20, 4);
|
|
123
|
+
entrada.writeUInt16LE(20, 6);
|
|
124
|
+
entrada.writeUInt16LE(0x0800, 8);
|
|
125
|
+
entrada.writeUInt16LE(8, 10);
|
|
126
|
+
entrada.writeUInt32LE(suma, 16);
|
|
127
|
+
entrada.writeUInt32LE(comprimido.length, 20);
|
|
128
|
+
entrada.writeUInt32LE(datos.length, 24);
|
|
129
|
+
entrada.writeUInt16LE(nombre.length, 28);
|
|
130
|
+
entrada.writeUInt32LE(desplazamiento, 42);
|
|
131
|
+
central.push(entrada, nombre);
|
|
132
|
+
|
|
133
|
+
desplazamiento += local.length + nombre.length + comprimido.length;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const cuerpoCentral = Buffer.concat(central);
|
|
137
|
+
const cola = Buffer.alloc(22);
|
|
138
|
+
const cuantos = central.length / 2;
|
|
139
|
+
cola.writeUInt32LE(0x06054b50, 0);
|
|
140
|
+
cola.writeUInt16LE(cuantos, 8);
|
|
141
|
+
cola.writeUInt16LE(cuantos, 10);
|
|
142
|
+
cola.writeUInt32LE(cuerpoCentral.length, 12);
|
|
143
|
+
cola.writeUInt32LE(desplazamiento, 16);
|
|
144
|
+
return Buffer.concat([...locales, cuerpoCentral, cola]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const args = process.argv.slice(2);
|
|
148
|
+
const opcion = (nombre) => {
|
|
149
|
+
const i = args.indexOf(`--${nombre}`);
|
|
150
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
154
|
+
// **Tiene que existir**, y esto no es cortesía: sin `--help` esta orden DESPLEGABA, así que
|
|
155
|
+
// un agente que la usa para orientarse lanzaba el proceso entero creyendo que preguntaba —
|
|
156
|
+
// medido, lo repitió 75 veces en un turno. Una opción que no se entiende no puede ejecutar.
|
|
157
|
+
console.error(`xone-desplegar-android — pone ESTE proyecto en un Android y deja la app arrancada.
|
|
158
|
+
|
|
159
|
+
xone-desplegar-android nombre de app tomado de app.xml
|
|
160
|
+
xone-desplegar-android --app MiApp nombre explícito
|
|
161
|
+
xone-desplegar-android --serie emulator-5554 con varios aparatos conectados
|
|
162
|
+
|
|
163
|
+
Hace la cadena entera: túnel adb, ZIP del proyecto (sin .xonecode ni .git), subida, reinicio del
|
|
164
|
+
framework y lanzamiento, y COMPRUEBA que la app esté viva pidiendo su árbol de controles.
|
|
165
|
+
Necesita \`adb\` en el PATH; el ZIP lo construye él mismo.
|
|
166
|
+
|
|
167
|
+
Si la app no arranca, lo primero que hay que mirar es si existe bd/gestion.db: la genera XOne
|
|
168
|
+
Studio o \`xone-db-tools\`, no esto, y sin ella la app muere aunque el verificador esté en verde.`);
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const serie = opcion("serie");
|
|
173
|
+
const adb = (...partes) =>
|
|
174
|
+
execFileSync("adb", [...(serie ? ["-s", serie] : []), ...partes], { encoding: "utf8" });
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Igual, pero sin dejar hablar al hijo por stderr.
|
|
178
|
+
*
|
|
179
|
+
* `execFileSync` hereda stderr por omisión, así que un `pm grant` de un permiso que no es de
|
|
180
|
+
* ejecución escupe una traza de Java entera. Con 63 permisos declarados eso son decenas de
|
|
181
|
+
* pantallas de ruido que acaban en el contexto del agente y le hacen creer que algo se rompió.
|
|
182
|
+
*/
|
|
183
|
+
const adbCallado = (...partes) =>
|
|
184
|
+
execFileSync("adb", [...(serie ? ["-s", serie] : []), ...partes], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Cómo se llama la app, en tres saltos y con el último declarado.
|
|
188
|
+
*
|
|
189
|
+
* **El `<app>` de un proyecto XOne real NO lleva `name`** — medido sobre cuatro proyectos del
|
|
190
|
+
* usuario: lleva `prefix`, `version`, `debug`… y nada más. Leerlo de ahí fue el primer intento y
|
|
191
|
+
* falla en todos menos en los de juguete. Donde sí está, cuando está, es en `app.ini`
|
|
192
|
+
* (`name=AppDemo`); y varios proyectos reales ni siquiera tienen `app.ini`.
|
|
193
|
+
*
|
|
194
|
+
* Por eso el último salto es **la carpeta**, que es lo que una persona usaría y lo que casa con
|
|
195
|
+
* cómo guarda el framework lo que sube (`app_<nombre en minúsculas>`). Se DICE cuál se usó, para
|
|
196
|
+
* que quien mire la salida pueda corregir con `--app` en vez de descubrirlo por el resultado.
|
|
197
|
+
*/
|
|
198
|
+
function nombreDeLaApp() {
|
|
199
|
+
const dicho = opcion("app");
|
|
200
|
+
if (dicho !== undefined) return dicho;
|
|
201
|
+
|
|
202
|
+
if (existsSync("app.ini")) {
|
|
203
|
+
const m = /^\s*name\s*=\s*(.+?)\s*$/im.exec(readFileSync("app.ini", "latin1"));
|
|
204
|
+
if (m !== null && m[1] !== "") {
|
|
205
|
+
console.log(` nombre de app.ini: ${m[1]}`);
|
|
206
|
+
return m[1];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const deLaCarpeta = basename(process.cwd());
|
|
211
|
+
console.log(` nombre de la carpeta: ${deLaCarpeta} (usa --app si no es ése)`);
|
|
212
|
+
return deLaCarpeta;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Concede los permisos de ejecución que el host pide, antes de reiniciarlo.
|
|
217
|
+
*
|
|
218
|
+
* **Sin esto la app no arranca y el síntoma engaña.** Medido: tras el reinicio, Android pone
|
|
219
|
+
* encima su `GrantPermissionsActivity` y la app se queda detrás esperando a que alguien toque
|
|
220
|
+
* «Permitir». El canal deja de contestar y el mensaje honesto —«no llegó a contestar el árbol»—
|
|
221
|
+
* manda a mirar la base de datos, que estaba perfectamente. O sea: un diálogo del SISTEMA
|
|
222
|
+
* disfrazado de app rota.
|
|
223
|
+
*
|
|
224
|
+
* Es lo mismo que hace `adb install -g`, y sobre un aparato de PRUEBAS es lo razonable: lo que
|
|
225
|
+
* se está automatizando es pulsar «Permitir» quince veces. Se conceden solo los que el paquete
|
|
226
|
+
* DECLARA —se leen del aparato, no de una lista nuestra que caducaría con la siguiente versión
|
|
227
|
+
* del framework— y los que no son de ejecución fallan uno a uno sin parar nada.
|
|
228
|
+
*/
|
|
229
|
+
function concederPermisos() {
|
|
230
|
+
let declaradas;
|
|
231
|
+
try {
|
|
232
|
+
const volcado = adb("shell", "dumpsys", "package", PAQUETE);
|
|
233
|
+
const trozo = /requested permissions:([\s\S]*?)(?:install permissions:|runtime permissions:|$)/.exec(volcado);
|
|
234
|
+
declaradas = (trozo?.[1] ?? "").split("\n").map((l) => l.trim().split(":")[0]).filter((l) => l.startsWith("android.permission."));
|
|
235
|
+
} catch {
|
|
236
|
+
console.log(" no se pudo leer qué permisos pide; sigo");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
let dados = 0;
|
|
240
|
+
for (const permiso of declaradas) {
|
|
241
|
+
try {
|
|
242
|
+
adbCallado("shell", "pm", "grant", PAQUETE, permiso);
|
|
243
|
+
dados += 1;
|
|
244
|
+
} catch {
|
|
245
|
+
// Los que no son de ejecución (`INTERNET`, `FOREGROUND_SERVICE`…) no se conceden así y
|
|
246
|
+
// fallan uno a uno. No es un error: es que no había nada que conceder.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
console.log(` ${dados} permiso(s) concedido(s) de ${declaradas.length} declarado(s)`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* La excepción de XOne de ESTE arranque, en las cuatro líneas que importan, o nada.
|
|
254
|
+
*
|
|
255
|
+
* Se lee del log y no de la pantalla porque el diálogo enseña el mensaje recortado y sin el
|
|
256
|
+
* fichero ni la línea; el log trae `Source`, `Cause`, `Line number` y `In include file`, que es
|
|
257
|
+
* lo que deja ir directo al sitio. Se piden pocas líneas: lo que se busca está al principio del
|
|
258
|
+
* arranque, y un volcado grande aquí sería ruido en la salida de un comando.
|
|
259
|
+
*/
|
|
260
|
+
function excepcionDeXOne() {
|
|
261
|
+
let log;
|
|
262
|
+
try {
|
|
263
|
+
log = adbCallado("logcat", "-d", "-t", "600");
|
|
264
|
+
} catch {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
const filas = log.split("\n");
|
|
268
|
+
const donde = filas.findIndex((l) => l.includes("XOneJavascriptException"));
|
|
269
|
+
if (donde < 0) return undefined;
|
|
270
|
+
// Desde la excepción, lo que viene detrás es su descripción: fuente, causa, línea y fichero.
|
|
271
|
+
const utiles = filas
|
|
272
|
+
.slice(donde, donde + 12)
|
|
273
|
+
.map((l) => l.replace(/^.*?\bSystem\.err:\s?/, "").trim())
|
|
274
|
+
.filter((l) => l !== "" && !l.startsWith("at "));
|
|
275
|
+
return utiles.slice(0, 6).join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const app = nombreDeLaApp();
|
|
279
|
+
|
|
280
|
+
{
|
|
281
|
+
/**
|
|
282
|
+
* Sin aparato NO se empieza, y se dice el siguiente paso HECHO.
|
|
283
|
+
*
|
|
284
|
+
* Antes el primer `adb forward` moría con «no devices/emulators found», que es cierto y no
|
|
285
|
+
* dice qué hacer: medido, el modelo se ponía a componer el arranque a mano —y `emulator` no
|
|
286
|
+
* está en el PATH—. Es la regla de `porQueNo`: cuando sabemos la salida, se escribe, no se
|
|
287
|
+
* aconseja.
|
|
288
|
+
*/
|
|
289
|
+
function exigirAparato() {
|
|
290
|
+
let lista = "";
|
|
291
|
+
try {
|
|
292
|
+
lista = execFileSync("adb", ["devices"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error(`no pude hablar con adb: ${error.message.split("\n")[0]}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
const hay = lista
|
|
298
|
+
.split(/\r?\n/)
|
|
299
|
+
.slice(1)
|
|
300
|
+
.some((l) => /\sdevice$/.test(l.trim()));
|
|
301
|
+
if (!hay) {
|
|
302
|
+
console.error("no hay ningún aparato conectado. Arranca el emulador primero:");
|
|
303
|
+
console.error(" xone-arrancar-android");
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
exigirAparato();
|
|
309
|
+
console.log(`1/5 túnel: ${PUERTO}`);
|
|
310
|
+
adb("forward", `tcp:${PUERTO}`, `tcp:${PUERTO}`);
|
|
311
|
+
|
|
312
|
+
console.log("2/5 empaquetando el proyecto (sin .xonecode ni .git)");
|
|
313
|
+
const cuerpo = crearZip(process.cwd(), [".xonecode", ".git"]);
|
|
314
|
+
console.log(` ${(cuerpo.length / 1024).toFixed(0)} KB`);
|
|
315
|
+
|
|
316
|
+
console.log(`3/5 subiendo a ${app}`);
|
|
317
|
+
const url = `https://127.0.0.1:${PUERTO}/file_upload?file=debug_app_update.zip&appName=${encodeURIComponent(app)}`;
|
|
318
|
+
const respuesta = await fetch(url, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
body: cuerpo,
|
|
321
|
+
headers: { "Content-Type": "application/octet-stream", "Content-Length": String(cuerpo.length) },
|
|
322
|
+
});
|
|
323
|
+
if (!respuesta.ok) {
|
|
324
|
+
console.error(`la subida falló con ${respuesta.status}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log("4/5 permisos y reinicio del framework");
|
|
329
|
+
concederPermisos();
|
|
330
|
+
// El buffer se vacía ANTES de reiniciar para que lo que se lea después sea de ESTE arranque
|
|
331
|
+
// y no la excepción del intento anterior — que es la forma más fácil de «arreglar» algo y
|
|
332
|
+
// creerse que sigue roto, o al revés.
|
|
333
|
+
try {
|
|
334
|
+
adbCallado("logcat", "-c");
|
|
335
|
+
} catch {
|
|
336
|
+
// Si no se puede vaciar, se sigue: el diagnóstico será menos preciso, no falso.
|
|
337
|
+
}
|
|
338
|
+
adb("shell", "am", "force-stop", PAQUETE);
|
|
339
|
+
adb("shell", "am", "start", "-n", ACTIVIDAD);
|
|
340
|
+
|
|
341
|
+
console.log("5/5 esperando al canal, lanzando y comprobando que está VIVA");
|
|
342
|
+
const cliente = join(import.meta.dirname, "xone-hotswap");
|
|
343
|
+
const mandar = (comando) =>
|
|
344
|
+
execFileSync("node", [cliente, JSON.stringify(comando)], { encoding: "utf8" }).trim();
|
|
345
|
+
const esperar = (segundos) => execFileSync("sleep", [String(segundos)]);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* **El canal no está en pie justo después del reinicio**, y esto se descubrió corriéndolo:
|
|
349
|
+
* el `am start` vuelve en cuanto Android acepta el intent, no cuando el servidor escucha,
|
|
350
|
+
* así que el primer intento moría con «Received network error or non-101 status code».
|
|
351
|
+
* Se espera a que CONTESTE, que es la condición de verdad, en vez de dormir un rato fijo.
|
|
352
|
+
*/
|
|
353
|
+
let listo = false;
|
|
354
|
+
for (let intento = 1; intento <= 15 && !listo; intento++) {
|
|
355
|
+
try {
|
|
356
|
+
mandar({ command: "launchApplication", appName: app });
|
|
357
|
+
listo = true;
|
|
358
|
+
} catch {
|
|
359
|
+
esperar(1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!listo) {
|
|
363
|
+
console.error(`el servidor hotswap no levantó tras el reinicio. Comprueba que ${PAQUETE} está instalado`);
|
|
364
|
+
console.error("y que el puerto es el 8443 (si estaba ocupado, el framework coge el siguiente libre).");
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// El árbol es la comprobación: `result:true` solo dice que el encargo se aceptó. Se
|
|
369
|
+
// reintenta porque la app tarda unos segundos en tener pantalla, y se espera a una
|
|
370
|
+
// CONDICIÓN y no a un número fijo de segundos.
|
|
371
|
+
for (let intento = 1; intento <= 10; intento++) {
|
|
372
|
+
const salida = mandar({ command: "getAllElements", format: "xone" });
|
|
373
|
+
if (!salida.includes("App is not running")) {
|
|
374
|
+
console.log(salida);
|
|
375
|
+
/**
|
|
376
|
+
* **La captura en la MISMA orden, porque lo que se paga es la ida y vuelta.** Medido: un
|
|
377
|
+
* «lanza la app y sácame una captura» costaba 17 llamadas al modelo, y cada una reenvía
|
|
378
|
+
* el historial entero — mientras que lo que devuelve cada comando es pequeño (1,6 KB el
|
|
379
|
+
* árbol, 108 bytes la captura). O sea que el gasto no es lo que se dice, es cuántas veces
|
|
380
|
+
* hay que volver a preguntar. Tres órdenes encadenadas por el modelo son tres viajes;
|
|
381
|
+
* ésta es una.
|
|
382
|
+
*/
|
|
383
|
+
if (args.includes("--captura")) {
|
|
384
|
+
try {
|
|
385
|
+
console.log(mandar({ command: "getScreenshot" }));
|
|
386
|
+
} catch {
|
|
387
|
+
console.error("la app arrancó pero la captura falló; el árbol de arriba sí es bueno.");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// **Estar VIVA no es estar BIEN.** Una app que revienta al cargar la configuración se
|
|
391
|
+
// queda con su diálogo de error delante, y el canal contesta igual: el árbol llega, la
|
|
392
|
+
// captura sale, y todo parece haber ido bien. Quien lo cuenta es el log, y no pedirlo
|
|
393
|
+
// aquí obliga a que alguien se acuerde de hacerlo — que es justo lo que no pasa.
|
|
394
|
+
const roto = excepcionDeXOne();
|
|
395
|
+
if (roto !== undefined) {
|
|
396
|
+
console.error("");
|
|
397
|
+
console.error("La app ARRANCÓ pero XOne lanzó una excepción al cargar:");
|
|
398
|
+
console.error(roto);
|
|
399
|
+
console.error("");
|
|
400
|
+
console.error("El runtime de XOne es ES5 (Rhino): nada de `let`, `const`, `=>` ni comas");
|
|
401
|
+
console.error("sobrantes, y una `var` no se puede declarar dos veces en la misma función.");
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
406
|
+
esperar(2);
|
|
407
|
+
}
|
|
408
|
+
console.error("la app no llegó a contestar el árbol de controles: NO está viva.");
|
|
409
|
+
// **Lo primero, la excepción**, y antes que cualquier consejo: si XOne reventó al cargar, eso
|
|
410
|
+
// es la respuesta entera y todo lo demás sobra. Aquí hace MÁS falta que en la rama de «está
|
|
411
|
+
// viva»: una app que no llega a arrancar no tiene pantalla que mirar ni árbol que pedir, así
|
|
412
|
+
// que sin esto lo único que queda es adivinar.
|
|
413
|
+
const roto = excepcionDeXOne();
|
|
414
|
+
if (roto !== undefined) {
|
|
415
|
+
console.error("");
|
|
416
|
+
console.error("XOne lanzó una excepción al cargar:");
|
|
417
|
+
console.error(roto);
|
|
418
|
+
console.error("");
|
|
419
|
+
console.error("El motor JS de XOne en Android es Rhino: un error de sintaxis en un fichero");
|
|
420
|
+
console.error("incluido para la app ENTERA. Mira el `;` al final de cada sentencia, las comas");
|
|
421
|
+
console.error("sobrantes y las `var` declaradas dos veces en la misma función.");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
// Antes de mandar a mirar la base de datos, se mira QUÉ hay en pantalla: un diálogo del
|
|
425
|
+
// sistema encima es el falso culpable más caro de este bucle.
|
|
426
|
+
try {
|
|
427
|
+
const foco = adb("shell", "dumpsys", "window").split("\n").find((l) => l.includes("mCurrentFocus")) ?? "";
|
|
428
|
+
if (foco.trim() !== "") console.error(`En pantalla ahora: ${foco.trim()}`);
|
|
429
|
+
if (foco.includes("permissioncontroller")) {
|
|
430
|
+
console.error("Hay un diálogo de PERMISOS de Android encima: la app está detrás, esperando.");
|
|
431
|
+
console.error("Vuelve a intentarlo; si insiste, contéstalo en el aparato.");
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
// Si no se puede preguntar, se sigue con el consejo de siempre.
|
|
436
|
+
}
|
|
437
|
+
console.error("Lo primero que hay que mirar es si existe bd/gestion.db (la genera XOne Studio, no esto).");
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* El canal de comandos del servidor hotswap, desde la línea de órdenes.
|
|
4
|
+
*
|
|
5
|
+
* POR QUÉ EXISTE ESTE FICHERO. Los comandos del servidor hotswap viajan por un **WebSocket**,
|
|
6
|
+
* y `curl` no habla WebSocket. En Android 5.0.2.2dev el `POST /command` por HTTP contesta
|
|
7
|
+
* **404** (eso es del flavor `playStoreDeveloper`, no del standalone), así que sin un cliente
|
|
8
|
+
* no hay forma de mandar `getAllElements` ni `getScreenshot` desde una shell. Esto es ese
|
|
9
|
+
* cliente, y por eso vive dentro de la skill: la skill dice QUÉ comandos hay, y esto es con
|
|
10
|
+
* qué se mandan.
|
|
11
|
+
*
|
|
12
|
+
* USO
|
|
13
|
+
* node "$XONECODE_SKILL_XONE_HOTSWAP/scripts/hotswap.mjs" '{"command":"getAllElements","format":"xone"}'
|
|
14
|
+
* node "$XONECODE_SKILL_XONE_HOTSWAP/scripts/hotswap.mjs" '{"command":"launchApplication","appName":"miapp"}' '{"command":"getScreenshot"}'
|
|
15
|
+
*
|
|
16
|
+
* Los comandos se mandan EN ORDEN y cada uno espera la respuesta del anterior.
|
|
17
|
+
*
|
|
18
|
+
* ANTES DE LLAMARLO (Android): tiene que estar el túnel, o `127.0.0.1:8443` no lleva a
|
|
19
|
+
* ninguna parte —el servidor escucha en el localhost del APARATO—:
|
|
20
|
+
* adb forward tcp:8443 tcp:8443
|
|
21
|
+
* En iOS no hay túnel: el simulador comparte la pila de red del Mac.
|
|
22
|
+
*
|
|
23
|
+
* VARIABLES
|
|
24
|
+
* HOTSWAP_URL el destino. Por omisión `wss://127.0.0.1:8443/hotswap`.
|
|
25
|
+
* XONECODE_ARTEFACTOS dónde dejar lo binario (una captura). Si no está, se dice y no se
|
|
26
|
+
* guarda: escribirlo en el proyecto sería ensuciar la app del usuario.
|
|
27
|
+
* HOTSWAP_TOPE_MS tope de espera de UNA respuesta. Por omisión 40000.
|
|
28
|
+
*
|
|
29
|
+
* LO QUE IMPRIME: una línea JSON por respuesta, en stdout. Una respuesta con un binario
|
|
30
|
+
* dentro NO se imprime: se guarda a fichero y lo que sale es su nombre y su tamaño. Un
|
|
31
|
+
* `getScreenshot` son ~57.000 caracteres de base64, y volcarlos en la salida de un comando
|
|
32
|
+
* es meterlos en el contexto para siempre sin que nadie pueda mirarlos.
|
|
33
|
+
*
|
|
34
|
+
* TRES COSAS MEDIDAS contra el framework real (Android 5.0.2.2dev, 16-09-2026), que son las
|
|
35
|
+
* que hacen que esto funcione y un cliente escrito de memoria no:
|
|
36
|
+
* - **El WebSocket vive en `/hotswap`**, no en la raíz: `/`, `/ws`, `/websocket` dan 400.
|
|
37
|
+
* - **El servidor habla PRIMERO**: al abrir manda `{"command":"server_hello","protocol_version":2}`.
|
|
38
|
+
* Se ESPERA ese saludo antes de mandar nada; no se provoca.
|
|
39
|
+
* - **La respuesta llega en `status`**, no en un campo `image`, y una captura viene en
|
|
40
|
+
* **JPEG** (`/9j/…`), no en PNG.
|
|
41
|
+
*/
|
|
42
|
+
// El certificado del aparato es autofirmado. Esto vale para ESTE proceso hijo y solo para él
|
|
43
|
+
// —no para el harness—, que es justo por lo que el cliente de xonecode no puede hacerlo así.
|
|
44
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
45
|
+
// Y se calla SU aviso, no todos: node grita en cada arranque que la verificación está
|
|
46
|
+
// desactivada, y ese grito acaba en la salida del comando —o sea en el contexto del agente—
|
|
47
|
+
// invitándole a «arreglar» algo que es deliberado y está acotado a este proceso hijo.
|
|
48
|
+
process.removeAllListeners("warning");
|
|
49
|
+
process.on("warning", (aviso) => {
|
|
50
|
+
if (!String(aviso.message).includes("NODE_TLS_REJECT_UNAUTHORIZED")) console.warn(aviso);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
import { writeFileSync } from "node:fs";
|
|
54
|
+
import { join } from "node:path";
|
|
55
|
+
|
|
56
|
+
const URL_POR_OMISION = "wss://127.0.0.1:8443/hotswap";
|
|
57
|
+
const TOPE_MS = Number(process.env.HOTSWAP_TOPE_MS ?? 40_000);
|
|
58
|
+
const ARTEFACTOS = process.env.XONECODE_ARTEFACTOS;
|
|
59
|
+
/**
|
|
60
|
+
* A partir de aquí una respuesta de TEXTO se guarda en vez de imprimirse.
|
|
61
|
+
*
|
|
62
|
+
* No decide si algo es binario —eso lo dice `extensionDe` mirando los bytes—, solo si cabe en
|
|
63
|
+
* la salida de un comando. El árbol de controles de una pantalla llena son varios miles de
|
|
64
|
+
* caracteres y volcarlos enteros en el contexto del agente cuesta más que la pregunta.
|
|
65
|
+
*/
|
|
66
|
+
const LARGO_PARA_GUARDAR = 2_000;
|
|
67
|
+
|
|
68
|
+
const pedidos = process.argv.slice(2);
|
|
69
|
+
if (pedidos.length === 0 || pedidos.includes("--help") || pedidos.includes("-h")) {
|
|
70
|
+
// El prompt del agente promete que `--help` contesta. Una promesa así, sin cumplir, cuesta
|
|
71
|
+
// un viaje y deja al modelo adivinando argumentos.
|
|
72
|
+
console.error(`xone-hotswap — manda comandos al servidor hotswap de la app (WebSocket).
|
|
73
|
+
|
|
74
|
+
xone-hotswap '{"command":"getAllElements","format":"xone"}'
|
|
75
|
+
xone-hotswap '{"command":"getScreenshot"}' la captura va a $XONECODE_ARTEFACTOS
|
|
76
|
+
xone-hotswap '{"command":"click","name":"MAP_BT"}' varios comandos, en orden, si pasas varios
|
|
77
|
+
|
|
78
|
+
Antes, en Android, hace falta el túnel: adb forward tcp:8443 tcp:8443
|
|
79
|
+
Variables: HOTSWAP_URL (${URL_POR_OMISION}), XONECODE_ARTEFACTOS, HOTSWAP_TOPE_MS.
|
|
80
|
+
El catálogo entero de comandos está en el SKILL.md de xone-hotswap.`);
|
|
81
|
+
process.exit(pedidos.length === 0 ? 1 : 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const comandos = [];
|
|
85
|
+
for (const arg of pedidos) {
|
|
86
|
+
try {
|
|
87
|
+
comandos.push(JSON.parse(arg));
|
|
88
|
+
} catch {
|
|
89
|
+
salir(`no es JSON válido: ${arg.slice(0, 80)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
function salir(motivo) {
|
|
95
|
+
console.error(`hotswap: ${motivo}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Qué es esto, por sus primeros bytes en base64 — nunca por el nombre del comando.
|
|
101
|
+
*
|
|
102
|
+
* **`undefined` significa «no es una imagen», y eso NO es «es binario desconocido».** Era el
|
|
103
|
+
* bug: cualquier campo de más de 2.000 caracteres se daba por base64 y se pasaba por
|
|
104
|
+
* `Buffer.from(valor, "base64")`. El árbol de `getAllElements` es JSON de TEXTO, así que en
|
|
105
|
+
* cuanto una pantalla tiene bastantes controles pasaba de ese largo, se «decodificaba»
|
|
106
|
+
* —descartando `{`, `"`, `:` y los espacios, que no son del alfabeto base64— y lo que quedaba
|
|
107
|
+
* en el disco eran unos 3/4 de bytes de basura, con nombre `captura-….bin`. Comprobado contra
|
|
108
|
+
* el aparato: el fichero real empezaba por `9da9 9e30 03ff` y un árbol JSON pasado por ese
|
|
109
|
+
* mismo `Buffer.from` empieza exactamente igual. O sea que el agente perdía la pantalla justo
|
|
110
|
+
* cuando la pantalla tenía algo que contar, y encima creía haber guardado una captura.
|
|
111
|
+
*/
|
|
112
|
+
function imagenDe(base64) {
|
|
113
|
+
if (base64.startsWith("/9j/")) return "jpg";
|
|
114
|
+
if (base64.startsWith("iVBORw0KGgo")) return "png";
|
|
115
|
+
if (base64.startsWith("UklGR")) return "webp";
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Guarda lo binario y lo sustituye por lo que se puede decir de ello.
|
|
121
|
+
*
|
|
122
|
+
* Mira TODOS los campos de la respuesta, no solo `status`: el campo donde viene el binario
|
|
123
|
+
* ha cambiado entre versiones, y un cliente que solo mire uno se calla justo cuando el
|
|
124
|
+
* servidor cambia de sitio.
|
|
125
|
+
*/
|
|
126
|
+
function sinBinarios(respuesta) {
|
|
127
|
+
const copia = { ...respuesta };
|
|
128
|
+
for (const [clave, valor] of Object.entries(copia)) {
|
|
129
|
+
if (typeof valor !== "string" || valor.length < LARGO_PARA_GUARDAR) continue;
|
|
130
|
+
const imagen = imagenDe(valor);
|
|
131
|
+
if (ARTEFACTOS === undefined) {
|
|
132
|
+
copia[clave] =
|
|
133
|
+
imagen === undefined
|
|
134
|
+
? `<${valor.length} caracteres; sin XONECODE_ARTEFACTOS no hay dónde guardarlos>`
|
|
135
|
+
: `<imagen ${imagen} de ${valor.length} car.; sin XONECODE_ARTEFACTOS no hay dónde guardarla>`;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Una imagen se DECODIFICA y se llama captura; lo demás es TEXTO y se guarda tal cual,
|
|
140
|
+
* con su extensión y con un nombre que dice lo que es. Llamar `captura-` a un árbol de
|
|
141
|
+
* controles no era solo feo: mandaba a quien lo leyera a buscar una foto.
|
|
142
|
+
*/
|
|
143
|
+
const nombre =
|
|
144
|
+
imagen === undefined
|
|
145
|
+
? `respuesta-${clave}-${Date.now()}.${valor.trimStart().startsWith("[") || valor.trimStart().startsWith("{") ? "json" : "txt"}`
|
|
146
|
+
: `captura-${Date.now()}.${imagen}`;
|
|
147
|
+
const bytes = imagen === undefined ? Buffer.from(valor, "utf8") : Buffer.from(valor, "base64");
|
|
148
|
+
writeFileSync(join(ARTEFACTOS, nombre), bytes);
|
|
149
|
+
copia[clave] = `<guardado como ${nombre}, ${bytes.length} bytes>`;
|
|
150
|
+
}
|
|
151
|
+
return copia;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const ws = new WebSocket(process.env.HOTSWAP_URL ?? URL_POR_OMISION);
|
|
155
|
+
let saludado = false;
|
|
156
|
+
let pendiente;
|
|
157
|
+
let reloj;
|
|
158
|
+
|
|
159
|
+
const rearmar = () => {
|
|
160
|
+
clearTimeout(reloj);
|
|
161
|
+
reloj = setTimeout(() => salir(`sin respuesta en ${TOPE_MS} ms`), TOPE_MS);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function siguiente() {
|
|
165
|
+
const comando = comandos.shift();
|
|
166
|
+
if (comando === undefined) {
|
|
167
|
+
clearTimeout(reloj);
|
|
168
|
+
ws.close();
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
pendiente = comando;
|
|
172
|
+
rearmar();
|
|
173
|
+
ws.send(JSON.stringify(comando));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
rearmar();
|
|
177
|
+
ws.onerror = (e) => salir(`no se pudo hablar con el aparato: ${e.message ?? e}`);
|
|
178
|
+
ws.onclose = (e) => {
|
|
179
|
+
if (pendiente !== undefined || comandos.length > 0) {
|
|
180
|
+
salir(`el canal se cerró antes de terminar (${e.code} ${e.reason || "sin motivo"})`);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
ws.onmessage = (ev) => {
|
|
184
|
+
const texto = typeof ev.data === "string" ? ev.data : Buffer.from(ev.data).toString();
|
|
185
|
+
let mensaje;
|
|
186
|
+
try {
|
|
187
|
+
mensaje = JSON.parse(texto);
|
|
188
|
+
} catch {
|
|
189
|
+
console.error(`hotswap: mensaje que no es JSON: ${texto.slice(0, 200)}`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// El servidor saluda primero y ese saludo no es la respuesta de nadie.
|
|
193
|
+
if (!saludado && mensaje.command === "server_hello") {
|
|
194
|
+
saludado = true;
|
|
195
|
+
siguiente();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
console.log(JSON.stringify({ comando: pendiente?.command, ...sinBinarios(mensaje) }));
|
|
199
|
+
pendiente = undefined;
|
|
200
|
+
siguiente();
|
|
201
|
+
};
|