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

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.
Files changed (44) hide show
  1. package/README.md +308 -211
  2. package/dist/core/messages.d.ts +63 -0
  3. package/dist/core/messages.js +84 -2
  4. package/dist/core/messages.js.map +1 -1
  5. package/dist/services/ansible.d.ts +7 -6
  6. package/dist/services/ansible.js +10 -10
  7. package/dist/services/ansible.js.map +1 -1
  8. package/dist/services/archive.d.ts +4 -4
  9. package/dist/services/archive.js +6 -6
  10. package/dist/services/archive.js.map +1 -1
  11. package/dist/services/argocd.d.ts +11 -8
  12. package/dist/services/argocd.js +14 -14
  13. package/dist/services/argocd.js.map +1 -1
  14. package/dist/services/az.d.ts +13 -8
  15. package/dist/services/az.js +17 -14
  16. package/dist/services/az.js.map +1 -1
  17. package/dist/services/docker.d.ts +10 -7
  18. package/dist/services/docker.js +12 -12
  19. package/dist/services/docker.js.map +1 -1
  20. package/dist/services/git.d.ts +9 -9
  21. package/dist/services/git.js +16 -16
  22. package/dist/services/git.js.map +1 -1
  23. package/dist/services/helm.d.ts +4 -4
  24. package/dist/services/helm.js +6 -6
  25. package/dist/services/helm.js.map +1 -1
  26. package/dist/services/kubectl.d.ts +4 -2
  27. package/dist/services/kubectl.js +13 -8
  28. package/dist/services/kubectl.js.map +1 -1
  29. package/dist/services/npm.d.ts +5 -5
  30. package/dist/services/npm.js +8 -8
  31. package/dist/services/npm.js.map +1 -1
  32. package/dist/services/oc.d.ts +12 -2
  33. package/dist/services/oc.js +22 -9
  34. package/dist/services/oc.js.map +1 -1
  35. package/dist/services/shell.d.ts +8 -0
  36. package/dist/services/shell.js +11 -0
  37. package/dist/services/shell.js.map +1 -1
  38. package/dist/services/tekton.d.ts +8 -6
  39. package/dist/services/tekton.js +10 -10
  40. package/dist/services/tekton.js.map +1 -1
  41. package/dist/services/terraform.d.ts +13 -8
  42. package/dist/services/terraform.js +14 -14
  43. package/dist/services/terraform.js.map +1 -1
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -1,114 +1,107 @@
1
- # devops-cli
1
+ # catops-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
 
7
16
  **Opción A — publicado en tu registro npm (público o privado tipo Verdaccio/Artifactory/GitHub Packages):**
8
17
 
9
18
  ```bash
10
- npm install devops-cli
11
- # o si lo publicas con scope propio, p.ej. @miorg/devops-cli
12
- npm install @miorg/devops-cli
19
+ npm install catops-cli
20
+ # o si lo publicas con scope propio, p.ej. @miorg/catops-cli
21
+ npm install @miorg/catops-cli
13
22
  ```
14
23
 
15
24
  **Opción B — sin publicar, directo desde este proyecto (útil mientras lo maduras):**
16
25
 
17
26
  ```bash
18
- # Dentro del repo de devops-cli
19
- npm pack # genera devops-cli-0.1.0.tgz
27
+ # Dentro del repo de catops-cli
28
+ npm pack # genera catops-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/catops-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):**
26
35
 
27
36
  ```bash
28
- # Dentro del repo de devops-cli
37
+ # Dentro del repo de catops-cli
29
38
  npm link
30
39
 
31
40
  # Dentro del proyecto consumidor
32
- npm link devops-cli
41
+ npm link catops-cli
33
42
  ```
34
43
 
35
44
  **Opción D — como dependencia de Git (monorepo o repo privado, sin registro npm):**
36
45
 
37
46
  ```bash
38
- npm install git+https://github.com/tu-org/devops-cli.git
47
+ npm install git+https://github.com/tu-org/catops-cli.git
39
48
  ```
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");`
44
- 2. El binario: `npx devops-cli` (o `devops-cli` si lo instalaste global con `-g`).
52
+ 1. La librería, tanto desde TS como desde JS puro:
53
+ ```typescript
54
+ import { Context, Menu, services, type MenuDefinition } from "catops-cli";
55
+ ```
56
+ ```javascript
57
+ const { Context, Menu, services } = require("catops-cli");
58
+ ```
59
+ 2. El binario: `npx catops-cli` (o `catops-cli` si lo instalaste global con `-g`).
45
60
 
46
61
  ## Publicar una nueva versión
47
62
 
48
63
  ```bash
49
64
  npm version patch # o minor / major
50
- npm publish # agrega --access public si usas un scope (@miorg/devops-cli)
65
+ npm publish # agrega --access public si usas un scope (@miorg/catops-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
+ catops-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, exec-options-passthrough.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`.
102
+ ## Uso rápido: menú con `devops.pipeline.js` + el bin
108
103
 
109
- ## Uso dentro de un proyecto que lo instaló
110
-
111
- Crea un `devops.pipeline.js` (o `devops.config.js` / `.devops-cli.js`) en la raíz de tu proyecto:
104
+ Crea un `devops.pipeline.js` (o `devops.config.js` / `.catops-cli.js`) en la raíz de tu proyecto:
112
105
 
113
106
  ```javascript
114
107
  // devops.pipeline.js
@@ -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
- npx devops-cli --debug --env=prod
122
+ npx catops-cli --debug --env=prod
135
123
  ```
136
124
 
137
- `devops-cli` detecta el archivo, arma el `Context` a partir de los flags/params de `argv`, y renderiza el menú.
125
+ `catops-cli` detecta el archivo, arma el `Context` a partir de los flags/params de `argv`, y renderiza el menú.
126
+
127
+ ## Uso directo en tu propio script (p. ej. con `tsx`)
138
128
 
139
- ## Uso como librería (sin el menú interactivo)
129
+ ```typescript
130
+ // src/index.ts
131
+ import { Context, Menu, type MenuDefinition } from "catops-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("catops-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
+ catops-cli --menu-selector=docker --docker-action=build
199
214
 
200
- ## Logging commands de Azure Pipelines (`ctx.services.azdo`)
215
+ # un solo nivel:
216
+ catops-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
+ catops-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 });
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.
219
223
 
220
- await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
224
+ ### Callbacks de éxito/error por item
221
225
 
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
-
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,113 +245,101 @@ 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 y en TODOS los servicios
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");
264
-
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
- }));
255
+ await ctx.services.docker.push(image); // igual que siempre
277
256
 
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
- ]));
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
+ ```
284
264
 
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
265
+ **Todos los comandos de todos los servicios** (`docker`, `git`, `kubectl`, `helm`, `npm`, `archive`, `terraform`, `ansible`, `argocd`, `tekton`, `oc`, `az`) aceptan este mismo control, sin que cambie nada de lo que ya usabas:
266
+
267
+ - Si la función ya recibía un **objeto de opciones** (la mayoría), agregale la clave `exec`:
268
+ ```javascript
269
+ await ctx.services.terraform.apply({ autoApprove: true, exec: { retry: 3 } });
270
+ await ctx.services.argocd.appSync("mi-app", { prune: true, exec: { retry: 3 } });
271
+ ```
272
+ - Si la función recibe **argumentos posicionales** (strings sueltos), `exec` va como el **último argumento**:
273
+ ```javascript
274
+ await ctx.services.docker.push("registry/app:v1", { retry: 3 });
275
+ await ctx.services.git.push({ retry: 3 });
276
+ await ctx.services.helm.uninstall("mi-app", { retry: 3 });
277
+ ```
278
+ - `kubectl` y `oc` combinan `exec` en el **mismo objeto** que ya usás para `kubeconfig`/`namespace`:
279
+ ```javascript
280
+ // 10 reintentos en un login inestable de OpenShift
281
+ await ctx.services.oc.login({
282
+ server: "https://api.cluster:6443",
283
+ token: process.env.OC_TOKEN,
284
+ namespace: "prod",
285
+ exec: { retry: 10, retryDelay: 2000 }
286
+ });
287
+
288
+ await ctx.services.kubectl.apply("deploy.yaml", { namespace: "prod", exec: { retry: 5, timeout: 30000 } });
289
+ ```
290
+
291
+ Defaults globales para todo el proceso (afecta a todos los comandos que no pasen su propio `exec`/opciones puntuales):
289
292
 
290
- // 3. (opcional) éxito, sin clasificación de área
291
- ctx.notifier.onSuccess(senders.log());
293
+ ```javascript
294
+ ctx.services.shell.configure({ retry: 3, timeout: 30000 });
292
295
  ```
293
296
 
294
- Tambien se pueden incluir parsers de error para convertir y usar errores amigables con las areas receptoras
297
+ `Context.parseArgv()` ya conecta flags de línea de comandos automáticamente a esos defaults globales:
295
298
 
299
+ ```bash
300
+ npx catops-cli --dry-run # activa dryRun global
301
+ npx catops-cli --retry=3 --timeout=15000
296
302
  ```
297
- const { classifiers, messages } = require("devops-cli");
298
-
299
- ctx.notifier.classify(classifiers.byCommand({ docker: "containers" }));
300
303
 
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
- ]));
304
+ ## Servicios de infraestructura incluidos
305
305
 
306
- ctx.notifier.channel("containers", senders.webhook({ url: TEAMS_WEBHOOK }));
307
- ```
306
+ ```javascript
307
+ await ctx.services.terraform.plan({ varFile: "prod.tfvars" });
308
+ await ctx.services.terraform.apply();
308
309
 
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.
310
+ await ctx.services.ansible.playbook("site.yml", { inventory: "hosts.ini" });
310
311
 
311
- ### Senders incluidos
312
+ await ctx.services.argocd.appSync("mi-app", { prune: true });
312
313
 
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) |
314
+ await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
320
315
 
321
- Puedes escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
316
+ await ctx.services.az.acrBuild({ registry: "miregistro", image: "app:v1" });
317
+ ```
322
318
 
323
- ## kubectl / oc: kubeconfig, namespace y espera cíclica del rollout
319
+ ## kubectl / oc: kubeconfig, namespace, retry/timeout, y espera cíclica del rollout
324
320
 
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):
321
+ `kubectl` y `oc` aceptan `{ kubeconfig, namespace, exec }` como último argumento en **todos** sus comandos (retrocompatible, sigue funcionando sin ese argumento ver la sección anterior para el detalle de `exec`):
326
322
 
327
323
  ```javascript
328
324
  await ctx.services.kubectl.apply("deploy.yaml", { kubeconfig: "/etc/kube/prod.yaml", namespace: "prod" });
329
- await ctx.services.kubectl.get("pods", "-o", "wide", { namespace: "staging" });
325
+ await ctx.services.kubectl.get("pods", "-o", "wide", { namespace: "staging", exec: { retry: 3 } });
330
326
 
331
- await ctx.services.oc.login({ server: "https://api.cluster:6443", token, namespace: "prod" });
327
+ await ctx.services.oc.login({ server: "https://api.cluster:6443", token, namespace: "prod", exec: { retry: 10 } });
332
328
  await ctx.services.oc.apply("deploy.yaml", { namespace: "prod" });
333
329
  ```
334
330
 
335
331
  ### `waitForDeployment` — validación cíclica del rollout
336
332
 
337
- Sondea el Deployment (o `DeploymentConfig` con `oc`) hasta que:
333
+ Sondea el Deployment (o `DeploymentConfig` con `oc` + `resourceType: "dc"`) hasta que:
338
334
 
339
335
  - llega a **estado exitoso** (réplicas listas/actualizadas == deseadas) → resuelve con `{ status: "success", ... }`,
340
336
  - se queda en **estado Failed** más de `failedGracePeriod` sin recuperarse → lanza `DeploymentRolloutError`,
341
337
  - supera **`maxRestarts`** reinicios acumulados entre todos sus pods → lanza `DeploymentRolloutError` de inmediato, sin esperar el grace period,
342
338
  - o se cumple el **`timeout`** global sin éxito → lanza `DeploymentRolloutError`.
343
339
 
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).
340
+ 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
341
 
346
342
  ```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
343
  await ctx.run("deploy-api", async () => {
353
344
  await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
354
345
 
@@ -363,11 +354,9 @@ await ctx.run("deploy-api", async () => {
363
354
  });
364
355
  ```
365
356
 
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
357
  ### `waitForDeploymentGroup` — validar todas las instancias de un mismo despliegue GitOps
369
358
 
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:
359
+ 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
360
 
372
361
  ```yaml
373
362
  metadata:
@@ -375,7 +364,7 @@ metadata:
375
364
  deployment-group: repository-14
376
365
  ```
377
366
 
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:
367
+ 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
368
 
380
369
  ```javascript
381
370
  await ctx.run("deploy-repo-14", () =>
@@ -391,10 +380,107 @@ await ctx.run("deploy-repo-14", () =>
391
380
  ```
392
381
 
393
382
  - 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ó).
383
+ - 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
384
  - Si el label **no matchea ningún deployment**, también lanza `DeploymentGroupRolloutError` (grupo vacío = error, no éxito silencioso).
396
385
 
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.
386
+ Con `oc`, ambas funciones aceptan `resourceType: "dc"` para apuntar a `DeploymentConfig` clásico en vez de `Deployment` nativo (default: `"deployment"`).
387
+
388
+ ## Logging commands de Azure Pipelines (`ctx.services.azdo`)
389
+
390
+ ```javascript
391
+ ctx.services.azdo.setVariable("BUILD_TAG", "v1.2.3");
392
+ ctx.services.azdo.logWarning("El caché de npm no se encontró, se reconstruye desde cero.");
393
+ ctx.services.azdo.group("Build");
394
+ // ... pasos ...
395
+ ctx.services.azdo.endGroup();
396
+ ```
397
+
398
+ ## Notificaciones: clasificar errores por área de TI, personalizar el mensaje, y enviarlos
399
+
400
+ `ctx.notifier` tiene tres responsabilidades independientes:
401
+
402
+ 1. **`classify()`** — decide a qué **área de TI** pertenece un error (para elegir a qué canal mandarlo).
403
+ 2. **`describeError()`** — decide el **mensaje** a reportar (reemplaza el stderr crudo por algo humano).
404
+ 3. **`channel()`** / **`onSuccess()`** — a qué **senders** se manda cada área.
405
+
406
+ ```javascript
407
+ const { classifiers, messages, senders } = require("catops-cli");
408
+
409
+ // 1. ¿A qué área de TI pertenece este error?
410
+ ctx.notifier.classify(classifiers.byCommand({
411
+ docker: "containers",
412
+ kubectl: "kubernetes",
413
+ oc: "kubernetes",
414
+ terraform: "infra",
415
+ ansible: "infra",
416
+ git: "scm",
417
+ argocd: "cd-pipeline",
418
+ tkn: "cd-pipeline",
419
+ az: "cloud-azure"
420
+ }));
421
+
422
+ // también podés clasificar por el texto del error:
423
+ ctx.notifier.classify(classifiers.byPattern([
424
+ [/permission denied|unauthorized/i, "security"],
425
+ [/timeout|ECONNREFUSED/i, "networking"],
426
+ [/no space left|ENOSPC/i, "infra"]
427
+ ]));
428
+
429
+ // 2. ¿qué mensaje se reporta? (opcional — sin esto, se usa el stderr crudo)
430
+ ctx.notifier.describeError(messages.byRule([
431
+ {
432
+ command: "docker", args: "push", pattern: /500 Internal Server Error/,
433
+ message: "Se ha reportado a infraestructura: falta de espacio en el registry"
434
+ },
435
+ {
436
+ command: "kubectl", pattern: /500/,
437
+ message: "El API server de Kubernetes devolvió 500, reintenta en unos minutos"
438
+ },
439
+ {
440
+ command: "terraform", pattern: /500/,
441
+ message: (error, ctx) => `Backend remoto de Terraform no respondió (env: ${ctx.params.env ?? "?"})`
442
+ }
443
+ ]));
444
+
445
+ // 3. ¿a dónde se manda cada área?
446
+ ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.TEAMS_WEBHOOK }));
447
+ ctx.notifier.channel("security", senders.http({ url: "https://security.miempresa.com/incidents" }));
448
+ ctx.notifier.channel("*", senders.file({ path: "./catops-cli-errors.log" })); // TODO error, sin importar el área
449
+
450
+ // (opcional) éxito, sin clasificación de área
451
+ ctx.notifier.onSuccess(senders.log());
452
+ ```
453
+
454
+ 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.
455
+
456
+ ### Clasificadores de área (`classifiers`)
457
+
458
+ | Fábrica | Uso |
459
+ |---|---|
460
+ | `classifiers.byCommand({ docker: "containers", ... })` | Mapea el comando que falló (adjunto automáticamente por `shell.exec`) a un área |
461
+ | `classifiers.byPattern([[regex, area], ...])` | Matchea contra el `stderr`/mensaje del error |
462
+
463
+ ### Formateadores de mensaje (`messages`)
464
+
465
+ | Fábrica | Uso |
466
+ |---|---|
467
+ | `messages.byPattern([[regex, mensaje], ...])` | Mismo mensaje sin importar el comando — solo mira el texto del error |
468
+ | `messages.byCommand({ docker: "mensaje fijo" })` | Mensaje fijo por comando, sin importar el detalle del error |
469
+ | `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". |
470
+
471
+ 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.
472
+
473
+ ### Senders incluidos
474
+
475
+ | Sender | Uso |
476
+ |---|---|
477
+ | `senders.log()` | Usa el logger interno (consola) |
478
+ | `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
479
+ | `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
480
+ | `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 |
481
+ | `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
482
+
483
+ Podés escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
398
484
 
399
485
  ## Tests
400
486
 
@@ -402,11 +488,22 @@ Igual que con `waitForDeployment`, el error lleva `command: "kubectl"` / `comman
402
488
  npm test
403
489
  ```
404
490
 
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.
491
+ `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:
492
+
493
+ - `context.test.js` — Context, flags, vars, parseArgv, dryRun global
494
+ - `shell.test.js` — retry, timeout, dryRun del motor shell.exec
495
+ - `services.test.js` — que docker/terraform/argocd arman bien sus argumentos
496
+ - `menu-selector.test.js` — selección automática por flag, en cascada de varios niveles
497
+ - `hooks-integration.test.js` — ctx.run() y items de menú con onSuccess/onError
498
+ - `notifier.test.js` — classify/channel/onSuccess/describeError/byRule
499
+ - `senders.test.js` — file/http/webhook/log contra servidores reales en localhost
500
+ - `kubectl.test.js`, `oc.test.js` — kubeconfig/namespace, waitForDeployment (éxito, timeout, Failed, maxRestarts)
501
+ - `exec-options-passthrough.test.js` — retry/timeout/dryRun (`exec`) llegando a todos los servicios, incluyendo un retry real que se recupera tras 2 fallos
502
+ - `deployment-group.test.js` — waitForDeploymentGroup (éxito total, fallo parcial, label sin matches, oc con `dc`)
406
503
 
407
504
  ## Siguientes pasos posibles
408
505
 
409
- - 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.
506
+ - Publicar en un registro privado (Verdaccio/Artifactory/GitHub Packages) para instalarlo con scope, p.ej. `@miorg/catops-cli`.
507
+ - Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón que `terraform.ts`/`docker.ts`.
412
508
  - CI propio (GitHub Actions/Azure Pipelines) que corra `npm test` en cada PR antes de `npm publish`.
509
+ - `--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.