mepcli 0.1.0 → 0.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 CodeTease
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CodeTease
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,51 +1,51 @@
1
- # Mep: Minimalist CLI Prompt
2
-
3
- **Mep** is a minimalist and zero-dependency library for creating interactive command-line prompts in Node.js. It focuses on simplicity, modern design, and robust input handling, including support for cursor movement and input validation.
4
-
5
- ## Features
6
-
7
- - **Zero Dependency:** Keeps your project clean and fast.
8
- - **Full-Featured Prompts:** Includes `text`, `password`, `select`, ch`eckbox, and `confirm`.
9
- - **Responsive Input:** Supports cursor movement (Left/Right) and character insertion/deletion in `text` and `password` prompts.
10
- - **Validation:** Built-in support for input validation with custom error messages.
11
- - **Elegant Look:** Uses ANSI colors for a clean, modern CLI experience.
12
-
13
- ## Installation
14
-
15
- ```bash
16
- npm install mepcli
17
- # or
18
- yarn add mepcli
19
- ```
20
-
21
- ## Usage Example
22
-
23
- Mep provides a static class facade, `MepCLI`, for all interactions.
24
- ```javascript
25
- import { MepCLI } from 'mepcli';
26
-
27
- async function setup() {
28
- // Text input with validation and cursor support
29
- const projectName = await MepCLI.text({
30
- message: "Enter the project name:",
31
- validate: (v) => v.length > 5 || "Must be longer than 5 chars",
32
- });
33
-
34
- // Select menu
35
- const choice = await MepCLI.select({
36
- message: "Choose an option:",
37
- choices: [
38
- { title: "Option A", value: 1 },
39
- { title: "Option B", value: 2 }
40
- ]
41
- });
42
-
43
- console.log(`\nProject: ${projectName}, Selected: ${choice}`);
44
- }
45
-
46
- setup();
47
- ```
48
-
49
- ## License
50
-
1
+ # Mep: Minimalist CLI Prompt
2
+
3
+ **Mep** is a minimalist and zero-dependency library for creating interactive command-line prompts in Node.js. It focuses on simplicity, modern design, and robust input handling, including support for cursor movement and input validation.
4
+
5
+ ## Features
6
+
7
+ - **Zero Dependency:** Keeps your project clean and fast.
8
+ - **Full-Featured Prompts:** Includes `text`, `password`, `select`, ch`eckbox, and `confirm`.
9
+ - **Responsive Input:** Supports cursor movement (Left/Right) and character insertion/deletion in `text` and `password` prompts.
10
+ - **Validation:** Built-in support for input validation with custom error messages.
11
+ - **Elegant Look:** Uses ANSI colors for a clean, modern CLI experience.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install mepcli
17
+ # or
18
+ yarn add mepcli
19
+ ```
20
+
21
+ ## Usage Example
22
+
23
+ Mep provides a static class facade, `MepCLI`, for all interactions.
24
+ ```javascript
25
+ import { MepCLI } from 'mepcli';
26
+
27
+ async function setup() {
28
+ // Text input with validation and cursor support
29
+ const projectName = await MepCLI.text({
30
+ message: "Enter the project name:",
31
+ validate: (v) => v.length > 5 || "Must be longer than 5 chars",
32
+ });
33
+
34
+ // Select menu
35
+ const choice = await MepCLI.select({
36
+ message: "Choose an option:",
37
+ choices: [
38
+ { title: "Option A", value: 1 },
39
+ { title: "Option B", value: 2 }
40
+ ]
41
+ });
42
+
43
+ console.log(`\nProject: ${projectName}, Selected: ${choice}`);
44
+ }
45
+
46
+ setup();
47
+ ```
48
+
49
+ ## License
50
+
51
51
  This project is under the **MIT License**.
package/dist/core.d.ts CHANGED
@@ -1,11 +1,18 @@
1
- import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions } from './types';
1
+ import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions, ThemeConfig, NumberOptions, ToggleOptions } from './types';
2
2
  /**
3
3
  * Public Facade for MepCLI
4
4
  */
5
5
  export declare class MepCLI {
6
+ static theme: ThemeConfig;
7
+ /**
8
+ * Shows a spinner while a promise is pending.
9
+ */
10
+ static spin<T>(message: string, taskPromise: Promise<T>): Promise<T>;
6
11
  static text(options: TextOptions): Promise<string>;
7
12
  static select(options: SelectOptions): Promise<any>;
8
13
  static checkbox(options: CheckboxOptions): Promise<any[]>;
9
14
  static confirm(options: ConfirmOptions): Promise<boolean>;
10
15
  static password(options: TextOptions): Promise<string>;
16
+ static number(options: NumberOptions): Promise<number>;
17
+ static toggle(options: ToggleOptions): Promise<boolean>;
11
18
  }
package/dist/core.js CHANGED
@@ -77,6 +77,7 @@ class TextPrompt extends Prompt {
77
77
  super(options);
78
78
  this.errorMsg = '';
79
79
  this.cursor = 0;
80
+ this.hasTyped = false;
80
81
  this.value = options.initial || '';
81
82
  this.cursor = this.value.length;
82
83
  }
@@ -91,21 +92,21 @@ class TextPrompt extends Prompt {
91
92
  }
92
93
  // 1. Render the Prompt Message
93
94
  this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
94
- const icon = this.errorMsg ? `${ansi_1.ANSI.FG_RED}✖` : `${ansi_1.ANSI.FG_GREEN}?`;
95
- this.print(`${icon} ${ansi_1.ANSI.BOLD}${this.options.message}${ansi_1.ANSI.RESET} `);
95
+ const icon = this.errorMsg ? `${MepCLI.theme.error}✖` : `${MepCLI.theme.success}?`;
96
+ this.print(`${icon} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `);
96
97
  // 2. Render the Value or Placeholder
97
- if (!this.value && this.options.placeholder && !this.errorMsg) {
98
- this.print(`${ansi_1.ANSI.FG_GRAY}${this.options.placeholder}${ansi_1.ANSI.RESET}`);
98
+ if (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped) {
99
+ this.print(`${MepCLI.theme.muted}${this.options.placeholder}${ansi_1.ANSI.RESET}`);
99
100
  // Move cursor back to start so typing overwrites placeholder visually
100
101
  this.print(`\x1b[${this.options.placeholder.length}D`);
101
102
  }
102
103
  else {
103
104
  const displayValue = this.options.isPassword ? '*'.repeat(this.value.length) : this.value;
104
- this.print(`${ansi_1.ANSI.FG_CYAN}${displayValue}${ansi_1.ANSI.RESET}`);
105
+ this.print(`${MepCLI.theme.main}${displayValue}${ansi_1.ANSI.RESET}`);
105
106
  }
106
107
  // 3. Handle Error Message
107
108
  if (this.errorMsg) {
108
- this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.FG_RED}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
109
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
109
110
  this.print(ansi_1.ANSI.UP); // Go back to input line
110
111
  // Re-calculate position to end of input
111
112
  const promptLen = this.options.message.length + 3; // Icon + 2 spaces
@@ -140,6 +141,7 @@ class TextPrompt extends Prompt {
140
141
  }
141
142
  // Backspace
142
143
  if (char === '\u0008' || char === '\x7f') {
144
+ this.hasTyped = true;
143
145
  if (this.cursor > 0) {
144
146
  this.value = this.value.slice(0, this.cursor - 1) + this.value.slice(this.cursor);
145
147
  this.cursor--;
@@ -166,6 +168,7 @@ class TextPrompt extends Prompt {
166
168
  }
167
169
  // Delete key
168
170
  if (char === '\u001b[3~') {
171
+ this.hasTyped = true;
169
172
  if (this.cursor < this.value.length) {
170
173
  this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + 1);
171
174
  this.errorMsg = '';
@@ -175,6 +178,7 @@ class TextPrompt extends Prompt {
175
178
  }
176
179
  // Regular Typing
177
180
  if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
181
+ this.hasTyped = true;
178
182
  this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
179
183
  this.cursor++;
180
184
  this.errorMsg = '';
@@ -187,40 +191,136 @@ class SelectPrompt extends Prompt {
187
191
  constructor(options) {
188
192
  super(options);
189
193
  this.selectedIndex = 0;
194
+ this.searchBuffer = '';
195
+ // Custom render to handle variable height clearing
196
+ this.lastRenderHeight = 0;
197
+ // Find first non-separator index
198
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
190
199
  }
191
- render(firstRender) {
192
- // Ensure cursor is HIDDEN for menus
193
- this.print(ansi_1.ANSI.HIDE_CURSOR);
194
- if (!firstRender) {
195
- this.print(`\x1b[${this.options.choices.length + 1}A`);
200
+ isSeparator(item) {
201
+ return item && item.separator === true;
202
+ }
203
+ findNextSelectableIndex(currentIndex, direction) {
204
+ let nextIndex = currentIndex + direction;
205
+ const choices = this.getFilteredChoices();
206
+ // Loop around logic
207
+ if (nextIndex < 0)
208
+ nextIndex = choices.length - 1;
209
+ if (nextIndex >= choices.length)
210
+ nextIndex = 0;
211
+ if (choices.length === 0)
212
+ return 0;
213
+ // Safety check to prevent infinite loop if all are separators (shouldn't happen in practice)
214
+ let count = 0;
215
+ while (this.isSeparator(choices[nextIndex]) && count < choices.length) {
216
+ nextIndex += direction;
217
+ if (nextIndex < 0)
218
+ nextIndex = choices.length - 1;
219
+ if (nextIndex >= choices.length)
220
+ nextIndex = 0;
221
+ count++;
196
222
  }
197
- this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
198
- this.print(`${ansi_1.ANSI.FG_GREEN}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${this.options.message}${ansi_1.ANSI.RESET}\n`);
199
- this.options.choices.forEach((choice, index) => {
200
- this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
201
- if (index === this.selectedIndex) {
202
- this.print(`${ansi_1.ANSI.FG_CYAN}❯ ${choice.title}${ansi_1.ANSI.RESET}\n`);
203
- }
204
- else {
205
- this.print(` ${choice.title}\n`);
206
- }
223
+ return nextIndex;
224
+ }
225
+ getFilteredChoices() {
226
+ if (!this.searchBuffer)
227
+ return this.options.choices;
228
+ return this.options.choices.filter(c => {
229
+ if (this.isSeparator(c))
230
+ return false; // Hide separators when searching
231
+ return c.title.toLowerCase().includes(this.searchBuffer.toLowerCase());
207
232
  });
208
233
  }
234
+ renderWrapper(firstRender) {
235
+ if (!firstRender && this.lastRenderHeight > 0) {
236
+ this.print(`\x1b[${this.lastRenderHeight}A`);
237
+ }
238
+ let output = '';
239
+ const choices = this.getFilteredChoices();
240
+ // Header
241
+ const searchStr = this.searchBuffer ? ` ${MepCLI.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
242
+ output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${searchStr}\n`;
243
+ if (choices.length === 0) {
244
+ output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT} ${MepCLI.theme.muted}No results found${ansi_1.ANSI.RESET}\n`;
245
+ }
246
+ else {
247
+ choices.forEach((choice, index) => {
248
+ output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`;
249
+ if (this.isSeparator(choice)) {
250
+ output += ` ${ansi_1.ANSI.DIM}${choice.text || '────────'}${ansi_1.ANSI.RESET}\n`;
251
+ }
252
+ else {
253
+ if (index === this.selectedIndex) {
254
+ output += `${MepCLI.theme.main}❯ ${choice.title}${ansi_1.ANSI.RESET}\n`;
255
+ }
256
+ else {
257
+ output += ` ${choice.title}\n`;
258
+ }
259
+ }
260
+ });
261
+ }
262
+ this.print(output);
263
+ // Clear remaining lines if list shrunk
264
+ const currentHeight = choices.length + 1 + (choices.length === 0 ? 1 : 0);
265
+ const linesToClear = this.lastRenderHeight - currentHeight;
266
+ if (linesToClear > 0) {
267
+ for (let i = 0; i < linesToClear; i++) {
268
+ this.print(`${ansi_1.ANSI.ERASE_LINE}\n`);
269
+ }
270
+ this.print(`\x1b[${linesToClear}A`); // Move back up
271
+ }
272
+ this.lastRenderHeight = currentHeight;
273
+ }
274
+ render(firstRender) {
275
+ this.print(ansi_1.ANSI.HIDE_CURSOR);
276
+ this.renderWrapper(firstRender);
277
+ }
209
278
  handleInput(char) {
279
+ const choices = this.getFilteredChoices();
210
280
  if (char === '\r' || char === '\n') {
281
+ if (choices.length === 0) {
282
+ this.searchBuffer = '';
283
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
284
+ this.render(false);
285
+ return;
286
+ }
287
+ if (this.isSeparator(choices[this.selectedIndex]))
288
+ return;
211
289
  this.cleanup();
212
- this.print(`\x1b[${this.options.choices.length - this.selectedIndex}B`);
213
290
  this.print(ansi_1.ANSI.SHOW_CURSOR);
214
291
  if (this._resolve)
215
- this._resolve(this.options.choices[this.selectedIndex].value);
292
+ this._resolve(choices[this.selectedIndex].value);
216
293
  return;
217
294
  }
218
295
  if (char === '\u001b[A') { // Up
219
- this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.choices.length - 1;
220
- this.render(false);
296
+ if (choices.length > 0) {
297
+ this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, -1);
298
+ this.render(false);
299
+ }
300
+ return;
221
301
  }
222
302
  if (char === '\u001b[B') { // Down
223
- this.selectedIndex = this.selectedIndex < this.options.choices.length - 1 ? this.selectedIndex + 1 : 0;
303
+ if (choices.length > 0) {
304
+ this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, 1);
305
+ this.render(false);
306
+ }
307
+ return;
308
+ }
309
+ // Backspace
310
+ if (char === '\u0008' || char === '\x7f') {
311
+ if (this.searchBuffer.length > 0) {
312
+ this.searchBuffer = this.searchBuffer.slice(0, -1);
313
+ this.selectedIndex = 0; // Reset selection
314
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
315
+ this.render(false);
316
+ }
317
+ return;
318
+ }
319
+ // Typing
320
+ if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
321
+ this.searchBuffer += char;
322
+ this.selectedIndex = 0; // Reset selection
323
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
224
324
  this.render(false);
225
325
  }
226
326
  }
@@ -241,22 +341,22 @@ class CheckboxPrompt extends Prompt {
241
341
  this.print(`\x1b[${this.options.choices.length + 1 + extraLines}A`);
242
342
  }
243
343
  this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
244
- const icon = this.errorMsg ? `${ansi_1.ANSI.FG_RED}✖` : `${ansi_1.ANSI.FG_GREEN}?`;
245
- this.print(`${icon} ${ansi_1.ANSI.BOLD}${this.options.message}${ansi_1.ANSI.RESET} ${ansi_1.ANSI.FG_GRAY}(Press <space> to select, <enter> to confirm)${ansi_1.ANSI.RESET}\n`);
344
+ const icon = this.errorMsg ? `${MepCLI.theme.error}✖` : `${MepCLI.theme.success}?`;
345
+ this.print(`${icon} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${MepCLI.theme.muted}(Press <space> to select, <enter> to confirm)${ansi_1.ANSI.RESET}\n`);
246
346
  this.options.choices.forEach((choice, index) => {
247
347
  this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
248
- const cursor = index === this.selectedIndex ? `${ansi_1.ANSI.FG_CYAN}❯${ansi_1.ANSI.RESET}` : ' ';
348
+ const cursor = index === this.selectedIndex ? `${MepCLI.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
249
349
  const isChecked = this.checkedState[index];
250
350
  const checkbox = isChecked
251
- ? `${ansi_1.ANSI.FG_GREEN}◉${ansi_1.ANSI.RESET}`
252
- : `${ansi_1.ANSI.FG_GRAY}◯${ansi_1.ANSI.RESET}`;
351
+ ? `${MepCLI.theme.success}◉${ansi_1.ANSI.RESET}`
352
+ : `${MepCLI.theme.muted}◯${ansi_1.ANSI.RESET}`;
253
353
  const title = index === this.selectedIndex
254
- ? `${ansi_1.ANSI.FG_CYAN}${choice.title}${ansi_1.ANSI.RESET}`
354
+ ? `${MepCLI.theme.main}${choice.title}${ansi_1.ANSI.RESET}`
255
355
  : choice.title;
256
356
  this.print(`${cursor} ${checkbox} ${title}\n`);
257
357
  });
258
358
  if (this.errorMsg) {
259
- this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.FG_RED}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
359
+ this.print(`${ansi_1.ANSI.ERASE_LINE}${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
260
360
  }
261
361
  else if (!firstRender) {
262
362
  this.print(`${ansi_1.ANSI.ERASE_LINE}`);
@@ -323,9 +423,9 @@ class ConfirmPrompt extends Prompt {
323
423
  this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
324
424
  }
325
425
  const hint = this.value ? `${ansi_1.ANSI.BOLD}Yes${ansi_1.ANSI.RESET}/no` : `yes/${ansi_1.ANSI.BOLD}No${ansi_1.ANSI.RESET}`;
326
- this.print(`${ansi_1.ANSI.FG_GREEN}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${this.options.message}${ansi_1.ANSI.RESET} ${ansi_1.ANSI.FG_GRAY}(${hint})${ansi_1.ANSI.RESET} `);
426
+ this.print(`${MepCLI.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${MepCLI.theme.muted}(${hint})${ansi_1.ANSI.RESET} `);
327
427
  const text = this.value ? 'Yes' : 'No';
328
- this.print(`${ansi_1.ANSI.FG_CYAN}${text}${ansi_1.ANSI.RESET}\x1b[${text.length}D`);
428
+ this.print(`${MepCLI.theme.main}${text}${ansi_1.ANSI.RESET}\x1b[${text.length}D`);
329
429
  }
330
430
  handleInput(char) {
331
431
  const c = char.toLowerCase();
@@ -343,10 +443,199 @@ class ConfirmPrompt extends Prompt {
343
443
  }
344
444
  }
345
445
  }
446
+ // --- Implementation: Toggle Prompt ---
447
+ class TogglePrompt extends Prompt {
448
+ constructor(options) {
449
+ super(options);
450
+ this.value = options.initial ?? false;
451
+ }
452
+ render(firstRender) {
453
+ this.print(ansi_1.ANSI.HIDE_CURSOR);
454
+ if (!firstRender) {
455
+ this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
456
+ }
457
+ const activeText = this.options.activeText || 'ON';
458
+ const inactiveText = this.options.inactiveText || 'OFF';
459
+ let toggleDisplay = '';
460
+ if (this.value) {
461
+ toggleDisplay = `${MepCLI.theme.main}[${ansi_1.ANSI.BOLD}${activeText}${ansi_1.ANSI.RESET}${MepCLI.theme.main}]${ansi_1.ANSI.RESET} ${MepCLI.theme.muted}${inactiveText}${ansi_1.ANSI.RESET}`;
462
+ }
463
+ else {
464
+ toggleDisplay = `${MepCLI.theme.muted}${activeText}${ansi_1.ANSI.RESET} ${MepCLI.theme.main}[${ansi_1.ANSI.BOLD}${inactiveText}${ansi_1.ANSI.RESET}${MepCLI.theme.main}]${ansi_1.ANSI.RESET}`;
465
+ }
466
+ this.print(`${MepCLI.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${toggleDisplay}`);
467
+ this.print(`\x1b[${toggleDisplay.length}D`); // Move back is not really needed as we hide cursor, but kept for consistency
468
+ }
469
+ handleInput(char) {
470
+ if (char === '\r' || char === '\n') {
471
+ this.submit(this.value);
472
+ return;
473
+ }
474
+ if (char === '\u001b[D' || char === '\u001b[C' || char === 'h' || char === 'l') { // Left/Right
475
+ this.value = !this.value;
476
+ this.render(false);
477
+ }
478
+ if (char === ' ') {
479
+ this.value = !this.value;
480
+ this.render(false);
481
+ }
482
+ }
483
+ }
484
+ // --- Implementation: Number Prompt ---
485
+ class NumberPrompt extends Prompt {
486
+ constructor(options) {
487
+ super(options);
488
+ this.cursor = 0;
489
+ this.errorMsg = '';
490
+ this.value = options.initial ?? 0;
491
+ this.stringValue = this.value.toString();
492
+ this.cursor = this.stringValue.length;
493
+ }
494
+ render(firstRender) {
495
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
496
+ if (!firstRender) {
497
+ this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
498
+ if (this.errorMsg) {
499
+ this.print(ansi_1.ANSI.UP + ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
500
+ }
501
+ }
502
+ // 1. Render the Prompt Message
503
+ this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
504
+ const icon = this.errorMsg ? `${MepCLI.theme.error}✖` : `${MepCLI.theme.success}?`;
505
+ this.print(`${icon} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `);
506
+ // 2. Render the Value
507
+ this.print(`${MepCLI.theme.main}${this.stringValue}${ansi_1.ANSI.RESET}`);
508
+ // 3. Handle Error Message
509
+ if (this.errorMsg) {
510
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
511
+ this.print(ansi_1.ANSI.UP);
512
+ const promptLen = this.options.message.length + 3;
513
+ const valLen = this.stringValue.length;
514
+ this.print(`\x1b[1000D\x1b[${promptLen + valLen}C`);
515
+ }
516
+ // 4. Position Cursor
517
+ const diff = this.stringValue.length - this.cursor;
518
+ if (diff > 0) {
519
+ this.print(`\x1b[${diff}D`);
520
+ }
521
+ }
522
+ handleInput(char) {
523
+ // Enter
524
+ if (char === '\r' || char === '\n') {
525
+ const num = parseFloat(this.stringValue);
526
+ if (isNaN(num)) {
527
+ this.errorMsg = 'Please enter a valid number.';
528
+ this.render(false);
529
+ return;
530
+ }
531
+ if (this.options.min !== undefined && num < this.options.min) {
532
+ this.errorMsg = `Minimum value is ${this.options.min}`;
533
+ this.render(false);
534
+ return;
535
+ }
536
+ if (this.options.max !== undefined && num > this.options.max) {
537
+ this.errorMsg = `Maximum value is ${this.options.max}`;
538
+ this.render(false);
539
+ return;
540
+ }
541
+ if (this.errorMsg) {
542
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
543
+ }
544
+ this.submit(num);
545
+ return;
546
+ }
547
+ // Up Arrow (Increment)
548
+ if (char === '\u001b[A') {
549
+ let num = parseFloat(this.stringValue) || 0;
550
+ num += (this.options.step ?? 1);
551
+ if (this.options.max !== undefined && num > this.options.max)
552
+ num = this.options.max;
553
+ this.stringValue = num.toString();
554
+ this.cursor = this.stringValue.length;
555
+ this.errorMsg = '';
556
+ this.render(false);
557
+ return;
558
+ }
559
+ // Down Arrow (Decrement)
560
+ if (char === '\u001b[B') {
561
+ let num = parseFloat(this.stringValue) || 0;
562
+ num -= (this.options.step ?? 1);
563
+ if (this.options.min !== undefined && num < this.options.min)
564
+ num = this.options.min;
565
+ this.stringValue = num.toString();
566
+ this.cursor = this.stringValue.length;
567
+ this.errorMsg = '';
568
+ this.render(false);
569
+ return;
570
+ }
571
+ // Backspace
572
+ if (char === '\u0008' || char === '\x7f') {
573
+ if (this.cursor > 0) {
574
+ this.stringValue = this.stringValue.slice(0, this.cursor - 1) + this.stringValue.slice(this.cursor);
575
+ this.cursor--;
576
+ this.errorMsg = '';
577
+ this.render(false);
578
+ }
579
+ return;
580
+ }
581
+ // Arrow Left
582
+ if (char === '\u001b[D') {
583
+ if (this.cursor > 0) {
584
+ this.cursor--;
585
+ this.render(false);
586
+ }
587
+ return;
588
+ }
589
+ // Arrow Right
590
+ if (char === '\u001b[C') {
591
+ if (this.cursor < this.stringValue.length) {
592
+ this.cursor++;
593
+ this.render(false);
594
+ }
595
+ return;
596
+ }
597
+ // Numeric Input (and . and -)
598
+ if (/^[0-9.\-]$/.test(char)) {
599
+ if (char === '-' && (this.cursor !== 0 || this.stringValue.includes('-')))
600
+ return;
601
+ if (char === '.' && this.stringValue.includes('.'))
602
+ return;
603
+ this.stringValue = this.stringValue.slice(0, this.cursor) + char + this.stringValue.slice(this.cursor);
604
+ this.cursor++;
605
+ this.errorMsg = '';
606
+ this.render(false);
607
+ }
608
+ }
609
+ }
346
610
  /**
347
611
  * Public Facade for MepCLI
348
612
  */
349
613
  class MepCLI {
614
+ /**
615
+ * Shows a spinner while a promise is pending.
616
+ */
617
+ static async spin(message, taskPromise) {
618
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
619
+ let i = 0;
620
+ process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
621
+ const interval = setInterval(() => {
622
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.main}${frames[i]}${ansi_1.ANSI.RESET} ${message}`);
623
+ i = (i + 1) % frames.length;
624
+ }, 80);
625
+ try {
626
+ const result = await taskPromise;
627
+ clearInterval(interval);
628
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}✔${ansi_1.ANSI.RESET} ${message}\n`);
629
+ process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
630
+ return result;
631
+ }
632
+ catch (error) {
633
+ clearInterval(interval);
634
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.error}✖${ansi_1.ANSI.RESET} ${message}\n`);
635
+ process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
636
+ throw error;
637
+ }
638
+ }
350
639
  static text(options) {
351
640
  return new TextPrompt(options).run();
352
641
  }
@@ -362,5 +651,18 @@ class MepCLI {
362
651
  static password(options) {
363
652
  return new TextPrompt({ ...options, isPassword: true }).run();
364
653
  }
654
+ static number(options) {
655
+ return new NumberPrompt(options).run();
656
+ }
657
+ static toggle(options) {
658
+ return new TogglePrompt(options).run();
659
+ }
365
660
  }
366
661
  exports.MepCLI = MepCLI;
662
+ MepCLI.theme = {
663
+ main: ansi_1.ANSI.FG_CYAN,
664
+ success: ansi_1.ANSI.FG_GREEN,
665
+ error: ansi_1.ANSI.FG_RED,
666
+ muted: ansi_1.ANSI.FG_GRAY,
667
+ title: ansi_1.ANSI.RESET
668
+ };
package/dist/types.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Type definitions for Mep CLI interactions.
3
3
  */
4
+ export interface ThemeConfig {
5
+ main: string;
6
+ success: string;
7
+ error: string;
8
+ muted: string;
9
+ title: string;
10
+ }
4
11
  export interface BaseOptions {
5
12
  message: string;
6
13
  }
@@ -10,13 +17,17 @@ export interface TextOptions extends BaseOptions {
10
17
  validate?: (value: string) => string | boolean;
11
18
  isPassword?: boolean;
12
19
  }
20
+ export interface Separator {
21
+ separator: true;
22
+ text?: string;
23
+ }
13
24
  export interface SelectChoice {
14
25
  title: string;
15
26
  value: any;
16
27
  description?: string;
17
28
  }
18
29
  export interface SelectOptions extends BaseOptions {
19
- choices: SelectChoice[];
30
+ choices: (SelectChoice | Separator)[];
20
31
  }
21
32
  export interface CheckboxChoice extends SelectChoice {
22
33
  selected?: boolean;
@@ -29,3 +40,15 @@ export interface CheckboxOptions extends BaseOptions {
29
40
  export interface ConfirmOptions extends BaseOptions {
30
41
  initial?: boolean;
31
42
  }
43
+ export interface NumberOptions extends BaseOptions {
44
+ initial?: number;
45
+ min?: number;
46
+ max?: number;
47
+ step?: number;
48
+ placeholder?: string;
49
+ }
50
+ export interface ToggleOptions extends BaseOptions {
51
+ initial?: boolean;
52
+ activeText?: string;
53
+ inactiveText?: string;
54
+ }
package/example.ts CHANGED
@@ -1,83 +1,83 @@
1
- import { MepCLI } from './src'; // Or mepcli if installed via NPM
2
-
3
- /**
4
- * Runs a comprehensive set of tests for all MepCLI prompt types.
5
- */
6
- async function runAllTests() {
7
- console.clear();
8
- console.log("--- MepCLI Comprehensive Test Suite (Neutralized) ---\n");
9
-
10
- try {
11
- // --- 1. Text Prompt Test (with Validation) ---
12
- const projectName = await MepCLI.text({
13
- message: "Enter the name for your new project:",
14
- placeholder: "e.g., minimalist-cli-app",
15
- initial: "MepProject",
16
- validate: (value) => {
17
- if (value.length < 3) {
18
- return "Project name must be at least 3 characters long.";
19
- }
20
- if (value.includes('&')) {
21
- return "Project name cannot contain '&' symbol.";
22
- }
23
- return true;
24
- }
25
- });
26
- console.log(`\n✅ Text Result: Project name set to '${projectName}'`);
27
-
28
- // --- 2. Password Prompt Test ---
29
- const apiKey = await MepCLI.password({
30
- message: "Enter the project's external API key:",
31
- placeholder: "Input will be hidden..."
32
- });
33
- // Note: Do not log the actual key in a real app.
34
- console.log(`\n✅ Password Result: API key entered (length: ${apiKey.length})`);
35
-
36
-
37
- // --- 3. Select Prompt Test (Single Choice) ---
38
- const theme = await MepCLI.select({
39
- message: "Choose your preferred editor color theme:",
40
- choices: [
41
- { title: "Dark Mode (Default)", value: "dark" },
42
- { title: "Light Mode (Classic)", value: "light" },
43
- { title: "High Contrast (Accessibility)", value: "contrast" },
44
- { title: "Monokai Pro", value: "monokai" },
45
- ]
46
- });
47
- console.log(`\n✅ Select Result: Chosen theme is: ${theme}`);
48
-
49
-
50
- // --- 4. Checkbox Prompt Test (Multi-Choice with Min/Max) ---
51
- const buildTools = await MepCLI.checkbox({
52
- message: "Select your required bundlers/build tools (Min 1, Max 2):",
53
- min: 1,
54
- max: 2,
55
- choices: [
56
- { title: "Webpack", value: "webpack" },
57
- { title: "Vite", value: "vite", selected: true },
58
- { title: "Rollup", value: "rollup" },
59
- { title: "esbuild", value: "esbuild" }
60
- ]
61
- });
62
- console.log(`\n✅ Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
63
-
64
-
65
- // --- 5. Confirm Prompt Test ---
66
- const proceed = await MepCLI.confirm({
67
- message: "Do you want to continue with the installation setup?",
68
- initial: true
69
- });
70
- console.log(`\n✅ Confirm Result: Setup decision: ${proceed ? 'Proceed' : 'Cancel'}`);
71
-
72
- console.log("\n--- All MepCLI tests completed successfully! ---");
73
-
74
- } catch (e) {
75
- if (e instanceof Error && e.message === 'User force closed') {
76
- console.log("\nOperation cancelled by user (Ctrl+C).");
77
- } else {
78
- console.error("\nAn error occurred during prompt execution:", e);
79
- }
80
- }
81
- }
82
-
1
+ import { MepCLI } from './src'; // Or mepcli if installed via NPM
2
+
3
+ /**
4
+ * Runs a comprehensive set of tests for all MepCLI prompt types.
5
+ */
6
+ async function runAllTests() {
7
+ console.clear();
8
+ console.log("--- MepCLI Comprehensive Test Suite (Neutralized) ---\n");
9
+
10
+ try {
11
+ // --- 1. Text Prompt Test (with Validation) ---
12
+ const projectName = await MepCLI.text({
13
+ message: "Enter the name for your new project:",
14
+ placeholder: "e.g., minimalist-cli-app",
15
+ initial: "MepProject",
16
+ validate: (value) => {
17
+ if (value.length < 3) {
18
+ return "Project name must be at least 3 characters long.";
19
+ }
20
+ if (value.includes('&')) {
21
+ return "Project name cannot contain '&' symbol.";
22
+ }
23
+ return true;
24
+ }
25
+ });
26
+ console.log(`\n✅ Text Result: Project name set to '${projectName}'`);
27
+
28
+ // --- 2. Password Prompt Test ---
29
+ const apiKey = await MepCLI.password({
30
+ message: "Enter the project's external API key:",
31
+ placeholder: "Input will be hidden..."
32
+ });
33
+ // Note: Do not log the actual key in a real app.
34
+ console.log(`\n✅ Password Result: API key entered (length: ${apiKey.length})`);
35
+
36
+
37
+ // --- 3. Select Prompt Test (Single Choice) ---
38
+ const theme = await MepCLI.select({
39
+ message: "Choose your preferred editor color theme:",
40
+ choices: [
41
+ { title: "Dark Mode (Default)", value: "dark" },
42
+ { title: "Light Mode (Classic)", value: "light" },
43
+ { title: "High Contrast (Accessibility)", value: "contrast" },
44
+ { title: "Monokai Pro", value: "monokai" },
45
+ ]
46
+ });
47
+ console.log(`\n✅ Select Result: Chosen theme is: ${theme}`);
48
+
49
+
50
+ // --- 4. Checkbox Prompt Test (Multi-Choice with Min/Max) ---
51
+ const buildTools = await MepCLI.checkbox({
52
+ message: "Select your required bundlers/build tools (Min 1, Max 2):",
53
+ min: 1,
54
+ max: 2,
55
+ choices: [
56
+ { title: "Webpack", value: "webpack" },
57
+ { title: "Vite", value: "vite", selected: true },
58
+ { title: "Rollup", value: "rollup" },
59
+ { title: "esbuild", value: "esbuild" }
60
+ ]
61
+ });
62
+ console.log(`\n✅ Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
63
+
64
+
65
+ // --- 5. Confirm Prompt Test ---
66
+ const proceed = await MepCLI.confirm({
67
+ message: "Do you want to continue with the installation setup?",
68
+ initial: true
69
+ });
70
+ console.log(`\n✅ Confirm Result: Setup decision: ${proceed ? 'Proceed' : 'Cancel'}`);
71
+
72
+ console.log("\n--- All MepCLI tests completed successfully! ---");
73
+
74
+ } catch (e) {
75
+ if (e instanceof Error && e.message === 'User force closed') {
76
+ console.log("\nOperation cancelled by user (Ctrl+C).");
77
+ } else {
78
+ console.error("\nAn error occurred during prompt execution:", e);
79
+ }
80
+ }
81
+ }
82
+
83
83
  runAllTests();
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
- {
2
- "name": "mepcli",
3
- "version": "0.1.0",
4
- "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/CodeTease/mep.git"
8
- },
9
- "main": "dist/index.js",
10
- "types": "dist/index.d.ts",
11
- "files": [
12
- "dist",
13
- "README.md",
14
- "LICENSE",
15
- "example.ts"
16
- ],
17
- "scripts": {
18
- "build": "tsc",
19
- "prepublishOnly": "npm run build",
20
- "test": "ts-node example.ts"
21
- },
22
- "keywords": [
23
- "cli",
24
- "prompt",
25
- "inquirer",
26
- "interactive",
27
- "zero-dependency",
28
- "codetease"
29
- ],
30
- "author": "CodeTease",
31
- "license": "MIT",
32
- "devDependencies": {
33
- "@types/node": "^20.0.0",
34
- "ts-node": "^10.9.0",
35
- "typescript": "^5.0.0"
36
- }
37
- }
1
+ {
2
+ "name": "mepcli",
3
+ "version": "0.2.0",
4
+ "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/CodeTease/mep.git"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE",
15
+ "example.ts"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepublishOnly": "npm run build",
20
+ "test": "ts-node example.ts"
21
+ },
22
+ "keywords": [
23
+ "cli",
24
+ "prompt",
25
+ "inquirer",
26
+ "interactive",
27
+ "zero-dependency",
28
+ "codetease"
29
+ ],
30
+ "author": "CodeTease",
31
+ "license": "MIT",
32
+ "devDependencies": {
33
+ "@types/node": "^20.19.25",
34
+ "ts-node": "^10.9.0",
35
+ "typescript": "^5.0.0"
36
+ }
37
+ }