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.
Files changed (315) hide show
  1. package/apps/web/dist/assets/{index-BB3SamhR.css → index-Boak51UP.css} +1 -1
  2. package/apps/web/dist/assets/index-Cg28x6nG.js +477 -0
  3. package/apps/web/dist/assets/{php-D_2--9WK.js → php-Cu6F_QCJ.js} +1 -1
  4. package/apps/web/dist/assets/{ruby-DSHSDbk1.js → ruby-BiqQPoC6.js} +1 -1
  5. package/apps/web/dist/index.html +2 -2
  6. package/dist/agent/{cloudstudioClient.js → cloudstudio/cloudstudioClient.js} +10 -0
  7. package/dist/agent/cloudstudio/cloudstudioClient.js.map +1 -0
  8. package/dist/agent/{cloudstudioMcp.js → cloudstudio/cloudstudioMcp.js} +5 -5
  9. package/dist/agent/cloudstudio/cloudstudioMcp.js.map +1 -0
  10. package/dist/agent/{descarga.js → cloudstudio/descarga.js} +17 -7
  11. package/dist/agent/cloudstudio/descarga.js.map +1 -0
  12. package/dist/agent/cloudstudio/manifiesto.js.map +1 -0
  13. package/dist/agent/cloudstudio/ramaActiva.js +11 -0
  14. package/dist/agent/cloudstudio/ramaActiva.js.map +1 -0
  15. package/dist/agent/{subida.js → cloudstudio/subida.js} +16 -7
  16. package/dist/agent/cloudstudio/subida.js.map +1 -0
  17. package/dist/agent/{zip.js → cloudstudio/zip.js} +1 -1
  18. package/dist/agent/cloudstudio/zip.js.map +1 -0
  19. package/dist/agent/{authEnDisco.js → config/authEnDisco.js} +3 -3
  20. package/dist/agent/config/authEnDisco.js.map +1 -0
  21. package/dist/agent/{catalogoModelos.js → config/catalogoModelos.js} +47 -1
  22. package/dist/agent/config/catalogoModelos.js.map +1 -0
  23. package/dist/agent/{configEnDisco.js → config/configEnDisco.js} +6 -6
  24. package/dist/agent/config/configEnDisco.js.map +1 -0
  25. package/dist/agent/{crearProyecto.js → config/crearProyecto.js} +2 -2
  26. package/dist/agent/config/crearProyecto.js.map +1 -0
  27. package/dist/agent/config/entorno.js.map +1 -0
  28. package/dist/agent/config/gemini.js.map +1 -0
  29. package/dist/agent/config/modelos.js +235 -0
  30. package/dist/agent/config/modelos.js.map +1 -0
  31. package/dist/agent/{modelosDeMotor.js → config/modelosDeMotor.js} +1 -1
  32. package/dist/agent/config/modelosDeMotor.js.map +1 -0
  33. package/dist/agent/config/mudanzaEnDisco.js +121 -0
  34. package/dist/agent/config/mudanzaEnDisco.js.map +1 -0
  35. package/dist/agent/{persona.js → config/persona.js} +1 -1
  36. package/dist/agent/config/persona.js.map +1 -0
  37. package/dist/agent/config/selectorEnMaquina.js +45 -0
  38. package/dist/agent/config/selectorEnMaquina.js.map +1 -0
  39. package/dist/agent/{settingsEnDisco.js → config/settingsEnDisco.js} +37 -2
  40. package/dist/agent/config/settingsEnDisco.js.map +1 -0
  41. package/dist/agent/{versionEnDisco.js → config/versionEnDisco.js} +8 -4
  42. package/dist/agent/config/versionEnDisco.js.map +1 -0
  43. package/dist/agent/dispositivos/arranqueDeEmulador.js +146 -0
  44. package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -0
  45. package/dist/agent/{dispositivosEnMaquina.js → dispositivos/dispositivosEnMaquina.js} +93 -6
  46. package/dist/agent/dispositivos/dispositivosEnMaquina.js.map +1 -0
  47. package/dist/agent/dispositivos/hotswap.js.map +1 -0
  48. package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -0
  49. package/dist/agent/dispositivos/juezVisual.js +152 -0
  50. package/dist/agent/dispositivos/juezVisual.js.map +1 -0
  51. package/dist/agent/{lanzamientoEnMaquina.js → dispositivos/lanzamientoEnMaquina.js} +1 -1
  52. package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -0
  53. package/dist/agent/{paqueteDelProyecto.js → dispositivos/paqueteDelProyecto.js} +5 -5
  54. package/dist/agent/dispositivos/paqueteDelProyecto.js.map +1 -0
  55. package/dist/agent/dispositivos/procesosEnMaquina.js.map +1 -0
  56. package/dist/agent/dispositivos/sensorDeArranque.js +154 -0
  57. package/dist/agent/dispositivos/sensorDeArranque.js.map +1 -0
  58. package/dist/agent/exportarPdf.js +140 -0
  59. package/dist/agent/exportarPdf.js.map +1 -0
  60. package/dist/agent/{arbolDeProyecto.js → grafo/arbolDeProyecto.js} +2 -2
  61. package/dist/agent/grafo/arbolDeProyecto.js.map +1 -0
  62. package/dist/agent/{artefactosEnDisco.js → grafo/artefactosEnDisco.js} +2 -2
  63. package/dist/agent/grafo/artefactosEnDisco.js.map +1 -0
  64. package/dist/agent/{busquedaRegex.js → grafo/busquedaRegex.js} +41 -1
  65. package/dist/agent/grafo/busquedaRegex.js.map +1 -0
  66. package/dist/agent/grafo/criticaVisual.js +115 -0
  67. package/dist/agent/grafo/criticaVisual.js.map +1 -0
  68. package/dist/agent/grafo/excluirTools.js +82 -0
  69. package/dist/agent/grafo/excluirTools.js.map +1 -0
  70. package/dist/agent/{memoriaDeProyecto.js → grafo/memoriaDeProyecto.js} +1 -1
  71. package/dist/agent/grafo/memoriaDeProyecto.js.map +1 -0
  72. package/dist/agent/grafo/navegacionXone.js +231 -0
  73. package/dist/agent/grafo/navegacionXone.js.map +1 -0
  74. package/dist/agent/{perfiles.js → grafo/perfiles.js} +96 -10
  75. package/dist/agent/grafo/perfiles.js.map +1 -0
  76. package/dist/agent/{proyecto.js → grafo/proyecto.js} +274 -24
  77. package/dist/agent/grafo/proyecto.js.map +1 -0
  78. package/dist/agent/grafo/skills.js +425 -0
  79. package/dist/agent/grafo/skills.js.map +1 -0
  80. package/dist/agent/grafo/xoneAgent.js +566 -0
  81. package/dist/agent/grafo/xoneAgent.js.map +1 -0
  82. package/dist/agent/navegacion/estilosEnDisco.js +110 -0
  83. package/dist/agent/navegacion/estilosEnDisco.js.map +1 -0
  84. package/dist/agent/navegacion/hechosEnDisco.js +26 -0
  85. package/dist/agent/navegacion/hechosEnDisco.js.map +1 -0
  86. package/dist/agent/navegacion/indiceEnDisco.js +36 -0
  87. package/dist/agent/navegacion/indiceEnDisco.js.map +1 -0
  88. package/dist/agent/navegacion/modeloDeProyecto.js +228 -0
  89. package/dist/agent/navegacion/modeloDeProyecto.js.map +1 -0
  90. package/dist/agent/raizDelPaquete.js +56 -0
  91. package/dist/agent/raizDelPaquete.js.map +1 -0
  92. package/dist/agent/{checkpointer.js → sesiones/checkpointer.js} +1 -1
  93. package/dist/agent/sesiones/checkpointer.js.map +1 -0
  94. package/dist/agent/sesiones/git.js.map +1 -0
  95. package/dist/agent/{gitSync.js → sesiones/gitSync.js} +3 -3
  96. package/dist/agent/sesiones/gitSync.js.map +1 -0
  97. package/dist/agent/{sesionGit.js → sesiones/sesionGit.js} +2 -2
  98. package/dist/agent/sesiones/sesionGit.js.map +1 -0
  99. package/dist/agent/subagentes/agentesEnDisco.js +818 -0
  100. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -0
  101. package/dist/agent/subagentes/consumoExterno.js.map +1 -0
  102. package/dist/agent/subagentes/escrituraDeCodex.js.map +1 -0
  103. package/dist/agent/{escrituraDeOpencode.js → subagentes/escrituraDeOpencode.js} +1 -1
  104. package/dist/agent/subagentes/escrituraDeOpencode.js.map +1 -0
  105. package/dist/agent/{escrituraExterna.js → subagentes/escrituraExterna.js} +59 -6
  106. package/dist/agent/subagentes/escrituraExterna.js.map +1 -0
  107. package/dist/agent/subagentes/pluginDeSkills.js +99 -0
  108. package/dist/agent/subagentes/pluginDeSkills.js.map +1 -0
  109. package/dist/agent/subagentes/subagenteCodex.js.map +1 -0
  110. package/dist/agent/{subagenteExterno.js → subagentes/subagenteExterno.js} +48 -0
  111. package/dist/agent/subagentes/subagenteExterno.js.map +1 -0
  112. package/dist/agent/{subagenteOpencode.js → subagentes/subagenteOpencode.js} +53 -1
  113. package/dist/agent/subagentes/subagenteOpencode.js.map +1 -0
  114. package/dist/agent/{aumentador.js → tareas/aumentador.js} +2 -2
  115. package/dist/agent/tareas/aumentador.js.map +1 -0
  116. package/dist/agent/{juezDeTarea.js → tareas/juezDeTarea.js} +6 -6
  117. package/dist/agent/tareas/juezDeTarea.js.map +1 -0
  118. package/dist/agent/{tareasEnDisco.js → tareas/tareasEnDisco.js} +53 -4
  119. package/dist/agent/tareas/tareasEnDisco.js.map +1 -0
  120. package/dist/agent/{diagnosticoDeTools.js → turno/diagnosticoDeTools.js} +5 -1
  121. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -0
  122. package/dist/agent/{guionizado.js → turno/guionizado.js} +1 -1
  123. package/dist/agent/turno/guionizado.js.map +1 -0
  124. package/dist/agent/turno/informeDeTraza.js +245 -0
  125. package/dist/agent/turno/informeDeTraza.js.map +1 -0
  126. package/dist/agent/turno/inspectorDePrompt.js +205 -0
  127. package/dist/agent/turno/inspectorDePrompt.js.map +1 -0
  128. package/dist/agent/{instantanea.js → turno/instantanea.js} +7 -2
  129. package/dist/agent/turno/instantanea.js.map +1 -0
  130. package/dist/agent/{interrupts.js → turno/interrupts.js} +2 -2
  131. package/dist/agent/turno/interrupts.js.map +1 -0
  132. package/dist/agent/turno/mensajes.js.map +1 -0
  133. package/dist/agent/turno/normalizar.js.map +1 -0
  134. package/dist/agent/{puente.js → turno/puente.js} +71 -4
  135. package/dist/agent/turno/puente.js.map +1 -0
  136. package/dist/agent/turno/resumenDeContexto.js +260 -0
  137. package/dist/agent/turno/resumenDeContexto.js.map +1 -0
  138. package/dist/agent/{resumenDeTool.js → turno/resumenDeTool.js} +33 -0
  139. package/dist/agent/turno/resumenDeTool.js.map +1 -0
  140. package/dist/agent/turno/rubrica.js +91 -0
  141. package/dist/agent/turno/rubrica.js.map +1 -0
  142. package/dist/agent/turno/textoDeTool.js.map +1 -0
  143. package/dist/agent/{turnoReal.js → turno/turnoReal.js} +210 -25
  144. package/dist/agent/turno/turnoReal.js.map +1 -0
  145. package/dist/agent/turno/verificador.js.map +1 -0
  146. package/dist/cli/config.js +1 -1
  147. package/dist/cli/config.js.map +1 -1
  148. package/dist/cli/consola.js +72 -9
  149. package/dist/cli/consola.js.map +1 -1
  150. package/dist/cli/describe.js +2 -2
  151. package/dist/cli/describe.js.map +1 -1
  152. package/dist/cli/doctor.js +1 -1
  153. package/dist/cli/doctor.js.map +1 -1
  154. package/dist/cli/fuentesDeDisco.js +29 -0
  155. package/dist/cli/fuentesDeDisco.js.map +1 -0
  156. package/dist/cli/main.js +87 -31
  157. package/dist/cli/main.js.map +1 -1
  158. package/dist/cli/run.js +40 -10
  159. package/dist/cli/run.js.map +1 -1
  160. package/dist/cli/traza.js +48 -0
  161. package/dist/cli/traza.js.map +1 -0
  162. package/dist/cli/tui/correrTui.js +4 -4
  163. package/dist/cli/tui/correrTui.js.map +1 -1
  164. package/dist/cli/verify.js +1 -1
  165. package/dist/cli/verify.js.map +1 -1
  166. package/dist/core/actos.js +1 -1
  167. package/dist/core/adjuntos.js +2 -2
  168. package/dist/core/agentes.js +247 -7
  169. package/dist/core/agentes.js.map +1 -1
  170. package/dist/core/artefactos.js +4 -4
  171. package/dist/core/config.js +1 -1
  172. package/dist/core/contextos.js +12 -1
  173. package/dist/core/contextos.js.map +1 -1
  174. package/dist/core/descargas.js +1 -1
  175. package/dist/core/dispositivos.js +53 -3
  176. package/dist/core/dispositivos.js.map +1 -1
  177. package/dist/core/esfuerzo.js +141 -0
  178. package/dist/core/esfuerzo.js.map +1 -0
  179. package/dist/core/esqueleto.js +1 -1
  180. package/dist/core/estilos.js +78 -0
  181. package/dist/core/estilos.js.map +1 -0
  182. package/dist/core/exportacion.js +140 -0
  183. package/dist/core/exportacion.js.map +1 -0
  184. package/dist/core/hechosDelProyecto.js +87 -0
  185. package/dist/core/hechosDelProyecto.js.map +1 -0
  186. package/dist/core/logDeDispositivo.js +143 -0
  187. package/dist/core/logDeDispositivo.js.map +1 -0
  188. package/dist/core/modelos.js +48 -3
  189. package/dist/core/modelos.js.map +1 -1
  190. package/dist/core/mudanzaDeWorkspace.js +93 -0
  191. package/dist/core/mudanzaDeWorkspace.js.map +1 -0
  192. package/dist/core/navegacion.js +134 -0
  193. package/dist/core/navegacion.js.map +1 -0
  194. package/dist/core/notify.js +5 -0
  195. package/dist/core/notify.js.map +1 -1
  196. package/dist/core/planDeSubida.js +1 -1
  197. package/dist/core/planes.js +70 -0
  198. package/dist/core/planes.js.map +1 -0
  199. package/dist/core/ports.js +4 -0
  200. package/dist/core/ports.js.map +1 -1
  201. package/dist/core/rubrica.js +90 -0
  202. package/dist/core/rubrica.js.map +1 -0
  203. package/dist/core/selectorDeCarpeta.js +87 -0
  204. package/dist/core/selectorDeCarpeta.js.map +1 -0
  205. package/dist/core/settings.js +56 -7
  206. package/dist/core/settings.js.map +1 -1
  207. package/dist/core/shellDeAgente.js +144 -0
  208. package/dist/core/shellDeAgente.js.map +1 -0
  209. package/dist/core/skills.js +158 -0
  210. package/dist/core/skills.js.map +1 -0
  211. package/dist/core/topeAgotado.js +69 -0
  212. package/dist/core/topeAgotado.js.map +1 -0
  213. package/dist/core/zipDeSkill.js +107 -0
  214. package/dist/core/zipDeSkill.js.map +1 -0
  215. package/dist/web/servidor/arranque.js +853 -54
  216. package/dist/web/servidor/arranque.js.map +1 -1
  217. package/dist/web/servidor/corredorDeTareas.js +3 -3
  218. package/dist/web/servidor/corredorDeTareas.js.map +1 -1
  219. package/dist/web/servidor/sesiones.js +34 -6
  220. package/dist/web/servidor/sesiones.js.map +1 -1
  221. package/dist/web/servidor/transporte.js +1 -1
  222. package/dist/web/servidor/transporte.js.map +1 -1
  223. package/dist/web/servidor/vestibulo.js +62 -13
  224. package/dist/web/servidor/vestibulo.js.map +1 -1
  225. package/package.json +7 -4
  226. package/skills/archify/SKILL.md +25 -2
  227. package/skills/artifacts-builder/SKILL.md +23 -0
  228. package/skills/xone-hotswap/SKILL.md +96 -0
  229. package/skills/xone-hotswap/scripts/xone-arrancar-android +202 -0
  230. package/skills/xone-hotswap/scripts/xone-arrancar-ios +85 -0
  231. package/skills/xone-hotswap/scripts/xone-captura-android +93 -0
  232. package/skills/xone-hotswap/scripts/xone-desplegar-android +439 -0
  233. package/skills/xone-hotswap/scripts/xone-hotswap +201 -0
  234. package/skills/xone-hotswap/scripts/xone-log-android +102 -0
  235. package/skills/xone-hotswap/scripts/xone-reiniciar-android +132 -0
  236. package/skills/xone-plan-builder/SKILL.md +22 -4
  237. package/skills/xone-spec-builder/SKILL.md +37 -7
  238. package/apps/web/dist/assets/index-CFYAoHdw.js +0 -477
  239. package/dist/agent/agentesEnDisco.js +0 -539
  240. package/dist/agent/agentesEnDisco.js.map +0 -1
  241. package/dist/agent/arbolDeProyecto.js.map +0 -1
  242. package/dist/agent/artefactosEnDisco.js.map +0 -1
  243. package/dist/agent/aumentador.js.map +0 -1
  244. package/dist/agent/authEnDisco.js.map +0 -1
  245. package/dist/agent/busquedaRegex.js.map +0 -1
  246. package/dist/agent/catalogoModelos.js.map +0 -1
  247. package/dist/agent/checkpointer.js.map +0 -1
  248. package/dist/agent/cloudstudioClient.js.map +0 -1
  249. package/dist/agent/cloudstudioMcp.js.map +0 -1
  250. package/dist/agent/configEnDisco.js.map +0 -1
  251. package/dist/agent/consumoExterno.js.map +0 -1
  252. package/dist/agent/crearProyecto.js.map +0 -1
  253. package/dist/agent/descarga.js.map +0 -1
  254. package/dist/agent/diagnosticoDeTools.js.map +0 -1
  255. package/dist/agent/dispositivosEnMaquina.js.map +0 -1
  256. package/dist/agent/entorno.js.map +0 -1
  257. package/dist/agent/escrituraDeCodex.js.map +0 -1
  258. package/dist/agent/escrituraDeOpencode.js.map +0 -1
  259. package/dist/agent/escrituraExterna.js.map +0 -1
  260. package/dist/agent/gemini.js.map +0 -1
  261. package/dist/agent/git.js.map +0 -1
  262. package/dist/agent/gitSync.js.map +0 -1
  263. package/dist/agent/guionizado.js.map +0 -1
  264. package/dist/agent/hotswap.js.map +0 -1
  265. package/dist/agent/instalacionEnMaquina.js.map +0 -1
  266. package/dist/agent/instantanea.js.map +0 -1
  267. package/dist/agent/interrupts.js.map +0 -1
  268. package/dist/agent/juezDeTarea.js.map +0 -1
  269. package/dist/agent/lanzamientoEnMaquina.js.map +0 -1
  270. package/dist/agent/manifiesto.js.map +0 -1
  271. package/dist/agent/memoriaDeProyecto.js.map +0 -1
  272. package/dist/agent/mensajes.js.map +0 -1
  273. package/dist/agent/modelos.js +0 -130
  274. package/dist/agent/modelos.js.map +0 -1
  275. package/dist/agent/modelosDeMotor.js.map +0 -1
  276. package/dist/agent/normalizar.js.map +0 -1
  277. package/dist/agent/paqueteDelProyecto.js.map +0 -1
  278. package/dist/agent/perfiles.js.map +0 -1
  279. package/dist/agent/persona.js.map +0 -1
  280. package/dist/agent/procesosEnMaquina.js.map +0 -1
  281. package/dist/agent/proyecto.js.map +0 -1
  282. package/dist/agent/puente.js.map +0 -1
  283. package/dist/agent/resumenDeContexto.js +0 -30
  284. package/dist/agent/resumenDeContexto.js.map +0 -1
  285. package/dist/agent/resumenDeTool.js.map +0 -1
  286. package/dist/agent/sesionGit.js.map +0 -1
  287. package/dist/agent/settingsEnDisco.js.map +0 -1
  288. package/dist/agent/skills.js +0 -65
  289. package/dist/agent/skills.js.map +0 -1
  290. package/dist/agent/subagenteCodex.js.map +0 -1
  291. package/dist/agent/subagenteExterno.js.map +0 -1
  292. package/dist/agent/subagenteOpencode.js.map +0 -1
  293. package/dist/agent/subida.js.map +0 -1
  294. package/dist/agent/tareasEnDisco.js.map +0 -1
  295. package/dist/agent/textoDeTool.js.map +0 -1
  296. package/dist/agent/turnoReal.js.map +0 -1
  297. package/dist/agent/verificador.js.map +0 -1
  298. package/dist/agent/versionEnDisco.js.map +0 -1
  299. package/dist/agent/xoneAgent.js +0 -347
  300. package/dist/agent/xoneAgent.js.map +0 -1
  301. package/dist/agent/zip.js.map +0 -1
  302. /package/dist/agent/{manifiesto.js → cloudstudio/manifiesto.js} +0 -0
  303. /package/dist/agent/{entorno.js → config/entorno.js} +0 -0
  304. /package/dist/agent/{gemini.js → config/gemini.js} +0 -0
  305. /package/dist/agent/{hotswap.js → dispositivos/hotswap.js} +0 -0
  306. /package/dist/agent/{instalacionEnMaquina.js → dispositivos/instalacionEnMaquina.js} +0 -0
  307. /package/dist/agent/{procesosEnMaquina.js → dispositivos/procesosEnMaquina.js} +0 -0
  308. /package/dist/agent/{git.js → sesiones/git.js} +0 -0
  309. /package/dist/agent/{consumoExterno.js → subagentes/consumoExterno.js} +0 -0
  310. /package/dist/agent/{escrituraDeCodex.js → subagentes/escrituraDeCodex.js} +0 -0
  311. /package/dist/agent/{subagenteCodex.js → subagentes/subagenteCodex.js} +0 -0
  312. /package/dist/agent/{mensajes.js → turno/mensajes.js} +0 -0
  313. /package/dist/agent/{normalizar.js → turno/normalizar.js} +0 -0
  314. /package/dist/agent/{textoDeTool.js → turno/textoDeTool.js} +0 -0
  315. /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
+ };