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.
- package/README.md +253 -176
- 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/contexto.js +102 -0
- package/dist/comandos/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +162 -152
- package/dist/comandos/guia.js +150 -62
- package/dist/comandos/instalacion.js +73 -41
- package/dist/comandos/login.js +25 -24
- package/dist/comandos/logout.js +17 -16
- package/dist/comandos/probar.js +177 -64
- package/dist/comandos/proyecto-del-repo.js +12 -2
- package/dist/comandos/proyecto.js +48 -34
- 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 +51 -0
- package/dist/entorno.js +57 -26
- 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/guardado/lugar.js +63 -0
- package/dist/guardado/protegido.js +38 -0
- package/dist/guardado/repo.js +98 -0
- package/dist/guia.js +3 -1
- package/dist/idempotencia.js +16 -28
- package/dist/instalacion.js +50 -11
- package/dist/lectura.js +10 -0
- package/dist/pruebas-guardadas.js +40 -22
- package/dist/repo-local.js +95 -13
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
package/dist/comandos/guia.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// src/comandos/guia.ts
|
|
2
|
-
// `
|
|
3
|
-
// (`.supcoflow/
|
|
4
|
-
// lee el archivo, manda UN pedido y traduce la respuesta a
|
|
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 ? "
|
|
19
|
-
const NOMBRE_DEL_ORIGEN = { repo: "
|
|
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 =
|
|
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",
|
|
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 ===
|
|
37
|
-
return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "
|
|
38
|
-
|
|
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", `${
|
|
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 : `
|
|
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 !== "" ? ` (
|
|
66
|
-
const que = error === "guia_no_subida" ? "
|
|
67
|
-
return con(SALIDA.esperandoAUnaPersona, { rama: c.rama ?? null, commit: c.commit ?? null }, `${que}${donde}:
|
|
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 `
|
|
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 : "") }; }) }, [`${
|
|
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: `
|
|
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: `
|
|
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.
|
|
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
|
-
|
|
139
|
-
|
|
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")}
|
|
145
|
-
: `POST ${rutaDe(p.proyecto, "/guia/publicar")}
|
|
146
|
-
const json = huella !== null
|
|
147
|
-
|
|
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" ? ` (
|
|
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)}
|
|
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 ? `
|
|
160
|
-
|
|
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)}
|
|
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
|
-
? `
|
|
166
|
-
: `
|
|
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: "
|
|
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(`
|
|
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 !== "" ? `
|
|
196
|
-
lineas.push(`
|
|
197
|
-
lineas.push("
|
|
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(`
|
|
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("
|
|
251
|
+
lineas.push("It's up to date with the branch.");
|
|
203
252
|
else if (alDia === false)
|
|
204
|
-
lineas.push("
|
|
253
|
+
lineas.push("The branch has another version: run `supcoflow guide publish`.");
|
|
205
254
|
else
|
|
206
|
-
lineas.push("
|
|
255
|
+
lineas.push("I couldn't check the branch right now.");
|
|
207
256
|
}
|
|
208
257
|
}
|
|
209
258
|
else {
|
|
210
|
-
lineas.push(`
|
|
211
|
-
lineas.push("
|
|
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(`
|
|
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
|
|
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, "
|
|
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: `
|
|
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: `
|
|
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
|
-
// `
|
|
3
|
-
// `.supcoflow/instalacion.json` con qué versiones se instaló el chat
|
|
4
|
-
// `package.json` de la app y del paquete instalado; nunca ejecuta
|
|
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 ===
|
|
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", `
|
|
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}
|
|
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)}
|
|
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, `
|
|
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", `
|
|
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]}
|
|
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
|
|
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 + " (
|
|
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
|
|
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", "
|
|
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
|
|
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 =
|
|
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",
|
|
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", "
|
|
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", "
|
|
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
|
|
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
|
|
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"), "
|
|
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
|
|
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}
|
|
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}
|
|
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", `
|
|
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
|
|
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: `
|
|
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", `
|
|
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: `
|
|
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
|
}
|