xonecode 0.21.0 → 0.22.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 (44) hide show
  1. package/apps/web/dist/assets/index-5rtynJV_.js +490 -0
  2. package/apps/web/dist/assets/index-Cq6c6WEF.css +1 -0
  3. package/apps/web/dist/assets/{php-UkjSyds-.js → php-BTFUgUDo.js} +1 -1
  4. package/apps/web/dist/assets/{ruby-e801JuzW.js → ruby-BKKiBtJK.js} +1 -1
  5. package/apps/web/dist/index.html +2 -2
  6. package/dist/agent/config/mudanzaEnDisco.js +237 -3
  7. package/dist/agent/config/mudanzaEnDisco.js.map +1 -1
  8. package/dist/agent/config/selectorEnMaquina.js +3 -2
  9. package/dist/agent/config/selectorEnMaquina.js.map +1 -1
  10. package/dist/agent/config/settingsEnDisco.js +76 -2
  11. package/dist/agent/config/settingsEnDisco.js.map +1 -1
  12. package/dist/agent/dispositivos/arranqueDeEmulador.js +27 -3
  13. package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -1
  14. package/dist/agent/dispositivos/instalacionEnMaquina.js +240 -29
  15. package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -1
  16. package/dist/agent/dispositivos/lanzamientoEnMaquina.js +12 -17
  17. package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -1
  18. package/dist/core/copiaDeAvd.js +111 -0
  19. package/dist/core/copiaDeAvd.js.map +1 -0
  20. package/dist/core/mudanzaDeWorkspace.js +105 -15
  21. package/dist/core/mudanzaDeWorkspace.js.map +1 -1
  22. package/dist/core/puertosDeAvd.js +64 -0
  23. package/dist/core/puertosDeAvd.js.map +1 -0
  24. package/dist/core/selectorDeCarpeta.js +88 -9
  25. package/dist/core/selectorDeCarpeta.js.map +1 -1
  26. package/dist/core/settings.js +60 -1
  27. package/dist/core/settings.js.map +1 -1
  28. package/dist/core/shellDeAgente.js +11 -0
  29. package/dist/core/shellDeAgente.js.map +1 -1
  30. package/dist/web/servidor/arranque.js +476 -43
  31. package/dist/web/servidor/arranque.js.map +1 -1
  32. package/dist/web/servidor/transporte.js.map +1 -1
  33. package/dist/web/servidor/vestibulo.js +154 -48
  34. package/dist/web/servidor/vestibulo.js.map +1 -1
  35. package/package.json +1 -1
  36. package/skills/xone-hotswap/SKILL.md +26 -10
  37. package/skills/xone-hotswap/lib/dispositivo.mjs +97 -3
  38. package/skills/xone-hotswap/references/conexion-y-despliegue.md +27 -15
  39. package/skills/xone-hotswap/scripts/xone-arrancar-android +9 -2
  40. package/skills/xone-hotswap/scripts/xone-desplegar-android +12 -6
  41. package/skills/xone-hotswap/scripts/xone-hotswap +32 -10
  42. package/skills/xone-hotswap/scripts/xone-reiniciar-android +23 -7
  43. package/apps/web/dist/assets/index-CFoqpKRk.css +0 -1
  44. package/apps/web/dist/assets/index-DgvLb2SM.js +0 -486
@@ -53,8 +53,9 @@ import { lanzarEnDispositivo, } from "../../agent/dispositivos/lanzamientoEnMaqu
53
53
  import { motivoDeBloqueo, puedeLanzarse, } from "../../core/puedeLanzarse.js";
54
54
  import { PLATAFORMAS_DE_DISPOSITIVO } from "../../core/settings.js";
55
55
  import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
56
- import { arrancarEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
57
- import { correrPasoDeReceta } from "../../agent/dispositivos/instalacionEnMaquina.js";
56
+ import { arrancarEmulador, pararEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
57
+ import { borrarAvd, clonarAvd, correrPasoDeReceta, crearAvd } from "../../agent/dispositivos/instalacionEnMaquina.js";
58
+ import { asignarPuertosPendientes, motivoDeNombreDeAvdInaceptable, motivoDePuertoInaceptable, puertoDeAvd, } from "../../core/puertosDeAvd.js";
58
59
  import { abrirCarpetaDelSistema, abrirDirectorioDelSistema } from "../../agent/config/selectorEnMaquina.js";
59
60
  import { modelosDeMotor } from "../../agent/config/modelosDeMotor.js";
60
61
  import { parsear, compatibleConOpenAi, esProveedorPersonalizado, idDeProveedorPersonalizado, motivoDeEndpointInaceptable, motivoDeSlugInaceptable, nombreDeProveedor, PROVEEDORES, slugDesdeNombre, resolver, SIN_CREDENCIAL, PAPELES, } from "../../core/modelos.js";
@@ -65,10 +66,10 @@ import { borrarCredencial, guardarCredencial } from "../../agent/config/authEnDi
65
66
  import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/config/configEnDisco.js";
66
67
  import { crearCheckpointerDeProyecto, mantenimientoDelCheckpointer, } from "../../agent/sesiones/checkpointer.js";
67
68
  import { resumenDePoda } from "../../core/podaDeCheckpoint.js";
68
- import { cargarSettings, guardarConcurrenciaDeTareas, guardarDepurar, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarEntorno as guardarEntornoEnDisco, olvidarEntornoDeSettings, copiasDeEntorno, borrarCopiasDeEntorno, } from "../../agent/config/settingsEnDisco.js";
69
+ import { cargarSettings, guardarConcurrenciaDeTareas, guardarDepurar, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarAjusteDeAvd, quitarAjustesDeAvd, guardarPuertosAsignados, guardarEntorno as guardarEntornoEnDisco, olvidarEntornoDeSettings, copiasDeEntorno, borrarCopiasDeEntorno, } from "../../agent/config/settingsEnDisco.js";
69
70
  import { dentroDelWorkspace, depuracionActiva, expandirConCasa, mismaRuta, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
70
71
  import { esModoDeEscritura, MODO_POR_OMISION, } from "../../core/modoDeEscritura.js";
71
- import { mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
72
+ import { megasDeLasMudanzas, mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
72
73
  import { elegirCarpetaEnMaquina, haySelectorDeCarpeta } from "../../agent/config/selectorEnMaquina.js";
73
74
  import { cloudstudioDelProyecto } from "../../agent/config/configEnDisco.js";
74
75
  import { abrirEnSistema, olvidarEntorno as olvidarCredencialesDeEntorno, rutaAuthPorDefecto } from "../../agent/cloudstudio/cloudstudioMcp.js";
@@ -1020,6 +1021,14 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1020
1021
  })),
1021
1022
  };
1022
1023
  };
1024
+ /**
1025
+ * El último progreso de la mudanza del workspace EN MARCHA, o ausente. Existe para la ráfaga:
1026
+ * un navegador que reconecta a mitad de una copia larga tira la mudanza al caerse el cable
1027
+ * (`store.ts#marcarDesconectado`) y el `resultado` solo va a quien está conectado al acabar,
1028
+ * así que sin esto no sabría que sigue una —y los controles del workspace se le quedarían
1029
+ * sueltos mientras el servidor niega cualquier otro cambio—.
1030
+ */
1031
+ let ultimoProgresoDeMudanza;
1023
1032
  /**
1024
1033
  * Engancha el cable a la consola que toque, con el transcript entero por delante.
1025
1034
  *
@@ -1114,6 +1123,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1114
1123
  cliente(tareas);
1115
1124
  if (workspace !== undefined)
1116
1125
  cliente(workspace);
1126
+ if (ultimoProgresoDeMudanza !== undefined)
1127
+ cliente({ clase: "mudanzaDeWorkspace", progreso: ultimoProgresoDeMudanza });
1117
1128
  if (depuracion !== undefined)
1118
1129
  cliente(depuracion);
1119
1130
  if (conectores !== undefined)
@@ -1130,7 +1141,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1130
1141
  // La foto de la máquina, si ya se tomó. Si no, se dispara abajo UNA vez y llega a
1131
1142
  // todos por el SSE cuando termine: no se espera aquí, que son varios procesos.
1132
1143
  if (informeDeDispositivos !== undefined) {
1133
- cliente({ clase: "dispositivos", informe: informeDeDispositivos, ajustes: ajustesDeDispositivos() });
1144
+ cliente(mensajeDeDispositivos(informeDeDispositivos));
1134
1145
  }
1135
1146
  // Y si hay turno corriendo, se dice: quien conecta a mitad no vio el mensaje que lo
1136
1147
  // anunció, y sin esto vería el compositor encendido y sin borde —«no pasa nada»—
@@ -1275,6 +1286,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1275
1286
  ? { clase: "workspace", ruta }
1276
1287
  : { clase: "workspace", ruta, puedeElegir: true };
1277
1288
  };
1289
+ /** Hay una mudanza del workspace en marcha: una segunda se niega, y crear una tarea también. */
1290
+ let mudandoWorkspace = false;
1278
1291
  const emitirWorkspace = () => {
1279
1292
  const m = mensajeDeWorkspace();
1280
1293
  if (m !== undefined)
@@ -1663,6 +1676,30 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1663
1676
  };
1664
1677
  /** Los ajustes de destino, o `{}` —que es «se miran todos»— si esta ejecución no los lee. */
1665
1678
  const ajustesDeDispositivos = () => opciones.ajustesDeDispositivos?.() ?? {};
1679
+ /**
1680
+ * Qué dispositivo tiene elegido CADA consola abierta, no solo la del foco: con dos proyectos
1681
+ * abiertos sobre dos emuladores, lo que la PASTILLA del chat tiene que poder decir es «este lo usa Tienda».
1682
+ *
1683
+ * Se pregunta a las consolas vivas y no se guarda, igual que `sesionesTrabajando`: es un
1684
+ * estado de este instante. El `proyecto` es el SEGMENTO de su carpeta —el mismo nombre que ya
1685
+ * viaja en el lanzamiento (`medirLanzable`) y que enseña la barra—, nunca la ruta: `sinRutas`.
1686
+ * **Límite declarado**: va por `basename(raiz)`, así que el mismo nombre de proyecto en dos
1687
+ * entornos (`<entorno>/<proyecto>`) se confunde.
1688
+ */
1689
+ const dispositivosEnUso = () => vestibulo
1690
+ .proyectosAbiertos()
1691
+ .flatMap((c) => (c.dispositivo === undefined ? [] : [{ id: c.dispositivo.id, proyecto: basename(c.raiz) }]));
1692
+ /**
1693
+ * El mensaje `dispositivos` entero, compuesto en UN sitio: la foto, los ajustes leídos AHORA y
1694
+ * quién usa qué. Con cada emisor montando el suyo, uno se quedaba sin el campo nuevo —una
1695
+ * pestaña que reconecta, o una verificación, borrarían el «en uso» de la pantalla—.
1696
+ */
1697
+ const mensajeDeDispositivos = (informe) => ({
1698
+ clase: "dispositivos",
1699
+ informe,
1700
+ ajustes: ajustesDeDispositivos(),
1701
+ enUso: dispositivosEnUso(),
1702
+ });
1666
1703
  let deteccionEnVuelo;
1667
1704
  /**
1668
1705
  * Cómo acabó el último «Arrancar». Viaja con la foto porque el resultado de arrancar ES la
@@ -1679,10 +1716,24 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1679
1716
  deteccionEnVuelo = (async () => {
1680
1717
  try {
1681
1718
  informeDeDispositivos = sinRutas(await detectar());
1719
+ // Cada AVD necesita SU puerto, y fijado —también el único, en silencio—: una propuesta que
1720
+ // se recalculara al crear otro AVD le movería el puerto a uno que ya se usa. Se siembra
1721
+ // ANTES de emitir para que la foto salga con los puertos ya puestos, y releyendo después
1722
+ // (`ajustesDeDispositivos` va al disco): lo emitido es lo que quedó escrito. Un AVD recién
1723
+ // creado desde Ajustes recibe el suyo aquí, en la medida que cierra su trabajo.
1724
+ const pendientes = asignarPuertosPendientes(informeDeDispositivos.avds, ajustesDeDispositivos());
1725
+ if (pendientes !== undefined && opciones.guardarPuertosAsignados !== undefined) {
1726
+ try {
1727
+ opciones.guardarPuertosAsignados(pendientes);
1728
+ }
1729
+ catch (error) {
1730
+ // No impide emitir: la foto sigue siendo verdad, y sin puerto guardado el AVD usa el
1731
+ // de fábrica, que es lo que había antes. Lo que no se puede es callarlo.
1732
+ informar(`no se pudieron guardar los puertos de los emuladores (${codigoDe(error)})`);
1733
+ }
1734
+ }
1682
1735
  emitir({
1683
- clase: "dispositivos",
1684
- informe: informeDeDispositivos,
1685
- ajustes: ajustesDeDispositivos(),
1736
+ ...mensajeDeDispositivos(informeDeDispositivos),
1686
1737
  ...(ultimoArranque === undefined ? {} : { arranque: ultimoArranque }),
1687
1738
  });
1688
1739
  }
@@ -1733,7 +1784,9 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1733
1784
  }
1734
1785
  arranqueEnVuelo = avd;
1735
1786
  try {
1736
- const r = await arrancar(avd);
1787
+ // «Sin ventana» es un ajuste de ESE AVD (Ajustes → Dispositivos), leído al arrancar y no
1788
+ // del cliente: lo que se pulsa es «Arrancar», y cómo se arranca ya está elegido.
1789
+ const r = await arrancar(avd, { sinVentana: ajustesDeDispositivos().avds?.[avd]?.sinVentana === true });
1737
1790
  ultimoArranque = { avd, ...r };
1738
1791
  }
1739
1792
  catch (error) {
@@ -1798,7 +1851,80 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1798
1851
  ...informeDeDispositivos,
1799
1852
  dispositivos: informeDeDispositivos.dispositivos.map((d) => (d.id === id ? { ...d, verificado } : d)),
1800
1853
  };
1801
- emitir({ clase: "dispositivos", informe: informeDeDispositivos, ajustes: ajustesDeDispositivos() });
1854
+ emitir(mensajeDeDispositivos(informeDeDispositivos));
1855
+ };
1856
+ /**
1857
+ * **Lo de UN AVD: su puerto del túnel y si arranca sin ventana.**
1858
+ *
1859
+ * El nombre llega por el cable, así que tiene que estar en NUESTRA última medida (la misma
1860
+ * guarda que «Arrancar»): guardar ajustes de un AVD que no existe llenaría `settings.json` de
1861
+ * nombres que nadie ve. El puerto pasa por `motivoDePuertoInaceptable` contra los ajustes de
1862
+ * AHORA —dos AVD con el mismo puerto son el fallo que todo esto viene a quitar—, y la negativa
1863
+ * se dice sin guardar NADA del mensaje, tampoco el `sinVentana` que viniera con él: medio
1864
+ * cambio aplicado es un cambio que nadie pidió.
1865
+ *
1866
+ * No se vuelve a medir: el ajuste no cambia la máquina. Se reemite la foto que hay con los
1867
+ * ajustes releídos, que es lo que el campo tiene que enseñar.
1868
+ */
1869
+ const atenderAjusteDeAvd = async (avd, cambio) => {
1870
+ const guardar = opciones.guardarAjusteDeAvd;
1871
+ if (guardar === undefined) {
1872
+ informar("esta ejecución no puede guardar los ajustes de los emuladores");
1873
+ return;
1874
+ }
1875
+ if (informeDeDispositivos === undefined)
1876
+ await atenderDispositivos().catch(contar);
1877
+ if (informeDeDispositivos === undefined || !informeDeDispositivos.avds.includes(avd)) {
1878
+ informar(`no consta ningún AVD llamado «${avd}» en la última medida`);
1879
+ return;
1880
+ }
1881
+ if (cambio.puerto !== undefined) {
1882
+ const motivo = motivoDePuertoInaceptable(cambio.puerto, avd, ajustesDeDispositivos());
1883
+ if (motivo !== undefined) {
1884
+ informar(`no se cambió el puerto de ${avd}: ${motivo}`);
1885
+ return;
1886
+ }
1887
+ }
1888
+ try {
1889
+ guardar(avd, cambio);
1890
+ }
1891
+ catch (error) {
1892
+ informar(`no se pudieron guardar los ajustes de ${avd} (${codigoDe(error)})`);
1893
+ return;
1894
+ }
1895
+ emitir(mensajeDeDispositivos(informeDeDispositivos));
1896
+ };
1897
+ /**
1898
+ * **Parar un emulador**, por su serie de la última medida y solo si ES un emulador: la serie
1899
+ * acaba siendo un argumento de `adb`, y un físico no se «para» desde aquí. Al acabar se mide
1900
+ * SIEMPRE, como al arrancar: la foto es la que dice si se paró.
1901
+ */
1902
+ const atenderParadaDeEmulador = async (id) => {
1903
+ const parar = opciones.pararEmulador;
1904
+ if (parar === undefined) {
1905
+ informar("esta ejecución no puede parar emuladores");
1906
+ return;
1907
+ }
1908
+ if (informeDeDispositivos === undefined)
1909
+ await atenderDispositivos().catch(contar);
1910
+ const dispositivo = informeDeDispositivos?.dispositivos.find((d) => d.id === id);
1911
+ if (dispositivo === undefined || dispositivo.clase !== "emulador") {
1912
+ informar(`no consta ningún emulador «${id}» en la última medida`);
1913
+ return;
1914
+ }
1915
+ try {
1916
+ const r = await parar(dispositivo.id);
1917
+ if (!r.ok)
1918
+ informar(`no se pudo parar ${dispositivo.nombre}: ${r.detalle}`);
1919
+ }
1920
+ catch (error) {
1921
+ // El puerto promete no lanzar; si lo hace, se cuenta y se mide igual.
1922
+ informar(`no se pudo parar ${dispositivo.nombre} (${codigoDe(error)})`);
1923
+ }
1924
+ finally {
1925
+ informeDeDispositivos = undefined;
1926
+ await atenderDispositivos().catch(contar);
1927
+ }
1802
1928
  };
1803
1929
  const atenderCatalogo = async (proveedor) => {
1804
1930
  if (!proveedorConocido(proveedor))
@@ -2927,7 +3053,17 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2927
3053
  // doble mal hecho, o un bug suyo en el camino síncrono— dejaría el recorrido en
2928
3054
  // `corriendo` para siempre y el botón muerto, que es justo lo que este `try` existe para
2929
3055
  // evitar. Su contrato es no lanzar nunca; esto es por si deja de cumplirlo.
2930
- const enCurso = lanzar({ dispositivo: medida.dispositivo, raiz: medida.abierto.raiz, app: medida.app }, { alFase });
3056
+ const enCurso = lanzar({
3057
+ dispositivo: medida.dispositivo,
3058
+ raiz: medida.abierto.raiz,
3059
+ app: medida.app,
3060
+ // El puerto LOCAL del túnel es el de SU AVD: con dos emuladores y el mismo puerto,
3061
+ // el `adb forward` del segundo le quita el túnel al primero sin error. Un físico no
3062
+ // tiene AVD y va por el de fábrica, que es lo que hace el lanzador sin puerto.
3063
+ ...(medida.dispositivo.avd === undefined
3064
+ ? {}
3065
+ : { puerto: puertoDeAvd(ajustesDeDispositivos(), medida.dispositivo.avd) }),
3066
+ }, { alFase });
2931
3067
  trabajo.cancelar = enCurso.cancelar;
2932
3068
  const resultado = await enCurso.terminado;
2933
3069
  // El último SIEMPRE se emite, aunque no haya pasado el plazo: es el que dice cómo acabó.
@@ -3675,40 +3811,25 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3675
3811
  ultimoProgreso = Date.now();
3676
3812
  };
3677
3813
  /**
3678
- * Lanza un paso, o cancela el que corre.
3814
+ * El cerrojo compartido: monta `trabajo`, emite el primer progreso y, al terminar, el último y
3815
+ * una medida nueva. Lo usan los pasos de receta y crear un AVD, que son la misma clase de
3816
+ * trabajo (`sdkmanager`/`avdmanager` sobre el mismo SDK): quien llama ya comprobó que no había
3817
+ * otro en marcha.
3679
3818
  *
3680
- * Al terminar se vuelve a MEDIR, y es lo que decide si el paso queda hecho: lo que diga el
3819
+ * Al terminar se vuelve a MEDIR, y es lo que decide si el trabajo queda hecho: lo que diga el
3681
3820
  * instalador es lo que él cree, y la foto es lo que hay. Es la misma regla que ya sigue
3682
3821
  * `instalar`.
3683
3822
  */
3684
- const atenderReceta = (mensaje) => {
3685
- if (mensaje.accion === "cancelar") {
3686
- trabajo?.cancelar();
3687
- return;
3688
- }
3689
- if (trabajo !== undefined) {
3690
- // Ya hay uno: se reenvía su estado en vez de lanzar otro.
3691
- emitirProgreso("corriendo");
3692
- return;
3693
- }
3694
- const correr = opciones.correrPasoDeReceta;
3695
- if (correr === undefined) {
3696
- informar("esta ejecución no puede ejecutar pasos de instalación");
3697
- return;
3698
- }
3699
- const enMarcha = correr(mensaje.id, mensaje.paso, (linea) => {
3700
- if (trabajo === undefined)
3701
- return;
3702
- trabajo.lineas.push(linea);
3703
- // A ritmo: cada emisión manda la cola entera, y por línea sería cuadrático en bytes
3704
- // con un `sdkmanager` que habla cada pocos milisegundos. Es la misma razón que
3705
- // `MS_ENTRE_PARCIALES` en la piel web.
3706
- if (Date.now() - ultimoProgreso >= MS_ENTRE_PROGRESOS)
3707
- emitirProgreso("corriendo");
3708
- });
3823
+ const correrTrabajo = (receta, paso, enMarcha,
3824
+ /**
3825
+ * Lo que el servidor apunta al acabar (la procedencia de un AVD nuevo, los ajustes de uno
3826
+ * eliminado), ANTES del último progreso y de la medida: así la foto que sigue ya lo trae.
3827
+ * Un fallo suyo se dice y no tumba el cierre del trabajo.
3828
+ */
3829
+ alAcabar) => {
3709
3830
  trabajo = {
3710
- receta: mensaje.id,
3711
- paso: mensaje.paso,
3831
+ receta,
3832
+ paso,
3712
3833
  titulo: enMarcha.titulo,
3713
3834
  lineas: [],
3714
3835
  cancelar: enMarcha.cancelar,
@@ -3717,6 +3838,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3717
3838
  emitirProgreso("corriendo");
3718
3839
  void (async () => {
3719
3840
  const resultado = await enMarcha.terminado;
3841
+ try {
3842
+ alAcabar?.(resultado);
3843
+ }
3844
+ catch (error) {
3845
+ informar(`no se pudo apuntar lo que cambió al acabar (${codigoDe(error)})`);
3846
+ }
3720
3847
  // El último progreso SIEMPRE se emite, aunque no haya pasado el plazo: es el que
3721
3848
  // completa el log y el que dice cómo acabó.
3722
3849
  ultimoProgreso = 0;
@@ -3727,6 +3854,145 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3727
3854
  await atenderDispositivos().catch(contar);
3728
3855
  })();
3729
3856
  };
3857
+ /**
3858
+ * Cada línea de la salida, a la cola del trabajo en curso y emitida A RITMO: cada emisión manda
3859
+ * la cola entera, y por línea sería cuadrático en bytes con un `sdkmanager` que habla cada
3860
+ * pocos milisegundos. Es la misma razón que `MS_ENTRE_PARCIALES` en la piel web.
3861
+ */
3862
+ const alSalirLineaDelTrabajo = (linea) => {
3863
+ if (trabajo === undefined)
3864
+ return;
3865
+ trabajo.lineas.push(linea);
3866
+ if (Date.now() - ultimoProgreso >= MS_ENTRE_PROGRESOS)
3867
+ emitirProgreso("corriendo");
3868
+ };
3869
+ /** Lanza un paso, o cancela el que corre. */
3870
+ const atenderReceta = (mensaje) => {
3871
+ if (mensaje.accion === "cancelar") {
3872
+ trabajo?.cancelar();
3873
+ return;
3874
+ }
3875
+ if (trabajo !== undefined) {
3876
+ // Ya hay uno: se reenvía su estado en vez de lanzar otro.
3877
+ emitirProgreso("corriendo");
3878
+ return;
3879
+ }
3880
+ const correr = opciones.correrPasoDeReceta;
3881
+ if (correr === undefined) {
3882
+ informar("esta ejecución no puede ejecutar pasos de instalación");
3883
+ return;
3884
+ }
3885
+ correrTrabajo(mensaje.id, mensaje.paso, correr(mensaje.id, mensaje.paso, alSalirLineaDelTrabajo));
3886
+ };
3887
+ /** ¿Corre ahora el AVD `avd`, según la ÚLTIMA medida? Todo lo que no es «apagado» o «no disponible» cuenta. */
3888
+ const baseEnMarcha = (avd) => (informeDeDispositivos?.dispositivos ?? []).some((d) => d.avd === avd && d.estado !== "apagado" && d.estado !== "no-disponible");
3889
+ /**
3890
+ * **Crear un AVD con nombre**, por el MISMO cerrojo que las recetas y contado igual
3891
+ * (`receta: "crear-avd"`, `paso: 0`): la pantalla ya sabe pintar ese log.
3892
+ *
3893
+ * El nombre acaba siendo un argumento de `avdmanager`, así que se valida aquí con la regla
3894
+ * pura (forma, largo, y que no exista ya en la última medida) antes de lanzar nada; sin medida
3895
+ * no se puede saber si se repite, así que se mide primero. El cerrojo se vuelve a mirar
3896
+ * DESPUÉS de esa espera: mientras se medía pudo arrancar otro trabajo.
3897
+ */
3898
+ const atenderCrearEmulador = async (nombre, base, conDatos) => {
3899
+ if (trabajo !== undefined) {
3900
+ emitirProgreso("corriendo");
3901
+ return;
3902
+ }
3903
+ const crear = opciones.crearAvd;
3904
+ if (crear === undefined) {
3905
+ informar("esta ejecución no puede crear emuladores");
3906
+ return;
3907
+ }
3908
+ if (informeDeDispositivos === undefined)
3909
+ await atenderDispositivos().catch(contar);
3910
+ if (informeDeDispositivos === undefined) {
3911
+ informar("no se creó el emulador: todavía no hay una medida con la que comprobar el nombre");
3912
+ return;
3913
+ }
3914
+ const motivo = motivoDeNombreDeAvdInaceptable(nombre, informeDeDispositivos.avds);
3915
+ if (motivo !== undefined) {
3916
+ informar(`no se creó el emulador: ${motivo}`);
3917
+ return;
3918
+ }
3919
+ if (base !== undefined) {
3920
+ // La base es un NOMBRE que acaba en una ruta: tiene que ser uno de los que la medida vio.
3921
+ if (!informeDeDispositivos.avds.includes(base)) {
3922
+ informar(`no se creó el emulador: ${base} no es un emulador que conozcamos`);
3923
+ return;
3924
+ }
3925
+ // Clonar copia discos que la base tiene bloqueados mientras corre (`multiinstance.lock`);
3926
+ // copiar solo la configuración no los toca y vale con ella encendida.
3927
+ if (conDatos === true && baseEnMarcha(base)) {
3928
+ informar(`no se creó el emulador: ${base} está en marcha; apágalo para clonarlo con lo instalado`);
3929
+ return;
3930
+ }
3931
+ }
3932
+ if (trabajo !== undefined) {
3933
+ emitirProgreso("corriendo");
3934
+ return;
3935
+ }
3936
+ correrTrabajo("crear-avd", 0, crear(nombre, alSalirLineaDelTrabajo, base === undefined ? undefined : { base, conDatos: conDatos === true }),
3937
+ // La procedencia se GUARDA al acabar bien: «copia de X» (configuración) o «clon de X» (con lo
3938
+ // instalado). Sin base no hay de quién, y un fallo no deja AVD al que apuntarla.
3939
+ base === undefined
3940
+ ? undefined
3941
+ : (r) => {
3942
+ if (r.estado !== "ok")
3943
+ return;
3944
+ opciones.guardarAjusteDeAvd?.(nombre, { copiaDe: base, ...(conDatos === true ? { clon: true } : {}) });
3945
+ });
3946
+ };
3947
+ /**
3948
+ * **Eliminar un AVD**, por el MISMO cerrojo que crear (`receta: "borrar-avd"`, `paso: 0`). El
3949
+ * nombre llega por el cable y acaba en un argumento de `avdmanager`, así que tiene que ser uno de
3950
+ * los de NUESTRA última medida. Tres negativas, en orden, y cada una se DICE (la del navegador no
3951
+ * llega de vuelta: el cliente aplica las mismas reglas antes y no deja pulsar):
3952
+ * - nunca el ÚLTIMO: es la base de las copias, y siempre tiene que quedar uno;
3953
+ * - nunca uno EN MARCHA (el mismo criterio que `baseEnMarcha`): el emulador tiene sus discos
3954
+ * bloqueados y borrar la carpeta por debajo lo dejaría roto.
3955
+ * Al acabar BIEN se quitan también sus ajustes (puerto, sin ventana, procedencia) para que su
3956
+ * puerto quede libre, y `correrTrabajo` vuelve a medir.
3957
+ */
3958
+ const atenderEliminarEmulador = async (avd) => {
3959
+ if (trabajo !== undefined) {
3960
+ emitirProgreso("corriendo");
3961
+ return;
3962
+ }
3963
+ const borrar = opciones.borrarAvd;
3964
+ if (borrar === undefined) {
3965
+ informar("esta ejecución no puede eliminar emuladores");
3966
+ return;
3967
+ }
3968
+ if (informeDeDispositivos === undefined)
3969
+ await atenderDispositivos().catch(contar);
3970
+ if (informeDeDispositivos === undefined) {
3971
+ informar("no se eliminó el emulador: todavía no hay una medida con la que comprobarlo");
3972
+ return;
3973
+ }
3974
+ if (!informeDeDispositivos.avds.includes(avd)) {
3975
+ informar(`no se eliminó el emulador: ${avd} no es un emulador que conozcamos`);
3976
+ return;
3977
+ }
3978
+ if (informeDeDispositivos.avds.length <= 1) {
3979
+ informar(`no se eliminó ${avd}: es el único: siempre tiene que quedar al menos uno`);
3980
+ return;
3981
+ }
3982
+ if (baseEnMarcha(avd)) {
3983
+ informar(`no se eliminó ${avd}: está en marcha; apágalo para eliminarlo`);
3984
+ return;
3985
+ }
3986
+ // Mientras se medía pudo arrancar otro trabajo.
3987
+ if (trabajo !== undefined) {
3988
+ emitirProgreso("corriendo");
3989
+ return;
3990
+ }
3991
+ correrTrabajo("borrar-avd", 0, borrar(avd, alSalirLineaDelTrabajo), (r) => {
3992
+ if (r.estado === "ok")
3993
+ opciones.quitarAjustesDeAvd?.(avd);
3994
+ });
3995
+ };
3730
3996
  /**
3731
3997
  * Los modelos de un motor externo, para el desplegable de un subagente.
3732
3998
  *
@@ -4896,6 +5162,51 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
4896
5162
  respuesta.end();
4897
5163
  return;
4898
5164
  }
5165
+ if (typeof mensaje === "object" &&
5166
+ mensaje !== null &&
5167
+ mensaje.clase === "eliminarEmulador" &&
5168
+ typeof mensaje.avd === "string") {
5169
+ void atenderEliminarEmulador(mensaje.avd).catch(contar);
5170
+ respuesta.writeHead(204);
5171
+ respuesta.end();
5172
+ return;
5173
+ }
5174
+ if (typeof mensaje === "object" &&
5175
+ mensaje !== null &&
5176
+ mensaje.clase === "crearEmulador" &&
5177
+ typeof mensaje.nombre === "string" &&
5178
+ (mensaje.base === undefined || typeof mensaje.base === "string") &&
5179
+ (mensaje.conDatos === undefined || typeof mensaje.conDatos === "boolean")) {
5180
+ // Suelto, como la receta: el progreso y la foto de después viajan por el SSE.
5181
+ void atenderCrearEmulador(mensaje.nombre, mensaje.base, mensaje.conDatos).catch(contar);
5182
+ respuesta.writeHead(204);
5183
+ respuesta.end();
5184
+ return;
5185
+ }
5186
+ if (typeof mensaje === "object" &&
5187
+ mensaje !== null &&
5188
+ mensaje.clase === "ajusteDeAvd" &&
5189
+ typeof mensaje.avd === "string" &&
5190
+ (mensaje.puerto === undefined || typeof mensaje.puerto === "number") &&
5191
+ (mensaje.sinVentana === undefined || typeof mensaje.sinVentana === "boolean")) {
5192
+ // Campo a campo: lo que no es un número o un booleano no llega a `settings.json`.
5193
+ void atenderAjusteDeAvd(mensaje.avd, {
5194
+ ...(mensaje.puerto === undefined ? {} : { puerto: mensaje.puerto }),
5195
+ ...(mensaje.sinVentana === undefined ? {} : { sinVentana: mensaje.sinVentana }),
5196
+ }).catch(contar);
5197
+ respuesta.writeHead(204);
5198
+ respuesta.end();
5199
+ return;
5200
+ }
5201
+ if (typeof mensaje === "object" &&
5202
+ mensaje !== null &&
5203
+ mensaje.clase === "pararEmulador" &&
5204
+ typeof mensaje.id === "string") {
5205
+ void atenderParadaDeEmulador(mensaje.id).catch(contar);
5206
+ respuesta.writeHead(204);
5207
+ respuesta.end();
5208
+ return;
5209
+ }
4899
5210
  /**
4900
5211
  * ANTES del `recibir` de la consola, y también antes que las demás clases de tarea: por
4901
5212
  * aquí no entra nada hacia ningún turno. Ver `atenderMirar`.
@@ -4912,6 +5223,13 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
4912
5223
  return;
4913
5224
  }
4914
5225
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "tarea") {
5226
+ if (mensaje.accion === "crear" && mudandoWorkspace) {
5227
+ // Una tarea creada a mitad de la mudanza guardaría la raíz VIEJA, que está a punto de
5228
+ // borrarse. Se niega con motivo, como cualquier otra negativa del cable.
5229
+ respuesta.writeHead(409, { "content-type": "application/json" });
5230
+ respuesta.end(JSON.stringify({ motivo: "se está cambiando la carpeta de los proyectos: crea la tarea cuando termine" }));
5231
+ return;
5232
+ }
4915
5233
  if (mensaje.accion === "crear" && opciones.colaDeTareas !== undefined) {
4916
5234
  const creada = atenderCrearTarea(mensaje.proyecto, mensaje.peticion, mensaje.encargo, mensaje.borrador);
4917
5235
  if (creada !== undefined) {
@@ -4972,9 +5290,84 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
4972
5290
  * Y se reemite SIEMPRE, se haya escrito o no: con el valor nuevo cuando valió y con el
4973
5291
  * de antes cuando no, así el campo acaba enseñando lo que hay en disco y no lo que se
4974
5292
  * tecleó. Un rechazo mudo dejaría la pantalla afirmando un ajuste que no está puesto.
5293
+ *
5294
+ * **Y cambiarla MUDA lo que ya está bajado**, en dos actos (`vestibulo.mudarWorkspace`):
5295
+ * `planear` contesta en la PROPIA respuesta qué se movería —por nombre y entorno, nunca
5296
+ * una ruta— o por qué no se puede, sin tocar nada; `aplicar` (o sin `accion`) contesta
5297
+ * 204 en el acto y el recorrido llega por `mudanzaDeWorkspace`, porque copiar y
5298
+ * verificar cientos de megas no cabe en una petición. La negativa que se sabe ANTES
5299
+ * de empezar —una ruta que no vale, una mudanza ya en marcha— va en un 409 con motivo.
4975
5300
  */
4976
- opciones.guardarWorkspace?.(mensaje.ruta);
4977
- emitirWorkspace();
5301
+ const negarWorkspace = (motivo) => {
5302
+ respuesta.writeHead(409, { "content-type": "application/json" });
5303
+ respuesta.end(JSON.stringify({ motivo }));
5304
+ };
5305
+ const tareasVivas = () => (opciones.colaDeTareas?.listar() ?? []).map((t) => ({ estado: t.estado, raiz: t.proyecto.raiz }));
5306
+ const resuelta = opciones.resolverWorkspace?.(mensaje.ruta);
5307
+ if (resuelta === undefined || opciones.casaXonecode === undefined) {
5308
+ // Esta ejecución no sabe mudar: el comportamiento de siempre, solo escribir el ajuste.
5309
+ opciones.guardarWorkspace?.(mensaje.ruta);
5310
+ emitirWorkspace();
5311
+ respuesta.writeHead(204);
5312
+ respuesta.end();
5313
+ return;
5314
+ }
5315
+ if ("motivo" in resuelta) {
5316
+ emitirWorkspace();
5317
+ negarWorkspace(resuelta.motivo);
5318
+ return;
5319
+ }
5320
+ if (mensaje.accion === "planear") {
5321
+ const plan = vestibulo.planearWorkspace(resuelta.ruta, opciones.casaXonecode, tareasVivas());
5322
+ let megas;
5323
+ if (plan.motivo === undefined && plan.mudanzas.length > 0) {
5324
+ megas = await megasDeLasMudanzas(plan.mudanzas).catch(() => undefined);
5325
+ }
5326
+ respuesta.writeHead(200, { "content-type": "application/json" });
5327
+ respuesta.end(JSON.stringify({
5328
+ proyectos: plan.mudanzas.map((m) => ({ entorno: m.entorno, proyecto: m.proyecto })),
5329
+ ...(megas === undefined ? {} : { megas }),
5330
+ ...(plan.motivo === undefined ? {} : { motivo: plan.motivo }),
5331
+ }));
5332
+ return;
5333
+ }
5334
+ if (mudandoWorkspace) {
5335
+ negarWorkspace("ya hay un cambio de carpeta en marcha: espera a que termine");
5336
+ return;
5337
+ }
5338
+ mudandoWorkspace = true;
5339
+ const casaXonecode = opciones.casaXonecode;
5340
+ const guardar = opciones.guardarWorkspace ?? (() => { });
5341
+ void (async () => {
5342
+ try {
5343
+ const { resultado, cerroLaAbierta } = await vestibulo.mudarWorkspace({
5344
+ hacia: resuelta.ruta,
5345
+ casaXonecode,
5346
+ tareas: tareasVivas,
5347
+ guardar,
5348
+ progreso: (progreso) => {
5349
+ ultimoProgresoDeMudanza = progreso;
5350
+ emitir({ clase: "mudanzaDeWorkspace", progreso });
5351
+ },
5352
+ });
5353
+ // El cable se quedaría enganchado a una consola cerrada: al vestíbulo, como al
5354
+ // borrar una copia. El alta siguiente ya trae las copias en la base nueva.
5355
+ if (cerroLaAbierta)
5356
+ adjuntar();
5357
+ emitir({ clase: "mudanzaDeWorkspace", resultado });
5358
+ }
5359
+ catch (error) {
5360
+ contar(error);
5361
+ emitir({ clase: "mudanzaDeWorkspace", resultado: { estado: "rechazado", motivo: "no se pudo cambiar la carpeta" } });
5362
+ }
5363
+ finally {
5364
+ mudandoWorkspace = false;
5365
+ ultimoProgresoDeMudanza = undefined;
5366
+ emitirWorkspace();
5367
+ opciones.revisarTareas?.();
5368
+ await anunciarAlta().catch(contar);
5369
+ }
5370
+ })().catch(contar);
4978
5371
  respuesta.writeHead(204);
4979
5372
  respuesta.end();
4980
5373
  return;
@@ -5181,6 +5574,10 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
5181
5574
  // Suelto: `anunciarAlta` consulta CloudStudio y el `POST` no puede quedarse
5182
5575
  // esperando por eso. El alta nueva llega por el SSE, como siempre.
5183
5576
  void anunciarAlta().catch(contar);
5577
+ // Y la foto que hay, sin volver a medir: lo que cambió es QUIÉN usa ese aparato, y
5578
+ // Ajustes lo enseña al lado de cada uno (`enUso`).
5579
+ if (informeDeDispositivos !== undefined)
5580
+ emitir(mensajeDeDispositivos(informeDeDispositivos));
5184
5581
  }
5185
5582
  respuesta.writeHead(204);
5186
5583
  respuesta.end();
@@ -5704,6 +6101,12 @@ export function ajusteDeWorkspaceCableado(opciones) {
5704
6101
  const guardar = opciones.guardar ?? ((ruta) => void guardarWorkspaceEnDisco(undefined, ruta));
5705
6102
  return {
5706
6103
  workspace: () => leer() ?? baseDeWorkspacePorOmision(),
6104
+ casaXonecode: join(opciones.casa, ".xonecode"),
6105
+ resolverWorkspace: (ruta) => {
6106
+ const absoluta = expandirConCasa(ruta, opciones.casa);
6107
+ const motivo = motivoDeWorkspaceInaceptable(absoluta);
6108
+ return motivo === undefined ? { ruta: absoluta } : { motivo };
6109
+ },
5707
6110
  guardarWorkspace: (ruta) => {
5708
6111
  const absoluta = expandirConCasa(ruta, opciones.casa);
5709
6112
  if (motivoDeWorkspaceInaceptable(absoluta) !== undefined)
@@ -5712,6 +6115,31 @@ export function ajusteDeWorkspaceCableado(opciones) {
5712
6115
  },
5713
6116
  };
5714
6117
  }
6118
+ /**
6119
+ * Los siete puertos de los emuladores, cableados con las funciones de verdad — extraídos por el
6120
+ * MISMO motivo que `ajusteDeWorkspaceCableado`: dentro de `arrancarConsolaWeb` serían un literal
6121
+ * que ningún test mira, y cada uno es un campo OPCIONAL de `montarRutas`, así que olvidarlo o
6122
+ * perder un argumento por el camino compila y pasa.
6123
+ *
6124
+ * `casa` es la del `settings.json` donde se guardan los ajustes por AVD; ausente, la del usuario
6125
+ * (`guardarAjusteDeAvd(undefined, …)`). `arrancar` entra solo para que un test vea qué recibe la
6126
+ * de verdad sin lanzar un emulador.
6127
+ */
6128
+ export function emuladoresCableados(opciones = {}) {
6129
+ const arrancar = opciones.arrancar ?? arrancarEmulador;
6130
+ return {
6131
+ arrancarEmulador: (avd, o) => arrancar(avd, {}, o ?? {}),
6132
+ // Con `conDatos` se CLONA la carpeta; sin él, solo la configuración de la base sobre un AVD vacío.
6133
+ crearAvd: (nombre, alSalirLinea, desde) => desde?.conDatos === true
6134
+ ? clonarAvd(desde.base, nombre, { alSalirLinea })
6135
+ : crearAvd(nombre, { alSalirLinea }, desde === undefined ? {} : { base: desde.base }),
6136
+ borrarAvd: (nombre, alSalirLinea) => borrarAvd(nombre, { alSalirLinea }),
6137
+ pararEmulador: (serie) => pararEmulador(serie),
6138
+ guardarAjusteDeAvd: (avd, cambio) => void guardarAjusteDeAvd(opciones.casa, avd, cambio),
6139
+ quitarAjustesDeAvd: (avd) => void quitarAjustesDeAvd(opciones.casa, avd),
6140
+ guardarPuertosAsignados: (asignados) => void guardarPuertosAsignados(opciones.casa, asignados),
6141
+ };
6142
+ }
5715
6143
  /**
5716
6144
  * Las dos mitades de la casilla «Depurar», cableadas — extraídas por el MISMO motivo que
5717
6145
  * `ajusteDeWorkspaceCableado`. `leer`/`guardar` entran por parámetro, y no como en el workspace
@@ -6198,7 +6626,12 @@ export async function arrancarConsolaWeb(opciones) {
6198
6626
  // nueve veces, y la que el comentario de justo abajo describe para las cuatro del
6199
6627
  // lanzamiento. `montarRutas` lo trata como ausente si falta, así que olvidarlo aquí
6200
6628
  // dejaría el botón diciendo «esta ejecución no puede arrancar emuladores» con todo verde.
6201
- arrancarEmulador: (avd) => arrancarEmulador(avd),
6629
+ // Los cinco de los emuladores —arrancar, crear, parar, y guardar lo de cada AVD—, con las
6630
+ // funciones REALES y compuestos fuera (`emuladoresCableados`) para que la composición tenga
6631
+ // test: `arrancarEmulador: (avd) => arrancarEmulador(avd)` compilaba igual perdiendo el
6632
+ // «sin ventana», y un campo opcional olvidado aquí deja el botón diciendo «esta ejecución no
6633
+ // puede» con todo en verde.
6634
+ ...emuladoresCableados(),
6202
6635
  /**
6203
6636
  * Las cuatro de «¿se puede lanzar y lánzalo?», compuestas con las funciones REALES.
6204
6637
  *