@cat-indev/catops-cli 0.0.1-alpha.5

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +397 -0
  3. package/dist/bin/devops-cli.d.ts +2 -0
  4. package/dist/bin/devops-cli.js +132 -0
  5. package/dist/bin/devops-cli.js.map +1 -0
  6. package/dist/core/Context.d.ts +42 -0
  7. package/dist/core/Context.js +153 -0
  8. package/dist/core/Context.js.map +1 -0
  9. package/dist/core/Menu.d.ts +23 -0
  10. package/dist/core/Menu.js +159 -0
  11. package/dist/core/Menu.js.map +1 -0
  12. package/dist/core/Notifier.d.ts +27 -0
  13. package/dist/core/Notifier.js +93 -0
  14. package/dist/core/Notifier.js.map +1 -0
  15. package/dist/core/classifiers.d.ts +31 -0
  16. package/dist/core/classifiers.js +53 -0
  17. package/dist/core/classifiers.js.map +1 -0
  18. package/dist/core/logger.d.ts +8 -0
  19. package/dist/core/logger.js +37 -0
  20. package/dist/core/logger.js.map +1 -0
  21. package/dist/core/prompt.d.ts +10 -0
  22. package/dist/core/prompt.js +87 -0
  23. package/dist/core/prompt.js.map +1 -0
  24. package/dist/core/senders.d.ts +27 -0
  25. package/dist/core/senders.js +109 -0
  26. package/dist/core/senders.js.map +1 -0
  27. package/dist/core/types.d.ts +105 -0
  28. package/dist/core/types.js +3 -0
  29. package/dist/core/types.js.map +1 -0
  30. package/dist/index.d.ts +12 -0
  31. package/dist/index.js +50 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/services/ansible.d.ts +11 -0
  34. package/dist/services/ansible.js +35 -0
  35. package/dist/services/ansible.js.map +1 -0
  36. package/dist/services/archive.d.ts +4 -0
  37. package/dist/services/archive.js +16 -0
  38. package/dist/services/archive.js.map +1 -0
  39. package/dist/services/argocd.d.ts +20 -0
  40. package/dist/services/argocd.js +48 -0
  41. package/dist/services/argocd.js.map +1 -0
  42. package/dist/services/az.d.ts +33 -0
  43. package/dist/services/az.js +48 -0
  44. package/dist/services/az.js.map +1 -0
  45. package/dist/services/azdo.d.ts +13 -0
  46. package/dist/services/azdo.js +57 -0
  47. package/dist/services/azdo.js.map +1 -0
  48. package/dist/services/docker.d.ts +18 -0
  49. package/dist/services/docker.js +36 -0
  50. package/dist/services/docker.js.map +1 -0
  51. package/dist/services/git.d.ts +9 -0
  52. package/dist/services/git.js +36 -0
  53. package/dist/services/git.js.map +1 -0
  54. package/dist/services/helm.d.ts +4 -0
  55. package/dist/services/helm.js +24 -0
  56. package/dist/services/helm.js.map +1 -0
  57. package/dist/services/index.d.ts +32 -0
  58. package/dist/services/index.js +81 -0
  59. package/dist/services/index.js.map +1 -0
  60. package/dist/services/kubectl.d.ts +135 -0
  61. package/dist/services/kubectl.js +266 -0
  62. package/dist/services/kubectl.js.map +1 -0
  63. package/dist/services/npm.d.ts +5 -0
  64. package/dist/services/npm.js +20 -0
  65. package/dist/services/npm.js.map +1 -0
  66. package/dist/services/oc.d.ts +146 -0
  67. package/dist/services/oc.js +296 -0
  68. package/dist/services/oc.js.map +1 -0
  69. package/dist/services/shell.d.ts +20 -0
  70. package/dist/services/shell.js +195 -0
  71. package/dist/services/shell.js.map +1 -0
  72. package/dist/services/tekton.d.ts +14 -0
  73. package/dist/services/tekton.js +46 -0
  74. package/dist/services/tekton.js.map +1 -0
  75. package/dist/services/terraform.d.ts +26 -0
  76. package/dist/services/terraform.js +63 -0
  77. package/dist/services/terraform.js.map +1 -0
  78. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,397 @@
1
+ # devops-cli
2
+
3
+ Framework interno para pipelines DevOps, empaquetado como librería npm instalable en cualquier proyecto.
4
+
5
+ ## Instalación
6
+
7
+ **Opción A — publicado en tu registro npm (público o privado tipo Verdaccio/Artifactory/GitHub Packages):**
8
+
9
+ ```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
13
+ ```
14
+
15
+ **Opción B — sin publicar, directo desde este proyecto (útil mientras lo maduras):**
16
+
17
+ ```bash
18
+ # Dentro del repo de devops-cli
19
+ npm pack # genera devops-cli-0.1.0.tgz
20
+
21
+ # Dentro del proyecto que lo va a consumir
22
+ npm install /ruta/a/devops-cli-0.1.0.tgz
23
+ ```
24
+
25
+ **Opción C — enlazado local con `npm link` (para desarrollar la librería y el proyecto que la consume al mismo tiempo):**
26
+
27
+ ```bash
28
+ # Dentro del repo de devops-cli
29
+ npm link
30
+
31
+ # Dentro del proyecto consumidor
32
+ npm link devops-cli
33
+ ```
34
+
35
+ **Opción D — como dependencia de Git (monorepo o repo privado, sin registro npm):**
36
+
37
+ ```bash
38
+ npm install git+https://github.com/tu-org/devops-cli.git
39
+ ```
40
+
41
+ Cualquiera de las 4 deja disponibles dos cosas en el proyecto consumidor:
42
+
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`).
45
+
46
+ ## Publicar una nueva versión
47
+
48
+ ```bash
49
+ npm version patch # o minor / major
50
+ npm publish # agrega --access public si usas un scope (@miorg/devops-cli)
51
+ ```
52
+
53
+ ## Piezas que integra
54
+
55
+ Une dos piezas que ya tenías:
56
+
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.
59
+
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
63
+
64
+ ```
65
+ src/
66
+ 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
71
+ 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 }
88
+ examples/
89
+ pipeline-example.js -> pipeline + menú de ejemplo
90
+ 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(...)
105
+ ```
106
+
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ó
110
+
111
+ Crea un `devops.pipeline.js` (o `devops.config.js` / `.devops-cli.js`) en la raíz de tu proyecto:
112
+
113
+ ```javascript
114
+ // devops.pipeline.js
115
+ module.exports = (ctx) => ({
116
+ title: "Pipeline",
117
+ options: {
118
+ Build: async () => {
119
+ await ctx.services.docker.build({
120
+ image: "registry/app:v1",
121
+ dockerfile: "Dockerfile"
122
+ });
123
+ },
124
+ Deploy: async () => {
125
+ await ctx.services.kubectl.apply("deployment.yaml");
126
+ }
127
+ }
128
+ });
129
+ ```
130
+
131
+ Y ejecuta:
132
+
133
+ ```bash
134
+ npx devops-cli --debug --env=prod
135
+ ```
136
+
137
+ `devops-cli` detecta el archivo, arma el `Context` a partir de los flags/params de `argv`, y renderiza el menú.
138
+
139
+ ## Uso como librería (sin el menú interactivo)
140
+
141
+ ```bash
142
+ npm run example -- --debug --env=prod
143
+ ```
144
+
145
+ ```javascript
146
+ const { Context, Menu } = require("./src");
147
+
148
+ const ctx = Context.parseArgv(); // llena flags/params desde argv
149
+
150
+ ctx.set("image", "registry/api:v1");
151
+
152
+ await ctx.services.git.checkout("develop");
153
+ await ctx.services.npm.ci();
154
+ await ctx.services.docker.build({ image: ctx.get("image"), dockerfile: "Dockerfile" });
155
+ await ctx.services.docker.push(ctx.get("image"));
156
+ await ctx.services.kubectl.apply("deployment.yaml");
157
+ ```
158
+
159
+ O con menús interactivos anidados:
160
+
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
+ ```
171
+
172
+ ## retry / timeout / dryRun en shell.exec
173
+
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**:
175
+
176
+ ```javascript
177
+ await ctx.services.docker.push(image); // igual que siempre
178
+
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
+ ```
186
+
187
+ También puedes fijar defaults globales para todo el proceso:
188
+
189
+ ```javascript
190
+ ctx.services.shell.configure({ retry: 3, timeout: 30000 });
191
+ ```
192
+
193
+ Y `Context.parseArgv()` ya conecta flags de línea de comandos automáticamente:
194
+
195
+ ```bash
196
+ npx devops-cli --dry-run # activa dryRun global
197
+ npx devops-cli --retry=3 --timeout=15000
198
+ ```
199
+
200
+ ## Logging commands de Azure Pipelines (`ctx.services.azdo`)
201
+
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();
208
+ ```
209
+
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" } });
221
+
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
+ ```
227
+
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
233
+
234
+ ```javascript
235
+ await ctx.run(
236
+ "deploy",
237
+ () => ctx.services.kubectl.apply("deployment.yaml"),
238
+ {
239
+ onSuccess: (result, ctx) => ctx.logger.success("Deploy OK"),
240
+ onError: (error, ctx) => ctx.logger.error(`Deploy falló: ${error.message}`)
241
+ }
242
+ );
243
+ ```
244
+
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
+ ```
257
+
258
+ ### Clasificar el error por área de TI y enrutarlo a canales
259
+
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:
261
+
262
+ ```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
+ }));
277
+
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
+ ]));
284
+
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());
292
+ ```
293
+
294
+ 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.
295
+
296
+ ### Senders incluidos
297
+
298
+ | Sender | Uso |
299
+ |---|---|
300
+ | `senders.log()` | Usa el logger interno (consola) |
301
+ | `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
302
+ | `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
303
+ | `senders.webhook({ url, format? })` | Como `http`, pero formatea `{ text: "❌ ..." }` por defecto (Slack/Teams/Discord-friendly) |
304
+ | `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
305
+
306
+ Puedes escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
307
+
308
+ ## kubectl / oc: kubeconfig, namespace y espera cíclica del rollout
309
+
310
+ `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):
311
+
312
+ ```javascript
313
+ await ctx.services.kubectl.apply("deploy.yaml", { kubeconfig: "/etc/kube/prod.yaml", namespace: "prod" });
314
+ await ctx.services.kubectl.get("pods", "-o", "wide", { namespace: "staging" });
315
+
316
+ await ctx.services.oc.login({ server: "https://api.cluster:6443", token, namespace: "prod" });
317
+ await ctx.services.oc.apply("deploy.yaml", { namespace: "prod" });
318
+ ```
319
+
320
+ ### `waitForDeployment` — validación cíclica del rollout
321
+
322
+ Sondea el Deployment (o `DeploymentConfig` con `oc`) hasta que:
323
+
324
+ - llega a **estado exitoso** (réplicas listas/actualizadas == deseadas) → resuelve con `{ status: "success", ... }`,
325
+ - se queda en **estado Failed** más de `failedGracePeriod` sin recuperarse → lanza `DeploymentRolloutError`,
326
+ - supera **`maxRestarts`** reinicios acumulados entre todos sus pods → lanza `DeploymentRolloutError` de inmediato, sin esperar el grace period,
327
+ - o se cumple el **`timeout`** global sin éxito → lanza `DeploymentRolloutError`.
328
+
329
+ 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).
330
+
331
+ ```javascript
332
+ const { classifiers } = require("devops-cli");
333
+
334
+ ctx.notifier.classify(classifiers.byCommand({ kubectl: "kubernetes", oc: "kubernetes" }));
335
+ ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.SLACK_K8S_WEBHOOK }));
336
+
337
+ await ctx.run("deploy-api", async () => {
338
+ await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
339
+
340
+ return ctx.services.kubectl.waitForDeployment({
341
+ deployment: "api",
342
+ namespace: "prod",
343
+ timeout: 5 * 60 * 1000, // 5 min totales antes de abortar
344
+ pollInterval: 5000, // chequea cada 5s
345
+ failedGracePeriod: 30_000, // si entra en Failed, espera 30s a que se recupere
346
+ maxRestarts: 5 // si supera 5 reinicios acumulados, aborta ya
347
+ });
348
+ });
349
+ ```
350
+
351
+ Con `oc`, usa `resourceType: "dc"` para apuntar a un `DeploymentConfig` clásico de OpenShift en vez de un `Deployment` nativo (default: `"deployment"`).
352
+
353
+ ### `waitForDeploymentGroup` — validar todas las instancias de un mismo despliegue GitOps
354
+
355
+ 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:
356
+
357
+ ```yaml
358
+ metadata:
359
+ labels:
360
+ deployment-group: repository-14
361
+ ```
362
+
363
+ `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:
364
+
365
+ ```javascript
366
+ await ctx.run("deploy-repo-14", () =>
367
+ ctx.services.kubectl.waitForDeploymentGroup({
368
+ label: { "deployment-group": "repository-14" }, // o el string ya armado: "deployment-group=repository-14"
369
+ namespace: "prod",
370
+ timeout: 5 * 60 * 1000,
371
+ pollInterval: 5000,
372
+ failedGracePeriod: 30_000,
373
+ maxRestarts: 5
374
+ })
375
+ );
376
+ ```
377
+
378
+ - Si **todas** llegan a estado exitoso → resuelve con `{ status: "success", deployments: [...] }` (el detalle de cada una).
379
+ - 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ó).
380
+ - Si el label **no matchea ningún deployment**, también lanza `DeploymentGroupRolloutError` (grupo vacío = error, no éxito silencioso).
381
+
382
+ 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.
383
+
384
+ ## Tests
385
+
386
+ ```bash
387
+ npm test
388
+ ```
389
+
390
+ 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.
391
+
392
+ ## Siguientes pasos posibles
393
+
394
+ - Publicar en un registro privado (Verdaccio/Artifactory/GitHub Packages) para instalarlo con scope, p.ej. `@miorg/devops-cli`.
395
+ - Agregar tipos (`.d.ts`) si el equipo usa TypeScript.
396
+ - Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón.
397
+ - CI propio (GitHub Actions/Azure Pipelines) que corra `npm test` en cada PR antes de `npm publish`.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const Context_1 = require("../core/Context");
40
+ const Menu_1 = require("../core/Menu");
41
+ const logger_1 = require("../core/logger");
42
+ const CONFIG_CANDIDATES = [
43
+ "devops.pipeline.js",
44
+ "devops.config.js",
45
+ ".catops-cli.js"
46
+ ];
47
+ function findConfig() {
48
+ const cwd = process.cwd();
49
+ for (const file of CONFIG_CANDIDATES) {
50
+ const fullPath = path.join(cwd, file);
51
+ if (fs.existsSync(fullPath)) {
52
+ return fullPath;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ function printHelp() {
58
+ console.log(`
59
+ catops-cli — framework CLI para pipelines DevOps
60
+
61
+ Uso:
62
+ catops-cli [--flag] [--param=valor]
63
+
64
+ Este comando busca, en el directorio actual, uno de estos archivos:
65
+ ${CONFIG_CANDIDATES.join("\n ")}
66
+
67
+ Ese archivo debe exportar una definición de menú, por ejemplo:
68
+
69
+ // devops.pipeline.js
70
+ module.exports = (ctx) => ({
71
+ title: "Pipeline",
72
+ "flag-selector": "--menu-selector",
73
+ options: {
74
+ Build: {
75
+ selector: "build",
76
+ action: async () => {
77
+ await ctx.services.docker.build({
78
+ image: "registry/app:v1",
79
+ dockerfile: "Dockerfile"
80
+ });
81
+ }
82
+ },
83
+ Deploy: {
84
+ selector: "deploy",
85
+ action: async () => {
86
+ await ctx.services.kubectl.apply("deployment.yaml");
87
+ }
88
+ }
89
+ }
90
+ });
91
+
92
+ Con esa definición, ambas formas funcionan:
93
+
94
+ catops-cli # menú interactivo
95
+ catops-cli --menu-selector=build # ejecuta "Build" directamente, sin prompts
96
+
97
+ Si "Deploy" fuera un submenú anidado con su propio "flag-selector", se puede
98
+ encadenar en un solo comando, por ejemplo:
99
+
100
+ catops-cli --menu-selector=deploy --deploy-target=staging
101
+
102
+ También puedes usar la librería directamente en tu código:
103
+
104
+ const { Context, Menu } = require("catops-cli");
105
+ import { Context, Menu } from "catops-cli"; // con tipados, en TS
106
+ `);
107
+ }
108
+ async function main() {
109
+ const configPath = findConfig();
110
+ if (!configPath) {
111
+ printHelp();
112
+ process.exit(0);
113
+ }
114
+ const ctx = Context_1.Context.parseArgv();
115
+ if (ctx.hasFlag("help") || ctx.hasFlag("h")) {
116
+ printHelp();
117
+ process.exit(0);
118
+ }
119
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
120
+ const definitionFactory = require(configPath);
121
+ const definition = typeof definitionFactory === "function"
122
+ ? await definitionFactory(ctx)
123
+ : definitionFactory;
124
+ logger_1.logger.info(`Usando configuración: ${path.relative(process.cwd(), configPath)}`);
125
+ await Menu_1.Menu.render(definition, ctx);
126
+ }
127
+ main().catch(error => {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ logger_1.logger.error(message);
130
+ process.exit(1);
131
+ });
132
+ //# sourceMappingURL=devops-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devops-cli.js","sourceRoot":"","sources":["../../src/bin/devops-cli.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,2CAA6B;AAC7B,uCAAyB;AACzB,6CAA0C;AAC1C,uCAAoC;AACpC,2CAAwC;AAGxC,MAAM,iBAAiB,GAAG;IACtB,oBAAoB;IACpB,kBAAkB;IAClB,gBAAgB;CACnB,CAAC;AAEF,SAAS,UAAU;IAEf,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAE1B,KAAK,MAAM,IAAI,IAAI,iBAAiB,EAAE,CAAC;QAEnC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAEtC,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,OAAO,QAAQ,CAAC;QACpB,CAAC;IAEL,CAAC;IAED,OAAO,IAAI,CAAC;AAEhB,CAAC;AAED,SAAS,SAAS;IAEd,OAAO,CAAC,GAAG,CAAC;;;;;;;IAOZ,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCjC,CAAC,CAAC;AAEH,CAAC;AAMD,KAAK,UAAU,IAAI;IAEf,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;IAEhC,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,GAAG,GAAG,iBAAO,CAAC,SAAS,EAAE,CAAC;IAEhC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,8EAA8E;IAC9E,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAoC,CAAC;IAEjF,MAAM,UAAU,GAAG,OAAO,iBAAiB,KAAK,UAAU;QACtD,CAAC,CAAC,MAAM,iBAAiB,CAAC,GAAG,CAAC;QAC9B,CAAC,CAAC,iBAAiB,CAAC;IAExB,eAAM,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IAEjF,MAAM,WAAI,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AAEvC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;IACjB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,eAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { type Logger } from "./logger";
2
+ import { type ServicesRegistry } from "../services";
3
+ import { Notifier } from "./Notifier";
4
+ import type { AskOptions, ConfirmOptions, SelectChoices } from "./prompt";
5
+ import type { TaskHooks } from "./types";
6
+ export type FlagValue = boolean | string;
7
+ export declare class Context {
8
+ private static instance;
9
+ flags: Record<string, FlagValue>;
10
+ params: Record<string, string>;
11
+ env: NodeJS.ProcessEnv;
12
+ vars: Record<string, unknown>;
13
+ results: Record<string, unknown>;
14
+ logger: Logger;
15
+ services: ServicesRegistry;
16
+ notifier: Notifier;
17
+ config: Record<string, unknown>;
18
+ state: Record<string, unknown>;
19
+ static current(): Context;
20
+ /** Reinicia el singleton. Útil sobre todo en tests. */
21
+ static reset(): Context;
22
+ setFlag(name: string, value?: FlagValue): this;
23
+ getFlag(name: string): FlagValue | undefined;
24
+ hasFlag(name: string): boolean;
25
+ set<T = unknown>(name: string, value: T): this;
26
+ get<T = unknown>(name: string): T | undefined;
27
+ /**
28
+ * Ejecuta una task, guarda su resultado en `ctx.results[taskId]`, y
29
+ * dispara los callbacks de éxito/error correspondientes:
30
+ * 1. `hooks.onSuccess` / `hooks.onError` (si los pasaste), y
31
+ * 2. el `notifier` global (clasifica el error por área de TI y
32
+ * lo reporta a los canales registrados con `ctx.notifier.channel(...)`).
33
+ *
34
+ * Si la task lanza, el error se re-lanza después de notificar, para no
35
+ * esconder fallas silenciosamente.
36
+ */
37
+ run<T>(taskId: string, callback: (ctx: Context) => Promise<T> | T, hooks?: TaskHooks<T>): Promise<T>;
38
+ ask(question: string, options?: AskOptions): Promise<string | undefined>;
39
+ confirm(question: string, options?: ConfirmOptions): Promise<boolean>;
40
+ select(title: string, choices: SelectChoices): Promise<string>;
41
+ static parseArgv(argv?: string[]): Context;
42
+ }