xonecode 0.17.0 → 0.17.2

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 (27) hide show
  1. package/apps/web/dist/assets/{index-CnskHXof.js → index-BRVuyPBf.js} +56 -56
  2. package/apps/web/dist/assets/{php-8TR3jUGL.js → php-B5LpMCn-.js} +1 -1
  3. package/apps/web/dist/assets/{ruby-IynpAZS-.js → ruby-Clm-qfwM.js} +1 -1
  4. package/apps/web/dist/index.html +1 -1
  5. package/dist/agent/config/settingsEnDisco.js +7 -3
  6. package/dist/agent/config/settingsEnDisco.js.map +1 -1
  7. package/dist/agent/grafo/validacionXone.js +8 -1
  8. package/dist/agent/grafo/validacionXone.js.map +1 -1
  9. package/dist/agent/subagentes/agentesEnDisco.js +9 -1
  10. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  11. package/dist/core/validacionDeEscritura.js +36 -0
  12. package/dist/core/validacionDeEscritura.js.map +1 -1
  13. package/package.json +1 -1
  14. package/skills/artifacts-builder/reference/estilo.md +3 -6
  15. package/skills/xone-development/SKILL.md +1 -5
  16. package/skills/xone-development/references/fundamentos/errores-comunes.md +0 -3
  17. package/skills/xone-hotswap/SKILL.md +8 -0
  18. package/skills/xone-hotswap/lib/dispositivo.mjs +14 -1
  19. package/skills/xone-hotswap/references/comandos.md +14 -0
  20. package/skills/xone-hotswap/scripts/xone-arrancar-android +3 -2
  21. package/skills/xone-hotswap/scripts/xone-arrancar-ios +2 -2
  22. package/skills/xone-hotswap/scripts/xone-captura-android +4 -4
  23. package/skills/xone-hotswap/scripts/xone-desplegar-android +6 -6
  24. package/skills/xone-hotswap/scripts/xone-log-android +2 -2
  25. package/skills/xone-hotswap/scripts/xone-recargar-android +269 -0
  26. package/skills/xone-hotswap/scripts/xone-reiniciar-android +6 -6
  27. package/skills/xone-project-generator/SKILL.md +1 -1
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Llevar al Android los ficheros que CAMBIARON, sin desplegar el proyecto entero.
4
+ *
5
+ * POR QUÉ EXISTE. `xone-desplegar-android` empaqueta, sube, concede permisos y reinicia el
6
+ * framework: es la cadena entera y está bien para el primer arranque. Para «cambié un botón» es
7
+ * desproporcionado. Este script compara los ficheros del proyecto con los del aparato
8
+ * (`getFileChecksum`), sube SOLO los que difieren (`uploadFile`) y los aplica por la vía más
9
+ * barata que se MIDIÓ que funciona, contra el framework 5.0.5.5dev:
10
+ *
11
+ * colección (.xne con <coll>) → `loadCollection`: se repinta en el acto, PERO solo en memoria.
12
+ * Por eso también se sube a disco: sin eso, relanzar la app la
13
+ * devuelve a la versión anterior.
14
+ * JavaScript (.js) → `loadIncludeFile` con compile=true. Subir el fichero NO basta,
15
+ * y `loadCollection` tampoco lo recarga.
16
+ * CSS, .ini, app.xml, mappings → relanzar la app (`launchApplication`). Medido: `setCssAttribute`,
17
+ * `relayout`, `refresh` y `loadCollection` contestan `result:true`
18
+ * y el color NO cambia; `loadIncludeFile` no admite css.
19
+ *
20
+ * **`result:true` es «aceptado», no «aplicado».** Lo que dice si funcionó es el árbol de
21
+ * controles, y por eso el script lo pide al acabar. Un atributo que XOne no conoce tampoco da
22
+ * error: comprueba lo que cambiaste, no el código de respuesta.
23
+ *
24
+ * USO (desde la raíz del proyecto, que es el `cwd`):
25
+ * xone-recargar-android compara todo y aplica lo que difiera
26
+ * xone-recargar-android MenuPrincipal.xne solo esos ficheros (sin comparar)
27
+ * xone-recargar-android --app MiApp
28
+ *
29
+ * **Necesita la app YA desplegada y el túnel puesto** (`xone-desplegar-android` la primera vez).
30
+ * Lo que no sabe aplicar —iconos, la base de datos, ficheros nuevos que la app no declara— lo
31
+ * dice y manda a desplegar entero, en vez de fingir.
32
+ *
33
+ * NO usa `adb`: habla solo con el canal hotswap, así que sirve igual en Windows.
34
+ */
35
+ import { execFileSync } from "node:child_process";
36
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
37
+ import { basename, extname, join, relative, sep } from "node:path";
38
+ import { NODE, dormir } from "../lib/dispositivo.mjs";
39
+
40
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
41
+ process.removeAllListeners("warning");
42
+ process.on("warning", (aviso) => {
43
+ if (!String(aviso.message).includes("NODE_TLS_REJECT_UNAUTHORIZED")) console.warn(aviso);
44
+ });
45
+
46
+ /** Lo que se sincroniza. El resto (iconos, `bd/`, `files/`) se dice, no se sube. */
47
+ const EXTENSIONES = new Set([".xne", ".js", ".css", ".ini", ".xml"]);
48
+ /** Carpetas que nunca viajan. */
49
+ const FUERA = [".xonecode", ".git", "node_modules", "bd", "files", "icons"];
50
+ /**
51
+ * Tope de lo que cabe en una línea de órdenes de Windows (32.767 caracteres en total). En otros
52
+ * sistemas el límite es de megas, así que solo se aplica allí.
53
+ */
54
+ const TOPE_BASE64_WINDOWS = 24_000;
55
+
56
+ const args = process.argv.slice(2);
57
+ const opcion = (nombre) => {
58
+ const i = args.indexOf(`--${nombre}`);
59
+ return i >= 0 ? args[i + 1] : undefined;
60
+ };
61
+ const posicionales = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1] === "--app"));
62
+
63
+ if (args.includes("--help") || args.includes("-h")) {
64
+ // Tiene que existir: una orden que no entiende una opción no puede ejecutar nada.
65
+ console.error(`xone-recargar-android — lleva al Android SOLO lo que cambió, sin desplegar entero.
66
+
67
+ xone-recargar-android compara el proyecto con el aparato y aplica lo distinto
68
+ xone-recargar-android A.xne functions.js solo esos ficheros
69
+ xone-recargar-android --app MiApp nombre explícito de la app
70
+
71
+ Colección → recarga en caliente. JavaScript → recarga del include. CSS, app.xml, app.ini y
72
+ mappings → relanza la app. Necesita la app ya desplegada (xone-desplegar-android) y el túnel.`);
73
+ process.exit(0);
74
+ }
75
+
76
+ const cliente = join(import.meta.dirname, "xone-hotswap");
77
+
78
+ /** Un comando del canal, en `clave=valor`. Devuelve el JSON de la respuesta. */
79
+ function mandar(comando, campos = {}) {
80
+ const pares = Object.entries(campos).map(([k, v]) =>
81
+ `${k}=${typeof v === "object" && v !== null ? JSON.stringify(v) : String(v)}`,
82
+ );
83
+ const salida = execFileSync(NODE, [cliente, comando, ...pares], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }).trim();
84
+ try {
85
+ return JSON.parse(salida);
86
+ } catch {
87
+ throw new Error(`el canal no devolvió JSON para ${comando}: ${salida.slice(0, 160)}`);
88
+ }
89
+ }
90
+
91
+ /** Como en el despliegue: `app.ini` (`name=`) y, si no, la carpeta. */
92
+ function nombreDeLaApp() {
93
+ const dicho = opcion("app");
94
+ if (dicho !== undefined) return dicho;
95
+ if (existsSync("app.ini")) {
96
+ const m = /^\s*name\s*=\s*(.+?)\s*$/im.exec(readFileSync("app.ini", "latin1"));
97
+ if (m !== null && m[1] !== "") return m[1];
98
+ }
99
+ return basename(process.cwd());
100
+ }
101
+
102
+ const TABLA_CRC = (() => {
103
+ const t = new Uint32Array(256);
104
+ for (let i = 0; i < 256; i++) {
105
+ let c = i;
106
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
107
+ t[i] = c >>> 0;
108
+ }
109
+ return t;
110
+ })();
111
+ function crc32(buf) {
112
+ let c = 0xffffffff;
113
+ for (let i = 0; i < buf.length; i++) c = TABLA_CRC[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
114
+ return (c ^ 0xffffffff) >>> 0;
115
+ }
116
+
117
+ /** Los ficheros del proyecto, en rutas relativas con `/`. Devuelve también los que no se sincronizan. */
118
+ function ficherosDelProyecto(dentro = process.cwd(), salida = { sincronizables: [], otros: [] }) {
119
+ for (const entrada of readdirSync(dentro, { withFileTypes: true })) {
120
+ const completa = join(dentro, entrada.name);
121
+ const rel = relative(process.cwd(), completa).split(sep).join("/");
122
+ if (FUERA.some((f) => rel === f || rel.startsWith(`${f}/`))) continue;
123
+ if (entrada.isSymbolicLink()) continue;
124
+ if (entrada.isDirectory()) ficherosDelProyecto(completa, salida);
125
+ else if (entrada.isFile()) (EXTENSIONES.has(extname(rel).toLowerCase()) ? salida.sincronizables : salida.otros).push(rel);
126
+ }
127
+ return salida;
128
+ }
129
+
130
+ const app = nombreDeLaApp();
131
+ /** Cómo guarda el framework lo que sube: `app_<nombre en minúsculas>` (medido). */
132
+ const carpetaEnElAparato = `app_${app.toLowerCase()}`;
133
+
134
+ /** ¿Está la app viva? El árbol de controles es la única comprobación que vale. */
135
+ function estaViva() {
136
+ try {
137
+ const r = mandar("getAllElements", { format: "xone" });
138
+ return r.result === true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ if (!estaViva()) {
145
+ console.error(`la app «${app}» no contesta en el canal hotswap.`);
146
+ console.error("Esto solo recarga lo cambiado: la primera vez (o si el aparato se reinició) hay que");
147
+ console.error("desplegar entera con xone-desplegar-android.");
148
+ process.exit(1);
149
+ }
150
+
151
+ const { sincronizables, otros } = ficherosDelProyecto();
152
+ let cambiados;
153
+ const nuevos = new Set();
154
+
155
+ if (posicionales.length > 0) {
156
+ cambiados = posicionales.map((p) => p.replaceAll("\\", "/"));
157
+ const ausentes = cambiados.filter((f) => !existsSync(f));
158
+ if (ausentes.length > 0) {
159
+ console.error(`no existe en el proyecto: ${ausentes.join(", ")}`);
160
+ process.exit(1);
161
+ }
162
+ } else {
163
+ /**
164
+ * Un fichero que el aparato no tiene hace FALLAR la llamada entera (`ENOENT` con su ruta), así
165
+ * que se va quitando de la petición y se cuenta como NUEVO. Es un bucle acotado: cada vuelta
166
+ * saca uno, y con 500 ficheros nuevos son 500 viajes — para eso está el despliegue entero.
167
+ */
168
+ let pedir = [...sincronizables];
169
+ let remotos = {};
170
+ for (let vuelta = 0; pedir.length > 0 && vuelta < 25; vuelta++) {
171
+ const r = mandar("getFileChecksum", { appName: app.toLowerCase(), files: pedir, checksumType: "crc32" });
172
+ if (r.result !== false && typeof r.result === "object") {
173
+ remotos = r.result;
174
+ break;
175
+ }
176
+ const falta = /app_[^/\\]+[/\\](.+?): open failed/.exec(String(r.exceptionMessage ?? ""))?.[1];
177
+ if (falta === undefined || !pedir.includes(falta)) {
178
+ console.error(`no pude comparar con el aparato: ${r.exceptionMessage ?? r.status ?? "sin motivo"}`);
179
+ process.exit(1);
180
+ }
181
+ nuevos.add(falta);
182
+ pedir = pedir.filter((f) => f !== falta);
183
+ }
184
+ if (nuevos.size >= 25) {
185
+ console.error("hay demasiados ficheros nuevos para el aparato: despliega entero con xone-desplegar-android.");
186
+ process.exit(1);
187
+ }
188
+ cambiados = sincronizables.filter(
189
+ (f) => nuevos.has(f) || String(remotos[f]) !== String(crc32(readFileSync(f))),
190
+ );
191
+ }
192
+
193
+ if (cambiados.length === 0) {
194
+ console.log("nada que recargar: los ficheros del proyecto ya son los del aparato.");
195
+ if (otros.length > 0 && posicionales.length === 0) {
196
+ console.log(`(no se comparan ${otros.length} fichero(s) que no son xne/js/css/ini/xml: iconos, bd, files…)`);
197
+ }
198
+ process.exit(0);
199
+ }
200
+
201
+ /** Cómo se aplica cada uno. */
202
+ function clase(rel) {
203
+ const ext = extname(rel).toLowerCase();
204
+ if (ext === ".js") return "js";
205
+ if (ext === ".xne") return /<coll[\s>]/.test(readFileSync(rel, "latin1")) ? "coleccion" : "relanzar";
206
+ return "relanzar";
207
+ }
208
+
209
+ // 1. Subir a disco (siempre): lo que solo está en memoria se pierde al relanzar.
210
+ const subidos = [];
211
+ for (const rel of cambiados) {
212
+ const base64 = readFileSync(rel).toString("base64");
213
+ if (process.platform === "win32" && base64.length > TOPE_BASE64_WINDOWS) {
214
+ console.error(`«${rel}» es demasiado grande para mandarlo por la línea de órdenes de Windows: despliega entero.`);
215
+ process.exit(1);
216
+ }
217
+ const r = mandar("uploadFile", { destinationPath: `${carpetaEnElAparato}/${rel}`, fileData: base64 });
218
+ if (r.result !== true) {
219
+ console.error(`no se pudo subir «${rel}»: ${r.exceptionMessage ?? r.status ?? "sin motivo"}`);
220
+ process.exit(1);
221
+ }
222
+ subidos.push(rel);
223
+ }
224
+
225
+ // 2. Aplicar. Si algo obliga a relanzar, el relanzamiento carga TODO desde disco y sobra el resto.
226
+ const clases = new Map(subidos.map((f) => [f, clase(f)]));
227
+ const hayQueRelanzar = [...clases.values()].includes("relanzar");
228
+ const fallos = [];
229
+
230
+ if (hayQueRelanzar) {
231
+ const r = mandar("launchApplication", { appName: app });
232
+ if (r.result !== true) fallos.push(`launchApplication: ${r.exceptionMessage ?? r.status ?? "sin motivo"}`);
233
+ for (let intento = 0; intento < 12; intento++) {
234
+ dormir(1);
235
+ if (estaViva()) break;
236
+ }
237
+ for (const [f] of clases) console.log(` ${f} → relanzada la app`);
238
+ } else {
239
+ for (const [f, c] of clases) {
240
+ if (c === "coleccion") {
241
+ const r = mandar("loadCollection", { xmlNode: readFileSync(f).toString("base64"), encoding: "UTF-8" });
242
+ if (r.result !== true) fallos.push(`${f}: loadCollection ${r.exceptionMessage ?? r.status ?? "sin motivo"}`);
243
+ else console.log(` ${f} → colección recargada en caliente`);
244
+ } else {
245
+ const r = mandar("loadIncludeFile", { file: f, language: "javascript", encoding: "UTF-8", compile: true });
246
+ if (r.result !== true) fallos.push(`${f}: loadIncludeFile ${r.exceptionMessage ?? r.status ?? "sin motivo"}`);
247
+ else console.log(` ${f} → JavaScript recargado`);
248
+ }
249
+ }
250
+ dormir(1);
251
+ }
252
+
253
+ if (fallos.length > 0) {
254
+ console.error("");
255
+ for (const f of fallos) console.error(`FALLÓ ${f}`);
256
+ console.error("Lo subido está en el aparato; para aplicarlo del todo, xone-desplegar-android.");
257
+ process.exit(1);
258
+ }
259
+
260
+ // 3. Comprobar: el árbol de controles, no el `result:true`.
261
+ if (!estaViva()) {
262
+ console.error("");
263
+ console.error("Se aplicó, pero la app YA NO responde: puede haber reventado al cargar lo nuevo.");
264
+ console.error("Mira el log con xone-log-android.");
265
+ process.exit(1);
266
+ }
267
+ const pantalla = mandar("getCurrentScreen");
268
+ console.log(`\nviva. ${typeof pantalla.status === "string" ? pantalla.status : ""}`);
269
+ console.log("Recuerda: result:true es «aceptado». Comprueba con `xone-hotswap elements` o `shot` que lo cambiado se ve.");
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import { execFileSync } from "node:child_process";
23
23
  import { join } from "node:path";
24
- import { serieAndroid } from "../lib/dispositivo.mjs";
24
+ import { serieAndroid, rutaDeAdb, NODE, dormir } from "../lib/dispositivo.mjs";
25
25
 
26
26
  const PAQUETE = "com.xone.android.framework";
27
27
  const ACTIVIDAD = `${PAQUETE}/com.xone.android.hotswap.activities.SetupActivity`;
@@ -49,11 +49,11 @@ const opcion = (nombre) => {
49
49
  const { serie, porque } = serieAndroid(opcion("serie"));
50
50
  if (serie !== undefined) process.stderr.write(`dispositivo: ${serie} (${porque})\n`);
51
51
  const adb = (...partes) =>
52
- execFileSync("adb", [...(serie ? ["-s", serie] : []), ...partes], { encoding: "utf8" });
52
+ execFileSync(rutaDeAdb(), [...(serie ? ["-s", serie] : []), ...partes], { encoding: "utf8" });
53
53
 
54
54
  const cliente = join(import.meta.dirname, "xone-hotswap");
55
55
  const mandar = (comando) =>
56
- execFileSync("node", [cliente, JSON.stringify(comando)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
56
+ execFileSync(NODE, [cliente, JSON.stringify(comando)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
57
57
 
58
58
  /**
59
59
  * Sin aparato NO se empieza, y se dice el siguiente paso HECHO.
@@ -66,7 +66,7 @@ const mandar = (comando) =>
66
66
  function exigirAparato() {
67
67
  let lista = "";
68
68
  try {
69
- lista = execFileSync("adb", ["devices"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
69
+ lista = execFileSync(rutaDeAdb(), ["devices"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
70
70
  } catch (error) {
71
71
  console.error(`no pude hablar con adb: ${error.message.split("\n")[0]}`);
72
72
  process.exit(1);
@@ -107,7 +107,7 @@ for (let intento = 1; intento <= 20 && !vivo; intento++) {
107
107
  mandar({ command: "getAllElements", format: "xone" });
108
108
  vivo = true;
109
109
  } catch {
110
- execFileSync("sleep", ["1"]);
110
+ dormir(1);
111
111
  }
112
112
  }
113
113
  if (!vivo) {
@@ -128,7 +128,7 @@ if (app === undefined) {
128
128
  console.log(salida);
129
129
  process.exit(0);
130
130
  }
131
- execFileSync("sleep", ["2"]);
131
+ dormir(2);
132
132
  }
133
133
  console.error(`«${app}» no llegó a contestar el árbol de controles: NO está viva.`);
134
134
  console.error("Si nunca se ha subido a este aparato, primero xone-desplegar-android.");
@@ -36,7 +36,7 @@ Respondes preguntas, depuras problemas y guías el desarrollo en XOne:
36
36
  ## Archivos de Referencia
37
37
 
38
38
 
39
- Consulta SIEMPRE estos archivos antes de responder. **Este `SKILL.md` es la referencia CORTA**: una lectura por omisión trae ~100 líneas (`DEFAULT_READ_LIMIT`), así que aquí solo van las capacidades, este índice y las reglas críticas; todo lo demás —flujo, plantillas, tamaños y nombres— vive abajo y cada fichero cabe entero. Están incluidos en la carpeta `references/` de este skill:
39
+ Consulta SIEMPRE estos archivos antes de responder. Están incluidos en la carpeta `references/` de este skill:
40
40
 
41
41
  | Fase / tema | Archivo |
42
42
  |---|---|