cdd-cli 3.1.3 → 3.1.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 (138) hide show
  1. package/.eslintrc.json +30 -0
  2. package/.github/workflows/docs.yml +35 -0
  3. package/CHANGELOG.md +44 -4
  4. package/FIXES_APPLIED.md +145 -147
  5. package/README.md +26 -0
  6. package/dist/App.js +20 -3
  7. package/dist/components/ContainerCreationPrompt.js +27 -1
  8. package/dist/components/ContainerList.js +11 -15
  9. package/dist/components/ContainerRow.js +73 -14
  10. package/dist/components/ContainerSection.js +16 -2
  11. package/dist/components/Footer.js +7 -1
  12. package/dist/components/Header.js +17 -1
  13. package/dist/components/LogViewer.js +21 -3
  14. package/dist/components/MessageFeedback.js +15 -1
  15. package/dist/components/PromptField.js +31 -1
  16. package/dist/components/StatsBar.js +46 -8
  17. package/dist/components/UsageMenu.js +30 -1
  18. package/dist/helpers/actionHelpers.js +17 -6
  19. package/dist/helpers/constants.js +30 -0
  20. package/dist/helpers/dockerService/dockerService.js +19 -0
  21. package/dist/helpers/dockerService/serviceComponents/containerActions.js +194 -35
  22. package/dist/helpers/dockerService/serviceComponents/containerList.js +45 -8
  23. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +16 -6
  24. package/dist/helpers/dockerService/serviceComponents/containerStats.js +28 -9
  25. package/dist/helpers/exitWithMessage.js +2 -1
  26. package/dist/helpers/logger.js +112 -0
  27. package/dist/helpers/safeCall.js +21 -0
  28. package/dist/helpers/validationHelpers.js +20 -1
  29. package/dist/hooks/creation/useContainerActions.js +2 -1
  30. package/dist/hooks/creation/useContainerCreation.js +5 -4
  31. package/dist/hooks/creation/useLogsViewer.js +3 -2
  32. package/dist/hooks/useContainers.js +6 -3
  33. package/dist/hooks/useControls.js +253 -122
  34. package/dist/hooks/useLogsStream.js +3 -2
  35. package/dist/index.js +1 -0
  36. package/docs/components_ContainerCreationPrompt.jsx.html +114 -0
  37. package/docs/components_ContainerRow.jsx.html +151 -0
  38. package/docs/components_ContainerSection.jsx.html +75 -0
  39. package/docs/components_Footer.jsx.html +66 -0
  40. package/docs/components_Header.jsx.html +84 -0
  41. package/docs/components_LogViewer.jsx.html +91 -0
  42. package/docs/components_MessageFeedback.jsx.html +76 -0
  43. package/docs/components_PromptField.jsx.html +99 -0
  44. package/docs/components_StatsBar.jsx.html +105 -0
  45. package/docs/components_UsageMenu.jsx.html +73 -0
  46. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  47. package/docs/fonts/OpenSans-Bold-webfont.svg +1830 -0
  48. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  49. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  50. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  51. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  52. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  53. package/docs/fonts/OpenSans-Italic-webfont.svg +1830 -0
  54. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  55. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  56. package/docs/fonts/OpenSans-Light-webfont.svg +1831 -0
  57. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  58. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  59. package/docs/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  60. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  61. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  62. package/docs/fonts/OpenSans-Regular-webfont.svg +1831 -0
  63. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  64. package/docs/global.html +6145 -0
  65. package/docs/helpers_actionHelpers.js.html +94 -0
  66. package/docs/helpers_constants.js.html +81 -0
  67. package/docs/helpers_dockerService_dockerService.js.html +79 -0
  68. package/docs/helpers_dockerService_serviceComponents_containerActions.js.html +153 -0
  69. package/docs/helpers_dockerService_serviceComponents_containerList.js.html +81 -0
  70. package/docs/helpers_dockerService_serviceComponents_containerLogs.js.html +85 -0
  71. package/docs/helpers_dockerService_serviceComponents_containerStats.js.html +107 -0
  72. package/docs/helpers_dockerService_serviceComponents_imageUtils.js.html +82 -0
  73. package/docs/helpers_exitWithMessage.js.html +77 -0
  74. package/docs/helpers_safeCall.js.html +68 -0
  75. package/docs/helpers_validationHelpers.js.html +96 -0
  76. package/docs/hooks_creation_useContainerActions.js.html +120 -0
  77. package/docs/hooks_creation_useContainerCreation.js.html +152 -0
  78. package/docs/hooks_creation_useLogsViewer.js.html +102 -0
  79. package/docs/hooks_useControls.js.html +298 -0
  80. package/docs/hooks_useLogsStream.js.html +82 -0
  81. package/docs/index.html +65 -0
  82. package/docs/index.js.html +65 -0
  83. package/docs/module-index.html +171 -0
  84. package/docs/scripts/linenumber.js +25 -0
  85. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  86. package/docs/scripts/prettify/lang-css.js +2 -0
  87. package/docs/scripts/prettify/prettify.js +28 -0
  88. package/docs/styles/jsdoc-default.css +358 -0
  89. package/docs/styles/prettify-jsdoc.css +111 -0
  90. package/docs/styles/prettify-tomorrow.css +132 -0
  91. package/fix-imports.cjs +33 -6
  92. package/jest.config.cjs +2 -1
  93. package/package.json +19 -9
  94. package/src/App.jsx +16 -2
  95. package/src/components/ContainerCreationPrompt.jsx +26 -0
  96. package/src/components/ContainerList.jsx +12 -13
  97. package/src/components/ContainerRow.jsx +55 -20
  98. package/src/components/ContainerSection.jsx +15 -1
  99. package/src/components/Footer.jsx +6 -1
  100. package/src/components/Header.jsx +16 -0
  101. package/src/components/LogViewer.jsx +19 -2
  102. package/src/components/MessageFeedback.jsx +14 -0
  103. package/src/components/PromptField.jsx +30 -0
  104. package/src/components/StatsBar.jsx +47 -8
  105. package/src/components/UsageMenu.jsx +38 -8
  106. package/src/helpers/actionHelpers.js +16 -7
  107. package/src/helpers/constants.js +30 -0
  108. package/src/helpers/dockerService/dockerService.js +19 -0
  109. package/src/helpers/dockerService/serviceComponents/containerActions.js +123 -12
  110. package/src/helpers/dockerService/serviceComponents/containerList.js +61 -23
  111. package/src/helpers/dockerService/serviceComponents/containerLogs.js +20 -6
  112. package/src/helpers/dockerService/serviceComponents/containerStats.js +24 -5
  113. package/src/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
  114. package/src/helpers/exitWithMessage.js +2 -1
  115. package/src/helpers/logger.js +113 -0
  116. package/src/helpers/safeCall.js +18 -0
  117. package/src/helpers/validationHelpers.js +21 -3
  118. package/src/hooks/creation/useContainerActions.js +4 -3
  119. package/src/hooks/creation/useContainerCreation.js +5 -4
  120. package/src/hooks/creation/useLogsViewer.js +2 -1
  121. package/src/hooks/useContainers.js +6 -3
  122. package/src/hooks/useControls.js +244 -91
  123. package/src/hooks/useLogsStream.js +2 -1
  124. package/src/index.js +1 -0
  125. package/test/actionHelpers.test.js +106 -0
  126. package/test/containerActions.test.js +138 -0
  127. package/test/containerList.test.js +79 -0
  128. package/test/jest.setup.js +7 -0
  129. package/test/safeCall.test.js +25 -0
  130. package/test/useContainerCreation.dom.test.js +97 -0
  131. package/test/validationHelpers.test.js +2 -2
  132. package/AUDIT_REPORT.md +0 -1004
  133. package/dist/cdd.bundle.js +0 -8
  134. package/dist/components/ContainerCreator.js +0 -81
  135. package/dist/components/StatsViewer.js +0 -0
  136. package/dist/helpers/dockerActions.js +0 -64
  137. package/dist/helpers/dockerService.js +0 -284
  138. package/dist/hooks/useStatsPolling.js +0 -54
package/src/App.jsx CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Main React component for the CDD CLI UI.
3
+ * Handles container listing, action feedback, optional creation prompt,
4
+ * and renders the live debug log panel (toggle with the D shortcut).
3
5
  *
4
6
  * @component
5
- * @returns {JSX.Element} The rendered app / La app renderizada
7
+ * @returns {JSX.Element} The rendered app
6
8
  * @example
7
- * // EN: Render the app
9
+ * // Render the app
8
10
  * <App />
9
11
  */
10
12
  import React from "react";
@@ -51,6 +53,18 @@ export default function App() {
51
53
  <Spacer />
52
54
  <MessageFeedback message={controls.message} color={controls.messageColor} />
53
55
  <UsageMenu />
56
+ {controls.showDebugLogs && (
57
+ <Box marginTop={1} flexDirection="column" borderStyle="round" borderColor="gray" padding={1}>
58
+ <Text color="cyan">Debug log — press D or ESC to close</Text>
59
+ {controls.debugLogs.length === 0 ? (
60
+ <Text dimColor>No debug entries yet. Run with CDD_LOG_LEVEL=debug for verbose output.</Text>
61
+ ) : (
62
+ controls.debugLogs.slice(-15).map((line, idx) => (
63
+ <Text key={idx} color="gray">{line}</Text>
64
+ ))
65
+ )}
66
+ </Box>
67
+ )}
54
68
  <Footer />
55
69
  </Box>
56
70
  {controls.showLogs && (
@@ -1,7 +1,23 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { PromptField, PromptMessage } from "./PromptField.jsx";
4
+ import PropTypes from 'prop-types';
4
5
 
6
+ /**
7
+ * Prompt UI shown when creating a new container.
8
+ * Presents a step-by-step form (image, name, ports, env) and displays
9
+ * a contextual message provided by the creation hook.
10
+ *
11
+ * @param {Object} props
12
+ * @param {number} props.step - Current step index (0..3)
13
+ * @param {string} props.imageName - Value for the image name field
14
+ * @param {string} props.containerName - Value for the container name field
15
+ * @param {string} props.portInput - Value for the ports input field
16
+ * @param {string} props.envInput - Value for the environment variables field
17
+ * @param {string} props.message - Contextual message to display (errors/hints)
18
+ * @param {string} props.messageColor - Color for the contextual message
19
+ * @returns {JSX.Element}
20
+ */
5
21
  export default function ContainerCreationPrompt(props) {
6
22
  const { step, imageName, containerName, portInput, envInput, message, messageColor } = props;
7
23
  const prompts = [
@@ -35,3 +51,13 @@ export default function ContainerCreationPrompt(props) {
35
51
  </Box>
36
52
  );
37
53
  }
54
+
55
+ ContainerCreationPrompt.propTypes = {
56
+ step: PropTypes.number.isRequired,
57
+ imageName: PropTypes.string,
58
+ containerName: PropTypes.string,
59
+ portInput: PropTypes.string,
60
+ envInput: PropTypes.string,
61
+ message: PropTypes.string,
62
+ messageColor: PropTypes.string,
63
+ };
@@ -2,32 +2,31 @@
2
2
  * List component for Docker containers.
3
3
  *
4
4
  * @component
5
- * @param {Object} props - Component props / Props del componente
6
- * @param {Array} props.containers - Containers to display / Contenedores a mostrar
7
- * @returns {JSX.Element} Rendered list / Lista renderizada
5
+ * @param {Object} props - Component props
6
+ * @param {Array} props.containers - Containers to display
7
+ * @returns {JSX.Element} Rendered list
8
8
  * @example
9
9
  * // Render with containers
10
10
  * <ContainerList containers={containers} />
11
11
  */
12
12
  import React from "react";
13
- import { Box, Text } from "ink";
13
+ import PropTypes from 'prop-types';
14
+ import { Box } from "ink";
14
15
  import ContainerRow from "./ContainerRow.jsx";
15
16
 
16
17
  export default function ContainerList({ containers, selected }) {
17
18
  return (
18
19
  <>
19
20
  {containers.map((container, i) => (
20
- <Box key={container.id} flexDirection="row" alignItems="flex-start">
21
- <Box width={2} minWidth={2} justifyContent="flex-end">
22
- <Text color={i === selected ? "green" : undefined}>
23
- {i === selected ? "➤" : " "}
24
- </Text>
25
- </Box>
26
- <Box flexGrow={1} paddingLeft={1}>
27
- <ContainerRow container={container} isSelected={i === selected} />
28
- </Box>
21
+ <Box key={container.id} flexDirection="row" paddingLeft={1}>
22
+ <ContainerRow container={container} isSelected={i === selected} />
29
23
  </Box>
30
24
  ))}
31
25
  </>
32
26
  );
33
27
  }
28
+
29
+ ContainerList.propTypes = {
30
+ containers: PropTypes.arrayOf(PropTypes.object).isRequired,
31
+ selected: PropTypes.number,
32
+ };
@@ -1,7 +1,9 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { getStats } from "../helpers/dockerService/serviceComponents/containerStats";
4
+ import { REFRESH_INTERVALS } from "../helpers/constants.js";
4
5
  import StatsBar from "./StatsBar.jsx";
6
+ import PropTypes from 'prop-types';
5
7
 
6
8
  const stateText = (state) => {
7
9
  if (state === "running") return { text: "🟢 RUNNING", color: "green" };
@@ -10,7 +12,14 @@ const stateText = (state) => {
10
12
  return { text: state.toUpperCase(), color: "gray" };
11
13
  };
12
14
 
13
- export default function ContainerRow({ container }) {
15
+ /**
16
+ * Row component that renders information and live stats for a container.
17
+ *
18
+ * @param {Object} props
19
+ * @param {Object} props.container - Container object with id, name, image, state and ports
20
+ * @returns {JSX.Element}
21
+ */
22
+ export default function ContainerRow({ container, isSelected = false }) {
14
23
  const { id, name, image, state } = container;
15
24
  const [stats, setStats] = useState({
16
25
  cpuPercent: 0,
@@ -18,13 +27,19 @@ export default function ContainerRow({ container }) {
18
27
  netIO: { rx: 0, tx: 0 },
19
28
  });
20
29
 
21
- // Format ports for display
30
+ // Format ports for display (no leading space to avoid layout shifts)
22
31
  const formatPorts = (ports) => {
23
32
  if (!ports || ports.length === 0) return "";
24
33
  if (Array.isArray(ports)) {
25
- return ports.map((p, i) => ` 🔗 ${p}`).join(" ");
34
+ return ports.map((p, _i) => `🔗 ${p}`).join(" ");
26
35
  }
27
- return ` 🔗 ${ports}`;
36
+ return `🔗 ${ports}`;
37
+ };
38
+
39
+ // Helper to truncate long strings to a max length without adding trailing spaces
40
+ const truncate = (s, max = 20) => {
41
+ if (!s) return "";
42
+ return s.length > max ? s.slice(0, max - 1) + "…" : s;
28
43
  };
29
44
 
30
45
  const [statsError, setStatsError] = useState("");
@@ -49,7 +64,7 @@ export default function ContainerRow({ container }) {
49
64
  };
50
65
 
51
66
  fetchStats();
52
- const timer = setInterval(fetchStats, 1500);
67
+ const timer = setInterval(fetchStats, REFRESH_INTERVALS.CONTAINER_STATS);
53
68
 
54
69
  return () => {
55
70
  isMounted = false;
@@ -60,22 +75,42 @@ export default function ContainerRow({ container }) {
60
75
  const stateInfo = stateText(state);
61
76
  return (
62
77
  <Box flexDirection="column" marginBottom={1}>
63
- <Box>
64
- <Text color="cyan">{name.padEnd(20)}</Text>
65
- <Text color="gray">{image.padEnd(20)}</Text>
66
- <Text color={stateInfo.color}>{stateInfo.text}</Text>
67
- <Text color="yellow">{formatPorts(container.ports)}</Text>
68
- {state === "running" && <Text> </Text>}
69
- {state === "running" && (
70
- <StatsBar
71
- cpu={parseFloat(stats.cpuPercent)}
72
- mem={parseFloat(stats.memPercent)}
73
- />
74
- )}
75
- {state === "running" && statsError && (
76
- <Text color="red">{statsError}</Text>
77
- )}
78
+ <Box flexDirection="row" alignItems="center">
79
+ <Box width={2} minWidth={2} justifyContent="flex-end" paddingRight={1}>
80
+ <Text color={isSelected ? "green" : undefined}>
81
+ {isSelected ? "➤" : " "}
82
+ </Text>
83
+ </Box>
84
+ <Box width={18} flexShrink={1} paddingRight={1}>
85
+ <Text color="cyan">{truncate(name, 18)}</Text>
86
+ </Box>
87
+ <Box width={18} flexShrink={1} paddingRight={1}>
88
+ <Text color="gray">{truncate(image, 18)}</Text>
89
+ </Box>
90
+ <Box width={14} minWidth={12} paddingRight={1}>
91
+ <Text color={stateInfo.color}>{stateInfo.text}</Text>
92
+ </Box>
93
+ <Box flexGrow={1} flexShrink={1} paddingLeft={0} paddingRight={1}>
94
+ <Text color="yellow">{formatPorts(container.ports)}</Text>
95
+ </Box>
96
+ <Box flexShrink={0} paddingLeft={1}>
97
+ {state === "running" ? (
98
+ <StatsBar cpu={parseFloat(stats.cpuPercent)} mem={parseFloat(stats.memPercent)} />
99
+ ) : null}
100
+ </Box>
78
101
  </Box>
102
+ {statsError && <Text color="red">{statsError}</Text>}
79
103
  </Box>
80
104
  );
81
105
  }
106
+
107
+ ContainerRow.propTypes = {
108
+ container: PropTypes.shape({
109
+ id: PropTypes.string.isRequired,
110
+ name: PropTypes.string.isRequired,
111
+ image: PropTypes.string,
112
+ state: PropTypes.string,
113
+ ports: PropTypes.oneOfType([PropTypes.array, PropTypes.string])
114
+ }).isRequired,
115
+ isSelected: PropTypes.bool,
116
+ };
@@ -1,10 +1,24 @@
1
1
  import React from "react";
2
2
  import { Text } from "ink";
3
3
  import ContainerList from "./ContainerList.jsx";
4
+ import PropTypes from 'prop-types';
4
5
 
6
+ /**
7
+ * Section that displays the list of containers or a message when none exist.
8
+ *
9
+ * @param {Object} props
10
+ * @param {Array<Object>} props.containers - Array of container objects to display
11
+ * @param {number} [props.selected] - Index of the currently selected container
12
+ * @returns {JSX.Element}
13
+ */
5
14
  export default function ContainerSection({ containers, selected }) {
6
- if (containers.length === 0) {
15
+ if (!containers || containers.length === 0) {
7
16
  return <Text>No containers found</Text>;
8
17
  }
9
18
  return <ContainerList containers={containers} selected={selected} />;
10
19
  }
20
+
21
+ ContainerSection.propTypes = {
22
+ containers: PropTypes.array.isRequired,
23
+ selected: PropTypes.number,
24
+ };
@@ -1,10 +1,15 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
 
4
+ /**
5
+ * Simple footer displayed at the bottom of the interface.
6
+ *
7
+ * @returns {JSX.Element}
8
+ */
4
9
  export default function Footer() {
5
10
  return (
6
11
  <Box justifyContent="flex-end" width="100%">
7
- <Text dimColor>Crafted by Carlos Cochero 2025</Text>
12
+ <Text dimColor>Crafted by Carlos Cochero 2025</Text>
8
13
  </Box>
9
14
  );
10
15
  }
@@ -1,6 +1,18 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import chalk from "chalk";
4
+ import PropTypes from 'prop-types';
5
+
6
+ /**
7
+ * Header component displayed at the top of the CLI UI.
8
+ *
9
+ * @component
10
+ * @param {Object} props
11
+ * @param {number} props.count - Number of containers currently detected
12
+ * @returns {JSX.Element}
13
+ * @example
14
+ * <Header count={3} />
15
+ */
4
16
 
5
17
  export default function Header({ count }) {
6
18
  return (
@@ -15,3 +27,7 @@ export default function Header({ count }) {
15
27
  </Box>
16
28
  );
17
29
  }
30
+
31
+ Header.propTypes = {
32
+ count: PropTypes.number.isRequired,
33
+ };
@@ -1,10 +1,21 @@
1
1
  import React from "react";
2
2
  import { Text, useInput } from "ink";
3
+ import { safeCall } from "../helpers/safeCall";
4
+ import PropTypes from 'prop-types';
3
5
 
6
+ /**
7
+ * Log viewer overlay component. Shows the most recent lines and closes on ESC.
8
+ *
9
+ * @param {Object} props
10
+ * @param {Array<string>} props.logs - Array of log lines
11
+ * @param {Function} [props.onExit] - Optional callback when the viewer closes
12
+ * @param {Object} [props.container] - Optional container metadata (used for title)
13
+ * @returns {JSX.Element}
14
+ */
4
15
  export default function LogViewer({ logs, onExit, container }) {
5
16
  useInput((input, key) => {
6
17
  if (key.escape) {
7
- onExit();
18
+ safeCall(onExit);
8
19
  }
9
20
  });
10
21
 
@@ -12,7 +23,7 @@ export default function LogViewer({ logs, onExit, container }) {
12
23
 
13
24
  return (
14
25
  <>
15
- <Text color="green">{container?.name || "Container"} logs, press ESC to exit</Text>
26
+ <Text color="green">{container?.name ?? "Container"} logs, press ESC to exit</Text>
16
27
  {visibleLogs.length === 0 ? (
17
28
  <Text dimColor>No logs...</Text>
18
29
  ) : (
@@ -21,3 +32,9 @@ export default function LogViewer({ logs, onExit, container }) {
21
32
  </>
22
33
  );
23
34
  }
35
+
36
+ LogViewer.propTypes = {
37
+ logs: PropTypes.arrayOf(PropTypes.string).isRequired,
38
+ onExit: PropTypes.func,
39
+ container: PropTypes.object,
40
+ };
@@ -1,6 +1,15 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
+ import PropTypes from 'prop-types';
3
4
 
5
+ /**
6
+ * Small component that shows a feedback message in the UI.
7
+ *
8
+ * @param {Object} props
9
+ * @param {string} props.message - Message to display. If falsy, component returns null.
10
+ * @param {string} [props.color] - Optional color name for the message text.
11
+ * @returns {JSX.Element|null}
12
+ */
4
13
  export default function MessageFeedback({ message, color }) {
5
14
  if (!message) return null;
6
15
  return (
@@ -9,3 +18,8 @@ export default function MessageFeedback({ message, color }) {
9
18
  </Box>
10
19
  );
11
20
  }
21
+
22
+ MessageFeedback.propTypes = {
23
+ message: PropTypes.string,
24
+ color: PropTypes.string,
25
+ };
@@ -1,6 +1,17 @@
1
1
  import React from "react";
2
2
  import { Text } from "ink";
3
+ import PropTypes from 'prop-types';
3
4
 
5
+ /**
6
+ * PromptField shows a label and the current typed value for an input field.
7
+ * The cursor is represented with a trailing underscore.
8
+ *
9
+ * @param {Object} props
10
+ * @param {string} props.label - Label shown above the input value
11
+ * @param {string} props.value - Current value of the input
12
+ * @param {boolean} [props.required=false] - Whether the field is required
13
+ * @returns {JSX.Element}
14
+ */
4
15
  export function PromptField({ label, value, required }) {
5
16
  // If the field is required and empty, show in red
6
17
  const isEmpty = required && !value.trim();
@@ -12,7 +23,26 @@ export function PromptField({ label, value, required }) {
12
23
  );
13
24
  }
14
25
 
26
+ /**
27
+ * Small helper to show a message below prompts (errors, hints, etc.).
28
+ *
29
+ * @param {Object} props
30
+ * @param {string} props.message - Message text to display
31
+ * @param {string} [props.color] - Optional color for the message
32
+ * @returns {JSX.Element|null}
33
+ */
15
34
  export function PromptMessage({ message, color }) {
16
35
  if (!message) return null;
17
36
  return <Text color={color || "yellow"}>{message}</Text>;
18
37
  }
38
+
39
+ PromptField.propTypes = {
40
+ label: PropTypes.string,
41
+ value: PropTypes.string,
42
+ required: PropTypes.bool,
43
+ };
44
+
45
+ PromptMessage.propTypes = {
46
+ message: PropTypes.string,
47
+ color: PropTypes.string,
48
+ };
@@ -1,25 +1,64 @@
1
1
  import React from "react";
2
2
  import { Text } from "ink";
3
3
  import chalk from "chalk";
4
+ import PropTypes from 'prop-types';
4
5
 
6
+ const BAR_WIDTH = 8;
7
+
8
+ /**
9
+ * Small visual bar that shows CPU and memory usage percentages.
10
+ *
11
+ * @param {Object} props
12
+ * @param {number} props.cpu - CPU percentage (values outside 0-100 are clamped)
13
+ * @param {number} props.mem - Memory percentage (values outside 0-100 are clamped)
14
+ * @returns {JSX.Element}
15
+ */
5
16
  export default function StatsBar({ cpu, mem }) {
6
- const cpuBar = makeBar(cpu);
7
- const memBar = makeBar(mem);
17
+ const cpuBar = makeBar(cpu, BAR_WIDTH);
18
+ const memBar = makeBar(mem, BAR_WIDTH);
8
19
 
9
20
  return (
10
21
  <Text>
11
- CPU: {cpuBar} {colorize(cpu)}% MEM: {memBar} {colorize(mem)}%
22
+ CPU:{cpuBar}{colorize(cpu, ` ${formatPercent(cpu)}%`)} MEM:{memBar}{colorize(mem, ` ${formatPercent(mem)}%`)}
12
23
  </Text>
13
24
  );
14
25
  }
15
26
 
16
- function makeBar(value) {
17
- const filled = Math.round(value / 10);
18
- const empty = 10 - filled;
19
- const bar = "▓".repeat(filled) + "░".repeat(empty);
20
- return colorize(value, bar);
27
+ StatsBar.propTypes = {
28
+ cpu: PropTypes.number.isRequired,
29
+ mem: PropTypes.number.isRequired,
30
+ };
31
+
32
+ /**
33
+ * Build a fixed-width bar representation from a numeric percentage.
34
+ *
35
+ * @param {number} value - Percentage value (values outside 0-100 are clamped)
36
+ * @param {number} width - Desired bar width in characters
37
+ * @returns {string} Colored bar string
38
+ */
39
+ function makeBar(value, width) {
40
+ const safeWidth = Math.max(1, Math.round(width));
41
+ const normalized = Number.isFinite(value) ? Math.min(Math.max(value, 0), 100) : 0;
42
+ const filled = Math.round((normalized / 100) * safeWidth);
43
+ const empty = Math.max(0, safeWidth - filled);
44
+ const bar = "█".repeat(filled) + "░".repeat(empty);
45
+ return colorize(normalized, bar);
46
+ }
47
+
48
+ function formatPercent(value) {
49
+ if (!Number.isFinite(value)) return 0;
50
+ const normalized = Math.min(Math.max(value, 0), 100);
51
+ if (normalized >= 10) return Math.round(normalized);
52
+ return normalized.toFixed(1).replace(/\.0$/, "");
21
53
  }
22
54
 
55
+ /**
56
+ * Colorize a value or text depending on thresholds.
57
+ *
58
+ * @param {number} value - Numeric value used to pick color; receives clamped percentage
59
+ * @param {string} [text] - Text to colorize, defaults to the numeric string
60
+ * @returns {string}
61
+ */
23
62
  function colorize(value, text = value.toString()) {
24
63
  if (value < 50) return chalk.greenBright(text);
25
64
  if (value < 80) return chalk.yellowBright(text);
@@ -1,17 +1,47 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
 
4
+ const SHORTCUTS = [
5
+ "Use ↑/↓ for navigation",
6
+ "I to initiate selected container",
7
+ "P to stop selected container",
8
+ "R to restart selected container",
9
+ "C to create a container",
10
+ "L to view logs of selected container",
11
+ "D to toggle debug log panel",
12
+ "E to remove selected container",
13
+ "Q to quit",
14
+ ];
15
+
16
+ const GRID_COLUMNS = 3;
17
+
18
+ /**
19
+ * Small usage menu that explains available keybindings to the user.
20
+ * Renders entries in a responsive grid to avoid an excessively long column.
21
+ *
22
+ * @returns {JSX.Element}
23
+ */
4
24
  export default function UsageMenu() {
25
+ const rowsPerColumn = Math.ceil(SHORTCUTS.length / GRID_COLUMNS);
26
+ const columns = Array.from({ length: GRID_COLUMNS }, (_, columnIndex) =>
27
+ SHORTCUTS.slice(columnIndex * rowsPerColumn, (columnIndex + 1) * rowsPerColumn)
28
+ ).filter((column) => column.length > 0);
29
+
5
30
  return (
6
31
  <Box flexDirection="column" marginTop={1}>
7
- <Text>Use ↑/↓ for navigation</Text>
8
- <Text>•I to initiate selected container</Text>
9
- <Text>•P to stop selected container</Text>
10
- <Text>•R to restart selected container</Text>
11
- <Text>•C to create a container</Text>
12
- <Text>•L to view logs of selected container</Text>
13
- <Text>•E to remove selected container</Text>
14
- <Text>•Q to quit</Text>
32
+ <Box flexDirection="row">
33
+ {columns.map((column, columnIndex) => (
34
+ <Box
35
+ key={`shortcut-column-${columnIndex}`}
36
+ flexDirection="column"
37
+ marginRight={columnIndex === columns.length - 1 ? 0 : 3}
38
+ >
39
+ {column.map((shortcut, rowIndex) => (
40
+ <Text key={`shortcut-${columnIndex}-${rowIndex}`}>{shortcut}</Text>
41
+ ))}
42
+ </Box>
43
+ ))}
44
+ </Box>
15
45
  </Box>
16
46
  );
17
47
  }
@@ -1,3 +1,5 @@
1
+ import { MESSAGE_TIMEOUTS } from "../helpers/constants.js";
2
+ import { logger } from "./logger.js";
1
3
  /**
2
4
  * Generic helper to perform a container action with user feedback.
3
5
  *
@@ -22,22 +24,29 @@ export async function handleAction({
22
24
  }) {
23
25
  const c = containers[selected];
24
26
  if (!c) return;
25
- if (stateCheck && stateCheck(c)) {
26
- setMessage(stateCheck(c));
27
- setMessageColor("red");
28
- setTimeout(() => setMessage(""), 2000);
29
- return;
27
+ if (stateCheck) {
28
+ const failureReason = stateCheck(c);
29
+ if (failureReason) {
30
+ logger.warn("Action %s blocked for container %s: %s", actionLabel, c.id, failureReason);
31
+ setMessage(failureReason);
32
+ setMessageColor("red");
33
+ setTimeout(() => setMessage(""), MESSAGE_TIMEOUTS.SHORT);
34
+ return;
35
+ }
30
36
  }
31
37
  setMessage(`${actionLabel} container...`);
32
38
  setMessageColor("green");
39
+ logger.info("Action %s requested for container %s", actionLabel, c.id);
33
40
  try {
34
41
  await actionFn(c.id);
35
42
  setMessage(`${actionLabel} container completed successfully`);
36
43
  setMessageColor("green");
37
- setTimeout(() => setMessage(""), 3000);
44
+ setTimeout(() => setMessage(""), MESSAGE_TIMEOUTS.DEFAULT);
45
+ logger.info("Action %s completed for container %s", actionLabel, c.id);
38
46
  } catch (err) {
39
47
  setMessage(`Failed to ${actionLabel.toLowerCase()} container.`);
40
48
  setMessageColor("red");
41
- setTimeout(() => setMessage(""), 3000);
49
+ setTimeout(() => setMessage(""), MESSAGE_TIMEOUTS.DEFAULT);
50
+ logger.error("Action %s failed for container %s", actionLabel, c.id, err);
42
51
  }
43
52
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Time intervals used across the app (milliseconds).
3
+ * - CONTAINER_LIST: how often to refresh the list of containers
4
+ * - CONTAINER_STATS: how often to fetch stats for running containers
5
+ */
6
+ export const REFRESH_INTERVALS = {
7
+ CONTAINER_LIST: 3000,
8
+ CONTAINER_STATS: 1500,
9
+ };
10
+
11
+ /**
12
+ * Timeout values for UI messages before they disappear.
13
+ */
14
+ export const MESSAGE_TIMEOUTS = {
15
+ SHORT: 2000,
16
+ DEFAULT: 3000,
17
+ };
18
+
19
+ /**
20
+ * Delay used before exiting the process when showing an exit message.
21
+ */
22
+ export const EXIT_DELAY = 500;
23
+
24
+ /**
25
+ * Operational timeouts for Docker operations and image pulls.
26
+ */
27
+ export const TIMEOUTS = {
28
+ CONTAINER_OP: 30000,
29
+ PULL_IMAGE: 300000,
30
+ };
@@ -4,6 +4,25 @@ import Docker from "dockerode";
4
4
  // - /var/run/docker.sock on Linux/Mac
5
5
  // - //./pipe/docker_engine on Windows
6
6
  // - Environment variables DOCKER_HOST, DOCKER_CERT_PATH, etc.
7
+
8
+ /**
9
+ * Typedef for the docker client used in this project.
10
+ * Use a generic object shape so JSDoc can render a meaningful type.
11
+ *
12
+ * @typedef {Object} DockerClient
13
+ * @property {Function} listContainers
14
+ * @property {Function} getContainer
15
+ * @property {Function} listImages
16
+ * @property {Function} createContainer
17
+ * @property {Function} pull
18
+ */
19
+
20
+ /**
21
+ * Shared Docker client instance (dockerode). Consumers import { docker }
22
+ * and use it to list/create/start/stop containers and images.
23
+ *
24
+ * @type {DockerClient}
25
+ */
7
26
  const docker = new Docker();
8
27
 
9
28
  export { docker };