supcoflow 0.5.0 → 0.6.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 +39 -2
- package/dist/api.js +27 -2
- package/dist/argumentos.js +6 -1
- package/dist/cli.js +7 -0
- package/dist/comandos/describir.js +21 -1
- package/dist/comandos/instalacion.js +207 -0
- package/dist/comandos/widget.js +32 -0
- package/dist/entorno.js +16 -1
- package/dist/flags-por-comando.js +3 -2
- package/dist/instalacion.js +86 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ La terminal de [Supcoflow](https://app.supcoflow.dev): iniciar sesión y manejar
|
|
|
4
4
|
desde la consola, o desde un asistente de código que corre comandos por vos.
|
|
5
5
|
|
|
6
6
|
Trae iniciar y cerrar sesión, ver con qué cuenta estás, conectar el repo con la App de GitHub,
|
|
7
|
-
crear su proyecto
|
|
7
|
+
crear su proyecto, configurar el chat y ponerlo en la app.
|
|
8
8
|
|
|
9
9
|
## Instalar
|
|
10
10
|
|
|
@@ -30,6 +30,8 @@ npx supcoflow login
|
|
|
30
30
|
| `supcoflow guia publicar` | Sube `.supcoflow/guia.md` directo (con el commit y la rama que declara tu clon, no verificados en GitHub). `--desde-github` verifica contra la rama principal en vez de subir el archivo. |
|
|
31
31
|
| `supcoflow guia ver` | Muestra la guía activa, de dónde viene y si la rama sigue al día. |
|
|
32
32
|
| `supcoflow guia usar --origen repo\|agente` | Elige cuál guía queda activa. |
|
|
33
|
+
| `supcoflow widget` | Lo que hay que pegar en la app para tener el chat (ver «Poner el chat en la app»). |
|
|
34
|
+
| `supcoflow instalacion anotar --skill 0.1.0` | Anota en `.supcoflow/instalacion.json` con qué versiones se instaló el chat. |
|
|
33
35
|
| `supcoflow describir --json` | Todo el contrato (comandos, flags, facultades de la clave, códigos) en una línea de JSON. |
|
|
34
36
|
|
|
35
37
|
`supcoflow --help` (o `supcoflow <comando> --help`) muestra la ayuda con ejemplos. Los comandos de
|
|
@@ -117,6 +119,41 @@ Problemas comunes: ← opcional; hast
|
|
|
117
119
|
- Texto antes del primer `## ` que no sea el `# ` del título → error. Una etiqueta repetida en una sección →
|
|
118
120
|
error. Una línea con `<` o `>` → error. Más de 64 KB → `guia_muy_grande`.
|
|
119
121
|
|
|
122
|
+
## Poner el chat en la app
|
|
123
|
+
|
|
124
|
+
`supcoflow widget` muestra lo que hay que pegar, armado por el motor con los ids de tu proyecto (no los
|
|
125
|
+
copies a mano: un id mal copiado deja el chat invisible sin ningún error). No escribe archivos de la app:
|
|
126
|
+
|
|
127
|
+
1. El paquete con su versión exacta (`npm i @supcoflow/chat@… --save-exact`, o lo mismo con tu gestor).
|
|
128
|
+
2. **Next.js (App Router):** un archivo envoltorio en la ruta que sugiere el motor
|
|
129
|
+
(`app/_supcoflow/chat-de-soporte.tsx`; si la app usa `src/app`, dentro de `src/app`), con el contenido
|
|
130
|
+
tal cual. Hace falta porque el componente del chat usa hooks de React y no trae `"use client"`, y un
|
|
131
|
+
`layout.tsx` es de servidor por defecto: importarlo directo ahí no anda. El envoltorio es esa directiva
|
|
132
|
+
más el componente con los ids.
|
|
133
|
+
3. Usar el envoltorio en el layout que cubre las pantallas donde se traban tus usuarios.
|
|
134
|
+
4. Si la app tiene una política de contenido (CSP) con `connect-src`, agregarle el origen del chat
|
|
135
|
+
conservando lo que ya tiene (una segunda `connect-src` se ignora).
|
|
136
|
+
|
|
137
|
+
La línea de `<script>` que también muestra está disponible pero no probada en una app real, y no es
|
|
138
|
+
para Next.js. `supcoflow widget --json` devuelve el objeto del motor tal cual.
|
|
139
|
+
|
|
140
|
+
Después, `supcoflow instalacion anotar --skill <versión> [--app <carpeta>]` deja en
|
|
141
|
+
`.supcoflow/instalacion.json` (para commitear) con qué versiones quedó: la skill, la terminal, el chat
|
|
142
|
+
declarado e instalado, y la configuración del proyecto. Nunca lleva un secreto ni una fecha; repetirlo
|
|
143
|
+
sin cambios no lo toca, y si el archivo tiene cambios sin commit sale 4 y no lo pisa.
|
|
144
|
+
|
|
145
|
+
## Con un asistente
|
|
146
|
+
|
|
147
|
+
Si usás un asistente de código como Claude Code, la skill de Supcoflow hace todo el camino
|
|
148
|
+
por vos: sesión, proyecto, configuración, guía, el chat en la app y el resumen.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx skills add supcoflow/skills
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Y en el asistente: `/supcoflow go`. Las decisiones sobre los datos de tus usuarios y qué pasa con cada
|
|
155
|
+
problema te las pregunta siempre; no sube nada a GitHub.
|
|
156
|
+
|
|
120
157
|
## El contrato
|
|
121
158
|
|
|
122
159
|
Pensado para que lo lea un programa, no sólo una persona:
|
|
@@ -126,7 +163,7 @@ Pensado para que lo lea un programa, no sólo una persona:
|
|
|
126
163
|
- **Sin preguntas sin terminal interactiva** (o con `--json`): si falta un dato, sale 3 diciendo qué
|
|
127
164
|
flag usar. `supcoflow login` sin terminal sale 3 y te indica el camino en dos pasos.
|
|
128
165
|
- **`--dry-run`** donde se puede simular (`login`, `login --start`, `logout`, `repo conectar --start`,
|
|
129
|
-
`proyecto crear`, `guia revisar`, `guia publicar`, `guia usar`). `login --wait
|
|
166
|
+
`proyecto crear`, `guia revisar`, `guia publicar`, `guia usar`, `instalacion anotar`). `login --wait
|
|
130
167
|
--dry-run` sale 3: sondear consume el código y no hay cómo simularlo.
|
|
131
168
|
|
|
132
169
|
| Código | Quiere decir |
|
package/dist/api.js
CHANGED
|
@@ -21,9 +21,9 @@ export function urlDeLaApi(env) {
|
|
|
21
21
|
return new URL(u.origin);
|
|
22
22
|
throw invalida();
|
|
23
23
|
}
|
|
24
|
-
// A mano a propósito: tiene que coincidir con package.json (0.
|
|
24
|
+
// A mano a propósito: tiene que coincidir con package.json (0.6.0); sólo identifica a la terminal
|
|
25
25
|
// en los logs del motor, no decide nada.
|
|
26
|
-
const VERSION = "0.
|
|
26
|
+
const VERSION = "0.6.0";
|
|
27
27
|
const PLAZO_POR_DEFECTO_MS = 15_000;
|
|
28
28
|
// Los dos POST que crean algo: detrás, el motor acuña con Vercel Connect y le pregunta a GitHub varias
|
|
29
29
|
// veces (instalación, repos, comandos, rama). Con 15 s una respuesta lenta pero buena se cortaba y
|
|
@@ -312,3 +312,28 @@ export function leerGuia(api, clave, proyectoId, f) {
|
|
|
312
312
|
export function elegirOrigen(api, clave, proyectoId, origen, f) {
|
|
313
313
|
return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}/origen`, { method: "POST", headers: JSON_BODY, body: JSON.stringify({ origen }) }, f);
|
|
314
314
|
}
|
|
315
|
+
// Se valida campo a campo: lo que se le pasa a la persona (y de ahí a un archivo de su app) no puede
|
|
316
|
+
// ser algo a medias. `contenido` se guarda tal cual, sin tocarlo: los ids ya vienen puestos por el motor.
|
|
317
|
+
function aWidget(v, status) {
|
|
318
|
+
const n = (typeof v.nextjs === "object" && v.nextjs !== null ? v.nextjs : {});
|
|
319
|
+
const s = (typeof v.script === "object" && v.script !== null ? v.script : {});
|
|
320
|
+
const [tenantId, projectId, backendUrl, version, instalar, csp] = [v.tenant_id, v.project_id, v.backend_url, v.version, v.instalar, v.csp].map(texto);
|
|
321
|
+
const [archivo, contenido, uso] = [n.archivo_sugerido, n.contenido, n.uso].map(texto);
|
|
322
|
+
const linea = texto(s.linea);
|
|
323
|
+
if (!tenantId || !projectId || !backendUrl || !version || !instalar || !csp || v.paquete !== "@supcoflow/chat"
|
|
324
|
+
|| (n.estado !== "probando" && n.estado !== "soportado") || !archivo || !contenido || !uso
|
|
325
|
+
|| s.estado !== "disponible_sin_soporte" || !linea)
|
|
326
|
+
throw inesperada(status);
|
|
327
|
+
return {
|
|
328
|
+
tenant_id: tenantId, project_id: projectId, backend_url: backendUrl, paquete: "@supcoflow/chat", version, instalar,
|
|
329
|
+
nextjs: { estado: n.estado, archivo_sugerido: archivo, contenido, uso },
|
|
330
|
+
script: { estado: "disponible_sin_soporte", linea },
|
|
331
|
+
csp,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
export async function leerWidget(api, clave, proyectoId, f) {
|
|
335
|
+
const r = await conClave(api, clave, `/api/proyectos/${encodeURIComponent(proyectoId)}/widget`, { method: "GET" }, f);
|
|
336
|
+
if (r.status !== 200)
|
|
337
|
+
throw rechazada(r);
|
|
338
|
+
return aWidget(r.cuerpo, r.status);
|
|
339
|
+
}
|
package/dist/argumentos.js
CHANGED
|
@@ -5,11 +5,12 @@ export const COMANDOS_COMPUESTOS = [
|
|
|
5
5
|
"chat prender", "chat apagar", "chat marca", "chat logo", "sitios agregar", "sitios quitar",
|
|
6
6
|
"datos fijar", "politica fijar", "claves crear", "claves listar", "claves revocar",
|
|
7
7
|
"guia revisar", "guia publicar", "guia ver", "guia usar",
|
|
8
|
+
"instalacion anotar",
|
|
8
9
|
];
|
|
9
10
|
// Sólo estas primeras palabras admiten una segunda: cualquier otro par sigue siendo «de más».
|
|
10
11
|
const PRIMERAS_PALABRAS = new Set(COMANDOS_COMPUESTOS.map((c) => c.split(" ")[0]));
|
|
11
12
|
const FLAGS_CON_VALOR = [
|
|
12
|
-
"repo", "install", "test", "sitio", "titulo", "saludo", "archivo", "permitir", "modo", "nombre", "guardar-en", "id", "origen",
|
|
13
|
+
"repo", "install", "test", "sitio", "titulo", "saludo", "archivo", "permitir", "modo", "nombre", "guardar-en", "id", "origen", "skill", "app",
|
|
13
14
|
];
|
|
14
15
|
// strict: un flag mal escrito (`--estart`) no se ignora en silencio: un asistente creería que
|
|
15
16
|
// pidió algo que no pidió.
|
|
@@ -40,6 +41,8 @@ export function leerArgumentos(argv) {
|
|
|
40
41
|
id: { type: "string" },
|
|
41
42
|
origen: { type: "string" },
|
|
42
43
|
"desde-github": { type: "boolean" },
|
|
44
|
+
skill: { type: "string" },
|
|
45
|
+
app: { type: "string" },
|
|
43
46
|
tema: { type: "string", multiple: true },
|
|
44
47
|
},
|
|
45
48
|
tokens: true,
|
|
@@ -97,6 +100,8 @@ export function leerArgumentos(argv) {
|
|
|
97
100
|
id: v.id ?? null,
|
|
98
101
|
origen: v.origen ?? null,
|
|
99
102
|
desdeGithub: v["desde-github"] === true,
|
|
103
|
+
skill: v.skill ?? null,
|
|
104
|
+
app: v.app ?? null,
|
|
100
105
|
tema: v.tema ?? [],
|
|
101
106
|
},
|
|
102
107
|
};
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { leerArgumentos } from "./argumentos.js";
|
|
|
7
7
|
import { clavesCrear, clavesListar, clavesRevocar } from "./comandos/claves.js";
|
|
8
8
|
import { chatLogo, chatMarca, chatPrenderApagar, comandoConfiguracion, sitios } from "./comandos/configurar.js";
|
|
9
9
|
import { guiaPublicar, guiaRevisar, guiaUsar, guiaVer } from "./comandos/guia.js";
|
|
10
|
+
import { instalacionAnotar } from "./comandos/instalacion.js";
|
|
10
11
|
import { comandoCatalogo, datosFijar, politicaFijar } from "./comandos/datos-y-politica.js";
|
|
11
12
|
import { ayuda, comandoDescribir, DESCRIPCION } from "./comandos/describir.js";
|
|
12
13
|
import { loginInteractivo, loginStart, loginWait } from "./comandos/login.js";
|
|
@@ -14,6 +15,7 @@ import { comandoLogout } from "./comandos/logout.js";
|
|
|
14
15
|
import { proyectoCrear, proyectoVer } from "./comandos/proyecto.js";
|
|
15
16
|
import { comandoQuienSoy } from "./comandos/quien-soy.js";
|
|
16
17
|
import { repoConectarInteractivo, repoConectarStart, repoConectarWait } from "./comandos/repo-conectar.js";
|
|
18
|
+
import { widget } from "./comandos/widget.js";
|
|
17
19
|
import { emitir, FalloDeLaTerminal, resultadoDeFallo, SALIDA } from "./contrato.js";
|
|
18
20
|
import { entornoReal } from "./entorno.js";
|
|
19
21
|
import { flagQueNoAplica } from "./flags-por-comando.js";
|
|
@@ -38,6 +40,7 @@ async function correr(argv) {
|
|
|
38
40
|
sitio: flags.sitio !== null, titulo: flags.titulo !== null, saludo: flags.saludo !== null, tema: flags.tema.length > 0,
|
|
39
41
|
archivo: flags.archivo !== null, permitir: flags.permitir !== null, modo: flags.modo !== null,
|
|
40
42
|
nombre: flags.nombre !== null, guardarEn: flags.guardarEn !== null, id: flags.id !== null, origen: flags.origen !== null, desdeGithub: flags.desdeGithub,
|
|
43
|
+
skill: flags.skill !== null, app: flags.app !== null,
|
|
41
44
|
};
|
|
42
45
|
const noAplica = flagQueNoAplica(comando, Object.keys(presentes).filter((k) => presentes[k]));
|
|
43
46
|
if (noAplica !== null)
|
|
@@ -123,6 +126,10 @@ async function correr(argv) {
|
|
|
123
126
|
return { r: await guiaVer(e.guia, flags), json };
|
|
124
127
|
case "guia usar":
|
|
125
128
|
return { r: await guiaUsar(e.guia, flags), json };
|
|
129
|
+
case "widget":
|
|
130
|
+
return { r: await widget(e.widget, flags), json };
|
|
131
|
+
case "instalacion anotar":
|
|
132
|
+
return { r: await instalacionAnotar(e.instalacion, flags), json };
|
|
126
133
|
}
|
|
127
134
|
throw new Error(`comando sin despacho: ${comando}`);
|
|
128
135
|
}
|
|
@@ -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.
|
|
6
|
+
const VERSION = "0.6.0";
|
|
7
7
|
export const DESCRIPCION = {
|
|
8
8
|
version: VERSION,
|
|
9
9
|
comandos: [
|
|
@@ -210,6 +210,26 @@ export const DESCRIPCION = {
|
|
|
210
210
|
ejemplos: ["supcoflow guia usar --origen repo --json"],
|
|
211
211
|
codigos: [0, 1, 2, 3, 4],
|
|
212
212
|
},
|
|
213
|
+
{
|
|
214
|
+
nombre: "widget",
|
|
215
|
+
resumen: "Muestra lo que hay que pegar en la app para tener el chat, armado por el motor con los ids del proyecto: el paquete y su versión exacta, el archivo envoltorio de Next.js (`nextjs.contenido`, tal cual, sin reescribir los ids), cómo usarlo, la línea de CSP y la de <script> (no para Next.js). No escribe archivos. Sale 4 sin_proyecto si el repo no tiene proyecto; 1 respuesta_inesperada si el motor manda algo a medias.",
|
|
216
|
+
flags: ["--repo owner/repo: el repo, si no es el «origin» de esta carpeta", "--json: el objeto del motor tal cual"],
|
|
217
|
+
ejemplos: ["supcoflow widget --json"],
|
|
218
|
+
codigos: [0, 1, 2, 3, 4],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
nombre: "instalacion anotar",
|
|
222
|
+
resumen: "Anota en .supcoflow/instalacion.json (la raíz de este clon) con qué versiones se instaló el chat: la skill, esta terminal, el paquete del chat (declarado en el package.json de la app e instalado en node_modules) y la configuración del proyecto; nunca un secreto ni una fecha. Repetir sin cambios no lo toca (sin_cambios: true). Lo reescribe sólo si git ya guarda el anterior: si tiene cambios sin commit (o está borrado sin commit) sale 4 cambios_sin_commit; si existe sin estar versionado (o ignorado), 4 archivo_no_versionado. Otros: 3 falta_skill / skill_invalida / app_fuera_del_repo / app_inexistente / sin_clon; 4 sin_proyecto / acuerdo_ilegible; 1 paquete_ilegible / configuracion_inesperada / archivo_no_escrito.",
|
|
223
|
+
flags: [
|
|
224
|
+
"--skill versión: la metadata.version de la skill que instala (semver, obligatorio; sin él sale 3 falta_skill sin pedir nada)",
|
|
225
|
+
"--app carpeta: la carpeta de la app, relativa a la raíz del repo (por defecto la raíz; fuera del repo sale 3 app_fuera_del_repo)",
|
|
226
|
+
"--repo owner/repo: el repo, si no es el «origin» de esta carpeta (sin clon local sale 3 sin_clon: el archivo va en el clon)",
|
|
227
|
+
"--dry-run: muestra el archivo y la diferencia sin escribir nada",
|
|
228
|
+
"--json",
|
|
229
|
+
],
|
|
230
|
+
ejemplos: ["supcoflow instalacion anotar --skill 0.1.0 --app web --json"],
|
|
231
|
+
codigos: [0, 1, 2, 3, 4],
|
|
232
|
+
},
|
|
213
233
|
],
|
|
214
234
|
flags_globales: ["--json: el resultado en una línea de JSON por stdout; los avisos van por stderr", "--help, -h: la ayuda con ejemplos"],
|
|
215
235
|
facultades: [
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/comandos/instalacion.ts
|
|
2
|
+
// `instalacion anotar --skill <versión> [--app <carpeta>] [--dry-run]` (bloque 6): deja en
|
|
3
|
+
// `.supcoflow/instalacion.json` con qué versiones se instaló el chat. Lee la configuración del motor y los
|
|
4
|
+
// `package.json` de la app y del paquete instalado; nunca ejecuta nada del repo (ni `npm ls` ni `node`).
|
|
5
|
+
//
|
|
6
|
+
// La regla de §13 («no pisa archivos ni cambios sin commit») adaptada a un archivo que tiene que poder cambiar
|
|
7
|
+
// (decisión 11): se reescribe sólo si git ya guarda el anterior. El control se hace ANTES de escribir; si la
|
|
8
|
+
// persona edita el archivo entre el control y el `rename`, su cambio se pierde. Esa carrera se acepta: es una
|
|
9
|
+
// persona instalando, y el archivo lo escribe sólo este comando.
|
|
10
|
+
import { realpathSync, statSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { resultadoDeFallo, SALIDA, taparSecretos } from "../contrato.js";
|
|
13
|
+
import { armarInstalacion, esSemver, PAQUETE_DEL_CHAT, RUTA_DE_LA_INSTALACION, serializar } from "../instalacion.js";
|
|
14
|
+
import { repoPedido } from "../repo-local.js";
|
|
15
|
+
import { fallo } from "./configurar.js";
|
|
16
|
+
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
17
|
+
export function esCarpetaReal(p) {
|
|
18
|
+
try {
|
|
19
|
+
return statSync(p).isDirectory();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function rutaRealOEnNada(p) {
|
|
26
|
+
try {
|
|
27
|
+
return realpathSync(p);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Los package.json son chicos; uno de más de 1 MB no es el de una app ni el de un paquete.
|
|
34
|
+
const TOPE_DEL_PACKAGE_JSON = 1024 * 1024;
|
|
35
|
+
const TOPE_DEL_ARCHIVO = 64 * 1024;
|
|
36
|
+
const adentro = (raiz, p) => p === raiz || p.startsWith(raiz + path.sep);
|
|
37
|
+
const limpio = (t) => taparSecretos(t.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""));
|
|
38
|
+
function leerJson(d, p, nombre) {
|
|
39
|
+
const leido = d.leerArchivoChico(p, TOPE_DEL_PACKAGE_JSON);
|
|
40
|
+
if ("error" in leido) {
|
|
41
|
+
if (leido.error === "no existe")
|
|
42
|
+
return { ok: false, noExiste: true };
|
|
43
|
+
return { ok: false, noExiste: false, resultado: fallo(SALIDA.inesperado, "paquete_ilegible", `No leo ${nombre}: ${limpio(leido.error)}.`) };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return { ok: true, valor: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(leido.bytes)) };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { ok: false, noExiste: false, resultado: fallo(SALIDA.inesperado, "paquete_ilegible", `${nombre} no es JSON válido.`) };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Como lo resuelve Node desde la app (revisión externa, hallazgo 4): el primer `node_modules/@supcoflow/chat`
|
|
53
|
+
// subiendo desde <app> hasta la raíz del repo. En un monorepo npm lo deja en la raíz; pnpm enlaza a
|
|
54
|
+
// `node_modules/.pnpm/…`, adentro del repo. Si la ruta real sale del repo, no se lee: lo que hay afuera no es
|
|
55
|
+
// de esta instalación y la terminal no lee fuera del repo por un enlace que el repo eligió.
|
|
56
|
+
function paqueteInstalado(d, raiz, app, avisos) {
|
|
57
|
+
for (let dir = app;; dir = path.dirname(dir)) {
|
|
58
|
+
const candidato = path.join(dir, "node_modules", ...PAQUETE_DEL_CHAT.split("/"), "package.json");
|
|
59
|
+
const real = d.rutaReal(candidato);
|
|
60
|
+
if (real !== null) {
|
|
61
|
+
if (!adentro(raiz, real)) {
|
|
62
|
+
const aviso = `${path.relative(raiz, candidato)} apunta fuera del repo; no lo leo y anoto el chat instalado como null.`;
|
|
63
|
+
avisos.push(aviso);
|
|
64
|
+
d.avisar(aviso);
|
|
65
|
+
return { ok: true, valor: null };
|
|
66
|
+
}
|
|
67
|
+
const j = leerJson(d, real, `el package.json de ${PAQUETE_DEL_CHAT} instalado`);
|
|
68
|
+
if (j.ok)
|
|
69
|
+
return j;
|
|
70
|
+
// Existía hace un instante: si ahora no está, tampoco se anota «no está».
|
|
71
|
+
return { ok: false, resultado: j.noExiste ? fallo(SALIDA.inesperado, "paquete_ilegible", `No leo el package.json de ${PAQUETE_DEL_CHAT} instalado.`) : j.resultado };
|
|
72
|
+
}
|
|
73
|
+
if (dir === raiz || path.dirname(dir) === dir)
|
|
74
|
+
return { ok: true, valor: null };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function estadoDelArchivo(d, raizGit, existe) {
|
|
78
|
+
const git = async (args) => {
|
|
79
|
+
const r = await d.git(args, raizGit);
|
|
80
|
+
if (r.codigo !== 0)
|
|
81
|
+
throw new Error(`git ${args[0]} falló; no escribo ${RUTA_DE_LA_INSTALACION} sin saber si tiene cambios.`);
|
|
82
|
+
return r.stdout;
|
|
83
|
+
};
|
|
84
|
+
const lineas = (await git(["status", "--porcelain", "--ignored", "--", RUTA_DE_LA_INSTALACION])).split("\n").filter((l) => l.trim() !== "");
|
|
85
|
+
if (lineas.some((l) => !l.startsWith("??") && !l.startsWith("!!")))
|
|
86
|
+
return "cambios_sin_commit";
|
|
87
|
+
if (lineas.length > 0)
|
|
88
|
+
return "archivo_no_versionado";
|
|
89
|
+
const versionado = (await git(["ls-files", "--", RUTA_DE_LA_INSTALACION])).trim() !== "";
|
|
90
|
+
return versionado || !existe ? "escribir" : "archivo_no_versionado";
|
|
91
|
+
}
|
|
92
|
+
// Un diff de líneas (LCS) para el texto: los dos lados son chicos. El anterior lo escribió quien sea, así que
|
|
93
|
+
// sale limpio de control y de secretos; el JSON nunca lo lleva.
|
|
94
|
+
function diff(antes, despues) {
|
|
95
|
+
if (antes === null)
|
|
96
|
+
return ["--- (no había archivo)", "+++ " + RUTA_DE_LA_INSTALACION, ...despues.trimEnd().split("\n").map((l) => "+ " + l)].join("\n");
|
|
97
|
+
const a = antes.replace(/\r\n?/g, "\n").replace(/\n$/, "").split("\n").map(limpio);
|
|
98
|
+
const b = despues.trimEnd().split("\n");
|
|
99
|
+
const n = a.length, m = b.length;
|
|
100
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
101
|
+
for (let i = n - 1; i >= 0; i--)
|
|
102
|
+
for (let j = m - 1; j >= 0; j--)
|
|
103
|
+
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
104
|
+
const lineas = ["--- " + RUTA_DE_LA_INSTALACION + " (antes)", "+++ " + RUTA_DE_LA_INSTALACION];
|
|
105
|
+
let i = 0, j = 0;
|
|
106
|
+
while (i < n || j < m) {
|
|
107
|
+
if (i < n && j < m && a[i] === b[j])
|
|
108
|
+
lineas.push(" " + a[i++]), j++;
|
|
109
|
+
else if (j < m && (i === n || lcs[i][j + 1] >= lcs[i + 1][j]))
|
|
110
|
+
lineas.push("+ " + b[j++]);
|
|
111
|
+
else
|
|
112
|
+
lineas.push("- " + a[i++]);
|
|
113
|
+
}
|
|
114
|
+
return lineas.join("\n");
|
|
115
|
+
}
|
|
116
|
+
const resumen = (x) => `skill ${x.skill}, terminal ${x.terminal}, chat declarado ${x.chat.declarado ?? "—"} / instalado ${x.chat.instalado ?? "—"}`;
|
|
117
|
+
export async function instalacionAnotar(d, flags) {
|
|
118
|
+
if (flags.skill === null)
|
|
119
|
+
return fallo(SALIDA.entradaInvalida, "falta_skill", "Falta la versión de la skill: `supcoflow instalacion anotar --skill 0.1.0`.");
|
|
120
|
+
if (!esSemver(flags.skill))
|
|
121
|
+
return fallo(SALIDA.entradaInvalida, "skill_invalida", "--skill va con una versión semver (por ejemplo 0.1.0).");
|
|
122
|
+
// ─── La raíz y la app, antes de cualquier pedido ───
|
|
123
|
+
let raizGit;
|
|
124
|
+
try {
|
|
125
|
+
raizGit = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).raiz;
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return resultadoDeFallo(e);
|
|
129
|
+
}
|
|
130
|
+
if (raizGit === null)
|
|
131
|
+
return fallo(SALIDA.entradaInvalida, "sin_clon", `No tengo el clon de ese repo acá, y ${RUTA_DE_LA_INSTALACION} va en el clon: parate en el clon del repo.`);
|
|
132
|
+
const raiz = d.rutaReal(raizGit);
|
|
133
|
+
if (raiz === null)
|
|
134
|
+
return fallo(SALIDA.inesperado, "inesperado", "No pude resolver la raíz del repo.");
|
|
135
|
+
// `realpath` DESPUÉS de resolver: un `--app` que es un enlace hacia afuera tiene que quedar afuera (Review Focus 4).
|
|
136
|
+
const app = d.rutaReal(path.resolve(raiz, flags.app ?? "."));
|
|
137
|
+
if (app === null)
|
|
138
|
+
return fallo(SALIDA.entradaInvalida, "app_inexistente", "La carpeta de --app no existe (va relativa a la raíz del repo).");
|
|
139
|
+
if (!adentro(raiz, app))
|
|
140
|
+
return fallo(SALIDA.entradaInvalida, "app_fuera_del_repo", "--app tiene que ser una carpeta dentro del repo (relativa a su raíz).");
|
|
141
|
+
// Un archivo no es una app: sin esto, leer `<archivo>/package.json` daba ENOTDIR y salía `paquete_ilegible`.
|
|
142
|
+
if (!d.esCarpeta(app))
|
|
143
|
+
return fallo(SALIDA.entradaInvalida, "app_inexistente", "--app tiene que ser una carpeta (la de la app), no un archivo.");
|
|
144
|
+
// ─── Proyecto → configuración → package.json de la app → paquete instalado → armar ───
|
|
145
|
+
const p = await proyectoDelRepo(d, flags);
|
|
146
|
+
if (!p.ok)
|
|
147
|
+
return p.resultado;
|
|
148
|
+
let configuracion;
|
|
149
|
+
try {
|
|
150
|
+
configuracion = await d.leerConfiguracion(d.api, p.clave, p.proyecto.id);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
return resultadoDeFallo(e);
|
|
154
|
+
}
|
|
155
|
+
const deLaApp = leerJson(d, path.join(app, "package.json"), "el package.json de la app");
|
|
156
|
+
if (!deLaApp.ok && !deLaApp.noExiste)
|
|
157
|
+
return deLaApp.resultado;
|
|
158
|
+
const avisos = [];
|
|
159
|
+
const instalado = paqueteInstalado(d, raiz, app, avisos);
|
|
160
|
+
if (!instalado.ok)
|
|
161
|
+
return instalado.resultado;
|
|
162
|
+
let x;
|
|
163
|
+
try {
|
|
164
|
+
x = armarInstalacion({ skill: flags.skill, terminal: d.terminal, paqueteDeLaApp: deLaApp.ok ? deLaApp.valor : null, paqueteInstalado: instalado.valor, configuracion });
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
return resultadoDeFallo(e);
|
|
168
|
+
}
|
|
169
|
+
const nuevo = serializar(x);
|
|
170
|
+
// ─── ¿Se puede escribir? ───
|
|
171
|
+
const archivo = path.join(raiz, RUTA_DE_LA_INSTALACION);
|
|
172
|
+
// Una `.supcoflow` que es un enlace hacia afuera: ni se lee (el diff mostraría un archivo de afuera) ni se escribe.
|
|
173
|
+
const carpeta = d.rutaReal(path.dirname(archivo));
|
|
174
|
+
if (carpeta !== null && !adentro(raiz, carpeta))
|
|
175
|
+
return fallo(SALIDA.inesperado, "inesperado", `${path.dirname(RUTA_DE_LA_INSTALACION)} no es una carpeta del repo (¿un enlace?); no escribo ahí.`);
|
|
176
|
+
const actual = d.leerArchivoChico(archivo, TOPE_DEL_ARCHIVO);
|
|
177
|
+
const existe = !("error" in actual) || actual.error !== "no existe";
|
|
178
|
+
const json = { ok: true, ruta: RUTA_DE_LA_INSTALACION, instalacion: x, ...(avisos.length > 0 ? { avisos } : {}) };
|
|
179
|
+
if ("bytes" in actual && Buffer.from(actual.bytes).equals(Buffer.from(nuevo, "utf8"))) {
|
|
180
|
+
return { codigo: SALIDA.ok, json: { ...json, ...(flags.dryRun ? { dry_run: true } : {}), sin_cambios: true }, texto: `${RUTA_DE_LA_INSTALACION} ya dice eso (${resumen(x)}); no lo toqué.` };
|
|
181
|
+
}
|
|
182
|
+
let estado;
|
|
183
|
+
try {
|
|
184
|
+
estado = await estadoDelArchivo(d, raizGit, existe);
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
return resultadoDeFallo(e);
|
|
188
|
+
}
|
|
189
|
+
if (estado === "cambios_sin_commit") {
|
|
190
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "cambios_sin_commit", `${RUTA_DE_LA_INSTALACION} tiene cambios sin commit (o se borró sin commit); no lo toqué. Commitealos o descartalos y volvé a correr.`);
|
|
191
|
+
}
|
|
192
|
+
if (estado === "archivo_no_versionado") {
|
|
193
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "archivo_no_versionado", `Ya hay un ${RUTA_DE_LA_INSTALACION} que git no guarda (sin commit o ignorado); no lo pisé. Commitealo o borralo y volvé a correr.`);
|
|
194
|
+
}
|
|
195
|
+
const antes = "bytes" in actual ? Buffer.from(actual.bytes).toString("utf8") : null;
|
|
196
|
+
const cambios = existe && antes === null ? `(el archivo anterior no se lee)\n${diff(null, nuevo)}` : diff(antes, nuevo);
|
|
197
|
+
if (flags.dryRun) {
|
|
198
|
+
return { codigo: SALIDA.ok, json: { ...json, dry_run: true, sin_cambios: false }, texto: `Con --dry-run no escribo; sin él anotaría (${resumen(x)}):\n${cambios}` };
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
d.escribir(archivo, nuevo);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return fallo(SALIDA.inesperado, "archivo_no_escrito", `No pude escribir ${RUTA_DE_LA_INSTALACION}. Volvé a correr \`supcoflow instalacion anotar\`.`);
|
|
205
|
+
}
|
|
206
|
+
return { codigo: SALIDA.ok, json: { ...json, sin_cambios: false }, texto: `Anoté ${RUTA_DE_LA_INSTALACION} (${resumen(x)}); commitealo cuando quieras.\n${cambios}` };
|
|
207
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
+
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
3
|
+
function textoDe(w) {
|
|
4
|
+
return [
|
|
5
|
+
"1. Instalá el paquete (con el gestor de la app, la versión exacta):",
|
|
6
|
+
` ${w.instalar}`,
|
|
7
|
+
"",
|
|
8
|
+
`2. Creá el archivo ${w.nextjs.archivo_sugerido} con este contenido (Next.js, ${w.nextjs.estado}):`,
|
|
9
|
+
w.nextjs.contenido.trimEnd(),
|
|
10
|
+
"",
|
|
11
|
+
"3. Usalo en el layout que cubre las pantallas donde se traban los usuarios:",
|
|
12
|
+
` ${w.nextjs.uso}`,
|
|
13
|
+
"",
|
|
14
|
+
"4. Política de contenido (CSP): si la app tiene `connect-src`, agregale este origen conservando lo que ya tiene:",
|
|
15
|
+
` ${w.csp}`,
|
|
16
|
+
"",
|
|
17
|
+
"Alternativa con <script> (disponible, no probada en una app real):",
|
|
18
|
+
` ${w.script.linea}`,
|
|
19
|
+
].join("\n");
|
|
20
|
+
}
|
|
21
|
+
export async function widget(d, flags) {
|
|
22
|
+
const p = await proyectoDelRepo(d, flags);
|
|
23
|
+
if (!p.ok)
|
|
24
|
+
return p.resultado;
|
|
25
|
+
try {
|
|
26
|
+
const w = await d.leerWidget(d.api, p.clave, p.proyecto.id);
|
|
27
|
+
return { codigo: SALIDA.ok, json: { ok: true, ...w }, texto: textoDe(w) };
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return resultadoDeFallo(e);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/entorno.js
CHANGED
|
@@ -5,8 +5,10 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { chmodSync, closeSync, constants, fchmodSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, rmSync, writeSync, } from "node:fs";
|
|
6
6
|
import { homedir, hostname } from "node:os";
|
|
7
7
|
import { basename, dirname, join } from "node:path";
|
|
8
|
-
import { buscarProyecto, canjear, configurar, crearConexion, crearProyecto, elegirOrigen, esClaveDeConsultaDelProyecto, leerCatalogo, leerConfiguracion, leerGuia, leerProyecto, listarClaves, pedirCodigo, quienSoy, revocarPropia, subirLogo, ultimaConexion, urlDeLaApi, publicarGuia, revisarGuia, } from "./api.js";
|
|
8
|
+
import { buscarProyecto, canjear, configurar, leerWidget, crearConexion, crearProyecto, elegirOrigen, esClaveDeConsultaDelProyecto, leerCatalogo, leerConfiguracion, leerGuia, leerProyecto, listarClaves, pedirCodigo, quienSoy, revocarPropia, subirLogo, ultimaConexion, urlDeLaApi, publicarGuia, revisarGuia, } from "./api.js";
|
|
9
9
|
import { crearCandados } from "./candado.js";
|
|
10
|
+
import { esCarpetaReal, rutaRealOEnNada } from "./comandos/instalacion.js";
|
|
11
|
+
import { DESCRIPCION } from "./comandos/describir.js";
|
|
10
12
|
import { almacenDeArchivo } from "./guardado/archivo.js";
|
|
11
13
|
import { crearGuardado } from "./guardado/index.js";
|
|
12
14
|
import { almacenDeLlavero } from "./guardado/llavero.js";
|
|
@@ -466,5 +468,18 @@ export function entornoReal(env = process.env) {
|
|
|
466
468
|
esClaveDeConsultaDelProyecto: (a, clave, id) => esClaveDeConsultaDelProyecto(a, clave, id, fetch),
|
|
467
469
|
idempotencia, candados, secretos,
|
|
468
470
|
},
|
|
471
|
+
widget: {
|
|
472
|
+
api, guardado, git: gitReal, cwd,
|
|
473
|
+
buscarProyecto: configurarEnElMotor.buscarProyecto,
|
|
474
|
+
leerWidget: (a, clave, id) => leerWidget(a, clave, id, fetch),
|
|
475
|
+
},
|
|
476
|
+
// La versión que se anota es la de `describir` (pinzada contra package.json por test): una sola fuente
|
|
477
|
+
// para lo que la terminal dice de sí misma.
|
|
478
|
+
instalacion: {
|
|
479
|
+
...configurarEnElMotor,
|
|
480
|
+
leerArchivoChico, rutaReal: rutaRealOEnNada, esCarpeta: esCarpetaReal,
|
|
481
|
+
escribir: archivosDelRepo(avisar).escribir,
|
|
482
|
+
terminal: DESCRIPCION.version,
|
|
483
|
+
},
|
|
469
484
|
};
|
|
470
485
|
}
|
|
@@ -9,8 +9,8 @@ const DE_CONFIGURAR_CON_REPO = [
|
|
|
9
9
|
export const FLAGS_POR_COMANDO = [
|
|
10
10
|
{ flags: ["start", "wait"], comandos: ["login", "repo conectar"], texto: "--start y --wait son de `supcoflow login` y `supcoflow repo conectar`." },
|
|
11
11
|
{
|
|
12
|
-
flags: ["repo"], comandos: ["repo conectar", "proyecto crear", "proyecto ver", ...DE_CONFIGURAR_CON_REPO],
|
|
13
|
-
texto: "--repo es de `supcoflow repo conectar`, `proyecto crear|ver` y los comandos que configuran un proyecto (no de `catalogo`).",
|
|
12
|
+
flags: ["repo"], comandos: ["repo conectar", "proyecto crear", "proyecto ver", ...DE_CONFIGURAR_CON_REPO, "widget", "instalacion anotar"],
|
|
13
|
+
texto: "--repo es de `supcoflow repo conectar`, `proyecto crear|ver`, `widget`, `instalacion anotar` y los comandos que configuran un proyecto (no de `catalogo`).",
|
|
14
14
|
},
|
|
15
15
|
{ flags: ["install", "test"], comandos: ["proyecto crear"], texto: "--install y --test son de `supcoflow proyecto crear`." },
|
|
16
16
|
{ flags: ["sitio"], comandos: ["sitios agregar", "sitios quitar"], texto: "--sitio es de `supcoflow sitios agregar|quitar`." },
|
|
@@ -22,6 +22,7 @@ export const FLAGS_POR_COMANDO = [
|
|
|
22
22
|
{ flags: ["origen"], comandos: ["guia usar"], texto: "--origen es de `supcoflow guia usar`." },
|
|
23
23
|
{ flags: ["desdeGithub"], comandos: ["guia publicar"], texto: "--desde-github es de `supcoflow guia publicar`." },
|
|
24
24
|
{ flags: ["id"], comandos: ["claves revocar"], texto: "--id es de `supcoflow claves revocar`." },
|
|
25
|
+
{ flags: ["skill", "app"], comandos: ["instalacion anotar"], texto: "--skill y --app son de `supcoflow instalacion anotar`." },
|
|
25
26
|
];
|
|
26
27
|
/** El mensaje del primer flag presente que el comando no acepta, o null si todo aplica. */
|
|
27
28
|
export function flagQueNoAplica(comando, presentes) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// `.supcoflow/instalacion.json` (bloque 6, decisión 3): con qué versiones se hizo la instalación, anotado en el
|
|
2
|
+
// repo de quien instala. Supcoflow no lo guarda ni lo ve. Puro: recibe lo ya leído y arma el archivo.
|
|
3
|
+
//
|
|
4
|
+
// El archivo queda en el repo y se commitea: un secreto acá se publica con el primer `push`. Por eso se arma con
|
|
5
|
+
// una LISTA CERRADA de campos, cada uno validado por separado; nunca un `...configuracion` (la configuración del
|
|
6
|
+
// motor trae `fijado_por`, `publicada_por`, huellas y lo que el motor agregue mañana: nada de eso es de este archivo).
|
|
7
|
+
import { FalloDeLaTerminal, SALIDA, taparSecretos } from "./contrato.js";
|
|
8
|
+
export const RUTA_DE_LA_INSTALACION = ".supcoflow/instalacion.json";
|
|
9
|
+
export const PAQUETE_DEL_CHAT = "@supcoflow/chat";
|
|
10
|
+
// SemVer 2.0 tal cual, con tope de largo: la versión de la skill la escribe un asistente y queda en el repo.
|
|
11
|
+
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
12
|
+
export const esSemver = (s) => s.length <= 64 && SEMVER.test(s);
|
|
13
|
+
const objeto = (v) => (typeof v === "object" && v !== null && !Array.isArray(v) ? v : null);
|
|
14
|
+
const entero = (v) => typeof v === "number" && Number.isInteger(v) && v >= 1;
|
|
15
|
+
const ilegible = (message) => new FalloDeLaTerminal(SALIDA.inesperado, "paquete_ilegible", message);
|
|
16
|
+
const inesperada = (que) => new FalloDeLaTerminal(SALIDA.inesperado, "configuracion_inesperada", `El motor contestó la configuración con ${que} que no entiendo; no anoto nada a medias. Probá de nuevo en un rato.`);
|
|
17
|
+
// Lo declarado en el `package.json` de la app lo escribió quien escribió el repo: puede ser un rango, un alias o
|
|
18
|
+
// cualquier cosa. Se copia tal cual sólo si es texto imprimible y corto; si no, no se adivina (`paquete_ilegible`).
|
|
19
|
+
// `taparSecretos` igual, por si alguien pegó una clave ahí: el archivo nuevo no la repite.
|
|
20
|
+
function declaradoEn(app) {
|
|
21
|
+
const a = objeto(app);
|
|
22
|
+
if (a === null)
|
|
23
|
+
return null;
|
|
24
|
+
const v = objeto(a.dependencies)?.[PAQUETE_DEL_CHAT] ?? objeto(a.devDependencies)?.[PAQUETE_DEL_CHAT];
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
return null;
|
|
27
|
+
if (typeof v !== "string" || v.length === 0 || v.length > 200 || !/^[\x20-\x7e]+$/.test(v)) {
|
|
28
|
+
throw ilegible(`La versión de ${PAQUETE_DEL_CHAT} en el package.json de la app no se entiende; no la anoto.`);
|
|
29
|
+
}
|
|
30
|
+
return taparSecretos(v);
|
|
31
|
+
}
|
|
32
|
+
// `null` es «no está instalado». Un archivo que existe pero sin versión válida NO es null: diría que no está.
|
|
33
|
+
function instaladoEn(paquete) {
|
|
34
|
+
if (paquete === null)
|
|
35
|
+
return null;
|
|
36
|
+
const v = objeto(paquete)?.version;
|
|
37
|
+
if (typeof v !== "string" || !esSemver(v))
|
|
38
|
+
throw ilegible(`El package.json de ${PAQUETE_DEL_CHAT} instalado no trae una versión que entienda.`);
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
function acuerdoDe(c) {
|
|
42
|
+
if (c.acuerdo === null)
|
|
43
|
+
return null;
|
|
44
|
+
const a = objeto(c.acuerdo);
|
|
45
|
+
// Ilegible es un estado conocido del motor (aplica cerrado), no «sin acuerdo»: anotarlo como null mentiría.
|
|
46
|
+
if (a?.ilegible === true) {
|
|
47
|
+
throw new FalloDeLaTerminal(SALIDA.haceFaltaUnaPersona, "acuerdo_ilegible", "El acuerdo de datos está fijado pero no se lee: revisalo en la consola y volvé a correr.");
|
|
48
|
+
}
|
|
49
|
+
if (a === null || !entero(a.version))
|
|
50
|
+
throw inesperada("un acuerdo");
|
|
51
|
+
return a.version;
|
|
52
|
+
}
|
|
53
|
+
function politicaDe(c) {
|
|
54
|
+
const modo = objeto(c.politica)?.modo;
|
|
55
|
+
if (modo !== "relevar" && modo !== "guardar")
|
|
56
|
+
throw inesperada("una política");
|
|
57
|
+
return modo;
|
|
58
|
+
}
|
|
59
|
+
function guiaDe(c) {
|
|
60
|
+
if (c.guia === null)
|
|
61
|
+
return null;
|
|
62
|
+
const g = objeto(c.guia);
|
|
63
|
+
if (g?.origen === "repo") {
|
|
64
|
+
if (g.procedencia !== "terminal" && g.procedencia !== "github")
|
|
65
|
+
throw inesperada("una guía");
|
|
66
|
+
if (g.commit !== null && (typeof g.commit !== "string" || !/^[0-9a-f]{40}$/.test(g.commit)))
|
|
67
|
+
throw inesperada("una guía");
|
|
68
|
+
return { origen: "repo", procedencia: g.procedencia, commit: g.commit };
|
|
69
|
+
}
|
|
70
|
+
if (g?.origen === "agente" && entero(g.version))
|
|
71
|
+
return { origen: "agente", version: g.version };
|
|
72
|
+
throw inesperada("una guía");
|
|
73
|
+
}
|
|
74
|
+
export function armarInstalacion(i) {
|
|
75
|
+
return {
|
|
76
|
+
version: 1,
|
|
77
|
+
skill: i.skill,
|
|
78
|
+
terminal: i.terminal,
|
|
79
|
+
chat: { declarado: declaradoEn(i.paqueteDeLaApp), instalado: instaladoEn(i.paqueteInstalado) },
|
|
80
|
+
configuracion: { acuerdo_version: acuerdoDe(i.configuracion), politica: politicaDe(i.configuracion), guia: guiaDe(i.configuracion) },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// El orden de las claves es el de `armarInstalacion`: el mismo estado da los mismos bytes, y eso es `sin_cambios`.
|
|
84
|
+
export function serializar(x) {
|
|
85
|
+
return JSON.stringify(x, null, 2) + "\n";
|
|
86
|
+
}
|
package/package.json
CHANGED