cdd-cli 3.2.0 → 3.2.2
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/CHANGELOG.md +29 -0
- package/SECURITY.md +5 -0
- package/__mocks__/ink.cjs +17 -0
- package/coverage/clover.xml +526 -436
- package/coverage/coverage-final.json +19 -15
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +376 -0
- package/coverage/lcov-report/components/PromptField.jsx.html +229 -0
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +244 -0
- package/coverage/lcov-report/components/index.html +146 -0
- package/coverage/lcov-report/helpers/constants.js.html +80 -17
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +41 -8
- 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 +27 -12
- package/coverage/lcov-report/helpers/logger.js.html +19 -19
- package/coverage/lcov-report/helpers/safeCall.js.html +3 -3
- package/coverage/lcov-report/helpers/validationHelpers.js.html +28 -76
- 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 +416 -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 +263 -122
- 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 +67 -52
- package/coverage/lcov.info +991 -805
- package/dist/App.js +11 -11
- package/dist/components/ContainerCreationPrompt.js +31 -10
- package/dist/components/ContainerList.js +3 -3
- package/dist/components/ContainerRow.js +20 -20
- package/dist/components/ContainerSection.js +3 -3
- 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 +44 -0
- package/dist/components/UsageMenu.js +3 -3
- package/dist/helpers/appInfo.js +7 -7
- package/dist/helpers/constants.js +114 -0
- package/dist/helpers/containerOptionsBuilder.js +3 -3
- 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 +9 -9
- package/dist/hooks/creation/useContainerCreation.js +138 -36
- package/dist/hooks/creation/useLogsViewer.js +2 -2
- package/dist/hooks/debug/useDebugLogs.js +4 -4
- package/dist/hooks/useContainerCommandRouter.js +13 -13
- package/dist/hooks/useContainers.js +3 -3
- package/dist/hooks/useControls.js +66 -35
- package/dist/hooks/useEraseConfirmation.js +3 -3
- package/dist/index.js +2 -2
- package/jest.config.cjs +7 -0
- package/package.json +7 -3
- package/src/App.jsx +33 -16
- package/src/components/ContainerCreationPrompt.jsx +43 -9
- package/src/components/ContainerList.jsx +3 -3
- package/src/components/ContainerRow.jsx +19 -16
- package/src/components/ContainerSection.jsx +3 -3
- 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 +53 -0
- package/src/components/UsageMenu.jsx +18 -13
- package/src/helpers/appInfo.js +7 -7
- package/src/helpers/constants.js +30 -9
- package/src/helpers/containerOptionsBuilder.js +16 -5
- 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 -15
- package/src/helpers/safeCall.js +2 -2
- package/src/helpers/validationHelpers.js +13 -29
- package/src/hooks/creation/useContainerActions.js +19 -12
- package/src/hooks/creation/useContainerCreation.js +150 -41
- 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 +115 -68
- package/src/hooks/useEraseConfirmation.js +3 -3
- package/src/hooks/useExitHandler.js +29 -26
- package/src/index.js +4 -4
- package/test/ContainerCreationPrompt.dom.test.js +68 -0
- package/test/SuggestionPanel.dom.test.js +53 -0
- package/test/constants.test.js +34 -0
- package/test/useContainerCreation.dom.test.js +123 -0
- package/test/useControls.dom.test.js +198 -2
|
@@ -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
|
+
}
|
package/src/helpers/logger.js
CHANGED
|
@@ -6,7 +6,7 @@ const LOG_LEVELS = {
|
|
|
6
6
|
error: 0,
|
|
7
7
|
warn: 1,
|
|
8
8
|
info: 2,
|
|
9
|
-
debug: 3
|
|
9
|
+
debug: 3,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -24,7 +24,7 @@ const LOG_LEVELS = {
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
function parseLevel(value) {
|
|
27
|
-
if (typeof value !==
|
|
27
|
+
if (typeof value !== 'string') {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
const normalized = value.trim().toLowerCase();
|
|
@@ -39,13 +39,13 @@ const envLevel =
|
|
|
39
39
|
parseLevel(process.env.LOG_LEVEL) ??
|
|
40
40
|
LOG_LEVELS.info;
|
|
41
41
|
|
|
42
|
-
const currentLevel = typeof envLevel ===
|
|
42
|
+
const currentLevel = typeof envLevel === 'number' ? envLevel : LOG_LEVELS.info;
|
|
43
43
|
|
|
44
44
|
const listeners = new Set();
|
|
45
45
|
|
|
46
46
|
function shouldLog(level) {
|
|
47
47
|
const target = LOG_LEVELS[level];
|
|
48
|
-
return typeof target ===
|
|
48
|
+
return typeof target === 'number' && target <= currentLevel;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function log(level, message, ...args) {
|
|
@@ -59,11 +59,11 @@ function log(level, message, ...args) {
|
|
|
59
59
|
const entry = { level, message, args, timestamp, formatted };
|
|
60
60
|
|
|
61
61
|
const shouldWriteToConsole =
|
|
62
|
-
listeners.size === 0 || level ===
|
|
62
|
+
listeners.size === 0 || level === 'error' || level === 'warn';
|
|
63
63
|
|
|
64
64
|
if (shouldWriteToConsole) {
|
|
65
|
-
const method = level ===
|
|
66
|
-
if (typeof console[method] ===
|
|
65
|
+
const method = level === 'debug' ? 'log' : level;
|
|
66
|
+
if (typeof console[method] === 'function') {
|
|
67
67
|
console[method](formatted, ...args);
|
|
68
68
|
} else {
|
|
69
69
|
console.log(formatted, ...args);
|
|
@@ -86,28 +86,28 @@ function log(level, message, ...args) {
|
|
|
86
86
|
*/
|
|
87
87
|
export const logger = {
|
|
88
88
|
debug(message, ...args) {
|
|
89
|
-
log(
|
|
89
|
+
log('debug', message, ...args);
|
|
90
90
|
},
|
|
91
91
|
info(message, ...args) {
|
|
92
|
-
log(
|
|
92
|
+
log('info', message, ...args);
|
|
93
93
|
},
|
|
94
94
|
warn(message, ...args) {
|
|
95
|
-
log(
|
|
95
|
+
log('warn', message, ...args);
|
|
96
96
|
},
|
|
97
97
|
error(message, ...args) {
|
|
98
|
-
log(
|
|
98
|
+
log('error', message, ...args);
|
|
99
99
|
},
|
|
100
100
|
/**
|
|
101
101
|
* Subscribe to log events.
|
|
102
102
|
*
|
|
103
|
-
|
|
104
|
-
|
|
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
105
|
*/
|
|
106
106
|
subscribe(listener) {
|
|
107
|
-
if (typeof listener !==
|
|
107
|
+
if (typeof listener !== 'function') {
|
|
108
108
|
return () => {};
|
|
109
109
|
}
|
|
110
110
|
listeners.add(listener);
|
|
111
111
|
return () => listeners.delete(listener);
|
|
112
|
-
}
|
|
112
|
+
},
|
|
113
113
|
};
|
package/src/helpers/safeCall.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logger } from
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Call a function safely: if it's a function call it with provided args and
|
|
@@ -12,7 +12,7 @@ export function safeCall(fn, ...args) {
|
|
|
12
12
|
try {
|
|
13
13
|
return fn(...args);
|
|
14
14
|
} catch (e) {
|
|
15
|
-
logger.error(
|
|
15
|
+
logger.error('safeCall caught error', e);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Reusable validations for ports and environment variables
|
|
2
|
+
import { normalizeImageName } from './imageNameUtils.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Validate a comma-separated list of port mappings in the form "host:container".
|
|
@@ -10,38 +11,18 @@
|
|
|
10
11
|
export function validatePorts(portInput) {
|
|
11
12
|
// Empty or whitespace-only input is considered valid (ports are optional)
|
|
12
13
|
if (!portInput || !portInput.trim()) return true;
|
|
13
|
-
const ports = portInput
|
|
14
|
+
const ports = portInput
|
|
15
|
+
.split(',')
|
|
16
|
+
.map((p) => p.trim())
|
|
17
|
+
.filter(Boolean);
|
|
14
18
|
if (ports.length === 0) return true;
|
|
15
|
-
const invalid = ports.find(pair => {
|
|
16
|
-
const [host, cont] = pair.split(
|
|
19
|
+
const invalid = ports.find((pair) => {
|
|
20
|
+
const [host, cont] = pair.split(':');
|
|
17
21
|
return !host || !cont || isNaN(Number(host)) || isNaN(Number(cont));
|
|
18
22
|
});
|
|
19
23
|
return !invalid;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
/**
|
|
23
|
-
* Normalize an image name to its base name (strips registry prefix and tag).
|
|
24
|
-
* e.g. "docker.io/library/postgres:16-alpine" → "postgres"
|
|
25
|
-
*
|
|
26
|
-
* @param {string} imageName - Raw image name from user
|
|
27
|
-
* @returns {string} Base image name (lowercase)
|
|
28
|
-
*/
|
|
29
|
-
function normalizeImageName(imageName) {
|
|
30
|
-
if (!imageName) return "";
|
|
31
|
-
// Remove registry prefix (anything before the last / when it contains a dot or colon before the /)
|
|
32
|
-
let name = imageName;
|
|
33
|
-
const slashIdx = name.lastIndexOf("/");
|
|
34
|
-
if (slashIdx !== -1) {
|
|
35
|
-
name = name.slice(slashIdx + 1);
|
|
36
|
-
}
|
|
37
|
-
// Remove tag
|
|
38
|
-
const colonIdx = name.indexOf(":");
|
|
39
|
-
if (colonIdx !== -1) {
|
|
40
|
-
name = name.slice(0, colonIdx);
|
|
41
|
-
}
|
|
42
|
-
return name.toLowerCase();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
26
|
/**
|
|
46
27
|
* Validate environment variable input as comma-separated VAR=value pairs.
|
|
47
28
|
*
|
|
@@ -64,9 +45,12 @@ export function validateEnvVars(envInput, imageName, imageProfiles) {
|
|
|
64
45
|
const syntaxErrors = [];
|
|
65
46
|
|
|
66
47
|
if (envInput && envInput.trim()) {
|
|
67
|
-
const vars = envInput
|
|
48
|
+
const vars = envInput
|
|
49
|
+
.split(',')
|
|
50
|
+
.map((v) => v.trim())
|
|
51
|
+
.filter(Boolean);
|
|
68
52
|
for (const v of vars) {
|
|
69
|
-
const eqIdx = v.indexOf(
|
|
53
|
+
const eqIdx = v.indexOf('=');
|
|
70
54
|
if (eqIdx === -1) {
|
|
71
55
|
syntaxErrors.push(`"${v}" is missing an '=' sign`);
|
|
72
56
|
continue;
|
|
@@ -93,7 +77,7 @@ export function validateEnvVars(envInput, imageName, imageProfiles) {
|
|
|
93
77
|
|
|
94
78
|
if (profile && profile.requiredEnv && profile.requiredEnv.length) {
|
|
95
79
|
for (const required of profile.requiredEnv) {
|
|
96
|
-
if (!parsedEnv[required] || parsedEnv[required].trim() ===
|
|
80
|
+
if (!parsedEnv[required] || parsedEnv[required].trim() === '') {
|
|
97
81
|
errors.push(`Missing required env var: ${required}`);
|
|
98
82
|
}
|
|
99
83
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { useState } from
|
|
2
|
-
import { safeCall } from
|
|
3
|
-
import {
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { safeCall } from '../../helpers/safeCall.js';
|
|
3
|
+
import {
|
|
4
|
+
startContainer as svcStartContainer,
|
|
5
|
+
stopContainer as svcStopContainer,
|
|
6
|
+
restartContainer as svcRestartContainer,
|
|
7
|
+
removeContainer as svcRemoveContainer,
|
|
8
|
+
} from '../../helpers/dockerService/serviceComponents/containerActions.js';
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* Custom hook to manage container actions (start, stop, restart, remove).
|
|
@@ -12,8 +17,8 @@ import { startContainer as svcStartContainer, stopContainer as svcStopContainer,
|
|
|
12
17
|
* @returns {Object} Action helpers and feedback state
|
|
13
18
|
*/
|
|
14
19
|
export function useContainerActions({ containers, onAction }) {
|
|
15
|
-
const [message, setMessage] = useState(
|
|
16
|
-
const [messageColor, setMessageColor] = useState(
|
|
20
|
+
const [message, setMessage] = useState('');
|
|
21
|
+
const [messageColor, setMessageColor] = useState('yellow');
|
|
17
22
|
|
|
18
23
|
/**
|
|
19
24
|
* Handles a generic container action and sets feedback.
|
|
@@ -29,20 +34,22 @@ export function useContainerActions({ containers, onAction }) {
|
|
|
29
34
|
const checkMsg = stateCheck(container);
|
|
30
35
|
if (checkMsg) {
|
|
31
36
|
setMessage(checkMsg);
|
|
32
|
-
setMessageColor(
|
|
37
|
+
setMessageColor('yellow');
|
|
33
38
|
return;
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
setMessage(`${actionLabel} container...`);
|
|
37
|
-
setMessageColor(
|
|
42
|
+
setMessageColor('yellow');
|
|
38
43
|
try {
|
|
39
44
|
await actionFn(container.id);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
setMessage(`${actionLabel} container successful.`);
|
|
46
|
+
setMessageColor('green');
|
|
47
|
+
safeCall(onAction);
|
|
43
48
|
} catch (err) {
|
|
44
|
-
setMessage(
|
|
45
|
-
|
|
49
|
+
setMessage(
|
|
50
|
+
`Failed to ${actionLabel.toLowerCase()} container: ${err.message}`
|
|
51
|
+
);
|
|
52
|
+
setMessageColor('red');
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
|