xonecode 0.20.3 → 0.22.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 (88) hide show
  1. package/apps/web/dist/assets/index-5rtynJV_.js +490 -0
  2. package/apps/web/dist/assets/index-Cq6c6WEF.css +1 -0
  3. package/apps/web/dist/assets/{php-CDg2Quc0.js → php-BTFUgUDo.js} +1 -1
  4. package/apps/web/dist/assets/{ruby-DlFaAWQT.js → ruby-BKKiBtJK.js} +1 -1
  5. package/apps/web/dist/index.html +2 -2
  6. package/dist/agent/conectores/servicioDeConectores.js +86 -45
  7. package/dist/agent/conectores/servicioDeConectores.js.map +1 -1
  8. package/dist/agent/config/modelos.js +4 -0
  9. package/dist/agent/config/modelos.js.map +1 -1
  10. package/dist/agent/config/mudanzaEnDisco.js +237 -3
  11. package/dist/agent/config/mudanzaEnDisco.js.map +1 -1
  12. package/dist/agent/config/selectorEnMaquina.js +3 -2
  13. package/dist/agent/config/selectorEnMaquina.js.map +1 -1
  14. package/dist/agent/config/settingsEnDisco.js +76 -2
  15. package/dist/agent/config/settingsEnDisco.js.map +1 -1
  16. package/dist/agent/dispositivos/arranqueDeEmulador.js +27 -3
  17. package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -1
  18. package/dist/agent/dispositivos/instalacionEnMaquina.js +240 -29
  19. package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -1
  20. package/dist/agent/dispositivos/lanzamientoEnMaquina.js +12 -17
  21. package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -1
  22. package/dist/agent/grafo/proyecto.js +91 -50
  23. package/dist/agent/grafo/proyecto.js.map +1 -1
  24. package/dist/agent/motores/trueforge/capacidades.js +22 -0
  25. package/dist/agent/motores/trueforge/capacidades.js.map +1 -1
  26. package/dist/agent/motores/trueforge/medidaAutomatica.js +16 -3
  27. package/dist/agent/motores/trueforge/medidaAutomatica.js.map +1 -1
  28. package/dist/agent/motores/trueforge/modeloLangchain.js +7 -1
  29. package/dist/agent/motores/trueforge/modeloLangchain.js.map +1 -1
  30. package/dist/agent/motores/trueforge/sesionTrueforge.js +135 -14
  31. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  32. package/dist/agent/motores/trueforge/toolsDeConectores.js +128 -0
  33. package/dist/agent/motores/trueforge/toolsDeConectores.js.map +1 -0
  34. package/dist/agent/motores/trueforge/traerDeStitch.js +142 -0
  35. package/dist/agent/motores/trueforge/traerDeStitch.js.map +1 -0
  36. package/dist/agent/subagentes/consumoExterno.js +15 -8
  37. package/dist/agent/subagentes/consumoExterno.js.map +1 -1
  38. package/dist/agent/subagentes/escrituraExterna.js +69 -2
  39. package/dist/agent/subagentes/escrituraExterna.js.map +1 -1
  40. package/dist/agent/subagentes/subagenteExterno.js +17 -1
  41. package/dist/agent/subagentes/subagenteExterno.js.map +1 -1
  42. package/dist/agent/turno/turnoReal.js +1 -0
  43. package/dist/agent/turno/turnoReal.js.map +1 -1
  44. package/dist/cli/main.js +5 -2
  45. package/dist/cli/main.js.map +1 -1
  46. package/dist/core/actos.js +44 -1
  47. package/dist/core/actos.js.map +1 -1
  48. package/dist/core/agentes.js +13 -2
  49. package/dist/core/agentes.js.map +1 -1
  50. package/dist/core/comandoExterno.js +69 -0
  51. package/dist/core/comandoExterno.js.map +1 -0
  52. package/dist/core/conectores.js +148 -3
  53. package/dist/core/conectores.js.map +1 -1
  54. package/dist/core/copiaDeAvd.js +111 -0
  55. package/dist/core/copiaDeAvd.js.map +1 -0
  56. package/dist/core/mudanzaDeWorkspace.js +105 -15
  57. package/dist/core/mudanzaDeWorkspace.js.map +1 -1
  58. package/dist/core/ports.js +28 -0
  59. package/dist/core/ports.js.map +1 -1
  60. package/dist/core/puertosDeAvd.js +64 -0
  61. package/dist/core/puertosDeAvd.js.map +1 -0
  62. package/dist/core/selectorDeCarpeta.js +88 -9
  63. package/dist/core/selectorDeCarpeta.js.map +1 -1
  64. package/dist/core/settings.js +60 -1
  65. package/dist/core/settings.js.map +1 -1
  66. package/dist/core/shellDeAgente.js +11 -0
  67. package/dist/core/shellDeAgente.js.map +1 -1
  68. package/dist/core/stitch.js +114 -0
  69. package/dist/core/stitch.js.map +1 -0
  70. package/dist/web/servidor/arranque.js +523 -52
  71. package/dist/web/servidor/arranque.js.map +1 -1
  72. package/dist/web/servidor/consolaDeTarea.js +4 -1
  73. package/dist/web/servidor/consolaDeTarea.js.map +1 -1
  74. package/dist/web/servidor/pielWeb.js +3 -1
  75. package/dist/web/servidor/pielWeb.js.map +1 -1
  76. package/dist/web/servidor/transporte.js.map +1 -1
  77. package/dist/web/servidor/vestibulo.js +154 -48
  78. package/dist/web/servidor/vestibulo.js.map +1 -1
  79. package/package.json +1 -1
  80. package/skills/xone-hotswap/SKILL.md +26 -10
  81. package/skills/xone-hotswap/lib/dispositivo.mjs +97 -3
  82. package/skills/xone-hotswap/references/conexion-y-despliegue.md +27 -15
  83. package/skills/xone-hotswap/scripts/xone-arrancar-android +9 -2
  84. package/skills/xone-hotswap/scripts/xone-desplegar-android +12 -6
  85. package/skills/xone-hotswap/scripts/xone-hotswap +32 -10
  86. package/skills/xone-hotswap/scripts/xone-reiniciar-android +23 -7
  87. package/apps/web/dist/assets/index-40sIfNsT.js +0 -485
  88. package/apps/web/dist/assets/index-BYTna8Os.css +0 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: xone-hotswap
3
- description: Probar una app XOne en un dispositivo o emulador LOCAL —Android o iOS— a través del servidor hotswap que la propia app host levanta en el dispositivo. Usar al desplegar una app en un móvil o simulador, al reiniciarla para que aplique cambios, al capturar la pantalla o el árbol de controles, al pulsar o rellenar controles desde fuera, al leer el log o consultar la base de datos del dispositivo, y al diagnosticar por qué el dispositivo no responde en el puerto 8443. Cubre las dos plataformas y dice, comando por comando, cuál de las dos lo tiene.
3
+ description: Probar una app XOne en un dispositivo o emulador LOCAL —Android o iOS— a través del servidor hotswap que la propia app host levanta en el dispositivo. Usar al desplegar una app en un móvil o simulador, al reiniciarla para que aplique cambios, al capturar la pantalla o el árbol de controles, al pulsar o rellenar controles desde fuera, al leer el log o consultar la base de datos del dispositivo, y al diagnosticar por qué el dispositivo no responde en su puerto. Cubre las dos plataformas y dice, comando por comando, cuál de las dos lo tiene.
4
4
  ---
5
5
 
6
6
  # Probar una app XOne en un dispositivo local (hotswap)
@@ -63,9 +63,12 @@ en vez de ser una foto de la pantalla.
63
63
 
64
64
  - **HTTPS con certificado autofirmado** en las dos plataformas. Todas las URLs son `https://`;
65
65
  con `curl` hace falta `-k`. Solo escucha en `http://` si alguien desactivó la conexión segura.
66
- - **El puerto 8443 es el POR DEFECTO, no una garantía.** En Android, si está ocupado —otra APK
67
- del framework instalada— el servidor coge el siguiente libre, y el real se ve en la pantalla
68
- del servidor hotswap (pestaña Información).
66
+ - **El puerto 8443 es el POR DEFECTO DENTRO DEL APARATO, no una garantía.** En Android, si está
67
+ ocupado —otra APK del framework instalada— el servidor coge el siguiente libre, y el real se ve
68
+ en la pantalla del servidor hotswap (pestaña Información). Es un puerto DEL APARATO: el que
69
+ usas desde este Mac es otro asunto y lo resuelven los scripts (ver «Varios emuladores a la vez»).
70
+ Los scripts resuelven el puerto LOCAL; el remoto es SIEMPRE 8443. Si el servidor cogió otro,
71
+ no tienes camino: díselo a la persona con el puerto que enseñe la pantalla, y no montes el túnel a mano.
69
72
  - **Subir NO aplica.** El proceso tiene cargado en memoria lo de antes: hay que reiniciar la app.
70
73
  - **La base de datos va cifrada con SQLCipher.** Subir un `.db` en claro termina en
71
74
  `database disk image is malformed (code 11)`.
@@ -160,8 +163,9 @@ Tres cosas de `xone-hotswap` que conviene saber antes de leer su salida:
160
163
  en el contexto para siempre.
161
164
  - **La respuesta viene en `status`** —no en un campo `image`— y una captura de Android es
162
165
  **JPEG**, no PNG.
163
- - Si muere con «no se pudo hablar con el aparato», casi siempre falta el túnel
164
- (`adb forward tcp:8443 tcp:8443`) o la app host no está viva.
166
+ - Si muere con «no se pudo hablar con el aparato», casi siempre la app host no está viva: relanza
167
+ `xone-desplegar-android` o `xone-reiniciar-android`, que ponen el túnel. Si el servidor está en OTRO
168
+ puerto dentro del aparato, relanzar no lo arregla (el túnel va siempre al 8443): avisa a la persona.
165
169
 
166
170
  Y `xone-desplegar-android` **no necesita un `zip` del sistema**: construye el ZIP él mismo con
167
171
  `zlib`, así que tampoco depende de que haya uno en Windows.
@@ -196,8 +200,8 @@ resuelve con el mismo localizador que la pestaña Ejecutar.
196
200
 
197
201
  ## Cómo llegar, según la plataforma
198
202
 
199
- **Android** — por `adb forward`, y entonces la IP es `127.0.0.1` (el localhost del PC, que adb
200
- tuneliza al dispositivo). **Qué aparato usan los scripts `xone-*`**: el que se les pase con
203
+ **Android** — por un túnel de `adb`, y entonces la IP es `127.0.0.1` (el localhost del PC, que adb
204
+ tuneliza al dispositivo). **El túnel lo ponen los scripts**, no tú (ver «Varios emuladores a la vez»). **Qué aparato usan los scripts `xone-*`**: el que se les pase con
201
205
  `--serie`; si no, el ELEGIDO en la sesión de XOneCode (lo leen de `$XONECODE_DISPOSITIVO` en cada
202
206
  ejecución); si no hay elección, un EMULADOR antes que un dispositivo físico. Lo dicen por stderr
203
207
  (`dispositivo: emulator-5554 (el de la sesión: Pixel 8)`). Con `adb` a mano y varios
@@ -206,9 +210,21 @@ dispositivos, `adb -s <serial>`:
206
210
  ```bash
207
211
  adb devices -l
208
212
  adb shell am start -n com.xone.android.framework/com.xone.android.hotswap.activities.SetupActivity
209
- adb forward tcp:8443 tcp:8443 # reaplicar tras cada reconexión del cable
210
213
  ```
211
214
 
215
+ ### Varios emuladores a la vez
216
+
217
+ Cada AVD tiene SU puerto en este Mac (Ajustes → Dispositivos; con un solo AVD es el 8443 y no hay
218
+ nada que tocar). Dentro del aparato el servidor sigue en el 8443. **El túnel lo ponen
219
+ `xone-desplegar-android` y `xone-reiniciar-android`** con el puerto del aparato de tu sesión —lo
220
+ resuelven por el AVD de la serie—, y `xone-hotswap` ya se conecta a ese puerto: no pases
221
+ `--puerto N` salvo que te lo pidan (fuerza el puerto local). **Si a desplegar/reiniciar les pasas `--serie`,
222
+ pasa EL MISMO `--serie` a `xone-hotswap`**: sin él habla con el aparato de la sesión. En iOS no hay túnel y es siempre el 8443.
223
+
224
+ **No hagas `adb forward` a mano** (se rechaza): dos túneles al mismo puerto local no conviven, y
225
+ el segundo le quita el aparato al primero sin dar error — otra sesión se quedaría hablando con tu
226
+ emulador. Para ver los túneles, `adb forward --list`.
227
+
212
228
  **iOS** — **no hay `adb` ni túnel**. El simulador comparte la pila de red del Mac, así que el
213
229
  servidor es alcanzable directamente; en dispositivo físico se llega por la IP de la LAN que
214
230
  muestra la propia app:
@@ -216,7 +232,7 @@ muestra la propia app:
216
232
  ```bash
217
233
  xcrun simctl list devices booted
218
234
  xcrun simctl launch <UDID> es.xone.studioapp.swift # arrancar el host levanta el servidor
219
- curl -sk -X POST https://localhost:8443/command -d '{"command":"getAllElements"}'
235
+ curl -sk -X POST https://localhost:8443/command -d '{"command":"getAllElements"}' # iOS: sin túnel, siempre 8443
220
236
  ```
221
237
 
222
238
  Detalle de despliegue, relanzado y endpoints de fichero: [conexión y despliegue](references/conexion-y-despliegue.md).
@@ -17,6 +17,8 @@
17
17
  */
18
18
  import { execFileSync } from "node:child_process";
19
19
  import { existsSync, readFileSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { join } from "node:path";
20
22
 
21
23
  /** El dispositivo elegido en la sesión, o `undefined`. Un fichero roto es «no hay elección». */
22
24
  export function dispositivoDeLaSesion(entorno = process.env) {
@@ -61,17 +63,18 @@ export function preferirEmulador(salidaDeAdbDevices) {
61
63
  }
62
64
 
63
65
  /** El serial de Android: `--serie`, el de la sesión o un emulador. Y dice de dónde salió. */
64
- export function serieAndroid(explicita, { entorno = process.env, adb = rutaDeAdb(entorno) } = {}) {
66
+ export function serieAndroid(explicita, { entorno = process.env, adb = rutaDeAdb(entorno), ejecutar = ejecutarAdb } = {}) {
65
67
  if (explicita) return { serie: explicita, porque: "pasado con --serie" };
66
68
  const elegido = dispositivoDeLaSesion(entorno);
67
69
  if (elegido?.plataforma === "android") return { serie: elegido.id, porque: `el de la sesión: ${elegido.nombre}` };
68
70
  try {
69
- const serie = preferirEmulador(execFileSync(adb, ["devices"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
71
+ // Con tope (`ejecutarAdb`): un adb colgado cae en el `catch` en vez de colgar el script.
72
+ const serie = preferirEmulador(ejecutar(adb, ["devices"]));
70
73
  if (serie !== undefined) {
71
74
  return { serie, porque: serie.startsWith("emulator-") ? "sin elegir en la sesión: un emulador" : "el único conectado" };
72
75
  }
73
76
  } catch {
74
- // Sin adb que conteste, el script sigue sin `-s` y su propia comprobación dirá qué falta.
77
+ // Sin adb que conteste (o con el tope cumplido), el script sigue sin `-s` y su propia comprobación dirá qué falta.
75
78
  }
76
79
  return { serie: undefined, porque: "sin elegir" };
77
80
  }
@@ -83,3 +86,94 @@ export function udidIos(explicito, entorno = process.env) {
83
86
  const elegido = dispositivoDeLaSesion(entorno);
84
87
  return elegido?.plataforma === "ios" && elegido.clase === "simulador" ? elegido.id : undefined;
85
88
  }
89
+
90
+ /** La forma de un nombre de AVD. Copia de `core/puertosDeAvd.ts#FORMA_DE_NOMBRE_DE_AVD`. */
91
+ export const FORMA_DE_NOMBRE_DE_AVD = /^[A-Za-z0-9._][A-Za-z0-9._-]*$/;
92
+
93
+ /** Las banderas de «sin ventana». Copia de `core/puertosDeAvd.ts#argsDeArranque` (un test las compara). */
94
+ export const BANDERAS_SIN_VENTANA = ["-no-window", "-no-audio", "-no-metrics"];
95
+
96
+ /** El puerto del hotswap DENTRO del aparato y el local por omisión. Copia de `core/puertosDeAvd.ts#PUERTO_DEL_HOTSWAP`. */
97
+ export const PUERTO_DEL_HOTSWAP = 8443;
98
+
99
+ // Con tope: un adb colgado cae en el `catch` de quien llama (puerto por omisión) en vez de colgar el script.
100
+ const ejecutarAdb = (binario, args) =>
101
+ execFileSync(binario, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 });
102
+
103
+ /** El AVD detrás de un emulador en marcha, por su consola. Un físico no tiene: `undefined` sin preguntar. */
104
+ export function avdDeLaSerie(serie, { entorno = process.env, ejecutar = ejecutarAdb } = {}) {
105
+ if (!serie || !serie.startsWith("emulator-")) return undefined;
106
+ try {
107
+ return ejecutar(rutaDeAdb(entorno), ["-s", serie, "emu", "avd", "name"])
108
+ .split(/\r?\n/)
109
+ .map((l) => l.trim())
110
+ .find((l) => l !== "" && l !== "OK");
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Lo guardado para ese AVD en `~/.xonecode/settings.json` (`dispositivos.avds`). Se lee en CADA
118
+ * ejecución, como el dispositivo de la sesión: cambiar el puerto en Ajustes alcanza al siguiente
119
+ * comando. Un fichero ausente o roto es «nada guardado».
120
+ */
121
+ export function ajusteDelAvd(avd, { casa = homedir(), leer = (r) => readFileSync(r, "utf8") } = {}) {
122
+ if (!avd) return {};
123
+ try {
124
+ const s = JSON.parse(leer(join(casa, ".xonecode", "settings.json")));
125
+ const a = s?.dispositivos?.avds?.[avd];
126
+ // El MISMO filtro que `core/settings.ts#validarAvds`: lo que allí se tira, aquí no se usa.
127
+ if (!FORMA_DE_NOMBRE_DE_AVD.test(avd) || typeof a !== "object" || a === null) return {};
128
+ const salida = {};
129
+ if (Number.isInteger(a.puerto) && a.puerto >= 1024 && a.puerto <= 65535) salida.puerto = a.puerto;
130
+ if (a.sinVentana === true) salida.sinVentana = true;
131
+ return salida;
132
+ } catch {
133
+ return {};
134
+ }
135
+ }
136
+
137
+ /**
138
+ * El puerto LOCAL del túnel. De más explícito a menos: `--puerto`, el guardado del AVD de esta
139
+ * serie, 8443. Misma regla que `core/puertosDeAvd.ts#puertoDeAvd` (un test compara las dos).
140
+ */
141
+ export function puertoAndroid(explicito, serie, deps = {}) {
142
+ return puertoConMotivo(explicito, serie, deps).puerto;
143
+ }
144
+
145
+ /**
146
+ * Lo mismo que `puertoAndroid`, y además DICE cuándo cae al 8443 sin haber podido saber de qué
147
+ * AVD es la serie (adb falló o venció el tope): ése es el puerto del primer AVD, y callarlo
148
+ * dejaría a un segundo emulador hablando con el aparato de otro. `porque` es `undefined` cuando
149
+ * no hay nada que avisar.
150
+ */
151
+ export function puertoConMotivo(explicito, serie, deps = {}) {
152
+ const n = Number(explicito);
153
+ if (explicito !== undefined && Number.isInteger(n) && n >= 1024 && n <= 65535) return { puerto: n };
154
+ const avd = avdDeLaSerie(serie, deps);
155
+ const guardado = ajusteDelAvd(avd, deps).puerto;
156
+ if (guardado !== undefined) return { puerto: guardado };
157
+ if (avd === undefined && serie?.startsWith("emulator-")) {
158
+ return { puerto: PUERTO_DEL_HOTSWAP, porque: `no pude saber qué AVD es ${serie}; uso el ${PUERTO_DEL_HOTSWAP}, que puede ser el de otro emulador` };
159
+ }
160
+ return { puerto: PUERTO_DEL_HOTSWAP };
161
+ }
162
+
163
+ /**
164
+ * Retira de los argumentos las opciones que llevan valor (`--puerto 9000`), estén donde estén, y
165
+ * deja el resto INTACTO (incluidos `--` y los `clave=valor`). Solo quita lo que ESTÁ: con un
166
+ * índice -1 (ausente) `i === -1 + 1` descartaría el primer argumento.
167
+ */
168
+ export function retirarOpciones(argumentos, nombres) {
169
+ const opciones = {};
170
+ const fuera = new Set();
171
+ for (const nombre of nombres) {
172
+ const i = argumentos.indexOf(nombre);
173
+ if (i < 0) continue;
174
+ opciones[nombre] = argumentos[i + 1];
175
+ fuera.add(i);
176
+ fuera.add(i + 1);
177
+ }
178
+ return { opciones, resto: argumentos.filter((_, i) => !fuera.has(i)) };
179
+ }
@@ -6,22 +6,28 @@ comandos, uno a uno: [comandos.md](comandos.md).
6
6
 
7
7
  ## 1. Llegar
8
8
 
9
- ### Android: `adb forward`
9
+ ### Android: el túnel de `adb`, que ponen los scripts
10
10
 
11
- El servidor escucha en `localhost` del dispositivo. Para alcanzarlo desde el PC:
11
+ El servidor escucha en `localhost` del dispositivo. Para alcanzarlo desde el PC hace falta un túnel
12
+ `adb -s <serie> forward tcp:<puerto del aparato> tcp:8443`, y **lo ponen `xone-desplegar-android` y
13
+ `xone-reiniciar-android`**, no tú: cada AVD tiene SU puerto local en este Mac (Ajustes →
14
+ Dispositivos; con un solo AVD es el 8443), que los scripts resuelven por el AVD de la serie.
15
+ `--puerto N` fuerza el puerto LOCAL. Con varios aparatos, si a desplegar/reiniciar les pasas `--serie`, pasa EL MISMO
16
+ `--serie` a `xone-hotswap`. **Un `adb forward` a mano se rechaza**: dos túneles al mismo puerto local no
17
+ conviven y el segundo le quita el aparato al primero sin error. Para ver los que hay:
12
18
 
13
19
  ```bash
14
20
  adb devices -l
15
- adb forward tcp:8443 tcp:8443 # reaplicar tras cada reconexión o reinicio del demonio
21
+ adb forward --list
16
22
  ```
17
23
 
18
- A partir de ahí la IP de destino es **`127.0.0.1`** (el localhost del PC, que adb tuneliza).
19
- Con varios dispositivos: `adb -s <serial> forward tcp:8443 tcp:8443`. Para deshacerlo:
20
- `adb forward --remove tcp:8443`.
24
+ A partir de ahí la IP de destino es **`127.0.0.1`** (el localhost del PC, que adb tuneliza) y el
25
+ puerto el del aparato; `xone-hotswap` ya lo sabe (`wss://127.0.0.1:<puerto del aparato>/hotswap`,
26
+ `HOTSWAP_URL` gana si está puesta).
21
27
 
22
28
  **El canal de los comandos es un WebSocket, y vive en `/hotswap`.** Medido el 16-sep-2026 contra
23
29
  `com.xone.android.framework` **5.0.2.2dev** (flavor standalone, Android): `wss://127.0.0.1:8443/hotswap`
24
- contesta `101`, mientras que `/`, `/ws` y `/api` contestan `400`. Y **el servidor habla primero**: al
30
+ contesta `101` (con un solo AVD, el 8443 del PC), mientras que `/`, `/ws` y `/api` contestan `400`. Y **el servidor habla primero**: al
25
31
  abrir manda `{"command":"server_hello","protocol_version":3}`, así que el cliente ESPERA el saludo en
26
32
  vez de provocarlo. En esta versión `POST /command` por HTTP da **404** — los comandos van por el
27
33
  WebSocket—, y por eso el `curl` de más abajo es solo para iOS.
@@ -36,9 +42,11 @@ adb shell am start -n com.xone.android.framework/com.xone.android.hotswap.activi
36
42
  adb shell am start -n com.xone.android.developer.framework/com.xone.android.hotswap.activities.SetupActivity
37
43
  ```
38
44
 
39
- **El puerto puede cambiar.** Si 8443 está ocupado (otra APK del framework instalada), el
40
- servidor coge el siguiente libre; el real se ve en la pestaña **Información** de esa pantalla.
41
- Ajusta el forward: `adb forward tcp:<local> tcp:<remoto>`.
45
+ **El puerto puede cambiar DENTRO del aparato.** Si 8443 está ocupado (otra APK del framework
46
+ instalada), el servidor coge el siguiente libre; el real se ve en la pestaña **Información** de esa
47
+ pantalla. Los scripts resuelven el puerto LOCAL (el del AVD); el remoto del túnel es SIEMPRE 8443. Si el
48
+ servidor está en otro, hoy no hay camino para el agente: avisa a la persona con el puerto de esa
49
+ pantalla, sin reintentar en bucle ni montar el túnel a mano.
42
50
 
43
51
  ### iOS: sin túnel
44
52
 
@@ -76,9 +84,12 @@ El comportamiento especial se dispara **solo** con ese nombre de fichero exacto:
76
84
  4. **El ZIP se elimina siempre al terminar**, tanto en éxito como en fallo de extracción.
77
85
  5. Tras una extracción correcta, la app aparece o se actualiza en la lista.
78
86
 
87
+ Lo hace `xone-desplegar-android` por el túnel del aparato. Como diagnóstico, sin script (con
88
+ `<puerto>` el local del AVD, no siempre 8443):
89
+
79
90
  ```bash
80
91
  curl -k -X POST --data-binary @debug_app_update.zip \
81
- "https://127.0.0.1:8443/file_upload?file=debug_app_update.zip&appName=MiApp"
92
+ "https://127.0.0.1:<puerto>/file_upload?file=debug_app_update.zip&appName=MiApp"
82
93
  ```
83
94
 
84
95
  > **El ZIP no limpia el destino.** La extracción añade y sobrescribe, pero no borra lo que ya
@@ -260,15 +271,16 @@ Tres causas, en este orden:
260
271
  arranca con el proceso. La pantalla del servidor lo levanta y además enseña IP y puerto
261
272
  reales:
262
273
  `adb shell am start -n com.xone.android.framework/com.xone.android.hotswap.activities.SetupActivity`
263
- 2. **El puerto no es 8443.** Si estaba ocupado —lo normal con DOS APK de framework
274
+ 2. **El puerto no es 8443 dentro del aparato.** Si estaba ocupado —lo normal con DOS APK de framework
264
275
  instaladas— el servidor tomó el siguiente libre. El override guardado se lee con
265
276
  `adb shell run-as com.xone.android.framework cat shared_prefs/hotswap_preferences.xml`
266
- (claves `port_number` y `use_secure_connection`); si no dice nada, se sondea 8443–8446
267
- rehaciendo el forward a cada puerto remoto.
277
+ (claves `port_number` y `use_secure_connection`); el túnel de los scripts va siempre al
278
+ 8443 del aparato, así que si el servidor cogió otro no hay camino para el agente: avisa a la
279
+ persona con el puerto que diga la pantalla del framework, y no montes el túnel a mano.
268
280
  3. **El SSL está desactivado** (`use_secure_connection` a `false`): entonces el servidor habla
269
281
  `http://` y el cliente tiene que ir en claro.
270
282
 
271
- Por LAN en vez de USB no hay forward: se apunta a la IP del dispositivo, la que enseña la barra
283
+ Por LAN en vez de USB no hay túnel: se apunta a la IP del dispositivo, la que enseña la barra
272
284
  superior de esa pantalla.
273
285
 
274
286
  ## Dos cosas del despliegue que no dan ningún error cuando se hacen mal
@@ -32,9 +32,10 @@
32
32
  * xone-arrancar-android el único AVD que haya
33
33
  * xone-arrancar-android --avd pixel8 con varios
34
34
  * xone-arrancar-android --lista qué AVDs hay, sin arrancar nada
35
+ * xone-arrancar-android --sin-ventana sin ventana (también si el AVD lo tiene marcado en Ajustes)
35
36
  */
36
37
  import { execFileSync, spawn } from "node:child_process";
37
- import { dormir } from "../lib/dispositivo.mjs";
38
+ import { dormir, ajusteDelAvd, BANDERAS_SIN_VENTANA } from "../lib/dispositivo.mjs";
38
39
 
39
40
  /** Lo que tarda un arranque en frío de un AVD con `google_apis`, con margen. Por debajo del
40
41
  * tope de reloj de UN comando del harness (600 s), que es quien mataría al script entero. */
@@ -53,6 +54,7 @@ if (args.includes("--help") || args.includes("-h")) {
53
54
  xone-arrancar-android el único AVD que haya
54
55
  xone-arrancar-android --avd pixel8 con varios
55
56
  xone-arrancar-android --lista qué AVDs hay, sin arrancar nada
57
+ xone-arrancar-android --sin-ventana sin ventana ni audio (también si el AVD lo tiene marcado en Ajustes)
56
58
 
57
59
  Si ya hay un emulador vivo con ese AVD, lo dice y no lanza nada. Espera a que el sistema
58
60
  diga \`sys.boot_completed=1\`, no solo a que el aparato aparezca en \`adb devices\`: sobre un
@@ -172,7 +174,12 @@ if (yaEstaba === undefined) {
172
174
  * pipes, no sale, y entonces el que muere por el tope de reloj es el SCRIPT —diez minutos
173
175
  * de turno tirados—. `unref` es lo que deja que este proceso acabe con el nieto vivo.
174
176
  */
175
- const hijo = spawn(EMULATOR, ["-avd", avd], { detached: true, stdio: "ignore" });
177
+ // «Sin ventana» viene de Ajustes → Dispositivos (por AVD) o de `--sin-ventana`. Es la misma
178
+ // receta que el botón de la web (`argsDeArranque`), la constante es copia de la de core porque la skill no importa TS (un test las compara).
179
+ const sinVentana = args.includes("--sin-ventana") || ajusteDelAvd(avd).sinVentana === true;
180
+ const banderas = sinVentana ? BANDERAS_SIN_VENTANA : [];
181
+ if (sinVentana) console.log(`${avd} arranca SIN ventana: para verlo, captura; para pararlo, adb -s <serie> emu kill`);
182
+ const hijo = spawn(EMULATOR, ["-avd", avd, ...banderas], { detached: true, stdio: "ignore" });
176
183
  let fallo;
177
184
  hijo.on("error", (e) => {
178
185
  fallo = e.message;
@@ -11,6 +11,7 @@
11
11
  * xone-desplegar-android
12
12
  * xone-desplegar-android --captura
13
13
  * xone-desplegar-android --app MiApp --serie emulator-5554
14
+ * xone-desplegar-android --puerto 8444 puerto LOCAL del túnel (por omisión el del aparato, Ajustes → Dispositivos)
14
15
  *
15
16
  * EL ORDEN, Y POR QUÉ ES ESE:
16
17
  * 1. **El túnel primero.** Sin `adb forward`, `127.0.0.1:8443` no lleva a ninguna parte: el
@@ -48,11 +49,10 @@ import { execFileSync } from "node:child_process";
48
49
  import { existsSync, readFileSync, readdirSync } from "node:fs";
49
50
  import { basename, join, relative, sep } from "node:path";
50
51
  import { deflateRawSync } from "node:zlib";
51
- import { serieAndroid, rutaDeAdb, NODE, dormir } from "../lib/dispositivo.mjs";
52
+ import { serieAndroid, rutaDeAdb, NODE, dormir, PUERTO_DEL_HOTSWAP, puertoConMotivo } from "../lib/dispositivo.mjs";
52
53
 
53
54
  const PAQUETE = "com.xone.android.framework";
54
55
  const ACTIVIDAD = `${PAQUETE}/com.xone.android.hotswap.activities.SetupActivity`;
55
- const PUERTO = 8443;
56
56
 
57
57
  /**
58
58
  * Un ZIP en memoria, sin `zip` del sistema y sin dependencias.
@@ -160,6 +160,7 @@ if (args.includes("--help") || args.includes("-h")) {
160
160
  xone-desplegar-android nombre de app tomado de app.xml
161
161
  xone-desplegar-android --app MiApp nombre explícito
162
162
  xone-desplegar-android --serie emulator-5554 con varios aparatos conectados
163
+ xone-desplegar-android --puerto 8444 puerto LOCAL del túnel (por omisión, el del aparato en Ajustes; si no, 8443)
163
164
 
164
165
  Hace la cadena entera: túnel adb, ZIP del proyecto (sin .xonecode ni .git), subida, reinicio del
165
166
  framework y lanzamiento, y COMPRUEBA que la app esté viva pidiendo su árbol de controles.
@@ -174,6 +175,10 @@ Studio o \`xone-db-tools\`, no esto, y sin ella la app muere aunque el verificad
174
175
  // dice por stderr con cuál y por qué: con varios aparatos, callarlo haría adivinar dónde pasó.
175
176
  const { serie, porque } = serieAndroid(opcion("serie"));
176
177
  if (serie !== undefined) process.stderr.write(`dispositivo: ${serie} (${porque})\n`);
178
+ // El puerto LOCAL es del aparato (Ajustes → Dispositivos): con dos emuladores, un 8443 fijo
179
+ // hacía que el segundo túnel le quitara el aparato al primero sin avisar. Dentro sigue en 8443.
180
+ const { puerto: PUERTO, porque: porquePuerto } = puertoConMotivo(opcion("puerto"), serie);
181
+ if (porquePuerto !== undefined) process.stderr.write(`puerto: ${PUERTO} (${porquePuerto})\n`);
177
182
  const adb = (...partes) =>
178
183
  execFileSync(rutaDeAdb(), [...(serie ? ["-s", serie] : []), ...partes], { encoding: "utf8" });
179
184
 
@@ -347,8 +352,8 @@ function exigirAparato() {
347
352
  }
348
353
 
349
354
  exigirAparato();
350
- console.log(`1/5 túnel: ${PUERTO}`);
351
- adb("forward", `tcp:${PUERTO}`, `tcp:${PUERTO}`);
355
+ console.log(`1/5 túnel: 127.0.0.1:${PUERTO} → ${PUERTO_DEL_HOTSWAP} del aparato`);
356
+ adb("forward", `tcp:${PUERTO}`, `tcp:${PUERTO_DEL_HOTSWAP}`);
352
357
 
353
358
  console.log("2/5 empaquetando el proyecto (sin .xonecode ni .git)");
354
359
  const cuerpo = crearZip(process.cwd(), [".xonecode", ".git"]);
@@ -400,7 +405,8 @@ function exigirAparato() {
400
405
  const pares = Object.entries(resto).map(([k, v]) =>
401
406
  `${k}=${typeof v === "object" && v !== null ? JSON.stringify(v) : String(v)}`,
402
407
  );
403
- return execFileSync(NODE, [cliente, command, ...pares], { encoding: "utf8" }).trim();
408
+ // El hijo habla por el MISMO puerto que el túnel de arriba, aunque venga de `--puerto`.
409
+ return execFileSync(NODE, [cliente, command, ...pares], { encoding: "utf8", env: { ...process.env, HOTSWAP_URL: process.env.HOTSWAP_URL ?? `wss://127.0.0.1:${PUERTO}/hotswap` } }).trim();
404
410
  };
405
411
  const esperar = (segundos) => dormir(segundos);
406
412
 
@@ -421,7 +427,7 @@ function exigirAparato() {
421
427
  }
422
428
  if (!listo) {
423
429
  console.error(`el servidor hotswap no levantó tras el reinicio. Comprueba que ${PAQUETE} está instalado`);
424
- console.error("y que el puerto es el 8443 (si estaba ocupado, el framework coge el siguiente libre).");
430
+ console.error(`y que el puerto del aparato es el ${PUERTO_DEL_HOTSWAP} (si estaba ocupado, el framework coge el siguiente libre; el local es el ${PUERTO}).`);
425
431
  process.exit(1);
426
432
  }
427
433
 
@@ -15,13 +15,16 @@
15
15
  *
16
16
  * Los comandos se mandan EN ORDEN y cada uno espera la respuesta del anterior.
17
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.
18
+ * ANTES DE LLAMARLO (Android): tiene que estar el túnel, o el puerto local no lleva a
19
+ * ninguna parte —el servidor escucha en el localhost del APARATO—. Lo ponen
20
+ * `xone-desplegar-android` y `xone-reiniciar-android`, con el puerto del aparato: no hagas
21
+ * `adb forward` a mano. Este cliente usa ese mismo puerto (`--puerto N` lo fuerza). Con varios
22
+ * aparatos, `--serie S` elige a cuál hablar: el MISMO que se pasó a desplegar/reiniciar.
23
+ * En iOS no hay túnel: el simulador comparte la pila de red del Mac (8443).
22
24
  *
23
25
  * VARIABLES
24
- * HOTSWAP_URL el destino. Por omisión `wss://127.0.0.1:8443/hotswap`.
26
+ * HOTSWAP_URL el destino. Por omisión `wss://127.0.0.1:<puerto del aparato>/hotswap`
27
+ * (8443 si el aparato no tiene otro guardado en Ajustes → Dispositivos).
25
28
  * XONECODE_ARTEFACTOS dónde dejar lo binario (una captura). Si no está, se dice y no se
26
29
  * guarda: escribirlo en el proyecto sería ensuciar la app del usuario.
27
30
  * HOTSWAP_TOPE_MS tope de espera de UNA respuesta. Por omisión 40000.
@@ -52,8 +55,21 @@ process.on("warning", (aviso) => {
52
55
 
53
56
  import { mkdirSync, writeFileSync } from "node:fs";
54
57
  import { join } from "node:path";
58
+ import { dispositivoDeLaSesion, puertoConMotivo, retirarOpciones, serieAndroid, PUERTO_DEL_HOTSWAP } from "../lib/dispositivo.mjs";
55
59
 
56
- const URL_POR_OMISION = "wss://127.0.0.1:8443/hotswap";
60
+ /**
61
+ * La URL cuando nadie dice `HOTSWAP_URL`: el puerto LOCAL del túnel es del aparato, no un 8443
62
+ * fijo (con dos emuladores, un segundo `adb forward` al mismo puerto le quita el aparato al
63
+ * primero sin error). Solo se resuelve si hace falta: con `HOTSWAP_URL` no se llama a adb.
64
+ */
65
+ function urlPorOmision(puertoPedido, seriePedida) {
66
+ // iOS no tiene túnel: el simulador comparte el localhost del Mac.
67
+ if (dispositivoDeLaSesion()?.plataforma === "ios") return `wss://127.0.0.1:${PUERTO_DEL_HOTSWAP}/hotswap`;
68
+ const { serie, porque } = serieAndroid(seriePedida);
69
+ const { puerto, porque: porquePuerto } = puertoConMotivo(puertoPedido, serie);
70
+ if (porquePuerto !== undefined) process.stderr.write(`puerto: ${puerto} (${porquePuerto})\n`);
71
+ return `wss://127.0.0.1:${puerto}/hotswap`;
72
+ }
57
73
  const TOPE_MS = Number(process.env.HOTSWAP_TOPE_MS ?? 40_000);
58
74
  const ARTEFACTOS = process.env.XONECODE_ARTEFACTOS;
59
75
  /**
@@ -89,7 +105,11 @@ const RUTA_VIRTUAL = process.env.XONECODE_HOTSWAP === undefined ? "/artefactos/"
89
105
  */
90
106
  const LARGO_PARA_GUARDAR = 2_000;
91
107
 
92
- const pedidos = process.argv.slice(2);
108
+ // `--puerto N` y `--serie S` se retiran ANTES de leer los comandos: no son de ninguno.
109
+ const argumentos = process.argv.slice(2);
110
+ const { opciones, resto: pedidos } = retirarOpciones(argumentos, ["--puerto", "--serie"]);
111
+ const puertoPedido = opciones["--puerto"];
112
+ const seriePedida = opciones["--serie"];
93
113
  if (pedidos.length === 0 || pedidos.includes("--help") || pedidos.includes("-h")) {
94
114
  // El prompt del agente promete que `--help` contesta. Una promesa así, sin cumplir, cuesta
95
115
  // un viaje y deja al modelo adivinando argumentos.
@@ -108,10 +128,12 @@ conexion, que es lo que hay que hacer (el servidor admite una sola y cierra la a
108
128
  Los valores se tipan solos: número, true/false y {...}/[...] como JSON.
109
129
  Con --dry-run se imprime lo que se enviaría, sin conectar.
110
130
 
111
- Antes, en Android, hace falta el túnel: adb forward tcp:8443 tcp:8443
131
+ Antes, en Android, hace falta el túnel: lo ponen xone-desplegar-android y xone-reiniciar-android con el
132
+ puerto del aparato; no hagas adb forward a mano. --puerto N fuerza el puerto local (8443 por omisión).
133
+ --serie S elige el aparato Android (el MISMO que pasaste a desplegar/reiniciar); sin ella, el de la sesión.
112
134
  Una respuesta larga no se imprime: una IMAGEN va a $XONECODE_ARTEFACTOS (y sale en el chat),
113
135
  el TEXTO va a $XONECODE_HOTSWAP y se relee con read_file en /hotswap/<nombre>.
114
- Variables: HOTSWAP_URL (${URL_POR_OMISION}), XONECODE_ARTEFACTOS, XONECODE_HOTSWAP, HOTSWAP_TOPE_MS.
136
+ Variables: HOTSWAP_URL (por omisión wss://127.0.0.1:<puerto del aparato>/hotswap), XONECODE_ARTEFACTOS, XONECODE_HOTSWAP, HOTSWAP_TOPE_MS.
115
137
  El catálogo entero de comandos está en el SKILL.md de xone-hotswap.`);
116
138
  process.exit(pedidos.length === 0 ? 1 : 0);
117
139
  }
@@ -306,7 +328,7 @@ function sinBinarios(respuesta) {
306
328
  return copia;
307
329
  }
308
330
 
309
- const ws = new WebSocket(process.env.HOTSWAP_URL ?? URL_POR_OMISION);
331
+ const ws = new WebSocket(process.env.HOTSWAP_URL ?? urlPorOmision(puertoPedido, seriePedida));
310
332
  let saludado = false;
311
333
  let pendiente;
312
334
  let reloj;
@@ -15,17 +15,17 @@
15
15
  * xone-reiniciar-android deja el host en su pantalla de inicio
16
16
  * xone-reiniciar-android --app MiApp y además lanza esa app
17
17
  * xone-reiniciar-android --serie emulator-5554 con varios aparatos conectados
18
+ * xone-reiniciar-android --puerto 8444 puerto LOCAL del túnel (por omisión el del aparato)
18
19
  *
19
20
  * **No despliega nada**: no sube ficheros ni toca el proyecto. Para eso está
20
21
  * `xone-desplegar-android`, que ya hace este reinicio como uno de sus pasos.
21
22
  */
22
23
  import { execFileSync } from "node:child_process";
23
24
  import { join } from "node:path";
24
- import { serieAndroid, rutaDeAdb, NODE, dormir } from "../lib/dispositivo.mjs";
25
+ import { serieAndroid, rutaDeAdb, NODE, dormir, PUERTO_DEL_HOTSWAP, puertoConMotivo } from "../lib/dispositivo.mjs";
25
26
 
26
27
  const PAQUETE = "com.xone.android.framework";
27
28
  const ACTIVIDAD = `${PAQUETE}/com.xone.android.hotswap.activities.SetupActivity`;
28
- const PUERTO = 8443;
29
29
 
30
30
  const args = process.argv.slice(2);
31
31
  if (args.includes("--help") || args.includes("-h")) {
@@ -34,6 +34,8 @@ if (args.includes("--help") || args.includes("-h")) {
34
34
  xone-reiniciar-android lo deja en su pantalla de inicio
35
35
  xone-reiniciar-android --app MiApp y además lanza esa app
36
36
  xone-reiniciar-android --serie emulator-5554 con varios aparatos conectados
37
+ xone-reiniciar-android --puerto 8444 puerto LOCAL del túnel (por omisión, el del aparato en
38
+ Ajustes → Dispositivos; si no, 8443)
37
39
 
38
40
  Úsalo cuando el canal no conteste: el host reabre la última app ejecutada, y si ésa se quedó
39
41
  en un error, nada responde aunque el aparato esté perfectamente. No despliega ni sube nada.`);
@@ -48,12 +50,26 @@ const opcion = (nombre) => {
48
50
  // dice por stderr con cuál y por qué: con varios aparatos, callarlo haría adivinar dónde pasó.
49
51
  const { serie, porque } = serieAndroid(opcion("serie"));
50
52
  if (serie !== undefined) process.stderr.write(`dispositivo: ${serie} (${porque})\n`);
53
+ // El puerto LOCAL es del aparato (Ajustes → Dispositivos): con dos emuladores, un 8443 fijo
54
+ // hacía que el segundo túnel le quitara el aparato al primero sin avisar. Dentro sigue en 8443.
55
+ const { puerto: PUERTO, porque: porquePuerto } = puertoConMotivo(opcion("puerto"), serie);
56
+ if (porquePuerto !== undefined) process.stderr.write(`puerto: ${PUERTO} (${porquePuerto})\n`);
51
57
  const adb = (...partes) =>
52
58
  execFileSync(rutaDeAdb(), [...(serie ? ["-s", serie] : []), ...partes], { encoding: "utf8" });
53
59
 
54
60
  const cliente = join(import.meta.dirname, "xone-hotswap");
55
- const mandar = (comando) =>
56
- execFileSync(NODE, [cliente, JSON.stringify(comando)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
61
+ // En `clave=valor`: el cliente ya no recibe JSON. Y por el MISMO puerto que el túnel de abajo.
62
+ const mandar = (comando) => {
63
+ const { command, ...resto } = comando;
64
+ const pares = Object.entries(resto).map(([k, v]) =>
65
+ `${k}=${typeof v === "object" && v !== null ? JSON.stringify(v) : String(v)}`,
66
+ );
67
+ return execFileSync(NODE, [cliente, command, ...pares], {
68
+ encoding: "utf8",
69
+ stdio: ["ignore", "pipe", "ignore"],
70
+ env: { ...process.env, HOTSWAP_URL: process.env.HOTSWAP_URL ?? `wss://127.0.0.1:${PUERTO}/hotswap` },
71
+ }).trim();
72
+ };
57
73
 
58
74
  /**
59
75
  * Sin aparato NO se empieza, y se dice el siguiente paso HECHO.
@@ -83,10 +99,10 @@ function exigirAparato() {
83
99
  }
84
100
 
85
101
  exigirAparato();
86
- console.log(`1/3 túnel y parada de ${PAQUETE}`);
102
+ console.log(`1/3 túnel (127.0.0.1:${PUERTO} → ${PUERTO_DEL_HOTSWAP} del aparato) y parada de ${PAQUETE}`);
87
103
  // El túnel PRIMERO y sin quitarlo después: hay que reaplicarlo tras cada reconexión del cable,
88
104
  // así que quitarlo dejaría al siguiente hablando con nadie.
89
- adb("forward", `tcp:${PUERTO}`, `tcp:${PUERTO}`);
105
+ adb("forward", `tcp:${PUERTO}`, `tcp:${PUERTO_DEL_HOTSWAP}`);
90
106
  adb("shell", "am", "force-stop", PAQUETE);
91
107
 
92
108
  console.log("2/3 levantando el host");
@@ -112,7 +128,7 @@ for (let intento = 1; intento <= 20 && !vivo; intento++) {
112
128
  }
113
129
  if (!vivo) {
114
130
  console.error(`el canal no levantó. Comprueba que ${PAQUETE} está instalado (adb shell pm list packages | grep xone)`);
115
- console.error(`y que el puerto es el ${PUERTO}: si estaba ocupado, el framework coge el siguiente libre y lo dice en su pantalla.`);
131
+ console.error(`y que el puerto del aparato es el ${PUERTO_DEL_HOTSWAP} (local ${PUERTO}): si estaba ocupado, el framework coge el siguiente libre y lo dice en su pantalla.`);
116
132
  process.exit(1);
117
133
  }
118
134