cdd-cli 3.2.0 → 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 (136) 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/.~lock.ROADMAP.md# +1 -0
  7. package/CHANGELOG.md +80 -0
  8. package/README.es.md +198 -0
  9. package/README.md +81 -23
  10. package/ROADMAP.md +647 -0
  11. package/SECURITY.md +5 -0
  12. package/__mocks__/ink.cjs +17 -0
  13. package/coverage/clover.xml +471 -500
  14. package/coverage/coverage-final.json +16 -16
  15. package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +400 -0
  16. package/coverage/lcov-report/ControlsHUD.jsx.html +256 -0
  17. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +394 -0
  18. package/coverage/lcov-report/components/PromptField.jsx.html +229 -0
  19. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +268 -0
  20. package/coverage/lcov-report/components/index.html +146 -0
  21. package/coverage/lcov-report/helpers/constants.js.html +139 -22
  22. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +75 -42
  23. package/coverage/lcov-report/helpers/dockerHubService.js.html +310 -0
  24. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +162 -84
  25. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +50 -32
  26. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +21 -21
  27. package/coverage/lcov-report/helpers/imageNameUtils.js.html +181 -0
  28. package/coverage/lcov-report/helpers/index.html +84 -54
  29. package/coverage/lcov-report/helpers/logger.js.html +68 -68
  30. package/coverage/lcov-report/helpers/safeCall.js.html +15 -15
  31. package/coverage/lcov-report/helpers/validationHelpers.js.html +66 -114
  32. package/coverage/lcov-report/hooks/creation/index.html +20 -20
  33. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +38 -17
  34. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +593 -89
  35. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +7 -7
  36. package/coverage/lcov-report/hooks/debug/index.html +5 -5
  37. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +35 -20
  38. package/coverage/lcov-report/hooks/index.html +40 -40
  39. package/coverage/lcov-report/hooks/navigation/index.html +19 -19
  40. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +38 -29
  41. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +119 -80
  42. package/coverage/lcov-report/hooks/useControls.js.html +306 -123
  43. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +8 -8
  44. package/coverage/lcov-report/hooks/useExitHandler.js.html +50 -41
  45. package/coverage/lcov-report/index.html +52 -67
  46. package/coverage/lcov.info +899 -952
  47. package/dist/App.js +18 -12
  48. package/dist/components/ContainerCreationPrompt.js +50 -13
  49. package/dist/components/ContainerList.js +3 -3
  50. package/dist/components/ContainerRow.js +20 -20
  51. package/dist/components/ContainerSection.js +3 -3
  52. package/dist/components/ControlsHUD.js +106 -0
  53. package/dist/components/Footer.js +2 -2
  54. package/dist/components/Header.js +7 -7
  55. package/dist/components/LogViewer.js +3 -3
  56. package/dist/components/MessageFeedback.js +2 -2
  57. package/dist/components/PromptField.js +4 -4
  58. package/dist/components/StatsBar.js +5 -5
  59. package/dist/components/SuggestionPanel.js +53 -0
  60. package/dist/components/UsageMenu.js +3 -3
  61. package/dist/helpers/appInfo.js +7 -7
  62. package/dist/helpers/constants.js +154 -1
  63. package/dist/helpers/containerOptionsBuilder.js +3 -3
  64. package/dist/helpers/dockerHubService.js +111 -0
  65. package/dist/helpers/dockerService/dockerService.js +1 -1
  66. package/dist/helpers/dockerService/serviceComponents/containerActions.js +99 -40
  67. package/dist/helpers/dockerService/serviceComponents/containerList.js +11 -11
  68. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +9 -9
  69. package/dist/helpers/dockerService/serviceComponents/containerStats.js +5 -5
  70. package/dist/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
  71. package/dist/helpers/exitWithMessage.js +10 -10
  72. package/dist/helpers/imageNameUtils.js +32 -0
  73. package/dist/helpers/logger.js +13 -13
  74. package/dist/helpers/safeCall.js +2 -2
  75. package/dist/helpers/validationHelpers.js +76 -21
  76. package/dist/hooks/creation/useContainerActions.js +28 -11
  77. package/dist/hooks/creation/useContainerCreation.js +452 -62
  78. package/dist/hooks/creation/useLogsViewer.js +2 -2
  79. package/dist/hooks/debug/useDebugLogs.js +4 -4
  80. package/dist/hooks/useContainerCommandRouter.js +14 -14
  81. package/dist/hooks/useContainers.js +3 -3
  82. package/dist/hooks/useControls.js +85 -38
  83. package/dist/hooks/useEraseConfirmation.js +3 -3
  84. package/dist/index.js +9 -2
  85. package/jest.config.cjs +7 -0
  86. package/package.json +7 -3
  87. package/src/App.jsx +39 -16
  88. package/src/components/ContainerCreationPrompt.jsx +54 -10
  89. package/src/components/ContainerList.jsx +3 -3
  90. package/src/components/ContainerRow.jsx +19 -16
  91. package/src/components/ContainerSection.jsx +3 -3
  92. package/src/components/ControlsHUD.jsx +66 -0
  93. package/src/components/Footer.jsx +2 -2
  94. package/src/components/Header.jsx +7 -7
  95. package/src/components/LogViewer.jsx +5 -3
  96. package/src/components/MessageFeedback.jsx +2 -2
  97. package/src/components/PromptField.jsx +4 -4
  98. package/src/components/StatsBar.jsx +11 -7
  99. package/src/components/SuggestionPanel.jsx +61 -0
  100. package/src/components/UsageMenu.jsx +18 -13
  101. package/src/helpers/appInfo.js +7 -7
  102. package/src/helpers/constants.js +48 -9
  103. package/src/helpers/containerOptionsBuilder.js +16 -5
  104. package/src/helpers/dockerHubService.js +75 -0
  105. package/src/helpers/dockerService/dockerService.js +1 -1
  106. package/src/helpers/dockerService/serviceComponents/containerActions.js +87 -61
  107. package/src/helpers/dockerService/serviceComponents/containerList.js +31 -25
  108. package/src/helpers/dockerService/serviceComponents/containerLogs.js +41 -26
  109. package/src/helpers/dockerService/serviceComponents/containerStats.js +13 -11
  110. package/src/helpers/dockerService/serviceComponents/imageUtils.js +5 -4
  111. package/src/helpers/exitWithMessage.js +13 -7
  112. package/src/helpers/imageNameUtils.js +32 -0
  113. package/src/helpers/logger.js +15 -16
  114. package/src/helpers/safeCall.js +2 -2
  115. package/src/helpers/validationHelpers.js +13 -29
  116. package/src/hooks/creation/useContainerActions.js +32 -12
  117. package/src/hooks/creation/useContainerCreation.js +293 -47
  118. package/src/hooks/creation/useLogsViewer.js +2 -2
  119. package/src/hooks/debug/useDebugLogs.js +15 -10
  120. package/src/hooks/navigation/useContainerSelection.js +17 -14
  121. package/src/hooks/useContainerCommandRouter.js +71 -58
  122. package/src/hooks/useContainers.js +4 -4
  123. package/src/hooks/useControls.js +135 -72
  124. package/src/hooks/useEraseConfirmation.js +3 -3
  125. package/src/hooks/useExitHandler.js +29 -26
  126. package/src/index.js +8 -4
  127. package/test/ContainerCreationPrompt.dom.test.js +313 -0
  128. package/test/ControlsHUD.dom.test.js +125 -0
  129. package/test/SuggestionPanel.dom.test.js +83 -0
  130. package/test/constants.test.js +41 -0
  131. package/test/dockerHubService.test.js +213 -0
  132. package/test/logger.test.js +58 -0
  133. package/test/resolveImageTag.test.js +26 -0
  134. package/test/useContainerCreation.dom.test.js +428 -2
  135. package/test/useContainerCreationHub.dom.test.js +184 -0
  136. package/test/useControls.dom.test.js +406 -4
@@ -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") {
138
- onStartCreate();
137
+ if (input === 'c') {
139
138
  creation.resetCreation();
139
+ onStartCreate();
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),
@@ -12,19 +12,20 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
12
12
  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; }
13
13
  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; } }
14
14
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
15
- import React from "react";
16
- import { useInput } from "ink";
17
- import { useContainerActions } from "./creation/useContainerActions.js";
18
- import { useContainerCreation } from "./creation/useContainerCreation.js";
19
- import { useLogsViewer } from "./creation/useLogsViewer.js";
20
- import { useContainerSelection } from "./navigation/useContainerSelection.js";
21
- import { useDebugLogs } from "./debug/useDebugLogs.js";
22
- import { useEraseConfirmation } from "./useEraseConfirmation.js";
23
- import { useExitHandler } from "./useExitHandler.js";
24
- import { useContainerCommandRouter } from "./useContainerCommandRouter.js";
25
- import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs.js";
26
- import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";
27
- import { buildContainerOptions } from "../helpers/containerOptionsBuilder.js";
15
+ import React from 'react';
16
+ import { useInput } from 'ink';
17
+ import { useContainerActions } from './creation/useContainerActions.js';
18
+ import { useContainerCreation } from './creation/useContainerCreation.js';
19
+ import { useLogsViewer } from './creation/useLogsViewer.js';
20
+ import { useContainerSelection } from './navigation/useContainerSelection.js';
21
+ import { useDebugLogs } from './debug/useDebugLogs.js';
22
+ import { useEraseConfirmation } from './useEraseConfirmation.js';
23
+ import { useExitHandler } from './useExitHandler.js';
24
+ import { useContainerCommandRouter } from './useContainerCommandRouter.js';
25
+ import { getLogsStream } from '../helpers/dockerService/serviceComponents/containerLogs.js';
26
+ import { createContainer as svcCreateContainer } from '../helpers/dockerService/serviceComponents/containerActions.js';
27
+ import { buildContainerOptions } from '../helpers/containerOptionsBuilder.js';
28
+ import { DB_IMAGES } from '../helpers/constants.js';
28
29
 
29
30
  // Principal hook to manage user inputs and control the app state
30
31
  /**
@@ -35,7 +36,9 @@ import { buildContainerOptions } from "../helpers/containerOptionsBuilder.js";
35
36
  * @returns {Object} controls - API for the App component
36
37
  */
37
38
  export function useControls() {
39
+ var _overrides$triggerHub, _overrides$isSearchin;
38
40
  var containers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
41
+ var overrides = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
39
42
  var _React$useState = React.useState(false),
40
43
  _React$useState2 = _slicedToArray(_React$useState, 2),
41
44
  creatingContainer = _React$useState2[0],
@@ -48,7 +51,7 @@ export function useControls() {
48
51
  var creation = useContainerCreation({
49
52
  onCreate: function () {
50
53
  var _onCreate = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(_ref) {
51
- var imageName, containerName, portInput, envInput, options, id, _t;
54
+ var imageName, containerName, portInput, envInput, options, _yield$svcCreateConta, id, ports, portMsg, _t;
52
55
  return _regenerator().w(function (_context) {
53
56
  while (1) switch (_context.p = _context.n) {
54
57
  case 0:
@@ -59,22 +62,27 @@ export function useControls() {
59
62
  portInput: portInput,
60
63
  envInput: envInput
61
64
  });
62
- actions.setMessage("Creating container ".concat(imageName, "..."));
63
- actions.setMessageColor("yellow");
65
+ actions.setTimedMessage("Creating container ".concat(imageName, "..."), 'yellow');
64
66
  _context.p = 1;
65
67
  _context.n = 2;
66
68
  return svcCreateContainer(imageName, options);
67
69
  case 2:
68
- id = _context.v;
69
- actions.setMessage("Created container ".concat(id));
70
- actions.setMessageColor("green");
70
+ _yield$svcCreateConta = _context.v;
71
+ id = _yield$svcCreateConta.id;
72
+ ports = _yield$svcCreateConta.ports;
73
+ portMsg = '';
74
+ if (ports && ports.length) {
75
+ portMsg = ' | Ports: ' + ports.map(function (p) {
76
+ return "".concat(p.hostPort, "\u2192").concat(p.containerPort, "/").concat(p.protocol);
77
+ }).join(', ');
78
+ }
79
+ actions.setTimedMessage("Created container ".concat(id).concat(portMsg), 'green');
71
80
  _context.n = 4;
72
81
  break;
73
82
  case 3:
74
83
  _context.p = 3;
75
84
  _t = _context.v;
76
- actions.setMessage("Error creating container: ".concat(_t.message));
77
- actions.setMessageColor("red");
85
+ actions.setTimedMessage("Error creating container: ".concat(_t.message), 'red');
78
86
  case 4:
79
87
  _context.p = 4;
80
88
  setCreatingContainer(false);
@@ -92,11 +100,16 @@ export function useControls() {
92
100
  onCancel: function onCancel() {
93
101
  return setCreatingContainer(false);
94
102
  },
95
- dbImages: ["mysql", "mariadb", "postgres", "mongo", "mssql", "redis"]
103
+ dbImages: DB_IMAGES
96
104
  });
97
105
  var logsViewer = useLogsViewer();
98
106
  var selection = useContainerSelection(containers.length);
99
107
  var debugLogs = useDebugLogs();
108
+
109
+ // Allow overrides for testability (e.g. injecting a mock triggerHubSearch)
110
+ var triggerHubSearch = (_overrides$triggerHub = overrides.triggerHubSearch) !== null && _overrides$triggerHub !== void 0 ? _overrides$triggerHub : creation.triggerHubSearch;
111
+ var isSearchingHub = (_overrides$isSearchin = overrides.isSearchingHub) !== null && _overrides$isSearchin !== void 0 ? _overrides$isSearchin : creation.isSearchingHub;
112
+ var insertNextSuggestedEnv = overrides.insertNextSuggestedEnv !== undefined ? overrides.insertNextSuggestedEnv : creation.insertNextSuggestedEnv;
100
113
  var eraseConfirmation = useEraseConfirmation({
101
114
  onConfirm: function onConfirm() {
102
115
  actions.handleAction({
@@ -117,14 +130,14 @@ export function useControls() {
117
130
  }
118
131
  return actionFn;
119
132
  }(),
120
- actionLabel: "Erasing",
133
+ actionLabel: 'Erasing',
121
134
  selected: selection.selected
122
135
  });
123
- actions.setMessageColor("yellow");
136
+ actions.setMessageColor('yellow');
124
137
  },
125
138
  onCancel: function onCancel() {
126
- actions.setMessage("");
127
- actions.setMessageColor("");
139
+ actions.setMessage('');
140
+ actions.setMessageColor('');
128
141
  }
129
142
  });
130
143
 
@@ -135,7 +148,7 @@ export function useControls() {
135
148
  var startLogsStream = React.useCallback(function (containerId) {
136
149
  getLogsStream(containerId, function (data) {
137
150
  return logsViewer.setLogs(function (prev) {
138
- var newLogs = [].concat(_toConsumableArray(prev), _toConsumableArray(data.split("\n").filter(Boolean)));
151
+ var newLogs = [].concat(_toConsumableArray(prev), _toConsumableArray(data.split('\n').filter(Boolean)));
139
152
  return newLogs.slice(-1000);
140
153
  });
141
154
  }, function () {}, function (err) {
@@ -153,8 +166,8 @@ export function useControls() {
153
166
  startLogsStream: startLogsStream,
154
167
  onStartErase: function onStartErase() {
155
168
  eraseConfirmation.startErase();
156
- actions.setMessage("Are you sure you want to erase this container? (y/n)");
157
- actions.setMessageColor("yellow");
169
+ actions.setMessage('Are you sure you want to erase this container? (y/n)');
170
+ actions.setMessageColor('yellow');
158
171
  },
159
172
  onToggleDebug: function onToggleDebug() {
160
173
  return debugLogs.setShowDebugLogs(function (prev) {
@@ -167,8 +180,8 @@ export function useControls() {
167
180
  });
168
181
  var exitHandler = useExitHandler({
169
182
  onBeforeExit: function onBeforeExit() {
170
- actions.setMessage("Exiting...");
171
- actions.setMessageColor("yellow");
183
+ actions.setMessage('Exiting...');
184
+ actions.setMessageColor('yellow');
172
185
  logsViewer.closeLogs();
173
186
  debugLogs.setShowDebugLogs(false);
174
187
  }
@@ -181,35 +194,69 @@ export function useControls() {
181
194
  var step = creation.step;
182
195
  var removeLastChar = function removeLastChar(setter) {
183
196
  return setter(function (value) {
184
- return (value || "").slice(0, -1);
197
+ return (value || '').slice(0, -1);
185
198
  });
186
199
  };
187
200
  var appendChar = function appendChar(setter, current, ch) {
188
- return setter((current || "") + ch);
201
+ return setter((current || '') + ch);
189
202
  };
190
203
  if (key.escape) {
191
204
  creation.cancelCreation();
192
205
  setCreatingContainer(false);
193
206
  return;
194
207
  }
195
- if (input === "\r" || input === "\n") {
208
+ if (input === '\r' || input === '\n') {
209
+ // If on step 0 with a focused suggestion, apply it instead of advancing
210
+ if (step === 0 && creation.selectedSuggestionIndex >= 0) {
211
+ creation.applyFocusedSuggestion();
212
+ return;
213
+ }
196
214
  creation.nextStep();
197
215
  return;
198
216
  }
217
+
218
+ // Tab on step 0: trigger Docker Hub search (with guards)
219
+ if (key.tab) {
220
+ if (step === 0) {
221
+ if (!isSearchingHub && (creation.imageName || '').trim() !== '') {
222
+ triggerHubSearch();
223
+ }
224
+ } else if (step === 3) {
225
+ insertNextSuggestedEnv === null || insertNextSuggestedEnv === void 0 || insertNextSuggestedEnv();
226
+ }
227
+ return;
228
+ }
229
+
230
+ // Arrow keys on step 0: navigate suggestion list
231
+ if (step === 0 && creation.suggestions.length > 0) {
232
+ if (key.upArrow) {
233
+ creation.moveSuggestionSelection(-1);
234
+ return;
235
+ }
236
+ if (key.downArrow) {
237
+ creation.moveSuggestionSelection(1);
238
+ return;
239
+ }
240
+ }
199
241
  if (key.backspace || key["delete"]) {
200
- if (step === 0) removeLastChar(creation.setImageName);
242
+ if (step === 0) {
243
+ var newValue = (creation.imageName || '').slice(0, -1);
244
+ creation.updateImageInput(newValue);
245
+ }
201
246
  if (step === 1) removeLastChar(creation.setContainerName);
202
247
  if (step === 2) removeLastChar(creation.setPortInput);
203
248
  if (step === 3) removeLastChar(creation.setEnvInput);
204
249
  return;
205
250
  }
206
251
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
207
- if (step === 0) appendChar(creation.setImageName, creation.imageName, input);
252
+ if (step === 0) {
253
+ creation.updateImageInput((creation.imageName || '') + input);
254
+ }
208
255
  if (step === 1) appendChar(creation.setContainerName, creation.containerName, input);
209
256
  if (step === 2) appendChar(creation.setPortInput, creation.portInput, input);
210
257
  if (step === 3) appendChar(creation.setEnvInput, creation.envInput, input);
211
258
  }
212
- }, [creation, setCreatingContainer]);
259
+ }, [creation, setCreatingContainer, triggerHubSearch, isSearchingHub, insertNextSuggestedEnv]);
213
260
 
214
261
  // Single keyboard entry point
215
262
  useInput(function (input, key) {
@@ -218,7 +265,7 @@ export function useControls() {
218
265
  return;
219
266
  }
220
267
  if (logsViewer.showLogs) {
221
- if (input === "q" || key.escape) {
268
+ if (input === 'q' || key.escape) {
222
269
  logsViewer.closeLogs();
223
270
  }
224
271
  return;
@@ -4,7 +4,7 @@ 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, useCallback } from "react";
7
+ import { useState, useCallback } from 'react';
8
8
 
9
9
  /**
10
10
  * Hook to manage the erase confirmation dialog flow.
@@ -28,12 +28,12 @@ export function useEraseConfirmation(_ref) {
28
28
  setConfirmErase(true);
29
29
  }, []);
30
30
  var processEraseConfirmation = useCallback(function (input, key) {
31
- if (input === "y" || input === "Y") {
31
+ if (input === 'y' || input === 'Y') {
32
32
  onConfirm();
33
33
  setConfirmErase(false);
34
34
  return;
35
35
  }
36
- if (input === "n" || input === "N" || key.escape) {
36
+ if (input === 'n' || input === 'N' || key.escape) {
37
37
  setConfirmErase(false);
38
38
  onCancel();
39
39
  return;
package/dist/index.js CHANGED
@@ -7,8 +7,15 @@
7
7
  * // Run the CLI
8
8
  * node index.js
9
9
  */
10
- import React from "react";
11
- import { render } from "ink";
10
+ import React from 'react';
11
+ import { render } from 'ink';
12
12
  import App from './App.js';
13
+ import { logger } from './helpers/logger.js';
14
+ process.on('uncaughtException', function (err) {
15
+ return logger.error('Uncaught exception', err);
16
+ });
17
+ process.on('unhandledRejection', function (reason) {
18
+ return logger.error('Unhandled rejection', reason);
19
+ });
13
20
  console.clear();
14
21
  render(/*#__PURE__*/React.createElement(App, null));
package/jest.config.cjs CHANGED
@@ -3,5 +3,12 @@ module.exports = {
3
3
  setupFilesAfterEnv: ['./test/jest.setup.js'],
4
4
  transform: {
5
5
  '^.+\\.[tj]sx?$': 'babel-jest'
6
+ },
7
+ // Allow babel-jest to transform ESM-only packages used by components
8
+ transformIgnorePatterns: [
9
+ '/node_modules/(?!(ink|ink-testing-library|ansi-styles|chalk|cli-cursor|cli-spinners|is-unicode-supported|restore-cursor|signal-exit|slice-ansi|strip-ansi|wrap-ansi|yoga-layout-prebuilt)/)'
10
+ ],
11
+ moduleNameMapper: {
12
+ '^ink$': '<rootDir>/__mocks__/ink.cjs'
6
13
  }
7
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdd-cli",
3
- "version": "3.2.0",
3
+ "version": "4.0.0",
4
4
  "description": "CLI Docker Dashboard",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,7 +8,9 @@
8
8
  "build": "NODE_ENV=production babel src --out-dir dist --extensions .js,.jsx && node fix-imports.cjs",
9
9
  "changes": "npm run build && node dist/index.js",
10
10
  "lint": "eslint src --ext .js,.jsx",
11
- "docs": "jsdoc -r -d docs src"
11
+ "docs": "jsdoc -r -d docs src",
12
+ "format": "prettier --write \"src/**/*.{js,jsx}\"",
13
+ "format:check": "prettier --check \"src/**/*.{js,jsx}\""
12
14
  },
13
15
  "keywords": [],
14
16
  "author": "",
@@ -35,9 +37,11 @@
35
37
  "babel-jest": "^29.6.1",
36
38
  "babel-plugin-module-resolver": "^5.0.2",
37
39
  "eslint": "^8.50.0",
40
+ "eslint-config-prettier": "^10.1.8",
38
41
  "eslint-plugin-react": "^7.37.5",
39
42
  "jest": "^29.6.1",
40
43
  "jest-environment-jsdom": "^29.7.0",
41
- "jsdoc": "^4.0.5"
44
+ "jsdoc": "^4.0.5",
45
+ "prettier": "^3.8.3"
42
46
  }
43
47
  }
package/src/App.jsx CHANGED
@@ -9,17 +9,17 @@
9
9
  * // Render the app
10
10
  * <App />
11
11
  */
12
- import React from "react";
13
- import { Box, Text, Spacer } from "ink";
14
- import { useContainers } from "./hooks/useContainers.js";
15
- import { useControls } from "./hooks/useControls.js";
16
- import ContainerSection from "./components/ContainerSection.jsx";
17
- import MessageFeedback from "./components/MessageFeedback.jsx";
18
- import Header from "./components/Header.jsx";
19
- import LogViewer from "./components/LogViewer.jsx";
20
- import ContainerCreationPrompt from "./components/ContainerCreationPrompt.jsx";
21
- import UsageMenu from "./components/UsageMenu.jsx";
22
- import Footer from "./components/Footer.jsx";
12
+ import React from 'react';
13
+ import { Box, Text, Spacer } from 'ink';
14
+ import { useContainers } from './hooks/useContainers.js';
15
+ import { useControls } from './hooks/useControls.js';
16
+ import ContainerSection from './components/ContainerSection.jsx';
17
+ import MessageFeedback from './components/MessageFeedback.jsx';
18
+ import Header from './components/Header.jsx';
19
+ import LogViewer from './components/LogViewer.jsx';
20
+ import ContainerCreationPrompt from './components/ContainerCreationPrompt.jsx';
21
+ import UsageMenu from './components/UsageMenu.jsx';
22
+ import Footer from './components/Footer.jsx';
23
23
 
24
24
  export default function App() {
25
25
  const { containers } = useContainers();
@@ -35,6 +35,12 @@ export default function App() {
35
35
  envInput={controls.envInput}
36
36
  message={controls.message}
37
37
  messageColor={controls.messageColor}
38
+ suggestions={controls.creation.suggestions}
39
+ selectedSuggestionIndex={controls.creation.selectedSuggestionIndex}
40
+ visibleOffset={controls.creation.visibleOffset}
41
+ isSearchingHub={controls.creation.isSearchingHub}
42
+ hubResults={controls.creation.hubResults}
43
+ hasSuggestedEnv={controls.creation.hasSuggestedEnv}
38
44
  />
39
45
  );
40
46
  }
@@ -49,18 +55,35 @@ export default function App() {
49
55
  >
50
56
  <Header count={containers.length} />
51
57
  <Text> </Text>
52
- <ContainerSection containers={containers} selected={controls.selected} />
58
+ <ContainerSection
59
+ containers={containers}
60
+ selected={controls.selected}
61
+ />
53
62
  <Spacer />
54
- <MessageFeedback message={controls.message} color={controls.messageColor} />
63
+ <MessageFeedback
64
+ message={controls.message}
65
+ color={controls.messageColor}
66
+ />
55
67
  <UsageMenu />
56
68
  {controls.showDebugLogs && (
57
- <Box marginTop={1} flexDirection="column" borderStyle="round" borderColor="gray" padding={1}>
69
+ <Box
70
+ marginTop={1}
71
+ flexDirection="column"
72
+ borderStyle="round"
73
+ borderColor="gray"
74
+ padding={1}
75
+ >
58
76
  <Text color="cyan">Debug log — press D or ESC to close</Text>
59
77
  {controls.debugLogs.length === 0 ? (
60
- <Text dimColor>No debug entries yet. Run with CDD_LOG_LEVEL=debug for verbose output.</Text>
78
+ <Text dimColor>
79
+ No debug entries yet. Run with CDD_LOG_LEVEL=debug for verbose
80
+ output.
81
+ </Text>
61
82
  ) : (
62
83
  controls.debugLogs.slice(-15).map((line, idx) => (
63
- <Text key={idx} color="gray">{line}</Text>
84
+ <Text key={idx} color="gray">
85
+ {line}
86
+ </Text>
64
87
  ))
65
88
  )}
66
89
  </Box>
@@ -1,6 +1,8 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import { PromptField, PromptMessage } from "./PromptField.jsx";
1
+ import React from 'react';
2
+ import { Box } from 'ink';
3
+ import { PromptField, PromptMessage } from './PromptField.jsx';
4
+ import { SuggestionPanel } from './SuggestionPanel.jsx';
5
+ import { ControlsHUD } from './ControlsHUD.jsx';
4
6
  import PropTypes from 'prop-types';
5
7
 
6
8
  /**
@@ -16,38 +18,71 @@ import PropTypes from 'prop-types';
16
18
  * @param {string} props.envInput - Value for the environment variables field
17
19
  * @param {string} props.message - Contextual message to display (errors/hints)
18
20
  * @param {string} props.messageColor - Color for the contextual message
21
+ * @param {string[]} [props.suggestions] - Autocomplete suggestions (shown only on step 0)
22
+ * @param {number} [props.selectedSuggestionIndex] - Currently focused suggestion index
23
+ * @param {number} [props.visibleOffset] - First visible suggestion offset
19
24
  * @returns {JSX.Element}
20
25
  */
21
26
  export default function ContainerCreationPrompt(props) {
22
- const { step, imageName, containerName, portInput, envInput, message, messageColor } = props;
27
+ const {
28
+ step,
29
+ imageName,
30
+ containerName,
31
+ portInput,
32
+ envInput,
33
+ message,
34
+ messageColor,
35
+ suggestions = [],
36
+ selectedSuggestionIndex = -1,
37
+ visibleOffset = 0,
38
+ hubResults = null,
39
+ isSearchingHub = false,
40
+ hasSuggestedEnv = false,
41
+ } = props;
23
42
  const prompts = [
24
43
  {
25
- label: "Name of the image to create (e.g., nginx:latest):",
44
+ label: 'Name of the image to create (e.g., nginx:1.27-alpine):',
26
45
  value: imageName,
27
46
  required: true,
28
47
  },
29
48
  {
30
- label: "Name of the container (optional):",
49
+ label: 'Name of the container (optional):',
31
50
  value: containerName,
32
51
  required: false,
33
52
  },
34
53
  {
35
- label: "Ports (optional, format 8080:80,443:443):",
54
+ label: 'Ports (optional, format 8080:80,443:443):',
36
55
  value: portInput,
37
56
  required: false,
38
57
  },
39
58
  {
40
- label: "Environment variables (optional, format VAR1=val1,VAR2=val2):",
59
+ label: 'Environment variables (optional, format VAR1=val1,VAR2=val2):',
41
60
  value: envInput,
42
61
  required: false,
43
62
  },
44
63
  ];
45
64
  const { label, value, required } = prompts[step] || {};
65
+ const activeItems = hubResults ?? suggestions;
66
+ const showSuggestions = step === 0 && (isSearchingHub || activeItems.length > 0);
67
+ const hasSuggestions = (suggestions?.length > 0) || (hubResults?.length > 0);
46
68
  return (
47
- <Box flexDirection="column" borderStyle="round" borderColor="yellow" padding={1}>
69
+ <Box
70
+ flexDirection="column"
71
+ borderStyle="round"
72
+ borderColor="yellow"
73
+ padding={1}
74
+ >
48
75
  <PromptField label={label} value={value} required={required} />
76
+ {showSuggestions && (
77
+ <SuggestionPanel
78
+ items={activeItems}
79
+ selectedIndex={selectedSuggestionIndex}
80
+ visibleOffset={visibleOffset}
81
+ isLoading={isSearchingHub}
82
+ />
83
+ )}
49
84
  <PromptMessage message={message} color={messageColor} />
50
- <Text dimColor>Press Enter to continue, Escape to cancel</Text>
85
+ <ControlsHUD step={step} hasSuggestions={hasSuggestions} isSearchingHub={isSearchingHub} hasSuggestedEnv={hasSuggestedEnv} />
51
86
  </Box>
52
87
  );
53
88
  }
@@ -60,4 +95,13 @@ ContainerCreationPrompt.propTypes = {
60
95
  envInput: PropTypes.string,
61
96
  message: PropTypes.string,
62
97
  messageColor: PropTypes.string,
98
+ suggestions: PropTypes.arrayOf(PropTypes.string),
99
+ selectedSuggestionIndex: PropTypes.number,
100
+ visibleOffset: PropTypes.number,
101
+ hubResults: PropTypes.arrayOf(PropTypes.string),
102
+ isSearchingHub: PropTypes.bool,
103
+ hasSuggestedEnv: PropTypes.bool,
63
104
  };
105
+
106
+ // Named export for test compatibility with jest ESM interop
107
+ export { ContainerCreationPrompt };
@@ -9,10 +9,10 @@
9
9
  * // Render with containers
10
10
  * <ContainerList containers={containers} />
11
11
  */
12
- import React from "react";
12
+ import React from 'react';
13
13
  import PropTypes from 'prop-types';
14
- import { Box } from "ink";
15
- import ContainerRow from "./ContainerRow.jsx";
14
+ import { Box } from 'ink';
15
+ import ContainerRow from './ContainerRow.jsx';
16
16
 
17
17
  export default function ContainerList({ containers, selected }) {
18
18
  return (