cdd-cli 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.github/workflows/ci.yml +29 -0
  2. package/CHANGELOG.md +27 -0
  3. package/CODE_OF_CONDUCT.md +12 -0
  4. package/CONTRIBUTING.md +22 -0
  5. package/LICENSE +23 -0
  6. package/README.md +73 -161
  7. package/SECURITY.md +5 -0
  8. package/babel.config.cjs +6 -0
  9. package/dist/App.js +38 -24
  10. package/dist/cdd.bundle.js +8 -0
  11. package/dist/components/ContainerCreationPrompt.js +48 -0
  12. package/dist/components/ContainerCreator.js +81 -0
  13. package/dist/components/ContainerList.js +24 -4
  14. package/dist/components/ContainerRow.js +66 -13
  15. package/dist/components/ContainerSection.js +14 -0
  16. package/dist/components/Footer.js +10 -0
  17. package/dist/components/PromptField.js +20 -0
  18. package/dist/components/UsageMenu.js +8 -0
  19. package/dist/helpers/actionHelpers.js +0 -1
  20. package/dist/helpers/dockerActions.js +13 -0
  21. package/dist/helpers/dockerService/dockerService.js +5 -0
  22. package/dist/helpers/dockerService/serviceComponents/containerActions.js +163 -0
  23. package/dist/helpers/dockerService/serviceComponents/containerList.js +57 -0
  24. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +24 -0
  25. package/dist/helpers/dockerService/serviceComponents/containerStats.js +50 -0
  26. package/dist/helpers/dockerService/serviceComponents/imageUtils.js +52 -0
  27. package/dist/helpers/dockerService.js +111 -0
  28. package/dist/helpers/validationHelpers.js +26 -0
  29. package/dist/hooks/creation/useContainerActions.js +171 -0
  30. package/dist/hooks/creation/useContainerCreation.js +141 -0
  31. package/dist/hooks/creation/useLogsViewer.js +62 -0
  32. package/dist/hooks/useContainers.js +11 -1
  33. package/dist/hooks/useControls.js +298 -78
  34. package/dist/hooks/useLogsStream.js +1 -1
  35. package/dist/index.js +11 -0
  36. package/esbuild.config.cjs +11 -0
  37. package/jest.config.cjs +6 -0
  38. package/package.json +10 -6
  39. package/src/App.jsx +43 -26
  40. package/src/components/ContainerCreationPrompt.jsx +37 -0
  41. package/src/components/ContainerList.jsx +23 -6
  42. package/src/components/ContainerRow.jsx +36 -19
  43. package/src/components/ContainerSection.jsx +10 -0
  44. package/src/components/Footer.jsx +10 -0
  45. package/src/components/PromptField.jsx +18 -0
  46. package/src/components/UsageMenu.jsx +17 -0
  47. package/src/helpers/actionHelpers.js +0 -1
  48. package/src/helpers/dockerService/dockerService.js +3 -0
  49. package/src/helpers/dockerService/serviceComponents/containerActions.js +52 -0
  50. package/src/helpers/dockerService/serviceComponents/containerList.js +26 -0
  51. package/src/helpers/dockerService/serviceComponents/containerLogs.js +19 -0
  52. package/src/helpers/dockerService/serviceComponents/containerStats.js +34 -0
  53. package/src/helpers/dockerService/serviceComponents/imageUtils.js +21 -0
  54. package/src/helpers/validationHelpers.js +16 -0
  55. package/src/hooks/creation/useContainerActions.js +68 -0
  56. package/src/hooks/creation/useContainerCreation.js +104 -0
  57. package/src/hooks/creation/useLogsViewer.js +50 -0
  58. package/src/hooks/useContainers.js +11 -1
  59. package/src/hooks/useControls.js +176 -55
  60. package/src/hooks/useLogsStream.js +1 -1
  61. package/src/index.js +13 -0
  62. package/test/validationHelpers.test.js +28 -0
  63. package/PLAN_KEYBINDINGS_Y_REALTIME.md +0 -260
  64. package/src/components/StatsViewer.jsx +0 -0
  65. package/src/helpers/dockerActions.js +0 -39
  66. package/src/helpers/dockerService.js +0 -88
@@ -1,49 +1,143 @@
1
-
2
- import React, { useState, useRef } from "react";
1
+ import React from "react";
2
+ import { useContainerActions } from "./creation/useContainerActions";
3
+ import { useContainerCreation } from "./creation/useContainerCreation";
4
+ import { useLogsViewer } from "./creation/useLogsViewer";
3
5
  import { useInput } from "ink";
4
- import { startContainer, stopContainer, restartContainer, getLogsStream } from "../helpers/dockerService";
5
- import { handleAction } from "../helpers/actionHelpers";
6
- import { exitWithMessage } from "../helpers/exitWithMessage";
7
-
6
+ import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs.js";
7
+ import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";
8
8
 
9
+ // Principal hook to manage user inputs and control the app state
9
10
  export function useControls(containers = []) {
10
- const [selected, setSelected] = useState(0);
11
- const [message, setMessage] = useState("");
12
- const [messageColor, setMessageColor] = useState("yellow");
13
- const [showLogs, setShowLogs] = useState(false);
14
- const [logs, setLogs] = useState([]);
15
- const logsStreamRef = useRef(null);
11
+ const [selected, setSelected] = React.useState(0);
12
+ const [creatingContainer, setCreatingContainer] = React.useState(false);
13
+ const [confirmErase, setConfirmErase] = React.useState(false);
16
14
  const total = containers.length;
17
15
 
18
- // Handler para salir de la vista de logs
19
- const exitLogs = () => {
20
- setShowLogs(false);
21
- setLogs([]);
22
- if (logsStreamRef.current) {
23
- logsStreamRef.current.destroy?.();
24
- logsStreamRef.current = null;
25
- }
26
- };
16
+ // Modular hooks
17
+ const actions = useContainerActions({ containers });
18
+ const creation = useContainerCreation({
19
+ onCreate: async ({ imageName, containerName, portInput, envInput }) => {
20
+ // Build Docker options
21
+ const env = (envInput || "").split(",").map(s => s.trim()).filter(Boolean);
22
+ const ports = (portInput || "").split(",").map(s => s.trim()).filter(Boolean);
23
+ const ExposedPorts = {};
24
+ const PortBindings = {};
25
+ ports.forEach(pair => {
26
+ const [host, cont] = pair.split(":");
27
+ if (!host || !cont) return;
28
+ const key = `${cont}/tcp`;
29
+ ExposedPorts[key] = {};
30
+ PortBindings[key] = PortBindings[key] || [];
31
+ PortBindings[key].push({ HostPort: `${host}` });
32
+ });
33
+
34
+ const options = {
35
+ Tty: true,
36
+ };
37
+ if (Object.keys(ExposedPorts).length) options.ExposedPorts = ExposedPorts;
38
+ if (Object.keys(PortBindings).length) options.HostConfig = { PortBindings };
39
+ if (env.length) options.Env = env;
40
+ if (containerName) options.name = containerName;
41
+
42
+ actions.setMessage(`Creating container ${imageName}...`);
43
+ actions.setMessageColor("yellow");
44
+ try {
45
+ const id = await svcCreateContainer(imageName, options);
46
+ actions.setMessage(`Created container ${id}`);
47
+ actions.setMessageColor("green");
48
+ } catch (err) {
49
+ actions.setMessage(`Error creating container: ${err.message}`);
50
+ actions.setMessageColor("red");
51
+ } finally {
52
+ setCreatingContainer(false);
53
+ }
54
+ },
55
+ onCancel: () => setCreatingContainer(false),
56
+ dbImages: ["mysql", "mariadb", "postgres", "mongo", "mssql", "redis"]
57
+ });
58
+ const logsViewer = useLogsViewer();
59
+
60
+ // Handler to exit logs (delegated to logsViewer)
61
+ const exitLogs = logsViewer.closeLogs;
27
62
 
28
63
  useInput((input, key) => {
29
- if (showLogs) {
64
+ // Confirmación de borrado
65
+ if (confirmErase) {
66
+ if (input === "y" || input === "Y") {
67
+ actions.handleAction({
68
+ actionFn: async (id) => await actions.removeContainer(id),
69
+ actionLabel: "Erasing",
70
+ selected,
71
+ });
72
+ setConfirmErase(false);
73
+ actions.setMessageColor("yellow");
74
+ return;
75
+ } else if (input === "n" || input === "N" || key.escape) {
76
+ setConfirmErase(false);
77
+ actions.setMessage("");
78
+ actions.setMessageColor("");
79
+ return;
80
+ } else {
81
+ actions.setMessage("Are you sure you want to erase this container? (y/n)");
82
+ actions.setMessageColor("yellow");
83
+ return;
84
+ }
85
+ }
86
+
87
+ // Logs viewer
88
+ if (logsViewer.showLogs) {
30
89
  if (input === "q" || key.escape) {
31
- exitLogs();
90
+ logsViewer.closeLogs();
32
91
  }
33
92
  return;
34
93
  }
35
94
 
95
+ // Container creation flow: delegate input to creation hook
96
+ if (creatingContainer) {
97
+ const step = creation.step;
98
+ const appendCharToField = (setter, value, ch) => {
99
+ setter((value || "") + ch);
100
+ };
101
+ // Escape -> cancel
102
+ if (key.escape) {
103
+ creation.cancelCreation();
104
+ setCreatingContainer(false);
105
+ return;
106
+ }
107
+ // Enter -> next step
108
+ if (input === "\r" || input === "\n") {
109
+ creation.nextStep();
110
+ return;
111
+ }
112
+ // Backspace/Delete support
113
+ if (key.backspace || key.delete) {
114
+ if (step === 0) creation.setImageName((v) => (v || "").slice(0, -1));
115
+ if (step === 1) creation.setContainerName((v) => (v || "").slice(0, -1));
116
+ if (step === 2) creation.setPortInput((v) => (v || "").slice(0, -1));
117
+ if (step === 3) creation.setEnvInput((v) => (v || "").slice(0, -1));
118
+ return;
119
+ }
120
+ // Printable character input (append)
121
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
122
+ if (step === 0) appendCharToField(creation.setImageName, creation.imageName, input);
123
+ if (step === 1) appendCharToField(creation.setContainerName, creation.containerName, input);
124
+ if (step === 2) appendCharToField(creation.setPortInput, creation.portInput, input);
125
+ if (step === 3) appendCharToField(creation.setEnvInput, creation.envInput, input);
126
+ return;
127
+ }
128
+ // Otherwise ignore
129
+ return;
130
+ }
131
+
36
132
  //==========================================================
37
133
  // Menu Navigation
38
134
  //==========================================================
39
- if (key.upArrow && total > 0) {
40
- setSelected((i) => (i === 0 ? total - 1 : i - 1));
41
- }
42
- if (key.downArrow && total > 0) {
43
- setSelected((i) => (i === total - 1 ? 0 : i + 1));
44
- }
135
+ if (key.upArrow && total > 0) setSelected((i) => (i === 0 ? total - 1 : i - 1));
136
+ if (key.downArrow && total > 0) setSelected((i) => (i === total - 1 ? 0 : i + 1));
45
137
  if (input === "q") {
46
- exitWithMessage({ setMessage, setMessageColor });
138
+ actions.setMessage("Exiting...");
139
+ actions.setMessageColor("yellow");
140
+ setTimeout(() => process.exit(0), 500);
47
141
  return;
48
142
  }
49
143
 
@@ -51,48 +145,75 @@ export function useControls(containers = []) {
51
145
  // Docker commands
52
146
  //==========================================================
53
147
  if (input === "i") {
54
- handleAction({
55
- containers,
56
- selected,
57
- actionFn: startContainer,
148
+ actions.handleAction({
149
+ actionFn: async (id) => await actions.startContainer(id),
58
150
  actionLabel: "Starting",
59
- setMessage,
60
- setMessageColor,
61
- stateCheck: c => (c.state === "running" || c.status === "running") && "Container is already running."
151
+ selected,
152
+ stateCheck: (c) => (c.state === "running" || c.status === "running") && "Container is already running."
62
153
  });
63
154
  }
64
155
  if (input === "p") {
65
- handleAction({
66
- containers,
67
- selected,
68
- actionFn: stopContainer,
156
+ actions.handleAction({
157
+ actionFn: async (id) => await actions.stopContainer(id),
69
158
  actionLabel: "Stopping",
70
- setMessage,
71
- setMessageColor,
72
- stateCheck: c => ((c.state === "exited" || c.status === "exited" || c.state === "stopped" || c.status === "stopped") && "Container is already stopped.")
159
+ selected,
160
+ stateCheck: (c) => (c.state === "exited" || c.status === "exited" || c.state === "stopped" || c.status === "stopped") && "Container is already stopped."
73
161
  });
74
162
  }
75
163
  if (input === "r") {
76
- handleAction({
77
- containers,
78
- selected,
79
- actionFn: restartContainer,
164
+ actions.handleAction({
165
+ actionFn: async (id) => await actions.restartContainer(id),
80
166
  actionLabel: "Restarting",
81
- setMessage,
82
- setMessageColor
167
+ selected,
83
168
  });
84
169
  }
170
+ if (input === "e" && containers[selected]) {
171
+ setConfirmErase(true);
172
+ actions.setMessage("Are you sure you want to erase this container? (y/n)");
173
+ actions.setMessageColor("yellow");
174
+ return;
175
+ }
85
176
  if (input === "l" && containers[selected]) {
86
- setShowLogs(true);
87
- setLogs([]);
177
+ logsViewer.openLogs();
88
178
  getLogsStream(
89
179
  containers[selected].id,
90
- (data) => setLogs((prev) => ([...prev, ...data.split("\n").filter(Boolean)])),
180
+ (data) => logsViewer.setLogs((prev) => [...prev, ...data.split("\n").filter(Boolean)]),
91
181
  () => {},
92
- (err) => setLogs((prev) => ([...prev, `Error: ${err.message}`]))
182
+ (err) => logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
93
183
  );
184
+ return;
185
+ }
186
+ if (input === "c") {
187
+ setCreatingContainer(true);
188
+ creation.setStep(0);
189
+ creation.setImageName("");
190
+ creation.setContainerName("");
191
+ creation.setPortInput("");
192
+ creation.setEnvInput("");
193
+ creation.setMessage("Insert the name of the image to create: ");
194
+ creation.setMessageColor("yellow");
94
195
  }
95
196
  });
96
197
 
97
- return { selected, setSelected, message, messageColor, showLogs, logs, exitLogs };
198
+ return {
199
+ selected,
200
+ setSelected,
201
+ // Map messaging to creation when creating, otherwise to actions
202
+ message: creatingContainer ? creation.message : actions.message,
203
+ messageColor: creatingContainer ? creation.messageColor : actions.messageColor,
204
+ showLogs: logsViewer.showLogs,
205
+ logs: logsViewer.logs,
206
+ exitLogs,
207
+ creatingContainer,
208
+ // Expose creation fields in the shape App expects
209
+ creationStep: creation.step,
210
+ imageNameInput: creation.imageName,
211
+ containerNameInput: creation.containerName,
212
+ portInput: creation.portInput,
213
+ envInput: creation.envInput,
214
+ creation,
215
+ actions,
216
+ logsViewer,
217
+ confirmErase,
218
+ };
98
219
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useRef, useCallback } from "react";
2
- import { getLogsStream } from "../helpers/dockerService";
2
+ import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs";
3
3
 
4
4
  export function useLogsStream() {
5
5
  const logsStreamRef = useRef(null);
package/src/index.js CHANGED
@@ -1,4 +1,17 @@
1
1
  #!/usr/bin/env node
2
+
3
+ /**
4
+ * Entry point for the CDD CLI application.
5
+ * Punto de entrada para la aplicación CLI de CDD.
6
+ *
7
+ * @module index
8
+ * @example
9
+ * // EN: Run the CLI
10
+ * // ES: Ejecutar el CLI
11
+ * node index.js
12
+ */
13
+
14
+
2
15
  import React from "react";
3
16
  import { render } from "ink";
4
17
  import App from './App';
@@ -0,0 +1,28 @@
1
+ let validatePorts;
2
+
3
+ beforeAll(async () => {
4
+ const mod = await import('../src/helpers/validationHelpers.js');
5
+ validatePorts = mod.validatePorts;
6
+ });
7
+
8
+ describe('validatePorts', () => {
9
+ test('valid single port mapping', () => {
10
+ expect(validatePorts('8080:80')).toBe(true);
11
+ });
12
+
13
+ test('valid multiple port mappings', () => {
14
+ expect(validatePorts('8080:80,443:443')).toBe(true);
15
+ });
16
+
17
+ test('invalid mapping missing host', () => {
18
+ expect(validatePorts(':80')).toBe(false);
19
+ });
20
+
21
+ test('invalid mapping non-numeric', () => {
22
+ expect(validatePorts('eighty:80')).toBe(false);
23
+ });
24
+
25
+ test('empty input returns false (must specify at least one port)', () => {
26
+ expect(validatePorts('')).toBe(false);
27
+ });
28
+ });
@@ -1,260 +0,0 @@
1
- # Plan de Implementación: Keybindings y Funcionalidades Avanzadas para CDD
2
-
3
- Este documento te guiará paso a paso para agregar keybindings y la integración de logs y estadísticas en tiempo real a tu proyecto CLI/CDD. Además, incluye recomendaciones de investigación para que puedas aprender y experimentar por ti mismo.
4
-
5
- ---
6
-
7
- ## 1. Keybindings (Atajos de Teclado)
8
-
9
- ### Objetivo
10
- Permitir la navegación y control de contenedores Docker usando el teclado.
11
-
12
- ### Acciones y Sugerencias
13
- - **↑ / ↓**: Navegar entre contenedores
14
- - **Enter**: Seleccionar contenedor
15
- - **L**: Mostrar logs en vivo
16
- - **S**: Mostrar estadísticas
17
- - **R**: Reiniciar contenedor
18
- - **I**: Iniciar contenedor
19
- - **P**: Pausar/Detener contenedor
20
- - **Q**: Salir
21
-
22
- ### ¿Cómo hacerlo?
23
-
24
- ### Usando Ink para Keybindings y UI CLI React
25
-
26
- Si ya tienes Ink instalado, es la mejor opción para construir tu CLI interactiva con React. Ink te permite crear componentes reutilizables, manejar el estado y capturar eventos de teclado de forma reactiva, todo en la terminal.
27
-
28
- #### Funciones y hooks clave de Ink:
29
-
30
- - **`<Text>` y `<Box>`**: Componentes básicos para mostrar texto y organizar la UI en la terminal.
31
- - **Por qué:** Permiten estructurar y dar formato a la salida, igual que en React web.
32
-
33
- - **`useInput`**: Hook para capturar cualquier tecla presionada por el usuario.
34
- - **Cómo:**
35
- ```js
36
- import { useInput } from 'ink';
37
- useInput((input, key) => {
38
- if (key.upArrow) { /* lógica para ↑ */ }
39
- if (key.downArrow) { /* lógica para ↓ */ }
40
- if (input === 'q') { /* salir */ }
41
- // ...otros keybindings
42
-
43
- ### ¿Cómo hacerlo?
44
-
45
- #### Usando Ink para Keybindings y UI CLI React
46
-
47
- Si ya tienes Ink instalado, es la mejor opción para construir tu CLI interactiva con React. Ink te permite crear componentes reutilizables, manejar el estado y capturar eventos de teclado de forma reactiva, todo en la terminal.
48
-
49
- ##### Funciones y hooks clave de Ink:
50
-
51
- - **`<Text>` y `<Box>`**: Componentes básicos para mostrar texto y organizar la UI en la terminal.
52
- - **Por qué:** Permiten estructurar y dar formato a la salida, igual que en React web.
53
-
54
- - **`useInput`**: Hook para capturar cualquier tecla presionada por el usuario.
55
- - **Cómo:**
56
- ```js
57
- import { useInput } from 'ink';
58
- useInput((input, key) => {
59
- if (key.upArrow) { /* lógica para ↑ */ }
60
- if (key.downArrow) { /* lógica para ↓ */ }
61
- if (input === 'q') { /* salir */ }
62
- // ...otros keybindings
63
- });
64
- ```
65
- - **Por qué:** Es la forma más sencilla y reactiva de manejar atajos de teclado en la terminal con Ink.
66
-
67
- - **Estado React**: Usa `useState` y `useEffect` igual que en React web para manejar selección, logs, stats, etc.
68
- - **Por qué:** Permite que la UI se actualice automáticamente cuando cambian los datos o el estado.
69
-
70
- ##### Ejemplo básico de navegación con Ink:
71
-
72
- ```js
73
- import React, { useState } from 'react';
74
- import { render, Box, Text, useInput } from 'ink';
75
-
76
- const containers = ['web', 'db', 'cache'];
77
-
78
- const App = () => {
79
- const [selected, setSelected] = useState(0);
80
-
81
- useInput((input, key) => {
82
- if (key.upArrow) setSelected(i => (i === 0 ? containers.length - 1 : i - 1));
83
- if (key.downArrow) setSelected(i => (i === containers.length - 1 ? 0 : i + 1));
84
- if (input === 'q') process.exit();
85
- // ...otros keybindings
86
- });
87
-
88
- return (
89
- <Box flexDirection="column">
90
- {containers.map((name, i) => (
91
- <Text key={name} color={i === selected ? 'green' : undefined}>
92
- {i === selected ? '>' : ' '} {name}
93
- </Text>
94
- ))}
95
- <Text>Usa ↑/↓ para navegar, Q para salir</Text>
96
- </Box>
97
- );
98
- };
99
-
100
- render(<App />);
101
- ```
102
-
103
- ##### ¿Por qué Ink?
104
- - Permite una experiencia de desarrollo y mantenimiento muy similar a React web.
105
- - Facilita la captura de teclas y el renderizado dinámico sin dependencias extra.
106
- - Es ideal para CLIs interactivas, menús, dashboards y visualización en tiempo real.
107
-
108
- Consulta la [documentación oficial de Ink](https://github.com/vadimdemedes/ink) para más ejemplos y detalles.
109
- #### Ejemplo básico de navegación con Ink:
110
-
111
- ```js
112
- import React, { useState } from 'react';
113
- import { render, Box, Text, useInput } from 'ink';
114
-
115
- const containers = ['web', 'db', 'cache'];
116
-
117
- const App = () => {
118
- const [selected, setSelected] = useState(0);
119
-
120
- useInput((input, key) => {
121
- if (key.upArrow) setSelected(i => (i === 0 ? containers.length - 1 : i - 1));
122
- if (key.downArrow) setSelected(i => (i === containers.length - 1 ? 0 : i + 1));
123
- if (input === 'q') process.exit();
124
- // ...otros keybindings
125
- });
126
-
127
- return (
128
- <Box flexDirection="column">
129
- {containers.map((name, i) => (
130
- <Text key={name} color={i === selected ? 'green' : undefined}>
131
- {i === selected ? '>' : ' '} {name}
132
- </Text>
133
- ))}
134
- <Text>Usa ↑/↓ para navegar, Q para salir</Text>
135
- </Box>
136
- );
137
- };
138
-
139
- render(<App />);
140
- ```
141
-
142
- #### ¿Por qué Ink?
143
- - Permite una experiencia de desarrollo y mantenimiento muy similar a React web.
144
- - Facilita la captura de teclas y el renderizado dinámico sin dependencias extra.
145
- - Es ideal para CLIs interactivas, menús, dashboards y visualización en tiempo real.
146
-
147
- Consulta la [documentación oficial de Ink](https://github.com/vadimdemedes/ink) para más ejemplos y detalles.
148
-
149
- ---
150
-
151
- ## 2. Integración de Logs y Stats en Tiempo Real
152
-
153
- ### Logs en Vivo
154
- - Utiliza el comando `docker logs -f <container>` para obtener logs en streaming.
155
- - Investiga cómo manejar streams en Node.js con `child_process.spawn`.
156
- - Si quieres mostrar los logs en la UI, revisa cómo actualizar el estado en tiempo real.
157
-
158
- ### Estadísticas en Tiempo Real
159
- - Usa el comando `docker stats <container> --no-stream` para obtener stats puntuales, o sin `--no-stream` para flujo continuo.
160
- - Investiga cómo parsear la salida de estos comandos y mostrarla en la UI.
161
- - Considera usar la librería [dockerode](https://github.com/apocas/dockerode) para interactuar con Docker desde Node.js de forma programática.
162
-
163
- ---
164
-
165
- ## 3. Recomendaciones de Investigación
166
-
167
- - **Node.js y Streams**: Aprende sobre el módulo `stream` y cómo manejar datos en tiempo real.
168
- - **Captura de Teclas en Terminal**: Explora cómo funcionan los eventos de teclado en aplicaciones CLI.
169
- - **Dockerode**: Investiga esta librería para controlar Docker desde Node.js.
170
- - **React y Eventos Globales**: Si tu UI es React, revisa cómo manejar eventos globales de teclado.
171
- - **Websockets**: Si decides separar backend y frontend, aprende sobre websockets para comunicación en tiempo real.
172
- - **UX en CLI**: Investiga buenas prácticas para interfaces de usuario en la terminal (ejemplo: [blessed](https://github.com/chjj/blessed)).
173
-
174
-
175
- ## 4. Siguiente Paso Sugerido
176
-
177
-
178
- ## 5. Recursos Útiles
179
-
180
-
181
- ---
182
-
183
- ## 6. Planificación Detallada Paso a Paso
184
-
185
- ### Paso 1: Definir el tipo de interfaz
186
- - Si tu aplicación es CLI y ya tienes Ink, úsalo como framework principal para toda la UI y keybindings.
187
- - Ink te permite crear componentes, manejar el estado y capturar teclas de forma reactiva, todo en la terminal.
188
- - Si necesitas algo que Ink no cubre, evalúa otras librerías, pero prioriza Ink para evitar dependencias innecesarias.
189
-
190
- ### Paso 2: Investigar y elegir librerías para keybindings
191
- - Para CLI con Ink, usa el hook `useInput` para capturar teclas y manejar la navegación y acciones.
192
- - Haz pruebas simples con `useInput` para capturar teclas y mostrar la tecla presionada o cambiar el estado.
193
-
194
- ### Paso 3: Implementar navegación entre contenedores (↑/↓)
195
- - Usa `useState` para el índice seleccionado.
196
- - Renderiza la lista de contenedores con `<Text>` y `<Box>`, resaltando el seleccionado.
197
- - Usa `useInput` para capturar las flechas y actualizar el índice.
198
- - Haz que la navegación sea cíclica para mejor UX.
199
-
200
- ### Paso 4: Implementar selección de contenedor (Enter)
201
- - Al presionar Enter, guarda el contenedor seleccionado en el estado.
202
- - Muestra un menú de acciones o detalles usando componentes condicionales de Ink.
203
-
204
- ### Paso 5: Implementar acciones sobre el contenedor
205
- - L: Mostrar logs en vivo
206
- - S: Mostrar estadísticas en tiempo real
207
- - R: Reiniciar contenedor
208
- - I: Iniciar contenedor
209
- - P: Pausar/Detener contenedor
210
- - Q: Salir
211
- - Para cada acción:
212
- - Usa `useInput` para detectar la tecla y cambiar el estado de la vista.
213
- - Crea una función que ejecute el comando Docker correspondiente usando `child_process` o `dockerode`.
214
- - Muestra el resultado en la UI usando componentes Ink.
215
-
216
- ### Paso 6: Integrar logs en tiempo real
217
- - Usa `docker logs -f <container>` con `child_process.spawn` para obtener logs en streaming.
218
- - Lee los datos del stream y actualiza el estado con los logs recibidos.
219
- - Renderiza los logs en tiempo real usando `<Text>` y el estado de React.
220
- - Permite salir de la vista de logs con una tecla usando `useInput`.
221
-
222
- ### Paso 7: Integrar estadísticas en tiempo real
223
- - Usa `docker stats <container>` o la API de `dockerode` para obtener stats.
224
- - Actualiza el estado periódicamente (con `setInterval` o un efecto) y renderiza los stats en la UI.
225
- - Permite salir de la vista de stats con una tecla usando `useInput`.
226
-
227
- ### Paso 8: Mejorar la experiencia de usuario
228
- - Agrega mensajes de ayuda o leyenda de teclas usando `<Text>`.
229
- - Maneja errores de Docker y muestra mensajes claros en la UI.
230
- - Permite refrescar la lista de contenedores con una tecla usando `useInput`.
231
-
232
- ### Paso 9: Refactorizar y documentar
233
- - Organiza el código en componentes Ink y hooks reutilizables.
234
- - Documenta cada función y componente.
235
- - Escribe un README con ejemplos de uso y keybindings.
236
-
237
- ### Paso 10: Pruebas y mejoras
238
- - Prueba todos los keybindings y flujos en la terminal.
239
- - Pide feedback a otros usuarios.
240
- - Agrega nuevas funcionalidades según necesidades.
241
-
242
- ---
243
-
244
- ## 7. Consejos para Aprender y Disfrutar el Proceso
245
- - Investiga cada librería antes de usarla.
246
- - Haz pruebas pequeñas y ve integrando poco a poco.
247
- - Lee la documentación oficial de Docker y Node.js.
248
- - No dudes en romper cosas: ¡así se aprende!
249
- - Si te atoras, busca ejemplos en GitHub o StackOverflow.
250
-
251
- ---
252
- - [Node.js Child Process](https://nodejs.org/api/child_process.html)
253
- - [Docker CLI Reference](https://docs.docker.com/engine/reference/commandline/cli/)
254
- - [Inquirer.js](https://www.npmjs.com/package/inquirer)
255
- - [dockerode](https://github.com/apocas/dockerode)
256
- - [blessed](https://github.com/chjj/blessed)
257
-
258
- ---
259
-
260
- ¡Diviértete programando y aprendiendo! Si tienes dudas sobre algún punto, investiga primero y luego pregunta para profundizar.
File without changes
@@ -1,39 +0,0 @@
1
- import { spawn } from "child_process";
2
-
3
- /**
4
- * Ejecuta una acción de Docker sobre un contenedor y maneja feedback visual.
5
- * @param {Object} params
6
- * @param {Array} params.containers - Lista de contenedores.
7
- * @param {number} params.selected - Índice del contenedor seleccionado.
8
- * @param {string} params.action - Acción docker (start, stop, restart).
9
- * @param {string} params.actionLabel - Texto para feedback (Starting, Stopping, etc).
10
- * @param {Function} params.setMessage - Setter de mensaje visual.
11
- * @param {Function} params.setMessageColor - Setter de color del mensaje.
12
- */
13
- export function handleDockerAction({ containers, selected, action, actionLabel, setMessage, setMessageColor }) {
14
- if (containers[selected]) {
15
- const c = containers[selected];
16
- const id = c.id || c.name;
17
- // Validación de estado
18
- if (action === "start" && (c.state === "running" || c.status === "running")) {
19
- setMessage("Container is already running.");
20
- setMessageColor("red");
21
- setTimeout(() => setMessage(""), 2000);
22
- return;
23
- }
24
- if (action === "stop" && (c.state === "exited" || c.status === "exited" || c.state === "stopped" || c.status === "stopped")) {
25
- setMessage("Container is already stopped.");
26
- setMessageColor("red");
27
- setTimeout(() => setMessage(""), 2000);
28
- return;
29
- }
30
- setMessage(`${actionLabel} container...`);
31
- setMessageColor("green");
32
- const child = spawn("docker", [action, id]);
33
- child.on("close", () => {
34
- setMessage(`${actionLabel} container...`);
35
- setMessageColor("green");
36
- setTimeout(() => setMessage(""), 3000);
37
- });
38
- }
39
- }