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
@@ -8,16 +8,21 @@ export function useDebugLogs() {
8
8
  useEffect(() => {
9
9
  const unsubscribe = logger.subscribe((entry) => {
10
10
  setDebugLogs((prev) => {
11
- const extras = (entry.args || []).map((arg) => {
12
- if (typeof arg === "string") return arg;
13
- if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
14
- try {
15
- return JSON.stringify(arg);
16
- } catch (err) {
17
- return "[unserializable]";
18
- }
19
- }).filter(Boolean);
20
- const line = extras.length ? `${entry.formatted} ${extras.join(" ")}` : entry.formatted;
11
+ const extras = (entry.args || [])
12
+ .map((arg) => {
13
+ if (typeof arg === 'string') return arg;
14
+ if (typeof arg === 'number' || typeof arg === 'boolean')
15
+ return String(arg);
16
+ try {
17
+ return JSON.stringify(arg);
18
+ } catch (err) {
19
+ return '[unserializable]';
20
+ }
21
+ })
22
+ .filter(Boolean);
23
+ const line = extras.length
24
+ ? `${entry.formatted} ${extras.join(' ')}`
25
+ : entry.formatted;
21
26
  const next = [...prev, line];
22
27
  return next.slice(-200);
23
28
  });
@@ -3,23 +3,26 @@ import { useState, useCallback } from 'react';
3
3
  export function useContainerSelection(total) {
4
4
  const [selected, setSelected] = useState(0);
5
5
 
6
- const handleNavigation = useCallback((input, key) => {
7
- if (total === 0) {
8
- return false;
9
- }
6
+ const handleNavigation = useCallback(
7
+ (input, key) => {
8
+ if (total === 0) {
9
+ return false;
10
+ }
10
11
 
11
- if (key.upArrow) {
12
- setSelected((i) => (i === 0 ? total - 1 : i - 1));
13
- return true;
14
- }
12
+ if (key.upArrow) {
13
+ setSelected((i) => (i === 0 ? total - 1 : i - 1));
14
+ return true;
15
+ }
15
16
 
16
- if (key.downArrow) {
17
- setSelected((i) => (i === total - 1 ? 0 : i + 1));
18
- return true;
19
- }
17
+ if (key.downArrow) {
18
+ setSelected((i) => (i === total - 1 ? 0 : i + 1));
19
+ return true;
20
+ }
20
21
 
21
- return false;
22
- }, [total]);
22
+ return false;
23
+ },
24
+ [total]
25
+ );
23
26
 
24
27
  return { selected, setSelected, handleNavigation };
25
28
  }
@@ -1,4 +1,4 @@
1
- import { useCallback } from "react";
1
+ import { useCallback } from 'react';
2
2
 
3
3
  /**
4
4
  * Hook responsible solely for routing Docker action keystrokes to the correct handler.
@@ -36,75 +36,88 @@ export function useContainerCommandRouter({
36
36
  onToggleDebug,
37
37
  onStartCreate,
38
38
  }) {
39
- const handleDockerCommands = useCallback((input) => {
40
- const container = containers[selected];
39
+ const handleDockerCommands = useCallback(
40
+ (input) => {
41
+ const container = containers[selected];
41
42
 
42
- if (input === "i") {
43
- actions.handleAction({
44
- actionFn: async (id) => await actions.startContainer(id),
45
- actionLabel: "Starting",
46
- selected,
47
- stateCheck: (c) =>
48
- (c.state === "running" || c.status === "running") &&
49
- "Container is already running.",
50
- });
51
- return true;
52
- }
43
+ if (input === 'i') {
44
+ actions.handleAction({
45
+ actionFn: async (id) => await actions.startContainer(id),
46
+ actionLabel: 'Starting',
47
+ selected,
48
+ stateCheck: (c) =>
49
+ (c.state === 'running' || c.status === 'running') &&
50
+ 'Container is already running.',
51
+ });
52
+ return true;
53
+ }
53
54
 
54
- if (input === "p") {
55
- actions.handleAction({
56
- actionFn: async (id) => await actions.stopContainer(id),
57
- actionLabel: "Stopping",
58
- selected,
59
- stateCheck: (c) =>
60
- (c.state === "exited" ||
61
- c.status === "exited" ||
62
- c.state === "stopped" ||
63
- c.status === "stopped") &&
64
- "Container is already stopped.",
65
- });
66
- return true;
67
- }
55
+ if (input === 'p') {
56
+ actions.handleAction({
57
+ actionFn: async (id) => await actions.stopContainer(id),
58
+ actionLabel: 'Stopping',
59
+ selected,
60
+ stateCheck: (c) =>
61
+ (c.state === 'exited' ||
62
+ c.status === 'exited' ||
63
+ c.state === 'stopped' ||
64
+ c.status === 'stopped') &&
65
+ 'Container is already stopped.',
66
+ });
67
+ return true;
68
+ }
68
69
 
69
- if (input === "r") {
70
- actions.handleAction({
71
- actionFn: async (id) => await actions.restartContainer(id),
72
- actionLabel: "Restarting",
73
- selected,
74
- });
75
- return true;
76
- }
70
+ if (input === 'r') {
71
+ actions.handleAction({
72
+ actionFn: async (id) => await actions.restartContainer(id),
73
+ actionLabel: 'Restarting',
74
+ selected,
75
+ });
76
+ return true;
77
+ }
77
78
 
78
- if (input === "e") {
79
- if (!container) {
79
+ if (input === 'e') {
80
+ if (!container) {
81
+ return true;
82
+ }
83
+ onStartErase();
80
84
  return true;
81
85
  }
82
- onStartErase();
83
- return true;
84
- }
85
86
 
86
- if (input === "l") {
87
- if (!container) {
87
+ if (input === 'l') {
88
+ if (!container) {
89
+ return true;
90
+ }
91
+ logsViewer.openLogs();
92
+ startLogsStream(container.id);
88
93
  return true;
89
94
  }
90
- logsViewer.openLogs();
91
- startLogsStream(container.id);
92
- return true;
93
- }
94
95
 
95
- if (input === "c") {
96
- onStartCreate();
97
- creation.resetCreation();
98
- return true;
99
- }
96
+ if (input === 'c') {
97
+ creation.resetCreation();
98
+ onStartCreate();
99
+ return true;
100
+ }
100
101
 
101
- if (input === "d") {
102
- onToggleDebug();
103
- return true;
104
- }
102
+ if (input === 'd') {
103
+ onToggleDebug();
104
+ return true;
105
+ }
105
106
 
106
- return false;
107
- }, [actions, containers, creation, logsViewer, onStartCreate, onStartErase, onToggleDebug, selected, startLogsStream]);
107
+ return false;
108
+ },
109
+ [
110
+ actions,
111
+ containers,
112
+ creation,
113
+ logsViewer,
114
+ onStartCreate,
115
+ onStartErase,
116
+ onToggleDebug,
117
+ selected,
118
+ startLogsStream,
119
+ ]
120
+ );
108
121
 
109
122
  return { handleDockerCommands };
110
123
  }
@@ -8,9 +8,9 @@
8
8
  * // Use in a component
9
9
  * const { containers } = useContainers();
10
10
  */
11
- import React, { useState, useEffect } from "react";
12
- import { getContainers } from "../helpers/dockerService/serviceComponents/containerList";
13
- import { REFRESH_INTERVALS } from "../helpers/constants.js";
11
+ import React, { useState, useEffect } from 'react';
12
+ import { getContainers } from '../helpers/dockerService/serviceComponents/containerList';
13
+ import { REFRESH_INTERVALS } from '../helpers/constants.js';
14
14
 
15
15
  export function useContainers() {
16
16
  const [containers, setContainers] = useState([]);
@@ -18,7 +18,7 @@ export function useContainers() {
18
18
  useEffect(() => {
19
19
  const fetch = async () => setContainers(await getContainers());
20
20
  fetch();
21
- const timer = setInterval(fetch, REFRESH_INTERVALS.CONTAINER_LIST);
21
+ const timer = setInterval(fetch, REFRESH_INTERVALS.CONTAINER_LIST);
22
22
  return () => clearInterval(timer);
23
23
  }, []);
24
24
 
@@ -1,17 +1,17 @@
1
- import React from "react";
2
- import { useInput } from "ink";
3
- import { useContainerActions } from "./creation/useContainerActions";
4
- import { useContainerCreation } from "./creation/useContainerCreation";
5
- import { useLogsViewer } from "./creation/useLogsViewer";
6
- import { useContainerSelection } from "./navigation/useContainerSelection";
7
- import { useDebugLogs } from "./debug/useDebugLogs";
8
- import { useEraseConfirmation } from "./useEraseConfirmation";
9
- import { useExitHandler } from "./useExitHandler";
10
- import { useContainerCommandRouter } from "./useContainerCommandRouter";
11
- import { getLogsStream } from "../helpers/dockerService/serviceComponents/containerLogs.js";
12
- import { createContainer as svcCreateContainer } from "../helpers/dockerService/serviceComponents/containerActions.js";
13
- import { buildContainerOptions } from "../helpers/containerOptionsBuilder.js";
14
- import { DB_IMAGES } from "../helpers/constants.js";
1
+ import React from 'react';
2
+ import { useInput } from 'ink';
3
+ import { useContainerActions } from './creation/useContainerActions';
4
+ import { useContainerCreation } from './creation/useContainerCreation';
5
+ import { useLogsViewer } from './creation/useLogsViewer';
6
+ import { useContainerSelection } from './navigation/useContainerSelection';
7
+ import { useDebugLogs } from './debug/useDebugLogs';
8
+ import { useEraseConfirmation } from './useEraseConfirmation';
9
+ import { useExitHandler } from './useExitHandler';
10
+ import { useContainerCommandRouter } from './useContainerCommandRouter';
11
+ import { getLogsStream } from '../helpers/dockerService/serviceComponents/containerLogs.js';
12
+ import { createContainer as svcCreateContainer } from '../helpers/dockerService/serviceComponents/containerActions.js';
13
+ import { buildContainerOptions } from '../helpers/containerOptionsBuilder.js';
14
+ import { DB_IMAGES } from '../helpers/constants.js';
15
15
 
16
16
  // Principal hook to manage user inputs and control the app state
17
17
  /**
@@ -21,7 +21,7 @@ import { DB_IMAGES } from "../helpers/constants.js";
21
21
  * @param {Array<Object>} containers - Current list of Docker containers
22
22
  * @returns {Object} controls - API for the App component
23
23
  */
24
- export function useControls(containers = []) {
24
+ export function useControls(containers = [], overrides = {}) {
25
25
  const [creatingContainer, setCreatingContainer] = React.useState(false);
26
26
 
27
27
  // — Modular hooks —
@@ -29,20 +29,26 @@ export function useControls(containers = []) {
29
29
 
30
30
  const creation = useContainerCreation({
31
31
  onCreate: async ({ imageName, containerName, portInput, envInput }) => {
32
- const options = buildContainerOptions({ imageName, containerName, portInput, envInput });
33
- actions.setMessage(`Creating container ${imageName}...`);
34
- actions.setMessageColor("yellow");
32
+ const options = buildContainerOptions({
33
+ imageName,
34
+ containerName,
35
+ portInput,
36
+ envInput,
37
+ });
38
+ actions.setTimedMessage(`Creating container ${imageName}...`, 'yellow');
35
39
  try {
36
40
  const { id, ports } = await svcCreateContainer(imageName, options);
37
- let portMsg = "";
41
+ let portMsg = '';
38
42
  if (ports && ports.length) {
39
- portMsg = " | Ports: " + ports.map(p => `${p.hostPort}→${p.containerPort}/${p.protocol}`).join(", ");
43
+ portMsg =
44
+ ' | Ports: ' +
45
+ ports
46
+ .map((p) => `${p.hostPort}→${p.containerPort}/${p.protocol}`)
47
+ .join(', ');
40
48
  }
41
- actions.setMessage(`Created container ${id}${portMsg}`);
42
- actions.setMessageColor("green");
49
+ actions.setTimedMessage(`Created container ${id}${portMsg}`, 'green');
43
50
  } catch (err) {
44
- actions.setMessage(`Error creating container: ${err.message}`);
45
- actions.setMessageColor("red");
51
+ actions.setTimedMessage(`Error creating container: ${err.message}`, 'red');
46
52
  } finally {
47
53
  setCreatingContainer(false);
48
54
  }
@@ -55,18 +61,25 @@ export function useControls(containers = []) {
55
61
  const selection = useContainerSelection(containers.length);
56
62
  const debugLogs = useDebugLogs();
57
63
 
64
+ // Allow overrides for testability (e.g. injecting a mock triggerHubSearch)
65
+ const triggerHubSearch = overrides.triggerHubSearch ?? creation.triggerHubSearch;
66
+ const isSearchingHub = overrides.isSearchingHub ?? creation.isSearchingHub;
67
+ const insertNextSuggestedEnv = overrides.insertNextSuggestedEnv !== undefined
68
+ ? overrides.insertNextSuggestedEnv
69
+ : creation.insertNextSuggestedEnv;
70
+
58
71
  const eraseConfirmation = useEraseConfirmation({
59
72
  onConfirm: () => {
60
73
  actions.handleAction({
61
74
  actionFn: async (id) => await actions.removeContainer(id),
62
- actionLabel: "Erasing",
75
+ actionLabel: 'Erasing',
63
76
  selected: selection.selected,
64
77
  });
65
- actions.setMessageColor("yellow");
78
+ actions.setMessageColor('yellow');
66
79
  },
67
80
  onCancel: () => {
68
- actions.setMessage("");
69
- actions.setMessageColor("");
81
+ actions.setMessage('');
82
+ actions.setMessageColor('');
70
83
  },
71
84
  });
72
85
 
@@ -74,17 +87,22 @@ export function useControls(containers = []) {
74
87
  * Pipe container logs into the viewer while applying a hard limit.
75
88
  * @param {string} containerId - Container identifier used by Docker.
76
89
  */
77
- const startLogsStream = React.useCallback((containerId) => {
78
- getLogsStream(
79
- containerId,
80
- (data) => logsViewer.setLogs((prev) => {
81
- const newLogs = [...prev, ...data.split("\n").filter(Boolean)];
82
- return newLogs.slice(-1000);
83
- }),
84
- () => {},
85
- (err) => logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
86
- );
87
- }, [logsViewer]);
90
+ const startLogsStream = React.useCallback(
91
+ (containerId) => {
92
+ getLogsStream(
93
+ containerId,
94
+ (data) =>
95
+ logsViewer.setLogs((prev) => {
96
+ const newLogs = [...prev, ...data.split('\n').filter(Boolean)];
97
+ return newLogs.slice(-1000);
98
+ }),
99
+ () => {},
100
+ (err) =>
101
+ logsViewer.setLogs((prev) => [...prev, `Error: ${err.message}`])
102
+ );
103
+ },
104
+ [logsViewer]
105
+ );
88
106
 
89
107
  const commandRouter = useContainerCommandRouter({
90
108
  actions,
@@ -95,8 +113,10 @@ export function useControls(containers = []) {
95
113
  startLogsStream,
96
114
  onStartErase: () => {
97
115
  eraseConfirmation.startErase();
98
- actions.setMessage("Are you sure you want to erase this container? (y/n)");
99
- actions.setMessageColor("yellow");
116
+ actions.setMessage(
117
+ 'Are you sure you want to erase this container? (y/n)'
118
+ );
119
+ actions.setMessageColor('yellow');
100
120
  },
101
121
  onToggleDebug: () => debugLogs.setShowDebugLogs((prev) => !prev),
102
122
  onStartCreate: () => setCreatingContainer(true),
@@ -104,8 +124,8 @@ export function useControls(containers = []) {
104
124
 
105
125
  const exitHandler = useExitHandler({
106
126
  onBeforeExit: () => {
107
- actions.setMessage("Exiting...");
108
- actions.setMessageColor("yellow");
127
+ actions.setMessage('Exiting...');
128
+ actions.setMessageColor('yellow');
109
129
  logsViewer.closeLogs();
110
130
  debugLogs.setShowDebugLogs(false);
111
131
  },
@@ -114,38 +134,79 @@ export function useControls(containers = []) {
114
134
  /**
115
135
  * Route keystrokes to the container creation wizard.
116
136
  */
117
- const processCreationInput = React.useCallback((input, key) => {
118
- const step = creation.step;
137
+ const processCreationInput = React.useCallback(
138
+ (input, key) => {
139
+ const step = creation.step;
119
140
 
120
- const removeLastChar = (setter) => setter((value) => (value || "").slice(0, -1));
121
- const appendChar = (setter, current, ch) => setter((current || "") + ch);
141
+ const removeLastChar = (setter) =>
142
+ setter((value) => (value || '').slice(0, -1));
143
+ const appendChar = (setter, current, ch) => setter((current || '') + ch);
122
144
 
123
- if (key.escape) {
124
- creation.cancelCreation();
125
- setCreatingContainer(false);
126
- return;
127
- }
145
+ if (key.escape) {
146
+ creation.cancelCreation();
147
+ setCreatingContainer(false);
148
+ return;
149
+ }
128
150
 
129
- if (input === "\r" || input === "\n") {
130
- creation.nextStep();
131
- return;
132
- }
151
+ if (input === '\r' || input === '\n') {
152
+ // If on step 0 with a focused suggestion, apply it instead of advancing
153
+ if (step === 0 && creation.selectedSuggestionIndex >= 0) {
154
+ creation.applyFocusedSuggestion();
155
+ return;
156
+ }
157
+ creation.nextStep();
158
+ return;
159
+ }
133
160
 
134
- if (key.backspace || key.delete) {
135
- if (step === 0) removeLastChar(creation.setImageName);
136
- if (step === 1) removeLastChar(creation.setContainerName);
137
- if (step === 2) removeLastChar(creation.setPortInput);
138
- if (step === 3) removeLastChar(creation.setEnvInput);
139
- return;
140
- }
161
+ // Tab on step 0: trigger Docker Hub search (with guards)
162
+ if (key.tab) {
163
+ if (step === 0) {
164
+ if (!isSearchingHub && (creation.imageName || '').trim() !== '') {
165
+ triggerHubSearch();
166
+ }
167
+ } else if (step === 3) {
168
+ insertNextSuggestedEnv?.();
169
+ }
170
+ return;
171
+ }
141
172
 
142
- if (input && input.length === 1 && !key.ctrl && !key.meta) {
143
- if (step === 0) appendChar(creation.setImageName, creation.imageName, input);
144
- if (step === 1) appendChar(creation.setContainerName, creation.containerName, input);
145
- if (step === 2) appendChar(creation.setPortInput, creation.portInput, input);
146
- if (step === 3) appendChar(creation.setEnvInput, creation.envInput, input);
147
- }
148
- }, [creation, setCreatingContainer]);
173
+ // Arrow keys on step 0: navigate suggestion list
174
+ if (step === 0 && creation.suggestions.length > 0) {
175
+ if (key.upArrow) {
176
+ creation.moveSuggestionSelection(-1);
177
+ return;
178
+ }
179
+ if (key.downArrow) {
180
+ creation.moveSuggestionSelection(1);
181
+ return;
182
+ }
183
+ }
184
+
185
+ if (key.backspace || key.delete) {
186
+ if (step === 0) {
187
+ const newValue = (creation.imageName || '').slice(0, -1);
188
+ creation.updateImageInput(newValue);
189
+ }
190
+ if (step === 1) removeLastChar(creation.setContainerName);
191
+ if (step === 2) removeLastChar(creation.setPortInput);
192
+ if (step === 3) removeLastChar(creation.setEnvInput);
193
+ return;
194
+ }
195
+
196
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
197
+ if (step === 0) {
198
+ creation.updateImageInput((creation.imageName || '') + input);
199
+ }
200
+ if (step === 1)
201
+ appendChar(creation.setContainerName, creation.containerName, input);
202
+ if (step === 2)
203
+ appendChar(creation.setPortInput, creation.portInput, input);
204
+ if (step === 3)
205
+ appendChar(creation.setEnvInput, creation.envInput, input);
206
+ }
207
+ },
208
+ [creation, setCreatingContainer, triggerHubSearch, isSearchingHub, insertNextSuggestedEnv]
209
+ );
149
210
 
150
211
  // Single keyboard entry point
151
212
  useInput((input, key) => {
@@ -155,7 +216,7 @@ export function useControls(containers = []) {
155
216
  }
156
217
 
157
218
  if (logsViewer.showLogs) {
158
- if (input === "q" || key.escape) {
219
+ if (input === 'q' || key.escape) {
159
220
  logsViewer.closeLogs();
160
221
  }
161
222
  return;
@@ -180,7 +241,9 @@ export function useControls(containers = []) {
180
241
  selected: selection.selected,
181
242
  setSelected: selection.setSelected,
182
243
  message: creatingContainer ? creation.message : actions.message,
183
- messageColor: creatingContainer ? creation.messageColor : actions.messageColor,
244
+ messageColor: creatingContainer
245
+ ? creation.messageColor
246
+ : actions.messageColor,
184
247
  showLogs: logsViewer.showLogs,
185
248
  logs: logsViewer.logs,
186
249
  exitLogs: logsViewer.closeLogs,
@@ -1,4 +1,4 @@
1
- import { useState, useCallback } from "react";
1
+ import { useState, useCallback } from 'react';
2
2
 
3
3
  /**
4
4
  * Hook to manage the erase confirmation dialog flow.
@@ -20,13 +20,13 @@ export function useEraseConfirmation({ onConfirm, onCancel }) {
20
20
 
21
21
  const processEraseConfirmation = useCallback(
22
22
  (input, key) => {
23
- if (input === "y" || input === "Y") {
23
+ if (input === 'y' || input === 'Y') {
24
24
  onConfirm();
25
25
  setConfirmErase(false);
26
26
  return;
27
27
  }
28
28
 
29
- if (input === "n" || input === "N" || key.escape) {
29
+ if (input === 'n' || input === 'N' || key.escape) {
30
30
  setConfirmErase(false);
31
31
  onCancel();
32
32
  return;
@@ -30,37 +30,40 @@ export function useExitHandler({ onBeforeExit } = {}) {
30
30
  * @param {string} input - Raw character input from Ink's useInput
31
31
  * @returns {boolean} True when the input was consumed, false otherwise
32
32
  */
33
- const handleExitCommand = useCallback((input) => {
34
- if (input !== 'q') {
35
- return false;
36
- }
33
+ const handleExitCommand = useCallback(
34
+ (input) => {
35
+ if (input !== 'q') {
36
+ return false;
37
+ }
37
38
 
38
- if (typeof onBeforeExit === 'function') {
39
- onBeforeExit();
40
- }
39
+ if (typeof onBeforeExit === 'function') {
40
+ onBeforeExit();
41
+ }
41
42
 
42
- const clearTerminal = () => {
43
- try {
44
- if (process.stdout && process.stdout.isTTY) {
45
- process.stdout.write('\u001Bc');
46
- } else {
47
- console.clear();
43
+ const clearTerminal = () => {
44
+ try {
45
+ if (process.stdout && process.stdout.isTTY) {
46
+ process.stdout.write('\u001Bc');
47
+ } else {
48
+ console.clear();
49
+ }
50
+ } catch (err) {
51
+ // Ignore clear errors to avoid blocking exit.
48
52
  }
49
- } catch (err) {
50
- // Ignore clear errors to avoid blocking exit.
51
- }
52
- };
53
+ };
53
54
 
54
- setTimeout(() => {
55
- clearTerminal();
56
- exit();
57
- if (process.env.NODE_ENV !== 'test') {
58
- setTimeout(() => process.exit(0), 50);
59
- }
60
- }, EXIT_DELAY);
55
+ setTimeout(() => {
56
+ clearTerminal();
57
+ exit();
58
+ if (process.env.NODE_ENV !== 'test') {
59
+ setTimeout(() => process.exit(0), 50);
60
+ }
61
+ }, EXIT_DELAY);
61
62
 
62
- return true;
63
- }, [exit, onBeforeExit]);
63
+ return true;
64
+ },
65
+ [exit, onBeforeExit]
66
+ );
64
67
 
65
68
  return { handleExitCommand };
66
69
  }
package/src/index.js CHANGED
@@ -8,9 +8,13 @@
8
8
  * node index.js
9
9
  */
10
10
 
11
- import React from "react";
12
- import { render } from "ink";
13
- import App from './App';
11
+ import React from 'react';
12
+ import { render } from 'ink';
13
+ import App from './App.jsx';
14
+ import { logger } from './helpers/logger.js';
15
+
16
+ process.on('uncaughtException', (err) => logger.error('Uncaught exception', err));
17
+ process.on('unhandledRejection', (reason) => logger.error('Unhandled rejection', reason));
14
18
 
15
19
  console.clear();
16
- render(<App />);
20
+ render(<App />);