cdd-cli 4.1.0 → 4.2.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.
package/README.es.md CHANGED
@@ -3,9 +3,6 @@
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/npm/v/cdd-cli?color=blue&label=npm%20package" alt="npm version"/>
5
5
  <img src="https://img.shields.io/npm/dt/cdd-cli?color=green&label=downloads" alt="npm downloads"/>
6
- <a href="https://github.com/caertos/cdd/actions/workflows/ci.yml">
7
- <img src="https://github.com/caertos/cdd/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status" />
8
- </a>
9
6
  <a href="https://deepwiki.com/Caertos/cdd"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
10
7
  </p>
11
8
 
@@ -13,20 +10,33 @@
13
10
 
14
11
  ---
15
12
 
16
- ## 🎉 Novedades en v4.1
13
+ ## 🎉 Novedades en v4.2
17
14
 
18
- **Abrí un shell dentro de cualquier contenedor con una sola tecla.**
15
+ **Campos de texto editables con cursor visible en el asistente de creación.**
19
16
 
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`.
17
+ Ahora los campos de texto del asistente se comportan como un campo de texto normal: un cursor que se ve, que se mueve con las flechas, que inserta y borra donde está.
21
18
 
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
19
+ - **Navegación con cursor** — `←` / `→` mueven el cursor dentro del campo
20
+ - **Inicio / Fin** — saltan al principio o al final del texto
21
+ - **Edición completa** — `Backspace` y `Delete` funcionan en cualquier posición
22
+ - **Atajos de teclado** — `Ctrl+A/E/W/U/K` para edición rápida
23
+ - **Pegado** — pegar texto lo inserta completo en la posición del cursor
24
+ - **Acentos y emojis** — manejo correcto por punto de código
25
25
 
26
26
  ---
27
27
 
28
28
  ## Versiones anteriores
29
29
 
30
+ ### v4.1 — Shell interactivo
31
+
32
+ **Abre un shell dentro de cualquier contenedor con una sola tecla.**
33
+
34
+ Presiona `S` y CDD te lleva a un shell interactivo completo (`bash` o `sh`) dentro del contenedor seleccionado — sin necesidad de escribir `docker exec`.
35
+
36
+ - **Shell auto-detectado** — CDD sondea el contenedor y elige `bash` o `sh` automáticamente
37
+ - **Soporte de terminal completo** — ejecuta `psql`, `python3`, `node`, `redis-cli`, o cualquier comando dentro del contenedor
38
+ - **Salida limpia** — escribe `exit` o presiona `Ctrl+D` para volver al dashboard
39
+
30
40
  ### v3.2 — Asistente de creación interactivo
31
41
 
32
42
  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:
@@ -85,18 +95,18 @@ cdd
85
95
 
86
96
  Usa `↑` / `↓` para navegar por los contenedores. Atajos de teclado disponibles:
87
97
 
88
- | Tecla | Acción |
89
- |-------|--------|
90
- | `↑` / `↓` | Navegar la lista de contenedores |
91
- | `I` | Iniciar el contenedor seleccionado |
92
- | `P` | Detener el contenedor seleccionado |
93
- | `R` | Reiniciar el contenedor seleccionado |
94
- | `C` | Abrir el asistente de creación |
95
- | `L` | Ver logs del contenedor seleccionado en tiempo real |
96
- | `S` | Abrir shell interactivo dentro del contenedor seleccionado |
97
- | `E` | Eliminar el contenedor seleccionado — requiere confirmación |
98
- | `D` | Activar/desactivar panel de debug en vivo |
99
- | `Q` | Salir |
98
+ | Tecla | Acción |
99
+ | --------- | ----------------------------------------------------------- |
100
+ | `↑` / `↓` | Navegar la lista de contenedores |
101
+ | `I` | Iniciar el contenedor seleccionado |
102
+ | `P` | Detener el contenedor seleccionado |
103
+ | `R` | Reiniciar el contenedor seleccionado |
104
+ | `C` | Abrir el asistente de creación |
105
+ | `L` | Ver logs del contenedor seleccionado en tiempo real |
106
+ | `S` | Abrir shell interactivo dentro del contenedor seleccionado |
107
+ | `E` | Eliminar el contenedor seleccionado — requiere confirmación |
108
+ | `D` | Activar/desactivar panel de debug en vivo |
109
+ | `Q` | Salir |
100
110
 
101
111
  ---
102
112
 
@@ -124,13 +134,13 @@ Ingresa un mapeo de puertos en formato `HOST:CONTENEDOR`, por ejemplo `8080:80`,
124
134
 
125
135
  Ingresa pares `CLAVE=VALOR` de a uno. Las **sugerencias contextuales** muestran variables recomendadas para la imagen seleccionada:
126
136
 
127
- | Imagen | Variables sugeridas |
128
- |--------|---------------------|
129
- | postgres | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` |
130
- | mysql | `MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE` |
131
- | redis | *(sin variables requeridas)* |
132
- | mongo | `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` |
133
- | node / nginx / otros | Variables de runtime comunes según corresponda |
137
+ | Imagen | Variables sugeridas |
138
+ | -------------------- | ---------------------------------------------------------- |
139
+ | postgres | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` |
140
+ | mysql | `MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE` |
141
+ | redis | _(sin variables requeridas)_ |
142
+ | mongo | `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` |
143
+ | node / nginx / otros | Variables de runtime comunes según corresponda |
134
144
 
135
145
  Presiona `Enter` en una línea vacía para terminar y crear el contenedor.
136
146
 
@@ -144,9 +154,9 @@ Presiona `S` desde el dashboard con un contenedor en ejecución seleccionado. CD
144
154
  2. Abrirá una sesión de terminal interactiva completa
145
155
  3. Te dejará en el shell del contenedor
146
156
 
147
- Desde ahí podés ejecutar cualquier comando — `psql` para PostgreSQL, `python3` para Python, `node` para Node.js, `redis-cli` para Redis, etc.
157
+ Desde ahí puedes ejecutar cualquier comando — `psql` para PostgreSQL, `python3` para Python, `node` para Node.js, `redis-cli` para Redis, etc.
148
158
 
149
- Escribí `exit` o presioná `Ctrl+D` para salir del shell y volver al dashboard de CDD.
159
+ Escribe `exit` o presiona `Ctrl+D` para salir del shell y volver al dashboard de CDD.
150
160
 
151
161
  ---
152
162
 
package/README.md CHANGED
@@ -3,9 +3,6 @@
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/npm/v/cdd-cli?color=blue&label=npm%20package" alt="npm version"/>
5
5
  <img src="https://img.shields.io/npm/dt/cdd-cli?color=green&label=downloads" alt="npm downloads"/>
6
- <a href="https://github.com/caertos/cdd/actions/workflows/ci.yml">
7
- <img src="https://github.com/caertos/cdd/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status" />
8
- </a>
9
6
  <a href="https://deepwiki.com/Caertos/cdd"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
10
7
  </p>
11
8
 
@@ -13,7 +10,24 @@
13
10
 
14
11
  ---
15
12
 
16
- ## 🎉 What's new in v4.1
13
+ ## 🎉 What's new in v4.2
14
+
15
+ **Editable text fields with visible cursor in the creation wizard.**
16
+
17
+ Text fields in the wizard now behave like a proper text input: a visible cursor that moves with arrow keys, inserts and deletes where it stands.
18
+
19
+ - **Cursor navigation** — `←` / `→` move the cursor within the field
20
+ - **Home / End** — jump to the start or end of the text
21
+ - **Full editing** — `Backspace` and `Delete` work at any cursor position
22
+ - **Keyboard shortcuts** — `Ctrl+A/E/W/U/K` for fast text editing
23
+ - **Paste support** — pasted text inserts at the cursor position
24
+ - **Accents and emojis** — correct handling via code-point segmentation
25
+
26
+ ---
27
+
28
+ ## Previous releases
29
+
30
+ ### v4.1 — Interactive shell
17
31
 
18
32
  **Open a shell inside any container with a single keystroke.**
19
33
 
@@ -23,10 +37,6 @@ Press `S` and CDD drops you into a full interactive shell (`bash` or `sh`) insid
23
37
  - **Full terminal support** — run `psql`, `python3`, `node`, `redis-cli`, or any command inside the container
24
38
  - **Clean exit** — type `exit` or press `Ctrl+D` to return to the dashboard
25
39
 
26
- ---
27
-
28
- ## Previous releases
29
-
30
40
  ### v3.2 — Interactive creation wizard
31
41
 
32
42
  Forget `docker run` flags, forgotten env vars, and broken `:latest` tags. Press `C` and CDD guides you through creating a container in seconds:
@@ -85,18 +95,18 @@ cdd
85
95
 
86
96
  Use `↑` / `↓` to navigate containers. Available keyboard shortcuts:
87
97
 
88
- | Key | Action |
89
- |-----|--------|
90
- | `↑` / `↓` | Navigate container list |
91
- | `I` | Start selected container |
92
- | `P` | Stop selected container |
93
- | `R` | Restart selected container |
94
- | `C` | Open creation wizard |
95
- | `L` | Stream logs for selected container |
96
- | `S` | Open interactive shell inside selected container |
97
- | `E` | Erase (remove) selected container — confirmation required |
98
- | `D` | Toggle live debug panel |
99
- | `Q` | Quit |
98
+ | Key | Action |
99
+ | --------- | --------------------------------------------------------- |
100
+ | `↑` / `↓` | Navigate container list |
101
+ | `I` | Start selected container |
102
+ | `P` | Stop selected container |
103
+ | `R` | Restart selected container |
104
+ | `C` | Open creation wizard |
105
+ | `L` | Stream logs for selected container |
106
+ | `S` | Open interactive shell inside selected container |
107
+ | `E` | Erase (remove) selected container — confirmation required |
108
+ | `D` | Toggle live debug panel |
109
+ | `Q` | Quit |
100
110
 
101
111
  ---
102
112
 
@@ -124,13 +134,13 @@ Enter a port mapping in `HOST:CONTAINER` format, e.g. `8080:80`, `5432:5432`. Le
124
134
 
125
135
  Enter `KEY=VALUE` pairs one at a time. **Contextual hints** show recommended variables for the selected image:
126
136
 
127
- | Image | Suggested vars |
128
- |-------|---------------|
129
- | postgres | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` |
130
- | mysql | `MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE` |
131
- | redis | *(no required vars)* |
132
- | mongo | `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` |
133
- | node / nginx / others | Common runtime vars as applicable |
137
+ | Image | Suggested vars |
138
+ | --------------------- | ---------------------------------------------------------- |
139
+ | postgres | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` |
140
+ | mysql | `MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE` |
141
+ | redis | _(no required vars)_ |
142
+ | mongo | `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` |
143
+ | node / nginx / others | Common runtime vars as applicable |
134
144
 
135
145
  Press `Enter` on an empty line to finish and create the container.
136
146
 
package/dist/App.js CHANGED
@@ -31,6 +31,7 @@ export default function App() {
31
31
  containerName: controls.containerNameInput,
32
32
  portInput: controls.portInput,
33
33
  envInput: controls.envInput,
34
+ cursors: controls.creation.cursors,
34
35
  message: controls.message,
35
36
  messageColor: controls.messageColor,
36
37
  suggestions: controls.creation.suggestions,
@@ -29,6 +29,8 @@ export default function ContainerCreationPrompt(props) {
29
29
  containerName = props.containerName,
30
30
  portInput = props.portInput,
31
31
  envInput = props.envInput,
32
+ _props$cursors = props.cursors,
33
+ cursors = _props$cursors === void 0 ? {} : _props$cursors,
32
34
  message = props.message,
33
35
  messageColor = props.messageColor,
34
36
  _props$suggestions = props.suggestions,
@@ -46,23 +48,28 @@ export default function ContainerCreationPrompt(props) {
46
48
  var prompts = [{
47
49
  label: 'Name of the image to create (e.g., nginx:1.27-alpine):',
48
50
  value: imageName,
51
+ cursor: cursors.imageName,
49
52
  required: true
50
53
  }, {
51
54
  label: 'Name of the container (optional):',
52
55
  value: containerName,
56
+ cursor: cursors.containerName,
53
57
  required: false
54
58
  }, {
55
59
  label: 'Ports (optional, format 8080:80,443:443):',
56
60
  value: portInput,
61
+ cursor: cursors.portInput,
57
62
  required: false
58
63
  }, {
59
64
  label: 'Environment variables (optional, format VAR1=val1,VAR2=val2):',
60
65
  value: envInput,
66
+ cursor: cursors.envInput,
61
67
  required: false
62
68
  }];
63
69
  var _ref = prompts[step] || {},
64
70
  label = _ref.label,
65
71
  value = _ref.value,
72
+ cursor = _ref.cursor,
66
73
  required = _ref.required;
67
74
  var activeItems = hubResults !== null && hubResults !== void 0 ? hubResults : suggestions;
68
75
  var showSuggestions = step === 0 && (isSearchingHub || activeItems.length > 0);
@@ -75,6 +82,7 @@ export default function ContainerCreationPrompt(props) {
75
82
  }, /*#__PURE__*/React.createElement(PromptField, {
76
83
  label: label,
77
84
  value: value,
85
+ cursor: cursor,
78
86
  required: required
79
87
  }), showSuggestions && /*#__PURE__*/React.createElement(SuggestionPanel, {
80
88
  items: activeItems,
@@ -97,6 +105,12 @@ ContainerCreationPrompt.propTypes = {
97
105
  containerName: PropTypes.string,
98
106
  portInput: PropTypes.string,
99
107
  envInput: PropTypes.string,
108
+ cursors: PropTypes.shape({
109
+ imageName: PropTypes.number,
110
+ containerName: PropTypes.number,
111
+ portInput: PropTypes.number,
112
+ envInput: PropTypes.number
113
+ }),
100
114
  message: PropTypes.string,
101
115
  messageColor: PropTypes.string,
102
116
  suggestions: PropTypes.arrayOf(PropTypes.string),
@@ -1,26 +1,32 @@
1
1
  import React from 'react';
2
+ import { TextField } from './TextField.js';
2
3
  import { Text } from 'ink';
3
4
  import PropTypes from 'prop-types';
4
5
 
5
6
  /**
6
7
  * PromptField shows a label and the current typed value for an input field.
7
- * The cursor is represented with a trailing underscore.
8
+ * Wraps TextField with cursor rendering.
8
9
  *
9
10
  * @param {Object} props
10
11
  * @param {string} props.label - Label shown above the input value
11
12
  * @param {string} props.value - Current value of the input
13
+ * @param {number} [props.cursor] - Cursor position (0..value.length). When
14
+ * omitted the cursor defaults to the end of the value.
12
15
  * @param {boolean} [props.required=false] - Whether the field is required
13
16
  * @returns {JSX.Element}
14
17
  */
15
18
  export function PromptField(_ref) {
16
19
  var label = _ref.label,
17
20
  value = _ref.value,
21
+ cursor = _ref.cursor,
18
22
  required = _ref.required;
19
- // If the field is required and empty, show in red
20
- var isEmpty = required && !value.trim();
21
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, label), /*#__PURE__*/React.createElement(Text, {
22
- color: isEmpty ? 'red' : 'cyan'
23
- }, value, "_"));
23
+ var cursorPos = cursor !== undefined ? cursor : (value || '').length;
24
+ return /*#__PURE__*/React.createElement(TextField, {
25
+ label: label,
26
+ value: value || '',
27
+ cursor: cursorPos,
28
+ required: required
29
+ });
24
30
  }
25
31
 
26
32
  /**
@@ -42,6 +48,7 @@ export function PromptMessage(_ref2) {
42
48
  PromptField.propTypes = {
43
49
  label: PropTypes.string,
44
50
  value: PropTypes.string,
51
+ cursor: PropTypes.number,
45
52
  required: PropTypes.bool
46
53
  };
47
54
  PromptMessage.propTypes = {
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { Text } from 'ink';
3
+ import PropTypes from 'prop-types';
4
+
5
+ /**
6
+ * TextField renders a single-line text input with a visible cursor.
7
+ * Purely presentational — does not capture keyboard input.
8
+ *
9
+ * The cursor inverts the character under it. When the cursor is at the end
10
+ * of the text, a block cursor (inverted space) is shown.
11
+ *
12
+ * @param {Object} props
13
+ * @param {string} props.label - Label shown above the field
14
+ * @param {string} props.value - Current text content
15
+ * @param {number} props.cursor - Cursor position (0..value.length)
16
+ * @param {boolean} [props.required=false] - Show in red if empty
17
+ * @param {boolean} [props.masked=false] - Reserved for TASK-5 (secrets)
18
+ * @param {string} [props.placeholder] - Dimmed text when empty
19
+ */
20
+ export function TextField(_ref) {
21
+ var _chars$clampedCursor;
22
+ var label = _ref.label,
23
+ _ref$value = _ref.value,
24
+ value = _ref$value === void 0 ? '' : _ref$value,
25
+ _ref$cursor = _ref.cursor,
26
+ cursor = _ref$cursor === void 0 ? 0 : _ref$cursor,
27
+ _ref$required = _ref.required,
28
+ required = _ref$required === void 0 ? false : _ref$required,
29
+ _ref$masked = _ref.masked,
30
+ masked = _ref$masked === void 0 ? false : _ref$masked;
31
+ var isEmpty = required && !value.trim();
32
+ var displayValue = masked ? '*'.repeat(value.length) : value;
33
+ var chars = Array.from(displayValue);
34
+ var clampedCursor = Math.max(0, Math.min(chars.length, cursor));
35
+ var before = chars.slice(0, clampedCursor).join('');
36
+ var atCursor = (_chars$clampedCursor = chars[clampedCursor]) !== null && _chars$clampedCursor !== void 0 ? _chars$clampedCursor : '';
37
+ var after = chars.slice(clampedCursor + 1).join('');
38
+ var color = isEmpty ? 'red' : 'cyan';
39
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, null, label), /*#__PURE__*/React.createElement(Text, {
40
+ color: color
41
+ }, before, atCursor ? /*#__PURE__*/React.createElement(Text, {
42
+ inverse: true
43
+ }, atCursor) : /*#__PURE__*/React.createElement(Text, {
44
+ inverse: true
45
+ }, " "), after));
46
+ }
47
+ TextField.propTypes = {
48
+ label: PropTypes.string.isRequired,
49
+ value: PropTypes.string,
50
+ cursor: PropTypes.number,
51
+ required: PropTypes.bool,
52
+ masked: PropTypes.bool,
53
+ placeholder: PropTypes.string
54
+ };
@@ -0,0 +1,293 @@
1
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
5
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
6
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
7
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
8
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
9
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
10
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
11
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
12
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
13
+ /**
14
+ * @typedef {Object} TextState
15
+ * @property {string} value - Current field content
16
+ * @property {number} cursor - Cursor position (0..value.length)
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} EditResult
21
+ * @property {TextState} state - Resulting state (same object if no change)
22
+ * @property {boolean} handled - true if the key was consumed by the editor
23
+ */
24
+
25
+ var BREAK_CHARS = ' \t./\\,;:|(){}[]<>!@#$%^&*+=~`"\'-';
26
+
27
+ /**
28
+ * Splits a string into code points (handles emojis via Array.from).
29
+ * @param {string} s
30
+ * @returns {string[]}
31
+ */
32
+ function chars(s) {
33
+ return Array.from(s);
34
+ }
35
+
36
+ /**
37
+ * Returns the index of the start of the word before cursor.
38
+ * A word boundary is any non-alphanumeric, non-underscore character.
39
+ *
40
+ * @param {string} value
41
+ * @param {number} cursor
42
+ * @returns {number}
43
+ */
44
+ export function wordStartBefore(value, cursor) {
45
+ var arr = chars(value);
46
+ if (cursor <= 0) return 0;
47
+ var i = cursor - 1;
48
+ // Skip trailing non-word chars
49
+ while (i > 0 && BREAK_CHARS.includes(arr[i])) i--;
50
+ // Skip word chars
51
+ while (i > 0 && !BREAK_CHARS.includes(arr[i - 1])) i--;
52
+ return i;
53
+ }
54
+
55
+ /**
56
+ * Returns the index of the start of the word after cursor.
57
+ *
58
+ * @param {string} value
59
+ * @param {number} cursor
60
+ * @returns {number}
61
+ */
62
+ export function wordStartAfter(value, cursor) {
63
+ var arr = chars(value);
64
+ if (cursor >= arr.length) return arr.length;
65
+ var i = cursor;
66
+ // Skip current word chars
67
+ while (i < arr.length && !BREAK_CHARS.includes(arr[i])) i++;
68
+ // Skip non-word chars (whitespace, punctuation)
69
+ while (i < arr.length && BREAK_CHARS.includes(arr[i])) i++;
70
+ return i;
71
+ }
72
+
73
+ /**
74
+ * Inserts text at the cursor position and leaves the cursor after the insertion.
75
+ *
76
+ * @param {TextState} state
77
+ * @param {string} text
78
+ * @returns {TextState}
79
+ */
80
+ export function insertAt(state, text) {
81
+ if (!text) return state;
82
+ var arr = chars(state.value);
83
+ var before = arr.slice(0, state.cursor);
84
+ var after = arr.slice(state.cursor);
85
+ var result = [].concat(_toConsumableArray(before), _toConsumableArray(chars(text)), _toConsumableArray(after)).join('');
86
+ return {
87
+ value: result,
88
+ cursor: state.cursor + chars(text).length
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Deletes the character before the cursor.
94
+ *
95
+ * @param {TextState} state
96
+ * @returns {TextState}
97
+ */
98
+ export function deleteBackward(state) {
99
+ var arr = chars(state.value);
100
+ if (state.cursor <= 0) return state;
101
+ var before = arr.slice(0, state.cursor - 1);
102
+ var after = arr.slice(state.cursor);
103
+ return {
104
+ value: before.concat(after).join(''),
105
+ cursor: state.cursor - 1
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Deletes the character under the cursor.
111
+ *
112
+ * @param {TextState} state
113
+ * @returns {TextState}
114
+ */
115
+ export function deleteForward(state) {
116
+ var arr = chars(state.value);
117
+ if (state.cursor >= arr.length) return state;
118
+ var before = arr.slice(0, state.cursor);
119
+ var after = arr.slice(state.cursor + 1);
120
+ return {
121
+ value: before.concat(after).join(''),
122
+ cursor: state.cursor
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Applies a keypress to a text state.
128
+ * Pure function: reads and writes nothing outside its arguments.
129
+ *
130
+ * @param {TextState} state
131
+ * @param {string} input - Raw Ink input (1 char, a paste, or '')
132
+ * @param {import('ink').Key} key - Ink special key object
133
+ * @returns {EditResult}
134
+ */
135
+ export function applyKeyToText(state, input, key) {
136
+ // No key combinations from this function
137
+ if (key.ctrl) {
138
+ if (input === 'a') {
139
+ // Ctrl+A → move to start
140
+ return {
141
+ state: _objectSpread(_objectSpread({}, state), {}, {
142
+ cursor: 0
143
+ }),
144
+ handled: true
145
+ };
146
+ }
147
+ if (input === 'e') {
148
+ // Ctrl+E → move to end
149
+ return {
150
+ state: _objectSpread(_objectSpread({}, state), {}, {
151
+ cursor: chars(state.value).length
152
+ }),
153
+ handled: true
154
+ };
155
+ }
156
+ if (input === 'w') {
157
+ // Ctrl+W → delete word before cursor
158
+ var newCursor = wordStartBefore(state.value, state.cursor);
159
+ var arr = chars(state.value);
160
+ var before = arr.slice(0, newCursor);
161
+ var after = arr.slice(state.cursor);
162
+ return {
163
+ state: {
164
+ value: before.concat(after).join(''),
165
+ cursor: newCursor
166
+ },
167
+ handled: true
168
+ };
169
+ }
170
+ if (input === 'u') {
171
+ // Ctrl+U → delete from start to cursor
172
+ var _arr = chars(state.value);
173
+ var _after = _arr.slice(state.cursor);
174
+ return {
175
+ state: {
176
+ value: _after.join(''),
177
+ cursor: 0
178
+ },
179
+ handled: true
180
+ };
181
+ }
182
+ if (input === 'k') {
183
+ // Ctrl+K → delete from cursor to end
184
+ var _arr2 = chars(state.value);
185
+ var _before = _arr2.slice(0, state.cursor);
186
+ return {
187
+ state: {
188
+ value: _before.join(''),
189
+ cursor: state.cursor
190
+ },
191
+ handled: true
192
+ };
193
+ }
194
+ // Other ctrl combos: not handled
195
+ return {
196
+ state: state,
197
+ handled: false
198
+ };
199
+ }
200
+ if (key.meta) {
201
+ return {
202
+ state: state,
203
+ handled: false
204
+ };
205
+ }
206
+ if (key.leftArrow) {
207
+ var _newCursor = Math.max(0, state.cursor - 1);
208
+ return {
209
+ state: _objectSpread(_objectSpread({}, state), {}, {
210
+ cursor: _newCursor
211
+ }),
212
+ handled: true
213
+ };
214
+ }
215
+ if (key.rightArrow) {
216
+ var len = chars(state.value).length;
217
+ var _newCursor2 = Math.min(len, state.cursor + 1);
218
+ return {
219
+ state: _objectSpread(_objectSpread({}, state), {}, {
220
+ cursor: _newCursor2
221
+ }),
222
+ handled: true
223
+ };
224
+ }
225
+ if (key.home) {
226
+ return {
227
+ state: _objectSpread(_objectSpread({}, state), {}, {
228
+ cursor: 0
229
+ }),
230
+ handled: true
231
+ };
232
+ }
233
+ if (key.end) {
234
+ return {
235
+ state: _objectSpread(_objectSpread({}, state), {}, {
236
+ cursor: chars(state.value).length
237
+ }),
238
+ handled: true
239
+ };
240
+ }
241
+ if (key.backspace) {
242
+ var newState = deleteBackward(state);
243
+ return {
244
+ state: newState,
245
+ handled: true
246
+ };
247
+ }
248
+ if (key["delete"]) {
249
+ var _newState = deleteForward(state);
250
+ return {
251
+ state: _newState,
252
+ handled: true
253
+ };
254
+ }
255
+
256
+ // Safety net: \x7f (DEL) is what most terminals send for Backspace.
257
+ // Ink v6 maps it to key.delete, but if we receive the raw char directly
258
+ // (e.g. from tests or non-Ink callers), handle it as backspace.
259
+ if (input === '\x7f' || input === '\b') {
260
+ var _newState2 = deleteBackward(state);
261
+ return {
262
+ state: _newState2,
263
+ handled: true
264
+ };
265
+ }
266
+
267
+ // Regular text input (single char or paste)
268
+ if (input && input.length >= 1) {
269
+ var _newState3 = insertAt(state, input);
270
+ return {
271
+ state: _newState3,
272
+ handled: true
273
+ };
274
+ }
275
+ return {
276
+ state: state,
277
+ handled: false
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Creates a TextState from a string, with cursor at the end.
283
+ *
284
+ * @param {string} value
285
+ * @returns {TextState}
286
+ */
287
+ export function textStateOf() {
288
+ var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
289
+ return {
290
+ value: value,
291
+ cursor: chars(value).length
292
+ };
293
+ }
@@ -19,6 +19,7 @@ import { validatePorts, validateEnvVars } from '../../helpers/validationHelpers.
19
19
  import { IMAGE_PROFILES, resolveImageTag } from '../../helpers/constants.js';
20
20
  import { safeCall } from '../../helpers/safeCall.js';
21
21
  import { searchDockerHub, formatHubResult } from '../../helpers/dockerHubService.js';
22
+ import { applyKeyToText } from '../../helpers/textEditing.js';
22
23
  var MAX_VISIBLE = 6;
23
24
  var INITIAL_FORM = {
24
25
  step: 0,
@@ -26,6 +27,12 @@ var INITIAL_FORM = {
26
27
  containerName: '',
27
28
  portInput: '',
28
29
  envInput: '',
30
+ cursors: {
31
+ imageName: 0,
32
+ containerName: 0,
33
+ portInput: 0,
34
+ envInput: 0
35
+ },
29
36
  message: '',
30
37
  messageColor: 'yellow',
31
38
  suggestions: [],
@@ -76,6 +83,7 @@ export function useContainerCreation(_ref) {
76
83
  containerName = form.containerName,
77
84
  portInput = form.portInput,
78
85
  envInput = form.envInput,
86
+ cursors = form.cursors,
79
87
  message = form.message,
80
88
  messageColor = form.messageColor,
81
89
  suggestions = form.suggestions,
@@ -183,6 +191,76 @@ export function useContainerCreation(_ref) {
183
191
  }
184
192
  };
185
193
 
194
+ /**
195
+ * Sets a field's text value and cursor position together.
196
+ * @param {'imageName'|'containerName'|'portInput'|'envInput'} fieldName
197
+ * @param {string} value
198
+ * @param {number} cursor
199
+ */
200
+ function setFieldText(fieldName, value, cursor) {
201
+ dispatch({
202
+ type: 'SET',
203
+ payload: _defineProperty(_defineProperty({}, fieldName, value), "cursors", _objectSpread(_objectSpread({}, form.cursors), {}, _defineProperty({}, fieldName, cursor)))
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Returns the field name associated with a wizard step.
209
+ * @param {number} step
210
+ * @returns {'imageName'|'containerName'|'portInput'|'envInput'|null}
211
+ */
212
+ function fieldForStep(step) {
213
+ var _step;
214
+ return (_step = ['imageName', 'containerName', 'portInput', 'envInput'][step]) !== null && _step !== void 0 ? _step : null;
215
+ }
216
+
217
+ /**
218
+ * Applies a keypress to the field associated with the current step.
219
+ * When the edited field is imageName, recalculates suggestions.
220
+ * @param {string} input - Raw Ink input
221
+ * @param {import('ink').Key} key - Ink key object
222
+ * @returns {boolean} true if the key was consumed
223
+ */
224
+ function handleFieldKey(input, key) {
225
+ var fieldName = fieldForStep(step);
226
+ if (!fieldName) return false;
227
+ var currentState = {
228
+ value: form[fieldName],
229
+ cursor: cursors[fieldName]
230
+ };
231
+ var _applyKeyToText = applyKeyToText(currentState, input, key),
232
+ newState = _applyKeyToText.state,
233
+ handled = _applyKeyToText.handled;
234
+ if (!handled) return false;
235
+ if (fieldName === 'imageName') {
236
+ var _activeHubRequestRef$;
237
+ // Recalculate suggestions when image name changes
238
+ (_activeHubRequestRef$ = activeHubRequestRef.current.controller) === null || _activeHubRequestRef$ === void 0 || _activeHubRequestRef$.abort();
239
+ activeHubRequestRef.current.requestId += 1;
240
+ setIsSearchingHub(false);
241
+ setHubResults(null);
242
+ var lower = newState.value.toLowerCase();
243
+ var matches = newState.value.trim() ? Object.keys(imageProfiles).filter(function (key) {
244
+ return key.includes(lower);
245
+ }) : [];
246
+ dispatch({
247
+ type: 'SET',
248
+ payload: {
249
+ imageName: newState.value,
250
+ cursors: _objectSpread(_objectSpread({}, form.cursors), {}, {
251
+ imageName: newState.cursor
252
+ }),
253
+ selectedSuggestionIndex: -1,
254
+ visibleOffset: 0,
255
+ suggestions: matches
256
+ }
257
+ });
258
+ } else {
259
+ setFieldText(fieldName, newState.value, newState.cursor);
260
+ }
261
+ return true;
262
+ }
263
+
186
264
  // Docker Hub search state (kept as useState because they are independent of form)
187
265
  var _useState = useState(false),
188
266
  _useState2 = _slicedToArray(_useState, 2),
@@ -249,15 +327,19 @@ export function useContainerCreation(_ref) {
249
327
 
250
328
  /**
251
329
  * Updates the image name input and recalculates autocomplete suggestions.
330
+ * Accepts either a plain string (backward compatible) or a TextState object.
252
331
  * Resets selectedSuggestionIndex to -1 on every keystroke.
253
332
  * Also aborts any in-flight Hub search and clears Hub results.
254
333
  *
255
- * @param {string} value - New raw image name typed by user
334
+ * @param {string|TextState} input - New image name (string or {value, cursor})
256
335
  */
257
- function updateImageInput(value) {
258
- var _activeHubRequestRef$;
336
+ function updateImageInput(input) {
337
+ var _input$cursor, _activeHubRequestRef$2;
338
+ var value = typeof input === 'string' ? input : input.value;
339
+ var cursor = typeof input === 'string' ? value.length : (_input$cursor = input.cursor) !== null && _input$cursor !== void 0 ? _input$cursor : value.length;
340
+
259
341
  // Abort any in-flight Hub search and clear its state
260
- (_activeHubRequestRef$ = activeHubRequestRef.current.controller) === null || _activeHubRequestRef$ === void 0 || _activeHubRequestRef$.abort();
342
+ (_activeHubRequestRef$2 = activeHubRequestRef.current.controller) === null || _activeHubRequestRef$2 === void 0 || _activeHubRequestRef$2.abort();
261
343
  activeHubRequestRef.current.requestId += 1;
262
344
  setIsSearchingHub(false);
263
345
  setHubResults(null);
@@ -269,6 +351,9 @@ export function useContainerCreation(_ref) {
269
351
  type: 'SET',
270
352
  payload: {
271
353
  imageName: value,
354
+ cursors: _objectSpread(_objectSpread({}, form.cursors), {}, {
355
+ imageName: cursor
356
+ }),
272
357
  selectedSuggestionIndex: -1,
273
358
  visibleOffset: 0,
274
359
  suggestions: matches
@@ -293,7 +378,7 @@ export function useContainerCreation(_ref) {
293
378
  */
294
379
  function _triggerHubSearch() {
295
380
  _triggerHubSearch = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
296
- var _activeHubRequestRef$2;
381
+ var _activeHubRequestRef$3;
297
382
  var query, requestId, controller, results, _t;
298
383
  return _regenerator().w(function (_context) {
299
384
  while (1) switch (_context.p = _context.n) {
@@ -313,7 +398,7 @@ export function useContainerCreation(_ref) {
313
398
  case 2:
314
399
  // Abort previous request (should already be aborted from updateImageInput,
315
400
  // but guard here too)
316
- (_activeHubRequestRef$2 = activeHubRequestRef.current.controller) === null || _activeHubRequestRef$2 === void 0 || _activeHubRequestRef$2.abort();
401
+ (_activeHubRequestRef$3 = activeHubRequestRef.current.controller) === null || _activeHubRequestRef$3 === void 0 || _activeHubRequestRef$3.abort();
317
402
  requestId = activeHubRequestRef.current.requestId + 1;
318
403
  activeHubRequestRef.current.requestId = requestId;
319
404
  controller = new AbortController();
@@ -386,10 +471,14 @@ export function useContainerCreation(_ref) {
386
471
  function applyFocusedSuggestion() {
387
472
  if (selectedSuggestionIndex < 0 || selectedSuggestionIndex >= suggestions.length) return;
388
473
  var chosen = suggestions[selectedSuggestionIndex];
474
+ var resolved = resolveImageTag(chosen, imageProfiles);
389
475
  dispatch({
390
476
  type: 'SET',
391
477
  payload: {
392
- imageName: resolveImageTag(chosen, imageProfiles),
478
+ imageName: resolved,
479
+ cursors: _objectSpread(_objectSpread({}, form.cursors), {}, {
480
+ imageName: resolved.length
481
+ }),
393
482
  suggestions: [],
394
483
  selectedSuggestionIndex: -1,
395
484
  visibleOffset: 0
@@ -411,6 +500,9 @@ export function useContainerCreation(_ref) {
411
500
  type: 'SET',
412
501
  payload: {
413
502
  imageName: resolved,
503
+ cursors: _objectSpread(_objectSpread({}, form.cursors), {}, {
504
+ imageName: resolved.length
505
+ }),
414
506
  step: 1
415
507
  }
416
508
  });
@@ -519,7 +611,10 @@ export function useContainerCreation(_ref) {
519
611
  dispatch({
520
612
  type: 'SET',
521
613
  payload: {
522
- envInput: newEnvInput
614
+ envInput: newEnvInput,
615
+ cursors: _objectSpread(_objectSpread({}, form.cursors), {}, {
616
+ envInput: newEnvInput.length
617
+ })
523
618
  }
524
619
  });
525
620
  }
@@ -540,6 +635,7 @@ export function useContainerCreation(_ref) {
540
635
  setPortInput: setPortInput,
541
636
  envInput: envInput,
542
637
  setEnvInput: setEnvInput,
638
+ cursors: cursors,
543
639
  message: message,
544
640
  setMessage: setMessage,
545
641
  messageColor: messageColor,
@@ -557,6 +653,8 @@ export function useContainerCreation(_ref) {
557
653
  cancelCreation: cancelCreation,
558
654
  resetCreation: resetCreation,
559
655
  insertNextSuggestedEnv: insertNextSuggestedEnv,
560
- hasSuggestedEnv: hasSuggestedEnv
656
+ hasSuggestedEnv: hasSuggestedEnv,
657
+ fieldForStep: fieldForStep,
658
+ handleFieldKey: handleFieldKey
561
659
  };
562
660
  }
@@ -1,3 +1,9 @@
1
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
5
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
6
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
1
7
  function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
2
8
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
3
9
  function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
@@ -197,14 +203,6 @@ export function useControls() {
197
203
  */
198
204
  var processCreationInput = React.useCallback(function (input, key) {
199
205
  var step = creation.step;
200
- var removeLastChar = function removeLastChar(setter) {
201
- return setter(function (value) {
202
- return (value || '').slice(0, -1);
203
- });
204
- };
205
- var appendChar = function appendChar(setter, current, ch) {
206
- return setter((current || '') + ch);
207
- };
208
206
  if (key.escape) {
209
207
  creation.cancelCreation();
210
208
  setCreatingContainer(false);
@@ -243,24 +241,16 @@ export function useControls() {
243
241
  return;
244
242
  }
245
243
  }
246
- if (key.backspace || key["delete"]) {
247
- if (step === 0) {
248
- var newValue = (creation.imageName || '').slice(0, -1);
249
- creation.updateImageInput(newValue);
250
- }
251
- if (step === 1) removeLastChar(creation.setContainerName);
252
- if (step === 2) removeLastChar(creation.setPortInput);
253
- if (step === 3) removeLastChar(creation.setEnvInput);
254
- return;
255
- }
256
- if (input && input.length === 1 && !key.ctrl && !key.meta) {
257
- if (step === 0) {
258
- creation.updateImageInput((creation.imageName || '') + input);
259
- }
260
- if (step === 1) appendChar(creation.setContainerName, creation.containerName, input);
261
- if (step === 2) appendChar(creation.setPortInput, creation.portInput, input);
262
- if (step === 3) appendChar(creation.setEnvInput, creation.envInput, input);
263
- }
244
+
245
+ // Ink v6 maps \x7f (Backspace on most terminals) to key.delete instead
246
+ // of key.backspace. Normalize so text editing always receives backspace.
247
+ var normalizedKey = key["delete"] && !key.backspace ? _objectSpread(_objectSpread({}, key), {}, {
248
+ "delete": false,
249
+ backspace: true
250
+ }) : key;
251
+
252
+ // Delegate to the text editor for all field input
253
+ if (creation.handleFieldKey(input, normalizedKey)) return;
264
254
  }, [creation, setCreatingContainer, triggerHubSearch, isSearchingHub, insertNextSuggestedEnv]);
265
255
 
266
256
  // Single keyboard entry point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdd-cli",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "CLI Docker Dashboard",
5
5
  "main": "index.js",
6
6
  "scripts": {