cdd-cli 3.1.9 → 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 +46 -0
- package/SECURITY.md +5 -0
- package/__mocks__/ink.cjs +17 -0
- package/coverage/clover.xml +694 -0
- package/coverage/coverage-final.json +21 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- 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/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +298 -0
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +244 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +931 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +307 -0
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +131 -0
- package/coverage/lcov-report/helpers/imageNameUtils.js.html +181 -0
- package/coverage/lcov-report/helpers/index.html +191 -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 +346 -0
- package/coverage/lcov-report/hooks/creation/index.html +146 -0
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +313 -0
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +805 -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 +187 -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 +169 -0
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +454 -0
- package/coverage/lcov-report/hooks/useControls.js.html +826 -0
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +202 -0
- package/coverage/lcov-report/hooks/useExitHandler.js.html +292 -0
- package/coverage/lcov-report/index.html +206 -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 +1402 -0
- 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 +41 -0
- package/src/helpers/containerOptionsBuilder.js +16 -5
- package/src/helpers/dockerService/dockerService.js +1 -1
- package/src/helpers/dockerService/serviceComponents/containerActions.js +116 -43
- 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 +62 -21
- package/src/hooks/creation/useContainerActions.js +19 -12
- package/src/hooks/creation/useContainerCreation.js +161 -37
- 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 +120 -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/containerActions.test.js +104 -13
- package/test/containerOptionsBuilder.test.js +36 -0
- package/test/useContainerCreation.dom.test.js +166 -2
- package/test/useControls.dom.test.js +313 -0
- package/test/validationHelpers.test.js +51 -0
package/src/hooks/useControls.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { useInput } from
|
|
3
|
-
import { useContainerActions } from
|
|
4
|
-
import { useContainerCreation } from
|
|
5
|
-
import { useLogsViewer } from
|
|
6
|
-
import { useContainerSelection } from
|
|
7
|
-
import { useDebugLogs } from
|
|
8
|
-
import { useEraseConfirmation } from
|
|
9
|
-
import { useExitHandler } from
|
|
10
|
-
import { useContainerCommandRouter } from
|
|
11
|
-
import { getLogsStream } from
|
|
12
|
-
import { createContainer as svcCreateContainer } from
|
|
13
|
-
import { buildContainerOptions } from
|
|
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';
|
|
14
15
|
|
|
15
16
|
// Principal hook to manage user inputs and control the app state
|
|
16
17
|
/**
|
|
@@ -28,22 +29,35 @@ export function useControls(containers = []) {
|
|
|
28
29
|
|
|
29
30
|
const creation = useContainerCreation({
|
|
30
31
|
onCreate: async ({ imageName, containerName, portInput, envInput }) => {
|
|
31
|
-
const options = buildContainerOptions({
|
|
32
|
+
const options = buildContainerOptions({
|
|
33
|
+
imageName,
|
|
34
|
+
containerName,
|
|
35
|
+
portInput,
|
|
36
|
+
envInput,
|
|
37
|
+
});
|
|
32
38
|
actions.setMessage(`Creating container ${imageName}...`);
|
|
33
|
-
actions.setMessageColor(
|
|
39
|
+
actions.setMessageColor('yellow');
|
|
34
40
|
try {
|
|
35
|
-
const id = await svcCreateContainer(imageName, options);
|
|
36
|
-
|
|
37
|
-
|
|
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');
|
|
38
52
|
} catch (err) {
|
|
39
53
|
actions.setMessage(`Error creating container: ${err.message}`);
|
|
40
|
-
actions.setMessageColor(
|
|
54
|
+
actions.setMessageColor('red');
|
|
41
55
|
} finally {
|
|
42
56
|
setCreatingContainer(false);
|
|
43
57
|
}
|
|
44
58
|
},
|
|
45
59
|
onCancel: () => setCreatingContainer(false),
|
|
46
|
-
dbImages:
|
|
60
|
+
dbImages: DB_IMAGES,
|
|
47
61
|
});
|
|
48
62
|
|
|
49
63
|
const logsViewer = useLogsViewer();
|
|
@@ -54,14 +68,14 @@ export function useControls(containers = []) {
|
|
|
54
68
|
onConfirm: () => {
|
|
55
69
|
actions.handleAction({
|
|
56
70
|
actionFn: async (id) => await actions.removeContainer(id),
|
|
57
|
-
actionLabel:
|
|
71
|
+
actionLabel: 'Erasing',
|
|
58
72
|
selected: selection.selected,
|
|
59
73
|
});
|
|
60
|
-
actions.setMessageColor(
|
|
74
|
+
actions.setMessageColor('yellow');
|
|
61
75
|
},
|
|
62
76
|
onCancel: () => {
|
|
63
|
-
actions.setMessage(
|
|
64
|
-
actions.setMessageColor(
|
|
77
|
+
actions.setMessage('');
|
|
78
|
+
actions.setMessageColor('');
|
|
65
79
|
},
|
|
66
80
|
});
|
|
67
81
|
|
|
@@ -69,17 +83,22 @@ export function useControls(containers = []) {
|
|
|
69
83
|
* Pipe container logs into the viewer while applying a hard limit.
|
|
70
84
|
* @param {string} containerId - Container identifier used by Docker.
|
|
71
85
|
*/
|
|
72
|
-
const startLogsStream = React.useCallback(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
);
|
|
83
102
|
|
|
84
103
|
const commandRouter = useContainerCommandRouter({
|
|
85
104
|
actions,
|
|
@@ -90,8 +109,10 @@ export function useControls(containers = []) {
|
|
|
90
109
|
startLogsStream,
|
|
91
110
|
onStartErase: () => {
|
|
92
111
|
eraseConfirmation.startErase();
|
|
93
|
-
actions.setMessage(
|
|
94
|
-
|
|
112
|
+
actions.setMessage(
|
|
113
|
+
'Are you sure you want to erase this container? (y/n)'
|
|
114
|
+
);
|
|
115
|
+
actions.setMessageColor('yellow');
|
|
95
116
|
},
|
|
96
117
|
onToggleDebug: () => debugLogs.setShowDebugLogs((prev) => !prev),
|
|
97
118
|
onStartCreate: () => setCreatingContainer(true),
|
|
@@ -99,8 +120,8 @@ export function useControls(containers = []) {
|
|
|
99
120
|
|
|
100
121
|
const exitHandler = useExitHandler({
|
|
101
122
|
onBeforeExit: () => {
|
|
102
|
-
actions.setMessage(
|
|
103
|
-
actions.setMessageColor(
|
|
123
|
+
actions.setMessage('Exiting...');
|
|
124
|
+
actions.setMessageColor('yellow');
|
|
104
125
|
logsViewer.closeLogs();
|
|
105
126
|
debugLogs.setShowDebugLogs(false);
|
|
106
127
|
},
|
|
@@ -109,38 +130,67 @@ export function useControls(containers = []) {
|
|
|
109
130
|
/**
|
|
110
131
|
* Route keystrokes to the container creation wizard.
|
|
111
132
|
*/
|
|
112
|
-
const processCreationInput = React.useCallback(
|
|
113
|
-
|
|
133
|
+
const processCreationInput = React.useCallback(
|
|
134
|
+
(input, key) => {
|
|
135
|
+
const step = creation.step;
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
const removeLastChar = (setter) =>
|
|
138
|
+
setter((value) => (value || '').slice(0, -1));
|
|
139
|
+
const appendChar = (setter, current, ch) => setter((current || '') + ch);
|
|
117
140
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
if (key.escape) {
|
|
142
|
+
creation.cancelCreation();
|
|
143
|
+
setCreatingContainer(false);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
123
146
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
128
156
|
|
|
129
|
-
|
|
130
|
-
if (step === 0
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
168
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
);
|
|
144
194
|
|
|
145
195
|
// Single keyboard entry point
|
|
146
196
|
useInput((input, key) => {
|
|
@@ -150,7 +200,7 @@ export function useControls(containers = []) {
|
|
|
150
200
|
}
|
|
151
201
|
|
|
152
202
|
if (logsViewer.showLogs) {
|
|
153
|
-
if (input ===
|
|
203
|
+
if (input === 'q' || key.escape) {
|
|
154
204
|
logsViewer.closeLogs();
|
|
155
205
|
}
|
|
156
206
|
return;
|
|
@@ -175,7 +225,9 @@ export function useControls(containers = []) {
|
|
|
175
225
|
selected: selection.selected,
|
|
176
226
|
setSelected: selection.setSelected,
|
|
177
227
|
message: creatingContainer ? creation.message : actions.message,
|
|
178
|
-
messageColor: creatingContainer
|
|
228
|
+
messageColor: creatingContainer
|
|
229
|
+
? creation.messageColor
|
|
230
|
+
: actions.messageColor,
|
|
179
231
|
showLogs: logsViewer.showLogs,
|
|
180
232
|
logs: logsViewer.logs,
|
|
181
233
|
exitLogs: logsViewer.closeLogs,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback } from
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Hook to manage the erase confirmation dialog flow.
|
|
@@ -20,13 +20,13 @@ export function useEraseConfirmation({ onConfirm, onCancel }) {
|
|
|
20
20
|
|
|
21
21
|
const processEraseConfirmation = useCallback(
|
|
22
22
|
(input, key) => {
|
|
23
|
-
if (input ===
|
|
23
|
+
if (input === 'y' || input === 'Y') {
|
|
24
24
|
onConfirm();
|
|
25
25
|
setConfirmErase(false);
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
if (input ===
|
|
29
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
30
30
|
setConfirmErase(false);
|
|
31
31
|
onCancel();
|
|
32
32
|
return;
|
|
@@ -30,37 +30,40 @@ export function useExitHandler({ onBeforeExit } = {}) {
|
|
|
30
30
|
* @param {string} input - Raw character input from Ink's useInput
|
|
31
31
|
* @returns {boolean} True when the input was consumed, false otherwise
|
|
32
32
|
*/
|
|
33
|
-
const handleExitCommand = useCallback(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const handleExitCommand = useCallback(
|
|
34
|
+
(input) => {
|
|
35
|
+
if (input !== 'q') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
if (typeof onBeforeExit === 'function') {
|
|
40
|
+
onBeforeExit();
|
|
41
|
+
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
48
52
|
}
|
|
49
|
-
}
|
|
50
|
-
// Ignore clear errors to avoid blocking exit.
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
+
};
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
clearTerminal();
|
|
57
|
+
exit();
|
|
58
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
59
|
+
setTimeout(() => process.exit(0), 50);
|
|
60
|
+
}
|
|
61
|
+
}, EXIT_DELAY);
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
[exit, onBeforeExit]
|
|
66
|
+
);
|
|
64
67
|
|
|
65
68
|
return { handleExitCommand };
|
|
66
69
|
}
|
package/src/index.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* node index.js
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import React from
|
|
12
|
-
import { render } from
|
|
13
|
-
import App from './App';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { render } from 'ink';
|
|
13
|
+
import App from './App.jsx';
|
|
14
14
|
|
|
15
15
|
console.clear();
|
|
16
|
-
render(<App />);
|
|
16
|
+
render(<App />);
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
});
|