supcoflow 0.0.0-stage → 0.1.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 +82 -2
- package/dist/api.js +133 -0
- package/dist/argumentos.js +43 -0
- package/dist/cli.js +76 -0
- package/dist/comandos/describir.js +106 -0
- package/dist/comandos/login.js +254 -0
- package/dist/comandos/logout.js +96 -0
- package/dist/comandos/quien-soy.js +50 -0
- package/dist/contrato.js +38 -0
- package/dist/entorno.js +152 -0
- package/dist/guardado/archivo.js +63 -0
- package/dist/guardado/index.js +258 -0
- package/dist/guardado/llavero.js +110 -0
- package/dist/guardado/repo.js +35 -0
- package/dist/sondeo.js +47 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
-
#
|
|
1
|
+
# supcoflow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
La terminal de [Supcoflow](https://app.supcoflow.dev): iniciar sesión y manejar el chat de soporte
|
|
4
|
+
desde la consola, o desde un asistente de código que corre comandos por vos.
|
|
5
|
+
|
|
6
|
+
Por ahora trae lo básico: iniciar y cerrar sesión, y ver con qué cuenta estás.
|
|
7
|
+
|
|
8
|
+
## Instalar
|
|
9
|
+
|
|
10
|
+
Hace falta Node 24 o más nuevo.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx supcoflow login
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Los comandos
|
|
17
|
+
|
|
18
|
+
| Comando | Qué hace |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `supcoflow login` | Te muestra un enlace y un código; lo aprobás en la consola y la terminal queda con su clave. |
|
|
21
|
+
| `supcoflow login --start` | El primer paso, solo: pide el código y sale 5 con el enlace para aprobarlo. |
|
|
22
|
+
| `supcoflow login --wait` | El segundo paso: espera la aprobación hasta 90 s. Si todavía no llegó, sale 5: volvé a correrlo. |
|
|
23
|
+
| `supcoflow logout` | Revoca la clave en uso y la borra de esta máquina. |
|
|
24
|
+
| `supcoflow quien-soy` | Con qué clave anda la terminal, a nombre de quién y en qué equipo. |
|
|
25
|
+
| `supcoflow describir --json` | Todo el contrato (comandos, flags, facultades de la clave, códigos) en una línea de JSON. |
|
|
26
|
+
|
|
27
|
+
`supcoflow --help` (o `supcoflow <comando> --help`) muestra la ayuda con ejemplos.
|
|
28
|
+
|
|
29
|
+
### Ejemplos
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Una persona frente a la terminal
|
|
33
|
+
supcoflow login
|
|
34
|
+
|
|
35
|
+
# Un asistente, sin terminal interactiva: dos pasos
|
|
36
|
+
supcoflow login --start --json # sale 5 con verification_uri_complete y user_code
|
|
37
|
+
supcoflow login --wait --json # sale 5 mientras la persona no apruebe; 0 cuando aprueba
|
|
38
|
+
|
|
39
|
+
supcoflow quien-soy --json
|
|
40
|
+
supcoflow logout --dry-run # qué revocaría, sin revocar nada
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## El contrato
|
|
44
|
+
|
|
45
|
+
Pensado para que lo lea un programa, no sólo una persona:
|
|
46
|
+
|
|
47
|
+
- **`--json`**: el resultado sale por stdout en **una línea** de JSON; los avisos van por stderr.
|
|
48
|
+
Los errores también: `{"ok":false,"error":"…","message":"…"}`, con su código de salida.
|
|
49
|
+
- **Sin preguntas sin terminal interactiva** (o con `--json`): si falta un dato, sale 3 diciendo qué
|
|
50
|
+
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`). `login --wait
|
|
52
|
+
--dry-run` sale 3: sondear consume el código y no hay cómo simularlo.
|
|
53
|
+
|
|
54
|
+
| Código | Quiere decir |
|
|
55
|
+
|---|---|
|
|
56
|
+
| 0 | ok |
|
|
57
|
+
| 1 | inesperado |
|
|
58
|
+
| 2 | falta login |
|
|
59
|
+
| 3 | entrada inválida |
|
|
60
|
+
| 4 | hace falta una persona |
|
|
61
|
+
| 5 | esperando a una persona: volvé a llamar |
|
|
62
|
+
| 6 | tope alcanzado |
|
|
63
|
+
|
|
64
|
+
## Dónde queda la clave
|
|
65
|
+
|
|
66
|
+
- En el **llavero del sistema** (Keychain en macOS, Secret Service con `secret-tool` en Linux).
|
|
67
|
+
- Si no hay llavero, en `~/.config/supcoflow/credenciales.json` (o `$XDG_CONFIG_HOME/supcoflow/…`)
|
|
68
|
+
con permisos `0600`, y la terminal te avisa.
|
|
69
|
+
- **Nunca dentro de un repo de git**: si ese archivo cayera dentro de uno (un `HOME` que es un repo
|
|
70
|
+
de dotfiles, por ejemplo), no guarda nada y te pide usar el llavero o `SUPCOFLOW_TOKEN`.
|
|
71
|
+
- **`SUPCOFLOW_TOKEN`** tiene prioridad sobre lo guardado: sirve para CI y para la nube.
|
|
72
|
+
- La clave nunca sale en un log ni en una salida `--json`.
|
|
73
|
+
|
|
74
|
+
Cuando iniciás sesión de nuevo, la clave anterior se revoca recién cuando la nueva quedó guardada; si
|
|
75
|
+
no se pudo revocar, queda anotada y se reintenta en el próximo `login` o `logout`.
|
|
76
|
+
|
|
77
|
+
## La API es fija
|
|
78
|
+
|
|
79
|
+
La terminal habla siempre con `https://app.supcoflow.dev`. Ningún archivo del repo puede cambiar
|
|
80
|
+
eso, así que clonar un repo ajeno y correr `supcoflow` no manda tu clave a otro servidor. Para
|
|
81
|
+
desarrollo, sólo la variable de entorno `SUPCOFLOW_API_URL` la mueve (`https`, o `http` contra
|
|
82
|
+
`localhost`), y lo guardado queda atado a la API que lo emitió: una clave de un motor local nunca
|
|
83
|
+
viaja a producción.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Los cuatro pedidos de la terminal al motor. Sin dependencias: `fetch` viene de Node y se inyecta.
|
|
2
|
+
import { FalloDeLaTerminal, SALIDA } from "./contrato.js";
|
|
3
|
+
// La URL viene FIJA (§5.2): si un archivo del repo pudiera cambiarla, clonar un repo ajeno y correr
|
|
4
|
+
// `supcoflow` mandaría el token a un servidor ajeno. Sólo el entorno del proceso la mueve, y `http`
|
|
5
|
+
// únicamente contra la propia máquina: contra un host remoto la clave viajaría en claro.
|
|
6
|
+
export const API_DE_PRODUCCION = "https://app.supcoflow.dev";
|
|
7
|
+
const LOCALES = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
8
|
+
export function urlDeLaApi(env) {
|
|
9
|
+
const crudo = env.SUPCOFLOW_API_URL;
|
|
10
|
+
if (crudo === undefined || crudo === "")
|
|
11
|
+
return new URL(API_DE_PRODUCCION);
|
|
12
|
+
const invalida = () => new FalloDeLaTerminal(SALIDA.entradaInvalida, "api_url_invalida", "SUPCOFLOW_API_URL tiene que ser https, o http contra localhost.");
|
|
13
|
+
let u;
|
|
14
|
+
try {
|
|
15
|
+
u = new URL(crudo);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw invalida();
|
|
19
|
+
}
|
|
20
|
+
if (u.protocol === "https:" || (u.protocol === "http:" && LOCALES.has(u.hostname)))
|
|
21
|
+
return new URL(u.origin);
|
|
22
|
+
throw invalida();
|
|
23
|
+
}
|
|
24
|
+
// A mano a propósito: tiene que coincidir con package.json (0.1.0); sólo identifica a la terminal
|
|
25
|
+
// en los logs del motor, no decide nada.
|
|
26
|
+
const VERSION = "0.1.0";
|
|
27
|
+
const PLAZO_POR_DEFECTO_MS = 15_000;
|
|
28
|
+
// Todo lo que vuelve de afuera se trata como no confiable: nunca se pega el cuerpo crudo en un
|
|
29
|
+
// mensaje (una página HTML no sirve en una línea y podría traer cualquier cosa), y el mensaje del
|
|
30
|
+
// error de red no se reenvía (podría llevar la URL o la clave).
|
|
31
|
+
async function pedir(api, ruta, init, f, plazoMs) {
|
|
32
|
+
let res;
|
|
33
|
+
let texto;
|
|
34
|
+
try {
|
|
35
|
+
res = await f(new URL(ruta, api), {
|
|
36
|
+
...init,
|
|
37
|
+
headers: { ...init.headers, "user-agent": `supcoflow-cli/${VERSION}` },
|
|
38
|
+
signal: AbortSignal.timeout(plazoMs ?? PLAZO_POR_DEFECTO_MS),
|
|
39
|
+
// Un 307/308 reenvía el cuerpo del formulario (con el device_code) adonde diga `Location`, aunque
|
|
40
|
+
// sea otro origen o http. Los cuatro endpoints del motor nunca redirigen: una redirección es
|
|
41
|
+
// anómala y sale como `motor_inalcanzable`, sin Location ni cuerpo en el mensaje.
|
|
42
|
+
redirect: "error",
|
|
43
|
+
});
|
|
44
|
+
texto = await res.text();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", "No pude comunicarme con el motor (red caída o demora).");
|
|
48
|
+
}
|
|
49
|
+
let cuerpo;
|
|
50
|
+
try {
|
|
51
|
+
cuerpo = JSON.parse(texto);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
cuerpo = undefined;
|
|
55
|
+
}
|
|
56
|
+
if (typeof cuerpo !== "object" || cuerpo === null || Array.isArray(cuerpo)) {
|
|
57
|
+
throw new FalloDeLaTerminal(SALIDA.inesperado, "respuesta_inesperada", `El motor respondió ${res.status} sin JSON.`);
|
|
58
|
+
}
|
|
59
|
+
return { status: res.status, cuerpo: cuerpo };
|
|
60
|
+
}
|
|
61
|
+
function sinServicio(status) {
|
|
62
|
+
return new FalloDeLaTerminal(SALIDA.inesperado, "motor_inalcanzable", `El motor respondió ${status}; probá en unos minutos.`);
|
|
63
|
+
}
|
|
64
|
+
function inesperada(status) {
|
|
65
|
+
return new FalloDeLaTerminal(SALIDA.inesperado, "respuesta_inesperada", `El motor respondió ${status} con algo que no esperaba.`);
|
|
66
|
+
}
|
|
67
|
+
const texto = (v) => (typeof v === "string" ? v : null);
|
|
68
|
+
const CLIENT_ID = "supcoflow-cli";
|
|
69
|
+
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
70
|
+
const FORMULARIO = { "content-type": "application/x-www-form-urlencoded" };
|
|
71
|
+
export async function pedirCodigo(api, maquina, f) {
|
|
72
|
+
const { status, cuerpo: c } = await pedir(api, "/api/dispositivo/codigo", { method: "POST", headers: FORMULARIO, body: new URLSearchParams({ client_id: CLIENT_ID, maquina }).toString() }, f);
|
|
73
|
+
if (status === 429 || status === 503) {
|
|
74
|
+
throw new FalloDeLaTerminal(SALIDA.inesperado, texto(c.error) ?? "demasiados_pedidos", "El motor no acepta más pedidos ahora; probá en unos minutos.");
|
|
75
|
+
}
|
|
76
|
+
if (status >= 500)
|
|
77
|
+
throw sinServicio(status);
|
|
78
|
+
const [deviceCode, userCode, verificationUri, verificationUriComplete] = [c.device_code, c.user_code, c.verification_uri, c.verification_uri_complete].map(texto);
|
|
79
|
+
if (status !== 200 || !deviceCode || !userCode || !verificationUri || !verificationUriComplete
|
|
80
|
+
|| typeof c.expires_in !== "number" || typeof c.interval !== "number")
|
|
81
|
+
throw inesperada(status);
|
|
82
|
+
return { deviceCode, userCode, verificationUri, verificationUriComplete, expiresIn: c.expires_in, interval: c.interval };
|
|
83
|
+
}
|
|
84
|
+
const ERRORES_DEL_CANJE = {
|
|
85
|
+
authorization_pending: "pendiente", slow_down: "slow_down", access_denied: "rechazado",
|
|
86
|
+
expired_token: "vencido", invalid_grant: "invalido",
|
|
87
|
+
};
|
|
88
|
+
// `plazoMs` es el techo de ESTE pedido: el sondeo lo achica cerca del final de su tramo para que
|
|
89
|
+
// un pedido colgado no lo pase de largo.
|
|
90
|
+
export async function canjear(api, deviceCode, f, plazoMs) {
|
|
91
|
+
const { status, cuerpo: c } = await pedir(api, "/api/dispositivo/token", { method: "POST", headers: FORMULARIO, body: new URLSearchParams({ grant_type: GRANT_TYPE, device_code: deviceCode, client_id: CLIENT_ID }).toString() }, f, plazoMs);
|
|
92
|
+
if (status >= 500)
|
|
93
|
+
throw sinServicio(status);
|
|
94
|
+
if (status === 200) {
|
|
95
|
+
const clave = texto(c.access_token);
|
|
96
|
+
const equipo = texto(c.equipo);
|
|
97
|
+
if (!clave || !equipo)
|
|
98
|
+
throw inesperada(status);
|
|
99
|
+
return { tipo: "token", clave, equipo };
|
|
100
|
+
}
|
|
101
|
+
const tipo = ERRORES_DEL_CANJE[texto(c.error) ?? ""];
|
|
102
|
+
if (status === 400 && tipo)
|
|
103
|
+
return { tipo };
|
|
104
|
+
throw inesperada(status);
|
|
105
|
+
}
|
|
106
|
+
const bearer = (clave) => ({ authorization: `Bearer ${clave}` });
|
|
107
|
+
export async function quienSoy(api, clave, f) {
|
|
108
|
+
const { status, cuerpo: c } = await pedir(api, "/api/yo", { method: "GET", headers: bearer(clave) }, f);
|
|
109
|
+
if (status === 401) {
|
|
110
|
+
throw new FalloDeLaTerminal(SALIDA.faltaLogin, "clave_invalida", texto(c.message) ?? "La clave ya no vale: iniciá sesión de nuevo.");
|
|
111
|
+
}
|
|
112
|
+
if (status >= 500)
|
|
113
|
+
throw sinServicio(status);
|
|
114
|
+
const u = c.usuario;
|
|
115
|
+
if (status !== 200 || !texto(c.via) || !texto(c.equipo))
|
|
116
|
+
throw inesperada(status);
|
|
117
|
+
return {
|
|
118
|
+
via: c.via,
|
|
119
|
+
equipo: c.equipo,
|
|
120
|
+
usuario: u ? { id: String(u.id), login: texto(u.login), email: texto(u.email) } : null,
|
|
121
|
+
proyecto: texto(c.proyecto),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export async function revocarPropia(api, clave, f, plazoMs) {
|
|
125
|
+
const { status } = await pedir(api, "/api/dispositivo/revocar", { method: "POST", headers: bearer(clave) }, f, plazoMs);
|
|
126
|
+
if (status === 200)
|
|
127
|
+
return "revocada";
|
|
128
|
+
if (status === 401)
|
|
129
|
+
return "ya_no_valia";
|
|
130
|
+
if (status >= 500)
|
|
131
|
+
throw sinServicio(status);
|
|
132
|
+
throw inesperada(status);
|
|
133
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { FalloDeLaTerminal, SALIDA } from "./contrato.js";
|
|
3
|
+
// strict: un flag mal escrito (`--estart`) no se ignora en silencio: un asistente creería que
|
|
4
|
+
// pidió algo que no pidió.
|
|
5
|
+
export function leerArgumentos(argv) {
|
|
6
|
+
let parsed;
|
|
7
|
+
try {
|
|
8
|
+
parsed = parseArgs({
|
|
9
|
+
args: argv,
|
|
10
|
+
strict: true,
|
|
11
|
+
allowPositionals: true,
|
|
12
|
+
options: {
|
|
13
|
+
json: { type: "boolean" },
|
|
14
|
+
"dry-run": { type: "boolean" },
|
|
15
|
+
help: { type: "boolean", short: "h" },
|
|
16
|
+
start: { type: "boolean" },
|
|
17
|
+
wait: { type: "boolean" },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
const code = e.code;
|
|
23
|
+
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
24
|
+
const m = /'(--?[^']+)'/.exec(e.message);
|
|
25
|
+
throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "flag_desconocido", `No conozco el flag ${m ? m[1] : "que pasaste"}. Probá \`supcoflow --help\`.`);
|
|
26
|
+
}
|
|
27
|
+
throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_invalidos", `${e.message} Probá \`supcoflow --help\`.`);
|
|
28
|
+
}
|
|
29
|
+
if (parsed.positionals.length > 1) {
|
|
30
|
+
throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "argumentos_de_mas", "Pasaste más de un comando. Probá `supcoflow --help`.");
|
|
31
|
+
}
|
|
32
|
+
const v = parsed.values;
|
|
33
|
+
return {
|
|
34
|
+
comando: parsed.positionals[0] ?? null,
|
|
35
|
+
flags: {
|
|
36
|
+
json: v.json === true,
|
|
37
|
+
dryRun: v["dry-run"] === true,
|
|
38
|
+
help: v.help === true,
|
|
39
|
+
start: v.start === true,
|
|
40
|
+
wait: v.wait === true,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// El binario `supcoflow`: lee los argumentos, arma el entorno real y despacha. Todo va dentro de un
|
|
3
|
+
// solo try/catch: cualquier cosa que se escape (un archivo de credenciales ilegible, un llavero roto)
|
|
4
|
+
// sale como un 1 limpio, en una línea si se pidió --json, sin traza y con los secretos tapados.
|
|
5
|
+
import { revocarPropia } from "./api.js";
|
|
6
|
+
import { leerArgumentos } from "./argumentos.js";
|
|
7
|
+
import { ayuda, comandoDescribir, DESCRIPCION } from "./comandos/describir.js";
|
|
8
|
+
import { loginInteractivo, loginStart, loginWait } from "./comandos/login.js";
|
|
9
|
+
import { comandoLogout } from "./comandos/logout.js";
|
|
10
|
+
import { comandoQuienSoy } from "./comandos/quien-soy.js";
|
|
11
|
+
import { emitir, FalloDeLaTerminal, resultadoDeFallo, SALIDA } from "./contrato.js";
|
|
12
|
+
import { entornoReal } from "./entorno.js";
|
|
13
|
+
const COMANDOS = new Set(DESCRIPCION.comandos.map((c) => c.nombre));
|
|
14
|
+
const invalida = (error, message) => new FalloDeLaTerminal(SALIDA.entradaInvalida, error, message);
|
|
15
|
+
async function correr(argv) {
|
|
16
|
+
// Si los argumentos no se pueden leer no se sabe qué flags hay: se mira `--json` a mano para que
|
|
17
|
+
// el error salga en el formato que pidió quien llama.
|
|
18
|
+
let json = argv.includes("--json");
|
|
19
|
+
try {
|
|
20
|
+
const { comando, flags } = leerArgumentos(argv);
|
|
21
|
+
json = flags.json;
|
|
22
|
+
if (comando !== null && !COMANDOS.has(comando)) {
|
|
23
|
+
throw invalida("comando_desconocido", `No conozco el comando «${comando}». Probá \`supcoflow --help\`.`);
|
|
24
|
+
}
|
|
25
|
+
if (flags.help || comando === null) {
|
|
26
|
+
const texto = ayuda(comando);
|
|
27
|
+
return { r: { codigo: SALIDA.ok, json: { ok: true, ayuda: texto }, texto }, json };
|
|
28
|
+
}
|
|
29
|
+
if ((flags.start || flags.wait) && comando !== "login") {
|
|
30
|
+
throw invalida("flag_no_aplica", "--start y --wait son sólo de `supcoflow login`.");
|
|
31
|
+
}
|
|
32
|
+
if (comando === "describir")
|
|
33
|
+
return { r: comandoDescribir(), json };
|
|
34
|
+
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
|
+
// ANTES de armar el entorno: no hay cómo simular un canje sin consumirlo, y este error no
|
|
39
|
+
// puede depender de leer nada guardado ni de llamar al motor.
|
|
40
|
+
if (flags.wait && flags.dryRun) {
|
|
41
|
+
throw invalida("dry_run_no_soportado", "`login --wait` no tiene --dry-run: sondear consume el código. Probá `supcoflow login --start --dry-run`.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const e = entornoReal();
|
|
45
|
+
switch (comando) {
|
|
46
|
+
case "login":
|
|
47
|
+
if (flags.start)
|
|
48
|
+
return { r: await loginStart(e.login, flags), json };
|
|
49
|
+
if (flags.wait)
|
|
50
|
+
return { r: await loginWait(e.login), json };
|
|
51
|
+
// Sin TTY, `login --dry-run` igual tiene algo que decir: lo que haría el primer paso.
|
|
52
|
+
if (flags.dryRun)
|
|
53
|
+
return { r: await loginStart(e.login, { dryRun: true }), json };
|
|
54
|
+
return { r: await loginInteractivo({ ...e.login, esTty: e.esTty, mostrar: e.mostrar }, flags), json };
|
|
55
|
+
case "logout":
|
|
56
|
+
return {
|
|
57
|
+
r: await comandoLogout({
|
|
58
|
+
api: e.api, guardado: e.guardado, ahora: e.login.ahora, avisar: e.avisar,
|
|
59
|
+
revocarPropia: (a, clave, plazoMs) => revocarPropia(a, clave, fetch, plazoMs),
|
|
60
|
+
}, flags),
|
|
61
|
+
json,
|
|
62
|
+
};
|
|
63
|
+
case "quien-soy":
|
|
64
|
+
return { r: await comandoQuienSoy({ api: e.api, guardado: e.guardado, quienSoy: e.quienSoy, avisar: e.avisar }), json };
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`comando sin despacho: ${comando}`);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { r: resultadoDeFallo(e), json };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// `process.exitCode` y nunca `process.exit()`: éste corta el proceso con stdout a medio escribir
|
|
73
|
+
// cuando la salida va a un pipe, y quien llama leería una línea de JSON partida.
|
|
74
|
+
const { r, json } = await correr(process.argv.slice(2));
|
|
75
|
+
emitir(r, json, (s) => void process.stdout.write(s));
|
|
76
|
+
process.exitCode = r.codigo;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// `supcoflow describir --json`: el contrato entero en una línea (§13), para que un asistente sepa
|
|
2
|
+
// qué comandos hay, con qué flags, qué puede la clave y qué quiere decir cada código, sin leer la
|
|
3
|
+
// ayuda pensada para personas. La ayuda se arma desde acá: una sola fuente, así no se desfasan.
|
|
4
|
+
import { SALIDA } from "../contrato.js";
|
|
5
|
+
// A mano a propósito, como en api.ts: tiene que coincidir con package.json (lo pinza un test).
|
|
6
|
+
const VERSION = "0.1.0";
|
|
7
|
+
export const DESCRIPCION = {
|
|
8
|
+
version: VERSION,
|
|
9
|
+
comandos: [
|
|
10
|
+
{
|
|
11
|
+
nombre: "login",
|
|
12
|
+
resumen: "Inicia sesión aprobando un código desde la consola. Sin terminal interactiva, en dos pasos: --start y después --wait.",
|
|
13
|
+
flags: [
|
|
14
|
+
"--start: pide el código y sale 5 con la URL para aprobarlo",
|
|
15
|
+
"--wait: espera la aprobación hasta 90 s; sale 5 si todavía no llegó (volvé a llamarlo)",
|
|
16
|
+
"--dry-run: dice qué haría sin pedir nada (no vale con --wait)",
|
|
17
|
+
"--json",
|
|
18
|
+
],
|
|
19
|
+
ejemplos: ["supcoflow login", "supcoflow login --start --json", "supcoflow login --wait --json"],
|
|
20
|
+
codigos: [0, 1, 2, 3, 4, 5],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
nombre: "logout",
|
|
24
|
+
resumen: "Revoca la clave en uso y la borra de esta máquina; reintenta las que quedaron por revocar.",
|
|
25
|
+
flags: ["--dry-run: dice qué revocaría sin revocar nada", "--json"],
|
|
26
|
+
ejemplos: ["supcoflow logout", "supcoflow logout --json"],
|
|
27
|
+
codigos: [0, 1, 3],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
nombre: "quien-soy",
|
|
31
|
+
resumen: "Muestra con qué clave anda la terminal, a nombre de quién y en qué equipo.",
|
|
32
|
+
flags: ["--json"],
|
|
33
|
+
ejemplos: ["supcoflow quien-soy", "supcoflow quien-soy --json"],
|
|
34
|
+
codigos: [0, 1, 2, 3],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
nombre: "describir",
|
|
38
|
+
resumen: "Describe comandos, flags, facultades y códigos de salida, para un asistente.",
|
|
39
|
+
flags: ["--json"],
|
|
40
|
+
ejemplos: ["supcoflow describir --json"],
|
|
41
|
+
codigos: [0],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
flags_globales: ["--json: el resultado en una línea de JSON por stdout; los avisos van por stderr", "--help, -h: la ayuda con ejemplos"],
|
|
45
|
+
facultades: [
|
|
46
|
+
{
|
|
47
|
+
nombre: "consultar",
|
|
48
|
+
incluye: "Identidad y equipo, proyectos, configuración, casos, issues, runs, costos, estado de la instalación",
|
|
49
|
+
con_api_key: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
nombre: "configurar",
|
|
53
|
+
incluye: "Crear/reusar proyecto, conectar o desconectar el repo, configurar y prender el chat, sitios permitidos, acuerdo de datos, política de problemas, publicar la guía, crear y revocar claves de consulta, perfiles de los agentes",
|
|
54
|
+
con_api_key: true,
|
|
55
|
+
},
|
|
56
|
+
{ nombre: "probar", incluye: "La prueba hasta el issue (gasta, dentro de los topes)", con_api_key: true },
|
|
57
|
+
{
|
|
58
|
+
nombre: "solo_consola",
|
|
59
|
+
incluye: "Subir el tope de gasto, borrar el proyecto, encender la fábrica, dar o sacar acceso a personas: sólo con sesión en la consola",
|
|
60
|
+
con_api_key: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
nombre: "operador",
|
|
64
|
+
incluye: "Solicitudes, /maquinaria, cerrar runs, la fábrica de operador: nunca por clave, aunque la persona sea operadora",
|
|
65
|
+
con_api_key: false,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
codigos: {
|
|
69
|
+
[SALIDA.ok]: "ok",
|
|
70
|
+
[SALIDA.inesperado]: "inesperado",
|
|
71
|
+
[SALIDA.faltaLogin]: "falta login",
|
|
72
|
+
[SALIDA.entradaInvalida]: "entrada inválida",
|
|
73
|
+
[SALIDA.haceFaltaUnaPersona]: "hace falta una persona",
|
|
74
|
+
[SALIDA.esperandoAUnaPersona]: "esperando a una persona (volvé a llamar)",
|
|
75
|
+
[SALIDA.topeAlcanzado]: "tope alcanzado",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
export function comandoDescribir() {
|
|
79
|
+
return { codigo: SALIDA.ok, json: { ok: true, ...DESCRIPCION }, texto: ayuda(null) };
|
|
80
|
+
}
|
|
81
|
+
function bloque(c) {
|
|
82
|
+
return [
|
|
83
|
+
` ${c.nombre}: ${c.resumen}`,
|
|
84
|
+
...c.flags.map((f) => ` ${f}`),
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
export function ayuda(comando) {
|
|
88
|
+
const elegidos = comando === null ? DESCRIPCION.comandos : DESCRIPCION.comandos.filter((c) => c.nombre === comando);
|
|
89
|
+
const uso = comando === null ? "supcoflow <comando> [flags]" : `supcoflow ${comando} [flags]`;
|
|
90
|
+
const codigos = Object.entries(DESCRIPCION.codigos).map(([n, t]) => ` ${n} ${t}`);
|
|
91
|
+
return [
|
|
92
|
+
`Uso: ${uso}`,
|
|
93
|
+
"",
|
|
94
|
+
comando === null ? "Comandos:" : "Comando:",
|
|
95
|
+
...elegidos.map(bloque),
|
|
96
|
+
"",
|
|
97
|
+
"Flags de todos los comandos:",
|
|
98
|
+
...DESCRIPCION.flags_globales.map((f) => ` ${f}`),
|
|
99
|
+
"",
|
|
100
|
+
"Ejemplos:",
|
|
101
|
+
...elegidos.flatMap((c) => c.ejemplos.map((e) => ` ${e}`)),
|
|
102
|
+
"",
|
|
103
|
+
"Códigos de salida:",
|
|
104
|
+
...codigos,
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|