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
@@ -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
  /**
@@ -48,7 +49,7 @@ export function useControls() {
48
49
  var creation = useContainerCreation({
49
50
  onCreate: function () {
50
51
  var _onCreate = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(_ref) {
51
- var imageName, containerName, portInput, envInput, options, id, _t;
52
+ var imageName, containerName, portInput, envInput, options, _yield$svcCreateConta, id, ports, portMsg, _t;
52
53
  return _regenerator().w(function (_context) {
53
54
  while (1) switch (_context.p = _context.n) {
54
55
  case 0:
@@ -60,21 +61,29 @@ export function useControls() {
60
61
  envInput: envInput
61
62
  });
62
63
  actions.setMessage("Creating container ".concat(imageName, "..."));
63
- actions.setMessageColor("yellow");
64
+ actions.setMessageColor('yellow');
64
65
  _context.p = 1;
65
66
  _context.n = 2;
66
67
  return svcCreateContainer(imageName, options);
67
68
  case 2:
68
- id = _context.v;
69
- actions.setMessage("Created container ".concat(id));
70
- actions.setMessageColor("green");
69
+ _yield$svcCreateConta = _context.v;
70
+ id = _yield$svcCreateConta.id;
71
+ ports = _yield$svcCreateConta.ports;
72
+ portMsg = '';
73
+ if (ports && ports.length) {
74
+ portMsg = ' | Ports: ' + ports.map(function (p) {
75
+ return "".concat(p.hostPort, "\u2192").concat(p.containerPort, "/").concat(p.protocol);
76
+ }).join(', ');
77
+ }
78
+ actions.setMessage("Created container ".concat(id).concat(portMsg));
79
+ actions.setMessageColor('green');
71
80
  _context.n = 4;
72
81
  break;
73
82
  case 3:
74
83
  _context.p = 3;
75
84
  _t = _context.v;
76
85
  actions.setMessage("Error creating container: ".concat(_t.message));
77
- actions.setMessageColor("red");
86
+ actions.setMessageColor('red');
78
87
  case 4:
79
88
  _context.p = 4;
80
89
  setCreatingContainer(false);
@@ -92,7 +101,7 @@ export function useControls() {
92
101
  onCancel: function onCancel() {
93
102
  return setCreatingContainer(false);
94
103
  },
95
- dbImages: ["mysql", "mariadb", "postgres", "mongo", "mssql", "redis"]
104
+ dbImages: DB_IMAGES
96
105
  });
97
106
  var logsViewer = useLogsViewer();
98
107
  var selection = useContainerSelection(containers.length);
@@ -117,14 +126,14 @@ export function useControls() {
117
126
  }
118
127
  return actionFn;
119
128
  }(),
120
- actionLabel: "Erasing",
129
+ actionLabel: 'Erasing',
121
130
  selected: selection.selected
122
131
  });
123
- actions.setMessageColor("yellow");
132
+ actions.setMessageColor('yellow');
124
133
  },
125
134
  onCancel: function onCancel() {
126
- actions.setMessage("");
127
- actions.setMessageColor("");
135
+ actions.setMessage('');
136
+ actions.setMessageColor('');
128
137
  }
129
138
  });
130
139
 
@@ -135,7 +144,7 @@ export function useControls() {
135
144
  var startLogsStream = React.useCallback(function (containerId) {
136
145
  getLogsStream(containerId, function (data) {
137
146
  return logsViewer.setLogs(function (prev) {
138
- var newLogs = [].concat(_toConsumableArray(prev), _toConsumableArray(data.split("\n").filter(Boolean)));
147
+ var newLogs = [].concat(_toConsumableArray(prev), _toConsumableArray(data.split('\n').filter(Boolean)));
139
148
  return newLogs.slice(-1000);
140
149
  });
141
150
  }, function () {}, function (err) {
@@ -153,8 +162,8 @@ export function useControls() {
153
162
  startLogsStream: startLogsStream,
154
163
  onStartErase: function onStartErase() {
155
164
  eraseConfirmation.startErase();
156
- actions.setMessage("Are you sure you want to erase this container? (y/n)");
157
- actions.setMessageColor("yellow");
165
+ actions.setMessage('Are you sure you want to erase this container? (y/n)');
166
+ actions.setMessageColor('yellow');
158
167
  },
159
168
  onToggleDebug: function onToggleDebug() {
160
169
  return debugLogs.setShowDebugLogs(function (prev) {
@@ -167,8 +176,8 @@ export function useControls() {
167
176
  });
168
177
  var exitHandler = useExitHandler({
169
178
  onBeforeExit: function onBeforeExit() {
170
- actions.setMessage("Exiting...");
171
- actions.setMessageColor("yellow");
179
+ actions.setMessage('Exiting...');
180
+ actions.setMessageColor('yellow');
172
181
  logsViewer.closeLogs();
173
182
  debugLogs.setShowDebugLogs(false);
174
183
  }
@@ -181,30 +190,52 @@ export function useControls() {
181
190
  var step = creation.step;
182
191
  var removeLastChar = function removeLastChar(setter) {
183
192
  return setter(function (value) {
184
- return (value || "").slice(0, -1);
193
+ return (value || '').slice(0, -1);
185
194
  });
186
195
  };
187
196
  var appendChar = function appendChar(setter, current, ch) {
188
- return setter((current || "") + ch);
197
+ return setter((current || '') + ch);
189
198
  };
190
199
  if (key.escape) {
191
200
  creation.cancelCreation();
192
201
  setCreatingContainer(false);
193
202
  return;
194
203
  }
195
- if (input === "\r" || input === "\n") {
204
+ if (input === '\r' || input === '\n') {
205
+ // If on step 0 with a focused suggestion, apply it instead of advancing
206
+ if (step === 0 && creation.selectedSuggestionIndex >= 0) {
207
+ creation.applyFocusedSuggestion();
208
+ return;
209
+ }
196
210
  creation.nextStep();
197
211
  return;
198
212
  }
213
+
214
+ // Arrow keys on step 0: navigate suggestion list
215
+ if (step === 0 && creation.suggestions.length > 0) {
216
+ if (key.upArrow) {
217
+ creation.moveSuggestionSelection(-1);
218
+ return;
219
+ }
220
+ if (key.downArrow) {
221
+ creation.moveSuggestionSelection(1);
222
+ return;
223
+ }
224
+ }
199
225
  if (key.backspace || key["delete"]) {
200
- if (step === 0) removeLastChar(creation.setImageName);
226
+ if (step === 0) {
227
+ var newValue = (creation.imageName || '').slice(0, -1);
228
+ creation.updateImageInput(newValue);
229
+ }
201
230
  if (step === 1) removeLastChar(creation.setContainerName);
202
231
  if (step === 2) removeLastChar(creation.setPortInput);
203
232
  if (step === 3) removeLastChar(creation.setEnvInput);
204
233
  return;
205
234
  }
206
235
  if (input && input.length === 1 && !key.ctrl && !key.meta) {
207
- if (step === 0) appendChar(creation.setImageName, creation.imageName, input);
236
+ if (step === 0) {
237
+ creation.updateImageInput((creation.imageName || '') + input);
238
+ }
208
239
  if (step === 1) appendChar(creation.setContainerName, creation.containerName, input);
209
240
  if (step === 2) appendChar(creation.setPortInput, creation.portInput, input);
210
241
  if (step === 3) appendChar(creation.setEnvInput, creation.envInput, input);
@@ -218,7 +249,7 @@ export function useControls() {
218
249
  return;
219
250
  }
220
251
  if (logsViewer.showLogs) {
221
- if (input === "q" || key.escape) {
252
+ if (input === 'q' || key.escape) {
222
253
  logsViewer.closeLogs();
223
254
  }
224
255
  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,8 @@
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
13
  console.clear();
14
14
  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": "3.2.2",
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();
@@ -49,18 +49,35 @@ export default function App() {
49
49
  >
50
50
  <Header count={containers.length} />
51
51
  <Text> </Text>
52
- <ContainerSection containers={containers} selected={controls.selected} />
52
+ <ContainerSection
53
+ containers={containers}
54
+ selected={controls.selected}
55
+ />
53
56
  <Spacer />
54
- <MessageFeedback message={controls.message} color={controls.messageColor} />
57
+ <MessageFeedback
58
+ message={controls.message}
59
+ color={controls.messageColor}
60
+ />
55
61
  <UsageMenu />
56
62
  {controls.showDebugLogs && (
57
- <Box marginTop={1} flexDirection="column" borderStyle="round" borderColor="gray" padding={1}>
63
+ <Box
64
+ marginTop={1}
65
+ flexDirection="column"
66
+ borderStyle="round"
67
+ borderColor="gray"
68
+ padding={1}
69
+ >
58
70
  <Text color="cyan">Debug log — press D or ESC to close</Text>
59
71
  {controls.debugLogs.length === 0 ? (
60
- <Text dimColor>No debug entries yet. Run with CDD_LOG_LEVEL=debug for verbose output.</Text>
72
+ <Text dimColor>
73
+ No debug entries yet. Run with CDD_LOG_LEVEL=debug for verbose
74
+ output.
75
+ </Text>
61
76
  ) : (
62
77
  controls.debugLogs.slice(-15).map((line, idx) => (
63
- <Text key={idx} color="gray">{line}</Text>
78
+ <Text key={idx} color="gray">
79
+ {line}
80
+ </Text>
64
81
  ))
65
82
  )}
66
83
  </Box>
@@ -1,6 +1,7 @@
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, Text } from 'ink';
3
+ import { PromptField, PromptMessage } from './PromptField.jsx';
4
+ import { SuggestionPanel } from './SuggestionPanel.jsx';
4
5
  import PropTypes from 'prop-types';
5
6
 
6
7
  /**
@@ -16,36 +17,63 @@ import PropTypes from 'prop-types';
16
17
  * @param {string} props.envInput - Value for the environment variables field
17
18
  * @param {string} props.message - Contextual message to display (errors/hints)
18
19
  * @param {string} props.messageColor - Color for the contextual message
20
+ * @param {string[]} [props.suggestions] - Autocomplete suggestions (shown only on step 0)
21
+ * @param {number} [props.selectedSuggestionIndex] - Currently focused suggestion index
22
+ * @param {number} [props.visibleOffset] - First visible suggestion offset
19
23
  * @returns {JSX.Element}
20
24
  */
21
25
  export default function ContainerCreationPrompt(props) {
22
- const { step, imageName, containerName, portInput, envInput, message, messageColor } = props;
26
+ const {
27
+ step,
28
+ imageName,
29
+ containerName,
30
+ portInput,
31
+ envInput,
32
+ message,
33
+ messageColor,
34
+ suggestions = [],
35
+ selectedSuggestionIndex = -1,
36
+ visibleOffset = 0,
37
+ } = props;
23
38
  const prompts = [
24
39
  {
25
- label: "Name of the image to create (e.g., nginx:latest):",
40
+ label: 'Name of the image to create (e.g., nginx:latest):',
26
41
  value: imageName,
27
42
  required: true,
28
43
  },
29
44
  {
30
- label: "Name of the container (optional):",
45
+ label: 'Name of the container (optional):',
31
46
  value: containerName,
32
47
  required: false,
33
48
  },
34
49
  {
35
- label: "Ports (optional, format 8080:80,443:443):",
50
+ label: 'Ports (optional, format 8080:80,443:443):',
36
51
  value: portInput,
37
52
  required: false,
38
53
  },
39
54
  {
40
- label: "Environment variables (optional, format VAR1=val1,VAR2=val2):",
55
+ label: 'Environment variables (optional, format VAR1=val1,VAR2=val2):',
41
56
  value: envInput,
42
57
  required: false,
43
58
  },
44
59
  ];
45
60
  const { label, value, required } = prompts[step] || {};
61
+ const showSuggestions = step === 0 && suggestions.length > 0;
46
62
  return (
47
- <Box flexDirection="column" borderStyle="round" borderColor="yellow" padding={1}>
63
+ <Box
64
+ flexDirection="column"
65
+ borderStyle="round"
66
+ borderColor="yellow"
67
+ padding={1}
68
+ >
48
69
  <PromptField label={label} value={value} required={required} />
70
+ {showSuggestions && (
71
+ <SuggestionPanel
72
+ items={suggestions}
73
+ selectedIndex={selectedSuggestionIndex}
74
+ visibleOffset={visibleOffset}
75
+ />
76
+ )}
49
77
  <PromptMessage message={message} color={messageColor} />
50
78
  <Text dimColor>Press Enter to continue, Escape to cancel</Text>
51
79
  </Box>
@@ -60,4 +88,10 @@ ContainerCreationPrompt.propTypes = {
60
88
  envInput: PropTypes.string,
61
89
  message: PropTypes.string,
62
90
  messageColor: PropTypes.string,
91
+ suggestions: PropTypes.arrayOf(PropTypes.string),
92
+ selectedSuggestionIndex: PropTypes.number,
93
+ visibleOffset: PropTypes.number,
63
94
  };
95
+
96
+ // Named export for test compatibility with jest ESM interop
97
+ 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 (
@@ -1,14 +1,14 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import StatsBar from "./StatsBar.jsx";
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import StatsBar from './StatsBar.jsx';
4
4
  import PropTypes from 'prop-types';
5
5
  import { useContainerStats } from '../hooks/useContainerStats.js';
6
6
 
7
7
  const stateText = (state) => {
8
- if (state === "running") return { text: "🟢 RUNNING", color: "green" };
9
- if (state === "exited") return { text: "🔴 EXITED", color: "red" };
10
- if (state === "paused") return { text: "🟠 PAUSED", color: "yellow" };
11
- return { text: state.toUpperCase(), color: "gray" };
8
+ if (state === 'running') return { text: '🟢 RUNNING', color: 'green' };
9
+ if (state === 'exited') return { text: '🔴 EXITED', color: 'red' };
10
+ if (state === 'paused') return { text: '🟠 PAUSED', color: 'yellow' };
11
+ return { text: state.toUpperCase(), color: 'gray' };
12
12
  };
13
13
 
14
14
  /**
@@ -24,17 +24,17 @@ export default function ContainerRow({ container, isSelected = false }) {
24
24
 
25
25
  // Format ports for display (no leading space to avoid layout shifts)
26
26
  const formatPorts = (ports) => {
27
- if (!ports || ports.length === 0) return "";
27
+ if (!ports || ports.length === 0) return '';
28
28
  if (Array.isArray(ports)) {
29
- return ports.map((p, _i) => `🔗 ${p}`).join(" ");
29
+ return ports.map((p, _i) => `🔗 ${p}`).join(' ');
30
30
  }
31
31
  return `🔗 ${ports}`;
32
32
  };
33
33
 
34
34
  // Helper to truncate long strings to a max length without adding trailing spaces
35
35
  const truncate = (s, max = 20) => {
36
- if (!s) return "";
37
- return s.length > max ? s.slice(0, max - 1) + "" : s;
36
+ if (!s) return '';
37
+ return s.length > max ? s.slice(0, max - 1) + '' : s;
38
38
  };
39
39
 
40
40
  const stateInfo = stateText(state);
@@ -42,8 +42,8 @@ export default function ContainerRow({ container, isSelected = false }) {
42
42
  <Box flexDirection="column" marginBottom={1}>
43
43
  <Box flexDirection="row" alignItems="center">
44
44
  <Box width={2} minWidth={2} justifyContent="flex-end" paddingRight={1}>
45
- <Text color={isSelected ? "green" : undefined}>
46
- {isSelected ? "" : " "}
45
+ <Text color={isSelected ? 'green' : undefined}>
46
+ {isSelected ? '' : ' '}
47
47
  </Text>
48
48
  </Box>
49
49
  <Box width={18} flexShrink={1} paddingRight={1}>
@@ -59,8 +59,11 @@ export default function ContainerRow({ container, isSelected = false }) {
59
59
  <Text color="yellow">{formatPorts(container.ports)}</Text>
60
60
  </Box>
61
61
  <Box flexShrink={0} paddingLeft={1}>
62
- {state === "running" ? (
63
- <StatsBar cpu={parseFloat(stats.cpuPercent)} mem={parseFloat(stats.memPercent)} />
62
+ {state === 'running' ? (
63
+ <StatsBar
64
+ cpu={parseFloat(stats.cpuPercent)}
65
+ mem={parseFloat(stats.memPercent)}
66
+ />
64
67
  ) : null}
65
68
  </Box>
66
69
  </Box>
@@ -75,7 +78,7 @@ ContainerRow.propTypes = {
75
78
  name: PropTypes.string.isRequired,
76
79
  image: PropTypes.string,
77
80
  state: PropTypes.string,
78
- ports: PropTypes.oneOfType([PropTypes.array, PropTypes.string])
81
+ ports: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
79
82
  }).isRequired,
80
83
  isSelected: PropTypes.bool,
81
84
  };
@@ -1,6 +1,6 @@
1
- import React from "react";
2
- import { Text } from "ink";
3
- import ContainerList from "./ContainerList.jsx";
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import ContainerList from './ContainerList.jsx';
4
4
  import PropTypes from 'prop-types';
5
5
 
6
6
  /**
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
3
 
4
4
  /**
5
5
  * Simple footer displayed at the bottom of the interface.
@@ -1,8 +1,8 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import chalk from "chalk";
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import chalk from 'chalk';
4
4
  import PropTypes from 'prop-types';
5
- import { getAppVersion } from "../helpers/appInfo.js";
5
+ import { getAppVersion } from '../helpers/appInfo.js';
6
6
 
7
7
  /**
8
8
  * Header component displayed at the top of the CLI UI.
@@ -18,18 +18,18 @@ import { getAppVersion } from "../helpers/appInfo.js";
18
18
  const VERSION = getAppVersion();
19
19
 
20
20
  export default function Header({ count }) {
21
- const formattedVersion = VERSION === "unknown" ? "unknown" : `v${VERSION}`;
21
+ const formattedVersion = VERSION === 'unknown' ? 'unknown' : `v${VERSION}`;
22
22
 
23
23
  return (
24
24
  <Box justifyContent="space-between">
25
25
  <Text color="cyanBright">
26
- 🐳 {chalk.bold("CDD")}
26
+ 🐳 {chalk.bold('CDD')}
27
27
  <Text color="gray"> — CLI Docker Dashboard</Text>
28
28
  </Text>
29
29
  <Box flexDirection="column" alignItems="flex-end">
30
30
  <Text color="gray">{formattedVersion}</Text>
31
31
  <Text color="gray">
32
- {count} container{count === 1 ? "" : "s"} found
32
+ {count} container{count === 1 ? '' : 's'} found
33
33
  </Text>
34
34
  </Box>
35
35
  </Box>
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { Text } from "ink";
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
3
  import PropTypes from 'prop-types';
4
4
 
5
5
  /**
@@ -17,7 +17,9 @@ export default function LogViewer({ logs, onExit: _onExit, container }) {
17
17
 
18
18
  return (
19
19
  <>
20
- <Text color="green">{container?.name ?? "Container"} logs, press ESC to exit</Text>
20
+ <Text color="green">
21
+ {container?.name ?? 'Container'} logs, press ESC to exit
22
+ </Text>
21
23
  {visibleLogs.length === 0 ? (
22
24
  <Text dimColor>No logs...</Text>
23
25
  ) : (
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
3
  import PropTypes from 'prop-types';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { Text } from "ink";
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
3
  import PropTypes from 'prop-types';
4
4
 
5
5
  /**
@@ -18,7 +18,7 @@ export function PromptField({ label, value, required }) {
18
18
  return (
19
19
  <>
20
20
  <Text>{label}</Text>
21
- <Text color={isEmpty ? "red" : "cyan"}>{value}_</Text>
21
+ <Text color={isEmpty ? 'red' : 'cyan'}>{value}_</Text>
22
22
  </>
23
23
  );
24
24
  }
@@ -33,7 +33,7 @@ export function PromptField({ label, value, required }) {
33
33
  */
34
34
  export function PromptMessage({ message, color }) {
35
35
  if (!message) return null;
36
- return <Text color={color || "yellow"}>{message}</Text>;
36
+ return <Text color={color || 'yellow'}>{message}</Text>;
37
37
  }
38
38
 
39
39
  PromptField.propTypes = {