mepcli 0.2.1 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -17
- package/dist/ansi.d.ts +2 -0
- package/dist/ansi.js +2 -0
- package/dist/base.d.ts +57 -0
- package/dist/base.js +341 -0
- package/dist/core.d.ts +6 -1
- package/dist/core.js +35 -775
- package/dist/input.d.ts +13 -0
- package/dist/input.js +89 -0
- package/dist/prompts/checkbox.d.ts +12 -0
- package/dist/prompts/checkbox.js +114 -0
- package/dist/prompts/confirm.d.ts +7 -0
- package/dist/prompts/confirm.js +42 -0
- package/dist/prompts/date.d.ts +10 -0
- package/dist/prompts/date.js +136 -0
- package/dist/prompts/file.d.ts +13 -0
- package/dist/prompts/file.js +217 -0
- package/dist/prompts/list.d.ts +9 -0
- package/dist/prompts/list.js +88 -0
- package/dist/prompts/multi-select.d.ts +14 -0
- package/dist/prompts/multi-select.js +123 -0
- package/dist/prompts/number.d.ts +10 -0
- package/dist/prompts/number.js +133 -0
- package/dist/prompts/select.d.ts +14 -0
- package/dist/prompts/select.js +148 -0
- package/dist/prompts/slider.d.ts +7 -0
- package/dist/prompts/slider.js +49 -0
- package/dist/prompts/text.d.ts +13 -0
- package/dist/prompts/text.js +321 -0
- package/dist/prompts/toggle.d.ts +7 -0
- package/dist/prompts/toggle.js +41 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +11 -0
- package/dist/types.d.ts +24 -0
- package/dist/utils.d.ts +23 -0
- package/dist/utils.js +135 -0
- package/example.ts +57 -5
- package/package.json +4 -4
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TextPrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
// --- Implementation: Text Prompt ---
|
|
9
|
+
class TextPrompt extends base_1.Prompt {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options);
|
|
12
|
+
this.errorMsg = '';
|
|
13
|
+
// cursor is now an index into the grapheme segments array
|
|
14
|
+
this.cursor = 0;
|
|
15
|
+
this.hasTyped = false;
|
|
16
|
+
this.segments = [];
|
|
17
|
+
this.value = options.initial || '';
|
|
18
|
+
// Initialize segments from value
|
|
19
|
+
this.segments = (0, utils_1.safeSplit)(this.value);
|
|
20
|
+
this.cursor = this.segments.length;
|
|
21
|
+
}
|
|
22
|
+
render(firstRender) {
|
|
23
|
+
// Calculate available width
|
|
24
|
+
const cols = process.stdout.columns || 80;
|
|
25
|
+
// 1. Prepare Prompt Label
|
|
26
|
+
const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
|
|
27
|
+
const hint = this.options.multiline ? ` ${theme_1.theme.muted}(Press Ctrl+D to submit)${ansi_1.ANSI.RESET}` : '';
|
|
28
|
+
const prefix = `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${hint} `;
|
|
29
|
+
// We need visual length of prefix to calculate available space for input on the first line
|
|
30
|
+
const prefixVisualLen = this.stripAnsi(prefix).length;
|
|
31
|
+
// 2. Prepare Value Display
|
|
32
|
+
let displayValueLines = [];
|
|
33
|
+
let cursorRelativeRow = 0;
|
|
34
|
+
let cursorRelativeCol = 0; // Visual column index
|
|
35
|
+
// Reconstruct value from segments for logic that needs raw string
|
|
36
|
+
this.value = this.segments.join('');
|
|
37
|
+
if (this.segments.length === 0 && this.options.placeholder && !this.errorMsg && !this.hasTyped) {
|
|
38
|
+
// Placeholder case
|
|
39
|
+
const placeholder = `${theme_1.theme.muted}${this.options.placeholder}${ansi_1.ANSI.RESET}`;
|
|
40
|
+
displayValueLines = [placeholder];
|
|
41
|
+
cursorRelativeRow = 0;
|
|
42
|
+
cursorRelativeCol = 0;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const rawValue = this.options.isPassword ? '*'.repeat(this.segments.length) : this.value;
|
|
46
|
+
// Note: password masking replaces each grapheme with '*'
|
|
47
|
+
// Split by lines (for multiline support)
|
|
48
|
+
const lines = rawValue.split('\n');
|
|
49
|
+
// Determine which line the cursor is on
|
|
50
|
+
// We need to map 'cursor' (segments index) to line/col.
|
|
51
|
+
// This is tricky because segments might contain '\n'.
|
|
52
|
+
// safeSplit treats '\n' as a segment.
|
|
53
|
+
let cursorLineIndex = 0;
|
|
54
|
+
let cursorSegmentIndexOnLine = 0;
|
|
55
|
+
let currentSegmentIndex = 0;
|
|
56
|
+
for (let i = 0; i < lines.length; i++) {
|
|
57
|
+
// How many segments in this line?
|
|
58
|
+
// We can't just use lines[i].length because that's chars.
|
|
59
|
+
// We need to split the line again or iterate segments.
|
|
60
|
+
// Iterating segments is safer.
|
|
61
|
+
// Let's assume we iterate global segments until we hit a newline segment
|
|
62
|
+
let lineSegmentsCount = 0;
|
|
63
|
+
// Since rawValue.split('\n') consumes the newlines, we need to account for them.
|
|
64
|
+
// Alternative: iterate this.segments
|
|
65
|
+
// Find where the cursor falls.
|
|
66
|
+
}
|
|
67
|
+
// Let's iterate segments to find cursor position (row, col)
|
|
68
|
+
cursorLineIndex = 0;
|
|
69
|
+
let colIndex = 0; // Visual column or char index?
|
|
70
|
+
// If we want visual cursor position, we need visual width of segments.
|
|
71
|
+
let visualColIndex = 0;
|
|
72
|
+
for (let i = 0; i < this.cursor; i++) {
|
|
73
|
+
const seg = this.segments[i];
|
|
74
|
+
if (seg === '\n') {
|
|
75
|
+
cursorLineIndex++;
|
|
76
|
+
visualColIndex = 0;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Calculate width of this segment?
|
|
80
|
+
// No, for simple text editor logic we often assume 1 char = 1 pos unless we do full layout.
|
|
81
|
+
// But here we want correct cursor placement over wide chars.
|
|
82
|
+
// So we should sum width.
|
|
83
|
+
// However, standard terminals handle wide chars by advancing cursor 2 spots.
|
|
84
|
+
// So we just need to sum the string length of the segment?
|
|
85
|
+
// Or 2 if it's wide?
|
|
86
|
+
// Standard terminal behavior:
|
|
87
|
+
// If I write an Emoji (2 cols), the cursor advances 2 cols.
|
|
88
|
+
// So visualColIndex should track stringWidth(seg).
|
|
89
|
+
// But if isPassword, it's '*'. Width 1.
|
|
90
|
+
if (this.options.isPassword) {
|
|
91
|
+
visualColIndex += 1;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Use our helper? Or just length?
|
|
95
|
+
// If we used stringWidth, it would be accurate.
|
|
96
|
+
// But we don't have access to stringWidth here easily unless we import it again (we did in base).
|
|
97
|
+
// Let's assume segment.length for now (byte length),
|
|
98
|
+
// because `\x1b[<N>C` moves N COLUMNS? No, N characters?
|
|
99
|
+
// ANSI `CUB` / `CUF` moves N *columns* usually?
|
|
100
|
+
// "The Cursor Forward (CUF) sequence moves the cursor forward by n columns."
|
|
101
|
+
// So if we have an emoji (2 cols), we need to move past it.
|
|
102
|
+
// If we print an emoji, cursor is at +2.
|
|
103
|
+
// Wait, if we use `renderFrame`, we rewrite everything.
|
|
104
|
+
// Then we calculate where to put the cursor.
|
|
105
|
+
// If line is "A <Emoji> B".
|
|
106
|
+
// Output: "A <Emoji> B".
|
|
107
|
+
// If cursor is after Emoji.
|
|
108
|
+
// We need to be at position: width("A") + width("<Emoji>").
|
|
109
|
+
// = 1 + 2 = 3.
|
|
110
|
+
// So `visualColIndex` should use `stringWidth(seg)`.
|
|
111
|
+
// But I didn't export `stringWidth` from `utils.ts` in the last step?
|
|
112
|
+
// Checking `src/utils.ts`... I did export it.
|
|
113
|
+
// But I need to import it here.
|
|
114
|
+
visualColIndex += this.options.isPassword ? 1 : this.getSegmentWidth(seg);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
cursorRelativeRow = cursorLineIndex;
|
|
119
|
+
cursorRelativeCol = visualColIndex;
|
|
120
|
+
// Now prepare lines for display (scrolling/truncation)
|
|
121
|
+
// We need to reconstruct lines from segments to apply styling/truncation logic per line.
|
|
122
|
+
let currentLineSegments = [];
|
|
123
|
+
let processedLines = []; // Array of segment arrays
|
|
124
|
+
for (const seg of this.segments) {
|
|
125
|
+
if (seg === '\n') {
|
|
126
|
+
processedLines.push(currentLineSegments);
|
|
127
|
+
currentLineSegments = [];
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
currentLineSegments.push(seg);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
processedLines.push(currentLineSegments); // Last line
|
|
134
|
+
processedLines.forEach((lineSegs, idx) => {
|
|
135
|
+
const isCursorLine = idx === cursorLineIndex;
|
|
136
|
+
const linePrefixLen = (idx === 0) ? prefixVisualLen : 0;
|
|
137
|
+
const maxContentLen = Math.max(10, cols - linePrefixLen - 1);
|
|
138
|
+
// Reconstruct line string for display calculation
|
|
139
|
+
// If password, join with *?
|
|
140
|
+
let visibleLine = '';
|
|
141
|
+
if (this.options.isPassword) {
|
|
142
|
+
visibleLine = '*'.repeat(lineSegs.length);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
visibleLine = lineSegs.join('');
|
|
146
|
+
}
|
|
147
|
+
// If this is cursor line, we need to handle horizontal scroll based on cursorRelativeCol.
|
|
148
|
+
// But cursorRelativeCol is global? No, we reset it on newline.
|
|
149
|
+
// So cursorRelativeCol above was correct for the current line.
|
|
150
|
+
if (isCursorLine) {
|
|
151
|
+
// Check if we need to scroll
|
|
152
|
+
// We need visual width of the line up to cursor.
|
|
153
|
+
// cursorRelativeCol holds that.
|
|
154
|
+
// If visual position > maxContentLen, we scroll.
|
|
155
|
+
// This logic is similar to before but needs to use widths.
|
|
156
|
+
// For simplicity, let's stick to the previous slice logic but apply it to SEGMENTS if possible.
|
|
157
|
+
// But slicing segments for display is safer.
|
|
158
|
+
// Let's implement simple tail truncation for now to keep it robust.
|
|
159
|
+
// Ideally we scroll, but scrolling with variable width chars is complex.
|
|
160
|
+
// "Good Enough": if it fits, show it. If not, truncate end.
|
|
161
|
+
// If cursor is beyond end, scroll (slice from left).
|
|
162
|
+
// Simplified: just show visibleLine truncated by base class renderFrame?
|
|
163
|
+
// But renderFrame truncates blindly. We want the cursor visible.
|
|
164
|
+
// Let's leave scrolling out for this specific "Backspace" fix task unless it's critical.
|
|
165
|
+
// The user asked for "Backspace Emoji fix".
|
|
166
|
+
// The scrolling logic is secondary but important.
|
|
167
|
+
// I will preserve the existing simple scrolling logic but using segments?
|
|
168
|
+
// No, let's just use the string for display and let renderFrame truncate.
|
|
169
|
+
// Fix: Ensure we don't crash or show garbage.
|
|
170
|
+
}
|
|
171
|
+
displayValueLines.push(theme_1.theme.main + visibleLine + ansi_1.ANSI.RESET);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// 3. Assemble Output
|
|
175
|
+
let output = '';
|
|
176
|
+
displayValueLines.forEach((lineStr, idx) => {
|
|
177
|
+
if (idx === 0) {
|
|
178
|
+
output += prefix + lineStr;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
output += '\n' + lineStr;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
if (this.errorMsg) {
|
|
185
|
+
output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
|
|
186
|
+
}
|
|
187
|
+
// 4. Render Frame
|
|
188
|
+
this.renderFrame(output);
|
|
189
|
+
this.print(ansi_1.ANSI.SHOW_CURSOR);
|
|
190
|
+
// 5. Move Cursor
|
|
191
|
+
const errorOffset = this.errorMsg ? 1 : 0;
|
|
192
|
+
const totalRows = displayValueLines.length + errorOffset;
|
|
193
|
+
const linesUp = (totalRows - 1) - cursorRelativeRow;
|
|
194
|
+
if (linesUp > 0) {
|
|
195
|
+
this.print(`\x1b[${linesUp}A`);
|
|
196
|
+
}
|
|
197
|
+
let targetCol = 0;
|
|
198
|
+
if (cursorRelativeRow === 0) {
|
|
199
|
+
targetCol = prefixVisualLen + cursorRelativeCol;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
targetCol = cursorRelativeCol;
|
|
203
|
+
}
|
|
204
|
+
this.print(ansi_1.ANSI.CURSOR_LEFT);
|
|
205
|
+
if (targetCol > 0) {
|
|
206
|
+
this.print(`\x1b[${targetCol}C`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Helper to get width of a segment
|
|
210
|
+
getSegmentWidth(seg) {
|
|
211
|
+
return (0, utils_1.stringWidth)(seg);
|
|
212
|
+
}
|
|
213
|
+
handleInput(char) {
|
|
214
|
+
// Enter
|
|
215
|
+
if (char === '\r' || char === '\n') {
|
|
216
|
+
if (this.options.multiline) {
|
|
217
|
+
// Insert newline segment
|
|
218
|
+
this.segments.splice(this.cursor, 0, '\n');
|
|
219
|
+
this.cursor++;
|
|
220
|
+
this.render(false);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
this.validateAndSubmit();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// Ctrl+D / Ctrl+S
|
|
227
|
+
if (this.options.multiline && (char === '\u0004' || char === '\u0013')) {
|
|
228
|
+
this.validateAndSubmit();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Backspace
|
|
232
|
+
if (char === '\u0008' || char === '\x7f') {
|
|
233
|
+
this.hasTyped = true;
|
|
234
|
+
if (this.cursor > 0) {
|
|
235
|
+
// Remove segment at cursor - 1
|
|
236
|
+
this.segments.splice(this.cursor - 1, 1);
|
|
237
|
+
this.cursor--;
|
|
238
|
+
this.errorMsg = '';
|
|
239
|
+
this.render(false);
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Arrow Left
|
|
244
|
+
if (this.isLeft(char)) {
|
|
245
|
+
if (this.cursor > 0) {
|
|
246
|
+
this.cursor--;
|
|
247
|
+
this.render(false);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Arrow Right
|
|
252
|
+
if (this.isRight(char)) {
|
|
253
|
+
if (this.cursor < this.segments.length) {
|
|
254
|
+
this.cursor++;
|
|
255
|
+
this.render(false);
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Delete key
|
|
260
|
+
if (char === '\u001b[3~') {
|
|
261
|
+
this.hasTyped = true;
|
|
262
|
+
if (this.cursor < this.segments.length) {
|
|
263
|
+
this.segments.splice(this.cursor, 1);
|
|
264
|
+
this.errorMsg = '';
|
|
265
|
+
this.render(false);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Regular Typing & Paste
|
|
270
|
+
// safeSplit the input char(s) - could be pasted text
|
|
271
|
+
if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
|
|
272
|
+
this.hasTyped = true;
|
|
273
|
+
const newSegments = (0, utils_1.safeSplit)(char);
|
|
274
|
+
this.segments.splice(this.cursor, 0, ...newSegments);
|
|
275
|
+
this.cursor += newSegments.length;
|
|
276
|
+
this.errorMsg = '';
|
|
277
|
+
this.render(false);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
validateAndSubmit() {
|
|
281
|
+
this.value = this.segments.join('');
|
|
282
|
+
if (this.options.validate) {
|
|
283
|
+
const result = this.options.validate(this.value);
|
|
284
|
+
if (result instanceof Promise) {
|
|
285
|
+
this.errorMsg = 'Validating...';
|
|
286
|
+
this.render(false);
|
|
287
|
+
result.then(valid => {
|
|
288
|
+
if (typeof valid === 'string' && valid.length > 0) {
|
|
289
|
+
this.errorMsg = valid;
|
|
290
|
+
this.render(false);
|
|
291
|
+
}
|
|
292
|
+
else if (valid === false) {
|
|
293
|
+
this.errorMsg = 'Invalid input';
|
|
294
|
+
this.render(false);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
this.errorMsg = '';
|
|
298
|
+
this.render(false);
|
|
299
|
+
this.submit(this.value);
|
|
300
|
+
}
|
|
301
|
+
}).catch(err => {
|
|
302
|
+
this.errorMsg = err.message || 'Validation failed';
|
|
303
|
+
this.render(false);
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (typeof result === 'string' && result.length > 0) {
|
|
308
|
+
this.errorMsg = result;
|
|
309
|
+
this.render(false);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (result === false) {
|
|
313
|
+
this.errorMsg = 'Invalid input';
|
|
314
|
+
this.render(false);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
this.submit(this.value);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.TextPrompt = TextPrompt;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Prompt } from '../base';
|
|
2
|
+
import { ToggleOptions } from '../types';
|
|
3
|
+
export declare class TogglePrompt extends Prompt<boolean, ToggleOptions> {
|
|
4
|
+
constructor(options: ToggleOptions);
|
|
5
|
+
protected render(firstRender: boolean): void;
|
|
6
|
+
protected handleInput(char: string): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TogglePrompt = void 0;
|
|
4
|
+
const ansi_1 = require("../ansi");
|
|
5
|
+
const base_1 = require("../base");
|
|
6
|
+
const theme_1 = require("../theme");
|
|
7
|
+
// --- Implementation: Toggle Prompt ---
|
|
8
|
+
class TogglePrompt extends base_1.Prompt {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this.value = options.initial ?? false;
|
|
12
|
+
}
|
|
13
|
+
render(firstRender) {
|
|
14
|
+
const activeText = this.options.activeText || 'ON';
|
|
15
|
+
const inactiveText = this.options.inactiveText || 'OFF';
|
|
16
|
+
let toggleDisplay = '';
|
|
17
|
+
if (this.value) {
|
|
18
|
+
toggleDisplay = `${theme_1.theme.main}[${ansi_1.ANSI.BOLD}${activeText}${ansi_1.ANSI.RESET}${theme_1.theme.main}]${ansi_1.ANSI.RESET} ${theme_1.theme.muted}${inactiveText}${ansi_1.ANSI.RESET}`;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
toggleDisplay = `${theme_1.theme.muted}${activeText}${ansi_1.ANSI.RESET} ${theme_1.theme.main}[${ansi_1.ANSI.BOLD}${inactiveText}${ansi_1.ANSI.RESET}${theme_1.theme.main}]${ansi_1.ANSI.RESET}`;
|
|
22
|
+
}
|
|
23
|
+
const output = `${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${toggleDisplay}`;
|
|
24
|
+
this.renderFrame(output);
|
|
25
|
+
}
|
|
26
|
+
handleInput(char) {
|
|
27
|
+
if (char === '\r' || char === '\n') {
|
|
28
|
+
this.submit(this.value);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (this.isLeft(char) || this.isRight(char) || char === 'h' || char === 'l') { // Left/Right
|
|
32
|
+
this.value = !this.value;
|
|
33
|
+
this.render(false);
|
|
34
|
+
}
|
|
35
|
+
if (char === ' ') {
|
|
36
|
+
this.value = !this.value;
|
|
37
|
+
this.render(false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.TogglePrompt = TogglePrompt;
|
package/dist/theme.d.ts
ADDED
package/dist/theme.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.theme = void 0;
|
|
4
|
+
const ansi_1 = require("./ansi");
|
|
5
|
+
exports.theme = {
|
|
6
|
+
main: ansi_1.ANSI.FG_CYAN,
|
|
7
|
+
success: ansi_1.ANSI.FG_GREEN,
|
|
8
|
+
error: ansi_1.ANSI.FG_RED,
|
|
9
|
+
muted: ansi_1.ANSI.FG_GRAY,
|
|
10
|
+
title: ansi_1.ANSI.RESET
|
|
11
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -53,3 +53,27 @@ export interface ToggleOptions extends BaseOptions {
|
|
|
53
53
|
activeText?: string;
|
|
54
54
|
inactiveText?: string;
|
|
55
55
|
}
|
|
56
|
+
export interface ListOptions extends BaseOptions {
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
initial?: string[];
|
|
59
|
+
validate?: (value: string[]) => string | boolean | Promise<string | boolean>;
|
|
60
|
+
}
|
|
61
|
+
export interface SliderOptions extends BaseOptions {
|
|
62
|
+
min: number;
|
|
63
|
+
max: number;
|
|
64
|
+
initial?: number;
|
|
65
|
+
step?: number;
|
|
66
|
+
unit?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface DateOptions extends BaseOptions {
|
|
69
|
+
initial?: Date;
|
|
70
|
+
min?: Date;
|
|
71
|
+
max?: Date;
|
|
72
|
+
}
|
|
73
|
+
export interface FileOptions extends BaseOptions {
|
|
74
|
+
basePath?: string;
|
|
75
|
+
extensions?: string[];
|
|
76
|
+
onlyDirectories?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface MultiSelectOptions extends CheckboxOptions {
|
|
79
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects terminal capabilities.
|
|
3
|
+
*/
|
|
4
|
+
export declare function detectCapabilities(): {
|
|
5
|
+
isCI: boolean;
|
|
6
|
+
hasTrueColor: boolean;
|
|
7
|
+
hasUnicode: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Strips ANSI escape codes from a string.
|
|
11
|
+
*/
|
|
12
|
+
export declare function stripAnsi(str: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Calculates the visual width of a string.
|
|
15
|
+
* Uses binary search for wide characters.
|
|
16
|
+
* Handles ANSI codes (zero width).
|
|
17
|
+
*/
|
|
18
|
+
export declare function stringWidth(str: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* Safely splits a string into an array of grapheme clusters.
|
|
21
|
+
* Uses Intl.Segmenter (Node 16+).
|
|
22
|
+
*/
|
|
23
|
+
export declare function safeSplit(str: string): string[];
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/utils.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.detectCapabilities = detectCapabilities;
|
|
5
|
+
exports.stripAnsi = stripAnsi;
|
|
6
|
+
exports.stringWidth = stringWidth;
|
|
7
|
+
exports.safeSplit = safeSplit;
|
|
8
|
+
/**
|
|
9
|
+
* Detects terminal capabilities.
|
|
10
|
+
*/
|
|
11
|
+
function detectCapabilities() {
|
|
12
|
+
const env = process.env;
|
|
13
|
+
// Check for CI
|
|
14
|
+
const isCI = !!env.CI;
|
|
15
|
+
// Check for True Color support
|
|
16
|
+
const hasTrueColor = env.COLORTERM === 'truecolor';
|
|
17
|
+
// Check for Unicode support
|
|
18
|
+
// Windows Terminal (WT_SESSION), VS Code (TERM_PROGRAM=vscode), or modern Linux terminals usually support it.
|
|
19
|
+
const isWindows = process.platform === 'win32';
|
|
20
|
+
const hasUnicode = !!env.WT_SESSION ||
|
|
21
|
+
env.TERM_PROGRAM === 'vscode' ||
|
|
22
|
+
env.TERM_PROGRAM === 'Apple_Terminal' ||
|
|
23
|
+
(!isWindows && env.LANG && env.LANG.toUpperCase().endsWith('UTF-8'));
|
|
24
|
+
return {
|
|
25
|
+
isCI,
|
|
26
|
+
hasTrueColor,
|
|
27
|
+
hasUnicode: hasUnicode || !isWindows // Assume non-windows has unicode by default if not strictly detected?
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Strips ANSI escape codes from a string.
|
|
32
|
+
*/
|
|
33
|
+
function stripAnsi(str) {
|
|
34
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sorted array of Unicode ranges that are typically full-width (2 columns).
|
|
38
|
+
* Includes CJK, Emoji, Fullwidth forms, etc.
|
|
39
|
+
* Format: [start, end] inclusive.
|
|
40
|
+
*/
|
|
41
|
+
const WIDE_RANGES = [
|
|
42
|
+
[0x1100, 0x11FF], // Hangul Jamo
|
|
43
|
+
[0x2E80, 0x2EFF], // CJK Radicals Supplement
|
|
44
|
+
[0x2F00, 0x2FDF], // Kangxi Radicals
|
|
45
|
+
[0x3000, 0x303F], // CJK Symbols and Punctuation
|
|
46
|
+
[0x3040, 0x309F], // Hiragana
|
|
47
|
+
[0x30A0, 0x30FF], // Katakana
|
|
48
|
+
[0x3100, 0x312F], // Bopomofo
|
|
49
|
+
[0x3130, 0x318F], // Hangul Compatibility Jamo
|
|
50
|
+
[0x3200, 0x32FF], // Enclosed CJK Letters and Months
|
|
51
|
+
[0x3300, 0x33FF], // CJK Compatibility
|
|
52
|
+
[0x3400, 0x4DBF], // CJK Unified Ideographs Extension A
|
|
53
|
+
[0x4E00, 0x9FFF], // CJK Unified Ideographs
|
|
54
|
+
[0xA960, 0xA97F], // Hangul Jamo Extended-A
|
|
55
|
+
[0xAC00, 0xD7AF], // Hangul Syllables
|
|
56
|
+
[0xD7B0, 0xD7FF], // Hangul Jamo Extended-B
|
|
57
|
+
[0xF900, 0xFAFF], // CJK Compatibility Ideographs
|
|
58
|
+
[0xFE10, 0xFE1F], // Vertical Forms
|
|
59
|
+
[0xFE30, 0xFE4F], // CJK Compatibility Forms
|
|
60
|
+
[0xFE50, 0xFE6F], // Small Form Variants
|
|
61
|
+
[0xFF01, 0xFF60], // Fullwidth ASCII variants
|
|
62
|
+
[0xFFE0, 0xFFE6], // Fullwidth currency/symbols
|
|
63
|
+
[0x1F300, 0x1F6FF], // Miscellaneous Symbols and Pictographs (Emoji)
|
|
64
|
+
[0x1F900, 0x1F9FF], // Supplemental Symbols and Pictographs
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Binary search to check if a code point is in the wide ranges.
|
|
68
|
+
*/
|
|
69
|
+
function isWideCodePoint(cp) {
|
|
70
|
+
let low = 0;
|
|
71
|
+
let high = WIDE_RANGES.length - 1;
|
|
72
|
+
while (low <= high) {
|
|
73
|
+
const mid = Math.floor((low + high) / 2);
|
|
74
|
+
const [start, end] = WIDE_RANGES[mid];
|
|
75
|
+
if (cp >= start && cp <= end) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
else if (cp < start) {
|
|
79
|
+
high = mid - 1;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
low = mid + 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Calculates the visual width of a string.
|
|
89
|
+
* Uses binary search for wide characters.
|
|
90
|
+
* Handles ANSI codes (zero width).
|
|
91
|
+
*/
|
|
92
|
+
function stringWidth(str) {
|
|
93
|
+
let width = 0;
|
|
94
|
+
let inAnsi = false;
|
|
95
|
+
for (let i = 0; i < str.length; i++) {
|
|
96
|
+
const code = str.charCodeAt(i);
|
|
97
|
+
// Simple ANSI parser state check
|
|
98
|
+
if (str[i] === '\x1b') {
|
|
99
|
+
inAnsi = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (inAnsi) {
|
|
103
|
+
if ((str[i] >= '@' && str[i] <= '~') || (str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) {
|
|
104
|
+
inAnsi = false;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Handle surrogate pairs for high code points (Emoji)
|
|
109
|
+
let cp = code;
|
|
110
|
+
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
|
|
111
|
+
const next = str.charCodeAt(i + 1);
|
|
112
|
+
if (next >= 0xDC00 && next <= 0xDFFF) {
|
|
113
|
+
// Calculate code point from surrogate pair
|
|
114
|
+
cp = (code - 0xD800) * 0x400 + (next - 0xDC00) + 0x10000;
|
|
115
|
+
i++; // Skip next char
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
width += isWideCodePoint(cp) ? 2 : 1;
|
|
119
|
+
}
|
|
120
|
+
return width;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Safely splits a string into an array of grapheme clusters.
|
|
124
|
+
* Uses Intl.Segmenter (Node 16+).
|
|
125
|
+
*/
|
|
126
|
+
function safeSplit(str) {
|
|
127
|
+
// @ts-ignore - Intl.Segmenter is available in Node 16+ but TS might complain depending on lib settings
|
|
128
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
129
|
+
const segments = segmenter.segment(str);
|
|
130
|
+
const result = [];
|
|
131
|
+
for (const segment of segments) {
|
|
132
|
+
result.push(segment.segment);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
package/example.ts
CHANGED
|
@@ -3,11 +3,12 @@ import { MepCLI } from './src'; // Or 'mepcli' if installed via NPM
|
|
|
3
3
|
/**
|
|
4
4
|
* Runs a comprehensive demo showcasing all MepCLI prompt types and utilities.
|
|
5
5
|
* This demonstrates all core functionalities including Text, Password, Select,
|
|
6
|
-
* Checkbox, Number, Toggle, Confirm,
|
|
6
|
+
* Checkbox, Number, Toggle, Confirm, List, Slider, Date, File, MultiSelect,
|
|
7
|
+
* and the Spin utility.
|
|
7
8
|
*/
|
|
8
9
|
async function runComprehensiveDemo() {
|
|
9
10
|
console.clear();
|
|
10
|
-
console.log("--- MepCLI Comprehensive Demo (All
|
|
11
|
+
console.log("--- MepCLI Comprehensive Demo (All 12 Prompts + Spin Utility) ---\n");
|
|
11
12
|
|
|
12
13
|
try {
|
|
13
14
|
// --- 1. Text Prompt (Input with Validation and initial value) ---
|
|
@@ -76,16 +77,67 @@ async function runComprehensiveDemo() {
|
|
|
76
77
|
});
|
|
77
78
|
console.log(`\n✅ Toggle Result: HTTPS enabled: ${isSecure}`);
|
|
78
79
|
|
|
79
|
-
// --- 7.
|
|
80
|
+
// --- 7. List / Tags Input (New) ---
|
|
81
|
+
const keywords = await MepCLI.list({
|
|
82
|
+
message: "Enter keywords for package.json (Enter to add, Backspace to remove):",
|
|
83
|
+
initial: ["cli", "mep"],
|
|
84
|
+
validate: (tags) => tags.length > 0 || "Please add at least one keyword."
|
|
85
|
+
});
|
|
86
|
+
console.log(`\n✅ List Result: Keywords: [${keywords.join(', ')}]`);
|
|
87
|
+
|
|
88
|
+
// --- 8. Slider / Scale (New) ---
|
|
89
|
+
const brightness = await MepCLI.slider({
|
|
90
|
+
message: "Set initial brightness:",
|
|
91
|
+
min: 0,
|
|
92
|
+
max: 100,
|
|
93
|
+
initial: 80,
|
|
94
|
+
step: 5,
|
|
95
|
+
unit: "%"
|
|
96
|
+
});
|
|
97
|
+
console.log(`\n✅ Slider Result: Brightness: ${brightness}%`);
|
|
98
|
+
|
|
99
|
+
// --- 9. Date / Time Picker (New) ---
|
|
100
|
+
// We capture 'now' once to ensure initial >= min
|
|
101
|
+
const now = new Date();
|
|
102
|
+
const releaseDate = await MepCLI.date({
|
|
103
|
+
message: "Schedule release date:",
|
|
104
|
+
initial: now,
|
|
105
|
+
min: now // Cannot be in the past
|
|
106
|
+
});
|
|
107
|
+
console.log(`\n✅ Date Result: Release set for: ${releaseDate.toLocaleString()}`);
|
|
108
|
+
|
|
109
|
+
// --- 10. File Path Selector (New) ---
|
|
110
|
+
const configPath = await MepCLI.file({
|
|
111
|
+
message: "Select configuration file (Tab to autocomplete):",
|
|
112
|
+
basePath: process.cwd()
|
|
113
|
+
});
|
|
114
|
+
console.log(`\n✅ File Result: Path: ${configPath}`);
|
|
115
|
+
|
|
116
|
+
// --- 11. Multi-Select Autocomplete (New) ---
|
|
117
|
+
const linters = await MepCLI.multiSelect({
|
|
118
|
+
message: "Select linters to install (Type to search, Space to select):",
|
|
119
|
+
choices: [
|
|
120
|
+
{ title: "ESLint", value: "eslint", selected: true },
|
|
121
|
+
{ title: "Prettier", value: "prettier" },
|
|
122
|
+
{ title: "Stylelint", value: "stylelint" },
|
|
123
|
+
{ title: "TSLint (Deprecated)", value: "tslint" },
|
|
124
|
+
{ title: "JSHint", value: "jshint" },
|
|
125
|
+
{ title: "StandardJS", value: "standard" }
|
|
126
|
+
],
|
|
127
|
+
min: 1
|
|
128
|
+
});
|
|
129
|
+
console.log(`\n✅ MultiSelect Result: Linters: [${linters.join(', ')}]`);
|
|
130
|
+
|
|
131
|
+
// --- 12. Confirm Prompt (Simple Yes/No) ---
|
|
80
132
|
const proceed = await MepCLI.confirm({
|
|
81
133
|
message: "Ready to deploy the project now?",
|
|
82
134
|
initial: true
|
|
83
135
|
});
|
|
84
136
|
console.log(`\n✅ Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
|
|
85
137
|
|
|
86
|
-
// ---
|
|
138
|
+
// --- 13. Spin Utility (Loading/Async Task Indicator) ---
|
|
87
139
|
await MepCLI.spin(
|
|
88
|
-
"Finalizing configuration and deploying
|
|
140
|
+
"Finalizing configuration and deploying...",
|
|
89
141
|
new Promise(resolve => setTimeout(resolve, 1500)) // Simulates a 1.5 second async task
|
|
90
142
|
);
|
|
91
143
|
console.log("\n--- Deployment successful! All MepCLI features demonstrated! ---");
|