cdd-cli 4.0.0 → 4.1.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 (130) hide show
  1. package/README.es.md +29 -5
  2. package/README.md +29 -5
  3. package/dist/components/UsageMenu.js +1 -1
  4. package/dist/helpers/appState.js +26 -0
  5. package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
  6. package/dist/hooks/creation/useContainerCreation.js +25 -6
  7. package/dist/hooks/useContainerCommandRouter.js +12 -2
  8. package/dist/hooks/useControls.js +5 -0
  9. package/dist/hooks/useShellMode.js +101 -0
  10. package/dist/index.js +3 -1
  11. package/package.json +4 -1
  12. package/.atl/skill-registry.md +0 -30
  13. package/.editorconfig +0 -12
  14. package/.eslintrc.json +0 -30
  15. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  17. package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
  18. package/.github/workflows/ci.yml +0 -32
  19. package/.github/workflows/docs.yml +0 -42
  20. package/.prettierignore +0 -4
  21. package/.prettierrc +0 -8
  22. package/.~lock.ROADMAP.md# +0 -1
  23. package/CHANGELOG.md +0 -216
  24. package/CODE_OF_CONDUCT.md +0 -12
  25. package/CONTRIBUTING.md +0 -35
  26. package/ROADMAP.md +0 -647
  27. package/SECURITY.md +0 -5
  28. package/__mocks__/ink.cjs +0 -17
  29. package/_config.yml +0 -13
  30. package/babel.config.cjs +0 -23
  31. package/coverage/clover.xml +0 -575
  32. package/coverage/coverage-final.json +0 -17
  33. package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +0 -400
  34. package/coverage/lcov-report/ControlsHUD.jsx.html +0 -256
  35. package/coverage/lcov-report/base.css +0 -224
  36. package/coverage/lcov-report/block-navigation.js +0 -87
  37. package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -394
  38. package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
  39. package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -268
  40. package/coverage/lcov-report/components/index.html +0 -146
  41. package/coverage/lcov-report/favicon.png +0 -0
  42. package/coverage/lcov-report/helpers/constants.js.html +0 -352
  43. package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
  44. package/coverage/lcov-report/helpers/dockerHubService.js.html +0 -310
  45. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
  46. package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
  47. package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
  48. package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
  49. package/coverage/lcov-report/helpers/index.html +0 -206
  50. package/coverage/lcov-report/helpers/logger.js.html +0 -424
  51. package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
  52. package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
  53. package/coverage/lcov-report/hooks/creation/index.html +0 -146
  54. package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
  55. package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -982
  56. package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
  57. package/coverage/lcov-report/hooks/debug/index.html +0 -116
  58. package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
  59. package/coverage/lcov-report/hooks/index.html +0 -161
  60. package/coverage/lcov-report/hooks/navigation/index.html +0 -116
  61. package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
  62. package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
  63. package/coverage/lcov-report/hooks/useControls.js.html +0 -868
  64. package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
  65. package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
  66. package/coverage/lcov-report/index.html +0 -176
  67. package/coverage/lcov-report/prettify.css +0 -1
  68. package/coverage/lcov-report/prettify.js +0 -2
  69. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  70. package/coverage/lcov-report/sorter.js +0 -210
  71. package/coverage/lcov.info +0 -1163
  72. package/fix-imports.cjs +0 -54
  73. package/jest.config.cjs +0 -14
  74. package/src/App.jsx +0 -102
  75. package/src/components/ContainerCreationPrompt.jsx +0 -107
  76. package/src/components/ContainerList.jsx +0 -32
  77. package/src/components/ContainerRow.jsx +0 -84
  78. package/src/components/ContainerSection.jsx +0 -24
  79. package/src/components/ControlsHUD.jsx +0 -66
  80. package/src/components/Footer.jsx +0 -15
  81. package/src/components/Header.jsx +0 -41
  82. package/src/components/LogViewer.jsx +0 -36
  83. package/src/components/MessageFeedback.jsx +0 -25
  84. package/src/components/PromptField.jsx +0 -48
  85. package/src/components/StatsBar.jsx +0 -70
  86. package/src/components/SuggestionPanel.jsx +0 -61
  87. package/src/components/UsageMenu.jsx +0 -52
  88. package/src/helpers/appInfo.js +0 -23
  89. package/src/helpers/constants.js +0 -89
  90. package/src/helpers/containerOptionsBuilder.js +0 -53
  91. package/src/helpers/dockerHubService.js +0 -75
  92. package/src/helpers/dockerService/dockerService.js +0 -28
  93. package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
  94. package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
  95. package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
  96. package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
  97. package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
  98. package/src/helpers/exitWithMessage.js +0 -32
  99. package/src/helpers/imageNameUtils.js +0 -32
  100. package/src/helpers/logger.js +0 -112
  101. package/src/helpers/safeCall.js +0 -18
  102. package/src/helpers/validationHelpers.js +0 -87
  103. package/src/hooks/creation/useContainerActions.js +0 -89
  104. package/src/hooks/creation/useContainerCreation.js +0 -377
  105. package/src/hooks/creation/useLogsViewer.js +0 -51
  106. package/src/hooks/debug/useDebugLogs.js +0 -34
  107. package/src/hooks/navigation/useContainerSelection.js +0 -28
  108. package/src/hooks/useContainerCommandRouter.js +0 -123
  109. package/src/hooks/useContainerStats.js +0 -44
  110. package/src/hooks/useContainers.js +0 -26
  111. package/src/hooks/useControls.js +0 -263
  112. package/src/hooks/useEraseConfirmation.js +0 -39
  113. package/src/hooks/useExitHandler.js +0 -69
  114. package/src/index.js +0 -20
  115. package/test/ContainerCreationPrompt.dom.test.js +0 -313
  116. package/test/ControlsHUD.dom.test.js +0 -125
  117. package/test/SuggestionPanel.dom.test.js +0 -83
  118. package/test/constants.test.js +0 -41
  119. package/test/containerActions.test.js +0 -229
  120. package/test/containerList.test.js +0 -79
  121. package/test/containerOptionsBuilder.test.js +0 -36
  122. package/test/dockerHubService.test.js +0 -213
  123. package/test/jest.setup.js +0 -7
  124. package/test/logger.test.js +0 -58
  125. package/test/resolveImageTag.test.js +0 -26
  126. package/test/safeCall.test.js +0 -25
  127. package/test/useContainerCreation.dom.test.js +0 -567
  128. package/test/useContainerCreationHub.dom.test.js +0 -184
  129. package/test/useControls.dom.test.js +0 -519
  130. package/test/validationHelpers.test.js +0 -111
package/README.es.md CHANGED
@@ -6,9 +6,6 @@
6
6
  <a href="https://github.com/caertos/cdd/actions/workflows/ci.yml">
7
7
  <img src="https://github.com/caertos/cdd/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status" />
8
8
  </a>
9
- <a href="https://caertos.github.io/cdd/">
10
- <img src="https://img.shields.io/badge/docs-GitHub%20Pages-blue" alt="Docs" />
11
- </a>
12
9
  <a href="https://deepwiki.com/Caertos/cdd"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
13
10
  </p>
14
11
 
@@ -16,9 +13,21 @@
16
13
 
17
14
  ---
18
15
 
19
- ## 🎉 Novedades en v3.2
16
+ ## 🎉 Novedades en v4.1
17
+
18
+ **Abrí un shell dentro de cualquier contenedor con una sola tecla.**
19
+
20
+ Presiona `S` y CDD te lleva a un shell interactivo completo (`bash` o `sh`) dentro del contenedor seleccionado — sin necesidad de escribir `docker exec`.
21
+
22
+ - **Shell auto-detectado** — CDD sondea el contenedor y elige `bash` o `sh` automáticamente
23
+ - **Soporte de terminal completo** — ejecutá `psql`, `python3`, `node`, `redis-cli`, o cualquier comando dentro del contenedor
24
+ - **Salida limpia** — escribí `exit` o presioná `Ctrl+D` para volver al dashboard
25
+
26
+ ---
27
+
28
+ ## Versiones anteriores
20
29
 
21
- **v3.2 trae el asistente de creación interactivo — y cambia las reglas del juego.**
30
+ ### v3.2 Asistente de creación interactivo
22
31
 
23
32
  Olvídate de los flags de `docker run`, las variables de entorno olvidadas y los tags `:latest` que fallan en silencio. Presiona `C` y CDD te guía para crear un contenedor en segundos:
24
33
 
@@ -84,6 +93,7 @@ Usa `↑` / `↓` para navegar por los contenedores. Atajos de teclado disponibl
84
93
  | `R` | Reiniciar el contenedor seleccionado |
85
94
  | `C` | Abrir el asistente de creación |
86
95
  | `L` | Ver logs del contenedor seleccionado en tiempo real |
96
+ | `S` | Abrir shell interactivo dentro del contenedor seleccionado |
87
97
  | `E` | Eliminar el contenedor seleccionado — requiere confirmación |
88
98
  | `D` | Activar/desactivar panel de debug en vivo |
89
99
  | `Q` | Salir |
@@ -126,6 +136,20 @@ Presiona `Enter` en una línea vacía para terminar y crear el contenedor.
126
136
 
127
137
  ---
128
138
 
139
+ ## Shell Interactivo
140
+
141
+ Presiona `S` desde el dashboard con un contenedor en ejecución seleccionado. CDD hará lo siguiente:
142
+
143
+ 1. Detectará el shell disponible dentro del contenedor (`bash` o `sh`)
144
+ 2. Abrirá una sesión de terminal interactiva completa
145
+ 3. Te dejará en el shell del contenedor
146
+
147
+ Desde ahí podés ejecutar cualquier comando — `psql` para PostgreSQL, `python3` para Python, `node` para Node.js, `redis-cli` para Redis, etc.
148
+
149
+ Escribí `exit` o presioná `Ctrl+D` para salir del shell y volver al dashboard de CDD.
150
+
151
+ ---
152
+
129
153
  ## Requisitos
130
154
 
131
155
  - Node.js >= 18
package/README.md CHANGED
@@ -6,9 +6,6 @@
6
6
  <a href="https://github.com/caertos/cdd/actions/workflows/ci.yml">
7
7
  <img src="https://github.com/caertos/cdd/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status" />
8
8
  </a>
9
- <a href="https://caertos.github.io/cdd/">
10
- <img src="https://img.shields.io/badge/docs-GitHub%20Pages-blue" alt="Docs" />
11
- </a>
12
9
  <a href="https://deepwiki.com/Caertos/cdd"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
13
10
  </p>
14
11
 
@@ -16,9 +13,21 @@
16
13
 
17
14
  ---
18
15
 
19
- ## 🎉 What's new in v3.2
16
+ ## 🎉 What's new in v4.1
17
+
18
+ **Open a shell inside any container with a single keystroke.**
19
+
20
+ Press `S` and CDD drops you into a full interactive shell (`bash` or `sh`) inside the selected container — no `docker exec` typing needed.
21
+
22
+ - **Auto-detected shell** — CDD probes the container and picks `bash` or `sh` automatically
23
+ - **Full terminal support** — run `psql`, `python3`, `node`, `redis-cli`, or any command inside the container
24
+ - **Clean exit** — type `exit` or press `Ctrl+D` to return to the dashboard
25
+
26
+ ---
27
+
28
+ ## Previous releases
20
29
 
21
- **v3.2 ships the interactive creation wizard — and it's a game changer.**
30
+ ### v3.2 Interactive creation wizard
22
31
 
23
32
  Forget `docker run` flags, forgotten env vars, and broken `:latest` tags. Press `C` and CDD guides you through creating a container in seconds:
24
33
 
@@ -84,6 +93,7 @@ Use `↑` / `↓` to navigate containers. Available keyboard shortcuts:
84
93
  | `R` | Restart selected container |
85
94
  | `C` | Open creation wizard |
86
95
  | `L` | Stream logs for selected container |
96
+ | `S` | Open interactive shell inside selected container |
87
97
  | `E` | Erase (remove) selected container — confirmation required |
88
98
  | `D` | Toggle live debug panel |
89
99
  | `Q` | Quit |
@@ -126,6 +136,20 @@ Press `Enter` on an empty line to finish and create the container.
126
136
 
127
137
  ---
128
138
 
139
+ ## Interactive Shell
140
+
141
+ Press `S` from the dashboard while a running container is selected. CDD will:
142
+
143
+ 1. Detect the available shell inside the container (`bash` or `sh`)
144
+ 2. Open a full interactive terminal session
145
+ 3. Drop you into the container's shell
146
+
147
+ From there you can run any command — `psql` for PostgreSQL, `python3` for Python, `node` for Node.js, `redis-cli` for Redis, etc.
148
+
149
+ Type `exit` or press `Ctrl+D` to leave the shell and return to the CDD dashboard.
150
+
151
+ ---
152
+
129
153
  ## Requirements
130
154
 
131
155
  - Node.js >= 18
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
- var SHORTCUTS = ['Use ↑/↓ for navigation', 'I to initiate selected container', 'P to stop selected container', 'R to restart selected container', 'C to create a container', 'L to view logs of selected container', 'D to toggle debug log panel', 'E to remove selected container', 'Q to quit'];
3
+ var SHORTCUTS = ['Use ↑/↓ for navigation', 'I to initiate selected container', 'P to stop selected container', 'R to restart selected container', 'C to create a container', 'L to view logs of selected container', 'S to open shell in selected container', 'D to toggle debug log panel', 'E to remove selected container', 'Q to quit'];
4
4
  var GRID_COLUMNS = 3;
5
5
 
6
6
  /**
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared reference to the Ink render instance.
3
+ * Set once in index.js after render(<App />) and read by useShellMode
4
+ * to unmount/remount the UI around interactive shell sessions.
5
+ *
6
+ * @module appState
7
+ */
8
+
9
+ /** @type {import('ink').RenderReturnType | null} */
10
+ var inkApp = null;
11
+
12
+ /**
13
+ * Store the Ink render return value.
14
+ * @param {import('ink').RenderReturnType} app
15
+ */
16
+ export function setInkApp(app) {
17
+ inkApp = app;
18
+ }
19
+
20
+ /**
21
+ * Get the Ink render return value.
22
+ * @returns {import('ink').RenderReturnType | null}
23
+ */
24
+ export function getInkApp() {
25
+ return inkApp;
26
+ }
@@ -0,0 +1,211 @@
1
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
2
+ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
3
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
4
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
5
+ import { spawn } from 'child_process';
6
+ import { docker } from '../dockerService.js';
7
+ import { logger } from '../../logger.js';
8
+ import { TIMEOUTS } from '../../constants.js';
9
+
10
+ /**
11
+ * Detect the default shell available inside a running container.
12
+ * Tries `bash` first, falls back to `sh`.
13
+ *
14
+ * @param {string} containerId - Docker container id
15
+ * @returns {Promise<string>} The shell command (e.g. 'bash' or 'sh')
16
+ */
17
+ export function detectShell(_x) {
18
+ return _detectShell.apply(this, arguments);
19
+ }
20
+
21
+ /**
22
+ * Spawn an interactive shell inside a running container using `docker exec -it`.
23
+ * Returns a promise that resolves when the user exits the shell.
24
+ *
25
+ * @param {string} containerId - Docker container id
26
+ * @param {Object} [options]
27
+ * @param {string} [options.shell] - Shell to use (auto-detected if omitted)
28
+ * @param {string} [options.workingDir] - Working directory inside the container
29
+ * @returns {Promise<{code: number|null, signal: string|null}>} Exit info
30
+ */
31
+ function _detectShell() {
32
+ _detectShell = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(containerId) {
33
+ var container, tryShell, hasBash, _t;
34
+ return _regenerator().w(function (_context) {
35
+ while (1) switch (_context.p = _context.n) {
36
+ case 0:
37
+ container = docker.getContainer(containerId);
38
+ tryShell = function tryShell(shell) {
39
+ return new Promise(function (resolve) {
40
+ container.exec({
41
+ Cmd: ['which', shell],
42
+ AttachStdout: true,
43
+ AttachStderr: false,
44
+ Tty: false
45
+ }, function (err, exec) {
46
+ if (err) {
47
+ resolve(false);
48
+ return;
49
+ }
50
+ exec.start({
51
+ Tty: false
52
+ }, function (startErr, stream) {
53
+ if (startErr) {
54
+ resolve(false);
55
+ return;
56
+ }
57
+ var output = '';
58
+ stream.on('data', function (chunk) {
59
+ output += chunk.toString();
60
+ });
61
+ stream.on('end', function () {
62
+ resolve(output.trim().length > 0);
63
+ });
64
+ stream.on('error', function () {
65
+ return resolve(false);
66
+ });
67
+ });
68
+ });
69
+ });
70
+ };
71
+ _context.p = 1;
72
+ _context.n = 2;
73
+ return Promise.race([tryShell('bash'), new Promise(function (r) {
74
+ return setTimeout(function () {
75
+ return r(false);
76
+ }, TIMEOUTS.CONTAINER_OP);
77
+ })]);
78
+ case 2:
79
+ hasBash = _context.v;
80
+ if (!hasBash) {
81
+ _context.n = 3;
82
+ break;
83
+ }
84
+ logger.debug('Detected bash in container %s', containerId);
85
+ return _context.a(2, 'bash');
86
+ case 3:
87
+ _context.n = 5;
88
+ break;
89
+ case 4:
90
+ _context.p = 4;
91
+ _t = _context.v;
92
+ case 5:
93
+ logger.debug('Falling back to sh in container %s', containerId);
94
+ return _context.a(2, 'sh');
95
+ }
96
+ }, _callee, null, [[1, 4]]);
97
+ }));
98
+ return _detectShell.apply(this, arguments);
99
+ }
100
+ export function execInteractive(_x2) {
101
+ return _execInteractive.apply(this, arguments);
102
+ }
103
+
104
+ /**
105
+ * Execute a non-interactive command inside a running container.
106
+ * Returns stdout and stderr as strings.
107
+ *
108
+ * @param {string} containerId - Docker container id
109
+ * @param {string[]} cmd - Command to execute (e.g. ['ls', '-la'])
110
+ * @param {Object} [options]
111
+ * @param {number} [options.timeout] - Timeout in ms
112
+ * @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
113
+ */
114
+ function _execInteractive() {
115
+ _execInteractive = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(containerId) {
116
+ var options,
117
+ shell,
118
+ args,
119
+ _args2 = arguments,
120
+ _t2;
121
+ return _regenerator().w(function (_context2) {
122
+ while (1) switch (_context2.n) {
123
+ case 0:
124
+ options = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : {};
125
+ _t2 = options.shell;
126
+ if (_t2) {
127
+ _context2.n = 2;
128
+ break;
129
+ }
130
+ _context2.n = 1;
131
+ return detectShell(containerId);
132
+ case 1:
133
+ _t2 = _context2.v;
134
+ case 2:
135
+ shell = _t2;
136
+ args = ['exec', '-it', containerId, shell];
137
+ if (options.workingDir) {
138
+ args.splice(3, 0, '-w', options.workingDir);
139
+ }
140
+ logger.info('Opening interactive shell in container %s (shell=%s)', containerId, shell);
141
+ return _context2.a(2, new Promise(function (resolve, reject) {
142
+ var child = spawn('docker', args, {
143
+ stdio: 'inherit',
144
+ shell: false
145
+ });
146
+ child.on('error', function (err) {
147
+ logger.error('Failed to spawn docker exec', err);
148
+ reject(new Error("Failed to open shell: ".concat(err.message)));
149
+ });
150
+ child.on('close', function (code, signal) {
151
+ logger.info('Interactive shell exited (code=%s, signal=%s)', code, signal);
152
+ resolve({
153
+ code: code,
154
+ signal: signal
155
+ });
156
+ });
157
+ }));
158
+ }
159
+ }, _callee2);
160
+ }));
161
+ return _execInteractive.apply(this, arguments);
162
+ }
163
+ export function execCommand(containerId, cmd) {
164
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
165
+ return new Promise(function (resolve, reject) {
166
+ var container = docker.getContainer(containerId);
167
+ container.exec({
168
+ Cmd: cmd,
169
+ AttachStdout: true,
170
+ AttachStderr: true,
171
+ Tty: false
172
+ }, function (err, exec) {
173
+ if (err) {
174
+ return reject(new Error("Exec create failed: ".concat(err.message)));
175
+ }
176
+ exec.start({
177
+ Tty: false
178
+ }, function (startErr, stream) {
179
+ if (startErr) {
180
+ return reject(new Error("Exec start failed: ".concat(startErr.message)));
181
+ }
182
+ var stdout = '';
183
+ var stderr = '';
184
+ stream.on('data', function (chunk) {
185
+ var str = chunk.toString();
186
+ // Docker multiplexed streams use 8-byte headers.
187
+ // For Tty=false, output is multiplexed; for simplicity we accumulate.
188
+ stdout += str;
189
+ });
190
+ stream.on('end', function () {
191
+ exec.inspect(function (inspectErr, info) {
192
+ var _info$ExitCode;
193
+ resolve({
194
+ stdout: stdout.trim(),
195
+ stderr: stderr.trim(),
196
+ exitCode: (_info$ExitCode = info === null || info === void 0 ? void 0 : info.ExitCode) !== null && _info$ExitCode !== void 0 ? _info$ExitCode : -1
197
+ });
198
+ });
199
+ });
200
+ stream.on('error', function (streamErr) {
201
+ reject(new Error("Stream error: ".concat(streamErr.message)));
202
+ });
203
+ if (options.timeout) {
204
+ setTimeout(function () {
205
+ reject(new Error('Command timed out'));
206
+ }, options.timeout);
207
+ }
208
+ });
209
+ });
210
+ });
211
+ }
@@ -200,6 +200,25 @@ export function useContainerCreation(_ref) {
200
200
 
201
201
  // Timer for auto-clearing messages
202
202
  var messageTimerRef = useRef(null);
203
+
204
+ /**
205
+ * Sets a permanent message (e.g. step instructions). No auto-clear.
206
+ */
207
+ function setStepMessage(msg) {
208
+ var color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'yellow';
209
+ clearTimeout(messageTimerRef.current);
210
+ dispatch({
211
+ type: 'SET',
212
+ payload: {
213
+ message: msg,
214
+ messageColor: color
215
+ }
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Sets a transient feedback message (e.g. validation errors). Auto-clears after `ms`.
221
+ */
203
222
  function setTimedMessage(msg) {
204
223
  var color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'yellow';
205
224
  var ms = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 4000;
@@ -395,7 +414,7 @@ export function useContainerCreation(_ref) {
395
414
  step: 1
396
415
  }
397
416
  });
398
- setTimedMessage('Optional: Enter container name or leave empty and press Enter', 'yellow');
417
+ setStepMessage('Optional: Enter container name or leave empty and press Enter', 'yellow');
399
418
  return;
400
419
  }
401
420
  if (step === 1) {
@@ -405,7 +424,7 @@ export function useContainerCreation(_ref) {
405
424
  step: 2
406
425
  }
407
426
  });
408
- setTimedMessage('Optional: Enter ports (format 8080:80,443:443) or leave empty and press Enter', 'yellow');
427
+ setStepMessage('Optional: Enter ports (format 8080:80,443:443) or leave empty and press Enter', 'yellow');
409
428
  return;
410
429
  }
411
430
  if (step === 2) {
@@ -428,13 +447,13 @@ export function useContainerCreation(_ref) {
428
447
  var profile = imageProfiles[_baseName];
429
448
  if (profile && profile.requiredEnv && profile.requiredEnv.length) {
430
449
  var suggestedPart = profile.suggestedEnv && profile.suggestedEnv.length ? " | Suggested: ".concat(profile.suggestedEnv.join(', ')) : '';
431
- setTimedMessage("Required env vars for ".concat(_baseName, ": ").concat(profile.requiredEnv.join(', '), ". Enter as VAR=val,VAR2=val2").concat(suggestedPart), 'yellow');
450
+ setStepMessage("Required env vars for ".concat(_baseName, ": ").concat(profile.requiredEnv.join(', '), ". Enter as VAR=val,VAR2=val2").concat(suggestedPart), 'yellow');
432
451
  } else if (profile && profile.suggestedEnv && profile.suggestedEnv.length) {
433
- setTimedMessage("Suggested env vars for ".concat(_baseName, ": ").concat(profile.suggestedEnv.join(', '), ". Enter as VAR=val,VAR2=val2 or leave empty."), 'yellow');
452
+ setStepMessage("Suggested env vars for ".concat(_baseName, ": ").concat(profile.suggestedEnv.join(', '), ". Enter as VAR=val,VAR2=val2 or leave empty."), 'yellow');
434
453
  } else if (isDb) {
435
- setTimedMessage('Warning: This image usually requires environment variables (e.g. MYSQL_ROOT_PASSWORD=my-secret-pw for MySQL, POSTGRES_PASSWORD=yourpassword for Postgres). Enter them as VAR=val,VAR2=val2 or leave empty and press Enter.', 'yellow');
454
+ setStepMessage('Warning: This image usually requires environment variables (e.g. MYSQL_ROOT_PASSWORD=my-secret-pw for MySQL, POSTGRES_PASSWORD=yourpassword for Postgres). Enter them as VAR=val,VAR2=val2 or leave empty and press Enter.', 'yellow');
436
455
  } else {
437
- setTimedMessage('Optional: Enter environment variables (format VAR1=val1,VAR2=val2) or leave empty and press Enter', 'yellow');
456
+ setStepMessage('Optional: Enter environment variables (format VAR1=val1,VAR2=val2) or leave empty and press Enter', 'yellow');
438
457
  }
439
458
  return;
440
459
  }
@@ -16,6 +16,7 @@ import { useCallback } from 'react';
16
16
  * l — open logs viewer and start log stream
17
17
  * c — open container creation wizard (reset + activate)
18
18
  * d — toggle debug log panel
19
+ * s — open interactive shell in container
19
20
  *
20
21
  * @param {Object} params
21
22
  * @param {Object} params.actions - API from useContainerActions
@@ -27,6 +28,7 @@ import { useCallback } from 'react';
27
28
  * @param {Function} params.onStartErase - Callback: activates confirmErase state
28
29
  * @param {Function} params.onToggleDebug - Callback: toggles showDebugLogs state
29
30
  * @param {Function} params.onStartCreate - Callback: sets creatingContainer = true
31
+ * @param {Function} params.onOpenShell - Callback: opens interactive shell
30
32
  * @returns {{ handleDockerCommands: Function }}
31
33
  */
32
34
  export function useContainerCommandRouter(_ref) {
@@ -38,7 +40,8 @@ export function useContainerCommandRouter(_ref) {
38
40
  startLogsStream = _ref.startLogsStream,
39
41
  onStartErase = _ref.onStartErase,
40
42
  onToggleDebug = _ref.onToggleDebug,
41
- onStartCreate = _ref.onStartCreate;
43
+ onStartCreate = _ref.onStartCreate,
44
+ onOpenShell = _ref.onOpenShell;
42
45
  var handleDockerCommands = useCallback(function (input) {
43
46
  var container = containers[selected];
44
47
  if (input === 'i') {
@@ -143,8 +146,15 @@ export function useContainerCommandRouter(_ref) {
143
146
  onToggleDebug();
144
147
  return true;
145
148
  }
149
+ if (input === 's') {
150
+ if (!container) {
151
+ return true;
152
+ }
153
+ onOpenShell(container);
154
+ return true;
155
+ }
146
156
  return false;
147
- }, [actions, containers, creation, logsViewer, onStartCreate, onStartErase, onToggleDebug, selected, startLogsStream]);
157
+ }, [actions, containers, creation, logsViewer, onStartCreate, onStartErase, onToggleDebug, onOpenShell, selected, startLogsStream]);
148
158
  return {
149
159
  handleDockerCommands: handleDockerCommands
150
160
  };
@@ -22,6 +22,7 @@ import { useDebugLogs } from './debug/useDebugLogs.js';
22
22
  import { useEraseConfirmation } from './useEraseConfirmation.js';
23
23
  import { useExitHandler } from './useExitHandler.js';
24
24
  import { useContainerCommandRouter } from './useContainerCommandRouter.js';
25
+ import { useShellMode } from './useShellMode.js';
25
26
  import { getLogsStream } from '../helpers/dockerService/serviceComponents/containerLogs.js';
26
27
  import { createContainer as svcCreateContainer } from '../helpers/dockerService/serviceComponents/containerActions.js';
27
28
  import { buildContainerOptions } from '../helpers/containerOptionsBuilder.js';
@@ -48,6 +49,7 @@ export function useControls() {
48
49
  var actions = useContainerActions({
49
50
  containers: containers
50
51
  });
52
+ var shellMode = useShellMode();
51
53
  var creation = useContainerCreation({
52
54
  onCreate: function () {
53
55
  var _onCreate = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(_ref) {
@@ -176,6 +178,9 @@ export function useControls() {
176
178
  },
177
179
  onStartCreate: function onStartCreate() {
178
180
  return setCreatingContainer(true);
181
+ },
182
+ onOpenShell: function onOpenShell(container) {
183
+ return shellMode.openShell(container);
179
184
  }
180
185
  });
181
186
  var exitHandler = useExitHandler({
@@ -0,0 +1,101 @@
1
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
2
+ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
3
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
4
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
5
+ import React from 'react';
6
+ import { render } from 'ink';
7
+ import { execInteractive, detectShell } from '../helpers/dockerService/serviceComponents/containerExec.js';
8
+ import { getInkApp, setInkApp } from '../helpers/appState.js';
9
+ import App from '../App.js';
10
+ import { logger } from '../helpers/logger.js';
11
+
12
+ /**
13
+ * Open an interactive shell inside a Docker container.
14
+ * Unmounts Ink, spawns `docker exec -it`, and re-renders the app on exit.
15
+ *
16
+ * @param {Object} container - Container object from Docker
17
+ * @returns {Promise<void>}
18
+ */
19
+ function openShell(_x) {
20
+ return _openShell.apply(this, arguments);
21
+ }
22
+ /**
23
+ * Hook that provides the shell mode API.
24
+ * In practice, the heavy lifting (unmount/spawn/remount) is handled by
25
+ * `openShell` outside the React tree. This hook just exposes a stable
26
+ * reference for useControls.
27
+ *
28
+ * @returns {{ openShell: Function }}
29
+ */
30
+ function _openShell() {
31
+ _openShell = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(container) {
32
+ var isRunning, containerId, containerName, inkApp, shell, newApp, _t;
33
+ return _regenerator().w(function (_context) {
34
+ while (1) switch (_context.p = _context.n) {
35
+ case 0:
36
+ if (container) {
37
+ _context.n = 1;
38
+ break;
39
+ }
40
+ return _context.a(2);
41
+ case 1:
42
+ isRunning = container.state === 'running' || container.status === 'running';
43
+ if (isRunning) {
44
+ _context.n = 2;
45
+ break;
46
+ }
47
+ logger.warn('Cannot open shell: container %s is not running', container.name);
48
+ return _context.a(2);
49
+ case 2:
50
+ containerId = container.id;
51
+ containerName = container.name || containerId.slice(0, 12); // Unmount Ink to release the terminal
52
+ inkApp = getInkApp();
53
+ if (inkApp && typeof inkApp.unmount === 'function') {
54
+ inkApp.unmount();
55
+ }
56
+
57
+ // Clear the screen for a clean shell experience
58
+ if (process.stdout && process.stdout.isTTY) {
59
+ process.stdout.write("\x1Bc");
60
+ }
61
+ _context.p = 3;
62
+ _context.n = 4;
63
+ return detectShell(containerId);
64
+ case 4:
65
+ shell = _context.v;
66
+ process.stdout.write("\x1B[36m[shell]\x1B[0m Opening ".concat(shell, " in ").concat(containerName, " (").concat(containerId.slice(0, 12), ")...\r\n"));
67
+ process.stdout.write('\x1b[33mType "exit" or press Ctrl+D to return to CDD.\x1b[0m\r\n\r\n');
68
+ _context.n = 5;
69
+ return execInteractive(containerId, {
70
+ shell: shell
71
+ });
72
+ case 5:
73
+ _context.n = 7;
74
+ break;
75
+ case 6:
76
+ _context.p = 6;
77
+ _t = _context.v;
78
+ process.stdout.write("\x1B[31m[shell] Error: ".concat(_t.message, "\x1B[0m\r\n"));
79
+ logger.error('Shell mode error', _t);
80
+ case 7:
81
+ _context.n = 8;
82
+ return new Promise(function (r) {
83
+ return setTimeout(r, 100);
84
+ });
85
+ case 8:
86
+ // Re-render the app
87
+ console.clear();
88
+ newApp = render(/*#__PURE__*/React.createElement(App, null));
89
+ setInkApp(newApp);
90
+ case 9:
91
+ return _context.a(2);
92
+ }
93
+ }, _callee, null, [[3, 6]]);
94
+ }));
95
+ return _openShell.apply(this, arguments);
96
+ }
97
+ export function useShellMode() {
98
+ return {
99
+ openShell: openShell
100
+ };
101
+ }
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import React from 'react';
11
11
  import { render } from 'ink';
12
12
  import App from './App.js';
13
13
  import { logger } from './helpers/logger.js';
14
+ import { setInkApp } from './helpers/appState.js';
14
15
  process.on('uncaughtException', function (err) {
15
16
  return logger.error('Uncaught exception', err);
16
17
  });
@@ -18,4 +19,5 @@ process.on('unhandledRejection', function (reason) {
18
19
  return logger.error('Unhandled rejection', reason);
19
20
  });
20
21
  console.clear();
21
- render(/*#__PURE__*/React.createElement(App, null));
22
+ var inkApp = render(/*#__PURE__*/React.createElement(App, null));
23
+ setInkApp(inkApp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdd-cli",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "CLI Docker Dashboard",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -16,6 +16,9 @@
16
16
  "author": "",
17
17
  "license": "ISC",
18
18
  "type": "module",
19
+ "files": [
20
+ "dist"
21
+ ],
19
22
  "dependencies": {
20
23
  "chalk": "^5.6.2",
21
24
  "dockerode": "^4.0.9",