cdd-cli 3.1.7 → 3.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.
- package/.eslintrc.json +1 -1
- package/.github/workflows/ci.yml +3 -0
- package/.github/workflows/docs.yml +8 -1
- package/CHANGELOG.md +40 -0
- package/CONTRIBUTING.md +28 -15
- package/README.md +56 -88
- package/babel.config.cjs +18 -1
- package/coverage/clover.xml +604 -0
- package/coverage/coverage-final.json +17 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +235 -0
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +211 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +853 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +289 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +131 -0
- package/coverage/lcov-report/helpers/index.html +176 -0
- package/coverage/lcov-report/helpers/logger.js.html +424 -0
- package/coverage/lcov-report/helpers/safeCall.js.html +139 -0
- package/coverage/lcov-report/helpers/validationHelpers.js.html +394 -0
- package/coverage/lcov-report/hooks/creation/index.html +146 -0
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +292 -0
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +478 -0
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +238 -0
- package/coverage/lcov-report/hooks/debug/index.html +116 -0
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +172 -0
- package/coverage/lcov-report/hooks/index.html +161 -0
- package/coverage/lcov-report/hooks/navigation/index.html +116 -0
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +160 -0
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +415 -0
- package/coverage/lcov-report/hooks/useControls.js.html +685 -0
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +202 -0
- package/coverage/lcov-report/hooks/useExitHandler.js.html +283 -0
- package/coverage/lcov-report/index.html +191 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +1216 -0
- package/dist/components/ContainerRow.js +5 -78
- package/dist/components/LogViewer.js +4 -9
- package/dist/helpers/containerOptionsBuilder.js +58 -0
- package/dist/hooks/creation/useContainerCreation.js +16 -1
- package/dist/hooks/debug/useDebugLogs.js +46 -0
- package/dist/hooks/navigation/useContainerSelection.js +36 -0
- package/dist/hooks/useContainerCommandRouter.js +151 -0
- package/dist/{helpers/actionHelpers.js → hooks/useContainerStats.js} +76 -78
- package/dist/hooks/useControls.js +83 -342
- package/dist/hooks/useEraseConfirmation.js +47 -0
- package/dist/hooks/useExitHandler.js +68 -0
- package/jest.config.cjs +1 -1
- package/package.json +2 -3
- package/src/components/ContainerRow.jsx +3 -38
- package/src/components/LogViewer.jsx +4 -10
- package/src/helpers/constants.js +20 -0
- package/src/helpers/containerOptionsBuilder.js +42 -0
- package/src/helpers/dockerService/serviceComponents/containerActions.js +55 -8
- package/src/helpers/validationHelpers.js +75 -18
- package/src/hooks/creation/useContainerCreation.js +34 -4
- package/src/hooks/debug/useDebugLogs.js +29 -0
- package/src/hooks/navigation/useContainerSelection.js +25 -0
- package/src/hooks/useContainerCommandRouter.js +110 -0
- package/src/hooks/useContainerStats.js +44 -0
- package/src/hooks/useControls.js +75 -274
- package/src/hooks/useEraseConfirmation.js +39 -0
- package/src/hooks/useExitHandler.js +66 -0
- package/test/containerActions.test.js +104 -13
- package/test/containerOptionsBuilder.test.js +36 -0
- package/test/useContainerCreation.dom.test.js +46 -2
- package/test/useControls.dom.test.js +117 -0
- package/test/validationHelpers.test.js +51 -0
- package/.babelrc +0 -6
- package/FIXES_APPLIED.md +0 -419
- package/SECURITY.md +0 -5
- package/dist/hooks/useLogsStream.js +0 -41
- package/docs/_config.yml +0 -13
- package/docs/components_ContainerCreationPrompt.jsx.html +0 -114
- package/docs/components_ContainerRow.jsx.html +0 -151
- package/docs/components_ContainerSection.jsx.html +0 -75
- package/docs/components_Footer.jsx.html +0 -66
- package/docs/components_Header.jsx.html +0 -84
- package/docs/components_LogViewer.jsx.html +0 -91
- package/docs/components_MessageFeedback.jsx.html +0 -76
- package/docs/components_PromptField.jsx.html +0 -99
- package/docs/components_StatsBar.jsx.html +0 -105
- package/docs/components_UsageMenu.jsx.html +0 -73
- package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
- package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
- package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
- package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/global.html +0 -6145
- package/docs/helpers_actionHelpers.js.html +0 -94
- package/docs/helpers_constants.js.html +0 -81
- package/docs/helpers_dockerService_dockerService.js.html +0 -79
- package/docs/helpers_dockerService_serviceComponents_containerActions.js.html +0 -153
- package/docs/helpers_dockerService_serviceComponents_containerList.js.html +0 -81
- package/docs/helpers_dockerService_serviceComponents_containerLogs.js.html +0 -85
- package/docs/helpers_dockerService_serviceComponents_containerStats.js.html +0 -107
- package/docs/helpers_dockerService_serviceComponents_imageUtils.js.html +0 -82
- package/docs/helpers_exitWithMessage.js.html +0 -77
- package/docs/helpers_safeCall.js.html +0 -68
- package/docs/helpers_validationHelpers.js.html +0 -96
- package/docs/hooks_creation_useContainerActions.js.html +0 -120
- package/docs/hooks_creation_useContainerCreation.js.html +0 -152
- package/docs/hooks_creation_useLogsViewer.js.html +0 -102
- package/docs/hooks_useControls.js.html +0 -298
- package/docs/hooks_useLogsStream.js.html +0 -82
- package/docs/index.html +0 -65
- package/docs/index.js.html +0 -65
- package/docs/module-index.html +0 -171
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/styles/jsdoc-default.css +0 -358
- package/docs/styles/prettify-jsdoc.css +0 -111
- package/docs/styles/prettify-tomorrow.css +0 -132
- package/esbuild.config.cjs +0 -11
- package/src/helpers/actionHelpers.js +0 -52
- package/src/hooks/useLogsStream.js +0 -31
- package/test/actionHelpers.test.js +0 -106
package/src/hooks/useControls.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { useInput } from "ink";
|
|
2
3
|
import { useContainerActions } from "./creation/useContainerActions";
|
|
3
4
|
import { useContainerCreation } from "./creation/useContainerCreation";
|
|
4
5
|
import { useLogsViewer } from "./creation/useLogsViewer";
|
|
5
|
-
import {
|
|
6
|
+
import { useContainerSelection } from "./navigation/useContainerSelection";
|
|
7
|
+
import { useDebugLogs } from "./debug/useDebugLogs";
|
|
8
|
+
import { useEraseConfirmation } from "./useEraseConfirmation";
|
|
9
|
+
import { useExitHandler } from "./useExitHandler";
|
|
10
|
+
import { useContainerCommandRouter } from "./useContainerCommandRouter";
|
|
6
11
|
import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs.js";
|
|
7
|
-
import { EXIT_DELAY } from "../helpers/constants.js";
|
|
8
12
|
import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";
|
|
9
|
-
import {
|
|
13
|
+
import { buildContainerOptions } from "../helpers/containerOptionsBuilder.js";
|
|
14
|
+
import { DB_IMAGES } from "../helpers/constants.js";
|
|
10
15
|
|
|
11
16
|
// Principal hook to manage user inputs and control the app state
|
|
12
17
|
/**
|
|
@@ -15,66 +20,25 @@ import { logger } from "../helpers/logger.js";
|
|
|
15
20
|
*
|
|
16
21
|
* @param {Array<Object>} containers - Current list of Docker containers
|
|
17
22
|
* @returns {Object} controls - API for the App component
|
|
18
|
-
* @property {number} selected - Index of the currently selected container
|
|
19
|
-
* @property {function} setSelected - Setter for selected index
|
|
20
|
-
* @property {string} message - Current feedback message (creation or actions)
|
|
21
|
-
* @property {string} messageColor - Color to show for the feedback message
|
|
22
|
-
* @property {boolean} showLogs - Whether the logs viewer is active
|
|
23
|
-
* @property {Array<string>} logs - Array of log lines currently collected
|
|
24
|
-
* @property {function} exitLogs - Helper to close the logs viewer
|
|
25
|
-
* @property {boolean} creatingContainer - Whether the create-container prompt is open
|
|
26
|
-
* @property {number} creationStep - Current step in the creation flow
|
|
27
|
-
* @property {string} imageNameInput - Current value of the image name field
|
|
28
|
-
* @property {string} containerNameInput - Current value of the container name field
|
|
29
|
-
* @property {string} portInput - Current value of the port input field
|
|
30
|
-
* @property {string} envInput - Current value of the env input field
|
|
31
|
-
* @property {Object} creation - The creation hook API (setters and helpers)
|
|
32
|
-
* @property {Object} actions - The actions hook API (helpers to start/stop/remove)
|
|
33
|
-
* @property {Object} logsViewer - The logs viewer hook API
|
|
34
|
-
* @property {boolean} showDebugLogs - Whether the debug log panel is visible
|
|
35
|
-
* @property {Array<string>} debugLogs - Buffered log lines captured from the shared logger
|
|
36
|
-
* @property {boolean} confirmErase - Whether the erase confirmation dialog is active
|
|
37
23
|
*/
|
|
38
24
|
export function useControls(containers = []) {
|
|
39
|
-
const [selected, setSelected] = React.useState(0);
|
|
40
25
|
const [creatingContainer, setCreatingContainer] = React.useState(false);
|
|
41
|
-
const [confirmErase, setConfirmErase] = React.useState(false);
|
|
42
|
-
const [showDebugLogs, setShowDebugLogs] = React.useState(false);
|
|
43
|
-
const [debugLogs, setDebugLogs] = React.useState([]);
|
|
44
|
-
const total = containers.length;
|
|
45
|
-
const { exit } = useApp();
|
|
46
26
|
|
|
47
|
-
// Modular hooks
|
|
27
|
+
// — Modular hooks —
|
|
48
28
|
const actions = useContainerActions({ containers });
|
|
29
|
+
|
|
49
30
|
const creation = useContainerCreation({
|
|
50
31
|
onCreate: async ({ imageName, containerName, portInput, envInput }) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const ExposedPorts = {};
|
|
55
|
-
const PortBindings = {};
|
|
56
|
-
ports.forEach(pair => {
|
|
57
|
-
const [host, cont] = pair.split(":");
|
|
58
|
-
if (!host || !cont) return;
|
|
59
|
-
const key = `${cont}/tcp`;
|
|
60
|
-
ExposedPorts[key] = {};
|
|
61
|
-
PortBindings[key] = PortBindings[key] || [];
|
|
62
|
-
PortBindings[key].push({ HostPort: `${host}` });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const options = {
|
|
66
|
-
Tty: true,
|
|
67
|
-
};
|
|
68
|
-
if (Object.keys(ExposedPorts).length) options.ExposedPorts = ExposedPorts;
|
|
69
|
-
if (Object.keys(PortBindings).length) options.HostConfig = { PortBindings };
|
|
70
|
-
if (env.length) options.Env = env;
|
|
71
|
-
if (containerName) options.name = containerName;
|
|
72
|
-
|
|
73
|
-
actions.setMessage(`Creating container ${imageName}...`);
|
|
74
|
-
actions.setMessageColor("yellow");
|
|
32
|
+
const options = buildContainerOptions({ imageName, containerName, portInput, envInput });
|
|
33
|
+
actions.setMessage(`Creating container ${imageName}...`);
|
|
34
|
+
actions.setMessageColor("yellow");
|
|
75
35
|
try {
|
|
76
|
-
const id = await svcCreateContainer(imageName, options);
|
|
77
|
-
|
|
36
|
+
const { id, ports } = await svcCreateContainer(imageName, options);
|
|
37
|
+
let portMsg = "";
|
|
38
|
+
if (ports && ports.length) {
|
|
39
|
+
portMsg = " | Ports: " + ports.map(p => `${p.hostPort}→${p.containerPort}/${p.protocol}`).join(", ");
|
|
40
|
+
}
|
|
41
|
+
actions.setMessage(`Created container ${id}${portMsg}`);
|
|
78
42
|
actions.setMessageColor("green");
|
|
79
43
|
} catch (err) {
|
|
80
44
|
actions.setMessage(`Error creating container: ${err.message}`);
|
|
@@ -84,12 +48,27 @@ export function useControls(containers = []) {
|
|
|
84
48
|
}
|
|
85
49
|
},
|
|
86
50
|
onCancel: () => setCreatingContainer(false),
|
|
87
|
-
dbImages:
|
|
51
|
+
dbImages: DB_IMAGES,
|
|
88
52
|
});
|
|
53
|
+
|
|
89
54
|
const logsViewer = useLogsViewer();
|
|
55
|
+
const selection = useContainerSelection(containers.length);
|
|
56
|
+
const debugLogs = useDebugLogs();
|
|
90
57
|
|
|
91
|
-
|
|
92
|
-
|
|
58
|
+
const eraseConfirmation = useEraseConfirmation({
|
|
59
|
+
onConfirm: () => {
|
|
60
|
+
actions.handleAction({
|
|
61
|
+
actionFn: async (id) => await actions.removeContainer(id),
|
|
62
|
+
actionLabel: "Erasing",
|
|
63
|
+
selected: selection.selected,
|
|
64
|
+
});
|
|
65
|
+
actions.setMessageColor("yellow");
|
|
66
|
+
},
|
|
67
|
+
onCancel: () => {
|
|
68
|
+
actions.setMessage("");
|
|
69
|
+
actions.setMessageColor("");
|
|
70
|
+
},
|
|
71
|
+
});
|
|
93
72
|
|
|
94
73
|
/**
|
|
95
74
|
* Pipe container logs into the viewer while applying a hard limit.
|
|
@@ -107,40 +86,30 @@ export function useControls(containers = []) {
|
|
|
107
86
|
);
|
|
108
87
|
}, [logsViewer]);
|
|
109
88
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
setConfirmErase(false);
|
|
89
|
+
const commandRouter = useContainerCommandRouter({
|
|
90
|
+
actions,
|
|
91
|
+
containers,
|
|
92
|
+
selected: selection.selected,
|
|
93
|
+
creation,
|
|
94
|
+
logsViewer,
|
|
95
|
+
startLogsStream,
|
|
96
|
+
onStartErase: () => {
|
|
97
|
+
eraseConfirmation.startErase();
|
|
98
|
+
actions.setMessage("Are you sure you want to erase this container? (y/n)");
|
|
121
99
|
actions.setMessageColor("yellow");
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
setConfirmErase(false);
|
|
127
|
-
actions.setMessage("");
|
|
128
|
-
actions.setMessageColor("");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
actions.setMessage("Are you sure you want to erase this container? (y/n)");
|
|
133
|
-
actions.setMessageColor("yellow");
|
|
134
|
-
}, [actions, selected]);
|
|
100
|
+
},
|
|
101
|
+
onToggleDebug: () => debugLogs.setShowDebugLogs((prev) => !prev),
|
|
102
|
+
onStartCreate: () => setCreatingContainer(true),
|
|
103
|
+
});
|
|
135
104
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (input === "q" || key.escape) {
|
|
105
|
+
const exitHandler = useExitHandler({
|
|
106
|
+
onBeforeExit: () => {
|
|
107
|
+
actions.setMessage("Exiting...");
|
|
108
|
+
actions.setMessageColor("yellow");
|
|
141
109
|
logsViewer.closeLogs();
|
|
142
|
-
|
|
143
|
-
|
|
110
|
+
debugLogs.setShowDebugLogs(false);
|
|
111
|
+
},
|
|
112
|
+
});
|
|
144
113
|
|
|
145
114
|
/**
|
|
146
115
|
* Route keystrokes to the container creation wizard.
|
|
@@ -178,164 +147,17 @@ export function useControls(containers = []) {
|
|
|
178
147
|
}
|
|
179
148
|
}, [creation, setCreatingContainer]);
|
|
180
149
|
|
|
181
|
-
|
|
182
|
-
* Navigate the container list using arrow keys.
|
|
183
|
-
* @returns {boolean} True when the input was consumed.
|
|
184
|
-
*/
|
|
185
|
-
const handleNavigation = React.useCallback((input, key) => {
|
|
186
|
-
if (total === 0) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (key.upArrow) {
|
|
191
|
-
setSelected((i) => (i === 0 ? total - 1 : i - 1));
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (key.downArrow) {
|
|
196
|
-
setSelected((i) => (i === total - 1 ? 0 : i + 1));
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return false;
|
|
201
|
-
}, [total]);
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Handle the quit command.
|
|
205
|
-
* @returns {boolean} True when the input was consumed.
|
|
206
|
-
*/
|
|
207
|
-
const handleExitCommand = React.useCallback((input) => {
|
|
208
|
-
if (input !== "q") {
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
actions.setMessage("Exiting...");
|
|
213
|
-
actions.setMessageColor("yellow");
|
|
214
|
-
logsViewer.closeLogs();
|
|
215
|
-
setShowDebugLogs(false);
|
|
216
|
-
|
|
217
|
-
const clearTerminal = () => {
|
|
218
|
-
try {
|
|
219
|
-
if (process.stdout && process.stdout.isTTY) {
|
|
220
|
-
process.stdout.write("\u001Bc");
|
|
221
|
-
} else {
|
|
222
|
-
console.clear();
|
|
223
|
-
}
|
|
224
|
-
} catch (err) {
|
|
225
|
-
// Ignore clear errors to avoid blocking exit.
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
setTimeout(() => {
|
|
230
|
-
clearTerminal();
|
|
231
|
-
exit();
|
|
232
|
-
if (process.env.NODE_ENV !== "test") {
|
|
233
|
-
setTimeout(() => process.exit(0), 50);
|
|
234
|
-
}
|
|
235
|
-
}, EXIT_DELAY);
|
|
236
|
-
return true;
|
|
237
|
-
}, [actions, exit, logsViewer]);
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Execute Docker-related commands and prompt transitions.
|
|
241
|
-
* @returns {boolean} True when the input was consumed.
|
|
242
|
-
*/
|
|
243
|
-
const handleDockerCommands = React.useCallback((input) => {
|
|
244
|
-
const container = containers[selected];
|
|
245
|
-
|
|
246
|
-
if (input === "i") {
|
|
247
|
-
actions.handleAction({
|
|
248
|
-
actionFn: async (id) => await actions.startContainer(id),
|
|
249
|
-
actionLabel: "Starting",
|
|
250
|
-
selected,
|
|
251
|
-
stateCheck: (c) => (c.state === "running" || c.status === "running") && "Container is already running."
|
|
252
|
-
});
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (input === "p") {
|
|
257
|
-
actions.handleAction({
|
|
258
|
-
actionFn: async (id) => await actions.stopContainer(id),
|
|
259
|
-
actionLabel: "Stopping",
|
|
260
|
-
selected,
|
|
261
|
-
stateCheck: (c) => (c.state === "exited" || c.status === "exited" || c.state === "stopped" || c.status === "stopped") && "Container is already stopped."
|
|
262
|
-
});
|
|
263
|
-
return true;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (input === "r") {
|
|
267
|
-
actions.handleAction({
|
|
268
|
-
actionFn: async (id) => await actions.restartContainer(id),
|
|
269
|
-
actionLabel: "Restarting",
|
|
270
|
-
selected,
|
|
271
|
-
});
|
|
272
|
-
return true;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (input === "e") {
|
|
276
|
-
if (!container) {
|
|
277
|
-
return true;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
setConfirmErase(true);
|
|
281
|
-
actions.setMessage("Are you sure you want to erase this container? (y/n)");
|
|
282
|
-
actions.setMessageColor("yellow");
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (input === "l") {
|
|
287
|
-
if (!container) {
|
|
288
|
-
return true;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
logsViewer.openLogs();
|
|
292
|
-
startLogsStream(container.id);
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (input === "c") {
|
|
297
|
-
setCreatingContainer(true);
|
|
298
|
-
creation.setStep(0);
|
|
299
|
-
creation.setImageName("");
|
|
300
|
-
creation.setContainerName("");
|
|
301
|
-
creation.setPortInput("");
|
|
302
|
-
creation.setEnvInput("");
|
|
303
|
-
creation.setMessage("Insert the name of the image to create: ");
|
|
304
|
-
creation.setMessageColor("yellow");
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (input === "d") {
|
|
309
|
-
setShowDebugLogs((prev) => !prev);
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return false;
|
|
314
|
-
}, [actions, containers, creation, logsViewer, selected, setConfirmErase, setCreatingContainer, startLogsStream, setShowDebugLogs]);
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Default handler executed when no special mode is active.
|
|
318
|
-
*/
|
|
319
|
-
const processGlobalInput = React.useCallback((input, key) => {
|
|
320
|
-
if (handleNavigation(input, key)) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (handleExitCommand(input)) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
handleDockerCommands(input);
|
|
329
|
-
}, [handleDockerCommands, handleExitCommand, handleNavigation]);
|
|
330
|
-
|
|
150
|
+
// Single keyboard entry point
|
|
331
151
|
useInput((input, key) => {
|
|
332
|
-
if (confirmErase) {
|
|
333
|
-
processEraseConfirmation(input, key);
|
|
152
|
+
if (eraseConfirmation.confirmErase) {
|
|
153
|
+
eraseConfirmation.processEraseConfirmation(input, key);
|
|
334
154
|
return;
|
|
335
155
|
}
|
|
336
156
|
|
|
337
157
|
if (logsViewer.showLogs) {
|
|
338
|
-
|
|
158
|
+
if (input === "q" || key.escape) {
|
|
159
|
+
logsViewer.closeLogs();
|
|
160
|
+
}
|
|
339
161
|
return;
|
|
340
162
|
}
|
|
341
163
|
|
|
@@ -344,46 +166,25 @@ export function useControls(containers = []) {
|
|
|
344
166
|
return;
|
|
345
167
|
}
|
|
346
168
|
|
|
347
|
-
if (showDebugLogs && key.escape) {
|
|
348
|
-
setShowDebugLogs(false);
|
|
169
|
+
if (debugLogs.showDebugLogs && key.escape) {
|
|
170
|
+
debugLogs.setShowDebugLogs(false);
|
|
349
171
|
return;
|
|
350
172
|
}
|
|
351
173
|
|
|
352
|
-
|
|
174
|
+
commandRouter.handleDockerCommands(input);
|
|
175
|
+
exitHandler.handleExitCommand(input);
|
|
176
|
+
selection.handleNavigation(input, key);
|
|
353
177
|
});
|
|
354
178
|
|
|
355
|
-
// Mirror global logger entries so the D shortcut can surface them without leaving the CLI.
|
|
356
|
-
React.useEffect(() => {
|
|
357
|
-
const unsubscribe = logger.subscribe((entry) => {
|
|
358
|
-
setDebugLogs((prev) => {
|
|
359
|
-
const extras = (entry.args || []).map((arg) => {
|
|
360
|
-
if (typeof arg === "string") return arg;
|
|
361
|
-
if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
|
|
362
|
-
try {
|
|
363
|
-
return JSON.stringify(arg);
|
|
364
|
-
} catch (err) {
|
|
365
|
-
return "[unserializable]";
|
|
366
|
-
}
|
|
367
|
-
}).filter(Boolean);
|
|
368
|
-
const line = extras.length ? `${entry.formatted} ${extras.join(" ")}` : entry.formatted;
|
|
369
|
-
const next = [...prev, line];
|
|
370
|
-
return next.slice(-200);
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
return unsubscribe;
|
|
374
|
-
}, []);
|
|
375
|
-
|
|
376
179
|
return {
|
|
377
|
-
selected,
|
|
378
|
-
setSelected,
|
|
379
|
-
// Map messaging to creation when creating, otherwise to actions
|
|
180
|
+
selected: selection.selected,
|
|
181
|
+
setSelected: selection.setSelected,
|
|
380
182
|
message: creatingContainer ? creation.message : actions.message,
|
|
381
183
|
messageColor: creatingContainer ? creation.messageColor : actions.messageColor,
|
|
382
184
|
showLogs: logsViewer.showLogs,
|
|
383
185
|
logs: logsViewer.logs,
|
|
384
|
-
exitLogs,
|
|
186
|
+
exitLogs: logsViewer.closeLogs,
|
|
385
187
|
creatingContainer,
|
|
386
|
-
// Expose creation fields in the shape App expects
|
|
387
188
|
creationStep: creation.step,
|
|
388
189
|
imageNameInput: creation.imageName,
|
|
389
190
|
containerNameInput: creation.containerName,
|
|
@@ -392,8 +193,8 @@ export function useControls(containers = []) {
|
|
|
392
193
|
creation,
|
|
393
194
|
actions,
|
|
394
195
|
logsViewer,
|
|
395
|
-
confirmErase,
|
|
396
|
-
showDebugLogs,
|
|
397
|
-
debugLogs,
|
|
196
|
+
confirmErase: eraseConfirmation.confirmErase,
|
|
197
|
+
showDebugLogs: debugLogs.showDebugLogs,
|
|
198
|
+
debugLogs: debugLogs.debugLogs,
|
|
398
199
|
};
|
|
399
200
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to manage the erase confirmation dialog flow.
|
|
5
|
+
*
|
|
6
|
+
* Encapsulates the `confirmErase` state and the input handler that resolves it.
|
|
7
|
+
* The caller is responsible for what happens on confirm/cancel via the callbacks.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} params
|
|
10
|
+
* @param {Function} params.onConfirm - Called when the user presses y/Y. Should trigger the erase action.
|
|
11
|
+
* @param {Function} params.onCancel - Called when the user presses n/N or Escape. Should clean up messages.
|
|
12
|
+
* @returns {{ confirmErase: boolean, startErase: Function, processEraseConfirmation: Function }}
|
|
13
|
+
*/
|
|
14
|
+
export function useEraseConfirmation({ onConfirm, onCancel }) {
|
|
15
|
+
const [confirmErase, setConfirmErase] = useState(false);
|
|
16
|
+
|
|
17
|
+
const startErase = useCallback(() => {
|
|
18
|
+
setConfirmErase(true);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const processEraseConfirmation = useCallback(
|
|
22
|
+
(input, key) => {
|
|
23
|
+
if (input === "y" || input === "Y") {
|
|
24
|
+
onConfirm();
|
|
25
|
+
setConfirmErase(false);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (input === "n" || input === "N" || key.escape) {
|
|
30
|
+
setConfirmErase(false);
|
|
31
|
+
onCancel();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[onConfirm, onCancel]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return { confirmErase, startErase, processEraseConfirmation };
|
|
39
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useApp } from 'ink';
|
|
3
|
+
import { EXIT_DELAY } from '../helpers/constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook that encapsulates the application exit sequence triggered by pressing `q`.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Detect the `q` keypress
|
|
10
|
+
* - Invoke an optional `onBeforeExit` callback for resource cleanup (e.g. closing logs)
|
|
11
|
+
* - Clear the terminal after a short delay
|
|
12
|
+
* - Call Ink's `exit()` and, outside of test environments, `process.exit(0)`
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} [options]
|
|
15
|
+
* @param {Function} [options.onBeforeExit] - Optional callback invoked synchronously
|
|
16
|
+
* before the exit delay starts. Use it to close logs, reset UI state, etc.
|
|
17
|
+
* @returns {{ handleExitCommand: Function }}
|
|
18
|
+
*/
|
|
19
|
+
export function useExitHandler({ onBeforeExit } = {}) {
|
|
20
|
+
const { exit } = useApp();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle the quit command.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors the exact logic of `handleExitCommand` in `useControls.js`:
|
|
26
|
+
* 1. Guard: return false if input is not 'q'
|
|
27
|
+
* 2. Call onBeforeExit (replaces inline logsViewer.closeLogs + setShowDebugLogs(false))
|
|
28
|
+
* 3. After EXIT_DELAY: clear terminal, call exit(), then process.exit(0) outside tests
|
|
29
|
+
*
|
|
30
|
+
* @param {string} input - Raw character input from Ink's useInput
|
|
31
|
+
* @returns {boolean} True when the input was consumed, false otherwise
|
|
32
|
+
*/
|
|
33
|
+
const handleExitCommand = useCallback((input) => {
|
|
34
|
+
if (input !== 'q') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof onBeforeExit === 'function') {
|
|
39
|
+
onBeforeExit();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const clearTerminal = () => {
|
|
43
|
+
try {
|
|
44
|
+
if (process.stdout && process.stdout.isTTY) {
|
|
45
|
+
process.stdout.write('\u001Bc');
|
|
46
|
+
} else {
|
|
47
|
+
console.clear();
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// Ignore clear errors to avoid blocking exit.
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
clearTerminal();
|
|
56
|
+
exit();
|
|
57
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
58
|
+
setTimeout(() => process.exit(0), 50);
|
|
59
|
+
}
|
|
60
|
+
}, EXIT_DELAY);
|
|
61
|
+
|
|
62
|
+
return true;
|
|
63
|
+
}, [exit, onBeforeExit]);
|
|
64
|
+
|
|
65
|
+
return { handleExitCommand };
|
|
66
|
+
}
|
|
@@ -3,14 +3,13 @@ import { jest } from '@jest/globals';
|
|
|
3
3
|
describe('containerActions service functions (mocked ESM imports)', () => {
|
|
4
4
|
afterEach(() => jest.resetModules());
|
|
5
5
|
|
|
6
|
-
test('createContainer returns id when image exists', async () => {
|
|
7
|
-
// mock imageUtils and dockerService before importing the module under test
|
|
6
|
+
test('createContainer returns { id, ports } when image exists', async () => {
|
|
8
7
|
const imageUtilsMock = {
|
|
9
8
|
imageExists: jest.fn().mockResolvedValue(true),
|
|
10
9
|
pullImage: jest.fn()
|
|
11
10
|
};
|
|
12
11
|
|
|
13
|
-
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
|
|
12
|
+
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '3306/tcp': {} } } });
|
|
14
13
|
const dockerMock = {
|
|
15
14
|
createContainer: jest.fn().mockResolvedValue({ id: 'cid-123' }),
|
|
16
15
|
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
@@ -27,16 +26,107 @@ describe('containerActions service functions (mocked ESM imports)', () => {
|
|
|
27
26
|
|
|
28
27
|
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
29
28
|
|
|
30
|
-
const
|
|
31
|
-
expect(
|
|
32
|
-
|
|
33
|
-
expect(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const result = await mod.createContainer('mysql:8', { Name: 'test' });
|
|
30
|
+
expect(result).toHaveProperty('id', 'cid-123');
|
|
31
|
+
expect(result).toHaveProperty('ports');
|
|
32
|
+
expect(Array.isArray(result.ports)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('createContainer ports contain containerPort, hostPort, protocol, source fields', async () => {
|
|
36
|
+
const imageUtilsMock = {
|
|
37
|
+
imageExists: jest.fn().mockResolvedValue(true),
|
|
38
|
+
pullImage: jest.fn()
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '80/tcp': {} } } });
|
|
42
|
+
const dockerMock = {
|
|
43
|
+
createContainer: jest.fn().mockResolvedValue({ id: 'cid-ports' }),
|
|
44
|
+
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
45
|
+
listContainers: jest.fn().mockResolvedValue([])
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
49
|
+
...imageUtilsMock
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
53
|
+
docker: dockerMock
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
57
|
+
const result = await mod.createContainer('nginx:latest', {});
|
|
58
|
+
expect(result.ports.length).toBeGreaterThan(0);
|
|
59
|
+
const port = result.ports[0];
|
|
60
|
+
expect(port).toHaveProperty('containerPort');
|
|
61
|
+
expect(port).toHaveProperty('hostPort');
|
|
62
|
+
expect(port).toHaveProperty('protocol');
|
|
63
|
+
expect(port).toHaveProperty('source');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('createContainer uses IMAGE_PROFILES defaultPort as fallback when ExposedPorts empty', async () => {
|
|
67
|
+
const imageUtilsMock = {
|
|
68
|
+
imageExists: jest.fn().mockResolvedValue(true),
|
|
69
|
+
pullImage: jest.fn()
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// inspect returns empty ExposedPorts
|
|
73
|
+
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
|
|
74
|
+
const dockerMock = {
|
|
75
|
+
createContainer: jest.fn().mockResolvedValue({ id: 'cid-fallback' }),
|
|
76
|
+
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
77
|
+
listContainers: jest.fn().mockResolvedValue([])
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
81
|
+
...imageUtilsMock
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
85
|
+
docker: dockerMock
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const { IMAGE_PROFILES } = await import('../src/helpers/constants.js');
|
|
89
|
+
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
90
|
+
const result = await mod.createContainer('mysql:8', {}, IMAGE_PROFILES);
|
|
91
|
+
|
|
92
|
+
expect(result.id).toBe('cid-fallback');
|
|
93
|
+
// Should have auto port from defaultPort fallback
|
|
94
|
+
const mysqlDefault = IMAGE_PROFILES.mysql.defaultPort;
|
|
95
|
+
const portEntry = result.ports.find(p => p.containerPort === mysqlDefault);
|
|
96
|
+
expect(portEntry).toBeDefined();
|
|
97
|
+
expect(portEntry.source).toBe('auto');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('createContainer returns { id, ports: [] } when no ports and image not in profiles', async () => {
|
|
101
|
+
const imageUtilsMock = {
|
|
102
|
+
imageExists: jest.fn().mockResolvedValue(true),
|
|
103
|
+
pullImage: jest.fn()
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
|
|
107
|
+
const dockerMock = {
|
|
108
|
+
createContainer: jest.fn().mockResolvedValue({ id: 'cid-noport' }),
|
|
109
|
+
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
110
|
+
listContainers: jest.fn().mockResolvedValue([])
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
114
|
+
...imageUtilsMock
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
118
|
+
docker: dockerMock
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
const { IMAGE_PROFILES } = await import('../src/helpers/constants.js');
|
|
122
|
+
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
123
|
+
const result = await mod.createContainer('unknownimage:latest', {}, IMAGE_PROFILES);
|
|
124
|
+
|
|
125
|
+
expect(result.id).toBe('cid-noport');
|
|
126
|
+
expect(result.ports).toEqual([]);
|
|
37
127
|
});
|
|
38
128
|
|
|
39
|
-
test('createContainer pulls image when not present and returns
|
|
129
|
+
test('createContainer pulls image when not present and returns { id, ports }', async () => {
|
|
40
130
|
const imageUtilsMock = {
|
|
41
131
|
imageExists: jest.fn().mockResolvedValue(false),
|
|
42
132
|
pullImage: jest.fn().mockResolvedValue(true)
|
|
@@ -58,8 +148,9 @@ describe('containerActions service functions (mocked ESM imports)', () => {
|
|
|
58
148
|
}));
|
|
59
149
|
|
|
60
150
|
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
61
|
-
const
|
|
62
|
-
expect(id).toBe('CID-456');
|
|
151
|
+
const result = await mod.createContainer('busybox:1.0');
|
|
152
|
+
expect(result.id).toBe('CID-456');
|
|
153
|
+
expect(result).toHaveProperty('ports');
|
|
63
154
|
const imageUtils = await import('../src/helpers/dockerService/serviceComponents/imageUtils.js');
|
|
64
155
|
expect(imageUtils.pullImage).toHaveBeenCalledWith('busybox:1.0');
|
|
65
156
|
});
|