supcoflow 0.1.0 → 0.5.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/README.md +83 -3
- package/dist/api.js +187 -6
- package/dist/argumentos.js +63 -3
- package/dist/candado.js +57 -0
- package/dist/cli.js +66 -5
- package/dist/comandos/clave-vigente.js +18 -0
- package/dist/comandos/claves.js +161 -0
- package/dist/comandos/configurar.js +187 -0
- package/dist/comandos/datos-y-politica.js +112 -0
- package/dist/comandos/describir.js +173 -3
- package/dist/comandos/guia.js +244 -0
- package/dist/comandos/proyecto-del-repo.js +26 -0
- package/dist/comandos/proyecto.js +213 -0
- package/dist/comandos/quien-soy.js +6 -13
- package/dist/comandos/repo-conectar.js +126 -0
- package/dist/entorno.js +323 -5
- package/dist/flags-por-comando.js +33 -0
- package/dist/guia.js +11 -0
- package/dist/idempotencia.js +88 -0
- package/dist/repo-local.js +68 -0
- package/dist/secreto-a-archivo.js +109 -0
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// `supcoflow proyecto crear` y `proyecto ver`: el proyecto del repo de esta carpeta (o de `--repo`).
|
|
2
|
+
// `crear` sale de una conexión ya confirmada (`repo conectar`) y deja `.supcoflow/proyecto.json` en la
|
|
3
|
+
// raíz del repo para que la próxima herramienta sepa de qué proyecto se trata sin preguntarle al motor.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
6
|
+
import { repoPedido } from "../repo-local.js";
|
|
7
|
+
import { claveVigente } from "./clave-vigente.js";
|
|
8
|
+
const PLAZO_DE_LECTURA_MS = 15_000;
|
|
9
|
+
const OPERACION = "proyecto_crear";
|
|
10
|
+
// Lo único que se lee de un `.supcoflow/proyecto.json` ajeno es su id, y sólo si tiene esta forma:
|
|
11
|
+
// así lo que salga a la terminal no puede ser otra cosa que un id (Review Focus 11).
|
|
12
|
+
const FORMA_DEL_ID = /^proj_[a-z0-9-]{1,80}$/;
|
|
13
|
+
// Los 422/409 del alta que el plan no listaba (alta-desde-conexion.ts del motor).
|
|
14
|
+
const RECHAZOS_DE_UNA_PERSONA = new Set(["instalacion_no_verificada", "repo_fuera_de_la_instalacion"]);
|
|
15
|
+
const RECHAZOS_TRANSITORIOS = new Set(["slug_ocupado", "repo_ya_conectado"]);
|
|
16
|
+
const fallo = (codigo, error, message, extra = {}) => ({
|
|
17
|
+
codigo, json: { ok: false, error, message, ...extra }, texto: message,
|
|
18
|
+
});
|
|
19
|
+
const comillas = (s) => `"${s.replace(/["\\]/g, "\\$&")}"`;
|
|
20
|
+
// El archivo va al repo y se commitea (§5.3): por eso lleva sólo lo que identifica al proyecto. Ni la
|
|
21
|
+
// URL de la API (un repo clonado que la trajera mandaría la clave a otro servidor, §5.2) ni nada
|
|
22
|
+
// secreto: cualquiera que lea el repo lo lee.
|
|
23
|
+
function contenidoDelArchivo(p) {
|
|
24
|
+
return JSON.stringify({ id: p.id, slug: p.slug, repo: p.repo }, null, 2) + "\n";
|
|
25
|
+
}
|
|
26
|
+
function idDelArchivo(crudo) {
|
|
27
|
+
try {
|
|
28
|
+
const v = JSON.parse(crudo);
|
|
29
|
+
if (typeof v !== "object" || v === null)
|
|
30
|
+
return null;
|
|
31
|
+
const id = v.id;
|
|
32
|
+
return typeof id === "string" && FORMA_DEL_ID.test(id) ? id : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Mismo contenido aunque el formato cambie (otra indentación, otro orden): no es «otro proyecto».
|
|
39
|
+
function mismoArchivo(crudo, p) {
|
|
40
|
+
if (crudo === contenidoDelArchivo(p))
|
|
41
|
+
return true;
|
|
42
|
+
try {
|
|
43
|
+
const v = JSON.parse(crudo);
|
|
44
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
45
|
+
return false;
|
|
46
|
+
const o = v;
|
|
47
|
+
return Object.keys(o).length === 3 && o.id === p.id && o.slug === p.slug && o.repo === p.repo;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
54
|
+
const nuevo = { id: p.id, slug: p.slug, repo: p.repo };
|
|
55
|
+
const queda = reusado ? `El proyecto ${p.id} ya existía y lo reusé` : `Creé el proyecto ${p.id}`;
|
|
56
|
+
let archivo = null;
|
|
57
|
+
if (raiz !== null) {
|
|
58
|
+
archivo = path.join(raiz, ".supcoflow", "proyecto.json");
|
|
59
|
+
const actual = d.archivos.leer(archivo);
|
|
60
|
+
if (actual !== null && !mismoArchivo(actual, p)) {
|
|
61
|
+
// Un segundo `go` no pisa archivos (§13): el que está puede ser de otro proyecto a propósito, y
|
|
62
|
+
// pisarlo cambiaría en silencio a qué proyecto apunta el repo. Se sale 4 para que una persona
|
|
63
|
+
// decida. Su contenido NUNCA se copia a la salida (puede traer cualquier cosa, una clave
|
|
64
|
+
// incluida, y `emitir` no sanea): sólo el id, si tiene forma de id.
|
|
65
|
+
const idActual = idDelArchivo(actual);
|
|
66
|
+
const message = `Ya hay un \`.supcoflow/proyecto.json\` de otro proyecto (${idActual ?? "que no reconozco"}); no lo toqué. `
|
|
67
|
+
+ `${queda} igual (${p.id}): si es el que querés, borrá el archivo y volvé a correr \`supcoflow proyecto crear\`.`;
|
|
68
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_distinto", message, { archivo, id_actual: idActual, nuevo, reusado });
|
|
69
|
+
}
|
|
70
|
+
if (actual === null) {
|
|
71
|
+
try {
|
|
72
|
+
d.archivos.escribir(archivo, contenidoDelArchivo(p));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
const message = `${queda} (${p.id}), pero no pude escribir ${archivo}. Volvé a correr \`supcoflow proyecto crear\`.`;
|
|
76
|
+
return fallo(SALIDA.inesperado, "archivo_no_escrito", message, { archivo, nuevo, reusado });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
codigo: SALIDA.ok,
|
|
82
|
+
json: { ok: true, proyecto: { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama }, reusado, archivo },
|
|
83
|
+
texto: `${queda}: ${p.repo}, rama ${p.rama}.${archivo !== null ? ` Lo anoté en ${archivo}.` : ""}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export async function proyectoCrear(d, flags) {
|
|
87
|
+
let repo;
|
|
88
|
+
let raiz;
|
|
89
|
+
try {
|
|
90
|
+
({ repo, raiz } = await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd }));
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
return resultadoDeFallo(e);
|
|
94
|
+
}
|
|
95
|
+
const leida = await claveVigente(d.guardado, d.api.origin);
|
|
96
|
+
if (!leida.ok)
|
|
97
|
+
return leida.resultado;
|
|
98
|
+
const clave = leida.vigente.clave;
|
|
99
|
+
let existente;
|
|
100
|
+
let conexion = null;
|
|
101
|
+
try {
|
|
102
|
+
existente = await d.buscarProyecto(d.api, clave, repo);
|
|
103
|
+
if (existente === null)
|
|
104
|
+
conexion = await d.ultimaConexion(d.api, clave, repo, PLAZO_DE_LECTURA_MS);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return resultadoDeFallo(e);
|
|
108
|
+
}
|
|
109
|
+
// Un repo con proyecto no vuelve a pasar por la conexión: el proyecto ya está atado a su instalación.
|
|
110
|
+
if (existente !== null) {
|
|
111
|
+
// Una clave guardada de un intento anterior (respuesta perdida, `en_curso`) ya no sirve: el proyecto
|
|
112
|
+
// existe. Si quedara, un alta futura del mismo repo (borrado y vuelto a crear) mandaría una clave
|
|
113
|
+
// vieja con otro contenido y el motor contestaría 409. Con --dry-run no se toca ni lo local.
|
|
114
|
+
if (!flags.dryRun)
|
|
115
|
+
await d.idempotencia.olvidar(OPERACION, repo);
|
|
116
|
+
if (flags.install !== null || flags.test !== null) {
|
|
117
|
+
// Sin copiar los valores: son texto del usuario y el aviso sólo dice qué pasó con ellos. Callarlo
|
|
118
|
+
// haría creer que el proyecto quedó con los comandos pedidos.
|
|
119
|
+
d.avisar("El proyecto ya existe: ignoré --install/--test. Para cambiar sus comandos hay que configurarlo.");
|
|
120
|
+
}
|
|
121
|
+
return resultadoDelProyecto(existente, true, raiz, d);
|
|
122
|
+
}
|
|
123
|
+
if (conexion === null || conexion.estado !== "conectado") {
|
|
124
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", "Conectá el repo primero: `supcoflow repo conectar`.", {
|
|
125
|
+
repo, ...(conexion !== null ? { estado: conexion.estado } : {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (flags.dryRun) {
|
|
129
|
+
const haria = `POST /api/conexiones/${conexion.id}/proyecto`;
|
|
130
|
+
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, repo, haria }, texto: `Con --dry-run no hago nada; sin él, crearía el proyecto de ${repo} (${haria}).` };
|
|
131
|
+
}
|
|
132
|
+
const cuerpo = {};
|
|
133
|
+
if (flags.install !== null)
|
|
134
|
+
cuerpo.install_command = flags.install;
|
|
135
|
+
if (flags.test !== null)
|
|
136
|
+
cuerpo.test_command = flags.test;
|
|
137
|
+
const key = await d.idempotencia.claveDe(OPERACION, repo);
|
|
138
|
+
let alta;
|
|
139
|
+
try {
|
|
140
|
+
alta = await d.crearProyecto(d.api, clave, conexion.id, cuerpo, key);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
// La clave NO se olvida (Review Focus 4): sin respuesta no se sabe si el motor creó el proyecto, y
|
|
144
|
+
// el reintento tiene que mandar la misma para recibir el mismo en vez de otro.
|
|
145
|
+
return resultadoDeFallo(e);
|
|
146
|
+
}
|
|
147
|
+
switch (alta.tipo) {
|
|
148
|
+
case "ok":
|
|
149
|
+
await d.idempotencia.olvidar(OPERACION, repo);
|
|
150
|
+
return resultadoDelProyecto(alta.proyecto, alta.reusado, raiz, d);
|
|
151
|
+
case "comandos_faltantes": {
|
|
152
|
+
// Otro intento lleva otro contenido (los flags): con la misma clave el motor contestaría 409.
|
|
153
|
+
await d.idempotencia.olvidar(OPERACION, repo);
|
|
154
|
+
const install = flags.install ?? "…";
|
|
155
|
+
const test = flags.test ?? "…";
|
|
156
|
+
const message = `${alta.message || "Faltan comandos del proyecto."} Pasalos a mano: \`supcoflow proyecto crear --install ${comillas(install)} --test ${comillas(test)}\`.`;
|
|
157
|
+
return fallo(SALIDA.entradaInvalida, "comandos_faltantes", message, { faltantes: alta.faltantes });
|
|
158
|
+
}
|
|
159
|
+
case "no_conectada":
|
|
160
|
+
await d.idempotencia.olvidar(OPERACION, repo);
|
|
161
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", alta.message || "Conectá el repo primero: `supcoflow repo conectar`.", { repo });
|
|
162
|
+
case "en_curso":
|
|
163
|
+
// Se conserva: el pedido en curso puede ser el nuestro y el reintento tiene que poder retomarlo.
|
|
164
|
+
return fallo(SALIDA.inesperado, "en_curso", "Otro pedido con esta clave está en curso; probá en un minuto.");
|
|
165
|
+
case "clave_reusada":
|
|
166
|
+
// No debería pasar (la clave es por repo y un reintento manda lo mismo); olvidarla deja reintentar limpio.
|
|
167
|
+
await d.idempotencia.olvidar(OPERACION, repo);
|
|
168
|
+
return fallo(SALIDA.inesperado, "clave_reusada", "El motor rechazó la clave de idempotencia guardada; la descarté, probá de nuevo.");
|
|
169
|
+
case "rechazo": {
|
|
170
|
+
// La clave se conserva: tras un rechazo el motor ya la soltó, y el próximo intento la reusa sin
|
|
171
|
+
// chocar. Lo que cambia es el código: no todo rechazo es culpa de lo que se escribió.
|
|
172
|
+
const message = alta.message || "El motor rechazó el pedido.";
|
|
173
|
+
if (RECHAZOS_DE_UNA_PERSONA.has(alta.error)) {
|
|
174
|
+
// Lo arregla alguien con permisos en GitHub (instalar la App, sumarle el repo): reintentar o
|
|
175
|
+
// cambiar flags no sirve, y un 3 haría que un asistente siguiera probando entradas.
|
|
176
|
+
return fallo(SALIDA.haceFaltaUnaPersona, alta.error, `${message} Revisá la instalación de la App en GitHub y volvé a conectar con \`supcoflow repo conectar\`.`);
|
|
177
|
+
}
|
|
178
|
+
// Dos altas al mismo tiempo, o un proyecto borrado en el medio: el mismo pedido otra vez anda.
|
|
179
|
+
if (RECHAZOS_TRANSITORIOS.has(alta.error))
|
|
180
|
+
return fallo(SALIDA.inesperado, alta.error, message);
|
|
181
|
+
return fallo(SALIDA.entradaInvalida, alta.error, message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export async function proyectoVer(d, flags) {
|
|
186
|
+
let repo;
|
|
187
|
+
try {
|
|
188
|
+
repo = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).repo;
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
return resultadoDeFallo(e);
|
|
192
|
+
}
|
|
193
|
+
const leida = await claveVigente(d.guardado, d.api.origin);
|
|
194
|
+
if (!leida.ok)
|
|
195
|
+
return leida.resultado;
|
|
196
|
+
const clave = leida.vigente.clave;
|
|
197
|
+
try {
|
|
198
|
+
const encontrado = await d.buscarProyecto(d.api, clave, repo);
|
|
199
|
+
if (encontrado === null) {
|
|
200
|
+
return { codigo: SALIDA.ok, json: { ok: true, repo, proyecto: null }, texto: "Este repo todavía no tiene proyecto: `supcoflow proyecto crear`." };
|
|
201
|
+
}
|
|
202
|
+
const { proyecto: p, github } = await d.leerProyecto(d.api, clave, encontrado.id);
|
|
203
|
+
const proyecto = { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama, install_command: p.installCommand, test_command: p.testCommand };
|
|
204
|
+
return {
|
|
205
|
+
codigo: SALIDA.ok,
|
|
206
|
+
json: { ok: true, proyecto, github: { estado: github.estado, verificado_en: github.verificadoEn } },
|
|
207
|
+
texto: `${p.id} (${p.repo}, rama ${p.rama}): instala con \`${p.installCommand}\`, prueba con \`${p.testCommand}\`. GitHub: ${github.estado ?? "sin verificar"}.`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
return resultadoDeFallo(e);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { FalloDeLaTerminal, resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
+
import { claveVigente } from "./clave-vigente.js";
|
|
2
3
|
export async function comandoQuienSoy(d) {
|
|
3
4
|
const origen = d.api.origin;
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (vigente
|
|
9
|
-
const message = "No pude leer la clave guardada (el llavero no respondió). Desbloquealo y probá de nuevo.";
|
|
10
|
-
return { codigo: SALIDA.inesperado, json: { ok: false, error: "llavero_no_responde", message }, texto: message };
|
|
11
|
-
}
|
|
12
|
-
if (vigente === null) {
|
|
13
|
-
const message = "Iniciá sesión con `supcoflow login`.";
|
|
14
|
-
return { codigo: SALIDA.faltaLogin, json: { ok: false, error: "sin_sesion", message }, texto: message };
|
|
15
|
-
}
|
|
16
|
-
if (!lectura.completa) {
|
|
5
|
+
const leida = await claveVigente(d.guardado, origen);
|
|
6
|
+
if (!leida.ok)
|
|
7
|
+
return leida.resultado;
|
|
8
|
+
const vigente = leida.vigente;
|
|
9
|
+
if (!vigente.completa) {
|
|
17
10
|
const otro = vigente.fuente === "archivo" ? "el llavero del sistema" : "el archivo de credenciales";
|
|
18
11
|
d.avisar(`Uso la clave de ${vigente.fuente}, pero no pude leer ${otro}: puede haber guardada otra más nueva.`);
|
|
19
12
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
+
import { repoPedido } from "../repo-local.js";
|
|
3
|
+
import { PLAZO_MINIMO_DE_SONDEO_MS, TRAMO_MAXIMO_MS } from "../sondeo.js";
|
|
4
|
+
import { claveVigente } from "./clave-vigente.js";
|
|
5
|
+
export const INTERVALO_DE_CONEXION_MS = 5_000;
|
|
6
|
+
const PLAZO_MAXIMO_DE_SONDEO_MS = 15_000;
|
|
7
|
+
const PAUSA_ENTRE_TRAMOS_MS = 1_000;
|
|
8
|
+
const fallo = (codigo, error, message, extra = {}) => ({
|
|
9
|
+
codigo, json: { ok: false, error, message, ...extra }, texto: message,
|
|
10
|
+
});
|
|
11
|
+
const datosDelPedido = (p) => ({ id: p.id, estado: p.estado, url: p.url, vence_en: p.venceEn });
|
|
12
|
+
// El desenlace de un pedido ya leído. El JSON nunca lleva la clave: sólo el pedido y el repo.
|
|
13
|
+
function resultadoDelPedido(repo, p, avisar) {
|
|
14
|
+
const pedido = datosDelPedido(p);
|
|
15
|
+
switch (p.estado) {
|
|
16
|
+
case "conectado":
|
|
17
|
+
return {
|
|
18
|
+
codigo: SALIDA.ok,
|
|
19
|
+
json: { ok: true, estado: "conectado", repo, repo_canonico: p.repoCanonico, pedido },
|
|
20
|
+
texto: `${p.repoCanonico ?? repo} está conectado.`,
|
|
21
|
+
};
|
|
22
|
+
case "repo_fuera_de_alcance": {
|
|
23
|
+
const message = `La App está instalada pero no alcanza a ${repo}: sumalo en GitHub y volvé a tocar Confirmar (${p.url}).`;
|
|
24
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_fuera_de_alcance", message, { repo, url: p.url, pedido });
|
|
25
|
+
}
|
|
26
|
+
case "vencido":
|
|
27
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "pedido_vencido", "El pedido venció. Pedí otro con `supcoflow repo conectar --start`.", { repo, pedido });
|
|
28
|
+
case "pendiente": {
|
|
29
|
+
// Un motivo anotado no cierra el pedido: la persona puede reintentar desde la misma página.
|
|
30
|
+
if (p.fallo !== null)
|
|
31
|
+
avisar(`La última confirmación falló: ${p.fallo}.`);
|
|
32
|
+
return {
|
|
33
|
+
codigo: SALIDA.esperandoAUnaPersona,
|
|
34
|
+
json: { ok: false, esperando: "persona", repo, url: p.url, pedido, ...(p.fallo !== null ? { fallo: p.fallo } : {}) },
|
|
35
|
+
texto: `Abrí ${p.url}, instalá la App en GitHub si hace falta y tocá Confirmar.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function repoConectarStart(d, flags) {
|
|
41
|
+
let repo;
|
|
42
|
+
try {
|
|
43
|
+
repo = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).repo;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return resultadoDeFallo(e);
|
|
47
|
+
}
|
|
48
|
+
if (flags.dryRun) {
|
|
49
|
+
return {
|
|
50
|
+
codigo: SALIDA.ok,
|
|
51
|
+
json: { ok: true, dry_run: true, repo, haria: "POST /api/conexiones" },
|
|
52
|
+
texto: `Con --dry-run no hago nada; sin él, pediría conectar ${repo} (POST /api/conexiones).`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const leida = await claveVigente(d.guardado, d.api.origin);
|
|
56
|
+
if (!leida.ok)
|
|
57
|
+
return leida.resultado;
|
|
58
|
+
let p;
|
|
59
|
+
try {
|
|
60
|
+
p = await d.crearConexion(d.api, leida.vigente.clave, repo);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return resultadoDeFallo(e);
|
|
64
|
+
}
|
|
65
|
+
return resultadoDelPedido(repo, p, d.avisar);
|
|
66
|
+
}
|
|
67
|
+
export async function repoConectarWait(d, flags) {
|
|
68
|
+
let repo;
|
|
69
|
+
try {
|
|
70
|
+
repo = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).repo;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return resultadoDeFallo(e);
|
|
74
|
+
}
|
|
75
|
+
const leida = await claveVigente(d.guardado, d.api.origin);
|
|
76
|
+
if (!leida.ok)
|
|
77
|
+
return leida.resultado;
|
|
78
|
+
const clave = leida.vigente.clave;
|
|
79
|
+
const limite = d.ahora() + Math.min(d.tramoMs ?? TRAMO_MAXIMO_MS, TRAMO_MAXIMO_MS);
|
|
80
|
+
let ultimo = null;
|
|
81
|
+
for (;;) {
|
|
82
|
+
const plazoMs = Math.min(PLAZO_MAXIMO_DE_SONDEO_MS, limite - d.ahora());
|
|
83
|
+
if (plazoMs < PLAZO_MINIMO_DE_SONDEO_MS && ultimo !== null)
|
|
84
|
+
break;
|
|
85
|
+
let p;
|
|
86
|
+
try {
|
|
87
|
+
// Un fallo del motor en medio del tramo sale 1 y no se reintenta en silencio (P13): quien llama
|
|
88
|
+
// decide si vuelve.
|
|
89
|
+
p = await d.ultimaConexion(d.api, clave, repo, Math.max(plazoMs, PLAZO_MINIMO_DE_SONDEO_MS));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return resultadoDeFallo(e);
|
|
93
|
+
}
|
|
94
|
+
if (p === null) {
|
|
95
|
+
return fallo(SALIDA.entradaInvalida, "sin_conexion_en_curso", `No hay una conexión en curso para ${repo}: empezá con \`supcoflow repo conectar --start\`.`, { repo });
|
|
96
|
+
}
|
|
97
|
+
ultimo = p;
|
|
98
|
+
if (p.estado !== "pendiente")
|
|
99
|
+
break;
|
|
100
|
+
// Dormir sólo si después cabe otro sondeo con plazo útil: dormir de más pasaría del tramo.
|
|
101
|
+
if (limite - (d.ahora() + INTERVALO_DE_CONEXION_MS) < PLAZO_MINIMO_DE_SONDEO_MS)
|
|
102
|
+
break;
|
|
103
|
+
await d.dormir(INTERVALO_DE_CONEXION_MS);
|
|
104
|
+
}
|
|
105
|
+
return resultadoDelPedido(repo, ultimo, d.avisar);
|
|
106
|
+
}
|
|
107
|
+
// La conexión de una persona frente a la terminal: los dos pasos encadenados. Con `--json` (o sin TTY)
|
|
108
|
+
// no se espera a nadie aunque haya terminal (§13, Review Focus 9): un asistente con PTY recibiría un
|
|
109
|
+
// comando que se cuelga hasta 30 minutos. Y se decide ANTES de tocar el motor, para no dejar un pedido
|
|
110
|
+
// que nadie va a mirar.
|
|
111
|
+
export async function repoConectarInteractivo(d, flags) {
|
|
112
|
+
if (!d.esTty || flags.json) {
|
|
113
|
+
return fallo(SALIDA.entradaInvalida, "hace_falta_tty", "Sin terminal interactiva usá `supcoflow repo conectar --start --json` y después `supcoflow repo conectar --wait --json`.");
|
|
114
|
+
}
|
|
115
|
+
const inicio = await repoConectarStart(d, flags);
|
|
116
|
+
if (inicio.codigo !== SALIDA.esperandoAUnaPersona)
|
|
117
|
+
return inicio;
|
|
118
|
+
d.mostrar(`${inicio.texto} Espero acá…`);
|
|
119
|
+
let r;
|
|
120
|
+
do {
|
|
121
|
+
r = await repoConectarWait(d, flags);
|
|
122
|
+
if (r.codigo === SALIDA.esperandoAUnaPersona)
|
|
123
|
+
await d.dormir(PAUSA_ENTRE_TRAMOS_MS);
|
|
124
|
+
} while (r.codigo === SALIDA.esperandoAUnaPersona);
|
|
125
|
+
return r;
|
|
126
|
+
}
|