supcoflow 0.1.0 → 0.5.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.
@@ -3,7 +3,7 @@
3
3
  // ayuda pensada para personas. La ayuda se arma desde acá: una sola fuente, así no se desfasan.
4
4
  import { SALIDA } from "../contrato.js";
5
5
  // A mano a propósito, como en api.ts: tiene que coincidir con package.json (lo pinza un test).
6
- const VERSION = "0.1.0";
6
+ const VERSION = "0.5.0";
7
7
  export const DESCRIPCION = {
8
8
  version: VERSION,
9
9
  comandos: [
@@ -40,17 +40,187 @@ export const DESCRIPCION = {
40
40
  ejemplos: ["supcoflow describir --json"],
41
41
  codigos: [0],
42
42
  },
43
+ {
44
+ nombre: "repo conectar",
45
+ resumen: "Conecta el repo de esta carpeta (o el de --repo) con la App de GitHub: una persona lo confirma en la consola. Sin terminal interactiva, en dos pasos: --start y después --wait.",
46
+ flags: [
47
+ "--start: pide la conexión y sale 5 con la URL para confirmarla",
48
+ "--wait: espera la confirmación hasta 90 s; sale 5 si todavía no llegó (volvé a llamarlo)",
49
+ "--repo owner/repo: el repo, si no es el «origin» de esta carpeta",
50
+ "--dry-run: dice qué pediría sin pedir nada (--wait sólo lee)",
51
+ "--json",
52
+ ],
53
+ ejemplos: ["supcoflow repo conectar", "supcoflow repo conectar --start --json", "supcoflow repo conectar --wait --json"],
54
+ codigos: [0, 1, 2, 3, 4, 5],
55
+ },
56
+ {
57
+ nombre: "proyecto crear",
58
+ resumen: "Crea (o reusa) el proyecto del repo ya conectado y lo anota en .supcoflow/proyecto.json; un archivo distinto nunca se pisa (sale 4).",
59
+ flags: [
60
+ "--install \"…\": el comando de instalación, cuando el motor no lo puede deducir",
61
+ "--test \"…\": el comando de pruebas, ídem",
62
+ "--repo owner/repo: el repo, si no es el «origin» de esta carpeta (sin escribir el archivo)",
63
+ "--dry-run: dice qué crearía sin crear nada",
64
+ "--json",
65
+ ],
66
+ ejemplos: ["supcoflow proyecto crear --json", "supcoflow proyecto crear --install \"pnpm i\" --test \"pnpm test\""],
67
+ codigos: [0, 1, 2, 3, 4],
68
+ },
69
+ {
70
+ nombre: "proyecto ver",
71
+ resumen: "Muestra el proyecto del repo: rama, comandos y el estado de la App de GitHub; null si todavía no tiene.",
72
+ flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--json"],
73
+ ejemplos: ["supcoflow proyecto ver --json"],
74
+ codigos: [0, 1, 2, 3],
75
+ },
76
+ {
77
+ nombre: "catalogo",
78
+ resumen: "Lista lo que se puede configurar en el chat (claves y valores admitidos) y el estado del contrato de la App.",
79
+ flags: ["--json"],
80
+ ejemplos: ["supcoflow catalogo --json"],
81
+ codigos: [0, 1, 2, 3],
82
+ },
83
+ {
84
+ nombre: "configuracion",
85
+ resumen: "Muestra la configuración del proyecto del repo: chat, sitios, marca, política de problemas y acuerdo de datos. El acuerdo frena lo que se guarda y lo que lee la IA que releva; no cubre lo que ve la IA del chat mientras conversa.",
86
+ flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--json"],
87
+ ejemplos: ["supcoflow configuracion --json"],
88
+ codigos: [0, 1, 2, 3, 4],
89
+ },
90
+ {
91
+ nombre: "chat prender",
92
+ resumen: "Prende el chat de soporte del proyecto. Hace falta una guía vigente (la publicada del repo o la aprobada de nuestro agente); si no, sale 4 (skill_no_aprobada).",
93
+ flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
94
+ ejemplos: ["supcoflow chat prender --json"],
95
+ codigos: [0, 1, 2, 3, 4],
96
+ },
97
+ {
98
+ nombre: "chat apagar",
99
+ resumen: "Apaga el chat de soporte del proyecto.",
100
+ flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
101
+ ejemplos: ["supcoflow chat apagar --json"],
102
+ codigos: [0, 1, 2, 3, 4],
103
+ },
104
+ {
105
+ nombre: "chat marca",
106
+ resumen: "Cambia título, saludo y colores del chat; lo que no nombrás queda como estaba.",
107
+ flags: [
108
+ "--titulo \"…\": el título del chat",
109
+ "--saludo \"…\": el saludo inicial",
110
+ "--tema clave=valor: un color o ajuste del tema (se repite, uno por clave)",
111
+ "--repo owner/repo: el repo, si no es el «origin» de esta carpeta",
112
+ "--dry-run: dice qué haría sin hacerlo",
113
+ "--json",
114
+ ],
115
+ ejemplos: ["supcoflow chat marca --titulo \"Ayuda\" --tema acento=#0a7"],
116
+ codigos: [0, 1, 2, 3, 4],
117
+ },
118
+ {
119
+ nombre: "chat logo",
120
+ resumen: "Sube el logo del chat (.png, .jpg o .webp, hasta 200 KB).",
121
+ flags: ["--archivo ruta: la imagen", "--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
122
+ ejemplos: ["supcoflow chat logo --archivo logo.png --json"],
123
+ codigos: [0, 1, 2, 3, 4],
124
+ },
125
+ {
126
+ nombre: "sitios agregar",
127
+ resumen: "Agrega un sitio desde el que el chat puede cargarse.",
128
+ flags: ["--sitio https://…: el sitio", "--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
129
+ ejemplos: ["supcoflow sitios agregar --sitio https://app.ejemplo.com"],
130
+ codigos: [0, 1, 2, 3, 4],
131
+ },
132
+ {
133
+ nombre: "sitios quitar",
134
+ resumen: "Quita un sitio de los que pueden cargar el chat.",
135
+ flags: ["--sitio https://…: el sitio", "--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
136
+ ejemplos: ["supcoflow sitios quitar --sitio https://app.ejemplo.com"],
137
+ codigos: [0, 1, 2, 3, 4],
138
+ },
139
+ {
140
+ nombre: "datos fijar",
141
+ resumen: "Fija el acuerdo de datos: qué datos pasan. Frena lo que se guarda y lo que lee la IA que releva el repo; no cubre lo que ve la IA del chat mientras conversa.",
142
+ flags: [
143
+ "--permitir lista: datos que pasan, separados por coma (usuario_id, usuario_rol, pagina_y_navegador, capturas, elementos, email, atributo:nombre) o `nada`",
144
+ "--repo owner/repo: el repo, si no es el «origin» de esta carpeta",
145
+ "--dry-run: dice qué haría sin hacerlo",
146
+ "--json",
147
+ ],
148
+ ejemplos: ["supcoflow datos fijar --permitir usuario_id,atributo:plan"],
149
+ codigos: [0, 1, 2, 3, 4],
150
+ },
151
+ {
152
+ nombre: "politica fijar",
153
+ resumen: "Fija qué hace el motor con un problema que llega por el chat: relevarlo en el acto (gasta) o guardarlo. Guardar frena sólo lo automático del chat: desde la consola una persona puede pedir el relevamiento, y cuesta.",
154
+ flags: ["--modo relevar|guardar", "--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
155
+ ejemplos: ["supcoflow politica fijar --modo guardar"],
156
+ codigos: [0, 1, 2, 3, 4],
157
+ },
158
+ {
159
+ nombre: "claves crear",
160
+ resumen: "Crea una clave de consulta (sólo lee su proyecto) y la guarda en un archivo 0600 fuera de todo repo; el secreto nunca sale por la salida.",
161
+ flags: [
162
+ "--nombre ci: para qué es la clave",
163
+ "--guardar-en ruta: el archivo donde queda (fuera de un repo; si ya tiene otra cosa sale 4 y no lo toca)",
164
+ "--repo owner/repo: el repo, si no es el «origin» de esta carpeta",
165
+ "--dry-run: dice qué haría sin hacerlo",
166
+ "--json",
167
+ ],
168
+ ejemplos: ["supcoflow claves crear --nombre ci --guardar-en ~/ci.key --json"],
169
+ codigos: [0, 1, 2, 3, 4],
170
+ },
171
+ {
172
+ nombre: "claves listar",
173
+ resumen: "Lista las claves de consulta activas del proyecto (id, nombre, fecha; nunca el secreto).",
174
+ flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--json"],
175
+ ejemplos: ["supcoflow claves listar --json"],
176
+ codigos: [0, 1, 2, 3, 4],
177
+ },
178
+ {
179
+ nombre: "claves revocar",
180
+ resumen: "Revoca una clave de consulta por su id; revocar dos veces deja lo mismo.",
181
+ flags: ["--id cla_…: la clave (la da `claves listar`)", "--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
182
+ ejemplos: ["supcoflow claves revocar --id cla_0123456789abcdef --json"],
183
+ codigos: [0, 1, 2, 3, 4],
184
+ },
185
+ {
186
+ nombre: "guia revisar",
187
+ resumen: "Revisa .supcoflow/guia.md (la raíz de este clon) contra el motor, sin publicar nada: dice si está bien armada y cuántas secciones tiene. Es el paso antes del commit.",
188
+ flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta (sin clon local sale 3 falta_guia: la guía se lee del archivo)", "--dry-run: dice qué mandaría sin mandarlo", "--json"],
189
+ ejemplos: ["supcoflow guia revisar --json"],
190
+ codigos: [0, 1, 2, 3, 4],
191
+ },
192
+ {
193
+ nombre: "guia publicar",
194
+ resumen: "Sube .supcoflow/guia.md directo: manda el contenido y el commit y la rama que declara este clon (si hay git y commits; si no, sin commit). El motor la revisa y la deja como guía activa; el commit queda como «declarado por la terminal, no verificado en GitHub». Con --desde-github conserva la vía anterior: manda la huella y el motor la compara con la de la rama principal; si todavía no está subida a esa rama (o es otra), sale 5.",
195
+ flags: ["--desde-github: no sube el archivo; verifica contra la rama principal de GitHub (sale 5 guia_no_subida / guia_distinta si no coincide)", "--repo owner/repo: el repo, si no es el «origin» de esta carpeta (sin clon local sale 3 falta_guia: la guía se lee del archivo)", "--dry-run: dice qué mandaría (o la huella con --desde-github), sin pedir nada", "--json"],
196
+ ejemplos: ["supcoflow guia publicar --json"],
197
+ codigos: [0, 1, 2, 3, 4, 5],
198
+ },
199
+ {
200
+ nombre: "guia ver",
201
+ resumen: "Muestra la guía activa del proyecto, de dónde viene y si la rama principal sigue teniendo esa versión (`al_dia` en el JSON).",
202
+ flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--json"],
203
+ ejemplos: ["supcoflow guia ver --json"],
204
+ codigos: [0, 1, 2, 3, 4],
205
+ },
206
+ {
207
+ nombre: "guia usar",
208
+ resumen: "Elige cuál guía queda activa cuando hay dos: la del repo o la de nuestro agente.",
209
+ flags: ["--origen repo|agente: cuál queda activa (obligatorio)", "--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--dry-run: dice qué haría sin hacerlo", "--json"],
210
+ ejemplos: ["supcoflow guia usar --origen repo --json"],
211
+ codigos: [0, 1, 2, 3, 4],
212
+ },
43
213
  ],
44
214
  flags_globales: ["--json: el resultado en una línea de JSON por stdout; los avisos van por stderr", "--help, -h: la ayuda con ejemplos"],
45
215
  facultades: [
46
216
  {
47
217
  nombre: "consultar",
48
- incluye: "Identidad y equipo, proyectos, configuración, casos, issues, runs, costos, estado de la instalación",
218
+ incluye: "Identidad y equipo, proyectos, configuración, casos, issues, runs, costos, estado de la instalación, leer y revisar la guía (guia revisar, guia ver)",
49
219
  con_api_key: true,
50
220
  },
51
221
  {
52
222
  nombre: "configurar",
53
- incluye: "Crear/reusar proyecto, conectar o desconectar el repo, configurar y prender el chat, sitios permitidos, acuerdo de datos, política de problemas, publicar la guía, crear y revocar claves de consulta, perfiles de los agentes",
223
+ incluye: "Crear/reusar proyecto, conectar o desconectar el repo, configurar y prender el chat, sitios permitidos, acuerdo de datos, política de problemas, publicar la guía, elegir la guía activa (guia usar), crear y revocar claves de consulta, perfiles de los agentes",
54
224
  con_api_key: true,
55
225
  },
56
226
  { nombre: "probar", incluye: "La prueba hasta el issue (gasta, dentro de los topes)", con_api_key: true },
@@ -0,0 +1,244 @@
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.
5
+ import path from "node:path";
6
+ import { resultadoDeFallo, SALIDA, taparSecretos } from "../contrato.js";
7
+ import { huellaDeGuia, MAX_BYTES_DE_LA_GUIA, RUTA_DE_LA_GUIA } from "../guia.js";
8
+ import { repoPedido } from "../repo-local.js";
9
+ import { desenlace, fallo, rutaDe } from "./configurar.js";
10
+ import { proyectoDelRepo } from "./proyecto-del-repo.js";
11
+ // Texto que viene del motor (o del repo, vía el motor) y sale a la terminal de una persona: sin secretos y sin
12
+ // caracteres de control (C0, DEL, C1), que un ESC movería el cursor. Texto y JSON salen del mismo texto ya limpio.
13
+ // Primero se quita el control y recién después se tapan los secretos: al revés, una clave partida por un
14
+ // control (U+0085, por ejemplo) quedaba entera al quitarlo y se imprimía sin tapar.
15
+ const limpio = (t) => taparSecretos(t.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""));
16
+ const cadena = (v) => (typeof v === "string" ? limpio(v) : "");
17
+ 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
+ // ─── El archivo local ───
21
+ // 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
+ // Con `--repo` y sin clon de ese repo no hay dónde leerla (publicar lee la rama, pero la huella sale del archivo).
23
+ async function leerLaGuia(d, flags) {
24
+ let raiz;
25
+ try {
26
+ raiz = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).raiz;
27
+ }
28
+ catch (e) {
29
+ return { ok: false, resultado: resultadoDeFallo(e) };
30
+ }
31
+ 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.`) };
33
+ }
34
+ const leido = d.leerArchivoChico(path.join(raiz, RUTA_DE_LA_GUIA), MAX_BYTES_DE_LA_GUIA);
35
+ 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)}.`) };
41
+ }
42
+ try {
43
+ // ignoreBOM: el BOM se CONSERVA en el texto; quitarlo es regla de la huella (`huellaDeGuia`), no de la lectura.
44
+ // 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) };
46
+ }
47
+ catch {
48
+ return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "guia_ilegible", `${RUTA_DE_LA_GUIA} no es UTF-8 válido.`) };
49
+ }
50
+ }
51
+ // ─── El desenlace propio de la guía, encima del de configurar ───
52
+ function desenlaceDeGuia(r, ok) {
53
+ const c = r.cuerpo;
54
+ // 2xx, 401, 5xx y cuerpos raros los resuelve el de configurar. Ojo: la terminal no distingue 500
55
+ // connect_no_disponible de 502 github_no_disponible (ambos llegan como falla del servicio): los dos son 1.
56
+ if (r.status < 400 || r.status === 401 || r.status >= 500 || typeof c !== "object" || c === null)
57
+ return desenlace(r, ok);
58
+ 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}).`);
60
+ const con = (codigo, extra = {}, texto = message) => ({ codigo, json: { ok: false, error, message: taparSecretos(message), ...extra }, texto });
61
+ switch (error) {
62
+ case "guia_no_subida":
63
+ case "guia_distinta": {
64
+ 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.`);
68
+ }
69
+ case "instalacion_ausente":
70
+ case "repo_fuera_de_alcance":
71
+ case "sin_guia_del_agente":
72
+ return con(SALIDA.haceFaltaUnaPersona);
73
+ case "guia_mal_armada": {
74
+ const errores = Array.isArray(c.errores) ? c.errores : [];
75
+ const lineas = errores.map((e) => {
76
+ const x = (e ?? {});
77
+ return `línea ${typeof x.linea === "number" ? x.linea : "?"}: ${cadena(x.motivo)}`;
78
+ });
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"));
80
+ }
81
+ default:
82
+ return desenlace(r, ok);
83
+ }
84
+ }
85
+ export async function guiaRevisar(d, flags) {
86
+ const g = await leerLaGuia(d, flags);
87
+ if (!g.ok)
88
+ return g.resultado;
89
+ const p = await proyectoDelRepo(d, flags);
90
+ if (!p.ok)
91
+ return p.resultado;
92
+ 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}.` };
95
+ }
96
+ 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
+ }));
100
+ }
101
+ catch (e) {
102
+ return resultadoDeFallo(e);
103
+ }
104
+ }
105
+ // Commit y rama que la terminal DECLARA: son del clon donde está el archivo, no verificados contra GitHub. Cualquier
106
+ // falla (sin git, sin commits, HEAD suelto, plazo vencido) devuelve vacío: declarar es un extra que nunca bloquea la
107
+ // subida ni pregunta nada, y lo que git imprima no sale a la terminal.
108
+ async function declarado(d, raiz) {
109
+ const leer = async (args) => {
110
+ try {
111
+ const r = await d.gitDeclarado(args, raiz);
112
+ return r.codigo === 0 ? r.stdout.trim() : null;
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ };
118
+ const commit = await leer(["rev-parse", "HEAD"]);
119
+ // Sin un commit de 40 hex no se manda ni la rama: una rama sin commit no dice de dónde salió la guía.
120
+ if (commit === null || !/^[0-9a-f]{40}$/.test(commit))
121
+ return {};
122
+ const rama = await leer(["rev-parse", "--abbrev-ref", "HEAD"]);
123
+ // «HEAD» a secas es la cabeza suelta: no es una rama.
124
+ return rama !== null && rama !== "" && rama !== "HEAD" ? { commit, rama } : { commit };
125
+ }
126
+ export async function guiaPublicar(d, flags) {
127
+ const g = await leerLaGuia(d, flags);
128
+ if (!g.ok)
129
+ return g.resultado;
130
+ const p = await proyectoDelRepo(d, flags);
131
+ if (!p.ok)
132
+ return p.resultado;
133
+ const github = flags.desdeGithub === true;
134
+ // La vía de GitHub manda la huella del archivo local TAL CUAL (con BOM y \r\n: normalizarla es regla de
135
+ // `huellaDeGuia`). La directa manda el contenido crudo y el motor calcula la huella: la terminal no la necesita.
136
+ const huella = github ? huellaDeGuia(g.texto) : null;
137
+ 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";
140
+ // El JSON del dry-run pasa por la misma limpieza que el texto (control primero, secretos después): la rama la
141
+ // decide quien escribió el clon, y un ESC en ella no debe llegar a la terminal por ninguna de las dos salidas.
142
+ if (flags.dryRun) {
143
+ 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}.` };
148
+ }
149
+ try {
150
+ return desenlaceDeGuia(await d.publicar(d.api, p.clave, p.proyecto.id, cuerpo, d.fetch), (c) => {
151
+ const guia = (c.guia ?? {});
152
+ const ya = c.ya_publicada === true;
153
+ 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})` : "";
155
+ const json = { ok: true, guia: c.guia ?? null, ya_publicada: ya, reemplaza: c.reemplaza ?? null };
156
+ if (github) {
157
+ const base = `commit ${corto(guia.commit)} de ${cadena(guia.rama)}, ${secciones(guia.secciones)}`;
158
+ // 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 };
161
+ }
162
+ // 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";
164
+ 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}.`;
167
+ return { codigo: SALIDA.ok, json: { ...json, commit_declarado: decl.commit ?? null }, texto };
168
+ });
169
+ }
170
+ catch (e) {
171
+ return resultadoDeFallo(e);
172
+ }
173
+ }
174
+ export async function guiaVer(d, flags) {
175
+ const p = await proyectoDelRepo(d, flags);
176
+ if (!p.ok)
177
+ return p.resultado;
178
+ try {
179
+ return desenlaceDeGuia(await d.ver(d.api, p.clave, p.proyecto.id, d.fetch), (c) => {
180
+ const guia = c.guia;
181
+ const otra = c.otra;
182
+ const rama = c.rama;
183
+ const alDia = rama && typeof rama.al_dia === "boolean" ? rama.al_dia : null;
184
+ const json = { ok: true, guia: guia ?? null, otra: otra ?? null, al_dia: alDia, rama: rama ?? null };
185
+ if (!guia)
186
+ return { codigo: SALIDA.ok, json, texto: "Este proyecto no tiene guía activa: `supcoflow guia publicar` sube la del repo." };
187
+ const lineas = [];
188
+ if (guia.origen === "repo") {
189
+ lineas.push(`Guía activa: ${NOMBRE_DEL_ORIGEN.repo} (versión ${guia.version}, ${secciones(guia.secciones)}).`);
190
+ if (guia.procedencia === "terminal") {
191
+ // Subida desde la terminal: el motor NO comparó nada contra GitHub a propósito (al_dia null), así que "no pude
192
+ // comprobar la rama" sería falso. Commit y rama son lo que declaró quien subió; el id crudo de quien publicó
193
+ // (usr_…) no le dice nada a una persona y la terminal no resuelve nombres: queda sólo en el JSON.
194
+ 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.");
198
+ }
199
+ else {
200
+ lineas.push(`Procedencia: ${cadena(guia.repo)}, rama ${cadena(guia.rama)}, commit ${corto(guia.commit)}, publicada por ${cadena(guia.publicada_por)}.`);
201
+ if (alDia === true)
202
+ lineas.push("Está al día con la rama.");
203
+ else if (alDia === false)
204
+ lineas.push("En la rama hay otra versión: corré `supcoflow guia publicar`.");
205
+ else
206
+ lineas.push("No pude comprobar la rama ahora.");
207
+ }
208
+ }
209
+ 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.");
212
+ }
213
+ 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.`);
215
+ }
216
+ return { codigo: SALIDA.ok, json, texto: lineas.join("\n") };
217
+ });
218
+ }
219
+ catch (e) {
220
+ return resultadoDeFallo(e);
221
+ }
222
+ }
223
+ export async function guiaUsar(d, flags) {
224
+ const origen = flags.origen ?? null;
225
+ 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`.");
229
+ const p = await proyectoDelRepo(d, flags);
230
+ if (!p.ok)
231
+ return p.resultado;
232
+ if (flags.dryRun) {
233
+ 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}.` };
235
+ }
236
+ try {
237
+ 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]}.`,
239
+ }));
240
+ }
241
+ catch (e) {
242
+ return resultadoDeFallo(e);
243
+ }
244
+ }
@@ -0,0 +1,26 @@
1
+ import { resultadoDeFallo, SALIDA } from "../contrato.js";
2
+ import { repoPedido } from "../repo-local.js";
3
+ import { claveVigente } from "./clave-vigente.js";
4
+ export async function proyectoDelRepo(d, flags) {
5
+ let repo;
6
+ try {
7
+ repo = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).repo;
8
+ }
9
+ catch (e) {
10
+ return { ok: false, resultado: resultadoDeFallo(e) };
11
+ }
12
+ const leida = await claveVigente(d.guardado, d.api.origin);
13
+ if (!leida.ok)
14
+ return { ok: false, resultado: leida.resultado };
15
+ try {
16
+ const proyecto = await d.buscarProyecto(d.api, leida.vigente.clave, repo);
17
+ if (proyecto === null) {
18
+ const message = `El repo ${repo} todavía no tiene proyecto. Creá el proyecto: \`supcoflow proyecto crear\`.`;
19
+ return { ok: false, resultado: { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "sin_proyecto", message, repo }, texto: message } };
20
+ }
21
+ return { ok: true, clave: leida.vigente.clave, proyecto };
22
+ }
23
+ catch (e) {
24
+ return { ok: false, resultado: resultadoDeFallo(e) };
25
+ }
26
+ }