supcoflow 0.7.0 → 0.8.1
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 +253 -176
- package/dist/alias.js +60 -0
- package/dist/api.js +41 -19
- package/dist/argumentos.js +92 -85
- package/dist/cli.js +40 -35
- package/dist/comandos/clave-vigente.js +2 -2
- package/dist/comandos/claves.js +25 -25
- package/dist/comandos/configurar.js +40 -40
- package/dist/comandos/contexto.js +102 -0
- package/dist/comandos/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +162 -152
- package/dist/comandos/guia.js +150 -62
- package/dist/comandos/instalacion.js +73 -41
- package/dist/comandos/login.js +25 -24
- package/dist/comandos/logout.js +17 -16
- package/dist/comandos/probar.js +177 -64
- package/dist/comandos/proyecto-del-repo.js +12 -2
- package/dist/comandos/proyecto.js +48 -34
- package/dist/comandos/quien-soy.js +8 -7
- package/dist/comandos/repo-conectar.js +9 -9
- package/dist/comandos/widget.js +9 -5
- package/dist/diagnostico.js +51 -0
- package/dist/entorno.js +57 -26
- package/dist/flags-por-comando.js +25 -21
- package/dist/guardado/archivo.js +2 -2
- package/dist/guardado/index.js +12 -8
- package/dist/guardado/llavero.js +5 -5
- package/dist/guardado/lugar.js +63 -0
- package/dist/guardado/protegido.js +38 -0
- package/dist/guardado/repo.js +98 -0
- package/dist/guia.js +3 -1
- package/dist/idempotencia.js +16 -28
- package/dist/instalacion.js +50 -11
- package/dist/lectura.js +10 -0
- package/dist/pruebas-guardadas.js +40 -22
- package/dist/repo-local.js +95 -13
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
|
@@ -5,11 +5,9 @@ import path from "node:path";
|
|
|
5
5
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
6
6
|
import { repoPedido } from "../repo-local.js";
|
|
7
7
|
import { claveVigente } from "./clave-vigente.js";
|
|
8
|
+
import { armarElContexto, coherenciaAlCrear, coherenciaDelClon, idDelArchivo, leerElArchivoDelClon, lineaDelContexto } from "./contexto.js";
|
|
8
9
|
const PLAZO_DE_LECTURA_MS = 15_000;
|
|
9
10
|
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
11
|
// Los 422/409 del alta que el plan no listaba (alta-desde-conexion.ts del motor).
|
|
14
12
|
const RECHAZOS_DE_UNA_PERSONA = new Set(["instalacion_no_verificada", "repo_fuera_de_la_instalacion"]);
|
|
15
13
|
const RECHAZOS_TRANSITORIOS = new Set(["slug_ocupado", "repo_ya_conectado"]);
|
|
@@ -23,18 +21,6 @@ const comillas = (s) => `"${s.replace(/["\\]/g, "\\$&")}"`;
|
|
|
23
21
|
function contenidoDelArchivo(p) {
|
|
24
22
|
return JSON.stringify({ id: p.id, slug: p.slug, repo: p.repo }, null, 2) + "\n";
|
|
25
23
|
}
|
|
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
24
|
// Mismo contenido aunque el formato cambie (otra indentación, otro orden): no es «otro proyecto».
|
|
39
25
|
function mismoArchivo(crudo, p) {
|
|
40
26
|
if (crudo === contenidoDelArchivo(p))
|
|
@@ -52,7 +38,7 @@ function mismoArchivo(crudo, p) {
|
|
|
52
38
|
}
|
|
53
39
|
function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
54
40
|
const nuevo = { id: p.id, slug: p.slug, repo: p.repo };
|
|
55
|
-
const queda = reusado ? `
|
|
41
|
+
const queda = reusado ? `Project ${p.id} already existed and I reused it` : `I created project ${p.id}`;
|
|
56
42
|
let archivo = null;
|
|
57
43
|
if (raiz !== null) {
|
|
58
44
|
archivo = path.join(raiz, ".supcoflow", "proyecto.json");
|
|
@@ -63,8 +49,8 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
63
49
|
// decida. Su contenido NUNCA se copia a la salida (puede traer cualquier cosa, una clave
|
|
64
50
|
// incluida, y `emitir` no sanea): sólo el id, si tiene forma de id.
|
|
65
51
|
const idActual = idDelArchivo(actual);
|
|
66
|
-
const message = `
|
|
67
|
-
+ `${queda}
|
|
52
|
+
const message = `There is already a \`.supcoflow/proyecto.json\` for another project (${idActual ?? "one I don't recognize"}); I didn't touch it. `
|
|
53
|
+
+ `${queda} anyway: if that's the one you want, delete the file and run \`supcoflow project create\` again.`;
|
|
68
54
|
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_distinto", message, { archivo, id_actual: idActual, nuevo, reusado });
|
|
69
55
|
}
|
|
70
56
|
if (actual === null) {
|
|
@@ -72,7 +58,7 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
72
58
|
d.archivos.escribir(archivo, contenidoDelArchivo(p));
|
|
73
59
|
}
|
|
74
60
|
catch {
|
|
75
|
-
const message = `${queda}
|
|
61
|
+
const message = `${queda}, but I couldn't write ${archivo}. Run \`supcoflow project create\` again.`;
|
|
76
62
|
return fallo(SALIDA.inesperado, "archivo_no_escrito", message, { archivo, nuevo, reusado });
|
|
77
63
|
}
|
|
78
64
|
}
|
|
@@ -80,7 +66,7 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
80
66
|
return {
|
|
81
67
|
codigo: SALIDA.ok,
|
|
82
68
|
json: { ok: true, proyecto: { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama }, reusado, archivo },
|
|
83
|
-
texto: `${queda}: ${p.repo},
|
|
69
|
+
texto: `${queda}: ${p.repo}, branch ${p.rama}.${archivo !== null ? ` Recorded in ${archivo}.` : ""}`,
|
|
84
70
|
};
|
|
85
71
|
}
|
|
86
72
|
export async function proyectoCrear(d, flags) {
|
|
@@ -96,12 +82,24 @@ export async function proyectoCrear(d, flags) {
|
|
|
96
82
|
if (!leida.ok)
|
|
97
83
|
return leida.resultado;
|
|
98
84
|
const clave = leida.vigente.clave;
|
|
85
|
+
// Antes de conectar o crear nada (fricción 6): el archivo del clon contra lo que la clave ve, y, si va a haber un alta, el
|
|
86
|
+
// dueño del repo contra la cuenta. Sale 4 `contexto_incoherente` sin tocar nada.
|
|
87
|
+
const archivo = leerElArchivoDelClon(d.archivos, raiz);
|
|
88
|
+
if (archivo.estado === "ilegible")
|
|
89
|
+
return coherenciaDelClon(archivo, null, repo);
|
|
99
90
|
let existente;
|
|
100
91
|
let conexion = null;
|
|
101
92
|
try {
|
|
102
93
|
existente = await d.buscarProyecto(d.api, clave, repo);
|
|
103
|
-
|
|
94
|
+
const incoherente = coherenciaDelClon(archivo, existente, repo);
|
|
95
|
+
if (incoherente !== null)
|
|
96
|
+
return incoherente;
|
|
97
|
+
if (existente === null) {
|
|
98
|
+
const alCrear = coherenciaAlCrear({ yo: await d.quienSoy(d.api, clave), fuente: leida.vigente.fuente, repo });
|
|
99
|
+
if (alCrear !== null)
|
|
100
|
+
return alCrear;
|
|
104
101
|
conexion = await d.ultimaConexion(d.api, clave, repo, PLAZO_DE_LECTURA_MS);
|
|
102
|
+
}
|
|
105
103
|
}
|
|
106
104
|
catch (e) {
|
|
107
105
|
return resultadoDeFallo(e);
|
|
@@ -116,18 +114,18 @@ export async function proyectoCrear(d, flags) {
|
|
|
116
114
|
if (flags.install !== null || flags.test !== null) {
|
|
117
115
|
// Sin copiar los valores: son texto del usuario y el aviso sólo dice qué pasó con ellos. Callarlo
|
|
118
116
|
// haría creer que el proyecto quedó con los comandos pedidos.
|
|
119
|
-
d.avisar("
|
|
117
|
+
d.avisar("The project already exists: I ignored --install/--test. To change its commands, it has to be configured.");
|
|
120
118
|
}
|
|
121
119
|
return resultadoDelProyecto(existente, true, raiz, d);
|
|
122
120
|
}
|
|
123
121
|
if (conexion === null || conexion.estado !== "conectado") {
|
|
124
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", "
|
|
122
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", "Connect the repo first: `supcoflow repo connect`.", {
|
|
125
123
|
repo, ...(conexion !== null ? { estado: conexion.estado } : {}),
|
|
126
124
|
});
|
|
127
125
|
}
|
|
128
126
|
if (flags.dryRun) {
|
|
129
127
|
const haria = `POST /api/conexiones/${conexion.id}/proyecto`;
|
|
130
|
-
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, repo, haria }, texto: `
|
|
128
|
+
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, repo, haria }, texto: `With --dry-run I do nothing; without it: create the project for ${repo} (${haria}).` };
|
|
131
129
|
}
|
|
132
130
|
const cuerpo = {};
|
|
133
131
|
if (flags.install !== null)
|
|
@@ -153,27 +151,27 @@ export async function proyectoCrear(d, flags) {
|
|
|
153
151
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
154
152
|
const install = flags.install ?? "…";
|
|
155
153
|
const test = flags.test ?? "…";
|
|
156
|
-
const message = `${alta.message || "
|
|
154
|
+
const message = `${alta.message || "The project is missing commands."} Pass them yourself: \`supcoflow project create --install ${comillas(install)} --test ${comillas(test)}\`.`;
|
|
157
155
|
return fallo(SALIDA.entradaInvalida, "comandos_faltantes", message, { faltantes: alta.faltantes });
|
|
158
156
|
}
|
|
159
157
|
case "no_conectada":
|
|
160
158
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
161
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", alta.message || "
|
|
159
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", alta.message || "Connect the repo first: `supcoflow repo connect`.", { repo });
|
|
162
160
|
case "en_curso":
|
|
163
161
|
// Se conserva: el pedido en curso puede ser el nuestro y el reintento tiene que poder retomarlo.
|
|
164
|
-
return fallo(SALIDA.inesperado, "en_curso", "
|
|
162
|
+
return fallo(SALIDA.inesperado, "en_curso", "Another request with this key is already in progress; try again in a minute.");
|
|
165
163
|
case "clave_reusada":
|
|
166
164
|
// No debería pasar (la clave es por repo y un reintento manda lo mismo); olvidarla deja reintentar limpio.
|
|
167
165
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
168
|
-
return fallo(SALIDA.inesperado, "clave_reusada", "
|
|
166
|
+
return fallo(SALIDA.inesperado, "clave_reusada", "The engine rejected the saved idempotency key; I discarded it, try again.");
|
|
169
167
|
case "rechazo": {
|
|
170
168
|
// La clave se conserva: tras un rechazo el motor ya la soltó, y el próximo intento la reusa sin
|
|
171
169
|
// chocar. Lo que cambia es el código: no todo rechazo es culpa de lo que se escribió.
|
|
172
|
-
const message = alta.message || "
|
|
170
|
+
const message = alta.message || "The engine rejected the request.";
|
|
173
171
|
if (RECHAZOS_DE_UNA_PERSONA.has(alta.error)) {
|
|
174
172
|
// Lo arregla alguien con permisos en GitHub (instalar la App, sumarle el repo): reintentar o
|
|
175
173
|
// cambiar flags no sirve, y un 3 haría que un asistente siguiera probando entradas.
|
|
176
|
-
return fallo(SALIDA.haceFaltaUnaPersona, alta.error, `${message}
|
|
174
|
+
return fallo(SALIDA.haceFaltaUnaPersona, alta.error, `${message} Check the App's installation on GitHub and connect again with \`supcoflow repo connect\`.`);
|
|
177
175
|
}
|
|
178
176
|
// Dos altas al mismo tiempo, o un proyecto borrado en el medio: el mismo pedido otra vez anda.
|
|
179
177
|
if (RECHAZOS_TRANSITORIOS.has(alta.error))
|
|
@@ -184,8 +182,9 @@ export async function proyectoCrear(d, flags) {
|
|
|
184
182
|
}
|
|
185
183
|
export async function proyectoVer(d, flags) {
|
|
186
184
|
let repo;
|
|
185
|
+
let raiz;
|
|
187
186
|
try {
|
|
188
|
-
repo =
|
|
187
|
+
({ repo, raiz } = await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd }));
|
|
189
188
|
}
|
|
190
189
|
catch (e) {
|
|
191
190
|
return resultadoDeFallo(e);
|
|
@@ -194,17 +193,32 @@ export async function proyectoVer(d, flags) {
|
|
|
194
193
|
if (!leida.ok)
|
|
195
194
|
return leida.resultado;
|
|
196
195
|
const clave = leida.vigente.clave;
|
|
196
|
+
const archivo = leerElArchivoDelClon(d.archivos, raiz);
|
|
197
|
+
if (archivo.estado === "ilegible")
|
|
198
|
+
return coherenciaDelClon(archivo, null, repo);
|
|
197
199
|
try {
|
|
198
200
|
const encontrado = await d.buscarProyecto(d.api, clave, repo);
|
|
201
|
+
const incoherente = coherenciaDelClon(archivo, encontrado, repo);
|
|
202
|
+
if (incoherente !== null)
|
|
203
|
+
return incoherente;
|
|
204
|
+
// Quién soy, en qué equipo, qué repo y qué proyecto: la línea que la persona lee al empezar, también cuando todo cierra.
|
|
205
|
+
const yo = await d.quienSoy(d.api, clave);
|
|
206
|
+
const contexto = armarElContexto({ yo, fuente: leida.vigente.fuente, repo, proyecto: encontrado, archivo });
|
|
207
|
+
const linea = lineaDelContexto(contexto, yo.proyecto);
|
|
199
208
|
if (encontrado === null) {
|
|
200
|
-
|
|
209
|
+
// Sin proyecto, el recorrido sigue con `repo connect`: el freno por cuenta (dueño distinto, sin GitHub) tiene que llegar ANTES,
|
|
210
|
+
// no al crear. Con proyecto existente no se compara nada, y con una clave de consulta tampoco.
|
|
211
|
+
const alCrear = coherenciaAlCrear({ yo, fuente: leida.vigente.fuente, repo });
|
|
212
|
+
if (alCrear !== null)
|
|
213
|
+
return alCrear;
|
|
214
|
+
return { codigo: SALIDA.ok, json: { ok: true, repo, proyecto: null, contexto }, texto: `${linea} This repo doesn't have a project yet: \`supcoflow project create\`.` };
|
|
201
215
|
}
|
|
202
216
|
const { proyecto: p, github } = await d.leerProyecto(d.api, clave, encontrado.id);
|
|
203
217
|
const proyecto = { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama, install_command: p.installCommand, test_command: p.testCommand };
|
|
204
218
|
return {
|
|
205
219
|
codigo: SALIDA.ok,
|
|
206
|
-
json: { ok: true, proyecto, github: { estado: github.estado, verificado_en: github.verificadoEn } },
|
|
207
|
-
texto: `${p.id} (${p.repo},
|
|
220
|
+
json: { ok: true, proyecto, github: { estado: github.estado, verificado_en: github.verificadoEn }, contexto },
|
|
221
|
+
texto: `${linea} ${p.id} (${p.repo}, branch ${p.rama}): installs with \`${p.installCommand}\`, tests with \`${p.testCommand}\`. GitHub: ${github.estado ?? "not verified"}.`,
|
|
208
222
|
};
|
|
209
223
|
}
|
|
210
224
|
catch (e) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FalloDeLaTerminal, resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
+
import { NOMBRE_DE_LA_FUENTE } from "../guardado/index.js";
|
|
2
3
|
import { claveVigente } from "./clave-vigente.js";
|
|
3
4
|
export async function comandoQuienSoy(d) {
|
|
4
5
|
const origen = d.api.origin;
|
|
@@ -7,20 +8,20 @@ export async function comandoQuienSoy(d) {
|
|
|
7
8
|
return leida.resultado;
|
|
8
9
|
const vigente = leida.vigente;
|
|
9
10
|
if (!vigente.completa) {
|
|
10
|
-
const otro = vigente.fuente === "archivo" ?
|
|
11
|
-
d.avisar(`
|
|
11
|
+
const otro = vigente.fuente === "archivo" ? NOMBRE_DE_LA_FUENTE.llavero : NOMBRE_DE_LA_FUENTE.archivo;
|
|
12
|
+
d.avisar(`I'm using the key in ${NOMBRE_DE_LA_FUENTE[vigente.fuente]}, but I couldn't read ${otro}: a newer key may be saved there.`);
|
|
12
13
|
}
|
|
13
14
|
// Leer no cuesta nada y avisa de algo que la persona no vería de otro modo: claves viejas vivas.
|
|
14
15
|
try {
|
|
15
16
|
const { claves, completa } = await d.guardado.leerPorRevocar(origen);
|
|
16
17
|
if (claves.length > 0) {
|
|
17
|
-
d.avisar(
|
|
18
|
+
d.avisar(`${claves.length} previous key(s) are noted down to be revoked; I'll try again on the next login or logout.`);
|
|
18
19
|
}
|
|
19
20
|
if (!completa)
|
|
20
|
-
d.avisar("
|
|
21
|
+
d.avisar("I couldn't read the whole list of keys waiting to be revoked.");
|
|
21
22
|
}
|
|
22
23
|
catch {
|
|
23
|
-
d.avisar("
|
|
24
|
+
d.avisar("I couldn't read the whole list of keys waiting to be revoked.");
|
|
24
25
|
}
|
|
25
26
|
let yo;
|
|
26
27
|
try {
|
|
@@ -30,7 +31,7 @@ export async function comandoQuienSoy(d) {
|
|
|
30
31
|
// Con la variable definida, «iniciá sesión de nuevo» no arregla nada: la variable sigue
|
|
31
32
|
// ganándole a la clave nueva. Hay que decir cuál es la que no vale.
|
|
32
33
|
if (e instanceof FalloDeLaTerminal && e.codigo === SALIDA.faltaLogin && vigente.fuente === "env") {
|
|
33
|
-
return resultadoDeFallo(new FalloDeLaTerminal(e.codigo, e.error, "
|
|
34
|
+
return resultadoDeFallo(new FalloDeLaTerminal(e.codigo, e.error, "The key in SUPCOFLOW_TOKEN is not valid: remove it from your environment or replace it with a valid one."));
|
|
34
35
|
}
|
|
35
36
|
return resultadoDeFallo(e);
|
|
36
37
|
}
|
|
@@ -38,6 +39,6 @@ export async function comandoQuienSoy(d) {
|
|
|
38
39
|
return {
|
|
39
40
|
codigo: SALIDA.ok,
|
|
40
41
|
json: { ok: true, via: yo.via, equipo: yo.equipo, usuario: yo.usuario, proyecto: yo.proyecto, fuente: vigente.fuente },
|
|
41
|
-
texto: `${quien}
|
|
42
|
+
texto: `${quien} in team ${yo.equipo} (key in ${NOMBRE_DE_LA_FUENTE[vigente.fuente]})`,
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -17,22 +17,22 @@ function resultadoDelPedido(repo, p, avisar) {
|
|
|
17
17
|
return {
|
|
18
18
|
codigo: SALIDA.ok,
|
|
19
19
|
json: { ok: true, estado: "conectado", repo, repo_canonico: p.repoCanonico, pedido },
|
|
20
|
-
texto: `${p.repoCanonico ?? repo}
|
|
20
|
+
texto: `${p.repoCanonico ?? repo} is connected.`,
|
|
21
21
|
};
|
|
22
22
|
case "repo_fuera_de_alcance": {
|
|
23
|
-
const message = `
|
|
23
|
+
const message = `The App is installed but doesn't have access to ${repo}: add it in GitHub and click Confirm again (${p.url}).`;
|
|
24
24
|
return fallo(SALIDA.haceFaltaUnaPersona, "repo_fuera_de_alcance", message, { repo, url: p.url, pedido });
|
|
25
25
|
}
|
|
26
26
|
case "vencido":
|
|
27
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "pedido_vencido", "
|
|
27
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "pedido_vencido", "The request has expired. Ask for a new one with `supcoflow repo connect --start`.", { repo, pedido });
|
|
28
28
|
case "pendiente": {
|
|
29
29
|
// Un motivo anotado no cierra el pedido: la persona puede reintentar desde la misma página.
|
|
30
30
|
if (p.fallo !== null)
|
|
31
|
-
avisar(`
|
|
31
|
+
avisar(`The last confirmation failed: ${p.fallo}.`);
|
|
32
32
|
return {
|
|
33
33
|
codigo: SALIDA.esperandoAUnaPersona,
|
|
34
34
|
json: { ok: false, esperando: "persona", repo, url: p.url, pedido, ...(p.fallo !== null ? { fallo: p.fallo } : {}) },
|
|
35
|
-
texto: `
|
|
35
|
+
texto: `Open ${p.url}, install the App on GitHub if needed, and click Confirm.`,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -49,7 +49,7 @@ export async function repoConectarStart(d, flags) {
|
|
|
49
49
|
return {
|
|
50
50
|
codigo: SALIDA.ok,
|
|
51
51
|
json: { ok: true, dry_run: true, repo, haria: "POST /api/conexiones" },
|
|
52
|
-
texto: `
|
|
52
|
+
texto: `With --dry-run I do nothing; without it: ask to connect ${repo} (POST /api/conexiones).`,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
const leida = await claveVigente(d.guardado, d.api.origin);
|
|
@@ -92,7 +92,7 @@ export async function repoConectarWait(d, flags) {
|
|
|
92
92
|
return resultadoDeFallo(e);
|
|
93
93
|
}
|
|
94
94
|
if (p === null) {
|
|
95
|
-
return fallo(SALIDA.entradaInvalida, "sin_conexion_en_curso", `
|
|
95
|
+
return fallo(SALIDA.entradaInvalida, "sin_conexion_en_curso", `There is no connection in progress for ${repo}: start with \`supcoflow repo connect --start\`.`, { repo });
|
|
96
96
|
}
|
|
97
97
|
ultimo = p;
|
|
98
98
|
if (p.estado !== "pendiente")
|
|
@@ -110,12 +110,12 @@ export async function repoConectarWait(d, flags) {
|
|
|
110
110
|
// que nadie va a mirar.
|
|
111
111
|
export async function repoConectarInteractivo(d, flags) {
|
|
112
112
|
if (!d.esTty || flags.json) {
|
|
113
|
-
return fallo(SALIDA.entradaInvalida, "hace_falta_tty", "
|
|
113
|
+
return fallo(SALIDA.entradaInvalida, "hace_falta_tty", "Without an interactive terminal, use `supcoflow repo connect --start --json` and then `supcoflow repo connect --wait --json`.");
|
|
114
114
|
}
|
|
115
115
|
const inicio = await repoConectarStart(d, flags);
|
|
116
116
|
if (inicio.codigo !== SALIDA.esperandoAUnaPersona)
|
|
117
117
|
return inicio;
|
|
118
|
-
d.mostrar(`${inicio.texto}
|
|
118
|
+
d.mostrar(`${inicio.texto} I'll wait here...`);
|
|
119
119
|
let r;
|
|
120
120
|
do {
|
|
121
121
|
r = await repoConectarWait(d, flags);
|
package/dist/comandos/widget.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
2
|
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
3
3
|
function textoDe(w) {
|
|
4
|
+
const anteriores = w.nextjs.archivos_anteriores;
|
|
4
5
|
return [
|
|
5
|
-
"1.
|
|
6
|
+
"1. Install the package (with the app's package manager, the exact version):",
|
|
6
7
|
` ${w.instalar}`,
|
|
7
8
|
"",
|
|
8
|
-
`2.
|
|
9
|
+
`2. Create the file ${w.nextjs.archivo_sugerido} with this content (Next.js, ${w.nextjs.estado}):`,
|
|
9
10
|
w.nextjs.contenido.trimEnd(),
|
|
11
|
+
...(anteriores.length > 0
|
|
12
|
+
? [` If the chat is already installed in ${anteriores.join(" or ")}, update that file in place instead of creating a second one.`]
|
|
13
|
+
: []),
|
|
10
14
|
"",
|
|
11
|
-
"3.
|
|
15
|
+
"3. Use it in the layout that covers the screens where users get stuck:",
|
|
12
16
|
` ${w.nextjs.uso}`,
|
|
13
17
|
"",
|
|
14
|
-
"4.
|
|
18
|
+
"4. Content Security Policy (CSP): if the app has `connect-src`, add this origin and keep what it already has:",
|
|
15
19
|
` ${w.csp}`,
|
|
16
20
|
"",
|
|
17
|
-
"
|
|
21
|
+
"Alternative with <script> (available, not tested in a real app):",
|
|
18
22
|
` ${w.script.linea}`,
|
|
19
23
|
].join("\n");
|
|
20
24
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { frasePorQueNoAtaClon } from "./repo-local.js";
|
|
2
|
+
export const TOTAL_DE_INSTALACION = 6;
|
|
3
|
+
// El orden y los nombres con que se dicen: los ids de los puntos del motor son datos suyos y no cambian.
|
|
4
|
+
const DE_LA_INSTALACION = [
|
|
5
|
+
{ punto: "sign-in", como: "sign-in" },
|
|
6
|
+
{ punto: "repo", como: "repo" },
|
|
7
|
+
{ punto: "instalacion", como: "GitHub App" },
|
|
8
|
+
{ punto: "sitios", como: "allowed sites" },
|
|
9
|
+
{ punto: "chat", como: "chat" },
|
|
10
|
+
{ punto: "guia", como: "guide" },
|
|
11
|
+
];
|
|
12
|
+
const DEL_MOTOR = new Set(["instalacion", "sitios", "chat", "guia"]);
|
|
13
|
+
export const lineaDeLoRevisado = (n) => `Checked ${DE_LA_INSTALACION.map((x) => x.como).slice(0, -1).join(", ")} and ${DE_LA_INSTALACION.at(-1).como}: ${n} of ${TOTAL_DE_INSTALACION}, at no cost.`;
|
|
14
|
+
export const nombreDelPunto = (punto) => DE_LA_INSTALACION.find((x) => x.punto === punto)?.como ?? punto;
|
|
15
|
+
export function puntoDeSesion() {
|
|
16
|
+
return { punto: "sign-in", ok: true, mensaje: "This request was authenticated: you're signed in.", grupo: "instalacion" };
|
|
17
|
+
}
|
|
18
|
+
export function puntoDelRepo(local, repoDelProyecto) {
|
|
19
|
+
const mal = (mensaje) => ({ punto: "repo", ok: false, mensaje, grupo: "instalacion" });
|
|
20
|
+
if (local.repo === null) {
|
|
21
|
+
if (local.motivo === "sin_git")
|
|
22
|
+
return mal("This folder isn't a git repo, so I can't tell which repo it is.");
|
|
23
|
+
if (local.motivo === "sin_origin")
|
|
24
|
+
return mal("This repo has no «origin» remote, so I can't tell which repo it is.");
|
|
25
|
+
if (local.motivo === "origin_alias_ssh")
|
|
26
|
+
return mal(`The «origin» of this repo isn't a GitHub repo. ${frasePorQueNoAtaClon(local, null)}`);
|
|
27
|
+
return mal("The «origin» of this repo isn't a GitHub repo.");
|
|
28
|
+
}
|
|
29
|
+
if (local.repo.toLowerCase() !== repoDelProyecto.toLowerCase()) {
|
|
30
|
+
return mal(`The origin of this folder is ${local.repo}, not the project's repo ${repoDelProyecto}.`);
|
|
31
|
+
}
|
|
32
|
+
return { punto: "repo", ok: true, mensaje: `The origin of this folder is ${local.repo}, the project's repo.`, grupo: "instalacion" };
|
|
33
|
+
}
|
|
34
|
+
/** Reparte los puntos del motor en los dos grupos y suma los dos locales al de la instalación. */
|
|
35
|
+
export function agruparPuntos(g, locales) {
|
|
36
|
+
const delMotor = g.puntos.map((p) => ({ ...p, grupo: p.grupo ?? (DEL_MOTOR.has(p.punto) ? "instalacion" : "prueba") }));
|
|
37
|
+
const instalacion = [...locales, ...delMotor.filter((p) => p.grupo === "instalacion")];
|
|
38
|
+
// Un punto del grupo que el motor no mandó no se da por bueno: se dice que falta y cuenta como no ok.
|
|
39
|
+
for (const { punto } of DE_LA_INSTALACION) {
|
|
40
|
+
if (!instalacion.some((p) => p.punto === punto)) {
|
|
41
|
+
instalacion.push({ punto, ok: false, mensaje: "The engine didn't report this point.", grupo: "instalacion" });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const orden = (p) => {
|
|
45
|
+
const i = DE_LA_INSTALACION.findIndex((x) => x.punto === p.punto);
|
|
46
|
+
return i === -1 ? DE_LA_INSTALACION.length : i;
|
|
47
|
+
};
|
|
48
|
+
return { instalacion: instalacion.sort((a, b) => orden(a) - orden(b)), prueba: delMotor.filter((p) => p.grupo === "prueba") };
|
|
49
|
+
}
|
|
50
|
+
/** Cuántos de los seis puntos de la instalación están bien (los extras que un motor nuevo sume no cambian el seis). */
|
|
51
|
+
export const contarBuenos = (instalacion) => DE_LA_INSTALACION.filter(({ punto }) => instalacion.some((p) => p.punto === punto && p.ok)).length;
|
package/dist/entorno.js
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
// demás recibe estas piezas inyectadas y se prueba con dobles; acá sólo se enchufan.
|
|
3
3
|
import { execFile, spawn } from "node:child_process";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { chmodSync, closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, rmSync, writeSync, } from "node:fs";
|
|
5
|
+
import { accessSync, chmodSync, closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, mkdirSync, statSync, openSync, readFileSync, readSync, realpathSync, renameSync, rmSync, writeSync, } from "node:fs";
|
|
6
|
+
import { NO_LEIDO } from "./lectura.js";
|
|
6
7
|
import { homedir, hostname } from "node:os";
|
|
7
8
|
import { basename, dirname, join } from "node:path";
|
|
8
|
-
import { buscarProyecto, canjear, configurar, leerDiagnostico, leerPrueba, leerWidget, pedirPrueba, crearConexion, crearProyecto, elegirOrigen, esClaveDeConsultaDelProyecto, leerCatalogo, leerConfiguracion, leerGuia, leerProyecto, listarClaves, pedirCodigo, quienSoy, revocarPropia, subirLogo, ultimaConexion, urlDeLaApi, publicarGuia, revisarGuia, } from "./api.js";
|
|
9
|
+
import { buscarProyecto, canjear, configurar, leerDiagnostico, leerPrueba, leerWidget, pedirPrueba, crearConexion, crearProyecto, elegirOrigen, esClaveDeConsultaDelProyecto, leerCatalogo, leerConfiguracion, leerGuia, leerProyecto, listarClaves, pedirCodigo, quienSoy, revocarPropia, subirLogo, ultimaConexion, urlDeLaApi, publicarGuia, revisarGuia, leerVersionesDeLaGuia, } from "./api.js";
|
|
9
10
|
import { crearCandados } from "./candado.js";
|
|
10
|
-
import { esCarpetaReal, rutaRealOEnNada } from "./comandos/instalacion.js";
|
|
11
|
+
import { esCarpetaReal, esEnlaceReal, rutaRealOEnNada } from "./comandos/instalacion.js";
|
|
11
12
|
import { DESCRIPCION } from "./comandos/describir.js";
|
|
12
13
|
import { almacenDeArchivo } from "./guardado/archivo.js";
|
|
13
14
|
import { crearGuardado } from "./guardado/index.js";
|
|
15
|
+
import { almacenProtegido } from "./guardado/protegido.js";
|
|
16
|
+
import { revisarElLugar } from "./guardado/lugar.js";
|
|
14
17
|
import { almacenDeLlavero } from "./guardado/llavero.js";
|
|
15
18
|
import { crearGuardaDeSecretos } from "./secreto-a-archivo.js";
|
|
16
19
|
import { crearRegistroDeIdempotencia } from "./idempotencia.js";
|
|
@@ -51,7 +54,7 @@ export const fsReal = {
|
|
|
51
54
|
return realpathSync(p);
|
|
52
55
|
}
|
|
53
56
|
catch {
|
|
54
|
-
throw new Error(`
|
|
57
|
+
throw new Error(`I can't resolve the real path of ${p} (a broken symlink?); I'm not saving anything there.`);
|
|
55
58
|
}
|
|
56
59
|
},
|
|
57
60
|
// `lstat` y no `existsSync`: éste sigue el enlace y ante uno roto dice «no existe», y el control
|
|
@@ -67,6 +70,29 @@ export const fsReal = {
|
|
|
67
70
|
}
|
|
68
71
|
},
|
|
69
72
|
};
|
|
73
|
+
// Lo que el prechequeo de la máquina (src/guardado/lugar.ts) ve del disco, sin escribir. `estado` sigue los enlaces (`stat`): un
|
|
74
|
+
// enlace roto es «no existe» para él, y `existe` (que es `lstat`) lo distingue.
|
|
75
|
+
export const fsDelLugarReal = {
|
|
76
|
+
...fsReal,
|
|
77
|
+
estado(p) {
|
|
78
|
+
try {
|
|
79
|
+
const st = statSync(p);
|
|
80
|
+
return { existe: true, tipo: st.isDirectory() ? "carpeta" : st.isFile() ? "archivo" : "otro", propio: typeof process.getuid !== "function" || st.uid === process.getuid() };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return { existe: false };
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
puede(p, que) {
|
|
87
|
+
try {
|
|
88
|
+
accessSync(p, que === "leer" ? constants.R_OK : constants.W_OK | constants.X_OK);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
70
96
|
// El candado de `claves crear` (src/candado.ts). `wx` es la exclusión: SÓLO `EEXIST` es «ya hay uno»;
|
|
71
97
|
// cualquier otro error (permisos, disco) se relanza y sale `no_disponible`, nunca «ocupado». La lectura no
|
|
72
98
|
// sigue enlaces: un `.lock` que alguien cambió por un enlace no se lee como si fuera el nuestro.
|
|
@@ -252,7 +278,7 @@ export const TOPE_DEL_ARCHIVO_DEL_REPO = 64 * 1024;
|
|
|
252
278
|
// Lo que devuelve `leer` cuando hay ALGO ahí que no se lee (un enlace, una carpeta, algo enorme, sin
|
|
253
279
|
// permiso). No es JSON a propósito: `proyecto crear` lo trata como un archivo distinto (sale 4, «no lo
|
|
254
280
|
// toqué») sin que nada de lo que hay ahí llegue a la salida.
|
|
255
|
-
export const ARCHIVO_QUE_NO_SE_LEE = "\u0000(
|
|
281
|
+
export const ARCHIVO_QUE_NO_SE_LEE = "\u0000(unreadable)";
|
|
256
282
|
// Un archivo que la persona nombra (el logo) con las mismas defensas que los del repo: sin seguir
|
|
257
283
|
// enlaces, sin FIFOs que cuelguen, sólo archivos comunes y con tope (se lee tope + 1 por si crece).
|
|
258
284
|
export function leerArchivoChico(p, tope) {
|
|
@@ -262,11 +288,11 @@ export function leerArchivoChico(p, tope) {
|
|
|
262
288
|
}
|
|
263
289
|
catch (e) {
|
|
264
290
|
const code = e.code;
|
|
265
|
-
return { error: code === "ENOENT" ?
|
|
291
|
+
return { error: code === "ENOENT" ? NO_LEIDO.noExiste : code === "ELOOP" ? NO_LEIDO.esUnEnlace : NO_LEIDO.noSePudoAbrir };
|
|
266
292
|
}
|
|
267
293
|
try {
|
|
268
294
|
if (!fstatSync(fd).isFile())
|
|
269
|
-
return { error:
|
|
295
|
+
return { error: NO_LEIDO.noEsComun };
|
|
270
296
|
const buf = Buffer.alloc(tope + 1);
|
|
271
297
|
let n = 0;
|
|
272
298
|
for (;;) {
|
|
@@ -275,12 +301,12 @@ export function leerArchivoChico(p, tope) {
|
|
|
275
301
|
break;
|
|
276
302
|
n += leidos;
|
|
277
303
|
if (n > tope)
|
|
278
|
-
return { error:
|
|
304
|
+
return { error: NO_LEIDO.demasiadoGrande };
|
|
279
305
|
}
|
|
280
306
|
return { bytes: new Uint8Array(buf.subarray(0, n)) };
|
|
281
307
|
}
|
|
282
308
|
catch {
|
|
283
|
-
return { error:
|
|
309
|
+
return { error: NO_LEIDO.noSePudoLeer };
|
|
284
310
|
}
|
|
285
311
|
finally {
|
|
286
312
|
closeSync(fd);
|
|
@@ -299,7 +325,7 @@ export function archivosDelRepo(avisar) {
|
|
|
299
325
|
}
|
|
300
326
|
};
|
|
301
327
|
const noSeLee = (p, porque) => {
|
|
302
|
-
avisar(`
|
|
328
|
+
avisar(`I can't read ${p}: ${porque}.`);
|
|
303
329
|
return ARCHIVO_QUE_NO_SE_LEE;
|
|
304
330
|
};
|
|
305
331
|
return {
|
|
@@ -308,7 +334,7 @@ export function archivosDelRepo(avisar) {
|
|
|
308
334
|
if (carpeta === "ausente")
|
|
309
335
|
return null;
|
|
310
336
|
if (carpeta === "ajena")
|
|
311
|
-
return noSeLee(p, `${basename(dirname(p))}
|
|
337
|
+
return noSeLee(p, `${basename(dirname(p))} isn't a regular folder (a symlink?)`);
|
|
312
338
|
let fd;
|
|
313
339
|
try {
|
|
314
340
|
// O_NOFOLLOW: un enlace en el último tramo falla (ELOOP) en vez de abrir su destino.
|
|
@@ -319,14 +345,14 @@ export function archivosDelRepo(avisar) {
|
|
|
319
345
|
const code = e.code;
|
|
320
346
|
if (code === "ENOENT")
|
|
321
347
|
return null;
|
|
322
|
-
return noSeLee(p, code === "ELOOP" ?
|
|
348
|
+
return noSeLee(p, code === "ELOOP" ? NO_LEIDO.esUnEnlace : NO_LEIDO.noSePudoAbrir);
|
|
323
349
|
}
|
|
324
350
|
try {
|
|
325
351
|
const st = fstatSync(fd);
|
|
326
352
|
if (!st.isFile())
|
|
327
|
-
return noSeLee(p,
|
|
353
|
+
return noSeLee(p, NO_LEIDO.noEsComun);
|
|
328
354
|
if (st.size > TOPE_DEL_ARCHIVO_DEL_REPO)
|
|
329
|
-
return noSeLee(p,
|
|
355
|
+
return noSeLee(p, NO_LEIDO.demasiadoGrande);
|
|
330
356
|
// Se lee hasta el tope + 1 y no `st.size`: el archivo puede crecer entre el fstat y la lectura.
|
|
331
357
|
const buf = Buffer.alloc(TOPE_DEL_ARCHIVO_DEL_REPO + 1);
|
|
332
358
|
let n = 0;
|
|
@@ -336,12 +362,12 @@ export function archivosDelRepo(avisar) {
|
|
|
336
362
|
break;
|
|
337
363
|
n += leidos;
|
|
338
364
|
if (n > TOPE_DEL_ARCHIVO_DEL_REPO)
|
|
339
|
-
return noSeLee(p,
|
|
365
|
+
return noSeLee(p, NO_LEIDO.demasiadoGrande);
|
|
340
366
|
}
|
|
341
367
|
return buf.subarray(0, n).toString("utf8");
|
|
342
368
|
}
|
|
343
369
|
catch {
|
|
344
|
-
return noSeLee(p,
|
|
370
|
+
return noSeLee(p, NO_LEIDO.noSePudoLeer);
|
|
345
371
|
}
|
|
346
372
|
finally {
|
|
347
373
|
closeSync(fd);
|
|
@@ -355,7 +381,7 @@ export function archivosDelRepo(avisar) {
|
|
|
355
381
|
const dir = dirname(p);
|
|
356
382
|
mkdirSync(dir, { recursive: true });
|
|
357
383
|
if (carpetaPropia(dir) !== "propia")
|
|
358
|
-
throw new Error(`${dir}
|
|
384
|
+
throw new Error(`${dir} isn't a regular folder`);
|
|
359
385
|
const tmp = join(dir, `.${basename(p)}.${randomUUID()}.tmp`);
|
|
360
386
|
try {
|
|
361
387
|
const fd = openSync(tmp, "wx", 0o644);
|
|
@@ -395,27 +421,31 @@ export function entornoReal(env = process.env) {
|
|
|
395
421
|
// Al lado de las credenciales y con el mismo almacén (carpeta 0700, archivo 0600, `.tmp` en
|
|
396
422
|
// exclusiva): nunca dentro del repo donde se corre el comando, que la publicaría con un `git add -A`.
|
|
397
423
|
const rutaDeIdempotencia = join(dirname(rutaDelArchivo), "idempotencia.json");
|
|
424
|
+
// Estos dos registros (y sólo estos) pueden vivir bajo una carpeta personal que es un repo de git: el almacén
|
|
425
|
+
// protegido comprueba con git, antes de cada escritura y cada borrado, que los deja afuera del repo.
|
|
426
|
+
const protegido = (ruta) => almacenProtegido(almacenDeArchivo(ruta, fsReal), { ruta, fs: fsReal, git: gitReal });
|
|
398
427
|
const idempotencia = crearRegistroDeIdempotencia({
|
|
399
428
|
origen: api.origin,
|
|
400
|
-
almacen:
|
|
429
|
+
almacen: protegido(rutaDeIdempotencia),
|
|
401
430
|
rutaDelArchivo: rutaDeIdempotencia,
|
|
402
|
-
fs: fsReal,
|
|
403
431
|
generar: randomUUID,
|
|
404
432
|
ahora: Date.now,
|
|
405
433
|
avisar,
|
|
406
434
|
});
|
|
407
|
-
// El pedido de `probar` (intento + última prueba), con el mismo almacén
|
|
435
|
+
// El pedido de `probar` (intento + última prueba), con el mismo almacén protegido.
|
|
408
436
|
const rutaDePruebas = join(dirname(rutaDelArchivo), "pruebas.json");
|
|
409
437
|
const pruebas = crearRegistroDePruebas({
|
|
410
|
-
origen: api.origin, almacen:
|
|
438
|
+
origen: api.origin, almacen: protegido(rutaDePruebas), rutaDelArchivo: rutaDePruebas, ahora: Date.now, avisar,
|
|
439
|
+
revisarElLugar: () => revisarElLugar({ ruta: rutaDePruebas, fs: fsDelLugarReal, git: gitReal }),
|
|
411
440
|
});
|
|
412
441
|
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
413
442
|
const cwd = process.cwd();
|
|
414
443
|
// Los candados de `claves crear` viven al lado de las credenciales, nunca en un repo.
|
|
415
444
|
const candados = crearCandados(fsDelCandadoReal, join(dirname(rutaDelArchivo), "candados"), process.pid, procesoVivo);
|
|
416
445
|
const secretos = crearGuardaDeSecretos(fsDeSecretosReal);
|
|
446
|
+
// `archivos`: `proyectoDelRepo` lee `.supcoflow/proyecto.json` del clon con él (fricción 6).
|
|
417
447
|
const configurarEnElMotor = {
|
|
418
|
-
api, guardado, git: gitReal, cwd, avisar,
|
|
448
|
+
api, guardado, git: gitReal, cwd, avisar, archivos: archivosDelRepo(avisar),
|
|
419
449
|
buscarProyecto: (a, clave, repo) => buscarProyecto(a, clave, repo, fetch),
|
|
420
450
|
leerConfiguracion: (a, clave, id) => leerConfiguracion(a, clave, id, fetch),
|
|
421
451
|
configurar: (a, clave, ruta, cuerpo, extra) => configurar(a, clave, ruta, cuerpo, fetch, extra),
|
|
@@ -453,6 +483,7 @@ export function entornoReal(env = process.env) {
|
|
|
453
483
|
proyecto: {
|
|
454
484
|
api, guardado, git: gitReal, cwd, idempotencia, archivos: archivosDelRepo(avisar), avisar,
|
|
455
485
|
buscarProyecto: (a, clave, repo) => buscarProyecto(a, clave, repo, fetch),
|
|
486
|
+
quienSoy: (a, clave) => quienSoy(a, clave, fetch),
|
|
456
487
|
ultimaConexion: (a, clave, repo, plazoMs) => ultimaConexion(a, clave, repo, fetch, plazoMs),
|
|
457
488
|
crearProyecto: (a, clave, conexionId, cuerpo, key) => crearProyecto(a, clave, conexionId, cuerpo, key, fetch),
|
|
458
489
|
leerProyecto: (a, clave, id) => leerProyecto(a, clave, id, fetch),
|
|
@@ -466,7 +497,7 @@ export function entornoReal(env = process.env) {
|
|
|
466
497
|
guia: {
|
|
467
498
|
...configurarEnElMotor,
|
|
468
499
|
leerArchivoChico, fetch, gitDeclarado: gitRapido,
|
|
469
|
-
revisar: revisarGuia, publicar: publicarGuia, ver: leerGuia, elegir: elegirOrigen,
|
|
500
|
+
revisar: revisarGuia, publicar: publicarGuia, ver: leerGuia, elegir: elegirOrigen, versiones: leerVersionesDeLaGuia,
|
|
470
501
|
},
|
|
471
502
|
claves: {
|
|
472
503
|
...configurarEnElMotor,
|
|
@@ -475,7 +506,7 @@ export function entornoReal(env = process.env) {
|
|
|
475
506
|
idempotencia, candados, secretos,
|
|
476
507
|
},
|
|
477
508
|
widget: {
|
|
478
|
-
api, guardado, git: gitReal, cwd,
|
|
509
|
+
api, guardado, git: gitReal, cwd, archivos: archivosDelRepo(avisar),
|
|
479
510
|
buscarProyecto: configurarEnElMotor.buscarProyecto,
|
|
480
511
|
leerWidget: (a, clave, id) => leerWidget(a, clave, id, fetch),
|
|
481
512
|
},
|
|
@@ -483,12 +514,12 @@ export function entornoReal(env = process.env) {
|
|
|
483
514
|
// para lo que la terminal dice de sí misma.
|
|
484
515
|
instalacion: {
|
|
485
516
|
...configurarEnElMotor,
|
|
486
|
-
leerArchivoChico, rutaReal: rutaRealOEnNada, esCarpeta: esCarpetaReal,
|
|
517
|
+
leerArchivoChico, rutaReal: rutaRealOEnNada, esCarpeta: esCarpetaReal, esEnlace: esEnlaceReal,
|
|
487
518
|
escribir: archivosDelRepo(avisar).escribir,
|
|
488
519
|
terminal: DESCRIPCION.version,
|
|
489
520
|
},
|
|
490
521
|
prueba: {
|
|
491
|
-
api, guardado, git: gitReal, cwd, avisar, pruebas, generar: randomUUID, ahora: Date.now, dormir, tramoMs,
|
|
522
|
+
api, guardado, git: gitReal, cwd, avisar, archivos: archivosDelRepo(avisar), pruebas, generar: randomUUID, ahora: Date.now, dormir, tramoMs,
|
|
492
523
|
buscarProyecto: configurarEnElMotor.buscarProyecto,
|
|
493
524
|
leerDiagnostico: (a, clave, id) => leerDiagnostico(a, clave, id, fetch),
|
|
494
525
|
pedirPrueba: (a, clave, id, intento) => pedirPrueba(a, clave, id, intento, fetch),
|