cdd-cli 3.2.2 → 4.1.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 (126) hide show
  1. package/README.es.md +222 -0
  2. package/README.md +108 -26
  3. package/dist/App.js +7 -1
  4. package/dist/components/ContainerCreationPrompt.js +26 -10
  5. package/dist/components/ControlsHUD.js +106 -0
  6. package/dist/components/SuggestionPanel.js +16 -7
  7. package/dist/components/UsageMenu.js +1 -1
  8. package/dist/helpers/appState.js +26 -0
  9. package/dist/helpers/constants.js +60 -21
  10. package/dist/helpers/dockerHubService.js +111 -0
  11. package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
  12. package/dist/helpers/logger.js +1 -1
  13. package/dist/hooks/creation/useContainerActions.js +22 -5
  14. package/dist/hooks/creation/useContainerCreation.js +414 -107
  15. package/dist/hooks/useContainerCommandRouter.js +13 -3
  16. package/dist/hooks/useControls.js +28 -7
  17. package/dist/hooks/useShellMode.js +101 -0
  18. package/dist/index.js +10 -1
  19. package/package.json +4 -1
  20. package/.atl/skill-registry.md +0 -30
  21. package/.editorconfig +0 -12
  22. package/.eslintrc.json +0 -30
  23. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  24. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  25. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
  26. package/.github/workflows/ci.yml +0 -32
  27. package/.github/workflows/docs.yml +0 -42
  28. package/.prettierignore +0 -4
  29. package/.prettierrc +0 -8
  30. package/CHANGELOG.md +0 -165
  31. package/CODE_OF_CONDUCT.md +0 -12
  32. package/CONTRIBUTING.md +0 -35
  33. package/SECURITY.md +0 -5
  34. package/__mocks__/ink.cjs +0 -17
  35. package/_config.yml +0 -13
  36. package/babel.config.cjs +0 -23
  37. package/coverage/clover.xml +0 -694
  38. package/coverage/coverage-final.json +0 -21
  39. package/coverage/lcov-report/base.css +0 -224
  40. package/coverage/lcov-report/block-navigation.js +0 -87
  41. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -376
  42. package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
  43. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -244
  44. package/coverage/lcov-report/components/index.html +0 -146
  45. package/coverage/lcov-report/favicon.png +0 -0
  46. package/coverage/lcov-report/helpers/constants.js.html +0 -298
  47. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
  48. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
  49. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
  50. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
  51. package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
  52. package/coverage/lcov-report/helpers/index.html +0 -191
  53. package/coverage/lcov-report/helpers/logger.js.html +0 -424
  54. package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
  55. package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
  56. package/coverage/lcov-report/hooks/creation/index.html +0 -146
  57. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
  58. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -805
  59. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
  60. package/coverage/lcov-report/hooks/debug/index.html +0 -116
  61. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
  62. package/coverage/lcov-report/hooks/index.html +0 -161
  63. package/coverage/lcov-report/hooks/navigation/index.html +0 -116
  64. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
  65. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
  66. package/coverage/lcov-report/hooks/useControls.js.html +0 -826
  67. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
  68. package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
  69. package/coverage/lcov-report/index.html +0 -206
  70. package/coverage/lcov-report/prettify.css +0 -1
  71. package/coverage/lcov-report/prettify.js +0 -2
  72. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  73. package/coverage/lcov-report/sorter.js +0 -210
  74. package/coverage/lcov.info +0 -1402
  75. package/fix-imports.cjs +0 -54
  76. package/jest.config.cjs +0 -14
  77. package/src/App.jsx +0 -96
  78. package/src/components/ContainerCreationPrompt.jsx +0 -97
  79. package/src/components/ContainerList.jsx +0 -32
  80. package/src/components/ContainerRow.jsx +0 -84
  81. package/src/components/ContainerSection.jsx +0 -24
  82. package/src/components/Footer.jsx +0 -15
  83. package/src/components/Header.jsx +0 -41
  84. package/src/components/LogViewer.jsx +0 -36
  85. package/src/components/MessageFeedback.jsx +0 -25
  86. package/src/components/PromptField.jsx +0 -48
  87. package/src/components/StatsBar.jsx +0 -70
  88. package/src/components/SuggestionPanel.jsx +0 -53
  89. package/src/components/UsageMenu.jsx +0 -52
  90. package/src/helpers/appInfo.js +0 -23
  91. package/src/helpers/constants.js +0 -71
  92. package/src/helpers/containerOptionsBuilder.js +0 -53
  93. package/src/helpers/dockerService/dockerService.js +0 -28
  94. package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
  95. package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
  96. package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
  97. package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
  98. package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
  99. package/src/helpers/exitWithMessage.js +0 -32
  100. package/src/helpers/imageNameUtils.js +0 -32
  101. package/src/helpers/logger.js +0 -113
  102. package/src/helpers/safeCall.js +0 -18
  103. package/src/helpers/validationHelpers.js +0 -87
  104. package/src/hooks/creation/useContainerActions.js +0 -76
  105. package/src/hooks/creation/useContainerCreation.js +0 -240
  106. package/src/hooks/creation/useLogsViewer.js +0 -51
  107. package/src/hooks/debug/useDebugLogs.js +0 -34
  108. package/src/hooks/navigation/useContainerSelection.js +0 -28
  109. package/src/hooks/useContainerCommandRouter.js +0 -123
  110. package/src/hooks/useContainerStats.js +0 -44
  111. package/src/hooks/useContainers.js +0 -26
  112. package/src/hooks/useControls.js +0 -247
  113. package/src/hooks/useEraseConfirmation.js +0 -39
  114. package/src/hooks/useExitHandler.js +0 -69
  115. package/src/index.js +0 -16
  116. package/test/ContainerCreationPrompt.dom.test.js +0 -68
  117. package/test/SuggestionPanel.dom.test.js +0 -53
  118. package/test/constants.test.js +0 -34
  119. package/test/containerActions.test.js +0 -229
  120. package/test/containerList.test.js +0 -79
  121. package/test/containerOptionsBuilder.test.js +0 -36
  122. package/test/jest.setup.js +0 -7
  123. package/test/safeCall.test.js +0 -25
  124. package/test/useContainerCreation.dom.test.js +0 -264
  125. package/test/useControls.dom.test.js +0 -313
  126. package/test/validationHelpers.test.js +0 -111
@@ -1,53 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import PropTypes from 'prop-types';
4
-
5
- /**
6
- * Renders an autocomplete suggestion panel as a boxed list below the image input.
7
- * Displays a window of `maxVisible` items starting at `visibleOffset`.
8
- * The focused row (selectedIndex) is highlighted cyan with a › prefix.
9
- *
10
- * @param {Object} props
11
- * @param {string[]} props.items - Full list of suggestion strings
12
- * @param {number} props.selectedIndex - Currently focused index (-1 = none)
13
- * @param {number} props.visibleOffset - First item index of the visible window
14
- * @param {number} [props.maxVisible=6] - Maximum number of items visible at once
15
- * @returns {JSX.Element|null}
16
- */
17
- export function SuggestionPanel({
18
- items,
19
- selectedIndex,
20
- visibleOffset,
21
- maxVisible = 6,
22
- }) {
23
- if (!items || items.length === 0) return null;
24
-
25
- const visible = items.slice(visibleOffset, visibleOffset + maxVisible);
26
-
27
- return (
28
- <Box
29
- flexDirection="column"
30
- borderStyle="single"
31
- borderColor="cyan"
32
- paddingX={1}
33
- data-testid="suggestion-panel"
34
- >
35
- {visible.map((item, localIdx) => {
36
- const globalIdx = visibleOffset + localIdx;
37
- const isFocused = globalIdx === selectedIndex;
38
- return (
39
- <Text key={item} color={isFocused ? 'cyan' : undefined} dimColor={!isFocused}>
40
- {isFocused ? `› ${item}` : ` ${item}`}
41
- </Text>
42
- );
43
- })}
44
- </Box>
45
- );
46
- }
47
-
48
- SuggestionPanel.propTypes = {
49
- items: PropTypes.arrayOf(PropTypes.string).isRequired,
50
- selectedIndex: PropTypes.number.isRequired,
51
- visibleOffset: PropTypes.number.isRequired,
52
- maxVisible: PropTypes.number,
53
- };
@@ -1,52 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
-
4
- const SHORTCUTS = [
5
- 'Use ↑/↓ for navigation',
6
- 'I to initiate selected container',
7
- 'P to stop selected container',
8
- 'R to restart selected container',
9
- 'C to create a container',
10
- 'L to view logs of selected container',
11
- 'D to toggle debug log panel',
12
- 'E to remove selected container',
13
- 'Q to quit',
14
- ];
15
-
16
- const GRID_COLUMNS = 3;
17
-
18
- /**
19
- * Small usage menu that explains available keybindings to the user.
20
- * Renders entries in a responsive grid to avoid an excessively long column.
21
- *
22
- * @returns {JSX.Element}
23
- */
24
- export default function UsageMenu() {
25
- const rowsPerColumn = Math.ceil(SHORTCUTS.length / GRID_COLUMNS);
26
- const columns = Array.from({ length: GRID_COLUMNS }, (_, columnIndex) =>
27
- SHORTCUTS.slice(
28
- columnIndex * rowsPerColumn,
29
- (columnIndex + 1) * rowsPerColumn
30
- )
31
- ).filter((column) => column.length > 0);
32
-
33
- return (
34
- <Box flexDirection="column" marginTop={1}>
35
- <Box flexDirection="row">
36
- {columns.map((column, columnIndex) => (
37
- <Box
38
- key={`shortcut-column-${columnIndex}`}
39
- flexDirection="column"
40
- marginRight={columnIndex === columns.length - 1 ? 0 : 3}
41
- >
42
- {column.map((shortcut, rowIndex) => (
43
- <Text key={`shortcut-${columnIndex}-${rowIndex}`}>
44
- {shortcut}
45
- </Text>
46
- ))}
47
- </Box>
48
- ))}
49
- </Box>
50
- </Box>
51
- );
52
- }
@@ -1,23 +0,0 @@
1
- import { readFileSync } from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- let cachedVersion;
6
-
7
- export function getAppVersion() {
8
- if (cachedVersion) {
9
- return cachedVersion;
10
- }
11
-
12
- try {
13
- const __filename = fileURLToPath(import.meta.url);
14
- const moduleDir = path.dirname(__filename);
15
- const packageJsonPath = path.resolve(moduleDir, '../../package.json');
16
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
17
- cachedVersion = packageJson.version ?? 'unknown';
18
- return cachedVersion;
19
- } catch (error) {
20
- cachedVersion = 'unknown';
21
- return cachedVersion;
22
- }
23
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Time intervals used across the app (milliseconds).
3
- * - CONTAINER_LIST: how often to refresh the list of containers
4
- * - CONTAINER_STATS: how often to fetch stats for running containers
5
- */
6
- export const REFRESH_INTERVALS = {
7
- CONTAINER_LIST: 3000,
8
- CONTAINER_STATS: 1500,
9
- };
10
-
11
- /**
12
- * Timeout values for UI messages before they disappear.
13
- */
14
- export const MESSAGE_TIMEOUTS = {
15
- SHORT: 2000,
16
- DEFAULT: 3000,
17
- };
18
-
19
- /**
20
- * Delay used before exiting the process when showing an exit message.
21
- */
22
- export const EXIT_DELAY = 500;
23
-
24
- /**
25
- * Operational timeouts for Docker operations and image pulls.
26
- */
27
- export const TIMEOUTS = {
28
- CONTAINER_OP: 30000,
29
- PULL_IMAGE: 300000,
30
- };
31
-
32
- /**
33
- * List of well-known database image names (without tags or registry prefixes).
34
- */
35
- export const DB_IMAGES = [
36
- 'mysql',
37
- 'mariadb',
38
- 'postgres',
39
- 'mongo',
40
- 'mssql',
41
- 'redis',
42
- ];
43
-
44
- /**
45
- * Per-image profiles with required env vars, default exposed port, and suggested env vars.
46
- * Keys are normalized image names (no tag, no registry prefix).
47
- *
48
- * @type {Record<string, { requiredEnv: string[], defaultPort: string, suggestedEnv: string[] }>}
49
- */
50
- export const IMAGE_PROFILES = {
51
- nginx: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
52
- httpd: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
53
- caddy: { requiredEnv: [], defaultPort: '80', suggestedEnv: [] },
54
- node: { requiredEnv: [], defaultPort: '3000', suggestedEnv: ['NODE_ENV=production'] },
55
- python: { requiredEnv: [], defaultPort: '8000', suggestedEnv: ['PYTHONUNBUFFERED=1'] },
56
- openjdk: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['JAVA_OPTS=-Xms256m -Xmx512m'] },
57
- golang: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['GOMAXPROCS=2'] },
58
- redis: { requiredEnv: [], defaultPort: '6379', suggestedEnv: ['REDIS_PASSWORD=change-me'] },
59
- memcached: { requiredEnv: [], defaultPort: '11211', suggestedEnv: [] },
60
- postgres: { requiredEnv: ['POSTGRES_PASSWORD'], defaultPort: '5432', suggestedEnv: ['POSTGRES_USER=postgres', 'POSTGRES_DB=app'] },
61
- mysql: { requiredEnv: ['MYSQL_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MYSQL_DATABASE=app', 'MYSQL_USER=app', 'MYSQL_PASSWORD=app123'] },
62
- mariadb: { requiredEnv: ['MARIADB_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MARIADB_DATABASE=app', 'MARIADB_USER=app', 'MARIADB_PASSWORD=app123'] },
63
- mongo: { requiredEnv: [], defaultPort: '27017', suggestedEnv: ['MONGO_INITDB_ROOT_USERNAME=admin', 'MONGO_INITDB_ROOT_PASSWORD=secret'] },
64
- mssql: { requiredEnv: ['ACCEPT_EULA', 'SA_PASSWORD'], defaultPort: '1433', suggestedEnv: ['ACCEPT_EULA=Y', 'MSSQL_PID=Developer'] },
65
- rabbitmq: { requiredEnv: [], defaultPort: '5672', suggestedEnv: ['RABBITMQ_DEFAULT_USER=guest', 'RABBITMQ_DEFAULT_PASS=guest'] },
66
- kafka: { requiredEnv: [], defaultPort: '9092', suggestedEnv: ['KAFKA_CFG_LISTENERS=PLAINTEXT://:9092', 'KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092'] },
67
- zookeeper: { requiredEnv: [], defaultPort: '2181', suggestedEnv: ['ALLOW_ANONYMOUS_LOGIN=yes'] },
68
- elasticsearch: { requiredEnv: [], defaultPort: '9200', suggestedEnv: ['discovery.type=single-node', 'xpack.security.enabled=false'] },
69
- minio: { requiredEnv: ['MINIO_ROOT_USER', 'MINIO_ROOT_PASSWORD'], defaultPort: '9000', suggestedEnv: ['MINIO_CONSOLE_ADDRESS=:9001'] },
70
- wordpress: { requiredEnv: ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME'], defaultPort: '8080', suggestedEnv: ['WORDPRESS_TABLE_PREFIX=wp_'] },
71
- };
@@ -1,53 +0,0 @@
1
- /**
2
- * Builds Docker container creation options from raw user inputs.
3
- *
4
- * This is a pure function — it has no side effects and calls no services.
5
- * It mirrors exactly the option-building logic in useControls.js onCreate callback.
6
- *
7
- * @param {object} params
8
- * @param {string} params.imageName - Docker image name (e.g. "nginx:alpine")
9
- * @param {string} [params.containerName] - Optional container name
10
- * @param {string} [params.portInput] - Comma-separated host:container port pairs
11
- * (e.g. "8080:80,3000:3000")
12
- * @param {string} [params.envInput] - Comma-separated KEY=VALUE pairs
13
- * (e.g. "NODE_ENV=production,PORT=3000")
14
- * @returns {object} Docker container options ready to pass to createContainer
15
- */
16
- export function buildContainerOptions({
17
- imageName: _imageName,
18
- containerName,
19
- portInput,
20
- envInput,
21
- }) {
22
- const env = (envInput || '')
23
- .split(',')
24
- .map((s) => s.trim())
25
- .filter(Boolean);
26
- const ports = (portInput || '')
27
- .split(',')
28
- .map((s) => s.trim())
29
- .filter(Boolean);
30
-
31
- const ExposedPorts = {};
32
- const PortBindings = {};
33
-
34
- ports.forEach((pair) => {
35
- const [host, cont] = pair.split(':');
36
- if (!host || !cont) return;
37
- const key = `${cont}/tcp`;
38
- ExposedPorts[key] = {};
39
- PortBindings[key] = PortBindings[key] || [];
40
- PortBindings[key].push({ HostPort: `${host}` });
41
- });
42
-
43
- const options = {
44
- Tty: true,
45
- };
46
-
47
- if (Object.keys(ExposedPorts).length) options.ExposedPorts = ExposedPorts;
48
- if (Object.keys(PortBindings).length) options.HostConfig = { PortBindings };
49
- if (env.length) options.Env = env;
50
- if (containerName) options.name = containerName;
51
-
52
- return options;
53
- }
@@ -1,28 +0,0 @@
1
- import Docker from 'dockerode';
2
-
3
- // Use default dockerode configuration which automatically handles:
4
- // - /var/run/docker.sock on Linux/Mac
5
- // - //./pipe/docker_engine on Windows
6
- // - Environment variables DOCKER_HOST, DOCKER_CERT_PATH, etc.
7
-
8
- /**
9
- * Typedef for the docker client used in this project.
10
- * Use a generic object shape so JSDoc can render a meaningful type.
11
- *
12
- * @typedef {Object} DockerClient
13
- * @property {Function} listContainers
14
- * @property {Function} getContainer
15
- * @property {Function} listImages
16
- * @property {Function} createContainer
17
- * @property {Function} pull
18
- */
19
-
20
- /**
21
- * Shared Docker client instance (dockerode). Consumers import { docker }
22
- * and use it to list/create/start/stop containers and images.
23
- *
24
- * @type {DockerClient}
25
- */
26
- const docker = new Docker();
27
-
28
- export { docker };
@@ -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
- }