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
@@ -5,9 +5,15 @@ import React, { useEffect } from 'react';
5
5
  import { render, act } from '@testing-library/react';
6
6
  import { jest } from '@jest/globals';
7
7
 
8
- // Mock ink so that useInput and useApp are no-ops (require real TTY in production)
8
+ // Mock ink capture the useInput handler so tests can simulate keypresses.
9
+ let _inputHandler = null;
10
+ const triggerInput = (input, key = {}) => {
11
+ if (_inputHandler) _inputHandler(input, key);
12
+ };
9
13
  await jest.unstable_mockModule('ink', () => ({
10
- useInput: () => {},
14
+ Box: ({ children, ...props }) => React.createElement('div', props, children),
15
+ Text: ({ children }) => React.createElement('span', null, children),
16
+ useInput: (fn) => { _inputHandler = fn; },
11
17
  useApp: () => ({ exit: () => {} }),
12
18
  }));
13
19
 
@@ -115,3 +121,193 @@ describe('useControls (FR6 — port mapping in success message)', () => {
115
121
  expect(expose.current.actions.messageColor).toBe('red');
116
122
  });
117
123
  });
124
+
125
+ describe('useControls — step 0 keyboard routing integration', () => {
126
+ beforeEach(() => {
127
+ mockSvcCreateContainer.mockReset();
128
+ });
129
+
130
+ test('creation.updateImageInput() filters suggestions (routing readiness check)', () => {
131
+ const expose = { current: null };
132
+ render(<HookTester containers={[]} expose={expose} />);
133
+
134
+ // updateImageInput should be exposed through creation hook
135
+ expect(typeof expose.current.creation.updateImageInput).toBe('function');
136
+
137
+ act(() => { expose.current.creation.updateImageInput('ng'); });
138
+
139
+ expect(expose.current.creation.suggestions).toContain('nginx');
140
+ expect(expose.current.creation.selectedSuggestionIndex).toBe(-1);
141
+ });
142
+
143
+ test('moveSuggestionSelection(1) then applyFocusedSuggestion() fills imageName', () => {
144
+ const expose = { current: null };
145
+ render(<HookTester containers={[]} expose={expose} />);
146
+
147
+ act(() => { expose.current.creation.updateImageInput('ng'); });
148
+ act(() => { expose.current.creation.moveSuggestionSelection(1); });
149
+ act(() => { expose.current.creation.applyFocusedSuggestion(); });
150
+
151
+ expect(expose.current.creation.imageName).toBe('nginx');
152
+ expect(expose.current.creation.step).toBe(0);
153
+ expect(expose.current.creation.suggestions).toHaveLength(0);
154
+ });
155
+
156
+ test('applyFocusedSuggestion() does not advance step (Enter with focus uses suggestion not nextStep)', () => {
157
+ const expose = { current: null };
158
+ render(<HookTester containers={[]} expose={expose} />);
159
+
160
+ act(() => { expose.current.creation.updateImageInput('ng'); });
161
+ act(() => { expose.current.creation.moveSuggestionSelection(1); });
162
+ act(() => { expose.current.creation.applyFocusedSuggestion(); });
163
+
164
+ // Step must remain 0 — autocomplete applied, wizard did not advance
165
+ expect(expose.current.creation.step).toBe(0);
166
+ });
167
+
168
+ test('nextStep() with imageName set advances to step 1 (normal flow without suggestions)', () => {
169
+ const expose = { current: null };
170
+ render(<HookTester containers={[]} expose={expose} />);
171
+
172
+ act(() => { expose.current.creation.setImageName('nginx'); });
173
+ act(() => { expose.current.creation.nextStep(); });
174
+
175
+ expect(expose.current.creation.step).toBe(1);
176
+ });
177
+ });
178
+
179
+ // ─────────────────────────────────────────────────────────────────────────────
180
+ // FR4 / FR5 / FR6 — Real keyboard routing via processCreationInput
181
+ // These tests simulate actual keypresses through the useInput callback.
182
+ // ─────────────────────────────────────────────────────────────────────────────
183
+ describe('useControls — FR4/FR5/FR6 keyboard routing via processCreationInput', () => {
184
+ beforeEach(() => {
185
+ _inputHandler = null;
186
+ mockSvcCreateContainer.mockReset();
187
+ });
188
+
189
+ /** Helper: render hook and enter creation mode by simulating 'c' keypress. */
190
+ function renderAndStartCreation(containers = []) {
191
+ const expose = { current: null };
192
+ render(<HookTester containers={containers} expose={expose} />);
193
+ // Trigger creation mode ('c' command in handleDockerCommands)
194
+ act(() => { triggerInput('c', {}); });
195
+ return expose;
196
+ }
197
+
198
+ // ── FR4: ↑/↓ only intercepted on step 0 with suggestions ────────────────
199
+
200
+ test('FR4 — ↓ arrow on step 0 with suggestions moves selection down', () => {
201
+ const expose = renderAndStartCreation();
202
+
203
+ // Type 'n' to get suggestions (nginx, node, etc.)
204
+ act(() => { triggerInput('n', {}); });
205
+ const suggestionsAfterType = expose.current.creation.suggestions;
206
+ expect(suggestionsAfterType.length).toBeGreaterThan(0);
207
+
208
+ act(() => { triggerInput('', { downArrow: true }); });
209
+ expect(expose.current.creation.selectedSuggestionIndex).toBe(0);
210
+ });
211
+
212
+ test('FR4 — ↑ arrow on step 0 with suggestions moves selection up', () => {
213
+ const expose = renderAndStartCreation();
214
+
215
+ act(() => { triggerInput('n', {}); });
216
+ // Move down twice, then up once → index should be 0
217
+ act(() => { triggerInput('', { downArrow: true }); });
218
+ act(() => { triggerInput('', { downArrow: true }); });
219
+ act(() => { triggerInput('', { upArrow: true }); });
220
+ expect(expose.current.creation.selectedSuggestionIndex).toBe(0);
221
+ });
222
+
223
+ test('FR4 — ↑/↓ are ignored on step 0 when suggestions list is empty', () => {
224
+ const expose = renderAndStartCreation();
225
+
226
+ // Type something unlikely to match → no suggestions
227
+ act(() => { triggerInput('x', {}); });
228
+ act(() => { triggerInput('x', {}); });
229
+ act(() => { triggerInput('x', {}); });
230
+ expect(expose.current.creation.suggestions).toHaveLength(0);
231
+
232
+ act(() => { triggerInput('', { downArrow: true }); });
233
+ // selectedSuggestionIndex stays at -1 (ignored)
234
+ expect(expose.current.creation.selectedSuggestionIndex).toBe(-1);
235
+ });
236
+
237
+ test('FR4 — ↑/↓ are ignored when step > 0 (no suggestions navigation on other steps)', () => {
238
+ const expose = renderAndStartCreation();
239
+
240
+ // Advance to step 1 by typing an image name and pressing Enter
241
+ act(() => { triggerInput('n', {}); }); // type → imageName='n'
242
+ act(() => { triggerInput('g', {}); }); // type → imageName='ng', suggestions appear
243
+ // Clear suggestions by pressing Escape then re-enter (shortcut: directly set step)
244
+ // Instead, clear imageName and press Enter with no suggestions
245
+ // Set imageName via direct API, advance step
246
+ act(() => { expose.current.creation.setImageName('nginx'); });
247
+ // Simulate Enter (\r) with no focused suggestion → should advance step
248
+ act(() => { triggerInput('\r', {}); });
249
+ expect(expose.current.creation.step).toBe(1);
250
+
251
+ const prevSelection = expose.current.creation.selectedSuggestionIndex;
252
+ act(() => { triggerInput('', { downArrow: true }); });
253
+ // selection is unchanged — arrow ignored on step 1
254
+ expect(expose.current.creation.selectedSuggestionIndex).toBe(prevSelection);
255
+ });
256
+
257
+ // ── FR5: Enter with focused suggestion → apply, NOT nextStep ─────────────
258
+
259
+ test('FR5 — Enter with selectedSuggestionIndex >= 0 applies suggestion and stays on step 0', () => {
260
+ const expose = renderAndStartCreation();
261
+
262
+ act(() => { triggerInput('n', {}); }); // type to get suggestions
263
+ expect(expose.current.creation.suggestions.length).toBeGreaterThan(0);
264
+
265
+ act(() => { triggerInput('', { downArrow: true }); }); // select first suggestion
266
+ expect(expose.current.creation.selectedSuggestionIndex).toBe(0);
267
+
268
+ // Press Enter → should apply suggestion, NOT advance step
269
+ act(() => { triggerInput('\r', {}); });
270
+
271
+ expect(expose.current.creation.step).toBe(0);
272
+ expect(expose.current.creation.imageName).not.toBe('');
273
+ expect(expose.current.creation.imageName).not.toBe('n');
274
+ });
275
+
276
+ test('FR5 — Enter with focused suggestion does NOT call nextStep (step stays 0)', () => {
277
+ const expose = renderAndStartCreation();
278
+
279
+ act(() => { triggerInput('n', {}); });
280
+ act(() => { triggerInput('', { downArrow: true }); }); // focus first
281
+
282
+ act(() => { triggerInput('\r', {}); }); // Enter with focus
283
+
284
+ // FR5: step must be 0 (nextStep was NOT called)
285
+ expect(expose.current.creation.step).toBe(0);
286
+ });
287
+
288
+ // ── FR6: Enter without focused suggestion → nextStep ─────────────────────
289
+
290
+ test('FR6 — Enter with selectedSuggestionIndex === -1 advances step', () => {
291
+ const expose = renderAndStartCreation();
292
+
293
+ act(() => { expose.current.creation.setImageName('nginx'); });
294
+ // No suggestion focused (index -1)
295
+ expect(expose.current.creation.selectedSuggestionIndex).toBe(-1);
296
+
297
+ act(() => { triggerInput('\r', {}); }); // Enter without focus
298
+ expect(expose.current.creation.step).toBe(1);
299
+ });
300
+
301
+ test('FR6 — Enter without focus does NOT apply suggestion (imageName unchanged)', () => {
302
+ const expose = renderAndStartCreation();
303
+
304
+ act(() => { expose.current.creation.setImageName('nginx'); });
305
+ const nameBefore = expose.current.creation.imageName;
306
+
307
+ act(() => { triggerInput('\r', {}); }); // advance to step 1
308
+
309
+ // imageName should be unchanged — applyFocusedSuggestion was NOT called
310
+ expect(expose.current.creation.imageName).toBe(nameBefore);
311
+ expect(expose.current.creation.step).toBe(1);
312
+ });
313
+ });