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.
Files changed (47) hide show
  1. package/apps/web/dist/assets/{index-LdUr7EoT.js → index-DFnjznH1.js} +34 -34
  2. package/apps/web/dist/assets/{php-Cg-eWGpQ.js → php-CX4yaeUn.js} +1 -1
  3. package/apps/web/dist/assets/{ruby-EcV7Hon3.js → ruby-Dklcf7v6.js} +1 -1
  4. package/apps/web/dist/index.html +1 -1
  5. package/dist/agent/grafo/escriturasEnSerie.js +57 -0
  6. package/dist/agent/grafo/escriturasEnSerie.js.map +1 -0
  7. package/dist/agent/grafo/proyecto.js +84 -3
  8. package/dist/agent/grafo/proyecto.js.map +1 -1
  9. package/dist/agent/grafo/traerDeLaMaquina.js +117 -0
  10. package/dist/agent/grafo/traerDeLaMaquina.js.map +1 -0
  11. package/dist/agent/grafo/validacionXone.js +200 -0
  12. package/dist/agent/grafo/validacionXone.js.map +1 -0
  13. package/dist/agent/grafo/xoneAgent.js +33 -1
  14. package/dist/agent/grafo/xoneAgent.js.map +1 -1
  15. package/dist/agent/subagentes/agentesEnDisco.js +34 -6
  16. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  17. package/dist/agent/trazaDeErroresEnDisco.js +45 -0
  18. package/dist/agent/trazaDeErroresEnDisco.js.map +1 -0
  19. package/dist/agent/turno/diagnosticoDeTools.js +10 -1
  20. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -1
  21. package/dist/agent/turno/informeDeTraza.js +129 -6
  22. package/dist/agent/turno/informeDeTraza.js.map +1 -1
  23. package/dist/agent/turno/puente.js +89 -5
  24. package/dist/agent/turno/puente.js.map +1 -1
  25. package/dist/agent/turno/resumenDeTool.js +28 -0
  26. package/dist/agent/turno/resumenDeTool.js.map +1 -1
  27. package/dist/agent/turno/turnoReal.js +20 -2
  28. package/dist/agent/turno/turnoReal.js.map +1 -1
  29. package/dist/core/serieDeEscrituras.js +63 -0
  30. package/dist/core/serieDeEscrituras.js.map +1 -0
  31. package/dist/core/traerDeLaMaquina.js +80 -0
  32. package/dist/core/traerDeLaMaquina.js.map +1 -0
  33. package/dist/core/trazaDeErrores.js +108 -0
  34. package/dist/core/trazaDeErrores.js.map +1 -0
  35. package/dist/core/validacionDeEscritura.js +105 -0
  36. package/dist/core/validacionDeEscritura.js.map +1 -0
  37. package/dist/web/servidor/consolaWeb.js +6 -0
  38. package/dist/web/servidor/consolaWeb.js.map +1 -1
  39. package/package.json +3 -2
  40. package/skills/xone-hotswap/SKILL.md +76 -8
  41. package/skills/xone-hotswap/references/comandos.md +33 -0
  42. package/skills/xone-hotswap/references/conexion-y-despliegue.md +61 -1
  43. package/skills/xone-hotswap/scripts/xone-arrancar-ios +1 -1
  44. package/skills/xone-hotswap/scripts/xone-captura-android +91 -3
  45. package/skills/xone-hotswap/scripts/xone-desplegar-android +80 -5
  46. package/skills/xone-hotswap/scripts/xone-hotswap +151 -8
  47. 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 '{"command":"getScreenshot"}'`
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 '{"command":"getScreenshot"}'\`, que captura lo que la app pinta.
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
- const nombre = pedido === undefined ? `captura-${Date.now()}.png` : basename(pedido);
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
- log = adbCallado("logcat", "-d", "-t", "600");
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
- const mandar = (comando) =>
344
- execFileSync("node", [cliente, JSON.stringify(comando)], { encoding: "utf8" }).trim();
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
- if (args.includes("--captura")) {
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 '{"command":"getAllElements","format":"xone"}'
99
- xone-hotswap '{"command":"getScreenshot"}' la captura va a $XONECODE_ARTEFACTOS
100
- xone-hotswap '{"command":"click","name":"MAP_BT"}' varios comandos, en orden, si pasas varios
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 arg of pedidos) {
112
- try {
113
- comandos.push(JSON.parse(arg));
114
- } catch {
115
- salir(`no es JSON válido: ${arg.slice(0, 80)}`);
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, X, PH | TEXT | Texto |
70
- | D, DT | TEXT | Fechas |
71
- | IMG, VD, F, M | TEXT | Rutas |
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, R | INTEGER | Boolean |
74
- | N1-N6 | REAL | Decimales |
75
- | C | TEXT | Combo |
76
- | B, Z, L, S, P, O | NO SE CREA | UI |
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