xonecode 0.26.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/apps/web/dist/assets/{EditorDeFichero-BW6i19HW.js → EditorDeFichero-ChyJtiiz.js} +1 -1
  2. package/apps/web/dist/assets/{index-CJycg35E.js → index-CFIrUOmW.js} +84 -84
  3. package/apps/web/dist/assets/{index-DICdq4UC.css → index-IptObD2z.css} +1 -1
  4. package/apps/web/dist/assets/{php-DxWUQTjM.js → php-B91ZcS-9.js} +1 -1
  5. package/apps/web/dist/assets/{ruby-gCKsUWxP.js → ruby-DrxrTSz0.js} +1 -1
  6. package/apps/web/dist/index.html +2 -2
  7. package/dist/agent/dispositivos/hotswap.js +27 -0
  8. package/dist/agent/dispositivos/hotswap.js.map +1 -1
  9. package/dist/agent/dispositivos/lanzamientoEnMaquina.js +33 -1
  10. package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -1
  11. package/dist/agent/dispositivos/recargaEnAparato.js +149 -0
  12. package/dist/agent/dispositivos/recargaEnAparato.js.map +1 -0
  13. package/dist/agent/grafo/arbolDeProyecto.js +34 -11
  14. package/dist/agent/grafo/arbolDeProyecto.js.map +1 -1
  15. package/dist/agent/grafo/codificacionDelProyecto.js +282 -0
  16. package/dist/agent/grafo/codificacionDelProyecto.js.map +1 -0
  17. package/dist/agent/grafo/proyecto.js +6 -1
  18. package/dist/agent/grafo/proyecto.js.map +1 -1
  19. package/dist/agent/motores/trueforge/sesionTrueforge.js +4 -1
  20. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  21. package/dist/agent/sesiones/baseDeFichero.js +8 -8
  22. package/dist/agent/sesiones/baseDeFichero.js.map +1 -1
  23. package/dist/agent/subagentes/escrituraExterna.js +4 -1
  24. package/dist/agent/subagentes/escrituraExterna.js.map +1 -1
  25. package/dist/agent/turno/turnoReal.js +4 -1
  26. package/dist/agent/turno/turnoReal.js.map +1 -1
  27. package/dist/core/codificacion.js +134 -0
  28. package/dist/core/codificacion.js.map +1 -0
  29. package/dist/core/recargaEnCaliente.js +114 -0
  30. package/dist/core/recargaEnCaliente.js.map +1 -0
  31. package/dist/web/servidor/arranque.js +184 -5
  32. package/dist/web/servidor/arranque.js.map +1 -1
  33. package/dist/web/servidor/transporte.js.map +1 -1
  34. package/package.json +1 -1
  35. package/skills/xone-hotswap/scripts/xone-desplegar-android +62 -24
  36. package/skills/xone-hotswap/scripts/xone-recargar-android +23 -1
@@ -0,0 +1,134 @@
1
+ /**
2
+ * La codificación de un fichero de texto del proyecto, en las dos direcciones, con UNA tabla por
3
+ * juego de caracteres.
4
+ *
5
+ * Los proyectos XOne traen sus `.xne` en UTF-8 o en ISO-8859 (`<?xml … encoding="iso-8859-15"?>`,
6
+ * lo que emiten las plantillas de la skill). XOne los lee según ese `encoding=`, así que guardarlos
7
+ * en otra codificación rompería cada tilde sin un error que leer: un bug mudo, y el mismo que la
8
+ * skill nombra como «encoding incoherente». Lo que hace falta es escribir en la codificación que el
9
+ * fichero YA tenía, o la que DECLARA si todavía no la delatan sus bytes.
10
+ *
11
+ * Dos tablas de 8 bits, y se elige UNA por fichero en las dos direcciones —leer con una y escribir
12
+ * con otra movería caracteres—:
13
+ * - `iso-8859-15` si el prólogo lo declara: el € en 0xA4.
14
+ * - `windows-1252` en todo lo demás (lo que `TextDecoder("latin1")` es en WHATWG): el € en 0x80, y
15
+ * los cinco bytes que windows-1252 no define salen como el control C1 del mismo número.
16
+ * Cada codificador se CONSTRUYE invirtiendo su decodificador byte a byte, no se copia de una tabla:
17
+ * así la ida y vuelta es exacta por construcción, C1 incluidos.
18
+ *
19
+ * Puro: `Uint8Array` y `TextDecoder`, sin `Buffer` ni disco.
20
+ */
21
+ function construir(etiqueta) {
22
+ const decodificador = new TextDecoder(etiqueta);
23
+ const byteDe = new Map();
24
+ for (let byte = 0; byte < 256; byte++) {
25
+ byteDe.set(decodificador.decode(Uint8Array.of(byte)).codePointAt(0), byte);
26
+ }
27
+ return { decodificar: (bytes) => decodificador.decode(bytes), byteDe };
28
+ }
29
+ /** Se construyen una vez, al cargar el módulo. `latin1` es la etiqueta que usaba ya la lectura. */
30
+ const TABLAS = {
31
+ "windows-1252": construir("latin1"),
32
+ "iso-8859-15": construir("iso-8859-15"),
33
+ };
34
+ /** Bytes de 8 bits → texto, con la tabla del fichero. */
35
+ export function decodificar8Bits(bytes, tabla) {
36
+ return TABLAS[tabla].decodificar(bytes);
37
+ }
38
+ /**
39
+ * Texto → bytes de 8 bits, o el PRIMER carácter que no cabe. Nunca se sustituye por `?`: un `?` en
40
+ * lugar de una letra es otra forma del bug mudo. Se recorre por CODE POINT, así que un emoji (dos
41
+ * unidades UTF-16) se nombra entero.
42
+ */
43
+ export function codificar8Bits(texto, tabla) {
44
+ const byteDe = TABLAS[tabla].byteDe;
45
+ const bytes = new Uint8Array(texto.length);
46
+ let n = 0;
47
+ let linea = 1;
48
+ for (const caracter of texto) {
49
+ const byte = byteDe.get(caracter.codePointAt(0));
50
+ if (byte === undefined)
51
+ return { caracter, linea };
52
+ if (caracter === "\n")
53
+ linea++;
54
+ bytes[n++] = byte;
55
+ }
56
+ return { bytes: bytes.subarray(0, n) };
57
+ }
58
+ /** Cómo se NOMBRA la tabla en un mensaje para una persona o para el agente. */
59
+ export function nombreDeTabla(tabla) {
60
+ return tabla === "iso-8859-15" ? "ISO-8859-15" : "Latin-1";
61
+ }
62
+ /**
63
+ * La codificación que DECLARA el prólogo XML, mirada como la mira `xone-linter`
64
+ * (`XmlParser.js#detectEncoding`): la cabeza del texto, `<?xml … encoding="…"`, y «8859-15» antes
65
+ * que «8859-1» (que lo contiene). Lo que no es un prólogo al principio —sin contar el BOM y los
66
+ * blancos— no declara nada, y una codificación que no sabemos escribir tampoco.
67
+ */
68
+ export function codificacionDeclarada(cabeza) {
69
+ const m = /^?\s*<\?xml[^?]*?encoding\s*=\s*["']([^"']+)["']/i.exec(cabeza.slice(0, 256));
70
+ if (m === null)
71
+ return undefined;
72
+ const declarada = m[1].toLowerCase();
73
+ if (declarada.includes("8859-15") || declarada === "latin-9" || declarada === "latin9")
74
+ return "iso-8859-15";
75
+ if (declarada.includes("8859-1") || declarada === "latin1" || declarada === "latin-1" || declarada.includes("1252")) {
76
+ return "windows-1252";
77
+ }
78
+ if (declarada === "utf-8" || declarada === "utf8")
79
+ return "utf-8";
80
+ return undefined;
81
+ }
82
+ /** La tabla de 8 bits de unos bytes que NO son UTF-8: la que declare su prólogo, o windows-1252. */
83
+ export function tablaDeLosBytes(bytes) {
84
+ // La cabeza se mira byte a byte (como `latin1`): el prólogo es ASCII en cualquier tabla.
85
+ const cabeza = decodificar8Bits(bytes.subarray(0, 256), "windows-1252");
86
+ return codificacionDeclarada(cabeza) === "iso-8859-15" ? "iso-8859-15" : "windows-1252";
87
+ }
88
+ /** UTF-8 ESTRICTO, el mismo criterio que la lectura para decidir que un fichero no lo es. */
89
+ export function esUtf8Valido(bytes) {
90
+ try {
91
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ function esAscii(bytes) {
99
+ return bytes.every((b) => b < 0x80);
100
+ }
101
+ /**
102
+ * En qué bytes se guarda `texto` sobre lo que hay (`actual`, ausente si el fichero es NUEVO). En
103
+ * orden:
104
+ * 1. Lo que ya no es UTF-8 sigue en 8 bits: con la tabla que declare lo que se escribe, si declara
105
+ * una de 8 bits (la que usará la próxima lectura), o si no con la de sus bytes (`tablaDeLosBytes`).
106
+ * 2. Un UTF-8 con algo que no es ASCII se queda en UTF-8: nunca se transcodifica lo que nadie tocó.
107
+ * 3. Un ASCII puro —que es UTF-8 válido y no delata nada— o un fichero nuevo siguen lo que DECLARE el
108
+ * texto que se escribe; sin declaración, UTF-8. Es el caso de toda colección nueva con la
109
+ * plantilla de la skill (`encoding="iso-8859-15"`): sin esto, su primera tilde iba en UTF-8 bajo
110
+ * una declaración de 8 bits.
111
+ */
112
+ export function codificacionParaEscribir(actual, texto) {
113
+ if (actual !== undefined && !esUtf8Valido(actual)) {
114
+ // Si lo que se escribe CAMBIA la declaración a otra tabla de 8 bits, manda la nueva: es con la
115
+ // que se volverá a leer, y escribir con la vieja dejaría un € en 0x80 leído luego como control.
116
+ const declarada = codificacionDeclarada(texto);
117
+ return declarada !== undefined && declarada !== "utf-8" ? declarada : tablaDeLosBytes(actual);
118
+ }
119
+ if (actual !== undefined && !esAscii(actual))
120
+ return "utf-8";
121
+ return codificacionDeclarada(texto) ?? "utf-8";
122
+ }
123
+ /**
124
+ * Bytes de un texto del proyecto → el texto que ve el AGENTE: UTF-8 si lo es (con el BOM dentro,
125
+ * como `readFile(…, "utf8")`, que es lo que hace la librería), y si no, su tabla de 8 bits. Es lo
126
+ * que tiene que leer cualquiera que compare contra lo que el agente leyó —el ANTES de la tarjeta de
127
+ * aprobación—: con UTF-8 a secas, un ancla con tilde no calzaba y cada línea con tilde salía cambiada.
128
+ */
129
+ export function decodificarComoTexto(bytes) {
130
+ return esUtf8Valido(bytes)
131
+ ? new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes)
132
+ : decodificar8Bits(bytes, tablaDeLosBytes(bytes));
133
+ }
134
+ //# sourceMappingURL=codificacion.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codificacion.js","sourceRoot":"","sources":["../../src/core/codificacion.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Qué hace el aparato con un fichero recién guardado en el editor: la regla de la recarga en
3
+ * caliente, pura.
4
+ *
5
+ * La regla es la de `skills/xone-hotswap/scripts/xone-recargar-android`, MEDIDA contra el
6
+ * framework 5.0.5.5dev, y no se inventa una nueva:
7
+ *
8
+ * colección (`.xne` con `<coll`) → `uploadFile` + `loadCollection`: se repinta en el acto. Solo
9
+ * en memoria, y por eso se sube también a disco — sin eso,
10
+ * relanzar la app la devuelve a la versión anterior.
11
+ * JavaScript (`.js`) → `uploadFile` + `loadIncludeFile` con `compile: true`.
12
+ * CSS, `.ini`, `app.xml`, un `.xne` sin colección (mappings)
13
+ * → `uploadFile`, y hace falta RELANZAR la app. Medido:
14
+ * `setCssAttribute`, `relayout` y `refresh` contestan
15
+ * `result:true` y no cambian nada.
16
+ * todo lo demás (iconos, `bd/`, `files/`) → no se recarga: lo lleva el despliegue entero.
17
+ *
18
+ * **Relanzar no se hace solo**: se pierde el estado de la pantalla en que está la persona, así
19
+ * que lo que se devuelve es «hace falta relanzar» y la pantalla lo OFRECE.
20
+ */
21
+ import { decodificarComoTexto, esUtf8Valido, tablaDeLosBytes } from "./codificacion.js";
22
+ /** Las extensiones que el aparato sabe tomar sueltas. Las mismas del script. */
23
+ const EXTENSIONES = new Set([".xne", ".js", ".css", ".ini", ".xml"]);
24
+ /** Carpetas que nunca viajan sueltas. Las mismas del script. */
25
+ const FUERA = ["bd", "files", "icons"];
26
+ /** Una colección se declara con `<coll`: lo mismo que mira el script, sobre la cabeza del texto. */
27
+ const DECLARA_COLECCION = /<coll[\s>/]/;
28
+ function extension(ruta) {
29
+ const nombre = ruta.slice(ruta.lastIndexOf("/") + 1);
30
+ const punto = nombre.lastIndexOf(".");
31
+ return punto <= 0 ? "" : nombre.slice(punto).toLowerCase();
32
+ }
33
+ /**
34
+ * Cómo se aplica `ruta` (relativa, con `/`) con estos `bytes`. El texto se mira byte a byte: la
35
+ * etiqueta `<coll` es ASCII en cualquier tabla.
36
+ */
37
+ export function claseDeRecarga(ruta, bytes) {
38
+ const limpia = ruta.replace(/^\/+/, "");
39
+ if (FUERA.some((f) => limpia === f || limpia.startsWith(`${f}/`)))
40
+ return "no-aplica";
41
+ const ext = extension(limpia);
42
+ if (!EXTENSIONES.has(ext))
43
+ return "no-aplica";
44
+ if (ext === ".js")
45
+ return "js";
46
+ if (ext === ".xne") {
47
+ let cabeza = "";
48
+ for (const b of bytes)
49
+ cabeza += String.fromCharCode(b);
50
+ return DECLARA_COLECCION.test(cabeza) ? "coleccion" : "relanzar";
51
+ }
52
+ return "relanzar";
53
+ }
54
+ /**
55
+ * El nombre de la codificación que se le dice al framework para que lea ESTOS bytes. Es la misma
56
+ * regla que la lectura del agente y del editor (`core/codificacion.ts`): UTF-8 si lo es; si no, la
57
+ * tabla de 8 bits que declare el prólogo (ISO-8859-15) o windows-1252. Mandar `UTF-8` a secas
58
+ * —lo que hace el script— enseñaba en el aparato un «�» en cada tilde de un `.xne` Latin-1.
59
+ * Los nombres son los de `java.nio.charset`, que es quien los lee al otro lado.
60
+ */
61
+ export function codificacionParaElAparato(bytes) {
62
+ if (esUtf8Valido(bytes))
63
+ return "UTF-8";
64
+ return tablaDeLosBytes(bytes) === "iso-8859-15" ? "ISO-8859-15" : "windows-1252";
65
+ }
66
+ /**
67
+ * Lo que se le da a `loadCollection`: el MISMO XML, en UTF-8 y con el prólogo diciendo UTF-8.
68
+ *
69
+ * MEDIDO en el emulador (framework 5.0.5.5dev) con un `.xne` en ISO-8859-15 y «Año ñá»: con sus
70
+ * bytes y `encoding: "ISO-8859-15"` el aparato pintaba «Año ñá» —y estropeaba también el
71
+ * «Contraseña» que ya estaba—; con el texto en UTF-8 y el prólogo cambiado a UTF-8 se ve bien. El
72
+ * framework decodifica con `encoding` y su analizador vuelve a leer el prólogo: si los dos no dicen
73
+ * lo mismo que los bytes, cada tilde sale doble. Solo para la copia EN MEMORIA: lo que se sube a
74
+ * disco (`uploadFile`) son los bytes tal cual, que es lo que el framework lee al relanzar.
75
+ */
76
+ export function xmlParaCargarEnMemoria(bytes) {
77
+ const texto = decodificarComoTexto(bytes).replace(/^\uFEFF/, "");
78
+ const conPrologoUtf8 = texto.replace(/^(\s*<\?xml[^?]*?encoding\s*=\s*)(["'])[^"']*\2/i, (_todo, antes, comilla) => `${antes}${comilla}UTF-8${comilla}`);
79
+ return new TextEncoder().encode(conPrologoUtf8);
80
+ }
81
+ /** Dónde guarda el framework lo que se le sube: `app_<nombre en minúsculas>/<ruta>` (medido). */
82
+ export function destinoEnElAparato(app, ruta) {
83
+ return `app_${app.toLowerCase()}/${ruta.replace(/^\/+/, "")}`;
84
+ }
85
+ /** La frase que enseña la cabecera del editor. Pura para que el test la fije. */
86
+ export function textoDeRecarga(r) {
87
+ switch (r.estado) {
88
+ case "aplicada":
89
+ return r.clase === "js"
90
+ ? "Aparato: JavaScript recargado; la app sigue viva"
91
+ : "Aparato: pantalla recargada en caliente; la app sigue viva";
92
+ case "relanzar":
93
+ return "Aparato: subido; para verlo hay que relanzar la app";
94
+ case "relanzada":
95
+ return "Aparato: app relanzada y viva";
96
+ case "sin-app":
97
+ // El motivo, si se sabe: «no hay aparato elegido» y «no está en la medida» no se arreglan
98
+ // en el mismo sitio que «la app no corre».
99
+ return r.motivo !== undefined && !r.motivo.includes("App is not running")
100
+ ? `Aparato: no se recarga (${r.motivo})`
101
+ : "Aparato: la app no está corriendo; lánzala desde Ejecutar o pulsa Probar";
102
+ case "desplegada":
103
+ return "Aparato: proyecto desplegado y la app viva";
104
+ case "desplegando":
105
+ return "Aparato: la app no corría; desplegando el proyecto (el recorrido, en Ejecutar)";
106
+ case "ocupado":
107
+ return "Aparato: hay un despliegue en curso; no se recarga encima";
108
+ case "no-aplica":
109
+ return "Aparato: este fichero no se recarga suelto; hace falta desplegar entero";
110
+ case "fallo":
111
+ return `Aparato: no se pudo recargar (${r.motivo ?? "sin motivo"})`;
112
+ }
113
+ }
114
+ //# sourceMappingURL=recargaEnCaliente.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recargaEnCaliente.js","sourceRoot":"","sources":["../../src/core/recargaEnCaliente.ts"],"names":[],"mappings":"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"}
@@ -55,8 +55,11 @@ import { PLATAFORMAS_DE_DISPOSITIVO } from "../../core/settings.js";
55
55
  import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
56
56
  import { arrancarEmulador, pararEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
57
57
  import { borrarAvd, clonarAvd, correrPasoDeReceta, crearAvd } from "../../agent/dispositivos/instalacionEnMaquina.js";
58
- import { asignarPuertosPendientes, motivoDeNombreDeAvdInaceptable, motivoDePuertoInaceptable, puertoDeAvd, } from "../../core/puertosDeAvd.js";
58
+ import { asignarPuertosPendientes, motivoDeNombreDeAvdInaceptable, motivoDePuertoInaceptable, puertoDeAvd, PUERTO_DEL_HOTSWAP, } from "../../core/puertosDeAvd.js";
59
59
  import { abrirCarpetaDelSistema, abrirDirectorioDelSistema } from "../../agent/config/selectorEnMaquina.js";
60
+ import { recargarFicheroDelProyecto, relanzarEnAparato, } from "../../agent/dispositivos/recargaEnAparato.js";
61
+ import { textoDeRecarga } from "../../core/recargaEnCaliente.js";
62
+ import { nombreDeApp } from "../../core/descriptoresDeApp.js";
60
63
  import { modelosDeMotor } from "../../agent/config/modelosDeMotor.js";
61
64
  import { parsear, compatibleConOpenAi, esProveedorPersonalizado, idDeProveedorPersonalizado, motivoDeEndpointInaceptable, motivoDeSlugInaceptable, nombreDeProveedor, PROVEEDORES, slugDesdeNombre, resolver, SIN_CREDENCIAL, PAPELES, } from "../../core/modelos.js";
62
65
  import { hayCredencial } from "../../cli/consola.js";
@@ -3020,15 +3023,21 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3020
3023
  * se pintó el botón. Una revalidación que no pasa se contesta con un `lanzamiento` en `fallo`
3021
3024
  * y NO se llama al lanzador.
3022
3025
  */
3023
- const atenderLanzarApp = async () => {
3026
+ /**
3027
+ * `alAcabar` es para quien lanzó por OTRO camino que la pestaña (el «Probar» del editor): le dice
3028
+ * cómo acabó, para que su línea no se quede en «desplegando» para siempre.
3029
+ */
3030
+ const atenderLanzarApp = async (alAcabar) => {
3024
3031
  // Ya hay uno: se reenvía su estado en vez de lanzar otro, como `atenderReceta`.
3025
3032
  if (lanzamiento !== undefined) {
3026
3033
  emitirLanzamiento("corriendo");
3034
+ alAcabar?.("ocupado");
3027
3035
  return;
3028
3036
  }
3029
3037
  const medida = await medirLanzamiento();
3030
3038
  if (medida === undefined) {
3031
3039
  informar("no hay ningún proyecto abierto que lanzar");
3040
+ alAcabar?.("fallo", "no hay ningún proyecto abierto");
3032
3041
  return;
3033
3042
  }
3034
3043
  const foto = medida.dispositivo === undefined ? undefined : fotoDeDispositivo(medida.dispositivo);
@@ -3043,6 +3052,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3043
3052
  ms: 0,
3044
3053
  motivo,
3045
3054
  });
3055
+ alAcabar?.("fallo", motivo);
3046
3056
  };
3047
3057
  const lanzar = opciones.lanzarEnDispositivo;
3048
3058
  const primeraFalta = medida.veredicto.causas[0];
@@ -3127,6 +3137,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3127
3137
  ultimoLanzamiento = 0;
3128
3138
  trabajo.fase = resultado.fase;
3129
3139
  emitirLanzamiento(resultado.estado, resultado.motivo);
3140
+ alAcabar?.(resultado.estado === "ok" ? "ok" : "fallo", resultado.motivo ?? (resultado.estado === "ok" ? undefined : resultado.estado));
3130
3141
  }
3131
3142
  catch (error) {
3132
3143
  // El contrato del lanzador es no lanzar nunca, así que esto es un bug suyo; aun así hay
@@ -3134,6 +3145,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3134
3145
  // muerto y sin una palabra.
3135
3146
  ultimoLanzamiento = 0;
3136
3147
  emitirLanzamiento("fallo", motivoLegible(error));
3148
+ alAcabar?.("fallo", motivoLegible(error));
3137
3149
  }
3138
3150
  finally {
3139
3151
  lanzamiento = undefined;
@@ -3814,7 +3826,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3814
3826
  * - lo demás —guardas de ruta, huella, tope— lo decide el puerto y lo DEVUELVE.
3815
3827
  * El `try` es el de `atenderFichero`: un fallo inesperado contesta sin la ruta de la máquina.
3816
3828
  */
3817
- const atenderGuardarFichero = async (ruta, texto, huella, id, proyecto) => {
3829
+ const atenderGuardarFichero = async (ruta, texto, huella, id, proyecto, recargar = false) => {
3818
3830
  const abierto = vestibulo.proyectoAbierto();
3819
3831
  if (abierto === undefined) {
3820
3832
  emitir({ clase: "ficheroGuardado", ruta, id, error: "no hay ningún proyecto abierto" });
@@ -3842,13 +3854,152 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3842
3854
  // `id` lo pone la pestaña en CADA guardado y vuelve en la respuesta: esta va a todas las
3843
3855
  // pestañas, y cada una reconoce la suya por él. La huella no vale —dos pestañas que parten de
3844
3856
  // la misma versión mandan la misma—.
3845
- emitir({ clase: "ficheroGuardado", ...(await opciones.escribirFichero(abierto.raiz, ruta, texto, huella)), id });
3857
+ const guardado = await opciones.escribirFichero(abierto.raiz, ruta, texto, huella);
3858
+ emitir({ clase: "ficheroGuardado", ...guardado, id });
3859
+ // DESPUÉS de contestar el guardado, y sin esperarla: el «Guardando…» no depende del aparato,
3860
+ // y nada de la recarga puede convertir un guardado bueno en uno fallido.
3861
+ if (recargar && guardado.huella !== undefined && guardado.error === undefined) {
3862
+ void atenderRecarga(abierto.raiz, ruta, { probar: false }).catch(contar);
3863
+ }
3846
3864
  }
3847
3865
  catch (error) {
3848
3866
  informar(`no se pudo guardar «${ruta}» (${codigoDe(error)})`);
3849
3867
  emitir({ clase: "ficheroGuardado", ruta, id, error: "no se pudo guardar el fichero" });
3850
3868
  }
3851
3869
  };
3870
+ /**
3871
+ * A qué aparato y app va una recarga, resuelto aquí y no en el cliente: el dispositivo ELEGIDO en
3872
+ * la sesión (si no hay, no se recarga — sin adivinar uno), Android, el puerto de SU AVD (con dos
3873
+ * emuladores, el 8443 le hablaría al otro sin error) y el nombre de la app de `app.ini`, el
3874
+ * MISMO que usa Ejecutar. `undefined` = no hay a dónde recargar, y eso no es un fallo.
3875
+ */
3876
+ const destinoDeRecarga = async (abierto) => {
3877
+ if (abierto.dispositivo === undefined)
3878
+ return { motivo: "no hay un aparato elegido en la sesión" };
3879
+ const { dispositivo } = dispositivoDeLaSesion(abierto.dispositivo, informeDeDispositivos);
3880
+ // La foto de Dispositivos solo se toma al conectar y al pulsar «Refrescar»: un emulador
3881
+ // arrancado después no está en ella, y decir «la app no corre» mandaría a buscar donde no es.
3882
+ if (dispositivo === undefined) {
3883
+ return { motivo: "el aparato de la sesión no está en la última medida: pulsa Refrescar en Dispositivos" };
3884
+ }
3885
+ if (dispositivo.plataforma !== "android")
3886
+ return { motivo: "la recarga en caliente solo existe en Android" };
3887
+ const ini = await leerDescriptor(abierto.raiz, "app.ini");
3888
+ const app = ini.texto === undefined ? undefined : nombreDeApp(ini.texto);
3889
+ if (app === undefined)
3890
+ return { motivo: "app.ini no dice el nombre de la app (name=)" };
3891
+ return {
3892
+ serie: dispositivo.id,
3893
+ puerto: dispositivo.avd === undefined ? PUERTO_DEL_HOTSWAP : puertoDeAvd(ajustesDeDispositivos(), dispositivo.avd),
3894
+ app,
3895
+ };
3896
+ };
3897
+ /**
3898
+ * UNA recarga a la vez para toda la máquina, como el lanzamiento: dos al mismo aparato se
3899
+ * cruzarían en el canal (las respuestas se emparejan por ORDEN). Un guardado que llega con una en
3900
+ * vuelo deja su ruta como SIGUIENTE, y la última pisa a la anterior: tras varios Cmd+S seguidos lo
3901
+ * que importa es cómo quedó el fichero, no cada versión intermedia.
3902
+ */
3903
+ let recargando = false;
3904
+ let siguienteRecarga;
3905
+ const emitirRecarga = (ruta, resultado) => {
3906
+ emitir({
3907
+ clase: "recargaEnAparato",
3908
+ ruta,
3909
+ estado: resultado.estado,
3910
+ ...(resultado.clase === undefined ? {} : { tipo: resultado.clase }),
3911
+ texto: textoDeRecarga(resultado),
3912
+ });
3913
+ };
3914
+ /**
3915
+ * Recargar en el aparato. **Nunca lanza ni contesta un error al guardado**: sin aparato o sin
3916
+ * app, dice `sin-app` y ya. Con `probar` y la app sin correr, lanza el despliegue entero de
3917
+ * Ejecutar —que sube el proyecto entero— en vez de recargar lo que nadie tiene cargado.
3918
+ */
3919
+ const atenderRecarga = async (raiz, ruta, { probar }) => {
3920
+ if (recargando) {
3921
+ siguienteRecarga = { raiz, ruta, probar };
3922
+ return;
3923
+ }
3924
+ recargando = true;
3925
+ try {
3926
+ const abierto = vestibulo.proyectoAbierto();
3927
+ if (abierto === undefined || abierto.raiz !== raiz)
3928
+ return;
3929
+ if (lanzamiento !== undefined) {
3930
+ emitirRecarga(ruta, { estado: "ocupado" });
3931
+ return;
3932
+ }
3933
+ const destino = await destinoDeRecarga(abierto);
3934
+ const recargar = opciones.recargarEnAparato;
3935
+ if ("motivo" in destino) {
3936
+ // Sin a dónde recargar tampoco hay a dónde desplegar: se dice por qué, y nada más.
3937
+ emitirRecarga(ruta, { estado: "sin-app", motivo: destino.motivo });
3938
+ return;
3939
+ }
3940
+ if (recargar === undefined) {
3941
+ emitirRecarga(ruta, { estado: "sin-app", motivo: "esta ejecución no recarga en el aparato" });
3942
+ return;
3943
+ }
3944
+ const resultado = await recargar(raiz, ruta, destino);
3945
+ if (resultado.estado === "sin-app" && probar) {
3946
+ emitirRecarga(ruta, { estado: "desplegando" });
3947
+ // Y cómo acabó, en la MISMA línea: sin esto se quedaba en «desplegando» para siempre.
3948
+ void atenderLanzarApp((estado, motivo) => {
3949
+ if (estado === "ok")
3950
+ emitirRecarga(ruta, { estado: "desplegada" });
3951
+ else if (estado === "ocupado")
3952
+ emitirRecarga(ruta, { estado: "ocupado" });
3953
+ else
3954
+ emitirRecarga(ruta, { estado: "fallo", motivo: `el despliegue no acabó bien: ${motivo ?? "sin motivo"}` });
3955
+ }).catch(contar);
3956
+ return;
3957
+ }
3958
+ emitirRecarga(ruta, resultado);
3959
+ }
3960
+ catch (error) {
3961
+ // Su contrato es no lanzar; si lo hace, se dice como fallo de la RECARGA, nunca del guardado.
3962
+ emitirRecarga(ruta, { estado: "fallo", motivo: motivoLegible(error) });
3963
+ }
3964
+ finally {
3965
+ recargando = false;
3966
+ const otra = siguienteRecarga;
3967
+ siguienteRecarga = undefined;
3968
+ if (otra !== undefined)
3969
+ void atenderRecarga(otra.raiz, otra.ruta, { probar: otra.probar }).catch(contar);
3970
+ }
3971
+ };
3972
+ /** Relanzar la app a petición (un CSS solo se ve así). Con las mismas reglas que la recarga. */
3973
+ const atenderRelanzar = async (ruta) => {
3974
+ const abierto = vestibulo.proyectoAbierto();
3975
+ if (abierto === undefined)
3976
+ return;
3977
+ if (lanzamiento !== undefined || recargando) {
3978
+ emitirRecarga(ruta, { estado: "ocupado" });
3979
+ return;
3980
+ }
3981
+ const destino = await destinoDeRecarga(abierto);
3982
+ const relanzar = opciones.relanzarEnAparato;
3983
+ if ("motivo" in destino || relanzar === undefined) {
3984
+ emitirRecarga(ruta, { estado: "sin-app", motivo: "motivo" in destino ? destino.motivo : "esta ejecución no relanza" });
3985
+ return;
3986
+ }
3987
+ recargando = true;
3988
+ try {
3989
+ emitirRecarga(ruta, await relanzar(destino));
3990
+ }
3991
+ catch (error) {
3992
+ emitirRecarga(ruta, { estado: "fallo", motivo: motivoLegible(error) });
3993
+ }
3994
+ finally {
3995
+ recargando = false;
3996
+ // Un guardado que llegó mientras se relanzaba quedó como siguiente: sin esto se perdía.
3997
+ const otra = siguienteRecarga;
3998
+ siguienteRecarga = undefined;
3999
+ if (otra !== undefined)
4000
+ void atenderRecarga(otra.raiz, otra.ruta, { probar: otra.probar }).catch(contar);
4001
+ }
4002
+ };
3852
4003
  /**
3853
4004
  * La base de las marcas del editor. Con la sesión ABIERTA (`abierto.sesion`, la misma que mira
3854
4005
  * Revisión): su ref es la foto del inicio. Sin puerto se contesta `sinBase` —el editor funciona
@@ -5377,7 +5528,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
5377
5528
  typeof mensaje.huella === "string" &&
5378
5529
  typeof mensaje.id === "string" &&
5379
5530
  typeof mensaje.proyecto === "string") {
5380
- void atenderGuardarFichero(mensaje.ruta, mensaje.texto, mensaje.huella, mensaje.id, mensaje.proyecto).catch(contar);
5531
+ void atenderGuardarFichero(mensaje.ruta, mensaje.texto, mensaje.huella, mensaje.id, mensaje.proyecto, mensaje.recargar === true).catch(contar);
5381
5532
  respuesta.writeHead(204);
5382
5533
  respuesta.end();
5383
5534
  return;
@@ -5879,6 +6030,22 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
5879
6030
  respuesta.end();
5880
6031
  return;
5881
6032
  }
6033
+ if (typeof mensaje === "object" &&
6034
+ mensaje !== null &&
6035
+ mensaje.clase === "recargarEnAparato" &&
6036
+ typeof mensaje.ruta === "string") {
6037
+ // Suelto: habla con el aparato, y la respuesta va por el SSE como `recargaEnAparato`.
6038
+ const abierto = vestibulo.proyectoAbierto();
6039
+ if (abierto !== undefined) {
6040
+ if (mensaje.relanzar === true)
6041
+ void atenderRelanzar(mensaje.ruta).catch(contar);
6042
+ else
6043
+ void atenderRecarga(abierto.raiz, mensaje.ruta, { probar: mensaje.probar === true }).catch(contar);
6044
+ }
6045
+ respuesta.writeHead(204);
6046
+ respuesta.end();
6047
+ return;
6048
+ }
5882
6049
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "cancelarLanzamiento") {
5883
6050
  // Este SÍ es síncrono: `cancelar` solo mata el grupo del proceso en curso.
5884
6051
  atenderCancelarLanzamiento();
@@ -6486,6 +6653,17 @@ export function ficherosCableados() {
6486
6653
  * (`guardarAjusteDeAvd(undefined, …)`). `arrancar` entra solo para que un test vea qué recibe la
6487
6654
  * de verdad sin lanzar un emulador.
6488
6655
  */
6656
+ /**
6657
+ * La recarga en caliente del editor, con las funciones de VERDAD y compuesta fuera del cableado
6658
+ * por la trampa de siempre: un campo opcional olvidado en `arrancarConsolaWeb` deja la casilla
6659
+ * pintada y sin hacer nada, con todo en verde. El `adb` y el canal reales los ponen ellas.
6660
+ */
6661
+ export function recargaCableada() {
6662
+ return {
6663
+ recargarEnAparato: (raiz, ruta, destino) => recargarFicheroDelProyecto(raiz, ruta, destino),
6664
+ relanzarEnAparato: (destino) => relanzarEnAparato(destino),
6665
+ };
6666
+ }
6489
6667
  export function emuladoresCableados(opciones = {}) {
6490
6668
  const arrancar = opciones.arrancar ?? arrancarEmulador;
6491
6669
  return {
@@ -7048,6 +7226,7 @@ export async function arrancarConsolaWeb(opciones) {
7048
7226
  * arriba: una composición de producción que solo existe en el cableado nadie la prueba.
7049
7227
  */
7050
7228
  lanzarEnDispositivo,
7229
+ ...recargaCableada(),
7051
7230
  catalogoDeModelos: async (proveedor) => {
7052
7231
  const modelos = await new CatalogoModelos(undefined, undefined, proveedoresPersonalizados).listar(proveedor);
7053
7232
  return modelos.map((m) => ({ id: m.id, ...(m.nombre === undefined ? {} : { nombre: m.nombre }) }));