my pkgs monorepo
1import * as readline from 'readline';
2import chalk from 'chalk';
3import * as fs from 'fs';
4import * as path from 'path';
5
6/**
7 * Validate if a file or directory exists
8 */
9export function fileExists(filepath: string): boolean {
10 try {
11 return fs.existsSync(filepath);
12 } catch {
13 return false;
14 }
15}
16
17/**
18 * Check if path is a directory
19 */
20export function isDirectory(filepath: string): boolean {
21 try {
22 return fs.statSync(filepath).isDirectory();
23 } catch {
24 return false;
25 }
26}
27
28/**
29 * Validate file path and provide helpful feedback
30 */
31export function validateFilePath(filepath: string, fileType: 'csv' | 'json' | 'directory'): { valid: boolean; message?: string } {
32 if (!filepath || filepath.trim() === '') {
33 return { valid: false, message: '⚠️ Path cannot be empty' };
34 }
35
36 const trimmedPath = filepath.trim();
37
38 if (!fileExists(trimmedPath)) {
39 // Try to provide helpful suggestions
40 const dir = path.dirname(trimmedPath);
41 const base = path.basename(trimmedPath);
42
43 if (!fileExists(dir)) {
44 return { valid: false, message: `⚠️ Directory does not exist: ${dir}` };
45 }
46
47 return { valid: false, message: `⚠️ File not found: ${base}\n Try checking the file name and path` };
48 }
49
50 // Check if it's a directory when we expect a file
51 if (fileType !== 'directory' && isDirectory(trimmedPath)) {
52 return { valid: false, message: `⚠️ Expected a file but got a directory: ${trimmedPath}` };
53 }
54
55 // Check file extension for specific types
56 if (fileType === 'csv' && !trimmedPath.toLowerCase().endsWith('.csv')) {
57 return { valid: false, message: `⚠️ Expected a CSV file, but got: ${path.extname(trimmedPath)}` };
58 }
59
60 if (fileType === 'json' && !isDirectory(trimmedPath) && !trimmedPath.toLowerCase().endsWith('.json')) {
61 return { valid: false, message: `⚠️ Expected a JSON file or directory, but got: ${path.extname(trimmedPath)}` };
62 }
63
64 return { valid: true };
65}
66
67/**
68 * Prompt user for input with validation and retry logic
69 */
70export async function promptWithValidation(
71 question: string,
72 validator?: (input: string) => { valid: boolean; message?: string },
73 isPassword = false
74): Promise<string> {
75 while (true) {
76 const input = await prompt(question, isPassword);
77
78 if (!validator) {
79 return input;
80 }
81
82 const result = validator(input);
83 if (result.valid) {
84 return input;
85 }
86
87 if (result.message) {
88 console.log(result.message);
89 }
90 console.log('Please try again.\n');
91 }
92}
93
94/**
95 * Strip surrounding quotes from a string (single or double quotes)
96 */
97function stripQuotes(str: string): string {
98 str = str.trim();
99 if ((str.startsWith("'") && str.endsWith("'")) ||
100 (str.startsWith('"') && str.endsWith('"'))) {
101 return str.slice(1, -1);
102 }
103 return str;
104}
105
106/**
107 * Display a menu and get user selection
108 */
109export async function menu(title: string, options: Array<{ key: string; label: string; description?: string }>): Promise<string> {
110 console.log(chalk.bold(`\n${title}`));
111 console.log(chalk.gray('─'.repeat(50)));
112
113 for (const option of options) {
114 if (option.description) {
115 console.log(` ${chalk.cyan(option.key)}) ${option.label}`);
116 console.log(` ${chalk.gray(option.description)}`);
117 } else {
118 console.log(` ${chalk.cyan(option.key)}) ${option.label}`);
119 }
120 }
121
122 console.log(chalk.gray('─'.repeat(50)));
123
124 const validKeys = options.map(o => o.key.toLowerCase());
125 let answer = '';
126
127 while (!validKeys.includes(answer.toLowerCase())) {
128 answer = await prompt('Select an option: ');
129 if (!validKeys.includes(answer.toLowerCase())) {
130 console.log(chalk.red(`Invalid option. Please choose: ${validKeys.join(', ')}`));
131 }
132 }
133
134 return answer.toLowerCase();
135}
136
137/**
138 * Confirm an action with the user
139 */
140export async function confirm(question: string, defaultYes = false): Promise<boolean> {
141 const suffix = defaultYes ? ' (Y/n) ' : ' (y/N) ';
142 const answer = await prompt(question + suffix);
143
144 if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
145 return true;
146 }
147 if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
148 return false;
149 }
150
151 return defaultYes;
152}
153
154/**
155 * Read user input from command line with proper password masking
156 */
157export function prompt(question: string, hideInput = false): Promise<string> {
158 return new Promise((resolve) => {
159 if (hideInput) {
160 // For password input, use raw mode
161 const stdin = process.stdin;
162 const wasRaw = stdin.isRaw;
163
164 // Set raw mode to capture individual keystrokes
165 if (stdin.isTTY) {
166 stdin.setRawMode(true);
167 }
168
169 stdin.resume();
170 stdin.setEncoding('utf8');
171
172 process.stdout.write(question);
173
174 let password = '';
175 const onData = (char: Buffer | string) => {
176 const charStr = char.toString();
177
178 switch (charStr) {
179 case '\n':
180 case '\r':
181 case '\u0004': // Ctrl-D
182 stdin.removeListener('data', onData);
183 if (stdin.isTTY) {
184 stdin.setRawMode(wasRaw);
185 }
186 stdin.pause();
187 process.stdout.write('\n');
188 resolve(password);
189 break;
190 case '\u0003': // Ctrl-C
191 process.exit(1);
192 break;
193 case '\u007f': // Backspace
194 case '\b': // Backspace
195 if (password.length > 0) {
196 password = password.slice(0, -1);
197 process.stdout.clearLine(0);
198 process.stdout.cursorTo(0);
199 process.stdout.write(question + '*'.repeat(password.length));
200 }
201 break;
202 default:
203 password += charStr;
204 process.stdout.write('*');
205 break;
206 }
207 };
208
209 stdin.on('data', onData);
210 } else {
211 const rl = readline.createInterface({
212 input: process.stdin,
213 output: process.stdout,
214 });
215
216 rl.question(question, (answer) => {
217 rl.close();
218 // Strip quotes from file paths
219 const cleaned = stripQuotes(answer);
220 resolve(cleaned);
221 });
222 }
223 });
224}