mepcli 0.2.0 → 0.2.5

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.
@@ -0,0 +1,245 @@
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
+ // --- Implementation: Text Prompt ---
8
+ class TextPrompt extends base_1.Prompt {
9
+ constructor(options) {
10
+ super(options);
11
+ this.errorMsg = '';
12
+ this.cursor = 0;
13
+ this.hasTyped = false;
14
+ this.renderLines = 1;
15
+ this.value = options.initial || '';
16
+ this.cursor = this.value.length;
17
+ }
18
+ render(firstRender) {
19
+ // TextPrompt needs the cursor visible!
20
+ this.print(ansi_1.ANSI.SHOW_CURSOR);
21
+ if (!firstRender) {
22
+ // Clear previous lines
23
+ for (let i = 0; i < this.renderLines; i++) {
24
+ this.print(ansi_1.ANSI.ERASE_LINE);
25
+ if (i < this.renderLines - 1)
26
+ this.print(ansi_1.ANSI.UP);
27
+ }
28
+ this.print(ansi_1.ANSI.CURSOR_LEFT);
29
+ }
30
+ let output = '';
31
+ // 1. Render the Prompt Message
32
+ const icon = this.errorMsg ? `${theme_1.theme.error}✖` : `${theme_1.theme.success}?`;
33
+ const multilineHint = this.options.multiline ? ` ${theme_1.theme.muted}(Press Ctrl+D to submit)${ansi_1.ANSI.RESET}` : '';
34
+ output += `${icon} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET}${multilineHint} `;
35
+ // 2. Render the Value or Placeholder
36
+ let displayValue = '';
37
+ if (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped) {
38
+ displayValue = `${theme_1.theme.muted}${this.options.placeholder}${ansi_1.ANSI.RESET}`;
39
+ }
40
+ else {
41
+ displayValue = this.options.isPassword ? '*'.repeat(this.value.length) : this.value;
42
+ displayValue = `${theme_1.theme.main}${displayValue}${ansi_1.ANSI.RESET}`;
43
+ }
44
+ output += displayValue;
45
+ // 3. Handle Error Message
46
+ if (this.errorMsg) {
47
+ output += `\n${theme_1.theme.error}>> ${this.errorMsg}${ansi_1.ANSI.RESET}`;
48
+ }
49
+ this.print(output);
50
+ // 4. Calculate Visual Metrics for Wrapping
51
+ const cols = process.stdout.columns || 80;
52
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
53
+ // Prompt String (visual part before value)
54
+ const promptStr = `${icon} ${theme_1.theme.title}${this.options.message} ${multilineHint} `;
55
+ const promptVisualLen = stripAnsi(promptStr).length;
56
+ // Value String (visual part)
57
+ const rawValue = (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped)
58
+ ? this.options.placeholder || ''
59
+ : (this.options.isPassword ? '*'.repeat(this.value.length) : this.value);
60
+ // Error String (visual part)
61
+ const errorVisualLines = this.errorMsg ? Math.ceil((3 + this.errorMsg.length) / cols) : 0;
62
+ // Calculate Total Lines and Cursor Position
63
+ // We simulate printing the prompt + value + error
64
+ let currentVisualLine = 0;
65
+ let currentCol = 0;
66
+ // State tracking for cursor
67
+ let cursorRow = 0;
68
+ let cursorCol = 0;
69
+ // Add Prompt
70
+ currentCol += promptVisualLen;
71
+ while (currentCol >= cols) {
72
+ currentVisualLine++;
73
+ currentCol -= cols;
74
+ }
75
+ // Add Value (Character by character to handle wrapping and cursor tracking accurately)
76
+ const valueLen = rawValue.length;
77
+ // If placeholder, we treat it as value for render height, but cursor is at 0
78
+ const isPlaceholder = (!this.value && this.options.placeholder && !this.errorMsg && !this.hasTyped);
79
+ for (let i = 0; i < valueLen; i++) {
80
+ // Check if we are at cursor position
81
+ if (!isPlaceholder && i === this.cursor) {
82
+ cursorRow = currentVisualLine;
83
+ cursorCol = currentCol;
84
+ }
85
+ const char = rawValue[i];
86
+ if (char === '\n') {
87
+ currentVisualLine++;
88
+ currentCol = 0;
89
+ }
90
+ else {
91
+ currentCol++;
92
+ if (currentCol >= cols) {
93
+ currentVisualLine++;
94
+ currentCol = 0;
95
+ }
96
+ }
97
+ }
98
+ // If cursor is at the very end
99
+ if (!isPlaceholder && this.cursor === valueLen) {
100
+ cursorRow = currentVisualLine;
101
+ cursorCol = currentCol;
102
+ }
103
+ // If placeholder, cursor is at start of value
104
+ if (isPlaceholder) {
105
+ let pCol = promptVisualLen;
106
+ let pRow = 0;
107
+ while (pCol >= cols) {
108
+ pRow++;
109
+ pCol -= cols;
110
+ }
111
+ cursorRow = pRow;
112
+ cursorCol = pCol;
113
+ }
114
+ // Final height
115
+ const totalValueRows = currentVisualLine + 1;
116
+ this.renderLines = totalValueRows + errorVisualLines;
117
+ // 5. Position Cursor Logic
118
+ const endRow = this.renderLines - 1;
119
+ // Move up to cursor row
120
+ const linesUp = endRow - cursorRow;
121
+ if (linesUp > 0) {
122
+ this.print(`\x1b[${linesUp}A`);
123
+ }
124
+ // Move to cursor col
125
+ this.print(ansi_1.ANSI.CURSOR_LEFT); // Go to col 0
126
+ if (cursorCol > 0) {
127
+ this.print(`\x1b[${cursorCol}C`);
128
+ }
129
+ }
130
+ handleInput(char) {
131
+ // Enter
132
+ if (char === '\r' || char === '\n') {
133
+ if (this.options.multiline) {
134
+ this.value = this.value.slice(0, this.cursor) + '\n' + this.value.slice(this.cursor);
135
+ this.cursor++;
136
+ this.render(false);
137
+ return;
138
+ }
139
+ this.validateAndSubmit();
140
+ return;
141
+ }
142
+ // Ctrl+D (EOF) or Ctrl+S for Submit in Multiline
143
+ if (this.options.multiline && (char === '\u0004' || char === '\u0013')) {
144
+ this.validateAndSubmit();
145
+ return;
146
+ }
147
+ // Backspace
148
+ if (char === '\u0008' || char === '\x7f') {
149
+ this.hasTyped = true;
150
+ if (this.cursor > 0) {
151
+ this.value = this.value.slice(0, this.cursor - 1) + this.value.slice(this.cursor);
152
+ this.cursor--;
153
+ this.errorMsg = '';
154
+ this.render(false);
155
+ }
156
+ return;
157
+ }
158
+ // Arrow Left
159
+ if (this.isLeft(char)) {
160
+ if (this.cursor > 0) {
161
+ this.cursor--;
162
+ this.render(false);
163
+ }
164
+ return;
165
+ }
166
+ // Arrow Right
167
+ if (this.isRight(char)) {
168
+ if (this.cursor < this.value.length) {
169
+ this.cursor++;
170
+ this.render(false);
171
+ }
172
+ return;
173
+ }
174
+ // Delete key
175
+ if (char === '\u001b[3~') {
176
+ this.hasTyped = true;
177
+ if (this.cursor < this.value.length) {
178
+ this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + 1);
179
+ this.errorMsg = '';
180
+ this.render(false);
181
+ }
182
+ return;
183
+ }
184
+ // Regular Typing & Paste
185
+ if (!/^[\x00-\x1F]/.test(char) && !char.startsWith('\x1b')) {
186
+ this.hasTyped = true;
187
+ this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
188
+ this.cursor += char.length;
189
+ this.errorMsg = '';
190
+ this.render(false);
191
+ }
192
+ }
193
+ validateAndSubmit() {
194
+ if (this.options.validate) {
195
+ const result = this.options.validate(this.value);
196
+ // Handle Promise validation
197
+ if (result instanceof Promise) {
198
+ // Show loading state
199
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${theme_1.theme.main}Validating...${ansi_1.ANSI.RESET}`);
200
+ this.print(ansi_1.ANSI.UP);
201
+ result.then(valid => {
202
+ // Clear loading message
203
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}`);
204
+ this.print(ansi_1.ANSI.UP);
205
+ if (typeof valid === 'string' && valid.length > 0) {
206
+ this.errorMsg = valid;
207
+ this.render(false);
208
+ }
209
+ else if (valid === false) {
210
+ this.errorMsg = 'Invalid input';
211
+ this.render(false);
212
+ }
213
+ else {
214
+ if (this.errorMsg) {
215
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
216
+ }
217
+ this.submit(this.value);
218
+ }
219
+ }).catch(err => {
220
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}`);
221
+ this.print(ansi_1.ANSI.UP);
222
+ this.errorMsg = err.message || 'Validation failed';
223
+ this.render(false);
224
+ });
225
+ return;
226
+ }
227
+ // Handle Sync validation
228
+ if (typeof result === 'string' && result.length > 0) {
229
+ this.errorMsg = result;
230
+ this.render(false);
231
+ return;
232
+ }
233
+ if (result === false) {
234
+ this.errorMsg = 'Invalid input';
235
+ this.render(false);
236
+ return;
237
+ }
238
+ }
239
+ if (this.errorMsg) {
240
+ this.print(`\n${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.UP}`);
241
+ }
242
+ this.submit(this.value);
243
+ }
244
+ }
245
+ 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,45 @@
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
+ this.print(ansi_1.ANSI.HIDE_CURSOR);
15
+ if (!firstRender) {
16
+ this.print(`${ansi_1.ANSI.ERASE_LINE}${ansi_1.ANSI.CURSOR_LEFT}`);
17
+ }
18
+ const activeText = this.options.activeText || 'ON';
19
+ const inactiveText = this.options.inactiveText || 'OFF';
20
+ let toggleDisplay = '';
21
+ if (this.value) {
22
+ 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}`;
23
+ }
24
+ else {
25
+ 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}`;
26
+ }
27
+ this.print(`${theme_1.theme.success}?${ansi_1.ANSI.RESET} ${ansi_1.ANSI.BOLD}${theme_1.theme.title}${this.options.message}${ansi_1.ANSI.RESET} ${toggleDisplay}`);
28
+ this.print(`\x1b[${toggleDisplay.length}D`); // Move back is not really needed as we hide cursor, but kept for consistency
29
+ }
30
+ handleInput(char) {
31
+ if (char === '\r' || char === '\n') {
32
+ this.submit(this.value);
33
+ return;
34
+ }
35
+ if (this.isLeft(char) || this.isRight(char) || char === 'h' || char === 'l') { // Left/Right
36
+ this.value = !this.value;
37
+ this.render(false);
38
+ }
39
+ if (char === ' ') {
40
+ this.value = !this.value;
41
+ this.render(false);
42
+ }
43
+ }
44
+ }
45
+ exports.TogglePrompt = TogglePrompt;
@@ -0,0 +1,2 @@
1
+ import { ThemeConfig } from './types';
2
+ export declare const theme: ThemeConfig;
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
@@ -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;
@@ -52,3 +53,27 @@ export interface ToggleOptions extends BaseOptions {
52
53
  activeText?: string;
53
54
  inactiveText?: string;
54
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/example.ts CHANGED
@@ -1,77 +1,149 @@
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, List, Slider, Date, File, MultiSelect,
7
+ * and the Spin utility.
5
8
  */
6
- async function runAllTests() {
9
+ async function runComprehensiveDemo() {
7
10
  console.clear();
8
- console.log("--- MepCLI Comprehensive Test Suite (Neutralized) ---\n");
11
+ console.log("--- MepCLI Comprehensive Demo (All 12 Prompts + Spin Utility) ---\n");
9
12
 
10
13
  try {
11
- // --- 1. Text Prompt Test (with Validation) ---
14
+ // --- 1. Text Prompt (Input with Validation and initial value) ---
12
15
  const projectName = await MepCLI.text({
13
16
  message: "Enter the name for your new project:",
14
17
  placeholder: "e.g., minimalist-cli-app",
15
18
  initial: "MepProject",
16
19
  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
- }
20
+ if (value.length < 3) return "Project name must be at least 3 characters long.";
23
21
  return true;
24
22
  }
25
23
  });
26
24
  console.log(`\n✅ Text Result: Project name set to '${projectName}'`);
27
25
 
28
- // --- 2. Password Prompt Test ---
26
+ // --- 2. Password Prompt (Hidden input) ---
29
27
  const apiKey = await MepCLI.password({
30
28
  message: "Enter the project's external API key:",
31
29
  placeholder: "Input will be hidden..."
32
30
  });
33
- // Note: Do not log the actual key in a real app.
34
31
  console.log(`\n✅ Password Result: API key entered (length: ${apiKey.length})`);
35
32
 
36
-
37
- // --- 3. Select Prompt Test (Single Choice) ---
33
+ // --- 3. Select Prompt (Single choice, supports filtering/searching by typing) ---
38
34
  const theme = await MepCLI.select({
39
35
  message: "Choose your preferred editor color theme:",
40
36
  choices: [
41
37
  { title: "Dark Mode (Default)", value: "dark" },
42
38
  { title: "Light Mode (Classic)", value: "light" },
43
39
  { title: "High Contrast (Accessibility)", value: "contrast" },
40
+ // Demonstrates a separator option
41
+ { separator: true, text: "--- Pro Themes ---" },
44
42
  { title: "Monokai Pro", value: "monokai" },
45
43
  ]
46
44
  });
47
45
  console.log(`\n✅ Select Result: Chosen theme is: ${theme}`);
48
46
 
49
-
50
- // --- 4. Checkbox Prompt Test (Multi-Choice with Min/Max) ---
47
+ // --- 4. Checkbox Prompt (Multi-choice with Min/Max limits) ---
51
48
  const buildTools = await MepCLI.checkbox({
52
49
  message: "Select your required bundlers/build tools (Min 1, Max 2):",
53
50
  min: 1,
54
51
  max: 2,
55
52
  choices: [
56
53
  { title: "Webpack", value: "webpack" },
57
- { title: "Vite", value: "vite", selected: true },
54
+ { title: "Vite", value: "vite", selected: true }, // Default selected state
58
55
  { title: "Rollup", value: "rollup" },
59
56
  { title: "esbuild", value: "esbuild" }
60
57
  ]
61
58
  });
62
59
  console.log(`\n✅ Checkbox Result: Selected build tools: [${buildTools.join(', ')}]`);
63
60
 
61
+ // --- 5. Number Prompt (Numeric input, supports Min/Max and Up/Down arrow for Step) ---
62
+ const port = await MepCLI.number({
63
+ message: "Which port should the server run on?",
64
+ initial: 3000,
65
+ min: 1024,
66
+ max: 65535,
67
+ step: 100 // Increments/decrements by 100 with arrows
68
+ });
69
+ console.log(`\n✅ Number Result: Server port: ${port}`);
70
+
71
+ // --- 6. Toggle Prompt (Boolean input, supports custom labels) ---
72
+ const isSecure = await MepCLI.toggle({
73
+ message: "Enable HTTPS/SSL for production?",
74
+ initial: false,
75
+ activeText: "SECURE", // Custom 'on' label
76
+ inactiveText: "INSECURE" // Custom 'off' label
77
+ });
78
+ console.log(`\n✅ Toggle Result: HTTPS enabled: ${isSecure}`);
79
+
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(', ')}]`);
64
130
 
65
- // --- 5. Confirm Prompt Test ---
131
+ // --- 12. Confirm Prompt (Simple Yes/No) ---
66
132
  const proceed = await MepCLI.confirm({
67
- message: "Do you want to continue with the installation setup?",
133
+ message: "Ready to deploy the project now?",
68
134
  initial: true
69
135
  });
70
- console.log(`\n✅ Confirm Result: Setup decision: ${proceed ? 'Proceed' : 'Cancel'}`);
136
+ console.log(`\n✅ Confirm Result: Deployment decision: ${proceed ? 'Proceed' : 'Cancel'}`);
71
137
 
72
- console.log("\n--- All MepCLI tests completed successfully! ---");
138
+ // --- 13. Spin Utility (Loading/Async Task Indicator) ---
139
+ await MepCLI.spin(
140
+ "Finalizing configuration and deploying...",
141
+ new Promise(resolve => setTimeout(resolve, 1500)) // Simulates a 1.5 second async task
142
+ );
143
+ console.log("\n--- Deployment successful! All MepCLI features demonstrated! ---");
73
144
 
74
145
  } catch (e) {
146
+ // Global handler for Ctrl+C closure
75
147
  if (e instanceof Error && e.message === 'User force closed') {
76
148
  console.log("\nOperation cancelled by user (Ctrl+C).");
77
149
  } else {
@@ -80,4 +152,4 @@ async function runAllTests() {
80
152
  }
81
153
  }
82
154
 
83
- runAllTests();
155
+ runComprehensiveDemo();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "0.2.0",
3
+ "version": "0.2.5",
4
4
  "description": "Zero-dependency, minimalist interactive CLI prompt for Node.js",
5
5
  "repository": {
6
6
  "type": "git",