cdd-cli 4.0.0 → 4.2.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.
Files changed (135) hide show
  1. package/README.es.md +60 -26
  2. package/README.md +60 -26
  3. package/dist/App.js +1 -0
  4. package/dist/components/ContainerCreationPrompt.js +14 -0
  5. package/dist/components/PromptField.js +13 -6
  6. package/dist/components/TextField.js +54 -0
  7. package/dist/components/UsageMenu.js +1 -1
  8. package/dist/helpers/appState.js +26 -0
  9. package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
  10. package/dist/helpers/textEditing.js +293 -0
  11. package/dist/hooks/creation/useContainerCreation.js +132 -15
  12. package/dist/hooks/useContainerCommandRouter.js +12 -2
  13. package/dist/hooks/useControls.js +21 -26
  14. package/dist/hooks/useShellMode.js +101 -0
  15. package/dist/index.js +3 -1
  16. package/package.json +4 -1
  17. package/.atl/skill-registry.md +0 -30
  18. package/.editorconfig +0 -12
  19. package/.eslintrc.json +0 -30
  20. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  21. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  22. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
  23. package/.github/workflows/ci.yml +0 -32
  24. package/.github/workflows/docs.yml +0 -42
  25. package/.prettierignore +0 -4
  26. package/.prettierrc +0 -8
  27. package/.~lock.ROADMAP.md# +0 -1
  28. package/CHANGELOG.md +0 -216
  29. package/CODE_OF_CONDUCT.md +0 -12
  30. package/CONTRIBUTING.md +0 -35
  31. package/ROADMAP.md +0 -647
  32. package/SECURITY.md +0 -5
  33. package/__mocks__/ink.cjs +0 -17
  34. package/_config.yml +0 -13
  35. package/babel.config.cjs +0 -23
  36. package/coverage/clover.xml +0 -575
  37. package/coverage/coverage-final.json +0 -17
  38. package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +0 -400
  39. package/coverage/lcov-report/ControlsHUD.jsx.html +0 -256
  40. package/coverage/lcov-report/base.css +0 -224
  41. package/coverage/lcov-report/block-navigation.js +0 -87
  42. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -394
  43. package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
  44. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -268
  45. package/coverage/lcov-report/components/index.html +0 -146
  46. package/coverage/lcov-report/favicon.png +0 -0
  47. package/coverage/lcov-report/helpers/constants.js.html +0 -352
  48. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
  49. package/coverage/lcov-report/helpers/dockerHubService.js.html +0 -310
  50. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
  51. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
  52. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
  53. package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
  54. package/coverage/lcov-report/helpers/index.html +0 -206
  55. package/coverage/lcov-report/helpers/logger.js.html +0 -424
  56. package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
  57. package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
  58. package/coverage/lcov-report/hooks/creation/index.html +0 -146
  59. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
  60. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -982
  61. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
  62. package/coverage/lcov-report/hooks/debug/index.html +0 -116
  63. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
  64. package/coverage/lcov-report/hooks/index.html +0 -161
  65. package/coverage/lcov-report/hooks/navigation/index.html +0 -116
  66. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
  67. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
  68. package/coverage/lcov-report/hooks/useControls.js.html +0 -868
  69. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
  70. package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
  71. package/coverage/lcov-report/index.html +0 -176
  72. package/coverage/lcov-report/prettify.css +0 -1
  73. package/coverage/lcov-report/prettify.js +0 -2
  74. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  75. package/coverage/lcov-report/sorter.js +0 -210
  76. package/coverage/lcov.info +0 -1163
  77. package/fix-imports.cjs +0 -54
  78. package/jest.config.cjs +0 -14
  79. package/src/App.jsx +0 -102
  80. package/src/components/ContainerCreationPrompt.jsx +0 -107
  81. package/src/components/ContainerList.jsx +0 -32
  82. package/src/components/ContainerRow.jsx +0 -84
  83. package/src/components/ContainerSection.jsx +0 -24
  84. package/src/components/ControlsHUD.jsx +0 -66
  85. package/src/components/Footer.jsx +0 -15
  86. package/src/components/Header.jsx +0 -41
  87. package/src/components/LogViewer.jsx +0 -36
  88. package/src/components/MessageFeedback.jsx +0 -25
  89. package/src/components/PromptField.jsx +0 -48
  90. package/src/components/StatsBar.jsx +0 -70
  91. package/src/components/SuggestionPanel.jsx +0 -61
  92. package/src/components/UsageMenu.jsx +0 -52
  93. package/src/helpers/appInfo.js +0 -23
  94. package/src/helpers/constants.js +0 -89
  95. package/src/helpers/containerOptionsBuilder.js +0 -53
  96. package/src/helpers/dockerHubService.js +0 -75
  97. package/src/helpers/dockerService/dockerService.js +0 -28
  98. package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
  99. package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
  100. package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
  101. package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
  102. package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
  103. package/src/helpers/exitWithMessage.js +0 -32
  104. package/src/helpers/imageNameUtils.js +0 -32
  105. package/src/helpers/logger.js +0 -112
  106. package/src/helpers/safeCall.js +0 -18
  107. package/src/helpers/validationHelpers.js +0 -87
  108. package/src/hooks/creation/useContainerActions.js +0 -89
  109. package/src/hooks/creation/useContainerCreation.js +0 -377
  110. package/src/hooks/creation/useLogsViewer.js +0 -51
  111. package/src/hooks/debug/useDebugLogs.js +0 -34
  112. package/src/hooks/navigation/useContainerSelection.js +0 -28
  113. package/src/hooks/useContainerCommandRouter.js +0 -123
  114. package/src/hooks/useContainerStats.js +0 -44
  115. package/src/hooks/useContainers.js +0 -26
  116. package/src/hooks/useControls.js +0 -263
  117. package/src/hooks/useEraseConfirmation.js +0 -39
  118. package/src/hooks/useExitHandler.js +0 -69
  119. package/src/index.js +0 -20
  120. package/test/ContainerCreationPrompt.dom.test.js +0 -313
  121. package/test/ControlsHUD.dom.test.js +0 -125
  122. package/test/SuggestionPanel.dom.test.js +0 -83
  123. package/test/constants.test.js +0 -41
  124. package/test/containerActions.test.js +0 -229
  125. package/test/containerList.test.js +0 -79
  126. package/test/containerOptionsBuilder.test.js +0 -36
  127. package/test/dockerHubService.test.js +0 -213
  128. package/test/jest.setup.js +0 -7
  129. package/test/logger.test.js +0 -58
  130. package/test/resolveImageTag.test.js +0 -26
  131. package/test/safeCall.test.js +0 -25
  132. package/test/useContainerCreation.dom.test.js +0 -567
  133. package/test/useContainerCreationHub.dom.test.js +0 -184
  134. package/test/useControls.dom.test.js +0 -519
  135. package/test/validationHelpers.test.js +0 -111
@@ -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
- }