xonecode 0.15.0 → 0.17.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.
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/apps/web/dist/assets/index-CnskHXof.js +485 -0
- package/apps/web/dist/assets/index-DLfF2uEK.css +1 -0
- package/apps/web/dist/assets/{php-CdRBnJRh.js → php-8TR3jUGL.js} +1 -1
- package/apps/web/dist/assets/{ruby-DRIeg398.js → ruby-IynpAZS-.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/dist/agent/conectores/gestorJira.js +164 -0
- package/dist/agent/conectores/gestorJira.js.map +1 -0
- package/dist/agent/conectores/gestorNotion.js +365 -0
- package/dist/agent/conectores/gestorNotion.js.map +1 -0
- package/dist/agent/conectores/servicioDeConectores.js +135 -34
- package/dist/agent/conectores/servicioDeConectores.js.map +1 -1
- package/dist/agent/config/configEnDisco.js +28 -0
- package/dist/agent/config/configEnDisco.js.map +1 -1
- package/dist/agent/config/selectorEnMaquina.js +9 -2
- package/dist/agent/config/selectorEnMaquina.js.map +1 -1
- package/dist/agent/config/settingsEnDisco.js +41 -1
- package/dist/agent/config/settingsEnDisco.js.map +1 -1
- package/dist/agent/sesiones/checkpointer.js +18 -0
- package/dist/agent/sesiones/checkpointer.js.map +1 -1
- package/dist/agent/sesiones/modeloDelCambio.js +5 -2
- package/dist/agent/sesiones/modeloDelCambio.js.map +1 -1
- package/dist/agent/sesiones/sesionGit.js +5 -1
- package/dist/agent/sesiones/sesionGit.js.map +1 -1
- package/dist/core/config.js +71 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/gestorDeTareas.js +202 -0
- package/dist/core/gestorDeTareas.js.map +1 -0
- package/dist/core/ports.js +43 -0
- package/dist/core/ports.js.map +1 -1
- package/dist/core/selectorDeCarpeta.js +12 -2
- package/dist/core/selectorDeCarpeta.js.map +1 -1
- package/dist/core/settings.js +38 -4
- package/dist/core/settings.js.map +1 -1
- package/dist/web/servidor/arranque.js +840 -36
- package/dist/web/servidor/arranque.js.map +1 -1
- package/dist/web/servidor/datosDeCierre.js +37 -0
- package/dist/web/servidor/datosDeCierre.js.map +1 -0
- package/dist/web/servidor/sesiones.js +20 -0
- package/dist/web/servidor/sesiones.js.map +1 -1
- package/dist/web/servidor/transporte.js.map +1 -1
- package/dist/web/servidor/vestibulo.js +79 -5
- package/dist/web/servidor/vestibulo.js.map +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/index-CBOJEbAY.css +0 -1
- package/apps/web/dist/assets/index-CNZQV957.js +0 -485
|
@@ -55,18 +55,18 @@ import { PLATAFORMAS_DE_DISPOSITIVO } from "../../core/settings.js";
|
|
|
55
55
|
import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
|
|
56
56
|
import { arrancarEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
|
|
57
57
|
import { correrPasoDeReceta } from "../../agent/dispositivos/instalacionEnMaquina.js";
|
|
58
|
-
import { abrirCarpetaDelSistema } from "../../agent/config/selectorEnMaquina.js";
|
|
58
|
+
import { abrirCarpetaDelSistema, abrirDirectorioDelSistema } from "../../agent/config/selectorEnMaquina.js";
|
|
59
59
|
import { modelosDeMotor } from "../../agent/config/modelosDeMotor.js";
|
|
60
60
|
import { parsear, compatibleConOpenAi, esProveedorPersonalizado, idDeProveedorPersonalizado, motivoDeEndpointInaceptable, motivoDeSlugInaceptable, nombreDeProveedor, PROVEEDORES, slugDesdeNombre, resolver, SIN_CREDENCIAL, PAPELES, } from "../../core/modelos.js";
|
|
61
61
|
import { hayCredencial } from "../../cli/consola.js";
|
|
62
62
|
import { motivoDeClaveInaceptable } from "../../core/config.js";
|
|
63
|
-
import { aplicarAuth, aplicarCredencialAlProceso, cargar, guardarModeloGlobal, } from "../../agent/config/configEnDisco.js";
|
|
63
|
+
import { aplicarAuth, aplicarCredencialAlProceso, cargar, guardarConectoresDeProyecto, guardarGestorDeProyecto, guardarModeloGlobal, } from "../../agent/config/configEnDisco.js";
|
|
64
64
|
import { borrarCredencial, guardarCredencial } from "../../agent/config/authEnDisco.js";
|
|
65
65
|
import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/config/configEnDisco.js";
|
|
66
66
|
import { crearCheckpointerDeProyecto, mantenimientoDelCheckpointer, } from "../../agent/sesiones/checkpointer.js";
|
|
67
67
|
import { resumenDePoda } from "../../core/podaDeCheckpoint.js";
|
|
68
68
|
import { cargarSettings, guardarConcurrenciaDeTareas, guardarDepurar, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarEntorno as guardarEntornoEnDisco, olvidarEntornoDeSettings, copiasDeEntorno, borrarCopiasDeEntorno, } from "../../agent/config/settingsEnDisco.js";
|
|
69
|
-
import { dentroDelWorkspace, depuracionActiva, expandirConCasa, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
|
|
69
|
+
import { dentroDelWorkspace, depuracionActiva, expandirConCasa, mismaRuta, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
|
|
70
70
|
import { esModoDeEscritura, MODO_POR_OMISION, } from "../../core/modoDeEscritura.js";
|
|
71
71
|
import { mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
|
|
72
72
|
import { elegirCarpetaEnMaquina, haySelectorDeCarpeta } from "../../agent/config/selectorEnMaquina.js";
|
|
@@ -107,8 +107,12 @@ import { baseDeWorkspaceLegada, baseDeWorkspacePorOmision, conexionDeVestibulo,
|
|
|
107
107
|
// doblan.
|
|
108
108
|
import { filaDeTarea } from "./transporte.js";
|
|
109
109
|
import { esEsfuerzo, nivelesDeEsfuerzo } from "../../core/esfuerzo.js";
|
|
110
|
-
import { definicionDelCable, RUTA_CALLBACK_MCP } from "../../core/conectores.js";
|
|
110
|
+
import { CATALOGO_DE_CONECTORES, definicionDelCable, RUTA_CALLBACK_MCP } from "../../core/conectores.js";
|
|
111
111
|
import { servicioDeConectoresCableado } from "../../agent/conectores/servicioDeConectores.js";
|
|
112
|
+
import { crearGestorJira } from "../../agent/conectores/gestorJira.js";
|
|
113
|
+
import { crearGestorNotion } from "../../agent/conectores/gestorNotion.js";
|
|
114
|
+
import { admiteMiasDelVinculo, motivoDeProyectoInaceptable, transicionPropuesta, comentarioDeCierre, } from "../../core/gestorDeTareas.js";
|
|
115
|
+
import { datosDeCierre } from "./datosDeCierre.js";
|
|
112
116
|
/** Las dos rutas del cable. El cliente las tiene escritas en `apps/web/src/conexion.ts`. */
|
|
113
117
|
/**
|
|
114
118
|
* Cuánto se espera al aviso de «lo que ya había sin commitear» antes de anunciar sin él.
|
|
@@ -208,6 +212,86 @@ function tipoServido(mime) {
|
|
|
208
212
|
function escaparHtml(texto) {
|
|
209
213
|
return texto.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
210
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* El texto con cada cosa con forma de CORREO cambiada por «[correo]». Para lo que cruza el cable
|
|
217
|
+
* desde el gestor de tareas: ni correos por el cable (IXCODE-11), y el texto de error de una tool
|
|
218
|
+
* de Jira es suyo, no nuestro. La forma es deliberadamente ANCHA —algo@algo.algo sin espacios—:
|
|
219
|
+
* tachar de más en un mensaje de error cuesta poco; dejar pasar un correo, no.
|
|
220
|
+
*/
|
|
221
|
+
export function sinCorreos(texto) {
|
|
222
|
+
return texto.replace(/[^\s@<>()"'«»]+@[^\s@<>()"'«»]+\.[^\s@<>()"'«»]+/g, "[correo]");
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* El mensaje `gestor` con los campos que su ACCIÓN pide, o `undefined`. Llega del navegador sin
|
|
226
|
+
* tipos: `atenderGestor` confía en esta forma, así que se comprueba aquí y no dentro.
|
|
227
|
+
*/
|
|
228
|
+
function gestorBienFormado(m) {
|
|
229
|
+
const x = m;
|
|
230
|
+
const texto = (k) => typeof x[k] === "string";
|
|
231
|
+
switch (x.accion) {
|
|
232
|
+
case "estado":
|
|
233
|
+
return { clase: "gestor", accion: "estado" };
|
|
234
|
+
case "desvincular":
|
|
235
|
+
return { clase: "gestor", accion: "desvincular" };
|
|
236
|
+
case "sitios":
|
|
237
|
+
return texto("conector") ? { clase: "gestor", accion: "sitios", conector: x.conector } : undefined;
|
|
238
|
+
case "proyectos":
|
|
239
|
+
return texto("conector") && texto("sitio")
|
|
240
|
+
? { clase: "gestor", accion: "proyectos", conector: x.conector, sitio: x.sitio }
|
|
241
|
+
: undefined;
|
|
242
|
+
case "buscarProyectos":
|
|
243
|
+
return texto("conector") && texto("texto")
|
|
244
|
+
? { clase: "gestor", accion: "buscarProyectos", conector: x.conector, texto: x.texto }
|
|
245
|
+
: undefined;
|
|
246
|
+
case "describir":
|
|
247
|
+
return texto("conector") && texto("proyecto")
|
|
248
|
+
? { clase: "gestor", accion: "describir", conector: x.conector, proyecto: x.proyecto }
|
|
249
|
+
: undefined;
|
|
250
|
+
case "vincular":
|
|
251
|
+
return texto("conector") && texto("sitio") && texto("proyecto")
|
|
252
|
+
? { clase: "gestor", accion: "vincular", conector: x.conector, sitio: x.sitio, proyecto: x.proyecto }
|
|
253
|
+
: undefined;
|
|
254
|
+
case "usarConector":
|
|
255
|
+
return texto("conector") && typeof x.usar === "boolean"
|
|
256
|
+
? { clase: "gestor", accion: "usarConector", conector: x.conector, usar: x.usar }
|
|
257
|
+
: undefined;
|
|
258
|
+
case "pendientes":
|
|
259
|
+
if (x.texto !== undefined && !texto("texto"))
|
|
260
|
+
return undefined;
|
|
261
|
+
if (x.mias !== undefined && typeof x.mias !== "boolean")
|
|
262
|
+
return undefined;
|
|
263
|
+
return {
|
|
264
|
+
clase: "gestor",
|
|
265
|
+
accion: "pendientes",
|
|
266
|
+
...(x.texto === undefined ? {} : { texto: x.texto }),
|
|
267
|
+
...(x.mias === true ? { mias: true } : {}),
|
|
268
|
+
};
|
|
269
|
+
case "ficha":
|
|
270
|
+
return texto("clave") ? { clase: "gestor", accion: "ficha", clave: x.clave } : undefined;
|
|
271
|
+
case "transiciones":
|
|
272
|
+
return texto("clave") && (x.para === "empezar" || x.para === "cerrar")
|
|
273
|
+
? { clase: "gestor", accion: "transiciones", clave: x.clave, para: x.para }
|
|
274
|
+
: undefined;
|
|
275
|
+
case "empezar": {
|
|
276
|
+
if (!texto("clave"))
|
|
277
|
+
return undefined;
|
|
278
|
+
if (x.transicion !== undefined && !texto("transicion"))
|
|
279
|
+
return undefined;
|
|
280
|
+
return { clase: "gestor", accion: "empezar", clave: x.clave, ...(x.transicion === undefined ? {} : { transicion: x.transicion }) };
|
|
281
|
+
}
|
|
282
|
+
case "borradorDeCierre":
|
|
283
|
+
return { clase: "gestor", accion: "borradorDeCierre" };
|
|
284
|
+
case "cerrar": {
|
|
285
|
+
if (!texto("comentario"))
|
|
286
|
+
return undefined;
|
|
287
|
+
if (x.transicion !== undefined && !texto("transicion"))
|
|
288
|
+
return undefined;
|
|
289
|
+
return { clase: "gestor", accion: "cerrar", comentario: x.comentario, ...(x.transicion === undefined ? {} : { transicion: x.transicion }) };
|
|
290
|
+
}
|
|
291
|
+
default:
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
211
295
|
/**
|
|
212
296
|
* Monta `/eventos` y `/accion` sobre un servidor ya levantado.
|
|
213
297
|
*
|
|
@@ -279,6 +363,13 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
279
363
|
let proyectoElegido;
|
|
280
364
|
let proyectos = [];
|
|
281
365
|
let ramas = [];
|
|
366
|
+
/**
|
|
367
|
+
* De QUÉ proyecto son las `ramas` (su id). Viaja con ellas en el alta: sin esto la ventana de
|
|
368
|
+
* sesión nueva pintaba las ramas del ÚLTIMO proyecto preguntado mientras llegaban las del
|
|
369
|
+
* nuevo, y se podía empezar con la rama de otro proyecto. Se asigna DESPUÉS de la espera,
|
|
370
|
+
* junto a las ramas, y se vacía ANTES: una consulta que falla no deja la pareja cruzada.
|
|
371
|
+
*/
|
|
372
|
+
let ramasDe;
|
|
282
373
|
/**
|
|
283
374
|
* El paso de cuenta ya conducido en ESTE proceso. Hace falta porque `origenDeTrabajo` se
|
|
284
375
|
* congela al construir el vestíbulo: `pasosPendientes()` seguiría diciendo «cuenta»
|
|
@@ -448,6 +539,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
448
539
|
// Solo si el entorno lo dice: ausente es «no lo he elegido», y el cliente aplica su
|
|
449
540
|
// omisión. Mandar `[]` en su lugar sería decir «ninguno», que es otra cosa.
|
|
450
541
|
...(e.proyectos === undefined ? {} : { proyectos: [...e.proyectos] }),
|
|
542
|
+
// Los fijados, con la misma regla: sin lista no viaja nada.
|
|
543
|
+
...(e.fijados === undefined || e.fijados.length === 0 ? {} : { fijados: [...e.fijados] }),
|
|
451
544
|
// Cuántas copias tiene bajadas: lo que la casilla de «borrarlas también» cuenta.
|
|
452
545
|
...(() => {
|
|
453
546
|
const copias = copiasDeEntorno(opciones.workspace?.() ?? baseDeWorkspacePorOmision(), e.id);
|
|
@@ -481,9 +574,20 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
481
574
|
// qué enseña la ventana de sesión nueva, y decidirlo en el cliente exigiría
|
|
482
575
|
// que supiera dónde vive la copia local.
|
|
483
576
|
...(hayCopiaLocal(p.nombre) ? { local: true } : {}),
|
|
577
|
+
// La rama de la que se BAJÓ la copia (`config.json`), que la barra pinta debajo del
|
|
578
|
+
// nombre. Solo con copia: sin ella no hay rama de nadie, y leer el fichero de una
|
|
579
|
+
// carpeta a medias diría la de un alta que no terminó.
|
|
580
|
+
...(() => {
|
|
581
|
+
if (!hayCopiaLocal(p.nombre))
|
|
582
|
+
return {};
|
|
583
|
+
const raiz = raizDeProyectoOSilencio(p.nombre);
|
|
584
|
+
const rama = raiz === undefined ? undefined : cloudstudioDelProyecto(raiz)?.rama;
|
|
585
|
+
return typeof rama === "string" && rama !== "" ? { rama } : {};
|
|
586
|
+
})(),
|
|
484
587
|
};
|
|
485
588
|
}),
|
|
486
589
|
ramas,
|
|
590
|
+
...(ramasDe === undefined || ramas.length === 0 ? {} : { ramasDe }),
|
|
487
591
|
proyectoAbierto,
|
|
488
592
|
...(modo === undefined ? {} : { modo }),
|
|
489
593
|
// Solo «XOneCode X.X.X», para el pie de la barra: ahí no hay sitio para el commit ni
|
|
@@ -1830,6 +1934,15 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
1830
1934
|
...(s.tarea === undefined ? {} : { deTarea: true }),
|
|
1831
1935
|
...(trabajando.has(s.id) ? { trabajando: true } : {}),
|
|
1832
1936
|
...(s.consumo === undefined ? {} : { consumo: s.consumo }),
|
|
1937
|
+
// Solo la CLAVE: el resto del ticket (conector, sitio) se queda en el host hasta
|
|
1938
|
+
// que algo lo necesite (`SesionDelCable.ticket`). `?.` y no `===undefined`: un
|
|
1939
|
+
// índice tocado a mano puede traer `ticket: null`, y esto no es sitio para tumbar
|
|
1940
|
+
// la lista ENTERA de sesiones del proyecto por una entrada mal formada.
|
|
1941
|
+
...(typeof s.ticket?.clave === "string" ? { ticket: s.ticket.clave } : {}),
|
|
1942
|
+
// IXCODE-15: y el CONECTOR del ticket, que es con quien se cierra (`cerrar` usa
|
|
1943
|
+
// `ticket.conector`, no el gestor vinculado ahora): «Cerrar en Jira» en una sesión de una
|
|
1944
|
+
// tarea de Jira aunque el proyecto ya esté vinculado a Notion. El sitio sigue en el host.
|
|
1945
|
+
...(typeof s.ticket?.clave === "string" && typeof s.ticket.conector === "string" ? { ticketConector: s.ticket.conector } : {}),
|
|
1833
1946
|
}));
|
|
1834
1947
|
}
|
|
1835
1948
|
catch {
|
|
@@ -2048,6 +2161,45 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
2048
2161
|
const anunciarAbriendo = (que) => {
|
|
2049
2162
|
emitir(que === undefined ? { clase: "abriendo", activo: false } : { clase: "abriendo", activo: true, ...que });
|
|
2050
2163
|
};
|
|
2164
|
+
/**
|
|
2165
|
+
* Lo que sigue a abrir una sesión desde el cable, sea la que la persona pidió en la barra
|
|
2166
|
+
* (`atenderSesion`) o la que abre «Empezar» en el panel del proyecto (`atenderGestor`). Una
|
|
2167
|
+
* sola copia: dos caminos que abren sesión con dos colas distintas dejarían a uno sin el
|
|
2168
|
+
* contador limpio o sin mirar la cola de tareas.
|
|
2169
|
+
*/
|
|
2170
|
+
const trasAbrirSesion = () => {
|
|
2171
|
+
// El cable se muda a la consola del proyecto, como en el alta: sin esto el usuario
|
|
2172
|
+
// mira un transcript vivo cuyas aprobaciones se rechazan solas al otro lado.
|
|
2173
|
+
adjuntar();
|
|
2174
|
+
/**
|
|
2175
|
+
* Y el contador se REEMITE, porque el gasto es de UNA conversación.
|
|
2176
|
+
*
|
|
2177
|
+
* `consumo` solo viaja cuando el consumo CAMBIA (`alCambiarConsumo`), y abrir otra
|
|
2178
|
+
* sesión no cambia nada: cambia de quién es la cuenta. Sin esto —medido en la pantalla
|
|
2179
|
+
* del usuario— la caja seguía enseñando los tokens de la conversación anterior hasta la
|
|
2180
|
+
* primera llamada al modelo de la nueva, que puede tardar.
|
|
2181
|
+
*
|
|
2182
|
+
* **No se usa `emitirConsumo()`, y ese es el detalle que costó el primer intento**: esa
|
|
2183
|
+
* función CALLA cuando no consta la cuenta, y al abrir no consta — el `SesionReal` no se
|
|
2184
|
+
* registra hasta que el primer turno lo construye. O sea que justo en el instante que
|
|
2185
|
+
* hay que limpiar, la vía normal no manda nada. Aquí el cero SÍ está medido: una
|
|
2186
|
+
* conversación recién abierta no ha gastado nada, y su tracker nace a cero aunque la
|
|
2187
|
+
* sesión sea vieja. El cliente no pinta un cero, así que el contador desaparece.
|
|
2188
|
+
*
|
|
2189
|
+
* Y si la cuenta SÍ consta —volver al foco de una consola que está corriendo un turno—
|
|
2190
|
+
* se manda la de verdad: mandar cero ahí borraría un contador vivo a mitad de turno.
|
|
2191
|
+
*/
|
|
2192
|
+
const alAbrir = vestibulo.consumoDeSesion();
|
|
2193
|
+
emitir({
|
|
2194
|
+
clase: "consumo",
|
|
2195
|
+
modelo: alAbrir?.modelo ?? SIN_CONSUMO,
|
|
2196
|
+
externo: alAbrir?.externo ?? SIN_CONSUMO,
|
|
2197
|
+
ventana: ventanaDeAhora(alAbrir?.contexto ?? 0),
|
|
2198
|
+
});
|
|
2199
|
+
// Y la cola se vuelve a mirar: este proyecto queda bloqueado para las tareas —gana la
|
|
2200
|
+
// persona— y el que estuviera abierto antes acaba de quedar libre.
|
|
2201
|
+
opciones.revisarTareas?.();
|
|
2202
|
+
};
|
|
2051
2203
|
const atenderSesion = async (peticion) => {
|
|
2052
2204
|
aviso = undefined;
|
|
2053
2205
|
// Antes de la primera espera, no después: entre el clic y aquí no hay nada que pintar.
|
|
@@ -2071,41 +2223,14 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
2071
2223
|
proyectoElegido = peticion.proyecto;
|
|
2072
2224
|
// La identidad ENTERA, no el id: el servidor abre por nombre. `identidad` ya está
|
|
2073
2225
|
// resuelta unas líneas más arriba contra el listado.
|
|
2226
|
+
ramas = [];
|
|
2227
|
+
ramasDe = undefined;
|
|
2074
2228
|
ramas = await vestibulo.ramasDe(entornoElegido, identidad ?? peticion.proyecto);
|
|
2229
|
+
ramasDe = peticion.proyecto;
|
|
2075
2230
|
return;
|
|
2076
2231
|
}
|
|
2077
2232
|
await vestibulo.abrirProyecto({ raiz, ...(peticion.sesion === undefined ? {} : { sesion: peticion.sesion }) });
|
|
2078
|
-
|
|
2079
|
-
// mira un transcript vivo cuyas aprobaciones se rechazan solas al otro lado.
|
|
2080
|
-
adjuntar();
|
|
2081
|
-
/**
|
|
2082
|
-
* Y el contador se REEMITE, porque el gasto es de UNA conversación.
|
|
2083
|
-
*
|
|
2084
|
-
* `consumo` solo viaja cuando el consumo CAMBIA (`alCambiarConsumo`), y abrir otra
|
|
2085
|
-
* sesión no cambia nada: cambia de quién es la cuenta. Sin esto —medido en la pantalla
|
|
2086
|
-
* del usuario— la caja seguía enseñando los tokens de la conversación anterior hasta la
|
|
2087
|
-
* primera llamada al modelo de la nueva, que puede tardar.
|
|
2088
|
-
*
|
|
2089
|
-
* **No se usa `emitirConsumo()`, y ese es el detalle que costó el primer intento**: esa
|
|
2090
|
-
* función CALLA cuando no consta la cuenta, y al abrir no consta — el `SesionReal` no se
|
|
2091
|
-
* registra hasta que el primer turno lo construye. O sea que justo en el instante que
|
|
2092
|
-
* hay que limpiar, la vía normal no manda nada. Aquí el cero SÍ está medido: una
|
|
2093
|
-
* conversación recién abierta no ha gastado nada, y su tracker nace a cero aunque la
|
|
2094
|
-
* sesión sea vieja. El cliente no pinta un cero, así que el contador desaparece.
|
|
2095
|
-
*
|
|
2096
|
-
* Y si la cuenta SÍ consta —volver al foco de una consola que está corriendo un turno—
|
|
2097
|
-
* se manda la de verdad: mandar cero ahí borraría un contador vivo a mitad de turno.
|
|
2098
|
-
*/
|
|
2099
|
-
const alAbrir = vestibulo.consumoDeSesion();
|
|
2100
|
-
emitir({
|
|
2101
|
-
clase: "consumo",
|
|
2102
|
-
modelo: alAbrir?.modelo ?? SIN_CONSUMO,
|
|
2103
|
-
externo: alAbrir?.externo ?? SIN_CONSUMO,
|
|
2104
|
-
ventana: ventanaDeAhora(alAbrir?.contexto ?? 0),
|
|
2105
|
-
});
|
|
2106
|
-
// Y la cola se vuelve a mirar: este proyecto queda bloqueado para las tareas —gana la
|
|
2107
|
-
// persona— y el que estuviera abierto antes acaba de quedar libre.
|
|
2108
|
-
opciones.revisarTareas?.();
|
|
2233
|
+
trasAbrirSesion();
|
|
2109
2234
|
}
|
|
2110
2235
|
catch (error) {
|
|
2111
2236
|
aviso = error instanceof Error ? error.message : String(error);
|
|
@@ -2843,6 +2968,15 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
2843
2968
|
const motivoLegible = (error) => typeof error === "object" && error !== null && "code" in error
|
|
2844
2969
|
? codigoDe(error)
|
|
2845
2970
|
: (error instanceof Error ? error.message : String(error)).split(/\r?\n/)[0].slice(0, 200);
|
|
2971
|
+
/**
|
|
2972
|
+
* `motivoLegible` para el GESTOR de tareas, con los correos TACHADOS: el texto de una tool de
|
|
2973
|
+
* Jira que falla llega tal cual (`servicioDeConectores.ts#ErrorDeTool`) y puede nombrar a una
|
|
2974
|
+
* persona por su correo, que no cruza el cable (IXCODE-11). Se tacha ANTES de recortar: un
|
|
2975
|
+
* correo cortado por el tope queda sin su punto y ya no lo reconoce ningún patrón.
|
|
2976
|
+
*/
|
|
2977
|
+
const motivoDelGestor = (error) => typeof error === "object" && error !== null && "code" in error
|
|
2978
|
+
? codigoDe(error)
|
|
2979
|
+
: sinCorreos((error instanceof Error ? error.message : String(error)).split(/\r?\n/)[0]).slice(0, 200);
|
|
2846
2980
|
/**
|
|
2847
2981
|
* El árbol del proyecto abierto. Sin proyecto no hay pestaña que lo pida, así que no se
|
|
2848
2982
|
* contesta nada; sin PUERTO sí se contesta, con error: un árbol que nunca llega deja al
|
|
@@ -2914,6 +3048,507 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
2914
3048
|
emitir({ clase: "planes", error: "no se pudieron leer los planes" });
|
|
2915
3049
|
}
|
|
2916
3050
|
};
|
|
3051
|
+
/**
|
|
3052
|
+
* El gestor de tareas de cada conector, UNO por conector mientras viva este cable
|
|
3053
|
+
* (IXCODE-11). La fábrica puede devolver una instancia nueva en cada llamada, y el adaptador
|
|
3054
|
+
* de Jira guarda en la instancia la url pública de cada sitio —la que compone el enlace de
|
|
3055
|
+
* cada tarea—, así que pedirla otra vez en cada mensaje perdería esa url. Se memoriza también
|
|
3056
|
+
* el `undefined`: un conector que no es un gestor no deja de serlo entre dos mensajes.
|
|
3057
|
+
*/
|
|
3058
|
+
const gestores = new Map();
|
|
3059
|
+
const gestorDe = (conector) => {
|
|
3060
|
+
if (opciones.gestorDeTareas === undefined)
|
|
3061
|
+
return undefined;
|
|
3062
|
+
if (!gestores.has(conector))
|
|
3063
|
+
gestores.set(conector, opciones.gestorDeTareas(conector));
|
|
3064
|
+
return gestores.get(conector);
|
|
3065
|
+
};
|
|
3066
|
+
/**
|
|
3067
|
+
* Las instancias cuyo `sitios()` ya contestó en este proceso, y el nombre de cada sitio.
|
|
3068
|
+
* Sin esa pregunta el adaptador no sabe la url del sitio y las tareas llegarían SIN enlace
|
|
3069
|
+
* —ausente, no inventado—; con ella llegan con el suyo. Se marca solo lo que CONTESTÓ: un
|
|
3070
|
+
* fallo se vuelve a intentar en la acción siguiente.
|
|
3071
|
+
*/
|
|
3072
|
+
const conSitios = new WeakSet();
|
|
3073
|
+
const nombreDelSitio = new Map();
|
|
3074
|
+
const preguntarSitios = async (gestor) => {
|
|
3075
|
+
const lista = (await gestor.sitios()).map((x) => ({ id: x.id, nombre: x.nombre }));
|
|
3076
|
+
conSitios.add(gestor);
|
|
3077
|
+
for (const x of lista)
|
|
3078
|
+
nombreDelSitio.set(x.id, x.nombre);
|
|
3079
|
+
return lista;
|
|
3080
|
+
};
|
|
3081
|
+
const asegurarSitios = async (gestor) => {
|
|
3082
|
+
if (!conSitios.has(gestor))
|
|
3083
|
+
await preguntarSitios(gestor);
|
|
3084
|
+
};
|
|
3085
|
+
/**
|
|
3086
|
+
* El nombre para mostrar de cada proyecto que un gestor ha NOMBRADO en este proceso (IXCODE-15):
|
|
3087
|
+
* de `proyectos` (Jira) o de `describir`/`vincular` (Notion, donde el proyecto es un
|
|
3088
|
+
* `collection://…` que no se lee). Lo que no se ha oído no se inventa: el vínculo va sin él.
|
|
3089
|
+
*/
|
|
3090
|
+
const nombreDelProyecto = new Map();
|
|
3091
|
+
const claveDeProyecto = (conector, sitio, proyecto) => JSON.stringify([conector, sitio, proyecto]);
|
|
3092
|
+
/**
|
|
3093
|
+
* El nombre del conector tal como lo enseña el catálogo («Jira», «Notion»): los rótulos salen de
|
|
3094
|
+
* aquí y no a fuego (IXCODE-15). Sin servicio, la fila del catálogo de CÓDIGO; sin fila, el id.
|
|
3095
|
+
*/
|
|
3096
|
+
const nombreDelConector = (conector) => {
|
|
3097
|
+
let catalogo = CATALOGO_DE_CONECTORES;
|
|
3098
|
+
try {
|
|
3099
|
+
if (servicioConectores !== undefined)
|
|
3100
|
+
catalogo = servicioConectores.lista().catalogo;
|
|
3101
|
+
}
|
|
3102
|
+
catch {
|
|
3103
|
+
// Un fichero de conectores ilegible no puede tumbar un rótulo: queda el catálogo de código.
|
|
3104
|
+
}
|
|
3105
|
+
return catalogo.find((c) => c.id === conector)?.nombre ?? conector;
|
|
3106
|
+
};
|
|
3107
|
+
/** El texto que «Empezar» deja en el compositor. Lo que no consta no se escribe: ni una
|
|
3108
|
+
* línea `undefined` por un enlace que falta, ni un párrafo vacío por una descripción vacía.
|
|
3109
|
+
* La cabecera lleva lo que se ENSEÑA (`etiqueta ?? clave`) y el gestor por su NOMBRE. */
|
|
3110
|
+
const borradorDeTarea = (f, conector) => {
|
|
3111
|
+
const cabecera = [`${f.etiqueta ?? f.clave} — ${f.titulo}`, `Estado: ${f.estado}`, ...(f.url === undefined ? [] : [f.url])].join("\n");
|
|
3112
|
+
const descripcion = f.descripcion.trim();
|
|
3113
|
+
return `Trabaja en esta tarea de ${nombreDelConector(conector)}.\n\n${cabecera}${descripcion === "" ? "" : `\n\n${descripcion}`}`;
|
|
3114
|
+
};
|
|
3115
|
+
/**
|
|
3116
|
+
* El gestor de tareas del proyecto abierto (IXCODE-11, panel del proyecto). Mismas reglas
|
|
3117
|
+
* que `atenderPlanes`: sin proyecto no se contesta; sin gestor en esta ejecución se contesta
|
|
3118
|
+
* con error —un panel en «consultando…» para siempre es un fallo mudo—; y un fallo viaja
|
|
3119
|
+
* sin la ruta de la máquina (`motivoLegible`: nuestra frase, o el código si es de Node).
|
|
3120
|
+
*
|
|
3121
|
+
* La configuración se lee y se escribe en el `config.json` del PROYECTO, nunca fusionada con
|
|
3122
|
+
* la global: el vínculo y los conectores son de esta carpeta.
|
|
3123
|
+
*
|
|
3124
|
+
* **Cuatro acciones ESCRIBEN en Jira (Task 10), y solo esas**: `transicionar` dentro de
|
|
3125
|
+
* `empezar` (con `transicion`) y dentro de `cerrar` (con `transicion`), y `comentar` dentro
|
|
3126
|
+
* de `cerrar`. Las cuatro entran por un mensaje EXPLÍCITO del cliente —la persona aprobó una
|
|
3127
|
+
* tarjeta—, nunca solas: `pendientes`, `transiciones` y `borradorDeCierre` son de solo
|
|
3128
|
+
* LECTURA. `empezar` abre una sesión y propone un borrador; `borradorDeCierre` compone el
|
|
3129
|
+
* texto de cierre de la sesión ABIERTA sin mandarlo.
|
|
3130
|
+
*/
|
|
3131
|
+
const atenderGestor = async (m) => {
|
|
3132
|
+
const abierto = vestibulo.proyectoAbierto();
|
|
3133
|
+
if (abierto === undefined)
|
|
3134
|
+
return;
|
|
3135
|
+
const raiz = abierto.raiz;
|
|
3136
|
+
// `sinCorreos` también aquí, y no solo en `motivoDelGestor`: la frase de un fallo puede
|
|
3137
|
+
// llevar un texto del gestor (una clave de proyecto, un nombre de conector) sin pasar por él.
|
|
3138
|
+
// El fallo de `ficha` lleva la CLAVE: con dos filas desplegadas una tras otra, el de la
|
|
3139
|
+
// primera no puede acabar pintado bajo la segunda.
|
|
3140
|
+
// IXCODE-15: el de `buscarProyectos` lleva el TEXTO buscado y el de `describir` la base PEDIDA,
|
|
3141
|
+
// por la misma razón que la clave de `ficha`: el cliente no pinta el fallo de una búsqueda o
|
|
3142
|
+
// una base que ya no tiene delante.
|
|
3143
|
+
const fallo = (motivo) => emitir({
|
|
3144
|
+
clase: "gestor",
|
|
3145
|
+
error: {
|
|
3146
|
+
accion: m.accion,
|
|
3147
|
+
motivo: sinCorreos(motivo),
|
|
3148
|
+
...(m.accion === "ficha" ? { clave: m.clave } : {}),
|
|
3149
|
+
...(m.accion === "buscarProyectos" ? { texto: m.texto } : {}),
|
|
3150
|
+
...(m.accion === "describir" ? { pedido: m.proyecto } : {}),
|
|
3151
|
+
},
|
|
3152
|
+
});
|
|
3153
|
+
if (opciones.gestorDeTareas === undefined)
|
|
3154
|
+
return fallo("esta ejecución no tiene gestor de tareas");
|
|
3155
|
+
const delProyecto = () => {
|
|
3156
|
+
const c = cargar(raiz).config.proyecto;
|
|
3157
|
+
return { conectores: c?.conectores ?? [], vinculo: c?.gestorDeTareas };
|
|
3158
|
+
};
|
|
3159
|
+
/**
|
|
3160
|
+
* El estado del gestor del proyecto, SIN tocar la red (IXCODE-15): lo contestan `vincular`,
|
|
3161
|
+
* `desvincular` y `usarConector`, y un gestor que no responde no puede colgar la casilla de un
|
|
3162
|
+
* conector del chat. `admiteMias` se decidió AL VINCULAR y vive en el `config.json`
|
|
3163
|
+
* (`admiteMiasDelVinculo`: lo guardado, o `true` para un Jira de antes); viaja solo con vínculo.
|
|
3164
|
+
*/
|
|
3165
|
+
const emitirEstado = () => {
|
|
3166
|
+
const { conectores, vinculo } = delProyecto();
|
|
3167
|
+
const nombre = vinculo === undefined ? undefined : nombreDelSitio.get(vinculo.sitio);
|
|
3168
|
+
// El nombre del proyecto se GUARDA con el vínculo (IXCODE-15) y sobrevive a un reinicio; lo
|
|
3169
|
+
// oído en este proceso solo sirve de respaldo para un vínculo escrito antes de ese campo.
|
|
3170
|
+
const nombreProyecto = vinculo === undefined
|
|
3171
|
+
? undefined
|
|
3172
|
+
: (vinculo.nombreDelProyecto ?? nombreDelProyecto.get(claveDeProyecto(vinculo.conector, vinculo.sitio, vinculo.proyecto)));
|
|
3173
|
+
const admiteMias = vinculo === undefined ? undefined : admiteMiasDelVinculo(vinculo);
|
|
3174
|
+
emitir({
|
|
3175
|
+
clase: "gestor",
|
|
3176
|
+
estado: {
|
|
3177
|
+
conectores,
|
|
3178
|
+
...(vinculo === undefined
|
|
3179
|
+
? {}
|
|
3180
|
+
: {
|
|
3181
|
+
vinculo: {
|
|
3182
|
+
conector: vinculo.conector,
|
|
3183
|
+
sitio: vinculo.sitio,
|
|
3184
|
+
proyecto: vinculo.proyecto,
|
|
3185
|
+
...(nombre === undefined ? {} : { nombreDelSitio: nombre }),
|
|
3186
|
+
...(nombreProyecto === undefined ? {} : { nombreDelProyecto: nombreProyecto }),
|
|
3187
|
+
},
|
|
3188
|
+
}),
|
|
3189
|
+
...(admiteMias === undefined ? {} : { admiteMias }),
|
|
3190
|
+
},
|
|
3191
|
+
});
|
|
3192
|
+
};
|
|
3193
|
+
/** Escribir el `config.json`: su error puede llevar la ruta, así que al cable va una frase fija. */
|
|
3194
|
+
const guardar = (escribir) => {
|
|
3195
|
+
try {
|
|
3196
|
+
escribir();
|
|
3197
|
+
return true;
|
|
3198
|
+
}
|
|
3199
|
+
catch (error) {
|
|
3200
|
+
informar(`no se pudo guardar la configuración del proyecto (${codigoDe(error)})`);
|
|
3201
|
+
fallo("no se pudo guardar la configuración del proyecto");
|
|
3202
|
+
return false;
|
|
3203
|
+
}
|
|
3204
|
+
};
|
|
3205
|
+
const gestorOFallo = (conector) => {
|
|
3206
|
+
const g = gestorDe(conector);
|
|
3207
|
+
if (g === undefined)
|
|
3208
|
+
fallo(`«${conector}» no es un gestor de tareas de esta ejecución`);
|
|
3209
|
+
return g;
|
|
3210
|
+
};
|
|
3211
|
+
/**
|
|
3212
|
+
* IXCODE-15: una LECTURA que depende del vínculo del proyecto (pendientes, ficha, las
|
|
3213
|
+
* transiciones de «empezar») tarda lo que tarde el gestor, y mientras tanto la persona puede
|
|
3214
|
+
* vincular OTRO. La respuesta es del vínculo de ANTES: se relee el de ahora al volver y, si ya
|
|
3215
|
+
* no es el mismo, la respuesta —o su fallo— se TIRA sin emitir nada. El panel ya pidió lo suyo
|
|
3216
|
+
* al nuevo vínculo; fundirla pintaría filas de Jira bajo la base de Notion. `undefined` = tirada.
|
|
3217
|
+
*/
|
|
3218
|
+
const delMismoVinculo = async (usado, leer) => {
|
|
3219
|
+
const sigue = () => {
|
|
3220
|
+
const ahora = delProyecto().vinculo;
|
|
3221
|
+
return ahora !== undefined && ahora.conector === usado.conector && ahora.sitio === usado.sitio && ahora.proyecto === usado.proyecto;
|
|
3222
|
+
};
|
|
3223
|
+
try {
|
|
3224
|
+
const valor = await leer();
|
|
3225
|
+
return sigue() ? { valor } : undefined;
|
|
3226
|
+
}
|
|
3227
|
+
catch (error) {
|
|
3228
|
+
if (!sigue())
|
|
3229
|
+
return undefined;
|
|
3230
|
+
throw error;
|
|
3231
|
+
}
|
|
3232
|
+
};
|
|
3233
|
+
let anunciada = false;
|
|
3234
|
+
try {
|
|
3235
|
+
switch (m.accion) {
|
|
3236
|
+
case "estado":
|
|
3237
|
+
return emitirEstado();
|
|
3238
|
+
case "sitios": {
|
|
3239
|
+
const g = gestorOFallo(m.conector);
|
|
3240
|
+
if (g === undefined)
|
|
3241
|
+
return;
|
|
3242
|
+
emitir({ clase: "gestor", sitios: { conector: m.conector, lista: await preguntarSitios(g) } });
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
case "proyectos": {
|
|
3246
|
+
const g = gestorOFallo(m.conector);
|
|
3247
|
+
if (g === undefined)
|
|
3248
|
+
return;
|
|
3249
|
+
const lista = (await g.proyectos(m.sitio)).map((x) => ({ clave: x.clave, nombre: x.nombre }));
|
|
3250
|
+
for (const x of lista)
|
|
3251
|
+
nombreDelProyecto.set(claveDeProyecto(m.conector, m.sitio, x.clave), x.nombre);
|
|
3252
|
+
emitir({ clase: "gestor", proyectos: { sitio: m.sitio, lista } });
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
// IXCODE-15, de solo LECTURA: buscar proyectos por texto (Notion: sus bases). Un gestor que
|
|
3256
|
+
// no busca (Jira) lo DICE; una lista vacía afirmaría que no hay nada que se llame así.
|
|
3257
|
+
case "buscarProyectos": {
|
|
3258
|
+
const g = gestorOFallo(m.conector);
|
|
3259
|
+
if (g === undefined)
|
|
3260
|
+
return;
|
|
3261
|
+
if (g.buscarProyectos === undefined)
|
|
3262
|
+
return fallo(`en ${nombreDelConector(m.conector)} los proyectos se eligen de la lista`);
|
|
3263
|
+
const lista = (await g.buscarProyectos(m.texto)).map((x) => ({ proyecto: x.proyecto, nombre: x.nombre, ...(x.ruta === undefined ? {} : { ruta: x.ruta }) }));
|
|
3264
|
+
emitir({ clase: "gestor", busqueda: { conector: m.conector, texto: m.texto, lista } });
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
// IXCODE-15, de solo LECTURA: lo que se entiende del esquema de un proyecto ANTES de
|
|
3268
|
+
// vincularlo. `pedido` es lo que llegó (el id de la base); `esquema.proyecto` es lo que
|
|
3269
|
+
// hay que mandar a `vincular` (el data source ya resuelto). Un «no vale» viaja como
|
|
3270
|
+
// `motivo`, no como `error`: la acción salió bien, y la respuesta es que esa base no sirve.
|
|
3271
|
+
case "describir": {
|
|
3272
|
+
const g = gestorOFallo(m.conector);
|
|
3273
|
+
if (g === undefined)
|
|
3274
|
+
return;
|
|
3275
|
+
if (g.describirProyecto === undefined)
|
|
3276
|
+
return fallo(`${nombreDelConector(m.conector)} no describe sus proyectos`);
|
|
3277
|
+
const [sitio] = await g.sitios();
|
|
3278
|
+
const d = await g.describirProyecto({ conector: m.conector, sitio: sitio?.id ?? "", proyecto: m.proyecto });
|
|
3279
|
+
if ("esquema" in d && sitio !== undefined)
|
|
3280
|
+
nombreDelProyecto.set(claveDeProyecto(m.conector, sitio.id, d.esquema.proyecto), d.esquema.nombre);
|
|
3281
|
+
emitir({ clase: "gestor", descripcion: { conector: m.conector, pedido: m.proyecto, ...d } });
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
case "vincular": {
|
|
3285
|
+
// La forma se comprueba ANTES de preguntar, con la regla de SU conector (IXCODE-15): la
|
|
3286
|
+
// clave de Jira entra en una JQL, el data source de Notion en un SQL.
|
|
3287
|
+
const motivo = motivoDeProyectoInaceptable(m.conector, m.proyecto);
|
|
3288
|
+
if (motivo !== undefined)
|
|
3289
|
+
return fallo(motivo);
|
|
3290
|
+
const g = gestorOFallo(m.conector);
|
|
3291
|
+
if (g === undefined)
|
|
3292
|
+
return;
|
|
3293
|
+
// Solo se escribe lo que el gestor DICE que existe: un vínculo a un proyecto que no se
|
|
3294
|
+
// ve no daría ni una pendiente, y el fallo aparecería lejos de donde se cometió. Quien
|
|
3295
|
+
// sabe DESCRIBIR (Notion) lo comprueba así —que el data source existe Y que su esquema
|
|
3296
|
+
// vale como gestor—; quien no (Jira), contra la lista de proyectos del sitio, como siempre.
|
|
3297
|
+
// `admiteMias` se DECIDE aquí, con la base ya descrita, y se guarda con el vínculo: así
|
|
3298
|
+
// `estado` no pregunta a nadie. Con esquema, solo si tiene una propiedad de persona;
|
|
3299
|
+
// sin describir (Jira), siempre —`currentUser()` no depende del proyecto—.
|
|
3300
|
+
let admiteMias = true;
|
|
3301
|
+
/** El nombre que el gestor DIJO del proyecto: se guarda con el vínculo, para mostrarlo tras un reinicio. */
|
|
3302
|
+
let nombreVisto;
|
|
3303
|
+
if (g.describirProyecto !== undefined) {
|
|
3304
|
+
if (!(await g.sitios()).some((x) => x.id === m.sitio))
|
|
3305
|
+
return fallo(`«${m.sitio}» no es un sitio de ${nombreDelConector(m.conector)}`);
|
|
3306
|
+
const d = await g.describirProyecto({ conector: m.conector, sitio: m.sitio, proyecto: m.proyecto });
|
|
3307
|
+
if ("motivo" in d)
|
|
3308
|
+
return fallo(d.motivo);
|
|
3309
|
+
if (d.esquema.proyecto !== m.proyecto)
|
|
3310
|
+
return fallo("ese proyecto no es el que se describió");
|
|
3311
|
+
nombreDelProyecto.set(claveDeProyecto(m.conector, m.sitio, m.proyecto), d.esquema.nombre);
|
|
3312
|
+
nombreVisto = d.esquema.nombre;
|
|
3313
|
+
admiteMias = d.esquema.asignado !== undefined;
|
|
3314
|
+
}
|
|
3315
|
+
else {
|
|
3316
|
+
const visibles = await g.proyectos(m.sitio);
|
|
3317
|
+
const visto = visibles.find((x) => x.clave === m.proyecto);
|
|
3318
|
+
if (visto === undefined)
|
|
3319
|
+
return fallo(`«${m.proyecto}» no está entre los proyectos de ese sitio`);
|
|
3320
|
+
nombreDelProyecto.set(claveDeProyecto(m.conector, m.sitio, m.proyecto), visto.nombre);
|
|
3321
|
+
nombreVisto = visto.nombre;
|
|
3322
|
+
}
|
|
3323
|
+
// UN gestor por proyecto (IXCODE-15): vincular uno SUSTITUYE al de antes, y el conector
|
|
3324
|
+
// del de antes deja de estar usado —el panel no tiene casilla para un gestor, así que
|
|
3325
|
+
// quedarse en `conectores` lo dejaría usado sin control que lo quite—.
|
|
3326
|
+
const { conectores, vinculo: anterior } = delProyecto();
|
|
3327
|
+
const vinculo = {
|
|
3328
|
+
conector: m.conector,
|
|
3329
|
+
sitio: m.sitio,
|
|
3330
|
+
proyecto: m.proyecto,
|
|
3331
|
+
admiteMias,
|
|
3332
|
+
...(nombreVisto.trim() === "" ? {} : { nombreDelProyecto: nombreVisto }),
|
|
3333
|
+
};
|
|
3334
|
+
if (!guardar(() => guardarGestorDeProyecto(raiz, vinculo)))
|
|
3335
|
+
return;
|
|
3336
|
+
const sinAnterior = anterior !== undefined && anterior.conector !== m.conector ? conectores.filter((c) => c !== anterior.conector) : conectores;
|
|
3337
|
+
const nuevos = sinAnterior.includes(m.conector) ? sinAnterior : [...sinAnterior, m.conector];
|
|
3338
|
+
if (nuevos.join("\u0000") !== conectores.join("\u0000") && !guardar(() => guardarConectoresDeProyecto(raiz, nuevos)))
|
|
3339
|
+
return;
|
|
3340
|
+
return emitirEstado();
|
|
3341
|
+
}
|
|
3342
|
+
case "desvincular": {
|
|
3343
|
+
// Desvincular también DEJA DE USAR el conector del vínculo: el panel ya no ofrece una
|
|
3344
|
+
// casilla aparte para un gestor de tareas —vincular lo marca como usado, y esto es la
|
|
3345
|
+
// vuelta—, así que quedarse en `conectores` lo dejaría usado sin control que lo quite.
|
|
3346
|
+
const { conectores, vinculo } = delProyecto();
|
|
3347
|
+
if (!guardar(() => guardarGestorDeProyecto(raiz, undefined)))
|
|
3348
|
+
return;
|
|
3349
|
+
if (vinculo !== undefined && conectores.includes(vinculo.conector)) {
|
|
3350
|
+
if (!guardar(() => guardarConectoresDeProyecto(raiz, conectores.filter((c) => c !== vinculo.conector))))
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
return emitirEstado();
|
|
3354
|
+
}
|
|
3355
|
+
case "usarConector": {
|
|
3356
|
+
// Solo se AÑADE lo que esta consola conoce (`catálogo ∪ definiciones`): un id cualquiera
|
|
3357
|
+
// escrito en el config.json no sería un conector de nadie. QUITAR no se comprueba, para
|
|
3358
|
+
// poder limpiar uno que ya no existe.
|
|
3359
|
+
if (m.usar) {
|
|
3360
|
+
if (servicioConectores === undefined)
|
|
3361
|
+
return fallo("esta ejecución no tiene conectores");
|
|
3362
|
+
if (!servicioConectores.lista().catalogo.some((c) => c.id === m.conector))
|
|
3363
|
+
return fallo(`«${m.conector}» no es un conector de esta consola`);
|
|
3364
|
+
}
|
|
3365
|
+
const { conectores } = delProyecto();
|
|
3366
|
+
const nuevos = m.usar
|
|
3367
|
+
? (conectores.includes(m.conector) ? conectores : [...conectores, m.conector])
|
|
3368
|
+
: conectores.filter((c) => c !== m.conector);
|
|
3369
|
+
if (!guardar(() => guardarConectoresDeProyecto(raiz, nuevos)))
|
|
3370
|
+
return;
|
|
3371
|
+
return emitirEstado();
|
|
3372
|
+
}
|
|
3373
|
+
case "pendientes": {
|
|
3374
|
+
const { vinculo } = delProyecto();
|
|
3375
|
+
if (vinculo === undefined)
|
|
3376
|
+
return fallo("este proyecto no tiene gestor de tareas");
|
|
3377
|
+
const g = gestorOFallo(vinculo.conector);
|
|
3378
|
+
if (g === undefined)
|
|
3379
|
+
return;
|
|
3380
|
+
const cuando = Date.now();
|
|
3381
|
+
const mias = m.mias === true;
|
|
3382
|
+
const leida = await delMismoVinculo(vinculo, async () => {
|
|
3383
|
+
await asegurarSitios(g);
|
|
3384
|
+
return g.pendientes(vinculo, m.texto, mias ? { mias } : undefined);
|
|
3385
|
+
});
|
|
3386
|
+
if (leida === undefined)
|
|
3387
|
+
return;
|
|
3388
|
+
const lista = leida.valor;
|
|
3389
|
+
emitir({
|
|
3390
|
+
clase: "gestor",
|
|
3391
|
+
pendientes: { cuando, ...(m.texto === undefined ? {} : { texto: m.texto }), ...(mias ? { mias: true } : {}), lista },
|
|
3392
|
+
});
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
// De solo LECTURA: la descripción de UNA tarea, para desplegarla en su fila. Viajan la
|
|
3396
|
+
// clave y el texto y nada más —el resto de la ficha ya está en la fila—.
|
|
3397
|
+
case "ficha": {
|
|
3398
|
+
const { vinculo } = delProyecto();
|
|
3399
|
+
if (vinculo === undefined)
|
|
3400
|
+
return fallo("este proyecto no tiene gestor de tareas");
|
|
3401
|
+
const g = gestorOFallo(vinculo.conector);
|
|
3402
|
+
if (g === undefined)
|
|
3403
|
+
return;
|
|
3404
|
+
const leida = await delMismoVinculo(vinculo, () => g.ficha(vinculo, m.clave));
|
|
3405
|
+
if (leida === undefined)
|
|
3406
|
+
return;
|
|
3407
|
+
const f = leida.valor;
|
|
3408
|
+
emitir({ clase: "gestor", ficha: { clave: f.clave, descripcion: f.descripcion } });
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3411
|
+
// Task 10 (IXCODE-11): la primera de las CUATRO acciones que escriben en Jira, y
|
|
3412
|
+
// solo LEE —la lista de transiciones y cuál se propone—, para las dos tarjetas que
|
|
3413
|
+
// van a pedir la aprobación (una tarea del cliente).
|
|
3414
|
+
case "transiciones": {
|
|
3415
|
+
// Las de CIERRE se piden al conector y sitio del TICKET de la sesión, que es a donde
|
|
3416
|
+
// `cerrar` las aplica: los ids de una transición son de SU sitio, y el vínculo del
|
|
3417
|
+
// proyecto puede apuntar ya a otro —o a ninguno—.
|
|
3418
|
+
const ticket = abierto.ticket;
|
|
3419
|
+
if (m.para === "cerrar" && ticket === undefined)
|
|
3420
|
+
return fallo("esta sesión no está ligada a una tarea");
|
|
3421
|
+
const vinculo = m.para === "cerrar" && ticket !== undefined
|
|
3422
|
+
? { conector: ticket.conector, sitio: ticket.sitio, proyecto: delProyecto().vinculo?.proyecto ?? "" }
|
|
3423
|
+
: delProyecto().vinculo;
|
|
3424
|
+
if (vinculo === undefined)
|
|
3425
|
+
return fallo("este proyecto no tiene gestor de tareas");
|
|
3426
|
+
const g = gestorOFallo(vinculo.conector);
|
|
3427
|
+
if (g === undefined)
|
|
3428
|
+
return;
|
|
3429
|
+
// Las de CIERRE van con el TICKET, que no cambia al revincular: solo las de «empezar»
|
|
3430
|
+
// (del vínculo del proyecto) se tiran si el vínculo cambió mientras se leían.
|
|
3431
|
+
const leida = m.para === "cerrar" ? { valor: await g.transiciones(vinculo, m.clave) } : await delMismoVinculo(vinculo, () => g.transiciones(vinculo, m.clave));
|
|
3432
|
+
if (leida === undefined)
|
|
3433
|
+
return;
|
|
3434
|
+
const lista = leida.valor;
|
|
3435
|
+
const propuesta = transicionPropuesta(lista, m.para)?.id;
|
|
3436
|
+
emitir({ clase: "gestor", transiciones: { clave: m.clave, para: m.para, lista, ...(propuesta === undefined ? {} : { propuesta }) } });
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
case "empezar": {
|
|
3440
|
+
// Con un turno en vuelo `abrirProyecto` devuelve la MISMA consola (el vestíbulo vuelve a
|
|
3441
|
+
// lo que trabaja): transicionar antes de saberlo movería el ticket en Jira y luego
|
|
3442
|
+
// contestaría «no se pudo abrir una sesión nueva». Se comprueba ANTES de tocar nada.
|
|
3443
|
+
if (abierto.turnoEnVuelo)
|
|
3444
|
+
return fallo("espera a que termine el turno");
|
|
3445
|
+
const { vinculo } = delProyecto();
|
|
3446
|
+
if (vinculo === undefined)
|
|
3447
|
+
return fallo("este proyecto no tiene gestor de tareas");
|
|
3448
|
+
const g = gestorOFallo(vinculo.conector);
|
|
3449
|
+
if (g === undefined)
|
|
3450
|
+
return;
|
|
3451
|
+
await asegurarSitios(g);
|
|
3452
|
+
// La ficha ANTES de abrir nada: si el gestor no contesta, no queda una sesión vacía.
|
|
3453
|
+
const ficha = await g.ficha(vinculo, m.clave);
|
|
3454
|
+
// La transición ANTES de abrir la sesión, y su fallo no aborta el resto: la persona
|
|
3455
|
+
// ya aprobó la tarjeta esperando entrar a trabajar, y quedarse sin sesión por un
|
|
3456
|
+
// «no se pudo transicionar» sería peor que decirlo y abrir igual con el borrador.
|
|
3457
|
+
if (m.transicion !== undefined) {
|
|
3458
|
+
try {
|
|
3459
|
+
await g.transicionar(vinculo, ficha.clave, m.transicion);
|
|
3460
|
+
}
|
|
3461
|
+
catch (error) {
|
|
3462
|
+
fallo(motivoDelGestor(error));
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const hiloDeAntes = abierto.idDeHilo;
|
|
3466
|
+
// Como en `atenderSesion`: un aviso de otra apertura no viaja en el alta de esta.
|
|
3467
|
+
aviso = undefined;
|
|
3468
|
+
// Una sesión NUEVA del mismo proyecto: el camino de `atenderSesion` sin `sesion`.
|
|
3469
|
+
const nueva = await vestibulo.abrirProyecto({ raiz });
|
|
3470
|
+
trasAbrirSesion();
|
|
3471
|
+
// El ticket se estampa solo si de verdad se abrió OTRA: estamparlo en la de antes
|
|
3472
|
+
// ligaría a la tarea una conversación que no se abrió para ella.
|
|
3473
|
+
if (nueva.raiz !== raiz || nueva.idDeHilo === hiloDeAntes)
|
|
3474
|
+
return fallo("no se pudo abrir una sesión nueva");
|
|
3475
|
+
nueva.fijarTicket({ conector: vinculo.conector, sitio: vinculo.sitio, clave: ficha.clave });
|
|
3476
|
+
// El alta ANTES que el borrador: el cliente tiene que estar ya en la sesión nueva
|
|
3477
|
+
// cuando el texto llega al compositor, o el cambio de sesión se lo llevaría.
|
|
3478
|
+
await anunciarAlta().catch(contar);
|
|
3479
|
+
anunciada = true;
|
|
3480
|
+
emitir({ clase: "gestor", borrador: { clave: ficha.clave, texto: borradorDeTarea(ficha, vinculo.conector) } });
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
// Las otras dos acciones que ESCRIBEN (Task 10): de la sesión ABIERTA, no del proyecto
|
|
3484
|
+
// —una sesión puede seguir ligada a un ticket aunque el proyecto se haya desvinculado
|
|
3485
|
+
// o vinculado a otro sitio después—, y solo con el turno ya terminado: escribir sobre
|
|
3486
|
+
// una sesión que sigue trabajando compite con lo que el agente está a punto de dejar.
|
|
3487
|
+
case "borradorDeCierre": {
|
|
3488
|
+
const ticket = abierto.ticket;
|
|
3489
|
+
if (ticket === undefined)
|
|
3490
|
+
return fallo("esta sesión no está ligada a una tarea");
|
|
3491
|
+
if (abierto.turnoEnVuelo)
|
|
3492
|
+
return fallo("espera a que termine el turno");
|
|
3493
|
+
const comentario = comentarioDeCierre(await datosDeCierre(raiz, abierto.idDeHilo));
|
|
3494
|
+
emitir({ clase: "gestor", cierre: { clave: ticket.clave, comentario } });
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
case "cerrar": {
|
|
3498
|
+
const ticket = abierto.ticket;
|
|
3499
|
+
if (ticket === undefined)
|
|
3500
|
+
return fallo("esta sesión no está ligada a una tarea");
|
|
3501
|
+
if (abierto.turnoEnVuelo)
|
|
3502
|
+
return fallo("espera a que termine el turno");
|
|
3503
|
+
if (m.comentario.trim() === "")
|
|
3504
|
+
return fallo("el comentario no puede estar vacío");
|
|
3505
|
+
const g = gestorOFallo(ticket.conector);
|
|
3506
|
+
if (g === undefined)
|
|
3507
|
+
return;
|
|
3508
|
+
// El adaptador de Jira solo mira `sitio` para comentar y transicionar
|
|
3509
|
+
// (`gestorJira.ts#comentar`/`#transicionar`), y el de Notion saca el data source de la
|
|
3510
|
+
// PROPIA página (`gestorNotion.ts#fuenteParaPagina`): el `proyecto` del vínculo del
|
|
3511
|
+
// TICKET no hace falta para escribir, así que basta con lo que la sesión guardó al
|
|
3512
|
+
// abrirse. Se completa con el del proyecto por si acaso; vacío si ya no hay vínculo.
|
|
3513
|
+
const vinculo = { conector: ticket.conector, sitio: ticket.sitio, proyecto: delProyecto().vinculo?.proyecto ?? "" };
|
|
3514
|
+
// Si `comentar` falla, la excepción sale al `catch` de fuera: NO se transiciona.
|
|
3515
|
+
await g.comentar(vinculo, ticket.clave, m.comentario);
|
|
3516
|
+
// El comentario YA está en Jira: si la transición falla, contestar `error{cerrar}`
|
|
3517
|
+
// invitaría a repetir el cierre entero —y a comentar DOS veces—. Sale `cerrado`, sin
|
|
3518
|
+
// `transicion` y con el motivo en `falloDeTransicion`.
|
|
3519
|
+
let falloDeTransicion;
|
|
3520
|
+
if (m.transicion !== undefined) {
|
|
3521
|
+
try {
|
|
3522
|
+
await g.transicionar(vinculo, ticket.clave, m.transicion);
|
|
3523
|
+
}
|
|
3524
|
+
catch (error) {
|
|
3525
|
+
falloDeTransicion = motivoDelGestor(error);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
emitir({
|
|
3529
|
+
clase: "gestor",
|
|
3530
|
+
cerrado: {
|
|
3531
|
+
clave: ticket.clave,
|
|
3532
|
+
comento: true,
|
|
3533
|
+
...(m.transicion === undefined || falloDeTransicion !== undefined ? {} : { transicion: m.transicion }),
|
|
3534
|
+
...(falloDeTransicion === undefined ? {} : { falloDeTransicion }),
|
|
3535
|
+
},
|
|
3536
|
+
});
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
catch (error) {
|
|
3542
|
+
fallo(motivoDelGestor(error));
|
|
3543
|
+
}
|
|
3544
|
+
finally {
|
|
3545
|
+
// `empezar` pudo cambiar de sesión y fallar después: el alta dice cuál es la de ahora,
|
|
3546
|
+
// igual que al final de `atenderSesion`. Si ya salió antes del borrador, no se repite. Las
|
|
3547
|
+
// demás acciones no mueven el foco, y no se reanuncia nada.
|
|
3548
|
+
if (m.accion === "empezar" && !anunciada)
|
|
3549
|
+
await anunciarAlta().catch(contar);
|
|
3550
|
+
}
|
|
3551
|
+
};
|
|
2917
3552
|
/**
|
|
2918
3553
|
* La foto del modelo XOne del proyecto abierto. Mismas reglas que el árbol: sin proyecto no
|
|
2919
3554
|
* se contesta, sin puerto se contesta con error —un «consultando…» eterno es un fallo mudo—,
|
|
@@ -3196,7 +3831,10 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
3196
3831
|
// alternativa a inventarse las del primero de la lista antes de que nadie elija.
|
|
3197
3832
|
proyectoElegido = proyecto;
|
|
3198
3833
|
// Igual que arriba: el servidor abre por NOMBRE, y el cable trae el id.
|
|
3834
|
+
ramas = [];
|
|
3835
|
+
ramasDe = undefined;
|
|
3199
3836
|
ramas = await vestibulo.ramasDe(entornoElegido, proyectos.find((p) => p.id === proyecto) ?? proyecto);
|
|
3837
|
+
ramasDe = proyecto;
|
|
3200
3838
|
return;
|
|
3201
3839
|
}
|
|
3202
3840
|
// El proyecto que viene en ESTE mensaje, no el cacheado: lo enviado es la verdad y
|
|
@@ -3873,6 +4511,111 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
3873
4511
|
respuesta.end();
|
|
3874
4512
|
return;
|
|
3875
4513
|
}
|
|
4514
|
+
if (typeof mensaje === "object" &&
|
|
4515
|
+
mensaje !== null &&
|
|
4516
|
+
mensaje.clase === "entorno" &&
|
|
4517
|
+
mensaje.accion === "fijados") {
|
|
4518
|
+
// El mismo trato que `visibles`: se guarda con el entorno y el alta siguiente lo trae.
|
|
4519
|
+
void vestibulo
|
|
4520
|
+
.guardarProyectosFijados(mensaje.entorno, mensaje.proyectos)
|
|
4521
|
+
.catch(contar)
|
|
4522
|
+
.finally(() => void anunciarAlta().catch(contar));
|
|
4523
|
+
respuesta.writeHead(204);
|
|
4524
|
+
respuesta.end();
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "copiaLocal" && mensaje.accion === "resumen") {
|
|
4528
|
+
// La FOTO del resumen del proyecto: lo que solo sabe el servidor, contestado en la
|
|
4529
|
+
// propia respuesta —como el 409 de las negativas—, así que llega solo a quien la pidió.
|
|
4530
|
+
// Del cable llega el ID; el nombre y la raíz los pone el servidor, como en `borrar`.
|
|
4531
|
+
const identidad = proyectos.find((p) => p.id === mensaje.proyecto);
|
|
4532
|
+
if (entornoElegido === undefined || identidad === undefined) {
|
|
4533
|
+
respuesta.writeHead(409, { "content-type": "application/json" });
|
|
4534
|
+
respuesta.end(JSON.stringify({ motivo: "ese proyecto no está en el listado del entorno activo" }));
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
const raiz = vestibulo.raizDeProyecto(entornoElegido, identidad.nombre);
|
|
4538
|
+
// Las tareas de ESTE proyecto las decide la RAÍZ y no el id: una tarea no lleva su
|
|
4539
|
+
// entorno, y el mismo id en otro entorno —o un clon del mismo servidor con otro id—
|
|
4540
|
+
// se colaría filtrando en el cliente. Viajan los ids; sus datos vivos ya están allí.
|
|
4541
|
+
const tareas = (opciones.colaDeTareas?.listar() ?? [])
|
|
4542
|
+
.filter((t) => mismaRuta(t.proyecto.raiz, raiz))
|
|
4543
|
+
.map((t) => t.id);
|
|
4544
|
+
let sync;
|
|
4545
|
+
if (esProyectoEnDisco(raiz)) {
|
|
4546
|
+
// La MISMA lectura que la banda de CloudStudio en Revisión, y no una segunda cuenta:
|
|
4547
|
+
// dos pantallas midiendo lo mismo de dos formas acaban diciendo dos cifras.
|
|
4548
|
+
const { clase: _clase, ...medida } = await lecturaDeSync(raiz);
|
|
4549
|
+
sync = medida;
|
|
4550
|
+
}
|
|
4551
|
+
const foto = { tareas, ...(sync === undefined ? {} : { sync }) };
|
|
4552
|
+
respuesta.writeHead(200, { "content-type": "application/json" });
|
|
4553
|
+
respuesta.end(JSON.stringify(foto));
|
|
4554
|
+
return;
|
|
4555
|
+
}
|
|
4556
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "copiaLocal" && mensaje.accion === "abrirCarpeta") {
|
|
4557
|
+
// Como `borrar`: del cable llega el ID, el nombre sale del listado del servidor y la
|
|
4558
|
+
// ruta la compone el vestíbulo. Solo con la copia BAJADA: una carpeta a medias de un alta
|
|
4559
|
+
// que falló no es «los ficheros del proyecto».
|
|
4560
|
+
const identidad = proyectos.find((p) => p.id === mensaje.proyecto);
|
|
4561
|
+
const negar = (motivo) => {
|
|
4562
|
+
respuesta.writeHead(409, { "content-type": "application/json" });
|
|
4563
|
+
respuesta.end(JSON.stringify({ motivo }));
|
|
4564
|
+
};
|
|
4565
|
+
if (entornoElegido === undefined || identidad === undefined) {
|
|
4566
|
+
negar("ese proyecto no está en el listado del entorno activo");
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
if (opciones.abrirCarpetaDeProyecto === undefined) {
|
|
4570
|
+
negar("esta ejecución no puede abrir carpetas del sistema");
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
const raiz = vestibulo.raizDeProyecto(entornoElegido, identidad.nombre);
|
|
4574
|
+
if (!esProyectoEnDisco(raiz)) {
|
|
4575
|
+
negar("ese proyecto no está descargado en este equipo");
|
|
4576
|
+
return;
|
|
4577
|
+
}
|
|
4578
|
+
opciones.abrirCarpetaDeProyecto(raiz);
|
|
4579
|
+
respuesta.writeHead(204);
|
|
4580
|
+
respuesta.end();
|
|
4581
|
+
return;
|
|
4582
|
+
}
|
|
4583
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "copiaLocal" && mensaje.accion === "borrar") {
|
|
4584
|
+
// Como `olvidar`: la negativa se decide AQUÍ y se ESPERA, para poder devolver el motivo
|
|
4585
|
+
// con un 409 — `informar` no llega al navegador desde el vestíbulo. El nombre sale del
|
|
4586
|
+
// listado del SERVIDOR para ese id: del cliente no se acepta ni un nombre ni una ruta.
|
|
4587
|
+
const identidad = proyectos.find((p) => p.id === mensaje.proyecto);
|
|
4588
|
+
if (entornoElegido === undefined || identidad === undefined) {
|
|
4589
|
+
respuesta.writeHead(409, { "content-type": "application/json" });
|
|
4590
|
+
respuesta.end(JSON.stringify({ motivo: "ese proyecto no está en el listado del entorno activo" }));
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
let resultado;
|
|
4594
|
+
try {
|
|
4595
|
+
resultado = await vestibulo.borrarCopia(entornoElegido, identidad.nombre, (opciones.colaDeTareas?.listar() ?? []).map((t) => ({ estado: t.estado, raiz: t.proyecto.raiz })));
|
|
4596
|
+
}
|
|
4597
|
+
catch (error) {
|
|
4598
|
+
contar(error);
|
|
4599
|
+
resultado = { cerroLaAbierta: false, motivo: "no se pudo borrar la copia local" };
|
|
4600
|
+
}
|
|
4601
|
+
// Lo mismo que al borrar la sesión abierta: el cable se quedaría enganchado a una
|
|
4602
|
+
// consola muerta, así que se muda al vestíbulo —el cliente pinta el escritorio—, y la
|
|
4603
|
+
// raíz queda LIBRE, así que la cola de tareas se vuelve a mirar.
|
|
4604
|
+
if (resultado.cerroLaAbierta) {
|
|
4605
|
+
adjuntar();
|
|
4606
|
+
opciones.revisarTareas?.();
|
|
4607
|
+
}
|
|
4608
|
+
// El alta siguiente ya trae `local` falso, y sin `proyectoActivo` si era el del foco.
|
|
4609
|
+
await anunciarAlta().catch(contar);
|
|
4610
|
+
if (resultado.motivo !== undefined) {
|
|
4611
|
+
respuesta.writeHead(409, { "content-type": "application/json" });
|
|
4612
|
+
respuesta.end(JSON.stringify({ motivo: resultado.motivo }));
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
respuesta.writeHead(204);
|
|
4616
|
+
respuesta.end();
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
3876
4619
|
if (typeof mensaje === "object" &&
|
|
3877
4620
|
mensaje !== null &&
|
|
3878
4621
|
mensaje.clase === "entorno" &&
|
|
@@ -3968,6 +4711,22 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
|
|
|
3968
4711
|
respuesta.end();
|
|
3969
4712
|
return;
|
|
3970
4713
|
}
|
|
4714
|
+
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "gestor") {
|
|
4715
|
+
// Los campos se comprueban por ACCIÓN, como `modeloDelCambio`: un mensaje mal formado
|
|
4716
|
+
// se contesta con su error, no revienta dentro de `atenderGestor`.
|
|
4717
|
+
const bien = gestorBienFormado(mensaje);
|
|
4718
|
+
if (bien === undefined) {
|
|
4719
|
+
const accion = typeof mensaje.accion === "string" ? mensaje.accion : "desconocida";
|
|
4720
|
+
if (vestibulo.proyectoAbierto() !== undefined)
|
|
4721
|
+
emitir({ clase: "gestor", error: { accion, motivo: "petición mal formada" } });
|
|
4722
|
+
}
|
|
4723
|
+
else {
|
|
4724
|
+
void atenderGestor(bien).catch(contar);
|
|
4725
|
+
}
|
|
4726
|
+
respuesta.writeHead(204);
|
|
4727
|
+
respuesta.end();
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
3971
4730
|
if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "colecciones") {
|
|
3972
4731
|
void atenderColecciones().catch(contar);
|
|
3973
4732
|
respuesta.writeHead(204);
|
|
@@ -4866,6 +5625,48 @@ export function ajusteDeDepuracionCableado(opciones = {}) {
|
|
|
4866
5625
|
export function ajusteDeConectoresCableado(opciones) {
|
|
4867
5626
|
return { conectores: (alCambiar) => servicioDeConectoresCableado({ casa: opciones.casa, alCambiar }) };
|
|
4868
5627
|
}
|
|
5628
|
+
/**
|
|
5629
|
+
* Las opciones `conectores` y `gestorDeTareas` de `montarRutas`, cableadas JUNTAS (IXCODE-11).
|
|
5630
|
+
*
|
|
5631
|
+
* Juntas porque el gestor de Jira llama por el MISMO servicio de conectores que `montarRutas`
|
|
5632
|
+
* construye con la fábrica `conectores` —el que lleva en memoria las autorizaciones
|
|
5633
|
+
* pendientes: un segundo servicio no vería la de Ajustes y pisaría su verificador PKCE—. Así
|
|
5634
|
+
* que aquí se ENVUELVE esa fábrica para quedarse con el servicio que devuelve, y el gestor le
|
|
5635
|
+
* llama a ese. El gestor de `"jira"` es UNO (el adaptador guarda la url de cada sitio en la
|
|
5636
|
+
* instancia), y el de `"notion"` también (guarda el esquema de cada base, IXCODE-15); cualquier
|
|
5637
|
+
* otro conector no es un gestor. Extraída y probada por el patrón de
|
|
5638
|
+
* fallo de siempre: un literal dentro de `arrancarConsolaWeb` no lo mira ningún test.
|
|
5639
|
+
*/
|
|
5640
|
+
export function ajusteDeGestorCableado(ajuste) {
|
|
5641
|
+
let servicio;
|
|
5642
|
+
/** Uno por conector: el de Jira guarda la url de cada sitio, el de Notion el esquema de cada base. */
|
|
5643
|
+
const gestores = new Map();
|
|
5644
|
+
/** Las fábricas de los gestores conocidos (IXCODE-15: Jira y Notion). Cualquier otro conector no es un gestor. */
|
|
5645
|
+
const FABRICAS = {
|
|
5646
|
+
jira: crearGestorJira,
|
|
5647
|
+
notion: crearGestorNotion,
|
|
5648
|
+
};
|
|
5649
|
+
return {
|
|
5650
|
+
conectores: (alCambiar) => (servicio = ajuste.conectores(alCambiar)),
|
|
5651
|
+
gestorDeTareas: (conector) => {
|
|
5652
|
+
const fabrica = Object.hasOwn(FABRICAS, conector) ? FABRICAS[conector] : undefined;
|
|
5653
|
+
if (fabrica === undefined)
|
|
5654
|
+
return undefined;
|
|
5655
|
+
let g = gestores.get(conector);
|
|
5656
|
+
if (g === undefined) {
|
|
5657
|
+
g = fabrica((nombre, args) => {
|
|
5658
|
+
// `montarRutas` construye el servicio al montar, antes de atender ningún mensaje; esto
|
|
5659
|
+
// solo salta si alguien usa el gestor sin haber pasado `conectores` a `montarRutas`.
|
|
5660
|
+
if (servicio === undefined)
|
|
5661
|
+
return Promise.reject(new Error("esta ejecución no tiene conectores"));
|
|
5662
|
+
return servicio.llamar(conector, nombre, args);
|
|
5663
|
+
});
|
|
5664
|
+
gestores.set(conector, g);
|
|
5665
|
+
}
|
|
5666
|
+
return g;
|
|
5667
|
+
},
|
|
5668
|
+
};
|
|
5669
|
+
}
|
|
4869
5670
|
export function construirCorredorDeTareasCableado(opciones) {
|
|
4870
5671
|
const disco = opciones.tareasFabrica === undefined ? undefined : opciones.tareasFabrica(opciones.informar);
|
|
4871
5672
|
/**
|
|
@@ -5185,7 +5986,9 @@ export async function arrancarConsolaWeb(opciones) {
|
|
|
5185
5986
|
...ajusteDeDepuracionCableado(),
|
|
5186
5987
|
// Los conectores MCP, con la MISMA disciplina: la composición extraída y probada
|
|
5187
5988
|
// (`ajusteDeConectoresCableado`), nunca un literal aquí dentro.
|
|
5188
|
-
|
|
5989
|
+
// Y el gestor de tareas (IXCODE-11) llama por ESE mismo servicio: los dos salen de
|
|
5990
|
+
// `ajusteDeGestorCableado`, que envuelve la fábrica de conectores.
|
|
5991
|
+
...ajusteDeGestorCableado(ajusteDeConectoresCableado({ casa: homedir() })),
|
|
5189
5992
|
// El selector nativo, solo si este sistema tiene uno. En el que no, la opción no se
|
|
5190
5993
|
// monta y el botón no llega a existir.
|
|
5191
5994
|
...(haySelectorDeCarpeta()
|
|
@@ -5247,6 +6050,7 @@ export async function arrancarConsolaWeb(opciones) {
|
|
|
5247
6050
|
guardarAjustesDeDispositivos: (ajustes) => void guardarDispositivos(undefined, ajustes),
|
|
5248
6051
|
instalarHerramienta: instalarHerramientaDeDispositivos,
|
|
5249
6052
|
abrirCarpetaDeHerramienta: abrirCarpetaDelSistema,
|
|
6053
|
+
abrirCarpetaDeProyecto: abrirDirectorioDelSistema,
|
|
5250
6054
|
verificarDispositivo,
|
|
5251
6055
|
// Con la función REAL, y no declarada y sin pasar: es la trampa que este repo ha pagado
|
|
5252
6056
|
// nueve veces, y la que el comentario de justo abajo describe para las cuatro del
|