cdd-cli 3.1.3 → 3.1.5

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 (138) hide show
  1. package/.eslintrc.json +30 -0
  2. package/.github/workflows/docs.yml +35 -0
  3. package/CHANGELOG.md +44 -4
  4. package/FIXES_APPLIED.md +145 -147
  5. package/README.md +26 -0
  6. package/dist/App.js +20 -3
  7. package/dist/components/ContainerCreationPrompt.js +27 -1
  8. package/dist/components/ContainerList.js +11 -15
  9. package/dist/components/ContainerRow.js +73 -14
  10. package/dist/components/ContainerSection.js +16 -2
  11. package/dist/components/Footer.js +7 -1
  12. package/dist/components/Header.js +17 -1
  13. package/dist/components/LogViewer.js +21 -3
  14. package/dist/components/MessageFeedback.js +15 -1
  15. package/dist/components/PromptField.js +31 -1
  16. package/dist/components/StatsBar.js +46 -8
  17. package/dist/components/UsageMenu.js +30 -1
  18. package/dist/helpers/actionHelpers.js +17 -6
  19. package/dist/helpers/constants.js +30 -0
  20. package/dist/helpers/dockerService/dockerService.js +19 -0
  21. package/dist/helpers/dockerService/serviceComponents/containerActions.js +194 -35
  22. package/dist/helpers/dockerService/serviceComponents/containerList.js +45 -8
  23. package/dist/helpers/dockerService/serviceComponents/containerLogs.js +16 -6
  24. package/dist/helpers/dockerService/serviceComponents/containerStats.js +28 -9
  25. package/dist/helpers/exitWithMessage.js +2 -1
  26. package/dist/helpers/logger.js +112 -0
  27. package/dist/helpers/safeCall.js +21 -0
  28. package/dist/helpers/validationHelpers.js +20 -1
  29. package/dist/hooks/creation/useContainerActions.js +2 -1
  30. package/dist/hooks/creation/useContainerCreation.js +5 -4
  31. package/dist/hooks/creation/useLogsViewer.js +3 -2
  32. package/dist/hooks/useContainers.js +6 -3
  33. package/dist/hooks/useControls.js +253 -122
  34. package/dist/hooks/useLogsStream.js +3 -2
  35. package/dist/index.js +1 -0
  36. package/docs/components_ContainerCreationPrompt.jsx.html +114 -0
  37. package/docs/components_ContainerRow.jsx.html +151 -0
  38. package/docs/components_ContainerSection.jsx.html +75 -0
  39. package/docs/components_Footer.jsx.html +66 -0
  40. package/docs/components_Header.jsx.html +84 -0
  41. package/docs/components_LogViewer.jsx.html +91 -0
  42. package/docs/components_MessageFeedback.jsx.html +76 -0
  43. package/docs/components_PromptField.jsx.html +99 -0
  44. package/docs/components_StatsBar.jsx.html +105 -0
  45. package/docs/components_UsageMenu.jsx.html +73 -0
  46. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  47. package/docs/fonts/OpenSans-Bold-webfont.svg +1830 -0
  48. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  49. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  50. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  51. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  52. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  53. package/docs/fonts/OpenSans-Italic-webfont.svg +1830 -0
  54. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  55. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  56. package/docs/fonts/OpenSans-Light-webfont.svg +1831 -0
  57. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  58. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  59. package/docs/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  60. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  61. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  62. package/docs/fonts/OpenSans-Regular-webfont.svg +1831 -0
  63. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  64. package/docs/global.html +6145 -0
  65. package/docs/helpers_actionHelpers.js.html +94 -0
  66. package/docs/helpers_constants.js.html +81 -0
  67. package/docs/helpers_dockerService_dockerService.js.html +79 -0
  68. package/docs/helpers_dockerService_serviceComponents_containerActions.js.html +153 -0
  69. package/docs/helpers_dockerService_serviceComponents_containerList.js.html +81 -0
  70. package/docs/helpers_dockerService_serviceComponents_containerLogs.js.html +85 -0
  71. package/docs/helpers_dockerService_serviceComponents_containerStats.js.html +107 -0
  72. package/docs/helpers_dockerService_serviceComponents_imageUtils.js.html +82 -0
  73. package/docs/helpers_exitWithMessage.js.html +77 -0
  74. package/docs/helpers_safeCall.js.html +68 -0
  75. package/docs/helpers_validationHelpers.js.html +96 -0
  76. package/docs/hooks_creation_useContainerActions.js.html +120 -0
  77. package/docs/hooks_creation_useContainerCreation.js.html +152 -0
  78. package/docs/hooks_creation_useLogsViewer.js.html +102 -0
  79. package/docs/hooks_useControls.js.html +298 -0
  80. package/docs/hooks_useLogsStream.js.html +82 -0
  81. package/docs/index.html +65 -0
  82. package/docs/index.js.html +65 -0
  83. package/docs/module-index.html +171 -0
  84. package/docs/scripts/linenumber.js +25 -0
  85. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  86. package/docs/scripts/prettify/lang-css.js +2 -0
  87. package/docs/scripts/prettify/prettify.js +28 -0
  88. package/docs/styles/jsdoc-default.css +358 -0
  89. package/docs/styles/prettify-jsdoc.css +111 -0
  90. package/docs/styles/prettify-tomorrow.css +132 -0
  91. package/fix-imports.cjs +33 -6
  92. package/jest.config.cjs +2 -1
  93. package/package.json +19 -9
  94. package/src/App.jsx +16 -2
  95. package/src/components/ContainerCreationPrompt.jsx +26 -0
  96. package/src/components/ContainerList.jsx +12 -13
  97. package/src/components/ContainerRow.jsx +55 -20
  98. package/src/components/ContainerSection.jsx +15 -1
  99. package/src/components/Footer.jsx +6 -1
  100. package/src/components/Header.jsx +16 -0
  101. package/src/components/LogViewer.jsx +19 -2
  102. package/src/components/MessageFeedback.jsx +14 -0
  103. package/src/components/PromptField.jsx +30 -0
  104. package/src/components/StatsBar.jsx +47 -8
  105. package/src/components/UsageMenu.jsx +38 -8
  106. package/src/helpers/actionHelpers.js +16 -7
  107. package/src/helpers/constants.js +30 -0
  108. package/src/helpers/dockerService/dockerService.js +19 -0
  109. package/src/helpers/dockerService/serviceComponents/containerActions.js +123 -12
  110. package/src/helpers/dockerService/serviceComponents/containerList.js +61 -23
  111. package/src/helpers/dockerService/serviceComponents/containerLogs.js +20 -6
  112. package/src/helpers/dockerService/serviceComponents/containerStats.js +24 -5
  113. package/src/helpers/dockerService/serviceComponents/imageUtils.js +1 -1
  114. package/src/helpers/exitWithMessage.js +2 -1
  115. package/src/helpers/logger.js +113 -0
  116. package/src/helpers/safeCall.js +18 -0
  117. package/src/helpers/validationHelpers.js +21 -3
  118. package/src/hooks/creation/useContainerActions.js +4 -3
  119. package/src/hooks/creation/useContainerCreation.js +5 -4
  120. package/src/hooks/creation/useLogsViewer.js +2 -1
  121. package/src/hooks/useContainers.js +6 -3
  122. package/src/hooks/useControls.js +244 -91
  123. package/src/hooks/useLogsStream.js +2 -1
  124. package/src/index.js +1 -0
  125. package/test/actionHelpers.test.js +106 -0
  126. package/test/containerActions.test.js +138 -0
  127. package/test/containerList.test.js +79 -0
  128. package/test/jest.setup.js +7 -0
  129. package/test/safeCall.test.js +25 -0
  130. package/test/useContainerCreation.dom.test.js +97 -0
  131. package/test/validationHelpers.test.js +2 -2
  132. package/AUDIT_REPORT.md +0 -1004
  133. package/dist/cdd.bundle.js +0 -8
  134. package/dist/components/ContainerCreator.js +0 -81
  135. package/dist/components/StatsViewer.js +0 -0
  136. package/dist/helpers/dockerActions.js +0 -64
  137. package/dist/helpers/dockerService.js +0 -284
  138. package/dist/hooks/useStatsPolling.js +0 -54
@@ -0,0 +1,138 @@
1
+ import { jest } from '@jest/globals';
2
+
3
+ describe('containerActions service functions (mocked ESM imports)', () => {
4
+ afterEach(() => jest.resetModules());
5
+
6
+ test('createContainer returns id when image exists', async () => {
7
+ // mock imageUtils and dockerService before importing the module under test
8
+ const imageUtilsMock = {
9
+ imageExists: jest.fn().mockResolvedValue(true),
10
+ pullImage: jest.fn()
11
+ };
12
+
13
+ const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
14
+ const dockerMock = {
15
+ createContainer: jest.fn().mockResolvedValue({ id: 'cid-123' }),
16
+ getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
17
+ listContainers: jest.fn().mockResolvedValue([])
18
+ };
19
+
20
+ await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
21
+ ...imageUtilsMock
22
+ }));
23
+
24
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
25
+ docker: dockerMock
26
+ }));
27
+
28
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
29
+
30
+ const id = await mod.createContainer('nginx:latest', { Name: 'test' });
31
+ expect(id).toBe('cid-123');
32
+ const imageUtils = await import('../src/helpers/dockerService/serviceComponents/imageUtils.js');
33
+ expect(imageUtils.imageExists).toHaveBeenCalledWith('nginx:latest');
34
+ const ds = await import('../src/helpers/dockerService/dockerService.js');
35
+ expect(ds.docker.createContainer).toHaveBeenCalled();
36
+ expect(ds.docker.getImage).toHaveBeenCalledWith('nginx:latest');
37
+ });
38
+
39
+ test('createContainer pulls image when not present and returns Id', async () => {
40
+ const imageUtilsMock = {
41
+ imageExists: jest.fn().mockResolvedValue(false),
42
+ pullImage: jest.fn().mockResolvedValue(true)
43
+ };
44
+
45
+ const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
46
+ const dockerMock = {
47
+ createContainer: jest.fn().mockResolvedValue({ Id: 'CID-456' }),
48
+ getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
49
+ listContainers: jest.fn().mockResolvedValue([])
50
+ };
51
+
52
+ await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
53
+ ...imageUtilsMock
54
+ }));
55
+
56
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
57
+ docker: dockerMock
58
+ }));
59
+
60
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
61
+ const id = await mod.createContainer('busybox:1.0');
62
+ expect(id).toBe('CID-456');
63
+ const imageUtils = await import('../src/helpers/dockerService/serviceComponents/imageUtils.js');
64
+ expect(imageUtils.pullImage).toHaveBeenCalledWith('busybox:1.0');
65
+ });
66
+
67
+ test('createContainer auto maps exposed ports when ports not provided', async () => {
68
+ const imageUtilsMock = {
69
+ imageExists: jest.fn().mockResolvedValue(true),
70
+ pullImage: jest.fn()
71
+ };
72
+
73
+ const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '80/tcp': {} } } });
74
+ const createContainerMock = jest.fn().mockResolvedValue({ id: 'cid-auto' });
75
+ const dockerMock = {
76
+ createContainer: createContainerMock,
77
+ getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
78
+ listContainers: jest.fn().mockResolvedValue([
79
+ { Ports: [{ PublicPort: 80, PrivatePort: 80 }] }
80
+ ])
81
+ };
82
+
83
+ await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
84
+ ...imageUtilsMock
85
+ }));
86
+
87
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
88
+ docker: dockerMock
89
+ }));
90
+
91
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
92
+ await mod.createContainer('nginx:alpine');
93
+
94
+ const [[createArgs]] = createContainerMock.mock.calls;
95
+ expect(createArgs.ExposedPorts).toEqual({ '80/tcp': {} });
96
+ expect(createArgs.HostConfig.PortBindings['80/tcp']).toEqual([{ HostPort: '81' }]);
97
+ expect(dockerMock.listContainers).toHaveBeenCalled();
98
+ });
99
+
100
+ test('removeContainer calls docker.remove with force', async () => {
101
+ const removeMock = jest.fn().mockResolvedValue(undefined);
102
+ const dockerMock = {
103
+ getContainer: jest.fn().mockReturnValue({ remove: removeMock })
104
+ };
105
+
106
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({ docker: dockerMock }));
107
+ await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({ imageExists: jest.fn(), pullImage: jest.fn() }));
108
+
109
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
110
+ await mod.removeContainer('abc');
111
+ const ds = await import('../src/helpers/dockerService/dockerService.js');
112
+ expect(ds.docker.getContainer).toHaveBeenCalledWith('abc');
113
+ expect(removeMock).toHaveBeenCalledWith({ force: true });
114
+ });
115
+
116
+ test('start/stop/restart call respective container methods', async () => {
117
+ const startMock = jest.fn().mockResolvedValue(undefined);
118
+ const stopMock = jest.fn().mockResolvedValue(undefined);
119
+ const restartMock = jest.fn().mockResolvedValue(undefined);
120
+
121
+ const dockerMock = {
122
+ getContainer: jest.fn().mockReturnValue({ start: startMock, stop: stopMock, restart: restartMock })
123
+ };
124
+
125
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({ docker: dockerMock }));
126
+ await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({ imageExists: jest.fn(), pullImage: jest.fn() }));
127
+
128
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
129
+
130
+ await mod.startContainer('c1');
131
+ await mod.stopContainer('c1');
132
+ await mod.restartContainer('c1');
133
+
134
+ expect(startMock).toHaveBeenCalled();
135
+ expect(stopMock).toHaveBeenCalled();
136
+ expect(restartMock).toHaveBeenCalled();
137
+ });
138
+ });
@@ -0,0 +1,79 @@
1
+ import { jest } from '@jest/globals';
2
+
3
+ describe('containerList.getContainers', () => {
4
+ afterEach(() => jest.resetModules());
5
+
6
+ const baseContainer = {
7
+ Id: 'abc123',
8
+ Image: 'nginx:latest',
9
+ State: 'running',
10
+ Status: 'Up 5 minutes',
11
+ Ports: []
12
+ };
13
+
14
+ test('falls back to container.Name when Names array is missing', async () => {
15
+ const dockerMock = {
16
+ listContainers: jest.fn().mockResolvedValue([
17
+ {
18
+ ...baseContainer,
19
+ Names: undefined,
20
+ Name: '/custom-container'
21
+ }
22
+ ])
23
+ };
24
+
25
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
26
+ docker: dockerMock
27
+ }));
28
+
29
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
30
+ const containers = await mod.getContainers();
31
+
32
+ expect(containers).toHaveLength(1);
33
+ expect(containers[0].name).toBe('custom-container');
34
+ });
35
+
36
+ test('returns Unknown when no usable name data is present', async () => {
37
+ const dockerMock = {
38
+ listContainers: jest.fn().mockResolvedValue([
39
+ {
40
+ ...baseContainer,
41
+ Names: [],
42
+ Name: '',
43
+ Id: 'def456'
44
+ }
45
+ ])
46
+ };
47
+
48
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
49
+ docker: dockerMock
50
+ }));
51
+
52
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
53
+ const containers = await mod.getContainers();
54
+
55
+ expect(containers).toHaveLength(1);
56
+ expect(containers[0].name).toBe('Unknown');
57
+ });
58
+
59
+ test('normalizes first entry in Names array', async () => {
60
+ const dockerMock = {
61
+ listContainers: jest.fn().mockResolvedValue([
62
+ {
63
+ ...baseContainer,
64
+ Names: ['///leading/slash', '/ignored-second']
65
+ }
66
+ ])
67
+ };
68
+
69
+ await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
70
+ docker: dockerMock
71
+ }));
72
+
73
+ const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
74
+ const containers = await mod.getContainers();
75
+
76
+ expect(containers).toHaveLength(1);
77
+ expect(containers[0].name).toBe('leading/slash');
78
+ });
79
+ });
@@ -0,0 +1,7 @@
1
+ // Minimal test setup for React/DOM environment
2
+ // Could add more global matchers here
3
+ import '@testing-library/jest-dom';
4
+
5
+ // Silence act() warnings in environments where receiver isn't concurrent
6
+ // (react-test-renderer/act warnings are printed, but tests can proceed)
7
+ // If needed, configure global helpers here.
@@ -0,0 +1,25 @@
1
+ import { safeCall } from '../src/helpers/safeCall.js';
2
+
3
+ describe('safeCall', () => {
4
+ test('calls function and returns value', () => {
5
+ function fn(a, b) {
6
+ fn.called = true;
7
+ fn.args = [a, b];
8
+ return a + b;
9
+ }
10
+ const res = safeCall(fn, 2, 3);
11
+ expect(fn.called).toBe(true);
12
+ expect(fn.args).toEqual([2, 3]);
13
+ expect(res).toBe(5);
14
+ });
15
+
16
+ test('returns undefined when fn is not a function', () => {
17
+ const res = safeCall(undefined, 1, 2);
18
+ expect(res).toBeUndefined();
19
+ });
20
+
21
+ test('catches errors thrown by function and does not rethrow', () => {
22
+ const bad = () => { throw new Error('boom'); };
23
+ expect(() => safeCall(bad)).not.toThrow();
24
+ });
25
+ });
@@ -0,0 +1,97 @@
1
+ import React, { useEffect } from 'react';
2
+ import { render, act } from '@testing-library/react';
3
+ import { useContainerCreation } from '../src/hooks/creation/useContainerCreation.js';
4
+
5
+ function HookTester({ onCreate, onCancel, dbImages, expose }) {
6
+ const hook = useContainerCreation({ onCreate, onCancel, dbImages });
7
+ // keep exposing the latest hook values on every render
8
+ useEffect(() => {
9
+ if (expose) expose.current = hook;
10
+ });
11
+ return null;
12
+ }
13
+
14
+ describe('useContainerCreation (DOM render)', () => {
15
+ test('image empty shows error and does not advance', () => {
16
+ const expose = { current: null };
17
+ render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} expose={expose} />);
18
+
19
+ expect(expose.current.step).toBe(0);
20
+
21
+ act(() => {
22
+ expose.current.nextStep();
23
+ });
24
+
25
+ expect(expose.current.message).toBe('Image name cannot be empty.');
26
+ expect(expose.current.step).toBe(0);
27
+ });
28
+
29
+ test('ports validation: invalid ports shows error', () => {
30
+ const expose = { current: null };
31
+ render(<HookTester onCreate={() => {}} onCancel={() => {}} dbImages={[]} expose={expose} />);
32
+
33
+ act(() => {
34
+ expose.current.setImageName('nginx');
35
+ });
36
+
37
+ act(() => {
38
+ expose.current.nextStep(); // to name
39
+ });
40
+
41
+ act(() => {
42
+ expose.current.nextStep(); // to ports
43
+ });
44
+
45
+ expect(expose.current.step).toBe(2);
46
+
47
+ act(() => {
48
+ expose.current.setPortInput('notaport');
49
+ });
50
+
51
+ act(() => {
52
+ expose.current.nextStep();
53
+ });
54
+
55
+ expect(expose.current.message).toBe('Port format must be host:container and both must be numbers (e.g. 8080:80)');
56
+ expect(expose.current.step).toBe(2);
57
+ });
58
+
59
+ test('complete flow calls onCreate with data', () => {
60
+ const created = [];
61
+ const onCreate = (data) => created.push(data);
62
+ const expose = { current: null };
63
+
64
+ render(<HookTester onCreate={onCreate} onCancel={() => {}} dbImages={[]} expose={expose} />);
65
+
66
+ act(() => {
67
+ expose.current.setImageName('redis');
68
+ });
69
+
70
+ act(() => {
71
+ expose.current.nextStep(); // to name
72
+ });
73
+
74
+ act(() => {
75
+ expose.current.nextStep(); // to ports
76
+ });
77
+
78
+ act(() => {
79
+ expose.current.setPortInput('');
80
+ });
81
+
82
+ act(() => {
83
+ expose.current.nextStep(); // to env
84
+ });
85
+
86
+ act(() => {
87
+ expose.current.setEnvInput('FOO=bar');
88
+ });
89
+
90
+ act(() => {
91
+ expose.current.nextStep(); // final create
92
+ });
93
+
94
+ expect(created.length).toBe(1);
95
+ expect(created[0]).toEqual({ imageName: 'redis', containerName: '', portInput: '', envInput: 'FOO=bar' });
96
+ });
97
+ });
@@ -24,8 +24,8 @@ describe('validatePorts', () => {
24
24
  expect(validatePorts('eighty:80')).toBe(false);
25
25
  });
26
26
 
27
- test('empty input returns false (must specify at least one port)', () => {
28
- expect(validatePorts('')).toBe(false);
27
+ test('empty input is valid (ports are optional)', () => {
28
+ expect(validatePorts('')).toBe(true);
29
29
  });
30
30
  });
31
31