cdd-cli 3.1.4 → 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 (136) hide show
  1. package/.eslintrc.json +30 -0
  2. package/.github/workflows/docs.yml +35 -0
  3. package/CHANGELOG.md +37 -4
  4. package/README.md +26 -0
  5. package/dist/App.js +20 -3
  6. package/dist/components/ContainerCreationPrompt.js +27 -1
  7. package/dist/components/ContainerList.js +11 -15
  8. package/dist/components/ContainerRow.js +73 -14
  9. package/dist/components/ContainerSection.js +16 -2
  10. package/dist/components/Footer.js +7 -1
  11. package/dist/components/Header.js +17 -1
  12. package/dist/components/LogViewer.js +21 -3
  13. package/dist/components/MessageFeedback.js +15 -1
  14. package/dist/components/PromptField.js +31 -1
  15. package/dist/components/StatsBar.js +46 -8
  16. package/dist/components/UsageMenu.js +30 -1
  17. package/dist/helpers/actionHelpers.js +17 -6
  18. package/dist/helpers/constants.js +30 -0
  19. package/dist/helpers/dockerService/dockerService.js +19 -0
  20. package/dist/helpers/dockerService/serviceComponents/containerActions.js +194 -35
  21. package/dist/helpers/dockerService/serviceComponents/containerList.js +45 -8
  22. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +16 -6
  23. package/dist/helpers/dockerService/serviceComponents/containerStats.js +28 -9
  24. package/dist/helpers/exitWithMessage.js +2 -1
  25. package/dist/helpers/logger.js +112 -0
  26. package/dist/helpers/safeCall.js +21 -0
  27. package/dist/helpers/validationHelpers.js +20 -1
  28. package/dist/hooks/creation/useContainerActions.js +2 -1
  29. package/dist/hooks/creation/useContainerCreation.js +5 -4
  30. package/dist/hooks/creation/useLogsViewer.js +3 -2
  31. package/dist/hooks/useContainers.js +6 -3
  32. package/dist/hooks/useControls.js +253 -122
  33. package/dist/hooks/useLogsStream.js +3 -2
  34. package/dist/index.js +1 -0
  35. package/docs/components_ContainerCreationPrompt.jsx.html +114 -0
  36. package/docs/components_ContainerRow.jsx.html +151 -0
  37. package/docs/components_ContainerSection.jsx.html +75 -0
  38. package/docs/components_Footer.jsx.html +66 -0
  39. package/docs/components_Header.jsx.html +84 -0
  40. package/docs/components_LogViewer.jsx.html +91 -0
  41. package/docs/components_MessageFeedback.jsx.html +76 -0
  42. package/docs/components_PromptField.jsx.html +99 -0
  43. package/docs/components_StatsBar.jsx.html +105 -0
  44. package/docs/components_UsageMenu.jsx.html +73 -0
  45. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  46. package/docs/fonts/OpenSans-Bold-webfont.svg +1830 -0
  47. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  48. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  49. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  50. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  51. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  52. package/docs/fonts/OpenSans-Italic-webfont.svg +1830 -0
  53. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  54. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  55. package/docs/fonts/OpenSans-Light-webfont.svg +1831 -0
  56. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  57. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  58. package/docs/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  59. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  60. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  61. package/docs/fonts/OpenSans-Regular-webfont.svg +1831 -0
  62. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  63. package/docs/global.html +6145 -0
  64. package/docs/helpers_actionHelpers.js.html +94 -0
  65. package/docs/helpers_constants.js.html +81 -0
  66. package/docs/helpers_dockerService_dockerService.js.html +79 -0
  67. package/docs/helpers_dockerService_serviceComponents_containerActions.js.html +153 -0
  68. package/docs/helpers_dockerService_serviceComponents_containerList.js.html +81 -0
  69. package/docs/helpers_dockerService_serviceComponents_containerLogs.js.html +85 -0
  70. package/docs/helpers_dockerService_serviceComponents_containerStats.js.html +107 -0
  71. package/docs/helpers_dockerService_serviceComponents_imageUtils.js.html +82 -0
  72. package/docs/helpers_exitWithMessage.js.html +77 -0
  73. package/docs/helpers_safeCall.js.html +68 -0
  74. package/docs/helpers_validationHelpers.js.html +96 -0
  75. package/docs/hooks_creation_useContainerActions.js.html +120 -0
  76. package/docs/hooks_creation_useContainerCreation.js.html +152 -0
  77. package/docs/hooks_creation_useLogsViewer.js.html +102 -0
  78. package/docs/hooks_useControls.js.html +298 -0
  79. package/docs/hooks_useLogsStream.js.html +82 -0
  80. package/docs/index.html +65 -0
  81. package/docs/index.js.html +65 -0
  82. package/docs/module-index.html +171 -0
  83. package/docs/scripts/linenumber.js +25 -0
  84. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  85. package/docs/scripts/prettify/lang-css.js +2 -0
  86. package/docs/scripts/prettify/prettify.js +28 -0
  87. package/docs/styles/jsdoc-default.css +358 -0
  88. package/docs/styles/prettify-jsdoc.css +111 -0
  89. package/docs/styles/prettify-tomorrow.css +132 -0
  90. package/fix-imports.cjs +33 -6
  91. package/jest.config.cjs +2 -1
  92. package/package.json +19 -9
  93. package/src/App.jsx +16 -2
  94. package/src/components/ContainerCreationPrompt.jsx +26 -0
  95. package/src/components/ContainerList.jsx +12 -13
  96. package/src/components/ContainerRow.jsx +55 -20
  97. package/src/components/ContainerSection.jsx +15 -1
  98. package/src/components/Footer.jsx +6 -1
  99. package/src/components/Header.jsx +16 -0
  100. package/src/components/LogViewer.jsx +19 -2
  101. package/src/components/MessageFeedback.jsx +14 -0
  102. package/src/components/PromptField.jsx +30 -0
  103. package/src/components/StatsBar.jsx +47 -8
  104. package/src/components/UsageMenu.jsx +38 -8
  105. package/src/helpers/actionHelpers.js +16 -7
  106. package/src/helpers/constants.js +30 -0
  107. package/src/helpers/dockerService/dockerService.js +19 -0
  108. package/src/helpers/dockerService/serviceComponents/containerActions.js +123 -12
  109. package/src/helpers/dockerService/serviceComponents/containerList.js +61 -23
  110. package/src/helpers/dockerService/serviceComponents/containerLogs.js +20 -6
  111. package/src/helpers/dockerService/serviceComponents/containerStats.js +24 -5
  112. package/src/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
  113. package/src/helpers/exitWithMessage.js +2 -1
  114. package/src/helpers/logger.js +113 -0
  115. package/src/helpers/safeCall.js +18 -0
  116. package/src/helpers/validationHelpers.js +21 -3
  117. package/src/hooks/creation/useContainerActions.js +4 -3
  118. package/src/hooks/creation/useContainerCreation.js +5 -4
  119. package/src/hooks/creation/useLogsViewer.js +2 -1
  120. package/src/hooks/useContainers.js +6 -3
  121. package/src/hooks/useControls.js +244 -91
  122. package/src/hooks/useLogsStream.js +2 -1
  123. package/src/index.js +1 -0
  124. package/test/actionHelpers.test.js +106 -0
  125. package/test/containerActions.test.js +138 -0
  126. package/test/containerList.test.js +79 -0
  127. package/test/jest.setup.js +7 -0
  128. package/test/safeCall.test.js +25 -0
  129. package/test/useContainerCreation.dom.test.js +97 -0
  130. package/test/validationHelpers.test.js +2 -2
  131. package/dist/cdd.bundle.js +0 -8
  132. package/dist/components/ContainerCreator.js +0 -81
  133. package/dist/components/StatsViewer.js +0 -0
  134. package/dist/helpers/dockerActions.js +0 -64
  135. package/dist/helpers/dockerService.js +0 -284
  136. package/dist/hooks/useStatsPolling.js +0 -54
@@ -1,13 +1,18 @@
1
- import { docker } from "../dockerService";
1
+ import { docker } from "../dockerService.js";
2
2
  import { imageExists, pullImage } from "./imageUtils.js";
3
+ import { TIMEOUTS } from "../../constants.js";
4
+ import { logger } from "../../logger.js";
3
5
 
4
6
  /**
5
- * Helper to add timeout to promises
6
- * @param {Promise} promise - Promise to wrap
7
- * @param {number} ms - Timeout in milliseconds
8
- * @returns {Promise}
7
+ * Helper to add timeout to promises. If the provided promise does not settle
8
+ * within `ms` milliseconds it will reject with an Error.
9
+ *
10
+ * @template T
11
+ * @param {Promise<T>} promise - Promise to wrap
12
+ * @param {number} [ms=TIMEOUTS.CONTAINER_OP] - Timeout in milliseconds
13
+ * @returns {Promise<T>} The original promise result or a rejection on timeout
9
14
  */
10
- function withTimeout(promise, ms = 30000) {
15
+ function withTimeout(promise, ms = TIMEOUTS.CONTAINER_OP) {
11
16
  return Promise.race([
12
17
  promise,
13
18
  new Promise((_, reject) =>
@@ -24,10 +29,13 @@ function withTimeout(promise, ms = 30000) {
24
29
  * @throws {Error} If Docker reports an error
25
30
  */
26
31
  export async function removeContainer(containerId) {
32
+ logger.debug("Removing container %s", containerId);
27
33
  const container = docker.getContainer(containerId);
28
34
  try {
29
- await withTimeout(container.remove({ force: true }), 30000);
35
+ await withTimeout(container.remove({ force: true }), TIMEOUTS.CONTAINER_OP);
36
+ logger.info("Removed container %s", containerId);
30
37
  } catch (err) {
38
+ logger.error("Failed to remove container %s", containerId, err);
31
39
  throw new Error('Error removing container: ' + err.message);
32
40
  }
33
41
  }
@@ -41,16 +49,21 @@ export async function removeContainer(containerId) {
41
49
  * @throws {Error} If image listing/pull or creation fails
42
50
  */
43
51
  export async function createContainer(imageName, options = {}) {
52
+ logger.info("Creating container from image %s", imageName);
44
53
  let exists;
45
54
  try {
46
55
  exists = await withTimeout(imageExists(imageName), 10000);
47
56
  } catch (err) {
57
+ logger.error("Could not check local images for %s", imageName, err);
48
58
  throw new Error('Error listing local images: ' + err.message);
49
59
  }
50
60
  if (!exists) {
61
+ logger.info("Image %s not found locally, pulling", imageName);
51
62
  try {
52
- await withTimeout(pullImage(imageName), 300000); // 5 minutes for pull
63
+ await withTimeout(pullImage(imageName), TIMEOUTS.PULL_IMAGE);
64
+ logger.info("Pulled image %s", imageName);
53
65
  } catch (err) {
66
+ logger.error("Failed to pull image %s", imageName, err);
54
67
  throw new Error('Could not pull image: ' + err.message);
55
68
  }
56
69
  }
@@ -59,10 +72,87 @@ export async function createContainer(imageName, options = {}) {
59
72
  Tty: true,
60
73
  ...options,
61
74
  };
75
+
76
+ const hasUserDefinedPorts = Boolean(createOpts.ExposedPorts && Object.keys(createOpts.ExposedPorts).length);
77
+
78
+ if (!hasUserDefinedPorts) {
79
+ try {
80
+ const image = docker.getImage(imageName);
81
+ if (image && typeof image.inspect === "function") {
82
+ let usedHostPorts = new Set();
83
+ if (typeof docker.listContainers === "function") {
84
+ try {
85
+ const containers = await withTimeout(docker.listContainers({ all: true }), TIMEOUTS.CONTAINER_OP);
86
+ containers.forEach((container) => {
87
+ (container.Ports || []).forEach((portInfo) => {
88
+ if (portInfo && portInfo.PublicPort) {
89
+ usedHostPorts.add(String(portInfo.PublicPort));
90
+ }
91
+ });
92
+ });
93
+ } catch (err) {
94
+ logger.warn("Could not inspect existing containers to auto-map ports", err);
95
+ }
96
+ }
97
+ const reservePort = (port) => {
98
+ const portStr = String(port);
99
+ usedHostPorts.add(portStr);
100
+ };
101
+ const pickNextAvailablePort = (base) => {
102
+ const numericBase = Number.parseInt(base, 10);
103
+ if (Number.isNaN(numericBase)) {
104
+ // If the base port is non-numeric, just return the base.
105
+ if (!usedHostPorts.has(base)) {
106
+ usedHostPorts.add(base);
107
+ return base;
108
+ }
109
+ let counter = 1;
110
+ let candidate = `${base}-${counter}`;
111
+ while (usedHostPorts.has(candidate)) {
112
+ counter += 1;
113
+ candidate = `${base}-${counter}`;
114
+ }
115
+ usedHostPorts.add(candidate);
116
+ return candidate;
117
+ }
118
+ let candidate = numericBase;
119
+ while (usedHostPorts.has(String(candidate))) {
120
+ candidate += 1;
121
+ }
122
+ reservePort(candidate);
123
+ return String(candidate);
124
+ };
125
+
126
+ const inspectData = await withTimeout(image.inspect(), TIMEOUTS.CONTAINER_OP);
127
+ const exposed = inspectData?.Config?.ExposedPorts || inspectData?.ContainerConfig?.ExposedPorts || {};
128
+ const exposedKeys = Object.keys(exposed || {});
129
+ if (exposedKeys.length) {
130
+ createOpts.ExposedPorts = createOpts.ExposedPorts || {};
131
+ createOpts.HostConfig = { ...(createOpts.HostConfig || {}) };
132
+ createOpts.HostConfig.PortBindings = { ...(createOpts.HostConfig.PortBindings || {}) };
133
+ exposedKeys.forEach((portKey) => {
134
+ if (!createOpts.ExposedPorts[portKey]) {
135
+ createOpts.ExposedPorts[portKey] = exposed[portKey] || {};
136
+ }
137
+ if (!createOpts.HostConfig.PortBindings[portKey] || !createOpts.HostConfig.PortBindings[portKey].length) {
138
+ const containerPort = portKey.split("/")[0];
139
+ const hostPort = pickNextAvailablePort(containerPort);
140
+ createOpts.HostConfig.PortBindings[portKey] = [{ HostPort: hostPort }];
141
+ }
142
+ });
143
+ }
144
+ }
145
+ } catch (err) {
146
+ logger.warn("Could not inspect image %s for default ports", imageName, err);
147
+ }
148
+ }
62
149
  try {
63
- const container = await withTimeout(docker.createContainer(createOpts), 30000);
150
+ const container = await withTimeout(docker.createContainer(createOpts), TIMEOUTS.CONTAINER_OP);
151
+ const containerId = container.id || container.Id;
152
+ logger.info("Created container %s from image %s", containerId, imageName);
64
153
  return container.id || container.Id;
65
154
  } catch (err) {
155
+ logger.error("Failed to create container from image %s", imageName, err);
66
156
  throw new Error('Error creating container: ' + err.message);
67
157
  }
68
158
  }
@@ -74,7 +164,14 @@ export async function createContainer(imageName, options = {}) {
74
164
  */
75
165
  export async function startContainer(containerId) {
76
166
  const container = docker.getContainer(containerId);
77
- await withTimeout(container.start(), 30000);
167
+ logger.debug("Starting container %s", containerId);
168
+ try {
169
+ await withTimeout(container.start(), TIMEOUTS.CONTAINER_OP);
170
+ logger.info("Started container %s", containerId);
171
+ } catch (err) {
172
+ logger.error("Failed to start container %s", containerId, err);
173
+ throw err;
174
+ }
78
175
  }
79
176
 
80
177
  /**
@@ -84,7 +181,14 @@ export async function startContainer(containerId) {
84
181
  */
85
182
  export async function stopContainer(containerId) {
86
183
  const container = docker.getContainer(containerId);
87
- await withTimeout(container.stop(), 30000);
184
+ logger.debug("Stopping container %s", containerId);
185
+ try {
186
+ await withTimeout(container.stop(), TIMEOUTS.CONTAINER_OP);
187
+ logger.info("Stopped container %s", containerId);
188
+ } catch (err) {
189
+ logger.error("Failed to stop container %s", containerId, err);
190
+ throw err;
191
+ }
88
192
  }
89
193
 
90
194
  /**
@@ -94,5 +198,12 @@ export async function stopContainer(containerId) {
94
198
  */
95
199
  export async function restartContainer(containerId) {
96
200
  const container = docker.getContainer(containerId);
97
- await withTimeout(container.restart(), 30000);
201
+ logger.debug("Restarting container %s", containerId);
202
+ try {
203
+ await withTimeout(container.restart(), TIMEOUTS.CONTAINER_OP);
204
+ logger.info("Restarted container %s", containerId);
205
+ } catch (err) {
206
+ logger.error("Failed to restart container %s", containerId, err);
207
+ throw err;
208
+ }
98
209
  }
@@ -1,30 +1,68 @@
1
- import { docker } from "../dockerService";
1
+ import { docker } from "../dockerService.js";
2
+ import { logger } from "../../logger.js";
3
+
4
+ const DEFAULT_CONTAINER_NAME = "Unknown";
5
+
6
+ /**
7
+ * Produce a safe, display-friendly container name from the raw Docker payload.
8
+ *
9
+ * @param {Object} container Docker container summary as returned by dockerode.
10
+ * @returns {string} Normalized name without leading slashes and with a fallback applied.
11
+ */
12
+ function normalizeContainerName(container) {
13
+ const names = container?.Names;
14
+
15
+ let rawName;
16
+ if (Array.isArray(names) && names.length > 0 && typeof names[0] === "string") {
17
+ rawName = names[0];
18
+ } else if (typeof names === "string" && names.trim().length > 0) {
19
+ rawName = names;
20
+ } else if (typeof container?.Name === "string" && container.Name.trim().length > 0) {
21
+ rawName = container.Name;
22
+ } else {
23
+ rawName = DEFAULT_CONTAINER_NAME;
24
+ }
25
+
26
+ if (typeof rawName !== "string") {
27
+ return DEFAULT_CONTAINER_NAME;
28
+ }
29
+
30
+ const cleanedName = rawName.replace(/^\/+/u, "").trim();
31
+ return cleanedName.length > 0 ? cleanedName : DEFAULT_CONTAINER_NAME;
32
+ }
2
33
 
3
34
  /**
4
35
  * Return a list of containers with normalized fields for the UI.
5
36
  * @returns {Promise<Array<Object>>}
6
37
  */
7
38
  export async function getContainers() {
8
- const containers = await docker.listContainers({ all: true });
9
- return containers.map((container) => ({
10
- id: container.Id,
11
- name: (container.Names && container.Names[0] || 'Unknown').replace("/", ""),
12
- image: container.Image,
13
- state: container.State,
14
- status: container.Status,
15
- ports:
16
- (() => {
17
- const publicPorts = container.Ports.filter((port) => port.PublicPort).map(
18
- (port) => `${port.PublicPort}:${port.PrivatePort}`
19
- );
20
- if (publicPorts.length > 0) {
21
- return [...new Set(publicPorts)];
22
- }
23
- // If there are no public ports, show private exposed ports
24
- const privatePorts = container.Ports.filter((port) => port.PrivatePort).map(
25
- (port) => `:${port.PrivatePort}`
26
- );
27
- return [...new Set(privatePorts)];
28
- })()
29
- }));
39
+ logger.debug("Listing containers");
40
+ try {
41
+ const containers = await docker.listContainers({ all: true });
42
+ logger.debug("Retrieved %d containers", containers.length);
43
+ return containers.map((container) => ({
44
+ id: container.Id,
45
+ name: normalizeContainerName(container),
46
+ image: container.Image,
47
+ state: container.State,
48
+ status: container.Status,
49
+ ports:
50
+ (() => {
51
+ const publicPorts = container.Ports.filter((port) => port.PublicPort).map(
52
+ (port) => `${port.PublicPort}:${port.PrivatePort}`
53
+ );
54
+ if (publicPorts.length > 0) {
55
+ return [...new Set(publicPorts)];
56
+ }
57
+ // Fall back to exposed private ports when there are no bindings.
58
+ const privatePorts = container.Ports.filter((port) => port.PrivatePort).map(
59
+ (port) => `${port.PrivatePort}`
60
+ );
61
+ return [...new Set(privatePorts)];
62
+ })()
63
+ }));
64
+ } catch (err) {
65
+ logger.error("Failed to list containers", err);
66
+ throw err;
67
+ }
30
68
  }
@@ -1,4 +1,6 @@
1
- import { docker } from "../dockerService";
1
+ import { docker } from "../dockerService.js";
2
+ import { safeCall } from "../../safeCall.js";
3
+ import { logger } from "../../logger.js";
2
4
 
3
5
  /**
4
6
  * Return a stream of logs from a container and call callbacks for events.
@@ -9,8 +11,11 @@ import { docker } from "../dockerService";
9
11
  * @param {Function} onError - Called on error
10
12
  */
11
13
  export function getLogsStream(containerId, onData, onEnd, onError) {
14
+ // Use shared safeCall util to call optional callbacks safely
15
+
12
16
  try {
13
17
  const container = docker.getContainer(containerId);
18
+ logger.debug("Opening logs stream for container %s", containerId);
14
19
  container.logs({
15
20
  follow: true,
16
21
  stdout: true,
@@ -18,14 +23,23 @@ export function getLogsStream(containerId, onData, onEnd, onError) {
18
23
  tail: 100
19
24
  }, (err, stream) => {
20
25
  if (err) {
21
- onError?.(err);
26
+ logger.error("Failed to open logs stream for container %s", containerId, err);
27
+ safeCall(onError, err);
22
28
  return;
23
29
  }
24
- stream.on('data', chunk => onData?.(chunk.toString()));
25
- stream.on('end', () => onEnd?.());
26
- stream.on('error', err => onError?.(err));
30
+ logger.info("Logs stream established for container %s", containerId);
31
+ stream.on('data', chunk => safeCall(onData, chunk.toString()));
32
+ stream.on('end', () => {
33
+ logger.debug("Logs stream ended for container %s", containerId);
34
+ safeCall(onEnd);
35
+ });
36
+ stream.on('error', streamErr => {
37
+ logger.error("Logs stream error for container %s", containerId, streamErr);
38
+ safeCall(onError, streamErr);
39
+ });
27
40
  });
28
41
  } catch (err) {
29
- onError?.(err);
42
+ logger.error("Unexpected error while opening logs stream for container %s", containerId, err);
43
+ safeCall(onError, err);
30
44
  }
31
45
  }
@@ -1,4 +1,5 @@
1
- import { docker } from "../dockerService";
1
+ import { docker } from "../dockerService.js";
2
+ import { logger } from "../../logger.js";
2
3
 
3
4
  /**
4
5
  * Retrieve a snapshot of container resource usage (CPU, memory, network).
@@ -8,7 +9,14 @@ import { docker } from "../dockerService";
8
9
  */
9
10
  export async function getStats(containerId) {
10
11
  const container = docker.getContainer(containerId);
11
- const stream = await container.stats({ stream: false });
12
+ logger.debug("Fetching stats for container %s", containerId);
13
+ let stream;
14
+ try {
15
+ stream = await container.stats({ stream: false });
16
+ } catch (err) {
17
+ logger.error("Failed to fetch stats for container %s", containerId, err);
18
+ throw err;
19
+ }
12
20
 
13
21
  const cpuDelta =
14
22
  stream.cpu_stats.cpu_usage.total_usage -
@@ -17,8 +25,16 @@ export async function getStats(containerId) {
17
25
  stream.cpu_stats.system_cpu_usage - stream.precpu_stats.system_cpu_usage;
18
26
 
19
27
  // Get number of CPUs for proper normalization
20
- const numCpus = stream.cpu_stats.online_cpus ||
21
- stream.cpu_stats.cpu_usage.percpu_usage?.length || 1;
28
+ let numCpus = 1;
29
+ if (typeof stream.cpu_stats.online_cpus === 'number' && stream.cpu_stats.online_cpus > 0) {
30
+ numCpus = stream.cpu_stats.online_cpus;
31
+ } else if (
32
+ stream.cpu_stats.cpu_usage &&
33
+ Array.isArray(stream.cpu_stats.cpu_usage.percpu_usage) &&
34
+ stream.cpu_stats.cpu_usage.percpu_usage.length > 0
35
+ ) {
36
+ numCpus = stream.cpu_stats.cpu_usage.percpu_usage.length;
37
+ }
22
38
 
23
39
  // Calculate normalized CPU percentage
24
40
  const cpuPercent = systemDelta > 0
@@ -40,9 +56,12 @@ export async function getStats(containerId) {
40
56
  .reduce((a, b) => a + b, 0)
41
57
  : 0;
42
58
 
43
- return {
59
+ const snapshot = {
44
60
  cpuPercent: cpuPercent.toFixed(1),
45
61
  memPercent: memPercent.toFixed(1),
46
62
  netIO: { rx, tx },
47
63
  };
64
+
65
+ logger.debug("Stats fetched for container %s", containerId, snapshot);
66
+ return snapshot;
48
67
  }
@@ -1,4 +1,4 @@
1
- import { docker } from "../dockerService";
1
+ import { docker } from "../dockerService.js";
2
2
 
3
3
  /**
4
4
  * Check whether an image exists locally.
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "child_process";
2
+ import { EXIT_DELAY } from "./constants";
2
3
 
3
4
  /**
4
5
  * Show an exit message using provided setters, clear the terminal and exit after a delay.
@@ -10,7 +11,7 @@ import { spawn } from "child_process";
10
11
  * @param {string} [params.color] - Color for the message
11
12
  * @param {number} [params.delay] - Delay in milliseconds before exiting
12
13
  */
13
- export function exitWithMessage({ setMessage, setMessageColor, message = "Exiting...", color = "yellow", delay = 1500 }) {
14
+ export function exitWithMessage({ setMessage, setMessageColor, message = "Exiting...", color = "yellow", delay = EXIT_DELAY }) {
14
15
  setMessage(message);
15
16
  setMessageColor(color);
16
17
  setTimeout(() => {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Supported log levels in ascending verbosity.
3
+ * Lower numeric values represent higher severity.
4
+ */
5
+ const LOG_LEVELS = {
6
+ error: 0,
7
+ warn: 1,
8
+ info: 2,
9
+ debug: 3
10
+ };
11
+
12
+ /**
13
+ * @typedef {Object} LogEntry
14
+ * @property {string} level Severity string (error, warn, info, debug)
15
+ * @property {string} message Primary log message
16
+ * @property {any[]} args Additional payload values forwarded to listeners
17
+ * @property {string} timestamp ISO timestamp when the entry was created
18
+ * @property {string} formatted Preformatted message used for console output
19
+ */
20
+
21
+ /**
22
+ * @callback LogListener
23
+ * @param {LogEntry} entry Log entry data emitted by the logger
24
+ */
25
+
26
+ function parseLevel(value) {
27
+ if (typeof value !== "string") {
28
+ return null;
29
+ }
30
+ const normalized = value.trim().toLowerCase();
31
+ if (normalized in LOG_LEVELS) {
32
+ return LOG_LEVELS[normalized];
33
+ }
34
+ return null;
35
+ }
36
+
37
+ const envLevel =
38
+ parseLevel(process.env.CDD_LOG_LEVEL) ??
39
+ parseLevel(process.env.LOG_LEVEL) ??
40
+ LOG_LEVELS.info;
41
+
42
+ const currentLevel = typeof envLevel === "number" ? envLevel : LOG_LEVELS.info;
43
+
44
+ const listeners = new Set();
45
+
46
+ function shouldLog(level) {
47
+ const target = LOG_LEVELS[level];
48
+ return typeof target === "number" && target <= currentLevel;
49
+ }
50
+
51
+ function log(level, message, ...args) {
52
+ if (!shouldLog(level)) {
53
+ return;
54
+ }
55
+
56
+ const timestamp = new Date().toISOString();
57
+ const upperLevel = level.toUpperCase();
58
+ const formatted = `[${timestamp}] [${upperLevel}] ${message}`;
59
+ const entry = { level, message, args, timestamp, formatted };
60
+
61
+ const shouldWriteToConsole =
62
+ listeners.size === 0 || level === "error" || level === "warn";
63
+
64
+ if (shouldWriteToConsole) {
65
+ const method = level === "debug" ? "log" : level;
66
+ if (typeof console[method] === "function") {
67
+ console[method](formatted, ...args);
68
+ } else {
69
+ console.log(formatted, ...args);
70
+ }
71
+ }
72
+
73
+ listeners.forEach((listener) => {
74
+ try {
75
+ listener(entry);
76
+ } catch (err) {
77
+ // Ignore listener errors so logging remains resilient.
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Shared logger instance with leveled output and in-memory subscriptions.
84
+ * Consumers can import `{ logger }` to emit messages or to listen for
85
+ * log entries (used by the CLI debug panel).
86
+ */
87
+ export const logger = {
88
+ debug(message, ...args) {
89
+ log("debug", message, ...args);
90
+ },
91
+ info(message, ...args) {
92
+ log("info", message, ...args);
93
+ },
94
+ warn(message, ...args) {
95
+ log("warn", message, ...args);
96
+ },
97
+ error(message, ...args) {
98
+ log("error", message, ...args);
99
+ },
100
+ /**
101
+ * Subscribe to log events.
102
+ *
103
+ * @param {LogListener} listener Listener invoked for every log entry that passes the current level filter.
104
+ * @returns {function(): void} Cleanup function that removes the listener when called.
105
+ */
106
+ subscribe(listener) {
107
+ if (typeof listener !== "function") {
108
+ return () => {};
109
+ }
110
+ listeners.add(listener);
111
+ return () => listeners.delete(listener);
112
+ }
113
+ };
@@ -0,0 +1,18 @@
1
+ import { logger } from "./logger.js";
2
+
3
+ /**
4
+ * Call a function safely: if it's a function call it with provided args and
5
+ * catch any errors so they don't bubble to the host application.
6
+ *
7
+ * @param {Function} fn
8
+ * @param {...any} args
9
+ */
10
+ export function safeCall(fn, ...args) {
11
+ if (typeof fn === 'function') {
12
+ try {
13
+ return fn(...args);
14
+ } catch (e) {
15
+ logger.error("safeCall caught error", e);
16
+ }
17
+ }
18
+ }
@@ -1,8 +1,17 @@
1
1
  // Reusable validations for ports and environment variables
2
2
 
3
+ /**
4
+ * Validate a comma-separated list of port mappings in the form "host:container".
5
+ * Examples of valid input: "8080:80, 3000:3000"
6
+ *
7
+ * @param {string} portInput - Comma-separated port mappings
8
+ * @returns {boolean} True when all mappings are valid, false otherwise
9
+ */
3
10
  export function validatePorts(portInput) {
11
+ // Empty or whitespace-only input is considered valid (ports are optional)
12
+ if (!portInput || !portInput.trim()) return true;
4
13
  const ports = portInput.split(",").map(p => p.trim()).filter(Boolean);
5
- if (ports.length === 0) return false;
14
+ if (ports.length === 0) return true;
6
15
  const invalid = ports.find(pair => {
7
16
  const [host, cont] = pair.split(":");
8
17
  return !host || !cont || isNaN(Number(host)) || isNaN(Number(cont));
@@ -10,9 +19,18 @@ export function validatePorts(portInput) {
10
19
  return !invalid;
11
20
  }
12
21
 
22
+ /**
23
+ * Validate environment variable input as comma-separated VAR=value pairs.
24
+ * Empty or whitespace-only input is considered valid (no env vars).
25
+ * Variable names must start with a letter or underscore and contain only
26
+ * alphanumeric characters and underscores.
27
+ *
28
+ * @param {string} envInput - Comma-separated environment variable assignments
29
+ * @returns {boolean} True when all env var assignments are valid, false otherwise
30
+ */
13
31
  export function validateEnvVars(envInput) {
14
32
  if (!envInput || !envInput.trim()) return true; // Empty is valid
15
-
33
+
16
34
  const vars = envInput.split(",").map(v => v.trim()).filter(Boolean);
17
35
  const invalid = vars.find(v => {
18
36
  const parts = v.split("=");
@@ -23,6 +41,6 @@ export function validateEnvVars(envInput) {
23
41
  if (!/^[A-Z_][A-Z0-9_]*$/i.test(varName)) return true;
24
42
  return false;
25
43
  });
26
-
44
+
27
45
  return !invalid;
28
46
  }
@@ -1,4 +1,5 @@
1
1
  import { useState } from "react";
2
+ import { safeCall } from "../../helpers/safeCall.js";
2
3
  import { startContainer as svcStartContainer, stopContainer as svcStopContainer, restartContainer as svcRestartContainer, removeContainer as svcRemoveContainer } from "../../helpers/dockerService/serviceComponents/containerActions.js";
3
4
 
4
5
  /**
@@ -36,9 +37,9 @@ export function useContainerActions({ containers, onAction }) {
36
37
  setMessageColor("yellow");
37
38
  try {
38
39
  await actionFn(container.id);
39
- setMessage(`${actionLabel} container successful.`);
40
- setMessageColor("green");
41
- if (onAction) onAction();
40
+ setMessage(`${actionLabel} container successful.`);
41
+ setMessageColor("green");
42
+ safeCall(onAction);
42
43
  } catch (err) {
43
44
  setMessage(`Failed to ${actionLabel.toLowerCase()} container: ${err.message}`);
44
45
  setMessageColor("red");
@@ -1,5 +1,6 @@
1
1
  import { useState } from "react";
2
- import { validatePorts, validateEnvVars } from "../../helpers/validationHelpers.js";
2
+ import { validatePorts } from "../../helpers/validationHelpers.js";
3
+ import { safeCall } from "../../helpers/safeCall.js";
3
4
 
4
5
  /**
5
6
  * Custom hook to manage the container creation flow, step by step.
@@ -60,8 +61,8 @@ export function useContainerCreation({ onCreate, onCancel, dbImages = [] }) {
60
61
  return;
61
62
  }
62
63
  if (step === 3) {
63
- // Final step: call onCreate with all data
64
- onCreate({ imageName, containerName, portInput, envInput });
64
+ // Final step: call onCreate with all data (safely)
65
+ safeCall(onCreate, { imageName, containerName, portInput, envInput });
65
66
  }
66
67
  }
67
68
 
@@ -76,7 +77,7 @@ export function useContainerCreation({ onCreate, onCancel, dbImages = [] }) {
76
77
  setEnvInput("");
77
78
  setMessage("");
78
79
  setMessageColor("yellow");
79
- if (onCancel) onCancel();
80
+ safeCall(onCancel);
80
81
  }
81
82
 
82
83
  return {
@@ -1,4 +1,5 @@
1
1
  import { useState, useRef } from "react";
2
+ import { safeCall } from "../../helpers/safeCall.js";
2
3
 
3
4
  /**
4
5
  * Custom hook to manage the logs viewer state and stream reference.
@@ -33,7 +34,7 @@ export function useLogsViewer() {
33
34
  setShowLogs(false);
34
35
  setLogs([]);
35
36
  if (logsStreamRef.current) {
36
- logsStreamRef.current.destroy?.();
37
+ safeCall(logsStreamRef.current.destroy?.bind(logsStreamRef.current));
37
38
  logsStreamRef.current = null;
38
39
  }
39
40
  }