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 +21 -21
- package/README.md +50 -50
- package/dist/core.d.ts +8 -1
- package/dist/core.js +526 -68
- package/dist/types.d.ts +26 -2
- package/example.ts +103 -83
- package/package.json +37 -37
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
94
|
-
const
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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(
|
|
444
|
+
this._resolve(choices[this.selectedIndex].value);
|
|
216
445
|
return;
|
|
217
446
|
}
|
|
218
447
|
if (char === '\u001b[A') { // Up
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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 ? `${
|
|
245
|
-
this.print(`${icon} ${ansi_1.ANSI.BOLD}${this.options.message}${ansi_1.ANSI.RESET} ${
|
|
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 ? `${
|
|
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
|
-
? `${
|
|
252
|
-
: `${
|
|
503
|
+
? `${MepCLI.theme.success}◉${ansi_1.ANSI.RESET}`
|
|
504
|
+
: `${MepCLI.theme.muted}◯${ansi_1.ANSI.RESET}`;
|
|
253
505
|
const title = index === this.selectedIndex
|
|
254
|
-
? `${
|
|
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}${
|
|
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(`${
|
|
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(`${
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{ title: "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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.
|
|
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
|
+
}
|