supcoflow 0.8.1 → 0.9.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 +57 -9
- package/dist/api.js +53 -4
- package/dist/argumentos.js +7 -2
- package/dist/cli.js +5 -2
- package/dist/comandos/describir.js +8 -7
- package/dist/comandos/instalacion.js +67 -26
- package/dist/comandos/probar.js +63 -3
- package/dist/comandos/widget.js +26 -0
- package/dist/diagnostico.js +43 -1
- package/dist/entorno.js +9 -1
- package/dist/flags-por-comando.js +5 -1
- package/dist/instalacion.js +88 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,8 +36,8 @@ the English ones.
|
|
|
36
36
|
| `supcoflow guide use --source repo\|agent` | Chooses which guide is active. |
|
|
37
37
|
| `supcoflow guide versions` | The guide that governs now and the repo's publications, newest first; at most one says «live». |
|
|
38
38
|
| `supcoflow widget` | What to paste into the app to get the chat (see «Put the chat in the app»). |
|
|
39
|
-
| `supcoflow install record --skill 0.3.1 --file app/_supcoflow/support-chat.tsx` | Records in `.supcoflow/instalacion.json` which versions the chat was installed with
|
|
40
|
-
| `supcoflow diagnose` | Free: how far the installation is (N of
|
|
39
|
+
| `supcoflow install record --skill 0.3.1 --file app/_supcoflow/support-chat.tsx` | Records in `.supcoflow/instalacion.json` which versions the chat was installed with, what was installed (the widget or the app's own interface), which files it lives in and which presentation contract it follows. |
|
|
40
|
+
| `supcoflow diagnose` | Free: how far the installation is (N of 7) and whether a test would go in (including whether this machine can save the test request). |
|
|
41
41
|
| `supcoflow test` | The end-to-end test (see «Test the installation»). |
|
|
42
42
|
| `supcoflow describe --json` | The whole contract (commands, flags, key capabilities, codes) in one line of JSON. |
|
|
43
43
|
|
|
@@ -191,13 +191,61 @@ wrongly copied id leaves the chat invisible without any error). It writes no fil
|
|
|
191
191
|
The `<script>` line it also shows is available but not tested in a real app, and is not for Next.js.
|
|
192
192
|
`supcoflow widget --json` returns the engine's object as is.
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
### Your own interface (the app builds the chat)
|
|
195
|
+
|
|
196
|
+
Supcoflow doesn't have to be the one that draws the chat. When the engine also describes the app's **own interface**
|
|
197
|
+
(`interfaz_propia` in `widget --json`; `Own interface` in the text), the code assistant builds the chat with the
|
|
198
|
+
app's own components, icons and styles, and Supcoflow only says what to install and which contract the interface
|
|
199
|
+
must fulfil. **There is no code to paste** in this form:
|
|
200
|
+
|
|
201
|
+
| Field (`interfaz_propia`) | What it is |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `estado` | `disponible`, or `sin_publicar` when the package version isn't on npm yet. With `sin_publicar` the text doesn't offer the path (it would be an `npm i` that fails): it says «not published yet; use the widget». The JSON still carries everything. |
|
|
204
|
+
| `paquete`, `version`, `instalar` | The package and its exact version, and the install command. |
|
|
205
|
+
| `tenant_id`, `project_id`, `backend_url`, `opciones_del_nucleo` | The ids and the backend, as the engine has them: don't copy them by hand. |
|
|
206
|
+
| `csp` | The Content Security Policy line to add to `connect-src`. |
|
|
207
|
+
| `contrato` (`version`, `archivo`, `documento`) | The presentation contract the interface has to follow: its version, and where the installed package carries it (`contract.json` and `docs/presentation-contract.md`, paths relative to the folder where `npm i` was run), so it can be read offline. |
|
|
208
|
+
| `entradas`, `documentos` | The package entry points (`@supcoflow/chat/core`, `/react`, `/testing`, `/testing/server`) and the docs inside the package. Names and paths, not code. |
|
|
209
|
+
|
|
210
|
+
The field is additive: an engine that doesn't send it (production, until the engine's T8 ships) gives exactly the
|
|
211
|
+
object it always gave, and the terminal adds nothing. If the engine sends `interfaz_propia` halfway (a field missing,
|
|
212
|
+
an `estado` it doesn't know, a `contrato.version` that isn't a semver, a path outside the package), the terminal ignores
|
|
213
|
+
that whole part, says so on stderr and shows the widget as always: it never shows half of it, and an optional block never
|
|
214
|
+
takes the widget down. Anything else inside `interfaz_propia` that the terminal doesn't know is not passed on.
|
|
215
|
+
|
|
216
|
+
Then, `supcoflow install record --skill <version> [--app <folder>] [--shape widget|own-interface] [--file <path>]…` leaves in
|
|
195
217
|
`.supcoflow/instalacion.json` (to commit) which versions it ended up with: the skill, the terminal, the chat declared
|
|
196
|
-
and installed, the project configuration and,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
218
|
+
and installed, the project configuration and, from version 3 (0.9.0), what the installation is:
|
|
219
|
+
|
|
220
|
+
- `forma`: `"widget"` or `"interfaz_propia"` (`--shape widget` / `--shape own-interface`), or `null` if not said.
|
|
221
|
+
- `archivos`: the files the chat lives in, relative to the repo root (no `..`, not absolute, not a symlink, up to 20).
|
|
222
|
+
`--file` can be repeated, in order, and replaces the whole list; the wrapper of the widget is one file, an own
|
|
223
|
+
interface usually several.
|
|
224
|
+
- `contrato`: the version of the presentation contract the **installed** package carries (its `contract.json`, found the
|
|
225
|
+
same way as its `package.json`: the closest `node_modules` going up to the repo root, never outside the repo), or `null`
|
|
226
|
+
if the package has none. A `contract.json` that is there but has no version is an error (`paquete_ilegible`).
|
|
227
|
+
|
|
228
|
+
Running it again without `--shape` or `--file` keeps the form and the files already recorded; changing `--shape` without
|
|
229
|
+
`--file` leaves `archivos` empty (the wrapper of the widget isn't the own interface, and the other way round). Versions 1 and
|
|
230
|
+
2 are read without error and as a widget, the only form there was: a version 2 `archivo` is read as `archivos: [archivo]`
|
|
231
|
+
(and rewritten as version 3). A file of version 4 or more (written by a newer supcoflow) is never rewritten: `install record`
|
|
232
|
+
exits 4 `escrito_por_una_terminal_mas_nueva` and `diagnose` says to update supcoflow. The JSON of the command keeps `archivo` (the first file) for the skills before 0.9.0 and adds
|
|
233
|
+
`archivos`, `forma` and `contrato`. It never contains a secret or a date; repeating it without changes doesn't touch it,
|
|
234
|
+
and if the file has uncommitted changes it exits 4 and doesn't overwrite it.
|
|
235
|
+
|
|
236
|
+
**Point 7 of `diagnose` (`interfaz`).** It is local and only reads. If `.supcoflow/instalacion.json` records an own
|
|
237
|
+
interface, it checks that every recorded file is still there, that a contract is recorded and that it isn't older than the
|
|
238
|
+
one the project declares today (`interfaz_propia.contrato.version`, asked from the engine; if that can't be asked, or the
|
|
239
|
+
engine doesn't send it, nothing is compared). Only a new **major** version fails the point (a new requirement comes only with
|
|
240
|
+
a major one): then it says to run the skill again and check that the interface still conforms. A newer minor or patch only
|
|
241
|
+
adds optional points: the point stays ok and says so. To see that a recorded file is still there it doesn't read it. With no
|
|
242
|
+
own interface recorded (no file, the widget, or nothing said) the point is ok («Not applicable»). It is advice: when it
|
|
243
|
+
fails it lowers the «N of 7», it is marked `aviso: true` in the JSON, and it doesn't stop a test.
|
|
244
|
+
|
|
245
|
+
**Before publishing 0.9.0.** `diagnose` now says «N of 7», and the skill of today (`SKILL.md`, `referencias/prueba.md`)
|
|
246
|
+
asks literally for «6 of 6», but it pins the terminal to 0.8.1 (never `@latest`), so it won't see 0.9.0 until a skill pins it.
|
|
247
|
+
The skill that pins 0.9.0 has to read `instalacion.ok` / `listo` (or `aviso`) instead of a fixed number, and 0.9.0 has to be on
|
|
248
|
+
npm before or together with it.
|
|
201
249
|
|
|
202
250
|
## Test the installation
|
|
203
251
|
|
|
@@ -206,7 +254,7 @@ the repo from the `origin`, or `--repo owner/repo`.
|
|
|
206
254
|
|
|
207
255
|
| Command | What it does |
|
|
208
256
|
|---|---|
|
|
209
|
-
| `supcoflow diagnose` | Free and writes nothing. It prints «Checked sign-in, repo, GitHub App, allowed sites, chat and
|
|
257
|
+
| `supcoflow diagnose` | Free and writes nothing. It prints «Checked sign-in, repo, GitHub App, allowed sites, chat, guide and interface: N of 7, at no cost.» and, apart, «Ready for a test: yes / no, because …» with the policy, the month's balance and open tests. Exits 0 if everything is ready, 4 if something needs fixing (with each point). `--json` gives `instalacion: { ok, total: 7, puntos }` and `prueba: { listo, puntos }`. The seventh point, `interfaz`, is explained in «Put the chat in the app». |
|
|
210
258
|
| `supcoflow test` | Without `--authorize-spend` it sends nothing: it exits 4 with the test's ceiling, the balance, the cap and what is committed this month, and the exact command to launch it. It also says, before you authorize anything, whether this machine can save the test request (`maquina: { ok, impedimentos }`) and any request still waiting for confirmation (`intento_pendiente: { techo_usd, politica }`). |
|
|
211
259
|
| `supcoflow test --authorize-spend --ceiling 2 --expect-policy investigate` | Requests the test: the problem comes in through the same door as the chat and runs an investigation that **spends** up to the ceiling you authorize (with the `save` policy it goes as far as the saved case and doesn't spend). Exits 5 with the id. `--message "…"` uses your own text. |
|
|
212
260
|
| `supcoflow test --wait` | Follows the last test up to 90 s: 5 while it runs (call it again), 0 with the issue, 4 if it needs a look in the console (it brings the link). The charge is shown as it is: `pendiente` right at the close, never 0. `--test-id prb_…` follows another one. |
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Los pedidos de la terminal al motor. Sin dependencias: `fetch` viene de Node y se inyecta.
|
|
2
2
|
import { FalloDeLaTerminal, SALIDA, taparSecretos } from "./contrato.js";
|
|
3
|
+
import { esSemver } from "./instalacion.js";
|
|
3
4
|
// La URL viene FIJA (§5.2): si un archivo del repo pudiera cambiarla, clonar un repo ajeno y correr
|
|
4
5
|
// `supcoflow` mandaría el token a un servidor ajeno. Sólo el entorno del proceso la mueve, y `http`
|
|
5
6
|
// únicamente contra la propia máquina: contra un host remoto la clave viajaría en claro.
|
|
@@ -23,7 +24,7 @@ export function urlDeLaApi(env) {
|
|
|
23
24
|
}
|
|
24
25
|
// A mano a propósito: tiene que coincidir con package.json (lo pinza test/describir.test.ts); sólo
|
|
25
26
|
// identifica a la terminal en los logs del motor, no decide nada.
|
|
26
|
-
const VERSION = "0.
|
|
27
|
+
const VERSION = "0.9.0";
|
|
27
28
|
const PLAZO_POR_DEFECTO_MS = 15_000;
|
|
28
29
|
// Los dos POST que crean algo: detrás, el motor acuña con Vercel Connect y le pregunta a GitHub varias
|
|
29
30
|
// veces (instalación, repos, comandos, rama). Con 15 s una respuesta lenta pero buena se cortaba y
|
|
@@ -318,7 +319,7 @@ export function elegirOrigen(api, clave, proyectoId, origen, f) {
|
|
|
318
319
|
}
|
|
319
320
|
// Se valida campo a campo: lo que se le pasa a la persona (y de ahí a un archivo de su app) no puede
|
|
320
321
|
// ser algo a medias. `contenido` se guarda tal cual, sin tocarlo: los ids ya vienen puestos por el motor.
|
|
321
|
-
function aWidget(v, status) {
|
|
322
|
+
function aWidget(v, status, avisar) {
|
|
322
323
|
const n = (typeof v.nextjs === "object" && v.nextjs !== null ? v.nextjs : {});
|
|
323
324
|
const s = (typeof v.script === "object" && v.script !== null ? v.script : {});
|
|
324
325
|
const [tenantId, projectId, backendUrl, version, instalar, csp] = [v.tenant_id, v.project_id, v.backend_url, v.version, v.instalar, v.csp].map(texto);
|
|
@@ -332,11 +333,58 @@ function aWidget(v, status) {
|
|
|
332
333
|
|| (n.estado !== "probando" && n.estado !== "soportado") || !archivo || !contenido || !uso
|
|
333
334
|
|| s.estado !== "disponible_sin_soporte" || !linea)
|
|
334
335
|
throw inesperada(status);
|
|
336
|
+
// Un bloque ADITIVO a medias no tumba el widget de siempre (revisión 1, P2-1): se descarta entero, con un aviso, y no se muestra
|
|
337
|
+
// nada de él (nunca la mitad).
|
|
338
|
+
let propia = aInterfazPropia(v.interfaz_propia);
|
|
339
|
+
if (propia === null) {
|
|
340
|
+
avisar?.("The engine described the app's own interface halfway; I'm ignoring that part.");
|
|
341
|
+
propia = undefined;
|
|
342
|
+
}
|
|
335
343
|
return {
|
|
336
344
|
tenant_id: tenantId, project_id: projectId, backend_url: backendUrl, paquete: "@supcoflow/chat", version, instalar,
|
|
337
345
|
nextjs: { estado: n.estado, archivo_sugerido: archivo, archivos_anteriores: anteriores, contenido, uso },
|
|
338
346
|
script: { estado: "disponible_sin_soporte", linea },
|
|
339
347
|
csp,
|
|
348
|
+
...(propia === undefined ? {} : { interfaz_propia: propia }),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
// `undefined` es «el motor no la manda» (producción de hoy; también `null`); `null` es «la mandó a medias»; y una lista cerrada de
|
|
352
|
+
// campos, anidados incluidos: lo que el motor sume mañana dentro de `interfaz_propia` no se copia (como el resto del widget).
|
|
353
|
+
// Sin `\\` tampoco: un `..` escrito con barras invertidas no se vería al partir por `/`.
|
|
354
|
+
const SIN_ESPACIOS_RAROS = /^[\x21-\x5b\x5d-\x7e]+$/;
|
|
355
|
+
const EN_EL_PAQUETE = "node_modules/@supcoflow/chat/";
|
|
356
|
+
const ENTRADA_DEL_PAQUETE = "@supcoflow/chat/";
|
|
357
|
+
// Una ruta o una entrada: un texto sin espacios ni control, que empiece donde tiene que empezar y no suba con `..`.
|
|
358
|
+
const conPrefijo = (v, prefijo) => {
|
|
359
|
+
const t = texto(v);
|
|
360
|
+
return t !== null && SIN_ESPACIOS_RAROS.test(t) && t.startsWith(prefijo) && t.length > prefijo.length && !t.split("/").includes("..") ? t : null;
|
|
361
|
+
};
|
|
362
|
+
const objetoONull = (v) => (typeof v === "object" && v !== null && !Array.isArray(v) ? v : null);
|
|
363
|
+
function aInterfazPropia(v) {
|
|
364
|
+
if (v === undefined || v === null)
|
|
365
|
+
return undefined;
|
|
366
|
+
const o = objetoONull(v);
|
|
367
|
+
const c = objetoONull(o?.contrato), e = objetoONull(o?.entradas), d = objetoONull(o?.documentos), n = objetoONull(o?.opciones_del_nucleo);
|
|
368
|
+
if (o === null || c === null || e === null || d === null || n === null)
|
|
369
|
+
return null;
|
|
370
|
+
const [version, instalar, tenantId, projectId, backendUrl, csp, nucleoTenant, nucleoProyecto] = [o.version, o.instalar, o.tenant_id, o.project_id, o.backend_url, o.csp, n.tenantId, n.projectId].map(texto);
|
|
371
|
+
const cVersion = texto(c.version);
|
|
372
|
+
const [cArchivo, cDocumento] = [c.archivo, c.documento].map((x) => conPrefijo(x, EN_EL_PAQUETE));
|
|
373
|
+
const entradas = [e.nucleo, e.react, e.pruebas, e.servidor_de_pruebas].map((x) => conPrefijo(x, ENTRADA_DEL_PAQUETE));
|
|
374
|
+
const docs = [d.nucleo, d.react, d.pruebas, d.conformidad, d.protocolo].map((x) => conPrefijo(x, EN_EL_PAQUETE));
|
|
375
|
+
if ((o.estado !== "sin_publicar" && o.estado !== "disponible") || o.paquete !== "@supcoflow/chat"
|
|
376
|
+
|| !version || !instalar || !tenantId || !projectId || !backendUrl || !csp || !nucleoTenant || !nucleoProyecto
|
|
377
|
+
|| !cVersion || !esSemver(cVersion) || cArchivo === null || cDocumento === null
|
|
378
|
+
|| entradas.includes(null) || docs.includes(null))
|
|
379
|
+
return null;
|
|
380
|
+
const [eNucleo, eReact, ePruebas, eServidor] = entradas;
|
|
381
|
+
const [dNucleo, dReact, dPruebas, dConformidad, dProtocolo] = docs;
|
|
382
|
+
return {
|
|
383
|
+
estado: o.estado, paquete: "@supcoflow/chat", version, instalar, tenant_id: tenantId, project_id: projectId, backend_url: backendUrl, csp,
|
|
384
|
+
opciones_del_nucleo: { tenantId: nucleoTenant, projectId: nucleoProyecto },
|
|
385
|
+
contrato: { version: cVersion, archivo: cArchivo, documento: cDocumento },
|
|
386
|
+
entradas: { nucleo: eNucleo, react: eReact, pruebas: ePruebas, servidor_de_pruebas: eServidor },
|
|
387
|
+
documentos: { nucleo: dNucleo, react: dReact, pruebas: dPruebas, conformidad: dConformidad, protocolo: dProtocolo },
|
|
340
388
|
};
|
|
341
389
|
}
|
|
342
390
|
// Una lista de textos no vacíos; ausente es lista vacía (un motor sin el campo), y cualquier otra forma es null.
|
|
@@ -348,11 +396,12 @@ function listaDeTextos(v) {
|
|
|
348
396
|
const textos = v.map(texto);
|
|
349
397
|
return textos.every((t) => t !== null && t !== "") ? textos : null;
|
|
350
398
|
}
|
|
351
|
-
|
|
399
|
+
// `avisar` recibe lo que la terminal descarta sin tumbar el comando (hoy: una `interfaz_propia` a medias).
|
|
400
|
+
export async function leerWidget(api, clave, proyectoId, f, avisar) {
|
|
352
401
|
const r = await conClave(api, clave, `/api/proyectos/${encodeURIComponent(proyectoId)}/widget`, { method: "GET" }, f);
|
|
353
402
|
if (r.status !== 200)
|
|
354
403
|
throw rechazada(r);
|
|
355
|
-
return aWidget(r.cuerpo, r.status);
|
|
404
|
+
return aWidget(r.cuerpo, r.status, avisar);
|
|
356
405
|
}
|
|
357
406
|
// ---- La prueba (bloque 7) ----
|
|
358
407
|
const rutaDelProyecto = (proyectoId) => `/api/proyectos/${encodeURIComponent(proyectoId)}`;
|
package/dist/argumentos.js
CHANGED
|
@@ -13,8 +13,10 @@ export const COMANDOS_COMPUESTOS = [
|
|
|
13
13
|
const PRIMERAS_PALABRAS = new Set([...COMANDOS_COMPUESTOS, ...Object.keys(COMANDOS_VIEJOS)].filter((c) => c.includes(" ")).map((c) => c.split(" ")[0]));
|
|
14
14
|
const FLAGS_CON_VALOR = [
|
|
15
15
|
"repo", "install", "test", "site", "title", "greeting", "file", "allow", "mode", "name", "save-to", "id", "source", "skill", "app",
|
|
16
|
-
"message", "test-id", "ceiling", "expect-policy",
|
|
16
|
+
"message", "test-id", "ceiling", "expect-policy", "shape",
|
|
17
17
|
];
|
|
18
|
+
// Los que sí se repiten (cada comando decide cuántos acepta: `cli.ts`, `archivosDeMas`); ninguno va vacío.
|
|
19
|
+
const FLAGS_REPETIBLES = new Set(["file"]);
|
|
18
20
|
const FLAGS_BOOLEANOS = ["json", "dry-run", "help", "start", "wait", "from-github", "authorize-spend"];
|
|
19
21
|
// El flag que el token nombró, ya llevado a su nombre canónico (el viejo y el nuevo cuentan como el mismo).
|
|
20
22
|
const canonico = (nombre) => FLAGS_VIEJOS[nombre] ?? nombre;
|
|
@@ -55,7 +57,7 @@ export function leerArgumentos(argv) {
|
|
|
55
57
|
// nombre viejo y el nuevo de un mismo flag) creería que se usó el primero. Se mira en los tokens y se rechaza.
|
|
56
58
|
for (const nombre of FLAGS_CON_VALOR) {
|
|
57
59
|
const toks = tokens.filter((t) => canonico(t.name) === nombre);
|
|
58
|
-
if (toks.length > 1 || toks.some((t) => t.value === "")) {
|
|
60
|
+
if ((toks.length > 1 && !FLAGS_REPETIBLES.has(nombre)) || toks.some((t) => t.value === "")) {
|
|
59
61
|
throw invalida("argumentos_invalidos", `--${nombre} goes once, with a value. Try \`supcoflow --help\`.`);
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -86,6 +88,7 @@ export function leerArgumentos(argv) {
|
|
|
86
88
|
return t === undefined ? null : (t.value ?? null);
|
|
87
89
|
};
|
|
88
90
|
const hay = (nombre) => tokens.some((x) => canonico(x.name) === nombre);
|
|
91
|
+
const archivos = tokens.filter((t) => canonico(t.name) === "file").map((t) => t.value ?? "");
|
|
89
92
|
return {
|
|
90
93
|
comando,
|
|
91
94
|
viejos,
|
|
@@ -111,6 +114,8 @@ export function leerArgumentos(argv) {
|
|
|
111
114
|
fromGithub: hay("from-github"),
|
|
112
115
|
skill: un("skill"),
|
|
113
116
|
app: un("app"),
|
|
117
|
+
files: archivos,
|
|
118
|
+
shape: un("shape"),
|
|
114
119
|
authorizeSpend: hay("authorize-spend"),
|
|
115
120
|
message: un("message"),
|
|
116
121
|
testId: un("test-id"),
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { repoConectarInteractivo, repoConectarStart, repoConectarWait } from "./
|
|
|
19
19
|
import { widget } from "./comandos/widget.js";
|
|
20
20
|
import { emitir, FalloDeLaTerminal, resultadoDeFallo, SALIDA } from "./contrato.js";
|
|
21
21
|
import { entornoReal } from "./entorno.js";
|
|
22
|
-
import { flagQueNoAplica } from "./flags-por-comando.js";
|
|
22
|
+
import { archivosDeMas, flagQueNoAplica } from "./flags-por-comando.js";
|
|
23
23
|
const COMANDOS = new Set(DESCRIPCION.comandos.map((c) => c.nombre));
|
|
24
24
|
const invalida = (error, message) => new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message);
|
|
25
25
|
// Los nombres que la terminal tenía antes de la 0.8.0 siguen valiendo (`src/alias.ts`), pero no salen en `describe`.
|
|
@@ -42,13 +42,16 @@ async function correr(argv) {
|
|
|
42
42
|
site: flags.site !== null, title: flags.title !== null, greeting: flags.greeting !== null, theme: flags.theme.length > 0,
|
|
43
43
|
file: flags.file !== null, allow: flags.allow !== null, mode: flags.mode !== null,
|
|
44
44
|
name: flags.name !== null, saveTo: flags.saveTo !== null, id: flags.id !== null, source: flags.source !== null, fromGithub: flags.fromGithub,
|
|
45
|
-
skill: flags.skill !== null, app: flags.app !== null,
|
|
45
|
+
skill: flags.skill !== null, app: flags.app !== null, shape: flags.shape !== null,
|
|
46
46
|
authorizeSpend: flags.authorizeSpend, message: flags.message !== null, testId: flags.testId !== null,
|
|
47
47
|
ceiling: flags.ceiling !== null, expectPolicy: flags.expectPolicy !== null,
|
|
48
48
|
};
|
|
49
49
|
const noAplica = flagQueNoAplica(comando, Object.keys(presentes).filter((k) => presentes[k]));
|
|
50
50
|
if (noAplica !== null)
|
|
51
51
|
throw invalida("flag_no_aplica", noAplica);
|
|
52
|
+
const deMas = archivosDeMas(comando, flags.files.length);
|
|
53
|
+
if (deMas !== null)
|
|
54
|
+
throw invalida("argumentos_invalidos", deMas);
|
|
52
55
|
if (flags.start && flags.wait) {
|
|
53
56
|
throw invalida("flags_incompatibles", "--start and --wait go separately: first one and then the other.");
|
|
54
57
|
}
|
|
@@ -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.9.0";
|
|
7
7
|
export const DESCRIPCION = {
|
|
8
8
|
version: VERSION,
|
|
9
9
|
comandos: [
|
|
@@ -219,17 +219,18 @@ export const DESCRIPCION = {
|
|
|
219
219
|
},
|
|
220
220
|
{
|
|
221
221
|
nombre: "widget",
|
|
222
|
-
resumen: "Shows what to paste into the app to get the chat, built by the engine with the project's ids: the package and its exact version, the Next.js wrapper file (`nextjs.contenido`, as is, without rewriting the ids), the file name to use (`nextjs.archivo_sugerido`) and the earlier names the chat may already be installed under (`nextjs.archivos_anteriores`), how to use it, the CSP line and the <script> one (not for Next.js). It writes no files. Exits 4 sin_proyecto if the repo has no project; 1 respuesta_inesperada if the engine sends
|
|
223
|
-
flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin»", "--json: the engine's object as is"],
|
|
222
|
+
resumen: "Shows what to paste into the app to get the chat, built by the engine with the project's ids: the package and its exact version, the Next.js wrapper file (`nextjs.contenido`, as is, without rewriting the ids), the file name to use (`nextjs.archivo_sugerido`) and the earlier names the chat may already be installed under (`nextjs.archivos_anteriores`), how to use it, the CSP line and the <script> one (not for Next.js). If the engine describes the app's own interface (`interfaz_propia`: the app builds the chat with its own components; there is no code to paste) it passes it on as the engine sent it: `estado` (`sin_publicar`: the version isn't on npm yet, so the text doesn't offer the path; `disponible`), the package with its exact version, the ids, the backend, the CSP line, the presentation contract (`contrato.version`, `contrato.archivo`, `contrato.documento`), the entry points (`entradas`) and the docs (`documentos`). A halfway `interfaz_propia` is ignored with a warning on stderr and the widget is shown as always. It writes no files. Exits 4 sin_proyecto if the repo has no project; 1 respuesta_inesperada if the engine sends the widget itself halfway.",
|
|
223
|
+
flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin»", "--json: the engine's object as is (`interfaz_propia` only when the engine sends it)"],
|
|
224
224
|
ejemplos: ["supcoflow widget --json"],
|
|
225
225
|
codigos: [0, 1, 2, 3, 4],
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
228
|
nombre: "install record",
|
|
229
|
-
resumen: "Records in .supcoflow/instalacion.json (this clone's root, version
|
|
229
|
+
resumen: "Records in .supcoflow/instalacion.json (this clone's root, version 3) which versions the chat was installed with: the skill, this terminal, the chat package (declared in the app's package.json and installed in node_modules), the project configuration and what was installed: `forma` (`widget` or `interfaz_propia`, with --shape), `archivos` (the files the chat lives in, with --file) and `contrato` (the version of the presentation contract the installed package carries in its contract.json, or null if it has none); never a secret or a date. A version 1 or 2 file is read without error as a widget (the `archivo` of a 2 is its file). Changing --shape without --file leaves `archivos` empty (the files of the other form aren't inherited). Repeating without changes doesn't touch it (sin_cambios: true), and running again without --shape or --file keeps the form and the files already recorded. The JSON adds `archivos`, `forma` and `contrato`; `archivo` (the first file) stays for the skills before 0.9.0. It only rewrites it if git already keeps the previous one: with uncommitted changes (or deleted without a commit) it exits 4 cambios_sin_commit; if it exists without being tracked (or ignored), 4 archivo_no_versionado. Others: 3 falta_skill / skill_invalida / archivo_invalido / forma_invalida / app_fuera_del_repo / app_inexistente / sin_clon; 4 sin_proyecto / acuerdo_ilegible / escrito_por_una_terminal_mas_nueva (the file is version 4 or more: it doesn't rewrite it); 1 paquete_ilegible / configuracion_inesperada / archivo_no_escrito.",
|
|
230
230
|
flags: [
|
|
231
231
|
"--skill version: the metadata.version of the skill that installs it (semver, required; without it exits 3 falta_skill without requesting anything)",
|
|
232
|
-
"--file path: the chat
|
|
232
|
+
"--file path: a file of the chat, relative to the repo root (no `..`, not absolute, not a symlink or through one; otherwise exits 3 archivo_invalido and writes nothing). It can be repeated (up to 20; an own interface lives in several files) and replaces the list already recorded",
|
|
233
|
+
"--shape widget|own-interface: what was installed: the packaged widget, or a chat the app builds with its own components (`forma: interfaz_propia`); anything else exits 3 forma_invalida. Without it the recorded form is kept",
|
|
233
234
|
"--app folder: the app's folder, relative to the repo root (the root by default; outside the repo it exits 3 app_fuera_del_repo)",
|
|
234
235
|
"--repo owner/repo: the repo, if it isn't this folder's «origin» (the file goes in this clone only if it is that repo: its origin, or a `supcoflow` remote at https://github.com/owner/repo.git; otherwise it exits 3 sin_clon saying what this clone is and the exact `git remote add supcoflow …`, without asking the engine)",
|
|
235
236
|
"--dry-run: shows the file and the difference without writing anything",
|
|
@@ -240,8 +241,8 @@ export const DESCRIPCION = {
|
|
|
240
241
|
},
|
|
241
242
|
{
|
|
242
243
|
nombre: "diagnose",
|
|
243
|
-
resumen: "Says, free of charge and writing nothing, how far the installation is (sign-in, repo, GitHub App, allowed sites, chat and
|
|
244
|
-
flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin»", "--json: { instalacion: { ok, total:
|
|
244
|
+
resumen: "Says, free of charge and writing nothing, how far the installation is (sign-in, repo, GitHub App, allowed sites, chat, guide and interface: N of 7) and, apart, whether a test of the project would go in and how far it would get: policy, monthly balance against the test's ceiling, open tests, whether THIS machine can save the test request (the local point `maquina`, checked without writing anything) and whatever else the engine adds. Exits 0 if everything is ready and 4 if something needs fixing (no_listo), with each point; if `maquina` isn't ok, `listo` and `prueba.listo` are false and the N of 7 doesn't change. The seventh point, `interfaz`, is local and only reads: if .supcoflow/instalacion.json records an own interface (`forma: interfaz_propia`) it checks that the recorded files are still there and that the recorded contract is not behind a new major version (the first number) of the one the project declares (`interfaz_propia.contrato.version` in `widget`; a newer minor or patch only adds optional points and stays ok, saying so); without an own interface it is ok («Not applicable»). It is advice, marked `aviso: true` in the JSON: it lowers the N of 7 but doesn't stop a test.",
|
|
245
|
+
flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin»", "--json: { instalacion: { ok, total: 7, puntos }, prueba: { listo, puntos }, maquina: { ok, impedimentos: [{ codigo, detalle }] }, intento_pendiente: { techo_usd, politica } | null, … }"],
|
|
245
246
|
ejemplos: ["supcoflow diagnose --json"],
|
|
246
247
|
codigos: [0, 1, 2, 3, 4],
|
|
247
248
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/comandos/instalacion.ts
|
|
2
|
-
// `install record --skill <versión> [--app <carpeta>] [--file <ruta>] [--dry-run]`
|
|
3
|
-
// `.supcoflow/instalacion.json` con qué versiones se instaló el chat y, desde la 0.
|
|
4
|
-
// (
|
|
5
|
-
//
|
|
2
|
+
// `install record --skill <versión> [--app <carpeta>] [--file <ruta>]... [--shape widget|own-interface] [--dry-run]`
|
|
3
|
+
// (bloque 6, 0.8.0 y 0.9.0): deja en `.supcoflow/instalacion.json` con qué versiones se instaló el chat y, desde la 0.9.0
|
|
4
|
+
// (v3, T8), qué se instaló (`--shape`: el widget o una interfaz propia de la app), en qué archivos quedó (`--file`, repetible)
|
|
5
|
+
// y qué versión del contrato de presentación trae el paquete instalado (su `contract.json`). Lee la configuración del motor y
|
|
6
|
+
// los `package.json` y `contract.json` de la app y del paquete instalado; nunca ejecuta nada del repo (ni `npm ls` ni `node`).
|
|
6
7
|
//
|
|
7
8
|
// La regla de §13 («no pisa archivos ni cambios sin commit») adaptada a un archivo que tiene que poder cambiar
|
|
8
9
|
// (decisión 11): se reescribe sólo si git ya guarda el anterior. El control se hace ANTES de escribir; si la
|
|
@@ -11,11 +12,13 @@
|
|
|
11
12
|
import { lstatSync, realpathSync, statSync } from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import { resultadoDeFallo, SALIDA, taparSecretos } from "../contrato.js";
|
|
14
|
-
import {
|
|
15
|
+
import { anotadoEn, armarInstalacion, leerInstalacionAnotada, esSemver, PAQUETE_DEL_CHAT, RUTA_DE_LA_INSTALACION, serializar, validarLosArchivos } from "../instalacion.js";
|
|
15
16
|
import { NO_LEIDO } from "../lectura.js";
|
|
16
17
|
import { repoPedido } from "../repo-local.js";
|
|
17
18
|
import { fallo } from "./configurar.js";
|
|
18
19
|
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
20
|
+
// `--shape` en inglés; al archivo va el nombre de la API (como `--mode save` y `guardar`).
|
|
21
|
+
const FORMA_DE_LA_FLAG = { widget: "widget", "own-interface": "interfaz_propia" };
|
|
19
22
|
export function esEnlaceReal(p) {
|
|
20
23
|
try {
|
|
21
24
|
return lstatSync(p).isSymbolicLink();
|
|
@@ -59,10 +62,22 @@ function leerJson(d, p, nombre) {
|
|
|
59
62
|
return { ok: false, noExiste: false, resultado: fallo(SALIDA.inesperado, "paquete_ilegible", `${nombre} isn't valid JSON.`) };
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
function contratoDelPaquete(d, raiz, carpetaReal, avisos) {
|
|
66
|
+
const candidato = path.join(carpetaReal, "contract.json");
|
|
67
|
+
const real = d.rutaReal(candidato);
|
|
68
|
+
if (real === null)
|
|
69
|
+
return { ok: true, valor: null };
|
|
70
|
+
if (!adentro(raiz, real)) {
|
|
71
|
+
const aviso = `${path.relative(raiz, candidato)} points outside the repo; I don't read it and I record the contract as null.`;
|
|
72
|
+
avisos.push(aviso);
|
|
73
|
+
d.avisar(aviso);
|
|
74
|
+
return { ok: true, valor: null };
|
|
75
|
+
}
|
|
76
|
+
const j = leerJson(d, real, `the contract.json of ${PAQUETE_DEL_CHAT}`);
|
|
77
|
+
if (j.ok)
|
|
78
|
+
return j;
|
|
79
|
+
return { ok: false, resultado: j.noExiste ? fallo(SALIDA.inesperado, "paquete_ilegible", `I can't read the contract.json of ${PAQUETE_DEL_CHAT}.`) : j.resultado };
|
|
80
|
+
}
|
|
66
81
|
function paqueteInstalado(d, raiz, app, avisos) {
|
|
67
82
|
for (let dir = app;; dir = path.dirname(dir)) {
|
|
68
83
|
const candidato = path.join(dir, "node_modules", ...PAQUETE_DEL_CHAT.split("/"), "package.json");
|
|
@@ -72,16 +87,18 @@ function paqueteInstalado(d, raiz, app, avisos) {
|
|
|
72
87
|
const aviso = `${path.relative(raiz, candidato)} points outside the repo; I don't read it and I record the installed chat as null.`;
|
|
73
88
|
avisos.push(aviso);
|
|
74
89
|
d.avisar(aviso);
|
|
75
|
-
return { ok: true, valor: null };
|
|
90
|
+
return { ok: true, valor: null, contrato: null };
|
|
76
91
|
}
|
|
77
92
|
const j = leerJson(d, real, `the installed package.json of ${PAQUETE_DEL_CHAT}`);
|
|
78
|
-
if (j.ok)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
if (!j.ok) {
|
|
94
|
+
// Existía hace un instante: si ahora no está, tampoco se anota «no está».
|
|
95
|
+
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 };
|
|
96
|
+
}
|
|
97
|
+
const c = contratoDelPaquete(d, raiz, path.dirname(real), avisos);
|
|
98
|
+
return c.ok ? { ok: true, valor: j.valor, contrato: c.valor } : c;
|
|
82
99
|
}
|
|
83
100
|
if (dir === raiz || path.dirname(dir) === dir)
|
|
84
|
-
return { ok: true, valor: null };
|
|
101
|
+
return { ok: true, valor: null, contrato: null };
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
104
|
async function estadoDelArchivo(d, raizGit, existe) {
|
|
@@ -123,19 +140,29 @@ function diff(antes, despues) {
|
|
|
123
140
|
}
|
|
124
141
|
return lineas.join("\n");
|
|
125
142
|
}
|
|
126
|
-
const resumen = (x) => `skill ${x.skill}, terminal ${x.terminal}, chat declared ${x.chat.declarado ?? "—"} / installed ${x.chat.instalado ?? "—"}
|
|
143
|
+
const resumen = (x) => `skill ${x.skill}, terminal ${x.terminal}, chat declared ${x.chat.declarado ?? "—"} / installed ${x.chat.instalado ?? "—"}`
|
|
144
|
+
+ `${x.forma === null ? "" : `, ${x.forma === "widget" ? "widget" : "own interface"}`}`
|
|
145
|
+
+ `${x.archivos.length === 0 ? "" : `, ${x.archivos.length === 1 ? "file" : "files"} ${x.archivos.join(", ")}`}`
|
|
146
|
+
+ `${x.contrato === null ? "" : `, contract ${x.contrato}`}`;
|
|
127
147
|
export async function instalacionAnotar(d, flags) {
|
|
128
148
|
if (flags.skill === null)
|
|
129
149
|
return fallo(SALIDA.entradaInvalida, "falta_skill", "The skill version is missing: `supcoflow install record --skill 0.1.0`.");
|
|
130
150
|
if (!esSemver(flags.skill))
|
|
131
151
|
return fallo(SALIDA.entradaInvalida, "skill_invalida", "--skill goes with a semver version (for example 0.1.0).");
|
|
132
|
-
//
|
|
133
|
-
let
|
|
134
|
-
if (flags.
|
|
135
|
-
|
|
152
|
+
// La forma y los archivos del chat se miran antes de cualquier pedido: algo imposible no gasta una vuelta al motor.
|
|
153
|
+
let formaPedida = null;
|
|
154
|
+
if (flags.shape !== undefined && flags.shape !== null) {
|
|
155
|
+
formaPedida = Object.hasOwn(FORMA_DE_LA_FLAG, flags.shape) ? FORMA_DE_LA_FLAG[flags.shape] : null;
|
|
156
|
+
if (formaPedida === null)
|
|
157
|
+
return fallo(SALIDA.entradaInvalida, "forma_invalida", "--shape goes with `widget` (the packaged widget) or `own-interface` (a chat the app builds with its own components).");
|
|
158
|
+
}
|
|
159
|
+
const crudos = flags.files !== undefined && flags.files.length > 0 ? flags.files : flags.file !== undefined && flags.file !== null ? [flags.file] : [];
|
|
160
|
+
let archivosPedidos = null;
|
|
161
|
+
if (crudos.length > 0) {
|
|
162
|
+
const v = validarLosArchivos(crudos);
|
|
136
163
|
if (!v.ok)
|
|
137
164
|
return fallo(SALIDA.entradaInvalida, "archivo_invalido", v.motivo);
|
|
138
|
-
|
|
165
|
+
archivosPedidos = v.rutas;
|
|
139
166
|
}
|
|
140
167
|
// ─── La raíz y la app, antes de cualquier pedido ───
|
|
141
168
|
let raizGit;
|
|
@@ -162,7 +189,7 @@ export async function instalacionAnotar(d, flags) {
|
|
|
162
189
|
return fallo(SALIDA.entradaInvalida, "app_inexistente", "--app has to be a folder (the app's), not a file.");
|
|
163
190
|
// `--file`: ni un enlace simbólico ni un camino que pase por uno (saldría del repo sin que se vea). El archivo todavía
|
|
164
191
|
// puede no existir (con `--dry-run` se anota antes de escribirlo), pero ninguna carpeta del camino puede ser un enlace.
|
|
165
|
-
|
|
192
|
+
for (const archivoPedido of archivosPedidos ?? []) {
|
|
166
193
|
const partes = archivoPedido.split("/");
|
|
167
194
|
for (let i = 1; i <= partes.length; i++) {
|
|
168
195
|
if (d.esEnlace(path.join(raiz, ...partes.slice(0, i)))) {
|
|
@@ -197,18 +224,32 @@ export async function instalacionAnotar(d, flags) {
|
|
|
197
224
|
const actual = d.leerArchivoChico(archivo, TOPE_DEL_ARCHIVO);
|
|
198
225
|
const existe = !("error" in actual) || actual.error !== NO_LEIDO.noExiste;
|
|
199
226
|
const antes = "bytes" in actual ? Buffer.from(actual.bytes).toString("utf8") : null;
|
|
200
|
-
// Re-correr sin `--file` no borra
|
|
201
|
-
|
|
227
|
+
// Re-correr sin `--shape` ni `--file` no borra lo que ya se anotó (v2: su `archivo` es el widget; v1: nada, sin error).
|
|
228
|
+
// `--file` reemplaza la lista entera y `--shape` la forma: lo que se pide manda, lo que no, se conserva.
|
|
229
|
+
// Un archivo de una terminal más nueva no se reescribe: se perdería lo que esa versión anotó.
|
|
230
|
+
if (leerInstalacionAnotada(antes).estado === "mas_nueva") {
|
|
231
|
+
return fallo(SALIDA.haceFaltaUnaPersona, "escrito_por_una_terminal_mas_nueva", `${RUTA_DE_LA_INSTALACION} was written by a newer supcoflow than this one; I didn't touch it. Update supcoflow (\`npx supcoflow@latest\`) and run again.`);
|
|
232
|
+
}
|
|
233
|
+
const anotado = anotadoEn(antes);
|
|
234
|
+
// Cambiar la forma sin decir los archivos no hereda los de la forma anterior (el envoltorio del widget no es la interfaz propia).
|
|
235
|
+
const cambiaLaForma = formaPedida !== null && anotado.forma !== null && formaPedida !== anotado.forma;
|
|
202
236
|
let x;
|
|
203
237
|
try {
|
|
204
|
-
x = armarInstalacion({
|
|
238
|
+
x = armarInstalacion({
|
|
239
|
+
skill: flags.skill, terminal: d.terminal, paqueteDeLaApp: deLaApp.ok ? deLaApp.valor : null, paqueteInstalado: instalado.valor, configuracion,
|
|
240
|
+
forma: formaPedida ?? anotado.forma, archivos: archivosPedidos ?? (cambiaLaForma ? [] : anotado.archivos), contrato: instalado.contrato,
|
|
241
|
+
});
|
|
205
242
|
}
|
|
206
243
|
catch (e) {
|
|
207
244
|
return resultadoDeFallo(e);
|
|
208
245
|
}
|
|
209
246
|
const nuevo = serializar(x);
|
|
210
247
|
// ─── ¿Se puede escribir? ───
|
|
211
|
-
|
|
248
|
+
// `archivo` (el primero) sigue en el JSON por las skills de antes de la 0.9.0; lo nuevo es `archivos`, `forma` y `contrato`.
|
|
249
|
+
const json = {
|
|
250
|
+
ok: true, ruta: RUTA_DE_LA_INSTALACION, instalacion: x, archivo: x.archivos[0] ?? null, archivos: x.archivos, forma: x.forma, contrato: x.contrato,
|
|
251
|
+
...(avisos.length > 0 ? { avisos } : {}),
|
|
252
|
+
};
|
|
212
253
|
if ("bytes" in actual && Buffer.from(actual.bytes).equals(Buffer.from(nuevo, "utf8"))) {
|
|
213
254
|
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.` };
|
|
214
255
|
}
|
package/dist/comandos/probar.js
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
// `supcoflow diagnose` y `supcoflow test` (bloque 7, la prueba): ¿un problema que entra por el
|
|
2
|
+
// chat llega hasta un issue? El diagnóstico es gratis y sólo lee. La prueba gasta un relevamiento (techo
|
|
3
|
+
// del motor, hoy US$ 2): sin `--authorize-spend` no se manda nada que escriba (R6).
|
|
4
|
+
//
|
|
5
|
+
// Lo que la persona vio es lo que viaja (R5, 0.8.0): `--authorize-spend` exige `--ceiling` (el número que se le
|
|
6
|
+
// mostró) y `--expect-policy` (la política con la que se le ofreció) y manda esos dos valores; no relee el
|
|
7
|
+
// diagnóstico para elegirlos. El motor los compara con los de ahora: `techo_distinto` / `politica_distinta` salen
|
|
8
|
+
// 4 con los valores nuevos y sin reintentar, y la persona decide de nuevo. El alias viejo `probar --autorizo-gasto`
|
|
9
|
+
// exige también `--ceiling` (un alias no puede ampliar lo autorizado) pero no `--expect-policy`.
|
|
10
|
+
//
|
|
11
|
+
// El ciclo de la clave (hallazgo 5), en el orden en que se lee abajo:
|
|
12
|
+
// 1. Sin intento guardado: clave nueva, y el intento ENTERO guardado antes del POST (estricto).
|
|
13
|
+
// 2. Con intento guardado: es un reintento; misma clave, mismo mensaje, mismo techo, misma política. Otro
|
|
14
|
+
// `--message`, otro `--ceiling` u otra `--expect-policy` sale 4 `intento_pendiente` sin mandar nada.
|
|
15
|
+
// 3. 202: la última prueba = la del motor y el intento cerrado, en una escritura (src/pruebas-guardadas.ts).
|
|
16
|
+
// Si falla, sale 1 con el id y el intento sigue abierto: el próximo recibe la misma prueba.
|
|
17
|
+
// 4. Ambigua (sin respuesta, 5xx, pausa, otro intento en curso): el intento se conserva. Rechazo
|
|
18
|
+
// definitivo: se cierra, el motor no creó nada con esa clave.
|
|
19
|
+
// 5. Una prueba nueva es `test --authorize-spend` sin intento abierto: clave nueva aunque el mensaje
|
|
20
|
+
// sea el mismo.
|
|
21
|
+
//
|
|
22
|
+
// `--wait` sigue la prueba por tramos (≤ 90 s el comando entero, como `login --wait`): el estado lo deriva
|
|
23
|
+
// el motor en cada llamada y acá sólo se traduce a un código de salida con la tabla de la prueba.
|
|
24
|
+
import path from "node:path";
|
|
1
25
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
|
-
import { agruparPuntos, contarBuenos, lineaDeLoRevisado, nombreDelPunto, puntoDeSesion, puntoDelRepo, TOTAL_DE_INSTALACION } from "../diagnostico.js";
|
|
26
|
+
import { agruparPuntos, contarBuenos, lineaDeLoRevisado, nombreDelPunto, puntoDeLaInterfaz, puntoDeSesion, puntoDelRepo, TOTAL_DE_INSTALACION } from "../diagnostico.js";
|
|
27
|
+
import { leerInstalacionAnotada, RUTA_DE_LA_INSTALACION } from "../instalacion.js";
|
|
3
28
|
import { leerRepoLocal } from "../repo-local.js";
|
|
4
29
|
import { PLAZO_MINIMO_DE_SONDEO_MS, TRAMO_MAXIMO_MS } from "../sondeo.js";
|
|
5
30
|
import { fallo, rutaDe } from "./configurar.js";
|
|
@@ -64,6 +89,41 @@ function puntoDeLaMaquina(r) {
|
|
|
64
89
|
? { punto: "maquina", ok: true, mensaje: "No obstacle found to saving the test request on this machine.", grupo: "prueba" }
|
|
65
90
|
: { punto: "maquina", ok: false, mensaje: `This machine can't save the test request yet: ${detalles(r)}`, grupo: "prueba" };
|
|
66
91
|
}
|
|
92
|
+
// El punto 7 (T8): lee `.supcoflow/instalacion.json` del clon y, sólo si hay una interfaz propia anotada, mira que sus archivos
|
|
93
|
+
// sigan y le pregunta al proyecto qué contrato declara hoy. Todo lo que no se pueda mirar queda «sin mirar», nunca en contra.
|
|
94
|
+
async function puntoDeLaInterfazDeEsteClon(d, local, proyecto, clave) {
|
|
95
|
+
const esElClon = local.raiz !== null && local.repo !== null && local.repo.toLowerCase() === proyecto.repo.toLowerCase();
|
|
96
|
+
if (!esElClon || d.archivos === undefined)
|
|
97
|
+
return puntoDeLaInterfaz({ lectura: null, faltan: [], delProyecto: null });
|
|
98
|
+
let lectura;
|
|
99
|
+
try {
|
|
100
|
+
lectura = leerInstalacionAnotada(d.archivos.leer(path.join(local.raiz, RUTA_DE_LA_INSTALACION)));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
lectura = { estado: "ilegible" };
|
|
104
|
+
}
|
|
105
|
+
if (lectura.estado !== "leida" || lectura.forma !== "interfaz_propia")
|
|
106
|
+
return puntoDeLaInterfaz({ lectura, faltan: [], delProyecto: null });
|
|
107
|
+
const faltan = lectura.archivos.filter((f) => {
|
|
108
|
+
try {
|
|
109
|
+
const completo = path.join(local.raiz, f);
|
|
110
|
+
return d.existe !== undefined ? !d.existe(completo) : d.archivos.leer(completo) === null;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
let delProyecto = null;
|
|
117
|
+
if (d.leerWidget !== undefined) {
|
|
118
|
+
try {
|
|
119
|
+
delProyecto = (await d.leerWidget(d.api, clave, proyecto.id)).interfaz_propia?.contrato.version ?? null;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
delProyecto = null; // no se pudo preguntar: no se compara
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return puntoDeLaInterfaz({ lectura, faltan, delProyecto });
|
|
126
|
+
}
|
|
67
127
|
export async function diagnosticar(d, flags) {
|
|
68
128
|
const p = await proyectoDelRepo(d, flags);
|
|
69
129
|
if (!p.ok)
|
|
@@ -84,10 +144,10 @@ export async function diagnosticar(d, flags) {
|
|
|
84
144
|
catch {
|
|
85
145
|
local = { repo: null, raiz: null, motivo: "sin_git" };
|
|
86
146
|
}
|
|
87
|
-
const agrupados = agruparPuntos(g, [puntoDeSesion(), puntoDelRepo(local, p.proyecto.repo)]);
|
|
147
|
+
const agrupados = agruparPuntos(g, [puntoDeSesion(), puntoDelRepo(local, p.proyecto.repo), await puntoDeLaInterfazDeEsteClon(d, local, p.proyecto, p.clave)]);
|
|
88
148
|
const instalacion = agrupados.instalacion;
|
|
89
149
|
// El punto local de la prueba (1A.1): ¿esta máquina puede guardar el pedido? Si no, la prueba no entra, y se dice acá y
|
|
90
|
-
// no después del OK de gasto. No cuenta en el «N of
|
|
150
|
+
// no después del OK de gasto. No cuenta en el «N of 7» de la instalación.
|
|
91
151
|
const revision = await revisarLaMaquina(d, p.proyecto.id);
|
|
92
152
|
const prueba = [puntoDeLaMaquina(revision), ...agrupados.prueba];
|
|
93
153
|
const buenos = contarBuenos(instalacion);
|
package/dist/comandos/widget.js
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { resultadoDeFallo, SALIDA } from "../contrato.js";
|
|
2
2
|
import { proyectoDelRepo } from "./proyecto-del-repo.js";
|
|
3
|
+
// La forma «interfaz propia» (T8): sin código para pegar. La app arma el chat con sus propios componentes; esto es lo que
|
|
4
|
+
// hay que instalar y el contrato que la interfaz tiene que cumplir. Todo sale del motor, tal cual (también las rutas). Con
|
|
5
|
+
// `sin_publicar` la versión todavía no está en npm: no se ofrece el camino (ni un `npm i` que fallaría); el JSON sí lo dice.
|
|
6
|
+
function textoDeLaInterfazPropia(p) {
|
|
7
|
+
if (p.estado !== "disponible") {
|
|
8
|
+
return ["", `Own interface: not published yet (${p.paquete}@${p.version} is not on npm); use the widget above.`];
|
|
9
|
+
}
|
|
10
|
+
return [
|
|
11
|
+
"",
|
|
12
|
+
"Own interface (build the chat with the app's own components, instead of steps 2 and 3; there is no code to paste):",
|
|
13
|
+
" Install the package (the exact version):",
|
|
14
|
+
` ${p.instalar}`,
|
|
15
|
+
` tenantId: ${p.opciones_del_nucleo.tenantId}`,
|
|
16
|
+
` projectId: ${p.opciones_del_nucleo.projectId}`,
|
|
17
|
+
` backendUrl: ${p.backend_url}`,
|
|
18
|
+
" Content Security Policy (CSP): if the app has `connect-src`, add this origin and keep what it already has:",
|
|
19
|
+
` ${p.csp}`,
|
|
20
|
+
` Presentation contract: ${p.contrato.version}. Once the package is installed, read it offline (paths relative to the folder where you ran npm i):`,
|
|
21
|
+
` ${p.contrato.archivo}`,
|
|
22
|
+
` ${p.contrato.documento}`,
|
|
23
|
+
" What the interface must do is in the contract; how it looks is the app's.",
|
|
24
|
+
` Entry points: ${p.entradas.nucleo} (core), ${p.entradas.react} (React), ${p.entradas.pruebas} and ${p.entradas.servidor_de_pruebas} (test bench).`,
|
|
25
|
+
` Docs: ${p.documentos.nucleo}, ${p.documentos.react}, ${p.documentos.pruebas}, ${p.documentos.conformidad}, ${p.documentos.protocolo}.`,
|
|
26
|
+
];
|
|
27
|
+
}
|
|
3
28
|
function textoDe(w) {
|
|
4
29
|
const anteriores = w.nextjs.archivos_anteriores;
|
|
5
30
|
return [
|
|
@@ -20,6 +45,7 @@ function textoDe(w) {
|
|
|
20
45
|
"",
|
|
21
46
|
"Alternative with <script> (available, not tested in a real app):",
|
|
22
47
|
` ${w.script.linea}`,
|
|
48
|
+
...(w.interfaz_propia === undefined ? [] : textoDeLaInterfazPropia(w.interfaz_propia)),
|
|
23
49
|
].join("\n");
|
|
24
50
|
}
|
|
25
51
|
export async function widget(d, flags) {
|
package/dist/diagnostico.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { compararSemver, RUTA_DE_LA_INSTALACION } from "./instalacion.js";
|
|
1
2
|
import { frasePorQueNoAtaClon } from "./repo-local.js";
|
|
2
|
-
export const TOTAL_DE_INSTALACION = 6;
|
|
3
3
|
// El orden y los nombres con que se dicen: los ids de los puntos del motor son datos suyos y no cambian.
|
|
4
4
|
const DE_LA_INSTALACION = [
|
|
5
5
|
{ punto: "sign-in", como: "sign-in" },
|
|
@@ -8,7 +8,9 @@ const DE_LA_INSTALACION = [
|
|
|
8
8
|
{ punto: "sitios", como: "allowed sites" },
|
|
9
9
|
{ punto: "chat", como: "chat" },
|
|
10
10
|
{ punto: "guia", como: "guide" },
|
|
11
|
+
{ punto: "interfaz", como: "interface" },
|
|
11
12
|
];
|
|
13
|
+
export const TOTAL_DE_INSTALACION = DE_LA_INSTALACION.length;
|
|
12
14
|
const DEL_MOTOR = new Set(["instalacion", "sitios", "chat", "guia"]);
|
|
13
15
|
export const lineaDeLoRevisado = (n) => `Checked ${DE_LA_INSTALACION.map((x) => x.como).slice(0, -1).join(", ")} and ${DE_LA_INSTALACION.at(-1).como}: ${n} of ${TOTAL_DE_INSTALACION}, at no cost.`;
|
|
14
16
|
export const nombreDelPunto = (punto) => DE_LA_INSTALACION.find((x) => x.punto === punto)?.como ?? punto;
|
|
@@ -31,6 +33,46 @@ export function puntoDelRepo(local, repoDelProyecto) {
|
|
|
31
33
|
}
|
|
32
34
|
return { punto: "repo", ok: true, mensaje: `The origin of this folder is ${local.repo}, the project's repo.`, grupo: "instalacion" };
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* El punto 7 (T8): la interfaz propia de la app. Puro: recibe lo ya leído. Es un AVISO local: baja el «N of 7» pero no frena una
|
|
38
|
+
* prueba (la prueba mira que un problema llegue a un issue, no cómo se ve el chat). Sin interfaz propia anotada no aplica y
|
|
39
|
+
* cuenta como ok. El contenido de `instalacion.json` nunca se copia al mensaje.
|
|
40
|
+
* · `lectura`: lo que dice el archivo (o `null` si no se miró: el clon no es el del repo del proyecto).
|
|
41
|
+
* · `faltan`: los archivos anotados que ya no están.
|
|
42
|
+
* · `delProyecto`: el contrato que declara el proyecto hoy (`interfaz_propia.contrato.version`), o null si no se supo.
|
|
43
|
+
*/
|
|
44
|
+
const mayor = (v) => Number(v.split(".")[0]);
|
|
45
|
+
export function puntoDeLaInterfaz(i) {
|
|
46
|
+
const punto = (ok, mensaje) => ({ punto: "interfaz", ok, mensaje, grupo: "instalacion", aviso: true });
|
|
47
|
+
if (i.lectura === null)
|
|
48
|
+
return punto(true, "Not checked: this folder isn't a clone of the project's repo, so I don't read its installation record.");
|
|
49
|
+
const l = i.lectura;
|
|
50
|
+
if (l.estado === "ausente")
|
|
51
|
+
return punto(true, `Not applicable: there's no ${RUTA_DE_LA_INSTALACION}, so no own interface is recorded.`);
|
|
52
|
+
if (l.estado === "mas_nueva")
|
|
53
|
+
return punto(false, `${RUTA_DE_LA_INSTALACION} was written by a newer supcoflow than this one: update supcoflow (\`npm i -g supcoflow@latest\` or \`npx supcoflow@latest\`) to read it.`);
|
|
54
|
+
if (l.estado === "ilegible")
|
|
55
|
+
return punto(false, `I can't read ${RUTA_DE_LA_INSTALACION}: run \`supcoflow install record\` again.`);
|
|
56
|
+
if (l.forma !== "interfaz_propia") {
|
|
57
|
+
return punto(true, l.forma === "widget" ? "Not applicable: the chat is recorded as the packaged widget, which has no interface contract to check." : "Not applicable: no own interface is recorded.");
|
|
58
|
+
}
|
|
59
|
+
if (l.archivos.length === 0)
|
|
60
|
+
return punto(false, "The installation says the chat is an own interface but records none of its files: run `supcoflow install record --file …` again.");
|
|
61
|
+
if (i.faltan.length > 0)
|
|
62
|
+
return punto(false, `These files of the interface are recorded but gone: ${i.faltan.join(", ")}. Run the skill again to update the record.`);
|
|
63
|
+
if (l.contrato === null) {
|
|
64
|
+
return punto(false, "The interface doesn't record which presentation contract it follows (the installed @supcoflow/chat has no contract.json): update the package and run `supcoflow install record` again.");
|
|
65
|
+
}
|
|
66
|
+
const archivos = `${l.archivos.length} ${l.archivos.length === 1 ? "file" : "files"}`;
|
|
67
|
+
if (i.delProyecto !== null && compararSemver(i.delProyecto, l.contrato) > 0) {
|
|
68
|
+
// Un obligatorio nuevo sólo llega con una versión MAYOR del contrato (plan §2.5.8): una menor o de parche suma lo opcional.
|
|
69
|
+
if (mayor(i.delProyecto) > mayor(l.contrato)) {
|
|
70
|
+
return punto(false, `The interface (${archivos}) follows contract ${l.contrato}, and the project now declares ${i.delProyecto}, a new major version: run the skill again to see what changed and check that the interface still conforms.`);
|
|
71
|
+
}
|
|
72
|
+
return punto(true, `The interface (${archivos}) follows contract ${l.contrato}; the project declares ${i.delProyecto}, which only adds optional points: run the skill again to see them.`);
|
|
73
|
+
}
|
|
74
|
+
return punto(true, `The interface (${archivos}) follows contract ${l.contrato}${i.delProyecto === l.contrato ? ", the one the project declares" : ""}.`);
|
|
75
|
+
}
|
|
34
76
|
/** Reparte los puntos del motor en los dos grupos y suma los dos locales al de la instalación. */
|
|
35
77
|
export function agruparPuntos(g, locales) {
|
|
36
78
|
const delMotor = g.puntos.map((p) => ({ ...p, grupo: p.grupo ?? (DEL_MOTOR.has(p.punto) ? "instalacion" : "prueba") }));
|
package/dist/entorno.js
CHANGED
|
@@ -508,7 +508,7 @@ export function entornoReal(env = process.env) {
|
|
|
508
508
|
widget: {
|
|
509
509
|
api, guardado, git: gitReal, cwd, archivos: archivosDelRepo(avisar),
|
|
510
510
|
buscarProyecto: configurarEnElMotor.buscarProyecto,
|
|
511
|
-
leerWidget: (a, clave, id) => leerWidget(a, clave, id, fetch),
|
|
511
|
+
leerWidget: (a, clave, id) => leerWidget(a, clave, id, fetch, avisar),
|
|
512
512
|
},
|
|
513
513
|
// La versión que se anota es la de `describir` (pinzada contra package.json por test): una sola fuente
|
|
514
514
|
// para lo que la terminal dice de sí misma.
|
|
@@ -522,6 +522,14 @@ export function entornoReal(env = process.env) {
|
|
|
522
522
|
api, guardado, git: gitReal, cwd, avisar, archivos: archivosDelRepo(avisar), pruebas, generar: randomUUID, ahora: Date.now, dormir, tramoMs,
|
|
523
523
|
buscarProyecto: configurarEnElMotor.buscarProyecto,
|
|
524
524
|
leerDiagnostico: (a, clave, id) => leerDiagnostico(a, clave, id, fetch),
|
|
525
|
+
leerWidget: (a, clave, id) => leerWidget(a, clave, id, fetch, avisar),
|
|
526
|
+
existe: (p) => { try {
|
|
527
|
+
lstatSync(p);
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return false;
|
|
532
|
+
} },
|
|
525
533
|
pedirPrueba: (a, clave, id, intento) => pedirPrueba(a, clave, id, intento, fetch),
|
|
526
534
|
leerPrueba: (a, clave, id, pruebaId, plazoMs) => leerPrueba(a, clave, id, pruebaId, fetch, plazoMs),
|
|
527
535
|
},
|
|
@@ -28,8 +28,12 @@ export const FLAGS_POR_COMANDO = [
|
|
|
28
28
|
flags: ["authorizeSpend", "ceiling", "expectPolicy", "message", "testId"], comandos: ["test"],
|
|
29
29
|
texto: "--authorize-spend, --ceiling, --expect-policy, --message and --test-id belong to `supcoflow test`.",
|
|
30
30
|
},
|
|
31
|
-
{ flags: ["skill", "app"], comandos: ["install record"], texto: "--skill and --
|
|
31
|
+
{ flags: ["skill", "app", "shape"], comandos: ["install record"], texto: "--skill, --app and --shape belong to `supcoflow install record`." },
|
|
32
32
|
];
|
|
33
|
+
/** `--file` se repite sólo en `install record` (la interfaz propia vive en varios archivos); en los demás va una vez. */
|
|
34
|
+
export function archivosDeMas(comando, cuantos) {
|
|
35
|
+
return cuantos > 1 && comando !== "install record" ? "--file goes once, with a value. Try `supcoflow --help`." : null;
|
|
36
|
+
}
|
|
33
37
|
/** El mensaje del primer flag presente que el comando no acepta, o null si todo aplica. */
|
|
34
38
|
export function flagQueNoAplica(comando, presentes) {
|
|
35
39
|
for (const regla of FLAGS_POR_COMANDO) {
|
package/dist/instalacion.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `.supcoflow/instalacion.json` (bloque 6, decisión 3; v2 en la 0.8.0): con qué versiones se hizo la instalación, anotado en el
|
|
1
|
+
// `.supcoflow/instalacion.json` (bloque 6, decisión 3; v2 en la 0.8.0; v3 en la 0.9.0): con qué versiones se hizo la instalación, anotado en el
|
|
2
2
|
// repo de quien instala. Supcoflow no lo guarda ni lo ve. Puro: recibe lo ya leído y arma el archivo.
|
|
3
3
|
//
|
|
4
4
|
// El archivo queda en el repo y se commitea: un secreto acá se publica con el primer `push`. Por eso se arma con
|
|
@@ -7,9 +7,22 @@
|
|
|
7
7
|
import { FalloDeLaTerminal, SALIDA, taparSecretos } from "./contrato.js";
|
|
8
8
|
export const RUTA_DE_LA_INSTALACION = ".supcoflow/instalacion.json";
|
|
9
9
|
export const PAQUETE_DEL_CHAT = "@supcoflow/chat";
|
|
10
|
+
export const FORMAS = ["widget", "interfaz_propia"];
|
|
11
|
+
export const MAX_ARCHIVOS_ANOTADOS = 20;
|
|
10
12
|
// 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
13
|
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
14
|
export const esSemver = (s) => s.length <= 64 && SEMVER.test(s);
|
|
15
|
+
/** Compara `major.minor.patch` número a número (1.10.0 > 1.9.0); lo que sigue al guion (prerelease) o al `+` no cuenta. */
|
|
16
|
+
export function compararSemver(a, b) {
|
|
17
|
+
const partes = (v) => v.split(/[-+]/)[0].split(".").map(Number);
|
|
18
|
+
const [x, y] = [partes(a), partes(b)];
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
21
|
+
if (d !== 0)
|
|
22
|
+
return d < 0 ? -1 : 1;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
13
26
|
const objeto = (v) => (typeof v === "object" && v !== null && !Array.isArray(v) ? v : null);
|
|
14
27
|
const entero = (v) => typeof v === "number" && Number.isInteger(v) && v >= 1;
|
|
15
28
|
const ilegible = (message) => new FalloDeLaTerminal(SALIDA.inesperado, "paquete_ilegible", message);
|
|
@@ -71,14 +84,26 @@ function guiaDe(c) {
|
|
|
71
84
|
return { origen: "agente", version: g.version };
|
|
72
85
|
throw inesperada("a guide");
|
|
73
86
|
}
|
|
87
|
+
// El `contract.json` del paquete instalado (T1 del sdk): sólo se copia su `version`, nunca el resto. Ausente es null (un
|
|
88
|
+
// paquete anterior al contrato); presente sin una versión que se entienda NO es null: diría que no hay contrato.
|
|
89
|
+
function contratoDe(contrato) {
|
|
90
|
+
if (contrato === null || contrato === undefined)
|
|
91
|
+
return null;
|
|
92
|
+
const v = objeto(contrato)?.version;
|
|
93
|
+
if (typeof v !== "string" || !esSemver(v))
|
|
94
|
+
throw ilegible(`The contract.json of ${PAQUETE_DEL_CHAT} has no version I understand.`);
|
|
95
|
+
return v;
|
|
96
|
+
}
|
|
74
97
|
export function armarInstalacion(i) {
|
|
75
98
|
return {
|
|
76
|
-
version:
|
|
99
|
+
version: 3,
|
|
77
100
|
skill: i.skill,
|
|
78
101
|
terminal: i.terminal,
|
|
79
102
|
chat: { declarado: declaradoEn(i.paqueteDeLaApp), instalado: instaladoEn(i.paqueteInstalado) },
|
|
80
103
|
configuracion: { acuerdo_version: acuerdoDe(i.configuracion), politica: politicaDe(i.configuracion), guia: guiaDe(i.configuracion) },
|
|
81
|
-
|
|
104
|
+
forma: i.forma ?? null,
|
|
105
|
+
archivos: [...(i.archivos ?? [])],
|
|
106
|
+
contrato: contratoDe(i.contrato),
|
|
82
107
|
};
|
|
83
108
|
}
|
|
84
109
|
// ─── El archivo del chat (`install record --file`) ───
|
|
@@ -99,25 +124,73 @@ export function validarElArchivoDelChat(crudo) {
|
|
|
99
124
|
return { ok: false, motivo: "--file is the chat file, not a folder: say its path." };
|
|
100
125
|
return { ok: true, ruta: partes.join("/") };
|
|
101
126
|
}
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
const SIN_NADA = { forma: null, archivos: [], contrato: null };
|
|
128
|
+
// Una lista de rutas anotadas: cada una tiene que ser válida Y estar ya normalizada (es como la escribe esta terminal); lo
|
|
129
|
+
// que no, se descarta de a una, sin error. Sin repetidas y con tope.
|
|
130
|
+
function rutasAnotadas(v) {
|
|
131
|
+
if (!Array.isArray(v))
|
|
132
|
+
return [];
|
|
133
|
+
const rutas = [];
|
|
134
|
+
for (const a of v) {
|
|
135
|
+
if (typeof a !== "string")
|
|
136
|
+
continue;
|
|
137
|
+
const r = validarElArchivoDelChat(a);
|
|
138
|
+
if (r.ok && r.ruta === a && !rutas.includes(a) && rutas.length < MAX_ARCHIVOS_ANOTADOS)
|
|
139
|
+
rutas.push(a);
|
|
140
|
+
}
|
|
141
|
+
return rutas;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Lo que un `instalacion.json` ya existente dice de la forma, los archivos y el contrato. v1 y cualquier cosa ilegible o inválida
|
|
145
|
+
* dan «nada»; una versión que esta terminal no conoce (4 o más) tampoco se adivina. Nunca un error.
|
|
146
|
+
*/
|
|
147
|
+
export function leerInstalacionAnotada(texto) {
|
|
104
148
|
if (texto === null)
|
|
105
|
-
return
|
|
149
|
+
return { estado: "ausente" };
|
|
106
150
|
let v;
|
|
107
151
|
try {
|
|
108
152
|
v = JSON.parse(texto);
|
|
109
153
|
}
|
|
110
154
|
catch {
|
|
111
|
-
return
|
|
155
|
+
return { estado: "ilegible" };
|
|
112
156
|
}
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
157
|
+
const o = objeto(v);
|
|
158
|
+
if (o === null)
|
|
159
|
+
return { estado: "ilegible" };
|
|
160
|
+
// Antes de la 0.9.0 la única forma que existía era el widget: una v1 o una v2 son un widget, con el archivo que la v2 anotó (si lo
|
|
161
|
+
// anotó; la v1 no tiene archivo aunque el campo aparezca).
|
|
162
|
+
if (o.version === 1)
|
|
163
|
+
return { estado: "leida", forma: "widget", archivos: [], contrato: null };
|
|
164
|
+
if (o.version === 2) {
|
|
165
|
+
const a = typeof o.archivo === "string" ? validarElArchivoDelChat(o.archivo) : null;
|
|
166
|
+
return { estado: "leida", forma: "widget", archivos: a !== null && a.ok && a.ruta === o.archivo ? [a.ruta] : [], contrato: null };
|
|
167
|
+
}
|
|
168
|
+
if (o.version === 3) {
|
|
169
|
+
const forma = FORMAS.find((f) => f === o.forma) ?? null;
|
|
170
|
+
const contrato = typeof o.contrato === "string" && esSemver(o.contrato) ? o.contrato : null;
|
|
171
|
+
return { estado: "leida", forma, archivos: rutasAnotadas(o.archivos), contrato };
|
|
172
|
+
}
|
|
173
|
+
if (typeof o.version === "number" && Number.isInteger(o.version) && o.version > 3)
|
|
174
|
+
return { estado: "mas_nueva" };
|
|
175
|
+
return { estado: "leida", ...SIN_NADA };
|
|
176
|
+
}
|
|
177
|
+
export function anotadoEn(texto) {
|
|
178
|
+
const l = leerInstalacionAnotada(texto);
|
|
179
|
+
return l.estado === "leida" ? { forma: l.forma, archivos: l.archivos, contrato: l.contrato } : SIN_NADA;
|
|
180
|
+
}
|
|
181
|
+
/** Valida y normaliza la lista de `--file`: cada ruta con `validarElArchivoDelChat`, sin repetidas, con tope. */
|
|
182
|
+
export function validarLosArchivos(crudos) {
|
|
183
|
+
const rutas = [];
|
|
184
|
+
for (const crudo of crudos) {
|
|
185
|
+
const r = validarElArchivoDelChat(crudo);
|
|
186
|
+
if (!r.ok)
|
|
187
|
+
return r;
|
|
188
|
+
if (!rutas.includes(r.ruta))
|
|
189
|
+
rutas.push(r.ruta);
|
|
190
|
+
}
|
|
191
|
+
if (rutas.length > MAX_ARCHIVOS_ANOTADOS)
|
|
192
|
+
return { ok: false, motivo: `--file goes up to ${MAX_ARCHIVOS_ANOTADOS} times: the files of the chat, not the whole app.` };
|
|
193
|
+
return { ok: true, rutas };
|
|
121
194
|
}
|
|
122
195
|
// El orden de las claves es el de `armarInstalacion`: el mismo estado da los mismos bytes, y eso es `sin_cambios`.
|
|
123
196
|
export function serializar(x) {
|
package/package.json
CHANGED