cdd-cli 3.2.0 → 4.0.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/.atl/skill-registry.md +30 -0
- package/.editorconfig +12 -0
- package/.eslintrc.json +1 -1
- package/.prettierignore +4 -0
- package/.prettierrc +8 -0
- package/.~lock.ROADMAP.md# +1 -0
- package/CHANGELOG.md +80 -0
- package/README.es.md +198 -0
- package/README.md +81 -23
- package/ROADMAP.md +647 -0
- package/SECURITY.md +5 -0
- package/__mocks__/ink.cjs +17 -0
- package/coverage/clover.xml +471 -500
- package/coverage/coverage-final.json +16 -16
- package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +400 -0
- package/coverage/lcov-report/ControlsHUD.jsx.html +256 -0
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +394 -0
- package/coverage/lcov-report/components/PromptField.jsx.html +229 -0
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +268 -0
- package/coverage/lcov-report/components/index.html +146 -0
- package/coverage/lcov-report/helpers/constants.js.html +139 -22
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +75 -42
- package/coverage/lcov-report/helpers/dockerHubService.js.html +310 -0
- 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 +84 -54
- package/coverage/lcov-report/helpers/logger.js.html +68 -68
- package/coverage/lcov-report/helpers/safeCall.js.html +15 -15
- package/coverage/lcov-report/helpers/validationHelpers.js.html +66 -114
- 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 +593 -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 +306 -123
- 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 +52 -67
- package/coverage/lcov.info +899 -952
- package/dist/App.js +18 -12
- package/dist/components/ContainerCreationPrompt.js +50 -13
- package/dist/components/ContainerList.js +3 -3
- package/dist/components/ContainerRow.js +20 -20
- package/dist/components/ContainerSection.js +3 -3
- package/dist/components/ControlsHUD.js +106 -0
- 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 +53 -0
- package/dist/components/UsageMenu.js +3 -3
- package/dist/helpers/appInfo.js +7 -7
- package/dist/helpers/constants.js +154 -1
- package/dist/helpers/containerOptionsBuilder.js +3 -3
- package/dist/helpers/dockerHubService.js +111 -0
- 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 +28 -11
- package/dist/hooks/creation/useContainerCreation.js +452 -62
- package/dist/hooks/creation/useLogsViewer.js +2 -2
- package/dist/hooks/debug/useDebugLogs.js +4 -4
- package/dist/hooks/useContainerCommandRouter.js +14 -14
- package/dist/hooks/useContainers.js +3 -3
- package/dist/hooks/useControls.js +85 -38
- package/dist/hooks/useEraseConfirmation.js +3 -3
- package/dist/index.js +9 -2
- package/jest.config.cjs +7 -0
- package/package.json +7 -3
- package/src/App.jsx +39 -16
- package/src/components/ContainerCreationPrompt.jsx +54 -10
- package/src/components/ContainerList.jsx +3 -3
- package/src/components/ContainerRow.jsx +19 -16
- package/src/components/ContainerSection.jsx +3 -3
- package/src/components/ControlsHUD.jsx +66 -0
- 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 +61 -0
- package/src/components/UsageMenu.jsx +18 -13
- package/src/helpers/appInfo.js +7 -7
- package/src/helpers/constants.js +48 -9
- package/src/helpers/containerOptionsBuilder.js +16 -5
- package/src/helpers/dockerHubService.js +75 -0
- 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 -16
- package/src/helpers/safeCall.js +2 -2
- package/src/helpers/validationHelpers.js +13 -29
- package/src/hooks/creation/useContainerActions.js +32 -12
- package/src/hooks/creation/useContainerCreation.js +293 -47
- 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 +135 -72
- package/src/hooks/useEraseConfirmation.js +3 -3
- package/src/hooks/useExitHandler.js +29 -26
- package/src/index.js +8 -4
- package/test/ContainerCreationPrompt.dom.test.js +313 -0
- package/test/ControlsHUD.dom.test.js +125 -0
- package/test/SuggestionPanel.dom.test.js +83 -0
- package/test/constants.test.js +41 -0
- package/test/dockerHubService.test.js +213 -0
- package/test/logger.test.js +58 -0
- package/test/resolveImageTag.test.js +26 -0
- package/test/useContainerCreation.dom.test.js +428 -2
- package/test/useContainerCreationHub.dom.test.js +184 -0
- package/test/useControls.dom.test.js +406 -4
|
@@ -8,16 +8,21 @@ export function useDebugLogs() {
|
|
|
8
8
|
useEffect(() => {
|
|
9
9
|
const unsubscribe = logger.subscribe((entry) => {
|
|
10
10
|
setDebugLogs((prev) => {
|
|
11
|
-
const extras = (entry.args || [])
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
const extras = (entry.args || [])
|
|
12
|
+
.map((arg) => {
|
|
13
|
+
if (typeof arg === 'string') return arg;
|
|
14
|
+
if (typeof arg === 'number' || typeof arg === 'boolean')
|
|
15
|
+
return String(arg);
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(arg);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return '[unserializable]';
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
const line = extras.length
|
|
24
|
+
? `${entry.formatted} ${extras.join(' ')}`
|
|
25
|
+
: entry.formatted;
|
|
21
26
|
const next = [...prev, line];
|
|
22
27
|
return next.slice(-200);
|
|
23
28
|
});
|
|
@@ -3,23 +3,26 @@ import { useState, useCallback } from 'react';
|
|
|
3
3
|
export function useContainerSelection(total) {
|
|
4
4
|
const [selected, setSelected] = useState(0);
|
|
5
5
|
|
|
6
|
-
const handleNavigation = useCallback(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const handleNavigation = useCallback(
|
|
7
|
+
(input, key) => {
|
|
8
|
+
if (total === 0) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (key.upArrow) {
|
|
13
|
+
setSelected((i) => (i === 0 ? total - 1 : i - 1));
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
if (key.downArrow) {
|
|
18
|
+
setSelected((i) => (i === total - 1 ? 0 : i + 1));
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
return false;
|
|
23
|
+
},
|
|
24
|
+
[total]
|
|
25
|
+
);
|
|
23
26
|
|
|
24
27
|
return { selected, setSelected, handleNavigation };
|
|
25
28
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback } from
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Hook responsible solely for routing Docker action keystrokes to the correct handler.
|
|
@@ -36,75 +36,88 @@ export function useContainerCommandRouter({
|
|
|
36
36
|
onToggleDebug,
|
|
37
37
|
onStartCreate,
|
|
38
38
|
}) {
|
|
39
|
-
const handleDockerCommands = useCallback(
|
|
40
|
-
|
|
39
|
+
const handleDockerCommands = useCallback(
|
|
40
|
+
(input) => {
|
|
41
|
+
const container = containers[selected];
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
if (input === 'i') {
|
|
44
|
+
actions.handleAction({
|
|
45
|
+
actionFn: async (id) => await actions.startContainer(id),
|
|
46
|
+
actionLabel: 'Starting',
|
|
47
|
+
selected,
|
|
48
|
+
stateCheck: (c) =>
|
|
49
|
+
(c.state === 'running' || c.status === 'running') &&
|
|
50
|
+
'Container is already running.',
|
|
51
|
+
});
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
if (input === 'p') {
|
|
56
|
+
actions.handleAction({
|
|
57
|
+
actionFn: async (id) => await actions.stopContainer(id),
|
|
58
|
+
actionLabel: 'Stopping',
|
|
59
|
+
selected,
|
|
60
|
+
stateCheck: (c) =>
|
|
61
|
+
(c.state === 'exited' ||
|
|
62
|
+
c.status === 'exited' ||
|
|
63
|
+
c.state === 'stopped' ||
|
|
64
|
+
c.status === 'stopped') &&
|
|
65
|
+
'Container is already stopped.',
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
if (input === 'r') {
|
|
71
|
+
actions.handleAction({
|
|
72
|
+
actionFn: async (id) => await actions.restartContainer(id),
|
|
73
|
+
actionLabel: 'Restarting',
|
|
74
|
+
selected,
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
if (input === 'e') {
|
|
80
|
+
if (!container) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
onStartErase();
|
|
80
84
|
return true;
|
|
81
85
|
}
|
|
82
|
-
onStartErase();
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
if (input === 'l') {
|
|
88
|
+
if (!container) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
logsViewer.openLogs();
|
|
92
|
+
startLogsStream(container.id);
|
|
88
93
|
return true;
|
|
89
94
|
}
|
|
90
|
-
logsViewer.openLogs();
|
|
91
|
-
startLogsStream(container.id);
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
if (input === 'c') {
|
|
97
|
+
creation.resetCreation();
|
|
98
|
+
onStartCreate();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
if (input === 'd') {
|
|
103
|
+
onToggleDebug();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
return false;
|
|
108
|
+
},
|
|
109
|
+
[
|
|
110
|
+
actions,
|
|
111
|
+
containers,
|
|
112
|
+
creation,
|
|
113
|
+
logsViewer,
|
|
114
|
+
onStartCreate,
|
|
115
|
+
onStartErase,
|
|
116
|
+
onToggleDebug,
|
|
117
|
+
selected,
|
|
118
|
+
startLogsStream,
|
|
119
|
+
]
|
|
120
|
+
);
|
|
108
121
|
|
|
109
122
|
return { handleDockerCommands };
|
|
110
123
|
}
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* // Use in a component
|
|
9
9
|
* const { containers } = useContainers();
|
|
10
10
|
*/
|
|
11
|
-
import React, { useState, useEffect } from
|
|
12
|
-
import { getContainers } from
|
|
13
|
-
import { REFRESH_INTERVALS } from
|
|
11
|
+
import React, { useState, useEffect } from 'react';
|
|
12
|
+
import { getContainers } from '../helpers/dockerService/serviceComponents/containerList';
|
|
13
|
+
import { REFRESH_INTERVALS } from '../helpers/constants.js';
|
|
14
14
|
|
|
15
15
|
export function useContainers() {
|
|
16
16
|
const [containers, setContainers] = useState([]);
|
|
@@ -18,7 +18,7 @@ export function useContainers() {
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
const fetch = async () => setContainers(await getContainers());
|
|
20
20
|
fetch();
|
|
21
|
-
|
|
21
|
+
const timer = setInterval(fetch, REFRESH_INTERVALS.CONTAINER_LIST);
|
|
22
22
|
return () => clearInterval(timer);
|
|
23
23
|
}, []);
|
|
24
24
|
|
package/src/hooks/useControls.js
CHANGED
|
@@ -1,17 +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
|
|
14
|
-
import { DB_IMAGES } 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';
|
|
15
15
|
|
|
16
16
|
// Principal hook to manage user inputs and control the app state
|
|
17
17
|
/**
|
|
@@ -21,7 +21,7 @@ import { DB_IMAGES } from "../helpers/constants.js";
|
|
|
21
21
|
* @param {Array<Object>} containers - Current list of Docker containers
|
|
22
22
|
* @returns {Object} controls - API for the App component
|
|
23
23
|
*/
|
|
24
|
-
export function useControls(containers = []) {
|
|
24
|
+
export function useControls(containers = [], overrides = {}) {
|
|
25
25
|
const [creatingContainer, setCreatingContainer] = React.useState(false);
|
|
26
26
|
|
|
27
27
|
// — Modular hooks —
|
|
@@ -29,20 +29,26 @@ export function useControls(containers = []) {
|
|
|
29
29
|
|
|
30
30
|
const creation = useContainerCreation({
|
|
31
31
|
onCreate: async ({ imageName, containerName, portInput, envInput }) => {
|
|
32
|
-
const options = buildContainerOptions({
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const options = buildContainerOptions({
|
|
33
|
+
imageName,
|
|
34
|
+
containerName,
|
|
35
|
+
portInput,
|
|
36
|
+
envInput,
|
|
37
|
+
});
|
|
38
|
+
actions.setTimedMessage(`Creating container ${imageName}...`, 'yellow');
|
|
35
39
|
try {
|
|
36
40
|
const { id, ports } = await svcCreateContainer(imageName, options);
|
|
37
|
-
let portMsg =
|
|
41
|
+
let portMsg = '';
|
|
38
42
|
if (ports && ports.length) {
|
|
39
|
-
portMsg =
|
|
43
|
+
portMsg =
|
|
44
|
+
' | Ports: ' +
|
|
45
|
+
ports
|
|
46
|
+
.map((p) => `${p.hostPort}→${p.containerPort}/${p.protocol}`)
|
|
47
|
+
.join(', ');
|
|
40
48
|
}
|
|
41
|
-
actions.
|
|
42
|
-
actions.setMessageColor("green");
|
|
49
|
+
actions.setTimedMessage(`Created container ${id}${portMsg}`, 'green');
|
|
43
50
|
} catch (err) {
|
|
44
|
-
actions.
|
|
45
|
-
actions.setMessageColor("red");
|
|
51
|
+
actions.setTimedMessage(`Error creating container: ${err.message}`, 'red');
|
|
46
52
|
} finally {
|
|
47
53
|
setCreatingContainer(false);
|
|
48
54
|
}
|
|
@@ -55,18 +61,25 @@ export function useControls(containers = []) {
|
|
|
55
61
|
const selection = useContainerSelection(containers.length);
|
|
56
62
|
const debugLogs = useDebugLogs();
|
|
57
63
|
|
|
64
|
+
// Allow overrides for testability (e.g. injecting a mock triggerHubSearch)
|
|
65
|
+
const triggerHubSearch = overrides.triggerHubSearch ?? creation.triggerHubSearch;
|
|
66
|
+
const isSearchingHub = overrides.isSearchingHub ?? creation.isSearchingHub;
|
|
67
|
+
const insertNextSuggestedEnv = overrides.insertNextSuggestedEnv !== undefined
|
|
68
|
+
? overrides.insertNextSuggestedEnv
|
|
69
|
+
: creation.insertNextSuggestedEnv;
|
|
70
|
+
|
|
58
71
|
const eraseConfirmation = useEraseConfirmation({
|
|
59
72
|
onConfirm: () => {
|
|
60
73
|
actions.handleAction({
|
|
61
74
|
actionFn: async (id) => await actions.removeContainer(id),
|
|
62
|
-
actionLabel:
|
|
75
|
+
actionLabel: 'Erasing',
|
|
63
76
|
selected: selection.selected,
|
|
64
77
|
});
|
|
65
|
-
actions.setMessageColor(
|
|
78
|
+
actions.setMessageColor('yellow');
|
|
66
79
|
},
|
|
67
80
|
onCancel: () => {
|
|
68
|
-
actions.setMessage(
|
|
69
|
-
actions.setMessageColor(
|
|
81
|
+
actions.setMessage('');
|
|
82
|
+
actions.setMessageColor('');
|
|
70
83
|
},
|
|
71
84
|
});
|
|
72
85
|
|
|
@@ -74,17 +87,22 @@ export function useControls(containers = []) {
|
|
|
74
87
|
* Pipe container logs into the viewer while applying a hard limit.
|
|
75
88
|
* @param {string} containerId - Container identifier used by Docker.
|
|
76
89
|
*/
|
|
77
|
-
const startLogsStream = React.useCallback(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const startLogsStream = React.useCallback(
|
|
91
|
+
(containerId) => {
|
|
92
|
+
getLogsStream(
|
|
93
|
+
containerId,
|
|
94
|
+
(data) =>
|
|
95
|
+
logsViewer.setLogs((prev) => {
|
|
96
|
+
const newLogs = [...prev, ...data.split('\n').filter(Boolean)];
|
|
97
|
+
return newLogs.slice(-1000);
|
|
98
|
+
}),
|
|
99
|
+
() => {},
|
|
100
|
+
(err) =>
|
|
101
|
+
logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
[logsViewer]
|
|
105
|
+
);
|
|
88
106
|
|
|
89
107
|
const commandRouter = useContainerCommandRouter({
|
|
90
108
|
actions,
|
|
@@ -95,8 +113,10 @@ export function useControls(containers = []) {
|
|
|
95
113
|
startLogsStream,
|
|
96
114
|
onStartErase: () => {
|
|
97
115
|
eraseConfirmation.startErase();
|
|
98
|
-
actions.setMessage(
|
|
99
|
-
|
|
116
|
+
actions.setMessage(
|
|
117
|
+
'Are you sure you want to erase this container? (y/n)'
|
|
118
|
+
);
|
|
119
|
+
actions.setMessageColor('yellow');
|
|
100
120
|
},
|
|
101
121
|
onToggleDebug: () => debugLogs.setShowDebugLogs((prev) => !prev),
|
|
102
122
|
onStartCreate: () => setCreatingContainer(true),
|
|
@@ -104,8 +124,8 @@ export function useControls(containers = []) {
|
|
|
104
124
|
|
|
105
125
|
const exitHandler = useExitHandler({
|
|
106
126
|
onBeforeExit: () => {
|
|
107
|
-
actions.setMessage(
|
|
108
|
-
actions.setMessageColor(
|
|
127
|
+
actions.setMessage('Exiting...');
|
|
128
|
+
actions.setMessageColor('yellow');
|
|
109
129
|
logsViewer.closeLogs();
|
|
110
130
|
debugLogs.setShowDebugLogs(false);
|
|
111
131
|
},
|
|
@@ -114,38 +134,79 @@ export function useControls(containers = []) {
|
|
|
114
134
|
/**
|
|
115
135
|
* Route keystrokes to the container creation wizard.
|
|
116
136
|
*/
|
|
117
|
-
const processCreationInput = React.useCallback(
|
|
118
|
-
|
|
137
|
+
const processCreationInput = React.useCallback(
|
|
138
|
+
(input, key) => {
|
|
139
|
+
const step = creation.step;
|
|
119
140
|
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
const removeLastChar = (setter) =>
|
|
142
|
+
setter((value) => (value || '').slice(0, -1));
|
|
143
|
+
const appendChar = (setter, current, ch) => setter((current || '') + ch);
|
|
122
144
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
if (key.escape) {
|
|
146
|
+
creation.cancelCreation();
|
|
147
|
+
setCreatingContainer(false);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
128
150
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
151
|
+
if (input === '\r' || input === '\n') {
|
|
152
|
+
// If on step 0 with a focused suggestion, apply it instead of advancing
|
|
153
|
+
if (step === 0 && creation.selectedSuggestionIndex >= 0) {
|
|
154
|
+
creation.applyFocusedSuggestion();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
creation.nextStep();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
133
160
|
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
// Tab on step 0: trigger Docker Hub search (with guards)
|
|
162
|
+
if (key.tab) {
|
|
163
|
+
if (step === 0) {
|
|
164
|
+
if (!isSearchingHub && (creation.imageName || '').trim() !== '') {
|
|
165
|
+
triggerHubSearch();
|
|
166
|
+
}
|
|
167
|
+
} else if (step === 3) {
|
|
168
|
+
insertNextSuggestedEnv?.();
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
141
172
|
|
|
142
|
-
|
|
143
|
-
if (step === 0
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
// Arrow keys on step 0: navigate suggestion list
|
|
174
|
+
if (step === 0 && creation.suggestions.length > 0) {
|
|
175
|
+
if (key.upArrow) {
|
|
176
|
+
creation.moveSuggestionSelection(-1);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (key.downArrow) {
|
|
180
|
+
creation.moveSuggestionSelection(1);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (key.backspace || key.delete) {
|
|
186
|
+
if (step === 0) {
|
|
187
|
+
const newValue = (creation.imageName || '').slice(0, -1);
|
|
188
|
+
creation.updateImageInput(newValue);
|
|
189
|
+
}
|
|
190
|
+
if (step === 1) removeLastChar(creation.setContainerName);
|
|
191
|
+
if (step === 2) removeLastChar(creation.setPortInput);
|
|
192
|
+
if (step === 3) removeLastChar(creation.setEnvInput);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
197
|
+
if (step === 0) {
|
|
198
|
+
creation.updateImageInput((creation.imageName || '') + input);
|
|
199
|
+
}
|
|
200
|
+
if (step === 1)
|
|
201
|
+
appendChar(creation.setContainerName, creation.containerName, input);
|
|
202
|
+
if (step === 2)
|
|
203
|
+
appendChar(creation.setPortInput, creation.portInput, input);
|
|
204
|
+
if (step === 3)
|
|
205
|
+
appendChar(creation.setEnvInput, creation.envInput, input);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
[creation, setCreatingContainer, triggerHubSearch, isSearchingHub, insertNextSuggestedEnv]
|
|
209
|
+
);
|
|
149
210
|
|
|
150
211
|
// Single keyboard entry point
|
|
151
212
|
useInput((input, key) => {
|
|
@@ -155,7 +216,7 @@ export function useControls(containers = []) {
|
|
|
155
216
|
}
|
|
156
217
|
|
|
157
218
|
if (logsViewer.showLogs) {
|
|
158
|
-
if (input ===
|
|
219
|
+
if (input === 'q' || key.escape) {
|
|
159
220
|
logsViewer.closeLogs();
|
|
160
221
|
}
|
|
161
222
|
return;
|
|
@@ -180,7 +241,9 @@ export function useControls(containers = []) {
|
|
|
180
241
|
selected: selection.selected,
|
|
181
242
|
setSelected: selection.setSelected,
|
|
182
243
|
message: creatingContainer ? creation.message : actions.message,
|
|
183
|
-
messageColor: creatingContainer
|
|
244
|
+
messageColor: creatingContainer
|
|
245
|
+
? creation.messageColor
|
|
246
|
+
: actions.messageColor,
|
|
184
247
|
showLogs: logsViewer.showLogs,
|
|
185
248
|
logs: logsViewer.logs,
|
|
186
249
|
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,13 @@
|
|
|
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
|
+
import { logger } from './helpers/logger.js';
|
|
15
|
+
|
|
16
|
+
process.on('uncaughtException', (err) => logger.error('Uncaught exception', err));
|
|
17
|
+
process.on('unhandledRejection', (reason) => logger.error('Unhandled rejection', reason));
|
|
14
18
|
|
|
15
19
|
console.clear();
|
|
16
|
-
render(<App />);
|
|
20
|
+
render(<App />);
|