cdd-cli 3.2.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.~lock.ROADMAP.md# +1 -0
  2. package/CHANGELOG.md +51 -0
  3. package/README.es.md +198 -0
  4. package/README.md +81 -23
  5. package/ROADMAP.md +647 -0
  6. package/coverage/clover.xml +400 -519
  7. package/coverage/coverage-final.json +16 -20
  8. package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +400 -0
  9. package/coverage/lcov-report/ControlsHUD.jsx.html +256 -0
  10. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +31 -13
  11. package/coverage/lcov-report/components/PromptField.jsx.html +4 -4
  12. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +37 -13
  13. package/coverage/lcov-report/components/index.html +14 -14
  14. package/coverage/lcov-report/helpers/constants.js.html +87 -33
  15. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +40 -40
  16. package/coverage/lcov-report/helpers/dockerHubService.js.html +310 -0
  17. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +1 -1
  18. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +1 -1
  19. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +1 -1
  20. package/coverage/lcov-report/helpers/imageNameUtils.js.html +18 -18
  21. package/coverage/lcov-report/helpers/index.html +77 -62
  22. package/coverage/lcov-report/helpers/logger.js.html +60 -60
  23. package/coverage/lcov-report/helpers/safeCall.js.html +14 -14
  24. package/coverage/lcov-report/helpers/validationHelpers.js.html +52 -52
  25. package/coverage/lcov-report/hooks/creation/index.html +19 -19
  26. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +4 -4
  27. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +225 -48
  28. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +5 -5
  29. package/coverage/lcov-report/hooks/debug/index.html +1 -1
  30. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +7 -7
  31. package/coverage/lcov-report/hooks/index.html +15 -15
  32. package/coverage/lcov-report/hooks/navigation/index.html +1 -1
  33. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +6 -6
  34. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +13 -13
  35. package/coverage/lcov-report/hooks/useControls.js.html +82 -40
  36. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +5 -5
  37. package/coverage/lcov-report/hooks/useExitHandler.js.html +6 -6
  38. package/coverage/lcov-report/index.html +38 -68
  39. package/coverage/lcov.info +742 -981
  40. package/dist/App.js +7 -1
  41. package/dist/components/ContainerCreationPrompt.js +26 -10
  42. package/dist/components/ControlsHUD.js +106 -0
  43. package/dist/components/SuggestionPanel.js +16 -7
  44. package/dist/helpers/constants.js +60 -21
  45. package/dist/helpers/dockerHubService.js +111 -0
  46. package/dist/helpers/logger.js +1 -1
  47. package/dist/hooks/creation/useContainerActions.js +22 -5
  48. package/dist/hooks/creation/useContainerCreation.js +395 -107
  49. package/dist/hooks/useContainerCommandRouter.js +1 -1
  50. package/dist/hooks/useControls.js +23 -7
  51. package/dist/index.js +7 -0
  52. package/package.json +1 -1
  53. package/src/App.jsx +6 -0
  54. package/src/components/ContainerCreationPrompt.jsx +15 -5
  55. package/src/components/ControlsHUD.jsx +66 -0
  56. package/src/components/SuggestionPanel.jsx +10 -2
  57. package/src/helpers/constants.js +38 -20
  58. package/src/helpers/dockerHubService.js +75 -0
  59. package/src/helpers/logger.js +1 -2
  60. package/src/hooks/creation/useContainerActions.js +19 -6
  61. package/src/hooks/creation/useContainerCreation.js +224 -87
  62. package/src/hooks/useContainerCommandRouter.js +1 -1
  63. package/src/hooks/useControls.js +24 -8
  64. package/src/index.js +4 -0
  65. package/test/ContainerCreationPrompt.dom.test.js +245 -0
  66. package/test/ControlsHUD.dom.test.js +125 -0
  67. package/test/SuggestionPanel.dom.test.js +30 -0
  68. package/test/constants.test.js +7 -0
  69. package/test/dockerHubService.test.js +213 -0
  70. package/test/logger.test.js +58 -0
  71. package/test/resolveImageTag.test.js +26 -0
  72. package/test/useContainerCreation.dom.test.js +306 -3
  73. package/test/useContainerCreationHub.dom.test.js +184 -0
  74. package/test/useControls.dom.test.js +212 -6
package/dist/App.js CHANGED
@@ -32,7 +32,13 @@ export default function App() {
32
32
  portInput: controls.portInput,
33
33
  envInput: controls.envInput,
34
34
  message: controls.message,
35
- messageColor: controls.messageColor
35
+ messageColor: controls.messageColor,
36
+ suggestions: controls.creation.suggestions,
37
+ selectedSuggestionIndex: controls.creation.selectedSuggestionIndex,
38
+ visibleOffset: controls.creation.visibleOffset,
39
+ isSearchingHub: controls.creation.isSearchingHub,
40
+ hubResults: controls.creation.hubResults,
41
+ hasSuggestedEnv: controls.creation.hasSuggestedEnv
36
42
  });
37
43
  }
38
44
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Box, {
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
- import { Box, Text } from 'ink';
2
+ import { Box } from 'ink';
3
3
  import { PromptField, PromptMessage } from './PromptField.js';
4
4
  import { SuggestionPanel } from './SuggestionPanel.js';
5
+ import { ControlsHUD } from './ControlsHUD.js';
5
6
  import PropTypes from 'prop-types';
6
7
 
7
8
  /**
@@ -35,9 +36,15 @@ export default function ContainerCreationPrompt(props) {
35
36
  _props$selectedSugges = props.selectedSuggestionIndex,
36
37
  selectedSuggestionIndex = _props$selectedSugges === void 0 ? -1 : _props$selectedSugges,
37
38
  _props$visibleOffset = props.visibleOffset,
38
- visibleOffset = _props$visibleOffset === void 0 ? 0 : _props$visibleOffset;
39
+ visibleOffset = _props$visibleOffset === void 0 ? 0 : _props$visibleOffset,
40
+ _props$hubResults = props.hubResults,
41
+ hubResults = _props$hubResults === void 0 ? null : _props$hubResults,
42
+ _props$isSearchingHub = props.isSearchingHub,
43
+ isSearchingHub = _props$isSearchingHub === void 0 ? false : _props$isSearchingHub,
44
+ _props$hasSuggestedEn = props.hasSuggestedEnv,
45
+ hasSuggestedEnv = _props$hasSuggestedEn === void 0 ? false : _props$hasSuggestedEn;
39
46
  var prompts = [{
40
- label: 'Name of the image to create (e.g., nginx:latest):',
47
+ label: 'Name of the image to create (e.g., nginx:1.27-alpine):',
41
48
  value: imageName,
42
49
  required: true
43
50
  }, {
@@ -57,7 +64,9 @@ export default function ContainerCreationPrompt(props) {
57
64
  label = _ref.label,
58
65
  value = _ref.value,
59
66
  required = _ref.required;
60
- var showSuggestions = step === 0 && suggestions.length > 0;
67
+ var activeItems = hubResults !== null && hubResults !== void 0 ? hubResults : suggestions;
68
+ var showSuggestions = step === 0 && (isSearchingHub || activeItems.length > 0);
69
+ var hasSuggestions = (suggestions === null || suggestions === void 0 ? void 0 : suggestions.length) > 0 || (hubResults === null || hubResults === void 0 ? void 0 : hubResults.length) > 0;
61
70
  return /*#__PURE__*/React.createElement(Box, {
62
71
  flexDirection: "column",
63
72
  borderStyle: "round",
@@ -68,15 +77,19 @@ export default function ContainerCreationPrompt(props) {
68
77
  value: value,
69
78
  required: required
70
79
  }), showSuggestions && /*#__PURE__*/React.createElement(SuggestionPanel, {
71
- items: suggestions,
80
+ items: activeItems,
72
81
  selectedIndex: selectedSuggestionIndex,
73
- visibleOffset: visibleOffset
82
+ visibleOffset: visibleOffset,
83
+ isLoading: isSearchingHub
74
84
  }), /*#__PURE__*/React.createElement(PromptMessage, {
75
85
  message: message,
76
86
  color: messageColor
77
- }), /*#__PURE__*/React.createElement(Text, {
78
- dimColor: true
79
- }, "Press Enter to continue, Escape to cancel"));
87
+ }), /*#__PURE__*/React.createElement(ControlsHUD, {
88
+ step: step,
89
+ hasSuggestions: hasSuggestions,
90
+ isSearchingHub: isSearchingHub,
91
+ hasSuggestedEnv: hasSuggestedEnv
92
+ }));
80
93
  }
81
94
  ContainerCreationPrompt.propTypes = {
82
95
  step: PropTypes.number.isRequired,
@@ -88,7 +101,10 @@ ContainerCreationPrompt.propTypes = {
88
101
  messageColor: PropTypes.string,
89
102
  suggestions: PropTypes.arrayOf(PropTypes.string),
90
103
  selectedSuggestionIndex: PropTypes.number,
91
- visibleOffset: PropTypes.number
104
+ visibleOffset: PropTypes.number,
105
+ hubResults: PropTypes.arrayOf(PropTypes.string),
106
+ isSearchingHub: PropTypes.bool,
107
+ hasSuggestedEnv: PropTypes.bool
92
108
  };
93
109
 
94
110
  // Named export for test compatibility with jest ESM interop
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+
4
+ /**
5
+ * Returns the contextual keyboard hints for the current wizard state.
6
+ *
7
+ * @param {number} step - Current wizard step (0-3)
8
+ * @param {boolean} hasSuggestions - Whether suggestions/hub results are visible
9
+ * @param {boolean} isSearchingHub - Whether a Hub search is in progress
10
+ * @param {boolean} [hasSuggestedEnv=false] - Whether the image has suggestedEnv entries
11
+ * @returns {{ key: string, label: string }[]}
12
+ */
13
+ function getHints(step, hasSuggestions, isSearchingHub) {
14
+ var hasSuggestedEnv = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
15
+ if (step === 0) {
16
+ if (hasSuggestions) {
17
+ return [{
18
+ key: '↑↓',
19
+ label: 'Navigate'
20
+ }, {
21
+ key: 'Enter',
22
+ label: 'Select'
23
+ }, {
24
+ key: 'Esc',
25
+ label: 'Cancel'
26
+ }];
27
+ }
28
+ var hints = [];
29
+ if (!isSearchingHub) hints.push({
30
+ key: 'Tab',
31
+ label: 'Search Hub'
32
+ });
33
+ hints.push({
34
+ key: '↑↓',
35
+ label: 'Browse'
36
+ });
37
+ hints.push({
38
+ key: 'Enter',
39
+ label: 'Confirm'
40
+ });
41
+ hints.push({
42
+ key: 'Esc',
43
+ label: 'Cancel'
44
+ });
45
+ return hints;
46
+ }
47
+ if (step === 3) {
48
+ var _hints = [];
49
+ if (hasSuggestedEnv) _hints.push({
50
+ key: 'Tab',
51
+ label: 'Insert next env'
52
+ });
53
+ _hints.push({
54
+ key: 'Enter',
55
+ label: 'Continue'
56
+ });
57
+ _hints.push({
58
+ key: 'Esc',
59
+ label: 'Cancel'
60
+ });
61
+ return _hints;
62
+ }
63
+ return [{
64
+ key: 'Enter',
65
+ label: 'Continue'
66
+ }, {
67
+ key: 'Esc',
68
+ label: 'Cancel'
69
+ }];
70
+ }
71
+
72
+ /**
73
+ * Displays a row of contextual keyboard shortcut hints for the active wizard step.
74
+ *
75
+ * @param {Object} props
76
+ * @param {number} props.step - Current wizard step
77
+ * @param {boolean} [props.hasSuggestions=false] - Whether suggestions/hub results are visible
78
+ * @param {boolean} [props.isSearchingHub=false] - Whether a Hub search is in progress
79
+ * @param {boolean} [props.hasSuggestedEnv=false] - Whether the image has suggestedEnv entries
80
+ */
81
+ function ControlsHUD(_ref) {
82
+ var step = _ref.step,
83
+ _ref$hasSuggestions = _ref.hasSuggestions,
84
+ hasSuggestions = _ref$hasSuggestions === void 0 ? false : _ref$hasSuggestions,
85
+ _ref$isSearchingHub = _ref.isSearchingHub,
86
+ isSearchingHub = _ref$isSearchingHub === void 0 ? false : _ref$isSearchingHub,
87
+ _ref$hasSuggestedEnv = _ref.hasSuggestedEnv,
88
+ hasSuggestedEnv = _ref$hasSuggestedEnv === void 0 ? false : _ref$hasSuggestedEnv;
89
+ var hints = getHints(step, hasSuggestions, isSearchingHub, hasSuggestedEnv);
90
+ return /*#__PURE__*/React.createElement(Box, {
91
+ flexDirection: "row",
92
+ columnGap: 2
93
+ }, hints.map(function (_ref2) {
94
+ var key = _ref2.key,
95
+ label = _ref2.label;
96
+ return /*#__PURE__*/React.createElement(Box, {
97
+ key: key
98
+ }, /*#__PURE__*/React.createElement(Text, {
99
+ color: "cyan"
100
+ }, "[", key, "]"), /*#__PURE__*/React.createElement(Text, {
101
+ dimColor: true
102
+ }, " ", label));
103
+ }));
104
+ }
105
+ export default ControlsHUD;
106
+ export { getHints, ControlsHUD };
@@ -19,26 +19,35 @@ export function SuggestionPanel(_ref) {
19
19
  selectedIndex = _ref.selectedIndex,
20
20
  visibleOffset = _ref.visibleOffset,
21
21
  _ref$maxVisible = _ref.maxVisible,
22
- maxVisible = _ref$maxVisible === void 0 ? 6 : _ref$maxVisible;
23
- if (!items || items.length === 0) return null;
24
- var visible = items.slice(visibleOffset, visibleOffset + maxVisible);
22
+ maxVisible = _ref$maxVisible === void 0 ? 6 : _ref$maxVisible,
23
+ _ref$isLoading = _ref.isLoading,
24
+ isLoading = _ref$isLoading === void 0 ? false : _ref$isLoading;
25
+ var hasItems = items && items.length > 0;
26
+ if (!hasItems && !isLoading) return null;
27
+ var visible = hasItems ? items.slice(visibleOffset, visibleOffset + maxVisible) : [];
25
28
  return /*#__PURE__*/React.createElement(Box, {
26
29
  flexDirection: "column",
27
30
  borderStyle: "single",
28
31
  borderColor: "cyan",
29
- paddingX: 1
32
+ paddingX: 1,
33
+ "data-testid": "suggestion-panel"
30
34
  }, visible.map(function (item, localIdx) {
31
35
  var globalIdx = visibleOffset + localIdx;
32
36
  var isFocused = globalIdx === selectedIndex;
33
37
  return /*#__PURE__*/React.createElement(Text, {
34
38
  key: item,
35
- color: isFocused ? 'cyan' : undefined
39
+ color: isFocused ? 'cyan' : undefined,
40
+ dimColor: !isFocused
36
41
  }, isFocused ? "\u203A ".concat(item) : " ".concat(item));
37
- }));
42
+ }), isLoading && /*#__PURE__*/React.createElement(Text, {
43
+ dimColor: true,
44
+ color: "gray"
45
+ }, ' [searching Docker Hub...]'));
38
46
  }
39
47
  SuggestionPanel.propTypes = {
40
48
  items: PropTypes.arrayOf(PropTypes.string).isRequired,
41
49
  selectedIndex: PropTypes.number.isRequired,
42
50
  visibleOffset: PropTypes.number.isRequired,
43
- maxVisible: PropTypes.number
51
+ maxVisible: PropTypes.number,
52
+ isLoading: PropTypes.bool
44
53
  };
@@ -44,101 +44,140 @@ export var IMAGE_PROFILES = {
44
44
  nginx: {
45
45
  requiredEnv: [],
46
46
  defaultPort: '80',
47
- suggestedEnv: []
47
+ suggestedEnv: [],
48
+ defaultTag: '1.27-alpine'
48
49
  },
49
50
  httpd: {
50
51
  requiredEnv: [],
51
52
  defaultPort: '80',
52
- suggestedEnv: []
53
+ suggestedEnv: [],
54
+ defaultTag: '2.4-alpine'
53
55
  },
54
56
  caddy: {
55
57
  requiredEnv: [],
56
58
  defaultPort: '80',
57
- suggestedEnv: []
59
+ suggestedEnv: [],
60
+ defaultTag: '2-alpine'
58
61
  },
59
62
  node: {
60
63
  requiredEnv: [],
61
64
  defaultPort: '3000',
62
- suggestedEnv: ['NODE_ENV=production']
65
+ suggestedEnv: ['NODE_ENV=production'],
66
+ defaultTag: '22-alpine'
63
67
  },
64
68
  python: {
65
69
  requiredEnv: [],
66
70
  defaultPort: '8000',
67
- suggestedEnv: ['PYTHONUNBUFFERED=1']
71
+ suggestedEnv: ['PYTHONUNBUFFERED=1'],
72
+ defaultTag: '3.12-slim'
68
73
  },
69
74
  openjdk: {
70
75
  requiredEnv: [],
71
76
  defaultPort: '8080',
72
- suggestedEnv: ['JAVA_OPTS=-Xms256m -Xmx512m']
77
+ suggestedEnv: ['JAVA_OPTS=-Xms256m -Xmx512m'],
78
+ defaultTag: '21-slim'
73
79
  },
74
80
  golang: {
75
81
  requiredEnv: [],
76
82
  defaultPort: '8080',
77
- suggestedEnv: ['GOMAXPROCS=2']
83
+ suggestedEnv: ['GOMAXPROCS=2'],
84
+ defaultTag: '1.22-alpine'
78
85
  },
79
86
  redis: {
80
87
  requiredEnv: [],
81
88
  defaultPort: '6379',
82
- suggestedEnv: ['REDIS_PASSWORD=change-me']
89
+ suggestedEnv: ['REDIS_PASSWORD=change-me'],
90
+ defaultTag: '7-alpine'
83
91
  },
84
92
  memcached: {
85
93
  requiredEnv: [],
86
94
  defaultPort: '11211',
87
- suggestedEnv: []
95
+ suggestedEnv: [],
96
+ defaultTag: '1.6-alpine'
88
97
  },
89
98
  postgres: {
90
99
  requiredEnv: ['POSTGRES_PASSWORD'],
91
100
  defaultPort: '5432',
92
- suggestedEnv: ['POSTGRES_USER=postgres', 'POSTGRES_DB=app']
101
+ suggestedEnv: ['POSTGRES_PASSWORD=secret', 'POSTGRES_USER=postgres', 'POSTGRES_DB=app'],
102
+ defaultTag: '17-alpine'
93
103
  },
94
104
  mysql: {
95
105
  requiredEnv: ['MYSQL_ROOT_PASSWORD'],
96
106
  defaultPort: '3306',
97
- suggestedEnv: ['MYSQL_DATABASE=app', 'MYSQL_USER=app', 'MYSQL_PASSWORD=app123']
107
+ suggestedEnv: ['MYSQL_ROOT_PASSWORD=secret', 'MYSQL_DATABASE=app', 'MYSQL_USER=app', 'MYSQL_PASSWORD=app123'],
108
+ defaultTag: '8.0'
98
109
  },
99
110
  mariadb: {
100
111
  requiredEnv: ['MARIADB_ROOT_PASSWORD'],
101
112
  defaultPort: '3306',
102
- suggestedEnv: ['MARIADB_DATABASE=app', 'MARIADB_USER=app', 'MARIADB_PASSWORD=app123']
113
+ suggestedEnv: ['MARIADB_ROOT_PASSWORD=secret', 'MARIADB_DATABASE=app', 'MARIADB_USER=app', 'MARIADB_PASSWORD=app123'],
114
+ defaultTag: '11-alpine'
103
115
  },
104
116
  mongo: {
105
117
  requiredEnv: [],
106
118
  defaultPort: '27017',
107
- suggestedEnv: ['MONGO_INITDB_ROOT_USERNAME=admin', 'MONGO_INITDB_ROOT_PASSWORD=secret']
119
+ suggestedEnv: ['MONGO_INITDB_ROOT_USERNAME=admin', 'MONGO_INITDB_ROOT_PASSWORD=secret'],
120
+ defaultTag: '7.0'
108
121
  },
109
122
  mssql: {
110
123
  requiredEnv: ['ACCEPT_EULA', 'SA_PASSWORD'],
111
124
  defaultPort: '1433',
112
- suggestedEnv: ['ACCEPT_EULA=Y', 'MSSQL_PID=Developer']
125
+ suggestedEnv: ['ACCEPT_EULA=Y', 'SA_PASSWORD=Strong!Passw0rd', 'MSSQL_PID=Developer'],
126
+ defaultTag: '2022-latest'
113
127
  },
114
128
  rabbitmq: {
115
129
  requiredEnv: [],
116
130
  defaultPort: '5672',
117
- suggestedEnv: ['RABBITMQ_DEFAULT_USER=guest', 'RABBITMQ_DEFAULT_PASS=guest']
131
+ suggestedEnv: ['RABBITMQ_DEFAULT_USER=guest', 'RABBITMQ_DEFAULT_PASS=guest'],
132
+ defaultTag: '3-management-alpine'
118
133
  },
119
134
  kafka: {
120
135
  requiredEnv: [],
121
136
  defaultPort: '9092',
122
- suggestedEnv: ['KAFKA_CFG_LISTENERS=PLAINTEXT://:9092']
137
+ suggestedEnv: ['KAFKA_CFG_LISTENERS=PLAINTEXT://:9092', 'KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092'],
138
+ defaultTag: '3.7'
123
139
  },
124
140
  zookeeper: {
125
141
  requiredEnv: [],
126
142
  defaultPort: '2181',
127
- suggestedEnv: ['ALLOW_ANONYMOUS_LOGIN=yes']
143
+ suggestedEnv: ['ALLOW_ANONYMOUS_LOGIN=yes'],
144
+ defaultTag: '3.9'
128
145
  },
129
146
  elasticsearch: {
130
147
  requiredEnv: [],
131
148
  defaultPort: '9200',
132
- suggestedEnv: ['discovery.type=single-node', 'xpack.security.enabled=false']
149
+ suggestedEnv: ['discovery.type=single-node', 'xpack.security.enabled=false'],
150
+ defaultTag: '8.13.0'
133
151
  },
134
152
  minio: {
135
153
  requiredEnv: ['MINIO_ROOT_USER', 'MINIO_ROOT_PASSWORD'],
136
154
  defaultPort: '9000',
137
- suggestedEnv: ['MINIO_CONSOLE_ADDRESS=:9001']
155
+ suggestedEnv: ['MINIO_ROOT_USER=admin', 'MINIO_ROOT_PASSWORD=secret', 'MINIO_CONSOLE_ADDRESS=:9001'],
156
+ defaultTag: 'latest'
138
157
  },
139
158
  wordpress: {
140
159
  requiredEnv: ['WORDPRESS_DB_HOST', 'WORDPRESS_DB_USER', 'WORDPRESS_DB_PASSWORD', 'WORDPRESS_DB_NAME'],
141
160
  defaultPort: '8080',
142
- suggestedEnv: ['WORDPRESS_TABLE_PREFIX=wp_']
161
+ suggestedEnv: ['WORDPRESS_DB_HOST=db', 'WORDPRESS_DB_USER=wp', 'WORDPRESS_DB_PASSWORD=secret', 'WORDPRESS_DB_NAME=wordpress', 'WORDPRESS_TABLE_PREFIX=wp_'],
162
+ defaultTag: '6.5-apache'
143
163
  }
144
- };
164
+ };
165
+
166
+ /**
167
+ * Given an image name (with or without tag), returns the image name with a
168
+ * default tag appended when the image matches a known profile and has no
169
+ * explicit tag. Returns the input unchanged otherwise.
170
+ *
171
+ * @param {string} imageName - e.g. 'postgres', 'postgres:15', 'unknownimage'
172
+ * @param {Record<string, { defaultTag: string }>} profiles
173
+ * @returns {string}
174
+ */
175
+ export function resolveImageTag(imageName) {
176
+ var profiles = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : IMAGE_PROFILES;
177
+ if (!imageName) return imageName;
178
+ if (imageName.includes(':')) return imageName;
179
+ var baseName = imageName.trim().toLowerCase().split('/').pop();
180
+ var profile = profiles[baseName];
181
+ if (!(profile !== null && profile !== void 0 && profile.defaultTag)) return imageName;
182
+ return "".concat(imageName, ":").concat(profile.defaultTag);
183
+ }
@@ -0,0 +1,111 @@
1
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
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
+ 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
+ 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
+ /**
6
+ * Docker Hub search helper.
7
+ * Pure function — no side effects beyond HTTP. All state management
8
+ * lives in the calling hook (useContainerCreation).
9
+ */
10
+
11
+ var DOCKER_HUB_SEARCH_URL = 'https://hub.docker.com/v2/search/repositories/';
12
+
13
+ /**
14
+ * Format a Docker Hub search result object into a display string.
15
+ *
16
+ * Format rules:
17
+ * - Official: "name [★ official] [Npulls+]" (if pullCount > 0)
18
+ * - Non-official with pulls: "name [Npulls+]"
19
+ * - Otherwise: "name"
20
+ *
21
+ * Pull count suffixes: ≥1B → "1B+", ≥1M → "1M+", ≥1K → "1K+", else raw number
22
+ *
23
+ * @param {{ name: string, isOfficial: boolean, pullCount: number }} result
24
+ * @returns {string}
25
+ */
26
+ export function formatHubResult(result) {
27
+ var name = result.name,
28
+ isOfficial = result.isOfficial,
29
+ pullCount = result.pullCount;
30
+ var pullSuffix = '';
31
+ if (pullCount >= 1e9) {
32
+ pullSuffix = " [1B+ pulls]";
33
+ } else if (pullCount >= 1e6) {
34
+ pullSuffix = " [".concat(Math.floor(pullCount / 1e6), "M+ pulls]");
35
+ } else if (pullCount >= 1e3) {
36
+ pullSuffix = " [".concat(Math.floor(pullCount / 1e3), "K+ pulls]");
37
+ } else if (pullCount > 0) {
38
+ pullSuffix = " [".concat(pullCount, " pulls]");
39
+ }
40
+ if (isOfficial) {
41
+ return "".concat(name, " [\u2605 official]").concat(pullSuffix);
42
+ }
43
+ return "".concat(name).concat(pullSuffix);
44
+ }
45
+
46
+ /**
47
+ * Search Docker Hub for images matching the given query.
48
+ *
49
+ * @param {string} query - Search term (will be trimmed and encoded)
50
+ * @param {Object} [options]
51
+ * @param {AbortSignal} [options.signal] - Abort signal; defaults to AbortSignal.timeout(5000)
52
+ * @returns {Promise<Array<{ name: string, isOfficial: boolean, pullCount: number, description: string }>>}
53
+ * @throws {Error} If response.ok is false or the network call rejects
54
+ */
55
+ export function searchDockerHub(_x) {
56
+ return _searchDockerHub.apply(this, arguments);
57
+ }
58
+ function _searchDockerHub() {
59
+ _searchDockerHub = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(query) {
60
+ var _options$signal, _data$results;
61
+ var options,
62
+ q,
63
+ url,
64
+ signal,
65
+ response,
66
+ data,
67
+ results,
68
+ _args = arguments;
69
+ return _regenerator().w(function (_context) {
70
+ while (1) switch (_context.n) {
71
+ case 0:
72
+ options = _args.length > 1 && _args[1] !== undefined ? _args[1] : {};
73
+ q = query.trim();
74
+ if (q) {
75
+ _context.n = 1;
76
+ break;
77
+ }
78
+ return _context.a(2, []);
79
+ case 1:
80
+ url = "".concat(DOCKER_HUB_SEARCH_URL, "?query=").concat(encodeURIComponent(q), "&page_size=10");
81
+ signal = (_options$signal = options.signal) !== null && _options$signal !== void 0 ? _options$signal : AbortSignal.timeout(5000);
82
+ _context.n = 2;
83
+ return fetch(url, {
84
+ signal: signal
85
+ });
86
+ case 2:
87
+ response = _context.v;
88
+ if (response.ok) {
89
+ _context.n = 3;
90
+ break;
91
+ }
92
+ throw new Error("Docker Hub search failed: ".concat(response.status));
93
+ case 3:
94
+ _context.n = 4;
95
+ return response.json();
96
+ case 4:
97
+ data = _context.v;
98
+ results = (_data$results = data.results) !== null && _data$results !== void 0 ? _data$results : [];
99
+ return _context.a(2, results.map(function (item) {
100
+ return {
101
+ name: item.repo_name,
102
+ isOfficial: item.is_official,
103
+ pullCount: item.pull_count,
104
+ description: item.short_description
105
+ };
106
+ }));
107
+ }
108
+ }, _callee);
109
+ }));
110
+ return _searchDockerHub.apply(this, arguments);
111
+ }
@@ -58,7 +58,7 @@ function log(level, message) {
58
58
  timestamp: timestamp,
59
59
  formatted: formatted
60
60
  };
61
- var shouldWriteToConsole = listeners.size === 0 || level === 'error' || level === 'warn';
61
+ var shouldWriteToConsole = listeners.size === 0;
62
62
  if (shouldWriteToConsole) {
63
63
  var method = level === 'debug' ? 'log' : level;
64
64
  if (typeof console[method] === 'function') {
@@ -8,7 +8,7 @@ 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';
11
+ import { useState, useRef, useEffect } from 'react';
12
12
  import { safeCall } from '../../helpers/safeCall.js';
13
13
  import { startContainer as svcStartContainer, stopContainer as svcStopContainer, restartContainer as svcRestartContainer, removeContainer as svcRemoveContainer } from '../../helpers/dockerService/serviceComponents/containerActions.js';
14
14
 
@@ -32,6 +32,24 @@ export function useContainerActions(_ref) {
32
32
  _useState4 = _slicedToArray(_useState3, 2),
33
33
  messageColor = _useState4[0],
34
34
  setMessageColor = _useState4[1];
35
+ var messageTimerRef = useRef(null);
36
+ function setTimedMessage(msg) {
37
+ var color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'yellow';
38
+ var ms = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 4000;
39
+ clearTimeout(messageTimerRef.current);
40
+ setMessage(msg);
41
+ if (color !== undefined) setMessageColor(color);
42
+ if (msg) {
43
+ messageTimerRef.current = setTimeout(function () {
44
+ return setMessage('');
45
+ }, ms);
46
+ }
47
+ }
48
+ useEffect(function () {
49
+ return function () {
50
+ return clearTimeout(messageTimerRef.current);
51
+ };
52
+ }, []);
35
53
 
36
54
  /**
37
55
  * Handles a generic container action and sets feedback.
@@ -76,16 +94,14 @@ export function useContainerActions(_ref) {
76
94
  _context5.n = 4;
77
95
  return actionFn(container.id);
78
96
  case 4:
79
- setMessage("".concat(actionLabel, " container successful."));
80
- setMessageColor('green');
97
+ setTimedMessage("".concat(actionLabel, " container successful."), 'green');
81
98
  safeCall(onAction);
82
99
  _context5.n = 6;
83
100
  break;
84
101
  case 5:
85
102
  _context5.p = 5;
86
103
  _t = _context5.v;
87
- setMessage("Failed to ".concat(actionLabel.toLowerCase(), " container: ").concat(_t.message));
88
- setMessageColor('red');
104
+ setTimedMessage("Failed to ".concat(actionLabel.toLowerCase(), " container: ").concat(_t.message), 'red');
89
105
  case 6:
90
106
  return _context5.a(2);
91
107
  }
@@ -96,6 +112,7 @@ export function useContainerActions(_ref) {
96
112
  return {
97
113
  message: message,
98
114
  setMessage: setMessage,
115
+ setTimedMessage: setTimedMessage,
99
116
  messageColor: messageColor,
100
117
  setMessageColor: setMessageColor,
101
118
  handleAction: handleAction,