@cat-indev/catops-cli 0.0.1-alpha.6 → 0.0.1-alpha.7

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
@@ -1,6 +1,15 @@
1
1
  # devops-cli
2
2
 
3
- Framework interno para pipelines DevOps, empaquetado como librería npm instalable en cualquier proyecto.
3
+ Framework para pipelines DevOps, escrito en **TypeScript** (100% usable desde JavaScript puro), empaquetado como librería npm instalable en cualquier proyecto.
4
+
5
+ Trae:
6
+
7
+ - Un **ExecutionContext** compartido (`flags`, `params`, `env`, `vars`, `results`, `logger`, `services`, `notifier`) para que ninguna task tenga que recibir parámetros manualmente.
8
+ - **14 servicios** listos (`shell`, `docker`, `git`, `kubectl`, `helm`, `npm`, `archive`, `terraform`, `ansible`, `argocd`, `tekton`, `oc`, `az`, `azdo`).
9
+ - **Menús interactivos** con navegación anidada y **selección automática por flag** (para correr pipelines sin prompts, ideal para CI).
10
+ - **retry / timeout / dryRun** en cada comando de shell.
11
+ - Validación cíclica de rollouts de Kubernetes/OpenShift (`waitForDeployment`, `waitForDeploymentGroup`).
12
+ - **Callbacks de éxito/error por tarea** + un sistema de **notificaciones clasificadas por área de TI**, con mensajes personalizables y senders (`log`, `file`, `http`, `webhook`, `websocket`).
4
13
 
5
14
  ## Instalación
6
15
 
@@ -16,10 +25,10 @@ npm install @miorg/devops-cli
16
25
 
17
26
  ```bash
18
27
  # Dentro del repo de devops-cli
19
- npm pack # genera devops-cli-0.1.0.tgz
28
+ npm pack # genera devops-cli-<version>.tgz
20
29
 
21
30
  # Dentro del proyecto que lo va a consumir
22
- npm install /ruta/a/devops-cli-0.1.0.tgz
31
+ npm install /ruta/a/devops-cli-<version>.tgz
23
32
  ```
24
33
 
25
34
  **Opción C — enlazado local con `npm link` (para desarrollar la librería y el proyecto que la consume al mismo tiempo):**
@@ -40,7 +49,13 @@ npm install git+https://github.com/tu-org/devops-cli.git
40
49
 
41
50
  Cualquiera de las 4 deja disponibles dos cosas en el proyecto consumidor:
42
51
 
43
- 1. La librería: `const { Context, Menu, services } = require("devops-cli");`
52
+ 1. La librería, tanto desde TS como desde JS puro:
53
+ ```typescript
54
+ import { Context, Menu, services, type MenuDefinition } from "devops-cli";
55
+ ```
56
+ ```javascript
57
+ const { Context, Menu, services } = require("devops-cli");
58
+ ```
44
59
  2. El binario: `npx devops-cli` (o `devops-cli` si lo instalaste global con `-g`).
45
60
 
46
61
  ## Publicar una nueva versión
@@ -50,63 +65,41 @@ npm version patch # o minor / major
50
65
  npm publish # agrega --access public si usas un scope (@miorg/devops-cli)
51
66
  ```
52
67
 
53
- ## Piezas que integra
54
-
55
- Une dos piezas que ya tenías:
68
+ `prepublishOnly` corre el build y los tests automáticamente antes de publicar.
56
69
 
57
- 1. El **ExecutionContext** (estado global: flags, params, env, vars, results, logger, prompt).
58
- 2. Los **servicios de shell** (`shell`, `docker`, `git`, `kubectl`, `helm`, `npm`, `archive`), integrados **tal cual** los diste, sin reescribir su lógica.
70
+ ## TypeScript
59
71
 
60
- La única pieza nueva es el pegamento: `ctx.services` apunta directamente a los módulos de `src/services`, así que cualquier task puede hacer `ctx.services.docker.build(...)` sin recibir nada por parámetro, tal como describías.
61
-
62
- ## Estructura
72
+ Todo `src/` está escrito en TypeScript, con `strict: true`. `npm run build` compila a `dist/` (JS + `.d.ts` + source maps por archivo) eso es lo único que se publica (ver `files` en `package.json`).
63
73
 
64
74
  ```
65
75
  src/
66
76
  core/
67
- Context.js -> ExecutionContext singleton (Context.current())
68
- Menu.js -> Renderer/Menu.render() para navegación jerárquica
69
- prompt.js -> ctx.ask / ctx.confirm / ctx.select (sin dependencias externas)
70
- logger.js -> logger usado por Context y por shell.js
77
+ Context.ts -> ExecutionContext singleton (Context.current() / Context.parseArgv())
78
+ Menu.ts -> Menu.render() con navegación anidada + selección automática por flag
79
+ Notifier.ts -> clasificación de errores por área + canales + senders
80
+ classifiers.ts -> fábricas de ErrorClassifier: byCommand, byPattern
81
+ messages.ts -> fábricas de ErrorMessageFormatter: byPattern, byCommand, byRule
82
+ senders.ts -> fábricas de Sender: log, file, http, webhook, websocket
83
+ prompt.ts -> ctx.ask / ctx.confirm / ctx.select (sin dependencias externas)
84
+ logger.ts -> logger usado por Context y por shell.ts
85
+ types.ts -> tipos compartidos (MenuDefinition, ExecOptions, NotificationEvent, ...)
71
86
  services/
72
- shell.js -> motor base (spawn), con retry/timeout/dryRun
73
- docker.js -> tal cual el original
74
- git.js -> tal cual el original
75
- kubectl.js -> tal cual el original
76
- helm.js -> tal cual el original
77
- npm.js -> tal cual el original
78
- archive.js -> tal cual el original
79
- terraform.js -> init/plan/apply/destroy/output/validate/fmt
80
- ansible.js -> playbook/adhoc/vaultEncrypt/vaultDecrypt/galaxyInstall
81
- argocd.js -> login/appSync/appGet/appWait/appSet/appList/appRollback
82
- tekton.js -> pipelineStart/pipelinerunList/pipelinerunLogs/taskStart/taskrunLogs
83
- oc.js -> login/project/apply/get/rollout/newApp/startBuild/logs
84
- az.js -> loginServicePrincipal/acrBuild/webappDeploy/aksGetCredentials/...
85
- azdo.js -> logging commands de Azure Pipelines (##vso)
86
- index.js -> registra todos los servicios anteriores
87
- index.js -> exporta { Context, Menu, logger, prompt, services }
87
+ shell.ts -> motor base (spawn), con retry/timeout/dryRun
88
+ docker.ts, git.ts, kubectl.ts, helm.ts, npm.ts, archive.ts
89
+ terraform.ts, ansible.ts, argocd.ts, tekton.ts, oc.ts, az.ts, azdo.ts
90
+ index.ts -> registra todos los servicios anteriores (ServicesRegistry)
91
+ index.ts -> entry point público: Context, Menu, Notifier, senders, classifiers, messages, services, tipos
92
+ bin/
93
+ devops-cli.ts -> CLI ejecutable (busca devops.pipeline.js en el proyecto consumidor)
88
94
  examples/
89
- pipeline-example.js -> pipeline + menú de ejemplo
95
+ pipeline-example.js -> pipeline + menú + notificaciones de ejemplo, corre contra dist/
90
96
  test/
91
- context.test.js -> Context, flags, vars, parseArgv, dryRun global
92
- shell.test.js -> retry, timeout, dryRun del motor shell.exec
93
- services.test.js -> verifica que docker/terraform/argocd arman bien los args
94
- ```
95
-
96
- ## Qué se corrigió para que "convivan"
97
-
98
- - `shell.js` usaba `logger.info(...)` / `logger.error(...)` sin importarlo. Se agregó `const logger = require("../core/logger")`.
99
- - `Context.js` hacía `this.logger = logger` sin importar `logger` tampoco. Ahora importa `./logger`.
100
- - Se agregó `this.services = services` dentro del constructor de `Context`, cableando exactamente el "Registry" que proponías al final de tu mensaje:
101
-
102
- ```javascript
103
- ctx.services.git.clone(...)
104
- ctx.services.docker.build(...)
97
+ context.test.js, shell.test.js, services.test.js, menu-selector.test.js,
98
+ notifier.test.js, senders.test.js, hooks-integration.test.js,
99
+ kubectl.test.js, oc.test.js, deployment-group.test.js
105
100
  ```
106
101
 
107
- - `Context.instance` pasó de crearse en la definición de la clase a crearse de forma perezosa en `Context.current()`, para evitar problemas de orden de carga con los `require` circulares entre `Context` → `services` → `shell` → `logger`.
108
-
109
- ## Uso dentro de un proyecto que lo instaló
102
+ ## Uso rápido: menú con `devops.pipeline.js` + el bin
110
103
 
111
104
  Crea un `devops.pipeline.js` (o `devops.config.js` / `.devops-cli.js`) en la raíz de tu proyecto:
112
105
 
@@ -116,34 +109,53 @@ module.exports = (ctx) => ({
116
109
  title: "Pipeline",
117
110
  options: {
118
111
  Build: async () => {
119
- await ctx.services.docker.build({
120
- image: "registry/app:v1",
121
- dockerfile: "Dockerfile"
122
- });
112
+ await ctx.services.docker.build({ image: "registry/app:v1", dockerfile: "Dockerfile" });
123
113
  },
124
114
  Deploy: async () => {
125
- await ctx.services.kubectl.apply("deployment.yaml");
115
+ await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
126
116
  }
127
117
  }
128
118
  });
129
119
  ```
130
120
 
131
- Y ejecuta:
132
-
133
121
  ```bash
134
122
  npx devops-cli --debug --env=prod
135
123
  ```
136
124
 
137
125
  `devops-cli` detecta el archivo, arma el `Context` a partir de los flags/params de `argv`, y renderiza el menú.
138
126
 
139
- ## Uso como librería (sin el menú interactivo)
127
+ ## Uso directo en tu propio script (p. ej. con `tsx`)
128
+
129
+ ```typescript
130
+ // src/index.ts
131
+ import { Context, Menu, type MenuDefinition } from "devops-cli";
132
+
133
+ const ctx = Context.parseArgv();
134
+
135
+ const mainMenu: MenuDefinition = {
136
+ title: "Pipeline",
137
+ "flag-selector": "--menu-selector",
138
+ options: {
139
+ Build: { selector: "build", action: () => ctx.services.docker.build({ image: "app:v1" }) }
140
+ }
141
+ };
142
+
143
+ Menu.render(mainMenu, ctx);
144
+ ```
145
+
146
+ ```json
147
+ { "scripts": { "dev": "tsx src/index.ts" } }
148
+ ```
140
149
 
141
150
  ```bash
142
- npm run example -- --debug --env=prod
151
+ npx tsx src/index.ts --menu-selector=build
152
+ npm run dev -- --menu-selector=build # con npm hace falta el "--" para reenviar flags
143
153
  ```
144
154
 
155
+ ## Uso como librería sin menú (pipeline lineal)
156
+
145
157
  ```javascript
146
- const { Context, Menu } = require("./src");
158
+ const { Context } = require("devops-cli"); // o require("./dist") dentro de este repo
147
159
 
148
160
  const ctx = Context.parseArgv(); // llena flags/params desde argv
149
161
 
@@ -153,83 +165,74 @@ await ctx.services.git.checkout("develop");
153
165
  await ctx.services.npm.ci();
154
166
  await ctx.services.docker.build({ image: ctx.get("image"), dockerfile: "Dockerfile" });
155
167
  await ctx.services.docker.push(ctx.get("image"));
156
- await ctx.services.kubectl.apply("deployment.yaml");
168
+ await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
157
169
  ```
158
170
 
159
- O con menús interactivos anidados:
171
+ ## Menús: definición, anidamiento y selectores automáticos por flag
160
172
 
161
- ```javascript
162
- await Menu.render({
163
- title: "Deploy",
164
- options: {
165
- Build: buildTask,
166
- Docker: dockerMenu, // submenú anidado
167
- Publish: publishTask
168
- }
169
- });
170
- ```
173
+ Un `MenuDefinition` es `{ title, options }`, donde cada entrada de `options` puede ser:
171
174
 
172
- ## retry / timeout / dryRun en shell.exec
175
+ - una **función** task directa: `Build: () => {...}`
176
+ - otro **`MenuDefinition`** — submenú directo: `Docker: dockerMenu`
177
+ - un **objeto largo** — para poder darle `selector`, `onSuccess`/`onError`, o envolver un submenú:
178
+ ```javascript
179
+ Build: { selector: "build", action: () => {...}, onSuccess: (r, ctx) => {...}, onError: (e, ctx) => {...} }
180
+ Docker: { selector: "docker", menu: dockerMenu }
181
+ // también podés inlinear el submenú directo con su propio selector al lado:
182
+ Docker: { selector: "docker", title: "Docker", "flag-selector": "--docker-action", options: {...} }
183
+ ```
173
184
 
174
- `shell.exec(command, ...args)` sigue aceptando exactamente los mismos argumentos que antes (por eso `docker.js`, `git.js`, etc. no necesitaron cambiar). Ahora, si el último argumento es un objeto plano, se interpreta como opciones **solo para esa llamada**:
185
+ ### Selección automática por flag
175
186
 
176
- ```javascript
177
- await ctx.services.docker.push(image); // igual que siempre
187
+ Cualquier `MenuDefinition` puede declarar `"flag-selector": "--algun-flag"`. Si el `Context` trae un param que matchea el `selector` de alguno de sus items, esa opción se ejecuta **automáticamente, sin ningún prompt**:
178
188
 
179
- await ctx.services.shell.exec("curl", "https://flaky-api.internal", {
180
- retry: 3, // reintentos totales (default: 1 = sin retry)
181
- retryDelay: 1000,// ms entre reintentos
182
- timeout: 5000, // ms antes de matar el proceso con SIGTERM
183
- dryRun: true // solo loguea el comando, no lo ejecuta
184
- });
185
- ```
189
+ ```javascript
190
+ const dockerMenu = {
191
+ title: "Docker",
192
+ "flag-selector": "--docker-action",
193
+ options: {
194
+ Build: { selector: "build", action: buildTask },
195
+ Push: { selector: "push", action: pushTask }
196
+ }
197
+ };
186
198
 
187
- También puedes fijar defaults globales para todo el proceso:
199
+ const mainMenu = {
200
+ title: "Pipeline",
201
+ "flag-selector": "--menu-selector",
202
+ options: {
203
+ Docker: { selector: "docker", menu: dockerMenu },
204
+ Deploy: { selector: "deploy", action: deployTask }
205
+ }
206
+ };
188
207
 
189
- ```javascript
190
- ctx.services.shell.configure({ retry: 3, timeout: 30000 });
208
+ Menu.render(mainMenu, ctx);
191
209
  ```
192
210
 
193
- Y `Context.parseArgv()` ya conecta flags de línea de comandos automáticamente:
194
-
195
211
  ```bash
196
- npx devops-cli --dry-run # activa dryRun global
197
- npx devops-cli --retry=3 --timeout=15000
198
- ```
212
+ # encadena ambos niveles en un solo comando, sin ningún prompt interactivo:
213
+ devops-cli --menu-selector=docker --docker-action=build
199
214
 
200
- ## Logging commands de Azure Pipelines (`ctx.services.azdo`)
215
+ # un solo nivel:
216
+ devops-cli --menu-selector=deploy
201
217
 
202
- ```javascript
203
- ctx.services.azdo.setVariable("BUILD_TAG", "v1.2.3");
204
- ctx.services.azdo.logWarning("El caché de npm no se encontró, se reconstruye desde cero.");
205
- ctx.services.azdo.group("Build");
206
- // ... pasos ...
207
- ctx.services.azdo.endGroup();
218
+ # sin flags -> menú interactivo normal
219
+ devops-cli
208
220
  ```
209
221
 
210
- ## Servicios de infraestructura ya integrados
211
-
212
- ```javascript
213
- await ctx.services.terraform.plan({ varFile: "prod.tfvars" });
214
- await ctx.services.terraform.apply();
215
-
216
- await ctx.services.ansible.playbook("site.yml", { inventory: "hosts.ini" });
217
-
218
- await ctx.services.argocd.appSync("mi-app", { prune: true });
219
-
220
- await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
222
+ Si el valor del flag no matchea ningún `selector` del nivel actual, cae de vuelta al menú interactivo (con un warning), en vez de fallar en seco. Cada submenú revisa su **propio** `flag-selector` de forma independiente, así que podés automatizar tantos niveles como quieras encadenando flags.
221
223
 
222
- await ctx.services.oc.login({ server: "https://api.cluster:6443", token: process.env.OC_TOKEN });
223
- await ctx.services.oc.rollout("mi-app");
224
+ ### Callbacks de éxito/error por item
224
225
 
225
- await ctx.services.az.acrBuild({ registry: "miregistro", image: "app:v1" });
226
+ ```javascript
227
+ Deploy: {
228
+ selector: "deploy",
229
+ action: () => ctx.services.kubectl.apply("deployment.yaml"),
230
+ onSuccess: (result, ctx) => ctx.logger.success("Deploy OK"),
231
+ onError: (error, ctx) => ctx.logger.error(`Deploy falló: ${error.message}`)
232
+ }
226
233
  ```
227
234
 
228
- ## Callbacks de éxito/error por tarea + notificaciones por área de TI
229
-
230
- Cada tarea (`ctx.run()` o un item de menú) puede llevar sus propios callbacks, y además reporta automáticamente al `notifier` global del `Context`.
231
-
232
- ### Callbacks por tarea
235
+ Mismo patrón con `ctx.run()` fuera de un menú:
233
236
 
234
237
  ```javascript
235
238
  await ctx.run(
@@ -242,87 +245,54 @@ await ctx.run(
242
245
  );
243
246
  ```
244
247
 
245
- En un menú, los mismos campos van directo en el item:
246
-
247
- ```javascript
248
- options: {
249
- Deploy: {
250
- selector: "deploy",
251
- action: () => ctx.services.kubectl.apply("deployment.yaml"),
252
- onSuccess: (result, ctx) => {...},
253
- onError: (error, ctx) => {...}
254
- }
255
- }
256
- ```
248
+ En ambos casos, además de tus callbacks, el resultado se reporta automáticamente al `ctx.notifier` (ver más abajo) — no hay que llamarlo a mano.
257
249
 
258
- ### Clasificar el error por área de TI y enrutarlo a canales
250
+ ## retry / timeout / dryRun en shell.exec
259
251
 
260
- `ctx.notifier` clasifica cada error (con la función que le des) y lo manda a los `senders` que hayas registrado para esa área:
252
+ `shell.exec(command, ...args)` sigue aceptando exactamente los mismos argumentos de siempre. Si el último argumento es un objeto plano, se interpreta como opciones **solo para esa llamada**:
261
253
 
262
254
  ```javascript
263
- const { classifiers, senders } = require("devops-cli");
255
+ await ctx.services.docker.push(image); // igual que siempre
264
256
 
265
- // 1. ¿A qué área de TI pertenece este error?
266
- ctx.notifier.classify(classifiers.byCommand({
267
- docker: "containers",
268
- kubectl: "kubernetes",
269
- oc: "kubernetes",
270
- terraform: "infra",
271
- ansible: "infra",
272
- git: "scm",
273
- argocd: "cd-pipeline",
274
- tkn: "cd-pipeline",
275
- az: "cloud-azure"
276
- }));
257
+ await ctx.services.shell.exec("curl", "https://flaky-api.internal", {
258
+ retry: 3, // reintentos totales (default: 1 = sin retry)
259
+ retryDelay: 1000, // ms entre reintentos
260
+ timeout: 5000, // ms antes de matar el proceso con SIGTERM
261
+ dryRun: true // solo loguea el comando, no lo ejecuta
262
+ });
263
+ ```
277
264
 
278
- // también puedes clasificar por el texto del error:
279
- ctx.notifier.classify(classifiers.byPattern([
280
- [/permission denied|unauthorized/i, "security"],
281
- [/timeout|ECONNREFUSED/i, "networking"],
282
- [/no space left|ENOSPC/i, "infra"]
283
- ]));
265
+ Defaults globales para todo el proceso:
284
266
 
285
- // 2. ¿A dónde se manda cada área?
286
- ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.SLACK_K8S_WEBHOOK }));
287
- ctx.notifier.channel("security", senders.http({ url: "https://security.miempresa.com/incidents" }));
288
- ctx.notifier.channel("*", senders.file({ path: "./devops-cli-errors.log" })); // TODO error, sin importar el área
289
-
290
- // 3. (opcional) éxito, sin clasificación de área
291
- ctx.notifier.onSuccess(senders.log());
267
+ ```javascript
268
+ ctx.services.shell.configure({ retry: 3, timeout: 30000 });
292
269
  ```
293
270
 
294
- Tambien se pueden incluir parsers de error para convertir y usar errores amigables con las areas receptoras
271
+ `Context.parseArgv()` ya conecta flags de línea de comandos automáticamente:
295
272
 
273
+ ```bash
274
+ npx devops-cli --dry-run # activa dryRun global
275
+ npx devops-cli --retry=3 --timeout=15000
296
276
  ```
297
- const { classifiers, messages } = require("devops-cli");
298
-
299
- ctx.notifier.classify(classifiers.byCommand({ docker: "containers" }));
300
277
 
301
- ctx.notifier.describeError(messages.byPattern([
302
- [/500 Internal Server Error/, "Se ha reportado a infraestructura: falta de espacio en el registry"],
303
- [/unauthorized|403/i, "Credenciales inválidas contra el registry, revisa el secret"]
304
- ]));
278
+ ## Servicios de infraestructura incluidos
305
279
 
306
- ctx.notifier.channel("containers", senders.webhook({ url: TEAMS_WEBHOOK }));
307
- ```
280
+ ```javascript
281
+ await ctx.services.terraform.plan({ varFile: "prod.tfvars" });
282
+ await ctx.services.terraform.apply();
308
283
 
309
- A partir de aquí, cualquier `ctx.run(...)` o item de menú con `action` reporta automáticamente al notifier — no hay que llamarlo a mano en cada task.
284
+ await ctx.services.ansible.playbook("site.yml", { inventory: "hosts.ini" });
310
285
 
311
- ### Senders incluidos
286
+ await ctx.services.argocd.appSync("mi-app", { prune: true });
312
287
 
313
- | Sender | Uso |
314
- |---|---|
315
- | `senders.log()` | Usa el logger interno (consola) |
316
- | `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
317
- | `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
318
- | `senders.webhook({ url, format? })` | Como `http`, pero formatea `{ text: "❌ ..." }` por defecto (Slack/Teams/Discord-friendly) |
319
- | `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
288
+ await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
320
289
 
321
- Puedes escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
290
+ await ctx.services.az.acrBuild({ registry: "miregistro", image: "app:v1" });
291
+ ```
322
292
 
323
293
  ## kubectl / oc: kubeconfig, namespace y espera cíclica del rollout
324
294
 
325
- `kubectl` y `oc` ahora aceptan `{ kubeconfig, namespace }` como último argumento en **todos** sus comandos (compatible con las llamadas de antes, que siguen funcionando sin ese argumento):
295
+ `kubectl` y `oc` aceptan `{ kubeconfig, namespace }` como último argumento en **todos** sus comandos (retrocompatible, sigue funcionando sin ese argumento):
326
296
 
327
297
  ```javascript
328
298
  await ctx.services.kubectl.apply("deploy.yaml", { kubeconfig: "/etc/kube/prod.yaml", namespace: "prod" });
@@ -334,21 +304,16 @@ await ctx.services.oc.apply("deploy.yaml", { namespace: "prod" });
334
304
 
335
305
  ### `waitForDeployment` — validación cíclica del rollout
336
306
 
337
- Sondea el Deployment (o `DeploymentConfig` con `oc`) hasta que:
307
+ Sondea el Deployment (o `DeploymentConfig` con `oc` + `resourceType: "dc"`) hasta que:
338
308
 
339
309
  - llega a **estado exitoso** (réplicas listas/actualizadas == deseadas) → resuelve con `{ status: "success", ... }`,
340
310
  - se queda en **estado Failed** más de `failedGracePeriod` sin recuperarse → lanza `DeploymentRolloutError`,
341
311
  - supera **`maxRestarts`** reinicios acumulados entre todos sus pods → lanza `DeploymentRolloutError` de inmediato, sin esperar el grace period,
342
312
  - o se cumple el **`timeout`** global sin éxito → lanza `DeploymentRolloutError`.
343
313
 
344
- En los tres casos de fallo, el polling se detiene ("se mata el proceso") y el error se re-lanza — listo para que `ctx.run(...)` lo capture y lo reporte automáticamente vía `ctx.notifier` (el error ya trae `command: "kubectl"` / `command: "oc"`, así que `classifiers.byCommand({ kubectl: "kubernetes" })` lo clasifica sin configuración extra).
314
+ En los tres casos de fallo, el polling se detiene y el error se re-lanza — listo para que `ctx.run(...)` lo capture y lo reporte automáticamente vía `ctx.notifier` (el error ya trae `command: "kubectl"` / `command: "oc"`, así que `classifiers.byCommand({ kubectl: "kubernetes" })` lo clasifica sin configuración extra).
345
315
 
346
316
  ```javascript
347
- const { classifiers } = require("devops-cli");
348
-
349
- ctx.notifier.classify(classifiers.byCommand({ kubectl: "kubernetes", oc: "kubernetes" }));
350
- ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.SLACK_K8S_WEBHOOK }));
351
-
352
317
  await ctx.run("deploy-api", async () => {
353
318
  await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
354
319
 
@@ -363,11 +328,9 @@ await ctx.run("deploy-api", async () => {
363
328
  });
364
329
  ```
365
330
 
366
- Con `oc`, usa `resourceType: "dc"` para apuntar a un `DeploymentConfig` clásico de OpenShift en vez de un `Deployment` nativo (default: `"deployment"`).
367
-
368
331
  ### `waitForDeploymentGroup` — validar todas las instancias de un mismo despliegue GitOps
369
332
 
370
- Pensada para el caso de GitOps donde un mismo repo termina desplegado como **varios Deployments** (una instancia por región/config/cliente, etc.), todos marcados con un label común, por ejemplo:
333
+ Pensada para el caso de GitOps donde un mismo repo termina desplegado como **varios Deployments** (una instancia por región/config/cliente, etc.), todos marcados con un label común:
371
334
 
372
335
  ```yaml
373
336
  metadata:
@@ -375,7 +338,7 @@ metadata:
375
338
  deployment-group: repository-14
376
339
  ```
377
340
 
378
- `waitForDeploymentGroup` descubre todas las instancias que compartan ese label y corre `waitForDeployment` sobre **cada una en paralelo**, con el mismo `timeout`/`pollInterval`/`failedGracePeriod`/`maxRestarts` para todas:
341
+ Descubre todas las instancias que compartan ese label y corre `waitForDeployment` sobre **cada una en paralelo**, con el mismo `timeout`/`pollInterval`/`failedGracePeriod`/`maxRestarts` para todas:
379
342
 
380
343
  ```javascript
381
344
  await ctx.run("deploy-repo-14", () =>
@@ -391,10 +354,107 @@ await ctx.run("deploy-repo-14", () =>
391
354
  ```
392
355
 
393
356
  - Si **todas** llegan a estado exitoso → resuelve con `{ status: "success", deployments: [...] }` (el detalle de cada una).
394
- - Si **alguna falla** (timeout individual, Failed sin recuperarse, o maxRestarts) → espera a que las demás terminen, y lanza `DeploymentGroupRolloutError` con `succeeded` (nombres que sí llegaron) y `failed` (nombres + motivo de cada una que no llegó).
357
+ - Si **alguna falla** → espera a que las demás terminen, y lanza `DeploymentGroupRolloutError` con `succeeded` (nombres que sí llegaron) y `failed` (nombre + status + mensaje de cada una que no).
395
358
  - Si el label **no matchea ningún deployment**, también lanza `DeploymentGroupRolloutError` (grupo vacío = error, no éxito silencioso).
396
359
 
397
- Igual que con `waitForDeployment`, el error lleva `command: "kubectl"` / `command: "oc"`, así que se clasifica solo con `classifiers.byCommand(...)` si usas el `notifier`. Con `oc`, también acepta `resourceType: "dc"` para agrupar `DeploymentConfig`s.
360
+ Con `oc`, ambas funciones aceptan `resourceType: "dc"` para apuntar a `DeploymentConfig` clásico en vez de `Deployment` nativo (default: `"deployment"`).
361
+
362
+ ## Logging commands de Azure Pipelines (`ctx.services.azdo`)
363
+
364
+ ```javascript
365
+ ctx.services.azdo.setVariable("BUILD_TAG", "v1.2.3");
366
+ ctx.services.azdo.logWarning("El caché de npm no se encontró, se reconstruye desde cero.");
367
+ ctx.services.azdo.group("Build");
368
+ // ... pasos ...
369
+ ctx.services.azdo.endGroup();
370
+ ```
371
+
372
+ ## Notificaciones: clasificar errores por área de TI, personalizar el mensaje, y enviarlos
373
+
374
+ `ctx.notifier` tiene tres responsabilidades independientes:
375
+
376
+ 1. **`classify()`** — decide a qué **área de TI** pertenece un error (para elegir a qué canal mandarlo).
377
+ 2. **`describeError()`** — decide el **mensaje** a reportar (reemplaza el stderr crudo por algo humano).
378
+ 3. **`channel()`** / **`onSuccess()`** — a qué **senders** se manda cada área.
379
+
380
+ ```javascript
381
+ const { classifiers, messages, senders } = require("devops-cli");
382
+
383
+ // 1. ¿A qué área de TI pertenece este error?
384
+ ctx.notifier.classify(classifiers.byCommand({
385
+ docker: "containers",
386
+ kubectl: "kubernetes",
387
+ oc: "kubernetes",
388
+ terraform: "infra",
389
+ ansible: "infra",
390
+ git: "scm",
391
+ argocd: "cd-pipeline",
392
+ tkn: "cd-pipeline",
393
+ az: "cloud-azure"
394
+ }));
395
+
396
+ // también podés clasificar por el texto del error:
397
+ ctx.notifier.classify(classifiers.byPattern([
398
+ [/permission denied|unauthorized/i, "security"],
399
+ [/timeout|ECONNREFUSED/i, "networking"],
400
+ [/no space left|ENOSPC/i, "infra"]
401
+ ]));
402
+
403
+ // 2. ¿qué mensaje se reporta? (opcional — sin esto, se usa el stderr crudo)
404
+ ctx.notifier.describeError(messages.byRule([
405
+ {
406
+ command: "docker", args: "push", pattern: /500 Internal Server Error/,
407
+ message: "Se ha reportado a infraestructura: falta de espacio en el registry"
408
+ },
409
+ {
410
+ command: "kubectl", pattern: /500/,
411
+ message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
412
+ },
413
+ {
414
+ command: "terraform", pattern: /500/,
415
+ message: (error, ctx) => `Backend remoto de Terraform no respondió (env: ${ctx.params.env ?? "?"})`
416
+ }
417
+ ]));
418
+
419
+ // 3. ¿a dónde se manda cada área?
420
+ ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.TEAMS_WEBHOOK }));
421
+ ctx.notifier.channel("security", senders.http({ url: "https://security.miempresa.com/incidents" }));
422
+ ctx.notifier.channel("*", senders.file({ path: "./devops-cli-errors.log" })); // TODO error, sin importar el área
423
+
424
+ // (opcional) éxito, sin clasificación de área
425
+ ctx.notifier.onSuccess(senders.log());
426
+ ```
427
+
428
+ A partir de aquí, cualquier `ctx.run(...)` o item de menú con `action` reporta automáticamente al notifier — no hay que llamarlo a mano en cada task.
429
+
430
+ ### Clasificadores de área (`classifiers`)
431
+
432
+ | Fábrica | Uso |
433
+ |---|---|
434
+ | `classifiers.byCommand({ docker: "containers", ... })` | Mapea el comando que falló (adjunto automáticamente por `shell.exec`) a un área |
435
+ | `classifiers.byPattern([[regex, area], ...])` | Matchea contra el `stderr`/mensaje del error |
436
+
437
+ ### Formateadores de mensaje (`messages`)
438
+
439
+ | Fábrica | Uso |
440
+ |---|---|
441
+ | `messages.byPattern([[regex, mensaje], ...])` | Mismo mensaje sin importar el comando — solo mira el texto del error |
442
+ | `messages.byCommand({ docker: "mensaje fijo" })` | Mensaje fijo por comando, sin importar el detalle del error |
443
+ | `messages.byRule([{ command?, args?, pattern?, message }, ...])` | **La opción avanzada**: combina comando + sub-comando (`args`, distingue `docker push` de `docker build`) + patrón de texto, todo en modo AND. `message` puede ser un string fijo o una función `(error, ctx) => string`. Resuelve el caso de "el mismo 500 puede venir de docker, kubectl o terraform, y cada uno necesita su propio mensaje". |
444
+
445
+ Si ningún classifier/formatter matchea, se usa el área `"unclassified"` y el mensaje crudo del error, respectivamente — nada se rompe si no configurás nada de esto.
446
+
447
+ ### Senders incluidos
448
+
449
+ | Sender | Uso |
450
+ |---|---|
451
+ | `senders.log()` | Usa el logger interno (consola) |
452
+ | `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
453
+ | `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
454
+ | `senders.webhook({ url, format? })` | Como `http`, pero formatea `{ text: "❌ ..." }` por defecto — compatible con Slack/Discord y con **Microsoft Teams** vía Workflows (Power Automate), pasando un `format` que arme el payload de Adaptive Card que Teams espera |
455
+ | `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
456
+
457
+ Podés escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
398
458
 
399
459
  ## Tests
400
460
 
@@ -402,11 +462,21 @@ Igual que con `waitForDeployment`, el error lleva `command: "kubectl"` / `comman
402
462
  npm test
403
463
  ```
404
464
 
405
- Corre sobre `node:test` (sin dependencias externas): valida el `Context`, el motor `shell.exec` (retry/timeout/dryRun) y que los servicios armen los comandos correctos, interceptando `shell.exec` en vez de ejecutar binarios reales.
465
+ `pretest` corre el build automáticamente, así los tests validan el `dist/` real que se publica (no el código fuente). Usa `node:test`, sin dependencias externas interceptando `shell.exec` en vez de ejecutar binarios reales:
466
+
467
+ - `context.test.js` — Context, flags, vars, parseArgv, dryRun global
468
+ - `shell.test.js` — retry, timeout, dryRun del motor shell.exec
469
+ - `services.test.js` — que docker/terraform/argocd arman bien sus argumentos
470
+ - `menu-selector.test.js` — selección automática por flag, en cascada de varios niveles
471
+ - `hooks-integration.test.js` — ctx.run() y items de menú con onSuccess/onError
472
+ - `notifier.test.js` — classify/channel/onSuccess/describeError/byRule
473
+ - `senders.test.js` — file/http/webhook/log contra servidores reales en localhost
474
+ - `kubectl.test.js`, `oc.test.js` — kubeconfig/namespace, waitForDeployment (éxito, timeout, Failed, maxRestarts)
475
+ - `deployment-group.test.js` — waitForDeploymentGroup (éxito total, fallo parcial, label sin matches, oc con `dc`)
406
476
 
407
477
  ## Siguientes pasos posibles
408
478
 
409
479
  - Publicar en un registro privado (Verdaccio/Artifactory/GitHub Packages) para instalarlo con scope, p.ej. `@miorg/devops-cli`.
410
- - Agregar tipos (`.d.ts`) si el equipo usa TypeScript.
411
- - Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón.
480
+ - Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón que `terraform.ts`/`docker.ts`.
412
481
  - CI propio (GitHub Actions/Azure Pipelines) que corra `npm test` en cada PR antes de `npm publish`.
482
+ - `--catch=throw` (o similar) para que un item de menú fallido mate el proceso completo en vez de solo loguear y seguir — útil corriendo vía `--menu-selector` dentro de un step de Azure Pipelines.
@@ -1,9 +1,15 @@
1
+ import type { Context } from "./Context";
1
2
  import type { ErrorMessageFormatter } from "./types";
2
3
  /**
3
4
  * Formateador de fábrica: prueba una lista de [regex, mensaje] contra el
4
5
  * stderr/mensaje del error y devuelve el mensaje personalizado del primer
5
6
  * patrón que matchee, en vez del stderr crudo.
6
7
  *
8
+ * Ojo: esto NO distingue por comando — un mismo patrón (ej. "500 Internal
9
+ * Server Error") aplica igual venga de `docker`, `kubectl` o `terraform`.
10
+ * Si necesitas distinguir por comando (y opcionalmente por sub-comando),
11
+ * usa `byRule` más abajo.
12
+ *
7
13
  * Ejemplo:
8
14
  * notifier.describeError(messages.byPattern([
9
15
  * [/500 Internal Server Error/, "Se reportó a infraestructura: falta de espacio en el registry"],
@@ -24,3 +30,60 @@ export declare function byPattern(rules: Array<[RegExp, string]>): ErrorMessageF
24
30
  * }));
25
31
  */
26
32
  export declare function byCommand(map: Record<string, string>): ErrorMessageFormatter;
33
+ /**
34
+ * Una regla de `byRule`: TODAS las condiciones que definas deben cumplirse
35
+ * (AND) para que aplique. Omitir una condición equivale a "no filtrar por
36
+ * eso" (matchea cualquier valor).
37
+ */
38
+ export interface MessageRule {
39
+ /**
40
+ * Comando exacto (o lista de comandos) que debe haber fallado, tal como
41
+ * lo ve `shell.exec` (el primer argumento: "docker", "kubectl", "oc",
42
+ * "terraform", "git", etc.). Sin esto, la regla no filtra por comando.
43
+ */
44
+ command?: string | string[];
45
+ /**
46
+ * Sub-comando / argumento que debe estar presente, por ejemplo "push"
47
+ * para distinguir `docker push` de `docker build`. Puede ser un string
48
+ * exacto (se busca entre los args, o como substring de los args unidos)
49
+ * o un RegExp contra los args unidos con espacios.
50
+ */
51
+ args?: string | RegExp;
52
+ /** Patrón contra el stderr/mensaje del error. Sin esto, no filtra por texto. */
53
+ pattern?: RegExp;
54
+ /** Mensaje final. Puede ser un string fijo o una función que lo arma dinámicamente. */
55
+ message: string | ((error: unknown, ctx: Context) => string);
56
+ }
57
+ /**
58
+ * Formateador de fábrica avanzado: combina comando + sub-comando/args +
59
+ * patrón de texto (todo opcional, en modo AND) para distinguir el MISMO
60
+ * error de red/HTTP según de dónde vino. Resuelve justo el caso de "un 500
61
+ * puede venir de docker push, de kubectl, o de terraform, y cada uno
62
+ * necesita su propio mensaje".
63
+ *
64
+ * Se prueban las reglas en orden; gana la primera que matchee todas sus
65
+ * condiciones. Una regla sin `command` ni `args` ni `pattern` matchea
66
+ * cualquier error (útil como catch-all al final de la lista).
67
+ *
68
+ * Ejemplo — el caso concreto de varios comandos devolviendo el mismo 500:
69
+ *
70
+ * notifier.describeError(messages.byRule([
71
+ * {
72
+ * command: "docker", args: "push", pattern: /500 Internal Server Error/,
73
+ * message: "Se ha reportado a infraestructura: falta de espacio en el registry"
74
+ * },
75
+ * {
76
+ * command: "kubectl", pattern: /500/,
77
+ * message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
78
+ * },
79
+ * {
80
+ * command: "terraform", pattern: /500/,
81
+ * message: (error, ctx) => `El backend remoto de Terraform State no respondió (env: ${ctx.params.env ?? "?"})`
82
+ * },
83
+ * {
84
+ * pattern: /500 Internal Server Error/,
85
+ * message: "Error 500 no clasificado por comando, revisar logs crudos"
86
+ * }
87
+ * ]));
88
+ */
89
+ export declare function byRule(rules: MessageRule[]): ErrorMessageFormatter;
@@ -2,11 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.byPattern = byPattern;
4
4
  exports.byCommand = byCommand;
5
+ exports.byRule = byRule;
6
+ function textOf(error) {
7
+ const execError = error;
8
+ return execError?.stderr || execError?.message || String(error);
9
+ }
5
10
  /**
6
11
  * Formateador de fábrica: prueba una lista de [regex, mensaje] contra el
7
12
  * stderr/mensaje del error y devuelve el mensaje personalizado del primer
8
13
  * patrón que matchee, en vez del stderr crudo.
9
14
  *
15
+ * Ojo: esto NO distingue por comando — un mismo patrón (ej. "500 Internal
16
+ * Server Error") aplica igual venga de `docker`, `kubectl` o `terraform`.
17
+ * Si necesitas distinguir por comando (y opcionalmente por sub-comando),
18
+ * usa `byRule` más abajo.
19
+ *
10
20
  * Ejemplo:
11
21
  * notifier.describeError(messages.byPattern([
12
22
  * [/500 Internal Server Error/, "Se reportó a infraestructura: falta de espacio en el registry"],
@@ -16,8 +26,7 @@ exports.byCommand = byCommand;
16
26
  */
17
27
  function byPattern(rules) {
18
28
  return (error) => {
19
- const execError = error;
20
- const text = execError?.stderr || execError?.message || String(error);
29
+ const text = textOf(error);
21
30
  for (const [pattern, message] of rules) {
22
31
  if (pattern.test(text)) {
23
32
  return message;
@@ -45,4 +54,77 @@ function byCommand(map) {
45
54
  return map[command];
46
55
  };
47
56
  }
57
+ function commandMatches(rule, command) {
58
+ if (!rule.command)
59
+ return true;
60
+ if (!command)
61
+ return false;
62
+ const candidates = Array.isArray(rule.command) ? rule.command : [rule.command];
63
+ return candidates.includes(command);
64
+ }
65
+ function argsMatch(rule, args) {
66
+ if (!rule.args)
67
+ return true;
68
+ if (!args || !args.length)
69
+ return false;
70
+ const joined = args.join(" ");
71
+ if (rule.args instanceof RegExp) {
72
+ return rule.args.test(joined);
73
+ }
74
+ return args.includes(rule.args) || joined.includes(rule.args);
75
+ }
76
+ function patternMatches(rule, text) {
77
+ if (!rule.pattern)
78
+ return true;
79
+ return rule.pattern.test(text);
80
+ }
81
+ /**
82
+ * Formateador de fábrica avanzado: combina comando + sub-comando/args +
83
+ * patrón de texto (todo opcional, en modo AND) para distinguir el MISMO
84
+ * error de red/HTTP según de dónde vino. Resuelve justo el caso de "un 500
85
+ * puede venir de docker push, de kubectl, o de terraform, y cada uno
86
+ * necesita su propio mensaje".
87
+ *
88
+ * Se prueban las reglas en orden; gana la primera que matchee todas sus
89
+ * condiciones. Una regla sin `command` ni `args` ni `pattern` matchea
90
+ * cualquier error (útil como catch-all al final de la lista).
91
+ *
92
+ * Ejemplo — el caso concreto de varios comandos devolviendo el mismo 500:
93
+ *
94
+ * notifier.describeError(messages.byRule([
95
+ * {
96
+ * command: "docker", args: "push", pattern: /500 Internal Server Error/,
97
+ * message: "Se ha reportado a infraestructura: falta de espacio en el registry"
98
+ * },
99
+ * {
100
+ * command: "kubectl", pattern: /500/,
101
+ * message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
102
+ * },
103
+ * {
104
+ * command: "terraform", pattern: /500/,
105
+ * message: (error, ctx) => `El backend remoto de Terraform State no respondió (env: ${ctx.params.env ?? "?"})`
106
+ * },
107
+ * {
108
+ * pattern: /500 Internal Server Error/,
109
+ * message: "Error 500 no clasificado por comando, revisar logs crudos"
110
+ * }
111
+ * ]));
112
+ */
113
+ function byRule(rules) {
114
+ return (error, ctx) => {
115
+ const execError = error;
116
+ const text = textOf(error);
117
+ for (const rule of rules) {
118
+ const matches = commandMatches(rule, execError?.command)
119
+ && argsMatch(rule, execError?.args)
120
+ && patternMatches(rule, text);
121
+ if (matches) {
122
+ return typeof rule.message === "function"
123
+ ? rule.message(error, ctx)
124
+ : rule.message;
125
+ }
126
+ }
127
+ return undefined;
128
+ };
129
+ }
48
130
  //# sourceMappingURL=messages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/core/messages.ts"],"names":[],"mappings":";;AAoBA,8BAiBC;AAaD,8BAYC;AAtDD;;;;;;;;;;;GAWG;AACH,SAAgB,SAAS,CAAC,KAA8B;IAEpD,OAAO,CAAC,KAAc,EAAE,EAAE;QAEtB,MAAM,SAAS,GAAG,KAAsB,CAAC;QACzC,MAAM,IAAI,GAAG,SAAS,EAAE,MAAM,IAAI,SAAS,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;QAEtE,KAAK,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;YACrC,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrB,OAAO,OAAO,CAAC;YACnB,CAAC;QACL,CAAC;QAED,OAAO,SAAS,CAAC;IAErB,CAAC,CAAC;AAEN,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,SAAS,CAAC,GAA2B;IAEjD,OAAO,CAAC,KAAc,EAAE,EAAE;QAEtB,MAAM,OAAO,GAAI,KAAuB,EAAE,OAAO,CAAC;QAElD,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAE/B,OAAO,GAAG,CAAC,OAAO,CAAC,CAAC;IAExB,CAAC,CAAC;AAEN,CAAC"}
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/core/messages.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-indev/catops-cli",
3
- "version": "0.0.1-alpha.6",
3
+ "version": "0.0.1-alpha.7",
4
4
  "description": "Framework CLI para pipelines DevOps (shell, docker, git, kubectl, helm, npm, terraform, ansible, argocd, tekton, oc, az) sobre un ExecutionContext compartido, con menus interactivos y selectores automaticos por flag. Escrito en TypeScript, 100% usable desde JavaScript.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",