supcoflow 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +209 -180
- package/dist/alias.js +60 -0
- package/dist/api.js +41 -19
- package/dist/argumentos.js +92 -85
- package/dist/cli.js +40 -35
- package/dist/comandos/clave-vigente.js +2 -2
- package/dist/comandos/claves.js +25 -25
- package/dist/comandos/configurar.js +40 -40
- package/dist/comandos/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +161 -151
- package/dist/comandos/guia.js +148 -61
- package/dist/comandos/instalacion.js +71 -40
- package/dist/comandos/login.js +25 -24
- package/dist/comandos/logout.js +17 -16
- package/dist/comandos/probar.js +141 -62
- package/dist/comandos/proyecto-del-repo.js +1 -1
- package/dist/comandos/proyecto.js +16 -16
- package/dist/comandos/quien-soy.js +8 -7
- package/dist/comandos/repo-conectar.js +9 -9
- package/dist/comandos/widget.js +9 -5
- package/dist/diagnostico.js +48 -0
- package/dist/entorno.js +19 -18
- package/dist/flags-por-comando.js +25 -21
- package/dist/guardado/archivo.js +2 -2
- package/dist/guardado/index.js +12 -8
- package/dist/guardado/llavero.js +5 -5
- package/dist/guia.js +3 -1
- package/dist/idempotencia.js +6 -6
- package/dist/instalacion.js +50 -11
- package/dist/lectura.js +10 -0
- package/dist/pruebas-guardadas.js +16 -10
- package/dist/repo-local.js +6 -6
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
package/dist/comandos/probar.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
+
import { agruparPuntos, contarBuenos, lineaDeLoRevisado, nombreDelPunto, puntoDeSesion, puntoDelRepo, TOTAL_DE_INSTALACION } from "../diagnostico.js";
|
|
3
|
+
import { leerRepoLocal } from "../repo-local.js";
|
|
2
4
|
import { PLAZO_MINIMO_DE_SONDEO_MS, TRAMO_MAXIMO_MS } from "../sondeo.js";
|
|
3
5
|
import { fallo, rutaDe } from "./configurar.js";
|
|
4
6
|
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
@@ -6,6 +8,7 @@ import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
|
6
8
|
// gastar un pedido (ni guardar un intento que el motor va a rechazar); la regla de verdad es la del motor.
|
|
7
9
|
const MAX_MENSAJE = 20_000;
|
|
8
10
|
const ID_DE_PRUEBA = /^prb_[0-9a-f]{16}$/;
|
|
11
|
+
const TECHO = /^\d+(?:\.\d{1,4})?$/;
|
|
9
12
|
const INTERVALO_DE_SEGUIMIENTO_MS = 5_000;
|
|
10
13
|
const PLAZO_MAXIMO_DE_SONDEO_MS = 15_000;
|
|
11
14
|
// La tabla de la prueba (brief M7, «los estados»): qué código sale en cada estado. Lo que no está acá es
|
|
@@ -16,7 +19,18 @@ const PIDEN_UNA_PERSONA = new Set([
|
|
|
16
19
|
"issue_con_cierre_anomalo", "fallido", "cierre_sin_evidencia", "reclamado_sin_run", "arranque_incierto", "esperando_cupo", "sin_despacho",
|
|
17
20
|
]);
|
|
18
21
|
const usd = (n) => `US$ ${n.toFixed(2)}`;
|
|
19
|
-
const usdONull = (n) => (n === null ? "
|
|
22
|
+
const usdONull = (n) => (n === null ? "unmeasured" : usd(n));
|
|
23
|
+
// La política como se dice en inglés; a la API siempre viaja la suya (`relevar` / `guardar`).
|
|
24
|
+
const POLITICA_EN_INGLES = { relevar: "investigate", guardar: "save" };
|
|
25
|
+
const enInglesLaPolitica = (p) => (p === null ? "none" : (POLITICA_EN_INGLES[p] ?? p));
|
|
26
|
+
const aLaPoliticaDeLaApi = (p) => p === "investigate" || p === "relevar" ? "relevar" : p === "save" || p === "guardar" ? "guardar" : null;
|
|
27
|
+
// El comando exacto para retomar un pedido guardado: con lo que se autorizó, no con lo que haya ahora. Un pedido SIN
|
|
28
|
+
// política (lo guardó la 0.7.0, o se pidió con el alias viejo) sólo se retoma por el alias: el comando canónico exige
|
|
29
|
+
// `--expect-policy` (3 falta_politica) y, con una política, ya no es el mismo pedido (4 intento_pendiente). No se inventa
|
|
30
|
+
// una política para él: cambiaría su cuerpo y su clave.
|
|
31
|
+
const comandoDeReintento = (i) => i.politicaEsperada === null
|
|
32
|
+
? `supcoflow probar --autorizo-gasto --ceiling ${i.techoUsd}`
|
|
33
|
+
: `supcoflow test --authorize-spend --ceiling ${i.techoUsd} --expect-policy ${enInglesLaPolitica(i.politicaEsperada)}`;
|
|
20
34
|
function datosDelDiagnostico(g) {
|
|
21
35
|
return {
|
|
22
36
|
listo: g.listo,
|
|
@@ -29,7 +43,7 @@ function datosDelDiagnostico(g) {
|
|
|
29
43
|
prueba_abierta: g.pruebaAbierta,
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
|
-
const lineasDePuntos = (
|
|
46
|
+
const lineasDePuntos = (puntos, nombre = (p) => p) => puntos.map((p) => ` [${p.ok ? "ok" : "missing"}] ${nombre(p.punto)}: ${p.mensaje}`);
|
|
33
47
|
export async function diagnosticar(d, flags) {
|
|
34
48
|
const p = await proyectoDelRepo(d, flags);
|
|
35
49
|
if (!p.ok)
|
|
@@ -41,43 +55,99 @@ export async function diagnosticar(d, flags) {
|
|
|
41
55
|
catch (e) {
|
|
42
56
|
return resultadoDeFallo(e);
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
// Los dos puntos que sólo la terminal puede ver: la llamada de arriba autenticó (si no, no se llega acá) y el
|
|
59
|
+
// `origin` de esta carpeta es el repo del proyecto. Sin git o sin origin no se adivina: el punto sale no ok.
|
|
60
|
+
let local;
|
|
61
|
+
try {
|
|
62
|
+
local = await leerRepoLocal(d.git, d.cwd);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
local = { repo: null, raiz: null, motivo: "sin_git" };
|
|
48
66
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
67
|
+
const { instalacion, prueba } = agruparPuntos(g, [puntoDeSesion(), puntoDelRepo(local, p.proyecto.repo)]);
|
|
68
|
+
const buenos = contarBuenos(instalacion);
|
|
69
|
+
const listo = g.listo && instalacion.every((x) => x.ok || !["sign-in", "repo"].includes(x.punto));
|
|
70
|
+
const datos = {
|
|
71
|
+
proyecto: { id: p.proyecto.id, repo: p.proyecto.repo },
|
|
72
|
+
...datosDelDiagnostico(g),
|
|
73
|
+
listo,
|
|
74
|
+
puntos: [...instalacion, ...prueba],
|
|
75
|
+
instalacion: { ok: buenos, total: TOTAL_DE_INSTALACION, puntos: instalacion },
|
|
76
|
+
prueba: { listo: g.listo, puntos: prueba },
|
|
77
|
+
};
|
|
78
|
+
const resumen = `Monthly balance ${usdONull(g.saldoUsd)} of ${usdONull(g.topeUsd)}; a test authorizes up to ${usd(g.techoUsd)}. Policy: ${enInglesLaPolitica(g.politica)}.`;
|
|
79
|
+
const motivos = [
|
|
80
|
+
...prueba.filter((x) => !x.ok).map((x) => x.mensaje),
|
|
81
|
+
...(buenos < TOTAL_DE_INSTALACION ? [`the installation isn't complete (${buenos} of ${TOTAL_DE_INSTALACION})`] : []),
|
|
82
|
+
];
|
|
83
|
+
const listaParaProbar = listo ? "Ready for a test: yes." : `Ready for a test: no, because ${motivos.join("; ") || "the engine says it isn't ready"}`;
|
|
84
|
+
const cuerpo = [
|
|
85
|
+
lineaDeLoRevisado(buenos),
|
|
86
|
+
...lineasDePuntos(instalacion, nombreDelPunto),
|
|
87
|
+
listaParaProbar,
|
|
88
|
+
...lineasDePuntos(prueba),
|
|
89
|
+
resumen,
|
|
90
|
+
];
|
|
91
|
+
if (listo)
|
|
92
|
+
return { codigo: SALIDA.ok, json: { ok: true, ...datos }, texto: cuerpo.join("\n") };
|
|
93
|
+
const message = `Something needs fixing before testing ${p.proyecto.repo}.`;
|
|
94
|
+
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "no_listo", message, ...datos }, texto: [message, ...cuerpo].join("\n") };
|
|
51
95
|
}
|
|
52
96
|
export async function probar(d, flags) {
|
|
53
97
|
// El reloj del tramo arranca con el comando: los 90 s son del comando entero, no sólo de los sondeos.
|
|
54
98
|
const inicio = d.ahora();
|
|
55
99
|
// Todo lo que se puede rechazar sin mirar nada, antes de tocar el motor o lo guardado.
|
|
56
|
-
if (flags.wait && (flags.
|
|
57
|
-
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "`
|
|
100
|
+
if (flags.wait && (flags.authorizeSpend || flags.message !== null || flags.ceiling !== null || flags.expectPolicy !== null)) {
|
|
101
|
+
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "`test --wait` only follows a test: no --authorize-spend, --ceiling, --expect-policy or --message. First `supcoflow test --authorize-spend --ceiling <usd> --expect-policy <policy>`, then `supcoflow test --wait`.");
|
|
102
|
+
}
|
|
103
|
+
if (!flags.wait && flags.testId !== null) {
|
|
104
|
+
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "--test-id goes with --wait: `supcoflow test --wait --test-id prb_…`.");
|
|
58
105
|
}
|
|
59
|
-
if (!flags.wait && flags.
|
|
60
|
-
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "--
|
|
106
|
+
if (!flags.authorizeSpend && !flags.wait && (flags.ceiling !== null || flags.expectPolicy !== null)) {
|
|
107
|
+
return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "--ceiling and --expect-policy go with --authorize-spend: they say what you authorize.");
|
|
61
108
|
}
|
|
62
|
-
if (flags.
|
|
63
|
-
return fallo(SALIDA.entradaInvalida, "prueba_invalida", "--
|
|
109
|
+
if (flags.testId !== null && !ID_DE_PRUEBA.test(flags.testId)) {
|
|
110
|
+
return fallo(SALIDA.entradaInvalida, "prueba_invalida", "--test-id is the id `supcoflow test` printed (prb_ and 16 characters).");
|
|
64
111
|
}
|
|
65
112
|
let mensaje = null;
|
|
66
|
-
if (flags.
|
|
67
|
-
mensaje = flags.
|
|
113
|
+
if (flags.message !== null) {
|
|
114
|
+
mensaje = flags.message.trim();
|
|
68
115
|
if (mensaje.length === 0 || mensaje.length > MAX_MENSAJE) {
|
|
69
|
-
return fallo(SALIDA.entradaInvalida, "mensaje_invalido", `--
|
|
116
|
+
return fallo(SALIDA.entradaInvalida, "mensaje_invalido", `--message goes with text, up to ${MAX_MENSAJE} characters.`);
|
|
70
117
|
}
|
|
71
118
|
}
|
|
119
|
+
// Lo que la persona autorizó: el techo y la política que vio. Sin eso no hay pedido que valga (R5).
|
|
120
|
+
let autorizado = null;
|
|
121
|
+
if (flags.authorizeSpend) {
|
|
122
|
+
if (flags.ceiling === null) {
|
|
123
|
+
const comoSeEscribe = "`supcoflow test --authorize-spend --ceiling <usd> --expect-policy investigate|save`";
|
|
124
|
+
return fallo(SALIDA.entradaInvalida, "falta_techo", flags.viejo === true
|
|
125
|
+
? `\`probar --autorizo-gasto\` now needs --ceiling <usd>, the number you were shown: an alias can't widen what you authorized. Use ${comoSeEscribe}. (The old behaviour is in supcoflow 0.7.0.)`
|
|
126
|
+
: `--authorize-spend needs --ceiling <usd>, the number you were shown: ${comoSeEscribe}.`);
|
|
127
|
+
}
|
|
128
|
+
if (!TECHO.test(flags.ceiling) || !(Number(flags.ceiling) > 0)) {
|
|
129
|
+
return fallo(SALIDA.entradaInvalida, "techo_invalido", "--ceiling is a positive number of US dollars, for example 2 or 2.50.");
|
|
130
|
+
}
|
|
131
|
+
let politicaEsperada = null;
|
|
132
|
+
if (flags.expectPolicy !== null) {
|
|
133
|
+
politicaEsperada = aLaPoliticaDeLaApi(flags.expectPolicy);
|
|
134
|
+
if (politicaEsperada === null)
|
|
135
|
+
return fallo(SALIDA.entradaInvalida, "politica_invalida", "--expect-policy is `investigate` or `save`.");
|
|
136
|
+
}
|
|
137
|
+
else if (flags.viejo !== true) {
|
|
138
|
+
return fallo(SALIDA.entradaInvalida, "falta_politica", "--authorize-spend needs --expect-policy investigate|save: the policy you were offered, so a yes to a test that won't investigate can't turn into a paid one.");
|
|
139
|
+
}
|
|
140
|
+
autorizado = { techoUsd: Number(flags.ceiling), politicaEsperada };
|
|
141
|
+
}
|
|
72
142
|
const p = await proyectoDelRepo(d, flags);
|
|
73
143
|
if (!p.ok)
|
|
74
144
|
return p.resultado;
|
|
75
145
|
const proyecto = p.proyecto;
|
|
76
146
|
const clave = p.clave;
|
|
77
147
|
if (flags.wait)
|
|
78
|
-
return seguir(d, clave, proyecto.id, flags.
|
|
148
|
+
return seguir(d, clave, proyecto.id, flags.testId, inicio);
|
|
79
149
|
// ── Sin el flag, o con --dry-run: sólo el diagnóstico (gratis). Nada que escriba, ni acá ni en el motor.
|
|
80
|
-
if (
|
|
150
|
+
if (autorizado === null || flags.dryRun) {
|
|
81
151
|
let g;
|
|
82
152
|
try {
|
|
83
153
|
g = await d.leerDiagnostico(d.api, clave, proyecto.id);
|
|
@@ -87,8 +157,8 @@ export async function probar(d, flags) {
|
|
|
87
157
|
}
|
|
88
158
|
const datos = { proyecto: { id: proyecto.id, repo: proyecto.repo }, ...datosDelDiagnostico(g) };
|
|
89
159
|
const gasto = g.politica === "guardar"
|
|
90
|
-
? `
|
|
91
|
-
: `
|
|
160
|
+
? `With the \`save\` policy the test goes as far as the saved case and spends nothing; the ceiling it authorizes is still ${usd(g.techoUsd)}.`
|
|
161
|
+
: `A test runs an investigation that spends up to ${usd(g.techoUsd)} (monthly balance ${usdONull(g.saldoUsd)} of ${usdONull(g.topeUsd)}).`;
|
|
92
162
|
if (flags.dryRun) {
|
|
93
163
|
// Lo guardado se mira sólo para decir qué haría; si no se puede leer, el dry-run igual contesta.
|
|
94
164
|
let pendiente = null;
|
|
@@ -99,37 +169,39 @@ export async function probar(d, flags) {
|
|
|
99
169
|
pendiente = null;
|
|
100
170
|
}
|
|
101
171
|
const ruta = rutaDe(proyecto, "/pruebas");
|
|
172
|
+
const cuerpo = autorizado === null ? `techo_usd: ${g.techoUsd}` : `techo_usd: ${autorizado.techoUsd}${autorizado.politicaEsperada !== null ? `, politica_esperada: ${autorizado.politicaEsperada}` : ""}`;
|
|
102
173
|
const haria = pendiente
|
|
103
|
-
? `
|
|
104
|
-
: `POST ${ruta} {
|
|
174
|
+
? `would resend the saved request that was never confirmed (POST ${ruta}, the same Idempotency-Key)`
|
|
175
|
+
: `would send POST ${ruta} {${cuerpo}${mensaje !== null ? ", mensaje" : ""}} with a new Idempotency-Key`;
|
|
105
176
|
return {
|
|
106
177
|
codigo: SALIDA.ok,
|
|
107
|
-
json: { ok: true, dry_run: true, autorizado: flags.
|
|
108
|
-
texto: `
|
|
178
|
+
json: { ok: true, dry_run: true, autorizado: flags.authorizeSpend, intento_pendiente: pendiente, haria, ...datos },
|
|
179
|
+
texto: `With --dry-run I do nothing. ${flags.authorizeSpend ? "Without --dry-run, I" : "With --authorize-spend and without --dry-run, I"}${haria.replace(/^would/, "")} (project ${proyecto.repo}, policy ${enInglesLaPolitica(g.politica)}). ${gasto}`,
|
|
109
180
|
};
|
|
110
181
|
}
|
|
111
|
-
const
|
|
182
|
+
const lanzar = `supcoflow test --authorize-spend --ceiling ${g.techoUsd} --expect-policy ${enInglesLaPolitica(g.politica)}`;
|
|
183
|
+
const message = `${gasto} To launch it: \`${lanzar}\`. \`supcoflow diagnose\` first says whether everything is ready.`;
|
|
112
184
|
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "falta_autorizacion", message, ...datos }, texto: message };
|
|
113
185
|
}
|
|
114
|
-
// ── Con --
|
|
186
|
+
// ── Con --authorize-spend: el ciclo de la clave.
|
|
115
187
|
let intento;
|
|
116
188
|
try {
|
|
117
189
|
const guardado = await d.pruebas.leer(proyecto.id);
|
|
118
190
|
if (guardado.intento !== null) {
|
|
119
|
-
|
|
120
|
-
|
|
191
|
+
const previo = guardado.intento;
|
|
192
|
+
const igual = Math.abs(previo.techoUsd - autorizado.techoUsd) < 1e-9 && previo.politicaEsperada === autorizado.politicaEsperada;
|
|
193
|
+
if (mensaje !== null && mensaje !== previo.mensaje) {
|
|
194
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente", `There's a test request waiting for confirmation; repeat \`${comandoDeReintento(previo)}\` without --message to recover it.`);
|
|
195
|
+
}
|
|
196
|
+
if (!igual) {
|
|
197
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente", `There's a test request waiting for confirmation, authorized with a ceiling of ${usd(previo.techoUsd)}; repeat it with exactly that to recover it: \`${comandoDeReintento(previo)}\`.`);
|
|
121
198
|
}
|
|
122
|
-
intento =
|
|
199
|
+
intento = previo;
|
|
123
200
|
}
|
|
124
201
|
else {
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return enCurso(g.pruebaAbierta);
|
|
129
|
-
// No frena (el motor decide lo que acepta), pero quien autorizó tiene que ver lo que falta.
|
|
130
|
-
for (const punto of g.puntos.filter((x) => !x.ok))
|
|
131
|
-
d.avisar(`Aviso (${punto.punto}): ${punto.mensaje}`);
|
|
132
|
-
intento = { clave: d.generar(), mensaje, techoUsd: g.techoUsd };
|
|
202
|
+
// Lo que se manda es lo que la persona vio (R5): no se relee el diagnóstico para elegirlo. El motor compara
|
|
203
|
+
// el techo y la política con los de ahora y rechaza si cambiaron.
|
|
204
|
+
intento = { clave: d.generar(), mensaje, techoUsd: autorizado.techoUsd, politicaEsperada: autorizado.politicaEsperada };
|
|
133
205
|
await d.pruebas.abrir(proyecto.id, intento);
|
|
134
206
|
}
|
|
135
207
|
}
|
|
@@ -142,17 +214,17 @@ export async function probar(d, flags) {
|
|
|
142
214
|
await d.pruebas.confirmar(proyecto.id, r.pruebaId);
|
|
143
215
|
}
|
|
144
216
|
catch {
|
|
145
|
-
const message = `
|
|
217
|
+
const message = `The engine accepted test ${r.pruebaId}, but I couldn't save it on this machine. Repeat \`${comandoDeReintento(intento)}\` (it recovers the same one) or follow it with \`supcoflow test --wait --test-id ${r.pruebaId}\`.`;
|
|
146
218
|
return { codigo: SALIDA.inesperado, json: { ok: false, error: "prueba_no_guardada", message, prueba_id: r.pruebaId, intento_pendiente: true }, texto: message };
|
|
147
219
|
}
|
|
148
220
|
return {
|
|
149
221
|
codigo: SALIDA.esperandoAUnaPersona,
|
|
150
222
|
json: { ok: false, esperando: "relevamiento", prueba_id: r.pruebaId, reporte_id: r.reporteId, estado: r.estado },
|
|
151
|
-
texto: `
|
|
223
|
+
texto: `Test ${r.pruebaId} requested (${r.estado}). Follow it with \`supcoflow test --wait\`.`,
|
|
152
224
|
};
|
|
153
225
|
}
|
|
154
226
|
if (r.tipo === "ambigua") {
|
|
155
|
-
const message = `${r.message}
|
|
227
|
+
const message = `${r.message} The request was saved: repeat \`${comandoDeReintento(intento)}\` and I'll pick up the same test, without asking for another.`;
|
|
156
228
|
return { codigo: SALIDA.inesperado, json: { ok: false, error: r.error, message, intento_pendiente: true }, texto: message };
|
|
157
229
|
}
|
|
158
230
|
// Un rechazo definitivo: el motor no creó nada con esta clave, así que el intento se cierra.
|
|
@@ -160,22 +232,29 @@ export async function probar(d, flags) {
|
|
|
160
232
|
await d.pruebas.descartar(proyecto.id);
|
|
161
233
|
}
|
|
162
234
|
catch {
|
|
163
|
-
d.avisar("
|
|
235
|
+
d.avisar("I couldn't close the saved request; the next `test --authorize-spend` will resend it.");
|
|
164
236
|
}
|
|
165
|
-
return rechazo(r, d.avisar);
|
|
237
|
+
return rechazo(r, intento, d.avisar);
|
|
166
238
|
}
|
|
167
239
|
function enCurso(pruebaId) {
|
|
168
|
-
const message = `
|
|
240
|
+
const message = `This project already has a test with no outcome (${pruebaId}). Follow it with \`supcoflow test --wait --test-id ${pruebaId}\`.`;
|
|
169
241
|
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "prueba_en_curso", message, prueba_id: pruebaId }, texto: message };
|
|
170
242
|
}
|
|
171
|
-
function rechazo(r, avisar) {
|
|
243
|
+
function rechazo(r, intento, avisar) {
|
|
172
244
|
const c = r.cuerpo;
|
|
173
245
|
switch (r.error) {
|
|
174
246
|
case "prueba_en_curso":
|
|
175
247
|
return typeof c.prueba_id === "string" ? enCurso(c.prueba_id) : fallo(SALIDA.haceFaltaUnaPersona, r.error, r.message);
|
|
176
248
|
case "techo_distinto": {
|
|
177
|
-
|
|
178
|
-
|
|
249
|
+
// Sin reintentar: lo que se autorizó ya no es lo que hay, y decidir de nuevo es de la persona (con el número nuevo).
|
|
250
|
+
const nuevo = typeof c.techo_usd === "number" ? c.techo_usd : null;
|
|
251
|
+
const message = `The ceiling for a test changed${nuevo !== null ? `: it's now ${usd(nuevo)}` : ""}, and you authorized ${usd(intento.techoUsd)}. Nothing was started. Ask again with the new number.`;
|
|
252
|
+
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: r.error, message, techo_usd: nuevo, autorizado_usd: intento.techoUsd }, texto: message };
|
|
253
|
+
}
|
|
254
|
+
case "politica_distinta": {
|
|
255
|
+
const ahora = typeof c.politica === "string" ? c.politica : typeof c.politica_actual === "string" ? c.politica_actual : null;
|
|
256
|
+
const message = `The project's policy changed${ahora !== null ? `: it's now \`${enInglesLaPolitica(ahora)}\`` : ""}, and you authorized a test under \`${enInglesLaPolitica(intento.politicaEsperada)}\`. Nothing was started. Ask again with the current policy.`;
|
|
257
|
+
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: r.error, message, politica: ahora, politica_esperada: intento.politicaEsperada }, texto: message };
|
|
179
258
|
}
|
|
180
259
|
case "tope_mensual":
|
|
181
260
|
return {
|
|
@@ -184,13 +263,13 @@ function rechazo(r, avisar) {
|
|
|
184
263
|
texto: r.message,
|
|
185
264
|
};
|
|
186
265
|
case "requiere_api_key": {
|
|
187
|
-
const message = `
|
|
266
|
+
const message = `The test is requested with your personal API key (the one \`supcoflow login\` leaves), not with a read-only key or the console session. ${r.message}`;
|
|
188
267
|
return fallo(SALIDA.haceFaltaUnaPersona, r.error, message);
|
|
189
268
|
}
|
|
190
269
|
case "idempotency_key_reusada":
|
|
191
270
|
// No debería pasar: siempre se reenvía lo guardado con su clave. Si pasa, la clave ya no sirve.
|
|
192
|
-
avisar("
|
|
193
|
-
return fallo(SALIDA.inesperado, r.error, `${r.message}
|
|
271
|
+
avisar("The engine says this request's key was already used with other content; I discarded it.");
|
|
272
|
+
return fallo(SALIDA.inesperado, r.error, `${r.message} Run \`${comandoDeReintento(intento)}\` again to ask for a new test.`);
|
|
194
273
|
}
|
|
195
274
|
if (r.status === 401)
|
|
196
275
|
return fallo(SALIDA.faltaLogin, "clave_invalida", r.message);
|
|
@@ -201,14 +280,14 @@ function rechazo(r, avisar) {
|
|
|
201
280
|
function textoDelCobro(c) {
|
|
202
281
|
// Pendiente y perdido nunca se muestran como cero: el monto todavía no se sabe, la cota es el techo.
|
|
203
282
|
if (c.estado === "leido" && c.usd !== null)
|
|
204
|
-
return `
|
|
283
|
+
return `Cost: ${usd(c.usd)}.`;
|
|
205
284
|
if (c.estado === "pendiente")
|
|
206
|
-
return `
|
|
285
|
+
return `Charge pending: it's read a few minutes after the run closes (up to ${usd(c.techo_usd)}).`;
|
|
207
286
|
if (c.estado === "perdido")
|
|
208
|
-
return `
|
|
287
|
+
return `Charge lost: the spend couldn't be read (up to ${usd(c.techo_usd)}).`;
|
|
209
288
|
if (c.estado === null)
|
|
210
289
|
return null;
|
|
211
|
-
return `
|
|
290
|
+
return `Charge: ${c.estado} (up to ${usd(c.techo_usd)}).`;
|
|
212
291
|
}
|
|
213
292
|
function resultadoDelSeguimiento(s) {
|
|
214
293
|
const datos = {
|
|
@@ -216,24 +295,24 @@ function resultadoDelSeguimiento(s) {
|
|
|
216
295
|
reporte: s.reporte, run: s.run, issue: s.issue, cobro: s.cobro,
|
|
217
296
|
};
|
|
218
297
|
const lineas = [
|
|
219
|
-
`
|
|
298
|
+
`Test ${s.pruebaId}: ${s.estado}.${s.mensaje ? ` ${s.mensaje}` : ""}`,
|
|
220
299
|
s.issue ? `Issue #${s.issue.numero}: ${s.issue.url}` : null,
|
|
221
|
-
s.run?.motivo ? `
|
|
300
|
+
s.run?.motivo ? `Run reason: ${s.run.motivo}` : null,
|
|
222
301
|
textoDelCobro(s.cobro),
|
|
223
302
|
];
|
|
224
303
|
if (ABIERTOS.has(s.estado)) {
|
|
225
|
-
lineas.push("
|
|
304
|
+
lineas.push("Run `supcoflow test --wait` again.");
|
|
226
305
|
return { codigo: SALIDA.esperandoAUnaPersona, json: { ok: false, esperando: "relevamiento", ...datos }, texto: lineas.filter(Boolean).join("\n") };
|
|
227
306
|
}
|
|
228
307
|
if (CON_DESENLACE_OK.has(s.estado))
|
|
229
308
|
return { codigo: SALIDA.ok, json: { ok: true, ...datos }, texto: lineas.filter(Boolean).join("\n") };
|
|
230
309
|
if (PIDEN_UNA_PERSONA.has(s.estado)) {
|
|
231
310
|
if (s.reporte)
|
|
232
|
-
lineas.push(`
|
|
311
|
+
lineas.push(`The case in the console: ${s.reporte.url}`);
|
|
233
312
|
const texto = lineas.filter(Boolean).join("\n");
|
|
234
313
|
return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: s.estado, message: s.mensaje ?? texto, ...datos }, texto };
|
|
235
314
|
}
|
|
236
|
-
const message = `
|
|
315
|
+
const message = `The engine says the test is in «${s.estado}», a state this version of the terminal doesn't know. Update supcoflow or look at it in the console.`;
|
|
237
316
|
return { codigo: SALIDA.inesperado, json: { ok: false, error: "estado_desconocido", message, ...datos }, texto: message };
|
|
238
317
|
}
|
|
239
318
|
async function seguir(d, clave, proyectoId, elegida, inicio) {
|
|
@@ -242,17 +321,17 @@ async function seguir(d, clave, proyectoId, elegida, inicio) {
|
|
|
242
321
|
try {
|
|
243
322
|
const g = await d.pruebas.leer(proyectoId);
|
|
244
323
|
// Un pedido sin confirmar gana sobre la última: seguir la anterior y salir 0 con SU issue se leería
|
|
245
|
-
// como el desenlace de la prueba recién pedida. Con `--
|
|
324
|
+
// como el desenlace de la prueba recién pedida. Con `--test-id` explícito no se llega acá.
|
|
246
325
|
pruebaId = g.ultima;
|
|
247
326
|
if (g.intento !== null) {
|
|
248
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente",
|
|
327
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente", `There's a test request waiting for confirmation; repeat \`${comandoDeReintento(g.intento)}\` without --message to recover it, and then \`--wait\`.`);
|
|
249
328
|
}
|
|
250
329
|
}
|
|
251
330
|
catch (e) {
|
|
252
331
|
return resultadoDeFallo(e);
|
|
253
332
|
}
|
|
254
333
|
if (pruebaId === null) {
|
|
255
|
-
return fallo(SALIDA.entradaInvalida, "sin_prueba", "
|
|
334
|
+
return fallo(SALIDA.entradaInvalida, "sin_prueba", "There's no saved test on this machine for this project: request one with `supcoflow test --authorize-spend --ceiling <usd> --expect-policy <policy>`, or pass `--test-id prb_…`.");
|
|
256
335
|
}
|
|
257
336
|
}
|
|
258
337
|
// Se aparta lo que puede tardar un sondeo forzado (el primero siempre va, aunque quede poco): así el
|
|
@@ -15,7 +15,7 @@ export async function proyectoDelRepo(d, flags) {
|
|
|
15
15
|
try {
|
|
16
16
|
const proyecto = await d.buscarProyecto(d.api, leida.vigente.clave, repo);
|
|
17
17
|
if (proyecto === null) {
|
|
18
|
-
const message = `
|
|
18
|
+
const message = `The repo ${repo} doesn't have a project yet. Create the project: \`supcoflow project create\`.`;
|
|
19
19
|
return { ok: false, resultado: { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "sin_proyecto", message, repo }, texto: message } };
|
|
20
20
|
}
|
|
21
21
|
return { ok: true, clave: leida.vigente.clave, proyecto };
|
|
@@ -52,7 +52,7 @@ function mismoArchivo(crudo, p) {
|
|
|
52
52
|
}
|
|
53
53
|
function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
54
54
|
const nuevo = { id: p.id, slug: p.slug, repo: p.repo };
|
|
55
|
-
const queda = reusado ? `
|
|
55
|
+
const queda = reusado ? `Project ${p.id} already existed and I reused it` : `I created project ${p.id}`;
|
|
56
56
|
let archivo = null;
|
|
57
57
|
if (raiz !== null) {
|
|
58
58
|
archivo = path.join(raiz, ".supcoflow", "proyecto.json");
|
|
@@ -63,8 +63,8 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
63
63
|
// decida. Su contenido NUNCA se copia a la salida (puede traer cualquier cosa, una clave
|
|
64
64
|
// incluida, y `emitir` no sanea): sólo el id, si tiene forma de id.
|
|
65
65
|
const idActual = idDelArchivo(actual);
|
|
66
|
-
const message = `
|
|
67
|
-
+ `${queda}
|
|
66
|
+
const message = `There is already a \`.supcoflow/proyecto.json\` for another project (${idActual ?? "one I don't recognize"}); I didn't touch it. `
|
|
67
|
+
+ `${queda} anyway: if that's the one you want, delete the file and run \`supcoflow project create\` again.`;
|
|
68
68
|
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_distinto", message, { archivo, id_actual: idActual, nuevo, reusado });
|
|
69
69
|
}
|
|
70
70
|
if (actual === null) {
|
|
@@ -72,7 +72,7 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
72
72
|
d.archivos.escribir(archivo, contenidoDelArchivo(p));
|
|
73
73
|
}
|
|
74
74
|
catch {
|
|
75
|
-
const message = `${queda}
|
|
75
|
+
const message = `${queda}, but I couldn't write ${archivo}. Run \`supcoflow project create\` again.`;
|
|
76
76
|
return fallo(SALIDA.inesperado, "archivo_no_escrito", message, { archivo, nuevo, reusado });
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -80,7 +80,7 @@ function resultadoDelProyecto(p, reusado, raiz, d) {
|
|
|
80
80
|
return {
|
|
81
81
|
codigo: SALIDA.ok,
|
|
82
82
|
json: { ok: true, proyecto: { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama }, reusado, archivo },
|
|
83
|
-
texto: `${queda}: ${p.repo},
|
|
83
|
+
texto: `${queda}: ${p.repo}, branch ${p.rama}.${archivo !== null ? ` Recorded in ${archivo}.` : ""}`,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
export async function proyectoCrear(d, flags) {
|
|
@@ -116,18 +116,18 @@ export async function proyectoCrear(d, flags) {
|
|
|
116
116
|
if (flags.install !== null || flags.test !== null) {
|
|
117
117
|
// Sin copiar los valores: son texto del usuario y el aviso sólo dice qué pasó con ellos. Callarlo
|
|
118
118
|
// haría creer que el proyecto quedó con los comandos pedidos.
|
|
119
|
-
d.avisar("
|
|
119
|
+
d.avisar("The project already exists: I ignored --install/--test. To change its commands, it has to be configured.");
|
|
120
120
|
}
|
|
121
121
|
return resultadoDelProyecto(existente, true, raiz, d);
|
|
122
122
|
}
|
|
123
123
|
if (conexion === null || conexion.estado !== "conectado") {
|
|
124
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", "
|
|
124
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", "Connect the repo first: `supcoflow repo connect`.", {
|
|
125
125
|
repo, ...(conexion !== null ? { estado: conexion.estado } : {}),
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
if (flags.dryRun) {
|
|
129
129
|
const haria = `POST /api/conexiones/${conexion.id}/proyecto`;
|
|
130
|
-
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, repo, haria }, texto: `
|
|
130
|
+
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, repo, haria }, texto: `With --dry-run I do nothing; without it: create the project for ${repo} (${haria}).` };
|
|
131
131
|
}
|
|
132
132
|
const cuerpo = {};
|
|
133
133
|
if (flags.install !== null)
|
|
@@ -153,27 +153,27 @@ export async function proyectoCrear(d, flags) {
|
|
|
153
153
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
154
154
|
const install = flags.install ?? "…";
|
|
155
155
|
const test = flags.test ?? "…";
|
|
156
|
-
const message = `${alta.message || "
|
|
156
|
+
const message = `${alta.message || "The project is missing commands."} Pass them yourself: \`supcoflow project create --install ${comillas(install)} --test ${comillas(test)}\`.`;
|
|
157
157
|
return fallo(SALIDA.entradaInvalida, "comandos_faltantes", message, { faltantes: alta.faltantes });
|
|
158
158
|
}
|
|
159
159
|
case "no_conectada":
|
|
160
160
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
161
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", alta.message || "
|
|
161
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "repo_sin_conectar", alta.message || "Connect the repo first: `supcoflow repo connect`.", { repo });
|
|
162
162
|
case "en_curso":
|
|
163
163
|
// Se conserva: el pedido en curso puede ser el nuestro y el reintento tiene que poder retomarlo.
|
|
164
|
-
return fallo(SALIDA.inesperado, "en_curso", "
|
|
164
|
+
return fallo(SALIDA.inesperado, "en_curso", "Another request with this key is already in progress; try again in a minute.");
|
|
165
165
|
case "clave_reusada":
|
|
166
166
|
// No debería pasar (la clave es por repo y un reintento manda lo mismo); olvidarla deja reintentar limpio.
|
|
167
167
|
await d.idempotencia.olvidar(OPERACION, repo);
|
|
168
|
-
return fallo(SALIDA.inesperado, "clave_reusada", "
|
|
168
|
+
return fallo(SALIDA.inesperado, "clave_reusada", "The engine rejected the saved idempotency key; I discarded it, try again.");
|
|
169
169
|
case "rechazo": {
|
|
170
170
|
// La clave se conserva: tras un rechazo el motor ya la soltó, y el próximo intento la reusa sin
|
|
171
171
|
// chocar. Lo que cambia es el código: no todo rechazo es culpa de lo que se escribió.
|
|
172
|
-
const message = alta.message || "
|
|
172
|
+
const message = alta.message || "The engine rejected the request.";
|
|
173
173
|
if (RECHAZOS_DE_UNA_PERSONA.has(alta.error)) {
|
|
174
174
|
// Lo arregla alguien con permisos en GitHub (instalar la App, sumarle el repo): reintentar o
|
|
175
175
|
// cambiar flags no sirve, y un 3 haría que un asistente siguiera probando entradas.
|
|
176
|
-
return fallo(SALIDA.haceFaltaUnaPersona, alta.error, `${message}
|
|
176
|
+
return fallo(SALIDA.haceFaltaUnaPersona, alta.error, `${message} Check the App's installation on GitHub and connect again with \`supcoflow repo connect\`.`);
|
|
177
177
|
}
|
|
178
178
|
// Dos altas al mismo tiempo, o un proyecto borrado en el medio: el mismo pedido otra vez anda.
|
|
179
179
|
if (RECHAZOS_TRANSITORIOS.has(alta.error))
|
|
@@ -197,14 +197,14 @@ export async function proyectoVer(d, flags) {
|
|
|
197
197
|
try {
|
|
198
198
|
const encontrado = await d.buscarProyecto(d.api, clave, repo);
|
|
199
199
|
if (encontrado === null) {
|
|
200
|
-
return { codigo: SALIDA.ok, json: { ok: true, repo, proyecto: null }, texto: "
|
|
200
|
+
return { codigo: SALIDA.ok, json: { ok: true, repo, proyecto: null }, texto: "This repo doesn't have a project yet: `supcoflow project create`." };
|
|
201
201
|
}
|
|
202
202
|
const { proyecto: p, github } = await d.leerProyecto(d.api, clave, encontrado.id);
|
|
203
203
|
const proyecto = { id: p.id, slug: p.slug, repo: p.repo, rama: p.rama, install_command: p.installCommand, test_command: p.testCommand };
|
|
204
204
|
return {
|
|
205
205
|
codigo: SALIDA.ok,
|
|
206
206
|
json: { ok: true, proyecto, github: { estado: github.estado, verificado_en: github.verificadoEn } },
|
|
207
|
-
texto: `${p.id} (${p.repo},
|
|
207
|
+
texto: `${p.id} (${p.repo}, branch ${p.rama}): installs with \`${p.installCommand}\`, tests with \`${p.testCommand}\`. GitHub: ${github.estado ?? "not verified"}.`,
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
catch (e) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FalloDeLaTerminal, resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
+
import { NOMBRE_DE_LA_FUENTE } from "../guardado/index.js";
|
|
2
3
|
import { claveVigente } from "./clave-vigente.js";
|
|
3
4
|
export async function comandoQuienSoy(d) {
|
|
4
5
|
const origen = d.api.origin;
|
|
@@ -7,20 +8,20 @@ export async function comandoQuienSoy(d) {
|
|
|
7
8
|
return leida.resultado;
|
|
8
9
|
const vigente = leida.vigente;
|
|
9
10
|
if (!vigente.completa) {
|
|
10
|
-
const otro = vigente.fuente === "archivo" ?
|
|
11
|
-
d.avisar(`
|
|
11
|
+
const otro = vigente.fuente === "archivo" ? NOMBRE_DE_LA_FUENTE.llavero : NOMBRE_DE_LA_FUENTE.archivo;
|
|
12
|
+
d.avisar(`I'm using the key in ${NOMBRE_DE_LA_FUENTE[vigente.fuente]}, but I couldn't read ${otro}: a newer key may be saved there.`);
|
|
12
13
|
}
|
|
13
14
|
// Leer no cuesta nada y avisa de algo que la persona no vería de otro modo: claves viejas vivas.
|
|
14
15
|
try {
|
|
15
16
|
const { claves, completa } = await d.guardado.leerPorRevocar(origen);
|
|
16
17
|
if (claves.length > 0) {
|
|
17
|
-
d.avisar(
|
|
18
|
+
d.avisar(`${claves.length} previous key(s) are noted down to be revoked; I'll try again on the next login or logout.`);
|
|
18
19
|
}
|
|
19
20
|
if (!completa)
|
|
20
|
-
d.avisar("
|
|
21
|
+
d.avisar("I couldn't read the whole list of keys waiting to be revoked.");
|
|
21
22
|
}
|
|
22
23
|
catch {
|
|
23
|
-
d.avisar("
|
|
24
|
+
d.avisar("I couldn't read the whole list of keys waiting to be revoked.");
|
|
24
25
|
}
|
|
25
26
|
let yo;
|
|
26
27
|
try {
|
|
@@ -30,7 +31,7 @@ export async function comandoQuienSoy(d) {
|
|
|
30
31
|
// Con la variable definida, «iniciá sesión de nuevo» no arregla nada: la variable sigue
|
|
31
32
|
// ganándole a la clave nueva. Hay que decir cuál es la que no vale.
|
|
32
33
|
if (e instanceof FalloDeLaTerminal && e.codigo === SALIDA.faltaLogin && vigente.fuente === "env") {
|
|
33
|
-
return resultadoDeFallo(new FalloDeLaTerminal(e.codigo, e.error, "
|
|
34
|
+
return resultadoDeFallo(new FalloDeLaTerminal(e.codigo, e.error, "The key in SUPCOFLOW_TOKEN is not valid: remove it from your environment or replace it with a valid one."));
|
|
34
35
|
}
|
|
35
36
|
return resultadoDeFallo(e);
|
|
36
37
|
}
|
|
@@ -38,6 +39,6 @@ export async function comandoQuienSoy(d) {
|
|
|
38
39
|
return {
|
|
39
40
|
codigo: SALIDA.ok,
|
|
40
41
|
json: { ok: true, via: yo.via, equipo: yo.equipo, usuario: yo.usuario, proyecto: yo.proyecto, fuente: vigente.fuente },
|
|
41
|
-
texto: `${quien}
|
|
42
|
+
texto: `${quien} in team ${yo.equipo} (key in ${NOMBRE_DE_LA_FUENTE[vigente.fuente]})`,
|
|
42
43
|
};
|
|
43
44
|
}
|