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