supcoflow 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
1
  // src/comandos/guia.ts
2
- // `guia revisar|publicar|ver|usar` (bloque 5): la guía del producto que vive en el repo del cliente
3
- // (`.supcoflow/guia.md`). La terminal NO decide qué es una guía buena —eso lo decide el parser del motor—;
4
- // lee el archivo, manda UN pedido y traduce la respuesta a los códigos de salida del contrato.
2
+ // `guide check|publish|show|use|versions` (bloque 5 y 0.8.0): la guía del producto que vive en el repo del cliente
3
+ // (`.supcoflow/guide.md`; el nombre viejo `guia.md` se lee sólo si `guide.md` no existe). La terminal NO decide qué
4
+ // es una guía buena —eso lo decide el parser del motor—; lee el archivo, manda UN pedido y traduce la respuesta a
5
+ // los códigos de salida del contrato.
5
6
  import path from "node:path";
6
7
  import { resultadoDeFallo, SALIDA, taparSecretos } from "../contrato.js";
7
- import { huellaDeGuia, MAX_BYTES_DE_LA_GUIA, RUTA_DE_LA_GUIA } from "../guia.js";
8
+ import { huellaDeGuia, MAX_BYTES_DE_LA_GUIA, RUTA_DE_LA_GUIA, RUTA_VIEJA_DE_LA_GUIA } from "../guia.js";
9
+ import { NO_LEIDO } from "../lectura.js";
8
10
  import { repoPedido } from "../repo-local.js";
9
11
  import { desenlace, fallo, rutaDe } from "./configurar.js";
10
12
  import { proyectoDelRepo } from "./proyecto-del-repo.js";
@@ -15,56 +17,88 @@ import { proyectoDelRepo } from "./proyecto-del-repo.js";
15
17
  const limpio = (t) => taparSecretos(t.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""));
16
18
  const cadena = (v) => (typeof v === "string" ? limpio(v) : "");
17
19
  const corto = (commit) => cadena(commit).slice(0, 7);
18
- const secciones = (n) => (typeof n === "number" ? `${n} ${n === 1 ? "sección" : "secciones"}` : "secciones sin contar");
19
- const NOMBRE_DEL_ORIGEN = { repo: "la del repo", agente: "la de nuestro agente" };
20
+ const secciones = (n) => (typeof n === "number" ? `${n} ${n === 1 ? "section" : "sections"}` : "sections not counted");
21
+ const NOMBRE_DEL_ORIGEN = { repo: "the repo's", agente: "our agent's" };
22
+ // El valor del flag `--source` para un origen de la API (el de nuestro agente se pide como `agent`).
23
+ const COMO_SE_PIDE = { repo: "repo", agente: "agent" };
24
+ // Los avisos del nombre del archivo; los códigos son los que usa el motor por GitHub (`aviso`).
25
+ const AVISOS_DEL_NOMBRE = {
26
+ nombre_viejo: `\`${RUTA_VIEJA_DE_LA_GUIA}\` is the old name: rename it to \`guide.md\`.`,
27
+ nombre_viejo_ignorado: `Both \`${RUTA_DE_LA_GUIA}\` and \`${RUTA_VIEJA_DE_LA_GUIA}\` exist: I use guide.md and ignore guia.md.`,
28
+ };
20
29
  // ─── El archivo local ───
21
30
  // Se busca en la raíz de ESTE clon, nunca en el cwd: parado en una subcarpeta sigue siendo la guía del repo.
22
31
  // Con `--repo` y sin clon de ese repo no hay dónde leerla (publicar lee la rama, pero la huella sale del archivo).
32
+ // Se lee `guide.md`; SÓLO si no existe se lee `guia.md` (si `guide.md` existe y no se puede leer, o no valida, es un
33
+ // error: nunca motivo para leer el viejo). Si están los dos manda `guide.md` y se avisa que el otro se ignora.
23
34
  async function leerLaGuia(d, flags) {
24
35
  let raiz;
36
+ let sinClon;
25
37
  try {
26
- raiz = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).raiz;
38
+ ({ raiz, sinClon } = await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd }));
27
39
  }
28
40
  catch (e) {
29
41
  return { ok: false, resultado: resultadoDeFallo(e) };
30
42
  }
31
43
  if (raiz === null) {
32
- return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `No tengo el clon de ese repo acá, y la guía se lee de ${RUTA_DE_LA_GUIA}: parate en el clon del repo.`) };
44
+ return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `${sinClon ?? "I don't have that repo's clone here."} The guide is read from ${RUTA_DE_LA_GUIA} in the repo's clone: stand in it.`) };
45
+ }
46
+ let ruta = RUTA_DE_LA_GUIA;
47
+ let aviso = null;
48
+ let leido = d.leerArchivoChico(path.join(raiz, ruta), MAX_BYTES_DE_LA_GUIA);
49
+ if ("error" in leido && leido.error === NO_LEIDO.noExiste) {
50
+ const vieja = d.leerArchivoChico(path.join(raiz, RUTA_VIEJA_DE_LA_GUIA), MAX_BYTES_DE_LA_GUIA);
51
+ if ("error" in vieja && vieja.error === NO_LEIDO.noExiste) {
52
+ return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `${RUTA_DE_LA_GUIA} is missing in the repo: write it (one section per screen) and run again.`) };
53
+ }
54
+ ruta = RUTA_VIEJA_DE_LA_GUIA;
55
+ aviso = "nombre_viejo";
56
+ leido = vieja;
57
+ }
58
+ else if (!("error" in leido)) {
59
+ // Con `guide.md` leído, el viejo sólo se mira para avisar que se ignora (no se lee su contenido).
60
+ const vieja = d.leerArchivoChico(path.join(raiz, RUTA_VIEJA_DE_LA_GUIA), 1);
61
+ if (!("error" in vieja) || vieja.error !== NO_LEIDO.noExiste)
62
+ aviso = "nombre_viejo_ignorado";
33
63
  }
34
- const leido = d.leerArchivoChico(path.join(raiz, RUTA_DE_LA_GUIA), MAX_BYTES_DE_LA_GUIA);
35
64
  if ("error" in leido) {
36
- if (leido.error === "no existe")
37
- return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `Falta ${RUTA_DE_LA_GUIA} en el repo: escribila (una sección por pantalla) y volvé a correr.`) };
38
- if (leido.error === "es demasiado grande")
39
- return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_muy_grande", `${RUTA_DE_LA_GUIA} pasa los ${MAX_BYTES_DE_LA_GUIA / 1024} KB: acortala.`) };
40
- return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `No leo ${RUTA_DE_LA_GUIA}: ${limpio(leido.error)}.`) };
65
+ if (leido.error === NO_LEIDO.demasiadoGrande)
66
+ return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_muy_grande", `${ruta} is over ${MAX_BYTES_DE_LA_GUIA / 1024} KB: shorten it.`) };
67
+ return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `I can't read ${ruta}: ${limpio(leido.error)}.`) };
41
68
  }
42
69
  try {
43
70
  // ignoreBOM: el BOM se CONSERVA en el texto; quitarlo es regla de la huella (`huellaDeGuia`), no de la lectura.
44
71
  // fatal: un byte inválido no se reemplaza en silencio, porque la huella de GitHub no coincidiría nunca.
45
- return { ok: true, raiz, texto: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(leido.bytes) };
72
+ return { ok: true, raiz, ruta, aviso, texto: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(leido.bytes) };
46
73
  }
47
74
  catch {
48
- return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `${RUTA_DE_LA_GUIA} no es UTF-8 válido.`) };
75
+ return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `${ruta} isn't valid UTF-8.`) };
49
76
  }
50
77
  }
78
+ // El aviso del nombre viaja en el JSON (`aviso`) y como última línea del texto, en lo que salió bien.
79
+ function conAviso(r, aviso) {
80
+ // Si el motor ya dijo el suyo (por GitHub lee el repo él mismo, en un SHA), manda ése.
81
+ if (aviso === null || r.codigo !== SALIDA.ok || "aviso" in r.json)
82
+ return r;
83
+ return { ...r, json: { ...r.json, aviso }, texto: `${r.texto}\n${AVISOS_DEL_NOMBRE[aviso]}` };
84
+ }
51
85
  // ─── El desenlace propio de la guía, encima del de configurar ───
52
- function desenlaceDeGuia(r, ok) {
86
+ function desenlaceDeGuia(r, ok, ruta = RUTA_DE_LA_GUIA) {
53
87
  const c = r.cuerpo;
54
88
  // 2xx, 401, 5xx y cuerpos raros los resuelve el de configurar. Ojo: la terminal no distingue 500
55
89
  // connect_no_disponible de 502 github_no_disponible (ambos llegan como falla del servicio): los dos son 1.
56
90
  if (r.status < 400 || r.status === 401 || r.status >= 500 || typeof c !== "object" || c === null)
57
91
  return desenlace(r, ok);
58
92
  const error = typeof c.error === "string" ? c.error : "rechazado";
59
- const message = limpio(typeof c.message === "string" ? c.message : `El motor rechazó el pedido (${r.status}).`);
93
+ const message = limpio(typeof c.message === "string" ? c.message : `The engine rejected the request (${r.status}).`);
60
94
  const con = (codigo, extra = {}, texto = message) => ({ codigo, json: { ok: false, error, message: taparSecretos(message), ...extra }, texto });
61
95
  switch (error) {
62
96
  case "guia_no_subida":
63
97
  case "guia_distinta": {
64
98
  const rama = cadena(c.rama), commit = cadena(c.commit);
65
- const donde = rama !== "" ? ` (rama ${rama}${commit !== "" ? `, commit ${corto(commit)}` : ""})` : "";
66
- const que = error === "guia_no_subida" ? "La guía no está en la rama principal del repo" : "La guía de la rama principal es otra, no la de tu archivo";
67
- return con(SALIDA.esperandoAUnaPersona, { rama: c.rama ?? null, commit: c.commit ?? null }, `${que}${donde}: subí la guía a la rama principal y volvé a correr.`);
99
+ const donde = rama !== "" ? ` (branch ${rama}${commit !== "" ? `, commit ${corto(commit)}` : ""})` : "";
100
+ const que = error === "guia_no_subida" ? "The guide isn't on the repo's main branch" : "The main branch's guide is another one, not your file's";
101
+ return con(SALIDA.esperandoAUnaPersona, { rama: c.rama ?? null, commit: c.commit ?? null }, `${que}${donde}: push the guide to the main branch and run again.`);
68
102
  }
69
103
  case "instalacion_ausente":
70
104
  case "repo_fuera_de_alcance":
@@ -74,9 +108,9 @@ function desenlaceDeGuia(r, ok) {
74
108
  const errores = Array.isArray(c.errores) ? c.errores : [];
75
109
  const lineas = errores.map((e) => {
76
110
  const x = (e ?? {});
77
- return `línea ${typeof x.linea === "number" ? x.linea : "?"}: ${cadena(x.motivo)}`;
111
+ return `line ${typeof x.linea === "number" ? x.linea : "?"}: ${cadena(x.motivo)}`;
78
112
  });
79
- return con(SALIDA.entradaInvalida, { errores: errores.map((e) => { const x = (e ?? {}); return { linea: x.linea ?? null, motivo: taparSecretos(typeof x.motivo === "string" ? x.motivo : "") }; }) }, [`${RUTA_DE_LA_GUIA} está mal armada:`, ...lineas].join("\n"));
113
+ return con(SALIDA.entradaInvalida, { errores: errores.map((e) => { const x = (e ?? {}); return { linea: x.linea ?? null, motivo: taparSecretos(typeof x.motivo === "string" ? x.motivo : "") }; }) }, [`${ruta} is badly formed:`, ...lineas].join("\n"));
80
114
  }
81
115
  default:
82
116
  return desenlace(r, ok);
@@ -90,13 +124,13 @@ export async function guiaRevisar(d, flags) {
90
124
  if (!p.ok)
91
125
  return p.resultado;
92
126
  if (flags.dryRun) {
93
- const haria = `POST ${rutaDe(p.proyecto, "/guia/revisar")} (${new TextEncoder().encode(g.texto).length} bytes)`;
94
- return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `Con --dry-run no hago nada; sin él: ${haria}.` };
127
+ const haria = `POST ${rutaDe(p.proyecto, "/guia/revisar")} (${new TextEncoder().encode(g.texto).length} bytes, from ${g.ruta})`;
128
+ return conAviso({ codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria, ruta: g.ruta }, texto: `With --dry-run I do nothing; without it: ${haria}.` }, g.aviso);
95
129
  }
96
130
  try {
97
- return desenlaceDeGuia(await d.revisar(d.api, p.clave, p.proyecto.id, g.texto, d.fetch), (c) => ({
98
- codigo: SALIDA.ok, json: { ok: true, secciones: c.secciones ?? null }, texto: `La guía está bien armada: ${secciones(c.secciones)}.`,
99
- }));
131
+ return conAviso(desenlaceDeGuia(await d.revisar(d.api, p.clave, p.proyecto.id, g.texto, d.fetch), (c) => ({
132
+ codigo: SALIDA.ok, json: { ok: true, secciones: c.secciones ?? null, ruta: g.ruta }, texto: `The guide is well formed: ${secciones(c.secciones)}.`,
133
+ }), g.ruta), g.aviso);
100
134
  }
101
135
  catch (e) {
102
136
  return resultadoDeFallo(e);
@@ -130,42 +164,53 @@ export async function guiaPublicar(d, flags) {
130
164
  const p = await proyectoDelRepo(d, flags);
131
165
  if (!p.ok)
132
166
  return p.resultado;
133
- const github = flags.desdeGithub === true;
167
+ const github = flags.fromGithub === true;
134
168
  // La vía de GitHub manda la huella del archivo local TAL CUAL (con BOM y \r\n: normalizarla es regla de
135
169
  // `huellaDeGuia`). La directa manda el contenido crudo y el motor calcula la huella: la terminal no la necesita.
136
170
  const huella = github ? huellaDeGuia(g.texto) : null;
137
171
  const decl = github ? {} : await declarado(d, g.raiz);
138
- const cuerpo = huella !== null ? { huella } : { contenido: g.texto, ...decl };
139
- const cuando = decl.commit ? `commit ${decl.commit.slice(0, 7)}${decl.rama ? ` de ${cadena(decl.rama)}` : ""} declarado` : "sin commit";
172
+ // La directa manda además la ruta que se leyó de verdad (dato declarado, como el commit y la rama). Por GitHub el
173
+ // motor resuelve los nombres él mismo, sobre un solo SHA de la rama principal.
174
+ const cuerpo = huella !== null ? { huella } : { contenido: g.texto, ruta: g.ruta, ...decl };
175
+ const cuando = decl.commit ? `commit ${decl.commit.slice(0, 7)}${decl.rama ? ` of ${cadena(decl.rama)}` : ""} declared` : "no commit";
140
176
  // El JSON del dry-run pasa por la misma limpieza que el texto (control primero, secretos después): la rama la
141
177
  // decide quien escribió el clon, y un ESC en ella no debe llegar a la terminal por ninguna de las dos salidas.
142
178
  if (flags.dryRun) {
143
179
  const haria = huella !== null
144
- ? `POST ${rutaDe(p.proyecto, "/guia/publicar")} con la huella ${huella.slice(0, 12)}…`
145
- : `POST ${rutaDe(p.proyecto, "/guia/publicar")} con el contenido (${new TextEncoder().encode(g.texto).length} bytes), ${cuando}`;
146
- const json = huella !== null ? { ok: true, dry_run: true, huella, haria } : { ok: true, dry_run: true, commit: decl.commit ? cadena(decl.commit) : null, rama: decl.rama ? cadena(decl.rama) : null, haria };
147
- return { codigo: SALIDA.ok, json, texto: `Con --dry-run no hago nada; sin él: ${haria}.` };
180
+ ? `POST ${rutaDe(p.proyecto, "/guia/publicar")} with the fingerprint ${huella.slice(0, 12)}…`
181
+ : `POST ${rutaDe(p.proyecto, "/guia/publicar")} with the content (${new TextEncoder().encode(g.texto).length} bytes, from ${g.ruta}), ${cuando}`;
182
+ const json = huella !== null
183
+ ? { ok: true, dry_run: true, huella, ruta: g.ruta, haria }
184
+ : { ok: true, dry_run: true, ruta: g.ruta, commit: decl.commit ? cadena(decl.commit) : null, rama: decl.rama ? cadena(decl.rama) : null, haria };
185
+ return conAviso({ codigo: SALIDA.ok, json, texto: `With --dry-run I do nothing; without it: ${haria}.` }, g.aviso);
148
186
  }
149
187
  try {
150
- return desenlaceDeGuia(await d.publicar(d.api, p.clave, p.proyecto.id, cuerpo, d.fetch), (c) => {
188
+ return conAviso(desenlaceDeGuia(await d.publicar(d.api, p.clave, p.proyecto.id, cuerpo, d.fetch), (c) => {
151
189
  const guia = (c.guia ?? {});
152
190
  const ya = c.ya_publicada === true;
153
191
  const rem = c.reemplaza;
154
- const reemplaza = !ya && rem && rem.origen === "agente" && typeof rem.version === "number" ? ` (reemplaza a la de nuestro agente v${rem.version})` : "";
192
+ const reemplaza = !ya && rem && rem.origen === "agente" && typeof rem.version === "number" ? ` (replaces our agent's v${rem.version})` : "";
155
193
  const json = { ok: true, guia: c.guia ?? null, ya_publicada: ya, reemplaza: c.reemplaza ?? null };
194
+ // Una resubida idéntica devuelve la fila vieja con su ruta histórica: se dice, para que nadie crea que el
195
+ // renombre quedó registrado.
196
+ const ruta = cadena(guia.ruta);
197
+ const nota = ya && ruta !== "" && ruta !== g.ruta ? `\nThat version keeps the file name it was first published with (${ruta}).` : "";
156
198
  if (github) {
157
- const base = `commit ${corto(guia.commit)} de ${cadena(guia.rama)}, ${secciones(guia.secciones)}`;
199
+ const base = `commit ${corto(guia.commit)} of ${cadena(guia.rama)}, ${secciones(guia.secciones)}`;
158
200
  // Republicar re-activa la guía del repo aunque antes rigiera la de nuestro agente: «sigue siendo» mentiría en ese caso.
159
- const texto = ya ? `Ya estaba publicada: ${base}. Quedó como guía activa.` : `Guía publicada: ${base}. Es la guía activa${reemplaza}.`;
160
- return { codigo: SALIDA.ok, json, texto };
201
+ const texto = ya ? `It was already published: ${base}. It stayed as the active guide.${nota}` : `Guide published: ${base}. It's the active guide${reemplaza}.${nota}`;
202
+ const delMotor = typeof c.aviso === "string" && c.aviso in AVISOS_DEL_NOMBRE ? c.aviso : null;
203
+ return delMotor === null
204
+ ? { codigo: SALIDA.ok, json, texto }
205
+ : { codigo: SALIDA.ok, json: { ...json, aviso: delMotor }, texto: `${texto}\n${AVISOS_DEL_NOMBRE[delMotor]}` };
161
206
  }
162
207
  // El commit se muestra como lo que es: lo que la terminal dijo, no lo que GitHub confirmó.
163
- const origen = decl.commit ? `commit ${decl.commit.slice(0, 7)} declarado por la terminal (no verificado en GitHub)` : "sin commit";
208
+ const origen = decl.commit ? `commit ${decl.commit.slice(0, 7)} declared by the terminal (not verified in GitHub)` : "no commit";
164
209
  const texto = ya
165
- ? `Ya estaba subida: ${secciones(guia.secciones)}. Quedó como guía activa; ${origen}.`
166
- : `Guía subida: ${secciones(guia.secciones)}. Es la guía activa${reemplaza}; ${origen}.`;
210
+ ? `It was already uploaded: ${secciones(guia.secciones)}. It stayed as the active guide; ${origen}.${nota}`
211
+ : `Guide uploaded: ${secciones(guia.secciones)}. It's the active guide${reemplaza}; ${origen}.${nota}`;
167
212
  return { codigo: SALIDA.ok, json: { ...json, commit_declarado: decl.commit ?? null }, texto };
168
- });
213
+ }, g.ruta), g.aviso);
169
214
  }
170
215
  catch (e) {
171
216
  return resultadoDeFallo(e);
@@ -183,35 +228,39 @@ export async function guiaVer(d, flags) {
183
228
  const alDia = rama && typeof rama.al_dia === "boolean" ? rama.al_dia : null;
184
229
  const json = { ok: true, guia: guia ?? null, otra: otra ?? null, al_dia: alDia, rama: rama ?? null };
185
230
  if (!guia)
186
- return { codigo: SALIDA.ok, json, texto: "Este proyecto no tiene guía activa: `supcoflow guia publicar` sube la del repo." };
231
+ return { codigo: SALIDA.ok, json, texto: "This project has no active guide: `supcoflow guide publish` uploads the repo's." };
187
232
  const lineas = [];
188
233
  if (guia.origen === "repo") {
189
- lineas.push(`Guía activa: ${NOMBRE_DEL_ORIGEN.repo} (versión ${guia.version}, ${secciones(guia.secciones)}).`);
234
+ lineas.push(`Active guide: ${NOMBRE_DEL_ORIGEN.repo} (version ${guia.version}, ${secciones(guia.secciones)}).`);
235
+ // La ruta es la que se leyó de verdad; por la terminal es declarada (el motor no la verificó), por GitHub no.
236
+ const ruta = cadena(guia.ruta);
237
+ if (ruta !== "")
238
+ lineas.push(`File: ${ruta}${guia.procedencia === "terminal" ? " (declared by the terminal)" : ""}.`);
190
239
  if (guia.procedencia === "terminal") {
191
240
  // Subida desde la terminal: el motor NO comparó nada contra GitHub a propósito (al_dia null), así que "no pude
192
241
  // comprobar la rama" sería falso. Commit y rama son lo que declaró quien subió; el id crudo de quien publicó
193
242
  // (usr_…) no le dice nada a una persona y la terminal no resuelve nombres: queda sólo en el JSON.
194
243
  const commit = cadena(guia.commit), rama = cadena(guia.rama);
195
- const partes = [cadena(guia.repo), rama !== "" ? `rama ${rama}` : "", commit !== "" ? `commit ${corto(commit)} (declarado)` : "sin commit declarado"].filter((x) => x !== "");
196
- lineas.push(`Procedencia: ${partes.join(", ")}.`);
197
- lineas.push("Subida desde la terminal; el commit es declarado, no verificado en GitHub.");
244
+ const partes = [cadena(guia.repo), rama !== "" ? `branch ${rama}` : "", commit !== "" ? `commit ${corto(commit)} (declared)` : "no declared commit"].filter((x) => x !== "");
245
+ lineas.push(`Source: ${partes.join(", ")}.`);
246
+ lineas.push("Uploaded from the terminal; the commit is declared, not verified in GitHub.");
198
247
  }
199
248
  else {
200
- lineas.push(`Procedencia: ${cadena(guia.repo)}, rama ${cadena(guia.rama)}, commit ${corto(guia.commit)}, publicada por ${cadena(guia.publicada_por)}.`);
249
+ lineas.push(`Source: ${cadena(guia.repo)}, branch ${cadena(guia.rama)}, commit ${corto(guia.commit)}, published by ${cadena(guia.publicada_por)}.`);
201
250
  if (alDia === true)
202
- lineas.push("Está al día con la rama.");
251
+ lineas.push("It's up to date with the branch.");
203
252
  else if (alDia === false)
204
- lineas.push("En la rama hay otra versión: corré `supcoflow guia publicar`.");
253
+ lineas.push("The branch has another version: run `supcoflow guide publish`.");
205
254
  else
206
- lineas.push("No pude comprobar la rama ahora.");
255
+ lineas.push("I couldn't check the branch right now.");
207
256
  }
208
257
  }
209
258
  else {
210
- lineas.push(`Guía activa: ${NOMBRE_DEL_ORIGEN.agente} (v${guia.version}, ${secciones(guia.secciones)}).`);
211
- lineas.push("Procedencia: la escribió nuestro agente y la aprobó una persona.");
259
+ lineas.push(`Active guide: ${NOMBRE_DEL_ORIGEN.agente} (v${guia.version}, ${secciones(guia.secciones)}).`);
260
+ lineas.push("Source: our agent wrote it and a person approved it.");
212
261
  }
213
262
  if (otra && (otra.origen === "repo" || otra.origen === "agente")) {
214
- lineas.push(`También hay ${NOMBRE_DEL_ORIGEN[otra.origen]} (v${typeof otra.version === "number" ? otra.version : "?"}): \`supcoflow guia usar --origen ${otra.origen}\` la activa.`);
263
+ lineas.push(`There's also ${NOMBRE_DEL_ORIGEN[otra.origen]} (v${typeof otra.version === "number" ? otra.version : "?"}): \`supcoflow guide use --source ${COMO_SE_PIDE[otra.origen]}\` activates it.`);
215
264
  }
216
265
  return { codigo: SALIDA.ok, json, texto: lineas.join("\n") };
217
266
  });
@@ -221,24 +270,63 @@ export async function guiaVer(d, flags) {
221
270
  }
222
271
  }
223
272
  export async function guiaUsar(d, flags) {
224
- const origen = flags.origen ?? null;
273
+ const pedido = flags.source ?? null;
274
+ if (pedido === null)
275
+ return fallo(SALIDA.entradaInvalida, "falta_origen", "Say which: `supcoflow guide use --source repo` or `--source agent`.");
276
+ // A la API siempre viaja su valor (`agente`); `agente` también se acepta, por el alias viejo.
277
+ const origen = pedido === "repo" ? "repo" : pedido === "agent" || pedido === "agente" ? "agente" : null;
225
278
  if (origen === null)
226
- return fallo(SALIDA.entradaInvalida, "falta_origen", "Decí cuál: `supcoflow guia usar --origen repo` o `--origen agente`.");
227
- if (origen !== "repo" && origen !== "agente")
228
- return fallo(SALIDA.entradaInvalida, "origen_invalido", "--origen es `repo` o `agente`.");
279
+ return fallo(SALIDA.entradaInvalida, "origen_invalido", "--source is `repo` or `agent`.");
229
280
  const p = await proyectoDelRepo(d, flags);
230
281
  if (!p.ok)
231
282
  return p.resultado;
232
283
  if (flags.dryRun) {
233
284
  const haria = `POST ${rutaDe(p.proyecto, "/guia/origen")} {origen: ${origen}}`;
234
- return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `Con --dry-run no hago nada; sin él: ${haria}.` };
285
+ return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `With --dry-run I do nothing; without it: ${haria}.` };
235
286
  }
236
287
  try {
237
288
  return desenlaceDeGuia(await d.elegir(d.api, p.clave, p.proyecto.id, origen, d.fetch), (c) => ({
238
- codigo: SALIDA.ok, json: { ok: true, guia: c.guia ?? null }, texto: `Ahora la guía activa es ${NOMBRE_DEL_ORIGEN[origen]}.`,
289
+ codigo: SALIDA.ok, json: { ok: true, guia: c.guia ?? null }, texto: `Now the active guide is ${NOMBRE_DEL_ORIGEN[origen]}.`,
239
290
  }));
240
291
  }
241
292
  catch (e) {
242
293
  return resultadoDeFallo(e);
243
294
  }
244
295
  }
296
+ // `guide versions`: la guía que rige ahora (la que lee el chat) y las publicaciones del repo, de la más nueva a la más
297
+ // vieja. Una sola publicación dice «live»: la que es la vigente (el motor lo decide; acá sólo se muestra).
298
+ export async function guiaVersiones(d, flags) {
299
+ const p = await proyectoDelRepo(d, flags);
300
+ if (!p.ok)
301
+ return p.resultado;
302
+ try {
303
+ return desenlaceDeGuia(await d.versiones(d.api, p.clave, p.proyecto.id, d.fetch), (c) => {
304
+ const vigente = typeof c.vigente === "object" && c.vigente !== null ? c.vigente : null;
305
+ const publicaciones = Array.isArray(c.publicaciones) ? c.publicaciones.map((x) => (typeof x === "object" && x !== null ? x : {})) : [];
306
+ const json = { ok: true, vigente, publicaciones };
307
+ const lineas = [];
308
+ if (vigente === null)
309
+ lineas.push("Active now: no guide.");
310
+ else if (vigente.origen === "agente")
311
+ lineas.push(`Active now: ${NOMBRE_DEL_ORIGEN.agente} guide, v${vigente.version} (${secciones(vigente.secciones)}).`);
312
+ else
313
+ lineas.push(`Active now: ${NOMBRE_DEL_ORIGEN.repo} guide, version ${vigente.version} (${secciones(vigente.secciones)}).`);
314
+ lineas.push(publicaciones.length === 0 ? "Published from the repo: none." : "Published from the repo, newest first:");
315
+ for (const x of publicaciones) {
316
+ const estado = x.live === true || x.estado === "live" ? "live" : "published";
317
+ const donde = [
318
+ x.procedencia === "terminal" ? "uploaded from the terminal (declared)" : x.procedencia === "github" ? "verified in GitHub" : "",
319
+ cadena(x.commit) !== "" ? `commit ${corto(x.commit)}` : "",
320
+ cadena(x.ruta),
321
+ cadena(x.publicada_por) !== "" ? `by ${cadena(x.publicada_por)}` : "",
322
+ cadena(x.publicada_en),
323
+ ].filter((t) => t !== "");
324
+ lineas.push(` v${x.version} ${estado}${donde.length > 0 ? ` · ${donde.join(" · ")}` : ""}`);
325
+ }
326
+ return { codigo: SALIDA.ok, json, texto: lineas.join("\n") };
327
+ });
328
+ }
329
+ catch (e) {
330
+ return resultadoDeFallo(e);
331
+ }
332
+ }
@@ -1,19 +1,29 @@
1
1
  // src/comandos/instalacion.ts
2
- // `instalacion anotar --skill <versión> [--app <carpeta>] [--dry-run]` (bloque 6): deja en
3
- // `.supcoflow/instalacion.json` con qué versiones se instaló el chat. Lee la configuración del motor y los
4
- // `package.json` de la app y del paquete instalado; nunca ejecuta nada del repo (ni `npm ls` ni `node`).
2
+ // `install record --skill <versión> [--app <carpeta>] [--file <ruta>] [--dry-run]` (bloque 6 y 0.8.0): deja en
3
+ // `.supcoflow/instalacion.json` con qué versiones se instaló el chat y, desde la 0.8.0 (v2), en qué archivo quedó
4
+ // (`--file`). Lee la configuración del motor y los `package.json` de la app y del paquete instalado; nunca ejecuta
5
+ // nada del repo (ni `npm ls` ni `node`).
5
6
  //
6
7
  // La regla de §13 («no pisa archivos ni cambios sin commit») adaptada a un archivo que tiene que poder cambiar
7
8
  // (decisión 11): se reescribe sólo si git ya guarda el anterior. El control se hace ANTES de escribir; si la
8
9
  // persona edita el archivo entre el control y el `rename`, su cambio se pierde. Esa carrera se acepta: es una
9
10
  // persona instalando, y el archivo lo escribe sólo este comando.
10
- import { realpathSync, statSync } from "node:fs";
11
+ import { lstatSync, realpathSync, statSync } from "node:fs";
11
12
  import path from "node:path";
12
13
  import { resultadoDeFallo, SALIDA, taparSecretos } from "../contrato.js";
13
- import { armarInstalacion, esSemver, PAQUETE_DEL_CHAT, RUTA_DE_LA_INSTALACION, serializar } from "../instalacion.js";
14
+ import { archivoAnotado, armarInstalacion, esSemver, PAQUETE_DEL_CHAT, RUTA_DE_LA_INSTALACION, serializar, validarElArchivoDelChat } from "../instalacion.js";
15
+ import { NO_LEIDO } from "../lectura.js";
14
16
  import { repoPedido } from "../repo-local.js";
15
17
  import { fallo } from "./configurar.js";
16
18
  import { proyectoDelRepo } from "./proyecto-del-repo.js";
19
+ export function esEnlaceReal(p) {
20
+ try {
21
+ return lstatSync(p).isSymbolicLink();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
17
27
  export function esCarpetaReal(p) {
18
28
  try {
19
29
  return statSync(p).isDirectory();
@@ -38,15 +48,15 @@ const limpio = (t) => taparSecretos(t.replace(/[\u0000-\u001f\u007f-\u009f]/g, "
38
48
  function leerJson(d, p, nombre) {
39
49
  const leido = d.leerArchivoChico(p, TOPE_DEL_PACKAGE_JSON);
40
50
  if ("error" in leido) {
41
- if (leido.error === "no existe")
51
+ if (leido.error === NO_LEIDO.noExiste)
42
52
  return { ok: false, noExiste: true };
43
- return { ok: false, noExiste: false, resultado: fallo(SALIDA.inesperado, "paquete_ilegible", `No leo ${nombre}: ${limpio(leido.error)}.`) };
53
+ return { ok: false, noExiste: false, resultado: fallo(SALIDA.inesperado, "paquete_ilegible", `I can't read ${nombre}: ${limpio(leido.error)}.`) };
44
54
  }
45
55
  try {
46
56
  return { ok: true, valor: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(leido.bytes)) };
47
57
  }
48
58
  catch {
49
- return { ok: false, noExiste: false, resultado: fallo(SALIDA.inesperado, "paquete_ilegible", `${nombre} no es JSON válido.`) };
59
+ return { ok: false, noExiste: false, resultado: fallo(SALIDA.inesperado, "paquete_ilegible", `${nombre} isn't valid JSON.`) };
50
60
  }
51
61
  }
52
62
  // Como lo resuelve Node desde la app (revisión externa, hallazgo 4): el primer `node_modules/@supcoflow/chat`
@@ -59,16 +69,16 @@ function paqueteInstalado(d, raiz, app, avisos) {
59
69
  const real = d.rutaReal(candidato);
60
70
  if (real !== null) {
61
71
  if (!adentro(raiz, real)) {
62
- const aviso = `${path.relative(raiz, candidato)} apunta fuera del repo; no lo leo y anoto el chat instalado como null.`;
72
+ const aviso = `${path.relative(raiz, candidato)} points outside the repo; I don't read it and I record the installed chat as null.`;
63
73
  avisos.push(aviso);
64
74
  d.avisar(aviso);
65
75
  return { ok: true, valor: null };
66
76
  }
67
- const j = leerJson(d, real, `el package.json de ${PAQUETE_DEL_CHAT} instalado`);
77
+ const j = leerJson(d, real, `the installed package.json of ${PAQUETE_DEL_CHAT}`);
68
78
  if (j.ok)
69
79
  return j;
70
80
  // Existía hace un instante: si ahora no está, tampoco se anota «no está».
71
- return { ok: false, resultado: j.noExiste ? fallo(SALIDA.inesperado, "paquete_ilegible", `No leo el package.json de ${PAQUETE_DEL_CHAT} instalado.`) : j.resultado };
81
+ return { ok: false, resultado: j.noExiste ? fallo(SALIDA.inesperado, "paquete_ilegible", `I can't read the installed package.json of ${PAQUETE_DEL_CHAT}.`) : j.resultado };
72
82
  }
73
83
  if (dir === raiz || path.dirname(dir) === dir)
74
84
  return { ok: true, valor: null };
@@ -78,7 +88,7 @@ async function estadoDelArchivo(d, raizGit, existe) {
78
88
  const git = async (args) => {
79
89
  const r = await d.git(args, raizGit);
80
90
  if (r.codigo !== 0)
81
- throw new Error(`git ${args[0]} falló; no escribo ${RUTA_DE_LA_INSTALACION} sin saber si tiene cambios.`);
91
+ throw new Error(`git ${args[0]} failed; I'm not writing ${RUTA_DE_LA_INSTALACION} without knowing whether it has changes.`);
82
92
  return r.stdout;
83
93
  };
84
94
  const lineas = (await git(["status", "--porcelain", "--ignored", "--", RUTA_DE_LA_INSTALACION])).split("\n").filter((l) => l.trim() !== "");
@@ -93,7 +103,7 @@ async function estadoDelArchivo(d, raizGit, existe) {
93
103
  // sale limpio de control y de secretos; el JSON nunca lo lleva.
94
104
  function diff(antes, despues) {
95
105
  if (antes === null)
96
- return ["--- (no había archivo)", "+++ " + RUTA_DE_LA_INSTALACION, ...despues.trimEnd().split("\n").map((l) => "+ " + l)].join("\n");
106
+ return ["--- (there was no file)", "+++ " + RUTA_DE_LA_INSTALACION, ...despues.trimEnd().split("\n").map((l) => "+ " + l)].join("\n");
97
107
  const a = antes.replace(/\r\n?/g, "\n").replace(/\n$/, "").split("\n").map(limpio);
98
108
  const b = despues.trimEnd().split("\n");
99
109
  const n = a.length, m = b.length;
@@ -101,7 +111,7 @@ function diff(antes, despues) {
101
111
  for (let i = n - 1; i >= 0; i--)
102
112
  for (let j = m - 1; j >= 0; j--)
103
113
  lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
104
- const lineas = ["--- " + RUTA_DE_LA_INSTALACION + " (antes)", "+++ " + RUTA_DE_LA_INSTALACION];
114
+ const lineas = ["--- " + RUTA_DE_LA_INSTALACION + " (before)", "+++ " + RUTA_DE_LA_INSTALACION];
105
115
  let i = 0, j = 0;
106
116
  while (i < n || j < m) {
107
117
  if (i < n && j < m && a[i] === b[j])
@@ -113,34 +123,53 @@ function diff(antes, despues) {
113
123
  }
114
124
  return lineas.join("\n");
115
125
  }
116
- const resumen = (x) => `skill ${x.skill}, terminal ${x.terminal}, chat declarado ${x.chat.declarado ?? "—"} / instalado ${x.chat.instalado ?? "—"}`;
126
+ const resumen = (x) => `skill ${x.skill}, terminal ${x.terminal}, chat declared ${x.chat.declarado ?? "—"} / installed ${x.chat.instalado ?? "—"}${x.archivo !== null ? `, file ${x.archivo}` : ""}`;
117
127
  export async function instalacionAnotar(d, flags) {
118
128
  if (flags.skill === null)
119
- return fallo(SALIDA.entradaInvalida, "falta_skill", "Falta la versión de la skill: `supcoflow instalacion anotar --skill 0.1.0`.");
129
+ return fallo(SALIDA.entradaInvalida, "falta_skill", "The skill version is missing: `supcoflow install record --skill 0.1.0`.");
120
130
  if (!esSemver(flags.skill))
121
- return fallo(SALIDA.entradaInvalida, "skill_invalida", "--skill va con una versión semver (por ejemplo 0.1.0).");
131
+ return fallo(SALIDA.entradaInvalida, "skill_invalida", "--skill goes with a semver version (for example 0.1.0).");
132
+ // El archivo del chat se mira antes de cualquier pedido: una ruta imposible no gasta una vuelta al motor.
133
+ let archivoPedido = null;
134
+ if (flags.file !== undefined && flags.file !== null) {
135
+ const v = validarElArchivoDelChat(flags.file);
136
+ if (!v.ok)
137
+ return fallo(SALIDA.entradaInvalida, "archivo_invalido", v.motivo);
138
+ archivoPedido = v.ruta;
139
+ }
122
140
  // ─── La raíz y la app, antes de cualquier pedido ───
123
141
  let raizGit;
142
+ let sinClon;
124
143
  try {
125
- raizGit = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).raiz;
144
+ ({ raiz: raizGit, sinClon } = await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd }));
126
145
  }
127
146
  catch (e) {
128
147
  return resultadoDeFallo(e);
129
148
  }
130
149
  if (raizGit === null)
131
- return fallo(SALIDA.entradaInvalida, "sin_clon", `No tengo el clon de ese repo acá, y ${RUTA_DE_LA_INSTALACION} va en el clon: parate en el clon del repo.`);
150
+ return fallo(SALIDA.entradaInvalida, "sin_clon", `${sinClon ?? "I don't have that repo's clone here."} ${RUTA_DE_LA_INSTALACION} goes in the repo's clone: stand in it.`);
132
151
  const raiz = d.rutaReal(raizGit);
133
152
  if (raiz === null)
134
- return fallo(SALIDA.inesperado, "inesperado", "No pude resolver la raíz del repo.");
153
+ return fallo(SALIDA.inesperado, "inesperado", "I couldn't resolve the repo's root.");
135
154
  // `realpath` DESPUÉS de resolver: un `--app` que es un enlace hacia afuera tiene que quedar afuera (Review Focus 4).
136
155
  const app = d.rutaReal(path.resolve(raiz, flags.app ?? "."));
137
156
  if (app === null)
138
- return fallo(SALIDA.entradaInvalida, "app_inexistente", "La carpeta de --app no existe (va relativa a la raíz del repo).");
157
+ return fallo(SALIDA.entradaInvalida, "app_inexistente", "The --app folder doesn't exist (it's relative to the repo root).");
139
158
  if (!adentro(raiz, app))
140
- return fallo(SALIDA.entradaInvalida, "app_fuera_del_repo", "--app tiene que ser una carpeta dentro del repo (relativa a su raíz).");
159
+ return fallo(SALIDA.entradaInvalida, "app_fuera_del_repo", "--app has to be a folder inside the repo (relative to its root).");
141
160
  // Un archivo no es una app: sin esto, leer `<archivo>/package.json` daba ENOTDIR y salía `paquete_ilegible`.
142
161
  if (!d.esCarpeta(app))
143
- return fallo(SALIDA.entradaInvalida, "app_inexistente", "--app tiene que ser una carpeta (la de la app), no un archivo.");
162
+ return fallo(SALIDA.entradaInvalida, "app_inexistente", "--app has to be a folder (the app's), not a file.");
163
+ // `--file`: ni un enlace simbólico ni un camino que pase por uno (saldría del repo sin que se vea). El archivo todavía
164
+ // puede no existir (con `--dry-run` se anota antes de escribirlo), pero ninguna carpeta del camino puede ser un enlace.
165
+ if (archivoPedido !== null) {
166
+ const partes = archivoPedido.split("/");
167
+ for (let i = 1; i <= partes.length; i++) {
168
+ if (d.esEnlace(path.join(raiz, ...partes.slice(0, i)))) {
169
+ return fallo(SALIDA.entradaInvalida, "archivo_invalido", `--file can't be (or go through) a symlink: ${partes.slice(0, i).join("/")} is one.`);
170
+ }
171
+ }
172
+ }
144
173
  // ─── Proyecto → configuración → package.json de la app → paquete instalado → armar ───
145
174
  const p = await proyectoDelRepo(d, flags);
146
175
  if (!p.ok)
@@ -152,32 +181,36 @@ export async function instalacionAnotar(d, flags) {
152
181
  catch (e) {
153
182
  return resultadoDeFallo(e);
154
183
  }
155
- const deLaApp = leerJson(d, path.join(app, "package.json"), "el package.json de la app");
184
+ const deLaApp = leerJson(d, path.join(app, "package.json"), "the app's package.json");
156
185
  if (!deLaApp.ok && !deLaApp.noExiste)
157
186
  return deLaApp.resultado;
158
187
  const avisos = [];
159
188
  const instalado = paqueteInstalado(d, raiz, app, avisos);
160
189
  if (!instalado.ok)
161
190
  return instalado.resultado;
191
+ // ─── El archivo de ahora: se lee antes de armar, porque `--file` ausente conserva el `archivo` ya anotado ───
192
+ const archivo = path.join(raiz, RUTA_DE_LA_INSTALACION);
193
+ // Una `.supcoflow` que es un enlace hacia afuera: ni se lee (el diff mostraría un archivo de afuera) ni se escribe.
194
+ const carpeta = d.rutaReal(path.dirname(archivo));
195
+ if (carpeta !== null && !adentro(raiz, carpeta))
196
+ return fallo(SALIDA.inesperado, "inesperado", `${path.dirname(RUTA_DE_LA_INSTALACION)} isn't a folder of the repo (a symlink?); I'm not writing there.`);
197
+ const actual = d.leerArchivoChico(archivo, TOPE_DEL_ARCHIVO);
198
+ const existe = !("error" in actual) || actual.error !== NO_LEIDO.noExiste;
199
+ const antes = "bytes" in actual ? Buffer.from(actual.bytes).toString("utf8") : null;
200
+ // Re-correr sin `--file` no borra el archivo que ya se anotó; un v1 (sin `archivo`) da null, sin error.
201
+ const archivoDelChat = archivoPedido ?? archivoAnotado(antes);
162
202
  let x;
163
203
  try {
164
- x = armarInstalacion({ skill: flags.skill, terminal: d.terminal, paqueteDeLaApp: deLaApp.ok ? deLaApp.valor : null, paqueteInstalado: instalado.valor, configuracion });
204
+ x = armarInstalacion({ skill: flags.skill, terminal: d.terminal, paqueteDeLaApp: deLaApp.ok ? deLaApp.valor : null, paqueteInstalado: instalado.valor, configuracion, archivo: archivoDelChat });
165
205
  }
166
206
  catch (e) {
167
207
  return resultadoDeFallo(e);
168
208
  }
169
209
  const nuevo = serializar(x);
170
210
  // ─── ¿Se puede escribir? ───
171
- const archivo = path.join(raiz, RUTA_DE_LA_INSTALACION);
172
- // Una `.supcoflow` que es un enlace hacia afuera: ni se lee (el diff mostraría un archivo de afuera) ni se escribe.
173
- const carpeta = d.rutaReal(path.dirname(archivo));
174
- if (carpeta !== null && !adentro(raiz, carpeta))
175
- return fallo(SALIDA.inesperado, "inesperado", `${path.dirname(RUTA_DE_LA_INSTALACION)} no es una carpeta del repo (¿un enlace?); no escribo ahí.`);
176
- const actual = d.leerArchivoChico(archivo, TOPE_DEL_ARCHIVO);
177
- const existe = !("error" in actual) || actual.error !== "no existe";
178
- const json = { ok: true, ruta: RUTA_DE_LA_INSTALACION, instalacion: x, ...(avisos.length > 0 ? { avisos } : {}) };
211
+ const json = { ok: true, ruta: RUTA_DE_LA_INSTALACION, instalacion: x, archivo: x.archivo, ...(avisos.length > 0 ? { avisos } : {}) };
179
212
  if ("bytes" in actual && Buffer.from(actual.bytes).equals(Buffer.from(nuevo, "utf8"))) {
180
- return { codigo: SALIDA.ok, json: { ...json, ...(flags.dryRun ? { dry_run: true } : {}), sin_cambios: true }, texto: `${RUTA_DE_LA_INSTALACION} ya dice eso (${resumen(x)}); no lo toqué.` };
213
+ return { codigo: SALIDA.ok, json: { ...json, ...(flags.dryRun ? { dry_run: true } : {}), sin_cambios: true }, texto: `${RUTA_DE_LA_INSTALACION} already says that (${resumen(x)}); I didn't touch it.` };
181
214
  }
182
215
  let estado;
183
216
  try {
@@ -187,21 +220,20 @@ export async function instalacionAnotar(d, flags) {
187
220
  return resultadoDeFallo(e);
188
221
  }
189
222
  if (estado === "cambios_sin_commit") {
190
- return fallo(SALIDA.haceFaltaUnaPersona, "cambios_sin_commit", `${RUTA_DE_LA_INSTALACION} tiene cambios sin commit (o se borró sin commit); no lo toqué. Commitealos o descartalos y volvé a correr.`);
223
+ return fallo(SALIDA.haceFaltaUnaPersona, "cambios_sin_commit", `${RUTA_DE_LA_INSTALACION} has uncommitted changes (or was deleted without a commit); I didn't touch it. Commit or discard them and run again.`);
191
224
  }
192
225
  if (estado === "archivo_no_versionado") {
193
- return fallo(SALIDA.haceFaltaUnaPersona, "archivo_no_versionado", `Ya hay un ${RUTA_DE_LA_INSTALACION} que git no guarda (sin commit o ignorado); no lo pisé. Commitealo o borralo y volvé a correr.`);
226
+ return fallo(SALIDA.haceFaltaUnaPersona, "archivo_no_versionado", `There's already a ${RUTA_DE_LA_INSTALACION} that git doesn't track (uncommitted or ignored); I didn't overwrite it. Commit or delete it and run again.`);
194
227
  }
195
- const antes = "bytes" in actual ? Buffer.from(actual.bytes).toString("utf8") : null;
196
- const cambios = existe && antes === null ? `(el archivo anterior no se lee)\n${diff(null, nuevo)}` : diff(antes, nuevo);
228
+ const cambios = existe && antes === null ? `(the previous file can't be read)\n${diff(null, nuevo)}` : diff(antes, nuevo);
197
229
  if (flags.dryRun) {
198
- return { codigo: SALIDA.ok, json: { ...json, dry_run: true, sin_cambios: false }, texto: `Con --dry-run no escribo; sin él anotaría (${resumen(x)}):\n${cambios}` };
230
+ return { codigo: SALIDA.ok, json: { ...json, dry_run: true, sin_cambios: false }, texto: `With --dry-run I don't write; without it I'd record (${resumen(x)}):\n${cambios}` };
199
231
  }
200
232
  try {
201
233
  d.escribir(archivo, nuevo);
202
234
  }
203
235
  catch {
204
- return fallo(SALIDA.inesperado, "archivo_no_escrito", `No pude escribir ${RUTA_DE_LA_INSTALACION}. Volvé a correr \`supcoflow instalacion anotar\`.`);
236
+ return fallo(SALIDA.inesperado, "archivo_no_escrito", `I couldn't write ${RUTA_DE_LA_INSTALACION}. Run \`supcoflow install record\` again.`);
205
237
  }
206
- return { codigo: SALIDA.ok, json: { ...json, sin_cambios: false }, texto: `Anoté ${RUTA_DE_LA_INSTALACION} (${resumen(x)}); commitealo cuando quieras.\n${cambios}` };
238
+ return { codigo: SALIDA.ok, json: { ...json, sin_cambios: false }, texto: `Recorded ${RUTA_DE_LA_INSTALACION} (${resumen(x)}); commit it whenever you like.\n${cambios}` };
207
239
  }