supcoflow 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -162
- package/dist/alias.js +60 -0
- package/dist/api.js +121 -16
- package/dist/argumentos.js +92 -78
- package/dist/cli.js +42 -31
- package/dist/comandos/clave-vigente.js +2 -2
- package/dist/comandos/claves.js +25 -25
- package/dist/comandos/configurar.js +40 -40
- package/dist/comandos/datos-y-politica.js +35 -23
- package/dist/comandos/describir.js +170 -138
- package/dist/comandos/guia.js +148 -61
- package/dist/comandos/instalacion.js +71 -40
- package/dist/comandos/login.js +25 -24
- package/dist/comandos/logout.js +17 -16
- package/dist/comandos/probar.js +359 -0
- package/dist/comandos/proyecto-del-repo.js +1 -1
- package/dist/comandos/proyecto.js +16 -16
- package/dist/comandos/quien-soy.js +8 -7
- package/dist/comandos/repo-conectar.js +9 -9
- package/dist/comandos/widget.js +9 -5
- package/dist/diagnostico.js +48 -0
- package/dist/entorno.js +32 -18
- package/dist/flags-por-comando.js +25 -19
- package/dist/guardado/archivo.js +2 -2
- package/dist/guardado/index.js +12 -8
- package/dist/guardado/llavero.js +5 -5
- package/dist/guia.js +3 -1
- package/dist/idempotencia.js +6 -6
- package/dist/instalacion.js +50 -11
- package/dist/lectura.js +10 -0
- package/dist/pruebas-guardadas.js +101 -0
- package/dist/repo-local.js +6 -6
- package/dist/secreto-a-archivo.js +6 -6
- package/package.json +1 -1
package/dist/comandos/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,11 +17,20 @@ 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;
|
|
25
36
|
try {
|
|
@@ -29,42 +40,64 @@ async function leerLaGuia(d, flags) {
|
|
|
29
40
|
return { ok: false, resultado: resultadoDeFallo(e) };
|
|
30
41
|
}
|
|
31
42
|
if (raiz === null) {
|
|
32
|
-
return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `
|
|
43
|
+
return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `I don't have that repo's clone here, and the guide is read from ${RUTA_DE_LA_GUIA}: stand in the repo's clone.`) };
|
|
44
|
+
}
|
|
45
|
+
let ruta = RUTA_DE_LA_GUIA;
|
|
46
|
+
let aviso = null;
|
|
47
|
+
let leido = d.leerArchivoChico(path.join(raiz, ruta), MAX_BYTES_DE_LA_GUIA);
|
|
48
|
+
if ("error" in leido && leido.error === NO_LEIDO.noExiste) {
|
|
49
|
+
const vieja = d.leerArchivoChico(path.join(raiz, RUTA_VIEJA_DE_LA_GUIA), MAX_BYTES_DE_LA_GUIA);
|
|
50
|
+
if ("error" in vieja && vieja.error === NO_LEIDO.noExiste) {
|
|
51
|
+
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.`) };
|
|
52
|
+
}
|
|
53
|
+
ruta = RUTA_VIEJA_DE_LA_GUIA;
|
|
54
|
+
aviso = "nombre_viejo";
|
|
55
|
+
leido = vieja;
|
|
56
|
+
}
|
|
57
|
+
else if (!("error" in leido)) {
|
|
58
|
+
// Con `guide.md` leído, el viejo sólo se mira para avisar que se ignora (no se lee su contenido).
|
|
59
|
+
const vieja = d.leerArchivoChico(path.join(raiz, RUTA_VIEJA_DE_LA_GUIA), 1);
|
|
60
|
+
if (!("error" in vieja) || vieja.error !== NO_LEIDO.noExiste)
|
|
61
|
+
aviso = "nombre_viejo_ignorado";
|
|
33
62
|
}
|
|
34
|
-
const leido = d.leerArchivoChico(path.join(raiz, RUTA_DE_LA_GUIA), MAX_BYTES_DE_LA_GUIA);
|
|
35
63
|
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)}.`) };
|
|
64
|
+
if (leido.error === NO_LEIDO.demasiadoGrande)
|
|
65
|
+
return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_muy_grande", `${ruta} is over ${MAX_BYTES_DE_LA_GUIA / 1024} KB: shorten it.`) };
|
|
66
|
+
return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `I can't read ${ruta}: ${limpio(leido.error)}.`) };
|
|
41
67
|
}
|
|
42
68
|
try {
|
|
43
69
|
// ignoreBOM: el BOM se CONSERVA en el texto; quitarlo es regla de la huella (`huellaDeGuia`), no de la lectura.
|
|
44
70
|
// 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) };
|
|
71
|
+
return { ok: true, raiz, ruta, aviso, texto: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(leido.bytes) };
|
|
46
72
|
}
|
|
47
73
|
catch {
|
|
48
|
-
return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `${
|
|
74
|
+
return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `${ruta} isn't valid UTF-8.`) };
|
|
49
75
|
}
|
|
50
76
|
}
|
|
77
|
+
// El aviso del nombre viaja en el JSON (`aviso`) y como última línea del texto, en lo que salió bien.
|
|
78
|
+
function conAviso(r, aviso) {
|
|
79
|
+
// Si el motor ya dijo el suyo (por GitHub lee el repo él mismo, en un SHA), manda ése.
|
|
80
|
+
if (aviso === null || r.codigo !== SALIDA.ok || "aviso" in r.json)
|
|
81
|
+
return r;
|
|
82
|
+
return { ...r, json: { ...r.json, aviso }, texto: `${r.texto}\n${AVISOS_DEL_NOMBRE[aviso]}` };
|
|
83
|
+
}
|
|
51
84
|
// ─── El desenlace propio de la guía, encima del de configurar ───
|
|
52
|
-
function desenlaceDeGuia(r, ok) {
|
|
85
|
+
function desenlaceDeGuia(r, ok, ruta = RUTA_DE_LA_GUIA) {
|
|
53
86
|
const c = r.cuerpo;
|
|
54
87
|
// 2xx, 401, 5xx y cuerpos raros los resuelve el de configurar. Ojo: la terminal no distingue 500
|
|
55
88
|
// connect_no_disponible de 502 github_no_disponible (ambos llegan como falla del servicio): los dos son 1.
|
|
56
89
|
if (r.status < 400 || r.status === 401 || r.status >= 500 || typeof c !== "object" || c === null)
|
|
57
90
|
return desenlace(r, ok);
|
|
58
91
|
const error = typeof c.error === "string" ? c.error : "rechazado";
|
|
59
|
-
const message = limpio(typeof c.message === "string" ? c.message : `
|
|
92
|
+
const message = limpio(typeof c.message === "string" ? c.message : `The engine rejected the request (${r.status}).`);
|
|
60
93
|
const con = (codigo, extra = {}, texto = message) => ({ codigo, json: { ok: false, error, message: taparSecretos(message), ...extra }, texto });
|
|
61
94
|
switch (error) {
|
|
62
95
|
case "guia_no_subida":
|
|
63
96
|
case "guia_distinta": {
|
|
64
97
|
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}:
|
|
98
|
+
const donde = rama !== "" ? ` (branch ${rama}${commit !== "" ? `, commit ${corto(commit)}` : ""})` : "";
|
|
99
|
+
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";
|
|
100
|
+
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
101
|
}
|
|
69
102
|
case "instalacion_ausente":
|
|
70
103
|
case "repo_fuera_de_alcance":
|
|
@@ -74,9 +107,9 @@ function desenlaceDeGuia(r, ok) {
|
|
|
74
107
|
const errores = Array.isArray(c.errores) ? c.errores : [];
|
|
75
108
|
const lineas = errores.map((e) => {
|
|
76
109
|
const x = (e ?? {});
|
|
77
|
-
return `
|
|
110
|
+
return `line ${typeof x.linea === "number" ? x.linea : "?"}: ${cadena(x.motivo)}`;
|
|
78
111
|
});
|
|
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 : "") }; }) }, [`${
|
|
112
|
+
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
113
|
}
|
|
81
114
|
default:
|
|
82
115
|
return desenlace(r, ok);
|
|
@@ -90,13 +123,13 @@ export async function guiaRevisar(d, flags) {
|
|
|
90
123
|
if (!p.ok)
|
|
91
124
|
return p.resultado;
|
|
92
125
|
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: `
|
|
126
|
+
const haria = `POST ${rutaDe(p.proyecto, "/guia/revisar")} (${new TextEncoder().encode(g.texto).length} bytes, from ${g.ruta})`;
|
|
127
|
+
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
128
|
}
|
|
96
129
|
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
|
-
}));
|
|
130
|
+
return conAviso(desenlaceDeGuia(await d.revisar(d.api, p.clave, p.proyecto.id, g.texto, d.fetch), (c) => ({
|
|
131
|
+
codigo: SALIDA.ok, json: { ok: true, secciones: c.secciones ?? null, ruta: g.ruta }, texto: `The guide is well formed: ${secciones(c.secciones)}.`,
|
|
132
|
+
}), g.ruta), g.aviso);
|
|
100
133
|
}
|
|
101
134
|
catch (e) {
|
|
102
135
|
return resultadoDeFallo(e);
|
|
@@ -130,42 +163,53 @@ export async function guiaPublicar(d, flags) {
|
|
|
130
163
|
const p = await proyectoDelRepo(d, flags);
|
|
131
164
|
if (!p.ok)
|
|
132
165
|
return p.resultado;
|
|
133
|
-
const github = flags.
|
|
166
|
+
const github = flags.fromGithub === true;
|
|
134
167
|
// La vía de GitHub manda la huella del archivo local TAL CUAL (con BOM y \r\n: normalizarla es regla de
|
|
135
168
|
// `huellaDeGuia`). La directa manda el contenido crudo y el motor calcula la huella: la terminal no la necesita.
|
|
136
169
|
const huella = github ? huellaDeGuia(g.texto) : null;
|
|
137
170
|
const decl = github ? {} : await declarado(d, g.raiz);
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
// La directa manda además la ruta que se leyó de verdad (dato declarado, como el commit y la rama). Por GitHub el
|
|
172
|
+
// motor resuelve los nombres él mismo, sobre un solo SHA de la rama principal.
|
|
173
|
+
const cuerpo = huella !== null ? { huella } : { contenido: g.texto, ruta: g.ruta, ...decl };
|
|
174
|
+
const cuando = decl.commit ? `commit ${decl.commit.slice(0, 7)}${decl.rama ? ` of ${cadena(decl.rama)}` : ""} declared` : "no commit";
|
|
140
175
|
// El JSON del dry-run pasa por la misma limpieza que el texto (control primero, secretos después): la rama la
|
|
141
176
|
// decide quien escribió el clon, y un ESC en ella no debe llegar a la terminal por ninguna de las dos salidas.
|
|
142
177
|
if (flags.dryRun) {
|
|
143
178
|
const haria = huella !== null
|
|
144
|
-
? `POST ${rutaDe(p.proyecto, "/guia/publicar")}
|
|
145
|
-
: `POST ${rutaDe(p.proyecto, "/guia/publicar")}
|
|
146
|
-
const json = huella !== null
|
|
147
|
-
|
|
179
|
+
? `POST ${rutaDe(p.proyecto, "/guia/publicar")} with the fingerprint ${huella.slice(0, 12)}…`
|
|
180
|
+
: `POST ${rutaDe(p.proyecto, "/guia/publicar")} with the content (${new TextEncoder().encode(g.texto).length} bytes, from ${g.ruta}), ${cuando}`;
|
|
181
|
+
const json = huella !== null
|
|
182
|
+
? { ok: true, dry_run: true, huella, ruta: g.ruta, haria }
|
|
183
|
+
: { ok: true, dry_run: true, ruta: g.ruta, commit: decl.commit ? cadena(decl.commit) : null, rama: decl.rama ? cadena(decl.rama) : null, haria };
|
|
184
|
+
return conAviso({ codigo: SALIDA.ok, json, texto: `With --dry-run I do nothing; without it: ${haria}.` }, g.aviso);
|
|
148
185
|
}
|
|
149
186
|
try {
|
|
150
|
-
return desenlaceDeGuia(await d.publicar(d.api, p.clave, p.proyecto.id, cuerpo, d.fetch), (c) => {
|
|
187
|
+
return conAviso(desenlaceDeGuia(await d.publicar(d.api, p.clave, p.proyecto.id, cuerpo, d.fetch), (c) => {
|
|
151
188
|
const guia = (c.guia ?? {});
|
|
152
189
|
const ya = c.ya_publicada === true;
|
|
153
190
|
const rem = c.reemplaza;
|
|
154
|
-
const reemplaza = !ya && rem && rem.origen === "agente" && typeof rem.version === "number" ? ` (
|
|
191
|
+
const reemplaza = !ya && rem && rem.origen === "agente" && typeof rem.version === "number" ? ` (replaces our agent's v${rem.version})` : "";
|
|
155
192
|
const json = { ok: true, guia: c.guia ?? null, ya_publicada: ya, reemplaza: c.reemplaza ?? null };
|
|
193
|
+
// Una resubida idéntica devuelve la fila vieja con su ruta histórica: se dice, para que nadie crea que el
|
|
194
|
+
// renombre quedó registrado.
|
|
195
|
+
const ruta = cadena(guia.ruta);
|
|
196
|
+
const nota = ya && ruta !== "" && ruta !== g.ruta ? `\nThat version keeps the file name it was first published with (${ruta}).` : "";
|
|
156
197
|
if (github) {
|
|
157
|
-
const base = `commit ${corto(guia.commit)}
|
|
198
|
+
const base = `commit ${corto(guia.commit)} of ${cadena(guia.rama)}, ${secciones(guia.secciones)}`;
|
|
158
199
|
// 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
|
-
|
|
200
|
+
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}`;
|
|
201
|
+
const delMotor = typeof c.aviso === "string" && c.aviso in AVISOS_DEL_NOMBRE ? c.aviso : null;
|
|
202
|
+
return delMotor === null
|
|
203
|
+
? { codigo: SALIDA.ok, json, texto }
|
|
204
|
+
: { codigo: SALIDA.ok, json: { ...json, aviso: delMotor }, texto: `${texto}\n${AVISOS_DEL_NOMBRE[delMotor]}` };
|
|
161
205
|
}
|
|
162
206
|
// 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)}
|
|
207
|
+
const origen = decl.commit ? `commit ${decl.commit.slice(0, 7)} declared by the terminal (not verified in GitHub)` : "no commit";
|
|
164
208
|
const texto = ya
|
|
165
|
-
? `
|
|
166
|
-
: `
|
|
209
|
+
? `It was already uploaded: ${secciones(guia.secciones)}. It stayed as the active guide; ${origen}.${nota}`
|
|
210
|
+
: `Guide uploaded: ${secciones(guia.secciones)}. It's the active guide${reemplaza}; ${origen}.${nota}`;
|
|
167
211
|
return { codigo: SALIDA.ok, json: { ...json, commit_declarado: decl.commit ?? null }, texto };
|
|
168
|
-
});
|
|
212
|
+
}, g.ruta), g.aviso);
|
|
169
213
|
}
|
|
170
214
|
catch (e) {
|
|
171
215
|
return resultadoDeFallo(e);
|
|
@@ -183,35 +227,39 @@ export async function guiaVer(d, flags) {
|
|
|
183
227
|
const alDia = rama && typeof rama.al_dia === "boolean" ? rama.al_dia : null;
|
|
184
228
|
const json = { ok: true, guia: guia ?? null, otra: otra ?? null, al_dia: alDia, rama: rama ?? null };
|
|
185
229
|
if (!guia)
|
|
186
|
-
return { codigo: SALIDA.ok, json, texto: "
|
|
230
|
+
return { codigo: SALIDA.ok, json, texto: "This project has no active guide: `supcoflow guide publish` uploads the repo's." };
|
|
187
231
|
const lineas = [];
|
|
188
232
|
if (guia.origen === "repo") {
|
|
189
|
-
lineas.push(`
|
|
233
|
+
lineas.push(`Active guide: ${NOMBRE_DEL_ORIGEN.repo} (version ${guia.version}, ${secciones(guia.secciones)}).`);
|
|
234
|
+
// La ruta es la que se leyó de verdad; por la terminal es declarada (el motor no la verificó), por GitHub no.
|
|
235
|
+
const ruta = cadena(guia.ruta);
|
|
236
|
+
if (ruta !== "")
|
|
237
|
+
lineas.push(`File: ${ruta}${guia.procedencia === "terminal" ? " (declared by the terminal)" : ""}.`);
|
|
190
238
|
if (guia.procedencia === "terminal") {
|
|
191
239
|
// Subida desde la terminal: el motor NO comparó nada contra GitHub a propósito (al_dia null), así que "no pude
|
|
192
240
|
// comprobar la rama" sería falso. Commit y rama son lo que declaró quien subió; el id crudo de quien publicó
|
|
193
241
|
// (usr_…) no le dice nada a una persona y la terminal no resuelve nombres: queda sólo en el JSON.
|
|
194
242
|
const commit = cadena(guia.commit), rama = cadena(guia.rama);
|
|
195
|
-
const partes = [cadena(guia.repo), rama !== "" ? `
|
|
196
|
-
lineas.push(`
|
|
197
|
-
lineas.push("
|
|
243
|
+
const partes = [cadena(guia.repo), rama !== "" ? `branch ${rama}` : "", commit !== "" ? `commit ${corto(commit)} (declared)` : "no declared commit"].filter((x) => x !== "");
|
|
244
|
+
lineas.push(`Source: ${partes.join(", ")}.`);
|
|
245
|
+
lineas.push("Uploaded from the terminal; the commit is declared, not verified in GitHub.");
|
|
198
246
|
}
|
|
199
247
|
else {
|
|
200
|
-
lineas.push(`
|
|
248
|
+
lineas.push(`Source: ${cadena(guia.repo)}, branch ${cadena(guia.rama)}, commit ${corto(guia.commit)}, published by ${cadena(guia.publicada_por)}.`);
|
|
201
249
|
if (alDia === true)
|
|
202
|
-
lineas.push("
|
|
250
|
+
lineas.push("It's up to date with the branch.");
|
|
203
251
|
else if (alDia === false)
|
|
204
|
-
lineas.push("
|
|
252
|
+
lineas.push("The branch has another version: run `supcoflow guide publish`.");
|
|
205
253
|
else
|
|
206
|
-
lineas.push("
|
|
254
|
+
lineas.push("I couldn't check the branch right now.");
|
|
207
255
|
}
|
|
208
256
|
}
|
|
209
257
|
else {
|
|
210
|
-
lineas.push(`
|
|
211
|
-
lineas.push("
|
|
258
|
+
lineas.push(`Active guide: ${NOMBRE_DEL_ORIGEN.agente} (v${guia.version}, ${secciones(guia.secciones)}).`);
|
|
259
|
+
lineas.push("Source: our agent wrote it and a person approved it.");
|
|
212
260
|
}
|
|
213
261
|
if (otra && (otra.origen === "repo" || otra.origen === "agente")) {
|
|
214
|
-
lineas.push(`
|
|
262
|
+
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
263
|
}
|
|
216
264
|
return { codigo: SALIDA.ok, json, texto: lineas.join("\n") };
|
|
217
265
|
});
|
|
@@ -221,24 +269,63 @@ export async function guiaVer(d, flags) {
|
|
|
221
269
|
}
|
|
222
270
|
}
|
|
223
271
|
export async function guiaUsar(d, flags) {
|
|
224
|
-
const
|
|
272
|
+
const pedido = flags.source ?? null;
|
|
273
|
+
if (pedido === null)
|
|
274
|
+
return fallo(SALIDA.entradaInvalida, "falta_origen", "Say which: `supcoflow guide use --source repo` or `--source agent`.");
|
|
275
|
+
// A la API siempre viaja su valor (`agente`); `agente` también se acepta, por el alias viejo.
|
|
276
|
+
const origen = pedido === "repo" ? "repo" : pedido === "agent" || pedido === "agente" ? "agente" : null;
|
|
225
277
|
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`.");
|
|
278
|
+
return fallo(SALIDA.entradaInvalida, "origen_invalido", "--source is `repo` or `agent`.");
|
|
229
279
|
const p = await proyectoDelRepo(d, flags);
|
|
230
280
|
if (!p.ok)
|
|
231
281
|
return p.resultado;
|
|
232
282
|
if (flags.dryRun) {
|
|
233
283
|
const haria = `POST ${rutaDe(p.proyecto, "/guia/origen")} {origen: ${origen}}`;
|
|
234
|
-
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `
|
|
284
|
+
return { codigo: SALIDA.ok, json: { ok: true, dry_run: true, haria }, texto: `With --dry-run I do nothing; without it: ${haria}.` };
|
|
235
285
|
}
|
|
236
286
|
try {
|
|
237
287
|
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: `
|
|
288
|
+
codigo: SALIDA.ok, json: { ok: true, guia: c.guia ?? null }, texto: `Now the active guide is ${NOMBRE_DEL_ORIGEN[origen]}.`,
|
|
239
289
|
}));
|
|
240
290
|
}
|
|
241
291
|
catch (e) {
|
|
242
292
|
return resultadoDeFallo(e);
|
|
243
293
|
}
|
|
244
294
|
}
|
|
295
|
+
// `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
|
|
296
|
+
// vieja. Una sola publicación dice «live»: la que es la vigente (el motor lo decide; acá sólo se muestra).
|
|
297
|
+
export async function guiaVersiones(d, flags) {
|
|
298
|
+
const p = await proyectoDelRepo(d, flags);
|
|
299
|
+
if (!p.ok)
|
|
300
|
+
return p.resultado;
|
|
301
|
+
try {
|
|
302
|
+
return desenlaceDeGuia(await d.versiones(d.api, p.clave, p.proyecto.id, d.fetch), (c) => {
|
|
303
|
+
const vigente = typeof c.vigente === "object" && c.vigente !== null ? c.vigente : null;
|
|
304
|
+
const publicaciones = Array.isArray(c.publicaciones) ? c.publicaciones.map((x) => (typeof x === "object" && x !== null ? x : {})) : [];
|
|
305
|
+
const json = { ok: true, vigente, publicaciones };
|
|
306
|
+
const lineas = [];
|
|
307
|
+
if (vigente === null)
|
|
308
|
+
lineas.push("Active now: no guide.");
|
|
309
|
+
else if (vigente.origen === "agente")
|
|
310
|
+
lineas.push(`Active now: ${NOMBRE_DEL_ORIGEN.agente} guide, v${vigente.version} (${secciones(vigente.secciones)}).`);
|
|
311
|
+
else
|
|
312
|
+
lineas.push(`Active now: ${NOMBRE_DEL_ORIGEN.repo} guide, version ${vigente.version} (${secciones(vigente.secciones)}).`);
|
|
313
|
+
lineas.push(publicaciones.length === 0 ? "Published from the repo: none." : "Published from the repo, newest first:");
|
|
314
|
+
for (const x of publicaciones) {
|
|
315
|
+
const estado = x.live === true || x.estado === "live" ? "live" : "published";
|
|
316
|
+
const donde = [
|
|
317
|
+
x.procedencia === "terminal" ? "uploaded from the terminal (declared)" : x.procedencia === "github" ? "verified in GitHub" : "",
|
|
318
|
+
cadena(x.commit) !== "" ? `commit ${corto(x.commit)}` : "",
|
|
319
|
+
cadena(x.ruta),
|
|
320
|
+
cadena(x.publicada_por) !== "" ? `by ${cadena(x.publicada_por)}` : "",
|
|
321
|
+
cadena(x.publicada_en),
|
|
322
|
+
].filter((t) => t !== "");
|
|
323
|
+
lineas.push(` v${x.version} ${estado}${donde.length > 0 ? ` · ${donde.join(" · ")}` : ""}`);
|
|
324
|
+
}
|
|
325
|
+
return { codigo: SALIDA.ok, json, texto: lineas.join("\n") };
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
return resultadoDeFallo(e);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -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,12 +123,20 @@ 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;
|
|
124
142
|
try {
|
|
@@ -128,19 +146,29 @@ export async function instalacionAnotar(d, flags) {
|
|
|
128
146
|
return resultadoDeFallo(e);
|
|
129
147
|
}
|
|
130
148
|
if (raizGit === null)
|
|
131
|
-
return fallo(SALIDA.entradaInvalida, "sin_clon", `
|
|
149
|
+
return fallo(SALIDA.entradaInvalida, "sin_clon", `I don't have that repo's clone here, and ${RUTA_DE_LA_INSTALACION} goes in the clone: stand in the repo's clone.`);
|
|
132
150
|
const raiz = d.rutaReal(raizGit);
|
|
133
151
|
if (raiz === null)
|
|
134
|
-
return fallo(SALIDA.inesperado, "inesperado", "
|
|
152
|
+
return fallo(SALIDA.inesperado, "inesperado", "I couldn't resolve the repo's root.");
|
|
135
153
|
// `realpath` DESPUÉS de resolver: un `--app` que es un enlace hacia afuera tiene que quedar afuera (Review Focus 4).
|
|
136
154
|
const app = d.rutaReal(path.resolve(raiz, flags.app ?? "."));
|
|
137
155
|
if (app === null)
|
|
138
|
-
return fallo(SALIDA.entradaInvalida, "app_inexistente", "
|
|
156
|
+
return fallo(SALIDA.entradaInvalida, "app_inexistente", "The --app folder doesn't exist (it's relative to the repo root).");
|
|
139
157
|
if (!adentro(raiz, app))
|
|
140
|
-
return fallo(SALIDA.entradaInvalida, "app_fuera_del_repo", "--app
|
|
158
|
+
return fallo(SALIDA.entradaInvalida, "app_fuera_del_repo", "--app has to be a folder inside the repo (relative to its root).");
|
|
141
159
|
// Un archivo no es una app: sin esto, leer `<archivo>/package.json` daba ENOTDIR y salía `paquete_ilegible`.
|
|
142
160
|
if (!d.esCarpeta(app))
|
|
143
|
-
return fallo(SALIDA.entradaInvalida, "app_inexistente", "--app
|
|
161
|
+
return fallo(SALIDA.entradaInvalida, "app_inexistente", "--app has to be a folder (the app's), not a file.");
|
|
162
|
+
// `--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
|
|
163
|
+
// puede no existir (con `--dry-run` se anota antes de escribirlo), pero ninguna carpeta del camino puede ser un enlace.
|
|
164
|
+
if (archivoPedido !== null) {
|
|
165
|
+
const partes = archivoPedido.split("/");
|
|
166
|
+
for (let i = 1; i <= partes.length; i++) {
|
|
167
|
+
if (d.esEnlace(path.join(raiz, ...partes.slice(0, i)))) {
|
|
168
|
+
return fallo(SALIDA.entradaInvalida, "archivo_invalido", `--file can't be (or go through) a symlink: ${partes.slice(0, i).join("/")} is one.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
144
172
|
// ─── Proyecto → configuración → package.json de la app → paquete instalado → armar ───
|
|
145
173
|
const p = await proyectoDelRepo(d, flags);
|
|
146
174
|
if (!p.ok)
|
|
@@ -152,32 +180,36 @@ export async function instalacionAnotar(d, flags) {
|
|
|
152
180
|
catch (e) {
|
|
153
181
|
return resultadoDeFallo(e);
|
|
154
182
|
}
|
|
155
|
-
const deLaApp = leerJson(d, path.join(app, "package.json"), "
|
|
183
|
+
const deLaApp = leerJson(d, path.join(app, "package.json"), "the app's package.json");
|
|
156
184
|
if (!deLaApp.ok && !deLaApp.noExiste)
|
|
157
185
|
return deLaApp.resultado;
|
|
158
186
|
const avisos = [];
|
|
159
187
|
const instalado = paqueteInstalado(d, raiz, app, avisos);
|
|
160
188
|
if (!instalado.ok)
|
|
161
189
|
return instalado.resultado;
|
|
190
|
+
// ─── El archivo de ahora: se lee antes de armar, porque `--file` ausente conserva el `archivo` ya anotado ───
|
|
191
|
+
const archivo = path.join(raiz, RUTA_DE_LA_INSTALACION);
|
|
192
|
+
// Una `.supcoflow` que es un enlace hacia afuera: ni se lee (el diff mostraría un archivo de afuera) ni se escribe.
|
|
193
|
+
const carpeta = d.rutaReal(path.dirname(archivo));
|
|
194
|
+
if (carpeta !== null && !adentro(raiz, carpeta))
|
|
195
|
+
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.`);
|
|
196
|
+
const actual = d.leerArchivoChico(archivo, TOPE_DEL_ARCHIVO);
|
|
197
|
+
const existe = !("error" in actual) || actual.error !== NO_LEIDO.noExiste;
|
|
198
|
+
const antes = "bytes" in actual ? Buffer.from(actual.bytes).toString("utf8") : null;
|
|
199
|
+
// Re-correr sin `--file` no borra el archivo que ya se anotó; un v1 (sin `archivo`) da null, sin error.
|
|
200
|
+
const archivoDelChat = archivoPedido ?? archivoAnotado(antes);
|
|
162
201
|
let x;
|
|
163
202
|
try {
|
|
164
|
-
x = armarInstalacion({ skill: flags.skill, terminal: d.terminal, paqueteDeLaApp: deLaApp.ok ? deLaApp.valor : null, paqueteInstalado: instalado.valor, configuracion });
|
|
203
|
+
x = armarInstalacion({ skill: flags.skill, terminal: d.terminal, paqueteDeLaApp: deLaApp.ok ? deLaApp.valor : null, paqueteInstalado: instalado.valor, configuracion, archivo: archivoDelChat });
|
|
165
204
|
}
|
|
166
205
|
catch (e) {
|
|
167
206
|
return resultadoDeFallo(e);
|
|
168
207
|
}
|
|
169
208
|
const nuevo = serializar(x);
|
|
170
209
|
// ─── ¿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 } : {}) };
|
|
210
|
+
const json = { ok: true, ruta: RUTA_DE_LA_INSTALACION, instalacion: x, archivo: x.archivo, ...(avisos.length > 0 ? { avisos } : {}) };
|
|
179
211
|
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}
|
|
212
|
+
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
213
|
}
|
|
182
214
|
let estado;
|
|
183
215
|
try {
|
|
@@ -187,21 +219,20 @@ export async function instalacionAnotar(d, flags) {
|
|
|
187
219
|
return resultadoDeFallo(e);
|
|
188
220
|
}
|
|
189
221
|
if (estado === "cambios_sin_commit") {
|
|
190
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "cambios_sin_commit", `${RUTA_DE_LA_INSTALACION}
|
|
222
|
+
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
223
|
}
|
|
192
224
|
if (estado === "archivo_no_versionado") {
|
|
193
|
-
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_no_versionado", `
|
|
225
|
+
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
226
|
}
|
|
195
|
-
const
|
|
196
|
-
const cambios = existe && antes === null ? `(el archivo anterior no se lee)\n${diff(null, nuevo)}` : diff(antes, nuevo);
|
|
227
|
+
const cambios = existe && antes === null ? `(the previous file can't be read)\n${diff(null, nuevo)}` : diff(antes, nuevo);
|
|
197
228
|
if (flags.dryRun) {
|
|
198
|
-
return { codigo: SALIDA.ok, json: { ...json, dry_run: true, sin_cambios: false }, texto: `
|
|
229
|
+
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
230
|
}
|
|
200
231
|
try {
|
|
201
232
|
d.escribir(archivo, nuevo);
|
|
202
233
|
}
|
|
203
234
|
catch {
|
|
204
|
-
return fallo(SALIDA.inesperado, "archivo_no_escrito", `
|
|
235
|
+
return fallo(SALIDA.inesperado, "archivo_no_escrito", `I couldn't write ${RUTA_DE_LA_INSTALACION}. Run \`supcoflow install record\` again.`);
|
|
205
236
|
}
|
|
206
|
-
return { codigo: SALIDA.ok, json: { ...json, sin_cambios: false }, texto: `
|
|
237
|
+
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
238
|
}
|