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.
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +32 -0
- package/apps/web/dist/assets/index-DBZTodio.js +477 -0
- package/apps/web/dist/assets/{index-BB3SamhR.css → index-rqecK6s2.css} +1 -1
- package/apps/web/dist/assets/{php-D_2--9WK.js → php-BBij3Lhc.js} +1 -1
- package/apps/web/dist/assets/{ruby-DSHSDbk1.js → ruby-Dq2YEaLo.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/dist/agent/cloudstudio/cloudstudioClient.js.map +1 -0
- package/dist/agent/{cloudstudioMcp.js → cloudstudio/cloudstudioMcp.js} +5 -5
- package/dist/agent/cloudstudio/cloudstudioMcp.js.map +1 -0
- package/dist/agent/{descarga.js → cloudstudio/descarga.js} +3 -3
- package/dist/agent/cloudstudio/descarga.js.map +1 -0
- package/dist/agent/cloudstudio/manifiesto.js.map +1 -0
- package/dist/agent/{subida.js → cloudstudio/subida.js} +3 -3
- package/dist/agent/cloudstudio/subida.js.map +1 -0
- package/dist/agent/{zip.js → cloudstudio/zip.js} +1 -1
- package/dist/agent/cloudstudio/zip.js.map +1 -0
- package/dist/agent/{authEnDisco.js → config/authEnDisco.js} +3 -3
- package/dist/agent/config/authEnDisco.js.map +1 -0
- package/dist/agent/{catalogoModelos.js → config/catalogoModelos.js} +2 -1
- package/dist/agent/config/catalogoModelos.js.map +1 -0
- package/dist/agent/{configEnDisco.js → config/configEnDisco.js} +6 -6
- package/dist/agent/config/configEnDisco.js.map +1 -0
- package/dist/agent/{crearProyecto.js → config/crearProyecto.js} +2 -2
- package/dist/agent/config/crearProyecto.js.map +1 -0
- package/dist/agent/config/entorno.js.map +1 -0
- package/dist/agent/config/gemini.js.map +1 -0
- package/dist/agent/{modelos.js → config/modelos.js} +17 -3
- package/dist/agent/config/modelos.js.map +1 -0
- package/dist/agent/{modelosDeMotor.js → config/modelosDeMotor.js} +1 -1
- package/dist/agent/config/modelosDeMotor.js.map +1 -0
- package/dist/agent/{persona.js → config/persona.js} +1 -1
- package/dist/agent/config/persona.js.map +1 -0
- package/dist/agent/{settingsEnDisco.js → config/settingsEnDisco.js} +2 -2
- package/dist/agent/config/settingsEnDisco.js.map +1 -0
- package/dist/agent/{versionEnDisco.js → config/versionEnDisco.js} +8 -4
- package/dist/agent/config/versionEnDisco.js.map +1 -0
- package/dist/agent/dispositivos/arranqueDeEmulador.js +146 -0
- package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -0
- package/dist/agent/{dispositivosEnMaquina.js → dispositivos/dispositivosEnMaquina.js} +93 -6
- package/dist/agent/dispositivos/dispositivosEnMaquina.js.map +1 -0
- package/dist/agent/dispositivos/hotswap.js.map +1 -0
- package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -0
- package/dist/agent/{lanzamientoEnMaquina.js → dispositivos/lanzamientoEnMaquina.js} +1 -1
- package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -0
- package/dist/agent/{paqueteDelProyecto.js → dispositivos/paqueteDelProyecto.js} +5 -5
- package/dist/agent/dispositivos/paqueteDelProyecto.js.map +1 -0
- package/dist/agent/dispositivos/procesosEnMaquina.js.map +1 -0
- package/dist/agent/{arbolDeProyecto.js → grafo/arbolDeProyecto.js} +2 -2
- package/dist/agent/grafo/arbolDeProyecto.js.map +1 -0
- package/dist/agent/{artefactosEnDisco.js → grafo/artefactosEnDisco.js} +2 -2
- package/dist/agent/grafo/artefactosEnDisco.js.map +1 -0
- package/dist/agent/{busquedaRegex.js → grafo/busquedaRegex.js} +41 -1
- package/dist/agent/grafo/busquedaRegex.js.map +1 -0
- package/dist/agent/grafo/excluirTools.js +82 -0
- package/dist/agent/grafo/excluirTools.js.map +1 -0
- package/dist/agent/{memoriaDeProyecto.js → grafo/memoriaDeProyecto.js} +1 -1
- package/dist/agent/grafo/memoriaDeProyecto.js.map +1 -0
- package/dist/agent/grafo/navegacionXone.js +198 -0
- package/dist/agent/grafo/navegacionXone.js.map +1 -0
- package/dist/agent/{perfiles.js → grafo/perfiles.js} +51 -8
- package/dist/agent/grafo/perfiles.js.map +1 -0
- package/dist/agent/{proyecto.js → grafo/proyecto.js} +207 -24
- package/dist/agent/grafo/proyecto.js.map +1 -0
- package/dist/agent/grafo/skills.js +393 -0
- package/dist/agent/grafo/skills.js.map +1 -0
- package/dist/agent/{xoneAgent.js → grafo/xoneAgent.js} +196 -67
- package/dist/agent/grafo/xoneAgent.js.map +1 -0
- package/dist/agent/navegacion/indiceEnDisco.js +21 -0
- package/dist/agent/navegacion/indiceEnDisco.js.map +1 -0
- package/dist/agent/navegacion/modeloDeProyecto.js +228 -0
- package/dist/agent/navegacion/modeloDeProyecto.js.map +1 -0
- package/dist/agent/raizDelPaquete.js +56 -0
- package/dist/agent/raizDelPaquete.js.map +1 -0
- package/dist/agent/{checkpointer.js → sesiones/checkpointer.js} +2 -2
- package/dist/agent/sesiones/checkpointer.js.map +1 -0
- package/dist/agent/sesiones/git.js.map +1 -0
- package/dist/agent/{gitSync.js → sesiones/gitSync.js} +3 -3
- package/dist/agent/sesiones/gitSync.js.map +1 -0
- package/dist/agent/{sesionGit.js → sesiones/sesionGit.js} +2 -2
- package/dist/agent/sesiones/sesionGit.js.map +1 -0
- package/dist/agent/{agentesEnDisco.js → subagentes/agentesEnDisco.js} +304 -86
- package/dist/agent/subagentes/agentesEnDisco.js.map +1 -0
- package/dist/agent/subagentes/consumoExterno.js.map +1 -0
- package/dist/agent/subagentes/escrituraDeCodex.js.map +1 -0
- package/dist/agent/{escrituraDeOpencode.js → subagentes/escrituraDeOpencode.js} +1 -1
- package/dist/agent/subagentes/escrituraDeOpencode.js.map +1 -0
- package/dist/agent/{escrituraExterna.js → subagentes/escrituraExterna.js} +6 -6
- package/dist/agent/subagentes/escrituraExterna.js.map +1 -0
- package/dist/agent/subagentes/subagenteCodex.js.map +1 -0
- package/dist/agent/subagentes/subagenteExterno.js.map +1 -0
- package/dist/agent/subagentes/subagenteOpencode.js.map +1 -0
- package/dist/agent/{aumentador.js → tareas/aumentador.js} +2 -2
- package/dist/agent/tareas/aumentador.js.map +1 -0
- package/dist/agent/{juezDeTarea.js → tareas/juezDeTarea.js} +5 -5
- package/dist/agent/tareas/juezDeTarea.js.map +1 -0
- package/dist/agent/{tareasEnDisco.js → tareas/tareasEnDisco.js} +4 -4
- package/dist/agent/tareas/tareasEnDisco.js.map +1 -0
- package/dist/agent/turno/diagnosticoDeTools.js.map +1 -0
- package/dist/agent/{guionizado.js → turno/guionizado.js} +1 -1
- package/dist/agent/turno/guionizado.js.map +1 -0
- package/dist/agent/turno/informeDeTraza.js +222 -0
- package/dist/agent/turno/informeDeTraza.js.map +1 -0
- package/dist/agent/turno/inspectorDePrompt.js +205 -0
- package/dist/agent/turno/inspectorDePrompt.js.map +1 -0
- package/dist/agent/{instantanea.js → turno/instantanea.js} +7 -2
- package/dist/agent/turno/instantanea.js.map +1 -0
- package/dist/agent/{interrupts.js → turno/interrupts.js} +2 -2
- package/dist/agent/turno/interrupts.js.map +1 -0
- package/dist/agent/turno/mensajes.js.map +1 -0
- package/dist/agent/turno/normalizar.js.map +1 -0
- package/dist/agent/{puente.js → turno/puente.js} +47 -3
- package/dist/agent/turno/puente.js.map +1 -0
- package/dist/agent/turno/resumenDeContexto.js +176 -0
- package/dist/agent/turno/resumenDeContexto.js.map +1 -0
- package/dist/agent/{resumenDeTool.js → turno/resumenDeTool.js} +15 -0
- package/dist/agent/turno/resumenDeTool.js.map +1 -0
- package/dist/agent/turno/textoDeTool.js.map +1 -0
- package/dist/agent/{turnoReal.js → turno/turnoReal.js} +38 -14
- package/dist/agent/turno/turnoReal.js.map +1 -0
- package/dist/agent/turno/verificador.js.map +1 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/consola.js +9 -9
- package/dist/cli/consola.js.map +1 -1
- package/dist/cli/describe.js +2 -2
- package/dist/cli/describe.js.map +1 -1
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/fuentesDeDisco.js +29 -0
- package/dist/cli/fuentesDeDisco.js.map +1 -0
- package/dist/cli/main.js +37 -28
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/run.js +32 -10
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/traza.js +48 -0
- package/dist/cli/traza.js.map +1 -0
- package/dist/cli/tui/correrTui.js +4 -4
- package/dist/cli/tui/correrTui.js.map +1 -1
- package/dist/cli/verify.js +1 -1
- package/dist/cli/verify.js.map +1 -1
- package/dist/core/actos.js +1 -1
- package/dist/core/adjuntos.js +2 -2
- package/dist/core/agentes.js +145 -7
- package/dist/core/agentes.js.map +1 -1
- package/dist/core/artefactos.js +4 -4
- package/dist/core/config.js +1 -1
- package/dist/core/contextos.js +12 -1
- package/dist/core/contextos.js.map +1 -1
- package/dist/core/descargas.js +1 -1
- package/dist/core/dispositivos.js +53 -3
- package/dist/core/dispositivos.js.map +1 -1
- package/dist/core/esqueleto.js +1 -1
- package/dist/core/modelos.js +16 -3
- package/dist/core/modelos.js.map +1 -1
- package/dist/core/navegacion.js +134 -0
- package/dist/core/navegacion.js.map +1 -0
- package/dist/core/notify.js +5 -0
- package/dist/core/notify.js.map +1 -1
- package/dist/core/planDeSubida.js +1 -1
- package/dist/core/settings.js +2 -2
- package/dist/core/shellDeAgente.js +99 -0
- package/dist/core/shellDeAgente.js.map +1 -0
- package/dist/core/skills.js +158 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/zipDeSkill.js +107 -0
- package/dist/core/zipDeSkill.js.map +1 -0
- package/dist/vendor/sqliteSaver.js +389 -0
- package/dist/vendor/sqliteSaver.js.map +1 -0
- package/dist/web/servidor/arranque.js +531 -47
- package/dist/web/servidor/arranque.js.map +1 -1
- package/dist/web/servidor/corredorDeTareas.js +3 -3
- package/dist/web/servidor/corredorDeTareas.js.map +1 -1
- package/dist/web/servidor/sesiones.js +4 -4
- package/dist/web/servidor/transporte.js +1 -1
- package/dist/web/servidor/transporte.js.map +1 -1
- package/dist/web/servidor/vestibulo.js +7 -7
- package/dist/web/servidor/vestibulo.js.map +1 -1
- package/package.json +9 -5
- package/skills/archify/SKILL.md +25 -2
- package/skills/artifacts-builder/SKILL.md +23 -0
- package/skills/xone-hotswap/SKILL.md +74 -0
- package/skills/xone-hotswap/scripts/xone-arrancar-ios +85 -0
- package/skills/xone-hotswap/scripts/xone-desplegar-android +411 -0
- package/skills/xone-hotswap/scripts/xone-hotswap +171 -0
- package/skills/xone-hotswap/scripts/xone-log-android +102 -0
- package/skills/xone-hotswap/scripts/xone-reiniciar-android +104 -0
- package/apps/web/dist/assets/index-CFYAoHdw.js +0 -477
- package/dist/agent/agentesEnDisco.js.map +0 -1
- package/dist/agent/arbolDeProyecto.js.map +0 -1
- package/dist/agent/artefactosEnDisco.js.map +0 -1
- package/dist/agent/aumentador.js.map +0 -1
- package/dist/agent/authEnDisco.js.map +0 -1
- package/dist/agent/busquedaRegex.js.map +0 -1
- package/dist/agent/catalogoModelos.js.map +0 -1
- package/dist/agent/checkpointer.js.map +0 -1
- package/dist/agent/cloudstudioClient.js.map +0 -1
- package/dist/agent/cloudstudioMcp.js.map +0 -1
- package/dist/agent/configEnDisco.js.map +0 -1
- package/dist/agent/consumoExterno.js.map +0 -1
- package/dist/agent/crearProyecto.js.map +0 -1
- package/dist/agent/descarga.js.map +0 -1
- package/dist/agent/diagnosticoDeTools.js.map +0 -1
- package/dist/agent/dispositivosEnMaquina.js.map +0 -1
- package/dist/agent/entorno.js.map +0 -1
- package/dist/agent/escrituraDeCodex.js.map +0 -1
- package/dist/agent/escrituraDeOpencode.js.map +0 -1
- package/dist/agent/escrituraExterna.js.map +0 -1
- package/dist/agent/gemini.js.map +0 -1
- package/dist/agent/git.js.map +0 -1
- package/dist/agent/gitSync.js.map +0 -1
- package/dist/agent/guionizado.js.map +0 -1
- package/dist/agent/hotswap.js.map +0 -1
- package/dist/agent/instalacionEnMaquina.js.map +0 -1
- package/dist/agent/instantanea.js.map +0 -1
- package/dist/agent/interrupts.js.map +0 -1
- package/dist/agent/juezDeTarea.js.map +0 -1
- package/dist/agent/lanzamientoEnMaquina.js.map +0 -1
- package/dist/agent/manifiesto.js.map +0 -1
- package/dist/agent/memoriaDeProyecto.js.map +0 -1
- package/dist/agent/mensajes.js.map +0 -1
- package/dist/agent/modelos.js.map +0 -1
- package/dist/agent/modelosDeMotor.js.map +0 -1
- package/dist/agent/normalizar.js.map +0 -1
- package/dist/agent/paqueteDelProyecto.js.map +0 -1
- package/dist/agent/perfiles.js.map +0 -1
- package/dist/agent/persona.js.map +0 -1
- package/dist/agent/procesosEnMaquina.js.map +0 -1
- package/dist/agent/proyecto.js.map +0 -1
- package/dist/agent/puente.js.map +0 -1
- package/dist/agent/resumenDeContexto.js +0 -30
- package/dist/agent/resumenDeContexto.js.map +0 -1
- package/dist/agent/resumenDeTool.js.map +0 -1
- package/dist/agent/sesionGit.js.map +0 -1
- package/dist/agent/settingsEnDisco.js.map +0 -1
- package/dist/agent/skills.js +0 -65
- package/dist/agent/skills.js.map +0 -1
- package/dist/agent/subagenteCodex.js.map +0 -1
- package/dist/agent/subagenteExterno.js.map +0 -1
- package/dist/agent/subagenteOpencode.js.map +0 -1
- package/dist/agent/subida.js.map +0 -1
- package/dist/agent/tareasEnDisco.js.map +0 -1
- package/dist/agent/textoDeTool.js.map +0 -1
- package/dist/agent/turnoReal.js.map +0 -1
- package/dist/agent/verificador.js.map +0 -1
- package/dist/agent/versionEnDisco.js.map +0 -1
- package/dist/agent/xoneAgent.js.map +0 -1
- package/dist/agent/zip.js.map +0 -1
- /package/dist/agent/{cloudstudioClient.js → cloudstudio/cloudstudioClient.js} +0 -0
- /package/dist/agent/{manifiesto.js → cloudstudio/manifiesto.js} +0 -0
- /package/dist/agent/{entorno.js → config/entorno.js} +0 -0
- /package/dist/agent/{gemini.js → config/gemini.js} +0 -0
- /package/dist/agent/{hotswap.js → dispositivos/hotswap.js} +0 -0
- /package/dist/agent/{instalacionEnMaquina.js → dispositivos/instalacionEnMaquina.js} +0 -0
- /package/dist/agent/{procesosEnMaquina.js → dispositivos/procesosEnMaquina.js} +0 -0
- /package/dist/agent/{git.js → sesiones/git.js} +0 -0
- /package/dist/agent/{consumoExterno.js → subagentes/consumoExterno.js} +0 -0
- /package/dist/agent/{escrituraDeCodex.js → subagentes/escrituraDeCodex.js} +0 -0
- /package/dist/agent/{subagenteCodex.js → subagentes/subagenteCodex.js} +0 -0
- /package/dist/agent/{subagenteExterno.js → subagentes/subagenteExterno.js} +0 -0
- /package/dist/agent/{subagenteOpencode.js → subagentes/subagenteOpencode.js} +0 -0
- /package/dist/agent/{diagnosticoDeTools.js → turno/diagnosticoDeTools.js} +0 -0
- /package/dist/agent/{mensajes.js → turno/mensajes.js} +0 -0
- /package/dist/agent/{normalizar.js → turno/normalizar.js} +0 -0
- /package/dist/agent/{textoDeTool.js → turno/textoDeTool.js} +0 -0
- /package/dist/agent/{verificador.js → turno/verificador.js} +0 -0
|
@@ -0,0 +1,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"));
|