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.
@@ -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} está conectado.`,
20
+ texto: `${p.repoCanonico ?? repo} is connected.`,
21
21
  };
22
22
  case "repo_fuera_de_alcance": {
23
- const message = `La App está instalada pero no alcanza a ${repo}: sumalo en GitHub y volvé a tocar Confirmar (${p.url}).`;
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", "El pedido venció. Pedí otro con `supcoflow repo conectar --start`.", { repo, pedido });
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(`La última confirmación falló: ${p.fallo}.`);
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: `Abrí ${p.url}, instalá la App en GitHub si hace falta y tocá Confirmar.`,
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: `Con --dry-run no hago nada; sin él, pediría conectar ${repo} (POST /api/conexiones).`,
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", `No hay una conexión en curso para ${repo}: empezá con \`supcoflow repo conectar --start\`.`, { repo });
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", "Sin terminal interactiva usá `supcoflow repo conectar --start --json` y después `supcoflow repo conectar --wait --json`.");
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} Espero acá…`);
118
+ d.mostrar(`${inicio.texto} I'll wait here...`);
119
119
  let r;
120
120
  do {
121
121
  r = await repoConectarWait(d, flags);
@@ -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. Instalá el paquete (con el gestor de la app, la versión exacta):",
6
+ "1. Install the package (with the app's package manager, the exact version):",
6
7
  ` ${w.instalar}`,
7
8
  "",
8
- `2. Creá el archivo ${w.nextjs.archivo_sugerido} con este contenido (Next.js, ${w.nextjs.estado}):`,
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. Usalo en el layout que cubre las pantallas donde se traban los usuarios:",
15
+ "3. Use it in the layout that covers the screens where users get stuck:",
12
16
  ` ${w.nextjs.uso}`,
13
17
  "",
14
- "4. Política de contenido (CSP): si la app tiene `connect-src`, agregale este origen conservando lo que ya tiene:",
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
- "Alternativa con <script> (disponible, no probada en una app real):",
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(`No puedo resolver la ruta real de ${p} (¿un enlace roto?); no guardo nada ahí.`);
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(no se lee)";
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" ? "no existe" : code === "ELOOP" ? "es un enlace" : "no se pudo abrir" };
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: "no es un archivo común" };
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: "es demasiado grande" };
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: "no se pudo leer" };
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(`No leo ${p}: ${porque}.`);
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))} no es una carpeta común (¿un enlace?)`);
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" ? "es un enlace" : "no se pudo abrir");
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, "no es un archivo común");
328
+ return noSeLee(p, NO_LEIDO.noEsComun);
328
329
  if (st.size > TOPE_DEL_ARCHIVO_DEL_REPO)
329
- return noSeLee(p, "es demasiado grande");
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, "es demasiado grande");
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, "no se pudo leer");
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} no es una carpeta común`);
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 `quien-soy` creería que preguntó por ese repo. Vive aparte de
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
- "configuracion", "chat prender", "chat apagar", "chat marca", "chat logo", "sitios agregar", "sitios quitar",
6
- "datos fijar", "politica fijar", "claves crear", "claves listar", "claves revocar",
7
- "guia revisar", "guia publicar", "guia ver", "guia usar",
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 conectar"], texto: "--start es de `supcoflow login` y `supcoflow repo conectar`." },
11
- { flags: ["wait"], comandos: ["login", "repo conectar", "probar"], texto: "--wait es de `supcoflow login`, `supcoflow repo conectar` y `supcoflow probar`." },
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 conectar", "proyecto crear", "proyecto ver", ...DE_CONFIGURAR_CON_REPO, "widget", "instalacion anotar", "diagnosticar", "probar"],
14
- texto: "--repo es de `supcoflow repo conectar`, `proyecto crear|ver`, `widget`, `instalacion anotar`, `diagnosticar`, `probar` y los comandos que configuran un proyecto (no de `catalogo`).",
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: ["proyecto crear"], texto: "--install y --test son de `supcoflow proyecto crear`." },
17
- { flags: ["sitio"], comandos: ["sitios agregar", "sitios quitar"], texto: "--sitio es de `supcoflow sitios agregar|quitar`." },
18
- { flags: ["titulo", "saludo", "tema"], comandos: ["chat marca"], texto: "--titulo, --saludo y --tema son de `supcoflow chat marca`." },
19
- { flags: ["archivo"], comandos: ["chat logo"], texto: "--archivo es de `supcoflow chat logo`." },
20
- { flags: ["permitir"], comandos: ["datos fijar"], texto: "--permitir es de `supcoflow datos fijar`." },
21
- { flags: ["modo"], comandos: ["politica fijar"], texto: "--modo es de `supcoflow politica fijar`." },
22
- { flags: ["nombre", "guardarEn"], comandos: ["claves crear"], texto: "--nombre y --guardar-en son de `supcoflow claves crear`." },
23
- { flags: ["origen"], comandos: ["guia usar"], texto: "--origen es de `supcoflow guia usar`." },
24
- { flags: ["desdeGithub"], comandos: ["guia publicar"], texto: "--desde-github es de `supcoflow guia publicar`." },
25
- { flags: ["id"], comandos: ["claves revocar"], texto: "--id es de `supcoflow claves revocar`." },
26
- { flags: ["autorizoGasto", "mensaje", "prueba"], comandos: ["probar"], texto: "--autorizo-gasto, --mensaje y --prueba son de `supcoflow probar`." },
27
- { flags: ["skill", "app"], comandos: ["instalacion anotar"], texto: "--skill y --app son de `supcoflow instalacion anotar`." },
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) {
@@ -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(`No entiendo el archivo de credenciales ${ruta}; borralo y volvé a iniciar sesión.`);
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(`No entiendo el archivo de credenciales ${ruta}; borralo y volvé a iniciar sesión.`);
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) {
@@ -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" ? "del llavero del sistema" : `de ${d.rutaDelArchivo}`)).join(" ni ");
75
- return new FalloDeLaTerminal(SALIDA.inesperado, "no_se_pudo_borrar", `No pude borrar la clave guardada ${donde}.`);
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", `No guardo ${que} dentro de un repo de git (${raiz}). Usá el llavero del sistema o exportá SUPCOFLOW_TOKEN.`);
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(`No hay llavero del sistema: guardé ${que} en ${d.rutaDelArchivo} con permisos 0600.`);
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, "la clave", techoMs) };
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), "el inicio de sesión en curso", techoMs);
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, "las claves por revocar");
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", "No pude anotar la clave anterior como «por revocar» ni en el llavero ni en el archivo.");
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
@@ -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(`No hay llavero del sistema (falta ${bin}).`);
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("El llavero devolvió algo que no entiendo.");
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("Cuenta del llavero inválida.");
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(`El llavero no contestó en ${Math.round(techoMs / 1000)} s.`);
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(`El llavero falló (código ${codigo}).`);
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
- export const RUTA_DE_LA_GUIA = ".supcoflow/guia.md";
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
@@ -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", `No pude guardar la clave de reintento en ${d.rutaDelArchivo}; sin ella, un reintento crearía otra clave. Revisá ese archivo (no puede estar dentro de un repo) y probá de nuevo.`);
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(`No pude leer ${d.rutaDelArchivo}: sigo con una clave de idempotencia sin guardar.`);
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(`No pude resolver la ruta de ${d.rutaDelArchivo}: sigo con una clave de idempotencia sin guardar.`);
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(`No guardo la clave de idempotencia dentro de un repo de git (${raiz}): un reintento mandará otra.`);
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(`No pude guardar la clave de idempotencia en ${d.rutaDelArchivo}: un reintento mandará otra.`);
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(`No pude borrar la clave de idempotencia de ${d.rutaDelArchivo}.`);
84
+ d.avisar(`I couldn't delete the idempotency key from ${d.rutaDelArchivo}.`);
85
85
  }
86
86
  },
87
87
  };