xonecode 0.26.0 → 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 (45) hide show
  1. package/apps/web/dist/assets/{EditorDeFichero-BOMIz5Wx.js → EditorDeFichero-ChyJtiiz.js} +1 -1
  2. package/apps/web/dist/assets/{index-DTsPoUh_.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-BHWCwDKR.js → php-B91ZcS-9.js} +1 -1
  5. package/apps/web/dist/assets/{ruby-D3HLU4IP.js → ruby-DrxrTSz0.js} +1 -1
  6. package/apps/web/dist/index.html +2 -2
  7. package/dist/agent/cloudstudio/subida.js +99 -26
  8. package/dist/agent/cloudstudio/subida.js.map +1 -1
  9. package/dist/agent/dispositivos/hotswap.js +27 -0
  10. package/dist/agent/dispositivos/hotswap.js.map +1 -1
  11. package/dist/agent/dispositivos/lanzamientoEnMaquina.js +33 -1
  12. package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -1
  13. package/dist/agent/dispositivos/recargaEnAparato.js +149 -0
  14. package/dist/agent/dispositivos/recargaEnAparato.js.map +1 -0
  15. package/dist/agent/grafo/arbolDeProyecto.js +34 -11
  16. package/dist/agent/grafo/arbolDeProyecto.js.map +1 -1
  17. package/dist/agent/grafo/codificacionDelProyecto.js +282 -0
  18. package/dist/agent/grafo/codificacionDelProyecto.js.map +1 -0
  19. package/dist/agent/grafo/proyecto.js +6 -1
  20. package/dist/agent/grafo/proyecto.js.map +1 -1
  21. package/dist/agent/motores/trueforge/sesionTrueforge.js +4 -1
  22. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  23. package/dist/agent/sesiones/baseDeFichero.js +8 -8
  24. package/dist/agent/sesiones/baseDeFichero.js.map +1 -1
  25. package/dist/agent/subagentes/escrituraExterna.js +4 -1
  26. package/dist/agent/subagentes/escrituraExterna.js.map +1 -1
  27. package/dist/agent/turno/turnoReal.js +4 -1
  28. package/dist/agent/turno/turnoReal.js.map +1 -1
  29. package/dist/cli/main.js +7 -1
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/core/actos.js.map +1 -1
  32. package/dist/core/codificacion.js +134 -0
  33. package/dist/core/codificacion.js.map +1 -0
  34. package/dist/core/ports.js +22 -2
  35. package/dist/core/ports.js.map +1 -1
  36. package/dist/core/recargaEnCaliente.js +114 -0
  37. package/dist/core/recargaEnCaliente.js.map +1 -0
  38. package/dist/core/verificacionDeSubida.js +58 -0
  39. package/dist/core/verificacionDeSubida.js.map +1 -0
  40. package/dist/web/servidor/arranque.js +184 -5
  41. package/dist/web/servidor/arranque.js.map +1 -1
  42. package/dist/web/servidor/transporte.js.map +1 -1
  43. package/package.json +1 -1
  44. package/skills/xone-hotswap/scripts/xone-desplegar-android +62 -24
  45. package/skills/xone-hotswap/scripts/xone-recargar-android +23 -1
@@ -0,0 +1,58 @@
1
+ /**
2
+ * La comprobación de lo que una subida dejó en CloudStudio: lo que se mandó contra lo que se
3
+ * relee. PURA, para poder probar el motivo sin red.
4
+ *
5
+ * Existe porque hay ficheros que se CORTAN al subirlos a Studio y la causa no se ha encontrado.
6
+ * Mientras tanto, cada texto subido se relee y se compara; un corte tiene que salir como fallo
7
+ * con cifras que se puedan medir —cuánto había, cuánto llegó, desde dónde difiere—, no como un
8
+ * «subido» que miente.
9
+ *
10
+ * La comparación normaliza SOLO dos transformaciones MEDIDAS contra el servidor real (una rama
11
+ * desechable de AppDemo), aplicadas a los dos lados:
12
+ * - **Studio quita el salto de línea FINAL** (o `studio_get_file` no lo devuelve): un
13
+ * «…'añadir';\n» de 76 B vuelve con 75 B, y un fichero de 457.890 B vuelve con 457.889 B,
14
+ * idéntico en todo lo demás. Un fichero SIN salto final vuelve idéntico.
15
+ * - **Studio convierte CRLF en LF** («\r\n» → «\n»).
16
+ * Nada más: el BOM se conserva, y tildes, emoji y «» llegan intactos, así que no se tocan.
17
+ * Normalizar a ciegas es justo cómo se esconde un corte; un corte de verdad (falta una línea,
18
+ * cambia algo en medio) sigue saliendo distinto. El motivo y los hashes se calculan sobre los
19
+ * textos EN CRUDO: son lo que hay de verdad a cada lado.
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ /** sha256 en hexadecimal minúsculo: el mismo formato que pide `studio_upload_file`. */
23
+ export function sha256Hex(datos) {
24
+ return createHash("sha256").update(datos).digest("hex");
25
+ }
26
+ /** Un número con el punto de miles del castellano, sin depender del ICU de la máquina. */
27
+ export function conMiles(n) {
28
+ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
29
+ }
30
+ /** Las dos transformaciones medidas de Studio (ver cabecera), y ninguna más. */
31
+ function comoLoGuardaStudio(texto) {
32
+ return texto.replace(/\r\n/g, "\n").replace(/\n+$/, "");
33
+ }
34
+ /** `undefined` = llegó igual. Si no, el motivo con las cifras y los dos hashes. */
35
+ export function diferenciaDeTexto(local, studio) {
36
+ if (comoLoGuardaStudio(local) === comoLoGuardaStudio(studio))
37
+ return undefined;
38
+ let desde = 0;
39
+ const tope = Math.min(local.length, studio.length);
40
+ while (desde < tope && local[desde] === studio[desde])
41
+ desde++;
42
+ const bytesLocal = Buffer.byteLength(local, "utf8");
43
+ const bytesStudio = Buffer.byteLength(studio, "utf8");
44
+ return {
45
+ motivo: `llegó distinto a Studio: ${conMiles(bytesLocal)} B en local, ${conMiles(bytesStudio)} B en Studio ` +
46
+ `(difieren desde el carácter ${conMiles(desde)})`,
47
+ sha256Local: sha256Hex(local),
48
+ sha256Studio: sha256Hex(studio),
49
+ };
50
+ }
51
+ /** Cuánto de un motivo de error remoto se deja en un fallo: lo justo para reconocerlo. */
52
+ const TOPE_DE_MOTIVO = 200;
53
+ /** El motivo de una relectura que no se pudo hacer: el fichero se escribió pero NO está comprobado. */
54
+ export function motivoSinComprobar(error) {
55
+ const corto = error.length > TOPE_DE_MOTIVO ? `${error.slice(0, TOPE_DE_MOTIVO)}…` : error;
56
+ return `subido pero no se pudo comprobar: ${corto}`;
57
+ }
58
+ //# sourceMappingURL=verificacionDeSubida.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verificacionDeSubida.js","sourceRoot":"","sources":["../../src/core/verificacionDeSubida.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,uFAAuF;AACvF,MAAM,UAAU,SAAS,CAAC,KAA0B;IAClD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,QAAQ,CAAC,CAAS;IAChC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;AACzD,CAAC;AASD,gFAAgF;AAChF,SAAS,kBAAkB,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC1D,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,MAAc;IAC7D,IAAI,kBAAkB,CAAC,KAAK,CAAC,KAAK,kBAAkB,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/E,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACnD,OAAO,KAAK,GAAG,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC;QAAE,KAAK,EAAE,CAAC;IAC/D,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACpD,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtD,OAAO;QACL,MAAM,EACJ,4BAA4B,QAAQ,CAAC,UAAU,CAAC,gBAAgB,QAAQ,CAAC,WAAW,CAAC,eAAe;YACpG,+BAA+B,QAAQ,CAAC,KAAK,CAAC,GAAG;QACnD,WAAW,EAAE,SAAS,CAAC,KAAK,CAAC;QAC7B,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC;KAChC,CAAC;AACJ,CAAC;AAED,0FAA0F;AAC1F,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,uGAAuG;AACvG,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3F,OAAO,qCAAqC,KAAK,EAAE,CAAC;AACtD,CAAC"}
@@ -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 }) }));