supcoflow 0.8.0 → 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 CHANGED
@@ -29,21 +29,56 @@ the English ones.
29
29
  | `supcoflow whoami` | Which key the terminal runs with, on whose behalf and in which team. |
30
30
  | `supcoflow repo connect` | Connects this folder's repo with the GitHub App: it gives you a link and you confirm it in the console. Without a terminal, `--start` and then `--wait`, like `login`. |
31
31
  | `supcoflow project create` | Creates (or reuses) the project of the connected repo and records it in `.supcoflow/proyecto.json`. If the engine can't work out the commands, `--install "…" --test "…"`. |
32
- | `supcoflow project show` | The repo's project: branch, commands and the state of the GitHub App. |
32
+ | `supcoflow project show` | The repo's project: branch, commands and the state of the GitHub App, starting with who you are: «You're @login in team T; repo O/R; project P (existing)». |
33
33
  | `supcoflow guide check` | Checks `.supcoflow/guide.md` against the engine without publishing anything. |
34
34
  | `supcoflow guide publish` | Uploads `.supcoflow/guide.md` directly (with the commit and branch your clone declares, not verified in GitHub). `--from-github` verifies against the main branch instead of uploading the file. |
35
35
  | `supcoflow guide show` | Shows the active guide, where it comes from, which file it was read from and whether the branch is still up to date. |
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.0 --file app/_supcoflow/support-chat.tsx` | Records in `.supcoflow/instalacion.json` which versions the chat was installed with and which file it lives in. |
40
- | `supcoflow diagnose` | Free: how far the installation is (N of 6) and whether a test would go in. |
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
 
44
44
  `supcoflow --help` (or `supcoflow <command> --help`) shows the help with examples. The repo and project commands
45
45
  take the repo from the folder's `origin`; `--repo owner/repo` picks it by hand.
46
46
 
47
+ ### When the `origin` isn't a GitHub address
48
+
49
+ The terminal only recognizes `origin` when its host is exactly `github.com` (`https://github.com/o/r`,
50
+ `git@github.com:o/r.git`, `ssh://git@github.com/o/r`). If yours is an SSH alias (`git@github-work:o/r.git`) or
51
+ another server, it never runs `ssh` or reads your SSH configuration (it could run programs of yours). You tell it
52
+ once, in your own clone, which GitHub repo this is:
53
+
54
+ ```bash
55
+ git remote add supcoflow https://github.com/owner/repo.git # it's never fetched
56
+ ```
57
+
58
+ A remote called `supcoflow` with those same strict forms ties the clone to that repo for every command. If `origin`
59
+ is a GitHub address, `origin` wins. The error says the exact command to run. With `--repo owner/repo`, the commands
60
+ that read or write a file of the clone (`guide check`, `guide publish`, `install record`) only use this clone if it is
61
+ that repo; otherwise they exit 3 saying what the clone is, without asking the engine. (`project create --repo` keeps
62
+ creating or reusing the project; when this clone is not that repo it does so without reading or writing this clone's
63
+ `.supcoflow/proyecto.json`, and when it is that repo it reads and records the file as usual.)
64
+
65
+ ### Is this repo the one for this account?
66
+
67
+ Every command that acts on the repo's project first checks what it can see, and exits 4 `contexto_incoherente` (with a
68
+ `motivo`) without touching anything when it doesn't add up:
69
+
70
+ - `proyecto_del_clon_distinto`: `.supcoflow/proyecto.json` notes a project your key doesn't see for this repo. Check
71
+ which account you signed in with (`supcoflow whoami`), or review that old file.
72
+ - `archivo_del_clon_ilegible`: that file exists but can't be read or understood (its content is never printed).
73
+ - `dueno_distinto_del_login` (only when this repo has no project yet, in `project show` and `project create`; `show` stops the walk-through before `repo connect`): the repo's owner on GitHub isn't the GitHub login of your
74
+ key. It is a *possible* mismatch (the engine compares GitHub's number, and a login can be renamed): sign in to the
75
+ console with GitHub again, or as the owner. If your key comes from `SUPCOFLOW_TOKEN`, that variable wins over a new
76
+ login, and the terminal doesn't touch it.
77
+ - `sin_github` (only when this repo has no project yet, in `project show` and `project create`): the account was signed in by email; connecting a repo needs GitHub.
78
+
79
+ A project that already exists is never blocked for belonging to another person of the team. This doesn't see a project
80
+ of the same repo in another team when the clone doesn't note it.
81
+
47
82
  ### From zero to a project
48
83
 
49
84
  1. `supcoflow login`: you approve the code in the console.
@@ -112,9 +147,9 @@ badly formed, that is an error: it never falls back to the old one.
112
147
  `supcoflow guide show` shows the active guide, the file it was read from and whether the branch is still up to date
113
148
  (`al_dia` in the JSON); `supcoflow guide versions` lists the guide that governs now and the repo's publications;
114
149
  `supcoflow guide use --source repo|agent` chooses which one is active when there is one from the repo and one from
115
- our agent. All of them take the repo from the `origin`, or `--repo owner/repo` (with `check` and `publish`, without a
116
- local clone of that repo it exits 3 `falta_guia`: the guide is read from the file); `check`, `publish` and `use`
117
- accept `--dry-run`; `--from-github` is only for `publish`.
150
+ our agent. All of them take the repo from the `origin` (or a `supcoflow` remote, see above), or `--repo owner/repo`
151
+ (with `check` and `publish`, if this clone isn't that repo it exits 3 `falta_guia` saying what the clone is: the guide
152
+ is read from the file); `check`, `publish` and `use` accept `--dry-run`; `--from-github` is only for `publish`.
118
153
 
119
154
  The format of `.supcoflow/guide.md`:
120
155
 
@@ -156,13 +191,61 @@ wrongly copied id leaves the chat invisible without any error). It writes no fil
156
191
  The `<script>` line it also shows is available but not tested in a real app, and is not for Next.js.
157
192
  `supcoflow widget --json` returns the engine's object as is.
158
193
 
159
- Then, `supcoflow install record --skill <version> [--app <folder>] [--file <path>]` leaves in
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
160
217
  `.supcoflow/instalacion.json` (to commit) which versions it ended up with: the skill, the terminal, the chat declared
161
- and installed, the project configuration and, with `--file`, the file the chat lives in (a path relative to the repo
162
- root, with no `..`, and not a symlink). The file is version 2; a version 1 file (from 0.7.0) is read without error as
163
- «no file recorded». Running it again without `--file` keeps the file already recorded. It never contains a secret or
164
- a date; repeating it without changes doesn't touch it, and if the file has uncommitted changes it exits 4 and doesn't
165
- overwrite it.
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.
166
249
 
167
250
  ## Test the installation
168
251
 
@@ -171,8 +254,8 @@ the repo from the `origin`, or `--repo owner/repo`.
171
254
 
172
255
  | Command | What it does |
173
256
  |---|---|
174
- | `supcoflow diagnose` | Free and writes nothing. It prints «Checked sign-in, repo, GitHub App, allowed sites, chat and guide: N of 6, 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: 6, puntos }` and `prueba: { listo, puntos }`. |
175
- | `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. |
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». |
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 }`). |
176
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. |
177
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. |
178
261
  | `supcoflow test --dry-run --authorize-spend --ceiling 2 --expect-policy investigate` | What it would do (project, ceiling, balance, policy), without requesting anything. |
@@ -190,6 +273,17 @@ requesting another. While a request is unconfirmed, another `--message`, `--ceil
190
273
  open test per project: another one exits 4 `prueba_en_curso` with the id of the open one. A test's issue stays open and
191
274
  is marked as a test.
192
275
 
276
+ **If your home folder is a git repo.** The request is saved in `~/.config/supcoflow/pruebas.json` (it carries the
277
+ ceiling, the policy and, if you passed one, your `--message`). That is allowed inside a repo only when git itself says
278
+ it ignores the file and doesn't track it: the first time, the terminal creates a `.gitignore` containing `*` in that
279
+ folder (only if there isn't one; it never edits yours), and then asks git (`git check-ignore`) about the file and its
280
+ `.tmp`, for every repo that contains the written or the real path. A file that is already tracked or added to the index
281
+ is refused with the exact `git rm --cached …` to run (the terminal never runs it or deletes anything of yours), and so
282
+ is a missing or failing git. The same goes for every write or delete of `idempotencia.json`. `diagnose` and `test`
283
+ check all this (and permissions, broken links and unreadable files) **before** you authorize, without writing
284
+ anything: «no obstacle found» is a check, not a promise that the disk won't fill up later. The credentials don't use
285
+ this exception: inside a repo they are still refused.
286
+
193
287
  ## With an assistant
194
288
 
195
289
  If you use a code assistant such as Claude Code, the Supcoflow skill does the whole path for you: session, project,
@@ -231,11 +325,13 @@ Made to be read by a program, not only by a person:
231
325
  - If there is no keychain, in `~/.config/supcoflow/credenciales.json` (or `$XDG_CONFIG_HOME/supcoflow/…`) with `0600`
232
326
  permissions, and the terminal warns you.
233
327
  - **Never inside a git repo**: if that file would land inside one (a `HOME` that is a dotfiles repo, for example), it
234
- saves nothing and asks you to use the keychain or `SUPCOFLOW_TOKEN`.
328
+ saves nothing and asks you to use the keychain or `SUPCOFLOW_TOKEN`. (The test request and the idempotency key are
329
+ the only things with an exception: see «Test the installation».)
235
330
  - **`SUPCOFLOW_TOKEN`** takes priority over what is saved: it is for CI and the cloud.
236
331
  - The key never appears in a log or in a `--json` output.
237
332
  - `project create` keeps its idempotency key (it isn't a secret: it only avoids creating twice if the answer is lost)
238
- in `idempotencia.json`, next to the credentials and with the same permissions.
333
+ in `idempotencia.json`, next to the credentials and with the same permissions. Inside a repo it is saved only if git
334
+ ignores it and doesn't track it; otherwise the terminal says so and goes on without saving it.
239
335
 
240
336
  When you sign in again, the previous key is revoked only after the new one is saved; if it couldn't be revoked, it is
241
337
  noted down and retried on the next `login` or `logout`.
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.8.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
- export async function leerWidget(api, clave, proyectoId, f) {
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)}`;
@@ -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
  }
@@ -0,0 +1,102 @@
1
+ // ¿Este repo va con la cuenta en la que estoy? (1A.1, fricción 6). La terminal junta lo que ya sabe y frena, ANTES de
2
+ // conectar, crear o publicar nada, ante lo que no cierra y se puede VER:
3
+ // - el `.supcoflow/proyecto.json` del clon contra el proyecto que la clave ve para ese repo;
4
+ // - ese archivo cuando no se entiende;
5
+ // - al crear un proyecto: el dueño del repo contra el login de GitHub de la clave, y la falta de GitHub.
6
+ // Qué NO cubre: un proyecto del mismo repo en OTRO equipo cuando el clon no tiene su archivo (el motor tendría que mirar
7
+ // en todos los equipos y eso filtra información entre clientes), ni prueba una identidad: el motor compara el número de
8
+ // GitHub y el login es una etiqueta que se puede cambiar. Por eso `dueno_distinto_del_login` es una posible incoherencia.
9
+ import path from "node:path";
10
+ import { SALIDA } from "../contrato.js";
11
+ // Lo único que se lee de un `.supcoflow/proyecto.json` ajeno es su id, y sólo si tiene esta forma:
12
+ // así lo que salga a la terminal no puede ser otra cosa que un id (Review Focus 11).
13
+ const FORMA_DEL_ID = /^proj_[a-z0-9-]{1,80}$/;
14
+ export const RUTA_DEL_ARCHIVO_DEL_CLON = path.join(".supcoflow", "proyecto.json");
15
+ export function idDelArchivo(crudo) {
16
+ try {
17
+ const v = JSON.parse(crudo);
18
+ if (typeof v !== "object" || v === null)
19
+ return null;
20
+ const id = v.id;
21
+ return typeof id === "string" && FORMA_DEL_ID.test(id) ? id : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ /** Los tres estados del archivo del clon. Nunca se imprime su contenido; sólo el id, si tiene forma de id. */
28
+ export function leerElArchivoDelClon(archivos, raiz) {
29
+ // Sin clon de este repo (un `--repo` ajeno a la carpeta) no hay archivo que le pertenezca.
30
+ if (archivos === undefined || raiz === null)
31
+ return { estado: "ausente" };
32
+ let crudo;
33
+ try {
34
+ crudo = archivos.leer(path.join(raiz, RUTA_DEL_ARCHIVO_DEL_CLON));
35
+ }
36
+ catch {
37
+ return { estado: "ilegible" };
38
+ }
39
+ if (crudo === null)
40
+ return { estado: "ausente" };
41
+ const id = idDelArchivo(crudo);
42
+ return id === null ? { estado: "ilegible" } : { estado: "valido", id };
43
+ }
44
+ export function contextoIncoherente(motivo, message, extra = {}) {
45
+ return { codigo: SALIDA.haceFaltaUnaPersona, json: { ok: false, error: "contexto_incoherente", motivo, message, ...extra }, texto: message };
46
+ }
47
+ /**
48
+ * El clon contra lo que la clave ve (lo que comparten TODOS los comandos que actúan sobre el proyecto del repo).
49
+ * `null` = coherente. `proyecto` es el que la clave ve para este repo, o null si no ve ninguno.
50
+ */
51
+ export function coherenciaDelClon(archivo, proyecto, repo) {
52
+ if (archivo.estado === "ilegible") {
53
+ return contextoIncoherente("archivo_del_clon_ilegible", "I can't read `.supcoflow/proyecto.json` in this clone; I won't act on this repo until it's readable. Look at it before going on.", { repo, en_el_clon: { estado: "ilegible", id: null } });
54
+ }
55
+ if (archivo.estado === "valido" && archivo.id !== proyecto?.id) {
56
+ return contextoIncoherente("proyecto_del_clon_distinto", `The project noted in this clone (${archivo.id}) isn't the one your account sees for this repo (${proyecto?.id ?? "none"}). `
57
+ + "Check which account you signed in with (`supcoflow whoami`), or, if that file is old, review it.", { repo, en_el_clon: { estado: "valido", id: archivo.id }, proyecto: proyecto?.id ?? null });
58
+ }
59
+ return null;
60
+ }
61
+ export function armarElContexto(d) {
62
+ const { yo } = d;
63
+ const soloLectura = yo.via === "clave_de_consulta";
64
+ const login = yo.usuario?.login ?? null;
65
+ const tipo = soloLectura ? "clave_de_consulta" : login !== null ? "github" : "email";
66
+ return {
67
+ equipo: yo.equipo,
68
+ identidad: tipo === "github" ? login : tipo,
69
+ identidad_tipo: tipo,
70
+ fuente: d.fuente,
71
+ repo: d.repo,
72
+ proyecto: d.proyecto?.id ?? null,
73
+ en_el_clon: { estado: d.archivo.estado, id: d.archivo.estado === "valido" ? d.archivo.id : null },
74
+ };
75
+ }
76
+ /** La línea que la persona lee: quién es, en qué equipo, qué repo y qué proyecto. */
77
+ export function lineaDelContexto(c, readOnlyDe = null) {
78
+ const quien = c.identidad_tipo === "github" ? `@${c.identidad}` : c.identidad_tipo === "email" ? "signed in with email (no GitHub)" : `using a read-only key${readOnlyDe ? ` for ${readOnlyDe}` : ""}`;
79
+ const cuenta = `You're ${quien} in team ${c.equipo}`;
80
+ const proyecto = c.proyecto !== null ? `project ${c.proyecto} (existing)` : `no project yet (it will be created in team ${c.equipo})`;
81
+ return `${cuenta}; repo ${c.repo}; ${proyecto}.`;
82
+ }
83
+ /**
84
+ * Sólo cuando la clave NO ve proyecto para el repo y se va a crear uno: lo que se ve de la cuenta contra el repo.
85
+ * Una clave de consulta no crea nada, así que no se compara su login con nadie.
86
+ */
87
+ export function coherenciaAlCrear(d) {
88
+ if (d.yo.via === "clave_de_consulta")
89
+ return null;
90
+ const login = d.yo.usuario?.login ?? null;
91
+ if (login === null) {
92
+ return contextoIncoherente("sin_github", "Connecting a repo needs a Supcoflow account signed in with GitHub.", { repo: d.repo });
93
+ }
94
+ const dueno = d.repo.split("/")[0];
95
+ if (dueno.toLowerCase() === login.toLowerCase())
96
+ return null;
97
+ const deLaVariable = d.fuente === "env"
98
+ ? " Your key comes from SUPCOFLOW_TOKEN, which wins over a new login: unset it or replace it first; I don't touch it."
99
+ : "";
100
+ return contextoIncoherente("dueno_distinto_del_login", `This repo's owner on GitHub (@${dueno}) isn't the GitHub account your key shows (@${login}). `
101
+ + `If you renamed your GitHub account, sign in to the Supcoflow console with GitHub again (that updates it) and run this again; otherwise sign in as @${dueno}.${deLaVariable}`, { repo: d.repo, dueno, login, ...(d.fuente === "env" ? { fuente: "env" } : {}) });
102
+ }
@@ -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.8.0";
6
+ const VERSION = "0.9.0";
7
7
  export const DESCRIPCION = {
8
8
  version: VERSION,
9
9
  comandos: [
@@ -55,11 +55,11 @@ export const DESCRIPCION = {
55
55
  },
56
56
  {
57
57
  nombre: "project create",
58
- resumen: "Creates (or reuses) the already connected repo's project and records it in .supcoflow/proyecto.json; a different existing file is never overwritten (exits 4).",
58
+ resumen: "Creates (or reuses) the already connected repo's project and records it in .supcoflow/proyecto.json; a different existing file is never overwritten (exits 4). Before connecting or creating anything it checks what it can see and exits 4 contexto_incoherente (motivo proyecto_del_clon_distinto: the clone notes a project your account doesn't see for this repo; archivo_del_clon_ilegible: .supcoflow/proyecto.json can't be read; dueno_distinto_del_login: a possible mismatch, the repo's owner on GitHub isn't the GitHub login of your key; sin_github: the account isn't signed in with GitHub) without touching anything.",
59
59
  flags: [
60
60
  "--install \"…\": the install command, when the engine can't work it out",
61
61
  "--test \"…\": the test command, same",
62
- "--repo owner/repo: the repo, if it isn't this folder's «origin» (without writing the file)",
62
+ "--repo owner/repo: the repo, if it isn't this folder's «origin» (without reading or writing this clone's file)",
63
63
  "--dry-run: says what it would create without creating anything",
64
64
  "--json",
65
65
  ],
@@ -68,10 +68,10 @@ export const DESCRIPCION = {
68
68
  },
69
69
  {
70
70
  nombre: "project show",
71
- resumen: "Shows the repo's project: branch, commands and the GitHub App's state; null if it has none yet.",
71
+ resumen: "Shows the repo's project: branch, commands and the GitHub App's state; null if it has none yet. It starts with who you are: `contexto` { equipo, identidad (the GitHub login, or «email», or «clave_de_consulta»), identidad_tipo, fuente, repo, proyecto (id or null), en_el_clon { estado: ausente|valido|ilegible, id } }, said as «You're @login in team T; repo O/R; project P (existing)». Exits 4 contexto_incoherente if this clone's .supcoflow/proyecto.json doesn't match what your key sees (proyecto_del_clon_distinto / archivo_del_clon_ilegible) or, when the repo has no project yet, if the account can't create one for it (dueno_distinto_del_login, a possible mismatch / sin_github): it stops BEFORE `repo connect`. The repo comes from the «origin» of this clone; if origin is an SSH alias or not GitHub, from a remote called `supcoflow` that points at https://github.com/owner/repo.git (the first time, `git remote add supcoflow https://github.com/owner/repo.git`: it is never fetched and ssh is never run).",
72
72
  flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin»", "--json"],
73
73
  ejemplos: ["supcoflow project show --json"],
74
- codigos: [0, 1, 2, 3],
74
+ codigos: [0, 1, 2, 3, 4],
75
75
  },
76
76
  {
77
77
  nombre: "catalog",
@@ -82,7 +82,7 @@ export const DESCRIPCION = {
82
82
  },
83
83
  {
84
84
  nombre: "config",
85
- resumen: "Shows the repo's project configuration: chat, sites, brand, problem policy and data agreement. The agreement limits what is saved and what the investigating AI reads; it doesn't cover what the chat's AI sees while it talks.",
85
+ resumen: "Shows the repo's project configuration: chat, sites, brand, problem policy and data agreement. The agreement limits what is saved and what the investigating AI reads; it doesn't cover what the chat's AI sees while it talks. Like every command that acts on the repo's project, it exits 4 contexto_incoherente without doing anything if this clone's .supcoflow/proyecto.json notes another project than the one your key sees (proyecto_del_clon_distinto) or can't be read (archivo_del_clon_ilegible).",
86
86
  flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin»", "--json"],
87
87
  ejemplos: ["supcoflow config --json"],
88
88
  codigos: [0, 1, 2, 3, 4],
@@ -185,14 +185,14 @@ export const DESCRIPCION = {
185
185
  {
186
186
  nombre: "guide check",
187
187
  resumen: "Checks .supcoflow/guide.md (this clone's root) against the engine, without publishing anything: says whether it's well formed and how many sections it has. It's the step before the commit. If guide.md doesn't exist it reads the old name .supcoflow/guia.md and says so.",
188
- flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin» (without a local clone it exits 3 falta_guia: the guide is read from the file)", "--dry-run: says what it would send without sending it", "--json"],
188
+ flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin» (the guide is read from 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 falta_guia saying what this clone is and the exact `git remote add supcoflow …`, without asking the engine)", "--dry-run: says what it would send without sending it", "--json"],
189
189
  ejemplos: ["supcoflow guide check --json"],
190
190
  codigos: [0, 1, 2, 3, 4],
191
191
  },
192
192
  {
193
193
  nombre: "guide publish",
194
194
  resumen: "Uploads .supcoflow/guide.md directly (or the old name guia.md, only if guide.md doesn't exist): sends the content, the file it read and the commit and branch this clone declares (if there's git and commits; otherwise, no commit). The engine checks it and makes it the active guide; the commit is kept as «declared by the terminal, not verified in GitHub». With --from-github it keeps the earlier route: it sends the fingerprint and the engine compares it with the main branch's; if it isn't pushed to that branch yet (or it's another one), it exits 5.",
195
- flags: ["--from-github: doesn't upload the file; verifies against GitHub's main branch (exits 5 guia_no_subida / guia_distinta if it doesn't match)", "--repo owner/repo: the repo, if it isn't this folder's «origin» (without a local clone it exits 3 falta_guia: the guide is read from the file)", "--dry-run: says what it would send (or the fingerprint with --from-github), without requesting anything", "--json"],
195
+ flags: ["--from-github: doesn't upload the file; verifies against GitHub's main branch (exits 5 guia_no_subida / guia_distinta if it doesn't match)", "--repo owner/repo: the repo, if it isn't this folder's «origin» (the guide is read from 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 falta_guia saying what this clone is and the exact `git remote add supcoflow …`, without asking the engine)", "--dry-run: says what it would send (or the fingerprint with --from-github), without requesting anything", "--json"],
196
196
  ejemplos: ["supcoflow guide publish --json"],
197
197
  codigos: [0, 1, 2, 3, 4, 5],
198
198
  },
@@ -219,35 +219,36 @@ 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 something halfway.",
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 2) 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, with --file, the file the chat lives in; never a secret or a date. Repeating without changes doesn't touch it (sin_cambios: true), and running again without --file keeps the file already recorded. 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 / app_fuera_del_repo / app_inexistente / sin_clon; 4 sin_proyecto / acuerdo_ilegible; 1 paquete_ilegible / configuracion_inesperada / archivo_no_escrito.",
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 file, relative to the repo root (no `..`, not absolute, not a symlink or through one; otherwise exits 3 archivo_invalido and writes nothing)",
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
- "--repo owner/repo: the repo, if it isn't this folder's «origin» (without a local clone it exits 3 sin_clon: the file goes in the clone)",
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",
236
237
  "--json",
237
238
  ],
238
- ejemplos: ["supcoflow install record --skill 0.3.0 --app web --file web/app/_supcoflow/support-chat.tsx --json"],
239
+ ejemplos: ["supcoflow install record --skill 0.3.1 --app web --file web/app/_supcoflow/support-chat.tsx --json"],
239
240
  codigos: [0, 1, 2, 3, 4],
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 guide: N of 6) 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 and whatever else the engine adds. Exits 0 if everything is ready and 4 if something needs fixing (no_listo), with each point.",
244
- flags: ["--repo owner/repo: the repo, if it isn't this folder's «origin»", "--json: { instalacion: { ok, total: 6, puntos }, prueba: { listo, puntos }, … }"],
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
  },
248
249
  {
249
250
  nombre: "test",
250
- resumen: "Tests the installation end to end: a problem comes in through the same door as the chat and ends up as an issue. It spends one investigation (up to the ceiling you authorize; with the save policy it goes as far as the saved case and doesn't spend). Without --authorize-spend it sends nothing: it exits 4 falta_autorizacion with techo_usd, saldo_usd, tope_usd and comprometido_usd. With --authorize-spend, --ceiling and --expect-policy it requests the test with exactly those values (it doesn't re-read anything to choose them) and exits 5 with prueba_id; then --wait follows it. If the ceiling or the policy changed in the meantime it exits 4 techo_distinto / politica_distinta with the new values and doesn't retry: ask again. Others: 4 prueba_en_curso (with its id), chat_deshabilitado, requiere_api_key, intento_pendiente; 6 tope_mensual; 1 with intento_pendiente: true if the answer didn't arrive (repeating resumes the same test).",
251
+ resumen: "Tests the installation end to end: a problem comes in through the same door as the chat and ends up as an issue. It spends one investigation (up to the ceiling you authorize; with the save policy it goes as far as the saved case and doesn't spend). Without --authorize-spend it sends nothing: it exits 4 falta_autorizacion with techo_usd, saldo_usd, tope_usd and comprometido_usd, and, checked before you say yes and without writing anything, `maquina` { ok, impedimentos: [{ codigo, detalle }] } (whether this machine can save the test request: if it isn't ok the message says why first) and `intento_pendiente` { techo_usd, politica } | null (a request waiting for confirmation: only its ceiling and policy, never its message or key). With --authorize-spend, --ceiling and --expect-policy it requests the test with exactly those values (it doesn't re-read anything to choose them) and exits 5 with prueba_id; then --wait follows it. If the ceiling or the policy changed in the meantime it exits 4 techo_distinto / politica_distinta with the new values and doesn't retry: ask again. Others: 4 prueba_en_curso (with its id), chat_deshabilitado, requiere_api_key, intento_pendiente; 6 tope_mensual; 1 with intento_pendiente: true if the answer didn't arrive (repeating resumes the same test).",
251
252
  flags: [
252
253
  "--authorize-spend: authorizes the spend of ONE test; without it nothing that writes is sent",
253
254
  "--ceiling usd: the ceiling you authorize, the number you were shown (required with --authorize-spend)",
@@ -33,14 +33,15 @@ const AVISOS_DEL_NOMBRE = {
33
33
  // error: nunca motivo para leer el viejo). Si están los dos manda `guide.md` y se avisa que el otro se ignora.
34
34
  async function leerLaGuia(d, flags) {
35
35
  let raiz;
36
+ let sinClon;
36
37
  try {
37
- raiz = (await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd })).raiz;
38
+ ({ raiz, sinClon } = await repoPedido({ flagRepo: flags.repo, git: d.git, cwd: d.cwd }));
38
39
  }
39
40
  catch (e) {
40
41
  return { ok: false, resultado: resultadoDeFallo(e) };
41
42
  }
42
43
  if (raiz === null) {
43
- return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `I don't have that repo's clone here, and the guide is read from ${RUTA_DE_LA_GUIA}: stand in the repo's clone.`) };
44
+ return { ok: false, resultado: fallo(SALIDA.entradaInvalida, "falta_guia", `${sinClon ?? "I don't have that repo's clone here."} The guide is read from ${RUTA_DE_LA_GUIA} in the repo's clone: stand in it.`) };
44
45
  }
45
46
  let ruta = RUTA_DE_LA_GUIA;
46
47
  let aviso = null;