my pkgs monorepo
at main 224 lines 6.2 kB view raw
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}