mepcli 0.2.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/dist/core.js CHANGED
@@ -78,6 +78,7 @@ class TextPrompt extends Prompt {
78
78
  this.errorMsg = '';
79
79
  this.cursor = 0;
80
80
  this.hasTyped = false;
81
+ this.renderLines = 1;
81
82
  this.value = options.initial || '';
82
83
  this.cursor = this.value.length;
83
84
  }
@@ -85,58 +86,142 @@ class TextPrompt extends Prompt {
85
86
  // TextPrompt needs the cursor visible!
86
87
  this.print(ansi_1.ANSI.SHOW_CURSOR);
87
88
  if (!firstRender) {
88
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
89
- if (this.errorMsg) {
90
- this.print(ansi_1.ANSI.UP + ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
89
+ // Clear previous lines
90
+ // Note: renderLines now represents visual wrapped lines
91
+ for (let i = 0; i < this.renderLines; i++) {
92
+ this.print(ansi_1.ANSI.ERASE_LINE);
93
+ if (i < this.renderLines - 1)
94
+ this.print(ansi_1.ANSI.UP);
91
95
  }
96
+ this.print(ansi_1.ANSI.CURSOR_LEFT);
92
97
  }
98
+ let output = '';
93
99
  // 1. Render the Prompt Message
94
- this.print(ansi_1.ANSI.ERASE_LINE + ansi_1.ANSI.CURSOR_LEFT);
95
100
  const icon = this.errorMsg ? `${MepCLI.theme.error}✖` : `${MepCLI.theme.success}?`;
96
- this.print(`${icon} ${ansi_1.ANSI.BOLD}${MepCLI.theme.title}${this.options.message}${ansi_1.ANSI.RESET} `);
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} `;
97
103
  // 2. Render the Value or Placeholder
104
+ let displayValue = '';
98
105
  if (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped) {
99
- this.print(`${MepCLI.theme.muted}${this.options.placeholder}${ansi_1.ANSI.RESET}`);
100
- // Move cursor back to start so typing overwrites placeholder visually
101
- this.print(`\x1b[${this.options.placeholder.length}D`);
106
+ displayValue = `${MepCLI.theme.muted}${this.options.placeholder}${ansi_1.ANSI.RESET}`;
102
107
  }
103
108
  else {
104
- const displayValue = this.options.isPassword ? '*'.repeat(this.value.length) : this.value;
105
- this.print(`${MepCLI.theme.main}${displayValue}${ansi_1.ANSI.RESET}`);
109
+ displayValue = this.options.isPassword ? '*'.repeat(this.value.length) : this.value;
110
+ displayValue = `${MepCLI.theme.main}${displayValue}${ansi_1.ANSI.RESET}`;
106
111
  }
112
+ output += displayValue;
107
113
  // 3. Handle Error Message
108
114
  if (this.errorMsg) {
109
- this.print(`\n${ansi_1.ANSI.ERASE_LINE}${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`);
110
- this.print(ansi_1.ANSI.UP); // Go back to input line
111
- // Re-calculate position to end of input
112
- const promptLen = this.options.message.length + 3; // Icon + 2 spaces
113
- const valLen = this.value.length;
114
- // Move to absolute start of line, then move right to end of string
115
- this.print(`\x1b[1000D\x1b[${promptLen + valLen}C`);
115
+ output += `\n${MepCLI.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
116
116
  }
117
- // 4. Position Cursor Logic
118
- // At this point, the physical cursor is at the END of the value string.
119
- // We need to move it LEFT by (length - cursor_index)
120
- const diff = this.value.length - this.cursor;
121
- if (diff > 0) {
122
- this.print(`\x1b[${diff}D`);
117
+ this.print(output);
118
+ // 4. Calculate Visual Metrics for Wrapping
119
+ const cols = process.stdout.columns || 80;
120
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
121
+ // Prompt String (visual part before value)
122
+ const promptStr = `${icon} ${MepCLI.theme.title}${this.options.message} ${multilineHint} `;
123
+ const promptVisualLen = stripAnsi(promptStr).length;
124
+ // Value String (visual part)
125
+ const rawValue = (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped)
126
+ ? this.options.placeholder || ''
127
+ : (this.options.isPassword ? '*'.repeat(this.value.length) : this.value);
128
+ // Error String (visual part)
129
+ const errorVisualLines = this.errorMsg ? Math.ceil((3 + this.errorMsg.length) / cols) : 0;
130
+ // Calculate Total Lines and Cursor Position
131
+ // We simulate printing the prompt + value + error
132
+ let currentVisualLine = 0;
133
+ let currentCol = 0;
134
+ // State tracking for cursor
135
+ let cursorRow = 0;
136
+ let cursorCol = 0;
137
+ // Add Prompt
138
+ currentCol += promptVisualLen;
139
+ while (currentCol >= cols) {
140
+ currentVisualLine++;
141
+ currentCol -= cols;
142
+ }
143
+ // Add Value (Character by character to handle wrapping and cursor tracking accurately)
144
+ // Note: This doesn't handle multi-width chars perfectly, but handles wrapping better than before
145
+ const valueLen = rawValue.length;
146
+ // If placeholder, we treat it as value for render height, but cursor is at 0
147
+ const isPlaceholder = (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped);
148
+ for (let i = 0; i < valueLen; i++) {
149
+ // Check if we are at cursor position
150
+ if (!isPlaceholder && i === this.cursor) {
151
+ cursorRow = currentVisualLine;
152
+ cursorCol = currentCol;
153
+ }
154
+ const char = rawValue[i];
155
+ if (char === '\n') {
156
+ currentVisualLine++;
157
+ currentCol = 0;
158
+ }
159
+ else {
160
+ currentCol++;
161
+ if (currentCol >= cols) {
162
+ currentVisualLine++;
163
+ currentCol = 0;
164
+ }
165
+ }
166
+ }
167
+ // If cursor is at the very end
168
+ if (!isPlaceholder && this.cursor === valueLen) {
169
+ cursorRow = currentVisualLine;
170
+ cursorCol = currentCol;
171
+ }
172
+ // If placeholder, cursor is at start of value
173
+ if (isPlaceholder) {
174
+ // Re-calc cursor position as if it's at index 0 of value
175
+ // Which is effectively where prompt ends
176
+ // We already updated currentCol/Line for prompt above, but loop continued for placeholder
177
+ // So we need to recalculate or store prompt end state
178
+ // Let's just use prompt end state:
179
+ let pCol = promptVisualLen;
180
+ let pRow = 0;
181
+ while (pCol >= cols) {
182
+ pRow++;
183
+ pCol -= cols;
184
+ }
185
+ cursorRow = pRow;
186
+ cursorCol = pCol;
187
+ }
188
+ // Final height
189
+ // If we are at col 0 of a new line (e.g. just wrapped or \n), we count that line
190
+ // currentVisualLine is 0-indexed index of the line we are on.
191
+ // Total lines = currentVisualLine + 1 + errorLines
192
+ // Special case: if input ends with \n, we are on a new empty line
193
+ const totalValueRows = currentVisualLine + 1;
194
+ this.renderLines = totalValueRows + errorVisualLines;
195
+ // 5. Position Cursor Logic
196
+ // We are currently at the end of output.
197
+ // End row relative to start: this.renderLines - 1
198
+ const endRow = this.renderLines - 1;
199
+ // Move up to cursor row
200
+ const linesUp = endRow - cursorRow;
201
+ if (linesUp > 0) {
202
+ this.print(`\x1b[${linesUp}A`);
203
+ }
204
+ // Move to cursor col
205
+ this.print(ansi_1.ANSI.CURSOR_LEFT); // Go to col 0
206
+ if (cursorCol > 0) {
207
+ this.print(`\x1b[${cursorCol}C`);
123
208
  }
124
209
  }
125
210
  handleInput(char) {
126
211
  // Enter
127
212
  if (char === '\r' || char === '\n') {
128
- if (this.options.validate) {
129
- const validationResult = this.options.validate(this.value);
130
- if (typeof validationResult === 'string' && validationResult.length > 0) {
131
- this.errorMsg = validationResult;
132
- this.render(false);
133
- return;
134
- }
135
- }
136
- if (this.errorMsg) {
137
- 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;
138
218
  }
139
- this.submit(this.value);
219
+ this.validateAndSubmit();
220
+ return;
221
+ }
222
+ // Ctrl+D (EOF) or Ctrl+S for Submit in Multiline
223
+ if (this.options.multiline && (char === '\u0004' || char === '\u0013')) {
224
+ this.validateAndSubmit();
140
225
  return;
141
226
  }
142
227
  // Backspace
@@ -176,15 +261,66 @@ class TextPrompt extends Prompt {
176
261
  }
177
262
  return;
178
263
  }
179
- // Regular Typing
180
- if (char.length === 1 && !/^[\x00-\x1F]/.test(char)) {
264
+ // Regular Typing & Paste
265
+ if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
181
266
  this.hasTyped = true;
182
267
  this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
183
- this.cursor++;
268
+ this.cursor += char.length;
184
269
  this.errorMsg = '';
185
270
  this.render(false);
186
271
  }
187
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
+ }
188
324
  }
189
325
  // --- Implementation: Select Prompt ---
190
326
  class SelectPrompt extends Prompt {
@@ -192,6 +328,8 @@ class SelectPrompt extends Prompt {
192
328
  super(options);
193
329
  this.selectedIndex = 0;
194
330
  this.searchBuffer = '';
331
+ this.scrollTop = 0;
332
+ this.pageSize = 7;
195
333
  // Custom render to handle variable height clearing
196
334
  this.lastRenderHeight = 0;
197
335
  // Find first non-separator index
@@ -237,6 +375,17 @@ class SelectPrompt extends Prompt {
237
375
  }
238
376
  let output = '';
239
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
+ }
240
389
  // Header
241
390
  const searchStr = this.searchBuffer ? ` ${MepCLI.theme.muted}(Filter: ${this.searchBuffer})${ansi_1.ANSI.RESET}` : '';
242
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`;
@@ -244,13 +393,15 @@ class SelectPrompt extends Prompt {
244
393
  output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT} ${MepCLI.theme.muted}No results found${ansi_1.ANSI.RESET}\n`;
245
394
  }
246
395
  else {
247
- choices.forEach((choice, index) => {
396
+ const visibleChoices = choices.slice(this.scrollTop, this.scrollTop + this.pageSize);
397
+ visibleChoices.forEach((choice, index) => {
398
+ const actualIndex = this.scrollTop + index;
248
399
  output += `${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`;
249
400
  if (this.isSeparator(choice)) {
250
401
  output += ` ${ansi_1.ANSI.DIM}${choice.text || '────────'}${ansi_1.ANSI.RESET}\n`;
251
402
  }
252
403
  else {
253
- if (index === this.selectedIndex) {
404
+ if (actualIndex === this.selectedIndex) {
254
405
  output += `${MepCLI.theme.main}❯ ${choice.title}${ansi_1.ANSI.RESET}\n`;
255
406
  }
256
407
  else {
@@ -261,7 +412,8 @@ class SelectPrompt extends Prompt {
261
412
  }
262
413
  this.print(output);
263
414
  // Clear remaining lines if list shrunk
264
- const currentHeight = choices.length + 1 + (choices.length === 0 ? 1 : 0);
415
+ const visibleCount = Math.min(choices.length, this.pageSize);
416
+ const currentHeight = visibleCount + 1 + (choices.length === 0 ? 1 : 0);
265
417
  const linesToClear = this.lastRenderHeight - currentHeight;
266
418
  if (linesToClear > 0) {
267
419
  for (let i = 0; i < linesToClear; i++) {
@@ -595,13 +747,17 @@ class NumberPrompt extends Prompt {
595
747
  return;
596
748
  }
597
749
  // Numeric Input (and . and -)
598
- if (/^[0-9.\-]$/.test(char)) {
599
- if (char === '-' && (this.cursor !== 0 || this.stringValue.includes('-')))
600
- return;
601
- if (char === '.' && this.stringValue.includes('.'))
602
- return;
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
603
759
  this.stringValue = this.stringValue.slice(0, this.cursor) + char + this.stringValue.slice(this.cursor);
604
- this.cursor++;
760
+ this.cursor += char.length;
605
761
  this.errorMsg = '';
606
762
  this.render(false);
607
763
  }
package/dist/types.d.ts CHANGED
@@ -14,8 +14,9 @@ export interface BaseOptions {
14
14
  export interface TextOptions extends BaseOptions {
15
15
  placeholder?: string;
16
16
  initial?: string;
17
- validate?: (value: string) => string | boolean;
17
+ validate?: (value: string) => string | boolean | Promise<string | boolean>;
18
18
  isPassword?: boolean;
19
+ multiline?: boolean;
19
20
  }
20
21
  export interface Separator {
21
22
  separator: true;
package/example.ts CHANGED
@@ -1,77 +1,97 @@
1
- import { MepCLI } from './src'; // Or mepcli if installed via NPM
1
+ import { MepCLI } from './src'; // Or 'mepcli' if installed via NPM
2
2
 
3
3
  /**
4
- * Runs a comprehensive set of tests for all MepCLI prompt types.
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.
5
7
  */
6
- async function runAllTests() {
8
+ async function runComprehensiveDemo() {
7
9
  console.clear();
8
- console.log("--- MepCLI Comprehensive Test Suite (Neutralized) ---\n");
10
+ console.log("--- MepCLI Comprehensive Demo (All 7 Prompts + Spin Utility) ---\n");
9
11
 
10
12
  try {
11
- // --- 1. Text Prompt Test (with Validation) ---
13
+ // --- 1. Text Prompt (Input with Validation and initial value) ---
12
14
  const projectName = await MepCLI.text({
13
15
  message: "Enter the name for your new project:",
14
16
  placeholder: "e.g., minimalist-cli-app",
15
17
  initial: "MepProject",
16
18
  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
- }
19
+ if (value.length < 3) return "Project name must be at least 3 characters long.";
23
20
  return true;
24
21
  }
25
22
  });
26
23
  console.log(`\n✅ Text Result: Project name set to '${projectName}'`);
27
24
 
28
- // --- 2. Password Prompt Test ---
25
+ // --- 2. Password Prompt (Hidden input) ---
29
26
  const apiKey = await MepCLI.password({
30
27
  message: "Enter the project's external API key:",
31
28
  placeholder: "Input will be hidden..."
32
29
  });
33
- // Note: Do not log the actual key in a real app.
34
30
  console.log(`\n✅ Password Result: API key entered (length: ${apiKey.length})`);
35
31
 
36
-
37
- // --- 3. Select Prompt Test (Single Choice) ---
32
+ // --- 3. Select Prompt (Single choice, supports filtering/searching by typing) ---
38
33
  const theme = await MepCLI.select({
39
34
  message: "Choose your preferred editor color theme:",
40
35
  choices: [
41
36
  { title: "Dark Mode (Default)", value: "dark" },
42
37
  { title: "Light Mode (Classic)", value: "light" },
43
38
  { title: "High Contrast (Accessibility)", value: "contrast" },
39
+ // Demonstrates a separator option
40
+ { separator: true, text: "--- Pro Themes ---" },
44
41
  { title: "Monokai Pro", value: "monokai" },
45
42
  ]
46
43
  });
47
44
  console.log(`\n✅ Select Result: Chosen theme is: ${theme}`);
48
45
 
49
-
50
- // --- 4. Checkbox Prompt Test (Multi-Choice with Min/Max) ---
46
+ // --- 4. Checkbox Prompt (Multi-choice with Min/Max limits) ---
51
47
  const buildTools = await MepCLI.checkbox({
52
48
  message: "Select your required bundlers/build tools (Min 1, Max 2):",
53
49
  min: 1,
54
50
  max: 2,
55
51
  choices: [
56
52
  { title: "Webpack", value: "webpack" },
57
- { title: "Vite", value: "vite", selected: true },
53
+ { title: "Vite", value: "vite", selected: true }, // Default selected state
58
54
  { title: "Rollup", value: "rollup" },
59
55
  { title: "esbuild", value: "esbuild" }
60
56
  ]
61
57
  });
62
58
  console.log(`\n✅ Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
63
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}`);
64
78
 
65
- // --- 5. Confirm Prompt Test ---
79
+ // --- 7. Confirm Prompt (Simple Yes/No) ---
66
80
  const proceed = await MepCLI.confirm({
67
- message: "Do you want to continue with the installation setup?",
81
+ message: "Ready to deploy the project now?",
68
82
  initial: true
69
83
  });
70
- console.log(`\n✅ Confirm Result: Setup decision: ${proceed ? 'Proceed' : 'Cancel'}`);
84
+ console.log(`\n✅ Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
71
85
 
72
- console.log("\n--- All MepCLI tests completed successfully! ---");
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! ---");
73
92
 
74
93
  } catch (e) {
94
+ // Global handler for Ctrl+C closure
75
95
  if (e instanceof Error && e.message === 'User force closed') {
76
96
  console.log("\nOperation cancelled by user (Ctrl+C).");
77
97
  } else {
@@ -80,4 +100,4 @@ async function runAllTests() {
80
100
  }
81
101
  }
82
102
 
83
- runAllTests();
103
+ runComprehensiveDemo();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
5
  "repository": {
6
6
  "type": "git",