mepcli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/ansi.d.ts +22 -0
- package/dist/ansi.js +27 -0
- package/dist/core.d.ts +11 -0
- package/dist/core.js +366 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.js +5 -0
- package/example.ts +83 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/README.md
ADDED
|
@@ -0,0 +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
|
+
|
|
51
|
+
This project is under the **MIT License**.
|
package/dist/ansi.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI Escape Codes
|
|
3
|
+
* Manual definitions to maintain zero-dependency status.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ANSI: {
|
|
6
|
+
RESET: string;
|
|
7
|
+
BOLD: string;
|
|
8
|
+
DIM: string;
|
|
9
|
+
ITALIC: string;
|
|
10
|
+
FG_GREEN: string;
|
|
11
|
+
FG_CYAN: string;
|
|
12
|
+
FG_YELLOW: string;
|
|
13
|
+
FG_RED: string;
|
|
14
|
+
FG_GRAY: string;
|
|
15
|
+
FG_WHITE: string;
|
|
16
|
+
ERASE_LINE: string;
|
|
17
|
+
CURSOR_LEFT: string;
|
|
18
|
+
HIDE_CURSOR: string;
|
|
19
|
+
SHOW_CURSOR: string;
|
|
20
|
+
UP: string;
|
|
21
|
+
DOWN: string;
|
|
22
|
+
};
|
package/dist/ansi.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ANSI Escape Codes
|
|
4
|
+
* Manual definitions to maintain zero-dependency status.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ANSI = void 0;
|
|
8
|
+
exports.ANSI = {
|
|
9
|
+
RESET: '\x1b[0m',
|
|
10
|
+
BOLD: '\x1b[1m',
|
|
11
|
+
DIM: '\x1b[2m',
|
|
12
|
+
ITALIC: '\x1b[3m',
|
|
13
|
+
// Colors
|
|
14
|
+
FG_GREEN: '\x1b[32m',
|
|
15
|
+
FG_CYAN: '\x1b[36m',
|
|
16
|
+
FG_YELLOW: '\x1b[33m',
|
|
17
|
+
FG_RED: '\x1b[31m',
|
|
18
|
+
FG_GRAY: '\x1b[90m',
|
|
19
|
+
FG_WHITE: '\x1b[37m',
|
|
20
|
+
// Cursor & Erasing
|
|
21
|
+
ERASE_LINE: '\x1b[2K', // Clear current line
|
|
22
|
+
CURSOR_LEFT: '\x1b[1000D', // Move cursor to start of line
|
|
23
|
+
HIDE_CURSOR: '\x1b[?25l',
|
|
24
|
+
SHOW_CURSOR: '\x1b[?25h',
|
|
25
|
+
UP: '\x1b[A',
|
|
26
|
+
DOWN: '\x1b[B',
|
|
27
|
+
};
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TextOptions, SelectOptions, ConfirmOptions, CheckboxOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Public Facade for MepCLI
|
|
4
|
+
*/
|
|
5
|
+
export declare class MepCLI {
|
|
6
|
+
static text(options: TextOptions): Promise<string>;
|
|
7
|
+
static select(options: SelectOptions): Promise<any>;
|
|
8
|
+
static checkbox(options: CheckboxOptions): Promise<any[]>;
|
|
9
|
+
static confirm(options: ConfirmOptions): Promise<boolean>;
|
|
10
|
+
static password(options: TextOptions): Promise<string>;
|
|
11
|
+
}
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MepCLI = void 0;
|
|
4
|
+
const ansi_1 = require("./ansi");
|
|
5
|
+
/**
|
|
6
|
+
* Abstract base class for all prompts.
|
|
7
|
+
* Handles common logic like stdin management, raw mode, and cleanup
|
|
8
|
+
* to enforce DRY (Don't Repeat Yourself) principles.
|
|
9
|
+
*/
|
|
10
|
+
class Prompt {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.stdin = process.stdin;
|
|
14
|
+
this.stdout = process.stdout;
|
|
15
|
+
}
|
|
16
|
+
print(text) {
|
|
17
|
+
this.stdout.write(text);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Starts the prompt interaction.
|
|
21
|
+
* Sets up raw mode and listeners, returning a Promise.
|
|
22
|
+
*/
|
|
23
|
+
run() {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
this._resolve = resolve;
|
|
26
|
+
this._reject = reject;
|
|
27
|
+
if (typeof this.stdin.setRawMode === 'function') {
|
|
28
|
+
this.stdin.setRawMode(true);
|
|
29
|
+
}
|
|
30
|
+
this.stdin.resume();
|
|
31
|
+
this.stdin.setEncoding('utf8');
|
|
32
|
+
// Initial render: Default to hidden cursor (good for menus)
|
|
33
|
+
// Subclasses like TextPrompt will explicitly show it if needed.
|
|
34
|
+
this.print(ansi_1.ANSI.HIDE_CURSOR);
|
|
35
|
+
this.render(true);
|
|
36
|
+
this._onDataHandler = (buffer) => {
|
|
37
|
+
const char = buffer.toString();
|
|
38
|
+
// Global Exit Handler (Ctrl+C)
|
|
39
|
+
if (char === '\u0003') {
|
|
40
|
+
this.cleanup();
|
|
41
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR + '\n');
|
|
42
|
+
if (this._reject)
|
|
43
|
+
this._reject(new Error('User force closed'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.handleInput(char, buffer);
|
|
47
|
+
};
|
|
48
|
+
this.stdin.on('data', this._onDataHandler);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cleans up listeners and restores stdin state.
|
|
53
|
+
*/
|
|
54
|
+
cleanup() {
|
|
55
|
+
if (this._onDataHandler) {
|
|
56
|
+
this.stdin.removeListener('data', this._onDataHandler);
|
|
57
|
+
}
|
|
58
|
+
if (typeof this.stdin.setRawMode === 'function') {
|
|
59
|
+
this.stdin.setRawMode(false);
|
|
60
|
+
}
|
|
61
|
+
this.stdin.pause();
|
|
62
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Submits the final value and resolves the promise.
|
|
66
|
+
*/
|
|
67
|
+
submit(result) {
|
|
68
|
+
this.cleanup();
|
|
69
|
+
this.print('\n');
|
|
70
|
+
if (this._resolve)
|
|
71
|
+
this._resolve(result);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// --- Implementation: Text Prompt ---
|
|
75
|
+
class TextPrompt extends Prompt {
|
|
76
|
+
constructor(options) {
|
|
77
|
+
super(options);
|
|
78
|
+
this.errorMsg = '';
|
|
79
|
+
this.cursor = 0;
|
|
80
|
+
this.value = options.initial || '';
|
|
81
|
+
this.cursor = this.value.length;
|
|
82
|
+
}
|
|
83
|
+
render(firstRender) {
|
|
84
|
+
// TextPrompt needs the cursor visible!
|
|
85
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
86
|
+
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);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 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} `);
|
|
96
|
+
// 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`);
|
|
101
|
+
}
|
|
102
|
+
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}`);
|
|
105
|
+
}
|
|
106
|
+
// 3. Handle Error Message
|
|
107
|
+
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
|
+
}
|
|
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`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
handleInput(char) {
|
|
125
|
+
// Enter
|
|
126
|
+
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}`);
|
|
137
|
+
}
|
|
138
|
+
this.submit(this.value);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Backspace
|
|
142
|
+
if (char === '\u0008' || char === '\x7f') {
|
|
143
|
+
if (this.cursor > 0) {
|
|
144
|
+
this.value = this.value.slice(0, this.cursor - 1) + this.value.slice(this.cursor);
|
|
145
|
+
this.cursor--;
|
|
146
|
+
this.errorMsg = '';
|
|
147
|
+
this.render(false);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Arrow Left
|
|
152
|
+
if (char === '\u001b[D') {
|
|
153
|
+
if (this.cursor > 0) {
|
|
154
|
+
this.cursor--;
|
|
155
|
+
this.render(false);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Arrow Right
|
|
160
|
+
if (char === '\u001b[C') {
|
|
161
|
+
if (this.cursor < this.value.length) {
|
|
162
|
+
this.cursor++;
|
|
163
|
+
this.render(false);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Delete key
|
|
168
|
+
if (char === '\u001b[3~') {
|
|
169
|
+
if (this.cursor < this.value.length) {
|
|
170
|
+
this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + 1);
|
|
171
|
+
this.errorMsg = '';
|
|
172
|
+
this.render(false);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Regular Typing
|
|
177
|
+
if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
|
|
178
|
+
this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
|
|
179
|
+
this.cursor++;
|
|
180
|
+
this.errorMsg = '';
|
|
181
|
+
this.render(false);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// --- Implementation: Select Prompt ---
|
|
186
|
+
class SelectPrompt extends Prompt {
|
|
187
|
+
constructor(options) {
|
|
188
|
+
super(options);
|
|
189
|
+
this.selectedIndex = 0;
|
|
190
|
+
}
|
|
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`);
|
|
196
|
+
}
|
|
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
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
handleInput(char) {
|
|
210
|
+
if (char === '\r' || char === '\n') {
|
|
211
|
+
this.cleanup();
|
|
212
|
+
this.print(`\x1b[${this.options.choices.length - this.selectedIndex}B`);
|
|
213
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
214
|
+
if (this._resolve)
|
|
215
|
+
this._resolve(this.options.choices[this.selectedIndex].value);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (char === '\u001b[A') { // Up
|
|
219
|
+
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.choices.length - 1;
|
|
220
|
+
this.render(false);
|
|
221
|
+
}
|
|
222
|
+
if (char === '\u001b[B') { // Down
|
|
223
|
+
this.selectedIndex = this.selectedIndex < this.options.choices.length - 1 ? this.selectedIndex + 1 : 0;
|
|
224
|
+
this.render(false);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// --- Implementation: Checkbox Prompt ---
|
|
229
|
+
class CheckboxPrompt extends Prompt {
|
|
230
|
+
constructor(options) {
|
|
231
|
+
super(options);
|
|
232
|
+
this.selectedIndex = 0;
|
|
233
|
+
this.errorMsg = '';
|
|
234
|
+
this.checkedState = options.choices.map(c => !!c.selected);
|
|
235
|
+
}
|
|
236
|
+
render(firstRender) {
|
|
237
|
+
// Ensure cursor is HIDDEN for menus
|
|
238
|
+
this.print(ansi_1.ANSI.HIDE_CURSOR);
|
|
239
|
+
if (!firstRender) {
|
|
240
|
+
const extraLines = this.errorMsg ? 1 : 0;
|
|
241
|
+
this.print(`\x1b[${this.options.choices.length + 1 + extraLines}A`);
|
|
242
|
+
}
|
|
243
|
+
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`);
|
|
246
|
+
this.options.choices.forEach((choice, index) => {
|
|
247
|
+
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}` : ' ';
|
|
249
|
+
const isChecked = this.checkedState[index];
|
|
250
|
+
const checkbox = isChecked
|
|
251
|
+
? `${ansi_1.ANSI.FG_GREEN}◉${ansi_1.ANSI.RESET}`
|
|
252
|
+
: `${ansi_1.ANSI.FG_GRAY}◯${ansi_1.ANSI.RESET}`;
|
|
253
|
+
const title = index === this.selectedIndex
|
|
254
|
+
? `${ansi_1.ANSI.FG_CYAN}${choice.title}${ansi_1.ANSI.RESET}`
|
|
255
|
+
: choice.title;
|
|
256
|
+
this.print(`${cursor} ${checkbox} ${title}\n`);
|
|
257
|
+
});
|
|
258
|
+
if (this.errorMsg) {
|
|
259
|
+
this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.FG_RED}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
|
|
260
|
+
}
|
|
261
|
+
else if (!firstRender) {
|
|
262
|
+
this.print(`${ansi_1.ANSI.ERASE_LINE}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
handleInput(char) {
|
|
266
|
+
if (char === '\r' || char === '\n') {
|
|
267
|
+
const selectedCount = this.checkedState.filter(Boolean).length;
|
|
268
|
+
const { min = 0, max } = this.options;
|
|
269
|
+
if (selectedCount < min) {
|
|
270
|
+
this.errorMsg = `You must select at least ${min} options.`;
|
|
271
|
+
this.render(false);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (max && selectedCount > max) {
|
|
275
|
+
this.errorMsg = `You can only select up to ${max} options.`;
|
|
276
|
+
this.render(false);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.cleanup();
|
|
280
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR + '\n');
|
|
281
|
+
const results = this.options.choices
|
|
282
|
+
.filter((_, i) => this.checkedState[i])
|
|
283
|
+
.map(c => c.value);
|
|
284
|
+
if (this._resolve)
|
|
285
|
+
this._resolve(results);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (char === ' ') {
|
|
289
|
+
const currentChecked = this.checkedState[this.selectedIndex];
|
|
290
|
+
const selectedCount = this.checkedState.filter(Boolean).length;
|
|
291
|
+
const { max } = this.options;
|
|
292
|
+
if (!currentChecked && max && selectedCount >= max) {
|
|
293
|
+
this.errorMsg = `Max ${max} selections allowed.`;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.checkedState[this.selectedIndex] = !currentChecked;
|
|
297
|
+
this.errorMsg = '';
|
|
298
|
+
}
|
|
299
|
+
this.render(false);
|
|
300
|
+
}
|
|
301
|
+
if (char === '\u001b[A') { // Up
|
|
302
|
+
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.choices.length - 1;
|
|
303
|
+
this.errorMsg = '';
|
|
304
|
+
this.render(false);
|
|
305
|
+
}
|
|
306
|
+
if (char === '\u001b[B') { // Down
|
|
307
|
+
this.selectedIndex = this.selectedIndex < this.options.choices.length - 1 ? this.selectedIndex + 1 : 0;
|
|
308
|
+
this.errorMsg = '';
|
|
309
|
+
this.render(false);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// --- Implementation: Confirm Prompt ---
|
|
314
|
+
class ConfirmPrompt extends Prompt {
|
|
315
|
+
constructor(options) {
|
|
316
|
+
super(options);
|
|
317
|
+
this.value = options.initial ?? true;
|
|
318
|
+
}
|
|
319
|
+
render(firstRender) {
|
|
320
|
+
// Hide cursor for confirm, user just hits Y/N or Enter
|
|
321
|
+
this.print(ansi_1.ANSI.HIDE_CURSOR);
|
|
322
|
+
if (!firstRender) {
|
|
323
|
+
this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
|
|
324
|
+
}
|
|
325
|
+
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} `);
|
|
327
|
+
const text = this.value ? 'Yes' : 'No';
|
|
328
|
+
this.print(`${ansi_1.ANSI.FG_CYAN}${text}${ansi_1.ANSI.RESET}\x1b[${text.length}D`);
|
|
329
|
+
}
|
|
330
|
+
handleInput(char) {
|
|
331
|
+
const c = char.toLowerCase();
|
|
332
|
+
if (c === '\r' || c === '\n') {
|
|
333
|
+
this.submit(this.value);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (c === 'y') {
|
|
337
|
+
this.value = true;
|
|
338
|
+
this.render(false);
|
|
339
|
+
}
|
|
340
|
+
if (c === 'n') {
|
|
341
|
+
this.value = false;
|
|
342
|
+
this.render(false);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Public Facade for MepCLI
|
|
348
|
+
*/
|
|
349
|
+
class MepCLI {
|
|
350
|
+
static text(options) {
|
|
351
|
+
return new TextPrompt(options).run();
|
|
352
|
+
}
|
|
353
|
+
static select(options) {
|
|
354
|
+
return new SelectPrompt(options).run();
|
|
355
|
+
}
|
|
356
|
+
static checkbox(options) {
|
|
357
|
+
return new CheckboxPrompt(options).run();
|
|
358
|
+
}
|
|
359
|
+
static confirm(options) {
|
|
360
|
+
return new ConfirmPrompt(options).run();
|
|
361
|
+
}
|
|
362
|
+
static password(options) {
|
|
363
|
+
return new TextPrompt({ ...options, isPassword: true }).run();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
exports.MepCLI = MepCLI;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Mep (Minimalist Interactive CLI Prompt)
|
|
4
|
+
* Export file
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
__exportStar(require("./types"), exports);
|
|
22
|
+
__exportStar(require("./core"), exports);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Mep CLI interactions.
|
|
3
|
+
*/
|
|
4
|
+
export interface BaseOptions {
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TextOptions extends BaseOptions {
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
initial?: string;
|
|
10
|
+
validate?: (value: string) => string | boolean;
|
|
11
|
+
isPassword?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SelectChoice {
|
|
14
|
+
title: string;
|
|
15
|
+
value: any;
|
|
16
|
+
description?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SelectOptions extends BaseOptions {
|
|
19
|
+
choices: SelectChoice[];
|
|
20
|
+
}
|
|
21
|
+
export interface CheckboxChoice extends SelectChoice {
|
|
22
|
+
selected?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface CheckboxOptions extends BaseOptions {
|
|
25
|
+
choices: CheckboxChoice[];
|
|
26
|
+
min?: number;
|
|
27
|
+
max?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ConfirmOptions extends BaseOptions {
|
|
30
|
+
initial?: boolean;
|
|
31
|
+
}
|
package/dist/types.js
ADDED
package/example.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { MepCLI } from './src'; // Or mepcli if installed via NPM
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs a comprehensive set of tests for all MepCLI prompt types.
|
|
5
|
+
*/
|
|
6
|
+
async function runAllTests() {
|
|
7
|
+
console.clear();
|
|
8
|
+
console.log("--- MepCLI Comprehensive Test Suite (Neutralized) ---\n");
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// --- 1. Text Prompt Test (with Validation) ---
|
|
12
|
+
const projectName = await MepCLI.text({
|
|
13
|
+
message: "Enter the name for your new project:",
|
|
14
|
+
placeholder: "e.g., minimalist-cli-app",
|
|
15
|
+
initial: "MepProject",
|
|
16
|
+
validate: (value) => {
|
|
17
|
+
if (value.length < 3) {
|
|
18
|
+
return "Project name must be at least 3 characters long.";
|
|
19
|
+
}
|
|
20
|
+
if (value.includes('&')) {
|
|
21
|
+
return "Project name cannot contain '&' symbol.";
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
console.log(`\n✅ Text Result: Project name set to '${projectName}'`);
|
|
27
|
+
|
|
28
|
+
// --- 2. Password Prompt Test ---
|
|
29
|
+
const apiKey = await MepCLI.password({
|
|
30
|
+
message: "Enter the project's external API key:",
|
|
31
|
+
placeholder: "Input will be hidden..."
|
|
32
|
+
});
|
|
33
|
+
// Note: Do not log the actual key in a real app.
|
|
34
|
+
console.log(`\n✅ Password Result: API key entered (length: ${apiKey.length})`);
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// --- 3. Select Prompt Test (Single Choice) ---
|
|
38
|
+
const theme = await MepCLI.select({
|
|
39
|
+
message: "Choose your preferred editor color theme:",
|
|
40
|
+
choices: [
|
|
41
|
+
{ title: "Dark Mode (Default)", value: "dark" },
|
|
42
|
+
{ title: "Light Mode (Classic)", value: "light" },
|
|
43
|
+
{ title: "High Contrast (Accessibility)", value: "contrast" },
|
|
44
|
+
{ title: "Monokai Pro", value: "monokai" },
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
console.log(`\n✅ Select Result: Chosen theme is: ${theme}`);
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
// --- 4. Checkbox Prompt Test (Multi-Choice with Min/Max) ---
|
|
51
|
+
const buildTools = await MepCLI.checkbox({
|
|
52
|
+
message: "Select your required bundlers/build tools (Min 1, Max 2):",
|
|
53
|
+
min: 1,
|
|
54
|
+
max: 2,
|
|
55
|
+
choices: [
|
|
56
|
+
{ title: "Webpack", value: "webpack" },
|
|
57
|
+
{ title: "Vite", value: "vite", selected: true },
|
|
58
|
+
{ title: "Rollup", value: "rollup" },
|
|
59
|
+
{ title: "esbuild", value: "esbuild" }
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
console.log(`\n✅ Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// --- 5. Confirm Prompt Test ---
|
|
66
|
+
const proceed = await MepCLI.confirm({
|
|
67
|
+
message: "Do you want to continue with the installation setup?",
|
|
68
|
+
initial: true
|
|
69
|
+
});
|
|
70
|
+
console.log(`\n✅ Confirm Result: Setup decision: ${proceed ? 'Proceed' : 'Cancel'}`);
|
|
71
|
+
|
|
72
|
+
console.log("\n--- All MepCLI tests completed successfully! ---");
|
|
73
|
+
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e instanceof Error && e.message === 'User force closed') {
|
|
76
|
+
console.log("\nOperation cancelled by user (Ctrl+C).");
|
|
77
|
+
} else {
|
|
78
|
+
console.error("\nAn error occurred during prompt execution:", e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
runAllTests();
|
package/package.json
ADDED
|
@@ -0,0 +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
|
+
}
|