cdd-cli 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.atl/skill-registry.md +30 -0
- package/.editorconfig +12 -0
- package/.eslintrc.json +1 -1
- package/.prettierignore +4 -0
- package/.prettierrc +8 -0
- package/.~lock.ROADMAP.md# +1 -0
- package/CHANGELOG.md +80 -0
- package/README.es.md +198 -0
- package/README.md +81 -23
- package/ROADMAP.md +647 -0
- package/SECURITY.md +5 -0
- package/__mocks__/ink.cjs +17 -0
- package/coverage/clover.xml +471 -500
- package/coverage/coverage-final.json +16 -16
- package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +400 -0
- package/coverage/lcov-report/ControlsHUD.jsx.html +256 -0
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +394 -0
- package/coverage/lcov-report/components/PromptField.jsx.html +229 -0
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +268 -0
- package/coverage/lcov-report/components/index.html +146 -0
- package/coverage/lcov-report/helpers/constants.js.html +139 -22
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +75 -42
- package/coverage/lcov-report/helpers/dockerHubService.js.html +310 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +162 -84
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +50 -32
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +21 -21
- package/coverage/lcov-report/helpers/imageNameUtils.js.html +181 -0
- package/coverage/lcov-report/helpers/index.html +84 -54
- package/coverage/lcov-report/helpers/logger.js.html +68 -68
- package/coverage/lcov-report/helpers/safeCall.js.html +15 -15
- package/coverage/lcov-report/helpers/validationHelpers.js.html +66 -114
- package/coverage/lcov-report/hooks/creation/index.html +20 -20
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +38 -17
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +593 -89
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +7 -7
- package/coverage/lcov-report/hooks/debug/index.html +5 -5
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +35 -20
- package/coverage/lcov-report/hooks/index.html +40 -40
- package/coverage/lcov-report/hooks/navigation/index.html +19 -19
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +38 -29
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +119 -80
- package/coverage/lcov-report/hooks/useControls.js.html +306 -123
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +8 -8
- package/coverage/lcov-report/hooks/useExitHandler.js.html +50 -41
- package/coverage/lcov-report/index.html +52 -67
- package/coverage/lcov.info +899 -952
- package/dist/App.js +18 -12
- package/dist/components/ContainerCreationPrompt.js +50 -13
- package/dist/components/ContainerList.js +3 -3
- package/dist/components/ContainerRow.js +20 -20
- package/dist/components/ContainerSection.js +3 -3
- package/dist/components/ControlsHUD.js +106 -0
- package/dist/components/Footer.js +2 -2
- package/dist/components/Header.js +7 -7
- package/dist/components/LogViewer.js +3 -3
- package/dist/components/MessageFeedback.js +2 -2
- package/dist/components/PromptField.js +4 -4
- package/dist/components/StatsBar.js +5 -5
- package/dist/components/SuggestionPanel.js +53 -0
- package/dist/components/UsageMenu.js +3 -3
- package/dist/helpers/appInfo.js +7 -7
- package/dist/helpers/constants.js +154 -1
- package/dist/helpers/containerOptionsBuilder.js +3 -3
- package/dist/helpers/dockerHubService.js +111 -0
- package/dist/helpers/dockerService/dockerService.js +1 -1
- package/dist/helpers/dockerService/serviceComponents/containerActions.js +99 -40
- package/dist/helpers/dockerService/serviceComponents/containerList.js +11 -11
- package/dist/helpers/dockerService/serviceComponents/containerLogs.js +9 -9
- package/dist/helpers/dockerService/serviceComponents/containerStats.js +5 -5
- package/dist/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
- package/dist/helpers/exitWithMessage.js +10 -10
- package/dist/helpers/imageNameUtils.js +32 -0
- package/dist/helpers/logger.js +13 -13
- package/dist/helpers/safeCall.js +2 -2
- package/dist/helpers/validationHelpers.js +76 -21
- package/dist/hooks/creation/useContainerActions.js +28 -11
- package/dist/hooks/creation/useContainerCreation.js +452 -62
- package/dist/hooks/creation/useLogsViewer.js +2 -2
- package/dist/hooks/debug/useDebugLogs.js +4 -4
- package/dist/hooks/useContainerCommandRouter.js +14 -14
- package/dist/hooks/useContainers.js +3 -3
- package/dist/hooks/useControls.js +85 -38
- package/dist/hooks/useEraseConfirmation.js +3 -3
- package/dist/index.js +9 -2
- package/jest.config.cjs +7 -0
- package/package.json +7 -3
- package/src/App.jsx +39 -16
- package/src/components/ContainerCreationPrompt.jsx +54 -10
- package/src/components/ContainerList.jsx +3 -3
- package/src/components/ContainerRow.jsx +19 -16
- package/src/components/ContainerSection.jsx +3 -3
- package/src/components/ControlsHUD.jsx +66 -0
- package/src/components/Footer.jsx +2 -2
- package/src/components/Header.jsx +7 -7
- package/src/components/LogViewer.jsx +5 -3
- package/src/components/MessageFeedback.jsx +2 -2
- package/src/components/PromptField.jsx +4 -4
- package/src/components/StatsBar.jsx +11 -7
- package/src/components/SuggestionPanel.jsx +61 -0
- package/src/components/UsageMenu.jsx +18 -13
- package/src/helpers/appInfo.js +7 -7
- package/src/helpers/constants.js +48 -9
- package/src/helpers/containerOptionsBuilder.js +16 -5
- package/src/helpers/dockerHubService.js +75 -0
- package/src/helpers/dockerService/dockerService.js +1 -1
- package/src/helpers/dockerService/serviceComponents/containerActions.js +87 -61
- package/src/helpers/dockerService/serviceComponents/containerList.js +31 -25
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +41 -26
- package/src/helpers/dockerService/serviceComponents/containerStats.js +13 -11
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +5 -4
- package/src/helpers/exitWithMessage.js +13 -7
- package/src/helpers/imageNameUtils.js +32 -0
- package/src/helpers/logger.js +15 -16
- package/src/helpers/safeCall.js +2 -2
- package/src/helpers/validationHelpers.js +13 -29
- package/src/hooks/creation/useContainerActions.js +32 -12
- package/src/hooks/creation/useContainerCreation.js +293 -47
- package/src/hooks/creation/useLogsViewer.js +2 -2
- package/src/hooks/debug/useDebugLogs.js +15 -10
- package/src/hooks/navigation/useContainerSelection.js +17 -14
- package/src/hooks/useContainerCommandRouter.js +71 -58
- package/src/hooks/useContainers.js +4 -4
- package/src/hooks/useControls.js +135 -72
- package/src/hooks/useEraseConfirmation.js +3 -3
- package/src/hooks/useExitHandler.js +29 -26
- package/src/index.js +8 -4
- package/test/ContainerCreationPrompt.dom.test.js +313 -0
- package/test/ControlsHUD.dom.test.js +125 -0
- package/test/SuggestionPanel.dom.test.js +83 -0
- package/test/constants.test.js +41 -0
- package/test/dockerHubService.test.js +213 -0
- package/test/logger.test.js +58 -0
- package/test/resolveImageTag.test.js +26 -0
- package/test/useContainerCreation.dom.test.js +428 -2
- package/test/useContainerCreationHub.dom.test.js +184 -0
- package/test/useControls.dom.test.js +406 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { docker } from
|
|
2
|
-
import { imageExists, pullImage } from
|
|
3
|
-
import { TIMEOUTS, IMAGE_PROFILES } from
|
|
4
|
-
import { logger } from
|
|
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';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Helper to add timeout to promises. If the provided promise does not settle
|
|
@@ -15,9 +16,9 @@ import { logger } from "../../logger.js";
|
|
|
15
16
|
function withTimeout(promise, ms = TIMEOUTS.CONTAINER_OP) {
|
|
16
17
|
return Promise.race([
|
|
17
18
|
promise,
|
|
18
|
-
new Promise((_, reject) =>
|
|
19
|
+
new Promise((_, reject) =>
|
|
19
20
|
setTimeout(() => reject(new Error('Operation timed out')), ms)
|
|
20
|
-
)
|
|
21
|
+
),
|
|
21
22
|
]);
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -29,34 +30,17 @@ function withTimeout(promise, ms = TIMEOUTS.CONTAINER_OP) {
|
|
|
29
30
|
* @throws {Error} If Docker reports an error
|
|
30
31
|
*/
|
|
31
32
|
export async function removeContainer(containerId) {
|
|
32
|
-
logger.debug(
|
|
33
|
+
logger.debug('Removing container %s', containerId);
|
|
33
34
|
const container = docker.getContainer(containerId);
|
|
34
35
|
try {
|
|
35
36
|
await withTimeout(container.remove({ force: true }), TIMEOUTS.CONTAINER_OP);
|
|
36
|
-
logger.info(
|
|
37
|
+
logger.info('Removed container %s', containerId);
|
|
37
38
|
} catch (err) {
|
|
38
|
-
logger.error(
|
|
39
|
+
logger.error('Failed to remove container %s', containerId, err);
|
|
39
40
|
throw new Error('Error removing container: ' + err.message);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
/**
|
|
44
|
-
* Normalize image name to base name (strips registry prefix and tag).
|
|
45
|
-
* e.g. "docker.io/library/postgres:16" → "postgres"
|
|
46
|
-
*
|
|
47
|
-
* @param {string} imageName - Raw image name
|
|
48
|
-
* @returns {string} Base name (lowercase)
|
|
49
|
-
*/
|
|
50
|
-
function normalizeImageName(imageName) {
|
|
51
|
-
if (!imageName) return "";
|
|
52
|
-
let name = imageName;
|
|
53
|
-
const slashIdx = name.lastIndexOf("/");
|
|
54
|
-
if (slashIdx !== -1) name = name.slice(slashIdx + 1);
|
|
55
|
-
const colonIdx = name.indexOf(":");
|
|
56
|
-
if (colonIdx !== -1) name = name.slice(0, colonIdx);
|
|
57
|
-
return name.toLowerCase();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
44
|
/**
|
|
61
45
|
* Create a new container from an image. If the image is missing locally, it will be pulled.
|
|
62
46
|
*
|
|
@@ -66,22 +50,26 @@ function normalizeImageName(imageName) {
|
|
|
66
50
|
* @returns {Promise<{id: string, ports: Array<{containerPort: string, hostPort: string, protocol: string, source: string}>}>}
|
|
67
51
|
* @throws {Error} If image listing/pull or creation fails
|
|
68
52
|
*/
|
|
69
|
-
export async function createContainer(
|
|
70
|
-
|
|
53
|
+
export async function createContainer(
|
|
54
|
+
imageName,
|
|
55
|
+
options = {},
|
|
56
|
+
imageProfiles = IMAGE_PROFILES
|
|
57
|
+
) {
|
|
58
|
+
logger.info('Creating container from image %s', imageName);
|
|
71
59
|
let exists;
|
|
72
60
|
try {
|
|
73
61
|
exists = await withTimeout(imageExists(imageName), 10000);
|
|
74
62
|
} catch (err) {
|
|
75
|
-
logger.error(
|
|
63
|
+
logger.error('Could not check local images for %s', imageName, err);
|
|
76
64
|
throw new Error('Error listing local images: ' + err.message);
|
|
77
65
|
}
|
|
78
66
|
if (!exists) {
|
|
79
|
-
logger.info(
|
|
67
|
+
logger.info('Image %s not found locally, pulling', imageName);
|
|
80
68
|
try {
|
|
81
69
|
await withTimeout(pullImage(imageName), TIMEOUTS.PULL_IMAGE);
|
|
82
|
-
logger.info(
|
|
70
|
+
logger.info('Pulled image %s', imageName);
|
|
83
71
|
} catch (err) {
|
|
84
|
-
logger.error(
|
|
72
|
+
logger.error('Failed to pull image %s', imageName, err);
|
|
85
73
|
throw new Error('Could not pull image: ' + err.message);
|
|
86
74
|
}
|
|
87
75
|
}
|
|
@@ -91,7 +79,9 @@ export async function createContainer(imageName, options = {}, imageProfiles = I
|
|
|
91
79
|
...options,
|
|
92
80
|
};
|
|
93
81
|
|
|
94
|
-
const hasUserDefinedPorts = Boolean(
|
|
82
|
+
const hasUserDefinedPorts = Boolean(
|
|
83
|
+
createOpts.ExposedPorts && Object.keys(createOpts.ExposedPorts).length
|
|
84
|
+
);
|
|
95
85
|
|
|
96
86
|
/** @type {Array<{containerPort: string, hostPort: string, protocol: string, source: string}>} */
|
|
97
87
|
const assignedPorts = [];
|
|
@@ -99,11 +89,14 @@ export async function createContainer(imageName, options = {}, imageProfiles = I
|
|
|
99
89
|
if (!hasUserDefinedPorts) {
|
|
100
90
|
try {
|
|
101
91
|
const image = docker.getImage(imageName);
|
|
102
|
-
if (image && typeof image.inspect ===
|
|
92
|
+
if (image && typeof image.inspect === 'function') {
|
|
103
93
|
let usedHostPorts = new Set();
|
|
104
|
-
if (typeof docker.listContainers ===
|
|
94
|
+
if (typeof docker.listContainers === 'function') {
|
|
105
95
|
try {
|
|
106
|
-
const containers = await withTimeout(
|
|
96
|
+
const containers = await withTimeout(
|
|
97
|
+
docker.listContainers({ all: true }),
|
|
98
|
+
TIMEOUTS.CONTAINER_OP
|
|
99
|
+
);
|
|
107
100
|
containers.forEach((container) => {
|
|
108
101
|
(container.Ports || []).forEach((portInfo) => {
|
|
109
102
|
if (portInfo && portInfo.PublicPort) {
|
|
@@ -112,7 +105,10 @@ export async function createContainer(imageName, options = {}, imageProfiles = I
|
|
|
112
105
|
});
|
|
113
106
|
});
|
|
114
107
|
} catch (err) {
|
|
115
|
-
logger.warn(
|
|
108
|
+
logger.warn(
|
|
109
|
+
'Could not inspect existing containers to auto-map ports',
|
|
110
|
+
err
|
|
111
|
+
);
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
114
|
const reservePort = (port) => {
|
|
@@ -143,8 +139,14 @@ export async function createContainer(imageName, options = {}, imageProfiles = I
|
|
|
143
139
|
return String(candidate);
|
|
144
140
|
};
|
|
145
141
|
|
|
146
|
-
const inspectData = await withTimeout(
|
|
147
|
-
|
|
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
|
+
{};
|
|
148
150
|
let exposedKeys = Object.keys(exposed || {});
|
|
149
151
|
|
|
150
152
|
// If no ExposedPorts in image, fall back to IMAGE_PROFILES defaultPort
|
|
@@ -161,45 +163,69 @@ export async function createContainer(imageName, options = {}, imageProfiles = I
|
|
|
161
163
|
if (exposedKeys.length) {
|
|
162
164
|
createOpts.ExposedPorts = createOpts.ExposedPorts || {};
|
|
163
165
|
createOpts.HostConfig = { ...(createOpts.HostConfig || {}) };
|
|
164
|
-
createOpts.HostConfig.PortBindings = {
|
|
166
|
+
createOpts.HostConfig.PortBindings = {
|
|
167
|
+
...(createOpts.HostConfig.PortBindings || {}),
|
|
168
|
+
};
|
|
165
169
|
exposedKeys.forEach((portKey) => {
|
|
166
170
|
if (!createOpts.ExposedPorts[portKey]) {
|
|
167
171
|
createOpts.ExposedPorts[portKey] = exposed[portKey] || {};
|
|
168
172
|
}
|
|
169
|
-
if (
|
|
170
|
-
|
|
173
|
+
if (
|
|
174
|
+
!createOpts.HostConfig.PortBindings[portKey] ||
|
|
175
|
+
!createOpts.HostConfig.PortBindings[portKey].length
|
|
176
|
+
) {
|
|
177
|
+
const parts = portKey.split('/');
|
|
171
178
|
const containerPort = parts[0];
|
|
172
|
-
const protocol = parts[1] ||
|
|
179
|
+
const protocol = parts[1] || 'tcp';
|
|
173
180
|
const hostPort = pickNextAvailablePort(containerPort);
|
|
174
|
-
createOpts.HostConfig.PortBindings[portKey] = [
|
|
175
|
-
|
|
181
|
+
createOpts.HostConfig.PortBindings[portKey] = [
|
|
182
|
+
{ HostPort: hostPort },
|
|
183
|
+
];
|
|
184
|
+
assignedPorts.push({
|
|
185
|
+
containerPort,
|
|
186
|
+
hostPort,
|
|
187
|
+
protocol,
|
|
188
|
+
source: 'auto',
|
|
189
|
+
});
|
|
176
190
|
}
|
|
177
191
|
});
|
|
178
192
|
}
|
|
179
193
|
}
|
|
180
194
|
} catch (err) {
|
|
181
|
-
logger.warn(
|
|
195
|
+
logger.warn(
|
|
196
|
+
'Could not inspect image %s for default ports',
|
|
197
|
+
imageName,
|
|
198
|
+
err
|
|
199
|
+
);
|
|
182
200
|
}
|
|
183
201
|
} else {
|
|
184
202
|
// User-defined ports: collect them with source: "user"
|
|
185
203
|
const portBindings = createOpts.HostConfig?.PortBindings || {};
|
|
186
204
|
Object.entries(portBindings).forEach(([portKey, bindings]) => {
|
|
187
|
-
const parts = portKey.split(
|
|
205
|
+
const parts = portKey.split('/');
|
|
188
206
|
const containerPort = parts[0];
|
|
189
|
-
const protocol = parts[1] ||
|
|
207
|
+
const protocol = parts[1] || 'tcp';
|
|
190
208
|
(bindings || []).forEach((b) => {
|
|
191
|
-
assignedPorts.push({
|
|
209
|
+
assignedPorts.push({
|
|
210
|
+
containerPort,
|
|
211
|
+
hostPort: b.HostPort,
|
|
212
|
+
protocol,
|
|
213
|
+
source: 'user',
|
|
214
|
+
});
|
|
192
215
|
});
|
|
193
216
|
});
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
try {
|
|
197
|
-
const container = await withTimeout(
|
|
220
|
+
const container = await withTimeout(
|
|
221
|
+
docker.createContainer(createOpts),
|
|
222
|
+
TIMEOUTS.CONTAINER_OP
|
|
223
|
+
);
|
|
198
224
|
const containerId = container.id || container.Id;
|
|
199
|
-
logger.info(
|
|
225
|
+
logger.info('Created container %s from image %s', containerId, imageName);
|
|
200
226
|
return { id: containerId, ports: assignedPorts };
|
|
201
227
|
} catch (err) {
|
|
202
|
-
logger.error(
|
|
228
|
+
logger.error('Failed to create container from image %s', imageName, err);
|
|
203
229
|
throw new Error('Error creating container: ' + err.message);
|
|
204
230
|
}
|
|
205
231
|
}
|
|
@@ -211,12 +237,12 @@ export async function createContainer(imageName, options = {}, imageProfiles = I
|
|
|
211
237
|
*/
|
|
212
238
|
export async function startContainer(containerId) {
|
|
213
239
|
const container = docker.getContainer(containerId);
|
|
214
|
-
logger.debug(
|
|
240
|
+
logger.debug('Starting container %s', containerId);
|
|
215
241
|
try {
|
|
216
242
|
await withTimeout(container.start(), TIMEOUTS.CONTAINER_OP);
|
|
217
|
-
logger.info(
|
|
243
|
+
logger.info('Started container %s', containerId);
|
|
218
244
|
} catch (err) {
|
|
219
|
-
logger.error(
|
|
245
|
+
logger.error('Failed to start container %s', containerId, err);
|
|
220
246
|
throw err;
|
|
221
247
|
}
|
|
222
248
|
}
|
|
@@ -228,12 +254,12 @@ export async function startContainer(containerId) {
|
|
|
228
254
|
*/
|
|
229
255
|
export async function stopContainer(containerId) {
|
|
230
256
|
const container = docker.getContainer(containerId);
|
|
231
|
-
logger.debug(
|
|
257
|
+
logger.debug('Stopping container %s', containerId);
|
|
232
258
|
try {
|
|
233
259
|
await withTimeout(container.stop(), TIMEOUTS.CONTAINER_OP);
|
|
234
|
-
logger.info(
|
|
260
|
+
logger.info('Stopped container %s', containerId);
|
|
235
261
|
} catch (err) {
|
|
236
|
-
logger.error(
|
|
262
|
+
logger.error('Failed to stop container %s', containerId, err);
|
|
237
263
|
throw err;
|
|
238
264
|
}
|
|
239
265
|
}
|
|
@@ -245,12 +271,12 @@ export async function stopContainer(containerId) {
|
|
|
245
271
|
*/
|
|
246
272
|
export async function restartContainer(containerId) {
|
|
247
273
|
const container = docker.getContainer(containerId);
|
|
248
|
-
logger.debug(
|
|
274
|
+
logger.debug('Restarting container %s', containerId);
|
|
249
275
|
try {
|
|
250
276
|
await withTimeout(container.restart(), TIMEOUTS.CONTAINER_OP);
|
|
251
|
-
logger.info(
|
|
277
|
+
logger.info('Restarted container %s', containerId);
|
|
252
278
|
} catch (err) {
|
|
253
|
-
logger.error(
|
|
279
|
+
logger.error('Failed to restart container %s', containerId, err);
|
|
254
280
|
throw err;
|
|
255
281
|
}
|
|
256
282
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { docker } from
|
|
2
|
-
import { logger } from
|
|
1
|
+
import { docker } from '../dockerService.js';
|
|
2
|
+
import { logger } from '../../logger.js';
|
|
3
3
|
|
|
4
|
-
const DEFAULT_CONTAINER_NAME =
|
|
4
|
+
const DEFAULT_CONTAINER_NAME = 'Unknown';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Produce a safe, display-friendly container name from the raw Docker payload.
|
|
@@ -13,21 +13,28 @@ function normalizeContainerName(container) {
|
|
|
13
13
|
const names = container?.Names;
|
|
14
14
|
|
|
15
15
|
let rawName;
|
|
16
|
-
if (
|
|
16
|
+
if (
|
|
17
|
+
Array.isArray(names) &&
|
|
18
|
+
names.length > 0 &&
|
|
19
|
+
typeof names[0] === 'string'
|
|
20
|
+
) {
|
|
17
21
|
rawName = names[0];
|
|
18
|
-
} else if (typeof names ===
|
|
22
|
+
} else if (typeof names === 'string' && names.trim().length > 0) {
|
|
19
23
|
rawName = names;
|
|
20
|
-
} else if (
|
|
24
|
+
} else if (
|
|
25
|
+
typeof container?.Name === 'string' &&
|
|
26
|
+
container.Name.trim().length > 0
|
|
27
|
+
) {
|
|
21
28
|
rawName = container.Name;
|
|
22
29
|
} else {
|
|
23
30
|
rawName = DEFAULT_CONTAINER_NAME;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
if (typeof rawName !==
|
|
33
|
+
if (typeof rawName !== 'string') {
|
|
27
34
|
return DEFAULT_CONTAINER_NAME;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
const cleanedName = rawName.replace(/^\/+/u,
|
|
37
|
+
const cleanedName = rawName.replace(/^\/+/u, '').trim();
|
|
31
38
|
return cleanedName.length > 0 ? cleanedName : DEFAULT_CONTAINER_NAME;
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -36,33 +43,32 @@ function normalizeContainerName(container) {
|
|
|
36
43
|
* @returns {Promise<Array<Object>>}
|
|
37
44
|
*/
|
|
38
45
|
export async function getContainers() {
|
|
39
|
-
logger.debug(
|
|
46
|
+
logger.debug('Listing containers');
|
|
40
47
|
try {
|
|
41
48
|
const containers = await docker.listContainers({ all: true });
|
|
42
|
-
logger.debug(
|
|
49
|
+
logger.debug('Retrieved %d containers', containers.length);
|
|
43
50
|
return containers.map((container) => ({
|
|
44
51
|
id: container.Id,
|
|
45
52
|
name: normalizeContainerName(container),
|
|
46
53
|
image: container.Image,
|
|
47
54
|
state: container.State,
|
|
48
55
|
status: container.Status,
|
|
49
|
-
ports:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})()
|
|
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
|
+
})(),
|
|
63
69
|
}));
|
|
64
70
|
} catch (err) {
|
|
65
|
-
logger.error(
|
|
71
|
+
logger.error('Failed to list containers', err);
|
|
66
72
|
throw err;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { docker } from
|
|
2
|
-
import { safeCall } from
|
|
3
|
-
import { logger } from
|
|
1
|
+
import { docker } from '../dockerService.js';
|
|
2
|
+
import { safeCall } from '../../safeCall.js';
|
|
3
|
+
import { logger } from '../../logger.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Return a stream of logs from a container and call callbacks for events.
|
|
@@ -15,31 +15,46 @@ export function getLogsStream(containerId, onData, onEnd, onError) {
|
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
const container = docker.getContainer(containerId);
|
|
18
|
-
logger.debug(
|
|
19
|
-
container.logs(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
});
|
|
29
50
|
}
|
|
30
|
-
|
|
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
|
-
});
|
|
40
|
-
});
|
|
51
|
+
);
|
|
41
52
|
} catch (err) {
|
|
42
|
-
logger.error(
|
|
53
|
+
logger.error(
|
|
54
|
+
'Unexpected error while opening logs stream for container %s',
|
|
55
|
+
containerId,
|
|
56
|
+
err
|
|
57
|
+
);
|
|
43
58
|
safeCall(onError, err);
|
|
44
59
|
}
|
|
45
60
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { docker } from
|
|
2
|
-
import { logger } from
|
|
1
|
+
import { docker } from '../dockerService.js';
|
|
2
|
+
import { logger } from '../../logger.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Retrieve a snapshot of container resource usage (CPU, memory, network).
|
|
@@ -9,12 +9,12 @@ import { logger } from "../../logger.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export async function getStats(containerId) {
|
|
11
11
|
const container = docker.getContainer(containerId);
|
|
12
|
-
logger.debug(
|
|
12
|
+
logger.debug('Fetching stats for container %s', containerId);
|
|
13
13
|
let stream;
|
|
14
14
|
try {
|
|
15
15
|
stream = await container.stats({ stream: false });
|
|
16
16
|
} catch (err) {
|
|
17
|
-
logger.error(
|
|
17
|
+
logger.error('Failed to fetch stats for container %s', containerId, err);
|
|
18
18
|
throw err;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -23,10 +23,13 @@ export async function getStats(containerId) {
|
|
|
23
23
|
stream.precpu_stats.cpu_usage.total_usage;
|
|
24
24
|
const systemDelta =
|
|
25
25
|
stream.cpu_stats.system_cpu_usage - stream.precpu_stats.system_cpu_usage;
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
// Get number of CPUs for proper normalization
|
|
28
28
|
let numCpus = 1;
|
|
29
|
-
if (
|
|
29
|
+
if (
|
|
30
|
+
typeof stream.cpu_stats.online_cpus === 'number' &&
|
|
31
|
+
stream.cpu_stats.online_cpus > 0
|
|
32
|
+
) {
|
|
30
33
|
numCpus = stream.cpu_stats.online_cpus;
|
|
31
34
|
} else if (
|
|
32
35
|
stream.cpu_stats.cpu_usage &&
|
|
@@ -35,11 +38,10 @@ export async function getStats(containerId) {
|
|
|
35
38
|
) {
|
|
36
39
|
numCpus = stream.cpu_stats.cpu_usage.percpu_usage.length;
|
|
37
40
|
}
|
|
38
|
-
|
|
41
|
+
|
|
39
42
|
// Calculate normalized CPU percentage
|
|
40
|
-
const cpuPercent =
|
|
41
|
-
? (
|
|
42
|
-
: 0;
|
|
43
|
+
const cpuPercent =
|
|
44
|
+
systemDelta > 0 ? (cpuDelta / systemDelta) * numCpus * 100 : 0;
|
|
43
45
|
|
|
44
46
|
const memUsage = stream.memory_stats.usage || 0;
|
|
45
47
|
const memLimit = stream.memory_stats.limit || 1;
|
|
@@ -62,6 +64,6 @@ export async function getStats(containerId) {
|
|
|
62
64
|
netIO: { rx, tx },
|
|
63
65
|
};
|
|
64
66
|
|
|
65
|
-
logger.debug(
|
|
67
|
+
logger.debug('Stats fetched for container %s', containerId, snapshot);
|
|
66
68
|
return snapshot;
|
|
67
69
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { docker } from
|
|
1
|
+
import { docker } from '../dockerService.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Check whether an image exists locally.
|
|
@@ -7,9 +7,10 @@ import { docker } from "../dockerService.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export async function imageExists(imageName) {
|
|
9
9
|
const images = await docker.listImages();
|
|
10
|
-
return images.some(
|
|
11
|
-
(img
|
|
12
|
-
|
|
10
|
+
return images.some(
|
|
11
|
+
(img) =>
|
|
12
|
+
(img.RepoTags || []).includes(imageName) ||
|
|
13
|
+
(img.RepoDigests || []).some((d) => d.includes(imageName))
|
|
13
14
|
);
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import { EXIT_DELAY } from
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { EXIT_DELAY } from './constants';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Show an exit message using provided setters, clear the terminal and exit after a delay.
|
|
@@ -11,15 +11,21 @@ import { EXIT_DELAY } from "./constants";
|
|
|
11
11
|
* @param {string} [params.color] - Color for the message
|
|
12
12
|
* @param {number} [params.delay] - Delay in milliseconds before exiting
|
|
13
13
|
*/
|
|
14
|
-
export function exitWithMessage({
|
|
14
|
+
export function exitWithMessage({
|
|
15
|
+
setMessage,
|
|
16
|
+
setMessageColor,
|
|
17
|
+
message = 'Exiting...',
|
|
18
|
+
color = 'yellow',
|
|
19
|
+
delay = EXIT_DELAY,
|
|
20
|
+
}) {
|
|
15
21
|
setMessage(message);
|
|
16
22
|
setMessageColor(color);
|
|
17
23
|
setTimeout(() => {
|
|
18
|
-
setMessage(
|
|
19
|
-
if (process.platform ===
|
|
20
|
-
spawn(
|
|
24
|
+
setMessage('');
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
spawn('cmd', ['/c', 'cls'], { stdio: 'inherit' });
|
|
21
27
|
} else {
|
|
22
|
-
spawn(
|
|
28
|
+
spawn('clear', [], { stdio: 'inherit' });
|
|
23
29
|
}
|
|
24
30
|
process.exit();
|
|
25
31
|
}, delay);
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|