supcoflow 0.7.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 +209 -180
- 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/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +161 -151
- 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 +141 -62
- 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 +19 -18
- 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/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 +16 -10
- package/dist/repo-local.js +6 -6
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
|
@@ -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,11 +3,12 @@
|
|
|
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, 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";
|
|
@@ -51,7 +52,7 @@ export const fsReal = {
|
|
|
51
52
|
return realpathSync(p);
|
|
52
53
|
}
|
|
53
54
|
catch {
|
|
54
|
-
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.`);
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
// `lstat` y no `existsSync`: éste sigue el enlace y ante uno roto dice «no existe», y el control
|
|
@@ -252,7 +253,7 @@ export const TOPE_DEL_ARCHIVO_DEL_REPO = 64 * 1024;
|
|
|
252
253
|
// Lo que devuelve `leer` cuando hay ALGO ahí que no se lee (un enlace, una carpeta, algo enorme, sin
|
|
253
254
|
// permiso). No es JSON a propósito: `proyecto crear` lo trata como un archivo distinto (sale 4, «no lo
|
|
254
255
|
// toqué») sin que nada de lo que hay ahí llegue a la salida.
|
|
255
|
-
export const ARCHIVO_QUE_NO_SE_LEE = "\u0000(
|
|
256
|
+
export const ARCHIVO_QUE_NO_SE_LEE = "\u0000(unreadable)";
|
|
256
257
|
// Un archivo que la persona nombra (el logo) con las mismas defensas que los del repo: sin seguir
|
|
257
258
|
// enlaces, sin FIFOs que cuelguen, sólo archivos comunes y con tope (se lee tope + 1 por si crece).
|
|
258
259
|
export function leerArchivoChico(p, tope) {
|
|
@@ -262,11 +263,11 @@ export function leerArchivoChico(p, tope) {
|
|
|
262
263
|
}
|
|
263
264
|
catch (e) {
|
|
264
265
|
const code = e.code;
|
|
265
|
-
return { error: code === "ENOENT" ?
|
|
266
|
+
return { error: code === "ENOENT" ? NO_LEIDO.noExiste : code === "ELOOP" ? NO_LEIDO.esUnEnlace : NO_LEIDO.noSePudoAbrir };
|
|
266
267
|
}
|
|
267
268
|
try {
|
|
268
269
|
if (!fstatSync(fd).isFile())
|
|
269
|
-
return { error:
|
|
270
|
+
return { error: NO_LEIDO.noEsComun };
|
|
270
271
|
const buf = Buffer.alloc(tope + 1);
|
|
271
272
|
let n = 0;
|
|
272
273
|
for (;;) {
|
|
@@ -275,12 +276,12 @@ export function leerArchivoChico(p, tope) {
|
|
|
275
276
|
break;
|
|
276
277
|
n += leidos;
|
|
277
278
|
if (n > tope)
|
|
278
|
-
return { error:
|
|
279
|
+
return { error: NO_LEIDO.demasiadoGrande };
|
|
279
280
|
}
|
|
280
281
|
return { bytes: new Uint8Array(buf.subarray(0, n)) };
|
|
281
282
|
}
|
|
282
283
|
catch {
|
|
283
|
-
return { error:
|
|
284
|
+
return { error: NO_LEIDO.noSePudoLeer };
|
|
284
285
|
}
|
|
285
286
|
finally {
|
|
286
287
|
closeSync(fd);
|
|
@@ -299,7 +300,7 @@ export function archivosDelRepo(avisar) {
|
|
|
299
300
|
}
|
|
300
301
|
};
|
|
301
302
|
const noSeLee = (p, porque) => {
|
|
302
|
-
avisar(`
|
|
303
|
+
avisar(`I can't read ${p}: ${porque}.`);
|
|
303
304
|
return ARCHIVO_QUE_NO_SE_LEE;
|
|
304
305
|
};
|
|
305
306
|
return {
|
|
@@ -308,7 +309,7 @@ export function archivosDelRepo(avisar) {
|
|
|
308
309
|
if (carpeta === "ausente")
|
|
309
310
|
return null;
|
|
310
311
|
if (carpeta === "ajena")
|
|
311
|
-
return noSeLee(p, `${basename(dirname(p))}
|
|
312
|
+
return noSeLee(p, `${basename(dirname(p))} isn't a regular folder (a symlink?)`);
|
|
312
313
|
let fd;
|
|
313
314
|
try {
|
|
314
315
|
// O_NOFOLLOW: un enlace en el último tramo falla (ELOOP) en vez de abrir su destino.
|
|
@@ -319,14 +320,14 @@ export function archivosDelRepo(avisar) {
|
|
|
319
320
|
const code = e.code;
|
|
320
321
|
if (code === "ENOENT")
|
|
321
322
|
return null;
|
|
322
|
-
return noSeLee(p, code === "ELOOP" ?
|
|
323
|
+
return noSeLee(p, code === "ELOOP" ? NO_LEIDO.esUnEnlace : NO_LEIDO.noSePudoAbrir);
|
|
323
324
|
}
|
|
324
325
|
try {
|
|
325
326
|
const st = fstatSync(fd);
|
|
326
327
|
if (!st.isFile())
|
|
327
|
-
return noSeLee(p,
|
|
328
|
+
return noSeLee(p, NO_LEIDO.noEsComun);
|
|
328
329
|
if (st.size > TOPE_DEL_ARCHIVO_DEL_REPO)
|
|
329
|
-
return noSeLee(p,
|
|
330
|
+
return noSeLee(p, NO_LEIDO.demasiadoGrande);
|
|
330
331
|
// Se lee hasta el tope + 1 y no `st.size`: el archivo puede crecer entre el fstat y la lectura.
|
|
331
332
|
const buf = Buffer.alloc(TOPE_DEL_ARCHIVO_DEL_REPO + 1);
|
|
332
333
|
let n = 0;
|
|
@@ -336,12 +337,12 @@ export function archivosDelRepo(avisar) {
|
|
|
336
337
|
break;
|
|
337
338
|
n += leidos;
|
|
338
339
|
if (n > TOPE_DEL_ARCHIVO_DEL_REPO)
|
|
339
|
-
return noSeLee(p,
|
|
340
|
+
return noSeLee(p, NO_LEIDO.demasiadoGrande);
|
|
340
341
|
}
|
|
341
342
|
return buf.subarray(0, n).toString("utf8");
|
|
342
343
|
}
|
|
343
344
|
catch {
|
|
344
|
-
return noSeLee(p,
|
|
345
|
+
return noSeLee(p, NO_LEIDO.noSePudoLeer);
|
|
345
346
|
}
|
|
346
347
|
finally {
|
|
347
348
|
closeSync(fd);
|
|
@@ -355,7 +356,7 @@ export function archivosDelRepo(avisar) {
|
|
|
355
356
|
const dir = dirname(p);
|
|
356
357
|
mkdirSync(dir, { recursive: true });
|
|
357
358
|
if (carpetaPropia(dir) !== "propia")
|
|
358
|
-
throw new Error(`${dir}
|
|
359
|
+
throw new Error(`${dir} isn't a regular folder`);
|
|
359
360
|
const tmp = join(dir, `.${basename(p)}.${randomUUID()}.tmp`);
|
|
360
361
|
try {
|
|
361
362
|
const fd = openSync(tmp, "wx", 0o644);
|
|
@@ -466,7 +467,7 @@ export function entornoReal(env = process.env) {
|
|
|
466
467
|
guia: {
|
|
467
468
|
...configurarEnElMotor,
|
|
468
469
|
leerArchivoChico, fetch, gitDeclarado: gitRapido,
|
|
469
|
-
revisar: revisarGuia, publicar: publicarGuia, ver: leerGuia, elegir: elegirOrigen,
|
|
470
|
+
revisar: revisarGuia, publicar: publicarGuia, ver: leerGuia, elegir: elegirOrigen, versiones: leerVersionesDeLaGuia,
|
|
470
471
|
},
|
|
471
472
|
claves: {
|
|
472
473
|
...configurarEnElMotor,
|
|
@@ -483,7 +484,7 @@ export function entornoReal(env = process.env) {
|
|
|
483
484
|
// para lo que la terminal dice de sí misma.
|
|
484
485
|
instalacion: {
|
|
485
486
|
...configurarEnElMotor,
|
|
486
|
-
leerArchivoChico, rutaReal: rutaRealOEnNada, esCarpeta: esCarpetaReal,
|
|
487
|
+
leerArchivoChico, rutaReal: rutaRealOEnNada, esCarpeta: esCarpetaReal, esEnlace: esEnlaceReal,
|
|
487
488
|
escribir: archivosDelRepo(avisar).escribir,
|
|
488
489
|
terminal: DESCRIPCION.version,
|
|
489
490
|
},
|
|
@@ -1,30 +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"], comandos: ["login", "repo
|
|
11
|
-
{ flags: ["wait"], comandos: ["login", "repo
|
|
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`." },
|
|
12
13
|
{
|
|
13
|
-
flags: ["repo"], comandos: ["repo
|
|
14
|
-
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`).",
|
|
15
16
|
},
|
|
16
|
-
{ flags: ["install", "test"], comandos: ["
|
|
17
|
-
{ flags: ["
|
|
18
|
-
{ flags: ["
|
|
19
|
-
{ flags: ["
|
|
20
|
-
{ flags: ["
|
|
21
|
-
{ flags: ["
|
|
22
|
-
{ flags: ["
|
|
23
|
-
{ flags: ["
|
|
24
|
-
{ flags: ["
|
|
25
|
-
{ flags: ["id"], comandos: ["
|
|
26
|
-
{
|
|
27
|
-
|
|
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`." },
|
|
28
32
|
];
|
|
29
33
|
/** El mensaje del primer flag presente que el comando no acepta, o null si todo aplica. */
|
|
30
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) {
|
package/dist/guardado/index.js
CHANGED
|
@@ -4,6 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import { FalloDeLaTerminal, SALIDA } from "../contrato.js";
|
|
5
5
|
import { LlaveroAusente, TECHO_DEL_LLAVERO_MS } from "./llavero.js";
|
|
6
6
|
import { dentroDeUnWorkingTree } from "./repo.js";
|
|
7
|
+
// Cómo se nombra cada fuente en un texto para la persona; el valor `fuente` del JSON sigue siendo el de arriba.
|
|
8
|
+
export const NOMBRE_DE_LA_FUENTE = {
|
|
9
|
+
llavero: "the system keychain", archivo: "the credentials file", env: "SUPCOFLOW_TOKEN",
|
|
10
|
+
};
|
|
7
11
|
// La cuenta ES el origen de la API que emitió lo guardado (Review Focus 3): una clave acuñada por
|
|
8
12
|
// un motor en `localhost` queda en otra cuenta que la de producción, y la terminal apuntada a
|
|
9
13
|
// producción simplemente no la encuentra (sale 2) en vez de mandarla a un servidor que no la emitió.
|
|
@@ -71,8 +75,8 @@ export function crearGuardado(d) {
|
|
|
71
75
|
}
|
|
72
76
|
const leerCuenta = async (cuenta, queda) => (await leerCuentaCompleta(cuenta, queda)).mejor;
|
|
73
77
|
const errorDeBorrado = (fallidos) => {
|
|
74
|
-
const donde = fallidos.map((n) => (n === "llavero" ?
|
|
75
|
-
return new FalloDeLaTerminal(SALIDA.inesperado, "no_se_pudo_borrar", `
|
|
78
|
+
const donde = fallidos.map((n) => (n === "llavero" ? NOMBRE_DE_LA_FUENTE.llavero : d.rutaDelArchivo)).join(" or from ");
|
|
79
|
+
return new FalloDeLaTerminal(SALIDA.inesperado, "no_se_pudo_borrar", `I couldn't delete the saved key from ${donde}.`);
|
|
76
80
|
};
|
|
77
81
|
// El chequeo del repo va ANTES de escribir (Review Focus 1): una vez escrita, la clave ya está a
|
|
78
82
|
// un `git add -A` de publicarse, y borrarla después no deshace un commit. Mira la ruta REAL además
|
|
@@ -81,10 +85,10 @@ export function crearGuardado(d) {
|
|
|
81
85
|
async function escribirEnArchivo(cuenta, r, que) {
|
|
82
86
|
const raiz = dentroDeUnWorkingTree(path.dirname(d.rutaDelArchivo), d.fs);
|
|
83
87
|
if (raiz !== null) {
|
|
84
|
-
throw new FalloDeLaTerminal(SALIDA.inesperado, "destino_dentro_de_un_repo", `
|
|
88
|
+
throw new FalloDeLaTerminal(SALIDA.inesperado, "destino_dentro_de_un_repo", `I won't save ${que} inside a git repo (${raiz}). Use the system keychain or export SUPCOFLOW_TOKEN.`);
|
|
85
89
|
}
|
|
86
90
|
await d.archivo.escribir(cuenta, r);
|
|
87
|
-
d.avisar(`
|
|
91
|
+
d.avisar(`There is no system keychain: I saved ${que} in ${d.rutaDelArchivo} with 0600 permissions.`);
|
|
88
92
|
}
|
|
89
93
|
async function escribirCuenta(cuenta, valor, que, techoMs) {
|
|
90
94
|
const queda = plazo(techoMs);
|
|
@@ -145,7 +149,7 @@ export function crearGuardado(d) {
|
|
|
145
149
|
return { valor: mejor === null ? null : { clave: mejor.r.valor, fuente: mejor.fuente }, completa };
|
|
146
150
|
},
|
|
147
151
|
async guardarClave(origen, clave, techoMs) {
|
|
148
|
-
return { donde: await escribirCuenta(cuentaDeClave(origen), clave, "
|
|
152
|
+
return { donde: await escribirCuenta(cuentaDeClave(origen), clave, "the key", techoMs) };
|
|
149
153
|
},
|
|
150
154
|
async borrarClave(origen) {
|
|
151
155
|
// Un borrado a medias de una clave no se informa como hecho: la que quedó sigue sirviendo.
|
|
@@ -174,7 +178,7 @@ export function crearGuardado(d) {
|
|
|
174
178
|
}
|
|
175
179
|
},
|
|
176
180
|
async guardarPendiente(origen, p, techoMs) {
|
|
177
|
-
await escribirCuenta(cuentaDePendiente(origen), JSON.stringify(p), "
|
|
181
|
+
await escribirCuenta(cuentaDePendiente(origen), JSON.stringify(p), "the login in progress", techoMs);
|
|
178
182
|
},
|
|
179
183
|
async borrarPendiente(origen, techoMs) {
|
|
180
184
|
// Nunca lanza: un pendiente viejo es inofensivo (su device_code ya se consumió o vence en
|
|
@@ -217,7 +221,7 @@ export function crearGuardado(d) {
|
|
|
217
221
|
const r = { valor: JSON.stringify([...l.valor, clave]), escritoEn: d.ahora() };
|
|
218
222
|
if (a.nombre === "archivo") {
|
|
219
223
|
try {
|
|
220
|
-
await escribirEnArchivo(cuenta, r, "
|
|
224
|
+
await escribirEnArchivo(cuenta, r, "the keys to revoke");
|
|
221
225
|
return;
|
|
222
226
|
}
|
|
223
227
|
catch (e) {
|
|
@@ -229,7 +233,7 @@ export function crearGuardado(d) {
|
|
|
229
233
|
if ((await intentar(a, queda, (x, t) => x.escribir(cuenta, r, t))).estado === "ok")
|
|
230
234
|
return;
|
|
231
235
|
}
|
|
232
|
-
throw new FalloDeLaTerminal(SALIDA.inesperado, "no_se_pudo_anotar", "
|
|
236
|
+
throw new FalloDeLaTerminal(SALIDA.inesperado, "no_se_pudo_anotar", "I couldn't note down the previous key as \"to revoke\" in either the keychain or the file.");
|
|
233
237
|
},
|
|
234
238
|
// Quitar sólo achica la lista, así que no pasa por el control del repo: no agrega ningún secreto.
|
|
235
239
|
// Un almacén presente que falla deja `completo = false`: la clave puede seguir anotada ahí, el
|
package/dist/guardado/llavero.js
CHANGED
|
@@ -9,7 +9,7 @@ export const TECHO_DEL_LLAVERO_MS = 5_000;
|
|
|
9
9
|
// nada que borrar en él; uno que está y falla puede tener datos adentro.
|
|
10
10
|
export class LlaveroAusente extends Error {
|
|
11
11
|
constructor(bin) {
|
|
12
|
-
super(`
|
|
12
|
+
super(`There is no system keychain (${bin} is missing).`);
|
|
13
13
|
this.name = "LlaveroAusente";
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -26,14 +26,14 @@ function decodificar(s) {
|
|
|
26
26
|
v = undefined;
|
|
27
27
|
}
|
|
28
28
|
if (!esRegistro(v))
|
|
29
|
-
throw new Error("
|
|
29
|
+
throw new Error("The keychain returned something I don't understand.");
|
|
30
30
|
return { valor: v.valor, escritoEn: v.escritoEn };
|
|
31
31
|
}
|
|
32
32
|
// La cuenta viaja en una línea de `security -i`, que también parte por espacios y entiende comillas:
|
|
33
33
|
// una cuenta con algo de eso se leería distinta de lo que se escribió. Un origen nunca lo tiene.
|
|
34
34
|
function cuentaSegura(cuenta) {
|
|
35
35
|
if (!/^[A-Za-z0-9:/.\[\]#_-]+$/.test(cuenta))
|
|
36
|
-
throw new Error("
|
|
36
|
+
throw new Error("Invalid keychain account.");
|
|
37
37
|
return cuenta;
|
|
38
38
|
}
|
|
39
39
|
// La ruta ABSOLUTA: un `security` del PATH podría ser otro programa (un `node_modules/.bin`, un
|
|
@@ -79,10 +79,10 @@ export function almacenDeLlavero(plataforma, correr, existe = existsSync) {
|
|
|
79
79
|
if (r.ausente)
|
|
80
80
|
throw new LlaveroAusente(bin);
|
|
81
81
|
if (r.vencido)
|
|
82
|
-
throw new Error(`
|
|
82
|
+
throw new Error(`The keychain did not respond within ${Math.round(techoMs / 1000)} s.`);
|
|
83
83
|
return r;
|
|
84
84
|
}
|
|
85
|
-
const fallo = (codigo) => new Error(`
|
|
85
|
+
const fallo = (codigo) => new Error(`The keychain failed (code ${codigo}).`);
|
|
86
86
|
return {
|
|
87
87
|
nombre: "llavero",
|
|
88
88
|
async leer(cuenta, techoMs = TECHO_DEL_LLAVERO_MS) {
|
package/dist/guia.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// La guía del producto que vive en el repo del cliente. Sin dependencias: sólo `node:crypto`.
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
|
|
3
|
+
// El nombre canónico desde la 0.8.0; `guia.md` es el viejo y se sigue leyendo SÓLO si `guide.md` no existe.
|
|
4
|
+
export const RUTA_DE_LA_GUIA = ".supcoflow/guide.md";
|
|
5
|
+
export const RUTA_VIEJA_DE_LA_GUIA = ".supcoflow/guia.md";
|
|
4
6
|
export const MAX_BYTES_DE_LA_GUIA = 64 * 1024;
|
|
5
7
|
// La misma regla que el motor (`src/guia/formato.ts`), copiada a propósito y clavada con los vectores
|
|
6
8
|
// de `test/vectores/huella.json` (idénticos en los dos repos). Se normaliza antes de hashear porque un
|
package/dist/idempotencia.js
CHANGED
|
@@ -15,7 +15,7 @@ export function crearRegistroDeIdempotencia(d) {
|
|
|
15
15
|
// repo) y el motor lo reusa. Una clave de consulta no tiene clave natural: sin la de idempotencia
|
|
16
16
|
// guardada, el reintento tras una respuesta perdida crea OTRA fila. Mejor no mandar nada y decir qué
|
|
17
17
|
// arreglar. Por eso, en estricto, cada «aviso y sigo» de abajo lanza.
|
|
18
|
-
const noGuardada = () => new FalloDeLaTerminal(SALIDA.inesperado, "idempotencia_no_guardada", `
|
|
18
|
+
const noGuardada = () => new FalloDeLaTerminal(SALIDA.inesperado, "idempotencia_no_guardada", `I couldn't save the retry key in ${d.rutaDelArchivo}; without it, a retry would create another key. Check that file (it can't be inside a repo) and try again.`);
|
|
19
19
|
return {
|
|
20
20
|
async claveDe(operacion, alcance, opciones = {}) {
|
|
21
21
|
const c = cuenta(operacion, alcance);
|
|
@@ -29,7 +29,7 @@ export function crearRegistroDeIdempotencia(d) {
|
|
|
29
29
|
throw noGuardada();
|
|
30
30
|
// Un archivo que no se entiende no se reescribe (perdería otras claves); se sigue sin guardar.
|
|
31
31
|
const nueva = d.generar();
|
|
32
|
-
d.avisar(`
|
|
32
|
+
d.avisar(`I couldn't read ${d.rutaDelArchivo}: I'm going on with an idempotency key that isn't saved.`);
|
|
33
33
|
return nueva;
|
|
34
34
|
}
|
|
35
35
|
const nueva = d.generar();
|
|
@@ -45,13 +45,13 @@ export function crearRegistroDeIdempotencia(d) {
|
|
|
45
45
|
catch {
|
|
46
46
|
if (opciones.estricta)
|
|
47
47
|
throw noGuardada();
|
|
48
|
-
d.avisar(`
|
|
48
|
+
d.avisar(`I couldn't resolve the path of ${d.rutaDelArchivo}: I'm going on with an idempotency key that isn't saved.`);
|
|
49
49
|
return nueva;
|
|
50
50
|
}
|
|
51
51
|
if (raiz !== null) {
|
|
52
52
|
if (opciones.estricta)
|
|
53
53
|
throw noGuardada();
|
|
54
|
-
d.avisar(`
|
|
54
|
+
d.avisar(`I'm not saving the idempotency key inside a git repo (${raiz}): a retry will send a different one.`);
|
|
55
55
|
return nueva;
|
|
56
56
|
}
|
|
57
57
|
try {
|
|
@@ -60,7 +60,7 @@ export function crearRegistroDeIdempotencia(d) {
|
|
|
60
60
|
catch {
|
|
61
61
|
if (opciones.estricta)
|
|
62
62
|
throw noGuardada();
|
|
63
|
-
d.avisar(`
|
|
63
|
+
d.avisar(`I couldn't save the idempotency key in ${d.rutaDelArchivo}: a retry will send a different one.`);
|
|
64
64
|
}
|
|
65
65
|
return nueva;
|
|
66
66
|
},
|
|
@@ -81,7 +81,7 @@ export function crearRegistroDeIdempotencia(d) {
|
|
|
81
81
|
await d.almacen.borrar(cuenta(operacion, alcance));
|
|
82
82
|
}
|
|
83
83
|
catch {
|
|
84
|
-
d.avisar(`
|
|
84
|
+
d.avisar(`I couldn't delete the idempotency key from ${d.rutaDelArchivo}.`);
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
};
|