UNPKG

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

560 lines • 22.1 kB
/** * Interactive Provider Selection Menu Implementation * * Provides an interactive CLI-based menu for selecting and configuring * AI providers for the Requirements Gathering Agent. * * @version 2.1.3 * @author Requirements Gathering Agent Team * @created June 2025 * * @filepath c:\Users\menno\Source\Repos\requirements-gathering-agent\src\modules\ai\interactive-menu.ts */ import * as readline from 'readline'; import { promises as fs } from 'fs'; import { join } from 'path'; import { CONFIG_FILENAME } from '../../constants.js'; import { PROVIDER_DEFINITIONS, getProviderById } from './provider-definitions.js'; import { InputValidationService } from '../cli/InputValidationService.js'; import { InteractiveErrorHandler } from '../cli/InteractiveErrorHandler.js'; export class InteractiveProviderMenu { readline; providers = new Map(); options; constructor(options = {}) { this.readline = readline.createInterface({ input: process.stdin, output: process.stdout }); this.options = { showMetrics: false, allowExit: true, autoRefresh: false, refreshInterval: 30000, ...options }; // Initialize providers map for (const provider of PROVIDER_DEFINITIONS) { this.providers.set(provider.id, provider); } } /** * Show the main interactive provider selection menu */ async showMenu() { while (true) { await this.clearScreen(); await this.displayMenu(); const choice = await this.getUserInput('\nEnter your choice: '); const result = await this.handleMenuChoice(choice); if (result === 'EXIT') { return null; // User chose to exit } if (result && result !== 'EXIT') { return result; // Provider selected } // Continue menu loop for invalid choices or menu actions (result === null) } } /** * Display the formatted provider menu */ async displayMenu() { console.log('šŸ¤– AI Provider Selection Menu\n'); console.log('Choose your AI provider for Requirements Gathering Agent:\n'); // Get all providers sorted by priority const providers = Array.from(this.providers.values()) .sort((a, b) => a.priority - b.priority); // Display each provider with status for (let i = 0; i < providers.length; i++) { const provider = providers[i]; const status = await provider.getStatus(); const statusIcon = this.getStatusIcon(status); const statusText = this.getStatusText(status); console.log(` ${i + 1}. ${statusIcon} ${provider.displayName} - ${statusText}`); console.log(` └─ ${provider.description}`); // Show detailed configuration status const configStatus = await this.getDetailedConfigStatus(provider); if (configStatus) { console.log(` └─ šŸ”§ Configuration: ${configStatus}`); } if (provider.recommendation) { console.log(` └─ šŸ’” ${provider.recommendation}`); } if (this.options.showMetrics && status.metrics) { console.log(` └─ šŸ“Š Avg response: ${status.metrics.averageResponseTime}ms`); } if (provider.setupGuide && !status.configured) { console.log(` └─ ā±ļø Setup time: ~${provider.setupGuide.estimatedTime}`); } console.log(''); } // Menu options const menuOffset = providers.length; console.log(` ${menuOffset + 1}. šŸ”§ Configure New Provider`); console.log(` ${menuOffset + 2}. šŸ“Š View Provider Status & Metrics`); console.log(` ${menuOffset + 3}. ā“ Help & Documentation`); if (this.options.allowExit) { console.log(` ${menuOffset + 4}. 🚫 Exit Menu`); } } /** * Handle user menu selection */ async handleMenuChoice(choice) { const providers = Array.from(this.providers.values()) .sort((a, b) => a.priority - b.priority); // Build valid choices array const validChoices = []; for (let i = 1; i <= providers.length; i++) { validChoices.push(i.toString()); } const menuOffset = providers.length; validChoices.push((menuOffset + 1).toString()); // Configure New Provider validChoices.push((menuOffset + 2).toString()); // View Status & Metrics validChoices.push((menuOffset + 3).toString()); // Help & Documentation if (this.options.allowExit) { validChoices.push((menuOffset + 4).toString()); // Exit } validChoices.push('0'); // Hidden exit option // Validate menu choice const validationResult = InputValidationService.validateMenuChoice(choice, validChoices); if (!validationResult.isValid) { const context = { operation: 'provider menu selection', userInput: choice, timestamp: new Date() }; const error = InteractiveErrorHandler.handleValidationError(validationResult, context); await InteractiveErrorHandler.displayError(error); await this.pause(); return null; } const choiceNum = parseInt(validationResult.sanitizedValue); // Handle provider selection (1-N) if (choiceNum >= 1 && choiceNum <= providers.length) { const selectedProvider = providers[choiceNum - 1]; return await this.handleProviderSelection(selectedProvider); } // Handle menu options switch (choiceNum) { case menuOffset + 1: // Configure New Provider return await this.configureNewProvider(); case menuOffset + 2: // View Status & Metrics await this.showProviderMetrics(); break; case menuOffset + 3: // Help & Documentation await this.showHelp(); break; case menuOffset + 4: // Exit if (this.options.allowExit) { return 'EXIT'; } break; case 0: // Hidden exit option return 'EXIT'; } return null; // Continue menu loop } /** * Handle selection of a specific provider */ async handleProviderSelection(provider) { const status = await provider.getStatus(); if (status.configured && status.available && status.connected) { // Provider ready - confirm selection console.log(`\nāœ… ${provider.displayName} is ready!`); console.log(` Features: ${provider.features.join(', ')}`); // Get confirmation with validation while (true) { const confirm = await this.getUserInput('Use this provider? (y/n): '); const validation = InputValidationService.validateYesNo(confirm); if (validation.isValid) { if (validation.sanitizedValue === 'yes') { return provider.id; } else { break; // User said no } } else { console.log(InputValidationService.formatValidationError(validation)); } } } else if (status.available && !status.configured) { // Provider available but needs setup console.log(`\nšŸ”§ ${provider.displayName} needs configuration.`); console.log(` Setup difficulty: ${provider.setupGuide.difficulty}`); console.log(` Estimated time: ${provider.setupGuide.estimatedTime}`); // Get setup confirmation with validation while (true) { const setup = await this.getUserInput('Start setup now? (y/n): '); const validation = InputValidationService.validateYesNo(setup); if (validation.isValid) { if (validation.sanitizedValue === 'yes') { const configured = await this.runProviderSetup(provider); if (configured) { return provider.id; } break; } else { break; // User said no } } else { console.log(InputValidationService.formatValidationError(validation)); } } } else { // Provider not available console.log(`\nāŒ ${provider.displayName} is not available.`); if (status.error) { console.log(` Error: ${status.error}`); } console.log(' Please check the setup requirements and try again.'); await this.pause(); } return null; // Continue menu } /** * Run interactive setup for a provider */ async runProviderSetup(provider) { console.log(`\nšŸš€ Setting up ${provider.displayName}\n`); // Display setup guide console.log(`šŸ“‹ Setup Guide (${provider.setupGuide.difficulty} - ~${provider.setupGuide.estimatedTime})\n`); for (let i = 0; i < provider.setupGuide.steps.length; i++) { const step = provider.setupGuide.steps[i]; console.log(`${i + 1}. ${step.title}`); if (step.description) { console.log(` ${step.description}`); } if (step.command) { console.log(` Command: ${step.command}`); } console.log(''); } // Show help links if (provider.setupGuide.helpLinks.length > 0) { console.log('šŸ“š Helpful Links:'); for (const link of provider.setupGuide.helpLinks) { console.log(` • ${link.title}: ${link.url}`); } console.log(''); } // Interactive configuration const config = {}; console.log('šŸ”§ Configuration:'); // Required configuration with validation for (const envVar of provider.requiredEnvVars) { const template = provider.configTemplate[envVar]; let prompt = `Enter ${envVar}`; if (template) { prompt += ` (example: ${template})`; } prompt += ': '; while (true) { const value = await this.getUserInput(prompt); let validation; // Apply specific validation based on environment variable type if (envVar.includes('API_KEY') || envVar.includes('TOKEN')) { validation = InputValidationService.validateApiKey(value, provider.id); } else if (envVar.includes('ENDPOINT') || envVar.includes('URL')) { validation = InputValidationService.validateUrl(value); } else { validation = InputValidationService.validateText(value, { required: true, minLength: 1, maxLength: 500, sanitize: true }, envVar); } if (validation.isValid) { config[envVar] = validation.sanitizedValue; break; } else { console.log(InputValidationService.formatValidationError(validation)); } } } // Optional configuration for (const envVar of provider.optionalEnvVars) { const template = provider.configTemplate[envVar]; let prompt = `Enter ${envVar} (optional)`; if (template) { prompt += ` (default: ${template})`; } prompt += ': '; const value = await this.getUserInput(prompt); if (value.trim()) { config[envVar] = value.trim(); } else if (template) { config[envVar] = template; } } // Validate and save configuration try { await this.saveProviderConfiguration(provider.id, config); // Test connection console.log('\nšŸ”„ Testing connection...'); const testResult = await this.testProviderConnection(provider.id); if (testResult.success) { console.log('āœ… Connection successful!'); return true; } else { console.log(`āŒ Connection failed: ${testResult.error}`); console.log('Configuration saved but connection test failed.'); console.log('You can try again later or check your configuration.'); await this.pause(); return false; } } catch (error) { console.log(`āŒ Configuration failed: ${error instanceof Error ? error.message : String(error)}`); return false; } } /** * Save provider configuration to .env file */ async saveProviderConfiguration(providerId, config) { const envPath = join(process.cwd(), '.env'); // Read existing .env file or create new content let existingContent = ''; try { existingContent = await fs.readFile(envPath, 'utf8'); } catch { // File doesn't exist, will create new } // Update environment variables let updatedContent = existingContent; for (const [key, value] of Object.entries(config)) { const envLine = `${key}=${value}`; const envRegex = new RegExp(`^${key}=.*$`, 'm'); if (envRegex.test(updatedContent)) { // Update existing line updatedContent = updatedContent.replace(envRegex, envLine); } else { // Add new line if (updatedContent && !updatedContent.endsWith('\n')) { updatedContent += '\n'; } updatedContent += envLine + '\n'; } } // Also persist CURRENT_PROVIDER to .env const currentProviderLine = `CURRENT_PROVIDER=${providerId}`; const currentProviderRegex = /^CURRENT_PROVIDER=.*$/m; if (currentProviderRegex.test(updatedContent)) { updatedContent = updatedContent.replace(currentProviderRegex, currentProviderLine); } else { if (updatedContent && !updatedContent.endsWith('\n')) { updatedContent += '\n'; } updatedContent += currentProviderLine + '\n'; } // Write updated content await fs.writeFile(envPath, updatedContent, 'utf8'); // Update process.env for (const [key, value] of Object.entries(config)) { process.env[key] = value; } process.env.CURRENT_PROVIDER = providerId; // Write to config-rga.json (create if missing, update if exists) const configPath = join(process.cwd(), CONFIG_FILENAME); let userConfig = {}; try { const raw = await fs.readFile(configPath, 'utf8'); userConfig = JSON.parse(raw); } catch { // File does not exist or is invalid, start fresh userConfig = {}; } userConfig.currentProvider = providerId; await fs.writeFile(configPath, JSON.stringify(userConfig, null, 2), 'utf8'); } /** * Test provider connection */ async testProviderConnection(providerId) { const provider = getProviderById(providerId); if (!provider) { return { success: false, error: 'Provider not found' }; } try { const startTime = Date.now(); // Perform basic connectivity test const status = await provider.getStatus(); const responseTime = Date.now() - startTime; return { success: status.connected, responseTime, error: status.error }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Configure a new provider (placeholder for future extension) */ async configureNewProvider() { console.log('\nšŸ”§ Custom Provider Configuration'); console.log('This feature allows you to configure additional providers.'); console.log('Currently supported providers are shown in the main menu.'); console.log('For custom configurations, please edit your .env file manually.'); await this.pause(); return null; } /** * Show provider metrics and status details */ async showProviderMetrics() { console.log('\nšŸ“Š Provider Status & Metrics\n'); for (const provider of this.providers.values()) { const status = await provider.getStatus(); console.log(`${provider.icon} ${provider.displayName}:`); console.log(` Status: ${this.getStatusIcon(status)} ${this.getStatusText(status)}`); console.log(` Category: ${provider.category}`); console.log(` Token Limit: ${provider.tokenLimit.toLocaleString()}`); console.log(` Features: ${provider.features.join(', ')}`); if (status.metrics) { console.log(` Metrics: ${status.metrics.totalCalls} calls, ${status.metrics.averageResponseTime}ms avg`); } if (status.error) { console.log(` Error: ${status.error}`); } console.log(''); } await this.pause(); } /** * Show help and documentation */ async showHelp() { console.log('\nā“ Help & Documentation\n'); console.log('šŸŽÆ Provider Selection Tips:'); console.log(' • Start with Google AI Studio for free tier'); console.log(' • Use GitHub AI if you have a GitHub account'); console.log(' • Choose Azure OpenAI for enterprise features'); console.log(' • Try Ollama for offline/local processing'); console.log('\nšŸ”§ Configuration Help:'); console.log(' • API keys are stored in your .env file'); console.log(' • Configuration is automatically tested'); console.log(' • You can switch providers anytime'); console.log('\nšŸ“š Documentation:'); console.log(' • GitHub: https://github.com/mdresch/requirements-gathering-agent'); console.log(' • Run with --help for CLI options'); console.log(' • Check generated documents for examples'); await this.pause(); } /** * Get status icon for provider status */ getStatusIcon(status) { if (status.configured && status.available && status.connected) { return 'āœ…'; } else if (status.available && !status.configured) { return 'āš ļø'; } else if (!status.available) { return 'āŒ'; } else { return 'šŸ”„'; } } /** * Get status text for provider status */ getStatusText(status) { if (status.configured && status.available && status.connected) { return 'READY'; } else if (status.available && !status.configured) { return 'NEEDS SETUP'; } else if (!status.available) { return 'NOT AVAILABLE'; } else { return 'CHECKING'; } } /** * Get detailed configuration status for a provider */ async getDetailedConfigStatus(provider) { const missingVars = []; const partialVars = []; // Check required variables for (const envVar of provider.requiredEnvVars) { if (!process.env[envVar]) { missingVars.push(envVar); } else if (envVar.includes('ENDPOINT') && !process.env[envVar]?.includes('http')) { partialVars.push(`${envVar} (invalid URL)`); } } // Check optional variables for (const envVar of provider.optionalEnvVars) { if (process.env[envVar] && envVar.includes('ENDPOINT') && !process.env[envVar]?.includes('http')) { partialVars.push(`${envVar} (invalid URL)`); } } if (missingVars.length > 0) { return `Missing: ${missingVars.join(', ')}`; } else if (partialVars.length > 0) { return `Warning: ${partialVars.join(', ')}`; } return null; } // Helper methods async getUserInput(prompt) { return new Promise((resolve) => { this.readline.question(prompt, (answer) => { resolve(answer.trim()); }); }); } async clearScreen() { // Check if we're in a TTY environment if (process.stdout.isTTY) { console.clear(); } else { console.log('\n' + '='.repeat(60) + '\n'); } } async pause() { await this.getUserInput('\nPress Enter to continue...'); } /** * Cleanup resources */ async cleanup() { this.readline.close(); } } //# sourceMappingURL=interactive-menu.js.map