cdd-cli 3.2.0 → 3.2.2

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 (120) hide show
  1. package/.atl/skill-registry.md +30 -0
  2. package/.editorconfig +12 -0
  3. package/.eslintrc.json +1 -1
  4. package/.prettierignore +4 -0
  5. package/.prettierrc +8 -0
  6. package/CHANGELOG.md +29 -0
  7. package/SECURITY.md +5 -0
  8. package/__mocks__/ink.cjs +17 -0
  9. package/coverage/clover.xml +526 -436
  10. package/coverage/coverage-final.json +19 -15
  11. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +376 -0
  12. package/coverage/lcov-report/components/PromptField.jsx.html +229 -0
  13. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +244 -0
  14. package/coverage/lcov-report/components/index.html +146 -0
  15. package/coverage/lcov-report/helpers/constants.js.html +80 -17
  16. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +41 -8
  17. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +162 -84
  18. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +50 -32
  19. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +21 -21
  20. package/coverage/lcov-report/helpers/imageNameUtils.js.html +181 -0
  21. package/coverage/lcov-report/helpers/index.html +27 -12
  22. package/coverage/lcov-report/helpers/logger.js.html +19 -19
  23. package/coverage/lcov-report/helpers/safeCall.js.html +3 -3
  24. package/coverage/lcov-report/helpers/validationHelpers.js.html +28 -76
  25. package/coverage/lcov-report/hooks/creation/index.html +20 -20
  26. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +38 -17
  27. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +416 -89
  28. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +7 -7
  29. package/coverage/lcov-report/hooks/debug/index.html +5 -5
  30. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +35 -20
  31. package/coverage/lcov-report/hooks/index.html +40 -40
  32. package/coverage/lcov-report/hooks/navigation/index.html +19 -19
  33. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +38 -29
  34. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +119 -80
  35. package/coverage/lcov-report/hooks/useControls.js.html +263 -122
  36. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +8 -8
  37. package/coverage/lcov-report/hooks/useExitHandler.js.html +50 -41
  38. package/coverage/lcov-report/index.html +67 -52
  39. package/coverage/lcov.info +991 -805
  40. package/dist/App.js +11 -11
  41. package/dist/components/ContainerCreationPrompt.js +31 -10
  42. package/dist/components/ContainerList.js +3 -3
  43. package/dist/components/ContainerRow.js +20 -20
  44. package/dist/components/ContainerSection.js +3 -3
  45. package/dist/components/Footer.js +2 -2
  46. package/dist/components/Header.js +7 -7
  47. package/dist/components/LogViewer.js +3 -3
  48. package/dist/components/MessageFeedback.js +2 -2
  49. package/dist/components/PromptField.js +4 -4
  50. package/dist/components/StatsBar.js +5 -5
  51. package/dist/components/SuggestionPanel.js +44 -0
  52. package/dist/components/UsageMenu.js +3 -3
  53. package/dist/helpers/appInfo.js +7 -7
  54. package/dist/helpers/constants.js +114 -0
  55. package/dist/helpers/containerOptionsBuilder.js +3 -3
  56. package/dist/helpers/dockerService/dockerService.js +1 -1
  57. package/dist/helpers/dockerService/serviceComponents/containerActions.js +99 -40
  58. package/dist/helpers/dockerService/serviceComponents/containerList.js +11 -11
  59. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +9 -9
  60. package/dist/helpers/dockerService/serviceComponents/containerStats.js +5 -5
  61. package/dist/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
  62. package/dist/helpers/exitWithMessage.js +10 -10
  63. package/dist/helpers/imageNameUtils.js +32 -0
  64. package/dist/helpers/logger.js +13 -13
  65. package/dist/helpers/safeCall.js +2 -2
  66. package/dist/helpers/validationHelpers.js +76 -21
  67. package/dist/hooks/creation/useContainerActions.js +9 -9
  68. package/dist/hooks/creation/useContainerCreation.js +138 -36
  69. package/dist/hooks/creation/useLogsViewer.js +2 -2
  70. package/dist/hooks/debug/useDebugLogs.js +4 -4
  71. package/dist/hooks/useContainerCommandRouter.js +13 -13
  72. package/dist/hooks/useContainers.js +3 -3
  73. package/dist/hooks/useControls.js +66 -35
  74. package/dist/hooks/useEraseConfirmation.js +3 -3
  75. package/dist/index.js +2 -2
  76. package/jest.config.cjs +7 -0
  77. package/package.json +7 -3
  78. package/src/App.jsx +33 -16
  79. package/src/components/ContainerCreationPrompt.jsx +43 -9
  80. package/src/components/ContainerList.jsx +3 -3
  81. package/src/components/ContainerRow.jsx +19 -16
  82. package/src/components/ContainerSection.jsx +3 -3
  83. package/src/components/Footer.jsx +2 -2
  84. package/src/components/Header.jsx +7 -7
  85. package/src/components/LogViewer.jsx +5 -3
  86. package/src/components/MessageFeedback.jsx +2 -2
  87. package/src/components/PromptField.jsx +4 -4
  88. package/src/components/StatsBar.jsx +11 -7
  89. package/src/components/SuggestionPanel.jsx +53 -0
  90. package/src/components/UsageMenu.jsx +18 -13
  91. package/src/helpers/appInfo.js +7 -7
  92. package/src/helpers/constants.js +30 -9
  93. package/src/helpers/containerOptionsBuilder.js +16 -5
  94. package/src/helpers/dockerService/dockerService.js +1 -1
  95. package/src/helpers/dockerService/serviceComponents/containerActions.js +87 -61
  96. package/src/helpers/dockerService/serviceComponents/containerList.js +31 -25
  97. package/src/helpers/dockerService/serviceComponents/containerLogs.js +41 -26
  98. package/src/helpers/dockerService/serviceComponents/containerStats.js +13 -11
  99. package/src/helpers/dockerService/serviceComponents/imageUtils.js +5 -4
  100. package/src/helpers/exitWithMessage.js +13 -7
  101. package/src/helpers/imageNameUtils.js +32 -0
  102. package/src/helpers/logger.js +15 -15
  103. package/src/helpers/safeCall.js +2 -2
  104. package/src/helpers/validationHelpers.js +13 -29
  105. package/src/hooks/creation/useContainerActions.js +19 -12
  106. package/src/hooks/creation/useContainerCreation.js +150 -41
  107. package/src/hooks/creation/useLogsViewer.js +2 -2
  108. package/src/hooks/debug/useDebugLogs.js +15 -10
  109. package/src/hooks/navigation/useContainerSelection.js +17 -14
  110. package/src/hooks/useContainerCommandRouter.js +71 -58
  111. package/src/hooks/useContainers.js +4 -4
  112. package/src/hooks/useControls.js +115 -68
  113. package/src/hooks/useEraseConfirmation.js +3 -3
  114. package/src/hooks/useExitHandler.js +29 -26
  115. package/src/index.js +4 -4
  116. package/test/ContainerCreationPrompt.dom.test.js +68 -0
  117. package/test/SuggestionPanel.dom.test.js +53 -0
  118. package/test/constants.test.js +34 -0
  119. package/test/useContainerCreation.dom.test.js +123 -0
  120. package/test/useControls.dom.test.js +198 -2
@@ -1,4 +1,4 @@
1
- import { logger } from "./logger.js";
1
+ import { logger } from './logger.js';
2
2
 
3
3
  /**
4
4
  * Call a function safely: if it's a function call it with provided args and
@@ -15,7 +15,7 @@ export function safeCall(fn) {
15
15
  }
16
16
  return fn.apply(void 0, args);
17
17
  } catch (e) {
18
- logger.error("safeCall caught error", e);
18
+ logger.error('safeCall caught error', e);
19
19
  }
20
20
  }
21
21
  }
@@ -1,3 +1,4 @@
1
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
1
2
  function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
2
3
  function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
4
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -5,6 +6,7 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
5
6
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
7
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
8
  // Reusable validations for ports and environment variables
9
+ import { normalizeImageName } from './imageNameUtils.js';
8
10
 
9
11
  /**
10
12
  * Validate a comma-separated list of port mappings in the form "host:container".
@@ -16,12 +18,12 @@ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
16
18
  export function validatePorts(portInput) {
17
19
  // Empty or whitespace-only input is considered valid (ports are optional)
18
20
  if (!portInput || !portInput.trim()) return true;
19
- var ports = portInput.split(",").map(function (p) {
21
+ var ports = portInput.split(',').map(function (p) {
20
22
  return p.trim();
21
23
  }).filter(Boolean);
22
24
  if (ports.length === 0) return true;
23
25
  var invalid = ports.find(function (pair) {
24
- var _pair$split = pair.split(":"),
26
+ var _pair$split = pair.split(':'),
25
27
  _pair$split2 = _slicedToArray(_pair$split, 2),
26
28
  host = _pair$split2[0],
27
29
  cont = _pair$split2[1];
@@ -32,27 +34,80 @@ export function validatePorts(portInput) {
32
34
 
33
35
  /**
34
36
  * Validate environment variable input as comma-separated VAR=value pairs.
35
- * Empty or whitespace-only input is considered valid (no env vars).
36
- * Variable names must start with a letter or underscore and contain only
37
- * alphanumeric characters and underscores.
37
+ *
38
+ * When called with 3 arguments (envInput, imageName, imageProfiles), performs
39
+ * contextual validation: checks that all required env vars for the given image
40
+ * are present and non-empty. Returns an object { valid, errors, parsedEnv }.
41
+ *
42
+ * When called with 1 argument (legacy), returns a boolean for backward compatibility.
38
43
  *
39
44
  * @param {string} envInput - Comma-separated environment variable assignments
40
- * @returns {boolean} True when all env var assignments are valid, false otherwise
45
+ * @param {string} [imageName] - Optional Docker image name for contextual validation
46
+ * @param {Object} [imageProfiles] - Optional map of image profiles (from constants.js)
47
+ * @returns {boolean|{valid: boolean, errors: string[], parsedEnv: Record<string,string>}}
41
48
  */
42
- export function validateEnvVars(envInput) {
43
- if (!envInput || !envInput.trim()) return true; // Empty is valid
49
+ export function validateEnvVars(envInput, imageName, imageProfiles) {
50
+ var contextual = imageName !== undefined && imageProfiles !== undefined;
44
51
 
45
- var vars = envInput.split(",").map(function (v) {
46
- return v.trim();
47
- }).filter(Boolean);
48
- var invalid = vars.find(function (v) {
49
- var parts = v.split("=");
50
- // Must have at least VAR=value format
51
- if (parts.length < 2) return true;
52
- var varName = parts[0].trim();
53
- // Variable names should be alphanumeric with underscores
54
- if (!/^[A-Z_][A-Z0-9_]*$/i.test(varName)) return true;
55
- return false;
56
- });
57
- return !invalid;
52
+ // Parse: split on FIRST '=' only
53
+ var parsedEnv = {};
54
+ var syntaxErrors = [];
55
+ if (envInput && envInput.trim()) {
56
+ var vars = envInput.split(',').map(function (v) {
57
+ return v.trim();
58
+ }).filter(Boolean);
59
+ var _iterator = _createForOfIteratorHelper(vars),
60
+ _step;
61
+ try {
62
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
63
+ var v = _step.value;
64
+ var eqIdx = v.indexOf('=');
65
+ if (eqIdx === -1) {
66
+ syntaxErrors.push("\"".concat(v, "\" is missing an '=' sign"));
67
+ continue;
68
+ }
69
+ var varName = v.slice(0, eqIdx).trim();
70
+ var varValue = v.slice(eqIdx + 1);
71
+ if (!/^[A-Z_][A-Z0-9_]*$/i.test(varName)) {
72
+ syntaxErrors.push("\"".concat(varName, "\" is not a valid variable name"));
73
+ continue;
74
+ }
75
+ parsedEnv[varName] = varValue;
76
+ }
77
+ } catch (err) {
78
+ _iterator.e(err);
79
+ } finally {
80
+ _iterator.f();
81
+ }
82
+ }
83
+ if (!contextual) {
84
+ // Legacy: return boolean
85
+ return syntaxErrors.length === 0;
86
+ }
87
+
88
+ // Contextual: also check required env vars from the profile
89
+ var errors = [].concat(syntaxErrors);
90
+ var baseName = normalizeImageName(imageName);
91
+ var profile = imageProfiles[baseName];
92
+ if (profile && profile.requiredEnv && profile.requiredEnv.length) {
93
+ var _iterator2 = _createForOfIteratorHelper(profile.requiredEnv),
94
+ _step2;
95
+ try {
96
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
97
+ var required = _step2.value;
98
+ if (!parsedEnv[required] || parsedEnv[required].trim() === '') {
99
+ errors.push("Missing required env var: ".concat(required));
100
+ }
101
+ }
102
+ } catch (err) {
103
+ _iterator2.e(err);
104
+ } finally {
105
+ _iterator2.f();
106
+ }
107
+ }
108
+ return {
109
+ valid: errors.length === 0,
110
+ errors: errors,
111
+ parsedEnv: parsedEnv
112
+ };
58
113
  }
@@ -8,9 +8,9 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
8
8
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
9
9
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
10
10
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
11
- import { useState } from "react";
12
- import { safeCall } from "../../helpers/safeCall.js";
13
- import { startContainer as svcStartContainer, stopContainer as svcStopContainer, restartContainer as svcRestartContainer, removeContainer as svcRemoveContainer } from "../../helpers/dockerService/serviceComponents/containerActions.js";
11
+ import { useState } from 'react';
12
+ import { safeCall } from '../../helpers/safeCall.js';
13
+ import { startContainer as svcStartContainer, stopContainer as svcStopContainer, restartContainer as svcRestartContainer, removeContainer as svcRemoveContainer } from '../../helpers/dockerService/serviceComponents/containerActions.js';
14
14
 
15
15
  /**
16
16
  * Custom hook to manage container actions (start, stop, restart, remove).
@@ -24,11 +24,11 @@ import { startContainer as svcStartContainer, stopContainer as svcStopContainer,
24
24
  export function useContainerActions(_ref) {
25
25
  var containers = _ref.containers,
26
26
  onAction = _ref.onAction;
27
- var _useState = useState(""),
27
+ var _useState = useState(''),
28
28
  _useState2 = _slicedToArray(_useState, 2),
29
29
  message = _useState2[0],
30
30
  setMessage = _useState2[1];
31
- var _useState3 = useState("yellow"),
31
+ var _useState3 = useState('yellow'),
32
32
  _useState4 = _slicedToArray(_useState3, 2),
33
33
  messageColor = _useState4[0],
34
34
  setMessageColor = _useState4[1];
@@ -67,17 +67,17 @@ export function useContainerActions(_ref) {
67
67
  break;
68
68
  }
69
69
  setMessage(checkMsg);
70
- setMessageColor("yellow");
70
+ setMessageColor('yellow');
71
71
  return _context5.a(2);
72
72
  case 2:
73
73
  setMessage("".concat(actionLabel, " container..."));
74
- setMessageColor("yellow");
74
+ setMessageColor('yellow');
75
75
  _context5.p = 3;
76
76
  _context5.n = 4;
77
77
  return actionFn(container.id);
78
78
  case 4:
79
79
  setMessage("".concat(actionLabel, " container successful."));
80
- setMessageColor("green");
80
+ setMessageColor('green');
81
81
  safeCall(onAction);
82
82
  _context5.n = 6;
83
83
  break;
@@ -85,7 +85,7 @@ export function useContainerActions(_ref) {
85
85
  _context5.p = 5;
86
86
  _t = _context5.v;
87
87
  setMessage("Failed to ".concat(actionLabel.toLowerCase(), " container: ").concat(_t.message));
88
- setMessageColor("red");
88
+ setMessageColor('red');
89
89
  case 6:
90
90
  return _context5.a(2);
91
91
  }
@@ -4,9 +4,11 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
4
4
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
5
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
6
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
- import { useState } from "react";
8
- import { validatePorts } from "../../helpers/validationHelpers.js";
9
- import { safeCall } from "../../helpers/safeCall.js";
7
+ import { useState } from 'react';
8
+ import { validatePorts, validateEnvVars } from '../../helpers/validationHelpers.js';
9
+ import { IMAGE_PROFILES } from '../../helpers/constants.js';
10
+ import { safeCall } from '../../helpers/safeCall.js';
11
+ var MAX_VISIBLE = 6;
10
12
 
11
13
  /**
12
14
  * Custom hook to manage the container creation flow, step by step.
@@ -15,85 +17,173 @@ import { safeCall } from "../../helpers/safeCall.js";
15
17
  * @param {Object} params
16
18
  * @param {Function} params.onCreate - Callback when creation is confirmed
17
19
  * @param {Function} params.onCancel - Callback when creation is cancelled
18
- * @param {Array<string>} params.dbImages - List of DB image names for env var warning
20
+ * @param {Array<string>} params.dbImages - List of DB image names for env var warning (legacy)
21
+ * @param {Object} [params.imageProfiles] - Image profiles map for contextual validation
19
22
  * @returns {Object} Creation state, setters, and helpers
20
23
  */
21
24
  export function useContainerCreation(_ref) {
22
25
  var onCreate = _ref.onCreate,
23
26
  onCancel = _ref.onCancel,
24
27
  _ref$dbImages = _ref.dbImages,
25
- dbImages = _ref$dbImages === void 0 ? [] : _ref$dbImages;
28
+ dbImages = _ref$dbImages === void 0 ? [] : _ref$dbImages,
29
+ _ref$imageProfiles = _ref.imageProfiles,
30
+ imageProfiles = _ref$imageProfiles === void 0 ? IMAGE_PROFILES : _ref$imageProfiles;
26
31
  var _useState = useState(0),
27
32
  _useState2 = _slicedToArray(_useState, 2),
28
33
  step = _useState2[0],
29
34
  setStep = _useState2[1]; // 0: image, 1: name, 2: ports, 3: env
30
- var _useState3 = useState(""),
35
+ var _useState3 = useState(''),
31
36
  _useState4 = _slicedToArray(_useState3, 2),
32
37
  imageName = _useState4[0],
33
38
  setImageName = _useState4[1];
34
- var _useState5 = useState(""),
39
+ var _useState5 = useState(''),
35
40
  _useState6 = _slicedToArray(_useState5, 2),
36
41
  containerName = _useState6[0],
37
42
  setContainerName = _useState6[1];
38
- var _useState7 = useState(""),
43
+ var _useState7 = useState(''),
39
44
  _useState8 = _slicedToArray(_useState7, 2),
40
45
  portInput = _useState8[0],
41
46
  setPortInput = _useState8[1];
42
- var _useState9 = useState(""),
47
+ var _useState9 = useState(''),
43
48
  _useState0 = _slicedToArray(_useState9, 2),
44
49
  envInput = _useState0[0],
45
50
  setEnvInput = _useState0[1];
46
- var _useState1 = useState(""),
51
+ var _useState1 = useState(''),
47
52
  _useState10 = _slicedToArray(_useState1, 2),
48
53
  message = _useState10[0],
49
54
  setMessage = _useState10[1];
50
- var _useState11 = useState("yellow"),
55
+ var _useState11 = useState('yellow'),
51
56
  _useState12 = _slicedToArray(_useState11, 2),
52
57
  messageColor = _useState12[0],
53
58
  setMessageColor = _useState12[1];
54
59
 
60
+ // Autocomplete state
61
+ var _useState13 = useState([]),
62
+ _useState14 = _slicedToArray(_useState13, 2),
63
+ suggestions = _useState14[0],
64
+ setSuggestions = _useState14[1];
65
+ var _useState15 = useState(-1),
66
+ _useState16 = _slicedToArray(_useState15, 2),
67
+ selectedSuggestionIndex = _useState16[0],
68
+ setSelectedSuggestionIndex = _useState16[1];
69
+ var _useState17 = useState(0),
70
+ _useState18 = _slicedToArray(_useState17, 2),
71
+ visibleOffset = _useState18[0],
72
+ setVisibleOffset = _useState18[1];
73
+
74
+ /**
75
+ * Updates the image name input and recalculates autocomplete suggestions.
76
+ * Resets selectedSuggestionIndex to -1 on every keystroke.
77
+ *
78
+ * @param {string} value - New raw image name typed by user
79
+ */
80
+ function updateImageInput(value) {
81
+ setImageName(value);
82
+ setSelectedSuggestionIndex(-1);
83
+ setVisibleOffset(0);
84
+ if (!value.trim()) {
85
+ setSuggestions([]);
86
+ return;
87
+ }
88
+ var lower = value.toLowerCase();
89
+ var matches = Object.keys(imageProfiles).filter(function (key) {
90
+ return key.includes(lower);
91
+ });
92
+ setSuggestions(matches);
93
+ }
94
+
95
+ /**
96
+ * Moves the suggestion selection up (-1) or down (+1).
97
+ * Clamps index to [-1, suggestions.length - 1].
98
+ * Adjusts visibleOffset to keep selected item in the visible window.
99
+ *
100
+ * @param {number} direction - -1 (up) or 1 (down)
101
+ */
102
+ function moveSuggestionSelection(direction) {
103
+ setSelectedSuggestionIndex(function (prev) {
104
+ var next = Math.max(-1, Math.min(suggestions.length - 1, prev + direction));
105
+ // Adjust visibleOffset to keep selected in view
106
+ setVisibleOffset(function (offset) {
107
+ if (next < offset) return Math.max(0, next);
108
+ if (next >= offset + MAX_VISIBLE) return next - MAX_VISIBLE + 1;
109
+ return offset;
110
+ });
111
+ return next;
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Applies the currently focused suggestion to imageName.
117
+ * Does NOT advance the step. Clears suggestions after applying.
118
+ */
119
+ function applyFocusedSuggestion() {
120
+ if (selectedSuggestionIndex < 0 || selectedSuggestionIndex >= suggestions.length) return;
121
+ var chosen = suggestions[selectedSuggestionIndex];
122
+ setImageName(chosen);
123
+ setSuggestions([]);
124
+ setSelectedSuggestionIndex(-1);
125
+ setVisibleOffset(0);
126
+ }
127
+
55
128
  /**
56
129
  * Advances to the next step, with validation and feedback.
57
130
  */
58
131
  function nextStep() {
59
132
  if (step === 0) {
60
133
  if (!imageName.trim()) {
61
- setMessage("Image name cannot be empty.");
62
- setMessageColor("red");
134
+ setMessage('Image name cannot be empty.');
135
+ setMessageColor('red');
63
136
  return;
64
137
  }
65
138
  setStep(1);
66
- setMessage("Optional: Enter container name or leave empty and press Enter");
67
- setMessageColor("yellow");
139
+ setMessage('Optional: Enter container name or leave empty and press Enter');
140
+ setMessageColor('yellow');
68
141
  return;
69
142
  }
70
143
  if (step === 1) {
71
144
  setStep(2);
72
- setMessage("Optional: Enter ports (format 8080:80,443:443) or leave empty and press Enter");
73
- setMessageColor("yellow");
145
+ setMessage('Optional: Enter ports (format 8080:80,443:443) or leave empty and press Enter');
146
+ setMessageColor('yellow');
74
147
  return;
75
148
  }
76
149
  if (step === 2) {
77
150
  // Ports are now optional - only validate if provided
78
151
  if (portInput.trim() && !validatePorts(portInput)) {
79
- setMessage("Port format must be host:container and both must be numbers (e.g. 8080:80)");
80
- setMessageColor("red");
152
+ setMessage('Port format must be host:container and both must be numbers (e.g. 8080:80)');
153
+ setMessageColor('red');
81
154
  return;
82
155
  }
83
156
  setStep(3);
84
157
  var isDb = dbImages.some(function (db) {
85
158
  return imageName.trim().toLowerCase().includes(db);
86
159
  });
87
- if (isDb) {
88
- setMessage("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.");
89
- setMessageColor("yellow");
160
+ // Check if the image has a profile with required env vars
161
+ var baseName = imageName.trim().toLowerCase().split(':')[0].split('/').pop();
162
+ var profile = imageProfiles[baseName];
163
+ if (profile && profile.requiredEnv && profile.requiredEnv.length) {
164
+ var suggestedPart = profile.suggestedEnv && profile.suggestedEnv.length ? " | Suggested: ".concat(profile.suggestedEnv.join(', ')) : '';
165
+ setMessage("Required env vars for ".concat(baseName, ": ").concat(profile.requiredEnv.join(', '), ". Enter as VAR=val,VAR2=val2").concat(suggestedPart));
166
+ setMessageColor('yellow');
167
+ } else if (profile && profile.suggestedEnv && profile.suggestedEnv.length) {
168
+ setMessage("Suggested env vars for ".concat(baseName, ": ").concat(profile.suggestedEnv.join(', '), ". Enter as VAR=val,VAR2=val2 or leave empty."));
169
+ setMessageColor('yellow');
170
+ } else if (isDb) {
171
+ setMessage('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.');
172
+ setMessageColor('yellow');
90
173
  } else {
91
- setMessage("Optional: Enter environment variables (format VAR1=val1,VAR2=val2) or leave empty and press Enter");
92
- setMessageColor("yellow");
174
+ setMessage('Optional: Enter environment variables (format VAR1=val1,VAR2=val2) or leave empty and press Enter');
175
+ setMessageColor('yellow');
93
176
  }
94
177
  return;
95
178
  }
96
179
  if (step === 3) {
180
+ // Contextual env validation using image profiles
181
+ var result = validateEnvVars(envInput, imageName, imageProfiles);
182
+ if (!result.valid) {
183
+ setMessage(result.errors.join(' | '));
184
+ setMessageColor('red');
185
+ return;
186
+ }
97
187
  // Final step: call onCreate with all data (safely)
98
188
  safeCall(onCreate, {
99
189
  imageName: imageName,
@@ -109,12 +199,15 @@ export function useContainerCreation(_ref) {
109
199
  */
110
200
  function cancelCreation() {
111
201
  setStep(0);
112
- setImageName("");
113
- setContainerName("");
114
- setPortInput("");
115
- setEnvInput("");
116
- setMessage("");
117
- setMessageColor("yellow");
202
+ setImageName('');
203
+ setContainerName('');
204
+ setPortInput('');
205
+ setEnvInput('');
206
+ setMessage('');
207
+ setMessageColor('yellow');
208
+ setSuggestions([]);
209
+ setSelectedSuggestionIndex(-1);
210
+ setVisibleOffset(0);
118
211
  safeCall(onCancel);
119
212
  }
120
213
 
@@ -124,12 +217,15 @@ export function useContainerCreation(_ref) {
124
217
  */
125
218
  function resetCreation() {
126
219
  setStep(0);
127
- setImageName("");
128
- setContainerName("");
129
- setPortInput("");
130
- setEnvInput("");
131
- setMessage("Insert the name of the image to create: ");
132
- setMessageColor("yellow");
220
+ setImageName('');
221
+ setContainerName('');
222
+ setPortInput('');
223
+ setEnvInput('');
224
+ setMessage('Insert the name of the image to create: ');
225
+ setMessageColor('yellow');
226
+ setSuggestions([]);
227
+ setSelectedSuggestionIndex(-1);
228
+ setVisibleOffset(0);
133
229
  }
134
230
  return {
135
231
  step: step,
@@ -146,6 +242,12 @@ export function useContainerCreation(_ref) {
146
242
  setMessage: setMessage,
147
243
  messageColor: messageColor,
148
244
  setMessageColor: setMessageColor,
245
+ suggestions: suggestions,
246
+ selectedSuggestionIndex: selectedSuggestionIndex,
247
+ visibleOffset: visibleOffset,
248
+ updateImageInput: updateImageInput,
249
+ moveSuggestionSelection: moveSuggestionSelection,
250
+ applyFocusedSuggestion: applyFocusedSuggestion,
149
251
  nextStep: nextStep,
150
252
  cancelCreation: cancelCreation,
151
253
  resetCreation: resetCreation
@@ -4,8 +4,8 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
4
4
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
5
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
6
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
- import { useState, useRef } from "react";
8
- import { safeCall } from "../../helpers/safeCall.js";
7
+ import { useState, useRef } from 'react';
8
+ import { safeCall } from '../../helpers/safeCall.js';
9
9
 
10
10
  /**
11
11
  * Custom hook to manage the logs viewer state and stream reference.
@@ -23,15 +23,15 @@ export function useDebugLogs() {
23
23
  var unsubscribe = logger.subscribe(function (entry) {
24
24
  setDebugLogs(function (prev) {
25
25
  var extras = (entry.args || []).map(function (arg) {
26
- if (typeof arg === "string") return arg;
27
- if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
26
+ if (typeof arg === 'string') return arg;
27
+ if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg);
28
28
  try {
29
29
  return JSON.stringify(arg);
30
30
  } catch (err) {
31
- return "[unserializable]";
31
+ return '[unserializable]';
32
32
  }
33
33
  }).filter(Boolean);
34
- var line = extras.length ? "".concat(entry.formatted, " ").concat(extras.join(" ")) : entry.formatted;
34
+ var line = extras.length ? "".concat(entry.formatted, " ").concat(extras.join(' ')) : entry.formatted;
35
35
  var next = [].concat(_toConsumableArray(prev), [line]);
36
36
  return next.slice(-200);
37
37
  });
@@ -2,7 +2,7 @@ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present,
2
2
  function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
3
3
  function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
4
4
  function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
5
- import { useCallback } from "react";
5
+ import { useCallback } from 'react';
6
6
 
7
7
  /**
8
8
  * Hook responsible solely for routing Docker action keystrokes to the correct handler.
@@ -41,7 +41,7 @@ export function useContainerCommandRouter(_ref) {
41
41
  onStartCreate = _ref.onStartCreate;
42
42
  var handleDockerCommands = useCallback(function (input) {
43
43
  var container = containers[selected];
44
- if (input === "i") {
44
+ if (input === 'i') {
45
45
  actions.handleAction({
46
46
  actionFn: function () {
47
47
  var _actionFn = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(id) {
@@ -60,15 +60,15 @@ export function useContainerCommandRouter(_ref) {
60
60
  }
61
61
  return actionFn;
62
62
  }(),
63
- actionLabel: "Starting",
63
+ actionLabel: 'Starting',
64
64
  selected: selected,
65
65
  stateCheck: function stateCheck(c) {
66
- return (c.state === "running" || c.status === "running") && "Container is already running.";
66
+ return (c.state === 'running' || c.status === 'running') && 'Container is already running.';
67
67
  }
68
68
  });
69
69
  return true;
70
70
  }
71
- if (input === "p") {
71
+ if (input === 'p') {
72
72
  actions.handleAction({
73
73
  actionFn: function () {
74
74
  var _actionFn2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(id) {
@@ -87,15 +87,15 @@ export function useContainerCommandRouter(_ref) {
87
87
  }
88
88
  return actionFn;
89
89
  }(),
90
- actionLabel: "Stopping",
90
+ actionLabel: 'Stopping',
91
91
  selected: selected,
92
92
  stateCheck: function stateCheck(c) {
93
- return (c.state === "exited" || c.status === "exited" || c.state === "stopped" || c.status === "stopped") && "Container is already stopped.";
93
+ return (c.state === 'exited' || c.status === 'exited' || c.state === 'stopped' || c.status === 'stopped') && 'Container is already stopped.';
94
94
  }
95
95
  });
96
96
  return true;
97
97
  }
98
- if (input === "r") {
98
+ if (input === 'r') {
99
99
  actions.handleAction({
100
100
  actionFn: function () {
101
101
  var _actionFn3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(id) {
@@ -114,19 +114,19 @@ export function useContainerCommandRouter(_ref) {
114
114
  }
115
115
  return actionFn;
116
116
  }(),
117
- actionLabel: "Restarting",
117
+ actionLabel: 'Restarting',
118
118
  selected: selected
119
119
  });
120
120
  return true;
121
121
  }
122
- if (input === "e") {
122
+ if (input === 'e') {
123
123
  if (!container) {
124
124
  return true;
125
125
  }
126
126
  onStartErase();
127
127
  return true;
128
128
  }
129
- if (input === "l") {
129
+ if (input === 'l') {
130
130
  if (!container) {
131
131
  return true;
132
132
  }
@@ -134,12 +134,12 @@ export function useContainerCommandRouter(_ref) {
134
134
  startLogsStream(container.id);
135
135
  return true;
136
136
  }
137
- if (input === "c") {
137
+ if (input === 'c') {
138
138
  onStartCreate();
139
139
  creation.resetCreation();
140
140
  return true;
141
141
  }
142
- if (input === "d") {
142
+ if (input === 'd') {
143
143
  onToggleDebug();
144
144
  return true;
145
145
  }
@@ -18,9 +18,9 @@ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
18
18
  * // Use in a component
19
19
  * const { containers } = useContainers();
20
20
  */
21
- import React, { useState, useEffect } from "react";
22
- import { getContainers } from "../helpers/dockerService/serviceComponents/containerList.js";
23
- import { REFRESH_INTERVALS } from "../helpers/constants.js";
21
+ import React, { useState, useEffect } from 'react';
22
+ import { getContainers } from '../helpers/dockerService/serviceComponents/containerList.js';
23
+ import { REFRESH_INTERVALS } from '../helpers/constants.js';
24
24
  export function useContainers() {
25
25
  var _useState = useState([]),
26
26
  _useState2 = _slicedToArray(_useState, 2),