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/comandos/claves.js
CHANGED
|
@@ -13,12 +13,12 @@ const OPERACION = "clave_crear";
|
|
|
13
13
|
const SUFIJO = /^scf_([0-9a-f]{16})_[0-9a-f]{64}$/;
|
|
14
14
|
const ID_DE_CLAVE = /^cla_[0-9a-f]{16}$/;
|
|
15
15
|
export async function clavesCrear(d, flags) {
|
|
16
|
-
if (flags.
|
|
17
|
-
return fallo(SALIDA.entradaInvalida, "falta_guardar_en", "
|
|
18
|
-
if (flags.
|
|
19
|
-
return fallo(SALIDA.entradaInvalida, "falta_nombre", "
|
|
16
|
+
if (flags.saveTo === null)
|
|
17
|
+
return fallo(SALIDA.entradaInvalida, "falta_guardar_en", "Say where to save the key: `supcoflow keys create --name ci --save-to ~/ci.key` (outside a repo).");
|
|
18
|
+
if (flags.name === null)
|
|
19
|
+
return fallo(SALIDA.entradaInvalida, "falta_nombre", "Give it a name that says what it's for: `--name ci`.");
|
|
20
20
|
// Absoluta desde ya: la salida dice dónde quedó sin depender de dónde estaba parado el comando.
|
|
21
|
-
const destino = path.resolve(d.cwd, flags.
|
|
21
|
+
const destino = path.resolve(d.cwd, flags.saveTo);
|
|
22
22
|
const p = await proyectoDelRepo(d, flags);
|
|
23
23
|
if (!p.ok)
|
|
24
24
|
return p.resultado;
|
|
@@ -28,8 +28,8 @@ export async function clavesCrear(d, flags) {
|
|
|
28
28
|
if (invalido !== null)
|
|
29
29
|
return fallo(invalido.codigo, "destino_invalido", `${destino}: ${invalido.error}.`);
|
|
30
30
|
if (flags.dryRun)
|
|
31
|
-
return dryRun(`POST ${ruta}
|
|
32
|
-
const alcance = `${p.proyecto.id}#${flags.
|
|
31
|
+
return dryRun(`POST ${ruta} and then …/confirmar`);
|
|
32
|
+
const alcance = `${p.proyecto.id}#${flags.name}`;
|
|
33
33
|
// El registro de idempotencia pone el alcance en minúsculas (`CI` y `ci` comparten fila): el candado
|
|
34
34
|
// tiene que coincidir, o dos procesos con la misma clave de reintento corrían a la vez.
|
|
35
35
|
// 1. El candado: el ciclo entero, de a uno por operación en esta máquina. Comprobar que el archivo
|
|
@@ -38,18 +38,18 @@ export async function clavesCrear(d, flags) {
|
|
|
38
38
|
// máquinas no comparten la clave de idempotencia, así que crean filas distintas y no se pisan.
|
|
39
39
|
const candado = d.candados.tomar(`${OPERACION}#${alcance.toLowerCase()}`);
|
|
40
40
|
if (candado.tipo === "ocupado")
|
|
41
|
-
return fallo(SALIDA.inesperado, "en_curso", "
|
|
41
|
+
return fallo(SALIDA.inesperado, "en_curso", "Another identical `supcoflow keys create` is running on this machine; wait for it to finish.");
|
|
42
42
|
if (candado.tipo === "abandonado") {
|
|
43
|
-
return fallo(SALIDA.inesperado, "candado_abandonado", `
|
|
43
|
+
return fallo(SALIDA.inesperado, "candado_abandonado", `A lock from a \`keys create\` that didn't finish was left behind: ${candado.ruta}. If nothing else is running, delete it and try again.`);
|
|
44
44
|
}
|
|
45
45
|
if (candado.tipo === "no_disponible") {
|
|
46
|
-
return fallo(SALIDA.inesperado, "candado_no_disponible", `
|
|
46
|
+
return fallo(SALIDA.inesperado, "candado_no_disponible", `I couldn't create the lock at ${candado.ruta} (permissions?). Check that folder and try again.`);
|
|
47
47
|
}
|
|
48
48
|
try {
|
|
49
49
|
const autentica = async (secreto) => d.esClaveDeConsultaDelProyecto(d.api, secreto, p.proyecto.id);
|
|
50
50
|
const yaCreada = async (claveId) => {
|
|
51
51
|
await d.idempotencia.olvidar(OPERACION, alcance);
|
|
52
|
-
return { codigo: SALIDA.ok, json: { ok: true, clave_id: claveId, archivo: destino, ya_confirmada: true }, texto: `
|
|
52
|
+
return { codigo: SALIDA.ok, json: { ok: true, clave_id: claveId, archivo: destino, ya_confirmada: true }, texto: `Key ${claveId} was already created; it's in ${destino}.` };
|
|
53
53
|
};
|
|
54
54
|
// 2. Mirar el destino antes de pedir nada. Una clave de consulta de ESTE proyecto que autentica es un
|
|
55
55
|
// éxito anterior: repetir el comando no crea otra fila (el archivo es la referencia durable; la clave
|
|
@@ -60,12 +60,12 @@ export async function clavesCrear(d, flags) {
|
|
|
60
60
|
if (hay.estado === "clave" && (await autentica(hay.secreto)))
|
|
61
61
|
return await yaCreada(hay.claveId);
|
|
62
62
|
if (hay.estado !== "ausente" && !(hay.estado === "clave" && (await d.idempotencia.tiene(OPERACION, alcance)))) {
|
|
63
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "destino_ocupado", `${destino}
|
|
63
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "destino_ocupado", `${destino} already has something that isn't a current key for this project; I'm not touching it. Delete the file or pick another destination with --save-to.`);
|
|
64
64
|
}
|
|
65
65
|
// 3. El ciclo. Estricta: sin la clave de reintento guardada no se manda nada (Review Focus 15).
|
|
66
66
|
const key = await d.idempotencia.claveDe(OPERACION, alcance, { estricta: true });
|
|
67
67
|
for (let vuelta = 0; vuelta < 2; vuelta++) {
|
|
68
|
-
const r = await d.configurar(d.api, p.clave, ruta, { nombre: flags.
|
|
68
|
+
const r = await d.configurar(d.api, p.clave, ruta, { nombre: flags.name }, { idempotencyKey: key });
|
|
69
69
|
if (r.status === 200 && r.cuerpo.estado === "activa") {
|
|
70
70
|
// El motor no rota una activa (no devuelve secreto): vale sólo si lo que tenemos es su secreto vigente.
|
|
71
71
|
const claveId = String(r.cuerpo.clave_id);
|
|
@@ -73,7 +73,7 @@ export async function clavesCrear(d, flags) {
|
|
|
73
73
|
if (ahora.estado === "clave" && ahora.claveId === claveId && (await autentica(ahora.secreto)))
|
|
74
74
|
return await yaCreada(claveId);
|
|
75
75
|
await d.idempotencia.olvidar(OPERACION, alcance);
|
|
76
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_desactualizado", `
|
|
76
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_desactualizado", `Key ${claveId} is active, but ${destino} doesn't hold its current secret. Revoke it with \`supcoflow keys revoke --id ${claveId}\` and create another.`);
|
|
77
77
|
}
|
|
78
78
|
if (r.status !== 201) {
|
|
79
79
|
if (r.cuerpo.error === "clave_vencida") {
|
|
@@ -81,7 +81,7 @@ export async function clavesCrear(d, flags) {
|
|
|
81
81
|
await d.idempotencia.olvidar(OPERACION, alcance);
|
|
82
82
|
// El archivo conserva el secreto pendiente vencido y un reintento daría 4 destino_ocupado. Borrarlo
|
|
83
83
|
// desde acá sería tocar un archivo que quizá no escribió este run; se lo pedimos a la persona.
|
|
84
|
-
return fallo(SALIDA.inesperado, "clave_vencida", `
|
|
84
|
+
return fallo(SALIDA.inesperado, "clave_vencida", `The pending key expired without being confirmed. Delete ${destino} (it holds the expired secret, which is no longer any use) and run the command again to create another.`);
|
|
85
85
|
}
|
|
86
86
|
// Misma clave de reintento con otro cuerpo: no se arregla reintentando, y guardada trabaría para siempre.
|
|
87
87
|
if (r.cuerpo.error === "idempotency_key_reusada")
|
|
@@ -91,25 +91,25 @@ export async function clavesCrear(d, flags) {
|
|
|
91
91
|
const claveId = String(r.cuerpo.clave_id);
|
|
92
92
|
const secreto = typeof r.cuerpo.clave === "string" && SUFIJO.test(r.cuerpo.clave) ? r.cuerpo.clave : null;
|
|
93
93
|
if (secreto === null)
|
|
94
|
-
return fallo(SALIDA.inesperado, "respuesta_inesperada", "
|
|
94
|
+
return fallo(SALIDA.inesperado, "respuesta_inesperada", "The engine didn't return a key in the expected form.");
|
|
95
95
|
// El id y el secreto tienen que ser de la misma fila: si no, lo que se guarda no es lo que se confirma.
|
|
96
96
|
if (claveId !== `cla_${SUFIJO.exec(secreto)[1]}`)
|
|
97
|
-
return fallo(SALIDA.inesperado, "respuesta_inesperada", "
|
|
97
|
+
return fallo(SALIDA.inesperado, "respuesta_inesperada", "The engine returned a key that doesn't match its id; I didn't save it.");
|
|
98
98
|
const g = d.secretos.guardar(destino, claveId, secreto);
|
|
99
99
|
// Sin guardarla NO se confirma: una pendiente sin confirmar no autentica y vence sola a los 10 minutos.
|
|
100
100
|
if (g !== "guardado")
|
|
101
|
-
return fallo(g.codigo, "no_se_guardo", `
|
|
101
|
+
return fallo(g.codigo, "no_se_guardo", `I didn't save the key in ${destino}: ${g.error}. It was not activated.`);
|
|
102
102
|
const c = await d.configurar(d.api, p.clave, `${ruta}/${encodeURIComponent(claveId)}/confirmar`, { clave: secreto });
|
|
103
103
|
if (c.status === 200) {
|
|
104
104
|
await d.idempotencia.olvidar(OPERACION, alcance);
|
|
105
|
-
return { codigo: SALIDA.ok, json: { ok: true, clave_id: claveId, archivo: destino }, texto: `
|
|
105
|
+
return { codigo: SALIDA.ok, json: { ok: true, clave_id: claveId, archivo: destino }, texto: `Created read-only key ${claveId} and saved it in ${destino}.` };
|
|
106
106
|
}
|
|
107
107
|
// Obsoleta: otro intento con la misma clave de idempotencia le acuñó otro secreto a la misma fila.
|
|
108
108
|
// Una vuelta más trae el vigente y lo guarda encima (mismo id).
|
|
109
109
|
if (c.cuerpo.error !== "confirmacion_obsoleta")
|
|
110
110
|
return desenlace(c, () => { throw new Error("inalcanzable"); });
|
|
111
111
|
}
|
|
112
|
-
return fallo(SALIDA.inesperado, "confirmacion_obsoleta", "
|
|
112
|
+
return fallo(SALIDA.inesperado, "confirmacion_obsoleta", "Another attempt changed the key while I was confirming it; run the command again.");
|
|
113
113
|
}
|
|
114
114
|
catch (e) {
|
|
115
115
|
// Red caída a mitad, o el registro estricto que no pudo guardar: la clave de idempotencia (si llegó a
|
|
@@ -128,8 +128,8 @@ export async function clavesListar(d, flags) {
|
|
|
128
128
|
// El motor ya manda sólo id, nombre y fecha (nunca el hash); acá se arma la forma pública igual.
|
|
129
129
|
const claves = (await d.listarClaves(d.api, p.clave, p.proyecto.id)).map((c) => ({ id: c.id, nombre: c.nombre, creada_en: c.creadaEn }));
|
|
130
130
|
const texto = claves.length === 0
|
|
131
|
-
? "
|
|
132
|
-
: `
|
|
131
|
+
? "There are no active read-only keys."
|
|
132
|
+
: `Active read-only keys: ${claves.map((c) => `${c.id} (${c.nombre})`).join(", ")}.`;
|
|
133
133
|
return { codigo: SALIDA.ok, json: { ok: true, claves }, texto };
|
|
134
134
|
}
|
|
135
135
|
catch (e) {
|
|
@@ -138,7 +138,7 @@ export async function clavesListar(d, flags) {
|
|
|
138
138
|
}
|
|
139
139
|
export async function clavesRevocar(d, flags) {
|
|
140
140
|
if (flags.id === null || !ID_DE_CLAVE.test(flags.id)) {
|
|
141
|
-
return fallo(SALIDA.entradaInvalida, "id_invalido", "
|
|
141
|
+
return fallo(SALIDA.entradaInvalida, "id_invalido", "Say which key to revoke by its id: `supcoflow keys revoke --id cla_…` (`supcoflow keys list` gives it).");
|
|
142
142
|
}
|
|
143
143
|
const claveId = flags.id;
|
|
144
144
|
const p = await proyectoDelRepo(d, flags);
|
|
@@ -151,9 +151,9 @@ export async function clavesRevocar(d, flags) {
|
|
|
151
151
|
const r = await d.configurar(d.api, p.clave, ruta, { clave_id: claveId });
|
|
152
152
|
// Revocar dos veces deja lo mismo que una: para quien lo pide, ya está hecho.
|
|
153
153
|
if (r.status === 409 && r.cuerpo.error === "ya_revocada") {
|
|
154
|
-
return { codigo: SALIDA.ok, json: { ok: true, clave_id: claveId, ya_revocada: true }, texto: `
|
|
154
|
+
return { codigo: SALIDA.ok, json: { ok: true, clave_id: claveId, ya_revocada: true }, texto: `Key ${claveId} was already revoked.` };
|
|
155
155
|
}
|
|
156
|
-
return desenlace(r, () => ({ codigo: SALIDA.ok, json: { ok: true, clave_id: claveId }, texto: `
|
|
156
|
+
return desenlace(r, () => ({ codigo: SALIDA.ok, json: { ok: true, clave_id: claveId }, texto: `Revoked key ${claveId}; it no longer authenticates.` }));
|
|
157
157
|
}
|
|
158
158
|
catch (e) {
|
|
159
159
|
return resultadoDeFallo(e);
|
|
@@ -14,12 +14,12 @@ export function desenlace(r, ok) {
|
|
|
14
14
|
// Propio de esta función (hoy `conClave` ya lo filtra antes): sin esto, un 401 o un 5xx caerían en el 3 de
|
|
15
15
|
// «entrada inválida» y la persona (o el asistente) corregiría una entrada que estaba bien.
|
|
16
16
|
if (r.status === 401)
|
|
17
|
-
return fallo(SALIDA.faltaLogin, "clave_invalida", "
|
|
17
|
+
return fallo(SALIDA.faltaLogin, "clave_invalida", "The key is no longer valid: log in again.");
|
|
18
18
|
if (r.status >= 500 || typeof r.cuerpo !== "object" || r.cuerpo === null) {
|
|
19
|
-
return fallo(SALIDA.inesperado, "respuesta_inesperada", `
|
|
19
|
+
return fallo(SALIDA.inesperado, "respuesta_inesperada", `The engine answered something I don't understand (${r.status}); try again in a while.`);
|
|
20
20
|
}
|
|
21
21
|
const error = typeof r.cuerpo.error === "string" ? r.cuerpo.error : "rechazado";
|
|
22
|
-
const message = taparSecretos(typeof r.cuerpo.message === "string" ? r.cuerpo.message : `
|
|
22
|
+
const message = taparSecretos(typeof r.cuerpo.message === "string" ? r.cuerpo.message : `The engine rejected the request (${r.status}).`);
|
|
23
23
|
if (PIDEN_UNA_PERSONA.has(error))
|
|
24
24
|
return fallo(SALIDA.haceFaltaUnaPersona, error, message);
|
|
25
25
|
if (TRANSITORIOS.has(error))
|
|
@@ -27,7 +27,7 @@ export function desenlace(r, ok) {
|
|
|
27
27
|
return fallo(SALIDA.entradaInvalida, error, message);
|
|
28
28
|
}
|
|
29
29
|
export const rutaDe = (p, resto) => `/api/proyectos/${encodeURIComponent(p.id)}${resto}`;
|
|
30
|
-
export const dryRun = (haria) => ({ codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `
|
|
30
|
+
export const dryRun = (haria) => ({ codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `With --dry-run I do nothing; without it: ${haria}.` });
|
|
31
31
|
export async function comandoConfiguracion(d, flags) {
|
|
32
32
|
const p = await proyectoDelRepo(d, flags);
|
|
33
33
|
if (!p.ok)
|
|
@@ -41,18 +41,18 @@ export async function comandoConfiguracion(d, flags) {
|
|
|
41
41
|
// promete «ninguna IA» ni «toda IA». `acuerdo` puede ser null, un acuerdo o {ilegible: true}.
|
|
42
42
|
const ac = c.acuerdo;
|
|
43
43
|
const acuerdo = ac === null || ac === undefined
|
|
44
|
-
? "
|
|
44
|
+
? "not set (everything gets through, as today)"
|
|
45
45
|
: ac.ilegible === true
|
|
46
|
-
? "
|
|
47
|
-
: "
|
|
48
|
-
const alcance = ac === null || ac === undefined ? "" : "
|
|
46
|
+
? "set but unreadable (check it in the console)"
|
|
47
|
+
: "set";
|
|
48
|
+
const alcance = ac === null || ac === undefined ? "" : " It limits what is saved and what the AI that investigates reads; it does not cover what the chat's AI sees while it talks.";
|
|
49
49
|
return {
|
|
50
50
|
codigo: SALIDA.ok,
|
|
51
51
|
json: { ok: true, proyecto: p.proyecto.id, ...c },
|
|
52
52
|
texto: [
|
|
53
|
-
`${p.proyecto.id}: chat ${chat.habilitado ? "
|
|
54
|
-
`
|
|
55
|
-
`
|
|
53
|
+
`${p.proyecto.id}: chat ${chat.habilitado ? "on" : "off"}, ${sitiosLista.length} site(s), data agreement ${acuerdo}.${alcance}`,
|
|
54
|
+
`Problem policy: ${textoDePolitica(c.politica)}`,
|
|
55
|
+
`Brand: ${textoDeMarca(c.marca)}`,
|
|
56
56
|
].join("\n"),
|
|
57
57
|
};
|
|
58
58
|
}
|
|
@@ -66,10 +66,10 @@ export async function comandoConfiguracion(d, flags) {
|
|
|
66
66
|
function textoDePolitica(v) {
|
|
67
67
|
const modo = v?.modo;
|
|
68
68
|
if (modo === "relevar")
|
|
69
|
-
return "
|
|
69
|
+
return "investigate: every problem is investigated and an issue is created (this costs money).";
|
|
70
70
|
if (modo === "guardar")
|
|
71
|
-
return "
|
|
72
|
-
return "
|
|
71
|
+
return "save: the chat's problems are saved without investigating; from the console a person can still ask for the investigation (this costs money).";
|
|
72
|
+
return "the engine didn't report it.";
|
|
73
73
|
}
|
|
74
74
|
// Título, saludo y tema los escribe quien configura el chat y llegan tal cual: un ESC en el título movería el
|
|
75
75
|
// cursor o cambiaría colores en la terminal de quien corre `configuracion`. En texto se quitan los caracteres
|
|
@@ -79,21 +79,21 @@ const limpio = (t) => t.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
|
79
79
|
// muestran las claves tal cual vinieron: el motor decide cuáles hay, la terminal no las reinterpreta.
|
|
80
80
|
function textoDeMarca(v) {
|
|
81
81
|
if (v === undefined)
|
|
82
|
-
return "
|
|
82
|
+
return "the engine didn't report it.";
|
|
83
83
|
if (v === null)
|
|
84
|
-
return "
|
|
84
|
+
return "not set (the chat uses the default one).";
|
|
85
85
|
const m = v;
|
|
86
86
|
const partes = [];
|
|
87
87
|
if (typeof m.titulo === "string" && m.titulo !== "")
|
|
88
|
-
partes.push(`
|
|
88
|
+
partes.push(`title «${limpio(m.titulo)}»`);
|
|
89
89
|
if (typeof m.saludo === "string" && m.saludo !== "")
|
|
90
|
-
partes.push(`
|
|
90
|
+
partes.push(`greeting «${limpio(m.saludo)}»`);
|
|
91
91
|
if (typeof m.tema === "object" && m.tema !== null) {
|
|
92
92
|
const colores = Object.entries(m.tema).filter(([, x]) => typeof x === "string").map(([k, x]) => `${limpio(k)} ${limpio(x)}`);
|
|
93
93
|
if (colores.length > 0)
|
|
94
|
-
partes.push(`
|
|
94
|
+
partes.push(`theme ${colores.join(", ")}`);
|
|
95
95
|
}
|
|
96
|
-
partes.push(typeof m.logo_url === "string" && m.logo_url !== "" ? "
|
|
96
|
+
partes.push(typeof m.logo_url === "string" && m.logo_url !== "" ? "with logo" : "no logo");
|
|
97
97
|
return partes.join(", ") + ".";
|
|
98
98
|
}
|
|
99
99
|
export async function chatPrenderApagar(d, flags, habilitar) {
|
|
@@ -104,35 +104,35 @@ export async function chatPrenderApagar(d, flags, habilitar) {
|
|
|
104
104
|
if (flags.dryRun)
|
|
105
105
|
return dryRun(`POST ${ruta} {habilitado: ${habilitar}}`);
|
|
106
106
|
return desenlace(await d.configurar(d.api, p.clave, ruta, { habilitado: habilitar }), () => ({
|
|
107
|
-
codigo: SALIDA.ok, json: { ok: true, habilitado: habilitar }, texto: habilitar ? "Chat
|
|
107
|
+
codigo: SALIDA.ok, json: { ok: true, habilitado: habilitar }, texto: habilitar ? "Chat turned on." : "Chat turned off.",
|
|
108
108
|
}));
|
|
109
109
|
}
|
|
110
110
|
export async function sitios(d, flags, accion) {
|
|
111
|
-
if (flags.
|
|
112
|
-
return fallo(SALIDA.entradaInvalida, "falta_sitio", `
|
|
111
|
+
if (flags.site === null)
|
|
112
|
+
return fallo(SALIDA.entradaInvalida, "falta_sitio", `The site is missing: \`supcoflow sites ${accion === "agregar" ? "add" : "remove"} --site https://your-app.com\`.`);
|
|
113
113
|
const p = await proyectoDelRepo(d, flags);
|
|
114
114
|
if (!p.ok)
|
|
115
115
|
return p.resultado;
|
|
116
116
|
const ruta = rutaDe(p.proyecto, "/origenes");
|
|
117
117
|
if (flags.dryRun)
|
|
118
118
|
return dryRun(`POST ${ruta} {accion: ${accion}}`);
|
|
119
|
-
return desenlace(await d.configurar(d.api, p.clave, ruta, { accion, origen: flags.
|
|
120
|
-
codigo: SALIDA.ok, json: { ok: true, sitios: c.origenes }, texto: `
|
|
119
|
+
return desenlace(await d.configurar(d.api, p.clave, ruta, { accion, origen: flags.site }), (c) => ({
|
|
120
|
+
codigo: SALIDA.ok, json: { ok: true, sitios: c.origenes }, texto: `Sites: ${c.origenes.join(", ") || "none"}.`,
|
|
121
121
|
}));
|
|
122
122
|
}
|
|
123
123
|
export async function chatMarca(d, flags) {
|
|
124
124
|
const tema = {};
|
|
125
|
-
for (const t of flags.
|
|
125
|
+
for (const t of flags.theme) {
|
|
126
126
|
const i = t.indexOf("=");
|
|
127
127
|
if (i <= 0 || i === t.length - 1)
|
|
128
|
-
return fallo(SALIDA.entradaInvalida, "tema_invalido", `--
|
|
128
|
+
return fallo(SALIDA.entradaInvalida, "tema_invalido", `--theme goes as key=value (for example acento=#0a7); I got «${t}».`);
|
|
129
129
|
const clave = t.slice(0, i);
|
|
130
130
|
if (clave in tema)
|
|
131
|
-
return fallo(SALIDA.entradaInvalida, "tema_invalido", `--
|
|
131
|
+
return fallo(SALIDA.entradaInvalida, "tema_invalido", `--theme ${clave} can only be given once.`);
|
|
132
132
|
tema[clave] = t.slice(i + 1);
|
|
133
133
|
}
|
|
134
|
-
if (flags.
|
|
135
|
-
return fallo(SALIDA.entradaInvalida, "marca_vacia", "
|
|
134
|
+
if (flags.title === null && flags.greeting === null && Object.keys(tema).length === 0) {
|
|
135
|
+
return fallo(SALIDA.entradaInvalida, "marca_vacia", "Give at least one of --title, --greeting or --theme.");
|
|
136
136
|
}
|
|
137
137
|
const p = await proyectoDelRepo(d, flags);
|
|
138
138
|
if (!p.ok)
|
|
@@ -151,8 +151,8 @@ export async function chatMarca(d, flags) {
|
|
|
151
151
|
return resultadoDeFallo(e);
|
|
152
152
|
}
|
|
153
153
|
const cuerpo = {};
|
|
154
|
-
const titulo = flags.
|
|
155
|
-
const saludo = flags.
|
|
154
|
+
const titulo = flags.title ?? (typeof actual?.titulo === "string" ? actual.titulo : null);
|
|
155
|
+
const saludo = flags.greeting ?? (typeof actual?.saludo === "string" ? actual.saludo : null);
|
|
156
156
|
if (titulo !== null)
|
|
157
157
|
cuerpo.titulo = titulo;
|
|
158
158
|
if (saludo !== null)
|
|
@@ -164,24 +164,24 @@ export async function chatMarca(d, flags) {
|
|
|
164
164
|
const ruta = rutaDe(p.proyecto, "/chat/marca");
|
|
165
165
|
if (flags.dryRun)
|
|
166
166
|
return dryRun(`POST ${ruta}`);
|
|
167
|
-
return desenlace(await d.configurar(d.api, p.clave, ruta, cuerpo), (c) => ({ codigo: SALIDA.ok, json: { ok: true, marca: c.marca }, texto: "
|
|
167
|
+
return desenlace(await d.configurar(d.api, p.clave, ruta, cuerpo), (c) => ({ codigo: SALIDA.ok, json: { ok: true, marca: c.marca }, texto: "Brand saved." }));
|
|
168
168
|
}
|
|
169
169
|
const MIMES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
|
|
170
170
|
// El tope del motor es 200 KB (`MAX_LOGO_BYTES`); acá se corta antes para no mandar de más. El motor decide.
|
|
171
171
|
const TOPE_DEL_LOGO = 200_000;
|
|
172
172
|
export async function chatLogo(d, flags) {
|
|
173
|
-
if (flags.
|
|
174
|
-
return fallo(SALIDA.entradaInvalida, "falta_archivo", "
|
|
175
|
-
const mime = MIMES[path.extname(flags.
|
|
173
|
+
if (flags.file === null)
|
|
174
|
+
return fallo(SALIDA.entradaInvalida, "falta_archivo", "The logo is missing: `supcoflow chat logo --file logo.png`.");
|
|
175
|
+
const mime = MIMES[path.extname(flags.file).toLowerCase()];
|
|
176
176
|
if (mime === undefined)
|
|
177
|
-
return fallo(SALIDA.entradaInvalida, "logo_invalido", "
|
|
178
|
-
const leido = d.leerArchivoChico(flags.
|
|
177
|
+
return fallo(SALIDA.entradaInvalida, "logo_invalido", "The logo must be .png, .jpg or .webp.");
|
|
178
|
+
const leido = d.leerArchivoChico(flags.file, TOPE_DEL_LOGO);
|
|
179
179
|
if ("error" in leido)
|
|
180
|
-
return fallo(SALIDA.entradaInvalida, "logo_ilegible", `
|
|
180
|
+
return fallo(SALIDA.entradaInvalida, "logo_ilegible", `I can't read ${flags.file}: ${leido.error}.`);
|
|
181
181
|
const p = await proyectoDelRepo(d, flags);
|
|
182
182
|
if (!p.ok)
|
|
183
183
|
return p.resultado;
|
|
184
184
|
if (flags.dryRun)
|
|
185
185
|
return dryRun(`POST ${rutaDe(p.proyecto, "/chat/logo")} (${leido.bytes.length} bytes)`);
|
|
186
|
-
return desenlace(await d.subirLogo(d.api, p.clave, p.proyecto.id, { bytes: leido.bytes, mime, nombre: path.basename(flags.
|
|
186
|
+
return desenlace(await d.subirLogo(d.api, p.clave, p.proyecto.id, { bytes: leido.bytes, mime, nombre: path.basename(flags.file) }), (c) => ({ codigo: SALIDA.ok, json: { ok: true, logo_url: c.logo_url }, texto: "Logo uploaded." }));
|
|
187
187
|
}
|
|
@@ -1,52 +1,63 @@
|
|
|
1
|
+
import { PERMITIDOS_VIEJOS, PREFIJO_VIEJO_DE_ATRIBUTO } from "../alias.js";
|
|
1
2
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
3
|
import { claveVigente } from "./clave-vigente.js";
|
|
3
4
|
import { desenlace } from "./configurar.js";
|
|
4
5
|
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
5
|
-
|
|
6
|
+
// Los nombres de `--allow` son ingleses; a la API viaja el suyo (`Permitido`). Los castellanos de la 0.7.0 valen igual
|
|
7
|
+
// (`src/alias.ts`).
|
|
8
|
+
const DE_LA_API = { user_id: "usuario_id", user_role: "usuario_rol", page_and_browser: "pagina_y_navegador", screenshots: "capturas", elements: "elementos", email: "email" };
|
|
9
|
+
const BANDERAS = Object.keys(DE_LA_API);
|
|
10
|
+
const PREFIJO_DEL_ATRIBUTO = "attribute:";
|
|
6
11
|
// Los límites del contrato (`Permitido.atributos`: ≤ 8, 1–40 caracteres). Se cortan acá para que un
|
|
7
|
-
// --
|
|
12
|
+
// --allow imposible salga 3 sin pedido; el motor sigue siendo quien decide.
|
|
8
13
|
const MAX_ATRIBUTOS = 8;
|
|
9
14
|
const MAX_LARGO_DE_ATRIBUTO = 40;
|
|
10
15
|
const fallo = (codigo, error, message) => ({ codigo, json: { ok: false, error, message }, texto: message });
|
|
11
16
|
const rutaDe = (p, resto) => `/api/proyectos/${encodeURIComponent(p.id)}${resto}`;
|
|
12
|
-
const dryRun = (haria) => ({ codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `
|
|
17
|
+
const dryRun = (haria) => ({ codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `With --dry-run I do nothing; without it: ${haria}.` });
|
|
18
|
+
// Lleva un valor de `--allow` (nuevo o viejo) a su forma inglesa canónica.
|
|
19
|
+
function aInglesElPermitido(x) {
|
|
20
|
+
if (x.startsWith(PREFIJO_VIEJO_DE_ATRIBUTO))
|
|
21
|
+
return PREFIJO_DEL_ATRIBUTO + x.slice(PREFIJO_VIEJO_DE_ATRIBUTO.length);
|
|
22
|
+
return PERMITIDOS_VIEJOS[x] ?? x;
|
|
23
|
+
}
|
|
13
24
|
export function leerPermitir(crudo) {
|
|
14
|
-
const partes = crudo.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
25
|
+
const partes = crudo.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map(aInglesElPermitido);
|
|
15
26
|
const p = { usuario_id: false, usuario_rol: false, atributos: [], pagina_y_navegador: false, capturas: false, elementos: false, email: false };
|
|
16
|
-
if (partes.length === 1 && partes[0] === "
|
|
27
|
+
if (partes.length === 1 && partes[0] === "nothing")
|
|
17
28
|
return p;
|
|
18
|
-
if (partes.length === 0 || partes.includes("
|
|
19
|
-
return { error: "`
|
|
29
|
+
if (partes.length === 0 || partes.includes("nothing"))
|
|
30
|
+
return { error: "`nothing` goes alone; otherwise, name what does get through." };
|
|
20
31
|
const vistos = new Set();
|
|
21
32
|
for (const x of partes) {
|
|
22
33
|
if (vistos.has(x))
|
|
23
|
-
return { error: `«${x}»
|
|
34
|
+
return { error: `«${x}» is repeated.` };
|
|
24
35
|
vistos.add(x);
|
|
25
|
-
if (x.startsWith(
|
|
36
|
+
if (x.startsWith(PREFIJO_DEL_ATRIBUTO)) {
|
|
26
37
|
// Recortado: « plan» y «plan» son el mismo atributo para quien lo escribe, y el motor lo compara exacto.
|
|
27
|
-
const nombre = x.slice(
|
|
38
|
+
const nombre = x.slice(PREFIJO_DEL_ATRIBUTO.length).trim();
|
|
28
39
|
if (nombre.length === 0)
|
|
29
|
-
return { error: "`
|
|
40
|
+
return { error: "`attribute:` goes with the attribute's exact name, for example attribute:plan." };
|
|
30
41
|
if (nombre.length > MAX_LARGO_DE_ATRIBUTO)
|
|
31
|
-
return { error: `
|
|
42
|
+
return { error: `An attribute's name has up to ${MAX_LARGO_DE_ATRIBUTO} characters.` };
|
|
32
43
|
if (p.atributos.length >= MAX_ATRIBUTOS)
|
|
33
|
-
return { error: `
|
|
44
|
+
return { error: `Up to ${MAX_ATRIBUTOS} attributes.` };
|
|
34
45
|
p.atributos.push(nombre);
|
|
35
46
|
}
|
|
36
47
|
else if (BANDERAS.includes(x)) {
|
|
37
|
-
p[x] = true;
|
|
48
|
+
p[DE_LA_API[x]] = true;
|
|
38
49
|
}
|
|
39
50
|
else {
|
|
40
|
-
return { error: `
|
|
51
|
+
return { error: `I don't know «${x}». Valid: ${BANDERAS.join(", ")}, attribute:<name>, or nothing.` };
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
return p;
|
|
44
55
|
}
|
|
45
56
|
export async function datosFijar(d, flags) {
|
|
46
|
-
if (flags.
|
|
47
|
-
return fallo(SALIDA.entradaInvalida, "falta_permitir", "
|
|
57
|
+
if (flags.allow === null) {
|
|
58
|
+
return fallo(SALIDA.entradaInvalida, "falta_permitir", "Say which data gets through: `supcoflow data set --allow user_id,screenshots` (or `--allow nothing`).");
|
|
48
59
|
}
|
|
49
|
-
const permitido = leerPermitir(flags.
|
|
60
|
+
const permitido = leerPermitir(flags.allow);
|
|
50
61
|
if ("error" in permitido)
|
|
51
62
|
return fallo(SALIDA.entradaInvalida, "permitir_invalido", permitido.error);
|
|
52
63
|
const p = await proyectoDelRepo(d, flags);
|
|
@@ -69,15 +80,16 @@ export async function datosFijar(d, flags) {
|
|
|
69
80
|
return {
|
|
70
81
|
codigo: SALIDA.ok,
|
|
71
82
|
json: { ok: true, acuerdo: c.acuerdo, alcance: "guardado_y_relevamiento" },
|
|
72
|
-
texto: `
|
|
83
|
+
texto: `Data agreement set (version ${version}). It limits what is saved and what the AI that investigates reads; not what the chat's AI sees while it talks.`,
|
|
73
84
|
};
|
|
74
85
|
});
|
|
75
86
|
}
|
|
76
87
|
export async function politicaFijar(d, flags) {
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
// A la API viaja `relevar` / `guardar`; `investigate` / `save` son sus nombres ingleses.
|
|
89
|
+
const modo = flags.mode === "investigate" || flags.mode === "relevar" ? "relevar" : flags.mode === "save" || flags.mode === "guardar" ? "guardar" : null;
|
|
90
|
+
if (modo === null) {
|
|
91
|
+
return fallo(SALIDA.entradaInvalida, "modo_invalido", "--mode is `investigate` or `save`: `supcoflow policy set --mode save`.");
|
|
79
92
|
}
|
|
80
|
-
const modo = flags.modo;
|
|
81
93
|
const p = await proyectoDelRepo(d, flags);
|
|
82
94
|
if (!p.ok)
|
|
83
95
|
return p.resultado;
|
|
@@ -94,7 +106,7 @@ export async function politicaFijar(d, flags) {
|
|
|
94
106
|
return desenlace(r, (c) => ({
|
|
95
107
|
codigo: SALIDA.ok,
|
|
96
108
|
json: { ok: true, politica: c.politica },
|
|
97
|
-
texto: `
|
|
109
|
+
texto: `Problem policy set: ${modo === "relevar" ? "investigate" : "save"}.`,
|
|
98
110
|
}));
|
|
99
111
|
}
|
|
100
112
|
export async function comandoCatalogo(d) {
|