xonecode 0.8.26 → 0.8.36
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-LdUr7EoT.js → index-DFnjznH1.js} +34 -34
- package/apps/web/dist/assets/{php-Cg-eWGpQ.js → php-CX4yaeUn.js} +1 -1
- package/apps/web/dist/assets/{ruby-EcV7Hon3.js → ruby-Dklcf7v6.js} +1 -1
- package/apps/web/dist/index.html +1 -1
- package/dist/agent/grafo/escriturasEnSerie.js +57 -0
- package/dist/agent/grafo/escriturasEnSerie.js.map +1 -0
- package/dist/agent/grafo/proyecto.js +84 -3
- package/dist/agent/grafo/proyecto.js.map +1 -1
- package/dist/agent/grafo/traerDeLaMaquina.js +117 -0
- package/dist/agent/grafo/traerDeLaMaquina.js.map +1 -0
- package/dist/agent/grafo/validacionXone.js +200 -0
- package/dist/agent/grafo/validacionXone.js.map +1 -0
- package/dist/agent/grafo/xoneAgent.js +33 -1
- package/dist/agent/grafo/xoneAgent.js.map +1 -1
- package/dist/agent/subagentes/agentesEnDisco.js +34 -6
- package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
- package/dist/agent/trazaDeErroresEnDisco.js +45 -0
- package/dist/agent/trazaDeErroresEnDisco.js.map +1 -0
- package/dist/agent/turno/diagnosticoDeTools.js +10 -1
- package/dist/agent/turno/diagnosticoDeTools.js.map +1 -1
- package/dist/agent/turno/informeDeTraza.js +129 -6
- package/dist/agent/turno/informeDeTraza.js.map +1 -1
- package/dist/agent/turno/puente.js +89 -5
- package/dist/agent/turno/puente.js.map +1 -1
- package/dist/agent/turno/resumenDeTool.js +28 -0
- package/dist/agent/turno/resumenDeTool.js.map +1 -1
- package/dist/agent/turno/turnoReal.js +20 -2
- package/dist/agent/turno/turnoReal.js.map +1 -1
- package/dist/core/serieDeEscrituras.js +63 -0
- package/dist/core/serieDeEscrituras.js.map +1 -0
- package/dist/core/traerDeLaMaquina.js +80 -0
- package/dist/core/traerDeLaMaquina.js.map +1 -0
- package/dist/core/trazaDeErrores.js +108 -0
- package/dist/core/trazaDeErrores.js.map +1 -0
- package/dist/core/validacionDeEscritura.js +105 -0
- package/dist/core/validacionDeEscritura.js.map +1 -0
- package/dist/web/servidor/consolaWeb.js +6 -0
- package/dist/web/servidor/consolaWeb.js.map +1 -1
- package/package.json +3 -2
- package/skills/xone-hotswap/SKILL.md +76 -8
- package/skills/xone-hotswap/references/comandos.md +33 -0
- package/skills/xone-hotswap/references/conexion-y-despliegue.md +61 -1
- package/skills/xone-hotswap/scripts/xone-arrancar-ios +1 -1
- package/skills/xone-hotswap/scripts/xone-captura-android +91 -3
- package/skills/xone-hotswap/scripts/xone-desplegar-android +80 -5
- package/skills/xone-hotswap/scripts/xone-hotswap +151 -8
- package/skills/xone-project-generator/references/fases-10-12-readmes-y-validacion.md +15 -7
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Una captura de la pantalla del aparato, POR LA VÍA NATIVA, a la carpeta de artefactos.
|
|
4
4
|
*
|
|
5
|
-
* POR QUÉ EXISTE, si el canal ya sabe capturar. `xone-hotswap
|
|
5
|
+
* POR QUÉ EXISTE, si el canal ya sabe capturar. `xone-hotswap shot`
|
|
6
6
|
* es la vía buena —la pide la app host, así que sale lo que la app pinta— pero necesita el
|
|
7
7
|
* túnel y la app viva. Cuando eso no está, lo único que queda es `adb exec-out screencap`, y
|
|
8
8
|
* medido: el modelo la compone él y redirige la salida a un fichero, que en el `cwd` de la
|
|
@@ -35,7 +35,7 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
35
35
|
xone-captura-android --serie <id> con varios aparatos conectados
|
|
36
36
|
|
|
37
37
|
Es la vía de RESPALDO: si el túnel y la app host están vivos, la buena es
|
|
38
|
-
\`xone-hotswap
|
|
38
|
+
\`xone-hotswap shot\`, que captura lo que la app pinta.
|
|
39
39
|
Nunca redirijas \`adb exec-out screencap\` a un fichero: el \`cwd\` es la raíz del
|
|
40
40
|
proyecto del usuario y ahí no se escribe.`);
|
|
41
41
|
process.exit(0);
|
|
@@ -58,7 +58,21 @@ const serie = opcion("serie");
|
|
|
58
58
|
// `basename` y no la cadena tal cual: el nombre lo elige el modelo, y un `../` aquí escribiría
|
|
59
59
|
// fuera de la carpeta de la sesión. Misma regla que `esRutaDeArtefacto` en el host.
|
|
60
60
|
const pedido = opcion("nombre");
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* **La EXTENSIÓN se pone si falta, y no es cosmética.**
|
|
63
|
+
*
|
|
64
|
+
* Medido en una sesión: el agente pidió `--nombre arranque_error` y el fichero quedó sin
|
|
65
|
+
* extensión. Lo que hizo después es el coste: **tres comandos más** —`file` para averiguar
|
|
66
|
+
* qué había bajado, `sips` para sus dimensiones y un `python3 -c "from PIL import Image…"`
|
|
67
|
+
* para intentar abrirlo—. Un artefacto sin extensión tampoco se sabe pintar: `mimeDeArtefacto`
|
|
68
|
+
* decide por la extensión y sin ella no afirma nada, así que la tarjeta del chat no lo enseña
|
|
69
|
+
* y el revisor visual no lo acepta.
|
|
70
|
+
*
|
|
71
|
+
* Se AÑADE en vez de rechazar: lo que el modelo quería está claro y un «no» aquí cuesta otro
|
|
72
|
+
* viaje para lo mismo.
|
|
73
|
+
*/
|
|
74
|
+
const conExtension = (n) => (/\.[A-Za-z0-9]{2,4}$/.test(n) ? n : `${n}.png`);
|
|
75
|
+
const nombre = pedido === undefined ? `captura-${Date.now()}.png` : conExtension(basename(pedido));
|
|
62
76
|
if (nombre === "" || nombre === "." || nombre === "..") {
|
|
63
77
|
console.error("ese nombre no vale para un fichero.");
|
|
64
78
|
process.exit(1);
|
|
@@ -82,6 +96,80 @@ if (bytes.length === 0) {
|
|
|
82
96
|
process.exit(1);
|
|
83
97
|
}
|
|
84
98
|
|
|
99
|
+
/**
|
|
100
|
+
* **Si XOne ha reventado, el LOG basta y la captura sobra.**
|
|
101
|
+
*
|
|
102
|
+
* Visto tres veces en sesiones reales: la app no arranca, y quien diagnostica fotografía el
|
|
103
|
+
* diálogo de error. La foto es legítima —el diálogo ES el error— pero **lo mismo está en el
|
|
104
|
+
* log en texto**, con fichero, línea, columna y la línea culpable, y cuesta unas 50 fichas
|
|
105
|
+
* contra las **3.375** que cuesta una captura de 1080×2400 en el revisor visual. Setenta veces
|
|
106
|
+
* más por saber lo mismo.
|
|
107
|
+
*
|
|
108
|
+
* Así que no se captura: se enseña la excepción y se dice por qué. Con `--igualmente` se
|
|
109
|
+
* captura de todos modos, que para una comprobación VISUAL —algo cortado, algo tapado— sigue
|
|
110
|
+
* siendo la herramienta buena aunque haya una excepción por ahí.
|
|
111
|
+
*
|
|
112
|
+
* Se mira acotado al PROCESO (`--pid`) y no a las últimas N líneas del aparato: 600 líneas de
|
|
113
|
+
* todo son un par de minutos aquí, y la excepción se sale de esa ventana — el mismo fallo que
|
|
114
|
+
* tenía `xone-desplegar-android`.
|
|
115
|
+
*/
|
|
116
|
+
if (!args.includes("--igualmente")) {
|
|
117
|
+
let roto;
|
|
118
|
+
try {
|
|
119
|
+
const enElAparato = (...partes) =>
|
|
120
|
+
execFileSync("adb", [...(serie ? ["-s", serie] : []), ...partes], {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
123
|
+
});
|
|
124
|
+
const pid = enElAparato("shell", "pidof", "com.xone.android.framework").trim().split(/\s+/)[0];
|
|
125
|
+
if (pid) {
|
|
126
|
+
const log = enElAparato("logcat", "-d", "--pid", pid, "-t", "600");
|
|
127
|
+
const filas = log.split("\n");
|
|
128
|
+
const i = filas.findIndex((l) => l.includes("XOneJavascriptException"));
|
|
129
|
+
if (i >= 0) {
|
|
130
|
+
roto = filas
|
|
131
|
+
.slice(i, i + 12)
|
|
132
|
+
.map((l) => l.replace(/^.*?\bSystem\.err:\s?/, "").trim())
|
|
133
|
+
.filter((l) => l !== "" && !l.startsWith("at "))
|
|
134
|
+
.slice(0, 6)
|
|
135
|
+
.join("\n");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Sin log no hay nada que objetar: se captura.
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* **Y si NO hay error, se dice también.** Es la otra mitad de la misma regla: si el log no
|
|
143
|
+
* da excepción y la app contesta en qué colección está, no hay nada que diagnosticar en una
|
|
144
|
+
* foto — y capturar «por si acaso» es el caso que más se repite. No se BLOQUEA, porque
|
|
145
|
+
* comprobar algo VISUAL sobre una app sana es legítimo y es para lo que la captura sirve;
|
|
146
|
+
* lo que se hace es decirle a quien la pidió lo que ya se sabe, para que no la pida por
|
|
147
|
+
* costumbre.
|
|
148
|
+
*/
|
|
149
|
+
if (roto === undefined) {
|
|
150
|
+
try {
|
|
151
|
+
const cliente = join(import.meta.dirname, "xone-hotswap");
|
|
152
|
+
const donde = execFileSync("node", [cliente, "screen"], { encoding: "utf8" }).trim();
|
|
153
|
+
const m = /\\"collection\\":\\"([^\\"]+)/.exec(donde);
|
|
154
|
+
if (m) {
|
|
155
|
+
console.log(`(el log no da error y la app responde en «${m[1]}»: si buscabas un fallo,`);
|
|
156
|
+
console.log(" no lo hay. Capturo igual, que para lo VISUAL es la herramienta buena.)");
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Sin canal no se puede decir nada del estado: se captura y ya.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (roto !== undefined) {
|
|
163
|
+
console.log("NO capturo: XOne reventó al cargar y eso ya está en el log, en texto:");
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(roto);
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log("Ahí tienes fichero, línea y columna. Una captura del diálogo diría lo mismo");
|
|
168
|
+
console.log("y cuesta ~3.375 fichas. Si de verdad quieres la imagen: --igualmente.");
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
85
173
|
const destino = join(ARTEFACTOS, nombre);
|
|
86
174
|
try {
|
|
87
175
|
writeFileSync(destino, bytes);
|
|
@@ -236,6 +236,26 @@ function concederPermisos() {
|
|
|
236
236
|
console.log(" no se pudo leer qué permisos pide; sigo");
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* **El de dibujar sobre otras apps NO se concede con `pm grant`, y sin él el arranque es una
|
|
241
|
+
* carrera contra el foco.**
|
|
242
|
+
*
|
|
243
|
+
* `SYSTEM_ALERT_WINDOW` es un appop, no un permiso de ejecución, así que el bucle de abajo lo
|
|
244
|
+
* intenta y falla en silencio con los demás. Y lo que concede no es cosmético: Android 10 y
|
|
245
|
+
* superiores BLOQUEAN que una app en segundo plano arranque una actividad, y ese permiso es la
|
|
246
|
+
* exención que reconocen. El framework se va a segundo plano con solo apagarse la pantalla, o
|
|
247
|
+
* en cuanto se cierra la app — así que sin esto `launchApplication` contesta
|
|
248
|
+
* «Cannot launch app while the framework is in the background» y el despliegue parece que
|
|
249
|
+
* funcionó sin que arrancara nada. Es buena parte del síntoma que teníamos apuntado como
|
|
250
|
+
* «`result:true` significa aceptado, no arrancó».
|
|
251
|
+
*
|
|
252
|
+
* Idempotente, y su fallo no para nada: en un aparato donde no aplique, se sigue.
|
|
253
|
+
*/
|
|
254
|
+
try {
|
|
255
|
+
adbCallado("shell", "appops", "set", PAQUETE, "SYSTEM_ALERT_WINDOW", "allow");
|
|
256
|
+
} catch {
|
|
257
|
+
// Aparato que no lo admite. El despliegue sigue; lo que se pierde es la garantía de foco.
|
|
258
|
+
}
|
|
239
259
|
let dados = 0;
|
|
240
260
|
for (const permiso of declaradas) {
|
|
241
261
|
try {
|
|
@@ -260,7 +280,24 @@ function concederPermisos() {
|
|
|
260
280
|
function excepcionDeXOne() {
|
|
261
281
|
let log;
|
|
262
282
|
try {
|
|
263
|
-
|
|
283
|
+
/**
|
|
284
|
+
* **Acotado al PROCESO, no a las últimas N líneas del aparato.** Era `-t 600` a secas, y
|
|
285
|
+
* eso no es «lo último de la app»: son 600 líneas de TODO —Chromium, el sistema, lo que
|
|
286
|
+
* haya—, que en este emulador son un par de minutos. La excepción de XOne se salía de esa
|
|
287
|
+
* ventana enseguida, el despliegue no la veía y no contaba nada.
|
|
288
|
+
*
|
|
289
|
+
* Y lo que pasa después es el motivo de arreglarlo: sin el texto, a quien diagnostica solo
|
|
290
|
+
* le queda **fotografiar el diálogo de error** —visto en una sesión real—, que cuesta 3.375
|
|
291
|
+
* tokens y dice exactamente lo mismo que estas seis líneas.
|
|
292
|
+
*
|
|
293
|
+
* Con `--pid`, 600 líneas son 600 líneas DE LA APP. Es como ya leía `xone-log-android`;
|
|
294
|
+
* que dos sitios miraran el mismo log de forma distinta era la razón de que uno lo
|
|
295
|
+
* encontrara y el otro no.
|
|
296
|
+
*/
|
|
297
|
+
const pid = adbCallado("shell", "pidof", PAQUETE).trim().split(/\s+/)[0];
|
|
298
|
+
log = pid
|
|
299
|
+
? adbCallado("logcat", "-d", "--pid", pid, "-t", "600")
|
|
300
|
+
: adbCallado("logcat", "-d", "-t", "600");
|
|
264
301
|
} catch {
|
|
265
302
|
return undefined;
|
|
266
303
|
}
|
|
@@ -340,8 +377,27 @@ function exigirAparato() {
|
|
|
340
377
|
|
|
341
378
|
console.log("5/5 esperando al canal, lanzando y comprobando que está VIVA");
|
|
342
379
|
const cliente = join(import.meta.dirname, "xone-hotswap");
|
|
343
|
-
|
|
344
|
-
|
|
380
|
+
/**
|
|
381
|
+
* **En `clave=valor`, que es lo único que el cliente entiende.**
|
|
382
|
+
*
|
|
383
|
+
* Esto mandaba `JSON.stringify(comando)` y se rompió ENTERO el día que el cliente dejó de
|
|
384
|
+
* aceptar JSON: el objeto se metía como NOMBRE del comando, el aparato contestaba
|
|
385
|
+
* `Unknown command: {…}` y el despliegue no lanzaba nada. Lanzar la app, pedir el árbol y
|
|
386
|
+
* capturar —las tres cosas que hace este paso— pasaban por aquí, así que se cayeron las
|
|
387
|
+
* tres a la vez, y en silencio: el script reintenta diez veces y acaba diciendo que el
|
|
388
|
+
* canal no levantó, que manda a mirar el puerto y el paquete en vez de al cliente.
|
|
389
|
+
*
|
|
390
|
+
* La conversión es la misma que hace el cliente al revés: cada campo que no es `command`
|
|
391
|
+
* sale como `clave=valor`, y un objeto o un array se serializan porque el tipado del
|
|
392
|
+
* cliente los reconoce por la forma.
|
|
393
|
+
*/
|
|
394
|
+
const mandar = (comando) => {
|
|
395
|
+
const { command, ...resto } = comando;
|
|
396
|
+
const pares = Object.entries(resto).map(([k, v]) =>
|
|
397
|
+
`${k}=${typeof v === "object" && v !== null ? JSON.stringify(v) : String(v)}`,
|
|
398
|
+
);
|
|
399
|
+
return execFileSync("node", [cliente, command, ...pares], { encoding: "utf8" }).trim();
|
|
400
|
+
};
|
|
345
401
|
const esperar = (segundos) => execFileSync("sleep", [String(segundos)]);
|
|
346
402
|
|
|
347
403
|
/**
|
|
@@ -380,18 +436,37 @@ function exigirAparato() {
|
|
|
380
436
|
* hay que volver a preguntar. Tres órdenes encadenadas por el modelo son tres viajes;
|
|
381
437
|
* ésta es una.
|
|
382
438
|
*/
|
|
383
|
-
|
|
439
|
+
/**
|
|
440
|
+
* **El log ANTES que la captura, y por un fallo visto en vivo.**
|
|
441
|
+
*
|
|
442
|
+
* El orden era al revés y producía una foto que engaña: la app contesta el árbol, se
|
|
443
|
+
* cae mientras se pide la captura, y lo que sale fotografiado es la **pantalla del
|
|
444
|
+
* propio servidor hotswap** —«Applications / MyAllXOne / Explore / Delete», con
|
|
445
|
+
* «Awaiting connection…» abajo—. Eso no es la app, y quien lo mire después no tiene
|
|
446
|
+
* forma de saberlo: parece una app en blanco. Ocurrió con un `--captura` real y el
|
|
447
|
+
* usuario lo cazó mirando el artefacto.
|
|
448
|
+
*
|
|
449
|
+
* Cuando XOne ha reventado al cargar, la respuesta entera está en el LOG y la captura
|
|
450
|
+
* solo añade ruido —y 3.375 tokens si alguien se la enseña al revisor visual—. Así que
|
|
451
|
+
* primero se mira el log; si reventó, se cuenta y NO se captura.
|
|
452
|
+
*/
|
|
453
|
+
const roto = excepcionDeXOne();
|
|
454
|
+
if (args.includes("--captura") && roto === undefined) {
|
|
384
455
|
try {
|
|
385
456
|
console.log(mandar({ command: "getScreenshot" }));
|
|
386
457
|
} catch {
|
|
387
458
|
console.error("la app arrancó pero la captura falló; el árbol de arriba sí es bueno.");
|
|
388
459
|
}
|
|
460
|
+
} else if (args.includes("--captura")) {
|
|
461
|
+
console.error(
|
|
462
|
+
"NO capturo: XOne reventó al cargar y lo que saldría en la foto es la pantalla del " +
|
|
463
|
+
"host, no tu app. Lo que pasó está en el log de aquí abajo.",
|
|
464
|
+
);
|
|
389
465
|
}
|
|
390
466
|
// **Estar VIVA no es estar BIEN.** Una app que revienta al cargar la configuración se
|
|
391
467
|
// queda con su diálogo de error delante, y el canal contesta igual: el árbol llega, la
|
|
392
468
|
// captura sale, y todo parece haber ido bien. Quien lo cuenta es el log, y no pedirlo
|
|
393
469
|
// aquí obliga a que alguien se acuerde de hacerlo — que es justo lo que no pasa.
|
|
394
|
-
const roto = excepcionDeXOne();
|
|
395
470
|
if (roto !== undefined) {
|
|
396
471
|
console.error("");
|
|
397
472
|
console.error("La app ARRANCÓ pero XOne lanzó una excepción al cargar:");
|
|
@@ -95,9 +95,18 @@ if (pedidos.length === 0 || pedidos.includes("--help") || pedidos.includes("-h")
|
|
|
95
95
|
// un viaje y deja al modelo adivinando argumentos.
|
|
96
96
|
console.error(`xone-hotswap — manda comandos al servidor hotswap de la app (WebSocket).
|
|
97
97
|
|
|
98
|
-
xone-hotswap
|
|
99
|
-
xone-hotswap
|
|
100
|
-
xone-hotswap
|
|
98
|
+
xone-hotswap click name=MAP_BT
|
|
99
|
+
xone-hotswap waitForElement name=BTN_OK timeout=5000 -- click name=BTN_OK
|
|
100
|
+
xone-hotswap screen en qué pantalla estás (barato)
|
|
101
|
+
xone-hotswap elements el árbol de controles (va a $XONECODE_HOTSWAP)
|
|
102
|
+
xone-hotswap shot la captura va a $XONECODE_ARTEFACTOS
|
|
103
|
+
xone-hotswap shot name=MAP_ACEPTAR scale=0.5 quality=40 recortada a un control
|
|
104
|
+
xone-hotswap sql MiApp "SELECT * FROM CLIENTES LIMIT 20"
|
|
105
|
+
|
|
106
|
+
Cada comando es "<nombre> clave=valor ..."; con -- se encadenan varios por la MISMA
|
|
107
|
+
conexion, que es lo que hay que hacer (el servidor admite una sola y cierra la anterior).
|
|
108
|
+
Los valores se tipan solos: número, true/false y {...}/[...] como JSON.
|
|
109
|
+
Con --dry-run se imprime lo que se enviaría, sin conectar.
|
|
101
110
|
|
|
102
111
|
Antes, en Android, hace falta el túnel: adb forward tcp:8443 tcp:8443
|
|
103
112
|
Una respuesta larga no se imprime: una IMAGEN va a $XONECODE_ARTEFACTOS (y sale en el chat),
|
|
@@ -107,13 +116,124 @@ El catálogo entero de comandos está en el SKILL.md de xone-hotswap.`);
|
|
|
107
116
|
process.exit(pedidos.length === 0 ? 1 : 0);
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
/**
|
|
120
|
+
* De `clave=valor` a los objetos del protocolo, con `--` separando comandos.
|
|
121
|
+
*
|
|
122
|
+
* **POR QUÉ SE FUE EL JSON.** Antes cada comando era un objeto JSON entre comillas
|
|
123
|
+
* (`'{"command":"click","name":"BTN_OK"}'`). Medido sobre cinco comandos típicos, esta
|
|
124
|
+
* sintaxis los escribe en un **38 % menos de caracteres** —un `click` pasa de 37 a 17— y
|
|
125
|
+
* quita el escapado, que es donde un modelo se equivoca y gasta un viaje en volver a
|
|
126
|
+
* intentarlo. No se aceptan las dos: una capa de compatibilidad es deuda desde el primer día,
|
|
127
|
+
* y el JSON solo se escribía dentro de esta misma skill.
|
|
128
|
+
*
|
|
129
|
+
* Los valores se TIPAN solos —número, `true`/`false`, y `{…}`/`[…]` como JSON— porque el
|
|
130
|
+
* protocolo distingue `timeout=5000` de `"5000"` y `where={"ID":"7"}` es un objeto. Lo que no
|
|
131
|
+
* encaje en ninguno se queda como cadena, que es lo que casi todo es.
|
|
132
|
+
*/
|
|
133
|
+
function valorTipado(bruto) {
|
|
134
|
+
if (bruto === "true") return true;
|
|
135
|
+
if (bruto === "false") return false;
|
|
136
|
+
if (bruto !== "" && !Number.isNaN(Number(bruto))) return Number(bruto);
|
|
137
|
+
if (/^[{[]/.test(bruto)) {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(bruto);
|
|
140
|
+
} catch {
|
|
141
|
+
salir(`«${bruto.slice(0, 60)}» empieza como JSON pero no lo es`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return bruto;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Los ATAJOS, que existen para lo que si no cuesta dos viajes o un base64 a mano.
|
|
149
|
+
*
|
|
150
|
+
* Cada uno es exactamente un comando del protocolo con sus campos ya puestos: no añaden
|
|
151
|
+
* capacidades, quitan la parte que se escribe mal. `shot` es el que más se nota — decodifica
|
|
152
|
+
* el JPEG y lo deja donde el harness lo anuncia.
|
|
153
|
+
*/
|
|
154
|
+
const ATAJOS = {
|
|
155
|
+
elements: (resto) => ({ command: "getAllElements", format: resto[0] ?? "xone" }),
|
|
156
|
+
screen: () => ({ command: "getCurrentScreen" }),
|
|
157
|
+
shot: (resto) => aplicarPares({ command: "getScreenshot" }, resto),
|
|
158
|
+
tables: (resto) => ({ command: "getDatabaseTableNames", appName: resto[0] }),
|
|
159
|
+
sql: (resto) => ({ command: "runSql", appName: resto[0], sql: resto.slice(1).join(" ") }),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
function aplicarPares(json, pares) {
|
|
163
|
+
for (const par of pares) {
|
|
164
|
+
const corte = par.indexOf("=");
|
|
165
|
+
if (corte <= 0) salir(`«${par}» no es clave=valor`);
|
|
166
|
+
json[par.slice(0, corte)] = valorTipado(par.slice(corte + 1));
|
|
167
|
+
}
|
|
168
|
+
return json;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* `--dry-run`: imprime lo que se enviaría y no conecta.
|
|
173
|
+
*
|
|
174
|
+
* No es comodidad: es lo que hace comprobable el tipado de los valores sin un aparato
|
|
175
|
+
* delante —`timeout=5000` tiene que salir como número y `where={...}` como objeto, y si salen
|
|
176
|
+
* como cadena el servidor los rechaza sin decir por qué—. También deja probar un comando
|
|
177
|
+
* escrito a mano antes de gastarle un viaje al canal.
|
|
178
|
+
*/
|
|
179
|
+
const seco = pedidos.includes("--dry-run");
|
|
180
|
+
const utiles = pedidos.filter((a) => a !== "--dry-run");
|
|
181
|
+
|
|
110
182
|
const comandos = [];
|
|
111
|
-
for (const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
183
|
+
for (const trozo of partirPorSeparador(utiles)) {
|
|
184
|
+
const [cabeza, ...resto] = trozo;
|
|
185
|
+
/**
|
|
186
|
+
* **El JSON viejo se RECHAZA con la forma buena escrita, nunca se acepta a medias.**
|
|
187
|
+
*
|
|
188
|
+
* Al quitar el JSON, una llamada con la sintaxis de antes no fallaba: metía el objeto
|
|
189
|
+
* ENTERO como nombre del comando y mandaba
|
|
190
|
+
* `{"command":"{\"command\":\"getAllElements\",…}"}`. El aparato contestaba
|
|
191
|
+
* `Unknown command: {"command":"getAllElements","format":"xone"}` —visto en pantalla— y
|
|
192
|
+
* quien lo leyera no tenía forma de saber que el problema era la sintaxis del cliente y no
|
|
193
|
+
* el comando: parecía que el framework no conocía `getAllElements`. Un fallo que miente
|
|
194
|
+
* sobre dónde está invita a reintentar, y reintentar es lo que cuesta.
|
|
195
|
+
*
|
|
196
|
+
* No se ACEPTA —eso era la capa de compatibilidad que se decidió no tener— pero tampoco se
|
|
197
|
+
* traga: se dice qué escribir, que es el patrón de `porQueNo` y el de `xone_navegacion`
|
|
198
|
+
* cuando no sabe contestar.
|
|
199
|
+
*/
|
|
200
|
+
if (cabeza.startsWith("{")) {
|
|
201
|
+
let comoJson;
|
|
202
|
+
try {
|
|
203
|
+
comoJson = JSON.parse(cabeza);
|
|
204
|
+
} catch {
|
|
205
|
+
comoJson = undefined;
|
|
206
|
+
}
|
|
207
|
+
const nombre = typeof comoJson?.command === "string" ? comoJson.command : undefined;
|
|
208
|
+
const pares = nombre
|
|
209
|
+
? Object.entries(comoJson)
|
|
210
|
+
.filter(([k]) => k !== "command")
|
|
211
|
+
.map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : String(v)}`)
|
|
212
|
+
.join(" ")
|
|
213
|
+
: "";
|
|
214
|
+
salir(
|
|
215
|
+
`este cliente ya NO recibe JSON. Escríbelo como "<comando> clave=valor".` +
|
|
216
|
+
(nombre ? `\n Lo que querías es: xone-hotswap ${nombre}${pares ? ` ${pares}` : ""}` : "") +
|
|
217
|
+
`\n Y con -- se encadenan varios por la misma conexión. Mira --help.`,
|
|
218
|
+
);
|
|
116
219
|
}
|
|
220
|
+
const atajo = ATAJOS[cabeza];
|
|
221
|
+
comandos.push(atajo ? atajo(resto) : aplicarPares({ command: cabeza }, resto));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (seco) {
|
|
225
|
+
for (const c of comandos) console.log(JSON.stringify(c));
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Parte `a x=1 -- b y=2` en `[["a","x=1"],["b","y=2"]]`. Un `--` suelto no abre comando. */
|
|
230
|
+
function partirPorSeparador(args) {
|
|
231
|
+
const trozos = [[]];
|
|
232
|
+
for (const a of args) {
|
|
233
|
+
if (a === "--") trozos.push([]);
|
|
234
|
+
else trozos[trozos.length - 1].push(a);
|
|
235
|
+
}
|
|
236
|
+
return trozos.filter((t) => t.length > 0);
|
|
117
237
|
}
|
|
118
238
|
|
|
119
239
|
|
|
@@ -230,6 +350,29 @@ ws.onmessage = (ev) => {
|
|
|
230
350
|
siguiente();
|
|
231
351
|
return;
|
|
232
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* **Las tramas PUSH no son la respuesta de nadie, y tragárselas DESINCRONIZA el canal.**
|
|
355
|
+
*
|
|
356
|
+
* El protocolo **no correla peticiones con respuestas**: no hay identificador de comando,
|
|
357
|
+
* así que se emparejan por ORDEN —se manda de uno en uno y se toma la siguiente trama que
|
|
358
|
+
* no sea un push—. Eso lo documenta el cliente del equipo del framework
|
|
359
|
+
* (`pack_ia/scripts/hotswap-cli.mjs`), y aquí solo se filtraba el saludo.
|
|
360
|
+
*
|
|
361
|
+
* La consecuencia de que falten las otras dos no es perder una línea: es que **el push se
|
|
362
|
+
* cuenta como la respuesta del comando pendiente y TODO lo que viene después va
|
|
363
|
+
* desfasado** — cada comando recibe la contestación del anterior. Y no falla siempre, solo
|
|
364
|
+
* cuando llega un push en medio, que es lo que lo hace difícil de ver.
|
|
365
|
+
*
|
|
366
|
+
* Las dos que faltaban:
|
|
367
|
+
* - `logger: true` — una línea de log que el aparato empuja. Las produce cualquier cosa
|
|
368
|
+
* que encienda el registro, o sea justo lo que se hace para diagnosticar.
|
|
369
|
+
* - `state` — el progreso de una descarga.
|
|
370
|
+
*/
|
|
371
|
+
if (mensaje.logger === true || mensaje.state !== undefined || mensaje.command === "server_hello") {
|
|
372
|
+
// No se imprime: quien pidió un comando no pidió esto, y meterlo en la salida es meterlo
|
|
373
|
+
// en el contexto del agente. Lo que se conserva es el ORDEN.
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
233
376
|
console.log(JSON.stringify({ comando: pendiente?.command, ...sinBinarios(mensaje) }));
|
|
234
377
|
pendiente = undefined;
|
|
235
378
|
siguiente();
|
|
@@ -66,14 +66,22 @@ Las tablas deben tener el prefijo `gen_` y estar en minúsculas (ej: `gen_empres
|
|
|
66
66
|
|
|
67
67
|
| Tipo XOne | SQLite | Descripción |
|
|
68
68
|
|-----------|--------|-------------|
|
|
69
|
-
| T, L, TL,
|
|
70
|
-
|
|
|
71
|
-
|
|
|
69
|
+
| T, L, TL, THTML, X | TEXT | Texto |
|
|
70
|
+
| TN, TN2-TN6 | TEXT | Número almacenado como texto |
|
|
71
|
+
| D, DT, TT | TEXT | Fechas y horas |
|
|
72
|
+
| IMG, PH, VD, DR, AT, WEB | TEXT | Rutas a fichero o URL |
|
|
72
73
|
| N | INTEGER | Entero |
|
|
73
|
-
| NC
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
74
|
+
| NC | INTEGER | Booleano (0/1) |
|
|
75
|
+
| N2-N6 | REAL | Decimales |
|
|
76
|
+
| B, Z, O | NO SE CREA | No persisten |
|
|
77
|
+
|
|
78
|
+
> Los tipos de esta tabla son los 30 de la **lista autoritativa** de
|
|
79
|
+
> `xone-help-docs` (`topics/02b-xml-prop-tipos.md`). Ojo con los que NO existen y
|
|
80
|
+
> parecen tipos: **no hay `C` (combo), `M` (mapa), `A` (autocomplete), `F`, `S`, `P`,
|
|
81
|
+
> `R` ni `N1`**. Un combo es `type="T"` (o `"N"`) con `mapcol` y `mapfld`; un mapa es
|
|
82
|
+
> `type="Z" viewmode="mapview"`; los sliders y las barras de progreso son
|
|
83
|
+
> **viewmodes** sobre `T`, `N` o `Z`. Si el modelo trae uno de esos nombres, el
|
|
84
|
+
> arreglo es cambiar el tipo en el `.xne`, no crear una columna para él.
|
|
77
85
|
|
|
78
86
|
### 11.4 Tarea 2: Insertar Datos Iniciales
|
|
79
87
|
|