mepcli 0.1.0 → 0.2.1

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,8 @@ class TextPrompt extends Prompt {
77
77
  super(options);
78
78
  this.errorMsg = '';
79
79
  this.cursor = 0;
80
+ this.hasTyped = false;
81
+ this.renderLines = 1;
80
82
  this.value = options.initial || '';
81
83
  this.cursor = this.value.length;
82
84
  }
@@ -84,62 +86,147 @@ class TextPrompt extends Prompt {
84
86
  // TextPrompt needs the cursor visible!
85
87
  this.print(ansi_1.ANSI.SHOW_CURSOR);
86
88
  if (!firstRender) {
87
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
88
- if (this.errorMsg) {
89
- this.print(ansi_1.ANSI.UP + ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
89
+ // Clear previous lines
90
+ // Note: renderLines now represents visual wrapped lines
91
+ for (let i = 0; i < this.renderLines; i++) {
92
+ this.print(ansi_1.ANSI.ERASE_LINE);
93
+ if (i < this.renderLines - 1)
94
+ this.print(ansi_1.ANSI.UP);
90
95
  }
96
+ this.print(ansi_1.ANSI.CURSOR_LEFT);
91
97
  }
98
+ let output = '';
92
99
  // 1. Render the Prompt Message
93
- 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} `);
100
+ const icon = this.errorMsg ? `${MepCLI.theme.error}✖` : `${MepCLI.theme.success}?`;
101
+ const multilineHint = this.options.multiline ? ` ${MepCLI.theme.muted}(Press Ctrl+D to submit)${ansi_1.ANSI.RESET}` : '';
102
+ output += `${icon} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${multilineHint} `;
96
103
  // 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}`);
99
- // Move cursor back to start so typing overwrites placeholder visually
100
- this.print(`\x1b[${this.options.placeholder.length}D`);
104
+ let displayValue = '';
105
+ if (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped) {
106
+ displayValue = `${MepCLI.theme.muted}${this.options.placeholder}${ansi_1.ANSI.RESET}`;
101
107
  }
102
108
  else {
103
- const displayValue = this.options.isPassword ? '*'.repeat(this.value.length) : this.value;
104
- this.print(`${ansi_1.ANSI.FG_CYAN}${displayValue}${ansi_1.ANSI.RESET}`);
109
+ displayValue = this.options.isPassword ? '*'.repeat(this.value.length) : this.value;
110
+ displayValue = `${MepCLI.theme.main}${displayValue}${ansi_1.ANSI.RESET}`;
105
111
  }
112
+ output += displayValue;
106
113
  // 3. Handle Error Message
107
114
  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(ansi_1.ANSI.UP); // Go back to input line
110
- // Re-calculate position to end of input
111
- const promptLen = this.options.message.length + 3; // Icon + 2 spaces
112
- const valLen = this.value.length;
113
- // Move to absolute start of line, then move right to end of string
114
- this.print(`\x1b[1000D\x1b[${promptLen + valLen}C`);
115
+ output += `\n${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
115
116
  }
116
- // 4. Position Cursor Logic
117
- // At this point, the physical cursor is at the END of the value string.
118
- // We need to move it LEFT by (length - cursor_index)
119
- const diff = this.value.length - this.cursor;
120
- if (diff > 0) {
121
- this.print(`\x1b[${diff}D`);
117
+ this.print(output);
118
+ // 4. Calculate Visual Metrics for Wrapping
119
+ const cols = process.stdout.columns || 80;
120
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
121
+ // Prompt String (visual part before value)
122
+ const promptStr = `${icon} ${MepCLI.theme.title}${this.options.message} ${multilineHint} `;
123
+ const promptVisualLen = stripAnsi(promptStr).length;
124
+ // Value String (visual part)
125
+ const rawValue = (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped)
126
+ ? this.options.placeholder || ''
127
+ : (this.options.isPassword ? '*'.repeat(this.value.length) : this.value);
128
+ // Error String (visual part)
129
+ const errorVisualLines = this.errorMsg ? Math.ceil((3 + this.errorMsg.length) / cols) : 0;
130
+ // Calculate Total Lines and Cursor Position
131
+ // We simulate printing the prompt + value + error
132
+ let currentVisualLine = 0;
133
+ let currentCol = 0;
134
+ // State tracking for cursor
135
+ let cursorRow = 0;
136
+ let cursorCol = 0;
137
+ // Add Prompt
138
+ currentCol += promptVisualLen;
139
+ while (currentCol >= cols) {
140
+ currentVisualLine++;
141
+ currentCol -= cols;
142
+ }
143
+ // Add Value (Character by character to handle wrapping and cursor tracking accurately)
144
+ // Note: This doesn't handle multi-width chars perfectly, but handles wrapping better than before
145
+ const valueLen = rawValue.length;
146
+ // If placeholder, we treat it as value for render height, but cursor is at 0
147
+ const isPlaceholder = (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped);
148
+ for (let i = 0; i < valueLen; i++) {
149
+ // Check if we are at cursor position
150
+ if (!isPlaceholder && i === this.cursor) {
151
+ cursorRow = currentVisualLine;
152
+ cursorCol = currentCol;
153
+ }
154
+ const char = rawValue[i];
155
+ if (char === '\n') {
156
+ currentVisualLine++;
157
+ currentCol = 0;
158
+ }
159
+ else {
160
+ currentCol++;
161
+ if (currentCol >= cols) {
162
+ currentVisualLine++;
163
+ currentCol = 0;
164
+ }
165
+ }
166
+ }
167
+ // If cursor is at the very end
168
+ if (!isPlaceholder && this.cursor === valueLen) {
169
+ cursorRow = currentVisualLine;
170
+ cursorCol = currentCol;
171
+ }
172
+ // If placeholder, cursor is at start of value
173
+ if (isPlaceholder) {
174
+ // Re-calc cursor position as if it's at index 0 of value
175
+ // Which is effectively where prompt ends
176
+ // We already updated currentCol/Line for prompt above, but loop continued for placeholder
177
+ // So we need to recalculate or store prompt end state
178
+ // Let's just use prompt end state:
179
+ let pCol = promptVisualLen;
180
+ let pRow = 0;
181
+ while (pCol >= cols) {
182
+ pRow++;
183
+ pCol -= cols;
184
+ }
185
+ cursorRow = pRow;
186
+ cursorCol = pCol;
187
+ }
188
+ // Final height
189
+ // If we are at col 0 of a new line (e.g. just wrapped or \n), we count that line
190
+ // currentVisualLine is 0-indexed index of the line we are on.
191
+ // Total lines = currentVisualLine + 1 + errorLines
192
+ // Special case: if input ends with \n, we are on a new empty line
193
+ const totalValueRows = currentVisualLine + 1;
194
+ this.renderLines = totalValueRows + errorVisualLines;
195
+ // 5. Position Cursor Logic
196
+ // We are currently at the end of output.
197
+ // End row relative to start: this.renderLines - 1
198
+ const endRow = this.renderLines - 1;
199
+ // Move up to cursor row
200
+ const linesUp = endRow - cursorRow;
201
+ if (linesUp > 0) {
202
+ this.print(`\x1b[${linesUp}A`);
203
+ }
204
+ // Move to cursor col
205
+ this.print(ansi_1.ANSI.CURSOR_LEFT); // Go to col 0
206
+ if (cursorCol > 0) {
207
+ this.print(`\x1b[${cursorCol}C`);
122
208
  }
123
209
  }
124
210
  handleInput(char) {
125
211
  // Enter
126
212
  if (char === '\r' || char === '\n') {
127
- if (this.options.validate) {
128
- const validationResult = this.options.validate(this.value);
129
- if (typeof validationResult === 'string' && validationResult.length > 0) {
130
- this.errorMsg = validationResult;
131
- this.render(false);
132
- return;
133
- }
134
- }
135
- if (this.errorMsg) {
136
- this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
213
+ if (this.options.multiline) {
214
+ this.value = this.value.slice(0, this.cursor) + '\n' + this.value.slice(this.cursor);
215
+ this.cursor++;
216
+ this.render(false);
217
+ return;
137
218
  }
138
- this.submit(this.value);
219
+ this.validateAndSubmit();
220
+ return;
221
+ }
222
+ // Ctrl+D (EOF) or Ctrl+S for Submit in Multiline
223
+ if (this.options.multiline && (char === '\u0004' || char === '\u0013')) {
224
+ this.validateAndSubmit();
139
225
  return;
140
226
  }
141
227
  // Backspace
142
228
  if (char === '\u0008' || char === '\x7f') {
229
+ this.hasTyped = true;
143
230
  if (this.cursor > 0) {
144
231
  this.value = this.value.slice(0, this.cursor - 1) + this.value.slice(this.cursor);
145
232
  this.cursor--;
@@ -166,6 +253,7 @@ class TextPrompt extends Prompt {
166
253
  }
167
254
  // Delete key
168
255
  if (char === '\u001b[3~') {
256
+ this.hasTyped = true;
169
257
  if (this.cursor < this.value.length) {
170
258
  this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + 1);
171
259
  this.errorMsg = '';
@@ -173,54 +261,218 @@ class TextPrompt extends Prompt {
173
261
  }
174
262
  return;
175
263
  }
176
- // Regular Typing
177
- if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
264
+ // Regular Typing & Paste
265
+ if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
266
+ this.hasTyped = true;
178
267
  this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
179
- this.cursor++;
268
+ this.cursor += char.length;
180
269
  this.errorMsg = '';
181
270
  this.render(false);
182
271
  }
183
272
  }
273
+ validateAndSubmit() {
274
+ if (this.options.validate) {
275
+ const result = this.options.validate(this.value);
276
+ // Handle Promise validation
277
+ if (result instanceof Promise) {
278
+ // Show loading state
279
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${MepCLI.theme.main}Validating...${ansi_1.ANSI.RESET}`);
280
+ this.print(ansi_1.ANSI.UP);
281
+ result.then(valid => {
282
+ // Clear loading message
283
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}`);
284
+ this.print(ansi_1.ANSI.UP);
285
+ if (typeof valid === 'string' && valid.length > 0) {
286
+ this.errorMsg = valid;
287
+ this.render(false);
288
+ }
289
+ else if (valid === false) {
290
+ this.errorMsg = 'Invalid input';
291
+ this.render(false);
292
+ }
293
+ else {
294
+ if (this.errorMsg) {
295
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
296
+ }
297
+ this.submit(this.value);
298
+ }
299
+ }).catch(err => {
300
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}`);
301
+ this.print(ansi_1.ANSI.UP);
302
+ this.errorMsg = err.message || 'Validation failed';
303
+ this.render(false);
304
+ });
305
+ return;
306
+ }
307
+ // Handle Sync validation
308
+ if (typeof result === 'string' && result.length > 0) {
309
+ this.errorMsg = result;
310
+ this.render(false);
311
+ return;
312
+ }
313
+ if (result === false) {
314
+ this.errorMsg = 'Invalid input';
315
+ this.render(false);
316
+ return;
317
+ }
318
+ }
319
+ if (this.errorMsg) {
320
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
321
+ }
322
+ this.submit(this.value);
323
+ }
184
324
  }
185
325
  // --- Implementation: Select Prompt ---
186
326
  class SelectPrompt extends Prompt {
187
327
  constructor(options) {
188
328
  super(options);
189
329
  this.selectedIndex = 0;
330
+ this.searchBuffer = '';
331
+ this.scrollTop = 0;
332
+ this.pageSize = 7;
333
+ // Custom render to handle variable height clearing
334
+ this.lastRenderHeight = 0;
335
+ // Find first non-separator index
336
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
190
337
  }
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`);
338
+ isSeparator(item) {
339
+ return item && item.separator === true;
340
+ }
341
+ findNextSelectableIndex(currentIndex, direction) {
342
+ let nextIndex = currentIndex + direction;
343
+ const choices = this.getFilteredChoices();
344
+ // Loop around logic
345
+ if (nextIndex < 0)
346
+ nextIndex = choices.length - 1;
347
+ if (nextIndex >= choices.length)
348
+ nextIndex = 0;
349
+ if (choices.length === 0)
350
+ return 0;
351
+ // Safety check to prevent infinite loop if all are separators (shouldn't happen in practice)
352
+ let count = 0;
353
+ while (this.isSeparator(choices[nextIndex]) && count < choices.length) {
354
+ nextIndex += direction;
355
+ if (nextIndex < 0)
356
+ nextIndex = choices.length - 1;
357
+ if (nextIndex >= choices.length)
358
+ nextIndex = 0;
359
+ count++;
196
360
  }
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
- }
361
+ return nextIndex;
362
+ }
363
+ getFilteredChoices() {
364
+ if (!this.searchBuffer)
365
+ return this.options.choices;
366
+ return this.options.choices.filter(c => {
367
+ if (this.isSeparator(c))
368
+ return false; // Hide separators when searching
369
+ return c.title.toLowerCase().includes(this.searchBuffer.toLowerCase());
207
370
  });
208
371
  }
372
+ renderWrapper(firstRender) {
373
+ if (!firstRender && this.lastRenderHeight > 0) {
374
+ this.print(`\x1b[${this.lastRenderHeight}A`);
375
+ }
376
+ let output = '';
377
+ const choices = this.getFilteredChoices();
378
+ // Adjust Scroll Top
379
+ if (this.selectedIndex < this.scrollTop) {
380
+ this.scrollTop = this.selectedIndex;
381
+ }
382
+ else if (this.selectedIndex >= this.scrollTop + this.pageSize) {
383
+ this.scrollTop = this.selectedIndex - this.pageSize + 1;
384
+ }
385
+ // Handle Filtering Edge Case: if list shrinks, scrollTop might be too high
386
+ if (this.scrollTop > choices.length - 1) {
387
+ this.scrollTop = Math.max(0, choices.length - this.pageSize);
388
+ }
389
+ // Header
390
+ const searchStr = this.searchBuffer ? ` ${MepCLI.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
391
+ 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`;
392
+ if (choices.length === 0) {
393
+ output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT} ${MepCLI.theme.muted}No results found${ansi_1.ANSI.RESET}\n`;
394
+ }
395
+ else {
396
+ const visibleChoices = choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
397
+ visibleChoices.forEach((choice, index) => {
398
+ const actualIndex = this.scrollTop + index;
399
+ output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`;
400
+ if (this.isSeparator(choice)) {
401
+ output += ` ${ansi_1.ANSI.DIM}${choice.text || '────────'}${ansi_1.ANSI.RESET}\n`;
402
+ }
403
+ else {
404
+ if (actualIndex === this.selectedIndex) {
405
+ output += `${MepCLI.theme.main}❯ ${choice.title}${ansi_1.ANSI.RESET}\n`;
406
+ }
407
+ else {
408
+ output += ` ${choice.title}\n`;
409
+ }
410
+ }
411
+ });
412
+ }
413
+ this.print(output);
414
+ // Clear remaining lines if list shrunk
415
+ const visibleCount = Math.min(choices.length, this.pageSize);
416
+ const currentHeight = visibleCount + 1 + (choices.length === 0 ? 1 : 0);
417
+ const linesToClear = this.lastRenderHeight - currentHeight;
418
+ if (linesToClear > 0) {
419
+ for (let i = 0; i < linesToClear; i++) {
420
+ this.print(`${ansi_1.ANSI.ERASE_LINE}\n`);
421
+ }
422
+ this.print(`\x1b[${linesToClear}A`); // Move back up
423
+ }
424
+ this.lastRenderHeight = currentHeight;
425
+ }
426
+ render(firstRender) {
427
+ this.print(ansi_1.ANSI.HIDE_CURSOR);
428
+ this.renderWrapper(firstRender);
429
+ }
209
430
  handleInput(char) {
431
+ const choices = this.getFilteredChoices();
210
432
  if (char === '\r' || char === '\n') {
433
+ if (choices.length === 0) {
434
+ this.searchBuffer = '';
435
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
436
+ this.render(false);
437
+ return;
438
+ }
439
+ if (this.isSeparator(choices[this.selectedIndex]))
440
+ return;
211
441
  this.cleanup();
212
- this.print(`\x1b[${this.options.choices.length - this.selectedIndex}B`);
213
442
  this.print(ansi_1.ANSI.SHOW_CURSOR);
214
443
  if (this._resolve)
215
- this._resolve(this.options.choices[this.selectedIndex].value);
444
+ this._resolve(choices[this.selectedIndex].value);
216
445
  return;
217
446
  }
218
447
  if (char === '\u001b[A') { // Up
219
- this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.choices.length - 1;
220
- this.render(false);
448
+ if (choices.length > 0) {
449
+ this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, -1);
450
+ this.render(false);
451
+ }
452
+ return;
221
453
  }
222
454
  if (char === '\u001b[B') { // Down
223
- this.selectedIndex = this.selectedIndex < this.options.choices.length - 1 ? this.selectedIndex + 1 : 0;
455
+ if (choices.length > 0) {
456
+ this.selectedIndex = this.findNextSelectableIndex(this.selectedIndex, 1);
457
+ this.render(false);
458
+ }
459
+ return;
460
+ }
461
+ // Backspace
462
+ if (char === '\u0008' || char === '\x7f') {
463
+ if (this.searchBuffer.length > 0) {
464
+ this.searchBuffer = this.searchBuffer.slice(0, -1);
465
+ this.selectedIndex = 0; // Reset selection
466
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
467
+ this.render(false);
468
+ }
469
+ return;
470
+ }
471
+ // Typing
472
+ if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
473
+ this.searchBuffer += char;
474
+ this.selectedIndex = 0; // Reset selection
475
+ this.selectedIndex = this.findNextSelectableIndex(-1, 1);
224
476
  this.render(false);
225
477
  }
226
478
  }
@@ -241,22 +493,22 @@ class CheckboxPrompt extends Prompt {
241
493
  this.print(`\x1b[${this.options.choices.length + 1 + extraLines}A`);
242
494
  }
243
495
  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`);
496
+ const icon = this.errorMsg ? `${MepCLI.theme.error}✖` : `${MepCLI.theme.success}?`;
497
+ 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
498
  this.options.choices.forEach((choice, index) => {
247
499
  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}` : ' ';
500
+ const cursor = index === this.selectedIndex ? `${MepCLI.theme.main}❯${ansi_1.ANSI.RESET}` : ' ';
249
501
  const isChecked = this.checkedState[index];
250
502
  const checkbox = isChecked
251
- ? `${ansi_1.ANSI.FG_GREEN}◉${ansi_1.ANSI.RESET}`
252
- : `${ansi_1.ANSI.FG_GRAY}◯${ansi_1.ANSI.RESET}`;
503
+ ? `${MepCLI.theme.success}◉${ansi_1.ANSI.RESET}`
504
+ : `${MepCLI.theme.muted}◯${ansi_1.ANSI.RESET}`;
253
505
  const title = index === this.selectedIndex
254
- ? `${ansi_1.ANSI.FG_CYAN}${choice.title}${ansi_1.ANSI.RESET}`
506
+ ? `${MepCLI.theme.main}${choice.title}${ansi_1.ANSI.RESET}`
255
507
  : choice.title;
256
508
  this.print(`${cursor} ${checkbox} ${title}\n`);
257
509
  });
258
510
  if (this.errorMsg) {
259
- this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.FG_RED}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
511
+ this.print(`${ansi_1.ANSI.ERASE_LINE}${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
260
512
  }
261
513
  else if (!firstRender) {
262
514
  this.print(`${ansi_1.ANSI.ERASE_LINE}`);
@@ -323,9 +575,9 @@ class ConfirmPrompt extends Prompt {
323
575
  this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
324
576
  }
325
577
  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} `);
578
+ 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
579
  const text = this.value ? 'Yes' : 'No';
328
- this.print(`${ansi_1.ANSI.FG_CYAN}${text}${ansi_1.ANSI.RESET}\x1b[${text.length}D`);
580
+ this.print(`${MepCLI.theme.main}${text}${ansi_1.ANSI.RESET}\x1b[${text.length}D`);
329
581
  }
330
582
  handleInput(char) {
331
583
  const c = char.toLowerCase();
@@ -343,10 +595,203 @@ class ConfirmPrompt extends Prompt {
343
595
  }
344
596
  }
345
597
  }
598
+ // --- Implementation: Toggle Prompt ---
599
+ class TogglePrompt extends Prompt {
600
+ constructor(options) {
601
+ super(options);
602
+ this.value = options.initial ?? false;
603
+ }
604
+ render(firstRender) {
605
+ this.print(ansi_1.ANSI.HIDE_CURSOR);
606
+ if (!firstRender) {
607
+ this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
608
+ }
609
+ const activeText = this.options.activeText || 'ON';
610
+ const inactiveText = this.options.inactiveText || 'OFF';
611
+ let toggleDisplay = '';
612
+ if (this.value) {
613
+ 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}`;
614
+ }
615
+ else {
616
+ 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}`;
617
+ }
618
+ this.print(`${MepCLI.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${toggleDisplay}`);
619
+ this.print(`\x1b[${toggleDisplay.length}D`); // Move back is not really needed as we hide cursor, but kept for consistency
620
+ }
621
+ handleInput(char) {
622
+ if (char === '\r' || char === '\n') {
623
+ this.submit(this.value);
624
+ return;
625
+ }
626
+ if (char === '\u001b[D' || char === '\u001b[C' || char === 'h' || char === 'l') { // Left/Right
627
+ this.value = !this.value;
628
+ this.render(false);
629
+ }
630
+ if (char === ' ') {
631
+ this.value = !this.value;
632
+ this.render(false);
633
+ }
634
+ }
635
+ }
636
+ // --- Implementation: Number Prompt ---
637
+ class NumberPrompt extends Prompt {
638
+ constructor(options) {
639
+ super(options);
640
+ this.cursor = 0;
641
+ this.errorMsg = '';
642
+ this.value = options.initial ?? 0;
643
+ this.stringValue = this.value.toString();
644
+ this.cursor = this.stringValue.length;
645
+ }
646
+ render(firstRender) {
647
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
648
+ if (!firstRender) {
649
+ this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
650
+ if (this.errorMsg) {
651
+ this.print(ansi_1.ANSI.UP + ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
652
+ }
653
+ }
654
+ // 1. Render the Prompt Message
655
+ this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
656
+ const icon = this.errorMsg ? `${MepCLI.theme.error}✖` : `${MepCLI.theme.success}?`;
657
+ this.print(`${icon} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `);
658
+ // 2. Render the Value
659
+ this.print(`${MepCLI.theme.main}${this.stringValue}${ansi_1.ANSI.RESET}`);
660
+ // 3. Handle Error Message
661
+ if (this.errorMsg) {
662
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
663
+ this.print(ansi_1.ANSI.UP);
664
+ const promptLen = this.options.message.length + 3;
665
+ const valLen = this.stringValue.length;
666
+ this.print(`\x1b[1000D\x1b[${promptLen + valLen}C`);
667
+ }
668
+ // 4. Position Cursor
669
+ const diff = this.stringValue.length - this.cursor;
670
+ if (diff > 0) {
671
+ this.print(`\x1b[${diff}D`);
672
+ }
673
+ }
674
+ handleInput(char) {
675
+ // Enter
676
+ if (char === '\r' || char === '\n') {
677
+ const num = parseFloat(this.stringValue);
678
+ if (isNaN(num)) {
679
+ this.errorMsg = 'Please enter a valid number.';
680
+ this.render(false);
681
+ return;
682
+ }
683
+ if (this.options.min !== undefined && num < this.options.min) {
684
+ this.errorMsg = `Minimum value is ${this.options.min}`;
685
+ this.render(false);
686
+ return;
687
+ }
688
+ if (this.options.max !== undefined && num > this.options.max) {
689
+ this.errorMsg = `Maximum value is ${this.options.max}`;
690
+ this.render(false);
691
+ return;
692
+ }
693
+ if (this.errorMsg) {
694
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
695
+ }
696
+ this.submit(num);
697
+ return;
698
+ }
699
+ // Up Arrow (Increment)
700
+ if (char === '\u001b[A') {
701
+ let num = parseFloat(this.stringValue) || 0;
702
+ num += (this.options.step ?? 1);
703
+ if (this.options.max !== undefined && num > this.options.max)
704
+ num = this.options.max;
705
+ this.stringValue = num.toString();
706
+ this.cursor = this.stringValue.length;
707
+ this.errorMsg = '';
708
+ this.render(false);
709
+ return;
710
+ }
711
+ // Down Arrow (Decrement)
712
+ if (char === '\u001b[B') {
713
+ let num = parseFloat(this.stringValue) || 0;
714
+ num -= (this.options.step ?? 1);
715
+ if (this.options.min !== undefined && num < this.options.min)
716
+ num = this.options.min;
717
+ this.stringValue = num.toString();
718
+ this.cursor = this.stringValue.length;
719
+ this.errorMsg = '';
720
+ this.render(false);
721
+ return;
722
+ }
723
+ // Backspace
724
+ if (char === '\u0008' || char === '\x7f') {
725
+ if (this.cursor > 0) {
726
+ this.stringValue = this.stringValue.slice(0, this.cursor - 1) + this.stringValue.slice(this.cursor);
727
+ this.cursor--;
728
+ this.errorMsg = '';
729
+ this.render(false);
730
+ }
731
+ return;
732
+ }
733
+ // Arrow Left
734
+ if (char === '\u001b[D') {
735
+ if (this.cursor > 0) {
736
+ this.cursor--;
737
+ this.render(false);
738
+ }
739
+ return;
740
+ }
741
+ // Arrow Right
742
+ if (char === '\u001b[C') {
743
+ if (this.cursor < this.stringValue.length) {
744
+ this.cursor++;
745
+ this.render(false);
746
+ }
747
+ return;
748
+ }
749
+ // Numeric Input (and . and -)
750
+ // Simple paste support for numbers is also good
751
+ if (/^[0-9.\-]+$/.test(char)) {
752
+ // Basic validation for pasted content
753
+ if (char.includes('-') && (this.cursor !== 0 || this.stringValue.includes('-') || char.lastIndexOf('-') > 0)) {
754
+ // If complex paste fails simple checks, ignore or let user correct
755
+ // For now, strict check on single char logic is preserved if we want,
756
+ // but let's allow pasting valid number strings
757
+ }
758
+ // Allow if it looks like a number part
759
+ this.stringValue = this.stringValue.slice(0, this.cursor) + char + this.stringValue.slice(this.cursor);
760
+ this.cursor += char.length;
761
+ this.errorMsg = '';
762
+ this.render(false);
763
+ }
764
+ }
765
+ }
346
766
  /**
347
767
  * Public Facade for MepCLI
348
768
  */
349
769
  class MepCLI {
770
+ /**
771
+ * Shows a spinner while a promise is pending.
772
+ */
773
+ static async spin(message, taskPromise) {
774
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
775
+ let i = 0;
776
+ process.stdout.write(ansi_1.ANSI.HIDE_CURSOR);
777
+ const interval = setInterval(() => {
778
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.main}${frames[i]}${ansi_1.ANSI.RESET} ${message}`);
779
+ i = (i + 1) % frames.length;
780
+ }, 80);
781
+ try {
782
+ const result = await taskPromise;
783
+ clearInterval(interval);
784
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.success}✔${ansi_1.ANSI.RESET} ${message}\n`);
785
+ process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
786
+ return result;
787
+ }
788
+ catch (error) {
789
+ clearInterval(interval);
790
+ process.stdout.write(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}${MepCLI.theme.error}✖${ansi_1.ANSI.RESET} ${message}\n`);
791
+ process.stdout.write(ansi_1.ANSI.SHOW_CURSOR);
792
+ throw error;
793
+ }
794
+ }
350
795
  static text(options) {
351
796
  return new TextPrompt(options).run();
352
797
  }
@@ -362,5 +807,18 @@ class MepCLI {
362
807
  static password(options) {
363
808
  return new TextPrompt({ ...options, isPassword: true }).run();
364
809
  }
810
+ static number(options) {
811
+ return new NumberPrompt(options).run();
812
+ }
813
+ static toggle(options) {
814
+ return new TogglePrompt(options).run();
815
+ }
365
816
  }
366
817
  exports.MepCLI = MepCLI;
818
+ MepCLI.theme = {
819
+ main: ansi_1.ANSI.FG_CYAN,
820
+ success: ansi_1.ANSI.FG_GREEN,
821
+ error: ansi_1.ANSI.FG_RED,
822
+ muted: ansi_1.ANSI.FG_GRAY,
823
+ title: ansi_1.ANSI.RESET
824
+ };
package/dist/types.d.ts CHANGED
@@ -1,14 +1,26 @@
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
  }
7
14
  export interface TextOptions extends BaseOptions {
8
15
  placeholder?: string;
9
16
  initial?: string;
10
- validate?: (value: string) => string | boolean;
17
+ validate?: (value: string) => string | boolean | Promise<string | boolean>;
11
18
  isPassword?: boolean;
19
+ multiline?: boolean;
20
+ }
21
+ export interface Separator {
22
+ separator: true;
23
+ text?: string;
12
24
  }
13
25
  export interface SelectChoice {
14
26
  title: string;
@@ -16,7 +28,7 @@ export interface SelectChoice {
16
28
  description?: string;
17
29
  }
18
30
  export interface SelectOptions extends BaseOptions {
19
- choices: SelectChoice[];
31
+ choices: (SelectChoice | Separator)[];
20
32
  }
21
33
  export interface CheckboxChoice extends SelectChoice {
22
34
  selected?: boolean;
@@ -29,3 +41,15 @@ export interface CheckboxOptions extends BaseOptions {
29
41
  export interface ConfirmOptions extends BaseOptions {
30
42
  initial?: boolean;
31
43
  }
44
+ export interface NumberOptions extends BaseOptions {
45
+ initial?: number;
46
+ min?: number;
47
+ max?: number;
48
+ step?: number;
49
+ placeholder?: string;
50
+ }
51
+ export interface ToggleOptions extends BaseOptions {
52
+ initial?: boolean;
53
+ activeText?: string;
54
+ inactiveText?: string;
55
+ }
package/example.ts CHANGED
@@ -1,83 +1,103 @@
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
- runAllTests();
1
+ import { MepCLI } from './src'; // Or 'mepcli' if installed via NPM
2
+
3
+ /**
4
+ * Runs a comprehensive demo showcasing all MepCLI prompt types and utilities.
5
+ * This demonstrates all core functionalities including Text, Password, Select,
6
+ * Checkbox, Number, Toggle, Confirm, and the Spin utility.
7
+ */
8
+ async function runComprehensiveDemo() {
9
+ console.clear();
10
+ console.log("--- MepCLI Comprehensive Demo (All 7 Prompts + Spin Utility) ---\n");
11
+
12
+ try {
13
+ // --- 1. Text Prompt (Input with Validation and initial value) ---
14
+ const projectName = await MepCLI.text({
15
+ message: "Enter the name for your new project:",
16
+ placeholder: "e.g., minimalist-cli-app",
17
+ initial: "MepProject",
18
+ validate: (value) => {
19
+ if (value.length < 3) return "Project name must be at least 3 characters long.";
20
+ return true;
21
+ }
22
+ });
23
+ console.log(`\n✅ Text Result: Project name set to '${projectName}'`);
24
+
25
+ // --- 2. Password Prompt (Hidden input) ---
26
+ const apiKey = await MepCLI.password({
27
+ message: "Enter the project's external API key:",
28
+ placeholder: "Input will be hidden..."
29
+ });
30
+ console.log(`\n✅ Password Result: API key entered (length: ${apiKey.length})`);
31
+
32
+ // --- 3. Select Prompt (Single choice, supports filtering/searching by typing) ---
33
+ const theme = await MepCLI.select({
34
+ message: "Choose your preferred editor color theme:",
35
+ choices: [
36
+ { title: "Dark Mode (Default)", value: "dark" },
37
+ { title: "Light Mode (Classic)", value: "light" },
38
+ { title: "High Contrast (Accessibility)", value: "contrast" },
39
+ // Demonstrates a separator option
40
+ { separator: true, text: "--- Pro Themes ---" },
41
+ { title: "Monokai Pro", value: "monokai" },
42
+ ]
43
+ });
44
+ console.log(`\n✅ Select Result: Chosen theme is: ${theme}`);
45
+
46
+ // --- 4. Checkbox Prompt (Multi-choice with Min/Max limits) ---
47
+ const buildTools = await MepCLI.checkbox({
48
+ message: "Select your required bundlers/build tools (Min 1, Max 2):",
49
+ min: 1,
50
+ max: 2,
51
+ choices: [
52
+ { title: "Webpack", value: "webpack" },
53
+ { title: "Vite", value: "vite", selected: true }, // Default selected state
54
+ { title: "Rollup", value: "rollup" },
55
+ { title: "esbuild", value: "esbuild" }
56
+ ]
57
+ });
58
+ console.log(`\n✅ Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
59
+
60
+ // --- 5. Number Prompt (Numeric input, supports Min/Max and Up/Down arrow for Step) ---
61
+ const port = await MepCLI.number({
62
+ message: "Which port should the server run on?",
63
+ initial: 3000,
64
+ min: 1024,
65
+ max: 65535,
66
+ step: 100 // Increments/decrements by 100 with arrows
67
+ });
68
+ console.log(`\n✅ Number Result: Server port: ${port}`);
69
+
70
+ // --- 6. Toggle Prompt (Boolean input, supports custom labels) ---
71
+ const isSecure = await MepCLI.toggle({
72
+ message: "Enable HTTPS/SSL for production?",
73
+ initial: false,
74
+ activeText: "SECURE", // Custom 'on' label
75
+ inactiveText: "INSECURE" // Custom 'off' label
76
+ });
77
+ console.log(`\n✅ Toggle Result: HTTPS enabled: ${isSecure}`);
78
+
79
+ // --- 7. Confirm Prompt (Simple Yes/No) ---
80
+ const proceed = await MepCLI.confirm({
81
+ message: "Ready to deploy the project now?",
82
+ initial: true
83
+ });
84
+ console.log(`\n✅ Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
85
+
86
+ // --- 8. Spin Utility (Loading/Async Task Indicator) ---
87
+ await MepCLI.spin(
88
+ "Finalizing configuration and deploying to Teaserverse...",
89
+ new Promise(resolve => setTimeout(resolve, 1500)) // Simulates a 1.5 second async task
90
+ );
91
+ console.log("\n--- Deployment successful! All MepCLI features demonstrated! ---");
92
+
93
+ } catch (e) {
94
+ // Global handler for Ctrl+C closure
95
+ if (e instanceof Error && e.message === 'User force closed') {
96
+ console.log("\nOperation cancelled by user (Ctrl+C).");
97
+ } else {
98
+ console.error("\nAn error occurred during prompt execution:", e);
99
+ }
100
+ }
101
+ }
102
+
103
+ runComprehensiveDemo();
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.1",
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
+ }