549 lines
18 KiB
PHP
549 lines
18 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Console\Components;
|
|
|
|
use App\Framework\Console\CommandGroupRegistry;
|
|
use App\Framework\Console\CommandHelp;
|
|
use App\Framework\Console\CommandHelpGenerator;
|
|
use App\Framework\Console\CommandHistory;
|
|
use App\Framework\Console\CommandList;
|
|
use App\Framework\Console\ConsoleColor;
|
|
use App\Framework\Console\ConsoleOutputInterface;
|
|
use App\Framework\Console\ErrorRecovery\CommandSuggestionEngine;
|
|
use App\Framework\Console\ExitCode;
|
|
use App\Framework\Console\ParameterInspector;
|
|
use App\Framework\DI\Container;
|
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
|
|
|
/**
|
|
* Dialog mode orchestrator - simple text-based interactive console
|
|
* Similar to AI assistant chat interface
|
|
*/
|
|
final readonly class ConsoleDialog
|
|
{
|
|
private bool $readlineAvailable = false;
|
|
|
|
private CommandSuggestionEngine $suggestionEngine;
|
|
|
|
private CommandHelpGenerator $helpGenerator;
|
|
|
|
public function __construct(
|
|
private ConsoleOutputInterface $output,
|
|
private DiscoveryRegistry $discoveryRegistry,
|
|
private CommandHistory $commandHistory,
|
|
private CommandGroupRegistry $groupRegistry,
|
|
private DialogCommandExecutor $commandExecutor,
|
|
private CommandList $commandList,
|
|
private Container $container,
|
|
private string $prompt = 'console> '
|
|
) {
|
|
$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<int, string>
|
|
*/
|
|
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<int, string>
|
|
*/
|
|
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 <command> 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<int, string>}
|
|
*/
|
|
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 <command>" 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);
|
|
}
|
|
}
|
|
}
|
|
|