cdd-cli 3.2.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.~lock.ROADMAP.md# +1 -0
  2. package/CHANGELOG.md +51 -0
  3. package/README.es.md +198 -0
  4. package/README.md +81 -23
  5. package/ROADMAP.md +647 -0
  6. package/coverage/clover.xml +400 -519
  7. package/coverage/coverage-final.json +16 -20
  8. package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +400 -0
  9. package/coverage/lcov-report/ControlsHUD.jsx.html +256 -0
  10. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +31 -13
  11. package/coverage/lcov-report/components/PromptField.jsx.html +4 -4
  12. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +37 -13
  13. package/coverage/lcov-report/components/index.html +14 -14
  14. package/coverage/lcov-report/helpers/constants.js.html +87 -33
  15. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +40 -40
  16. package/coverage/lcov-report/helpers/dockerHubService.js.html +310 -0
  17. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +1 -1
  18. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +1 -1
  19. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +1 -1
  20. package/coverage/lcov-report/helpers/imageNameUtils.js.html +18 -18
  21. package/coverage/lcov-report/helpers/index.html +77 -62
  22. package/coverage/lcov-report/helpers/logger.js.html +60 -60
  23. package/coverage/lcov-report/helpers/safeCall.js.html +14 -14
  24. package/coverage/lcov-report/helpers/validationHelpers.js.html +52 -52
  25. package/coverage/lcov-report/hooks/creation/index.html +19 -19
  26. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +4 -4
  27. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +225 -48
  28. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +5 -5
  29. package/coverage/lcov-report/hooks/debug/index.html +1 -1
  30. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +7 -7
  31. package/coverage/lcov-report/hooks/index.html +15 -15
  32. package/coverage/lcov-report/hooks/navigation/index.html +1 -1
  33. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +6 -6
  34. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +13 -13
  35. package/coverage/lcov-report/hooks/useControls.js.html +82 -40
  36. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +5 -5
  37. package/coverage/lcov-report/hooks/useExitHandler.js.html +6 -6
  38. package/coverage/lcov-report/index.html +38 -68
  39. package/coverage/lcov.info +742 -981
  40. package/dist/App.js +7 -1
  41. package/dist/components/ContainerCreationPrompt.js +26 -10
  42. package/dist/components/ControlsHUD.js +106 -0
  43. package/dist/components/SuggestionPanel.js +16 -7
  44. package/dist/helpers/constants.js +60 -21
  45. package/dist/helpers/dockerHubService.js +111 -0
  46. package/dist/helpers/logger.js +1 -1
  47. package/dist/hooks/creation/useContainerActions.js +22 -5
  48. package/dist/hooks/creation/useContainerCreation.js +395 -107
  49. package/dist/hooks/useContainerCommandRouter.js +1 -1
  50. package/dist/hooks/useControls.js +23 -7
  51. package/dist/index.js +7 -0
  52. package/package.json +1 -1
  53. package/src/App.jsx +6 -0
  54. package/src/components/ContainerCreationPrompt.jsx +15 -5
  55. package/src/components/ControlsHUD.jsx +66 -0
  56. package/src/components/SuggestionPanel.jsx +10 -2
  57. package/src/helpers/constants.js +38 -20
  58. package/src/helpers/dockerHubService.js +75 -0
  59. package/src/helpers/logger.js +1 -2
  60. package/src/hooks/creation/useContainerActions.js +19 -6
  61. package/src/hooks/creation/useContainerCreation.js +224 -87
  62. package/src/hooks/useContainerCommandRouter.js +1 -1
  63. package/src/hooks/useControls.js +24 -8
  64. package/src/index.js +4 -0
  65. package/test/ContainerCreationPrompt.dom.test.js +245 -0
  66. package/test/ControlsHUD.dom.test.js +125 -0
  67. package/test/SuggestionPanel.dom.test.js +30 -0
  68. package/test/constants.test.js +7 -0
  69. package/test/dockerHubService.test.js +213 -0
  70. package/test/logger.test.js +58 -0
  71. package/test/resolveImageTag.test.js +26 -0
  72. package/test/useContainerCreation.dom.test.js +306 -3
  73. package/test/useContainerCreationHub.dom.test.js +184 -0
  74. package/test/useControls.dom.test.js +212 -6
@@ -38,8 +38,8 @@ await jest.unstable_mockModule(
38
38
 
39
39
  const { useControls } = await import('../src/hooks/useControls.js');
40
40
 
41
- function HookTester({ containers, expose }) {
42
- const hook = useControls(containers);
41
+ function HookTester({ containers, expose, overrides }) {
42
+ const hook = useControls(containers, overrides);
43
43
  useEffect(() => {
44
44
  if (expose) expose.current = hook;
45
45
  });
@@ -148,7 +148,7 @@ describe('useControls — step 0 keyboard routing integration', () => {
148
148
  act(() => { expose.current.creation.moveSuggestionSelection(1); });
149
149
  act(() => { expose.current.creation.applyFocusedSuggestion(); });
150
150
 
151
- expect(expose.current.creation.imageName).toBe('nginx');
151
+ expect(expose.current.creation.imageName).toBe('nginx:1.27-alpine');
152
152
  expect(expose.current.creation.step).toBe(0);
153
153
  expect(expose.current.creation.suggestions).toHaveLength(0);
154
154
  });
@@ -302,12 +302,218 @@ describe('useControls — FR4/FR5/FR6 keyboard routing via processCreationInput'
302
302
  const expose = renderAndStartCreation();
303
303
 
304
304
  act(() => { expose.current.creation.setImageName('nginx'); });
305
- const nameBefore = expose.current.creation.imageName;
306
305
 
307
306
  act(() => { triggerInput('\r', {}); }); // advance to step 1
308
307
 
309
- // imageName should be unchangedapplyFocusedSuggestion was NOT called
310
- expect(expose.current.creation.imageName).toBe(nameBefore);
308
+ // nextStep resolves the tagnginx becomes nginx:1.27-alpine; step advances to 1
309
+ // applyFocusedSuggestion was NOT called (no suggestion was focused)
310
+ expect(expose.current.creation.imageName).toBe('nginx:1.27-alpine');
311
311
  expect(expose.current.creation.step).toBe(1);
312
312
  });
313
313
  });
314
+
315
+ // ─────────────────────────────────────────────────────────────────────────────
316
+ // Tab on step 3 → insertNextSuggestedEnv
317
+ // ─────────────────────────────────────────────────────────────────────────────
318
+ describe('useControls — Tab on step 3 calls insertNextSuggestedEnv', () => {
319
+ beforeEach(() => {
320
+ _inputHandler = null;
321
+ mockSvcCreateContainer.mockReset();
322
+ });
323
+
324
+ test('Tab on step 3 calls insertNextSuggestedEnv()', () => {
325
+ const mockInsertNextSuggestedEnv = jest.fn();
326
+ const expose = { current: null };
327
+ render(
328
+ <HookTester
329
+ containers={[]}
330
+ expose={expose}
331
+ overrides={{ insertNextSuggestedEnv: mockInsertNextSuggestedEnv }}
332
+ />
333
+ );
334
+ // Enter creation mode
335
+ act(() => { triggerInput('c', {}); });
336
+ // Advance to step 3
337
+ act(() => { expose.current.creation.setImageName('postgres'); });
338
+ act(() => { triggerInput('\r', {}); }); // step 0 → 1
339
+ act(() => { triggerInput('\r', {}); }); // step 1 → 2
340
+ act(() => { triggerInput('\r', {}); }); // step 2 → 3
341
+ expect(expose.current.creation.step).toBe(3);
342
+
343
+ act(() => { triggerInput('', { tab: true }); });
344
+
345
+ expect(mockInsertNextSuggestedEnv).toHaveBeenCalledTimes(1);
346
+ });
347
+
348
+ test('Tab on step 3 when insertNextSuggestedEnv is undefined → no-op (safe guard)', () => {
349
+ const expose = { current: null };
350
+ render(
351
+ <HookTester
352
+ containers={[]}
353
+ expose={expose}
354
+ overrides={{ insertNextSuggestedEnv: undefined }}
355
+ />
356
+ );
357
+ act(() => { triggerInput('c', {}); });
358
+ act(() => { expose.current.creation.setImageName('nginx'); });
359
+ act(() => { triggerInput('\r', {}); });
360
+ act(() => { triggerInput('\r', {}); });
361
+ act(() => { triggerInput('\r', {}); });
362
+
363
+ // Should not throw
364
+ expect(() => {
365
+ act(() => { triggerInput('', { tab: true }); });
366
+ }).not.toThrow();
367
+ });
368
+ });
369
+ describe('useControls — FR7 Tab triggers hub search on step 0', () => {
370
+ beforeEach(() => {
371
+ _inputHandler = null;
372
+ mockSvcCreateContainer.mockReset();
373
+ });
374
+
375
+ /** Helper: render in creation mode with a triggerHubSearch mock override */
376
+ function renderCreationWithSearch({ isSearchingHub = false, imageName: initialImageName = '' } = {}) {
377
+ const mockTriggerHubSearch = jest.fn();
378
+ const expose = { current: null };
379
+
380
+ render(
381
+ <HookTester
382
+ containers={[]}
383
+ expose={expose}
384
+ overrides={{ triggerHubSearch: mockTriggerHubSearch, isSearchingHub }}
385
+ />
386
+ );
387
+ // Enter creation mode
388
+ act(() => { triggerInput('c', {}); });
389
+ // Set imageName if provided
390
+ if (initialImageName) {
391
+ act(() => { expose.current.creation.setImageName(initialImageName); });
392
+ }
393
+ return { expose, mockTriggerHubSearch };
394
+ }
395
+
396
+ test('FR7 — Tab on step 0 calls triggerHubSearch()', () => {
397
+ const { mockTriggerHubSearch } = renderCreationWithSearch({ imageName: 'nginx' });
398
+
399
+ act(() => { triggerInput('', { tab: true }); });
400
+
401
+ expect(mockTriggerHubSearch).toHaveBeenCalledTimes(1);
402
+ });
403
+
404
+ test('FR7 — Tab on step 0 when isSearchingHub=true does NOT call triggerHubSearch (guard)', () => {
405
+ const { mockTriggerHubSearch } = renderCreationWithSearch({
406
+ isSearchingHub: true,
407
+ imageName: 'nginx',
408
+ });
409
+
410
+ act(() => { triggerInput('', { tab: true }); });
411
+
412
+ expect(mockTriggerHubSearch).not.toHaveBeenCalled();
413
+ });
414
+
415
+ test('FR7 — Tab on step 0 when imageName is empty string does NOT call triggerHubSearch (guard)', () => {
416
+ const { mockTriggerHubSearch } = renderCreationWithSearch({ imageName: '' });
417
+
418
+ act(() => { triggerInput('', { tab: true }); });
419
+
420
+ expect(mockTriggerHubSearch).not.toHaveBeenCalled();
421
+ });
422
+
423
+ test('FR7 — Tab on step > 0 does NOT call triggerHubSearch (only active on step 0)', () => {
424
+ const { expose, mockTriggerHubSearch } = renderCreationWithSearch({ imageName: 'nginx' });
425
+
426
+ // Advance to step 1
427
+ act(() => { triggerInput('\r', {}); });
428
+ expect(expose.current.creation.step).toBe(1);
429
+
430
+ act(() => { triggerInput('', { tab: true }); });
431
+
432
+ expect(mockTriggerHubSearch).not.toHaveBeenCalled();
433
+ });
434
+ });
435
+
436
+ describe('useControls — success/error message clears after 4000ms', () => {
437
+
438
+ test('success message from container creation is scheduled to clear (setTimedMessage called)', async () => {
439
+ const timeoutSpy = jest.spyOn(global, 'setTimeout');
440
+ mockSvcCreateContainer.mockResolvedValue({ id: 'cid-timer', ports: [] });
441
+
442
+ const expose = { current: null };
443
+ render(<HookTester containers={[]} expose={expose} />);
444
+
445
+ await act(async () => {
446
+ await completeCreationWizard(expose, 'nginx');
447
+ });
448
+
449
+ // setTimedMessage must have scheduled a timeout to clear the message
450
+ const clearCallArgs = timeoutSpy.mock.calls.find(
451
+ ([fn, delay]) => delay === 4000
452
+ );
453
+ expect(clearCallArgs).toBeDefined();
454
+ timeoutSpy.mockRestore();
455
+ });
456
+
457
+ test('error message from container creation is scheduled to clear (setTimedMessage called)', async () => {
458
+ const timeoutSpy = jest.spyOn(global, 'setTimeout');
459
+ mockSvcCreateContainer.mockRejectedValue(new Error('not found'));
460
+
461
+ const expose = { current: null };
462
+ render(<HookTester containers={[]} expose={expose} />);
463
+
464
+ await act(async () => {
465
+ await completeCreationWizard(expose, 'badimage');
466
+ });
467
+
468
+ const clearCallArgs = timeoutSpy.mock.calls.find(
469
+ ([fn, delay]) => delay === 4000
470
+ );
471
+ expect(clearCallArgs).toBeDefined();
472
+ timeoutSpy.mockRestore();
473
+ });
474
+ });
475
+
476
+ describe('useControls — pressing C calls resetCreation BEFORE onStartCreate', () => {
477
+ beforeEach(() => {
478
+ _inputHandler = null;
479
+ mockSvcCreateContainer.mockReset();
480
+ });
481
+
482
+ test('pressing C resets creation state before entering creation mode', () => {
483
+ const callOrder = [];
484
+ const expose = { current: null };
485
+
486
+ // We render with no overrides, but we can spy on the internal calls
487
+ // by checking that imageName is reset (empty) when creatingContainer becomes true.
488
+ // Strategy: set imageName to something, then press C, verify state is reset in same render.
489
+ render(<HookTester containers={[]} expose={expose} />);
490
+
491
+ // Give the hook an existing state to verify it gets reset
492
+ act(() => { triggerInput('c', {}); }); // enter creation mode
493
+ act(() => { expose.current.creation.setImageName('dirty-state'); });
494
+
495
+ // Exit creation mode and re-enter via C to test reset order
496
+ act(() => {
497
+ // Press escape to cancel creation
498
+ if (_inputHandler) _inputHandler('', { escape: true });
499
+ });
500
+
501
+ // Now press C again — this should reset THEN show creation mode
502
+ act(() => { triggerInput('c', {}); });
503
+
504
+ // After pressing C: creatingContainer should be true AND imageName should be reset to ''
505
+ expect(expose.current.creatingContainer).toBe(true);
506
+ // imageName is '' (from resetCreation), NOT 'dirty-state'
507
+ expect(expose.current.creation.imageName).toBe('');
508
+ });
509
+
510
+ test('after pressing C, creation.message is set to the wizard prompt (resetCreation ran)', () => {
511
+ const expose = { current: null };
512
+ render(<HookTester containers={[]} expose={expose} />);
513
+
514
+ act(() => { triggerInput('c', {}); });
515
+
516
+ expect(expose.current.creatingContainer).toBe(true);
517
+ expect(expose.current.creation.message).toBe('Insert the name of the image to create: ');
518
+ });
519
+ });