cdd-cli 4.0.0 → 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 +29 -5
- package/README.md +29 -5
- package/dist/components/UsageMenu.js +1 -1
- package/dist/helpers/appState.js +26 -0
- package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
- package/dist/hooks/creation/useContainerCreation.js +25 -6
- package/dist/hooks/useContainerCommandRouter.js +12 -2
- package/dist/hooks/useControls.js +5 -0
- package/dist/hooks/useShellMode.js +101 -0
- package/dist/index.js +3 -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/.~lock.ROADMAP.md# +0 -1
- package/CHANGELOG.md +0 -216
- package/CODE_OF_CONDUCT.md +0 -12
- package/CONTRIBUTING.md +0 -35
- package/ROADMAP.md +0 -647
- 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 -575
- package/coverage/coverage-final.json +0 -17
- package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +0 -400
- package/coverage/lcov-report/ControlsHUD.jsx.html +0 -256
- 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 -394
- package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -268
- 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 -352
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
- package/coverage/lcov-report/helpers/dockerHubService.js.html +0 -310
- 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 -206
- 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 -982
- 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 -868
- 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 -176
- 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 -1163
- package/fix-imports.cjs +0 -54
- package/jest.config.cjs +0 -14
- package/src/App.jsx +0 -102
- package/src/components/ContainerCreationPrompt.jsx +0 -107
- package/src/components/ContainerList.jsx +0 -32
- package/src/components/ContainerRow.jsx +0 -84
- package/src/components/ContainerSection.jsx +0 -24
- package/src/components/ControlsHUD.jsx +0 -66
- 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 -61
- package/src/components/UsageMenu.jsx +0 -52
- package/src/helpers/appInfo.js +0 -23
- package/src/helpers/constants.js +0 -89
- package/src/helpers/containerOptionsBuilder.js +0 -53
- package/src/helpers/dockerHubService.js +0 -75
- 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 -112
- package/src/helpers/safeCall.js +0 -18
- package/src/helpers/validationHelpers.js +0 -87
- package/src/hooks/creation/useContainerActions.js +0 -89
- package/src/hooks/creation/useContainerCreation.js +0 -377
- 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 -263
- package/src/hooks/useEraseConfirmation.js +0 -39
- package/src/hooks/useExitHandler.js +0 -69
- package/src/index.js +0 -20
- package/test/ContainerCreationPrompt.dom.test.js +0 -313
- package/test/ControlsHUD.dom.test.js +0 -125
- package/test/SuggestionPanel.dom.test.js +0 -83
- package/test/constants.test.js +0 -41
- package/test/containerActions.test.js +0 -229
- package/test/containerList.test.js +0 -79
- package/test/containerOptionsBuilder.test.js +0 -36
- package/test/dockerHubService.test.js +0 -213
- package/test/jest.setup.js +0 -7
- package/test/logger.test.js +0 -58
- package/test/resolveImageTag.test.js +0 -26
- package/test/safeCall.test.js +0 -25
- package/test/useContainerCreation.dom.test.js +0 -567
- package/test/useContainerCreationHub.dom.test.js +0 -184
- package/test/useControls.dom.test.js +0 -519
- package/test/validationHelpers.test.js +0 -111
package/src/helpers/safeCall.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { logger } from './logger.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Call a function safely: if it's a function call it with provided args and
|
|
5
|
-
* catch any errors so they don't bubble to the host application.
|
|
6
|
-
*
|
|
7
|
-
* @param {Function} fn
|
|
8
|
-
* @param {...any} args
|
|
9
|
-
*/
|
|
10
|
-
export function safeCall(fn, ...args) {
|
|
11
|
-
if (typeof fn === 'function') {
|
|
12
|
-
try {
|
|
13
|
-
return fn(...args);
|
|
14
|
-
} catch (e) {
|
|
15
|
-
logger.error('safeCall caught error', e);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Reusable validations for ports and environment variables
|
|
2
|
-
import { normalizeImageName } from './imageNameUtils.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Validate a comma-separated list of port mappings in the form "host:container".
|
|
6
|
-
* Examples of valid input: "8080:80, 3000:3000"
|
|
7
|
-
*
|
|
8
|
-
* @param {string} portInput - Comma-separated port mappings
|
|
9
|
-
* @returns {boolean} True when all mappings are valid, false otherwise
|
|
10
|
-
*/
|
|
11
|
-
export function validatePorts(portInput) {
|
|
12
|
-
// Empty or whitespace-only input is considered valid (ports are optional)
|
|
13
|
-
if (!portInput || !portInput.trim()) return true;
|
|
14
|
-
const ports = portInput
|
|
15
|
-
.split(',')
|
|
16
|
-
.map((p) => p.trim())
|
|
17
|
-
.filter(Boolean);
|
|
18
|
-
if (ports.length === 0) return true;
|
|
19
|
-
const invalid = ports.find((pair) => {
|
|
20
|
-
const [host, cont] = pair.split(':');
|
|
21
|
-
return !host || !cont || isNaN(Number(host)) || isNaN(Number(cont));
|
|
22
|
-
});
|
|
23
|
-
return !invalid;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Validate environment variable input as comma-separated VAR=value pairs.
|
|
28
|
-
*
|
|
29
|
-
* When called with 3 arguments (envInput, imageName, imageProfiles), performs
|
|
30
|
-
* contextual validation: checks that all required env vars for the given image
|
|
31
|
-
* are present and non-empty. Returns an object { valid, errors, parsedEnv }.
|
|
32
|
-
*
|
|
33
|
-
* When called with 1 argument (legacy), returns a boolean for backward compatibility.
|
|
34
|
-
*
|
|
35
|
-
* @param {string} envInput - Comma-separated environment variable assignments
|
|
36
|
-
* @param {string} [imageName] - Optional Docker image name for contextual validation
|
|
37
|
-
* @param {Object} [imageProfiles] - Optional map of image profiles (from constants.js)
|
|
38
|
-
* @returns {boolean|{valid: boolean, errors: string[], parsedEnv: Record<string,string>}}
|
|
39
|
-
*/
|
|
40
|
-
export function validateEnvVars(envInput, imageName, imageProfiles) {
|
|
41
|
-
const contextual = imageName !== undefined && imageProfiles !== undefined;
|
|
42
|
-
|
|
43
|
-
// Parse: split on FIRST '=' only
|
|
44
|
-
const parsedEnv = {};
|
|
45
|
-
const syntaxErrors = [];
|
|
46
|
-
|
|
47
|
-
if (envInput && envInput.trim()) {
|
|
48
|
-
const vars = envInput
|
|
49
|
-
.split(',')
|
|
50
|
-
.map((v) => v.trim())
|
|
51
|
-
.filter(Boolean);
|
|
52
|
-
for (const v of vars) {
|
|
53
|
-
const eqIdx = v.indexOf('=');
|
|
54
|
-
if (eqIdx === -1) {
|
|
55
|
-
syntaxErrors.push(`"${v}" is missing an '=' sign`);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const varName = v.slice(0, eqIdx).trim();
|
|
59
|
-
const varValue = v.slice(eqIdx + 1);
|
|
60
|
-
if (!/^[A-Z_][A-Z0-9_]*$/i.test(varName)) {
|
|
61
|
-
syntaxErrors.push(`"${varName}" is not a valid variable name`);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
parsedEnv[varName] = varValue;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!contextual) {
|
|
69
|
-
// Legacy: return boolean
|
|
70
|
-
return syntaxErrors.length === 0;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Contextual: also check required env vars from the profile
|
|
74
|
-
const errors = [...syntaxErrors];
|
|
75
|
-
const baseName = normalizeImageName(imageName);
|
|
76
|
-
const profile = imageProfiles[baseName];
|
|
77
|
-
|
|
78
|
-
if (profile && profile.requiredEnv && profile.requiredEnv.length) {
|
|
79
|
-
for (const required of profile.requiredEnv) {
|
|
80
|
-
if (!parsedEnv[required] || parsedEnv[required].trim() === '') {
|
|
81
|
-
errors.push(`Missing required env var: ${required}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return { valid: errors.length === 0, errors, parsedEnv };
|
|
87
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import { safeCall } from '../../helpers/safeCall.js';
|
|
3
|
-
import {
|
|
4
|
-
startContainer as svcStartContainer,
|
|
5
|
-
stopContainer as svcStopContainer,
|
|
6
|
-
restartContainer as svcRestartContainer,
|
|
7
|
-
removeContainer as svcRemoveContainer,
|
|
8
|
-
} from '../../helpers/dockerService/serviceComponents/containerActions.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Custom hook to manage container actions (start, stop, restart, remove).
|
|
12
|
-
* Handles feedback messages and exposes helpers for each action.
|
|
13
|
-
*
|
|
14
|
-
* @param {Object} params
|
|
15
|
-
* @param {Array} params.containers - List of containers
|
|
16
|
-
* @param {Function} params.onAction - Callback after action is performed
|
|
17
|
-
* @returns {Object} Action helpers and feedback state
|
|
18
|
-
*/
|
|
19
|
-
export function useContainerActions({ containers, onAction }) {
|
|
20
|
-
const [message, setMessage] = useState('');
|
|
21
|
-
const [messageColor, setMessageColor] = useState('yellow');
|
|
22
|
-
|
|
23
|
-
const messageTimerRef = useRef(null);
|
|
24
|
-
|
|
25
|
-
function setTimedMessage(msg, color = 'yellow', ms = 4000) {
|
|
26
|
-
clearTimeout(messageTimerRef.current);
|
|
27
|
-
setMessage(msg);
|
|
28
|
-
if (color !== undefined) setMessageColor(color);
|
|
29
|
-
if (msg) {
|
|
30
|
-
messageTimerRef.current = setTimeout(() => setMessage(''), ms);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
useEffect(() => () => clearTimeout(messageTimerRef.current), []);
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Handles a generic container action and sets feedback.
|
|
38
|
-
* @param {Function} actionFn - The async action function (start, stop, etc)
|
|
39
|
-
* @param {string} actionLabel - Action label for feedback
|
|
40
|
-
* @param {number} selected - Index of selected container
|
|
41
|
-
* @param {Function} [stateCheck] - Optional function to check state before action
|
|
42
|
-
*/
|
|
43
|
-
async function handleAction({ actionFn, actionLabel, selected, stateCheck }) {
|
|
44
|
-
const container = containers[selected];
|
|
45
|
-
if (!container) return;
|
|
46
|
-
if (stateCheck) {
|
|
47
|
-
const checkMsg = stateCheck(container);
|
|
48
|
-
if (checkMsg) {
|
|
49
|
-
setMessage(checkMsg);
|
|
50
|
-
setMessageColor('yellow');
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
setMessage(`${actionLabel} container...`);
|
|
55
|
-
setMessageColor('yellow');
|
|
56
|
-
try {
|
|
57
|
-
await actionFn(container.id);
|
|
58
|
-
setTimedMessage(`${actionLabel} container successful.`, 'green');
|
|
59
|
-
safeCall(onAction);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
setTimedMessage(
|
|
62
|
-
`Failed to ${actionLabel.toLowerCase()} container: ${err.message}`,
|
|
63
|
-
'red'
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
message,
|
|
70
|
-
setMessage,
|
|
71
|
-
setTimedMessage,
|
|
72
|
-
messageColor,
|
|
73
|
-
setMessageColor,
|
|
74
|
-
handleAction,
|
|
75
|
-
// Expose concrete actions so callers can use actions.startContainer(id)
|
|
76
|
-
startContainer: async (id) => {
|
|
77
|
-
return await svcStartContainer(id);
|
|
78
|
-
},
|
|
79
|
-
stopContainer: async (id) => {
|
|
80
|
-
return await svcStopContainer(id);
|
|
81
|
-
},
|
|
82
|
-
restartContainer: async (id) => {
|
|
83
|
-
return await svcRestartContainer(id);
|
|
84
|
-
},
|
|
85
|
-
removeContainer: async (id) => {
|
|
86
|
-
return await svcRemoveContainer(id);
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import { useReducer, useState, useRef, useEffect } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
validatePorts,
|
|
4
|
-
validateEnvVars,
|
|
5
|
-
} from '../../helpers/validationHelpers.js';
|
|
6
|
-
import { IMAGE_PROFILES, resolveImageTag } from '../../helpers/constants.js';
|
|
7
|
-
import { safeCall } from '../../helpers/safeCall.js';
|
|
8
|
-
import { searchDockerHub, formatHubResult } from '../../helpers/dockerHubService.js';
|
|
9
|
-
|
|
10
|
-
const MAX_VISIBLE = 6;
|
|
11
|
-
|
|
12
|
-
const INITIAL_FORM = {
|
|
13
|
-
step: 0,
|
|
14
|
-
imageName: '',
|
|
15
|
-
containerName: '',
|
|
16
|
-
portInput: '',
|
|
17
|
-
envInput: '',
|
|
18
|
-
message: '',
|
|
19
|
-
messageColor: 'yellow',
|
|
20
|
-
suggestions: [],
|
|
21
|
-
selectedSuggestionIndex: -1,
|
|
22
|
-
visibleOffset: 0,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function formReducer(state, action) {
|
|
26
|
-
switch (action.type) {
|
|
27
|
-
case 'RESET': return { ...INITIAL_FORM };
|
|
28
|
-
case 'RESET_WIZARD': return { ...INITIAL_FORM, message: 'Insert the name of the image to create: ', messageColor: 'yellow' };
|
|
29
|
-
case 'SET': return { ...state, ...action.payload };
|
|
30
|
-
default: return state;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Custom hook to manage the container creation flow, step by step.
|
|
36
|
-
* Handles input, validation, and feedback for each creation step.
|
|
37
|
-
*
|
|
38
|
-
* @param {Object} params
|
|
39
|
-
* @param {Function} params.onCreate - Callback when creation is confirmed
|
|
40
|
-
* @param {Function} params.onCancel - Callback when creation is cancelled
|
|
41
|
-
* @param {Array<string>} params.dbImages - List of DB image names for env var warning (legacy)
|
|
42
|
-
* @param {Object} [params.imageProfiles] - Image profiles map for contextual validation
|
|
43
|
-
* @returns {Object} Creation state, setters, and helpers
|
|
44
|
-
*/
|
|
45
|
-
export function useContainerCreation({
|
|
46
|
-
onCreate,
|
|
47
|
-
onCancel,
|
|
48
|
-
dbImages = [],
|
|
49
|
-
imageProfiles = IMAGE_PROFILES,
|
|
50
|
-
}) {
|
|
51
|
-
const [form, dispatch] = useReducer(formReducer, INITIAL_FORM);
|
|
52
|
-
const {
|
|
53
|
-
step,
|
|
54
|
-
imageName,
|
|
55
|
-
containerName,
|
|
56
|
-
portInput,
|
|
57
|
-
envInput,
|
|
58
|
-
message,
|
|
59
|
-
messageColor,
|
|
60
|
-
suggestions,
|
|
61
|
-
selectedSuggestionIndex,
|
|
62
|
-
visibleOffset,
|
|
63
|
-
} = form;
|
|
64
|
-
|
|
65
|
-
// Convenience setters that mirror the old useState API
|
|
66
|
-
const setStep = (v) => dispatch({ type: 'SET', payload: { step: v } });
|
|
67
|
-
const setImageName = (v) => dispatch({ type: 'SET', payload: { imageName: v } });
|
|
68
|
-
const setContainerName = (v) => dispatch({ type: 'SET', payload: { containerName: v } });
|
|
69
|
-
const setPortInput = (v) => dispatch({ type: 'SET', payload: { portInput: v } });
|
|
70
|
-
const setEnvInput = (v) =>
|
|
71
|
-
dispatch({ type: 'SET', payload: { envInput: typeof v === 'function' ? v(form.envInput) : v } });
|
|
72
|
-
const setMessage = (v) => dispatch({ type: 'SET', payload: { message: v } });
|
|
73
|
-
const setMessageColor = (v) => dispatch({ type: 'SET', payload: { messageColor: v } });
|
|
74
|
-
const setSuggestions = (v) => dispatch({ type: 'SET', payload: { suggestions: v } });
|
|
75
|
-
const setSelectedSuggestionIndex = (fn) => {
|
|
76
|
-
// Support both direct value and updater function
|
|
77
|
-
if (typeof fn === 'function') {
|
|
78
|
-
dispatch({ type: 'SET', payload: { selectedSuggestionIndex: fn(form.selectedSuggestionIndex) } });
|
|
79
|
-
} else {
|
|
80
|
-
dispatch({ type: 'SET', payload: { selectedSuggestionIndex: fn } });
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
const setVisibleOffset = (fn) => {
|
|
84
|
-
if (typeof fn === 'function') {
|
|
85
|
-
dispatch({ type: 'SET', payload: { visibleOffset: fn(form.visibleOffset) } });
|
|
86
|
-
} else {
|
|
87
|
-
dispatch({ type: 'SET', payload: { visibleOffset: fn } });
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Docker Hub search state (kept as useState because they are independent of form)
|
|
92
|
-
const [isSearchingHub, setIsSearchingHub] = useState(false);
|
|
93
|
-
const [hubResults, setHubResults] = useState(null);
|
|
94
|
-
// Tracks active request: { controller: AbortController|null, requestId: number }
|
|
95
|
-
const activeHubRequestRef = useRef({ controller: null, requestId: 0 });
|
|
96
|
-
|
|
97
|
-
// Timer for auto-clearing messages
|
|
98
|
-
const messageTimerRef = useRef(null);
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Sets a permanent message (e.g. step instructions). No auto-clear.
|
|
102
|
-
*/
|
|
103
|
-
function setStepMessage(msg, color = 'yellow') {
|
|
104
|
-
clearTimeout(messageTimerRef.current);
|
|
105
|
-
dispatch({ type: 'SET', payload: { message: msg, messageColor: color } });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Sets a transient feedback message (e.g. validation errors). Auto-clears after `ms`.
|
|
110
|
-
*/
|
|
111
|
-
function setTimedMessage(msg, color = 'yellow', ms = 4000) {
|
|
112
|
-
clearTimeout(messageTimerRef.current);
|
|
113
|
-
dispatch({ type: 'SET', payload: { message: msg, messageColor: color } });
|
|
114
|
-
if (msg) {
|
|
115
|
-
messageTimerRef.current = setTimeout(() => dispatch({ type: 'SET', payload: { message: '' } }), ms);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
useEffect(() => () => clearTimeout(messageTimerRef.current), []);
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Updates the image name input and recalculates autocomplete suggestions.
|
|
123
|
-
* Resets selectedSuggestionIndex to -1 on every keystroke.
|
|
124
|
-
* Also aborts any in-flight Hub search and clears Hub results.
|
|
125
|
-
*
|
|
126
|
-
* @param {string} value - New raw image name typed by user
|
|
127
|
-
*/
|
|
128
|
-
function updateImageInput(value) {
|
|
129
|
-
// Abort any in-flight Hub search and clear its state
|
|
130
|
-
activeHubRequestRef.current.controller?.abort();
|
|
131
|
-
activeHubRequestRef.current.requestId += 1;
|
|
132
|
-
setIsSearchingHub(false);
|
|
133
|
-
setHubResults(null);
|
|
134
|
-
|
|
135
|
-
const lower = value.toLowerCase();
|
|
136
|
-
const matches = value.trim()
|
|
137
|
-
? Object.keys(imageProfiles).filter((key) => key.includes(lower))
|
|
138
|
-
: [];
|
|
139
|
-
|
|
140
|
-
dispatch({
|
|
141
|
-
type: 'SET',
|
|
142
|
-
payload: {
|
|
143
|
-
imageName: value,
|
|
144
|
-
selectedSuggestionIndex: -1,
|
|
145
|
-
visibleOffset: 0,
|
|
146
|
-
suggestions: matches,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Triggers a Docker Hub search for the current imageName.
|
|
153
|
-
* Guards: imageName must be non-empty; no concurrent search allowed.
|
|
154
|
-
* Uses AbortController + requestId to handle race conditions.
|
|
155
|
-
*/
|
|
156
|
-
async function triggerHubSearch() {
|
|
157
|
-
const query = imageName.trim();
|
|
158
|
-
if (!query) return;
|
|
159
|
-
if (isSearchingHub) return;
|
|
160
|
-
|
|
161
|
-
// Abort previous request (should already be aborted from updateImageInput,
|
|
162
|
-
// but guard here too)
|
|
163
|
-
activeHubRequestRef.current.controller?.abort();
|
|
164
|
-
|
|
165
|
-
const requestId = activeHubRequestRef.current.requestId + 1;
|
|
166
|
-
activeHubRequestRef.current.requestId = requestId;
|
|
167
|
-
|
|
168
|
-
const controller = new AbortController();
|
|
169
|
-
activeHubRequestRef.current.controller = controller;
|
|
170
|
-
|
|
171
|
-
setIsSearchingHub(true);
|
|
172
|
-
dispatch({ type: 'SET', payload: { selectedSuggestionIndex: -1, visibleOffset: 0 } });
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const results = await searchDockerHub(query, { signal: controller.signal });
|
|
176
|
-
// Ignore stale responses
|
|
177
|
-
if (activeHubRequestRef.current.requestId !== requestId) return;
|
|
178
|
-
setHubResults(results.map(formatHubResult));
|
|
179
|
-
} catch (err) {
|
|
180
|
-
if (activeHubRequestRef.current.requestId !== requestId) return;
|
|
181
|
-
// Silence AbortError; for all other errors fall back to static suggestions
|
|
182
|
-
setHubResults(null);
|
|
183
|
-
} finally {
|
|
184
|
-
if (activeHubRequestRef.current.requestId === requestId) {
|
|
185
|
-
setIsSearchingHub(false);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Moves the suggestion selection up (-1) or down (+1).
|
|
192
|
-
* Clamps index to [-1, suggestions.length - 1].
|
|
193
|
-
* Adjusts visibleOffset to keep selected item in the visible window.
|
|
194
|
-
*
|
|
195
|
-
* @param {number} direction - -1 (up) or 1 (down)
|
|
196
|
-
*/
|
|
197
|
-
function moveSuggestionSelection(direction) {
|
|
198
|
-
const next = Math.max(-1, Math.min(suggestions.length - 1, selectedSuggestionIndex + direction));
|
|
199
|
-
let newOffset = visibleOffset;
|
|
200
|
-
if (next < visibleOffset) newOffset = Math.max(0, next);
|
|
201
|
-
else if (next >= visibleOffset + MAX_VISIBLE) newOffset = next - MAX_VISIBLE + 1;
|
|
202
|
-
dispatch({ type: 'SET', payload: { selectedSuggestionIndex: next, visibleOffset: newOffset } });
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Applies the currently focused suggestion to imageName.
|
|
207
|
-
* Does NOT advance the step. Clears suggestions after applying.
|
|
208
|
-
*/
|
|
209
|
-
function applyFocusedSuggestion() {
|
|
210
|
-
if (selectedSuggestionIndex < 0 || selectedSuggestionIndex >= suggestions.length) return;
|
|
211
|
-
const chosen = suggestions[selectedSuggestionIndex];
|
|
212
|
-
dispatch({
|
|
213
|
-
type: 'SET',
|
|
214
|
-
payload: {
|
|
215
|
-
imageName: resolveImageTag(chosen, imageProfiles),
|
|
216
|
-
suggestions: [],
|
|
217
|
-
selectedSuggestionIndex: -1,
|
|
218
|
-
visibleOffset: 0,
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Advances to the next step, with validation and feedback.
|
|
225
|
-
*/
|
|
226
|
-
function nextStep() {
|
|
227
|
-
if (step === 0) {
|
|
228
|
-
if (!imageName.trim()) {
|
|
229
|
-
setTimedMessage('Image name cannot be empty.', 'red');
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const resolved = resolveImageTag(imageName, imageProfiles);
|
|
233
|
-
dispatch({ type: 'SET', payload: { imageName: resolved, step: 1 } });
|
|
234
|
-
setStepMessage(
|
|
235
|
-
'Optional: Enter container name or leave empty and press Enter',
|
|
236
|
-
'yellow'
|
|
237
|
-
);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (step === 1) {
|
|
241
|
-
dispatch({ type: 'SET', payload: { step: 2 } });
|
|
242
|
-
setStepMessage(
|
|
243
|
-
'Optional: Enter ports (format 8080:80,443:443) or leave empty and press Enter',
|
|
244
|
-
'yellow'
|
|
245
|
-
);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (step === 2) {
|
|
249
|
-
// Ports are now optional - only validate if provided
|
|
250
|
-
if (portInput.trim() && !validatePorts(portInput)) {
|
|
251
|
-
setTimedMessage(
|
|
252
|
-
'Port format must be host:container and both must be numbers (e.g. 8080:80)',
|
|
253
|
-
'red'
|
|
254
|
-
);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
dispatch({ type: 'SET', payload: { step: 3 } });
|
|
258
|
-
const isDb = dbImages.some((db) =>
|
|
259
|
-
imageName.trim().toLowerCase().includes(db)
|
|
260
|
-
);
|
|
261
|
-
// Check if the image has a profile with required env vars
|
|
262
|
-
const baseName = imageName
|
|
263
|
-
.trim()
|
|
264
|
-
.toLowerCase()
|
|
265
|
-
.split(':')[0]
|
|
266
|
-
.split('/')
|
|
267
|
-
.pop();
|
|
268
|
-
const profile = imageProfiles[baseName];
|
|
269
|
-
if (profile && profile.requiredEnv && profile.requiredEnv.length) {
|
|
270
|
-
const suggestedPart =
|
|
271
|
-
profile.suggestedEnv && profile.suggestedEnv.length
|
|
272
|
-
? ` | Suggested: ${profile.suggestedEnv.join(', ')}`
|
|
273
|
-
: '';
|
|
274
|
-
setStepMessage(
|
|
275
|
-
`Required env vars for ${baseName}: ${profile.requiredEnv.join(', ')}. Enter as VAR=val,VAR2=val2${suggestedPart}`,
|
|
276
|
-
'yellow'
|
|
277
|
-
);
|
|
278
|
-
} else if (profile && profile.suggestedEnv && profile.suggestedEnv.length) {
|
|
279
|
-
setStepMessage(
|
|
280
|
-
`Suggested env vars for ${baseName}: ${profile.suggestedEnv.join(', ')}. Enter as VAR=val,VAR2=val2 or leave empty.`,
|
|
281
|
-
'yellow'
|
|
282
|
-
);
|
|
283
|
-
} else if (isDb) {
|
|
284
|
-
setStepMessage(
|
|
285
|
-
'Warning: This image usually requires environment variables (e.g. MYSQL_ROOT_PASSWORD=my-secret-pw for MySQL, POSTGRES_PASSWORD=yourpassword for Postgres). Enter them as VAR=val,VAR2=val2 or leave empty and press Enter.',
|
|
286
|
-
'yellow'
|
|
287
|
-
);
|
|
288
|
-
} else {
|
|
289
|
-
setStepMessage(
|
|
290
|
-
'Optional: Enter environment variables (format VAR1=val1,VAR2=val2) or leave empty and press Enter',
|
|
291
|
-
'yellow'
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (step === 3) {
|
|
297
|
-
// Contextual env validation using image profiles
|
|
298
|
-
const result = validateEnvVars(envInput, imageName, imageProfiles);
|
|
299
|
-
if (!result.valid) {
|
|
300
|
-
setTimedMessage(result.errors.join(' | '), 'red');
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
// Final step: call onCreate with all data (safely)
|
|
304
|
-
safeCall(onCreate, { imageName, containerName, portInput, envInput });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Cancels the creation process and resets state.
|
|
310
|
-
*/
|
|
311
|
-
function cancelCreation() {
|
|
312
|
-
dispatch({ type: 'RESET' });
|
|
313
|
-
safeCall(onCancel);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Resets all creation state to initial values without triggering onCancel.
|
|
318
|
-
* Used by the command router when the user presses 'c' to open the creation wizard.
|
|
319
|
-
*/
|
|
320
|
-
function resetCreation() {
|
|
321
|
-
dispatch({ type: 'RESET_WIZARD' });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Inserts the next pending suggested env var into envInput.
|
|
326
|
-
* Derives "next" by comparing keys already in envInput against suggestedEnv.
|
|
327
|
-
* When all suggestions are already present, sets a feedback message.
|
|
328
|
-
*/
|
|
329
|
-
function insertNextSuggestedEnv() {
|
|
330
|
-
const baseName = imageName.trim().toLowerCase().split(':')[0].split('/').pop();
|
|
331
|
-
const profile = imageProfiles[baseName];
|
|
332
|
-
const suggested = profile?.suggestedEnv ?? [];
|
|
333
|
-
if (!suggested.length) return;
|
|
334
|
-
const addedKeys = envInput.split(',').map(s => s.split('=')[0].trim()).filter(Boolean);
|
|
335
|
-
const next = suggested.find(s => !addedKeys.includes(s.split('=')[0]));
|
|
336
|
-
if (!next) { setTimedMessage('All suggested env vars added', 'yellow'); return; }
|
|
337
|
-
const newEnvInput = envInput ? `${envInput},${next}` : next;
|
|
338
|
-
dispatch({ type: 'SET', payload: { envInput: newEnvInput } });
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* True when the current image profile has at least one suggestedEnv entry.
|
|
343
|
-
*/
|
|
344
|
-
const baseName = imageName.trim().toLowerCase().split(':')[0].split('/').pop();
|
|
345
|
-
const hasSuggestedEnv = (imageProfiles[baseName]?.suggestedEnv?.length ?? 0) > 0;
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
step,
|
|
349
|
-
setStep,
|
|
350
|
-
imageName,
|
|
351
|
-
setImageName,
|
|
352
|
-
containerName,
|
|
353
|
-
setContainerName,
|
|
354
|
-
portInput,
|
|
355
|
-
setPortInput,
|
|
356
|
-
envInput,
|
|
357
|
-
setEnvInput,
|
|
358
|
-
message,
|
|
359
|
-
setMessage,
|
|
360
|
-
messageColor,
|
|
361
|
-
setMessageColor,
|
|
362
|
-
suggestions,
|
|
363
|
-
selectedSuggestionIndex,
|
|
364
|
-
visibleOffset,
|
|
365
|
-
isSearchingHub,
|
|
366
|
-
hubResults,
|
|
367
|
-
updateImageInput,
|
|
368
|
-
triggerHubSearch,
|
|
369
|
-
moveSuggestionSelection,
|
|
370
|
-
applyFocusedSuggestion,
|
|
371
|
-
nextStep,
|
|
372
|
-
cancelCreation,
|
|
373
|
-
resetCreation,
|
|
374
|
-
insertNextSuggestedEnv,
|
|
375
|
-
hasSuggestedEnv,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { useState, useRef } from 'react';
|
|
2
|
-
import { safeCall } from '../../helpers/safeCall.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Custom hook to manage the logs viewer state and stream reference.
|
|
6
|
-
* Handles showing/hiding logs and storing log lines.
|
|
7
|
-
*
|
|
8
|
-
* @returns {Object} Logs viewer state and helpers
|
|
9
|
-
* @property {boolean} showLogs - Whether the logs viewer is open
|
|
10
|
-
* @property {function} setShowLogs - Setter for showLogs
|
|
11
|
-
* @property {Array<string>} logs - Array of log lines
|
|
12
|
-
* @property {function} setLogs - Setter for logs
|
|
13
|
-
* @property {object} logsStreamRef - Ref to the logs stream (for cleanup)
|
|
14
|
-
* @property {function} openLogs - Helper to open logs for a container
|
|
15
|
-
* @property {function} closeLogs - Helper to close logs and cleanup
|
|
16
|
-
*/
|
|
17
|
-
export function useLogsViewer() {
|
|
18
|
-
const [showLogs, setShowLogs] = useState(false);
|
|
19
|
-
const [logs, setLogs] = useState([]);
|
|
20
|
-
const logsStreamRef = useRef(null);
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Opens the logs viewer and resets logs.
|
|
24
|
-
*/
|
|
25
|
-
function openLogs() {
|
|
26
|
-
setShowLogs(true);
|
|
27
|
-
setLogs([]);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Closes the logs viewer and cleans up the stream.
|
|
32
|
-
*/
|
|
33
|
-
function closeLogs() {
|
|
34
|
-
setShowLogs(false);
|
|
35
|
-
setLogs([]);
|
|
36
|
-
if (logsStreamRef.current) {
|
|
37
|
-
safeCall(logsStreamRef.current.destroy?.bind(logsStreamRef.current));
|
|
38
|
-
logsStreamRef.current = null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
showLogs,
|
|
44
|
-
setShowLogs,
|
|
45
|
-
logs,
|
|
46
|
-
setLogs,
|
|
47
|
-
logsStreamRef,
|
|
48
|
-
openLogs,
|
|
49
|
-
closeLogs,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { logger } from '../../helpers/logger.js';
|
|
3
|
-
|
|
4
|
-
export function useDebugLogs() {
|
|
5
|
-
const [showDebugLogs, setShowDebugLogs] = useState(false);
|
|
6
|
-
const [debugLogs, setDebugLogs] = useState([]);
|
|
7
|
-
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
const unsubscribe = logger.subscribe((entry) => {
|
|
10
|
-
setDebugLogs((prev) => {
|
|
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;
|
|
26
|
-
const next = [...prev, line];
|
|
27
|
-
return next.slice(-200);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
return unsubscribe;
|
|
31
|
-
}, []);
|
|
32
|
-
|
|
33
|
-
return { showDebugLogs, setShowDebugLogs, debugLogs };
|
|
34
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react';
|
|
2
|
-
|
|
3
|
-
export function useContainerSelection(total) {
|
|
4
|
-
const [selected, setSelected] = useState(0);
|
|
5
|
-
|
|
6
|
-
const handleNavigation = useCallback(
|
|
7
|
-
(input, key) => {
|
|
8
|
-
if (total === 0) {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (key.upArrow) {
|
|
13
|
-
setSelected((i) => (i === 0 ? total - 1 : i - 1));
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (key.downArrow) {
|
|
18
|
-
setSelected((i) => (i === total - 1 ? 0 : i + 1));
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return false;
|
|
23
|
-
},
|
|
24
|
-
[total]
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
return { selected, setSelected, handleNavigation };
|
|
28
|
-
}
|