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
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import StatsBar from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import StatsBar from './StatsBar.jsx';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import { useContainerStats } from '../hooks/useContainerStats.js';
|
|
6
6
|
|
|
7
7
|
const stateText = (state) => {
|
|
8
|
-
if (state ===
|
|
9
|
-
if (state ===
|
|
10
|
-
if (state ===
|
|
11
|
-
return { text: state.toUpperCase(), color:
|
|
8
|
+
if (state === 'running') return { text: '🟢 RUNNING', color: 'green' };
|
|
9
|
+
if (state === 'exited') return { text: '🔴 EXITED', color: 'red' };
|
|
10
|
+
if (state === 'paused') return { text: '🟠 PAUSED', color: 'yellow' };
|
|
11
|
+
return { text: state.toUpperCase(), color: 'gray' };
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -24,17 +24,17 @@ export default function ContainerRow({ container, isSelected = false }) {
|
|
|
24
24
|
|
|
25
25
|
// Format ports for display (no leading space to avoid layout shifts)
|
|
26
26
|
const formatPorts = (ports) => {
|
|
27
|
-
if (!ports || ports.length === 0) return
|
|
27
|
+
if (!ports || ports.length === 0) return '';
|
|
28
28
|
if (Array.isArray(ports)) {
|
|
29
|
-
return ports.map((p, _i) => `🔗 ${p}`).join(
|
|
29
|
+
return ports.map((p, _i) => `🔗 ${p}`).join(' ');
|
|
30
30
|
}
|
|
31
31
|
return `🔗 ${ports}`;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
// Helper to truncate long strings to a max length without adding trailing spaces
|
|
35
35
|
const truncate = (s, max = 20) => {
|
|
36
|
-
if (!s) return
|
|
37
|
-
return s.length > max ? s.slice(0, max - 1) +
|
|
36
|
+
if (!s) return '';
|
|
37
|
+
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
const stateInfo = stateText(state);
|
|
@@ -42,8 +42,8 @@ export default function ContainerRow({ container, isSelected = false }) {
|
|
|
42
42
|
<Box flexDirection="column" marginBottom={1}>
|
|
43
43
|
<Box flexDirection="row" alignItems="center">
|
|
44
44
|
<Box width={2} minWidth={2} justifyContent="flex-end" paddingRight={1}>
|
|
45
|
-
<Text color={isSelected ?
|
|
46
|
-
{isSelected ?
|
|
45
|
+
<Text color={isSelected ? 'green' : undefined}>
|
|
46
|
+
{isSelected ? '➤' : ' '}
|
|
47
47
|
</Text>
|
|
48
48
|
</Box>
|
|
49
49
|
<Box width={18} flexShrink={1} paddingRight={1}>
|
|
@@ -59,8 +59,11 @@ export default function ContainerRow({ container, isSelected = false }) {
|
|
|
59
59
|
<Text color="yellow">{formatPorts(container.ports)}</Text>
|
|
60
60
|
</Box>
|
|
61
61
|
<Box flexShrink={0} paddingLeft={1}>
|
|
62
|
-
{state ===
|
|
63
|
-
<StatsBar
|
|
62
|
+
{state === 'running' ? (
|
|
63
|
+
<StatsBar
|
|
64
|
+
cpu={parseFloat(stats.cpuPercent)}
|
|
65
|
+
mem={parseFloat(stats.memPercent)}
|
|
66
|
+
/>
|
|
64
67
|
) : null}
|
|
65
68
|
</Box>
|
|
66
69
|
</Box>
|
|
@@ -75,7 +78,7 @@ ContainerRow.propTypes = {
|
|
|
75
78
|
name: PropTypes.string.isRequired,
|
|
76
79
|
image: PropTypes.string,
|
|
77
80
|
state: PropTypes.string,
|
|
78
|
-
ports: PropTypes.oneOfType([PropTypes.array, PropTypes.string])
|
|
81
|
+
ports: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
|
|
79
82
|
}).isRequired,
|
|
80
83
|
isSelected: PropTypes.bool,
|
|
81
84
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns the contextual keyboard hints for the current wizard state.
|
|
6
|
+
*
|
|
7
|
+
* @param {number} step - Current wizard step (0-3)
|
|
8
|
+
* @param {boolean} hasSuggestions - Whether suggestions/hub results are visible
|
|
9
|
+
* @param {boolean} isSearchingHub - Whether a Hub search is in progress
|
|
10
|
+
* @param {boolean} [hasSuggestedEnv=false] - Whether the image has suggestedEnv entries
|
|
11
|
+
* @returns {{ key: string, label: string }[]}
|
|
12
|
+
*/
|
|
13
|
+
function getHints(step, hasSuggestions, isSearchingHub, hasSuggestedEnv = false) {
|
|
14
|
+
if (step === 0) {
|
|
15
|
+
if (hasSuggestions) {
|
|
16
|
+
return [
|
|
17
|
+
{ key: '↑↓', label: 'Navigate' },
|
|
18
|
+
{ key: 'Enter', label: 'Select' },
|
|
19
|
+
{ key: 'Esc', label: 'Cancel' },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
const hints = [];
|
|
23
|
+
if (!isSearchingHub) hints.push({ key: 'Tab', label: 'Search Hub' });
|
|
24
|
+
hints.push({ key: '↑↓', label: 'Browse' });
|
|
25
|
+
hints.push({ key: 'Enter', label: 'Confirm' });
|
|
26
|
+
hints.push({ key: 'Esc', label: 'Cancel' });
|
|
27
|
+
return hints;
|
|
28
|
+
}
|
|
29
|
+
if (step === 3) {
|
|
30
|
+
const hints = [];
|
|
31
|
+
if (hasSuggestedEnv) hints.push({ key: 'Tab', label: 'Insert next env' });
|
|
32
|
+
hints.push({ key: 'Enter', label: 'Continue' });
|
|
33
|
+
hints.push({ key: 'Esc', label: 'Cancel' });
|
|
34
|
+
return hints;
|
|
35
|
+
}
|
|
36
|
+
return [
|
|
37
|
+
{ key: 'Enter', label: 'Continue' },
|
|
38
|
+
{ key: 'Esc', label: 'Cancel' },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Displays a row of contextual keyboard shortcut hints for the active wizard step.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} props
|
|
46
|
+
* @param {number} props.step - Current wizard step
|
|
47
|
+
* @param {boolean} [props.hasSuggestions=false] - Whether suggestions/hub results are visible
|
|
48
|
+
* @param {boolean} [props.isSearchingHub=false] - Whether a Hub search is in progress
|
|
49
|
+
* @param {boolean} [props.hasSuggestedEnv=false] - Whether the image has suggestedEnv entries
|
|
50
|
+
*/
|
|
51
|
+
function ControlsHUD({ step, hasSuggestions = false, isSearchingHub = false, hasSuggestedEnv = false }) {
|
|
52
|
+
const hints = getHints(step, hasSuggestions, isSearchingHub, hasSuggestedEnv);
|
|
53
|
+
return (
|
|
54
|
+
<Box flexDirection="row" columnGap={2}>
|
|
55
|
+
{hints.map(({ key, label }) => (
|
|
56
|
+
<Box key={key}>
|
|
57
|
+
<Text color="cyan">[{key}]</Text>
|
|
58
|
+
<Text dimColor> {label}</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
))}
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default ControlsHUD;
|
|
66
|
+
export { getHints, ControlsHUD };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import chalk from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
|
-
import { getAppVersion } from
|
|
5
|
+
import { getAppVersion } from '../helpers/appInfo.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Header component displayed at the top of the CLI UI.
|
|
@@ -18,18 +18,18 @@ import { getAppVersion } from "../helpers/appInfo.js";
|
|
|
18
18
|
const VERSION = getAppVersion();
|
|
19
19
|
|
|
20
20
|
export default function Header({ count }) {
|
|
21
|
-
const formattedVersion = VERSION ===
|
|
21
|
+
const formattedVersion = VERSION === 'unknown' ? 'unknown' : `v${VERSION}`;
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
24
|
<Box justifyContent="space-between">
|
|
25
25
|
<Text color="cyanBright">
|
|
26
|
-
🐳 {chalk.bold(
|
|
26
|
+
🐳 {chalk.bold('CDD')}
|
|
27
27
|
<Text color="gray"> — CLI Docker Dashboard</Text>
|
|
28
28
|
</Text>
|
|
29
29
|
<Box flexDirection="column" alignItems="flex-end">
|
|
30
30
|
<Text color="gray">{formattedVersion}</Text>
|
|
31
31
|
<Text color="gray">
|
|
32
|
-
{count} container{count === 1 ?
|
|
32
|
+
{count} container{count === 1 ? '' : 's'} found
|
|
33
33
|
</Text>
|
|
34
34
|
</Box>
|
|
35
35
|
</Box>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { Text } from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -17,7 +17,9 @@ export default function LogViewer({ logs, onExit: _onExit, container }) {
|
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<>
|
|
20
|
-
<Text color="green">
|
|
20
|
+
<Text color="green">
|
|
21
|
+
{container?.name ?? 'Container'} logs, press ESC to exit
|
|
22
|
+
</Text>
|
|
21
23
|
{visibleLogs.length === 0 ? (
|
|
22
24
|
<Text dimColor>No logs...</Text>
|
|
23
25
|
) : (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { Text } from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -18,7 +18,7 @@ export function PromptField({ label, value, required }) {
|
|
|
18
18
|
return (
|
|
19
19
|
<>
|
|
20
20
|
<Text>{label}</Text>
|
|
21
|
-
<Text color={isEmpty ?
|
|
21
|
+
<Text color={isEmpty ? 'red' : 'cyan'}>{value}_</Text>
|
|
22
22
|
</>
|
|
23
23
|
);
|
|
24
24
|
}
|
|
@@ -33,7 +33,7 @@ export function PromptField({ label, value, required }) {
|
|
|
33
33
|
*/
|
|
34
34
|
export function PromptMessage({ message, color }) {
|
|
35
35
|
if (!message) return null;
|
|
36
|
-
return <Text color={color ||
|
|
36
|
+
return <Text color={color || 'yellow'}>{message}</Text>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
PromptField.propTypes = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { Text } from
|
|
3
|
-
import chalk from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
|
|
6
6
|
const BAR_WIDTH = 8;
|
|
@@ -19,7 +19,9 @@ export default function StatsBar({ cpu, mem }) {
|
|
|
19
19
|
|
|
20
20
|
return (
|
|
21
21
|
<Text>
|
|
22
|
-
CPU:{cpuBar}
|
|
22
|
+
CPU:{cpuBar}
|
|
23
|
+
{colorize(cpu, ` ${formatPercent(cpu)}%`)} MEM:{memBar}
|
|
24
|
+
{colorize(mem, ` ${formatPercent(mem)}%`)}
|
|
23
25
|
</Text>
|
|
24
26
|
);
|
|
25
27
|
}
|
|
@@ -38,10 +40,12 @@ StatsBar.propTypes = {
|
|
|
38
40
|
*/
|
|
39
41
|
function makeBar(value, width) {
|
|
40
42
|
const safeWidth = Math.max(1, Math.round(width));
|
|
41
|
-
const normalized = Number.isFinite(value)
|
|
43
|
+
const normalized = Number.isFinite(value)
|
|
44
|
+
? Math.min(Math.max(value, 0), 100)
|
|
45
|
+
: 0;
|
|
42
46
|
const filled = Math.round((normalized / 100) * safeWidth);
|
|
43
47
|
const empty = Math.max(0, safeWidth - filled);
|
|
44
|
-
const bar =
|
|
48
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
45
49
|
return colorize(normalized, bar);
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -49,7 +53,7 @@ function formatPercent(value) {
|
|
|
49
53
|
if (!Number.isFinite(value)) return 0;
|
|
50
54
|
const normalized = Math.min(Math.max(value, 0), 100);
|
|
51
55
|
if (normalized >= 10) return Math.round(normalized);
|
|
52
|
-
return normalized.toFixed(1).replace(/\.0$/,
|
|
56
|
+
return normalized.toFixed(1).replace(/\.0$/, '');
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/**
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Renders an autocomplete suggestion panel as a boxed list below the image input.
|
|
7
|
+
* Displays a window of `maxVisible` items starting at `visibleOffset`.
|
|
8
|
+
* The focused row (selectedIndex) is highlighted cyan with a › prefix.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {string[]} props.items - Full list of suggestion strings
|
|
12
|
+
* @param {number} props.selectedIndex - Currently focused index (-1 = none)
|
|
13
|
+
* @param {number} props.visibleOffset - First item index of the visible window
|
|
14
|
+
* @param {number} [props.maxVisible=6] - Maximum number of items visible at once
|
|
15
|
+
* @returns {JSX.Element|null}
|
|
16
|
+
*/
|
|
17
|
+
export function SuggestionPanel({
|
|
18
|
+
items,
|
|
19
|
+
selectedIndex,
|
|
20
|
+
visibleOffset,
|
|
21
|
+
maxVisible = 6,
|
|
22
|
+
isLoading = false,
|
|
23
|
+
}) {
|
|
24
|
+
const hasItems = items && items.length > 0;
|
|
25
|
+
if (!hasItems && !isLoading) return null;
|
|
26
|
+
|
|
27
|
+
const visible = hasItems ? items.slice(visibleOffset, visibleOffset + maxVisible) : [];
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Box
|
|
31
|
+
flexDirection="column"
|
|
32
|
+
borderStyle="single"
|
|
33
|
+
borderColor="cyan"
|
|
34
|
+
paddingX={1}
|
|
35
|
+
data-testid="suggestion-panel"
|
|
36
|
+
>
|
|
37
|
+
{visible.map((item, localIdx) => {
|
|
38
|
+
const globalIdx = visibleOffset + localIdx;
|
|
39
|
+
const isFocused = globalIdx === selectedIndex;
|
|
40
|
+
return (
|
|
41
|
+
<Text key={item} color={isFocused ? 'cyan' : undefined} dimColor={!isFocused}>
|
|
42
|
+
{isFocused ? `› ${item}` : ` ${item}`}
|
|
43
|
+
</Text>
|
|
44
|
+
);
|
|
45
|
+
})}
|
|
46
|
+
{isLoading && (
|
|
47
|
+
<Text dimColor color="gray">
|
|
48
|
+
{' [searching Docker Hub...]'}
|
|
49
|
+
</Text>
|
|
50
|
+
)}
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
SuggestionPanel.propTypes = {
|
|
56
|
+
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
57
|
+
selectedIndex: PropTypes.number.isRequired,
|
|
58
|
+
visibleOffset: PropTypes.number.isRequired,
|
|
59
|
+
maxVisible: PropTypes.number,
|
|
60
|
+
isLoading: PropTypes.bool,
|
|
61
|
+
};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { Box, Text } from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
3
|
|
|
4
4
|
const SHORTCUTS = [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
'Use ↑/↓ for navigation',
|
|
6
|
+
'I to initiate selected container',
|
|
7
|
+
'P to stop selected container',
|
|
8
|
+
'R to restart selected container',
|
|
9
|
+
'C to create a container',
|
|
10
|
+
'L to view logs of selected container',
|
|
11
|
+
'D to toggle debug log panel',
|
|
12
|
+
'E to remove selected container',
|
|
13
|
+
'Q to quit',
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
const GRID_COLUMNS = 3;
|
|
@@ -24,7 +24,10 @@ const GRID_COLUMNS = 3;
|
|
|
24
24
|
export default function UsageMenu() {
|
|
25
25
|
const rowsPerColumn = Math.ceil(SHORTCUTS.length / GRID_COLUMNS);
|
|
26
26
|
const columns = Array.from({ length: GRID_COLUMNS }, (_, columnIndex) =>
|
|
27
|
-
SHORTCUTS.slice(
|
|
27
|
+
SHORTCUTS.slice(
|
|
28
|
+
columnIndex * rowsPerColumn,
|
|
29
|
+
(columnIndex + 1) * rowsPerColumn
|
|
30
|
+
)
|
|
28
31
|
).filter((column) => column.length > 0);
|
|
29
32
|
|
|
30
33
|
return (
|
|
@@ -37,7 +40,9 @@ export default function UsageMenu() {
|
|
|
37
40
|
marginRight={columnIndex === columns.length - 1 ? 0 : 3}
|
|
38
41
|
>
|
|
39
42
|
{column.map((shortcut, rowIndex) => (
|
|
40
|
-
<Text key={`shortcut-${columnIndex}-${rowIndex}`}>
|
|
43
|
+
<Text key={`shortcut-${columnIndex}-${rowIndex}`}>
|
|
44
|
+
{shortcut}
|
|
45
|
+
</Text>
|
|
41
46
|
))}
|
|
42
47
|
</Box>
|
|
43
48
|
))}
|
package/src/helpers/appInfo.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import path from
|
|
3
|
-
import { fileURLToPath } from
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
5
5
|
let cachedVersion;
|
|
6
6
|
|
|
@@ -12,12 +12,12 @@ export function getAppVersion() {
|
|
|
12
12
|
try {
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
14
|
const moduleDir = path.dirname(__filename);
|
|
15
|
-
const packageJsonPath = path.resolve(moduleDir,
|
|
16
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath,
|
|
17
|
-
cachedVersion = packageJson.version ??
|
|
15
|
+
const packageJsonPath = path.resolve(moduleDir, '../../package.json');
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
17
|
+
cachedVersion = packageJson.version ?? 'unknown';
|
|
18
18
|
return cachedVersion;
|
|
19
19
|
} catch (error) {
|
|
20
|
-
cachedVersion =
|
|
20
|
+
cachedVersion = 'unknown';
|
|
21
21
|
return cachedVersion;
|
|
22
22
|
}
|
|
23
23
|
}
|
package/src/helpers/constants.js
CHANGED
|
@@ -32,19 +32,58 @@ export const TIMEOUTS = {
|
|
|
32
32
|
/**
|
|
33
33
|
* List of well-known database image names (without tags or registry prefixes).
|
|
34
34
|
*/
|
|
35
|
-
export const DB_IMAGES = [
|
|
35
|
+
export const DB_IMAGES = [
|
|
36
|
+
'mysql',
|
|
37
|
+
'mariadb',
|
|
38
|
+
'postgres',
|
|
39
|
+
'mongo',
|
|
40
|
+
'mssql',
|
|
41
|
+
'redis',
|
|
42
|
+
];
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
|
-
* Per-image profiles with required env vars
|
|
45
|
+
* Per-image profiles with required env vars, default exposed port, and suggested env vars.
|
|
39
46
|
* Keys are normalized image names (no tag, no registry prefix).
|
|
40
47
|
*
|
|
41
|
-
* @type {Record<string, { requiredEnv: string[], defaultPort: string }>}
|
|
48
|
+
* @type {Record<string, { requiredEnv: string[], defaultPort: string, suggestedEnv: string[] }>}
|
|
42
49
|
*/
|
|
43
50
|
export const IMAGE_PROFILES = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
nginx: { requiredEnv: [], defaultPort: '80', suggestedEnv: [], defaultTag: '1.27-alpine' },
|
|
52
|
+
httpd: { requiredEnv: [], defaultPort: '80', suggestedEnv: [], defaultTag: '2.4-alpine' },
|
|
53
|
+
caddy: { requiredEnv: [], defaultPort: '80', suggestedEnv: [], defaultTag: '2-alpine' },
|
|
54
|
+
node: { requiredEnv: [], defaultPort: '3000', suggestedEnv: ['NODE_ENV=production'], defaultTag: '22-alpine' },
|
|
55
|
+
python: { requiredEnv: [], defaultPort: '8000', suggestedEnv: ['PYTHONUNBUFFERED=1'], defaultTag: '3.12-slim' },
|
|
56
|
+
openjdk: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['JAVA_OPTS=-Xms256m -Xmx512m'], defaultTag: '21-slim' },
|
|
57
|
+
golang: { requiredEnv: [], defaultPort: '8080', suggestedEnv: ['GOMAXPROCS=2'], defaultTag: '1.22-alpine' },
|
|
58
|
+
redis: { requiredEnv: [], defaultPort: '6379', suggestedEnv: ['REDIS_PASSWORD=change-me'], defaultTag: '7-alpine' },
|
|
59
|
+
memcached: { requiredEnv: [], defaultPort: '11211', suggestedEnv: [], defaultTag: '1.6-alpine' },
|
|
60
|
+
postgres: { requiredEnv: ['POSTGRES_PASSWORD'], defaultPort: '5432', suggestedEnv: ['POSTGRES_PASSWORD=secret', 'POSTGRES_USER=postgres', 'POSTGRES_DB=app'], defaultTag: '17-alpine' },
|
|
61
|
+
mysql: { requiredEnv: ['MYSQL_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MYSQL_ROOT_PASSWORD=secret', 'MYSQL_DATABASE=app', 'MYSQL_USER=app', 'MYSQL_PASSWORD=app123'], defaultTag: '8.0' },
|
|
62
|
+
mariadb: { requiredEnv: ['MARIADB_ROOT_PASSWORD'], defaultPort: '3306', suggestedEnv: ['MARIADB_ROOT_PASSWORD=secret', 'MARIADB_DATABASE=app', 'MARIADB_USER=app', 'MARIADB_PASSWORD=app123'], defaultTag: '11-alpine' },
|
|
63
|
+
mongo: { requiredEnv: [], defaultPort: '27017', suggestedEnv: ['MONGO_INITDB_ROOT_USERNAME=admin', 'MONGO_INITDB_ROOT_PASSWORD=secret'], defaultTag: '7.0' },
|
|
64
|
+
mssql: { requiredEnv: ['ACCEPT_EULA', 'SA_PASSWORD'], defaultPort: '1433', suggestedEnv: ['ACCEPT_EULA=Y', 'SA_PASSWORD=Strong!Passw0rd', 'MSSQL_PID=Developer'], defaultTag: '2022-latest' },
|
|
65
|
+
rabbitmq: { requiredEnv: [], defaultPort: '5672', suggestedEnv: ['RABBITMQ_DEFAULT_USER=guest', 'RABBITMQ_DEFAULT_PASS=guest'], defaultTag: '3-management-alpine' },
|
|
66
|
+
kafka: { requiredEnv: [], defaultPort: '9092', suggestedEnv: ['KAFKA_CFG_LISTENERS=PLAINTEXT://:9092', 'KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092'], defaultTag: '3.7' },
|
|
67
|
+
zookeeper: { requiredEnv: [], defaultPort: '2181', suggestedEnv: ['ALLOW_ANONYMOUS_LOGIN=yes'], defaultTag: '3.9' },
|
|
68
|
+
elasticsearch: { requiredEnv: [], defaultPort: '9200', suggestedEnv: ['discovery.type=single-node', 'xpack.security.enabled=false'], defaultTag: '8.13.0' },
|
|
69
|
+
minio: { requiredEnv: ['MINIO_ROOT_USER', 'MINIO_ROOT_PASSWORD'], defaultPort: '9000', suggestedEnv: ['MINIO_ROOT_USER=admin', 'MINIO_ROOT_PASSWORD=secret', 'MINIO_CONSOLE_ADDRESS=:9001'], defaultTag: 'latest' },
|
|
70
|
+
wordpress: { requiredEnv: ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME'], defaultPort: '8080', suggestedEnv: ['WORDPRESS_DB_HOST=db', 'WORDPRESS_DB_USER=wp', 'WORDPRESS_DB_PASSWORD=secret', 'WORDPRESS_DB_NAME=wordpress', 'WORDPRESS_TABLE_PREFIX=wp_'], defaultTag: '6.5-apache' },
|
|
50
71
|
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Given an image name (with or without tag), returns the image name with a
|
|
75
|
+
* default tag appended when the image matches a known profile and has no
|
|
76
|
+
* explicit tag. Returns the input unchanged otherwise.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} imageName - e.g. 'postgres', 'postgres:15', 'unknownimage'
|
|
79
|
+
* @param {Record<string, { defaultTag: string }>} profiles
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function resolveImageTag(imageName, profiles = IMAGE_PROFILES) {
|
|
83
|
+
if (!imageName) return imageName;
|
|
84
|
+
if (imageName.includes(':')) return imageName;
|
|
85
|
+
const baseName = imageName.trim().toLowerCase().split('/').pop();
|
|
86
|
+
const profile = profiles[baseName];
|
|
87
|
+
if (!profile?.defaultTag) return imageName;
|
|
88
|
+
return `${imageName}:${profile.defaultTag}`;
|
|
89
|
+
}
|
|
@@ -13,15 +13,26 @@
|
|
|
13
13
|
* (e.g. "NODE_ENV=production,PORT=3000")
|
|
14
14
|
* @returns {object} Docker container options ready to pass to createContainer
|
|
15
15
|
*/
|
|
16
|
-
export function buildContainerOptions({
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
export function buildContainerOptions({
|
|
17
|
+
imageName: _imageName,
|
|
18
|
+
containerName,
|
|
19
|
+
portInput,
|
|
20
|
+
envInput,
|
|
21
|
+
}) {
|
|
22
|
+
const env = (envInput || '')
|
|
23
|
+
.split(',')
|
|
24
|
+
.map((s) => s.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
const ports = (portInput || '')
|
|
27
|
+
.split(',')
|
|
28
|
+
.map((s) => s.trim())
|
|
29
|
+
.filter(Boolean);
|
|
19
30
|
|
|
20
31
|
const ExposedPorts = {};
|
|
21
32
|
const PortBindings = {};
|
|
22
33
|
|
|
23
|
-
ports.forEach(pair => {
|
|
24
|
-
const [host, cont] = pair.split(
|
|
34
|
+
ports.forEach((pair) => {
|
|
35
|
+
const [host, cont] = pair.split(':');
|
|
25
36
|
if (!host || !cont) return;
|
|
26
37
|
const key = `${cont}/tcp`;
|
|
27
38
|
ExposedPorts[key] = {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Hub search helper.
|
|
3
|
+
* Pure function — no side effects beyond HTTP. All state management
|
|
4
|
+
* lives in the calling hook (useContainerCreation).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DOCKER_HUB_SEARCH_URL =
|
|
8
|
+
'https://hub.docker.com/v2/search/repositories/';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format a Docker Hub search result object into a display string.
|
|
12
|
+
*
|
|
13
|
+
* Format rules:
|
|
14
|
+
* - Official: "name [★ official] [Npulls+]" (if pullCount > 0)
|
|
15
|
+
* - Non-official with pulls: "name [Npulls+]"
|
|
16
|
+
* - Otherwise: "name"
|
|
17
|
+
*
|
|
18
|
+
* Pull count suffixes: ≥1B → "1B+", ≥1M → "1M+", ≥1K → "1K+", else raw number
|
|
19
|
+
*
|
|
20
|
+
* @param {{ name: string, isOfficial: boolean, pullCount: number }} result
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function formatHubResult(result) {
|
|
24
|
+
const { name, isOfficial, pullCount } = result;
|
|
25
|
+
|
|
26
|
+
let pullSuffix = '';
|
|
27
|
+
if (pullCount >= 1e9) {
|
|
28
|
+
pullSuffix = ` [1B+ pulls]`;
|
|
29
|
+
} else if (pullCount >= 1e6) {
|
|
30
|
+
pullSuffix = ` [${Math.floor(pullCount / 1e6)}M+ pulls]`;
|
|
31
|
+
} else if (pullCount >= 1e3) {
|
|
32
|
+
pullSuffix = ` [${Math.floor(pullCount / 1e3)}K+ pulls]`;
|
|
33
|
+
} else if (pullCount > 0) {
|
|
34
|
+
pullSuffix = ` [${pullCount} pulls]`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isOfficial) {
|
|
38
|
+
return `${name} [★ official]${pullSuffix}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return `${name}${pullSuffix}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Search Docker Hub for images matching the given query.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} query - Search term (will be trimmed and encoded)
|
|
48
|
+
* @param {Object} [options]
|
|
49
|
+
* @param {AbortSignal} [options.signal] - Abort signal; defaults to AbortSignal.timeout(5000)
|
|
50
|
+
* @returns {Promise<Array<{ name: string, isOfficial: boolean, pullCount: number, description: string }>>}
|
|
51
|
+
* @throws {Error} If response.ok is false or the network call rejects
|
|
52
|
+
*/
|
|
53
|
+
export async function searchDockerHub(query, options = {}) {
|
|
54
|
+
const q = query.trim();
|
|
55
|
+
if (!q) return [];
|
|
56
|
+
|
|
57
|
+
const url = `${DOCKER_HUB_SEARCH_URL}?query=${encodeURIComponent(q)}&page_size=10`;
|
|
58
|
+
const signal = options.signal ?? AbortSignal.timeout(5000);
|
|
59
|
+
|
|
60
|
+
const response = await fetch(url, { signal });
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Docker Hub search failed: ${response.status}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
const results = data.results ?? [];
|
|
68
|
+
|
|
69
|
+
return results.map((item) => ({
|
|
70
|
+
name: item.repo_name,
|
|
71
|
+
isOfficial: item.is_official,
|
|
72
|
+
pullCount: item.pull_count,
|
|
73
|
+
description: item.short_description,
|
|
74
|
+
}));
|
|
75
|
+
}
|