cdd-cli 4.0.0 → 4.2.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 (135) hide show
  1. package/README.es.md +60 -26
  2. package/README.md +60 -26
  3. package/dist/App.js +1 -0
  4. package/dist/components/ContainerCreationPrompt.js +14 -0
  5. package/dist/components/PromptField.js +13 -6
  6. package/dist/components/TextField.js +54 -0
  7. package/dist/components/UsageMenu.js +1 -1
  8. package/dist/helpers/appState.js +26 -0
  9. package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
  10. package/dist/helpers/textEditing.js +293 -0
  11. package/dist/hooks/creation/useContainerCreation.js +132 -15
  12. package/dist/hooks/useContainerCommandRouter.js +12 -2
  13. package/dist/hooks/useControls.js +21 -26
  14. package/dist/hooks/useShellMode.js +101 -0
  15. package/dist/index.js +3 -1
  16. package/package.json +4 -1
  17. package/.atl/skill-registry.md +0 -30
  18. package/.editorconfig +0 -12
  19. package/.eslintrc.json +0 -30
  20. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  21. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  22. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
  23. package/.github/workflows/ci.yml +0 -32
  24. package/.github/workflows/docs.yml +0 -42
  25. package/.prettierignore +0 -4
  26. package/.prettierrc +0 -8
  27. package/.~lock.ROADMAP.md# +0 -1
  28. package/CHANGELOG.md +0 -216
  29. package/CODE_OF_CONDUCT.md +0 -12
  30. package/CONTRIBUTING.md +0 -35
  31. package/ROADMAP.md +0 -647
  32. package/SECURITY.md +0 -5
  33. package/__mocks__/ink.cjs +0 -17
  34. package/_config.yml +0 -13
  35. package/babel.config.cjs +0 -23
  36. package/coverage/clover.xml +0 -575
  37. package/coverage/coverage-final.json +0 -17
  38. package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +0 -400
  39. package/coverage/lcov-report/ControlsHUD.jsx.html +0 -256
  40. package/coverage/lcov-report/base.css +0 -224
  41. package/coverage/lcov-report/block-navigation.js +0 -87
  42. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -394
  43. package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
  44. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -268
  45. package/coverage/lcov-report/components/index.html +0 -146
  46. package/coverage/lcov-report/favicon.png +0 -0
  47. package/coverage/lcov-report/helpers/constants.js.html +0 -352
  48. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
  49. package/coverage/lcov-report/helpers/dockerHubService.js.html +0 -310
  50. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
  51. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
  52. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
  53. package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
  54. package/coverage/lcov-report/helpers/index.html +0 -206
  55. package/coverage/lcov-report/helpers/logger.js.html +0 -424
  56. package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
  57. package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
  58. package/coverage/lcov-report/hooks/creation/index.html +0 -146
  59. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
  60. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -982
  61. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
  62. package/coverage/lcov-report/hooks/debug/index.html +0 -116
  63. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
  64. package/coverage/lcov-report/hooks/index.html +0 -161
  65. package/coverage/lcov-report/hooks/navigation/index.html +0 -116
  66. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
  67. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
  68. package/coverage/lcov-report/hooks/useControls.js.html +0 -868
  69. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
  70. package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
  71. package/coverage/lcov-report/index.html +0 -176
  72. package/coverage/lcov-report/prettify.css +0 -1
  73. package/coverage/lcov-report/prettify.js +0 -2
  74. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  75. package/coverage/lcov-report/sorter.js +0 -210
  76. package/coverage/lcov.info +0 -1163
  77. package/fix-imports.cjs +0 -54
  78. package/jest.config.cjs +0 -14
  79. package/src/App.jsx +0 -102
  80. package/src/components/ContainerCreationPrompt.jsx +0 -107
  81. package/src/components/ContainerList.jsx +0 -32
  82. package/src/components/ContainerRow.jsx +0 -84
  83. package/src/components/ContainerSection.jsx +0 -24
  84. package/src/components/ControlsHUD.jsx +0 -66
  85. package/src/components/Footer.jsx +0 -15
  86. package/src/components/Header.jsx +0 -41
  87. package/src/components/LogViewer.jsx +0 -36
  88. package/src/components/MessageFeedback.jsx +0 -25
  89. package/src/components/PromptField.jsx +0 -48
  90. package/src/components/StatsBar.jsx +0 -70
  91. package/src/components/SuggestionPanel.jsx +0 -61
  92. package/src/components/UsageMenu.jsx +0 -52
  93. package/src/helpers/appInfo.js +0 -23
  94. package/src/helpers/constants.js +0 -89
  95. package/src/helpers/containerOptionsBuilder.js +0 -53
  96. package/src/helpers/dockerHubService.js +0 -75
  97. package/src/helpers/dockerService/dockerService.js +0 -28
  98. package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
  99. package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
  100. package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
  101. package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
  102. package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
  103. package/src/helpers/exitWithMessage.js +0 -32
  104. package/src/helpers/imageNameUtils.js +0 -32
  105. package/src/helpers/logger.js +0 -112
  106. package/src/helpers/safeCall.js +0 -18
  107. package/src/helpers/validationHelpers.js +0 -87
  108. package/src/hooks/creation/useContainerActions.js +0 -89
  109. package/src/hooks/creation/useContainerCreation.js +0 -377
  110. package/src/hooks/creation/useLogsViewer.js +0 -51
  111. package/src/hooks/debug/useDebugLogs.js +0 -34
  112. package/src/hooks/navigation/useContainerSelection.js +0 -28
  113. package/src/hooks/useContainerCommandRouter.js +0 -123
  114. package/src/hooks/useContainerStats.js +0 -44
  115. package/src/hooks/useContainers.js +0 -26
  116. package/src/hooks/useControls.js +0 -263
  117. package/src/hooks/useEraseConfirmation.js +0 -39
  118. package/src/hooks/useExitHandler.js +0 -69
  119. package/src/index.js +0 -20
  120. package/test/ContainerCreationPrompt.dom.test.js +0 -313
  121. package/test/ControlsHUD.dom.test.js +0 -125
  122. package/test/SuggestionPanel.dom.test.js +0 -83
  123. package/test/constants.test.js +0 -41
  124. package/test/containerActions.test.js +0 -229
  125. package/test/containerList.test.js +0 -79
  126. package/test/containerOptionsBuilder.test.js +0 -36
  127. package/test/dockerHubService.test.js +0 -213
  128. package/test/jest.setup.js +0 -7
  129. package/test/logger.test.js +0 -58
  130. package/test/resolveImageTag.test.js +0 -26
  131. package/test/safeCall.test.js +0 -25
  132. package/test/useContainerCreation.dom.test.js +0 -567
  133. package/test/useContainerCreationHub.dom.test.js +0 -184
  134. package/test/useControls.dom.test.js +0 -519
  135. package/test/validationHelpers.test.js +0 -111
@@ -1,282 +0,0 @@
1
- import { docker } from '../dockerService.js';
2
- import { imageExists, pullImage } from './imageUtils.js';
3
- import { TIMEOUTS, IMAGE_PROFILES } from '../../constants.js';
4
- import { logger } from '../../logger.js';
5
- import { normalizeImageName } from '../../imageNameUtils.js';
6
-
7
- /**
8
- * Helper to add timeout to promises. If the provided promise does not settle
9
- * within `ms` milliseconds it will reject with an Error.
10
- *
11
- * @template T
12
- * @param {Promise<T>} promise - Promise to wrap
13
- * @param {number} [ms=TIMEOUTS.CONTAINER_OP] - Timeout in milliseconds
14
- * @returns {Promise<T>} The original promise result or a rejection on timeout
15
- */
16
- function withTimeout(promise, ms = TIMEOUTS.CONTAINER_OP) {
17
- return Promise.race([
18
- promise,
19
- new Promise((_, reject) =>
20
- setTimeout(() => reject(new Error('Operation timed out')), ms)
21
- ),
22
- ]);
23
- }
24
-
25
- /**
26
- * Remove (delete) a container by id. Force removal so running containers are stopped first.
27
- *
28
- * @param {string} containerId - Docker container id
29
- * @returns {Promise<void>} Resolves when removal completes
30
- * @throws {Error} If Docker reports an error
31
- */
32
- export async function removeContainer(containerId) {
33
- logger.debug('Removing container %s', containerId);
34
- const container = docker.getContainer(containerId);
35
- try {
36
- await withTimeout(container.remove({ force: true }), TIMEOUTS.CONTAINER_OP);
37
- logger.info('Removed container %s', containerId);
38
- } catch (err) {
39
- logger.error('Failed to remove container %s', containerId, err);
40
- throw new Error('Error removing container: ' + err.message);
41
- }
42
- }
43
-
44
- /**
45
- * Create a new container from an image. If the image is missing locally, it will be pulled.
46
- *
47
- * @param {string} imageName - Image name (e.g. 'nginx:alpine')
48
- * @param {Object} [options] - Docker create options (Env, ExposedPorts, HostConfig, name, etc.)
49
- * @param {Object} [imageProfiles] - Image profiles map for defaultPort fallback
50
- * @returns {Promise<{id: string, ports: Array<{containerPort: string, hostPort: string, protocol: string, source: string}>}>}
51
- * @throws {Error} If image listing/pull or creation fails
52
- */
53
- export async function createContainer(
54
- imageName,
55
- options = {},
56
- imageProfiles = IMAGE_PROFILES
57
- ) {
58
- logger.info('Creating container from image %s', imageName);
59
- let exists;
60
- try {
61
- exists = await withTimeout(imageExists(imageName), 10000);
62
- } catch (err) {
63
- logger.error('Could not check local images for %s', imageName, err);
64
- throw new Error('Error listing local images: ' + err.message);
65
- }
66
- if (!exists) {
67
- logger.info('Image %s not found locally, pulling', imageName);
68
- try {
69
- await withTimeout(pullImage(imageName), TIMEOUTS.PULL_IMAGE);
70
- logger.info('Pulled image %s', imageName);
71
- } catch (err) {
72
- logger.error('Failed to pull image %s', imageName, err);
73
- throw new Error('Could not pull image: ' + err.message);
74
- }
75
- }
76
- const createOpts = {
77
- Image: imageName,
78
- Tty: true,
79
- ...options,
80
- };
81
-
82
- const hasUserDefinedPorts = Boolean(
83
- createOpts.ExposedPorts && Object.keys(createOpts.ExposedPorts).length
84
- );
85
-
86
- /** @type {Array<{containerPort: string, hostPort: string, protocol: string, source: string}>} */
87
- const assignedPorts = [];
88
-
89
- if (!hasUserDefinedPorts) {
90
- try {
91
- const image = docker.getImage(imageName);
92
- if (image && typeof image.inspect === 'function') {
93
- let usedHostPorts = new Set();
94
- if (typeof docker.listContainers === 'function') {
95
- try {
96
- const containers = await withTimeout(
97
- docker.listContainers({ all: true }),
98
- TIMEOUTS.CONTAINER_OP
99
- );
100
- containers.forEach((container) => {
101
- (container.Ports || []).forEach((portInfo) => {
102
- if (portInfo && portInfo.PublicPort) {
103
- usedHostPorts.add(String(portInfo.PublicPort));
104
- }
105
- });
106
- });
107
- } catch (err) {
108
- logger.warn(
109
- 'Could not inspect existing containers to auto-map ports',
110
- err
111
- );
112
- }
113
- }
114
- const reservePort = (port) => {
115
- const portStr = String(port);
116
- usedHostPorts.add(portStr);
117
- };
118
- const pickNextAvailablePort = (base) => {
119
- const numericBase = Number.parseInt(base, 10);
120
- if (Number.isNaN(numericBase)) {
121
- if (!usedHostPorts.has(base)) {
122
- usedHostPorts.add(base);
123
- return base;
124
- }
125
- let counter = 1;
126
- let candidate = `${base}-${counter}`;
127
- while (usedHostPorts.has(candidate)) {
128
- counter += 1;
129
- candidate = `${base}-${counter}`;
130
- }
131
- usedHostPorts.add(candidate);
132
- return candidate;
133
- }
134
- let candidate = numericBase;
135
- while (usedHostPorts.has(String(candidate))) {
136
- candidate += 1;
137
- }
138
- reservePort(candidate);
139
- return String(candidate);
140
- };
141
-
142
- const inspectData = await withTimeout(
143
- image.inspect(),
144
- TIMEOUTS.CONTAINER_OP
145
- );
146
- let exposed =
147
- inspectData?.Config?.ExposedPorts ||
148
- inspectData?.ContainerConfig?.ExposedPorts ||
149
- {};
150
- let exposedKeys = Object.keys(exposed || {});
151
-
152
- // If no ExposedPorts in image, fall back to IMAGE_PROFILES defaultPort
153
- if (!exposedKeys.length) {
154
- const baseName = normalizeImageName(imageName);
155
- const profile = imageProfiles[baseName];
156
- if (profile && profile.defaultPort) {
157
- const fallbackKey = `${profile.defaultPort}/tcp`;
158
- exposed = { [fallbackKey]: {} };
159
- exposedKeys = [fallbackKey];
160
- }
161
- }
162
-
163
- if (exposedKeys.length) {
164
- createOpts.ExposedPorts = createOpts.ExposedPorts || {};
165
- createOpts.HostConfig = { ...(createOpts.HostConfig || {}) };
166
- createOpts.HostConfig.PortBindings = {
167
- ...(createOpts.HostConfig.PortBindings || {}),
168
- };
169
- exposedKeys.forEach((portKey) => {
170
- if (!createOpts.ExposedPorts[portKey]) {
171
- createOpts.ExposedPorts[portKey] = exposed[portKey] || {};
172
- }
173
- if (
174
- !createOpts.HostConfig.PortBindings[portKey] ||
175
- !createOpts.HostConfig.PortBindings[portKey].length
176
- ) {
177
- const parts = portKey.split('/');
178
- const containerPort = parts[0];
179
- const protocol = parts[1] || 'tcp';
180
- const hostPort = pickNextAvailablePort(containerPort);
181
- createOpts.HostConfig.PortBindings[portKey] = [
182
- { HostPort: hostPort },
183
- ];
184
- assignedPorts.push({
185
- containerPort,
186
- hostPort,
187
- protocol,
188
- source: 'auto',
189
- });
190
- }
191
- });
192
- }
193
- }
194
- } catch (err) {
195
- logger.warn(
196
- 'Could not inspect image %s for default ports',
197
- imageName,
198
- err
199
- );
200
- }
201
- } else {
202
- // User-defined ports: collect them with source: "user"
203
- const portBindings = createOpts.HostConfig?.PortBindings || {};
204
- Object.entries(portBindings).forEach(([portKey, bindings]) => {
205
- const parts = portKey.split('/');
206
- const containerPort = parts[0];
207
- const protocol = parts[1] || 'tcp';
208
- (bindings || []).forEach((b) => {
209
- assignedPorts.push({
210
- containerPort,
211
- hostPort: b.HostPort,
212
- protocol,
213
- source: 'user',
214
- });
215
- });
216
- });
217
- }
218
-
219
- try {
220
- const container = await withTimeout(
221
- docker.createContainer(createOpts),
222
- TIMEOUTS.CONTAINER_OP
223
- );
224
- const containerId = container.id || container.Id;
225
- logger.info('Created container %s from image %s', containerId, imageName);
226
- return { id: containerId, ports: assignedPorts };
227
- } catch (err) {
228
- logger.error('Failed to create container from image %s', imageName, err);
229
- throw new Error('Error creating container: ' + err.message);
230
- }
231
- }
232
-
233
- /**
234
- * Start a container by id.
235
- * @param {string} containerId - Docker container id
236
- * @returns {Promise<void>}
237
- */
238
- export async function startContainer(containerId) {
239
- const container = docker.getContainer(containerId);
240
- logger.debug('Starting container %s', containerId);
241
- try {
242
- await withTimeout(container.start(), TIMEOUTS.CONTAINER_OP);
243
- logger.info('Started container %s', containerId);
244
- } catch (err) {
245
- logger.error('Failed to start container %s', containerId, err);
246
- throw err;
247
- }
248
- }
249
-
250
- /**
251
- * Stop a container by id.
252
- * @param {string} containerId - Docker container id
253
- * @returns {Promise<void>}
254
- */
255
- export async function stopContainer(containerId) {
256
- const container = docker.getContainer(containerId);
257
- logger.debug('Stopping container %s', containerId);
258
- try {
259
- await withTimeout(container.stop(), TIMEOUTS.CONTAINER_OP);
260
- logger.info('Stopped container %s', containerId);
261
- } catch (err) {
262
- logger.error('Failed to stop container %s', containerId, err);
263
- throw err;
264
- }
265
- }
266
-
267
- /**
268
- * Restart a container by id.
269
- * @param {string} containerId - Docker container id
270
- * @returns {Promise<void>}
271
- */
272
- export async function restartContainer(containerId) {
273
- const container = docker.getContainer(containerId);
274
- logger.debug('Restarting container %s', containerId);
275
- try {
276
- await withTimeout(container.restart(), TIMEOUTS.CONTAINER_OP);
277
- logger.info('Restarted container %s', containerId);
278
- } catch (err) {
279
- logger.error('Failed to restart container %s', containerId, err);
280
- throw err;
281
- }
282
- }
@@ -1,74 +0,0 @@
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 (
17
- Array.isArray(names) &&
18
- names.length > 0 &&
19
- typeof names[0] === 'string'
20
- ) {
21
- rawName = names[0];
22
- } else if (typeof names === 'string' && names.trim().length > 0) {
23
- rawName = names;
24
- } else if (
25
- typeof container?.Name === 'string' &&
26
- container.Name.trim().length > 0
27
- ) {
28
- rawName = container.Name;
29
- } else {
30
- rawName = DEFAULT_CONTAINER_NAME;
31
- }
32
-
33
- if (typeof rawName !== 'string') {
34
- return DEFAULT_CONTAINER_NAME;
35
- }
36
-
37
- const cleanedName = rawName.replace(/^\/+/u, '').trim();
38
- return cleanedName.length > 0 ? cleanedName : DEFAULT_CONTAINER_NAME;
39
- }
40
-
41
- /**
42
- * Return a list of containers with normalized fields for the UI.
43
- * @returns {Promise<Array<Object>>}
44
- */
45
- export async function getContainers() {
46
- logger.debug('Listing containers');
47
- try {
48
- const containers = await docker.listContainers({ all: true });
49
- logger.debug('Retrieved %d containers', containers.length);
50
- return containers.map((container) => ({
51
- id: container.Id,
52
- name: normalizeContainerName(container),
53
- image: container.Image,
54
- state: container.State,
55
- status: container.Status,
56
- ports: (() => {
57
- const publicPorts = container.Ports.filter(
58
- (port) => port.PublicPort
59
- ).map((port) => `${port.PublicPort}:${port.PrivatePort}`);
60
- if (publicPorts.length > 0) {
61
- return [...new Set(publicPorts)];
62
- }
63
- // Fall back to exposed private ports when there are no bindings.
64
- const privatePorts = container.Ports.filter(
65
- (port) => port.PrivatePort
66
- ).map((port) => `${port.PrivatePort}`);
67
- return [...new Set(privatePorts)];
68
- })(),
69
- }));
70
- } catch (err) {
71
- logger.error('Failed to list containers', err);
72
- throw err;
73
- }
74
- }
@@ -1,60 +0,0 @@
1
- import { docker } from '../dockerService.js';
2
- import { safeCall } from '../../safeCall.js';
3
- import { logger } from '../../logger.js';
4
-
5
- /**
6
- * Return a stream of logs from a container and call callbacks for events.
7
- *
8
- * @param {string} containerId - Docker container id
9
- * @param {Function} onData - Called with chunk string when data arrives
10
- * @param {Function} onEnd - Called when stream ends
11
- * @param {Function} onError - Called on error
12
- */
13
- export function getLogsStream(containerId, onData, onEnd, onError) {
14
- // Use shared safeCall util to call optional callbacks safely
15
-
16
- try {
17
- const container = docker.getContainer(containerId);
18
- logger.debug('Opening logs stream for container %s', containerId);
19
- container.logs(
20
- {
21
- follow: true,
22
- stdout: true,
23
- stderr: true,
24
- tail: 100,
25
- },
26
- (err, stream) => {
27
- if (err) {
28
- logger.error(
29
- 'Failed to open logs stream for container %s',
30
- containerId,
31
- err
32
- );
33
- safeCall(onError, err);
34
- return;
35
- }
36
- logger.info('Logs stream established for container %s', containerId);
37
- stream.on('data', (chunk) => safeCall(onData, chunk.toString()));
38
- stream.on('end', () => {
39
- logger.debug('Logs stream ended for container %s', containerId);
40
- safeCall(onEnd);
41
- });
42
- stream.on('error', (streamErr) => {
43
- logger.error(
44
- 'Logs stream error for container %s',
45
- containerId,
46
- streamErr
47
- );
48
- safeCall(onError, streamErr);
49
- });
50
- }
51
- );
52
- } catch (err) {
53
- logger.error(
54
- 'Unexpected error while opening logs stream for container %s',
55
- containerId,
56
- err
57
- );
58
- safeCall(onError, err);
59
- }
60
- }
@@ -1,69 +0,0 @@
1
- import { docker } from '../dockerService.js';
2
- import { logger } from '../../logger.js';
3
-
4
- /**
5
- * Retrieve a snapshot of container resource usage (CPU, memory, network).
6
- *
7
- * @param {string} containerId - Docker container id
8
- * @returns {Promise<Object>} Object with cpuPercent, memPercent and netIO {rx,tx}
9
- */
10
- export async function getStats(containerId) {
11
- const container = docker.getContainer(containerId);
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
- }
20
-
21
- const cpuDelta =
22
- stream.cpu_stats.cpu_usage.total_usage -
23
- stream.precpu_stats.cpu_usage.total_usage;
24
- const systemDelta =
25
- stream.cpu_stats.system_cpu_usage - stream.precpu_stats.system_cpu_usage;
26
-
27
- // Get number of CPUs for proper normalization
28
- let numCpus = 1;
29
- if (
30
- typeof stream.cpu_stats.online_cpus === 'number' &&
31
- stream.cpu_stats.online_cpus > 0
32
- ) {
33
- numCpus = stream.cpu_stats.online_cpus;
34
- } else if (
35
- stream.cpu_stats.cpu_usage &&
36
- Array.isArray(stream.cpu_stats.cpu_usage.percpu_usage) &&
37
- stream.cpu_stats.cpu_usage.percpu_usage.length > 0
38
- ) {
39
- numCpus = stream.cpu_stats.cpu_usage.percpu_usage.length;
40
- }
41
-
42
- // Calculate normalized CPU percentage
43
- const cpuPercent =
44
- systemDelta > 0 ? (cpuDelta / systemDelta) * numCpus * 100 : 0;
45
-
46
- const memUsage = stream.memory_stats.usage || 0;
47
- const memLimit = stream.memory_stats.limit || 1;
48
- const memPercent = (memUsage / memLimit) * 100;
49
-
50
- const rx = stream.networks
51
- ? Object.values(stream.networks)
52
- .map((n) => n.rx_bytes)
53
- .reduce((a, b) => a + b, 0)
54
- : 0;
55
- const tx = stream.networks
56
- ? Object.values(stream.networks)
57
- .map((n) => n.tx_bytes)
58
- .reduce((a, b) => a + b, 0)
59
- : 0;
60
-
61
- const snapshot = {
62
- cpuPercent: cpuPercent.toFixed(1),
63
- memPercent: memPercent.toFixed(1),
64
- netIO: { rx, tx },
65
- };
66
-
67
- logger.debug('Stats fetched for container %s', containerId, snapshot);
68
- return snapshot;
69
- }
@@ -1,32 +0,0 @@
1
- import { docker } from '../dockerService.js';
2
-
3
- /**
4
- * Check whether an image exists locally.
5
- * @param {string} imageName - Image name or tag
6
- * @returns {Promise<boolean>}
7
- */
8
- export async function imageExists(imageName) {
9
- const images = await docker.listImages();
10
- return images.some(
11
- (img) =>
12
- (img.RepoTags || []).includes(imageName) ||
13
- (img.RepoDigests || []).some((d) => d.includes(imageName))
14
- );
15
- }
16
-
17
- /**
18
- * Pull an image from the registry.
19
- * @param {string} imageName - Image name to pull
20
- * @returns {Promise<void>}
21
- */
22
- export async function pullImage(imageName) {
23
- await new Promise((resolve, reject) => {
24
- docker.pull(imageName, (err, stream) => {
25
- if (err) return reject(new Error('Error pulling image: ' + err.message));
26
- docker.modem.followProgress(stream, (pullErr) => {
27
- if (pullErr) reject(new Error('Error during pull: ' + pullErr.message));
28
- else resolve();
29
- });
30
- });
31
- });
32
- }
@@ -1,32 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import { EXIT_DELAY } from './constants';
3
-
4
- /**
5
- * Show an exit message using provided setters, clear the terminal and exit after a delay.
6
- *
7
- * @param {Object} params
8
- * @param {Function} params.setMessage - Setter for message text
9
- * @param {Function} params.setMessageColor - Setter for message color
10
- * @param {string} [params.message] - Message to display
11
- * @param {string} [params.color] - Color for the message
12
- * @param {number} [params.delay] - Delay in milliseconds before exiting
13
- */
14
- export function exitWithMessage({
15
- setMessage,
16
- setMessageColor,
17
- message = 'Exiting...',
18
- color = 'yellow',
19
- delay = EXIT_DELAY,
20
- }) {
21
- setMessage(message);
22
- setMessageColor(color);
23
- setTimeout(() => {
24
- setMessage('');
25
- if (process.platform === 'win32') {
26
- spawn('cmd', ['/c', 'cls'], { stdio: 'inherit' });
27
- } else {
28
- spawn('clear', [], { stdio: 'inherit' });
29
- }
30
- process.exit();
31
- }, delay);
32
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Utility for normalizing Docker image names.
3
- * Extracted from validationHelpers.js and containerActions.js — both had
4
- * identical implementations; consolidated here to avoid duplication.
5
- */
6
-
7
- /**
8
- * Normalize an image name to its base name (strips registry prefix and tag).
9
- *
10
- * Examples:
11
- * "docker.io/library/postgres:16-alpine" → "postgres"
12
- * "nginx:alpine" → "nginx"
13
- * "myregistry.io/myorg/myapp:latest" → "myapp"
14
- *
15
- * @param {string} imageName - Raw image name (from user input or Docker API)
16
- * @returns {string} Base image name in lowercase, empty string if input is falsy
17
- */
18
- export function normalizeImageName(imageName) {
19
- if (!imageName) return '';
20
- // Remove registry prefix (everything up to and including the last '/')
21
- let name = imageName;
22
- const slashIdx = name.lastIndexOf('/');
23
- if (slashIdx !== -1) {
24
- name = name.slice(slashIdx + 1);
25
- }
26
- // Remove tag (everything from the first ':' onward)
27
- const colonIdx = name.indexOf(':');
28
- if (colonIdx !== -1) {
29
- name = name.slice(0, colonIdx);
30
- }
31
- return name.toLowerCase();
32
- }
@@ -1,112 +0,0 @@
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 = listeners.size === 0;
62
-
63
- if (shouldWriteToConsole) {
64
- const method = level === 'debug' ? 'log' : level;
65
- if (typeof console[method] === 'function') {
66
- console[method](formatted, ...args);
67
- } else {
68
- console.log(formatted, ...args);
69
- }
70
- }
71
-
72
- listeners.forEach((listener) => {
73
- try {
74
- listener(entry);
75
- } catch (err) {
76
- // Ignore listener errors so logging remains resilient.
77
- }
78
- });
79
- }
80
-
81
- /**
82
- * Shared logger instance with leveled output and in-memory subscriptions.
83
- * Consumers can import `{ logger }` to emit messages or to listen for
84
- * log entries (used by the CLI debug panel).
85
- */
86
- export const logger = {
87
- debug(message, ...args) {
88
- log('debug', message, ...args);
89
- },
90
- info(message, ...args) {
91
- log('info', message, ...args);
92
- },
93
- warn(message, ...args) {
94
- log('warn', message, ...args);
95
- },
96
- error(message, ...args) {
97
- log('error', message, ...args);
98
- },
99
- /**
100
- * Subscribe to log events.
101
- *
102
- * @param {LogListener} listener Listener invoked for every log entry that passes the current level filter.
103
- * @returns {function(): void} Cleanup function that removes the listener when called.
104
- */
105
- subscribe(listener) {
106
- if (typeof listener !== 'function') {
107
- return () => {};
108
- }
109
- listeners.add(listener);
110
- return () => listeners.delete(listener);
111
- },
112
- };