supcoflow 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -9,7 +9,7 @@ export function urlDeLaApi(env) {
9
9
  const crudo = env.SUPCOFLOW_API_URL;
10
10
  if (crudo === undefined || crudo === "")
11
11
  return new URL(API_DE_PRODUCCION);
12
- const invalida = () => new FalloDeLaTerminal(SALIDA.entradaInvalida, "api_url_invalida", "SUPCOFLOW_API_URL tiene que ser https, o http contra localhost.");
12
+ const invalida = () => new FalloDeLaTerminal(SALIDA.entradaInvalida, "api_url_invalida", "SUPCOFLOW_API_URL must be https, or http against localhost.");
13
13
  let u;
14
14
  try {
15
15
  u = new URL(crudo);
@@ -23,7 +23,7 @@ export function urlDeLaApi(env) {
23
23
  }
24
24
  // A mano a propósito: tiene que coincidir con package.json (lo pinza test/describir.test.ts); sólo
25
25
  // identifica a la terminal en los logs del motor, no decide nada.
26
- const VERSION = "0.7.0";
26
+ const VERSION = "0.8.1";
27
27
  const PLAZO_POR_DEFECTO_MS = 15_000;
28
28
  // Los dos POST que crean algo: detrás, el motor acuña con Vercel Connect y le pregunta a GitHub varias
29
29
  // veces (instalación, repos, comandos, rama). Con 15 s una respuesta lenta pero buena se cortaba y
@@ -49,7 +49,7 @@ async function pedir(api, ruta, init, f, plazoMs) {
49
49
  texto = await res.text();
50
50
  }
51
51
  catch {
52
- throw new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", "No pude comunicarme con el motor (red caída o demora).");
52
+ throw new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", "I couldn't reach the engine (the network is down or it took too long).");
53
53
  }
54
54
  let cuerpo;
55
55
  try {
@@ -59,15 +59,15 @@ async function pedir(api, ruta, init, f, plazoMs) {
59
59
  cuerpo = undefined;
60
60
  }
61
61
  if (typeof cuerpo !== "object" || cuerpo === null || Array.isArray(cuerpo)) {
62
- throw new FalloDeLaTerminal(SALIDA.inesperado, "respuesta_inesperada", `El motor respondió ${res.status} sin JSON.`);
62
+ throw new FalloDeLaTerminal(SALIDA.inesperado, "respuesta_inesperada", `The engine answered ${res.status} without JSON.`);
63
63
  }
64
64
  return { status: res.status, cuerpo: cuerpo };
65
65
  }
66
66
  function sinServicio(status) {
67
- return new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", `El motor respondió ${status}; probá en unos minutos.`);
67
+ return new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", `The engine answered ${status}; try again in a few minutes.`);
68
68
  }
69
69
  function inesperada(status) {
70
- return new FalloDeLaTerminal(SALIDA.inesperado, "respuesta_inesperada", `El motor respondió ${status} con algo que no esperaba.`);
70
+ return new FalloDeLaTerminal(SALIDA.inesperado, "respuesta_inesperada", `The engine answered ${status} with something I didn't expect.`);
71
71
  }
72
72
  const texto = (v) => (typeof v === "string" ? v : null);
73
73
  const CLIENT_ID = "supcoflow-cli";
@@ -76,7 +76,7 @@ const FORMULARIO = { "content-type": "application/x-www-form-urlencoded" };
76
76
  export async function pedirCodigo(api, maquina, f) {
77
77
  const { status, cuerpo: c } = await pedir(api, "/api/dispositivo/codigo", { method: "POST", headers: FORMULARIO, body: new URLSearchParams({ client_id: CLIENT_ID, maquina }).toString() }, f);
78
78
  if (status === 429 || status === 503) {
79
- throw new FalloDeLaTerminal(SALIDA.inesperado, texto(c.error) ?? "demasiados_pedidos", "El motor no acepta más pedidos ahora; probá en unos minutos.");
79
+ throw new FalloDeLaTerminal(SALIDA.inesperado, texto(c.error) ?? "demasiados_pedidos", "The engine isn't accepting more requests right now; try again in a few minutes.");
80
80
  }
81
81
  if (status >= 500)
82
82
  throw sinServicio(status);
@@ -112,7 +112,7 @@ const bearer = (clave) => ({ authorization: `Bearer ${clave}` });
112
112
  export async function quienSoy(api, clave, f) {
113
113
  const { status, cuerpo: c } = await pedir(api, "/api/yo", { method: "GET", headers: bearer(clave) }, f);
114
114
  if (status === 401) {
115
- throw new FalloDeLaTerminal(SALIDA.faltaLogin, "clave_invalida", texto(c.message) ?? "La clave ya no vale: iniciá sesión de nuevo.");
115
+ throw new FalloDeLaTerminal(SALIDA.faltaLogin, "clave_invalida", texto(c.message) ?? "The key is no longer valid: log in again.");
116
116
  }
117
117
  if (status >= 500)
118
118
  throw sinServicio(status);
@@ -146,16 +146,16 @@ async function conClave(api, clave, ruta, init, f, plazoMs) {
146
146
  const error = texto(r.cuerpo.error) ?? "respuesta_inesperada";
147
147
  const message = (porDefecto) => taparSecretos(texto(r.cuerpo.message) ?? porDefecto);
148
148
  if (r.status === 401) {
149
- throw new FalloDeLaTerminal(SALIDA.faltaLogin, "clave_invalida", message("La clave ya no vale: iniciá sesión de nuevo."));
149
+ throw new FalloDeLaTerminal(SALIDA.faltaLogin, "clave_invalida", message("The key is no longer valid: log in again."));
150
150
  }
151
151
  if (r.status === 403 && (error === "requiere_persona" || error === "clave_de_consulta")) {
152
- throw new FalloDeLaTerminal(SALIDA.haceFaltaUnaPersona, error, message("Esto lo tiene que hacer una persona con sesión."));
152
+ throw new FalloDeLaTerminal(SALIDA.haceFaltaUnaPersona, error, message("A person who is logged in has to do this."));
153
153
  }
154
154
  if (r.status === 422 && error === "cuenta_sin_github") {
155
- throw new FalloDeLaTerminal(SALIDA.haceFaltaUnaPersona, error, message("Tu cuenta no tiene GitHub asociado."));
155
+ throw new FalloDeLaTerminal(SALIDA.haceFaltaUnaPersona, error, message("Your account has no GitHub linked."));
156
156
  }
157
157
  if (r.status === 400 && error === "repo_invalido") {
158
- throw new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message("El repo tiene que ser owner/repo."));
158
+ throw new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message("The repo must be owner/repo."));
159
159
  }
160
160
  if (r.status >= 500)
161
161
  throw sinServicio(r.status);
@@ -181,7 +181,7 @@ function aProyecto(v, status) {
181
181
  function rechazada(r) {
182
182
  const error = texto(r.cuerpo.error);
183
183
  if (r.status >= 400 && r.status < 500 && error) {
184
- return new FalloDeLaTerminal(SALIDA.entradaInvalida, error, taparSecretos(texto(r.cuerpo.message) ?? `El motor rechazó el pedido (${r.status}).`));
184
+ return new FalloDeLaTerminal(SALIDA.entradaInvalida, error, taparSecretos(texto(r.cuerpo.message) ?? `The engine rejected the request (${r.status}).`));
185
185
  }
186
186
  return inesperada(r.status);
187
187
  }
@@ -309,6 +309,10 @@ export function publicarGuia(api, clave, proyectoId, cuerpo, f) {
309
309
  export function leerGuia(api, clave, proyectoId, f) {
310
310
  return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}?comprobar=1`, { method: "GET" }, f, PLAZO_DE_ALTA_MS);
311
311
  }
312
+ // Las publicaciones del repo y la guía que rige ahora (`guide versions`): sólo lectura, la misma clase que `leerGuia`.
313
+ export function leerVersionesDeLaGuia(api, clave, proyectoId, f) {
314
+ return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}/versiones`, { method: "GET" }, f, PLAZO_DE_ALTA_MS);
315
+ }
312
316
  export function elegirOrigen(api, clave, proyectoId, origen, f) {
313
317
  return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}/origen`, { method: "POST", headers: JSON_BODY, body: JSON.stringify({ origen }) }, f);
314
318
  }
@@ -320,17 +324,30 @@ function aWidget(v, status) {
320
324
  const [tenantId, projectId, backendUrl, version, instalar, csp] = [v.tenant_id, v.project_id, v.backend_url, v.version, v.instalar, v.csp].map(texto);
321
325
  const [archivo, contenido, uso] = [n.archivo_sugerido, n.contenido, n.uso].map(texto);
322
326
  const linea = texto(s.linea);
327
+ // AD9: `archivos_anteriores` vive sólo dentro de `nextjs`, al lado de `archivo_sugerido`; arriba de todo no se lee.
328
+ const anteriores = listaDeTextos(n.archivos_anteriores);
329
+ if (anteriores === null)
330
+ throw inesperada(status);
323
331
  if (!tenantId || !projectId || !backendUrl || !version || !instalar || !csp || v.paquete !== "@supcoflow/chat"
324
332
  || (n.estado !== "probando" && n.estado !== "soportado") || !archivo || !contenido || !uso
325
333
  || s.estado !== "disponible_sin_soporte" || !linea)
326
334
  throw inesperada(status);
327
335
  return {
328
336
  tenant_id: tenantId, project_id: projectId, backend_url: backendUrl, paquete: "@supcoflow/chat", version, instalar,
329
- nextjs: { estado: n.estado, archivo_sugerido: archivo, contenido, uso },
337
+ nextjs: { estado: n.estado, archivo_sugerido: archivo, archivos_anteriores: anteriores, contenido, uso },
330
338
  script: { estado: "disponible_sin_soporte", linea },
331
339
  csp,
332
340
  };
333
341
  }
342
+ // Una lista de textos no vacíos; ausente es lista vacía (un motor sin el campo), y cualquier otra forma es null.
343
+ function listaDeTextos(v) {
344
+ if (v === undefined || v === null)
345
+ return [];
346
+ if (!Array.isArray(v))
347
+ return null;
348
+ const textos = v.map(texto);
349
+ return textos.every((t) => t !== null && t !== "") ? textos : null;
350
+ }
334
351
  export async function leerWidget(api, clave, proyectoId, f) {
335
352
  const r = await conClave(api, clave, `/api/proyectos/${encodeURIComponent(proyectoId)}/widget`, { method: "GET" }, f);
336
353
  if (r.status !== 200)
@@ -354,7 +371,10 @@ export async function leerDiagnostico(api, clave, proyectoId, f) {
354
371
  throw inesperada(r.status);
355
372
  return {
356
373
  listo: c.listo,
357
- puntos: puntos.map((p) => ({ punto: p.punto, ok: p.ok, mensaje: taparSecretos(p.mensaje) })),
374
+ puntos: puntos.map((p) => ({
375
+ punto: p.punto, ok: p.ok, mensaje: taparSecretos(p.mensaje),
376
+ ...(p.grupo === "instalacion" || p.grupo === "prueba" ? { grupo: p.grupo } : {}),
377
+ })),
358
378
  politica: c.politica,
359
379
  techoUsd: c.techo_usd,
360
380
  saldoUsd: saldo, topeUsd: tope, comprometidoUsd: comprometido,
@@ -369,6 +389,8 @@ export async function pedirPrueba(api, clave, proyectoId, intento, f) {
369
389
  const cuerpo = { techo_usd: intento.techoUsd };
370
390
  if (intento.mensaje !== null)
371
391
  cuerpo.mensaje = intento.mensaje;
392
+ if (intento.politicaEsperada !== null)
393
+ cuerpo.politica_esperada = intento.politicaEsperada;
372
394
  let r;
373
395
  try {
374
396
  r = await pedir(api, `${rutaDelProyecto(proyectoId)}/pruebas`, {
@@ -376,23 +398,23 @@ export async function pedirPrueba(api, clave, proyectoId, intento, f) {
376
398
  }, f, PLAZO_DE_ALTA_MS);
377
399
  }
378
400
  catch (e) {
379
- const fallo = e instanceof FalloDeLaTerminal ? e : new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", "No pude comunicarme con el motor.");
401
+ const fallo = e instanceof FalloDeLaTerminal ? e : new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", "I couldn't reach the engine.");
380
402
  return { tipo: "ambigua", error: fallo.error, message: fallo.message };
381
403
  }
382
404
  const error = texto(r.cuerpo.error);
383
- const message = taparSecretos(texto(r.cuerpo.message) ?? `El motor respondió ${r.status}.`);
405
+ const message = taparSecretos(texto(r.cuerpo.message) ?? `The engine answered ${r.status}.`);
384
406
  if (r.status === 202 || r.status === 200 || r.status === 201) {
385
407
  const [pruebaId, reporteId, estado] = [r.cuerpo.prueba_id, r.cuerpo.reporte_id, r.cuerpo.estado].map(texto);
386
408
  if (pruebaId && /^prb_[0-9a-f]{16}$/.test(pruebaId) && reporteId && estado)
387
409
  return { tipo: "aceptada", pruebaId, reporteId, estado };
388
- return { tipo: "ambigua", error: "respuesta_inesperada", message: `El motor respondió ${r.status} sin la prueba.` };
410
+ return { tipo: "ambigua", error: "respuesta_inesperada", message: `The engine answered ${r.status} without the test.` };
389
411
  }
390
412
  if (r.status >= 500 || (r.status === 409 && error === "idempotency_en_curso")) {
391
413
  return { tipo: "ambigua", error: error ?? "motor_inalcanzable", message };
392
414
  }
393
415
  if (r.status >= 400)
394
416
  return { tipo: "rechazo", status: r.status, error: error ?? "rechazado", message, cuerpo: r.cuerpo };
395
- return { tipo: "ambigua", error: "respuesta_inesperada", message: `El motor respondió ${r.status} con algo que no esperaba.` };
417
+ return { tipo: "ambigua", error: "respuesta_inesperada", message: `The engine answered ${r.status} with something I didn't expect.` };
396
418
  }
397
419
  export async function leerPrueba(api, clave, proyectoId, pruebaId, f, plazoMs) {
398
420
  const r = await conClave(api, clave, `${rutaDelProyecto(proyectoId)}/pruebas/${encodeURIComponent(pruebaId)}`, { method: "GET" }, f, plazoMs);
@@ -1,115 +1,122 @@
1
1
  import { parseArgs } from "node:util";
2
+ import { COMANDOS_VIEJOS, FLAGS_VIEJOS } from "./alias.js";
2
3
  import { FalloDeLaTerminal, SALIDA } from "./contrato.js";
3
4
  export const COMANDOS_COMPUESTOS = [
4
- "repo conectar", "proyecto crear", "proyecto ver",
5
- "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",
8
- "instalacion anotar",
5
+ "repo connect", "project create", "project show",
6
+ "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",
9
+ "install record",
9
10
  ];
10
- // Sólo estas primeras palabras admiten una segunda: cualquier otro par sigue siendo «de más».
11
- const PRIMERAS_PALABRAS = new Set(COMANDOS_COMPUESTOS.map((c) => c.split(" ")[0]));
11
+ // Sólo estas primeras palabras admiten una segunda: cualquier otro par sigue siendo «de más». Entran también las de
12
+ // los nombres viejos (`guia publicar`), que se resuelven más abajo.
13
+ const PRIMERAS_PALABRAS = new Set([...COMANDOS_COMPUESTOS, ...Object.keys(COMANDOS_VIEJOS)].filter((c) => c.includes(" ")).map((c) => c.split(" ")[0]));
12
14
  const FLAGS_CON_VALOR = [
13
- "repo", "install", "test", "sitio", "titulo", "saludo", "archivo", "permitir", "modo", "nombre", "guardar-en", "id", "origen", "skill", "app",
14
- "mensaje", "prueba",
15
+ "repo", "install", "test", "site", "title", "greeting", "file", "allow", "mode", "name", "save-to", "id", "source", "skill", "app",
16
+ "message", "test-id", "ceiling", "expect-policy",
15
17
  ];
18
+ const FLAGS_BOOLEANOS = ["json", "dry-run", "help", "start", "wait", "from-github", "authorize-spend"];
19
+ // El flag que el token nombró, ya llevado a su nombre canónico (el viejo y el nuevo cuentan como el mismo).
20
+ const canonico = (nombre) => FLAGS_VIEJOS[nombre] ?? nombre;
21
+ const invalida = (error, message) => new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message);
22
+ function opciones() {
23
+ const o = {};
24
+ for (const f of FLAGS_BOOLEANOS)
25
+ o[f] = f === "help" ? { type: "boolean", short: "h" } : { type: "boolean" };
26
+ for (const f of FLAGS_CON_VALOR)
27
+ o[f] = { type: "string" };
28
+ o.theme = { type: "string", multiple: true };
29
+ for (const [viejo, nuevo] of Object.entries(FLAGS_VIEJOS))
30
+ o[viejo] = { ...o[nuevo] };
31
+ return o;
32
+ }
16
33
  // strict: un flag mal escrito (`--estart`) no se ignora en silencio: un asistente creería que
17
34
  // pidió algo que no pidió.
18
35
  export function leerArgumentos(argv) {
19
36
  let parsed;
20
37
  try {
21
- parsed = parseArgs({
22
- args: argv,
23
- strict: true,
24
- allowPositionals: true,
25
- options: {
26
- json: { type: "boolean" },
27
- "dry-run": { type: "boolean" },
28
- help: { type: "boolean", short: "h" },
29
- start: { type: "boolean" },
30
- wait: { type: "boolean" },
31
- repo: { type: "string" },
32
- install: { type: "string" },
33
- test: { type: "string" },
34
- sitio: { type: "string" },
35
- titulo: { type: "string" },
36
- saludo: { type: "string" },
37
- archivo: { type: "string" },
38
- permitir: { type: "string" },
39
- modo: { type: "string" },
40
- nombre: { type: "string" },
41
- "guardar-en": { type: "string" },
42
- id: { type: "string" },
43
- origen: { type: "string" },
44
- "desde-github": { type: "boolean" },
45
- skill: { type: "string" },
46
- app: { type: "string" },
47
- "autorizo-gasto": { type: "boolean" },
48
- mensaje: { type: "string" },
49
- prueba: { type: "string" },
50
- tema: { type: "string", multiple: true },
51
- },
52
- tokens: true,
53
- });
38
+ parsed = parseArgs({ args: argv, strict: true, allowPositionals: true, options: opciones(), tokens: true });
54
39
  }
55
40
  catch (e) {
56
41
  const code = e.code;
57
42
  if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
58
43
  const m = /'(--?[^']+)'/.exec(e.message);
59
- throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "flag_desconocido", `No conozco el flag ${m ? m[1] : "que pasaste"}. Probá \`supcoflow --help\`.`);
44
+ throw invalida("flag_desconocido", `Unknown flag ${m ? m[1] : "you passed"}. Try \`supcoflow --help\`.`);
60
45
  }
61
- throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_invalidos", `${e.message} Probá \`supcoflow --help\`.`);
46
+ throw invalida("argumentos_invalidos", `${e.message} Try \`supcoflow --help\`.`);
62
47
  }
63
48
  const pos = parsed.positionals;
64
- const deMas = () => new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_de_mas", "Pasaste más de un comando. Probá `supcoflow --help`.");
65
- if (pos.length > 2 || (pos.length === 2 && !PRIMERAS_PALABRAS.has(pos[0])))
66
- throw deMas();
67
- // `parseArgs` se queda con el último valor de un flag repetido: un asistente que repite `--repo`
68
- // creería que se usó el primero. Se mira en los tokens y se rechaza.
49
+ if (pos.length > 2 || (pos.length === 2 && !PRIMERAS_PALABRAS.has(pos[0]))) {
50
+ throw invalida("argumentos_de_mas", "You passed more than one command. Try `supcoflow --help`.");
51
+ }
52
+ const tokens = (parsed.tokens ?? []).filter((t) => t.kind === "option");
53
+ const viejos = [];
54
+ // `parseArgs` se queda con el último valor de un flag repetido: un asistente que repite `--repo` (o que pasa el
55
+ // nombre viejo y el nuevo de un mismo flag) creería que se usó el primero. Se mira en los tokens y se rechaza.
69
56
  for (const nombre of FLAGS_CON_VALOR) {
70
- const toks = (parsed.tokens ?? []).filter((t) => t.kind === "option" && t.name === nombre);
71
- if (toks.length > 1 || toks.some((t) => t.kind === "option" && t.value === "")) {
72
- throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_invalidos", `--${nombre} va una sola vez y con un valor. Probá \`supcoflow --help\`.`);
57
+ const toks = tokens.filter((t) => canonico(t.name) === nombre);
58
+ if (toks.length > 1 || toks.some((t) => t.value === "")) {
59
+ throw invalida("argumentos_invalidos", `--${nombre} goes once, with a value. Try \`supcoflow --help\`.`);
73
60
  }
74
61
  }
75
- // `--tema` sí se repite (uno por clave), pero ninguno vacío: un `--tema ""` es un error de quien arma
76
- // el comando, no «sin tema».
77
- if ((parsed.values.tema ?? []).some((t) => t === "")) {
78
- throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_invalidos", "--tema va con un valor clave=valor. Probá `supcoflow --help`.");
79
- }
62
+ for (const t of tokens)
63
+ if (t.name in FLAGS_VIEJOS)
64
+ viejos.push(`--${t.name}`);
65
+ // `--theme` sí se repite (uno por clave), pero ninguno vacío: un `--theme ""` es un error de quien arma
66
+ // el comando, no «sin tema». Se junta desde los tokens para conservar el orden entre el nombre viejo y el nuevo.
67
+ const tema = tokens.filter((t) => canonico(t.name) === "theme").map((t) => t.value ?? "");
68
+ if (tema.some((t) => t === ""))
69
+ throw invalida("argumentos_invalidos", "--theme goes with a key=value. Try `supcoflow --help`.");
80
70
  // Un par desconocido (`repo borrar`) queda como la primera palabra sola: el despacho la rechaza
81
71
  // como comando desconocido, con el mismo mensaje que cualquier otra.
82
- const compuesto = pos.length === 2 ? `${pos[0]} ${pos[1]}` : null;
83
- const comando = compuesto !== null && COMANDOS_COMPUESTOS.includes(compuesto) ? compuesto : (pos[0] ?? null);
84
- const v = parsed.values;
72
+ const escrito = pos.length === 2 ? `${pos[0]} ${pos[1]}` : (pos[0] ?? null);
73
+ let comando;
74
+ if (escrito !== null && escrito in COMANDOS_VIEJOS) {
75
+ comando = COMANDOS_VIEJOS[escrito];
76
+ viejos.unshift(escrito);
77
+ }
78
+ else if (pos.length === 2 && COMANDOS_COMPUESTOS.includes(escrito)) {
79
+ comando = escrito;
80
+ }
81
+ else {
82
+ comando = pos[0] ?? null;
83
+ }
84
+ const un = (nombre) => {
85
+ const t = tokens.find((x) => canonico(x.name) === nombre);
86
+ return t === undefined ? null : (t.value ?? null);
87
+ };
88
+ const hay = (nombre) => tokens.some((x) => canonico(x.name) === nombre);
85
89
  return {
86
90
  comando,
91
+ viejos,
87
92
  flags: {
88
- json: v.json === true,
89
- dryRun: v["dry-run"] === true,
90
- help: v.help === true,
91
- start: v.start === true,
92
- wait: v.wait === true,
93
- repo: v.repo ?? null,
94
- install: v.install ?? null,
95
- test: v.test ?? null,
96
- sitio: v.sitio ?? null,
97
- titulo: v.titulo ?? null,
98
- saludo: v.saludo ?? null,
99
- archivo: v.archivo ?? null,
100
- permitir: v.permitir ?? null,
101
- modo: v.modo ?? null,
102
- nombre: v.nombre ?? null,
103
- guardarEn: v["guardar-en"] ?? null,
104
- id: v.id ?? null,
105
- origen: v.origen ?? null,
106
- desdeGithub: v["desde-github"] === true,
107
- skill: v.skill ?? null,
108
- app: v.app ?? null,
109
- autorizoGasto: v["autorizo-gasto"] === true,
110
- mensaje: v.mensaje ?? null,
111
- prueba: v.prueba ?? null,
112
- tema: v.tema ?? [],
93
+ json: hay("json"),
94
+ dryRun: hay("dry-run"),
95
+ help: hay("help"),
96
+ start: hay("start"),
97
+ wait: hay("wait"),
98
+ repo: un("repo"),
99
+ install: un("install"),
100
+ test: un("test"),
101
+ site: un("site"),
102
+ title: un("title"),
103
+ greeting: un("greeting"),
104
+ file: un("file"),
105
+ allow: un("allow"),
106
+ mode: un("mode"),
107
+ name: un("name"),
108
+ saveTo: un("save-to"),
109
+ id: un("id"),
110
+ source: un("source"),
111
+ fromGithub: hay("from-github"),
112
+ skill: un("skill"),
113
+ app: un("app"),
114
+ authorizeSpend: hay("authorize-spend"),
115
+ message: un("message"),
116
+ testId: un("test-id"),
117
+ ceiling: un("ceiling"),
118
+ expectPolicy: un("expect-policy"),
119
+ theme: tema,
113
120
  },
114
121
  };
115
122
  }
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { revocarPropia } from "./api.js";
6
6
  import { leerArgumentos } from "./argumentos.js";
7
7
  import { clavesCrear, clavesListar, clavesRevocar } from "./comandos/claves.js";
8
8
  import { chatLogo, chatMarca, chatPrenderApagar, comandoConfiguracion, sitios } from "./comandos/configurar.js";
9
- import { guiaPublicar, guiaRevisar, guiaUsar, guiaVer } from "./comandos/guia.js";
9
+ import { guiaPublicar, guiaRevisar, guiaUsar, guiaVer, guiaVersiones } from "./comandos/guia.js";
10
10
  import { instalacionAnotar } from "./comandos/instalacion.js";
11
11
  import { comandoCatalogo, datosFijar, politicaFijar } from "./comandos/datos-y-politica.js";
12
12
  import { ayuda, comandoDescribir, DESCRIPCION } from "./comandos/describir.js";
@@ -22,15 +22,16 @@ import { entornoReal } from "./entorno.js";
22
22
  import { flagQueNoAplica } from "./flags-por-comando.js";
23
23
  const COMANDOS = new Set(DESCRIPCION.comandos.map((c) => c.nombre));
24
24
  const invalida = (error, message) => new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message);
25
+ // Los nombres que la terminal tenía antes de la 0.8.0 siguen valiendo (`src/alias.ts`), pero no salen en `describe`.
25
26
  async function correr(argv) {
26
27
  // Si los argumentos no se pueden leer no se sabe qué flags hay: se mira `--json` a mano para que
27
28
  // el error salga en el formato que pidió quien llama.
28
29
  let json = argv.includes("--json");
29
30
  try {
30
- const { comando, flags } = leerArgumentos(argv);
31
+ const { comando, flags, viejos } = leerArgumentos(argv);
31
32
  json = flags.json;
32
33
  if (comando !== null && !COMANDOS.has(comando)) {
33
- throw invalida("comando_desconocido", `No conozco el comando «${comando}». Probá \`supcoflow --help\`.`);
34
+ throw invalida("comando_desconocido", `Unknown command «${comando}». Try \`supcoflow --help\`.`);
34
35
  }
35
36
  if (flags.help || comando === null) {
36
37
  const texto = ayuda(comando);
@@ -38,25 +39,26 @@ async function correr(argv) {
38
39
  }
39
40
  const presentes = {
40
41
  start: flags.start, wait: flags.wait, repo: flags.repo !== null, install: flags.install !== null, test: flags.test !== null,
41
- sitio: flags.sitio !== null, titulo: flags.titulo !== null, saludo: flags.saludo !== null, tema: flags.tema.length > 0,
42
- archivo: flags.archivo !== null, permitir: flags.permitir !== null, modo: flags.modo !== null,
43
- nombre: flags.nombre !== null, guardarEn: flags.guardarEn !== null, id: flags.id !== null, origen: flags.origen !== null, desdeGithub: flags.desdeGithub,
42
+ site: flags.site !== null, title: flags.title !== null, greeting: flags.greeting !== null, theme: flags.theme.length > 0,
43
+ file: flags.file !== null, allow: flags.allow !== null, mode: flags.mode !== null,
44
+ name: flags.name !== null, saveTo: flags.saveTo !== null, id: flags.id !== null, source: flags.source !== null, fromGithub: flags.fromGithub,
44
45
  skill: flags.skill !== null, app: flags.app !== null,
45
- autorizoGasto: flags.autorizoGasto, mensaje: flags.mensaje !== null, prueba: flags.prueba !== null,
46
+ authorizeSpend: flags.authorizeSpend, message: flags.message !== null, testId: flags.testId !== null,
47
+ ceiling: flags.ceiling !== null, expectPolicy: flags.expectPolicy !== null,
46
48
  };
47
49
  const noAplica = flagQueNoAplica(comando, Object.keys(presentes).filter((k) => presentes[k]));
48
50
  if (noAplica !== null)
49
51
  throw invalida("flag_no_aplica", noAplica);
50
52
  if (flags.start && flags.wait) {
51
- throw invalida("flags_incompatibles", "--start y --wait van por separado: primero uno y después el otro.");
53
+ throw invalida("flags_incompatibles", "--start and --wait go separately: first one and then the other.");
52
54
  }
53
- if (comando === "describir")
55
+ if (comando === "describe")
54
56
  return { r: comandoDescribir(), json };
55
57
  if (comando === "login") {
56
58
  // ANTES de armar el entorno: no hay cómo simular un canje sin consumirlo, y este error no
57
59
  // puede depender de leer nada guardado ni de llamar al motor.
58
60
  if (flags.wait && flags.dryRun) {
59
- throw invalida("dry_run_no_soportado", "`login --wait` no tiene --dry-run: sondear consume el código. Probá `supcoflow login --start --dry-run`.");
61
+ throw invalida("dry_run_no_soportado", "`login --wait` has no --dry-run: polling consumes the code. Try `supcoflow login --start --dry-run`.");
60
62
  }
61
63
  }
62
64
  const e = entornoReal();
@@ -78,9 +80,9 @@ async function correr(argv) {
78
80
  }, flags),
79
81
  json,
80
82
  };
81
- case "quien-soy":
83
+ case "whoami":
82
84
  return { r: await comandoQuienSoy({ api: e.api, guardado: e.guardado, quienSoy: e.quienSoy, avisar: e.avisar }), json };
83
- case "repo conectar":
85
+ case "repo connect":
84
86
  if (flags.start)
85
87
  return { r: await repoConectarStart(e.conexion, flags), json };
86
88
  // `--wait` sólo lee (un GET por tramo): con --dry-run haría lo mismo, así que no se rechaza.
@@ -90,54 +92,57 @@ async function correr(argv) {
90
92
  if (flags.dryRun)
91
93
  return { r: await repoConectarStart(e.conexion, { repo: flags.repo, dryRun: true }), json };
92
94
  return { r: await repoConectarInteractivo({ ...e.conexion, esTty: e.esTty, mostrar: e.mostrar }, flags), json };
93
- case "proyecto crear":
95
+ case "project create":
94
96
  return { r: await proyectoCrear(e.proyecto, flags), json };
95
- case "proyecto ver":
97
+ case "project show":
96
98
  return { r: await proyectoVer(e.proyecto, flags), json };
97
- case "catalogo":
99
+ case "catalog":
98
100
  return { r: await comandoCatalogo(e.configurar), json };
99
- case "configuracion":
101
+ case "config":
100
102
  return { r: await comandoConfiguracion(e.configurar, flags), json };
101
- case "chat prender":
103
+ case "chat on":
102
104
  return { r: await chatPrenderApagar(e.configurar, flags, true), json };
103
- case "chat apagar":
105
+ case "chat off":
104
106
  return { r: await chatPrenderApagar(e.configurar, flags, false), json };
105
- case "chat marca":
107
+ case "chat brand":
106
108
  return { r: await chatMarca(e.configurar, flags), json };
107
109
  case "chat logo":
108
110
  return { r: await chatLogo(e.configurar, flags), json };
109
- case "sitios agregar":
111
+ case "sites add":
110
112
  return { r: await sitios(e.configurar, flags, "agregar"), json };
111
- case "sitios quitar":
113
+ case "sites remove":
112
114
  return { r: await sitios(e.configurar, flags, "quitar"), json };
113
- case "datos fijar":
115
+ case "data set":
114
116
  return { r: await datosFijar(e.configurar, flags), json };
115
- case "politica fijar":
117
+ case "policy set":
116
118
  return { r: await politicaFijar(e.configurar, flags), json };
117
- case "claves crear":
119
+ case "keys create":
118
120
  return { r: await clavesCrear(e.claves, flags), json };
119
- case "claves listar":
121
+ case "keys list":
120
122
  return { r: await clavesListar(e.claves, flags), json };
121
- case "claves revocar":
123
+ case "keys revoke":
122
124
  return { r: await clavesRevocar(e.claves, flags), json };
123
- case "guia revisar":
125
+ case "guide check":
124
126
  return { r: await guiaRevisar(e.guia, flags), json };
125
- case "guia publicar":
127
+ case "guide publish":
126
128
  return { r: await guiaPublicar(e.guia, flags), json };
127
- case "guia ver":
129
+ case "guide show":
128
130
  return { r: await guiaVer(e.guia, flags), json };
129
- case "guia usar":
131
+ case "guide use":
130
132
  return { r: await guiaUsar(e.guia, flags), json };
133
+ case "guide versions":
134
+ return { r: await guiaVersiones(e.guia, flags), json };
131
135
  case "widget":
132
136
  return { r: await widget(e.widget, flags), json };
133
- case "instalacion anotar":
137
+ case "install record":
134
138
  return { r: await instalacionAnotar(e.instalacion, flags), json };
135
- case "diagnosticar":
139
+ case "diagnose":
136
140
  return { r: await diagnosticar(e.prueba, flags), json };
137
- case "probar":
138
- return { r: await probar(e.prueba, flags), json };
141
+ case "test":
142
+ // El alias viejo (`probar`, o `--autorizo-gasto` con cualquier nombre) no puede ampliar lo autorizado: lo marca.
143
+ return { r: await probar(e.prueba, { ...flags, viejo: viejos.includes("probar") || viejos.includes("--autorizo-gasto") }), json };
139
144
  }
140
- throw new Error(`comando sin despacho: ${comando}`);
145
+ throw new Error(`command without dispatch: ${comando}`);
141
146
  }
142
147
  catch (e) {
143
148
  return { r: resultadoDeFallo(e), json };
@@ -7,11 +7,11 @@ export async function claveVigente(guardado, origen) {
7
7
  // «Iniciá sesión» con la clave guardada en un llavero que no contestó invita a un login que la
8
8
  // pisaría: no es «sin sesión», es «no pude mirar» (Review Focus 12).
9
9
  if (vigente === null && !lectura.completa) {
10
- const message = "No pude leer la clave guardada (el llavero no respondió). Desbloquealo y probá de nuevo.";
10
+ const message = "I couldn't read the saved key (the keychain didn't respond). Unlock it and try again.";
11
11
  return { ok: false, resultado: { codigo: SALIDA.inesperado, json: { ok: false, error: "llavero_no_responde", message }, texto: message } };
12
12
  }
13
13
  if (vigente === null) {
14
- const message = "Iniciá sesión con `supcoflow login`.";
14
+ const message = "Log in with `supcoflow login`.";
15
15
  return { ok: false, resultado: { codigo: SALIDA.faltaLogin, json: { ok: false, error: "sin_sesion", message }, texto: message } };
16
16
  }
17
17
  return { ok: true, vigente: { clave: vigente.clave, fuente: vigente.fuente, completa: lectura.completa } };