' ) { $this->readlineAvailable = extension_loaded('readline'); $this->suggestionEngine = new CommandSuggestionEngine($commandList); $this->helpGenerator = new CommandHelpGenerator(new ParameterInspector()); if ($this->readlineAvailable) { $this->setupReadline(); } } /** * Run the dialog mode */ public function run(): ExitCode { try { $this->showWelcome(); $this->mainLoop(); } catch (\Throwable $e) { $this->output->writeError('Fatal error: ' . $e->getMessage()); if ($this->output->isTerminal()) { $this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY); } return ExitCode::GENERAL_ERROR; } finally { $this->cleanup(); } return ExitCode::SUCCESS; } /** * Setup readline for history and completion */ private function setupReadline(): void { // Load history from CommandHistory $history = $this->commandHistory->getRecentHistory(100); foreach ($history as $entry) { if (isset($entry['command'])) { readline_add_history($entry['command']); } } // Set completion function readline_completion_function([$this, 'completeCommand']); // Enable completion readline_info('completion_append_character', ' '); readline_info('completion_suppress_append', false); } /** * Readline completion callback * * @param string $text * @param int $start * @param int $end * @return array */ public function completeCommand(string $text, int $start, int $end): array { $line = readline_info('line_buffer'); $words = explode(' ', $line); $currentWord = $words[$start] ?? ''; // If we're at the first word (command name), suggest commands if ($start === 0) { return $this->getCommandSuggestions($currentWord); } // For subsequent words, could implement argument completion // For now, return empty return []; } /** * Get command suggestions for autocomplete * * @return array */ private function getCommandSuggestions(string $partial): array { $suggestions = []; // Get suggestions from history first $historySuggestions = $this->commandHistory->getSuggestions($partial, 10); foreach ($historySuggestions as $suggestion) { $suggestions[] = $suggestion['command']; } // Get suggestions from all commands $commands = $this->commandList->getAllCommands(); foreach ($commands as $command) { if (str_starts_with(strtolower($command->name), strtolower($partial))) { if (! in_array($command->name, $suggestions)) { $suggestions[] = $command->name; } } } return array_unique($suggestions); } /** * Main dialog loop */ private function mainLoop(): void { $running = true; while ($running) { // Handle signals if (function_exists('pcntl_signal_dispatch')) { pcntl_signal_dispatch(); } // Read input $input = $this->readInput(); // Handle empty input if ($input === null || trim($input) === '') { continue; } $input = trim($input); // Handle exit commands if (in_array(strtolower($input), ['exit', 'quit', 'q', ':q'])) { $this->output->writeLine('Goodbye! 👋', ConsoleColor::CYAN); break; } // Handle help command if (in_array(strtolower($input), ['help', '?', ':help', 'h'])) { $this->showHelp(); continue; } // Handle help syntax if (preg_match('/^help\s+(.+)$/i', $input, $matches)) { $commandName = trim($matches[1]); $this->showCommandHelp($commandName); continue; } // Handle history command if (strtolower($input) === 'history' || strtolower($input) === ':history') { $this->showHistory(); continue; } // Handle clear command if (strtolower($input) === 'clear' || strtolower($input) === ':clear') { $this->clearScreen(); continue; } // Parse command and arguments $parts = $this->parseInput($input); $commandName = $parts['command']; $arguments = $parts['arguments']; // Check if command exists if (! $this->commandList->has($commandName)) { $this->handleCommandNotFound($commandName); continue; } // Execute command $exitCode = $this->commandExecutor->executeCommand($commandName, $arguments); // Add to readline history if available if ($this->readlineAvailable) { readline_add_history($input); } // Show suggestions if command failed if ($exitCode->value !== 0) { $this->showContextualHelp($commandName); } } } /** * Read input from user */ private function readInput(): ?string { if ($this->readlineAvailable) { $input = readline($this->prompt); if ($input === false) { return null; } return $input; } // Fallback to fgets if readline not available $this->output->write($this->prompt, ConsoleColor::BRIGHT_CYAN); $input = fgets(STDIN); if ($input === false) { return null; } return rtrim($input, "\n\r"); } /** * Parse input into command and arguments * * @return array{command: string, arguments: array} */ private function parseInput(string $input): array { // Handle quoted strings $parts = []; $current = ''; $inQuotes = false; $quoteChar = ''; for ($i = 0; $i < strlen($input); $i++) { $char = $input[$i]; if (($char === '"' || $char === "'") && ($i === 0 || $input[$i - 1] !== '\\')) { if ($inQuotes && $char === $quoteChar) { $inQuotes = false; $quoteChar = ''; } elseif (! $inQuotes) { $inQuotes = true; $quoteChar = $char; } } elseif ($char === ' ' && ! $inQuotes) { if ($current !== '') { $parts[] = $current; $current = ''; } } else { $current .= $char; } } if ($current !== '') { $parts[] = $current; } if (empty($parts)) { return ['command' => '', 'arguments' => []]; } return [ 'command' => $parts[0], 'arguments' => array_slice($parts, 1), ]; } /** * Handle command not found */ private function handleCommandNotFound(string $commandName): void { $this->output->writeLine(''); $this->output->writeError("Command '{$commandName}' not found."); // Get suggestions $suggestions = $this->suggestionEngine->suggestCommand($commandName); if ($suggestions->hasSuggestions()) { $this->output->writeLine(''); $this->output->writeLine('Did you mean one of these?', ConsoleColor::YELLOW); foreach ($suggestions->suggestions as $suggestion) { $confidence = round($suggestion->similarity * 100); $this->output->writeLine( " • {$suggestion->command->name} ({$confidence}% match)", ConsoleColor::CYAN ); if ($suggestion->command->description) { $this->output->writeLine( " {$suggestion->command->description}", ConsoleColor::GRAY ); } } } $this->output->writeLine(''); $this->output->writeLine('Type "help" to see all available commands.', ConsoleColor::GRAY); $this->output->writeLine(''); } /** * Show contextual help for a command */ private function showContextualHelp(string $commandName): void { if (! $this->commandList->has($commandName)) { return; } $command = $this->commandList->get($commandName); $this->output->writeLine(''); $this->output->writeLine('💡 Tip: Use "help ' . $commandName . '" for detailed help.', ConsoleColor::CYAN); if ($command->description) { $this->output->writeLine("Description: {$command->description}", ConsoleColor::GRAY); } $this->output->writeLine(''); } /** * Show welcome message */ private function showWelcome(): void { $this->output->writeLine(''); $this->output->writeLine('🤖 Console Dialog Mode', ConsoleColor::BRIGHT_CYAN); $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); $this->output->writeLine(''); $commandCount = $this->commandList->count(); $this->output->writeLine("Available commands: {$commandCount}", ConsoleColor::GRAY); if ($this->readlineAvailable) { $this->output->writeLine('✓ Readline support enabled (Tab completion, ↑/↓ history)', ConsoleColor::GREEN); } else { $this->output->writeLine('⚠ Readline not available (install php-readline for better experience)', ConsoleColor::YELLOW); } $this->output->writeLine(''); $this->output->writeLine('Type "help" for available commands or "exit" to quit.', ConsoleColor::GRAY); $this->output->writeLine(''); } /** * Show help */ private function showHelp(): void { $categories = $this->groupRegistry->getOrganizedCommands(); $this->output->writeLine(''); $this->output->writeLine('📚 Available Commands', ConsoleColor::BRIGHT_CYAN); $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); $this->output->writeLine(''); foreach ($categories as $category => $commands) { $this->output->writeLine("{$category}:", ConsoleColor::BRIGHT_YELLOW); foreach ($commands as $command) { $name = $command->name ?? 'unknown'; $description = $command->description ?? 'No description'; $this->output->writeLine(" {$name}", ConsoleColor::WHITE); $this->output->writeLine(" {$description}", ConsoleColor::GRAY); } $this->output->writeLine(''); } $this->output->writeLine('💡 Tips:', ConsoleColor::BRIGHT_YELLOW); $this->output->writeLine(' • Type a command name to execute it', ConsoleColor::GRAY); $this->output->writeLine(' • Use Tab for command completion', ConsoleColor::GRAY); $this->output->writeLine(' • Use ↑/↓ to navigate command history', ConsoleColor::GRAY); $this->output->writeLine(' • Type "help " for detailed help', ConsoleColor::GRAY); $this->output->writeLine(' • Type "exit" or "quit" to leave', ConsoleColor::GRAY); $this->output->writeLine(''); } /** * Show detailed help for a specific command */ private function showCommandHelp(string $commandName): void { $this->output->writeLine(''); $this->output->writeLine("📖 Command Help: {$commandName}", ConsoleColor::BRIGHT_CYAN); $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); $this->output->writeLine(''); if (! $this->commandList->has($commandName)) { $this->output->writeError("Command '{$commandName}' not found."); $suggestions = $this->suggestionEngine->suggestCommand($commandName); if ($suggestions->hasSuggestions()) { $this->output->writeLine(''); $this->output->writeLine('Did you mean one of these?', ConsoleColor::YELLOW); foreach ($suggestions->suggestions as $suggestion) { $this->output->writeLine(" • {$suggestion->command->name}", ConsoleColor::CYAN); } } $this->output->writeLine(''); return; } try { $command = $this->commandList->get($commandName); $discoveredAttributes = $this->discoveryRegistry->attributes()->get(\App\Framework\Console\ConsoleCommand::class); // Find the command object $commandObject = null; foreach ($discoveredAttributes as $discovered) { $attribute = $discovered->createAttributeInstance(); if ($attribute && $attribute->name === $commandName) { $className = $discovered->className->getFullyQualified(); $commandObject = $this->container->get($className); break; } } if ($commandObject) { $help = $this->helpGenerator->generateHelp($commandObject); $this->displayFormattedHelp($help); } else { // Fallback to basic info $this->output->writeLine("Name: {$command->name}", ConsoleColor::WHITE); if ($command->description) { $this->output->writeLine("Description: {$command->description}", ConsoleColor::GRAY); } } } catch (\Throwable $e) { $this->output->writeError("Failed to generate help: {$e->getMessage()}"); if ($this->output->isTerminal()) { $this->output->writeLine($e->getTraceAsString(), ConsoleColor::GRAY); } } $this->output->writeLine(''); } /** * Display formatted help */ private function displayFormattedHelp(CommandHelp $help): void { $sections = $help->formatAsColoredText(); foreach ($sections as $section) { $color = match ($section['color']) { 'BRIGHT_CYAN' => ConsoleColor::BRIGHT_CYAN, 'BRIGHT_YELLOW' => ConsoleColor::BRIGHT_YELLOW, 'BRIGHT_WHITE' => ConsoleColor::BRIGHT_WHITE, 'BRIGHT_GREEN' => ConsoleColor::BRIGHT_GREEN, 'WHITE' => ConsoleColor::WHITE, 'GRAY' => ConsoleColor::GRAY, 'YELLOW' => ConsoleColor::YELLOW, 'RED' => ConsoleColor::RED, default => ConsoleColor::WHITE }; $this->output->writeLine($section['text'], $color); } } /** * Show command history */ private function showHistory(): void { $history = $this->commandHistory->getRecentHistory(20); $this->output->writeLine(''); $this->output->writeLine('📜 Command History', ConsoleColor::BRIGHT_CYAN); $this->output->writeLine(str_repeat('═', 60), ConsoleColor::GRAY); $this->output->writeLine(''); if (empty($history)) { $this->output->writeLine('No commands in history yet.', ConsoleColor::GRAY); } else { foreach ($history as $index => $entry) { $command = $entry['command'] ?? 'unknown'; $count = $entry['count'] ?? 1; $timestamp = isset($entry['timestamp']) ? date('Y-m-d H:i:s', $entry['timestamp']) : 'unknown'; $this->output->writeLine( sprintf('%3d. %s (used %d times, last: %s)', $index + 1, $command, $count, $timestamp), ConsoleColor::WHITE ); } } $this->output->writeLine(''); } /** * Clear screen */ private function clearScreen(): void { if (function_exists('shell_exec') && $this->output->isTerminal()) { // ANSI clear screen sequence $this->output->write("\033[2J\033[H"); } } /** * Cleanup resources */ private function cleanup(): void { if ($this->readlineAvailable) { // Save history readline_write_history(null); } } }