supcoflow 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -162
- package/dist/alias.js +60 -0
- package/dist/api.js +121 -16
- package/dist/argumentos.js +92 -78
- package/dist/cli.js +42 -31
- package/dist/comandos/clave-vigente.js +2 -2
- package/dist/comandos/claves.js +25 -25
- package/dist/comandos/configurar.js +40 -40
- package/dist/comandos/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +170 -138
- package/dist/comandos/guia.js +148 -61
- package/dist/comandos/instalacion.js +71 -40
- package/dist/comandos/login.js +25 -24
- package/dist/comandos/logout.js +17 -16
- package/dist/comandos/probar.js +359 -0
- package/dist/comandos/proyecto-del-repo.js +1 -1
- package/dist/comandos/proyecto.js +16 -16
- package/dist/comandos/quien-soy.js +8 -7
- package/dist/comandos/repo-conectar.js +9 -9
- package/dist/comandos/widget.js +9 -5
- package/dist/diagnostico.js +48 -0
- package/dist/entorno.js +32 -18
- package/dist/flags-por-comando.js +25 -19
- package/dist/guardado/archivo.js +2 -2
- package/dist/guardado/index.js +12 -8
- package/dist/guardado/llavero.js +5 -5
- package/dist/guia.js +3 -1
- package/dist/idempotencia.js +6 -6
- package/dist/instalacion.js +50 -11
- package/dist/lectura.js +10 -0
- package/dist/pruebas-guardadas.js +101 -0
- package/dist/repo-local.js +6 -6
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
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
|
|
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);
|
|
@@ -21,9 +21,9 @@ export function urlDeLaApi(env) {
|
|
|
21
21
|
return new URL(u.origin);
|
|
22
22
|
throw invalida();
|
|
23
23
|
}
|
|
24
|
-
// A mano a propósito: tiene que coincidir con package.json (
|
|
25
|
-
// en los logs del motor, no decide nada.
|
|
26
|
-
const VERSION = "0.
|
|
24
|
+
// A mano a propósito: tiene que coincidir con package.json (lo pinza test/describir.test.ts); sólo
|
|
25
|
+
// identifica a la terminal en los logs del motor, no decide nada.
|
|
26
|
+
const VERSION = "0.8.0";
|
|
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", "
|
|
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", `
|
|
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", `
|
|
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", `
|
|
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", "
|
|
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) ?? "
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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) ?? `
|
|
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,20 +324,121 @@ 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)
|
|
337
354
|
throw rechazada(r);
|
|
338
355
|
return aWidget(r.cuerpo, r.status);
|
|
339
356
|
}
|
|
357
|
+
// ---- La prueba (bloque 7) ----
|
|
358
|
+
const rutaDelProyecto = (proyectoId) => `/api/proyectos/${encodeURIComponent(proyectoId)}`;
|
|
359
|
+
const numeroONull = (v) => (v === null ? null : typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
360
|
+
export async function leerDiagnostico(api, clave, proyectoId, f) {
|
|
361
|
+
const r = await conClave(api, clave, `${rutaDelProyecto(proyectoId)}/diagnostico`, { method: "GET" }, f);
|
|
362
|
+
if (r.status !== 200)
|
|
363
|
+
throw rechazada(r);
|
|
364
|
+
const c = r.cuerpo;
|
|
365
|
+
const puntos = Array.isArray(c.puntos) ? c.puntos.map((x) => objeto(x, r.status)) : null;
|
|
366
|
+
const [saldo, tope, comprometido] = [c.saldo_usd, c.tope_usd, c.comprometido_usd].map(numeroONull);
|
|
367
|
+
if (typeof c.listo !== "boolean" || puntos === null || !texto(c.politica) || typeof c.techo_usd !== "number"
|
|
368
|
+
|| saldo === undefined || tope === undefined || comprometido === undefined
|
|
369
|
+
|| (c.prueba_abierta !== null && !texto(c.prueba_abierta))
|
|
370
|
+
|| puntos.some((p) => !texto(p.punto) || typeof p.ok !== "boolean" || typeof p.mensaje !== "string"))
|
|
371
|
+
throw inesperada(r.status);
|
|
372
|
+
return {
|
|
373
|
+
listo: c.listo,
|
|
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
|
+
})),
|
|
378
|
+
politica: c.politica,
|
|
379
|
+
techoUsd: c.techo_usd,
|
|
380
|
+
saldoUsd: saldo, topeUsd: tope, comprometidoUsd: comprometido,
|
|
381
|
+
pruebaAbierta: texto(c.prueba_abierta),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// Sin `conClave`: su 401/403/5xx lanzan, y acá cada uno tiene que decidir qué pasa con el intento.
|
|
385
|
+
// `idempotency_en_curso` es ambigua: otro intento con la misma clave sigue corriendo y puede crearla.
|
|
386
|
+
// Un 503 de la pausa de despachos (D10) también: el motor no guardó nada, pero conservar el intento no
|
|
387
|
+
// cuesta (el reintento con la misma clave crea la prueba una sola vez) y un 503 genérico no se distingue.
|
|
388
|
+
export async function pedirPrueba(api, clave, proyectoId, intento, f) {
|
|
389
|
+
const cuerpo = { techo_usd: intento.techoUsd };
|
|
390
|
+
if (intento.mensaje !== null)
|
|
391
|
+
cuerpo.mensaje = intento.mensaje;
|
|
392
|
+
if (intento.politicaEsperada !== null)
|
|
393
|
+
cuerpo.politica_esperada = intento.politicaEsperada;
|
|
394
|
+
let r;
|
|
395
|
+
try {
|
|
396
|
+
r = await pedir(api, `${rutaDelProyecto(proyectoId)}/pruebas`, {
|
|
397
|
+
method: "POST", headers: { ...JSON_BODY, ...bearer(clave), "idempotency-key": intento.clave }, body: JSON.stringify(cuerpo),
|
|
398
|
+
}, f, PLAZO_DE_ALTA_MS);
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
const fallo = e instanceof FalloDeLaTerminal ? e : new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", "I couldn't reach the engine.");
|
|
402
|
+
return { tipo: "ambigua", error: fallo.error, message: fallo.message };
|
|
403
|
+
}
|
|
404
|
+
const error = texto(r.cuerpo.error);
|
|
405
|
+
const message = taparSecretos(texto(r.cuerpo.message) ?? `The engine answered ${r.status}.`);
|
|
406
|
+
if (r.status === 202 || r.status === 200 || r.status === 201) {
|
|
407
|
+
const [pruebaId, reporteId, estado] = [r.cuerpo.prueba_id, r.cuerpo.reporte_id, r.cuerpo.estado].map(texto);
|
|
408
|
+
if (pruebaId && /^prb_[0-9a-f]{16}$/.test(pruebaId) && reporteId && estado)
|
|
409
|
+
return { tipo: "aceptada", pruebaId, reporteId, estado };
|
|
410
|
+
return { tipo: "ambigua", error: "respuesta_inesperada", message: `The engine answered ${r.status} without the test.` };
|
|
411
|
+
}
|
|
412
|
+
if (r.status >= 500 || (r.status === 409 && error === "idempotency_en_curso")) {
|
|
413
|
+
return { tipo: "ambigua", error: error ?? "motor_inalcanzable", message };
|
|
414
|
+
}
|
|
415
|
+
if (r.status >= 400)
|
|
416
|
+
return { tipo: "rechazo", status: r.status, error: error ?? "rechazado", message, cuerpo: r.cuerpo };
|
|
417
|
+
return { tipo: "ambigua", error: "respuesta_inesperada", message: `The engine answered ${r.status} with something I didn't expect.` };
|
|
418
|
+
}
|
|
419
|
+
export async function leerPrueba(api, clave, proyectoId, pruebaId, f, plazoMs) {
|
|
420
|
+
const r = await conClave(api, clave, `${rutaDelProyecto(proyectoId)}/pruebas/${encodeURIComponent(pruebaId)}`, { method: "GET" }, f, plazoMs);
|
|
421
|
+
if (r.status !== 200)
|
|
422
|
+
throw rechazada(r);
|
|
423
|
+
const c = r.cuerpo;
|
|
424
|
+
const cobro = objeto(c.cobro, r.status);
|
|
425
|
+
const usd = numeroONull(cobro.usd);
|
|
426
|
+
if (!texto(c.prueba_id) || !texto(c.estado) || typeof cobro.techo_usd !== "number" || usd === undefined
|
|
427
|
+
|| (cobro.estado !== null && !texto(cobro.estado)))
|
|
428
|
+
throw inesperada(r.status);
|
|
429
|
+
const rep = c.reporte === null || c.reporte === undefined ? null : objeto(c.reporte, r.status);
|
|
430
|
+
const run = c.run === null || c.run === undefined ? null : objeto(c.run, r.status);
|
|
431
|
+
const issue = c.issue === null || c.issue === undefined ? null : objeto(c.issue, r.status);
|
|
432
|
+
if (issue !== null && (typeof issue.numero !== "number" || !texto(issue.url)))
|
|
433
|
+
throw inesperada(r.status);
|
|
434
|
+
return {
|
|
435
|
+
pruebaId: c.prueba_id,
|
|
436
|
+
estado: c.estado,
|
|
437
|
+
abierta: typeof c.abierta === "boolean" ? c.abierta : null,
|
|
438
|
+
mensaje: texto(c.mensaje) === null ? null : taparSecretos(c.mensaje),
|
|
439
|
+
reporte: rep === null || !texto(rep.id) || !texto(rep.url) ? null : { id: rep.id, url: rep.url },
|
|
440
|
+
run: run === null ? null : { id: String(run.id), status: String(run.status), motivo: texto(run.motivo) },
|
|
441
|
+
issue: issue === null ? null : { numero: issue.numero, url: issue.url },
|
|
442
|
+
cobro: { estado: texto(cobro.estado), usd, techo_usd: cobro.techo_usd },
|
|
443
|
+
};
|
|
444
|
+
}
|
package/dist/argumentos.js
CHANGED
|
@@ -1,108 +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
|
|
5
|
-
"chat
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
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
|
-
|
|
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", "
|
|
15
|
+
"repo", "install", "test", "site", "title", "greeting", "file", "allow", "mode", "name", "save-to", "id", "source", "skill", "app",
|
|
16
|
+
"message", "test-id", "ceiling", "expect-policy",
|
|
14
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
|
+
}
|
|
15
33
|
// strict: un flag mal escrito (`--estart`) no se ignora en silencio: un asistente creería que
|
|
16
34
|
// pidió algo que no pidió.
|
|
17
35
|
export function leerArgumentos(argv) {
|
|
18
36
|
let parsed;
|
|
19
37
|
try {
|
|
20
|
-
parsed = parseArgs({
|
|
21
|
-
args: argv,
|
|
22
|
-
strict: true,
|
|
23
|
-
allowPositionals: true,
|
|
24
|
-
options: {
|
|
25
|
-
json: { type: "boolean" },
|
|
26
|
-
"dry-run": { type: "boolean" },
|
|
27
|
-
help: { type: "boolean", short: "h" },
|
|
28
|
-
start: { type: "boolean" },
|
|
29
|
-
wait: { type: "boolean" },
|
|
30
|
-
repo: { type: "string" },
|
|
31
|
-
install: { type: "string" },
|
|
32
|
-
test: { type: "string" },
|
|
33
|
-
sitio: { type: "string" },
|
|
34
|
-
titulo: { type: "string" },
|
|
35
|
-
saludo: { type: "string" },
|
|
36
|
-
archivo: { type: "string" },
|
|
37
|
-
permitir: { type: "string" },
|
|
38
|
-
modo: { type: "string" },
|
|
39
|
-
nombre: { type: "string" },
|
|
40
|
-
"guardar-en": { type: "string" },
|
|
41
|
-
id: { type: "string" },
|
|
42
|
-
origen: { type: "string" },
|
|
43
|
-
"desde-github": { type: "boolean" },
|
|
44
|
-
skill: { type: "string" },
|
|
45
|
-
app: { type: "string" },
|
|
46
|
-
tema: { type: "string", multiple: true },
|
|
47
|
-
},
|
|
48
|
-
tokens: true,
|
|
49
|
-
});
|
|
38
|
+
parsed = parseArgs({ args: argv, strict: true, allowPositionals: true, options: opciones(), tokens: true });
|
|
50
39
|
}
|
|
51
40
|
catch (e) {
|
|
52
41
|
const code = e.code;
|
|
53
42
|
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
54
43
|
const m = /'(--?[^']+)'/.exec(e.message);
|
|
55
|
-
throw
|
|
44
|
+
throw invalida("flag_desconocido", `Unknown flag ${m ? m[1] : "you passed"}. Try \`supcoflow --help\`.`);
|
|
56
45
|
}
|
|
57
|
-
throw
|
|
46
|
+
throw invalida("argumentos_invalidos", `${e.message} Try \`supcoflow --help\`.`);
|
|
58
47
|
}
|
|
59
48
|
const pos = parsed.positionals;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
65
56
|
for (const nombre of FLAGS_CON_VALOR) {
|
|
66
|
-
const toks =
|
|
67
|
-
if (toks.length > 1 || toks.some((t) => t.
|
|
68
|
-
throw
|
|
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\`.`);
|
|
69
60
|
}
|
|
70
61
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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`.");
|
|
76
70
|
// Un par desconocido (`repo borrar`) queda como la primera palabra sola: el despacho la rechaza
|
|
77
71
|
// como comando desconocido, con el mismo mensaje que cualquier otra.
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
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);
|
|
81
89
|
return {
|
|
82
90
|
comando,
|
|
91
|
+
viejos,
|
|
83
92
|
flags: {
|
|
84
|
-
json:
|
|
85
|
-
dryRun:
|
|
86
|
-
help:
|
|
87
|
-
start:
|
|
88
|
-
wait:
|
|
89
|
-
repo:
|
|
90
|
-
install:
|
|
91
|
-
test:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
id:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
skill:
|
|
104
|
-
app:
|
|
105
|
-
|
|
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,
|
|
106
120
|
},
|
|
107
121
|
};
|
|
108
122
|
}
|
package/dist/cli.js
CHANGED
|
@@ -6,12 +6,13 @@ 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";
|
|
13
13
|
import { loginInteractivo, loginStart, loginWait } from "./comandos/login.js";
|
|
14
14
|
import { comandoLogout } from "./comandos/logout.js";
|
|
15
|
+
import { diagnosticar, probar } from "./comandos/probar.js";
|
|
15
16
|
import { proyectoCrear, proyectoVer } from "./comandos/proyecto.js";
|
|
16
17
|
import { comandoQuienSoy } from "./comandos/quien-soy.js";
|
|
17
18
|
import { repoConectarInteractivo, repoConectarStart, repoConectarWait } from "./comandos/repo-conectar.js";
|
|
@@ -21,15 +22,16 @@ import { entornoReal } from "./entorno.js";
|
|
|
21
22
|
import { flagQueNoAplica } from "./flags-por-comando.js";
|
|
22
23
|
const COMANDOS = new Set(DESCRIPCION.comandos.map((c) => c.nombre));
|
|
23
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`.
|
|
24
26
|
async function correr(argv) {
|
|
25
27
|
// Si los argumentos no se pueden leer no se sabe qué flags hay: se mira `--json` a mano para que
|
|
26
28
|
// el error salga en el formato que pidió quien llama.
|
|
27
29
|
let json = argv.includes("--json");
|
|
28
30
|
try {
|
|
29
|
-
const { comando, flags } = leerArgumentos(argv);
|
|
31
|
+
const { comando, flags, viejos } = leerArgumentos(argv);
|
|
30
32
|
json = flags.json;
|
|
31
33
|
if (comando !== null && !COMANDOS.has(comando)) {
|
|
32
|
-
throw invalida("comando_desconocido", `
|
|
34
|
+
throw invalida("comando_desconocido", `Unknown command «${comando}». Try \`supcoflow --help\`.`);
|
|
33
35
|
}
|
|
34
36
|
if (flags.help || comando === null) {
|
|
35
37
|
const texto = ayuda(comando);
|
|
@@ -37,24 +39,26 @@ async function correr(argv) {
|
|
|
37
39
|
}
|
|
38
40
|
const presentes = {
|
|
39
41
|
start: flags.start, wait: flags.wait, repo: flags.repo !== null, install: flags.install !== null, test: flags.test !== null,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
43
45
|
skill: flags.skill !== null, app: flags.app !== null,
|
|
46
|
+
authorizeSpend: flags.authorizeSpend, message: flags.message !== null, testId: flags.testId !== null,
|
|
47
|
+
ceiling: flags.ceiling !== null, expectPolicy: flags.expectPolicy !== null,
|
|
44
48
|
};
|
|
45
49
|
const noAplica = flagQueNoAplica(comando, Object.keys(presentes).filter((k) => presentes[k]));
|
|
46
50
|
if (noAplica !== null)
|
|
47
51
|
throw invalida("flag_no_aplica", noAplica);
|
|
48
52
|
if (flags.start && flags.wait) {
|
|
49
|
-
throw invalida("flags_incompatibles", "--start
|
|
53
|
+
throw invalida("flags_incompatibles", "--start and --wait go separately: first one and then the other.");
|
|
50
54
|
}
|
|
51
|
-
if (comando === "
|
|
55
|
+
if (comando === "describe")
|
|
52
56
|
return { r: comandoDescribir(), json };
|
|
53
57
|
if (comando === "login") {
|
|
54
58
|
// ANTES de armar el entorno: no hay cómo simular un canje sin consumirlo, y este error no
|
|
55
59
|
// puede depender de leer nada guardado ni de llamar al motor.
|
|
56
60
|
if (flags.wait && flags.dryRun) {
|
|
57
|
-
throw invalida("dry_run_no_soportado", "`login --wait` no
|
|
61
|
+
throw invalida("dry_run_no_soportado", "`login --wait` has no --dry-run: polling consumes the code. Try `supcoflow login --start --dry-run`.");
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
const e = entornoReal();
|
|
@@ -76,9 +80,9 @@ async function correr(argv) {
|
|
|
76
80
|
}, flags),
|
|
77
81
|
json,
|
|
78
82
|
};
|
|
79
|
-
case "
|
|
83
|
+
case "whoami":
|
|
80
84
|
return { r: await comandoQuienSoy({ api: e.api, guardado: e.guardado, quienSoy: e.quienSoy, avisar: e.avisar }), json };
|
|
81
|
-
case "repo
|
|
85
|
+
case "repo connect":
|
|
82
86
|
if (flags.start)
|
|
83
87
|
return { r: await repoConectarStart(e.conexion, flags), json };
|
|
84
88
|
// `--wait` sólo lee (un GET por tramo): con --dry-run haría lo mismo, así que no se rechaza.
|
|
@@ -88,50 +92,57 @@ async function correr(argv) {
|
|
|
88
92
|
if (flags.dryRun)
|
|
89
93
|
return { r: await repoConectarStart(e.conexion, { repo: flags.repo, dryRun: true }), json };
|
|
90
94
|
return { r: await repoConectarInteractivo({ ...e.conexion, esTty: e.esTty, mostrar: e.mostrar }, flags), json };
|
|
91
|
-
case "
|
|
95
|
+
case "project create":
|
|
92
96
|
return { r: await proyectoCrear(e.proyecto, flags), json };
|
|
93
|
-
case "
|
|
97
|
+
case "project show":
|
|
94
98
|
return { r: await proyectoVer(e.proyecto, flags), json };
|
|
95
|
-
case "
|
|
99
|
+
case "catalog":
|
|
96
100
|
return { r: await comandoCatalogo(e.configurar), json };
|
|
97
|
-
case "
|
|
101
|
+
case "config":
|
|
98
102
|
return { r: await comandoConfiguracion(e.configurar, flags), json };
|
|
99
|
-
case "chat
|
|
103
|
+
case "chat on":
|
|
100
104
|
return { r: await chatPrenderApagar(e.configurar, flags, true), json };
|
|
101
|
-
case "chat
|
|
105
|
+
case "chat off":
|
|
102
106
|
return { r: await chatPrenderApagar(e.configurar, flags, false), json };
|
|
103
|
-
case "chat
|
|
107
|
+
case "chat brand":
|
|
104
108
|
return { r: await chatMarca(e.configurar, flags), json };
|
|
105
109
|
case "chat logo":
|
|
106
110
|
return { r: await chatLogo(e.configurar, flags), json };
|
|
107
|
-
case "
|
|
111
|
+
case "sites add":
|
|
108
112
|
return { r: await sitios(e.configurar, flags, "agregar"), json };
|
|
109
|
-
case "
|
|
113
|
+
case "sites remove":
|
|
110
114
|
return { r: await sitios(e.configurar, flags, "quitar"), json };
|
|
111
|
-
case "
|
|
115
|
+
case "data set":
|
|
112
116
|
return { r: await datosFijar(e.configurar, flags), json };
|
|
113
|
-
case "
|
|
117
|
+
case "policy set":
|
|
114
118
|
return { r: await politicaFijar(e.configurar, flags), json };
|
|
115
|
-
case "
|
|
119
|
+
case "keys create":
|
|
116
120
|
return { r: await clavesCrear(e.claves, flags), json };
|
|
117
|
-
case "
|
|
121
|
+
case "keys list":
|
|
118
122
|
return { r: await clavesListar(e.claves, flags), json };
|
|
119
|
-
case "
|
|
123
|
+
case "keys revoke":
|
|
120
124
|
return { r: await clavesRevocar(e.claves, flags), json };
|
|
121
|
-
case "
|
|
125
|
+
case "guide check":
|
|
122
126
|
return { r: await guiaRevisar(e.guia, flags), json };
|
|
123
|
-
case "
|
|
127
|
+
case "guide publish":
|
|
124
128
|
return { r: await guiaPublicar(e.guia, flags), json };
|
|
125
|
-
case "
|
|
129
|
+
case "guide show":
|
|
126
130
|
return { r: await guiaVer(e.guia, flags), json };
|
|
127
|
-
case "
|
|
131
|
+
case "guide use":
|
|
128
132
|
return { r: await guiaUsar(e.guia, flags), json };
|
|
133
|
+
case "guide versions":
|
|
134
|
+
return { r: await guiaVersiones(e.guia, flags), json };
|
|
129
135
|
case "widget":
|
|
130
136
|
return { r: await widget(e.widget, flags), json };
|
|
131
|
-
case "
|
|
137
|
+
case "install record":
|
|
132
138
|
return { r: await instalacionAnotar(e.instalacion, flags), json };
|
|
139
|
+
case "diagnose":
|
|
140
|
+
return { r: await diagnosticar(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 };
|
|
133
144
|
}
|
|
134
|
-
throw new Error(`
|
|
145
|
+
throw new Error(`command without dispatch: ${comando}`);
|
|
135
146
|
}
|
|
136
147
|
catch (e) {
|
|
137
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 = "
|
|
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 = "
|
|
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 } };
|