adpa-enterprise-framework-automation
Version:
Modular, standards-compliant Node.js/TypeScript automation framework for enterprise requirements, project, and data management. Provides CLI and API for BABOK v3, PMBOK 7th Edition, and DMBOK 2.0 (in progress). Production-ready Express.js API with TypeSpe
442 lines ⢠16.3 kB
JavaScript
/**
* Enhanced Prompt Service with Input Validation and Error Handling
* Provides robust user input handling with validation, sanitization, and error recovery
*/
import * as readline from 'readline';
import { InputValidationService } from './InputValidationService.js';
import { InteractiveErrorHandler } from './InteractiveErrorHandler.js';
export class EnhancedPromptService {
rl;
isActive = false;
constructor(rl) {
this.rl = rl || readline.createInterface({
input: process.stdin,
output: process.stdout
});
}
/**
* Enhanced prompt with validation and error handling
*/
async prompt(options) {
const { message, validator, required = true, defaultValue, maxRetries = 3, timeout = 30000, mask = false, multiline = false, suggestions = [], helpText } = options;
let attempts = 0;
this.isActive = true;
// Show help text if provided
if (helpText) {
console.log(`š” ${helpText}`);
}
// Show suggestions if provided
if (suggestions.length > 0) {
console.log('š” Suggestions:');
suggestions.forEach(suggestion => console.log(` ⢠${suggestion}`));
}
while (attempts < maxRetries && this.isActive) {
attempts++;
try {
const input = await this.getInput(message, defaultValue, timeout, mask, multiline);
// Handle user cancellation
if (input === null) {
return {
success: false,
error: 'User cancelled input',
attempts,
cancelled: true
};
}
// Apply default value if input is empty and default is provided
const finalInput = input.trim() === '' && defaultValue ? defaultValue : input;
// Check required field
if (required && finalInput.trim() === '') {
console.log('ā This field is required. Please enter a value.');
if (attempts < maxRetries) {
console.log(`š” Attempt ${attempts}/${maxRetries}`);
}
continue;
}
// Apply validation if provided
if (validator) {
const validationResult = validator(finalInput);
InputValidationService.trackValidationError(validationResult);
if (!validationResult.isValid) {
console.log(InputValidationService.formatValidationError(validationResult));
if (attempts < maxRetries) {
console.log(`š” Please try again (Attempt ${attempts}/${maxRetries})`);
}
continue;
}
return {
success: true,
value: validationResult.sanitizedValue,
attempts,
cancelled: false
};
}
// No validation, return sanitized input
return {
success: true,
value: InputValidationService.sanitizeInputSecure(finalInput),
attempts,
cancelled: false
};
}
catch (error) {
const context = {
operation: 'user input prompt',
userInput: message,
timestamp: new Date()
};
const interactiveError = InteractiveErrorHandler.handleUnknownError(error, context);
await InteractiveErrorHandler.displayError(interactiveError);
if (attempts < maxRetries) {
console.log(`š” Please try again (Attempt ${attempts}/${maxRetries})`);
}
}
}
return {
success: false,
error: `Maximum retry attempts (${maxRetries}) exceeded`,
attempts,
cancelled: false
};
}
/**
* Prompt for menu choice with enhanced validation
*/
async promptMenuChoice(message, validChoices) {
return this.prompt({
message,
validator: (input) => InputValidationService.validateMenuChoice(input, validChoices),
required: true,
maxRetries: 5,
suggestions: [
`Valid choices: ${validChoices.join(', ')}`,
'Navigation: back, home, help, exit, status, refresh'
]
});
}
/**
* Prompt for project name with validation
*/
async promptProjectName(message = 'Enter project name:') {
return this.prompt({
message,
validator: InputValidationService.validateProjectName,
required: true,
helpText: 'Project name should be 3-50 characters, alphanumeric with hyphens/underscores'
});
}
/**
* Prompt for file path with security validation
*/
async promptFilePath(message = 'Enter file path:') {
return this.prompt({
message,
validator: InputValidationService.validatePathSecure,
required: true,
helpText: 'Use relative paths within the project directory'
});
}
/**
* Prompt for numeric input with range validation
*/
async promptNumber(message, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
const result = await this.prompt({
message,
validator: (input) => InputValidationService.validateNumericRange(input, min, max),
required: true,
helpText: `Enter a number between ${min} and ${max}`
});
if (result.success && result.value) {
return {
...result,
value: parseFloat(result.value)
};
}
return {
...result,
value: undefined
};
}
/**
* Prompt for yes/no confirmation
*/
async promptConfirm(message, defaultValue = false) {
const defaultText = defaultValue ? 'Y/n' : 'y/N';
const result = await this.prompt({
message: `${message} (${defaultText}):`,
validator: InputValidationService.validateYesNo,
required: false,
defaultValue: defaultValue ? 'yes' : 'no',
suggestions: ['yes, y, true, 1 for yes', 'no, n, false, 0 for no']
});
if (result.success && result.value) {
const value = result.value.toLowerCase();
return {
...result,
value: ['yes', 'y', 'true', '1'].includes(value)
};
}
return {
...result,
value: undefined
};
}
/**
* Prompt for password with masking
*/
async promptPassword(message = 'Enter password:') {
return this.prompt({
message,
validator: (input) => InputValidationService.validateText(input, {
required: true,
minLength: 3
}, 'password'),
required: true,
mask: true,
helpText: 'Password will be hidden as you type'
});
}
/**
* Prompt for email with validation
*/
async promptEmail(message = 'Enter email address:') {
return this.prompt({
message,
validator: InputValidationService.validateEmail,
required: true,
helpText: 'Enter a valid email address (e.g., user@example.com)'
});
}
/**
* Prompt for URL with validation
*/
async promptUrl(message = 'Enter URL:') {
return this.prompt({
message,
validator: InputValidationService.validateUrl,
required: true,
helpText: 'Enter a valid URL (e.g., https://example.com)'
});
}
/**
* Prompt for API key with validation
*/
async promptApiKey(provider, message) {
return this.prompt({
message: message || `Enter ${provider} API key:`,
validator: (input) => InputValidationService.validateApiKey(input, provider),
required: true,
mask: true,
helpText: `Enter your ${provider} API key (will be hidden as you type)`
});
}
/**
* Prompt for multiple choice selection
*/
async promptChoice(message, choices, allowMultiple = false) {
console.log(`\n${message}`);
choices.forEach((choice, index) => {
console.log(` ${index + 1}. ${choice}`);
});
const validChoices = choices.map((_, index) => (index + 1).toString());
if (allowMultiple) {
const result = await this.prompt({
message: 'Enter choice numbers separated by commas (e.g., 1,3,5):',
validator: (input) => {
const selections = input.split(',').map(s => s.trim());
for (const selection of selections) {
if (!validChoices.includes(selection)) {
return {
isValid: false,
error: `Invalid choice: ${selection}`,
suggestions: [`Valid choices: ${validChoices.join(', ')}`]
};
}
}
return {
isValid: true,
sanitizedValue: selections.join(',')
};
},
required: true,
suggestions: ['Enter multiple numbers separated by commas', 'Example: 1,3,5']
});
if (result.success && result.value) {
const indices = result.value.split(',').map(s => parseInt(s.trim()) - 1);
return {
...result,
value: indices.map(i => choices[i])
};
}
return {
...result,
value: undefined
};
}
else {
const result = await this.prompt({
message: 'Enter choice number:',
validator: (input) => InputValidationService.validateText(input, {
required: true,
allowedValues: validChoices,
caseSensitive: true
}, 'choice'),
required: true,
suggestions: [`Valid choices: ${validChoices.join(', ')}`]
});
if (result.success && result.value) {
const index = parseInt(result.value) - 1;
return {
...result,
value: choices[index]
};
}
return result;
}
}
/**
* Get raw input with timeout and cancellation support
*/
getInput(message, defaultValue, timeout = 30000, mask = false, multiline = false) {
return new Promise((resolve) => {
const displayMessage = defaultValue
? `${message} [${defaultValue}]: `
: `${message} `;
let timeoutId = null;
let resolved = false;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
const handleTimeout = () => {
if (!resolved) {
resolved = true;
cleanup();
console.log('\nā° Input timed out. Please try again.');
resolve(null);
}
};
const handleInput = (input) => {
if (!resolved) {
resolved = true;
cleanup();
resolve(input);
}
};
const handleCancel = () => {
if (!resolved) {
resolved = true;
cleanup();
console.log('\nš« Input cancelled by user.');
resolve(null);
}
};
// Set up timeout
timeoutId = setTimeout(handleTimeout, timeout);
// Handle Ctrl+C cancellation
const originalListeners = process.listeners('SIGINT');
process.removeAllListeners('SIGINT');
process.once('SIGINT', handleCancel);
if (mask) {
// For password input, use a different approach
this.getMaskedInput(displayMessage).then(handleInput).catch(() => handleCancel());
}
else if (multiline) {
// For multiline input
this.getMultilineInput(displayMessage).then(handleInput).catch(() => handleCancel());
}
else {
// Standard input
this.rl.question(displayMessage, (input) => {
// Restore original SIGINT listeners
process.removeAllListeners('SIGINT');
originalListeners.forEach(listener => process.on('SIGINT', listener));
handleInput(input);
});
}
});
}
/**
* Get masked input for passwords
*/
getMaskedInput(message) {
return new Promise((resolve, reject) => {
process.stdout.write(message);
const stdin = process.stdin;
stdin.setRawMode(true);
stdin.resume();
stdin.setEncoding('utf8');
let input = '';
const onData = (char) => {
switch (char) {
case '\n':
case '\r':
case '\u0004': // Ctrl+D
stdin.setRawMode(false);
stdin.pause();
stdin.removeListener('data', onData);
process.stdout.write('\n');
resolve(input);
break;
case '\u0003': // Ctrl+C
stdin.setRawMode(false);
stdin.pause();
stdin.removeListener('data', onData);
reject(new Error('User cancelled'));
break;
case '\u007f': // Backspace
if (input.length > 0) {
input = input.slice(0, -1);
process.stdout.write('\b \b');
}
break;
default:
if (char.charCodeAt(0) >= 32) { // Printable characters
input += char;
process.stdout.write('*');
}
break;
}
};
stdin.on('data', onData);
});
}
/**
* Get multiline input
*/
getMultilineInput(message) {
return new Promise((resolve) => {
console.log(message);
console.log('š” Enter multiple lines. Type "END" on a new line to finish.');
const lines = [];
const handleLine = (line) => {
if (line.trim() === 'END') {
this.rl.removeListener('line', handleLine);
resolve(lines.join('\n'));
}
else {
lines.push(line);
}
};
this.rl.on('line', handleLine);
});
}
/**
* Cancel active prompt
*/
cancel() {
this.isActive = false;
}
/**
* Close the prompt service
*/
close() {
this.isActive = false;
if (this.rl) {
this.rl.close();
}
}
}
//# sourceMappingURL=EnhancedPromptService.js.map