supcoflow 0.1.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -3
- package/dist/api.js +187 -6
- package/dist/argumentos.js +63 -3
- package/dist/candado.js +57 -0
- package/dist/cli.js +66 -5
- package/dist/comandos/clave-vigente.js +18 -0
- package/dist/comandos/claves.js +161 -0
- package/dist/comandos/configurar.js +187 -0
- package/dist/comandos/datos-y-politica.js +112 -0
- package/dist/comandos/describir.js +173 -3
- package/dist/comandos/guia.js +244 -0
- package/dist/comandos/proyecto-del-repo.js +26 -0
- package/dist/comandos/proyecto.js +213 -0
- package/dist/comandos/quien-soy.js +6 -13
- package/dist/comandos/repo-conectar.js +126 -0
- package/dist/entorno.js +323 -5
- package/dist/flags-por-comando.js +33 -0
- package/dist/guia.js +11 -0
- package/dist/idempotencia.js +88 -0
- package/dist/repo-local.js +68 -0
- package/dist/secreto-a-archivo.js +109 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
La terminal de [Supcoflow](https://app.supcoflow.dev): iniciar sesión y manejar el chat de soporte
|
|
4
4
|
desde la consola, o desde un asistente de código que corre comandos por vos.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Trae iniciar y cerrar sesión, ver con qué cuenta estás, conectar el repo con la App de GitHub,
|
|
7
|
+
crear su proyecto y configurar el chat.
|
|
7
8
|
|
|
8
9
|
## Instalar
|
|
9
10
|
|
|
@@ -22,9 +23,24 @@ npx supcoflow login
|
|
|
22
23
|
| `supcoflow login --wait` | El segundo paso: espera la aprobación hasta 90 s. Si todavía no llegó, sale 5: volvé a correrlo. |
|
|
23
24
|
| `supcoflow logout` | Revoca la clave en uso y la borra de esta máquina. |
|
|
24
25
|
| `supcoflow quien-soy` | Con qué clave anda la terminal, a nombre de quién y en qué equipo. |
|
|
26
|
+
| `supcoflow repo conectar` | Conecta el repo de esta carpeta con la App de GitHub: te da un enlace, lo confirmás en la consola. Sin terminal, `--start` y después `--wait`, como `login`. |
|
|
27
|
+
| `supcoflow proyecto crear` | Crea (o reusa) el proyecto del repo ya conectado y lo anota en `.supcoflow/proyecto.json`. Si el motor no deduce los comandos, `--install "…" --test "…"`. |
|
|
28
|
+
| `supcoflow proyecto ver` | El proyecto del repo: rama, comandos y el estado de la App de GitHub. |
|
|
29
|
+
| `supcoflow guia revisar` | Revisa `.supcoflow/guia.md` contra el motor sin publicar nada. |
|
|
30
|
+
| `supcoflow guia publicar` | Sube `.supcoflow/guia.md` directo (con el commit y la rama que declara tu clon, no verificados en GitHub). `--desde-github` verifica contra la rama principal en vez de subir el archivo. |
|
|
31
|
+
| `supcoflow guia ver` | Muestra la guía activa, de dónde viene y si la rama sigue al día. |
|
|
32
|
+
| `supcoflow guia usar --origen repo\|agente` | Elige cuál guía queda activa. |
|
|
25
33
|
| `supcoflow describir --json` | Todo el contrato (comandos, flags, facultades de la clave, códigos) en una línea de JSON. |
|
|
26
34
|
|
|
27
|
-
`supcoflow --help` (o `supcoflow <comando> --help`) muestra la ayuda con ejemplos.
|
|
35
|
+
`supcoflow --help` (o `supcoflow <comando> --help`) muestra la ayuda con ejemplos. Los comandos de
|
|
36
|
+
repo y proyecto toman el repo del `origin` de la carpeta; `--repo owner/repo` lo elige a mano.
|
|
37
|
+
|
|
38
|
+
### De cero a un proyecto
|
|
39
|
+
|
|
40
|
+
1. `supcoflow login`: aprobás el código en la consola.
|
|
41
|
+
2. `supcoflow repo conectar`: instalás la App en GitHub si hace falta y tocás Confirmar.
|
|
42
|
+
3. `supcoflow proyecto crear`: queda el proyecto y `.supcoflow/proyecto.json` para commitear.
|
|
43
|
+
4. `supcoflow proyecto ver`: para revisar cómo quedó.
|
|
28
44
|
|
|
29
45
|
### Ejemplos
|
|
30
46
|
|
|
@@ -36,10 +52,71 @@ supcoflow login
|
|
|
36
52
|
supcoflow login --start --json # sale 5 con verification_uri_complete y user_code
|
|
37
53
|
supcoflow login --wait --json # sale 5 mientras la persona no apruebe; 0 cuando aprueba
|
|
38
54
|
|
|
55
|
+
supcoflow repo conectar --start --json # sale 5 con la url para confirmar
|
|
56
|
+
supcoflow repo conectar --wait --json # sale 5 mientras nadie confirme; 0 conectado
|
|
57
|
+
supcoflow proyecto crear --json # 4 si hay un .supcoflow/proyecto.json distinto (no lo pisa)
|
|
58
|
+
supcoflow proyecto crear --install "pnpm i" --test "pnpm test"
|
|
59
|
+
supcoflow proyecto ver --json
|
|
60
|
+
|
|
39
61
|
supcoflow quien-soy --json
|
|
40
62
|
supcoflow logout --dry-run # qué revocaría, sin revocar nada
|
|
41
63
|
```
|
|
42
64
|
|
|
65
|
+
## Configurar
|
|
66
|
+
|
|
67
|
+
Con el repo ya conectado y su proyecto creado, se configura el chat sin entrar a la consola (la clave
|
|
68
|
+
tiene que ser de sesión, no de consulta). Todos toman el repo del `origin`, o `--repo owner/repo`, y
|
|
69
|
+
los que escriben aceptan `--dry-run`.
|
|
70
|
+
|
|
71
|
+
| Comando | Qué hace |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `supcoflow catalogo` | Qué se puede configurar y con qué valores. |
|
|
74
|
+
| `supcoflow configuracion` | Chat, sitios, marca y acuerdo de datos del proyecto. |
|
|
75
|
+
| `supcoflow chat prender` / `chat apagar` | Prende o apaga el chat (hace falta una guía: publicá la del repo con `supcoflow guia publicar` o generá y aprobá la de nuestro agente en Brain). |
|
|
76
|
+
| `supcoflow chat marca --titulo "Ayuda" --tema acento=#0a7` | Título, saludo y colores; lo no nombrado queda igual. |
|
|
77
|
+
| `supcoflow chat logo --archivo logo.png` | Sube el logo (.png, .jpg o .webp, hasta 200 KB). |
|
|
78
|
+
| `supcoflow sitios agregar\|quitar --sitio https://app.ejemplo.com` | Sitios desde los que carga el chat. |
|
|
79
|
+
| `supcoflow datos fijar --permitir usuario_id,atributo:plan` | Acuerdo de datos: frena lo que se guarda y lo que lee la IA que releva; no cubre lo que ve la IA del chat mientras conversa. |
|
|
80
|
+
| `supcoflow politica fijar --modo guardar` | Qué hace el motor con un problema que llega por el chat: `relevar` (gasta) o `guardar`. Guardar frena sólo lo automático del chat; una persona puede pedir el relevamiento desde la consola, y cuesta. |
|
|
81
|
+
| `supcoflow claves crear --nombre ci --guardar-en ~/ci.key` | Clave de consulta para CI: el secreto queda sólo en ese archivo (0600, fuera de un repo), nunca en la salida. |
|
|
82
|
+
| `supcoflow claves listar` / `claves revocar --id cla_…` | Ver y revocar claves de consulta. |
|
|
83
|
+
|
|
84
|
+
## La guía
|
|
85
|
+
|
|
86
|
+
La guía le cuenta al chat qué hace cada pantalla de tu producto. Vive en el repo, en
|
|
87
|
+
`.supcoflow/guia.md` (en la raíz). El flujo, en este orden:
|
|
88
|
+
|
|
89
|
+
1. `supcoflow guia revisar`: la revisa contra el motor sin publicar nada (si está mal armada, sale 3 con la línea de cada error).
|
|
90
|
+
2. `supcoflow guia publicar`: sube el archivo directo y queda como guía activa. Manda también el commit y la rama de tu
|
|
91
|
+
clon (`git rev-parse HEAD`, si hay git y commits; si no, va sin commit): son declarados por la terminal, no
|
|
92
|
+
verificados en GitHub, y así se muestran. Repetir con el mismo archivo no duplica.
|
|
93
|
+
3. Opcional, `supcoflow guia publicar --desde-github`: no sube el archivo; manda su huella y el motor la compara con la
|
|
94
|
+
de la rama principal de GitHub (commit verificado). Si todavía no está subida (o la rama tiene otra), sale 5:
|
|
95
|
+
subí la guía a la rama principal y volvé a correr.
|
|
96
|
+
|
|
97
|
+
`supcoflow guia ver` muestra la guía activa y si la rama sigue al día (`al_dia` en el JSON);
|
|
98
|
+
`supcoflow guia usar --origen repo|agente` elige cuál queda activa cuando hay una del repo y otra de nuestro agente.
|
|
99
|
+
Todos toman el repo del `origin`, o `--repo owner/repo` (con `revisar` y `publicar`, sin clon local de ese repo sale 3 `falta_guia`: la guía se lee del archivo); `revisar`, `publicar` y `usar` aceptan `--dry-run`; `--desde-github` es sólo de `publicar`.
|
|
100
|
+
|
|
101
|
+
El formato de `.supcoflow/guia.md`:
|
|
102
|
+
|
|
103
|
+
```markdown
|
|
104
|
+
# Guía de Mi App ← opcional; se ignora
|
|
105
|
+
|
|
106
|
+
## Facturas ← una sección por pantalla o función (1 a 60). Nombre: 1–120 caracteres
|
|
107
|
+
Para qué sirve: Ver y pagar las facturas del mes. ← obligatorio, 1–600
|
|
108
|
+
Qué puede hacer el usuario: Descargar el PDF, pagar con ← obligatorio, 1–600; puede seguir
|
|
109
|
+
tarjeta y ver el historial. en las líneas de abajo
|
|
110
|
+
Problemas comunes: ← opcional; hasta 10, cada uno 1–300
|
|
111
|
+
- El botón Pagar no responde con tarjetas de débito.
|
|
112
|
+
- El PDF sale en blanco en Safari.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- Las etiquetas se reconocen al principio de la línea, sin distinguir mayúsculas y con o sin tildes
|
|
116
|
+
(«Para que sirve:» vale). Una línea que no es etiqueta continúa el campo anterior (se une con un espacio).
|
|
117
|
+
- Texto antes del primer `## ` que no sea el `# ` del título → error. Una etiqueta repetida en una sección →
|
|
118
|
+
error. Una línea con `<` o `>` → error. Más de 64 KB → `guia_muy_grande`.
|
|
119
|
+
|
|
43
120
|
## El contrato
|
|
44
121
|
|
|
45
122
|
Pensado para que lo lea un programa, no sólo una persona:
|
|
@@ -48,7 +125,8 @@ Pensado para que lo lea un programa, no sólo una persona:
|
|
|
48
125
|
Los errores también: `{"ok":false,"error":"…","message":"…"}`, con su código de salida.
|
|
49
126
|
- **Sin preguntas sin terminal interactiva** (o con `--json`): si falta un dato, sale 3 diciendo qué
|
|
50
127
|
flag usar. `supcoflow login` sin terminal sale 3 y te indica el camino en dos pasos.
|
|
51
|
-
- **`--dry-run`** donde se puede simular (`login`, `login --start`, `logout
|
|
128
|
+
- **`--dry-run`** donde se puede simular (`login`, `login --start`, `logout`, `repo conectar --start`,
|
|
129
|
+
`proyecto crear`, `guia revisar`, `guia publicar`, `guia usar`). `login --wait
|
|
52
130
|
--dry-run` sale 3: sondear consume el código y no hay cómo simularlo.
|
|
53
131
|
|
|
54
132
|
| Código | Quiere decir |
|
|
@@ -70,6 +148,8 @@ Pensado para que lo lea un programa, no sólo una persona:
|
|
|
70
148
|
de dotfiles, por ejemplo), no guarda nada y te pide usar el llavero o `SUPCOFLOW_TOKEN`.
|
|
71
149
|
- **`SUPCOFLOW_TOKEN`** tiene prioridad sobre lo guardado: sirve para CI y para la nube.
|
|
72
150
|
- La clave nunca sale en un log ni en una salida `--json`.
|
|
151
|
+
- `proyecto crear` guarda su clave de idempotencia (no es un secreto: sólo evita crear dos veces si se
|
|
152
|
+
pierde la respuesta) en `idempotencia.json`, al lado de las credenciales y con los mismos permisos.
|
|
73
153
|
|
|
74
154
|
Cuando iniciás sesión de nuevo, la clave anterior se revoca recién cuando la nueva quedó guardada; si
|
|
75
155
|
no se pudo revocar, queda anotada y se reintenta en el próximo `login` o `logout`.
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Los
|
|
2
|
-
import { FalloDeLaTerminal, SALIDA } from "./contrato.js";
|
|
1
|
+
// Los pedidos de la terminal al motor. Sin dependencias: `fetch` viene de Node y se inyecta.
|
|
2
|
+
import { FalloDeLaTerminal, SALIDA, taparSecretos } from "./contrato.js";
|
|
3
3
|
// La URL viene FIJA (§5.2): si un archivo del repo pudiera cambiarla, clonar un repo ajeno y correr
|
|
4
4
|
// `supcoflow` mandaría el token a un servidor ajeno. Sólo el entorno del proceso la mueve, y `http`
|
|
5
5
|
// únicamente contra la propia máquina: contra un host remoto la clave viajaría en claro.
|
|
@@ -21,10 +21,14 @@ export function urlDeLaApi(env) {
|
|
|
21
21
|
return new URL(u.origin);
|
|
22
22
|
throw invalida();
|
|
23
23
|
}
|
|
24
|
-
// A mano a propósito: tiene que coincidir con package.json (0.
|
|
24
|
+
// A mano a propósito: tiene que coincidir con package.json (0.5.0); sólo identifica a la terminal
|
|
25
25
|
// en los logs del motor, no decide nada.
|
|
26
|
-
const VERSION = "0.
|
|
26
|
+
const VERSION = "0.5.0";
|
|
27
27
|
const PLAZO_POR_DEFECTO_MS = 15_000;
|
|
28
|
+
// Los dos POST que crean algo: detrás, el motor acuña con Vercel Connect y le pregunta a GitHub varias
|
|
29
|
+
// veces (instalación, repos, comandos, rama). Con 15 s una respuesta lenta pero buena se cortaba y
|
|
30
|
+
// sólo quedaba reintentar; el sondeo no los usa, así que su tope por tramo no cambia.
|
|
31
|
+
const PLAZO_DE_ALTA_MS = 30_000;
|
|
28
32
|
// Todo lo que vuelve de afuera se trata como no confiable: nunca se pega el cuerpo crudo en un
|
|
29
33
|
// mensaje (una página HTML no sirve en una línea y podría traer cualquier cosa), y el mensaje del
|
|
30
34
|
// error de red no se reenvía (podría llevar la URL o la clave).
|
|
@@ -37,8 +41,9 @@ async function pedir(api, ruta, init, f, plazoMs) {
|
|
|
37
41
|
headers: { ...init.headers, "user-agent": `supcoflow-cli/${VERSION}` },
|
|
38
42
|
signal: AbortSignal.timeout(plazoMs ?? PLAZO_POR_DEFECTO_MS),
|
|
39
43
|
// Un 307/308 reenvía el cuerpo del formulario (con el device_code) adonde diga `Location`, aunque
|
|
40
|
-
// sea otro origen o http
|
|
41
|
-
// anómala y sale como `motor_inalcanzable`, sin
|
|
44
|
+
// sea otro origen o http; con el Bearer pasaría lo mismo con la clave. Ninguna ruta del motor que
|
|
45
|
+
// llama la terminal redirige: una redirección es anómala y sale como `motor_inalcanzable`, sin
|
|
46
|
+
// Location ni cuerpo en el mensaje.
|
|
42
47
|
redirect: "error",
|
|
43
48
|
});
|
|
44
49
|
texto = await res.text();
|
|
@@ -131,3 +136,179 @@ export async function revocarPropia(api, clave, f, plazoMs) {
|
|
|
131
136
|
throw sinServicio(status);
|
|
132
137
|
throw inesperada(status);
|
|
133
138
|
}
|
|
139
|
+
// ---- Conexión del repo y proyecto (bloque 3) ----
|
|
140
|
+
// Una sola puerta para los pedidos con clave: suma el Bearer y traduce lo que significa lo mismo en
|
|
141
|
+
// todos (clave rota, clave sin alcance, repo mal escrito, cuenta sin GitHub). Lo que es propio de
|
|
142
|
+
// cada pedido (404, 409, 422 de comandos) lo decide quien llama. Los `message` del motor son texto de
|
|
143
|
+
// afuera: pasan por taparSecretos antes de salir en una línea de la terminal.
|
|
144
|
+
async function conClave(api, clave, ruta, init, f, plazoMs) {
|
|
145
|
+
const r = await pedir(api, ruta, { ...init, headers: { ...init.headers, ...bearer(clave) } }, f, plazoMs);
|
|
146
|
+
const error = texto(r.cuerpo.error) ?? "respuesta_inesperada";
|
|
147
|
+
const message = (porDefecto) => taparSecretos(texto(r.cuerpo.message) ?? porDefecto);
|
|
148
|
+
if (r.status === 401) {
|
|
149
|
+
throw new FalloDeLaTerminal(SALIDA.faltaLogin, "clave_invalida", message("La clave ya no vale: iniciá sesión de nuevo."));
|
|
150
|
+
}
|
|
151
|
+
if (r.status === 403 && (error === "requiere_persona" || error === "clave_de_consulta")) {
|
|
152
|
+
throw new FalloDeLaTerminal(SALIDA.haceFaltaUnaPersona, error, message("Esto lo tiene que hacer una persona con sesión."));
|
|
153
|
+
}
|
|
154
|
+
if (r.status === 422 && error === "cuenta_sin_github") {
|
|
155
|
+
throw new FalloDeLaTerminal(SALIDA.haceFaltaUnaPersona, error, message("Tu cuenta no tiene GitHub asociado."));
|
|
156
|
+
}
|
|
157
|
+
if (r.status === 400 && error === "repo_invalido") {
|
|
158
|
+
throw new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message("El repo tiene que ser owner/repo."));
|
|
159
|
+
}
|
|
160
|
+
if (r.status >= 500)
|
|
161
|
+
throw sinServicio(r.status);
|
|
162
|
+
return r;
|
|
163
|
+
}
|
|
164
|
+
const ESTADOS_DE_PEDIDO = new Set(["pendiente", "conectado", "repo_fuera_de_alcance", "vencido"]);
|
|
165
|
+
function aPedido(v, status) {
|
|
166
|
+
const c = (typeof v === "object" && v !== null ? v : {});
|
|
167
|
+
const [id, repo, estado, url, venceEn] = [c.id, c.repo, c.estado, c.url, c.vence_en].map(texto);
|
|
168
|
+
if (!id || !repo || !estado || !ESTADOS_DE_PEDIDO.has(estado) || !url || !venceEn)
|
|
169
|
+
throw inesperada(status);
|
|
170
|
+
return { id, repo, estado: estado, url, venceEn, repoCanonico: texto(c.repo_canonico), fallo: texto(c.fallo) };
|
|
171
|
+
}
|
|
172
|
+
function aProyecto(v, status) {
|
|
173
|
+
const c = (typeof v === "object" && v !== null ? v : {});
|
|
174
|
+
const [id, slug, repo, rama, installCommand, testCommand] = [c.id, c.slug, c.repo, c.rama, c.install_command, c.test_command].map(texto);
|
|
175
|
+
if (!id || !slug || !repo || !rama || installCommand === null || testCommand === null)
|
|
176
|
+
throw inesperada(status);
|
|
177
|
+
return { id, slug, repo, rama, installCommand, testCommand };
|
|
178
|
+
}
|
|
179
|
+
// Un fallo propio del pedido que el mapeo común no cubrió (404, 409…): sale como entrada inválida
|
|
180
|
+
// con el motivo del motor, nunca con el cuerpo crudo.
|
|
181
|
+
function rechazada(r) {
|
|
182
|
+
const error = texto(r.cuerpo.error);
|
|
183
|
+
if (r.status >= 400 && r.status < 500 && error) {
|
|
184
|
+
return new FalloDeLaTerminal(SALIDA.entradaInvalida, error, taparSecretos(texto(r.cuerpo.message) ?? `El motor rechazó el pedido (${r.status}).`));
|
|
185
|
+
}
|
|
186
|
+
return inesperada(r.status);
|
|
187
|
+
}
|
|
188
|
+
const JSON_BODY = { "content-type": "application/json" };
|
|
189
|
+
export async function crearConexion(api, clave, repo, f) {
|
|
190
|
+
const r = await conClave(api, clave, "/api/conexiones", { method: "POST", headers: JSON_BODY, body: JSON.stringify({ repo }) }, f, PLAZO_DE_ALTA_MS);
|
|
191
|
+
if (r.status !== 200 && r.status !== 201)
|
|
192
|
+
throw rechazada(r);
|
|
193
|
+
return aPedido(r.cuerpo.pedido, r.status);
|
|
194
|
+
}
|
|
195
|
+
export async function ultimaConexion(api, clave, repo, f, plazoMs) {
|
|
196
|
+
const r = await conClave(api, clave, `/api/conexiones?${new URLSearchParams({ repo })}`, { method: "GET" }, f, plazoMs);
|
|
197
|
+
if (r.status !== 200)
|
|
198
|
+
throw rechazada(r);
|
|
199
|
+
return r.cuerpo.pedido === null ? null : aPedido(r.cuerpo.pedido, r.status);
|
|
200
|
+
}
|
|
201
|
+
export async function crearProyecto(api, clave, conexionId, cuerpo, idempotencyKey, f) {
|
|
202
|
+
const r = await conClave(api, clave, `/api/conexiones/${encodeURIComponent(conexionId)}/proyecto`, { method: "POST", headers: { ...JSON_BODY, "idempotency-key": idempotencyKey }, body: JSON.stringify(cuerpo) }, f, PLAZO_DE_ALTA_MS);
|
|
203
|
+
const error = texto(r.cuerpo.error);
|
|
204
|
+
const message = taparSecretos(texto(r.cuerpo.message) ?? "");
|
|
205
|
+
if (r.status === 200 || r.status === 201) {
|
|
206
|
+
return { tipo: "ok", proyecto: aProyecto(r.cuerpo.proyecto, r.status), reusado: r.status === 200 };
|
|
207
|
+
}
|
|
208
|
+
if (r.status === 422 && error === "comandos_faltantes") {
|
|
209
|
+
const faltantes = Array.isArray(r.cuerpo.faltantes) ? r.cuerpo.faltantes.filter((x) => typeof x === "string") : [];
|
|
210
|
+
return { tipo: "comandos_faltantes", faltantes, message };
|
|
211
|
+
}
|
|
212
|
+
if (r.status === 409 && error === "idempotency_en_curso")
|
|
213
|
+
return { tipo: "en_curso" };
|
|
214
|
+
if (r.status === 409 && error === "idempotency_key_reusada")
|
|
215
|
+
return { tipo: "clave_reusada" };
|
|
216
|
+
if (r.status === 409 && error === "conexion_no_conectada")
|
|
217
|
+
return { tipo: "no_conectada", message };
|
|
218
|
+
if (r.status >= 400 && r.status < 500 && error)
|
|
219
|
+
return { tipo: "rechazo", error, message };
|
|
220
|
+
throw inesperada(r.status);
|
|
221
|
+
}
|
|
222
|
+
export async function buscarProyecto(api, clave, repo, f) {
|
|
223
|
+
const r = await conClave(api, clave, `/api/proyectos?${new URLSearchParams({ repo })}`, { method: "GET" }, f);
|
|
224
|
+
if (r.status !== 200)
|
|
225
|
+
throw rechazada(r);
|
|
226
|
+
return r.cuerpo.proyecto === null ? null : aProyecto(r.cuerpo.proyecto, r.status);
|
|
227
|
+
}
|
|
228
|
+
export async function leerProyecto(api, clave, id, f) {
|
|
229
|
+
const r = await conClave(api, clave, `/api/proyectos/${encodeURIComponent(id)}`, { method: "GET" }, f);
|
|
230
|
+
if (r.status !== 200)
|
|
231
|
+
throw rechazada(r);
|
|
232
|
+
const g = (typeof r.cuerpo.github === "object" && r.cuerpo.github !== null ? r.cuerpo.github : {});
|
|
233
|
+
return { proyecto: aProyecto(r.cuerpo.proyecto, r.status), github: { estado: texto(g.estado), verificadoEn: texto(g.verificado_en) } };
|
|
234
|
+
}
|
|
235
|
+
// ---- Configurar (bloque 4) ----
|
|
236
|
+
const objeto = (v, status) => {
|
|
237
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
238
|
+
throw inesperada(status);
|
|
239
|
+
return v;
|
|
240
|
+
};
|
|
241
|
+
export async function leerCatalogo(api, clave, f) {
|
|
242
|
+
const r = await conClave(api, clave, "/api/catalogo", { method: "GET" }, f);
|
|
243
|
+
if (r.status !== 200 || !Array.isArray(r.cuerpo.supcoflow))
|
|
244
|
+
throw rechazada(r);
|
|
245
|
+
return r.cuerpo;
|
|
246
|
+
}
|
|
247
|
+
export async function leerConfiguracion(api, clave, proyectoId, f) {
|
|
248
|
+
const r = await conClave(api, clave, `/api/proyectos/${encodeURIComponent(proyectoId)}/configuracion`, { method: "GET" }, f);
|
|
249
|
+
if (r.status !== 200)
|
|
250
|
+
throw rechazada(r);
|
|
251
|
+
objeto(r.cuerpo.chat, r.status);
|
|
252
|
+
if (!Array.isArray(r.cuerpo.sitios))
|
|
253
|
+
throw inesperada(r.status);
|
|
254
|
+
return r.cuerpo;
|
|
255
|
+
}
|
|
256
|
+
// Las escrituras devuelven la respuesta entera: qué significa un 409 lo decide cada comando (prender sin
|
|
257
|
+
// guía es «hace falta una persona»; la lista que cambió es «probá de nuevo»).
|
|
258
|
+
export async function configurar(api, clave, ruta, cuerpo, f, extra = {}) {
|
|
259
|
+
const headers = { ...JSON_BODY };
|
|
260
|
+
if (extra.idempotencyKey !== undefined)
|
|
261
|
+
headers["idempotency-key"] = extra.idempotencyKey;
|
|
262
|
+
return conClave(api, clave, ruta, { method: "POST", headers, body: JSON.stringify(cuerpo) }, f);
|
|
263
|
+
}
|
|
264
|
+
export async function subirLogo(api, clave, proyectoId, archivo, f) {
|
|
265
|
+
const form = new FormData();
|
|
266
|
+
form.set("logo", new Blob([archivo.bytes], { type: archivo.mime }), archivo.nombre);
|
|
267
|
+
// Sin content-type a mano: `fetch` pone el multipart con su boundary.
|
|
268
|
+
return conClave(api, clave, `/api/proyectos/${encodeURIComponent(proyectoId)}/chat/logo`, { method: "POST", body: form }, f);
|
|
269
|
+
}
|
|
270
|
+
export async function listarClaves(api, clave, proyectoId, f) {
|
|
271
|
+
const r = await conClave(api, clave, `/api/proyectos/${encodeURIComponent(proyectoId)}/claves`, { method: "GET" }, f);
|
|
272
|
+
if (r.status !== 200 || !Array.isArray(r.cuerpo.claves))
|
|
273
|
+
throw rechazada(r);
|
|
274
|
+
return r.cuerpo.claves.map((x) => {
|
|
275
|
+
const o = objeto(x, r.status);
|
|
276
|
+
const [id, nombre, creadaEn] = [o.id, o.nombre, o.creada_en].map(texto);
|
|
277
|
+
if (!id || !nombre || !creadaEn)
|
|
278
|
+
throw inesperada(r.status);
|
|
279
|
+
return { id, nombre, creadaEn };
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
// ¿Esta clave es una clave de CONSULTA de este proyecto y autentica? La prueba de posesión de
|
|
283
|
+
// `claves crear`: el motor es el único que sabe cuál generación es la vigente, y de qué alcance. Que lea el
|
|
284
|
+
// proyecto no alcanza: una clave personal también lo lee y puede mucho más (tercera revisión, hallazgo 2).
|
|
285
|
+
// Sin `conClave`: un 401 acá es la respuesta («no»), no «iniciá sesión».
|
|
286
|
+
export async function esClaveDeConsultaDelProyecto(api, clave, proyectoId, f) {
|
|
287
|
+
const r = await pedir(api, "/api/yo", { method: "GET", headers: bearer(clave) }, f);
|
|
288
|
+
if (r.status === 401)
|
|
289
|
+
return false;
|
|
290
|
+
if (r.status >= 500)
|
|
291
|
+
throw sinServicio(r.status);
|
|
292
|
+
if (r.status !== 200)
|
|
293
|
+
throw inesperada(r.status);
|
|
294
|
+
return r.cuerpo.via === "clave_de_consulta" && r.cuerpo.proyecto === proyectoId;
|
|
295
|
+
}
|
|
296
|
+
// ---- La guía (bloque 5) ----
|
|
297
|
+
// Devuelven la respuesta entera: qué significa cada 409/422 lo decide el comando (`guia_no_subida` es
|
|
298
|
+
// un código de salida propio; `guia_mal_armada` lleva la lista de errores por línea).
|
|
299
|
+
const rutaDeLaGuia = (proyectoId) => `/api/proyectos/${encodeURIComponent(proyectoId)}/guia`;
|
|
300
|
+
export function revisarGuia(api, clave, proyectoId, contenido, f) {
|
|
301
|
+
return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}/revisar`, { method: "POST", headers: JSON_BODY, body: JSON.stringify({ contenido }) }, f);
|
|
302
|
+
}
|
|
303
|
+
export function publicarGuia(api, clave, proyectoId, cuerpo, f) {
|
|
304
|
+
// La vía de GitHub acuña con Connect y le pregunta a GitHub por la rama: el mismo plazo que las altas (la directa no
|
|
305
|
+
// llama a GitHub, pero comparte el plazo para no tener dos reglas).
|
|
306
|
+
return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}/publicar`, { method: "POST", headers: JSON_BODY, body: JSON.stringify(cuerpo) }, f, PLAZO_DE_ALTA_MS);
|
|
307
|
+
}
|
|
308
|
+
// Siempre con `comprobar=1`: `guia ver` quiere el `al_dia` de la rama; el motor lo ignora si el origen no es el repo.
|
|
309
|
+
export function leerGuia(api, clave, proyectoId, f) {
|
|
310
|
+
return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}?comprobar=1`, { method: "GET" }, f, PLAZO_DE_ALTA_MS);
|
|
311
|
+
}
|
|
312
|
+
export function elegirOrigen(api, clave, proyectoId, origen, f) {
|
|
313
|
+
return conClave(api, clave, `${rutaDeLaGuia(proyectoId)}/origen`, { method: "POST", headers: JSON_BODY, body: JSON.stringify({ origen }) }, f);
|
|
314
|
+
}
|
package/dist/argumentos.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { FalloDeLaTerminal, SALIDA } from "./contrato.js";
|
|
3
|
+
export const COMANDOS_COMPUESTOS = [
|
|
4
|
+
"repo conectar", "proyecto crear", "proyecto ver",
|
|
5
|
+
"chat prender", "chat apagar", "chat marca", "chat logo", "sitios agregar", "sitios quitar",
|
|
6
|
+
"datos fijar", "politica fijar", "claves crear", "claves listar", "claves revocar",
|
|
7
|
+
"guia revisar", "guia publicar", "guia ver", "guia usar",
|
|
8
|
+
];
|
|
9
|
+
// Sólo estas primeras palabras admiten una segunda: cualquier otro par sigue siendo «de más».
|
|
10
|
+
const PRIMERAS_PALABRAS = new Set(COMANDOS_COMPUESTOS.map((c) => c.split(" ")[0]));
|
|
11
|
+
const FLAGS_CON_VALOR = [
|
|
12
|
+
"repo", "install", "test", "sitio", "titulo", "saludo", "archivo", "permitir", "modo", "nombre", "guardar-en", "id", "origen",
|
|
13
|
+
];
|
|
3
14
|
// strict: un flag mal escrito (`--estart`) no se ignora en silencio: un asistente creería que
|
|
4
15
|
// pidió algo que no pidió.
|
|
5
16
|
export function leerArgumentos(argv) {
|
|
@@ -15,7 +26,23 @@ export function leerArgumentos(argv) {
|
|
|
15
26
|
help: { type: "boolean", short: "h" },
|
|
16
27
|
start: { type: "boolean" },
|
|
17
28
|
wait: { type: "boolean" },
|
|
29
|
+
repo: { type: "string" },
|
|
30
|
+
install: { type: "string" },
|
|
31
|
+
test: { type: "string" },
|
|
32
|
+
sitio: { type: "string" },
|
|
33
|
+
titulo: { type: "string" },
|
|
34
|
+
saludo: { type: "string" },
|
|
35
|
+
archivo: { type: "string" },
|
|
36
|
+
permitir: { type: "string" },
|
|
37
|
+
modo: { type: "string" },
|
|
38
|
+
nombre: { type: "string" },
|
|
39
|
+
"guardar-en": { type: "string" },
|
|
40
|
+
id: { type: "string" },
|
|
41
|
+
origen: { type: "string" },
|
|
42
|
+
"desde-github": { type: "boolean" },
|
|
43
|
+
tema: { type: "string", multiple: true },
|
|
18
44
|
},
|
|
45
|
+
tokens: true,
|
|
19
46
|
});
|
|
20
47
|
}
|
|
21
48
|
catch (e) {
|
|
@@ -26,18 +53,51 @@ export function leerArgumentos(argv) {
|
|
|
26
53
|
}
|
|
27
54
|
throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_invalidos", `${e.message} Probá \`supcoflow --help\`.`);
|
|
28
55
|
}
|
|
29
|
-
|
|
30
|
-
|
|
56
|
+
const pos = parsed.positionals;
|
|
57
|
+
const deMas = () => new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_de_mas", "Pasaste más de un comando. Probá `supcoflow --help`.");
|
|
58
|
+
if (pos.length > 2 || (pos.length === 2 && !PRIMERAS_PALABRAS.has(pos[0])))
|
|
59
|
+
throw deMas();
|
|
60
|
+
// `parseArgs` se queda con el último valor de un flag repetido: un asistente que repite `--repo`
|
|
61
|
+
// creería que se usó el primero. Se mira en los tokens y se rechaza.
|
|
62
|
+
for (const nombre of FLAGS_CON_VALOR) {
|
|
63
|
+
const toks = (parsed.tokens ?? []).filter((t) => t.kind === "option" && t.name === nombre);
|
|
64
|
+
if (toks.length > 1 || toks.some((t) => t.kind === "option" && t.value === "")) {
|
|
65
|
+
throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_invalidos", `--${nombre} va una sola vez y con un valor. Probá \`supcoflow --help\`.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// `--tema` sí se repite (uno por clave), pero ninguno vacío: un `--tema ""` es un error de quien arma
|
|
69
|
+
// el comando, no «sin tema».
|
|
70
|
+
if ((parsed.values.tema ?? []).some((t) => t === "")) {
|
|
71
|
+
throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_invalidos", "--tema va con un valor clave=valor. Probá `supcoflow --help`.");
|
|
31
72
|
}
|
|
73
|
+
// Un par desconocido (`repo borrar`) queda como la primera palabra sola: el despacho la rechaza
|
|
74
|
+
// como comando desconocido, con el mismo mensaje que cualquier otra.
|
|
75
|
+
const compuesto = pos.length === 2 ? `${pos[0]} ${pos[1]}` : null;
|
|
76
|
+
const comando = compuesto !== null && COMANDOS_COMPUESTOS.includes(compuesto) ? compuesto : (pos[0] ?? null);
|
|
32
77
|
const v = parsed.values;
|
|
33
78
|
return {
|
|
34
|
-
comando
|
|
79
|
+
comando,
|
|
35
80
|
flags: {
|
|
36
81
|
json: v.json === true,
|
|
37
82
|
dryRun: v["dry-run"] === true,
|
|
38
83
|
help: v.help === true,
|
|
39
84
|
start: v.start === true,
|
|
40
85
|
wait: v.wait === true,
|
|
86
|
+
repo: v.repo ?? null,
|
|
87
|
+
install: v.install ?? null,
|
|
88
|
+
test: v.test ?? null,
|
|
89
|
+
sitio: v.sitio ?? null,
|
|
90
|
+
titulo: v.titulo ?? null,
|
|
91
|
+
saludo: v.saludo ?? null,
|
|
92
|
+
archivo: v.archivo ?? null,
|
|
93
|
+
permitir: v.permitir ?? null,
|
|
94
|
+
modo: v.modo ?? null,
|
|
95
|
+
nombre: v.nombre ?? null,
|
|
96
|
+
guardarEn: v["guardar-en"] ?? null,
|
|
97
|
+
id: v.id ?? null,
|
|
98
|
+
origen: v.origen ?? null,
|
|
99
|
+
desdeGithub: v["desde-github"] === true,
|
|
100
|
+
tema: v.tema ?? [],
|
|
41
101
|
},
|
|
42
102
|
};
|
|
43
103
|
}
|
package/dist/candado.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Un candado por operación en ESTA máquina (segunda revisión externa del 2026-10-16, hallazgo 1): comprobar
|
|
2
|
+
// que un archivo autentica y después escribirlo no es atómico, y dos `claves crear` del mismo proyecto y
|
|
3
|
+
// nombre pisaban uno el secreto del otro. Un archivo creado en exclusiva es el candado más simple que
|
|
4
|
+
// funciona entre procesos sin dependencias.
|
|
5
|
+
//
|
|
6
|
+
// ─── No se recupera solo (tercera revisión, hallazgo 1) ─────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// La primera versión borraba un candado «viejo» y tomaba uno nuevo. Leer la fecha y después borrar no es
|
|
9
|
+
// atómico: dos procesos que encontraban el mismo candado viejo entraban los dos, y el primero en soltar
|
|
10
|
+
// borraba el del otro. Ahora un candado de un proceso que ya no existe se informa con su ruta y lo borra una
|
|
11
|
+
// persona, que sabe que no hay nadie corriendo. Y `soltar` sólo borra si el archivo sigue siendo el suyo.
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
export function crearCandados(fs, carpeta, pid, procesoVivo) {
|
|
15
|
+
return {
|
|
16
|
+
tomar(nombre) {
|
|
17
|
+
// El nombre lleva el id del proyecto y el nombre de la clave que eligió la persona: el hash lo vuelve
|
|
18
|
+
// un nombre de archivo sin barras ni puntos, sea lo que sea.
|
|
19
|
+
const ruta = path.join(carpeta, `${createHash("sha256").update(nombre).digest("hex")}.lock`);
|
|
20
|
+
const token = randomUUID();
|
|
21
|
+
let creado;
|
|
22
|
+
try {
|
|
23
|
+
fs.crearDir(carpeta, 0o700);
|
|
24
|
+
creado = fs.crearExclusivo(ruta, JSON.stringify({ pid, token }), 0o600);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { tipo: "no_disponible", ruta };
|
|
28
|
+
}
|
|
29
|
+
if (!creado) {
|
|
30
|
+
let duenio = null;
|
|
31
|
+
try {
|
|
32
|
+
const v = JSON.parse(fs.leer(ruta) ?? "null");
|
|
33
|
+
duenio = typeof v?.pid === "number" ? v.pid : null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
duenio = null;
|
|
37
|
+
}
|
|
38
|
+
// Un candado que no se entiende tampoco se toca: «abandonado» le pide a una persona que mire.
|
|
39
|
+
return duenio !== null && procesoVivo(duenio) ? { tipo: "ocupado" } : { tipo: "abandonado", ruta };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
tipo: "tomado",
|
|
43
|
+
soltar() {
|
|
44
|
+
// Sólo si sigue siendo MÍO: si una persona lo borró y otro tomó uno nuevo, no se toca.
|
|
45
|
+
try {
|
|
46
|
+
const v = JSON.parse(fs.leer(ruta) ?? "null");
|
|
47
|
+
if (v?.token === token)
|
|
48
|
+
fs.borrar(ruta);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* best-effort: si no se pudo, queda abandonado y se informa con su ruta */
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
// sale como un 1 limpio, en una línea si se pidió --json, sin traza y con los secretos tapados.
|
|
5
5
|
import { revocarPropia } from "./api.js";
|
|
6
6
|
import { leerArgumentos } from "./argumentos.js";
|
|
7
|
+
import { clavesCrear, clavesListar, clavesRevocar } from "./comandos/claves.js";
|
|
8
|
+
import { chatLogo, chatMarca, chatPrenderApagar, comandoConfiguracion, sitios } from "./comandos/configurar.js";
|
|
9
|
+
import { guiaPublicar, guiaRevisar, guiaUsar, guiaVer } from "./comandos/guia.js";
|
|
10
|
+
import { comandoCatalogo, datosFijar, politicaFijar } from "./comandos/datos-y-politica.js";
|
|
7
11
|
import { ayuda, comandoDescribir, DESCRIPCION } from "./comandos/describir.js";
|
|
8
12
|
import { loginInteractivo, loginStart, loginWait } from "./comandos/login.js";
|
|
9
13
|
import { comandoLogout } from "./comandos/logout.js";
|
|
14
|
+
import { proyectoCrear, proyectoVer } from "./comandos/proyecto.js";
|
|
10
15
|
import { comandoQuienSoy } from "./comandos/quien-soy.js";
|
|
16
|
+
import { repoConectarInteractivo, repoConectarStart, repoConectarWait } from "./comandos/repo-conectar.js";
|
|
11
17
|
import { emitir, FalloDeLaTerminal, resultadoDeFallo, SALIDA } from "./contrato.js";
|
|
12
18
|
import { entornoReal } from "./entorno.js";
|
|
19
|
+
import { flagQueNoAplica } from "./flags-por-comando.js";
|
|
13
20
|
const COMANDOS = new Set(DESCRIPCION.comandos.map((c) => c.nombre));
|
|
14
21
|
const invalida = (error, message) => new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message);
|
|
15
22
|
async function correr(argv) {
|
|
@@ -26,15 +33,21 @@ async function correr(argv) {
|
|
|
26
33
|
const texto = ayuda(comando);
|
|
27
34
|
return { r: { codigo: SALIDA.ok, json: { ok: true, ayuda: texto }, texto }, json };
|
|
28
35
|
}
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const presentes = {
|
|
37
|
+
start: flags.start, wait: flags.wait, repo: flags.repo !== null, install: flags.install !== null, test: flags.test !== null,
|
|
38
|
+
sitio: flags.sitio !== null, titulo: flags.titulo !== null, saludo: flags.saludo !== null, tema: flags.tema.length > 0,
|
|
39
|
+
archivo: flags.archivo !== null, permitir: flags.permitir !== null, modo: flags.modo !== null,
|
|
40
|
+
nombre: flags.nombre !== null, guardarEn: flags.guardarEn !== null, id: flags.id !== null, origen: flags.origen !== null, desdeGithub: flags.desdeGithub,
|
|
41
|
+
};
|
|
42
|
+
const noAplica = flagQueNoAplica(comando, Object.keys(presentes).filter((k) => presentes[k]));
|
|
43
|
+
if (noAplica !== null)
|
|
44
|
+
throw invalida("flag_no_aplica", noAplica);
|
|
45
|
+
if (flags.start && flags.wait) {
|
|
46
|
+
throw invalida("flags_incompatibles", "--start y --wait van por separado: primero uno y después el otro.");
|
|
31
47
|
}
|
|
32
48
|
if (comando === "describir")
|
|
33
49
|
return { r: comandoDescribir(), json };
|
|
34
50
|
if (comando === "login") {
|
|
35
|
-
if (flags.start && flags.wait) {
|
|
36
|
-
throw invalida("flags_incompatibles", "--start y --wait van por separado: primero uno y después el otro.");
|
|
37
|
-
}
|
|
38
51
|
// ANTES de armar el entorno: no hay cómo simular un canje sin consumirlo, y este error no
|
|
39
52
|
// puede depender de leer nada guardado ni de llamar al motor.
|
|
40
53
|
if (flags.wait && flags.dryRun) {
|
|
@@ -62,6 +75,54 @@ async function correr(argv) {
|
|
|
62
75
|
};
|
|
63
76
|
case "quien-soy":
|
|
64
77
|
return { r: await comandoQuienSoy({ api: e.api, guardado: e.guardado, quienSoy: e.quienSoy, avisar: e.avisar }), json };
|
|
78
|
+
case "repo conectar":
|
|
79
|
+
if (flags.start)
|
|
80
|
+
return { r: await repoConectarStart(e.conexion, flags), json };
|
|
81
|
+
// `--wait` sólo lee (un GET por tramo): con --dry-run haría lo mismo, así que no se rechaza.
|
|
82
|
+
if (flags.wait)
|
|
83
|
+
return { r: await repoConectarWait(e.conexion, flags), json };
|
|
84
|
+
// Como `login`: sin TTY, --dry-run igual dice lo que haría el primer paso, sin tocar el motor.
|
|
85
|
+
if (flags.dryRun)
|
|
86
|
+
return { r: await repoConectarStart(e.conexion, { repo: flags.repo, dryRun: true }), json };
|
|
87
|
+
return { r: await repoConectarInteractivo({ ...e.conexion, esTty: e.esTty, mostrar: e.mostrar }, flags), json };
|
|
88
|
+
case "proyecto crear":
|
|
89
|
+
return { r: await proyectoCrear(e.proyecto, flags), json };
|
|
90
|
+
case "proyecto ver":
|
|
91
|
+
return { r: await proyectoVer(e.proyecto, flags), json };
|
|
92
|
+
case "catalogo":
|
|
93
|
+
return { r: await comandoCatalogo(e.configurar), json };
|
|
94
|
+
case "configuracion":
|
|
95
|
+
return { r: await comandoConfiguracion(e.configurar, flags), json };
|
|
96
|
+
case "chat prender":
|
|
97
|
+
return { r: await chatPrenderApagar(e.configurar, flags, true), json };
|
|
98
|
+
case "chat apagar":
|
|
99
|
+
return { r: await chatPrenderApagar(e.configurar, flags, false), json };
|
|
100
|
+
case "chat marca":
|
|
101
|
+
return { r: await chatMarca(e.configurar, flags), json };
|
|
102
|
+
case "chat logo":
|
|
103
|
+
return { r: await chatLogo(e.configurar, flags), json };
|
|
104
|
+
case "sitios agregar":
|
|
105
|
+
return { r: await sitios(e.configurar, flags, "agregar"), json };
|
|
106
|
+
case "sitios quitar":
|
|
107
|
+
return { r: await sitios(e.configurar, flags, "quitar"), json };
|
|
108
|
+
case "datos fijar":
|
|
109
|
+
return { r: await datosFijar(e.configurar, flags), json };
|
|
110
|
+
case "politica fijar":
|
|
111
|
+
return { r: await politicaFijar(e.configurar, flags), json };
|
|
112
|
+
case "claves crear":
|
|
113
|
+
return { r: await clavesCrear(e.claves, flags), json };
|
|
114
|
+
case "claves listar":
|
|
115
|
+
return { r: await clavesListar(e.claves, flags), json };
|
|
116
|
+
case "claves revocar":
|
|
117
|
+
return { r: await clavesRevocar(e.claves, flags), json };
|
|
118
|
+
case "guia revisar":
|
|
119
|
+
return { r: await guiaRevisar(e.guia, flags), json };
|
|
120
|
+
case "guia publicar":
|
|
121
|
+
return { r: await guiaPublicar(e.guia, flags), json };
|
|
122
|
+
case "guia ver":
|
|
123
|
+
return { r: await guiaVer(e.guia, flags), json };
|
|
124
|
+
case "guia usar":
|
|
125
|
+
return { r: await guiaUsar(e.guia, flags), json };
|
|
65
126
|
}
|
|
66
127
|
throw new Error(`comando sin despacho: ${comando}`);
|
|
67
128
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// La clave con la que la terminal le habla al motor, leída igual para todo comando que la necesite
|
|
2
|
+
// (`quien-soy`, `repo conectar`, …): un solo lugar decide qué es «sin sesión» y qué es «no pude mirar».
|
|
3
|
+
import { SALIDA } from "../contrato.js";
|
|
4
|
+
export async function claveVigente(guardado, origen) {
|
|
5
|
+
const lectura = await guardado.leerClaveCompleta(origen);
|
|
6
|
+
const vigente = lectura.valor;
|
|
7
|
+
// «Iniciá sesión» con la clave guardada en un llavero que no contestó invita a un login que la
|
|
8
|
+
// pisaría: no es «sin sesión», es «no pude mirar» (Review Focus 12).
|
|
9
|
+
if (vigente === null && !lectura.completa) {
|
|
10
|
+
const message = "No pude leer la clave guardada (el llavero no respondió). Desbloquealo y probá de nuevo.";
|
|
11
|
+
return { ok: false, resultado: { codigo: SALIDA.inesperado, json: { ok: false, error: "llavero_no_responde", message }, texto: message } };
|
|
12
|
+
}
|
|
13
|
+
if (vigente === null) {
|
|
14
|
+
const message = "Iniciá sesión con `supcoflow login`.";
|
|
15
|
+
return { ok: false, resultado: { codigo: SALIDA.faltaLogin, json: { ok: false, error: "sin_sesion", message }, texto: message } };
|
|
16
|
+
}
|
|
17
|
+
return { ok: true, vigente: { clave: vigente.clave, fuente: vigente.fuente, completa: lectura.completa } };
|
|
18
|
+
}
|