xonecode 0.5.0 → 0.6.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.
Files changed (266) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +32 -0
  3. package/apps/web/dist/assets/index-DBZTodio.js +477 -0
  4. package/apps/web/dist/assets/{index-BB3SamhR.css → index-rqecK6s2.css} +1 -1
  5. package/apps/web/dist/assets/{php-D_2--9WK.js → php-BBij3Lhc.js} +1 -1
  6. package/apps/web/dist/assets/{ruby-DSHSDbk1.js → ruby-Dq2YEaLo.js} +1 -1
  7. package/apps/web/dist/index.html +2 -2
  8. package/dist/agent/cloudstudio/cloudstudioClient.js.map +1 -0
  9. package/dist/agent/{cloudstudioMcp.js → cloudstudio/cloudstudioMcp.js} +5 -5
  10. package/dist/agent/cloudstudio/cloudstudioMcp.js.map +1 -0
  11. package/dist/agent/{descarga.js → cloudstudio/descarga.js} +3 -3
  12. package/dist/agent/cloudstudio/descarga.js.map +1 -0
  13. package/dist/agent/cloudstudio/manifiesto.js.map +1 -0
  14. package/dist/agent/{subida.js → cloudstudio/subida.js} +3 -3
  15. package/dist/agent/cloudstudio/subida.js.map +1 -0
  16. package/dist/agent/{zip.js → cloudstudio/zip.js} +1 -1
  17. package/dist/agent/cloudstudio/zip.js.map +1 -0
  18. package/dist/agent/{authEnDisco.js → config/authEnDisco.js} +3 -3
  19. package/dist/agent/config/authEnDisco.js.map +1 -0
  20. package/dist/agent/{catalogoModelos.js → config/catalogoModelos.js} +2 -1
  21. package/dist/agent/config/catalogoModelos.js.map +1 -0
  22. package/dist/agent/{configEnDisco.js → config/configEnDisco.js} +6 -6
  23. package/dist/agent/config/configEnDisco.js.map +1 -0
  24. package/dist/agent/{crearProyecto.js → config/crearProyecto.js} +2 -2
  25. package/dist/agent/config/crearProyecto.js.map +1 -0
  26. package/dist/agent/config/entorno.js.map +1 -0
  27. package/dist/agent/config/gemini.js.map +1 -0
  28. package/dist/agent/{modelos.js → config/modelos.js} +17 -3
  29. package/dist/agent/config/modelos.js.map +1 -0
  30. package/dist/agent/{modelosDeMotor.js → config/modelosDeMotor.js} +1 -1
  31. package/dist/agent/config/modelosDeMotor.js.map +1 -0
  32. package/dist/agent/{persona.js → config/persona.js} +1 -1
  33. package/dist/agent/config/persona.js.map +1 -0
  34. package/dist/agent/{settingsEnDisco.js → config/settingsEnDisco.js} +2 -2
  35. package/dist/agent/config/settingsEnDisco.js.map +1 -0
  36. package/dist/agent/{versionEnDisco.js → config/versionEnDisco.js} +8 -4
  37. package/dist/agent/config/versionEnDisco.js.map +1 -0
  38. package/dist/agent/dispositivos/arranqueDeEmulador.js +146 -0
  39. package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -0
  40. package/dist/agent/{dispositivosEnMaquina.js → dispositivos/dispositivosEnMaquina.js} +93 -6
  41. package/dist/agent/dispositivos/dispositivosEnMaquina.js.map +1 -0
  42. package/dist/agent/dispositivos/hotswap.js.map +1 -0
  43. package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -0
  44. package/dist/agent/{lanzamientoEnMaquina.js → dispositivos/lanzamientoEnMaquina.js} +1 -1
  45. package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -0
  46. package/dist/agent/{paqueteDelProyecto.js → dispositivos/paqueteDelProyecto.js} +5 -5
  47. package/dist/agent/dispositivos/paqueteDelProyecto.js.map +1 -0
  48. package/dist/agent/dispositivos/procesosEnMaquina.js.map +1 -0
  49. package/dist/agent/{arbolDeProyecto.js → grafo/arbolDeProyecto.js} +2 -2
  50. package/dist/agent/grafo/arbolDeProyecto.js.map +1 -0
  51. package/dist/agent/{artefactosEnDisco.js → grafo/artefactosEnDisco.js} +2 -2
  52. package/dist/agent/grafo/artefactosEnDisco.js.map +1 -0
  53. package/dist/agent/{busquedaRegex.js → grafo/busquedaRegex.js} +41 -1
  54. package/dist/agent/grafo/busquedaRegex.js.map +1 -0
  55. package/dist/agent/grafo/excluirTools.js +82 -0
  56. package/dist/agent/grafo/excluirTools.js.map +1 -0
  57. package/dist/agent/{memoriaDeProyecto.js → grafo/memoriaDeProyecto.js} +1 -1
  58. package/dist/agent/grafo/memoriaDeProyecto.js.map +1 -0
  59. package/dist/agent/grafo/navegacionXone.js +198 -0
  60. package/dist/agent/grafo/navegacionXone.js.map +1 -0
  61. package/dist/agent/{perfiles.js → grafo/perfiles.js} +51 -8
  62. package/dist/agent/grafo/perfiles.js.map +1 -0
  63. package/dist/agent/{proyecto.js → grafo/proyecto.js} +207 -24
  64. package/dist/agent/grafo/proyecto.js.map +1 -0
  65. package/dist/agent/grafo/skills.js +393 -0
  66. package/dist/agent/grafo/skills.js.map +1 -0
  67. package/dist/agent/{xoneAgent.js → grafo/xoneAgent.js} +196 -67
  68. package/dist/agent/grafo/xoneAgent.js.map +1 -0
  69. package/dist/agent/navegacion/indiceEnDisco.js +21 -0
  70. package/dist/agent/navegacion/indiceEnDisco.js.map +1 -0
  71. package/dist/agent/navegacion/modeloDeProyecto.js +228 -0
  72. package/dist/agent/navegacion/modeloDeProyecto.js.map +1 -0
  73. package/dist/agent/raizDelPaquete.js +56 -0
  74. package/dist/agent/raizDelPaquete.js.map +1 -0
  75. package/dist/agent/{checkpointer.js → sesiones/checkpointer.js} +2 -2
  76. package/dist/agent/sesiones/checkpointer.js.map +1 -0
  77. package/dist/agent/sesiones/git.js.map +1 -0
  78. package/dist/agent/{gitSync.js → sesiones/gitSync.js} +3 -3
  79. package/dist/agent/sesiones/gitSync.js.map +1 -0
  80. package/dist/agent/{sesionGit.js → sesiones/sesionGit.js} +2 -2
  81. package/dist/agent/sesiones/sesionGit.js.map +1 -0
  82. package/dist/agent/{agentesEnDisco.js → subagentes/agentesEnDisco.js} +304 -86
  83. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -0
  84. package/dist/agent/subagentes/consumoExterno.js.map +1 -0
  85. package/dist/agent/subagentes/escrituraDeCodex.js.map +1 -0
  86. package/dist/agent/{escrituraDeOpencode.js → subagentes/escrituraDeOpencode.js} +1 -1
  87. package/dist/agent/subagentes/escrituraDeOpencode.js.map +1 -0
  88. package/dist/agent/{escrituraExterna.js → subagentes/escrituraExterna.js} +6 -6
  89. package/dist/agent/subagentes/escrituraExterna.js.map +1 -0
  90. package/dist/agent/subagentes/subagenteCodex.js.map +1 -0
  91. package/dist/agent/subagentes/subagenteExterno.js.map +1 -0
  92. package/dist/agent/subagentes/subagenteOpencode.js.map +1 -0
  93. package/dist/agent/{aumentador.js → tareas/aumentador.js} +2 -2
  94. package/dist/agent/tareas/aumentador.js.map +1 -0
  95. package/dist/agent/{juezDeTarea.js → tareas/juezDeTarea.js} +5 -5
  96. package/dist/agent/tareas/juezDeTarea.js.map +1 -0
  97. package/dist/agent/{tareasEnDisco.js → tareas/tareasEnDisco.js} +4 -4
  98. package/dist/agent/tareas/tareasEnDisco.js.map +1 -0
  99. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -0
  100. package/dist/agent/{guionizado.js → turno/guionizado.js} +1 -1
  101. package/dist/agent/turno/guionizado.js.map +1 -0
  102. package/dist/agent/turno/informeDeTraza.js +222 -0
  103. package/dist/agent/turno/informeDeTraza.js.map +1 -0
  104. package/dist/agent/turno/inspectorDePrompt.js +205 -0
  105. package/dist/agent/turno/inspectorDePrompt.js.map +1 -0
  106. package/dist/agent/{instantanea.js → turno/instantanea.js} +7 -2
  107. package/dist/agent/turno/instantanea.js.map +1 -0
  108. package/dist/agent/{interrupts.js → turno/interrupts.js} +2 -2
  109. package/dist/agent/turno/interrupts.js.map +1 -0
  110. package/dist/agent/turno/mensajes.js.map +1 -0
  111. package/dist/agent/turno/normalizar.js.map +1 -0
  112. package/dist/agent/{puente.js → turno/puente.js} +47 -3
  113. package/dist/agent/turno/puente.js.map +1 -0
  114. package/dist/agent/turno/resumenDeContexto.js +176 -0
  115. package/dist/agent/turno/resumenDeContexto.js.map +1 -0
  116. package/dist/agent/{resumenDeTool.js → turno/resumenDeTool.js} +15 -0
  117. package/dist/agent/turno/resumenDeTool.js.map +1 -0
  118. package/dist/agent/turno/textoDeTool.js.map +1 -0
  119. package/dist/agent/{turnoReal.js → turno/turnoReal.js} +38 -14
  120. package/dist/agent/turno/turnoReal.js.map +1 -0
  121. package/dist/agent/turno/verificador.js.map +1 -0
  122. package/dist/cli/config.js +1 -1
  123. package/dist/cli/config.js.map +1 -1
  124. package/dist/cli/consola.js +9 -9
  125. package/dist/cli/consola.js.map +1 -1
  126. package/dist/cli/describe.js +2 -2
  127. package/dist/cli/describe.js.map +1 -1
  128. package/dist/cli/doctor.js +1 -1
  129. package/dist/cli/doctor.js.map +1 -1
  130. package/dist/cli/fuentesDeDisco.js +29 -0
  131. package/dist/cli/fuentesDeDisco.js.map +1 -0
  132. package/dist/cli/main.js +37 -28
  133. package/dist/cli/main.js.map +1 -1
  134. package/dist/cli/run.js +32 -10
  135. package/dist/cli/run.js.map +1 -1
  136. package/dist/cli/traza.js +48 -0
  137. package/dist/cli/traza.js.map +1 -0
  138. package/dist/cli/tui/correrTui.js +4 -4
  139. package/dist/cli/tui/correrTui.js.map +1 -1
  140. package/dist/cli/verify.js +1 -1
  141. package/dist/cli/verify.js.map +1 -1
  142. package/dist/core/actos.js +1 -1
  143. package/dist/core/adjuntos.js +2 -2
  144. package/dist/core/agentes.js +145 -7
  145. package/dist/core/agentes.js.map +1 -1
  146. package/dist/core/artefactos.js +4 -4
  147. package/dist/core/config.js +1 -1
  148. package/dist/core/contextos.js +12 -1
  149. package/dist/core/contextos.js.map +1 -1
  150. package/dist/core/descargas.js +1 -1
  151. package/dist/core/dispositivos.js +53 -3
  152. package/dist/core/dispositivos.js.map +1 -1
  153. package/dist/core/esqueleto.js +1 -1
  154. package/dist/core/modelos.js +16 -3
  155. package/dist/core/modelos.js.map +1 -1
  156. package/dist/core/navegacion.js +134 -0
  157. package/dist/core/navegacion.js.map +1 -0
  158. package/dist/core/notify.js +5 -0
  159. package/dist/core/notify.js.map +1 -1
  160. package/dist/core/planDeSubida.js +1 -1
  161. package/dist/core/settings.js +2 -2
  162. package/dist/core/shellDeAgente.js +99 -0
  163. package/dist/core/shellDeAgente.js.map +1 -0
  164. package/dist/core/skills.js +158 -0
  165. package/dist/core/skills.js.map +1 -0
  166. package/dist/core/zipDeSkill.js +107 -0
  167. package/dist/core/zipDeSkill.js.map +1 -0
  168. package/dist/vendor/sqliteSaver.js +389 -0
  169. package/dist/vendor/sqliteSaver.js.map +1 -0
  170. package/dist/web/servidor/arranque.js +531 -47
  171. package/dist/web/servidor/arranque.js.map +1 -1
  172. package/dist/web/servidor/corredorDeTareas.js +3 -3
  173. package/dist/web/servidor/corredorDeTareas.js.map +1 -1
  174. package/dist/web/servidor/sesiones.js +4 -4
  175. package/dist/web/servidor/transporte.js +1 -1
  176. package/dist/web/servidor/transporte.js.map +1 -1
  177. package/dist/web/servidor/vestibulo.js +7 -7
  178. package/dist/web/servidor/vestibulo.js.map +1 -1
  179. package/package.json +9 -5
  180. package/skills/archify/SKILL.md +25 -2
  181. package/skills/artifacts-builder/SKILL.md +23 -0
  182. package/skills/xone-hotswap/SKILL.md +74 -0
  183. package/skills/xone-hotswap/scripts/xone-arrancar-ios +85 -0
  184. package/skills/xone-hotswap/scripts/xone-desplegar-android +411 -0
  185. package/skills/xone-hotswap/scripts/xone-hotswap +171 -0
  186. package/skills/xone-hotswap/scripts/xone-log-android +102 -0
  187. package/skills/xone-hotswap/scripts/xone-reiniciar-android +104 -0
  188. package/apps/web/dist/assets/index-CFYAoHdw.js +0 -477
  189. package/dist/agent/agentesEnDisco.js.map +0 -1
  190. package/dist/agent/arbolDeProyecto.js.map +0 -1
  191. package/dist/agent/artefactosEnDisco.js.map +0 -1
  192. package/dist/agent/aumentador.js.map +0 -1
  193. package/dist/agent/authEnDisco.js.map +0 -1
  194. package/dist/agent/busquedaRegex.js.map +0 -1
  195. package/dist/agent/catalogoModelos.js.map +0 -1
  196. package/dist/agent/checkpointer.js.map +0 -1
  197. package/dist/agent/cloudstudioClient.js.map +0 -1
  198. package/dist/agent/cloudstudioMcp.js.map +0 -1
  199. package/dist/agent/configEnDisco.js.map +0 -1
  200. package/dist/agent/consumoExterno.js.map +0 -1
  201. package/dist/agent/crearProyecto.js.map +0 -1
  202. package/dist/agent/descarga.js.map +0 -1
  203. package/dist/agent/diagnosticoDeTools.js.map +0 -1
  204. package/dist/agent/dispositivosEnMaquina.js.map +0 -1
  205. package/dist/agent/entorno.js.map +0 -1
  206. package/dist/agent/escrituraDeCodex.js.map +0 -1
  207. package/dist/agent/escrituraDeOpencode.js.map +0 -1
  208. package/dist/agent/escrituraExterna.js.map +0 -1
  209. package/dist/agent/gemini.js.map +0 -1
  210. package/dist/agent/git.js.map +0 -1
  211. package/dist/agent/gitSync.js.map +0 -1
  212. package/dist/agent/guionizado.js.map +0 -1
  213. package/dist/agent/hotswap.js.map +0 -1
  214. package/dist/agent/instalacionEnMaquina.js.map +0 -1
  215. package/dist/agent/instantanea.js.map +0 -1
  216. package/dist/agent/interrupts.js.map +0 -1
  217. package/dist/agent/juezDeTarea.js.map +0 -1
  218. package/dist/agent/lanzamientoEnMaquina.js.map +0 -1
  219. package/dist/agent/manifiesto.js.map +0 -1
  220. package/dist/agent/memoriaDeProyecto.js.map +0 -1
  221. package/dist/agent/mensajes.js.map +0 -1
  222. package/dist/agent/modelos.js.map +0 -1
  223. package/dist/agent/modelosDeMotor.js.map +0 -1
  224. package/dist/agent/normalizar.js.map +0 -1
  225. package/dist/agent/paqueteDelProyecto.js.map +0 -1
  226. package/dist/agent/perfiles.js.map +0 -1
  227. package/dist/agent/persona.js.map +0 -1
  228. package/dist/agent/procesosEnMaquina.js.map +0 -1
  229. package/dist/agent/proyecto.js.map +0 -1
  230. package/dist/agent/puente.js.map +0 -1
  231. package/dist/agent/resumenDeContexto.js +0 -30
  232. package/dist/agent/resumenDeContexto.js.map +0 -1
  233. package/dist/agent/resumenDeTool.js.map +0 -1
  234. package/dist/agent/sesionGit.js.map +0 -1
  235. package/dist/agent/settingsEnDisco.js.map +0 -1
  236. package/dist/agent/skills.js +0 -65
  237. package/dist/agent/skills.js.map +0 -1
  238. package/dist/agent/subagenteCodex.js.map +0 -1
  239. package/dist/agent/subagenteExterno.js.map +0 -1
  240. package/dist/agent/subagenteOpencode.js.map +0 -1
  241. package/dist/agent/subida.js.map +0 -1
  242. package/dist/agent/tareasEnDisco.js.map +0 -1
  243. package/dist/agent/textoDeTool.js.map +0 -1
  244. package/dist/agent/turnoReal.js.map +0 -1
  245. package/dist/agent/verificador.js.map +0 -1
  246. package/dist/agent/versionEnDisco.js.map +0 -1
  247. package/dist/agent/xoneAgent.js.map +0 -1
  248. package/dist/agent/zip.js.map +0 -1
  249. /package/dist/agent/{cloudstudioClient.js → cloudstudio/cloudstudioClient.js} +0 -0
  250. /package/dist/agent/{manifiesto.js → cloudstudio/manifiesto.js} +0 -0
  251. /package/dist/agent/{entorno.js → config/entorno.js} +0 -0
  252. /package/dist/agent/{gemini.js → config/gemini.js} +0 -0
  253. /package/dist/agent/{hotswap.js → dispositivos/hotswap.js} +0 -0
  254. /package/dist/agent/{instalacionEnMaquina.js → dispositivos/instalacionEnMaquina.js} +0 -0
  255. /package/dist/agent/{procesosEnMaquina.js → dispositivos/procesosEnMaquina.js} +0 -0
  256. /package/dist/agent/{git.js → sesiones/git.js} +0 -0
  257. /package/dist/agent/{consumoExterno.js → subagentes/consumoExterno.js} +0 -0
  258. /package/dist/agent/{escrituraDeCodex.js → subagentes/escrituraDeCodex.js} +0 -0
  259. /package/dist/agent/{subagenteCodex.js → subagentes/subagenteCodex.js} +0 -0
  260. /package/dist/agent/{subagenteExterno.js → subagentes/subagenteExterno.js} +0 -0
  261. /package/dist/agent/{subagenteOpencode.js → subagentes/subagenteOpencode.js} +0 -0
  262. /package/dist/agent/{diagnosticoDeTools.js → turno/diagnosticoDeTools.js} +0 -0
  263. /package/dist/agent/{mensajes.js → turno/mensajes.js} +0 -0
  264. /package/dist/agent/{normalizar.js → turno/normalizar.js} +0 -0
  265. /package/dist/agent/{textoDeTool.js → turno/textoDeTool.js} +0 -0
  266. /package/dist/agent/{verificador.js → turno/verificador.js} +0 -0
@@ -0,0 +1,411 @@
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
+ console.log(`1/5 túnel: ${PUERTO}`);
282
+ adb("forward", `tcp:${PUERTO}`, `tcp:${PUERTO}`);
283
+
284
+ console.log("2/5 empaquetando el proyecto (sin .xonecode ni .git)");
285
+ const cuerpo = crearZip(process.cwd(), [".xonecode", ".git"]);
286
+ console.log(` ${(cuerpo.length / 1024).toFixed(0)} KB`);
287
+
288
+ console.log(`3/5 subiendo a ${app}`);
289
+ const url = `https://127.0.0.1:${PUERTO}/file_upload?file=debug_app_update.zip&appName=${encodeURIComponent(app)}`;
290
+ const respuesta = await fetch(url, {
291
+ method: "POST",
292
+ body: cuerpo,
293
+ headers: { "Content-Type": "application/octet-stream", "Content-Length": String(cuerpo.length) },
294
+ });
295
+ if (!respuesta.ok) {
296
+ console.error(`la subida falló con ${respuesta.status}`);
297
+ process.exit(1);
298
+ }
299
+
300
+ console.log("4/5 permisos y reinicio del framework");
301
+ concederPermisos();
302
+ // El buffer se vacía ANTES de reiniciar para que lo que se lea después sea de ESTE arranque
303
+ // y no la excepción del intento anterior — que es la forma más fácil de «arreglar» algo y
304
+ // creerse que sigue roto, o al revés.
305
+ try {
306
+ adbCallado("logcat", "-c");
307
+ } catch {
308
+ // Si no se puede vaciar, se sigue: el diagnóstico será menos preciso, no falso.
309
+ }
310
+ adb("shell", "am", "force-stop", PAQUETE);
311
+ adb("shell", "am", "start", "-n", ACTIVIDAD);
312
+
313
+ console.log("5/5 esperando al canal, lanzando y comprobando que está VIVA");
314
+ const cliente = join(import.meta.dirname, "xone-hotswap");
315
+ const mandar = (comando) =>
316
+ execFileSync("node", [cliente, JSON.stringify(comando)], { encoding: "utf8" }).trim();
317
+ const esperar = (segundos) => execFileSync("sleep", [String(segundos)]);
318
+
319
+ /**
320
+ * **El canal no está en pie justo después del reinicio**, y esto se descubrió corriéndolo:
321
+ * el `am start` vuelve en cuanto Android acepta el intent, no cuando el servidor escucha,
322
+ * así que el primer intento moría con «Received network error or non-101 status code».
323
+ * Se espera a que CONTESTE, que es la condición de verdad, en vez de dormir un rato fijo.
324
+ */
325
+ let listo = false;
326
+ for (let intento = 1; intento <= 15 && !listo; intento++) {
327
+ try {
328
+ mandar({ command: "launchApplication", appName: app });
329
+ listo = true;
330
+ } catch {
331
+ esperar(1);
332
+ }
333
+ }
334
+ if (!listo) {
335
+ console.error(`el servidor hotswap no levantó tras el reinicio. Comprueba que ${PAQUETE} está instalado`);
336
+ console.error("y que el puerto es el 8443 (si estaba ocupado, el framework coge el siguiente libre).");
337
+ process.exit(1);
338
+ }
339
+
340
+ // El árbol es la comprobación: `result:true` solo dice que el encargo se aceptó. Se
341
+ // reintenta porque la app tarda unos segundos en tener pantalla, y se espera a una
342
+ // CONDICIÓN y no a un número fijo de segundos.
343
+ for (let intento = 1; intento <= 10; intento++) {
344
+ const salida = mandar({ command: "getAllElements", format: "xone" });
345
+ if (!salida.includes("App is not running")) {
346
+ console.log(salida);
347
+ /**
348
+ * **La captura en la MISMA orden, porque lo que se paga es la ida y vuelta.** Medido: un
349
+ * «lanza la app y sácame una captura» costaba 17 llamadas al modelo, y cada una reenvía
350
+ * el historial entero — mientras que lo que devuelve cada comando es pequeño (1,6 KB el
351
+ * árbol, 108 bytes la captura). O sea que el gasto no es lo que se dice, es cuántas veces
352
+ * hay que volver a preguntar. Tres órdenes encadenadas por el modelo son tres viajes;
353
+ * ésta es una.
354
+ */
355
+ if (args.includes("--captura")) {
356
+ try {
357
+ console.log(mandar({ command: "getScreenshot" }));
358
+ } catch {
359
+ console.error("la app arrancó pero la captura falló; el árbol de arriba sí es bueno.");
360
+ }
361
+ }
362
+ // **Estar VIVA no es estar BIEN.** Una app que revienta al cargar la configuración se
363
+ // queda con su diálogo de error delante, y el canal contesta igual: el árbol llega, la
364
+ // captura sale, y todo parece haber ido bien. Quien lo cuenta es el log, y no pedirlo
365
+ // aquí obliga a que alguien se acuerde de hacerlo — que es justo lo que no pasa.
366
+ const roto = excepcionDeXOne();
367
+ if (roto !== undefined) {
368
+ console.error("");
369
+ console.error("La app ARRANCÓ pero XOne lanzó una excepción al cargar:");
370
+ console.error(roto);
371
+ console.error("");
372
+ console.error("El runtime de XOne es ES5 (Rhino): nada de `let`, `const`, `=>` ni comas");
373
+ console.error("sobrantes, y una `var` no se puede declarar dos veces en la misma función.");
374
+ process.exit(1);
375
+ }
376
+ process.exit(0);
377
+ }
378
+ esperar(2);
379
+ }
380
+ console.error("la app no llegó a contestar el árbol de controles: NO está viva.");
381
+ // **Lo primero, la excepción**, y antes que cualquier consejo: si XOne reventó al cargar, eso
382
+ // es la respuesta entera y todo lo demás sobra. Aquí hace MÁS falta que en la rama de «está
383
+ // viva»: una app que no llega a arrancar no tiene pantalla que mirar ni árbol que pedir, así
384
+ // que sin esto lo único que queda es adivinar.
385
+ const roto = excepcionDeXOne();
386
+ if (roto !== undefined) {
387
+ console.error("");
388
+ console.error("XOne lanzó una excepción al cargar:");
389
+ console.error(roto);
390
+ console.error("");
391
+ console.error("El motor JS de XOne en Android es Rhino: un error de sintaxis en un fichero");
392
+ console.error("incluido para la app ENTERA. Mira el `;` al final de cada sentencia, las comas");
393
+ console.error("sobrantes y las `var` declaradas dos veces en la misma función.");
394
+ process.exit(1);
395
+ }
396
+ // Antes de mandar a mirar la base de datos, se mira QUÉ hay en pantalla: un diálogo del
397
+ // sistema encima es el falso culpable más caro de este bucle.
398
+ try {
399
+ const foco = adb("shell", "dumpsys", "window").split("\n").find((l) => l.includes("mCurrentFocus")) ?? "";
400
+ if (foco.trim() !== "") console.error(`En pantalla ahora: ${foco.trim()}`);
401
+ if (foco.includes("permissioncontroller")) {
402
+ console.error("Hay un diálogo de PERMISOS de Android encima: la app está detrás, esperando.");
403
+ console.error("Vuelve a intentarlo; si insiste, contéstalo en el aparato.");
404
+ process.exit(1);
405
+ }
406
+ } catch {
407
+ // Si no se puede preguntar, se sigue con el consejo de siempre.
408
+ }
409
+ console.error("Lo primero que hay que mirar es si existe bd/gestion.db (la genera XOne Studio, no esto).");
410
+ process.exit(1);
411
+ }
@@ -0,0 +1,171 @@
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
+ /** A partir de aquí se da por binario y no se imprime. */
60
+ const LARGO_DE_BINARIO = 2_000;
61
+
62
+ const pedidos = process.argv.slice(2);
63
+ if (pedidos.length === 0 || pedidos.includes("--help") || pedidos.includes("-h")) {
64
+ // El prompt del agente promete que `--help` contesta. Una promesa así, sin cumplir, cuesta
65
+ // un viaje y deja al modelo adivinando argumentos.
66
+ console.error(`xone-hotswap — manda comandos al servidor hotswap de la app (WebSocket).
67
+
68
+ xone-hotswap '{"command":"getAllElements","format":"xone"}'
69
+ xone-hotswap '{"command":"getScreenshot"}' la captura va a $XONECODE_ARTEFACTOS
70
+ xone-hotswap '{"command":"click","name":"MAP_BT"}' varios comandos, en orden, si pasas varios
71
+
72
+ Antes, en Android, hace falta el túnel: adb forward tcp:8443 tcp:8443
73
+ Variables: HOTSWAP_URL (${URL_POR_OMISION}), XONECODE_ARTEFACTOS, HOTSWAP_TOPE_MS.
74
+ El catálogo entero de comandos está en el SKILL.md de xone-hotswap.`);
75
+ process.exit(pedidos.length === 0 ? 1 : 0);
76
+ }
77
+
78
+ const comandos = [];
79
+ for (const arg of pedidos) {
80
+ try {
81
+ comandos.push(JSON.parse(arg));
82
+ } catch {
83
+ salir(`no es JSON válido: ${arg.slice(0, 80)}`);
84
+ }
85
+ }
86
+
87
+
88
+ function salir(motivo) {
89
+ console.error(`hotswap: ${motivo}`);
90
+ process.exit(1);
91
+ }
92
+
93
+ /** Extensión por los primeros bytes en base64, sin adivinar por el nombre del comando. */
94
+ function extensionDe(base64) {
95
+ if (base64.startsWith("/9j/")) return "jpg";
96
+ if (base64.startsWith("iVBORw0KGgo")) return "png";
97
+ if (base64.startsWith("UklGR")) return "webp";
98
+ return "bin";
99
+ }
100
+
101
+ /**
102
+ * Guarda lo binario y lo sustituye por lo que se puede decir de ello.
103
+ *
104
+ * Mira TODOS los campos de la respuesta, no solo `status`: el campo donde viene el binario
105
+ * ha cambiado entre versiones, y un cliente que solo mire uno se calla justo cuando el
106
+ * servidor cambia de sitio.
107
+ */
108
+ function sinBinarios(respuesta) {
109
+ const copia = { ...respuesta };
110
+ for (const [clave, valor] of Object.entries(copia)) {
111
+ if (typeof valor !== "string" || valor.length < LARGO_DE_BINARIO) continue;
112
+ if (ARTEFACTOS === undefined) {
113
+ copia[clave] = `<${valor.length} car. de binario; sin XONECODE_ARTEFACTOS no hay dónde guardarlo>`;
114
+ continue;
115
+ }
116
+ const nombre = `captura-${Date.now()}.${extensionDe(valor)}`;
117
+ const bytes = Buffer.from(valor, "base64");
118
+ writeFileSync(join(ARTEFACTOS, nombre), bytes);
119
+ copia[clave] = `<guardado como ${nombre}, ${bytes.length} bytes>`;
120
+ }
121
+ return copia;
122
+ }
123
+
124
+ const ws = new WebSocket(process.env.HOTSWAP_URL ?? URL_POR_OMISION);
125
+ let saludado = false;
126
+ let pendiente;
127
+ let reloj;
128
+
129
+ const rearmar = () => {
130
+ clearTimeout(reloj);
131
+ reloj = setTimeout(() => salir(`sin respuesta en ${TOPE_MS} ms`), TOPE_MS);
132
+ };
133
+
134
+ function siguiente() {
135
+ const comando = comandos.shift();
136
+ if (comando === undefined) {
137
+ clearTimeout(reloj);
138
+ ws.close();
139
+ process.exit(0);
140
+ }
141
+ pendiente = comando;
142
+ rearmar();
143
+ ws.send(JSON.stringify(comando));
144
+ }
145
+
146
+ rearmar();
147
+ ws.onerror = (e) => salir(`no se pudo hablar con el aparato: ${e.message ?? e}`);
148
+ ws.onclose = (e) => {
149
+ if (pendiente !== undefined || comandos.length > 0) {
150
+ salir(`el canal se cerró antes de terminar (${e.code} ${e.reason || "sin motivo"})`);
151
+ }
152
+ };
153
+ ws.onmessage = (ev) => {
154
+ const texto = typeof ev.data === "string" ? ev.data : Buffer.from(ev.data).toString();
155
+ let mensaje;
156
+ try {
157
+ mensaje = JSON.parse(texto);
158
+ } catch {
159
+ console.error(`hotswap: mensaje que no es JSON: ${texto.slice(0, 200)}`);
160
+ return;
161
+ }
162
+ // El servidor saluda primero y ese saludo no es la respuesta de nadie.
163
+ if (!saludado && mensaje.command === "server_hello") {
164
+ saludado = true;
165
+ siguiente();
166
+ return;
167
+ }
168
+ console.log(JSON.stringify({ comando: pendiente?.command, ...sinBinarios(mensaje) }));
169
+ pendiente = undefined;
170
+ siguiente();
171
+ };
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * El log del aparato, acotado a lo que sirve para diagnosticar: excepciones y errores del host.
4
+ *
5
+ * POR QUÉ NO ES `adb logcat` A SECAS. Un volcado crudo de este emulador son megabytes de
6
+ * `WindowManager` y `Transition requested`, con líneas sueltas de 2 KB: metido en el contexto de
7
+ * un agente cuesta más que todo el turno y entierra justo lo que se busca. Aquí se filtra por el
8
+ * **PID del host** —así sale TODO lo suyo, incluidas las excepciones de JavaScript del runtime
9
+ * XOne— y, aparte, los `FATAL`/`AndroidRuntime` de cualquiera, que son los que cuentan cuando la
10
+ * app se acaba de morir y ya no tiene PID.
11
+ *
12
+ * **Nunca sigue el log.** `adb logcat` sin `-d` no termina jamás, y un comando que no termina
13
+ * cuelga el turno entero. Si quieres aislar lo que pasa a partir de ahora: `--limpiar`, haces lo
14
+ * que sea, y vuelves a pedirlo.
15
+ *
16
+ * USO:
17
+ * xone-log-android errores y excepciones recientes
18
+ * xone-log-android --lineas 400 más cola
19
+ * xone-log-android --todo todo lo del host, no solo errores
20
+ * xone-log-android --limpiar vacía el buffer y sale (para aislar lo siguiente)
21
+ * xone-log-android --serie emulator-5554
22
+ */
23
+ import { execFileSync } from "node:child_process";
24
+
25
+ const PAQUETE = "com.xone.android.framework";
26
+
27
+ const args = process.argv.slice(2);
28
+ if (args.includes("--help") || args.includes("-h")) {
29
+ console.error(`xone-log-android — el log del aparato, acotado al host de XOne.
30
+
31
+ xone-log-android errores y excepciones recientes (por omisión, 200 líneas)
32
+ xone-log-android --lineas 400 más cola
33
+ xone-log-android --todo todo lo del host, no solo errores
34
+ xone-log-android --limpiar vacía el buffer y sale, para aislar lo que pase después
35
+ xone-log-android --serie <id> con varios aparatos conectados
36
+
37
+ Filtra por el PID del host, así que trae también las excepciones de JavaScript del runtime.
38
+ Nunca sigue el log: un comando que no termina cuelga el turno.`);
39
+ process.exit(0);
40
+ }
41
+
42
+ const opcion = (nombre) => {
43
+ const i = args.indexOf(`--${nombre}`);
44
+ return i >= 0 ? args[i + 1] : undefined;
45
+ };
46
+ const serie = opcion("serie");
47
+ const adb = (...partes) =>
48
+ execFileSync("adb", [...(serie ? ["-s", serie] : []), ...partes], {
49
+ encoding: "utf8",
50
+ maxBuffer: 64 * 1024 * 1024,
51
+ });
52
+
53
+ if (args.includes("--limpiar")) {
54
+ adb("logcat", "-c");
55
+ console.log("buffer del log vaciado. Haz lo que quieras aislar y vuelve a pedir el log.");
56
+ process.exit(0);
57
+ }
58
+
59
+ const lineas = Number(opcion("lineas") ?? 200);
60
+ const todo = args.includes("--todo");
61
+
62
+ /** El PID del host, si está vivo. Sin él no se puede acotar por proceso: se dice y se sigue. */
63
+ let pid;
64
+ try {
65
+ pid = adb("shell", "pidof", PAQUETE).trim().split(/\s+/)[0];
66
+ } catch {
67
+ pid = undefined;
68
+ }
69
+
70
+ const salida = [];
71
+
72
+ if (pid !== undefined && pid !== "") {
73
+ const suyo = adb("logcat", "-d", "--pid", pid, "-t", String(lineas));
74
+ salida.push(`--- ${PAQUETE} (pid ${pid}) ---`);
75
+ salida.push(todo ? suyo : soloInteresante(suyo));
76
+ } else {
77
+ salida.push(`--- ${PAQUETE} no está vivo ahora mismo: solo queda lo que dejó escrito ---`);
78
+ }
79
+
80
+ /**
81
+ * Las caídas duras van aparte y SIN el PID, a propósito: cuando una app se muere, el proceso
82
+ * que escribió la excepción ya no existe, así que filtrar por PID se lleva por delante justo la
83
+ * traza que explica la muerte.
84
+ */
85
+ const caidas = adb("logcat", "-d", "-t", String(lineas * 4), "AndroidRuntime:E", "DEBUG:F", "*:F", "*:S").trim();
86
+ if (caidas !== "") {
87
+ salida.push("", "--- caídas duras (cualquier proceso) ---", caidas);
88
+ }
89
+
90
+ /** Lo que merece la pena leer de un volcado del host: errores, avisos y trazas de excepción. */
91
+ function soloInteresante(texto) {
92
+ const filas = texto.split("\n").filter((l) => {
93
+ if (/^\s*$/.test(l)) return false;
94
+ // El nivel va tras el PID/TID: `09-18 08:21:17.053 815 880 E Tag: mensaje`.
95
+ if (/\s[EWF]\s/.test(l)) return true;
96
+ // Y el cuerpo de una traza de Java, que no lleva nivel en cada línea.
97
+ return /\bat [\w$.]+\(|Caused by:|Exception|Error:/.test(l);
98
+ });
99
+ return filas.length === 0 ? "(sin errores ni avisos en esas líneas)" : filas.join("\n");
100
+ }
101
+
102
+ console.log(salida.join("\n"));