supcoflow 0.6.0 → 0.8.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 +210 -162
- package/dist/alias.js +60 -0
- package/dist/api.js +121 -16
- package/dist/argumentos.js +92 -78
- package/dist/cli.js +42 -31
- 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/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +170 -138
- package/dist/comandos/guia.js +148 -61
- package/dist/comandos/instalacion.js +71 -40
- package/dist/comandos/login.js +25 -24
- package/dist/comandos/logout.js +17 -16
- package/dist/comandos/probar.js +359 -0
- package/dist/comandos/proyecto-del-repo.js +1 -1
- package/dist/comandos/proyecto.js +16 -16
- 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 +48 -0
- package/dist/entorno.js +32 -18
- package/dist/flags-por-comando.js +25 -19
- package/dist/guardado/archivo.js +2 -2
- package/dist/guardado/index.js +12 -8
- package/dist/guardado/llavero.js +5 -5
- package/dist/guia.js +3 -1
- package/dist/idempotencia.js +6 -6
- package/dist/instalacion.js +50 -11
- package/dist/lectura.js +10 -0
- package/dist/pruebas-guardadas.js +101 -0
- package/dist/repo-local.js +6 -6
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ export async function proyectoDelRepo(d, flags) {
|
|
|
15
15
|
try {
|
|
16
16
|
const proyecto = await d.buscarProyecto(d.api, leida.vigente.clave, repo);
|
|
17
17
|
if (proyecto === null) {
|
|
18
|
-
const message = `
|
|
18
|
+
const message = `The repo ${repo} doesn't have a project yet. Create the project: \`supcoflow project create\`.`;
|
|
19
19
|
return { ok: false, resultado: { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "sin_proyecto", message, repo }, texto: message } };
|
|
20
20
|
}
|
|
21
21
|
return { ok: true, clave: leida.vigente.clave, proyecto };
|
|
@@ -52,7 +52,7 @@ function mismoArchivo(crudo, p) {
|
|
|
52
52
|
}
|
|
53
53
|
function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
54
54
|
const nuevo = { id: p.id, slug: p.slug, repo: p.repo };
|
|
55
|
-
const queda = reusado ? `
|
|
55
|
+
const queda = reusado ? `Project ${p.id} already existed and I reused it` : `I created project ${p.id}`;
|
|
56
56
|
let archivo = null;
|
|
57
57
|
if (raiz !== null) {
|
|
58
58
|
archivo = path.join(raiz, ".supcoflow", "proyecto.json");
|
|
@@ -63,8 +63,8 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
63
63
|
// decida. Su contenido NUNCA se copia a la salida (puede traer cualquier cosa, una clave
|
|
64
64
|
// incluida, y `emitir` no sanea): sólo el id, si tiene forma de id.
|
|
65
65
|
const idActual = idDelArchivo(actual);
|
|
66
|
-
const message = `
|
|
67
|
-
+ `${queda}
|
|
66
|
+
const message = `There is already a \`.supcoflow/proyecto.json\` for another project (${idActual ?? "one I don't recognize"}); I didn't touch it. `
|
|
67
|
+
+ `${queda} anyway: if that's the one you want, delete the file and run \`supcoflow project create\` again.`;
|
|
68
68
|
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_distinto", message, { archivo, id_actual: idActual, nuevo, reusado });
|
|
69
69
|
}
|
|
70
70
|
if (actual === null) {
|
|
@@ -72,7 +72,7 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
72
72
|
d.archivos.escribir(archivo, contenidoDelArchivo(p));
|
|
73
73
|
}
|
|
74
74
|
catch {
|
|
75
|
-
const message = `${queda}
|
|
75
|
+
const message = `${queda}, but I couldn't write ${archivo}. Run \`supcoflow project create\` again.`;
|
|
76
76
|
return fallo(SALIDA.inesperado, "archivo_no_escrito", message, { archivo, nuevo, reusado });
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -80,7 +80,7 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
80
80
|
return {
|
|
81
81
|
codigo: SALIDA.ok,
|
|
82
82
|
json: { ok: true, proyecto: { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama }, reusado, archivo },
|
|
83
|
-
texto: `${queda}: ${p.repo},
|
|
83
|
+
texto: `${queda}: ${p.repo}, branch ${p.rama}.${archivo !== null ? ` Recorded in ${archivo}.` : ""}`,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
export async function proyectoCrear(d, flags) {
|
|
@@ -116,18 +116,18 @@ export async function proyectoCrear(d, flags) {
|
|
|
116
116
|
if (flags.install !== null || flags.test !== null) {
|
|
117
117
|
// Sin copiar los valores: son texto del usuario y el aviso sólo dice qué pasó con ellos. Callarlo
|
|
118
118
|
// haría creer que el proyecto quedó con los comandos pedidos.
|
|
119
|
-
d.avisar("
|
|
119
|
+
d.avisar("The project already exists: I ignored --install/--test. To change its commands, it has to be configured.");
|
|
120
120
|
}
|
|
121
121
|
return resultadoDelProyecto(existente, true, raiz, d);
|
|
122
122
|
}
|
|
123
123
|
if (conexion === null || conexion.estado !== "conectado") {
|
|
124
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", "
|
|
124
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", "Connect the repo first: `supcoflow repo connect`.", {
|
|
125
125
|
repo, ...(conexion !== null ? { estado: conexion.estado } : {}),
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
if (flags.dryRun) {
|
|
129
129
|
const haria = `POST /api/conexiones/${conexion.id}/proyecto`;
|
|
130
|
-
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, repo, haria }, texto: `
|
|
130
|
+
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
131
|
}
|
|
132
132
|
const cuerpo = {};
|
|
133
133
|
if (flags.install !== null)
|
|
@@ -153,27 +153,27 @@ export async function proyectoCrear(d, flags) {
|
|
|
153
153
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
154
154
|
const install = flags.install ?? "…";
|
|
155
155
|
const test = flags.test ?? "…";
|
|
156
|
-
const message = `${alta.message || "
|
|
156
|
+
const message = `${alta.message || "The project is missing commands."} Pass them yourself: \`supcoflow project create --install ${comillas(install)} --test ${comillas(test)}\`.`;
|
|
157
157
|
return fallo(SALIDA.entradaInvalida, "comandos_faltantes", message, { faltantes: alta.faltantes });
|
|
158
158
|
}
|
|
159
159
|
case "no_conectada":
|
|
160
160
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
161
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", alta.message || "
|
|
161
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", alta.message || "Connect the repo first: `supcoflow repo connect`.", { repo });
|
|
162
162
|
case "en_curso":
|
|
163
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", "
|
|
164
|
+
return fallo(SALIDA.inesperado, "en_curso", "Another request with this key is already in progress; try again in a minute.");
|
|
165
165
|
case "clave_reusada":
|
|
166
166
|
// No debería pasar (la clave es por repo y un reintento manda lo mismo); olvidarla deja reintentar limpio.
|
|
167
167
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
168
|
-
return fallo(SALIDA.inesperado, "clave_reusada", "
|
|
168
|
+
return fallo(SALIDA.inesperado, "clave_reusada", "The engine rejected the saved idempotency key; I discarded it, try again.");
|
|
169
169
|
case "rechazo": {
|
|
170
170
|
// La clave se conserva: tras un rechazo el motor ya la soltó, y el próximo intento la reusa sin
|
|
171
171
|
// chocar. Lo que cambia es el código: no todo rechazo es culpa de lo que se escribió.
|
|
172
|
-
const message = alta.message || "
|
|
172
|
+
const message = alta.message || "The engine rejected the request.";
|
|
173
173
|
if (RECHAZOS_DE_UNA_PERSONA.has(alta.error)) {
|
|
174
174
|
// Lo arregla alguien con permisos en GitHub (instalar la App, sumarle el repo): reintentar o
|
|
175
175
|
// cambiar flags no sirve, y un 3 haría que un asistente siguiera probando entradas.
|
|
176
|
-
return fallo(SALIDA.haceFaltaUnaPersona, alta.error, `${message}
|
|
176
|
+
return fallo(SALIDA.haceFaltaUnaPersona, alta.error, `${message} Check the App's installation on GitHub and connect again with \`supcoflow repo connect\`.`);
|
|
177
177
|
}
|
|
178
178
|
// Dos altas al mismo tiempo, o un proyecto borrado en el medio: el mismo pedido otra vez anda.
|
|
179
179
|
if (RECHAZOS_TRANSITORIOS.has(alta.error))
|
|
@@ -197,14 +197,14 @@ export async function proyectoVer(d, flags) {
|
|
|
197
197
|
try {
|
|
198
198
|
const encontrado = await d.buscarProyecto(d.api, clave, repo);
|
|
199
199
|
if (encontrado === null) {
|
|
200
|
-
return { codigo: SALIDA.ok, json: { ok: true, repo, proyecto: null }, texto: "
|
|
200
|
+
return { codigo: SALIDA.ok, json: { ok: true, repo, proyecto: null }, texto: "This repo doesn't have a project yet: `supcoflow project create`." };
|
|
201
201
|
}
|
|
202
202
|
const { proyecto: p, github } = await d.leerProyecto(d.api, clave, encontrado.id);
|
|
203
203
|
const proyecto = { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama, install_command: p.installCommand, test_command: p.testCommand };
|
|
204
204
|
return {
|
|
205
205
|
codigo: SALIDA.ok,
|
|
206
206
|
json: { ok: true, proyecto, github: { estado: github.estado, verificado_en: github.verificadoEn } },
|
|
207
|
-
texto: `${p.id} (${p.repo},
|
|
207
|
+
texto: `${p.id} (${p.repo}, branch ${p.rama}): installs with \`${p.installCommand}\`, tests with \`${p.testCommand}\`. GitHub: ${github.estado ?? "not verified"}.`,
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
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,48 @@
|
|
|
1
|
+
export const TOTAL_DE_INSTALACION = 6;
|
|
2
|
+
// El orden y los nombres con que se dicen: los ids de los puntos del motor son datos suyos y no cambian.
|
|
3
|
+
const DE_LA_INSTALACION = [
|
|
4
|
+
{ punto: "sign-in", como: "sign-in" },
|
|
5
|
+
{ punto: "repo", como: "repo" },
|
|
6
|
+
{ punto: "instalacion", como: "GitHub App" },
|
|
7
|
+
{ punto: "sitios", como: "allowed sites" },
|
|
8
|
+
{ punto: "chat", como: "chat" },
|
|
9
|
+
{ punto: "guia", como: "guide" },
|
|
10
|
+
];
|
|
11
|
+
const DEL_MOTOR = new Set(["instalacion", "sitios", "chat", "guia"]);
|
|
12
|
+
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.`;
|
|
13
|
+
export const nombreDelPunto = (punto) => DE_LA_INSTALACION.find((x) => x.punto === punto)?.como ?? punto;
|
|
14
|
+
export function puntoDeSesion() {
|
|
15
|
+
return { punto: "sign-in", ok: true, mensaje: "This request was authenticated: you're signed in.", grupo: "instalacion" };
|
|
16
|
+
}
|
|
17
|
+
export function puntoDelRepo(local, repoDelProyecto) {
|
|
18
|
+
const mal = (mensaje) => ({ punto: "repo", ok: false, mensaje, grupo: "instalacion" });
|
|
19
|
+
if (local.repo === null) {
|
|
20
|
+
if (local.motivo === "sin_git")
|
|
21
|
+
return mal("This folder isn't a git repo, so I can't tell which repo it is.");
|
|
22
|
+
if (local.motivo === "sin_origin")
|
|
23
|
+
return mal("This repo has no «origin» remote, so I can't tell which repo it is.");
|
|
24
|
+
return mal("The «origin» of this repo isn't a GitHub repo.");
|
|
25
|
+
}
|
|
26
|
+
if (local.repo.toLowerCase() !== repoDelProyecto.toLowerCase()) {
|
|
27
|
+
return mal(`The origin of this folder is ${local.repo}, not the project's repo ${repoDelProyecto}.`);
|
|
28
|
+
}
|
|
29
|
+
return { punto: "repo", ok: true, mensaje: `The origin of this folder is ${local.repo}, the project's repo.`, grupo: "instalacion" };
|
|
30
|
+
}
|
|
31
|
+
/** Reparte los puntos del motor en los dos grupos y suma los dos locales al de la instalación. */
|
|
32
|
+
export function agruparPuntos(g, locales) {
|
|
33
|
+
const delMotor = g.puntos.map((p) => ({ ...p, grupo: p.grupo ?? (DEL_MOTOR.has(p.punto) ? "instalacion" : "prueba") }));
|
|
34
|
+
const instalacion = [...locales, ...delMotor.filter((p) => p.grupo === "instalacion")];
|
|
35
|
+
// Un punto del grupo que el motor no mandó no se da por bueno: se dice que falta y cuenta como no ok.
|
|
36
|
+
for (const { punto } of DE_LA_INSTALACION) {
|
|
37
|
+
if (!instalacion.some((p) => p.punto === punto)) {
|
|
38
|
+
instalacion.push({ punto, ok: false, mensaje: "The engine didn't report this point.", grupo: "instalacion" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const orden = (p) => {
|
|
42
|
+
const i = DE_LA_INSTALACION.findIndex((x) => x.punto === p.punto);
|
|
43
|
+
return i === -1 ? DE_LA_INSTALACION.length : i;
|
|
44
|
+
};
|
|
45
|
+
return { instalacion: instalacion.sort((a, b) => orden(a) - orden(b)), prueba: delMotor.filter((p) => p.grupo === "prueba") };
|
|
46
|
+
}
|
|
47
|
+
/** Cuántos de los seis puntos de la instalación están bien (los extras que un motor nuevo sume no cambian el seis). */
|
|
48
|
+
export const contarBuenos = (instalacion) => DE_LA_INSTALACION.filter(({ punto }) => instalacion.some((p) => p.punto === punto && p.ok)).length;
|
package/dist/entorno.js
CHANGED
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
import { execFile, spawn } from "node:child_process";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { chmodSync, closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, mkdirSync, 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, leerWidget, 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";
|
|
14
15
|
import { almacenDeLlavero } from "./guardado/llavero.js";
|
|
15
16
|
import { crearGuardaDeSecretos } from "./secreto-a-archivo.js";
|
|
16
17
|
import { crearRegistroDeIdempotencia } from "./idempotencia.js";
|
|
18
|
+
import { crearRegistroDePruebas } from "./pruebas-guardadas.js";
|
|
17
19
|
import { TRAMO_MAXIMO_MS } from "./sondeo.js";
|
|
18
20
|
// El `.tmp` se crea en exclusiva (`wx`: falla si quedó algo, enlace incluido, en vez de escribir a
|
|
19
21
|
// través de él) y el `fchmod` va aparte porque el modo de `open` pasa por la umask del proceso.
|
|
@@ -50,7 +52,7 @@ export const fsReal = {
|
|
|
50
52
|
return realpathSync(p);
|
|
51
53
|
}
|
|
52
54
|
catch {
|
|
53
|
-
throw new Error(`
|
|
55
|
+
throw new Error(`I can't resolve the real path of ${p} (a broken symlink?); I'm not saving anything there.`);
|
|
54
56
|
}
|
|
55
57
|
},
|
|
56
58
|
// `lstat` y no `existsSync`: éste sigue el enlace y ante uno roto dice «no existe», y el control
|
|
@@ -251,7 +253,7 @@ export const TOPE_DEL_ARCHIVO_DEL_REPO = 64 * 1024;
|
|
|
251
253
|
// Lo que devuelve `leer` cuando hay ALGO ahí que no se lee (un enlace, una carpeta, algo enorme, sin
|
|
252
254
|
// permiso). No es JSON a propósito: `proyecto crear` lo trata como un archivo distinto (sale 4, «no lo
|
|
253
255
|
// toqué») sin que nada de lo que hay ahí llegue a la salida.
|
|
254
|
-
export const ARCHIVO_QUE_NO_SE_LEE = "\u0000(
|
|
256
|
+
export const ARCHIVO_QUE_NO_SE_LEE = "\u0000(unreadable)";
|
|
255
257
|
// Un archivo que la persona nombra (el logo) con las mismas defensas que los del repo: sin seguir
|
|
256
258
|
// enlaces, sin FIFOs que cuelguen, sólo archivos comunes y con tope (se lee tope + 1 por si crece).
|
|
257
259
|
export function leerArchivoChico(p, tope) {
|
|
@@ -261,11 +263,11 @@ export function leerArchivoChico(p, tope) {
|
|
|
261
263
|
}
|
|
262
264
|
catch (e) {
|
|
263
265
|
const code = e.code;
|
|
264
|
-
return { error: code === "ENOENT" ?
|
|
266
|
+
return { error: code === "ENOENT" ? NO_LEIDO.noExiste : code === "ELOOP" ? NO_LEIDO.esUnEnlace : NO_LEIDO.noSePudoAbrir };
|
|
265
267
|
}
|
|
266
268
|
try {
|
|
267
269
|
if (!fstatSync(fd).isFile())
|
|
268
|
-
return { error:
|
|
270
|
+
return { error: NO_LEIDO.noEsComun };
|
|
269
271
|
const buf = Buffer.alloc(tope + 1);
|
|
270
272
|
let n = 0;
|
|
271
273
|
for (;;) {
|
|
@@ -274,12 +276,12 @@ export function leerArchivoChico(p, tope) {
|
|
|
274
276
|
break;
|
|
275
277
|
n += leidos;
|
|
276
278
|
if (n > tope)
|
|
277
|
-
return { error:
|
|
279
|
+
return { error: NO_LEIDO.demasiadoGrande };
|
|
278
280
|
}
|
|
279
281
|
return { bytes: new Uint8Array(buf.subarray(0, n)) };
|
|
280
282
|
}
|
|
281
283
|
catch {
|
|
282
|
-
return { error:
|
|
284
|
+
return { error: NO_LEIDO.noSePudoLeer };
|
|
283
285
|
}
|
|
284
286
|
finally {
|
|
285
287
|
closeSync(fd);
|
|
@@ -298,7 +300,7 @@ export function archivosDelRepo(avisar) {
|
|
|
298
300
|
}
|
|
299
301
|
};
|
|
300
302
|
const noSeLee = (p, porque) => {
|
|
301
|
-
avisar(`
|
|
303
|
+
avisar(`I can't read ${p}: ${porque}.`);
|
|
302
304
|
return ARCHIVO_QUE_NO_SE_LEE;
|
|
303
305
|
};
|
|
304
306
|
return {
|
|
@@ -307,7 +309,7 @@ export function archivosDelRepo(avisar) {
|
|
|
307
309
|
if (carpeta === "ausente")
|
|
308
310
|
return null;
|
|
309
311
|
if (carpeta === "ajena")
|
|
310
|
-
return noSeLee(p, `${basename(dirname(p))}
|
|
312
|
+
return noSeLee(p, `${basename(dirname(p))} isn't a regular folder (a symlink?)`);
|
|
311
313
|
let fd;
|
|
312
314
|
try {
|
|
313
315
|
// O_NOFOLLOW: un enlace en el último tramo falla (ELOOP) en vez de abrir su destino.
|
|
@@ -318,14 +320,14 @@ export function archivosDelRepo(avisar) {
|
|
|
318
320
|
const code = e.code;
|
|
319
321
|
if (code === "ENOENT")
|
|
320
322
|
return null;
|
|
321
|
-
return noSeLee(p, code === "ELOOP" ?
|
|
323
|
+
return noSeLee(p, code === "ELOOP" ? NO_LEIDO.esUnEnlace : NO_LEIDO.noSePudoAbrir);
|
|
322
324
|
}
|
|
323
325
|
try {
|
|
324
326
|
const st = fstatSync(fd);
|
|
325
327
|
if (!st.isFile())
|
|
326
|
-
return noSeLee(p,
|
|
328
|
+
return noSeLee(p, NO_LEIDO.noEsComun);
|
|
327
329
|
if (st.size > TOPE_DEL_ARCHIVO_DEL_REPO)
|
|
328
|
-
return noSeLee(p,
|
|
330
|
+
return noSeLee(p, NO_LEIDO.demasiadoGrande);
|
|
329
331
|
// Se lee hasta el tope + 1 y no `st.size`: el archivo puede crecer entre el fstat y la lectura.
|
|
330
332
|
const buf = Buffer.alloc(TOPE_DEL_ARCHIVO_DEL_REPO + 1);
|
|
331
333
|
let n = 0;
|
|
@@ -335,12 +337,12 @@ export function archivosDelRepo(avisar) {
|
|
|
335
337
|
break;
|
|
336
338
|
n += leidos;
|
|
337
339
|
if (n > TOPE_DEL_ARCHIVO_DEL_REPO)
|
|
338
|
-
return noSeLee(p,
|
|
340
|
+
return noSeLee(p, NO_LEIDO.demasiadoGrande);
|
|
339
341
|
}
|
|
340
342
|
return buf.subarray(0, n).toString("utf8");
|
|
341
343
|
}
|
|
342
344
|
catch {
|
|
343
|
-
return noSeLee(p,
|
|
345
|
+
return noSeLee(p, NO_LEIDO.noSePudoLeer);
|
|
344
346
|
}
|
|
345
347
|
finally {
|
|
346
348
|
closeSync(fd);
|
|
@@ -354,7 +356,7 @@ export function archivosDelRepo(avisar) {
|
|
|
354
356
|
const dir = dirname(p);
|
|
355
357
|
mkdirSync(dir, { recursive: true });
|
|
356
358
|
if (carpetaPropia(dir) !== "propia")
|
|
357
|
-
throw new Error(`${dir}
|
|
359
|
+
throw new Error(`${dir} isn't a regular folder`);
|
|
358
360
|
const tmp = join(dir, `.${basename(p)}.${randomUUID()}.tmp`);
|
|
359
361
|
try {
|
|
360
362
|
const fd = openSync(tmp, "wx", 0o644);
|
|
@@ -403,6 +405,11 @@ export function entornoReal(env = process.env) {
|
|
|
403
405
|
ahora: Date.now,
|
|
404
406
|
avisar,
|
|
405
407
|
});
|
|
408
|
+
// El pedido de `probar` (intento + última prueba), con el mismo almacén y la misma regla: fuera de un repo.
|
|
409
|
+
const rutaDePruebas = join(dirname(rutaDelArchivo), "pruebas.json");
|
|
410
|
+
const pruebas = crearRegistroDePruebas({
|
|
411
|
+
origen: api.origin, almacen: almacenDeArchivo(rutaDePruebas, fsReal), rutaDelArchivo: rutaDePruebas, fs: fsReal, ahora: Date.now, avisar,
|
|
412
|
+
});
|
|
406
413
|
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
407
414
|
const cwd = process.cwd();
|
|
408
415
|
// Los candados de `claves crear` viven al lado de las credenciales, nunca en un repo.
|
|
@@ -460,7 +467,7 @@ export function entornoReal(env = process.env) {
|
|
|
460
467
|
guia: {
|
|
461
468
|
...configurarEnElMotor,
|
|
462
469
|
leerArchivoChico, fetch, gitDeclarado: gitRapido,
|
|
463
|
-
revisar: revisarGuia, publicar: publicarGuia, ver: leerGuia, elegir: elegirOrigen,
|
|
470
|
+
revisar: revisarGuia, publicar: publicarGuia, ver: leerGuia, elegir: elegirOrigen, versiones: leerVersionesDeLaGuia,
|
|
464
471
|
},
|
|
465
472
|
claves: {
|
|
466
473
|
...configurarEnElMotor,
|
|
@@ -477,9 +484,16 @@ export function entornoReal(env = process.env) {
|
|
|
477
484
|
// para lo que la terminal dice de sí misma.
|
|
478
485
|
instalacion: {
|
|
479
486
|
...configurarEnElMotor,
|
|
480
|
-
leerArchivoChico, rutaReal: rutaRealOEnNada, esCarpeta: esCarpetaReal,
|
|
487
|
+
leerArchivoChico, rutaReal: rutaRealOEnNada, esCarpeta: esCarpetaReal, esEnlace: esEnlaceReal,
|
|
481
488
|
escribir: archivosDelRepo(avisar).escribir,
|
|
482
489
|
terminal: DESCRIPCION.version,
|
|
483
490
|
},
|
|
491
|
+
prueba: {
|
|
492
|
+
api, guardado, git: gitReal, cwd, avisar, pruebas, generar: randomUUID, ahora: Date.now, dormir, tramoMs,
|
|
493
|
+
buscarProyecto: configurarEnElMotor.buscarProyecto,
|
|
494
|
+
leerDiagnostico: (a, clave, id) => leerDiagnostico(a, clave, id, fetch),
|
|
495
|
+
pedirPrueba: (a, clave, id, intento) => pedirPrueba(a, clave, id, intento, fetch),
|
|
496
|
+
leerPrueba: (a, clave, id, pruebaId, plazoMs) => leerPrueba(a, clave, id, pruebaId, fetch, plazoMs),
|
|
497
|
+
},
|
|
484
498
|
};
|
|
485
499
|
}
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
// Qué comandos aceptan cada flag que no es global. Un flag que no aplica sale 3 en vez de ignorarse:
|
|
2
|
-
// un asistente que pasa `--repo` a `
|
|
3
|
-
// `cli.ts` (que corre al importarse) para poder probarse sin lanzar el binario.
|
|
2
|
+
// un asistente que pasa `--repo` a `whoami` creería que preguntó por ese repo. Vive aparte de
|
|
3
|
+
// `cli.ts` (que corre al importarse) para poder probarse sin lanzar el binario. Los nombres de los
|
|
4
|
+
// campos son los de `Flags`; los textos nombran los flags y comandos canónicos.
|
|
4
5
|
const DE_CONFIGURAR_CON_REPO = [
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"config", "chat on", "chat off", "chat brand", "chat logo", "sites add", "sites remove",
|
|
7
|
+
"data set", "policy set", "keys create", "keys list", "keys revoke",
|
|
8
|
+
"guide check", "guide publish", "guide show", "guide use", "guide versions",
|
|
8
9
|
];
|
|
9
10
|
export const FLAGS_POR_COMANDO = [
|
|
10
|
-
{ flags: ["start"
|
|
11
|
+
{ flags: ["start"], comandos: ["login", "repo connect"], texto: "--start belongs to `supcoflow login` and `supcoflow repo connect`." },
|
|
12
|
+
{ flags: ["wait"], comandos: ["login", "repo connect", "test"], texto: "--wait belongs to `supcoflow login`, `supcoflow repo connect` and `supcoflow test`." },
|
|
11
13
|
{
|
|
12
|
-
flags: ["repo"], comandos: ["repo
|
|
13
|
-
texto: "--repo
|
|
14
|
+
flags: ["repo"], comandos: ["repo connect", "project create", "project show", ...DE_CONFIGURAR_CON_REPO, "widget", "install record", "diagnose", "test"],
|
|
15
|
+
texto: "--repo belongs to `supcoflow repo connect`, `project create|show`, `widget`, `install record`, `diagnose`, `test` and the commands that configure a project (not to `catalog`).",
|
|
14
16
|
},
|
|
15
|
-
{ flags: ["install", "test"], comandos: ["
|
|
16
|
-
{ flags: ["
|
|
17
|
-
{ flags: ["
|
|
18
|
-
{ flags: ["
|
|
19
|
-
{ flags: ["
|
|
20
|
-
{ flags: ["
|
|
21
|
-
{ flags: ["
|
|
22
|
-
{ flags: ["
|
|
23
|
-
{ flags: ["
|
|
24
|
-
{ flags: ["id"], comandos: ["
|
|
25
|
-
{
|
|
17
|
+
{ flags: ["install", "test"], comandos: ["project create"], texto: "--install and --test belong to `supcoflow project create`." },
|
|
18
|
+
{ flags: ["site"], comandos: ["sites add", "sites remove"], texto: "--site belongs to `supcoflow sites add|remove`." },
|
|
19
|
+
{ flags: ["title", "greeting", "theme"], comandos: ["chat brand"], texto: "--title, --greeting and --theme belong to `supcoflow chat brand`." },
|
|
20
|
+
{ flags: ["file"], comandos: ["chat logo", "install record"], texto: "--file belongs to `supcoflow chat logo` and `supcoflow install record`." },
|
|
21
|
+
{ flags: ["allow"], comandos: ["data set"], texto: "--allow belongs to `supcoflow data set`." },
|
|
22
|
+
{ flags: ["mode"], comandos: ["policy set"], texto: "--mode belongs to `supcoflow policy set`." },
|
|
23
|
+
{ flags: ["name", "saveTo"], comandos: ["keys create"], texto: "--name and --save-to belong to `supcoflow keys create`." },
|
|
24
|
+
{ flags: ["source"], comandos: ["guide use"], texto: "--source belongs to `supcoflow guide use`." },
|
|
25
|
+
{ flags: ["fromGithub"], comandos: ["guide publish"], texto: "--from-github belongs to `supcoflow guide publish`." },
|
|
26
|
+
{ flags: ["id"], comandos: ["keys revoke"], texto: "--id belongs to `supcoflow keys revoke`." },
|
|
27
|
+
{
|
|
28
|
+
flags: ["authorizeSpend", "ceiling", "expectPolicy", "message", "testId"], comandos: ["test"],
|
|
29
|
+
texto: "--authorize-spend, --ceiling, --expect-policy, --message and --test-id belong to `supcoflow test`.",
|
|
30
|
+
},
|
|
31
|
+
{ flags: ["skill", "app"], comandos: ["install record"], texto: "--skill and --app belong to `supcoflow install record`." },
|
|
26
32
|
];
|
|
27
33
|
/** El mensaje del primer flag presente que el comando no acepta, o null si todo aplica. */
|
|
28
34
|
export function flagQueNoAplica(comando, presentes) {
|
package/dist/guardado/archivo.js
CHANGED
|
@@ -21,7 +21,7 @@ export function almacenDeArchivo(ruta, fs) {
|
|
|
21
21
|
v = undefined;
|
|
22
22
|
}
|
|
23
23
|
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
24
|
-
throw new Error(`
|
|
24
|
+
throw new Error(`I don't understand the credentials file ${ruta}; delete it and log in again.`);
|
|
25
25
|
}
|
|
26
26
|
return v;
|
|
27
27
|
}
|
|
@@ -44,7 +44,7 @@ export function almacenDeArchivo(ruta, fs) {
|
|
|
44
44
|
return null;
|
|
45
45
|
const r = t[cuenta];
|
|
46
46
|
if (!esRegistro(r))
|
|
47
|
-
throw new Error(`
|
|
47
|
+
throw new Error(`I don't understand the credentials file ${ruta}; delete it and log in again.`);
|
|
48
48
|
return { valor: r.valor, escritoEn: r.escritoEn };
|
|
49
49
|
},
|
|
50
50
|
async escribir(cuenta, r) {
|