cdd-cli 2.0.1 → 3.0.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.
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import chalk from "chalk";
4
- import { getStats } from "../dockerService.js";
4
+ import { getStats } from "../helpers/dockerService.js";
5
5
  import StatsBar from "./StatsBar";
6
6
 
7
7
  const colorByState = (state) => {
@@ -37,13 +37,16 @@ export default function ContainerRow({ container }) {
37
37
  {state === "running"
38
38
  ? chalk.greenBright("🟢 RUNNING")
39
39
  : chalk.redBright(`🔴 ${state.toUpperCase()}`)}
40
+ {" "}
41
+ {chalk.yellow(container.ports)}
42
+ {" "}
43
+ {state === "running" && (
44
+ <StatsBar
45
+ cpu={parseFloat(stats.cpuPercent)}
46
+ mem={parseFloat(stats.memPercent)}
47
+ />
48
+ )}
40
49
  </Text>
41
- {state === "running" && (
42
- <StatsBar
43
- cpu={parseFloat(stats.cpuPercent)}
44
- mem={parseFloat(stats.memPercent)}
45
- />
46
- )}
47
50
  </Box>
48
51
  );
49
52
  }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Text, useInput } from "ink";
3
+
4
+ export default function LogViewer({ logs, onExit, container }) {
5
+ useInput((input, key) => {
6
+ if (key.escape) {
7
+ onExit();
8
+ }
9
+ });
10
+
11
+ const visibleLogs = logs.slice(-15);
12
+
13
+ return (
14
+ <>
15
+ <Text color="green">{container?.name || "Container"} logs, press ESC to exit</Text>
16
+ {visibleLogs.length === 0 ? (
17
+ <Text dimColor>No logs...</Text>
18
+ ) : (
19
+ visibleLogs.map((line, idx) => <Text key={idx}>{line}</Text>)
20
+ )}
21
+ </>
22
+ );
23
+ }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export default function MessageFeedback({ message, color }) {
5
+ if (!message) return null;
6
+ return (
7
+ <Box marginBottom={1}>
8
+ <Text color={color}>{message}</Text>
9
+ </Box>
10
+ );
11
+ }
File without changes
@@ -0,0 +1,31 @@
1
+ // Helper para manejar acciones docker con feedback visual y validación
2
+ export async function handleAction({
3
+ containers,
4
+ selected,
5
+ actionFn,
6
+ actionLabel,
7
+ setMessage,
8
+ setMessageColor,
9
+ stateCheck
10
+ }) {
11
+ const c = containers[selected];
12
+ if (!c) return;
13
+ if (stateCheck && stateCheck(c)) {
14
+ setMessage(stateCheck(c));
15
+ setMessageColor("red");
16
+ setTimeout(() => setMessage(""), 2000);
17
+ return;
18
+ }
19
+ setMessage(`${actionLabel} container...`);
20
+ setMessageColor("green");
21
+ try {
22
+ await actionFn(c.id);
23
+ setMessage(`${actionLabel} container...`);
24
+ setMessageColor("green");
25
+ setTimeout(() => setMessage(""), 3000);
26
+ } catch (err) {
27
+ setMessage(`Failed to ${actionLabel.toLowerCase()} container.`);
28
+ setMessageColor("red");
29
+ setTimeout(() => setMessage(""), 3000);
30
+ }
31
+ }
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1,88 @@
1
+
2
+ import Docker from "dockerode";
3
+ const docker = new Docker({ socketPath: "/var/run/docker.sock" });
4
+
5
+ export function getLogsStream(containerId, onData, onEnd, onError) {
6
+ const container = docker.getContainer(containerId);
7
+ container.logs({
8
+ follow: true,
9
+ stdout: true,
10
+ stderr: true,
11
+ tail: 100
12
+ }, (err, stream) => {
13
+ if (err) {
14
+ onError?.(err);
15
+ return;
16
+ }
17
+ stream.on('data', chunk => onData?.(chunk.toString()));
18
+ stream.on('end', () => onEnd?.());
19
+ stream.on('error', err => onError?.(err));
20
+ });
21
+ }
22
+
23
+ export async function startContainer(containerId) {
24
+ const container = docker.getContainer(containerId);
25
+ await container.start();
26
+ }
27
+
28
+ export async function stopContainer(containerId) {
29
+ const container = docker.getContainer(containerId);
30
+ await container.stop();
31
+ }
32
+
33
+ export async function restartContainer(containerId) {
34
+ const container = docker.getContainer(containerId);
35
+ await container.restart();
36
+ }
37
+
38
+ export async function getContainers() {
39
+ const containers = await docker.listContainers({ all: true });
40
+ return containers.map((container) => ({
41
+ id: container.Id,
42
+ name: container.Names[0].replace("/", ""),
43
+ image: container.Image,
44
+ state: container.State,
45
+ status: container.Status,
46
+ ports:
47
+ [
48
+ ...new Set(
49
+ container.Ports.filter((port) => port.PublicPort).map(
50
+ (port) => `${port.PublicPort}:${port.PrivatePort}`
51
+ )
52
+ ),
53
+ ]
54
+ }));
55
+ }
56
+
57
+ export async function getStats(containerId) {
58
+ const container = docker.getContainer(containerId);
59
+ const stream = await container.stats({ stream: false });
60
+
61
+ const cpuDelta =
62
+ stream.cpu_stats.cpu_usage.total_usage -
63
+ stream.precpu_stats.cpu_usage.total_usage;
64
+ const systemDelta =
65
+ stream.cpu_stats.system_cpu_usage - stream.precpu_stats.system_cpu_usage;
66
+ const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 : 0;
67
+
68
+ const memUsage = stream.memory_stats.usage || 0;
69
+ const memLimit = stream.memory_stats.limit || 1;
70
+ const memPercent = (memUsage / memLimit) * 100;
71
+
72
+ const rx = stream.networks
73
+ ? Object.values(stream.networks)
74
+ .map((n) => n.rx_bytes)
75
+ .reduce((a, b) => a + b, 0)
76
+ : 0;
77
+ const tx = stream.networks
78
+ ? Object.values(stream.networks)
79
+ .map((n) => n.tx_bytes)
80
+ .reduce((a, b) => a + b, 0)
81
+ : 0;
82
+
83
+ return {
84
+ cpuPercent: cpuPercent.toFixed(1),
85
+ memPercent: memPercent.toFixed(1),
86
+ netIO: { rx, tx },
87
+ };
88
+ }
@@ -0,0 +1,15 @@
1
+ import { spawn } from "child_process";
2
+
3
+ export function exitWithMessage({ setMessage, setMessageColor, message = "Exiting...", color = "yellow", delay = 1500 }) {
4
+ setMessage(message);
5
+ setMessageColor(color);
6
+ setTimeout(() => {
7
+ setMessage("");
8
+ if (process.platform === "win32") {
9
+ spawn("cmd", ["/c", "cls"], { stdio: "inherit" });
10
+ } else {
11
+ spawn("clear", [], { stdio: "inherit" });
12
+ }
13
+ process.exit();
14
+ }, delay);
15
+ }
@@ -0,0 +1,15 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { getContainers } from "../helpers/dockerService";
3
+
4
+ export function useContainers() {
5
+ const [containers, setContainers] = useState([]);
6
+
7
+ useEffect(() => {
8
+ const fetch = async () => setContainers(await getContainers());
9
+ fetch();
10
+ const timer = setInterval(fetch, 3000);
11
+ return () => clearInterval(timer);
12
+ }, []);
13
+
14
+ return { containers };
15
+ }
@@ -0,0 +1,98 @@
1
+
2
+ import React, { useState, useRef } from "react";
3
+ 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
+
8
+
9
+ 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);
16
+ const total = containers.length;
17
+
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
+ };
27
+
28
+ useInput((input, key) => {
29
+ if (showLogs) {
30
+ if (input === "q" || key.escape) {
31
+ exitLogs();
32
+ }
33
+ return;
34
+ }
35
+
36
+ //==========================================================
37
+ // Menu Navigation
38
+ //==========================================================
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
+ }
45
+ if (input === "q") {
46
+ exitWithMessage({ setMessage, setMessageColor });
47
+ return;
48
+ }
49
+
50
+ //==========================================================
51
+ // Docker commands
52
+ //==========================================================
53
+ if (input === "i") {
54
+ handleAction({
55
+ containers,
56
+ selected,
57
+ actionFn: startContainer,
58
+ actionLabel: "Starting",
59
+ setMessage,
60
+ setMessageColor,
61
+ stateCheck: c => (c.state === "running" || c.status === "running") && "Container is already running."
62
+ });
63
+ }
64
+ if (input === "p") {
65
+ handleAction({
66
+ containers,
67
+ selected,
68
+ actionFn: stopContainer,
69
+ 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.")
73
+ });
74
+ }
75
+ if (input === "r") {
76
+ handleAction({
77
+ containers,
78
+ selected,
79
+ actionFn: restartContainer,
80
+ actionLabel: "Restarting",
81
+ setMessage,
82
+ setMessageColor
83
+ });
84
+ }
85
+ if (input === "l" && containers[selected]) {
86
+ setShowLogs(true);
87
+ setLogs([]);
88
+ getLogsStream(
89
+ containers[selected].id,
90
+ (data) => setLogs((prev) => ([...prev, ...data.split("\n").filter(Boolean)])),
91
+ () => {},
92
+ (err) => setLogs((prev) => ([...prev, `Error: ${err.message}`]))
93
+ );
94
+ }
95
+ });
96
+
97
+ return { selected, setSelected, message, messageColor, showLogs, logs, exitLogs };
98
+ }
@@ -0,0 +1,25 @@
1
+ import React, { useRef, useCallback } from "react";
2
+ import { getLogsStream } from "../helpers/dockerService";
3
+
4
+ export function useLogsStream() {
5
+ const logsStreamRef = useRef(null);
6
+
7
+ const openLogs = useCallback((containerId, setLogs) => {
8
+ setLogs([]);
9
+ logsStreamRef.current = getLogsStream(
10
+ containerId,
11
+ (data) => setLogs((prev) => ([...prev, ...data.split("\n").filter(Boolean)])),
12
+ () => {},
13
+ (err) => setLogs((prev) => ([...prev, `Error: ${err.message}`]))
14
+ );
15
+ }, []);
16
+
17
+ const closeLogs = useCallback(() => {
18
+ if (logsStreamRef.current) {
19
+ logsStreamRef.current.destroy?.();
20
+ logsStreamRef.current = null;
21
+ }
22
+ }, []);
23
+
24
+ return { openLogs, closeLogs };
25
+ }
@@ -1,49 +0,0 @@
1
- import Docker from 'dockerode';
2
- const docker = new Docker({ socketPath: '/var/run/docker.sock'});
3
-
4
- export async function getContainers() {
5
- const containers = await
6
- docker.listContainers({ all: true });
7
- return containers.map(container => ({
8
- id: container.Id,
9
- name: container.Names[0].replace('/', ''),
10
- image: container.Image,
11
- state: container.State,
12
- status: container.Status,
13
- ports: container.Ports.map(port => `${port.PublicPort
14
- || '' }:${port.PrivatePort}`).join(', ') || '-'
15
- }))
16
- }
17
-
18
- export async function getStats(containerId) {
19
- const container = docker.getContainer(containerId);
20
- const stream = await container.stats({ stream: false });
21
-
22
- const cpuDelta =
23
- stream.cpu_stats.cpu_usage.total_usage -
24
- stream.precpu_stats.cpu_usage.total_usage;
25
- const systemDelta =
26
- stream.cpu_stats.system_cpu_usage - stream.precpu_stats.system_cpu_usage;
27
- const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 : 0;
28
-
29
- const memUsage = stream.memory_stats.usage || 0;
30
- const memLimit = stream.memory_stats.limit || 1;
31
- const memPercent = (memUsage / memLimit) * 100;
32
-
33
- const rx = stream.networks
34
- ? Object.values(stream.networks)
35
- .map(n => n.rx_bytes)
36
- .reduce((a, b) => a + b, 0)
37
- : 0;
38
- const tx = stream.networks
39
- ? Object.values(stream.networks)
40
- .map(n => n.tx_bytes)
41
- .reduce((a, b) => a + b, 0)
42
- : 0;
43
-
44
- return {
45
- cpuPercent: cpuPercent.toFixed(1),
46
- memPercent: memPercent.toFixed(1),
47
- netIO: { rx, tx },
48
- };
49
- }