xonecode 0.21.0 → 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.
- package/apps/web/dist/assets/index-5rtynJV_.js +490 -0
- package/apps/web/dist/assets/index-Cq6c6WEF.css +1 -0
- package/apps/web/dist/assets/{php-UkjSyds-.js → php-BTFUgUDo.js} +1 -1
- package/apps/web/dist/assets/{ruby-e801JuzW.js → ruby-BKKiBtJK.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/dist/agent/config/mudanzaEnDisco.js +237 -3
- package/dist/agent/config/mudanzaEnDisco.js.map +1 -1
- package/dist/agent/config/selectorEnMaquina.js +3 -2
- package/dist/agent/config/selectorEnMaquina.js.map +1 -1
- package/dist/agent/config/settingsEnDisco.js +76 -2
- package/dist/agent/config/settingsEnDisco.js.map +1 -1
- package/dist/agent/dispositivos/arranqueDeEmulador.js +27 -3
- package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -1
- package/dist/agent/dispositivos/instalacionEnMaquina.js +240 -29
- package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -1
- package/dist/agent/dispositivos/lanzamientoEnMaquina.js +12 -17
- package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -1
- package/dist/core/copiaDeAvd.js +111 -0
- package/dist/core/copiaDeAvd.js.map +1 -0
- package/dist/core/mudanzaDeWorkspace.js +105 -15
- package/dist/core/mudanzaDeWorkspace.js.map +1 -1
- package/dist/core/puertosDeAvd.js +64 -0
- package/dist/core/puertosDeAvd.js.map +1 -0
- package/dist/core/selectorDeCarpeta.js +88 -9
- package/dist/core/selectorDeCarpeta.js.map +1 -1
- package/dist/core/settings.js +60 -1
- package/dist/core/settings.js.map +1 -1
- package/dist/core/shellDeAgente.js +11 -0
- package/dist/core/shellDeAgente.js.map +1 -1
- package/dist/web/servidor/arranque.js +476 -43
- package/dist/web/servidor/arranque.js.map +1 -1
- package/dist/web/servidor/transporte.js.map +1 -1
- package/dist/web/servidor/vestibulo.js +154 -48
- package/dist/web/servidor/vestibulo.js.map +1 -1
- package/package.json +1 -1
- package/skills/xone-hotswap/SKILL.md +26 -10
- package/skills/xone-hotswap/lib/dispositivo.mjs +97 -3
- package/skills/xone-hotswap/references/conexion-y-despliegue.md +27 -15
- package/skills/xone-hotswap/scripts/xone-arrancar-android +9 -2
- package/skills/xone-hotswap/scripts/xone-desplegar-android +12 -6
- package/skills/xone-hotswap/scripts/xone-hotswap +32 -10
- package/skills/xone-hotswap/scripts/xone-reiniciar-android +23 -7
- package/apps/web/dist/assets/index-CFoqpKRk.css +0 -1
- package/apps/web/dist/assets/index-DgvLb2SM.js +0 -486
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
20
|
-
`
|
|
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
|
|
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
|
|
40
|
-
servidor coge el siguiente libre; el real se ve en la pestaña **Información** de esa
|
|
41
|
-
|
|
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
|
|
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`);
|
|
267
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
351
|
-
adb("forward", `tcp:${PUERTO}`, `tcp:${
|
|
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
|
-
|
|
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(
|
|
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
|
|
19
|
-
* ninguna parte —el servidor escucha en el localhost del APARATO
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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 ??
|
|
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
|
-
|
|
56
|
-
|
|
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:${
|
|
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
|
|