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.
- package/README.es.md +222 -0
- package/README.md +108 -26
- package/dist/App.js +7 -1
- package/dist/components/ContainerCreationPrompt.js +26 -10
- package/dist/components/ControlsHUD.js +106 -0
- package/dist/components/SuggestionPanel.js +16 -7
- package/dist/components/UsageMenu.js +1 -1
- package/dist/helpers/appState.js +26 -0
- package/dist/helpers/constants.js +60 -21
- package/dist/helpers/dockerHubService.js +111 -0
- package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
- package/dist/helpers/logger.js +1 -1
- package/dist/hooks/creation/useContainerActions.js +22 -5
- package/dist/hooks/creation/useContainerCreation.js +414 -107
- package/dist/hooks/useContainerCommandRouter.js +13 -3
- package/dist/hooks/useControls.js +28 -7
- package/dist/hooks/useShellMode.js +101 -0
- package/dist/index.js +10 -1
- package/package.json +4 -1
- package/.atl/skill-registry.md +0 -30
- package/.editorconfig +0 -12
- package/.eslintrc.json +0 -30
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/docs.yml +0 -42
- package/.prettierignore +0 -4
- package/.prettierrc +0 -8
- package/CHANGELOG.md +0 -165
- package/CODE_OF_CONDUCT.md +0 -12
- package/CONTRIBUTING.md +0 -35
- package/SECURITY.md +0 -5
- package/__mocks__/ink.cjs +0 -17
- package/_config.yml +0 -13
- package/babel.config.cjs +0 -23
- package/coverage/clover.xml +0 -694
- package/coverage/coverage-final.json +0 -21
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -376
- package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -244
- package/coverage/lcov-report/components/index.html +0 -146
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +0 -298
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
- package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
- package/coverage/lcov-report/helpers/index.html +0 -191
- package/coverage/lcov-report/helpers/logger.js.html +0 -424
- package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
- package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
- package/coverage/lcov-report/hooks/creation/index.html +0 -146
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -805
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
- package/coverage/lcov-report/hooks/debug/index.html +0 -116
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
- package/coverage/lcov-report/hooks/index.html +0 -161
- package/coverage/lcov-report/hooks/navigation/index.html +0 -116
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
- package/coverage/lcov-report/hooks/useControls.js.html +0 -826
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
- package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
- package/coverage/lcov-report/index.html +0 -206
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov.info +0 -1402
- package/fix-imports.cjs +0 -54
- package/jest.config.cjs +0 -14
- package/src/App.jsx +0 -96
- package/src/components/ContainerCreationPrompt.jsx +0 -97
- package/src/components/ContainerList.jsx +0 -32
- package/src/components/ContainerRow.jsx +0 -84
- package/src/components/ContainerSection.jsx +0 -24
- package/src/components/Footer.jsx +0 -15
- package/src/components/Header.jsx +0 -41
- package/src/components/LogViewer.jsx +0 -36
- package/src/components/MessageFeedback.jsx +0 -25
- package/src/components/PromptField.jsx +0 -48
- package/src/components/StatsBar.jsx +0 -70
- package/src/components/SuggestionPanel.jsx +0 -53
- package/src/components/UsageMenu.jsx +0 -52
- package/src/helpers/appInfo.js +0 -23
- package/src/helpers/constants.js +0 -71
- package/src/helpers/containerOptionsBuilder.js +0 -53
- package/src/helpers/dockerService/dockerService.js +0 -28
- package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
- package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
- package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
- package/src/helpers/exitWithMessage.js +0 -32
- package/src/helpers/imageNameUtils.js +0 -32
- package/src/helpers/logger.js +0 -113
- package/src/helpers/safeCall.js +0 -18
- package/src/helpers/validationHelpers.js +0 -87
- package/src/hooks/creation/useContainerActions.js +0 -76
- package/src/hooks/creation/useContainerCreation.js +0 -240
- package/src/hooks/creation/useLogsViewer.js +0 -51
- package/src/hooks/debug/useDebugLogs.js +0 -34
- package/src/hooks/navigation/useContainerSelection.js +0 -28
- package/src/hooks/useContainerCommandRouter.js +0 -123
- package/src/hooks/useContainerStats.js +0 -44
- package/src/hooks/useContainers.js +0 -26
- package/src/hooks/useControls.js +0 -247
- package/src/hooks/useEraseConfirmation.js +0 -39
- package/src/hooks/useExitHandler.js +0 -69
- package/src/index.js +0 -16
- package/test/ContainerCreationPrompt.dom.test.js +0 -68
- package/test/SuggestionPanel.dom.test.js +0 -53
- package/test/constants.test.js +0 -34
- package/test/containerActions.test.js +0 -229
- package/test/containerList.test.js +0 -79
- package/test/containerOptionsBuilder.test.js +0 -36
- package/test/jest.setup.js +0 -7
- package/test/safeCall.test.js +0 -25
- package/test/useContainerCreation.dom.test.js +0 -264
- package/test/useControls.dom.test.js +0 -313
- package/test/validationHelpers.test.js +0 -111
package/src/hooks/useControls.js
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useInput } from 'ink';
|
|
3
|
-
import { useContainerActions } from './creation/useContainerActions';
|
|
4
|
-
import { useContainerCreation } from './creation/useContainerCreation';
|
|
5
|
-
import { useLogsViewer } from './creation/useLogsViewer';
|
|
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';
|
|
11
|
-
import { getLogsStream } from '../helpers/dockerService/serviceComponents/containerLogs.js';
|
|
12
|
-
import { createContainer as svcCreateContainer } from '../helpers/dockerService/serviceComponents/containerActions.js';
|
|
13
|
-
import { buildContainerOptions } from '../helpers/containerOptionsBuilder.js';
|
|
14
|
-
import { DB_IMAGES } from '../helpers/constants.js';
|
|
15
|
-
|
|
16
|
-
// Principal hook to manage user inputs and control the app state
|
|
17
|
-
/**
|
|
18
|
-
* Main hook that wires user input, creation, actions and logs viewing.
|
|
19
|
-
* It coordinates the modular hooks and exposes a compact API consumed by the App.
|
|
20
|
-
*
|
|
21
|
-
* @param {Array<Object>} containers - Current list of Docker containers
|
|
22
|
-
* @returns {Object} controls - API for the App component
|
|
23
|
-
*/
|
|
24
|
-
export function useControls(containers = []) {
|
|
25
|
-
const [creatingContainer, setCreatingContainer] = React.useState(false);
|
|
26
|
-
|
|
27
|
-
// — Modular hooks —
|
|
28
|
-
const actions = useContainerActions({ containers });
|
|
29
|
-
|
|
30
|
-
const creation = useContainerCreation({
|
|
31
|
-
onCreate: async ({ imageName, containerName, portInput, envInput }) => {
|
|
32
|
-
const options = buildContainerOptions({
|
|
33
|
-
imageName,
|
|
34
|
-
containerName,
|
|
35
|
-
portInput,
|
|
36
|
-
envInput,
|
|
37
|
-
});
|
|
38
|
-
actions.setMessage(`Creating container ${imageName}...`);
|
|
39
|
-
actions.setMessageColor('yellow');
|
|
40
|
-
try {
|
|
41
|
-
const { id, ports } = await svcCreateContainer(imageName, options);
|
|
42
|
-
let portMsg = '';
|
|
43
|
-
if (ports && ports.length) {
|
|
44
|
-
portMsg =
|
|
45
|
-
' | Ports: ' +
|
|
46
|
-
ports
|
|
47
|
-
.map((p) => `${p.hostPort}→${p.containerPort}/${p.protocol}`)
|
|
48
|
-
.join(', ');
|
|
49
|
-
}
|
|
50
|
-
actions.setMessage(`Created container ${id}${portMsg}`);
|
|
51
|
-
actions.setMessageColor('green');
|
|
52
|
-
} catch (err) {
|
|
53
|
-
actions.setMessage(`Error creating container: ${err.message}`);
|
|
54
|
-
actions.setMessageColor('red');
|
|
55
|
-
} finally {
|
|
56
|
-
setCreatingContainer(false);
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
onCancel: () => setCreatingContainer(false),
|
|
60
|
-
dbImages: DB_IMAGES,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const logsViewer = useLogsViewer();
|
|
64
|
-
const selection = useContainerSelection(containers.length);
|
|
65
|
-
const debugLogs = useDebugLogs();
|
|
66
|
-
|
|
67
|
-
const eraseConfirmation = useEraseConfirmation({
|
|
68
|
-
onConfirm: () => {
|
|
69
|
-
actions.handleAction({
|
|
70
|
-
actionFn: async (id) => await actions.removeContainer(id),
|
|
71
|
-
actionLabel: 'Erasing',
|
|
72
|
-
selected: selection.selected,
|
|
73
|
-
});
|
|
74
|
-
actions.setMessageColor('yellow');
|
|
75
|
-
},
|
|
76
|
-
onCancel: () => {
|
|
77
|
-
actions.setMessage('');
|
|
78
|
-
actions.setMessageColor('');
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Pipe container logs into the viewer while applying a hard limit.
|
|
84
|
-
* @param {string} containerId - Container identifier used by Docker.
|
|
85
|
-
*/
|
|
86
|
-
const startLogsStream = React.useCallback(
|
|
87
|
-
(containerId) => {
|
|
88
|
-
getLogsStream(
|
|
89
|
-
containerId,
|
|
90
|
-
(data) =>
|
|
91
|
-
logsViewer.setLogs((prev) => {
|
|
92
|
-
const newLogs = [...prev, ...data.split('\n').filter(Boolean)];
|
|
93
|
-
return newLogs.slice(-1000);
|
|
94
|
-
}),
|
|
95
|
-
() => {},
|
|
96
|
-
(err) =>
|
|
97
|
-
logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
|
|
98
|
-
);
|
|
99
|
-
},
|
|
100
|
-
[logsViewer]
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const commandRouter = useContainerCommandRouter({
|
|
104
|
-
actions,
|
|
105
|
-
containers,
|
|
106
|
-
selected: selection.selected,
|
|
107
|
-
creation,
|
|
108
|
-
logsViewer,
|
|
109
|
-
startLogsStream,
|
|
110
|
-
onStartErase: () => {
|
|
111
|
-
eraseConfirmation.startErase();
|
|
112
|
-
actions.setMessage(
|
|
113
|
-
'Are you sure you want to erase this container? (y/n)'
|
|
114
|
-
);
|
|
115
|
-
actions.setMessageColor('yellow');
|
|
116
|
-
},
|
|
117
|
-
onToggleDebug: () => debugLogs.setShowDebugLogs((prev) => !prev),
|
|
118
|
-
onStartCreate: () => setCreatingContainer(true),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const exitHandler = useExitHandler({
|
|
122
|
-
onBeforeExit: () => {
|
|
123
|
-
actions.setMessage('Exiting...');
|
|
124
|
-
actions.setMessageColor('yellow');
|
|
125
|
-
logsViewer.closeLogs();
|
|
126
|
-
debugLogs.setShowDebugLogs(false);
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Route keystrokes to the container creation wizard.
|
|
132
|
-
*/
|
|
133
|
-
const processCreationInput = React.useCallback(
|
|
134
|
-
(input, key) => {
|
|
135
|
-
const step = creation.step;
|
|
136
|
-
|
|
137
|
-
const removeLastChar = (setter) =>
|
|
138
|
-
setter((value) => (value || '').slice(0, -1));
|
|
139
|
-
const appendChar = (setter, current, ch) => setter((current || '') + ch);
|
|
140
|
-
|
|
141
|
-
if (key.escape) {
|
|
142
|
-
creation.cancelCreation();
|
|
143
|
-
setCreatingContainer(false);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (input === '\r' || input === '\n') {
|
|
148
|
-
// If on step 0 with a focused suggestion, apply it instead of advancing
|
|
149
|
-
if (step === 0 && creation.selectedSuggestionIndex >= 0) {
|
|
150
|
-
creation.applyFocusedSuggestion();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
creation.nextStep();
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Arrow keys on step 0: navigate suggestion list
|
|
158
|
-
if (step === 0 && creation.suggestions.length > 0) {
|
|
159
|
-
if (key.upArrow) {
|
|
160
|
-
creation.moveSuggestionSelection(-1);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (key.downArrow) {
|
|
164
|
-
creation.moveSuggestionSelection(1);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (key.backspace || key.delete) {
|
|
170
|
-
if (step === 0) {
|
|
171
|
-
const newValue = (creation.imageName || '').slice(0, -1);
|
|
172
|
-
creation.updateImageInput(newValue);
|
|
173
|
-
}
|
|
174
|
-
if (step === 1) removeLastChar(creation.setContainerName);
|
|
175
|
-
if (step === 2) removeLastChar(creation.setPortInput);
|
|
176
|
-
if (step === 3) removeLastChar(creation.setEnvInput);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
181
|
-
if (step === 0) {
|
|
182
|
-
creation.updateImageInput((creation.imageName || '') + input);
|
|
183
|
-
}
|
|
184
|
-
if (step === 1)
|
|
185
|
-
appendChar(creation.setContainerName, creation.containerName, input);
|
|
186
|
-
if (step === 2)
|
|
187
|
-
appendChar(creation.setPortInput, creation.portInput, input);
|
|
188
|
-
if (step === 3)
|
|
189
|
-
appendChar(creation.setEnvInput, creation.envInput, input);
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
[creation, setCreatingContainer]
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
// Single keyboard entry point
|
|
196
|
-
useInput((input, key) => {
|
|
197
|
-
if (eraseConfirmation.confirmErase) {
|
|
198
|
-
eraseConfirmation.processEraseConfirmation(input, key);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (logsViewer.showLogs) {
|
|
203
|
-
if (input === 'q' || key.escape) {
|
|
204
|
-
logsViewer.closeLogs();
|
|
205
|
-
}
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (creatingContainer) {
|
|
210
|
-
processCreationInput(input, key);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (debugLogs.showDebugLogs && key.escape) {
|
|
215
|
-
debugLogs.setShowDebugLogs(false);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
commandRouter.handleDockerCommands(input);
|
|
220
|
-
exitHandler.handleExitCommand(input);
|
|
221
|
-
selection.handleNavigation(input, key);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
selected: selection.selected,
|
|
226
|
-
setSelected: selection.setSelected,
|
|
227
|
-
message: creatingContainer ? creation.message : actions.message,
|
|
228
|
-
messageColor: creatingContainer
|
|
229
|
-
? creation.messageColor
|
|
230
|
-
: actions.messageColor,
|
|
231
|
-
showLogs: logsViewer.showLogs,
|
|
232
|
-
logs: logsViewer.logs,
|
|
233
|
-
exitLogs: logsViewer.closeLogs,
|
|
234
|
-
creatingContainer,
|
|
235
|
-
creationStep: creation.step,
|
|
236
|
-
imageNameInput: creation.imageName,
|
|
237
|
-
containerNameInput: creation.containerName,
|
|
238
|
-
portInput: creation.portInput,
|
|
239
|
-
envInput: creation.envInput,
|
|
240
|
-
creation,
|
|
241
|
-
actions,
|
|
242
|
-
logsViewer,
|
|
243
|
-
confirmErase: eraseConfirmation.confirmErase,
|
|
244
|
-
showDebugLogs: debugLogs.showDebugLogs,
|
|
245
|
-
debugLogs: debugLogs.debugLogs,
|
|
246
|
-
};
|
|
247
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
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(
|
|
34
|
-
(input) => {
|
|
35
|
-
if (input !== 'q') {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (typeof onBeforeExit === 'function') {
|
|
40
|
-
onBeforeExit();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const clearTerminal = () => {
|
|
44
|
-
try {
|
|
45
|
-
if (process.stdout && process.stdout.isTTY) {
|
|
46
|
-
process.stdout.write('\u001Bc');
|
|
47
|
-
} else {
|
|
48
|
-
console.clear();
|
|
49
|
-
}
|
|
50
|
-
} catch (err) {
|
|
51
|
-
// Ignore clear errors to avoid blocking exit.
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
setTimeout(() => {
|
|
56
|
-
clearTerminal();
|
|
57
|
-
exit();
|
|
58
|
-
if (process.env.NODE_ENV !== 'test') {
|
|
59
|
-
setTimeout(() => process.exit(0), 50);
|
|
60
|
-
}
|
|
61
|
-
}, EXIT_DELAY);
|
|
62
|
-
|
|
63
|
-
return true;
|
|
64
|
-
},
|
|
65
|
-
[exit, onBeforeExit]
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return { handleExitCommand };
|
|
69
|
-
}
|
package/src/index.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Entry point for the CDD CLI application.
|
|
4
|
-
*
|
|
5
|
-
* @module index
|
|
6
|
-
* @example
|
|
7
|
-
* // Run the CLI
|
|
8
|
-
* node index.js
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import { render } from 'ink';
|
|
13
|
-
import App from './App.jsx';
|
|
14
|
-
|
|
15
|
-
console.clear();
|
|
16
|
-
render(<App />);
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
import React from 'react';
|
|
5
|
-
import { render } from '@testing-library/react';
|
|
6
|
-
import { ContainerCreationPrompt } from '../src/components/ContainerCreationPrompt.jsx';
|
|
7
|
-
|
|
8
|
-
describe('ContainerCreationPrompt — SuggestionPanel wiring', () => {
|
|
9
|
-
test('does NOT render SuggestionPanel when step > 0', () => {
|
|
10
|
-
const { queryByText } = render(
|
|
11
|
-
<ContainerCreationPrompt
|
|
12
|
-
step={1}
|
|
13
|
-
imageName="nginx"
|
|
14
|
-
containerName=""
|
|
15
|
-
portInput=""
|
|
16
|
-
envInput=""
|
|
17
|
-
message=""
|
|
18
|
-
messageColor="yellow"
|
|
19
|
-
suggestions={['nginx', 'node']}
|
|
20
|
-
selectedSuggestionIndex={-1}
|
|
21
|
-
visibleOffset={0}
|
|
22
|
-
/>
|
|
23
|
-
);
|
|
24
|
-
// On step 1, the imageName field is not active — no suggestion panel
|
|
25
|
-
expect(queryByText(/^nginx$/)).toBeNull();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('does NOT render SuggestionPanel on step 0 when suggestions is empty', () => {
|
|
29
|
-
const { queryByTestId } = render(
|
|
30
|
-
<ContainerCreationPrompt
|
|
31
|
-
step={0}
|
|
32
|
-
imageName="ng"
|
|
33
|
-
containerName=""
|
|
34
|
-
portInput=""
|
|
35
|
-
envInput=""
|
|
36
|
-
message=""
|
|
37
|
-
messageColor="yellow"
|
|
38
|
-
suggestions={[]}
|
|
39
|
-
selectedSuggestionIndex={-1}
|
|
40
|
-
visibleOffset={0}
|
|
41
|
-
/>
|
|
42
|
-
);
|
|
43
|
-
// No suggestion panel should render when suggestions array is empty
|
|
44
|
-
expect(queryByTestId('suggestion-panel')).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('renders SuggestionPanel items on step 0 when suggestions exist', () => {
|
|
48
|
-
const { getAllByText, getByText } = render(
|
|
49
|
-
<ContainerCreationPrompt
|
|
50
|
-
step={0}
|
|
51
|
-
imageName="ng"
|
|
52
|
-
containerName=""
|
|
53
|
-
portInput=""
|
|
54
|
-
envInput=""
|
|
55
|
-
message=""
|
|
56
|
-
messageColor="yellow"
|
|
57
|
-
suggestions={['nginx', 'golang']}
|
|
58
|
-
selectedSuggestionIndex={-1}
|
|
59
|
-
visibleOffset={0}
|
|
60
|
-
/>
|
|
61
|
-
);
|
|
62
|
-
// golang is not in any label — appears only in suggestion panel
|
|
63
|
-
expect(getByText(/golang/)).toBeTruthy();
|
|
64
|
-
// nginx appears in suggestion panel (may also appear in label text, but it does appear)
|
|
65
|
-
const nginxEls = getAllByText(/nginx/);
|
|
66
|
-
expect(nginxEls.length).toBeGreaterThan(0);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
import React from 'react';
|
|
5
|
-
import { render } from '@testing-library/react';
|
|
6
|
-
import { SuggestionPanel } from '../src/components/SuggestionPanel.jsx';
|
|
7
|
-
|
|
8
|
-
describe('SuggestionPanel', () => {
|
|
9
|
-
const items = ['caddy', 'elasticsearch', 'golang', 'httpd', 'kafka', 'mariadb', 'memcached', 'minio'];
|
|
10
|
-
|
|
11
|
-
test('renders nothing when items is empty', () => {
|
|
12
|
-
const { container } = render(
|
|
13
|
-
<SuggestionPanel items={[]} selectedIndex={-1} visibleOffset={0} maxVisible={5} />
|
|
14
|
-
);
|
|
15
|
-
expect(container.firstChild).toBeNull();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('renders only maxVisible items from visibleOffset', () => {
|
|
19
|
-
const { getByText, queryByText } = render(
|
|
20
|
-
<SuggestionPanel items={items} selectedIndex={-1} visibleOffset={0} maxVisible={5} />
|
|
21
|
-
);
|
|
22
|
-
expect(getByText(/caddy/)).toBeTruthy();
|
|
23
|
-
expect(getByText(/elasticsearch/)).toBeTruthy();
|
|
24
|
-
expect(getByText(/kafka/)).toBeTruthy();
|
|
25
|
-
expect(queryByText(/mariadb/)).toBeNull();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('respects visibleOffset by skipping early items', () => {
|
|
29
|
-
const { queryByText, getByText } = render(
|
|
30
|
-
<SuggestionPanel items={items} selectedIndex={-1} visibleOffset={2} maxVisible={3} />
|
|
31
|
-
);
|
|
32
|
-
expect(getByText(/golang/)).toBeTruthy();
|
|
33
|
-
expect(getByText(/httpd/)).toBeTruthy();
|
|
34
|
-
expect(getByText(/kafka/)).toBeTruthy();
|
|
35
|
-
expect(queryByText(/caddy/)).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('focused row has › prefix marker', () => {
|
|
39
|
-
const { getByText } = render(
|
|
40
|
-
<SuggestionPanel items={items} selectedIndex={1} visibleOffset={0} maxVisible={5} />
|
|
41
|
-
);
|
|
42
|
-
const el = getByText(/› elasticsearch/);
|
|
43
|
-
expect(el).toBeTruthy();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('non-focused rows do NOT have › prefix', () => {
|
|
47
|
-
const { getByText } = render(
|
|
48
|
-
<SuggestionPanel items={items} selectedIndex={1} visibleOffset={0} maxVisible={5} />
|
|
49
|
-
);
|
|
50
|
-
const el = getByText(/caddy/);
|
|
51
|
-
expect(el.textContent).not.toMatch(/›/);
|
|
52
|
-
});
|
|
53
|
-
});
|
package/test/constants.test.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment node
|
|
3
|
-
*/
|
|
4
|
-
import { IMAGE_PROFILES } from '../src/helpers/constants.js';
|
|
5
|
-
|
|
6
|
-
describe('IMAGE_PROFILES', () => {
|
|
7
|
-
test('has ~20 entries', () => {
|
|
8
|
-
const count = Object.keys(IMAGE_PROFILES).length;
|
|
9
|
-
expect(count).toBeGreaterThanOrEqual(20);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test('every entry has requiredEnv (array), defaultPort (string), and suggestedEnv (array)', () => {
|
|
13
|
-
for (const [key, profile] of Object.entries(IMAGE_PROFILES)) {
|
|
14
|
-
expect(Array.isArray(profile.requiredEnv)).toBe(true);
|
|
15
|
-
expect(typeof profile.defaultPort).toBe('string');
|
|
16
|
-
expect(Array.isArray(profile.suggestedEnv)).toBe(true);
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('postgres has POSTGRES_PASSWORD in requiredEnv and suggestedEnv hints', () => {
|
|
21
|
-
expect(IMAGE_PROFILES.postgres.requiredEnv).toContain('POSTGRES_PASSWORD');
|
|
22
|
-
expect(IMAGE_PROFILES.postgres.suggestedEnv.length).toBeGreaterThan(0);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('nginx has empty requiredEnv, defaultPort 80, and empty suggestedEnv', () => {
|
|
26
|
-
expect(IMAGE_PROFILES.nginx.requiredEnv).toEqual([]);
|
|
27
|
-
expect(IMAGE_PROFILES.nginx.defaultPort).toBe('80');
|
|
28
|
-
expect(IMAGE_PROFILES.nginx.suggestedEnv).toEqual([]);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('node has suggestedEnv with NODE_ENV hint', () => {
|
|
32
|
-
expect(IMAGE_PROFILES.node.suggestedEnv).toContain('NODE_ENV=production');
|
|
33
|
-
});
|
|
34
|
-
});
|