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.
@@ -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 ? "sin medir" : usd(n));
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 = (g) => g.puntos.map((p) => ` [${p.ok ? "ok" : "falta"}] ${p.punto}: ${p.mensaje}`);
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
- const datos = { proyecto: { id: p.proyecto.id, repo: p.proyecto.repo }, ...datosDelDiagnostico(g) };
45
- const resumen = `Saldo del mes ${usdONull(g.saldoUsd)} de ${usdONull(g.topeUsd)}; una prueba autoriza hasta ${usd(g.techoUsd)}. Política: ${g.politica}.`;
46
- if (g.listo) {
47
- return { codigo: SALIDA.ok, json: { ok: true, ...datos }, texto: [`Todo listo para probar ${p.proyecto.repo}.`, ...lineasDePuntos(g), resumen].join("\n") };
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 message = `Hay algo para arreglar antes de probar ${p.proyecto.repo}.`;
50
- return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "no_listo", message, ...datos }, texto: [message, ...lineasDePuntos(g), resumen].join("\n") };
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.autorizoGasto || flags.mensaje !== null)) {
57
- return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "`probar --wait` sólo sigue una prueba: sin --autorizo-gasto ni --mensaje. Primero `supcoflow probar --autorizo-gasto` y después `supcoflow probar --wait`.");
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.prueba !== null) {
60
- return fallo(SALIDA.entradaInvalida, "flags_incompatibles", "--prueba va con --wait: `supcoflow probar --wait --prueba prb_…`.");
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.prueba !== null && !ID_DE_PRUEBA.test(flags.prueba)) {
63
- return fallo(SALIDA.entradaInvalida, "prueba_invalida", "--prueba es el id que dio `supcoflow probar` (prb_ y 16 caracteres).");
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.mensaje !== null) {
67
- mensaje = flags.mensaje.trim();
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", `--mensaje va con texto, hasta ${MAX_MENSAJE} caracteres.`);
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.prueba, inicio);
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 (!flags.autorizoGasto || flags.dryRun) {
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
- ? `Con la política \`guardar\` la prueba llega hasta el caso guardado y no gasta; el techo que se autoriza igual es ${usd(g.techoUsd)}.`
91
- : `Una prueba corre un relevamiento que gasta hasta ${usd(g.techoUsd)} (saldo del mes ${usdONull(g.saldoUsd)} de ${usdONull(g.topeUsd)}).`;
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
- ? `reenviaría el pedido guardado sin confirmar (POST ${ruta}, la misma Idempotency-Key)`
104
- : `POST ${ruta} {techo_usd: ${g.techoUsd}${mensaje !== null ? ", mensaje" : ""}} con una Idempotency-Key nueva`;
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.autorizoGasto, intento_pendiente: pendiente, haria, ...datos },
108
- texto: `Con --dry-run no hago nada. ${flags.autorizoGasto ? "Sin --dry-run" : "Con --autorizo-gasto y sin --dry-run"}: ${haria} (proyecto ${proyecto.repo}, política ${g.politica}). ${gasto}`,
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 message = `${gasto} Para lanzarla: \`supcoflow probar --autorizo-gasto\`. Antes, \`supcoflow diagnosticar\` dice si está todo listo.`;
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 --autorizo-gasto: el ciclo de la clave.
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
- if (mensaje !== null && mensaje !== guardado.intento.mensaje) {
120
- return fallo(SALIDA.haceFaltaUnaPersona, "intento_pendiente", "Hay una prueba pedida sin confirmar; repetí `supcoflow probar --autorizo-gasto` sin `--mensaje` para recuperarla.");
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 = guardado.intento;
199
+ intento = previo;
123
200
  }
124
201
  else {
125
- // El techo que se autoriza es el que dice el motor ahora (D6), no uno escrito en la terminal.
126
- const g = await d.leerDiagnostico(d.api, clave, proyecto.id);
127
- if (g.pruebaAbierta !== null)
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 = `El motor aceptó la prueba ${r.pruebaId}, pero no pude guardarla en esta máquina. Repetí \`supcoflow probar --autorizo-gasto\` (recupera la misma) o seguila con \`supcoflow probar --wait --prueba ${r.pruebaId}\`.`;
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: `Prueba ${r.pruebaId} pedida (${r.estado}). Seguila con \`supcoflow probar --wait\`.`,
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} El pedido quedó guardado: repetí \`supcoflow probar --autorizo-gasto\` y retomo la misma prueba, sin pedir otra.`;
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("No pude cerrar el pedido guardado; el próximo `probar --autorizo-gasto` lo va a reenviar.");
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 = `Ya hay una prueba de este proyecto sin desenlace (${pruebaId}). Seguila con \`supcoflow probar --wait --prueba ${pruebaId}\`.`;
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
- const message = `${r.message} Volvé a correr \`supcoflow probar\` para ver el techo vigente y autorizalo.`;
178
- return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: r.error, message, techo_usd: c.techo_usd ?? null }, texto: message };
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 = `La prueba se pide con tu API key personal (la que deja \`supcoflow login\`), no con una clave de consulta ni con la sesión de la consola. ${r.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("El motor dice que la clave del pedido ya se usó con otro contenido; la descarté.");
193
- return fallo(SALIDA.inesperado, r.error, `${r.message} Volvé a correr \`supcoflow probar --autorizo-gasto\` para pedir una prueba nueva.`);
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 `Costo: ${usd(c.usd)}.`;
283
+ return `Cost: ${usd(c.usd)}.`;
205
284
  if (c.estado === "pendiente")
206
- return `Cobro pendiente: se lee unos minutos después del cierre (hasta ${usd(c.techo_usd)}).`;
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 `Cobro perdido: no se pudo leer el gasto (hasta ${usd(c.techo_usd)}).`;
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 `Cobro: ${c.estado} (hasta ${usd(c.techo_usd)}).`;
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
- `Prueba ${s.pruebaId}: ${s.estado}.${s.mensaje ? ` ${s.mensaje}` : ""}`,
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 ? `Motivo del run: ${s.run.motivo}` : null,
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("Volvé a correr `supcoflow probar --wait`.");
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(`El caso en la consola: ${s.reporte.url}`);
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 = `El motor dice que la prueba está en «${s.estado}», un estado que esta versión de la terminal no conoce. Actualizá supcoflow o mirala en la consola.`;
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 `--prueba` explícito no se llega acá.
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", "Hay una prueba pedida sin confirmar; repetí `supcoflow probar --autorizo-gasto` sin `--mensaje` para recuperarla, y después `--wait`.");
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", "No hay una prueba guardada en esta máquina para este proyecto: pedila con `supcoflow probar --autorizo-gasto`, o pasá `--prueba prb_…`.");
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 = `El repo ${repo} todavía no tiene proyecto. Creá el proyecto: \`supcoflow proyecto crear\`.`;
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 ? `El proyecto ${p.id} ya existía y lo reusé` : `Creé el proyecto ${p.id}`;
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 = `Ya hay un \`.supcoflow/proyecto.json\` de otro proyecto (${idActual ?? "que no reconozco"}); no lo toqué. `
67
- + `${queda} igual (${p.id}): si es el que querés, borrá el archivo y volvé a correr \`supcoflow proyecto crear\`.`;
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} (${p.id}), pero no pude escribir ${archivo}. Volvé a correr \`supcoflow proyecto crear\`.`;
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}, rama ${p.rama}.${archivo !== null ? ` Lo anoté en ${archivo}.` : ""}`,
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("El proyecto ya existe: ignoré --install/--test. Para cambiar sus comandos hay que configurarlo.");
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", "Conectá el repo primero: `supcoflow repo 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: `Con --dry-run no hago nada; sin él, crearía el proyecto de ${repo} (${haria}).` };
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 || "Faltan comandos del proyecto."} Pasalos a mano: \`supcoflow proyecto crear --install ${comillas(install)} --test ${comillas(test)}\`.`;
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 || "Conectá el repo primero: `supcoflow repo conectar`.", { repo });
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", "Otro pedido con esta clave está en curso; probá en un minuto.");
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", "El motor rechazó la clave de idempotencia guardada; la descarté, probá de nuevo.");
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 || "El motor rechazó el pedido.";
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} Revisá la instalación de la App en GitHub y volvé a conectar con \`supcoflow repo conectar\`.`);
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: "Este repo todavía no tiene proyecto: `supcoflow proyecto crear`." };
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}, rama ${p.rama}): instala con \`${p.installCommand}\`, prueba con \`${p.testCommand}\`. GitHub: ${github.estado ?? "sin verificar"}.`,
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" ? "el llavero del sistema" : "el archivo de credenciales";
11
- d.avisar(`Uso la clave de ${vigente.fuente}, pero no pude leer ${otro}: puede haber guardada otra más nueva.`);
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(`Hay ${claves.length} clave(s) anterior(es) anotadas para revocar; se reintenta en el próximo login o logout.`);
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("No pude leer entera la lista de claves por revocar.");
21
+ d.avisar("I couldn't read the whole list of keys waiting to be revoked.");
21
22
  }
22
23
  catch {
23
- d.avisar("No pude leer entera la lista de claves por revocar.");
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, "La clave de SUPCOFLOW_TOKEN no vale: sacala de tu entorno o reemplazala por una vigente."));
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} en el equipo ${yo.equipo} (clave en ${vigente.fuente})`,
42
+ texto: `${quien} in team ${yo.equipo} (key in ${NOMBRE_DE_LA_FUENTE[vigente.fuente]})`,
42
43
  };
43
44
  }