refactor(console, id, config): Dialog mode in Console, consolidated id modul, added config support for ini directives
This commit is contained in:
548
src/Framework/Console/Components/ConsoleDialog.php
Normal file
548
src/Framework/Console/Components/ConsoleDialog.php
Normal file
@@ -0,0 +1,548 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user