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
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+ import { jest } from '@jest/globals';
5
+ import { formatHubResult } from '../src/helpers/dockerHubService.js';
6
+
7
+ // We mock fetch at the global level using jest.spyOn
8
+ // The module under test is imported dynamically after spy setup
9
+
10
+ describe('searchDockerHub — unit tests', () => {
11
+ let searchDockerHub;
12
+ let fetchSpy;
13
+
14
+ beforeEach(async () => {
15
+ // Reset modules so each test gets fresh state
16
+ jest.resetModules();
17
+ // Dynamically import after setting up spy
18
+ ({ searchDockerHub } = await import('../src/helpers/dockerHubService.js'));
19
+ });
20
+
21
+ afterEach(() => {
22
+ if (fetchSpy) fetchSpy.mockRestore();
23
+ fetchSpy = null;
24
+ });
25
+
26
+ // ── 1. Query trim & encode ──────────────────────────────────────────────
27
+
28
+ test('trims whitespace from query before encoding', async () => {
29
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
30
+ ok: true,
31
+ json: async () => ({ results: [] }),
32
+ });
33
+
34
+ await searchDockerHub(' nginx ');
35
+
36
+ const calledUrl = fetchSpy.mock.calls[0][0];
37
+ expect(calledUrl).toContain('query=nginx');
38
+ expect(calledUrl).not.toContain('query=+nginx');
39
+ expect(calledUrl).not.toContain(' ');
40
+ });
41
+
42
+ test('encodes special characters in query', async () => {
43
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
44
+ ok: true,
45
+ json: async () => ({ results: [] }),
46
+ });
47
+
48
+ await searchDockerHub('hello world');
49
+
50
+ const calledUrl = fetchSpy.mock.calls[0][0];
51
+ expect(calledUrl).toContain('query=hello%20world');
52
+ });
53
+
54
+ test('returns [] immediately for empty query (no fetch call)', async () => {
55
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
56
+ ok: true,
57
+ json: async () => ({ results: [] }),
58
+ });
59
+
60
+ const result = await searchDockerHub(' ');
61
+
62
+ expect(result).toEqual([]);
63
+ expect(fetchSpy).not.toHaveBeenCalled();
64
+ });
65
+
66
+ // ── 2. AbortSignal.timeout(5000) default ──────────────────────────────
67
+
68
+ test('uses AbortSignal.timeout(5000) by default when no signal provided', async () => {
69
+ const timeoutSignalSpy = jest.spyOn(AbortSignal, 'timeout');
70
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
71
+ ok: true,
72
+ json: async () => ({ results: [] }),
73
+ });
74
+
75
+ await searchDockerHub('nginx');
76
+
77
+ expect(timeoutSignalSpy).toHaveBeenCalledWith(5000);
78
+ timeoutSignalSpy.mockRestore();
79
+ });
80
+
81
+ test('does NOT call AbortSignal.timeout when signal is injected', async () => {
82
+ const timeoutSignalSpy = jest.spyOn(AbortSignal, 'timeout');
83
+ const controller = new AbortController();
84
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
85
+ ok: true,
86
+ json: async () => ({ results: [] }),
87
+ });
88
+
89
+ await searchDockerHub('nginx', { signal: controller.signal });
90
+
91
+ expect(timeoutSignalSpy).not.toHaveBeenCalled();
92
+ timeoutSignalSpy.mockRestore();
93
+ });
94
+
95
+ // ── 3. Injected signal is passed to fetch ──────────────────────────────
96
+
97
+ test('passes injected signal to fetch', async () => {
98
+ const controller = new AbortController();
99
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
100
+ ok: true,
101
+ json: async () => ({ results: [] }),
102
+ });
103
+
104
+ await searchDockerHub('nginx', { signal: controller.signal });
105
+
106
+ const fetchOptions = fetchSpy.mock.calls[0][1];
107
+ expect(fetchOptions.signal).toBe(controller.signal);
108
+ });
109
+
110
+ // ── 4. Result mapping ──────────────────────────────────────────────────
111
+
112
+ test('maps results to normalized objects with name, isOfficial, pullCount, description', async () => {
113
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
114
+ ok: true,
115
+ json: async () => ({
116
+ results: [
117
+ {
118
+ repo_name: 'library/nginx',
119
+ is_official: true,
120
+ pull_count: 1000000000,
121
+ short_description: 'Official nginx image',
122
+ },
123
+ {
124
+ repo_name: 'bitnami/nginx',
125
+ is_official: false,
126
+ pull_count: 50000000,
127
+ short_description: 'Bitnami nginx image',
128
+ },
129
+ ],
130
+ }),
131
+ });
132
+
133
+ const results = await searchDockerHub('nginx');
134
+
135
+ expect(results).toHaveLength(2);
136
+ expect(results[0]).toEqual({
137
+ name: 'library/nginx',
138
+ isOfficial: true,
139
+ pullCount: 1000000000,
140
+ description: 'Official nginx image',
141
+ });
142
+ expect(results[1]).toEqual({
143
+ name: 'bitnami/nginx',
144
+ isOfficial: false,
145
+ pullCount: 50000000,
146
+ description: 'Bitnami nginx image',
147
+ });
148
+ });
149
+
150
+ test('returns empty array when results is missing from response', async () => {
151
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
152
+ ok: true,
153
+ json: async () => ({}),
154
+ });
155
+
156
+ const results = await searchDockerHub('nginx');
157
+ expect(results).toEqual([]);
158
+ });
159
+
160
+ // ── 5. Non-OK rejection ────────────────────────────────────────────────
161
+
162
+ test('throws when response.ok is false', async () => {
163
+ fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
164
+ ok: false,
165
+ status: 429,
166
+ json: async () => ({}),
167
+ });
168
+
169
+ await expect(searchDockerHub('nginx')).rejects.toThrow(
170
+ 'Docker Hub search failed: 429'
171
+ );
172
+ });
173
+
174
+ test('re-throws AbortError from fetch', async () => {
175
+ const abortErr = new DOMException('Aborted', 'AbortError');
176
+ fetchSpy = jest.spyOn(global, 'fetch').mockRejectedValue(abortErr);
177
+
178
+ await expect(searchDockerHub('nginx')).rejects.toThrow('Aborted');
179
+ });
180
+ });
181
+
182
+ describe('formatHubResult — unit tests', () => {
183
+ test('official repo includes [★ official]', () => {
184
+ const result = formatHubResult({ name: 'library/nginx', isOfficial: true, pullCount: 0 });
185
+ expect(result).toContain('[★ official]');
186
+ expect(result).toContain('library/nginx');
187
+ });
188
+
189
+ test('high pull count (≥1B) is formatted as 1B+', () => {
190
+ const result = formatHubResult({ name: 'nginx', isOfficial: true, pullCount: 1e9 });
191
+ expect(result).toContain('[1B+ pulls]');
192
+ });
193
+
194
+ test('pull count ≥1M is formatted as NM+', () => {
195
+ const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 500e6 });
196
+ expect(result).toContain('[500M+ pulls]');
197
+ });
198
+
199
+ test('pull count ≥1K is formatted as NK+', () => {
200
+ const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 500e3 });
201
+ expect(result).toContain('[500K+ pulls]');
202
+ });
203
+
204
+ test('no pull count → just name (no suffix)', () => {
205
+ const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 0 });
206
+ expect(result).toBe('myimage');
207
+ });
208
+
209
+ test('unofficial + low pull count → just name with raw count', () => {
210
+ const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 42 });
211
+ expect(result).toBe('myimage [42 pulls]');
212
+ });
213
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Tests for src/helpers/logger.js
3
+ * Verifies that console output is suppressed when Ink listeners are active.
4
+ */
5
+ import { jest } from '@jest/globals';
6
+
7
+ const { logger } = await import('../src/helpers/logger.js');
8
+
9
+ describe('logger — console output suppression', () => {
10
+ let consoleSpy;
11
+ let warnSpy;
12
+
13
+ beforeEach(() => {
14
+ consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
15
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
16
+ });
17
+
18
+ afterEach(() => {
19
+ consoleSpy.mockRestore();
20
+ warnSpy.mockRestore();
21
+ });
22
+
23
+ test('when NO listeners are registered, logger.error calls console.error (fallback)', () => {
24
+ logger.error('test error no listener');
25
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
26
+ });
27
+
28
+ test('when a listener IS registered, logger.error does NOT call console.error', () => {
29
+ const unsubscribe = logger.subscribe(() => {});
30
+ logger.error('test error with listener');
31
+ expect(consoleSpy).not.toHaveBeenCalled();
32
+ unsubscribe();
33
+ });
34
+
35
+ test('when a listener IS registered, logger.warn does NOT call console.warn', () => {
36
+ const unsubscribe = logger.subscribe(() => {});
37
+ logger.warn('test warn with listener');
38
+ expect(warnSpy).not.toHaveBeenCalled();
39
+ unsubscribe();
40
+ });
41
+
42
+ test('after unsubscribing last listener, logger.error resumes calling console.error', () => {
43
+ const unsubscribe = logger.subscribe(() => {});
44
+ unsubscribe();
45
+ logger.error('after unsubscribe');
46
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
47
+ });
48
+
49
+ test('listener is invoked with the log entry even when console output is suppressed', () => {
50
+ const received = [];
51
+ const unsubscribe = logger.subscribe((entry) => received.push(entry));
52
+ logger.error('entry delivered', 'extra-arg');
53
+ expect(received).toHaveLength(1);
54
+ expect(received[0].level).toBe('error');
55
+ expect(received[0].message).toBe('entry delivered');
56
+ unsubscribe();
57
+ });
58
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+ import { resolveImageTag, IMAGE_PROFILES } from '../src/helpers/constants.js';
5
+
6
+ describe('resolveImageTag', () => {
7
+ test('appends defaultTag when image has no tag', () => {
8
+ expect(resolveImageTag('postgres', IMAGE_PROFILES)).toBe('postgres:17-alpine');
9
+ });
10
+
11
+ test('preserves explicit tag when image already has one', () => {
12
+ expect(resolveImageTag('postgres:15', IMAGE_PROFILES)).toBe('postgres:15');
13
+ });
14
+
15
+ test('returns image name unchanged when no matching profile', () => {
16
+ expect(resolveImageTag('unknownimage', IMAGE_PROFILES)).toBe('unknownimage');
17
+ });
18
+
19
+ test('appends defaultTag for nginx', () => {
20
+ expect(resolveImageTag('nginx', IMAGE_PROFILES)).toBe('nginx:1.27-alpine');
21
+ });
22
+
23
+ test('passes through empty string unchanged', () => {
24
+ expect(resolveImageTag('', IMAGE_PROFILES)).toBe('');
25
+ });
26
+ });